'소프트웨어개발/Spring'에 해당되는 글 18건

  1. Jackson, 커스텀 @JsonFilter로 조건에 맞는 필드만 JSON 변환하기
  2. Spring Web MVC, @Controller 어노테이션 정리
  3. Spring Web MVC, @Controller 메써드 리턴 타입 정리
  4. Spring Boot, SSL 활성화하기
  5. Spring Boot, AspectJ 스타일의 Spring AOP 구현하기
  6. Spring, HandlerInterceptor(인터셉터) 구현하기
  7. Spring Boot, Logback을 이용한 로그 출력하기
  8. Spring, RestTemplate으로 REST 클라이언트 구현하기
  9. Spring Boot, 작업 스케쥴러 데몬 구현하기
  10. Spring Boot, JSON 변환, LocalDateTime을 ISO8601으로 출력하기

Jackson, 커스텀 @JsonFilter로 조건에 맞는 필드만 JSON 변환하기

개요

JacksonJava 진영의 대표적인 POJO-JSON 상호 변환 라이브러리이다. 이번 글에서는 @JsonFilter를 이용하여 POJO 오브젝트의 필드 값을 기준으로 조건에 맞는 필드만 선택적으로 JSON으로 변환하는 예를 소개하고자 한다.

라이브러리 종속성 추가

dependencies {
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.1'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.1'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.1'
    compile group: 'org.projectlombok', name: 'lombok', version: '1.16.10'
}
  • Spring Boot(1.4.0) 기반의 웹 프로젝트라면 Jackson이 이미 추가되어 있으므로 별도로 추가할 필요가 없다.

  • 필수는 아니지만 POJO 클래스를 효율적으로 작성하기 위해 lombok 아티팩트를 추가하였다.

POJO 클래스 작성

package com.jsonobject.example;

import com.fasterxml.jackson.annotation.JsonFilter;
import lombok.Data;

@JsonFilter("userJsonFilter")
@Data
public class User {

    private int id;

    private String email;

    private String password;

    private boolean hasProfileImage;

    private String profileImageUrl;
}
  • 간단한 회원 정보를 저장하는 목적의 일반적인 POJO 클래스를 작성하였다. Lombok이 제공하는 @Data 클래스 레벨 어노테이션으로 RequiredArgsConstructor, Getter, Setter 작성을 생략하였다.

  • @JsonFilter 클래스 레벨 어노테이션으로 JSON 변환시 사용할 필터를 명시하였다. (필터 작성 및 등록은 아래 설명할 것이다.)

JsonFilter 클래스 작성

package com.jsonobject.example;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;

public class UserJsonFilter extends SimpleBeanPropertyFilter {

    @Override
    public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception {

        if (pojo instanceof User) {
            if (canSerializeAsField((User) pojo, writer.getName())) {
                super.serializeAsField(pojo, jgen, provider, writer);
            }
        } else {
            super.serializeAsField(pojo, jgen, provider, writer);
        }
    }

    private boolean canSerializeAsField(User user, String fieldName) {

        if (fieldName.equals("password")) {
            return false;
        }
        if (fieldName.equals("profileImageUrl") && !user.isHasProfileImage()) {
            return false;
        }

        return true;
    }
}
  • SimpleBeanPropertyFilter.serializeAsField() 메써드는 JSON 변환 시점에 대상 POJO의 모든 필드에 대해 실행된다. 이를 통해 필드 단위로 변환 여부를 결정할 수 있다.

  • POJO 단위로 대응 필터를 작성하여 JSON 변환 정책을 세울 수 있다. 위의 경우 회원 정보를 담는 User 클래스에 대해 아래 정책을 적용하였다.
1. password 필드는 JSON으로 변환하지 않는다.
2. hasProfileImage 필드의 값이 true일 경우에만 profileImageUrl을 JSON으로 변환한다.

ObjectMapper 필터 등록 및 변환

User user = new User();
user.setId(1);
user.setEmail("someone@gmail.com");
user.setPassword("secret");
user.setHasProfileImage(false);
user.setProfileImageUrl(null);

ObjectMapper mapper = new ObjectMapper();
mapper.setFilterProvider(new SimpleFilterProvider().addFilter("userJsonFilter", new UserJsonFilter()));
mapper.writeValueAsString(user));

변환 결과는 아래와 같다. 필터가 정상적으로 적용되었음을 확인할 수 있다.

{
  "id" : 1,
  "email" : "someone@gmail.com",
  "hasProfileImage" : false
}

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Web MVC, @Controller 어노테이션 정리

클래스 레벨

