Spring Boot로 프로젝트를 진행할 때, Local 노트북 환경에서는 문제없이 동작하던 시간 비교 로직이 리눅스 우분투 환경에서는 예상과 다르게 동작하는 현상을 발견했다. Client에서 요청에 finished_at과 같은 (날짜 + 시간) 데이터를 전달할 때, 원하는 값에 9시간이 더해져서 DB에 저장되고 있었다. 이 글에서는 이러한 문제가 왜 발생했는지 분석하고, 지금까지 Spring Boot 개발 과정에서 적용되었던 시간 관련 설정들을 정리하며 학습한 내용을 공유하고자 합니다.
1. 문제 원인 파악
Client에게 요청을 받아와 저장하는 로직에 log를 남기고 우분투 서버에서 확인해 보았을 때, 다음과 같은 문제를 발견할 수 있었다.
- LocalDateTime.now()로 생성된 날짜가 UTC로 생성된다.
- Request로 받아온 날짜는 정상적으로 찍히지만 db에 저장할 때 9시간이 더해져서 저장된다.
먼저 LocalDateTime.now()로 생성된 날짜는 db에 저장될 때 원하는 값으로 저장되고 있었다. 그러나, Client의 Response에는 UTC로 응답되고 있었다.
DB 저장 -> KST
Client Response -> UTC
KST -> 한국시간
다음으로 유저의 Reqeust로 받아온 날짜는 저장될 때는 한국시간으로 잘 받아오지만 한국 시간에 9시간이 더해져 DB에 저장되고 있었다. 해당 데이터를 재조회시 Response로는 정상적으로 KST로 응답하고있었다.
DB저장 -> KST + 9시간
Client Response -> KST
해당 로그를 통해 파악한 문제는 다음과 같다.
- LocalDateTime.now()로 날짜를 생성할 때, 우분투에서 배포한 애플리케이션의 경우 UTC로 생성된다.
- UTC로 생성된 날짜가 DB에 저장될 때 9시간이 더해져서 저장된다.
- Request로 받아온 날짜의 경우 한국 시간으로 입력되었지만, 애플리케이션에서는 UTC로 취급해 DB에 저장시 9시간이 더해진다.
1번 문제의 경우 LocalDateTime.now()가 어떤 기준으로 날짜를 생성하는지를 파악하면 되었다. 따라서 LocalDateTime의 공식문서를 확인하였는데 LocalDateTime의 경우 따로 타임존이 존재하지 않았고 따라서 Clock.systemDefaultZone()를 호출하여 값을 생성한다는 것을 알 수 있었다.
이후, 진행한 것은 노트북 환경의 JVM 기본 타임존과 우분투 환경의 JVM 기본 타임존을 살펴보았다.
노트북과 우분투 환경에서 Spring Boot 애플리케이션을 실행시키고 다음 명령어로 프로세스 id를 파악한다.
jps -l

이후, 다음 명령어를 통해 JVM 기본 타임존을 확인하였다.
jcmd 25124 VM.system_properties | Select-String "user.timezone"
우분투의 경우 UTC 시간대였고 노트북의 경우 한국 시간대로 나왔다.


즉, 문제의 원인은 JVM 기본 타임존이 다른 부분에서 발생하고 있었다.
그럼 DB에 잘못 저장되는 이유는 뭘까?
이는 데이터소스설정 시 serverTimezone을 한국 시간대로 지정해두었기 때문입니다. 반면, 우분투 환경에서는 LocalDateTime.now()로 생성되거나 클라이언트 요청을 통해 전달받은 시간이 모두 UTC 기준으로 처리되고 있었습니다. 그 결과, JVM에서 생성한 시간이 DB에 저장될 때, 설정된 UTC기준으로 9시간이 추가되어 저장되는 문제가 발생한 것입니다.
datasource:
url: jdbc:mysql://localhost:3306/example?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
2. 문제 해결
문제 해결은 간단했다. 다음 코드를 추가하여 JVM의 타임존을 Asia/Seoul로 설정만 해주면 해결된다.
@Component
@Slf4j
public class TimeZoneConfig {
@PostConstruct
public void setupDefaultTimeZone() {
ZoneId before = ZoneId.systemDefault();
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
ZoneId after = ZoneId.systemDefault();
log.info("JVM Default TimeZone {}→{}", before, after);
}
}
3. 설정 파일의 jackson.time-zone: Asia/Seoul
지금까지 설정파일에 사용해왔던 다음 설정값은 의미가 없음을 이번 기회에 파악해볼 수 있었다.
spring:
jackson:
time-zone: Asia/Seoul
그 이유는 LocalDateTime 타입의 경우 타임존 정보가 없는데, 위 설정의 경우 타임존 정보를 가지고 있어야 직렬화/역직렬화할 때 의미가 있었다.
즉, 요청이나 응답에서 정확히 변환하는 것을 보려면 필드 타입이 타임존 정보를 갖고 있어야 가능한 것이었다.
(ex. OffestDateTime, ZonedDateTIme 등)
따라서, 프로젝트에서 현재 (날짜+시간) 정보로 사용하는 타입은 LocalDateTime이었고 그에 따라 해당 설정정보를 제거하였다.
4. 마무리
먼저, 우분투 환경에서 JVM의 기본 타임존이 UTC로 설정되어 있다는 점을 미처 예상하지 못했습니다. 또한 개발 과정에서 자주 사용하던 LocalDateTime 타입 역시, 그 원리를 충분히 이해하지 않고 사용해왔다는 사실을 깨닫게 되었습니다.
이번 글을 통해 Java의 날짜/시간 API가 타임존이 있는 타입과 없는 타입을 명확히 구분하여 관리한다는 점을 알 수 있었고, 제가 사용 중인 LocalDateTime이 과연 적절한 선택이었는지 돌아봐야겠다고 생각하게 되었습니다.
'Spring Boot' 카테고리의 다른 글
| Spring Boot 로깅 (2) - logback-spring.xml 설정 및 Grafana Loki 연동 (0) | 2025.09.21 |
|---|---|
| Spring Boot 로깅 (1) - 기초 (0) | 2025.09.16 |
| Spring Rest Docs (3) - Validation 규칙을 API 문서에 추가하기 (restdocs-api-spec) (1) | 2025.07.08 |
| Spring Rest Docs (2) - API 문서에 디자인 입히기 (swagger, redoc) (0) | 2025.07.08 |
| Spring Rest Docs 사용하기 (1) - 기본 설정과 API 문서 생성 (0) | 2025.07.04 |