Skip to content

[BE] Zzimkkong Time Zone Issue 해결 방안

Jungseok Sung edited this page Sep 20, 2021 · 20 revisions

너무 대서사시 였기 때문에, 최종 해결책과 그에 대한 해설로 요약해보았습니다.

결론 (최종 해결책)

1번 설정

ZzimkkongApplication 클래스에 default timezone 설정

// ZzimkkongApplication
@PostConstruct
void started() {
  TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}

2번 설정

Spring Boot Profile 설정 (Application - DB 사이 session time zone 설정)

// application-prod.properties
app.datasource.master.url=jdbc:mysql://192.168.1.238:3306/zzimkkong?characterEncoding=UTF-8&useLegacyDatetimeCode=false
…
spring.jpa.properties.hibernate.jdbc.time_zone=Asia/Seoul

// applicaiton-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3306/zzimkkong?characterEncoding=UTF-8&useLegacyDatetimeCode=false
spring.jpa.properties.hibernate.jdbc.time_zone=Asia/Seoul

설정들이 필요한 이유

1번 설정

현재시간 이전의 예약시간은 예약이 불가능하도록 검증하는 부분 때문.

EC2 OS의 timezone은 UTC이므로 LocalDateTime.now()를 하면 UTC의 현재시간이 나온다. 예약자는 KST기준으로 예약하는데 검증은 UTC 현재시간으로 하니 timezone 이슈가 발생한 것. 1번 설정을 통해 Application상의 timezone을 OS 세팅에 관계없이 KST로 맞춰줄 수 있다. 이로써 이 문제는 해결.

하지만 이제 DB에 날짜 시간 관련 데이터 저장하거나 가져오는 부분에서 데이터 정합성(timezone에 따른 날짜 conversion) 문제가 간헐적으로 발생. 그래서 2번 설정이 필요하게 됨

2번 설정 (좀 깁니다 ㅠ)

사실 1번 설정을 통해 모든 것은 해결된 상태이다. DB와 데이터 정합성 문제도 사실은 1번 설정으로 해결된다!!

문제는 웹 애플리케이션을 새롭게 띄우는 경우 (i.e. 젠킨스 CI/CD로 인한 재배포) 이다. 애플리케이션을 새로 띄우면 DB와의 커넥션을 새롭게 생성한다. 이 connection session의 time zone은 별도의 설정이 없다면 Application (JVM)의 timezone을 토대로 timezone sync를 맞춰준다.

근데 1번 설정을 통해 Application timezone을 분명히 KST로 바꾸어 줬는데 왜 sync가 맞지않는 문제가 발생할까?

그 이유는 @PostConsturct 때문이다. @PostConstruct의 설명은 다음과 같다

The PostConstruct annotation is used on a method that needs to be executed after dependency injection is done to perform any initialization.

즉, 필요한 모든 Bean들이 모두 DI된 후 실행되어야하는 메서드인 것이다.

ZzimkkongApplication에 선언된 @PostConstruct이므로 모든 bean들이 application context에 올라간 후 1번 설정 메서드가 실행될 것이다. 그리고 그 bean들 중에는 DB 관련 bean들도 존재할 것이다. @PostConstruct가 선언된 메서드가 실행되기 이전에 DB 관련 bean 등록이 이루어 지는 것이다. 그리고 그 당시의 Application의 timezone은 아직 UTC이다! (@PostConstruct 실행 이전이기 때문). 결과적으로, Application이 새로 띄워지면 Application의 timezone은 분명히 KST 이지만 DB와의 connection에서는 UTC로 session timezone이 세팅되는 것이다. 그래서 DB와 데이터를 주고받을 때, db connection session은 application과 db사이에 타임존 간극이 존재한다고 생각한다. 결과적으로, Application과 DB사이의 날짜 교환이 일어날 때, 날짜 관련 데이터들은 자동으로 변환 (time conversion)된다.

// 흐름
Application 재실행 (UTC) 
-> Application Context 생성 중… DB 관련 bean 설정 도 이때 됨 (UTC) 
-> @PostConstruct 메서드 실행 후 Application 띄움 (KST)

로그로 직접 확인한 결과는 다음과 같다

로그

로그에서 알 수 있듯이 HikariPool, Hibernate와 같은 DB 관련 설정이 모두 끝나고 난 후에야 @PostConstruct 메서드가 실행되는 것을 확인할 수 있다. 그러면 당연히 이 과정속에서 DB connection session 설정이 이루어졌을 테고 해당 session의 time zone은 그 당시 application의 timezone인 UTC로 설정 되었을 것이다. 빨간색 WARN 로그에서 확인할 수 있듯이 time zone setting가장 마지막에 이루어진다. 이 부분이 문제가 되는 것이다.

하지만 어느정도 시간이 지나면 (실험 결과 대략 40 ~ 50분, 아래 사진 참고) timezone sync가 또 자동으로 맞춰지긴 한다. 여태까지 계속 timezone 문제 해결 해오면서, 해결 됐다가 안됐다가 한다고 느꼈던게 이 부분 때문이라고 생각한다.

(뇌피셜 주의) 이는 Application과 DB 사이의 커넥션 refresh 주기(?)가 있는 것으로 추정된다. 이부분은 아직 확실하게 밝혀지지 않았다. 하지만 가능성 농후

(뇌피셜 → 오피셜)