Spring Web MVC@Controller 클래스에서 클래스 레벨에 명시 가능한 어노테이션은 아래와 같다.


  • @ResponseBody: 모든 메써드의 리턴 오브젝트를 적절한 형태로 변환 후 HTTP 응답 메시지의 바디에 담아 반환한다. (오브젝트에서 바디로의 실제 변환은 HttpMessageConverter 인터페이스의 각 구현 클래스들이 수행한다. 예를 들면 String 리턴 타입은 StringHttpMessageConverter가 변환을 담당한다.) 클래스 레벨에 명시하면 View로의 반환을 완전히 배제하기 때문에 REST API 구현시 적합하다.


  • @RestController: @Controller에 위에 설명한 @ResponseBody를 추가한 효과를 가진다. REST API 구현을 염두한 축약형 어노테이션이다.


  • @RequestMapping: 응답을 수행할 HTTP 요청을 명시할 수 있다.

    • path (String, String[]): 요청 URI를 명시한다. 배열을 통한 복수개의 요청 URI를 지정할 수 있다. 클래스 레벨에 명시한 값은 모든 메써드 레벨에 명시한 path 값 앞에 붙어 접두어 역할을 한다.
@RequestMapping("/v1/players")
  • @PropertySource: 프라퍼티 소스를 명시할 수 있다. 필드 레벨에서 @Value 등을 통해 클래스 레벨에서 명시한 프라퍼티 소스의 값을 주입할 수 있다.
// ROOT CLASSPATH(/src/main/java/resources)에 위치한 some.properties를 프라퍼티 소스로 명시한다.
@PropertySource("classpath:/some.properties")

클래스 필드 레벨

필드 레벨에 명시 가능한 어노테이션은 아래와 같다.


  • @Autowired: 적합한 이름의 스프링 빈을 자동으로 주입한다. 명백하게 1개의 스프링 빈만 존재할 경우 사용한다.


  • @Resource: 지정한 이름의 스프링 빈을 주입한다. 같은 타입의 스프링 빈이 2개 이상 존재할 경우 사용한다.

    • name (String): 주입할 스프링 빈의 이름이다.

  • @Value: 시스템 환경 변수, Java 환경 변수, Spring 환경 변수, 프라퍼티 리소스 상수 등의 값을 주입한다.


메써드 레벨

  • @RequestMapping: 메써드가 어떤 HTTP 요청을 처리할 것인가를 작성한다. 구체적인 작성 예는 아래와 같다.

@RequestMapping(
  /**
   * HTTP URI가 "/users"인 것만 처리한다. 만약 앞서 클래스 레벨에서 "/v1"이 명시되었다면 "/v1/users"를 처리하게 된다.
   */
  path = "/users",

  /**
   * HTTP 요청 메써드가 "POST"인 것만 처리한다.
   */
  method = RequestMethod.POST,

  /**
   * HTTP 요청 헤더가 "Content-Type: application/json;charset=UTF-8"인 것만 처리한다.
   * 다른 값이 들어올 경우 org.springframework.web.HttpMediaTypeNotSupportedException을 발생시킨다.
   * HTTP 요청 헤더에 명시된 Content-Type은 HTTP 요청 바디의 형식을 의미한다. 즉, 서버에서는 JSON 형식의 바디만 처리하겠다는 의미이다.
   * 참고로 GET 요청은 바디를 가지지 않으므로 아래 파라메터를 명시할 필요가 없다.
   */
  consumes = MediaType.APPLICATION_JSON_UTF8_VALUE,

  /**
   * HTTP 응답 헤더로 "Content-Type: application/json;charset=UTF-8"을 반환한다.
   * 생략할 경우 메써드 리턴 타입에 따라 Content-Type을 자동으로 판단하여 반환한다.
   */
  produces = MediaType.APPLICATION_JSON_UTF8_VALUE
)

메써드 아규먼트 레벨

  • 특정 어노테이션 없이 POJO 클래스를 아규먼트로 명시하면 HTTP 요청 메시지의 QueryString 문자열을 변환하여 저장한다.

  • @Valid: JSR-303 (Bean Validation) 스펙에 명시된 어노테이션으로 요청 파라메터에 대한 유효성 검사가 가능하다. 유효성 검사 실패시 org.springframework.validation.BindException이 발생한다. 대상 아규먼트는 기본 타입 및 POJO 타입 모두 가능하다.

  • @RequestBody: HTTP 요청 메시지의 BODY를 변환하여 담을 아규먼트 앞에 명시한다. HTTP 스펙에 따라 GET 이외의 메써드에서만 명시 가능하다. (무시하고 명시할 경우 org.springframework.http.converter.HttpMessageNotReadableException이 발생한다.) 한편 앞서 설명한 @Valid를 추가로 명시할 수 있다. 유효성 검사 실패시 org.springframework.web.bind.MethodArgumentNotValidException을 발생시킨다.
저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Web MVC, @Controller 메써드 리턴 타입 정리

Spring Web MVC@Controller 클래스에서 메써드의 처리 가능한 리턴 타입은 아래와 같다.


  • String: 문자열이다. 크게 2가지로 처리가 나뉜다. @ResponseBody가 클래스 또는 메써드 레벨에 명시되어 있다면 문자열은 그대로 HTTP 응답 메시지의 Body에 담겨 반환된다. 명시되어 있지 않다면 문자열은 View의 이름으로 인식하여 사전 설정된 ViewResolver에 의해 처리된다.

  • ?: 모든 POJO 오브젝트이다. 해당 오브젝트는 기본 값으로 JSON으로 변환되어 HTTP 응답 메시지의 Body에 담겨 반환된다.(XML 형식으로 변환하고자 할 경우 @RequestMappingproduces 필드에 MediaType.APPLICATION_XML_VALUE을 명시한다.)

  • ResponseEntity<?>: HTTP 응답 메시지이다. Status Code, Header, Body를 상황에 맞게 직접 빌드하여 반환할 수 있다. Body에는 POJO 오브젝트를 담을 수 있으며 처리 방식은 앞서 설명한 것과 동일하다.
