Spring Boot에서 Java8 java.time 패키지(LocalDateTime, ZonedDateTime) 사용

이전의 Spring 환경에서는 시간을 다루는 클래스를 가지고 데이터베이스에 매핑해 사용할 때 문자열로 치환해서 사용하거나 Joda Time을 사용해 TypeHandler로 연결하였으나 최신의 Spring Boot는 java.time 패키지에 대한 내장 핸들러를 지원하므로 최대한 간단한 구현구조를 이루는데 집중해 해당 내용을 살펴본다.

통상적으로 국제적인 처리를 목적으로 하는 경우 mysql 서버는 UTC기준으로 세팅하고 datetime을 UTC 기준으로 사용하므로 여기에서는 웹서버의 Timezone을 UTC로 하는지 아니면 KST로 하는지에 집중해서 살펴본다.

아래의 내용을 살펴보기 이전에 미리 알아봐야 할 사항을 간단히 정리한다.

  • 최신의 Spring Boot는 java.time패키지에 대해서 TypeHandler를 지원하므로 별도의 Mybatis에 대한 TypeHandler가 필요하지 않다.
  • LocalDateTime 및 ZonedDateTime은 모두 @JsonFormat으로 원하는대로 포맷팅이 가능하다. 
  • LocalDateTime은 timezone에 대한 offset 정보가 없기 때문에 서버의 timezone 설정에 따라간다

아래의 표는 클라이언트에서 웹서버로 웹서버에서 DB로 DB에서 웹서버를 거쳐 클라이언트로 가는 과정에서 값이 어떻게 세팅되는지 확인하는 표이다.

LocalDateTime

입력 문자열
서버바인딩(UTC)
서버바인딩(KST)
DB저장(UTC)
DB저장(KST)
DB에 가져와서 Response출력(UTC)
DB에 가져와서 Response출력(KST)
2020-02-02T20:20:202020-02-02T20:20:202020-02-02T20:20:202020-02-02 20:20:202020-02-02 11:20:202020-02-02T20:20:202020-02-02T20:20:20
  • LocalDateTime은 별도의 timezone offset이 없으므로 클라이언트에서 보내는 시간이 서버의 timezone과 동일하다고 생각하고 값을 받아들인다.
  • 서버의 timezone과 DB의 timezone이 다른경우 해당 시간 만큼 offset을 적용하여 DB의 timezone 기준으로 DB에 저장된다.
  • DB저장과 동일하게 DB에서 값을 가져오는 경우에는 DB의 timezone과 서버의 timezone의 offset 만큼 변환하여 설정된다.

ZonedDateTime

입력 문자열
서버바인딩(UTC)
서버바인딩(KST)
DB저장(UTC)
DB저장(KST)
DB에 가져와서 Response출력(UTC)
DB에 가져와서 Response출력(KST)
2020-02-02T20:20:20+00:002020-02-02T20:20:20Z[UTC]2020-02-03T05:20:20+09:00[Asia/Seoul]2020-02-02 20:20:202020-02-02 20:20:202020-02-02T20:20:20Z2020-02-03T05:20:20+09:00
2020-02-02T20:20:20+09:002020-02-02T11:20:20Z[UTC]2020-02-02T20:20:20+09:00[Asia/Seoul]2020-02-02 11:20:202020-02-02 11:20:202020-02-02T11:20:20Z2020-02-02T20:20:20+09:00
  • ZonedDateTime은 timezone에 대한 offset이 있으므로 서버에도 동일한 형식으로 저장된다. 다만 서버의 timezone에 따라서 표시되는 표현만 달라진다.
  • DB에 저장될 때는 LocalDateTime과 동일하게 서버와 DB의 timezone 차이를 반영하여 UTC 기준으로 시간이 설정된다.
  • DB에서 값을 가져올때도 LocalDateTime과 동일하게 서버와 DB의 timezone 차이를 반영하여 UTC 기준으로 시간이 설정된다.

결론적으로 다음처럼 생각해 볼 수 있다.

  • 웹서버의 timezone을 UTC로 설정할 수 있고 Request, Response의 기준을 UTC로 할 수 있다면 웹서버, DB를 모두 UTC로 하고 LocalDateTime으로 하는게 일관성있고 편하다.
  • 클라이언트에서 Response값을 받아 변환할 수 없는 구조라면 ZonedDateTime을 쓰고 get 메서드에서 변환하는 등의 작업을 하는것이 좋다.
  • Joda Time이나 TypeHandler 없이도 일관성 있게 변환이 이루어지므로 Spring Boot에서 지원하는 구조를 사용하자.
  • 최신의 Spring Boot가 아닌경우 Java8 패키지에 대한 지원이 없을 수 있으므로 이 때는 Joda Time을 사용하기 보다 LocalDateTime, ZonedDateTime을 사용하고 TypeHandler로 JSR-310 구현체를 사용한다.(http://bluesky-devstudy.blogspot.com/2016/10/mybatis-java-8-typehandler.html)

위 작업에서 웹서버의 timezone을 설정하기위해 다음의 코드를 사용하였다.(정황상 config등에서 웹서버의 timezone을 지정하는 방법이 있을것 같다.)

@EnableScheduling
@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    SpringApplication.run(Application.class, args);
  }
}

데이터 처리를 위한 Model 클래스에는 특별한 처리를 하지 않았으며 이에 대한 결과는 위 표와 같다. 필요에 따라서는 @JsonFormat으로 입/출력되는 포맷을 변경할 수 있다.

