백엔드 시스템에서 시간(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
→ 디버깅 지옥
반응형