저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, SSL 활성화하기

application.properties

server.port=8443
server.ssl.key-store=classpath:keystore.jks
server.ssl.key-store-password=
server.ssl.key-password=

  • server.port에는 HTTPS 요청을 처리할 포트를 명시한다.

  • server.ssl.key-store에는 JDKkeytool 명령어로 생성한 .jks 파일의 CLASSPATH 경로를 명시한다. 프로젝트의 /src/main/resources/keystore.jks에 위치했다면 classpath:keystore.jks가 된다.

  • .jks 파일을 생성하기 위해서는 가입한 SSL 인증서 사이트가 제공하는 .crt 파일이 필요하다. (SSL 인증서가 없다면 StartSSL이 제공하는 무료 SSL 인증서 발급 서비스를 추천한다.)

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, AspectJ 스타일의 Spring AOP 구현하기

개요

만약 내가 작성한 모든 메써드의 실행 전후로 로그를 남기고 싶다면? 데이터베이스에 대한 쓰기 작업 전후의 트랜잭션 관리를 일일이 명시하지 않고 자동으로 하고 싶다면? 전통적인 클래스 관점의 OOP 세계에서는 깔끔하게 대응하기가 쉽지 않다. 답은 Aspect 관점의 AOP(Aspect Oriented Programming)를 도입하는 것이다. Java 진영에는 AspectJ라는 훌륭한 AOP 프레임워크가 존재하며 이를 이용하여 개발자는 커스텀 어노테이션을 적용한 자신 만의 편리한 Aspect를 작성할 수 있다.(AspectJ의 훌륭한 적용 사례 중 하나는 jcabi-aspects 라이브러리이다.) Spring 또한 AspectJ 스타일을 수용한 프록시 기반의 Spring AOP를 제공한다. 대상 클래스의 메써드 단위로만 적용 가능한 제한적인 AOP를 제공하지만 일반적인 상황에서 사용하는 데는 충분하다.

라이브러리 종속성 추가

dependencies {
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-aop'
}
  • Spring Boot 기반 애플리케이션에서 Spring AOP를 사용하려면 spring-boot-starter-aop 아티팩트를 추가하면 된다.

@EnableAspectAutoProxy

@SpringBootApplication
@EnableAspectAutoProxy
public class SomeApplication {

    public static void main(String[] args) {

        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }
}
  • @SpringBootApplication 또는 @Configuration을 명시한 클래스에 @EnableAspectAutoProxy를 명시하면 Spring AOP를 사용하기 위한 첫 준비가 끝난다.
  • @EnableAspectAutoProxyXML 기반의 ApplicationContext 설정에서의 <aop:aspectj-autoproxy />와 동일한 기능을 한다.

@Aspect

@Component
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE)
public class SomeAspect {

    // Aspect 메써드 작성
}
  • 스프링 빈에 @Aspect를 명시하면 해당 빈이 Aspect로 작동한다.
  • 클래스 레벨에 @Order를 명시하여 @Aspect 빈 간의 작동 순서를 정할 수 있다. int 타입의 정수로 순서를 정할 수 있는데 값이 낮을수록 우선순위가 높다. 기본값은 가장 낮은 우선순위를 가지는 Ordered.LOWEST_PRECEDENCE이다.
  • @Aspect가 명시된 빈에는 어드바이스(Advice)라 불리는 메써드를 작성할 수 있다. 대상 스프링 빈의 메써드의 호출에 끼어드는 시점과 방법에 따라 @Before, @After, @AfterReturning, @AfterThrowing, @Around 등을 명시할 수 있다.

@Before

@Aspect 클래스의 메써드 레벨에 @Before 어드바이스를 명시하면 대상 메써드의 실행 전에 끼어 들어 원하는 작업을 할 수 있다. 끼어들기만 할 뿐 대상 메써드의 제어나 가공은 불가능하다.

@Before("execution(* com.jsonobject.example.*.*(..))")
public void doSomethingBefore(JoinPoint joinPoint) {

    // 끼어들어 실행할 로직을 작성
}
  • 어드바이스에 작성된 파라메터는 PointCut이라 부르는 표현식이다. 끼어들 메써드의 범위를 지정할 수 있다.
execution(* com.jsonobject.example.*.*(..))

*                     : 모든 리턴 타입
com.jsonobject.example: 특정 패키지
*                     : 모든 클래스
*                     : 모든 메써드
..                    : 모든 아규먼트
  • 아규먼트로 전달 받는 JoinPoint 오브젝트는 끼어든 메써드의 정보를 담고 있다.

