카테고리 없음

Java Spring 프로젝트에서 시간(Timezone)을 UTC로 통일

iamhyeon 2026. 1. 2. 23:34

백엔드 시스템에서 시간(Time)은 생각보다 훨씬 까다로운 주제이다.

특히 다음과 같은 상황에서는 문제가 자주 발생한다.

  • 서버는 한국(KST)인데, 사용자는 해외에 있음
  • DB에 저장된 시간이 환경마다 다르게 보임
  • 로컬에서는 정상인데, 운영 서버에서 시간이 9시간 어긋남
  • 로그, 배치, 만료 시간 계산이 뒤틀림

“모든 내부 시간은 UTC로 저장하고, 보여줄 때만 변환한다.”

 

 

1. 왜 UTC로 통일해야 하는가?

로컬 타임존(KST 등)으로 저장할 때 문제점

  • 서버 위치가 바뀌면 시간 기준이 달라진다
  • 컨테이너(Docker), 클라우드 환경에서 TZ 설정 누락 시 오동작
  • 여러 국가 사용자 서비스 시 시간 계산이 복잡해진다
  • 서머타임(DST) 문제 발생 가능

UTC로 저장하면 얻는 장점

  • 서버 위치와 무관한 일관성
  • DB, 로그, 배치, 캐시 만료 기준 통일
  • 시간 비교/계산이 단순해짐
  • 프론트/클라이언트에서 자유롭게 로컬 시간 변환 가능

그래서 실무에서는 거의 항상 다음 원칙을 따른다.

저장: UTC
표현: 사용자 Timezone

 

2. Java에서 절대 사용하지 말아야 할 타입

 

❌ java.util.Date

  • 타임존 개념이 코드상 드러나지 않음
  • API가 모호하고 deprecated에 가까움

❌ LocalDateTime

  • 타임존 정보가 없음
  • UTC인지 KST인지 구분 불가능
  • DB에 저장 시 가장 많은 사고를 유발
LocalDateTime.now(); // 이게 UTC인지, KST인지 알 수 없다

 

 

3. Instant 사용

Instant는 UTC 기준의 타임스탬프를 표현하는 클래스이다.

Instant now = Instant.now();
  • 항상 UTC 기준
  • Epoch Time(1970-01-01T00:00:00Z) 기준
  • 타임존 개념이 명확함
  • 서버 타임존 설정에 영향받지 않음

 

4. Spring Boot에서 JVM Timezone을 UTC로 고정하기

4-1. 가장 먼저 해야 할 설정

@PostConstruct
public void init() {
    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
}

또는 JVM 옵션으로 설정한다.

-Duser.timezone=UTC

이 설정은 다음을 보장한다.

  • Instant.now() 기준 명확
  • LocalDateTime 변환 시도 시 기준 UTC
  • Jackson 직렬화 일관성 확보

 

5. JPA Entity에서 Instant 사용하기

5-1. Entity 예시

@Entity
public class Order {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false)
    private Instant createdAt;

    @Column(nullable = false)
    private Instant updatedAt;
}

5-2. 자동 생성 시간 처리

@PrePersist
public void onCreate() {
    this.createdAt = Instant.now();
    this.updatedAt = Instant.now();
}

@PreUpdate
public void onUpdate() {
    this.updatedAt = Instant.now();
}

👉 이 시점의 Instant.now()는 항상 UTC이다.

 

6. MySQL / MariaDB에서 시간 컬럼 타입 선택

TIMESTAMP + Instant 조합을 사용한다.

 

6-1. TIMESTAMP vs DATETIME 차이

항목 TIMESTAMP DATETIME
타임존 인식 O (세션 TZ 반영) ❌ 없음
저장 방식 UTC 기준 저장 값 그대로 저장
추천 여부 ✅ 추천 ❌ 비권장

추천 컬럼 정의

created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL

MySQL, MariaDB 모두 동일하게 사용 가능하다.

 

7. DB 타임존 설정 (매우 중요)

7-1. DB 세션 타임존을 UTC로 맞춘다

MySQL / MariaDB

SET time_zone = '+00:00';

7-2. 영구 설정 (my.cnf)

[mysqld]
default-time-zone = '+00:00'

 

 

8. Spring JPA + DB 연결 설정

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/app_db?serverTimezone=UTC

MariaDB도 동일하다.

jdbc:mariadb://localhost:3306/app_db?serverTimezone=UTC

이 설정이 없으면 다음 문제가 발생한다.

  • DB는 UTC인데
  • JDBC 드라이버가 로컬 TZ로 해석
  • 결과적으로 9시간 밀림 현상 발생

 

9. Jackson(JSON 직렬화) 설정

Instant는 기본적으로 ISO-8601 UTC 형식으로 직렬화된다.

"createdAt": "2024-01-01T12:00:00Z"

명시적 설정 (권장)

spring:
  jackson:
    time-zone: UTC

 

 

10. 프론트엔드로 내려줄 때는 어떻게 하나?

백엔드 원칙

  • 항상 UTC Instant로 내려준다
  • 절대 KST로 변환해서 내려주지 않는다

프론트엔드에서 변환

const date = new Date("2024-01-01T12:00:00Z");
date.toLocaleString("ko-KR");

👉 표현 책임은 프론트엔드에 둔다.

 

11. 실무에서 가장 자주 발생하는 실수

❌ LocalDateTime + DATETIME 조합

→ 시간 기준 붕괴

❌ DB는 UTC, JVM은 KST

→ 조회 시 9시간 차이 발생

❌ 로그는 UTC, API 응답은 KST

→ 디버깅 지옥

반응형