Skip to content

[BE] Zzimkkong Time Zone Issue 해결 방안

Jungseok Sung edited this page Nov 2, 2021 · 20 revisions

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

결론 (최종 해결책)

우리는 여태까지 타임존 역경을 헤쳐오며 마침내 문제 상황을 정확하게 파악할 수 있게 되었다. 그리고 나름대로 우리만의 괜찮은 해법도 적용해서 해결했다. 그 과정은 아래 과거의 유물들들 파트에 나와있으니 확인 바란다.

결론적으로, 최종 해결법은 허무하게도 다음과 같다.

public static void main(String[] args) {
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
    SpringApplication.run(ZzimkkongApplication.class, args);
}

단순히 @PostConstruct를 안쓰고 timezone setting을 application run 전에 해주면 되는 것이었다.... TimeZone 클래스는 내부적으로 다음과 같은 default time zone static field를 가지고있다

private static volatile TimeZone defaultTimeZone;

그리고 TimeZone.setDefault()메서드는 해당 필드에 대한 setter이다. static field 이기 때문에 단순히 application이 구동 (run)에 들어가기 전에 setting만 해주면 되는 것이었다...

실험결과는 당연히 잘된다!!!!

Application 구동

Application을 구동할 때 타임존 설정 과정이 어떻게 되는지 로그를 직접 찍어봤다

// 흐름
(JVM) default timezone setting -> application context (DB connection session time zone setting) -> app running

예약 시 time sync 확인

예약 시작 시간: 오후 1시 10분 (13:10)

예약 끝나는 시간: 오후 1시 40분 (13:40)

save와 find 로직시 time sync 확인

DB에 저장상태 확인

너무 명백한 기초적인 사실이었다. 이미 모든 상황을 이해하고 있었음에도 왜 진작에 이렇게 생각 못했을까. 그 이유는 구글링해서 나온 지식을 비판적으로 수용하지않고 그대로 적용하려고만 생각했기 때문이 아닐까 (=@PostConstruct무지성 적용). 구글링에 의존하되 그 마법같은 해결법을 마법으로만 남겨놓으면 안되겠다는 생각이 들었다. 우리 모두 항상 조심합시다!!

마지막으로 해당 해결책에 대한 인사이트를 주신 당근마켓 플랫폼 서버 개발팀 모 개발자님께 감사의 말씀을 전합니다. 깨우침을 주셔서 정말 감사합니다...

과거의 유물들

아래의 설명은 저희가 겪은 타임존 문제 상황에 대한 이해의 용도로 읽어주시길 바랍니다.

어떠한 문제상황이 있었고, 이에 대한 원인을 파악하고 이해하는 과정 정도로 이해해주시면 될 것 같습니다.

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://[DB서버IP]: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