@After

@After 어드바이스를 명시하면 대상 메써드의 실행 후에 끼어 들어 원하는 작업을 할 수 있다. 역시 끼어들기만 할 뿐 대상 메써드의 제어나 가공은 불가능하다.

@After("execution(* com.jsonobject.example.*.*(..))")
public void doSomethingAfter(JoinPoint joinPoint) {

    // 끼어들어 실행할 로직을 작성
}

@AfterReturning

@Aspect 클래스의 메써드 레벨에 @AfterReturning을 명시하면 해당 메써드의 실행이 종료되어 값을 리턴할 때 끼어 들 수 있다. 리턴 값을 확인할 수 있을 뿐 대상 메써드의 제어나 가공은 불가능하다.

@AfterReturning(pointcut = "execution(* com.jsonobject.example.*.*(..))", returning = "result")
public void doSomethingAfterReturning(JoinPoint joinPoint, Object result) {

    // 끼어들어 실행할 로직을 작성
}

@Around

@Around 어드바이스는 앞서 설명한 어드바이스의 기능을 모두 포괄하는 종합선물세트와도 같다. 대상 메써드를 감싸는 느낌으로 실행 전후 시점에 원하는 작업을 할 수 있다. 대상 메써드의 실행 제어 및 리턴 값 가공도 가능하다.

// 특정 어노테이션이 명시된 모든 메써드의 실행 전후로 끼어들 수 있다.
@Around("@annotation(someAnnotation)")
public Object doSomethingAround(final ProceedingJoinPoint joinPoint, final SomeAnnotation someAnnotation) {

    // 대상 메써드 실행 전 끼어들어 실행할 로직을 작성

    Object result = joinPoint.proceed();

    // 대상 메써드 실행 후 끼어들어 실행할 로직을 작성, 리턴 값을 가공할 수 있다.

    return result;
}

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring, HandlerInterceptor(인터셉터) 구현하기

개요

Java EE에는 HTTP 요청에 대한 응답을 수행하는 HttpServlet(@WebServlet)이 존재하며 이러한 서블릿 실행 전후 시점에 임의의 처리를 가능하게 해주는 Filter(@WebFilter)가 존재한다. 필터는 체인 형태로 여러 개를 사용할 수 있다. Spring Web MVC 또한 동일한 기능의 HandlerInterceptor를 제공한다. Filter는 로우 레벨의 처리 로직을, HandlerInterceptor는 회원 인증 검사 등의 비즈니스 레벨의 처리 로직을 작성하는데 적합하다. 이번 글에서는 HandlerInterceptor의 사용 예를 간단히 설명하고자 한다.

HandlerInterceptor 작성

package com.jsonobject.example;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class HttpInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(

            HttpServletRequest request,
            HttpServletResponse response,
            Object handler

    ) throws Exception {

        // HTTP 요청 처리 전 수행할 로직 작성

        return true;
    }

    @Override
    public void postHandle(

            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            ModelAndView modelAndView

    ) throws Exception {

        // HTTP 요청 처리 후 수행할 로직 작성
    }
}
  • 인터셉터 클래스를 작성하려면 HandlerInterceptor 인터페이스를 구현해야 한다. 만약 전처리 또는 후처리만 하고자 할 경우 인터페이스의 모든 메써드를 구현하는 것은 번거롭다. 위와 같이 HandlerInterceptorAdapter 추상 클래스를 상속하면 원하는 메써드만 작성할 수 있어 편리하다.

  • 인터셉터 클래스는 @Component 어노테이션을 통해 스프링 빈이 될 수 있다. 따라서 의존성 주입을 통한 비즈니스 로직을 처리하기 편리하다.

WebMvcConfigurer 작성