@Data
public class TimezoneTest {
  LocalDateTime localDateTime;
  ZonedDateTime zonedDateTime;
}

mybatis에도 별다른 처리 없이 DateTime type의 컬럼에 INSERT/SELECT를 실행하였다.

int insertTimezoneTest(TimezoneTest timezoneTest);
TimezoneTest selectTimezoneTest();
 
<insert id="insertTimezoneTest" parameterType="com.preludeb.model.TimezoneTest">
  INSERT INTO timezone_test
  (local_date_time, zoned_date_time)
  VALUES
  (#{localDateTime}, #{zonedDateTime})
</insert>
 
<select id="selectTimezoneTest" resultType="com.preludeb.model.TimezoneTest">
  SELECT * FROM timezone_test
  ORDER BY idx DESC
  LIMIT 0, 1
</select>

[Spring]WebClient를 이용한 REST API 호출 Spring Framework

Spring에서 REST API를 호출하기 위한 라이브러리로 RestTemplate이 있는데 이 녀석은 동기 버전이기 때문에 Webflux와 같이 사용하기 어려워 문서를 찾다보니 WebClient가 있어 사용해 보았다.

참고로 JAVADOC에 보면 RestTemplate에 대해서는 다음처럼 NOTE가 있는데

NOTE: As of 5.0, the non-blocking, reactive org.springframework.web.reactive.client.WebClient offers a modern alternative to the RestTemplate with efficient support for both sync and async, as well as streaming scenarios. The RestTemplate will be deprecated in a future version and will not have major new features added going forward. See the WebClient section of the Spring Framework reference documentation for more details and example code.

요약하면 RestTemplate은 deprecated될 예정이며 더 나은 체계를 가지는 WebClient가 있으니까 그걸 써라 정도의 의미로 보인다.(비동기 버전인 AsyncRestTemplate은 이미 deprecated되어 있음) WebClient에서 동기형태로 호출할 수 있으니 RestTemplate도 대체가 가능하긴한데 그건 아래 코드에서 살펴보기로 한다.

WebClient로 비동기 호출을 했을때 RestTemplate의 동기 호출에 대해서 다르거나 좋은 점은 다음과 같다.

1. WebClient는 Netty등의 비동기 통신 라이브러리를 기반으로 한다.
2. 이벤트 루프 구조를 사용하므로 최대 스레드에 대한 관리등의 고민을 할 필요가 없다. 스택오버플로에서 WebClient에 대한 max threads에 대한 문의에 다음과 같은 코멘트가 있는데

Spring WebClient is a No-Blocking IO http client while ReactorClientHttpConnector is a Reactor-Netty based implementation. Said that I can suggest to do not warry about connection pool but focus on a complete no blocking service call. 

WebClient는 비동기 Netty 기반이니까 커넥션 풀에 대한 고민은 하지 말라는 이야기고 구조가 다르기 때문에 RestTemplate의 스레드 관련 설정 값도 없다.

3. 기본적으로 비동기 처리이므로(동기 형태로도 가능) REST 호출 후 추가적으로 기다릴 필요가 없을 때나 다른 처리와의 의존성이 없을때 다른 처리가 이 REST 호출을 기다리지 않아도 된다.

WebClient를 사용하기 위해서는 Gradle 기준으로 다음의 의존성을 요구한다. 

compile 'org.springframework.boot:spring-boot-starter-webflux'
compile 'io.projectreactor:reactor-test'
의존성의 내용처럼 WebClient는 WebFlux(Spring의 비동기 HTTP 처리)와의 연동을 전제로 구성되어있고 기본적인 동작은 비동기 처리를 전제로 한다.

WebClient는 builder를 통해 생성하며 builder는 thread-safe 하지 않으므로 각 호출 혹은 Injection Point에 대해서 별도의 인스턴스를 생성해야 한다. 때문에 RestTemplate을 Bean으로 만들어 사용하는 것과 가장 비슷하게 사용하려면 다음처럼 코딩해서 사용하면 된다. webflux에 대한 의존성을 추가하면 WebClient.Builder의 의존성은 Spring이 처리해주므로 Builder에 대한 Bean은 직접 생성하지 않아도 된다.

@Service
public class TestService {
    private final WebClient webClient;

    public TestService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("http://www.test.com").build();
    }

...

이렇게 생성한 webClient는 다음처럼 비동기 호출하여 사용할 수 있다.

long userId = 10;

        Mono<String> result = webClient
                .get()
                .uri(String.format("/getuser", userId))
                .retrieve()
                .bodyToMono(String.class);

        result.subscribe(response -> {
System.out.println("response : " + response);
        }, e -> {
            System.out.println("error message : " + e.getMessage());
        });
위의 호출에서는 단순히 응답 내용을 얻기 위해서 rerieve()를 사용했지만 exchange()를 사용하면 헤더 등의 부가 정보에 접근할 수 있다. 비동기 호출이므로 WebClient의 반환값은 Mono<T>로 받아 subscribe로 처리하지만 동기의 경우에는 block() 메서드를 사용하면 RestTemplate 처럼 동기식으로 처리할 수 있다.

        String result = webClient
                .get()
                .uri(String.format("/getuser", userId))
                .retrieve()
                .bodyToMono(String.class).block();
System.out.println("response : " + result);

1 2 3 4 5 6 7 8 9 10 다음