Skip to content

Refactor/66 utc migration#69

Merged
zzmnxn merged 8 commits intodevfrom
refactor/66-UTC_migration
Jan 24, 2026
Merged

Refactor/66 utc migration#69
zzmnxn merged 8 commits intodevfrom
refactor/66-UTC_migration

Conversation

@zzmnxn
Copy link
Contributor

@zzmnxn zzmnxn commented Jan 16, 2026

🔗 Related Issue

📝 Description

UTC 마이그레이션: Back-end는 절대 시간(UTC), Front-end는 상대 시간(Local) 원칙 도입

기획 배경 및 비즈니스 문제

  • 글로벌 서비스 확장 대비: 현재 프로젝트가 한국 시간(KST)에 종속되어 있어, 글로벌 서비스 확장 시 데이터 정합성 문제가 발생할 수 있습니다.
  • 데이터 신뢰성 향상: 클라이언트가 임의로 생성 시간을 조작하는 것을 방지하고, 서버 호스팅 지역(IDC)의 시스템 시간 설정과 무관하게 일관된 서비스 운영이 필요합니다.
  • API 문서화 개선: 프론트엔드 개발자가 UTC 시간을 로컬 타임존으로 변환해야 한다는 점을 명확히 인지할 수 있도록 Swagger 문서에 가이드를 추가합니다.

🛠 Changes

DTO & 도메인 타입 변경 (테스트 코드에도 반영)

LocalDateTime createdAt → OffsetDateTime createdAt으로 변경하여 UTC 정보 손실 방지

API 문서화 개선

Swagger Schema 업데이트: 모든 OffsetDateTime 필드에 다음 정보 추가
UTC 기준 명시
ISO-8601 형식 명시
프론트엔드 변환 가이드 포함
예시에 "Z" 포함 (예: "2024-05-20T10:00:00.000Z")

JSON 직렬화 설정

@jsonformat 어노테이션 추가: ISO-8601 형식(yyyy-MM-dd'T'HH:mm:ss.SSSXXX)으로 직렬화하여 UTC 정보 포함

✅ Test Checklist

  • 컴파일 및 빌드
  • gradlew compileJava 성공 확인
  • Linter 에러 없음 확인
  • 기존 테스트 검증
  • DiaryServiceTest.다이어리_생성_UTC_자동생성_테스트() 확인 - 이미 OffsetDateTime 검증 로직 포함
  • 관련 테스트 코드에서 OffsetDateTime 사용 확인 (수정 불필요)
  • API 응답 형식 검증
  • Swagger 문서에서 UTC 표준 명시 및 프론트엔드 변환 가이드 확인
  • 응답 예시에 "Z" 포함 확인 (ISO-8601 형식)
  • 데이터 정합성
  • Diary, Board, VisitedCountry 엔티티의 @PrePersist에서 OffsetDateTime.now(ZoneOffset.UTC) 사용 확인
  • application.yml에서 serverTimezone=UTC 설정 확인

@zzmnxn
Copy link
Contributor Author

zzmnxn commented Jan 16, 2026

UTC 작동 흐름

1. 백엔드: 요청 수신 시점 (UTC 생성)

프론트엔드 → POST /api/diaries
           ↓
DiaryService.createDiary()
// DiaryService.java:86
OffsetDateTime dateTime = OffsetDateTime.now(ZoneOffset.UTC);
// 예: 2024-12-20T10:30:00.000Z (UTC 기준)
  • 클라이언트가 시간을 보내지 않음
  • 서버가 요청 수신 시점을 UTC로 생성
  • 서버 시스템 시간대와 무관하게 UTC로 고정

2. 엔티티: 자동 타임스탬프 생성 (UTC)

// Diary.java:63-68
@PrePersist
protected void onCreate() {
    if (this.createdAt == null) {
        this.createdAt = OffsetDateTime.now(ZoneOffset.UTC);
        // 예: 2024-12-20T10:30:00.000Z
    }
}
  • @PrePersist: DB 저장 전 실행
  • OffsetDateTime.now(ZoneOffset.UTC): UTC 기준 생성
  • createdAt이 null이면 자동 설정

3. 데이터베이스: UTC로 저장

application.yml:6
serverTimezone=UTC
// MySQL에 저장되는 값
createdAt: 2024-12-20 10:30:00 (UTC 시간대로 저장)
dateTime: 2024-12-20 10:30:00 (UTC 시간대로 저장)
  • JDBC URL의 serverTimezone=UTC 설정으로 MySQL에 UTC로 저장
  • 서버/DB 인스턴스의 로컬 시간대와 무관하게 UTC 기준으로 저장

4. API 응답: UTC 정보 포함하여 반환