package com.jsonobject.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class HttpInterceptorConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private HttpInterceptor httpInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(httpInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/public/**");
    }
}
  • 인터셉터 클래스만으로는 아무 일도 일어나지 않는다. 작성한 인터셉터가 작동할 수 있도록 등록해주어야 한다. WebMvcConfigurer 인터페이스를 구현한 @Configuration 클래스를 작성하면 된다. WebMvcConfigurerAdapter 추상 클래스를 상속하여 필요한 메써드만 깔끔하게 작성할 수 있다.

  • 인터셉터를 등록할 때는 인터셉터를 적용할 요청 주소의 패턴과 제외할 요청 주소의 패턴을 명시하여 선택적 적용이 가능하다.

ResponseBodyAdvice 작성

package com.jsonobject.example;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class HttpResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(

            MethodParameter returnType,
            Class<? extends HttpMessageConverter<?>> converterType
    ) {

        return true;
    }

    @Override
    public Object beforeBodyWrite(

            Object body,
            MethodParameter returnType,
            MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request,
            ServerHttpResponse response
    ) {

        // HTTP 요청 처리 후 응답을 가공하는 로직 작성
        response.getHeaders().add("some-header", "some-value");

        return body;
    }
}
  • 인터셉터가 적용된 HTTP 요청을 처리하는 @Controller 클래스의 메써드가 메시지 변환이 필요한 오브젝트를 응답한다면(@ResponseBody, ResponseEntity 오브젝트 반환시) 인터셉터의 후처리 시점에 응답을 가공하는 작업(예를 들면 HTTP 헤더의 추가)이 불가능해진다. 이 경우 ResponseBodyAdvice 인터페이스를 구현한 클래스를 작성하여 해결할 수 있다.

  • beforeBodyWrite 메써드에 후가공이 필요한 로직을 작성한다. 내 경우 HTTP 요청에 대한 트랜잭션 ID를 헤더에 추가하는 로직을 이 부분에 작성하였다.

참고 글

다른 읽을만한 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, Logback을 이용한 로그 출력하기

먼저 읽어볼만한 글

라이브러리 종속성 추가

dependencies {
    compile group: 'net.logstash.logback', name: 'logstash-logback-encoder', version: '4.7'
}
  • Spring Boot 기반의 애플리케이션(spring-boot-start 또는 spring-boot-start-web 아티팩트를 추가한 상태)은 기본적으로 spring-boot-starter-logging 아티팩트를 포함하므로 별도의 라이브러리 종속성을 추가할 필요가 없다. Spring Boot는 내부적으로 Logback 로깅 라이브러리를 사용하여 로그를 출력하며 개발자는 추상체인 SLF4J를 사용하여 로그를 기록하면 된다.

  • 최근의 로그 관리 기법의 추세는 ELK Stack 등을 이용한 중앙집중 방식이다. ELK Stack 스택을 통해 로그를 수집할 경우 전통적인 라인 단위의 텍스트 로그보다 JSON 형식의 로그를 사용하는 것이 효율적이다. logstash-logback-encoder 아티팩트를 추가함으로서 ELK Stack에 대응하는 로그를 손쉽게 생성할 수 있다.

Decorator 작성

로그를 일반적인 문자열이 아닌 JSON 형식으로 출력하면 여러가지 장점이 있다. JSON 로그 생성시 이를 도와주는 클래스를 아래와 같이 작성한다.


/src/main/java/com/jsonobject/logback/PrettyPrintingDecorator.java 파일을 아래와 같이 작성한다.

package com.jsonobject.logback;

import com.fasterxml.jackson.core.JsonGenerator;
import net.logstash.logback.decorate.JsonGeneratorDecorator;

public class PrettyPrintingDecorator implements JsonGeneratorDecorator {

    @Override
    public JsonGenerator decorate(JsonGenerator generator) {

        return generator.useDefaultPrettyPrinter();
    }
}

/src/main/java/com/jsonobject/logback/JsonFactoryDecorator.java 파일을 아래와 같이 작성한다.

package com.jsonobject.logback;

import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import net.logstash.logback.decorate.JsonFactoryDecorator;

public class ISO8601DateDecorator implements JsonFactoryDecorator {

    @Override
    public MappingJsonFactory decorate(MappingJsonFactory factory) {

        ObjectMapper codec = factory.getCodec();
        codec.setDateFormat(new ISO8601DateFormat());

        return factory;
    }
}

로그 환경 설정

/src/main/resources/logback-spring.xml 파일을 아래와 같이 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="LOG_PATH" value="/var/log/application"/>

    <!-- 콘솔 로그 출력 -->
    <appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <Pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</Pattern>
        </encoder>
    </appender>

    <!-- Logstash JSON 형식으로 파일 로그 생성 -->
    <!-- /var/log/applicaion/log.json 파일에 최신 로그 생성 -->
    <!-- /var/log/applicaion/log_2016-07-15.0.json 파일에 과거 로그 보관
    <!-- 5MB 초과, 날짜 변경 시점마다 생성, 생성된지 3일 이상된 파일은 삭제 -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_PATH}/log.json</File>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="com.jsonobject.example.logback.PrettyPrintingDecorator"/>
            <jsonFactoryDecorator class="com.jsonobject.example.logback.ISO8601DateDecorator"/>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_PATH}/log_%d{yyyy-MM-dd}.%i.json</FileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>5MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>3</maxHistory>
        </rollingPolicy>
    </appender>

    <!-- INFO 레벨 이하 로그를 콘솔 및 파일로 출력 -->
    <root level="INFO">
        <appender-ref ref="TEXT_CONSOLE"/>
        <appender-ref ref="JSON_FILE"/>
    </root>

</configuration>
  • 별도의 로깅 설정을 하지 않을 경우 Spring BootINFO 이하 레벨(TRACE, DEBUG를 제외한 INFO, WARN, ERROR)의 모든 로그를 콘솔에 출력한다. 로깅 설정은 /src/main/resources/logback-spring.xml 파일에 작성한다.(/src/main/resources/application.properties에 작성하는 방법도 있지만 설정이 제한적으로 추천하지 않는다.)

SYSLOG/UDP 전송을 이용한 ELK 로그 수집하기

LogstashSocketAppender를 이용하면 syslog 원격 서버에 대한 UDP 로그 전송이 가능하다. 낮은 부하로 애플리케이션 로그를 중앙에서 실시간으로 수집할 수 있다는 장점이 있다.

<appender name="JSON_UDP" class="net.logstash.logback.appender.LogstashSocketAppender">
    <syslogHost>127.0.0.1</syslogHost>
    <port>514</port>
    <!-- 기본적으로 생성되는 필드명을 변경할 수 있다. -->
    <fieldNames>
        <!-- @timestamp 필드명으로 기본 생성되는 로그 생성 일시를 created_datetime 필드명으로 출력되도록 변경한다. -->
        <timestamp>created_datetime</timestamp>
    </fieldNames>
    <!-- 로그 앞에 서버 식별 목적의 임의의 접두어를 추가할 수 있다. -->
    <prefix class="ch.qos.logback.classic.PatternLayout">
        <pattern>someapp:</pattern>
    </prefix>
</appender>
  • 접두어 패턴으로 입력한 문자열은 syslog 서버에서 programname 항목으로 인식된다. 어펜더가 생성하는 JSON 형식의 로그는 msg 항목으로 인식된다.

syslog 서버에는 아래 형식으로 전송된다.

127.0.0.1    Jul 17 04:46:53                    someapp:{"created_datetime":"2016-07-17T04:46:53.342+09:00","@version":1,"message":"Started Application in 5.689 seconds (JVM running for 6.448)","logger_name":"com.jsonobject.example.Application","thread_name":"main","level":"INFO","level_value":20000,"HOSTNAME":"dev"}

syslog 서버에서는 아래와 같이 접두어를 기반으로 로그 저장소를 설정할 수 있다.

$ vi /etc/rsyslog.conf
:programname, isequal, "someapp" -/var/log/someapp.json
  • 표현식으로는 isequal, startswith, contains를 사용할 수 있다.

$template SomeAppLogFormat,"%msg%\n"
$template SomeAppLogPath,"/var/log/someapp.json"
if $programname == 'someapp' then -?SomeAppLogPath;SomeAppLogFormat

Logstash 서버에서는 아래와 같이 환경 설정을 적용한다.

# Logstash 환경 설정 파일을 작성한다.
$ vi /etc/logstash/conf.d/someapp-filter.conf
filter {
  date {
    match => [ "created_datetime", "yyyy-MM-dd'T'HH:mm:ss.SSSZZ" ]
    target => "@timestamp"
  }
}

# 설정한 환경 설정에 문제가 없는지 확인한다.
$ service logstash configtest
Configuration OK

# 환경 설정을 적용하기 위해 Logstash를 재시작한다.
$ systemctl restart logstash
  • 앞서 애플리케이션에서 created_datetime 필드에 담아 전송한 로그 생성 일시를 파씽하여 @timestamp 필드로 저장한다. 따라서 Kibana에서 로그 생성 일시를 기준으로 정렬된 결과를 편리하게 확인할 수 있다. (만약 애플리케이션의 LogstashSocketAppender이 제공하는 @timestamp 필드를 그대로 사용하면 Logstash는 이를 무시하고 수집한 시간으로 일괄 갱신해버린다. Logstash의 버그로 추정된다.)


  • match에 사용되는 날짜 및 시간 형식은 Joda-TimeDateTimeFormatter의 패턴 만이 허용된다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring, RestTemplate으로 REST 클라이언트 구현하기

개요

웹의 시대가 열린지는 오래되었지만 REST API가 본격적으로 유행한지는 얼마 되지 않았다. 안타깝게도 국내는 비즈니스 로직 구현에만 초점을 맞추어 GET, POST 메써드의 구분 없이 API 요청을 허용한다거나(심지어 POST 요청에 쿼리 스트링을 담기도 한다.) 상태 코드를 사용하지 않고 무조건 200 응답 후 바디에 독자적인 코드를 재정의하는 등 HTTP 스펙의 권고사항을 무시한 API 개발이 너무나도 흔한 상황이다. 그럼에도 REST는 가독성, 유지보수성 등을 고려했을 때 꼭 필요한 설계 철학이다.(세계 최대의 커뮤니티 reddit 또한 REST API를 제공한다.) 이번 글에서는 이러한 REST API를 이용(소비)하는 클라이언트로서 RestTemplate의 사용 예를 설명하고자 한다.

라이브러리 종속성 추가

dependencies {
    compile group: 'org.springframework', name: 'spring-web', version: '4.2.6.RELEASE'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.7.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.7.4'
    compile group: 'org.projectlombok', name: 'lombok', version: '1.16.8'
}
  • RestTemplate를 사용하기 위해 spring-web 아티팩트를 추가하였다.

  • RestTemplatePOJO-JSON 상호 변환시 기본값으로 Jackson 라이브러리를 사용한다. Jackson이 정상적으로 동작하려면 jackson-core, jackson-databind, jackson-annotations 3개 아티팩트를 추가해야 한다.

  • 만약 Jackson을 추가하지 않고 Gson을 추가하면 RestTemplateGson을 대신 사용한다. 어떤 HttpMessageConverter 구현 클래스를 등록하느냐의 차이로 MappingJackson2HttpMessageConverter, GsonHttpMessageConverter가 이 역할을 수행하며 RestTemplate 오브젝트 생성시 생성자로 명시적인 전달이 가능하다.

  • 만약 Spring Boot 기반의 프로젝트로 spring-boot-starter-web 아티팩트를 추가한 상태라면 위에 열거한 아티팩트를 추가할 필요가 없다. 모두 포함되어 있기 때문이다.(Gson은 따로 추가해야 한다.)

  • lombok 아티팩트 추가는 필수가 아닌 선택이다. LombokPOJO 작성시 반복적이고 지루한 RequiredArgsConstructor, Getter, Setter, ToString 메써드 작성을 자동화 해준다.(적지 않은 수의 국내 개발자들 이 작업이 귀찮아 POJO 대신 Map을 사용하는데 이는 Java가 가진 많은 장점을 포기하는 것이다.)

HTML GET 요청

단순한 HTML 응답을 받는 예이다.

HttpHeaders header = new HttpHeaders();
header.add(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE);
ResponseEntity<String> response = new RestTemplate().exchange("http://www.naver.com/", HttpMethod.GET, new HttpEntity(header), String.class);

JSON GET 요청

에코 응답을 제공하는 JSON Test API를 사용하여 예를 작성해보겠다. JSON GET 요청에 앞서 응답을 받을 POJO 클래스를 구현한다. 아래 HeadersResponse 클래스는 JSON 응답을 담는 역할을 한다.

@Data
public class HeadersResponse {

    @JsonProperty("Accept-Language")
    private String acceptLanguage;

    @JsonProperty("Host")
    private String host;

    @JsonProperty("User-Agent")
    private String userAgent;

    @JsonProperty("Accept")
    private String accept;
}
  • 앞서 언급한 Lombok을 이용하여 POJO 구현시 반복적인 메써드의 작성을 제거했다. 클래스 레벨에 @Data를 명시하면 모든 것을 컴파일 시점에 대신 생성해준다.
  • @JsonPropertyPOJO-JSON 상호변환시 정확한 멤버 변수에 맵핑해주는 역할을 한다. Jackson이 제공하는 기능으로 Gson을 사용한다면 Gson이 제공하는 기능을 사용해야 한다.

작성된 POJO을 이용한 요청 예이다.

HttpHeaders header = new HttpHeaders();
header.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_UTF8_VALUE);
ResponseEntity<HeadersResponse> response = new RestTemplate().exchange("http://headers.jsontest.com/", HttpMethod.GET, new HttpEntity(header), HeadersResponse.class);

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, 작업 스케쥴러 데몬 구현하기

먼저 읽어볼만한 글

build.gradle

작업 스케쥴러 성격의 Spring Boot 프로젝트를 새로 생성할 경우 /build.gradle 파일을 아래 내용으로 교체한다. 기존 프로젝트에 적용하고자 할 경우에는 이 부분은 생략한다.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'org.springframework.boot', name: 'spring-boot-gradle-plugin', version: '1.4.0.RELEASE'
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'spring-boot-task-scheduler-example'
    version = '0.0.1'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile group: 'org.springframework.boot', name: 'spring-boot-starter'
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.5'
}
  • Spring Boot 기반의 웹 애플리케이션 개발시에는 spring-boot-starter-web 스타터를 프로젝트에 추가했지만 작업 스케쥴러 성격의 애플리케이션에서는 웹 애플리케이션의 기능이 필요 없으므로 Spring Boot의 기본 기능만 제공하는 spring-boot-starter 스타터를 추가한다.(빌드시 약 5.8MB로 매우 가볍다.)

  • spring-boot-starter 스타터는 spring-boot-starter-logging(Logback 사용) 스타터를 기본 포함한다. 작업 성격에 따라 spring-boot-starter-jdbc, spring-boot-starter-mail, spring-boot-starter-redis 스타터를 추가하면 된다.

Application.java

/src/main/java/com.jsonobject.example/Application.java 파일을 아래와 같이 작성한다.

package com.jsonobject.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {

    public static void main(String[] args) {

        ApplicationContext ctx = SpringApplication.run(Application.class, args);
    }

    @Bean
    public ScheduledExecutorFactoryBean scheduledExecutorService() {

        ScheduledExecutorFactoryBean bean = new ScheduledExecutorFactoryBean();
        bean.setPoolSize(5);

        return bean;
    }
}
  • @EnableScheduling을 명시하여 작업 스케쥴러를 활성화한다. 명시하지 않을 경우 애플리케이션은 시작과 동시에 종료된다. 애플리케이션은 대기 상태를 유지하며 정해진 일정대로 ScheduledThreadPoolExecutor에 의해 관리되는 Worker 쓰레드를 실행하여 스케쥴 작업을 처리한다.

  • 스케쥴 작업은 기본적으로 단 1개의 쓰레드에 의해 실행된다. 즉, 2개 이상의 작업을 같은 시간에 실행되도록 설정할 경우 1개 작업을 먼저 실행하여 완료 후 다른 작업을 순차적으로 실행하는 식이다. 여러 작업이 동시에 실행되기를 원할 경우 쓰레드 풀의 크기를 2개 이상으로 설정하면 된다. 쓰레드 풀을 구성하지 않을 경우 작업 스케쥴의 의해 실행될 빈의 메써드에 @Async를 명시하는 것도 한 방법이다.

CronTable.java

/src/main/java/com.jsonobject.example/CronTable.java 파일을 아래와 같이 작성한다.

package com.jsonobject.example;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class CronTable {

    // 매일 5시 30분 0초에 실행한다.
    @Scheduled(cron = "0 30 5 * * *")
    public void aJob() {

        // 실행될 로직
    }

    // 매월 1일 0시 0분 0초에 실행한다.
    @Scheduled(cron = "0 0 0 1 * *")
    public void anotherJob() {

        // 실행될 로직
    }
}
  • 특정 주기로 실행될 메써드 레벨에 @Scheduled를 명시한다. 위 예와 같이 cron 문법을 사용하여 편리하게 작업 실행 시점을 결정할 수 있다.
  • @Scheduled이 명시된 메써드는 아규먼트를 가질 수 없다. 또한 반환 타입은 void이어야 한다.

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고

Spring Boot, JSON 변환, LocalDateTime을 ISO8601으로 출력하기

개요

Spring Boot(또는 Spring 4 MVC)는 HTTP 요청-응답시 application/json;charset=UTF-8 형식의 메시지에 대해 JSON 문자열-Java 오브젝트를 자동으로 상호 변환해준다. 이 기능을 입맛에 맞게 잘 사용하려면 동작의 상세 원리를 이해하는 것이 필수이다.

먼저 읽어볼만한 글

POJO-JSON 상호 변환의 상세

  • MappingJackson2HttpMessageConverter 클래스가 Spring Boot 초기 구동시 JacksonHttpMessageConvertersConfiguration 클래스에 의해 싱글턴 빈으로 등록되어 POJO-JSON간의 상호 변환을 수행한다.

  • MappingJackson2HttpMessageConverter는 이름에서 보여지듯이 Java 진영에서 널리 쓰이는 JSON 라이브러리인 Jackson(Spring Boot 1.3.5 기준 Jackson 2.6 내장)을 사용하여 메시지를 상호 변환한다.

  • MappingJackson2HttpMessageConverterHttpMessageConverter 인터페이스의 구현체로 read(), write() 메써드를 이용하여 JSON을 처리한다. 내부적으로는 Jackson이 제공하는 ObjectMapper 클래스를 사용하는데 커스텀 설정을 적용하여 미리 빈으로 등록해두면 변환시 자동으로 해당 빈을 대신 사용한다.

ObjectMapper 커스터마이징

현재 시간을 JSON을 반환하는 간단한 기능의 컨트롤러 클래스를 아래와 같이 작성해보자.

package com.jsonobject.example;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.time.LocalDateTime;

@Controller
@RequestMapping(value = "/now")
public class JsonController {

    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity<?> now() {

        return new ResponseEntity<LocalDateTime>(LocalDateTime.now(), HttpStatus.OK);
    }
}

위 기능을 요청하면 아래와 같이 응답한다.

{
    "hour": 2,
    "minute": 7,
    "nano": 468000000,
    "second": 1,
    "dayOfMonth": 11,
    "dayOfWeek": "SATURDAY",
    "dayOfYear": 163,
    "month": "JUNE",
    "monthValue": 6,
    "year": 2016,
    "chronology": {
        "id": "ISO",
        "calendarType": "iso8601"
    }
}

위는 Java 8이 제공하는 LocalDateTime 오브젝트가 JSON으로 변환되어 출력된 결과이다. 클라이언트에게 보여주기에는 지나치게 친절하면서 장황한 데이터 출력이다. 최근 REST API의 추세는 날짜/시간을 ISO-8601 형식으로 응답하는 것이다. ObjectMapper 오브젝트의 커스터마이징 빈 등록으로 이를 해결할 수 있다.


/build.gradle의 의존성에 아래 라이브러리를 추가한다.

dependencies {
    compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4'
}
  • Jackson Datatype JSR310LocalDateTime을 비롯한 Java 8의 날짜/시간 관련 오브젝트를 인식할 수 있는 Jackson 라이브러리의 추가 모듈이다.

커스터마이징된 빈을 등록하기 위한 @Configuration 클래스를 아래와 같이 작성한다.

package com.jsonobject.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class JsonConfig {

    @Bean
    public ObjectMapper objectMapper() {

        return Jackson2ObjectMapperBuilder.json().featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).modules(new JavaTimeModule()).build();
    }
}

동일한 요청을 다시 해보자. 전과 다르게 아래와 같이 ISO-8601 형식으로 출력되는 것을 확인할 수 있다.

"2016-06-11T03:57:47.146"

참고 글

저작자 표시 비영리 동일 조건 변경 허락
신고