Files
project-auth-server/docs/examples/language/time.md
T

4.7 KiB

time 타입 / 포맷 예시

이 문서는 time 타입 / 포맷 기준을 코드 예시로 확인하기 위한 자료입니다.
핵심 기준은 시간 값을 문자열이나 숫자로 들고 다니지 않고, 시점 / 날짜 / 시각 / 기간 / 시간대 의미에 맞는 java.time 타입으로 표현하는 것입니다.

좋은 예시 1: event timestamp는 Instant

public record AuditEvent(
        String action,
        Long userId,
        Instant occurredAt
) {
}

왜 좋은가:

  • timeline 위 한 점을 명확하게 표현한다.
  • 로깅, 저장, 비교에 적합하다.

좋은 예시 2: 사람 기준 날짜 의미는 LocalDate / YearMonth

public record UserProfile(
        LocalDate birthDate,
        YearMonth cardExpiry
) {
}

왜 좋은가:

  • 시간대와 무관한 사람 기준 날짜 의미를 타입으로 드러낸다.
  • 생일과 카드 만료월처럼 서로 다른 날짜 의미를 구분한다.

좋은 예시 3: 현재 시각은 Clock 기반

public class TokenIssuer {

    private final Clock clock;

    public TokenIssuer(Clock clock) {
        this.clock = clock;
    }

    public Instant issueTime() {
        return Instant.now(clock);
    }

    public Instant expiryTime(Duration ttl) {
        return Instant.now(clock).plus(ttl);
    }
}

왜 좋은가:

  • 현재 시각을 고정해 테스트하기 쉽다.
  • static now() 호출이 코드 곳곳에 흩어지지 않는다.

좋은 예시 4: 시간 간격은 Duration

private static final Duration ACCESS_TOKEN_TTL = Duration.ofMinutes(30);

왜 좋은가:

  • 1800 같은 매직 숫자보다 의미가 분명하다.
  • 초, 밀리초, 분 단위 혼동이 줄어든다.

좋은 예시 5: 외부 응답 포맷은 경계에서 처리

String value = DateTimeFormatter.ISO_INSTANT.format(event.occurredAt());

왜 좋은가:

  • 내부 로직은 Instant를 유지한다.
  • 문자열 포맷은 serialization, logging, external API adapter 같은 boundary에서만 수행한다.

좋은 예시 6: 실제 zone 계산이 필요할 때만 ZonedDateTime

ZonedDateTime reservationTime = localReservationTime.atZone(ZoneId.of("Asia/Seoul"));

왜 좋은가:

  • 서울 지역 wall-clock 시간이라는 의미가 필요할 때만 zone을 붙인다.
  • 시간대 규칙이 필요한 계산임을 코드에 드러낸다.

나쁜 예시 1: createdAt을 LocalDateTime으로 저장

private LocalDateTime createdAt;

문제:

  • 절대 시점이 아니라 zone/offset 없는 wall-clock 값이 된다.
  • 시스템 간 교환, 저장, 비교에서 의미가 흔들린다.

개선:

private Instant createdAt;

나쁜 예시 2: business logic에서 기본 시스템 zone 의존

LocalDate today = LocalDate.now();

문제:

  • JVM 기본 time-zone에 암묵적으로 의존한다.
  • 테스트와 운영 환경에 따라 결과가 달라질 수 있다.

개선:

LocalDate today = LocalDate.now(clock);

또는:

LocalDate today = LocalDate.now(zoneId);

나쁜 예시 3: 문자열로 시간 비교

if (request.startTime().compareTo("09:00") >= 0) {
    // open
}

문제:

  • 타입 의미가 사라진다.
  • 포맷 변화에 취약하다.

개선:

if (!request.startTime().isBefore(LocalTime.of(9, 0))) {
    // open
}

나쁜 예시 4: legacy API 사용

Date now = new Date();
Timestamp expiresAt = new Timestamp(System.currentTimeMillis() + 1_800_000);

문제:

  • 새 코드 기준으로 java.time보다 의미가 덜 명확하다.
  • 시간 단위와 시스템 clock 의존이 코드에 흩어진다.

개선:

Instant now = Instant.now(clock);
Instant expiresAt = now.plus(Duration.ofMinutes(30));

나쁜 예시 5: wall-clock 의미인데 Instant 남용

public record StoreHours(
        Instant opensAt,
        Instant closesAt
) {
}

문제:

  • 영업 시작/종료는 보통 지역 wall-clock 의미다.
  • 절대 시점 타입이 도메인 의미를 흐린다.

개선:

public record StoreHours(
        LocalTime opensAt,
        LocalTime closesAt
) {
}

나쁜 예시 6: Instant를 DTO에서 문자열로 직접 조립

public record TokenResponse(
        String expiresAt
) {
    public static TokenResponse from(Instant expiresAt) {
        return new TokenResponse(expiresAt.toString());
    }
}

문제:

  • DTO 조립 코드가 시간 포맷 정책을 직접 가진다.
  • 응답 포맷 변경이 여러 DTO 생성 코드로 퍼질 수 있다.

개선 방향:

  • response serialization 설정이나 전용 formatter 경계에서 포맷한다.
  • 내부 모델과 유스케이스 결과는 Instant 같은 typed value를 유지한다.