- 채팅 어플리케이션 개발 중에 채팅을 보낸 시간이 제대로 저장이 안되는 버그가 발생하였고, 문제를 해결한 과정을 정리해 보았다.
- 사용 스택
- Server : Spring boot & AWS EC2 Ubuntu 인스턴스
- DB : MongoDB
- 제가 해결한 방식만이 옳은 건 아니니 참고만 해주시길 바랍니다!
- 문제 1: LocalDateTime을 MongoDB가 자동으로 UTC로 저장해버리는 문제
- 채팅을 저장하려고 할때 LocalDateTime.now() 메소드를 통해 MongoDB에 LocalDateTime타입 그대로 저장을 했다. 여기서 Java 어플리케이션 자체에선 KST로 잘 출력되던게 DB에 BSON으로 Converting되는 과정에서 UTC로 변환되어서 저장이 되어버렸다.
- LocalDateTime 객체 뜯어보기
- 우선 LocalDateTime.now() 는 현재 컴퓨터 서버의 시간을 기준으로 해서 설정된다. 따라서 local에서 java application을 돌려보면 제대로 KST로 설정되어서 찍힌다.
- 그렇지만 LocalDateTime은 Timezone 정보를 갖고 있지 않으며, systemUTC() 메소드를 사용하고 있기 때문에 표준 UTC 시간을 사용하여 해당 클래스들은 한 지역이나 시간대에 묶이지 않으며 어느 한 지역 혹은 타임존에 얽매이지 않는다. 즉, 지역성을 부여하기 전에는 의미를 지니지 않은 클래스들이라고 할 수 있다.
- EX) 내가 일어나는 시간을 7시라고 했을 때, 전세계 어딜 가든 7시에 일어날 것이다. 반드시 우리나라 시간으로 7시에 일어난다.. 이게 아닌 것처럼
- EX) 또는 내가 11월 18일에 태어났다고 했을 때 전세계 어딜 가든 11월 18일에 태어났다고 할것이다. 미국 시간으로는 11월 17일에 태어났어요~ 하진 않는다.
이 클래스는 시간대를 저장하거나 나타내지 않습니다. 대신, 이것은 생일과 같은날짜와 시계에 보이는 현지 시간을 결합한 것입니다. 오프셋과 표준시와 같은 추가 정보가 제공되지 않는 한 타임라인의 특정 지점(Instant)을 나타낼 수 없습니다.
- 따라서 LocalDateTime은 글로벌한 서비스나, 미국같이 지역별로 timezone이 다른 서비스를 운영할때는 딱히 적합하진 않다. 우리나라처럼 지역별로 시차가 나지 않는다면 뭐..
- 참고 : https://jaimemin.tistory.com/1537
- 따라서 LocalDateTime.now()로 Spring 로그를 찍었을때는 문제가 없었으나, 문제는 MongoDB에서 해당 객체를 저장할때 UTC 시간대로 변환하여 저장한다는 것이었다.
- 여기서 LocalDateTime이 아닌 다른 타입에 대해 알아보자.
- Instant
-
- Instant는 UTC의 타임라인에 있는 한 순간(moment)으로, 1970년 UTC의 첫 번째 모멘트의 발생 이후 nano초 동안의 시간이다.대부분의 비즈니스 로직, 데이터 스토리지 및 데이터 교환은 UTC여야 하므로 Instant는 자주 사용할 수 있는 편리한 클래스이다.
- 여기서 ZoneId가 할당된 Instant를 ZoneDateTime이라고 한다.
- 거의 모든 백엔드, 데이터베이스, 비즈니스 로직, 데이터 지속성, 그리고 데이터 교환은 UTC 형식이어야 한다. 그러나 사용자에게 보일 때는 사용자가 예상하는 표준 시간대로 변환해서 출력해야 하므로 따로 Convert를 해줘야 한다.
-
- 따라서 나는 타임존이 부여된 Instant인 ZonedDateTime 객체를 사용하여 DB에 곧바로 저장하려 했으나 MongoDB는 ZonedDateTimeConverter를 지원하지 않으며, 오류가 발생한다. 따라서 String이나 Date 타입으로 저장하여 따로 Converter를 만들어 줘야 한다.
-
🔗 https://www.baeldung.com/spring-data-mongodb-zoneddatetimeorg.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class java.time.ZonedDateTime
- 우선은 우리나라에만 국한된 서비스이므로 로컬 서버에는 KST로 찍히니까 시간을 ToString 객체로 저장해서 아예 변동이 없게 개발하려고 했다. (멍청)
LocalDateTime.now().ToString()
- Instant
- 문제 2 : AWS 서버에 올렸더니 시간대가 또 제대로 안찍힘.
- 아예 String으로 바꿔서 저장했으니 MongoDB의 문제는 아님. java application 내에서 타임 존이 제대로 안찍히는 것 같다.
- 또 문제가 뭐냐..하고 봤더니 우선 AWS 서버 타임존을 설정을 안해줬었다.
- sudo rm /etc/localtime
- sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
- 그래도 서버에 올리고 로그를 확인하면 LocalDateTime.now()의 시간대가 UTC로 찍힌다. 원인을 계속 구글링해보니 spring boot 프로젝트가 jvm단의 시간을 따르기 때문에 jar 파일을 실행시킬때 타임존 옵션을 따로 주어야 했다.
java -jar -Duser.timezone=Asia/Seoul capstone-0.0.1-SNAPSHOT.jar
- 또는 스프링 프로젝트 자체에서 Timezone을 설정할 수 있다.
@SpringBootApplication public class BreadMapBackendApplication { // Bean 생명주기를 이용한 timezone 설정 @PostConstruct public void started() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); } public static void main(String[] args) { SpringApplication.run(BreadMapBackendApplication.class, args); } }
- 작동원리
- @PostConstruct 는 빈이 완전히 초기화되고 한번만 호출되는 메소드이다.
- 따라서 어플리케이션을 초기에 실행할 때 원하는 대로 Timezone을 변경할 수 있게 된다.
- 또는 LocalDateTime 객체 자체에 타임존을 설정해주면 +9시간이 되게 찍히고, 이를 String으로 저장하면 DB단에서 따로 변경되는 것이 없으므로 문제가 발생하지 않았다. 그러나 LocalDateTime은 어떤 시간대의 특정한 시간을 나타내는게 아니고, 채팅 어플리케이션에서는 정확히 언제 보냈는지가 중요하기 때문에 사용을 지양하자.
chat.setCreatedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")).toString());
- 최종고찰
- 우선 LocalDateTime보다는 최대한 타임존의 정보가 있고 변환이 쉬운 ZonedDateTime을 사용하고, DB엔 UTC 시간대로 저장한 이후에 사용자의 타임존에 맞게 Convert하도록 해야 할 것 같다.
- 어찌됐든 데이터를 계속 일관성있게 관리하는게 중요한 것 같다. 정답은 없지만 DB-Server-Web Application 사이에 타임존의 불일치가 발생하지 않도록 한다.
- 따라서 내가 생각한 가장 적합한 방식은 다음과 같다.
- Host Server와 MongoDB 그 어느 쪽에도 종속적이지 않은 방법인 것 같다.
//1. 스프링 어플리케이션 자체 타임존을 UTC로 설정
//2.ZonedDateTime 객체를 생성 (UTC가 디폴트값이며, 서버에 영향 받지 않음)
ZonedDateTime utcTime = ZonedDateTime.now();
//3.MongoDB에 저장하고자 할때는 UTC로 저장한다.
//그러나 ZonedDateTime 타입 그대로 저장할수 없으니 String type으로 저장. (MongoDB에서 따로 변환 X)
chat.setCreatedAt(utcTime.toString());
//4.그 다음 데이터를 불러오고자 할 때는 DB에 저장된 String 값을 ZonedDateTime으로 컨버팅해준다.
public static ZonedDateTime toZonedDateTime(String datetimeString) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd.HH-mm-ss");
return ZonedDateTime.of(LocalDateTime.parse(datetimeString, formatter));
}
//5.마지막으로 컨버팅된 ZonedDateTime 값을 각 타임존에 맞게 설정해준다.
ZonedDateTime seoulTime
=ZonedDateTime.of(toZoneDateTime(datetimeString), ZoneId.of("Asia/Seoul"));
+) 개발을 실제로 하다보면 생각지도 못한 문제에 많이 부딪히는 것 같다.. 또 한번 알아가는 좋은 경험 ! :)
🙏참고 문서
https://docs.oracle.com/javase/10/docs/api/java/time/LocalDateTime.html
https://docs.oracle.com/javase/10/docs/api/java/time/ZonedDateTime.html
http://egloos.zum.com/preludeb/v/7482820
https://perfectacle.github.io/2018/09/26/java8-date-time/
'Web > Java (Spring+JSP)' 카테고리의 다른 글
Spring Webflux/Netty/MongoDB로 채팅 서버 구현 (0) | 2022.05.24 |
---|---|
Spring Boot에서 구글 소셜 로그인 REST 방식으로 구현하기 (14) | 2022.03.11 |
JWT을 활용한 로그인/ Interceptor를 활용한 인가 처리 구현 (0) | 2022.03.11 |
🍃 Spring Boot로 개발한 Restful API 작동 프로세스 (0) | 2022.03.03 |
Spring 핵심 원리 #9- 컴포넌트 스캔 (0) | 2022.01.15 |