코드잇 BE스프린트

알림 조회 시 겪은 타임존 변환 문제와 해결과정

beginner-development 2025. 12. 15. 15:08

코드잇 부트캠프 Spring 백엔드 과정의 고급 프로젝트를 진행하면서, 알림 도메인에 대한 개발을 진행하였다.
모든 엔티티의 공통 변수는 Base Entity를 만들어 관리하도록 하였고 createdAt, updatedAt, deletedAt을 Base에 자료형을 LocalDateTime으로 선언하여 두었는데, LocalDateTime은 불변 객체라 한 번 생성되면 값이 변경되지 않고 날짜와 시간 개념이 명확하며 타임존과 분리되어 있어 순수한 시간 표현에 적합하다는 장점이 있다. 하지만 알림 도메인 개발도중에 문제가 발생하였다.

발생한 문제

DB에 알림 발생 시간이 저장될 때 LocalDateTime이 기본 시스템 타임존인 UTC로 저장된다. 하지만 LocalDateTime 자체는 들고 있는 타임존 정보 자체가 없기 때문에 저장된 발생 시간을 호출할 때 Front에서 발생 시간을 KST로 인식하여 그대로 들고 오기 때문에 알림 발생 시간은 항상 9시간 차이가 생겨났다.

저장되는 시간을 로그로 관찰
알림 도메인 개발하면서 발생한 문제

해결과정

첫번째 과정 : 

이 문제를 해결하기 위해 고민하던 도중, 
'그러면 애초에 저장할 때부터 KST로 저장하면 되는게 아닌가?'라는 생각이 떠올랐다. 그래도 확실하게 짚고 넘어가는게 좋을거 같아 강사님에게 질문을 드려봤는데 DB에 저장할 때는 UTC로 저장하는게 맞다고 한다. 그 이유를 찾아보니 UTC는 국제표준시이며 시간대 변환의 기준점이기에 전 세계적으로 동일한 시점을 나타내는 명확성을 가지며, 시스템의 모든 부분에서 시간 데이터를 일관되게 처리할 수 있는 일관성을 가지기 때문이었다.

 

두번째 과정 : 

첫번째 과정에서 강사님에게 자문을 구하면서 강사님께서 아래와 같은 설정을 application.yml에 넣으면 응답할 때 자동으로 기준시간 변경이 가능할 것이라고 알려주셨다.

spring:
  jackson:
    time-zone: Asia/Seoul


그러면 KST로 저장하는 방법은 안되니 LocalDateTime으로 저장하되 이 설정을 이용해서 '알림 조회에서 KST로 변환된 시간을 가져오도록 하면 되겠구나!'라고 생각했으나.....
똑같은 오류가 발생했다!

그래서 웹 서칭을 하며 알아보던 중, 'LocalDateTime은 "타임존/오프셋이 없는 순수 날짜,시간" 타입이라 jackson이 타임존을 변환하려 할 때 저장된 시간이 UTC기준인지, KST기준인지 전혀 알 수 없다.' 라는 사실을 알게 되었다. 자동 변환이 작동하지 않는다는 것을 알게 된 이후, 수동으로라도 변환을 시켜주기 위해 Mapper에 아래처럼 타임존 수동변환 로직을 추가해주었다.

@Named("utcToKst")
    default LocalDateTime utcToKst(LocalDateTime utc) {
        if (utc == null) {
            return null;
        }

        return utc
                .atOffset(ZoneOffset.UTC)
                .atZoneSameInstant(ZoneId.of("Asia/Seoul"))
                .toLocalDateTime();
    }

자! 이제 되었겠지!

라고 생각했으나 팀 프로젝트를 진행하면서 사용 중인 코드래빗이 리뷰를 해주면서 "LocalDateTime을 UTC로 판단하는 것은 섯부르다! 엔티티가 서버의 로컬 시간을 저장하므로 해당 구현이 UTC라고 가정하는 것은 잘못되었다!"라고 코멘트를 날렸다. 이런 코멘트가 달린 이유를 알아보니 LocalDateTime은 JVM의 로컬 시스템 기준, 타임존 정보 없이 날짜와 시간을 함께 다루는데 사용되기 때문이었다. 즉, JVM이 다른 서버에 설치되어 로컬 시스템의 시간 기준이 변하게 되면, LocalDateTime으로 저장된 시간의 기준 시간도 변경되는 것이다.

 

세번째 과정 : 

두번째 과정을 겪고 강사님에게 다시 질문을 드리니, 강사님께서는 '타입이름이 LocalDateTime이면 개발자던 DBA던 로컬시간으로 인식하는 경우가 있기에 엔티티에는 UTC와 Instant를 쓰고 응답할 때만 변환해서 줄 것 같다.'라는 답변을 주셨다.

그리고 나는 이 때 깨달음을 얻었다.

아! 무조건 LocalDateTime을 고집하지 않아도 되는거구나! DB에 넣을 때 UTC로 넣어야 되면 생성할 때부터 UTC로 생성하면 되는거였어!

하지만 Base Entity의 시간 변수들의 데이터 타입을 변경하게 되면 모든 도메인에 영향이 있기 때문에 이 문제는 팀원들과 회의 시간에 충분한 상의 후, LocalDateTime 대신 Instant를 사용하기로 하였다. 이후, LocalDateTime이 사용되는 부분을 전부 Instant로 변경하고 Mapper에 생성해 놓은 타임존 수동 변환 로직은 제거, application.yml의 설정으로 타임존 자동 변환이 가능하도록 설정해 놓았다.

 

결과

세번째 과정까지 겪은 후, 테스트해보니 9시간의 차이 없이 모든 정상적으로 알림이 생성되었다!

 

UTC를 사용하는 것은 Instant 외에도 OffsetDateTime이 있지만, OffsetDateTime은 날짜 + 시간 + UTC 오프셋과 같은 형식이라 타임존 ID없이 오프셋만 존재하기 때문에 DB에 저장시 '정확한 시점 보존'이라는 목적에 부합하지 않아 사용하지 않는다.(Offset은 "표현정보")