최근 프로젝트에서 관련 테스트 코드를 작성하던 도중, 문득 시간 설정 방식에 큰 허점이 있다는 사실을 깨달았다. 여행 기록 서비스의 타겟은 한국인이지만, 서비스의 핵심은 해외 여행 기록이다. 사용자가 비행기를 타고 지구 반대편으로 떠나는 순간, 지금의 코드로는 감당할 수 없는 문제들이 기다리고 있었다.
1️⃣ 무엇이 문제였을까? (기존 방식 분석)
기존 코드는 당연하게 "한국(Asia/Seoul)" 시간대만을 바라보고 있었다. 이 코드는 해외 유저들의 타임라인은 엉망으로 만든다.
A. Java 엔티티와 비즈니스 로직의 한계
현재 Trip 엔티티는 LocalDate, Diary 엔티티는 LocalDateTime을 사용하고 있었다. 문제는 LocalDateTime에는 타임존(Timezone) 정보가 없다는 점이다. 단순히 "오전 10시"라고 저장하면, 이게 서울의 10시인지 뉴욕의 10시인지 알 길이 없다.
더 큰 문제는 @PrePersist를 통한 기본값 설정 방식이었다.
// Trip.java & Diary.java의 기존 로직
this.startDate = LocalDate.now();
this.createdAt = LocalDateTime.now();
now()는 서버의 시스템 시간을 따라간다. 서버가 한국에 있고 사용자가 뉴욕(시차 -14시간)에 있다면, 사용자가 현지에서 1월 1일 밤에 설레는 마음으로 여행을 생성해도 서버는 이미 날을 넘긴 1월 2일로 기록해버리게 된다.
B. 데이터베이스와 API 설정의 종속성
application.yml의 DB 연결 설정에도 serverTimezone=Asia/Seoul이 박혀 있었다. DB가 특정 국가의 시간대에 종속되어 버린 것이죠. API 요청에서도 Z(UTC) 형식을 예시로 들고는 있었지만, 이를 타임존 정보가 없는 LocalDateTime으로 파싱하는 순간 정보가 유실될 위험이 컸다.
2️⃣ "Backend is Absolute, Frontend is Relative"
테스트 코드를 짜면서 역할 분담의 고민에 빠졌다. 팀 프로젝트에서 프론트엔드에서 위치 처리를 담당하기로 했기에, "자동 생성되는 시간은 누가 담당해야 하는가?"에 대해 역할 분담이 모호해졌다.
구글링을 한 결과 , 글로벌 서비스의 대원칙을 찾았다.
바로 "Backend is Absolute(UTC), Frontend is Relative(Local)"
- 백엔드는 절대적인 기준점인 UTC로 시간을 찍어서 저장한다.
- 프론트엔드는 서버가 준 UTC 시간을 가져와 사용자의 기기 설정에 맞는 로컬 시간으로 보여준다.
이 원칙을 도입하면 누가 시간을 생성할지 헷갈릴 필요가 없다. 백엔드가 기준을 잡고, 프론트는 그 기준을 해석해서 보여주기만 하면 된다.
3️⃣ UTC 표준화를 선택하며
다행히 아직 데이터베이스 마이그레이션 적용 전이라 저장 형식을 변경하는 게 어렵지 않았다. 이번 리팩토링을 통해 얻을 수 있는 이점은 명확하다.
- 글로벌 정합성: 사용자가 지구 반대편 어디에 있든 데이터는 절대적인 시간(UTC)으로 정확히 기록된다.
- 유지보수 용이성: 나중에 서비스가 커져서 서버 인프라를 해외 리전으로 옮기더라도 코드를 한 줄도 수정할 필요가 없다.
- 데이터 신뢰성: 모든 기록의 시간 기준을 백엔드 서버로 통일함으로써, 클라이언트 측의 임의적인 시간 조작을 원천 차단할 수 있다.
단순히 now()를 호출하던 습관에서 벗어나, 데이터가 생성되는 시점과 타임존의 의미를 다시 한번 되새겨보는 계기가 되었다.
4️⃣ 어떻게 바꿨을까? (Before & After)
코드를 하나씩 뜯어고치기 시작했다.
A. 데이터베이스 설정 (application.yml)
가장 먼저 DB가 한국 시간이라는 '특정 국가'의 틀에서 벗어나게 했다.
# Before
url: jdbc:mysql://<db-host>/<database>?serverTimezone=Asia/Seoul
# After
url: jdbc:mysql://<db-host>/<database>?serverTimezone=UTC
B. 엔티티 수정: 타임존 정보 살리기 (Diary.java)
단순 숫자의 나열인 LocalDateTime을 버리고, 시차 정보까지 담을 수 있는 OffsetDateTime으로 교체했다. 그리고 시간을 찍는 주체를 '서버'로 명확히 했다.
// Example entity logic
@PrePersist
protected void onCreate() {
// Store server-side absolute time (UTC)
this.createdAt = OffsetDateTime.now(ZoneOffset.UTC);
}
C. DTO 수정: 역할의 분리 (CreateDiaryRequest.java)
프론트엔드에게 "몇 시인지 알려줘"라고 묻지 않기로 했다. dateTime 필드를 과감히 삭제하여 혼선을 방지했다.
// [Before]
// private String dateTime; // 프론트에서 보내주던 필드
// [After]
// 해당 필드 삭제!
// "시간은 서버가 알아서 할 테니, 프론트는 데이터에만 집중하세요."
📌 마치며:
이번 작업을 통해 단순히 코드를 고친 것을 넘어, 글로벌 서비스가 갖춰야 할 기본기'에 대해 깊이 고민해볼 수 있었다.
처음에는 프론트와 백엔드 중 누가 시간을 담당해야 할지 헷갈렸지만, 백엔드는 절대적 기준(UTC)을, 프론트는 사용자 맞춤형 로컬(Local)을이라는 원칙을 세우니 모든 게 명쾌해졌다.
테스트 코드 덕분에 운영 환경에서 문제가 될 수 있었던 버그를 미리 잡은 것 같아 정말 다행이다. 😊
'Backend > Trouble Shooting' 카테고리의 다른 글
| JPA 자동 스키마 관리에서 Flyway 기반 DB 마이그레이션 (0) | 2026.02.03 |
|---|---|
| [Spring Boot] Swagger(Springdoc)에서 제네릭 공통 응답 스키마가 꼬이는 문제 해결기 (0) | 2026.01.22 |
| [SpringBoot] UTC 시간 처리, 코드가 아니라 설정으로 정리하기 (Hibernate 6 & Jackson) (0) | 2026.01.21 |
| [SpringBoot] 검증 로직의 위치(@Valid vs Service) : 테스트 코드로 드러난 설계 문제 (0) | 2026.01.14 |
| [SpringBoot] 프로젝트에 SonarCloud 연동하기: Gradle + GitHub Actions (0) | 2026.01.05 |