// DiaryResponseDto.java:34-39
@Schema(description = "일기 작성 날짜 및 시간 (UTC 기준, ISO-8601 형식). 
                      프론트엔드에서는 사용자의 로컬 타임존으로 변환하여 표시해야 합니다.", 
        example = "2024-05-20T10:00:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
private final OffsetDateTime dateTime;
// API 응답 (JSON)
{
  "dateTime": "2024-12-20T10:30:00.000Z",  // ← Z = UTC 표시
  "createdAt": "2024-12-20T10:30:00.000Z"  // ← Z = UTC 표시
}
  • @JsonFormat: ISO-8601 형식으로 직렬화
  • "Z": UTC 표시 (예: +00:00)
  • Swagger 문서에 UTC 명시 및 변환 가이드 포함

5. 프론트엔드: 로컬 시간으로 변환 (클라이언트 측)

// 프론트엔드 예시 (JavaScript)
const utcTime = "2024-12-20T10:30:00.000Z";  // 서버에서 받은 UTC 시간

// 브라우저/앱의 로컬 타임존으로 변환
const localTime = new Date(utcTime);
// 한국(KST, UTC+9): 2024-12-20 19:30:00
// 미국 뉴욕(EST, UTC-5): 2024-12-20 05:30:00
// 일본(JST, UTC+9): 2024-12-20 19:30:00

// 표시
console.log(localTime.toLocaleString());
  • 프론트엔드는 변환만 담당
  • 브라우저/앱의 타임존 설정에 따라 자동 변환
  • 서버 시간은 항상 UTC로 고정

전체 흐름 요약

[사용자 액션]
  └─> 일기 작성 버튼 클릭 (한국 시간 19:30)
       ↓
[프론트엔드]
  └─> POST /api/diaries (dateTime 필드 없음)
       ↓
[백엔드 - DiaryService]
  └─> OffsetDateTime.now(ZoneOffset.UTC)
       → 생성 시간: 2024-12-20T10:30:00.000Z (UTC)
       ↓
[엔티티 - Diary @PrePersist]
  └─> createdAt = OffsetDateTime.now(ZoneOffset.UTC)
       → 2024-12-20T10:30:00.000Z
       ↓
[데이터베이스]
  └─> MySQL (serverTimezone=UTC)
       → 저장: 2024-12-20 10:30:00 (UTC 기준)
       ↓
[API 응답]
  └─> JSON: { "dateTime": "2024-12-20T10:30:00.000Z" }
       ↓
[프론트엔드]
  └─> new Date("2024-12-20T10:30:00.000Z")
       → 한국 사용자에게: "2024-12-20 19:30" (KST, UTC+9)
       → 미국 사용자에게: "2024-12-20 05:30" (EST, UTC-5)

핵심 포인트

  1. 백엔드는 항상 UTC를 사용

    • 시간 생성: OffsetDateTime.now(ZoneOffset.UTC)
    • DB 저장: serverTimezone=UTC
  2. 프론트엔드는 로컬 타임존으로 변환

    • 서버 응답의 UTC 시간을 사용자 기기의 로컬 시간으로 변환하여 표시
  3. 일관된 기준으로 저장

    • 서버 위치와 무관하게 UTC로 통일
    • 표시는 각 사용자의 로컬 시간으로 변환
  4. UTC 정보 보존

    • OffsetDateTime 사용으로 UTC 정보 유지
    • ISO-8601 형식 ("Z" 포함)으로 직렬화

이 방식으로 시간대 이슈 없이 일관된 시간을 저장하고 표시할 수 있습니다.

@zzmnxn zzmnxn linked an issue Jan 16, 2026 that may be closed by this pull request
5 tasks
@zzmnxn zzmnxn requested a review from chwwwon January 16, 2026 05:42

@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
Copy link
Contributor

@chwwwon chwwwon Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OffsetDateTime을 사용하는 도메인에 공통적으로 적용되는 리뷰입니다.
MySQL의 DATETIME / TIMESTAMP는 offset 정보를 저장하지 않아서 버전에 따라

  • offset 손실
  • 자동 변환 오류
  • 런타임 매핑 에러 가능
    등의 문제가 발생할 수 있다고 합니다.
    코파일럿이 아래와 같이 제안해주었는데 OffsetDateTimeAttributeConverter 파일 추가를 고려해보는 것도 좋을 것 같아요!

권장 수정 (가장 안전한 방법)
👉 JPA AttributeConverter 추가 (DB 변경 없이 해결)
새 파일 추가
src/main/java/zim/tave/memory/config/OffsetDateTimeAttributeConverter.java

@Converter(autoApply = true)
public class OffsetDateTimeAttributeConverter
        implements AttributeConverter<OffsetDateTime, Timestamp> {

    @Override
    public Timestamp convertToDatabaseColumn(OffsetDateTime odt) {
        return odt == null ? null : Timestamp.from(odt.toInstant());
    }

    @Override
    public OffsetDateTime convertToEntityAttribute(Timestamp ts) {
        return ts == null ? null
            : OffsetDateTime.ofInstant(ts.toInstant(), ZoneOffset.UTC);
    }
}

@Schema(description = "보드 생성 시각", example = "2025-12-01T14:20:00")
private String createdAt;
@Schema(description = "보드 생성 시각 (UTC 기준, ISO-8601 형식). 프론트엔드에서는 사용자의 로컬 타임존으로 변환하여 표시해야 합니다.", example = "2025-12-01T14:20:00.000Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 각 DTO 필드마다 @jsonformat이 개별 지정되어 있는데, 이 경우 "Z" / "+00:00" 혼재 가능성이 있어 전역 Jackson 설정으로 UTC + ISO-8601 고정하는 방법은 어떨까요?
application.yml에 아래와 같이 추가하는 방법도 있고, config를 추가하는 방법도 있으니 고려해보면 좋을 것 같아용

spring:
  jackson:
    time-zone: UTC
    serialization:
      write-dates-as-timestamps: false

Copy link
Contributor

@chwwwon chwwwon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리뷰 확인해주세요!

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@zzmnxn zzmnxn merged commit 4446342 into dev Jan 24, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 글로벌 타임 UTC 표준화 (Date/Time UTC Migration)

2 participants