HikariPool은 connection의 life cycle을 max-lifetime 옵션을 통해 관장한다. default30분이다. 즉, 30분동안 커넥션을 유지하고 이 시간이 지나면 커넥션 풀의 커넥션들을 새로운 커넥션으로 갈아끼운다. 위에 취소선 그어진 뇌피셜이 맞았던 것이다.

이를 실제로 확인해보기 위해서 HikariPool의 max-lifetime 설정을 1분으로 설정해주고 실제로 1분이 지나면 커넥션을 갈아끼우는지 실험을 해봤다.

그 결과 아래 사진과 같이 진짜 1분 후에 db와의 커넥션이 다시 맺어지면서 time zone 이슈가 해결되는 것을 확인할 수 있다!! 이제서야 모든 비밀이 풀렸다. 이 HikariPool의 max-liftime개념 때문에, 배포후 time zone이 정상화되는데 30분의 시간이 소요됐던 것이다. 그리고 이 텀 때문에, 타임존 문제가 자꾸 오락가락 하는 것 처럼 보였던 것이다.

어찌 됐건, 우리가 원하는 것은 Application과 DB의 connection시 timezone sync가 Application을 띄우는 순간 바로 맞아 떨어지는 것이다.

그래서 2번 설정이 필요하다!

2번 설정을 통해, Hibernate가 직접 명시적으로 time zone을 지정 해주도록 설정할 수 있다.

그렇다면 Application과 DB connection이 맺어질 때, Application (JVM) timezone을 사용하지 않고 Hibernate직접적으로 session timezone에 대한 컨트롤이 들어간다. 그러면 DB connection session이 만들어지는 순간 time zone을 KST로 명시해줄 수가 있고, Application이 올라가는 순간 DB와 sync가 즉시 맞아 떨어지게 되는 것이다.

추가로, Hibernate가 직접 session timezone을 컨트롤하기 때문에 DB 서버의 timezone 세팅이 어떻게 되어있는지는 상관이 없다. 이는 현재 우리가 DB에 날짜 시간 데이터를 DATETIME 데이터를 저장하고 있기 때문이다. 참고로 DATETIME은 timezone 정보가 없는 날짜 데이터 타입이고(그냥 VARCHAR라고 생각하면 됨) TIMESTAMP는 timezone 정보가 포함된 날짜 데이터 타입 (UTC 기준으로 변환하여 저장)이다. 그래서 DB의 timezone setting은 우리 서비스에서는 크게 의미가 없다고 보면 된다.

번외편

OS 설정 변경을 통한 Time Zone issue 해결법

요구사항

  1. JVM의 time zone은 KST여야 한다 (LocalDateTime.now()의 필요성 때문)
  2. 모든 시간 데이터에 대해서 Time Conversion이 일어나지 않아야 한다

JVM 타임존 설정 OS (EC2 Ubuntu) 단에서 바꾸기

초기 timezone issue를 해결하기 위한 방법으로 OS의 timezone을 변경하려고 시도한 적 있었다.

정확히 아래와 같은 커맨드로 말이다.


sudo rm /etc/localtime

sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

분명히 OS가 KST로 바뀌었음을 두 눈으로 확인도 했었다.

그런데, 이 방법으로 해결되지 않았다.

분명히 JVM은 OS의 timezone을 따라간다고 했었는데 왜그럴까? 이론대로라면 JVM도 이제 KST로 바뀌어야하는데, 실험 결과 JVM은 그대로 UTC로 timezone을 설정하고 있었다.

그 이유는 JVM은 OS (EC2 Ubuntu 기준)의 timezone을 읽어올 때, /etc/timezone이라는 파일을 읽어서 timezone을 세팅하기 때문이다. 그래서 심볼릭 링크를 통해 OS의 timezone을 변경해줘도 Application이 올라갈 때 JVM의 timezone에 아무런 영향을 주지 못한 것이다. 즉, OS 자체의 시간대를 바꾸는 것은 의미가 없다.

그래서 /etc/timezone이라는 파일을 다음과 같이 변경한 후 재실행 해보았다.

// 기존 /etc/timezone 
Etc/UTC

// 변경 후 /etc/timezone
Asia/Seoul

이론대로라면, 이제 JVM은 KST로 세팅되어야하고 결과적으로 이 방법도 현재 우리의 요구사항을 모두 해결해 줄 수 있어야한다. 확인해보자.

1번 요구사항

  • 예약 현재시간 이전 validation

예약을 생성했을 때, 제대로 검증함을 확인할 수 있다!

2번 요구사항

Application - DB에서 time conversion이 일어나는지 검사해봤다 이는 공간 생성을 통해서 실험 해보았다.

available start time: 07:00, available end time: 23:00

  • Application -> DB

  • DB -> Application

모두 sync가 맞아 떨어지는 것을 확인 할 수 있다!!

이 해결책은 scale-out에 불리하다. OS의 설정변경을 통해 JVM을 컨트롤하는 방법이 OS 마다 다를 수 있고, 매번 새 EC2로 확장할 때 마다 이 번거로운 작업을 해줘야하는 것은 구리다고 생각한다. 현재 우리의 해결책은 Application 상에서 단순한 설정으로 OS 환경에 걱정없이 타임존 이슈를 해결할 수 있기 때문에 더 좋은 해결책이라고 생각한다.

Reference

Clone this wiki locally