-
Notifications
You must be signed in to change notification settings - Fork 20
[BE] Zzimkkong Time Zone Issue 해결 방안
너무 대서사시 였기 때문에, 최종 해결책과 그에 대한 해설로 요약해보았습니다.
[20230713 기준] 서버 내 타임존은 UTC
로 관리한다!!
관련 PR (https://github.com/woowacourse-teams/2021-zzimkkong/pull/793) 참고
우리는 여태까지 타임존 역경을 헤쳐오며 마침내 문제 상황을 정확하게 파악할 수 있게 되었다. 그리고 나름대로 우리만의 괜찮은 해법도 적용해서 해결했다. 그 과정은 아래 과거의 유물들들 파트에 나와있으니 확인 바란다.
결론적으로, 최종 해결법은 허무하게도 다음과 같다.
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을 구동할 때 타임존 설정 과정이 어떻게 되는지 로그를 직접 찍어봤다
// 흐름
(JVM) default timezone setting -> application context (DB connection session time zone setting) -> app running
예약 시작 시간: 오후 1시 10분 (13:10)
예약 끝나는 시간: 오후 1시 40분 (13:40)
너무 명백한 기초적인 사실이었다. 이미 모든 상황을 이해하고 있었음에도 왜 진작에 이렇게 생각 못했을까.
그 이유는 구글링해서 나온 지식을 비판적으로 수용하지않고 그대로 적용하려고만 생각했기 때문이 아닐까 (=@PostConstruct
의 무지성 적용). 구글링에 의존하되 그 마법같은 해결법을 마법으로만 남겨놓으면 안되겠다는 생각이 들었다. 우리 모두 항상 조심합시다!!
마지막으로 해당 해결책에 대한 인사이트를 주신 당근마켓 플랫폼 서버 개발팀 모 개발자님께 감사의 말씀을 전합니다. 깨우침을 주셔서 정말 감사합니다...
ZzimkkongApplication 클래스에 default timezone 설정
// ZzimkkongApplication
@PostConstruct
void started() {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
}
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
현재시간 이전의 예약시간은 예약이 불가능하도록 검증하는 부분
때문.
EC2 OS의 timezone은 UTC이므로 LocalDateTime.now()
를 하면 UTC의 현재시간이 나온다. 예약자는 KST기준으로 예약하는데 검증은 UTC 현재시간으로 하니 timezone 이슈가 발생한 것. 1번 설정을 통해 Application상의 timezone을 OS 세팅에 관계없이 KST로 맞춰줄 수 있다. 이로써 이 문제는 해결.
하지만 이제 DB에 날짜 시간 관련 데이터 저장하거나 가져오는 부분
에서 데이터 정합성(timezone에 따른 날짜 conversion) 문제가 간헐적으로
발생. 그래서 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
옵션을 통해 관장한다. default
는 30분
이다. 즉, 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은 우리 서비스에서는 크게 의미가 없다고 보면 된다.
요구사항
- JVM의 time zone은 KST여야 한다 (LocalDateTime.now()의 필요성 때문)
- 모든 시간 데이터에 대해서 Time Conversion이 일어나지 않아야 한다
초기 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로 세팅되어야하고 결과적으로 이 방법도 현재 우리의 요구사항을 모두 해결해 줄 수 있어야한다. 확인해보자.
- 예약 현재시간 이전 validation
예약을 생성했을 때, 제대로 검증함을 확인할 수 있다!
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 환경에 걱정없이 타임존 이슈를 해결할 수 있기 때문에 더 좋은 해결책이라고 생각한다.
- https://www.baeldung.com/mysql-jdbc-timezone-spring-boot
- https://moelholm.com/blog/2016/11/09/spring-boot-controlling-timezones-with-hibernate
- https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#basic-datetime-time-zone
- https://coderanch.com/t/729990/frameworks/spring-jpa-properties-hibernate-jdbc
- http://abh0518.net/tok/?p=652
- https://hyunsoori.tistory.com/2