222 lines
4.7 KiB
Markdown
222 lines
4.7 KiB
Markdown
# time 타입 / 포맷 예시
|
|
|
|
이 문서는 [time 타입 / 포맷 기준](../../standards/language/time.md)을 코드 예시로 확인하기 위한 자료입니다.
|
|
핵심 기준은 시간 값을 문자열이나 숫자로 들고 다니지 않고, 시점 / 날짜 / 시각 / 기간 / 시간대 의미에 맞는 `java.time` 타입으로 표현하는 것입니다.
|
|
|
|
## 좋은 예시 1: event timestamp는 Instant
|
|
|
|
```java
|
|
public record AuditEvent(
|
|
String action,
|
|
Long userId,
|
|
Instant occurredAt
|
|
) {
|
|
}
|
|
```
|
|
|
|
왜 좋은가:
|
|
|
|
- timeline 위 한 점을 명확하게 표현한다.
|
|
- 로깅, 저장, 비교에 적합하다.
|
|
|
|
## 좋은 예시 2: 사람 기준 날짜 의미는 LocalDate / YearMonth
|
|
|
|
```java
|
|
public record UserProfile(
|
|
LocalDate birthDate,
|
|
YearMonth cardExpiry
|
|
) {
|
|
}
|
|
```
|
|
|
|
왜 좋은가:
|
|
|
|
- 시간대와 무관한 사람 기준 날짜 의미를 타입으로 드러낸다.
|
|
- 생일과 카드 만료월처럼 서로 다른 날짜 의미를 구분한다.
|
|
|
|
## 좋은 예시 3: 현재 시각은 Clock 기반
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
private static final Duration ACCESS_TOKEN_TTL = Duration.ofMinutes(30);
|
|
```
|
|
|
|
왜 좋은가:
|
|
|
|
- `1800` 같은 매직 숫자보다 의미가 분명하다.
|
|
- 초, 밀리초, 분 단위 혼동이 줄어든다.
|
|
|
|
## 좋은 예시 5: 외부 응답 포맷은 경계에서 처리
|
|
|
|
```java
|
|
String value = DateTimeFormatter.ISO_INSTANT.format(event.occurredAt());
|
|
```
|
|
|
|
왜 좋은가:
|
|
|
|
- 내부 로직은 `Instant`를 유지한다.
|
|
- 문자열 포맷은 serialization, logging, external API adapter 같은 boundary에서만 수행한다.
|
|
|
|
## 좋은 예시 6: 실제 zone 계산이 필요할 때만 ZonedDateTime
|
|
|
|
```java
|
|
ZonedDateTime reservationTime = localReservationTime.atZone(ZoneId.of("Asia/Seoul"));
|
|
```
|
|
|
|
왜 좋은가:
|
|
|
|
- 서울 지역 wall-clock 시간이라는 의미가 필요할 때만 zone을 붙인다.
|
|
- 시간대 규칙이 필요한 계산임을 코드에 드러낸다.
|
|
|
|
## 나쁜 예시 1: createdAt을 LocalDateTime으로 저장
|
|
|
|
```java
|
|
private LocalDateTime createdAt;
|
|
```
|
|
|
|
문제:
|
|
|
|
- 절대 시점이 아니라 zone/offset 없는 wall-clock 값이 된다.
|
|
- 시스템 간 교환, 저장, 비교에서 의미가 흔들린다.
|
|
|
|
개선:
|
|
|
|
```java
|
|
private Instant createdAt;
|
|
```
|
|
|
|
## 나쁜 예시 2: business logic에서 기본 시스템 zone 의존
|
|
|
|
```java
|
|
LocalDate today = LocalDate.now();
|
|
```
|
|
|
|
문제:
|
|
|
|
- JVM 기본 time-zone에 암묵적으로 의존한다.
|
|
- 테스트와 운영 환경에 따라 결과가 달라질 수 있다.
|
|
|
|
개선:
|
|
|
|
```java
|
|
LocalDate today = LocalDate.now(clock);
|
|
```
|
|
|
|
또는:
|
|
|
|
```java
|
|
LocalDate today = LocalDate.now(zoneId);
|
|
```
|
|
|
|
## 나쁜 예시 3: 문자열로 시간 비교
|
|
|
|
```java
|
|
if (request.startTime().compareTo("09:00") >= 0) {
|
|
// open
|
|
}
|
|
```
|
|
|
|
문제:
|
|
|
|
- 타입 의미가 사라진다.
|
|
- 포맷 변화에 취약하다.
|
|
|
|
개선:
|
|
|
|
```java
|
|
if (!request.startTime().isBefore(LocalTime.of(9, 0))) {
|
|
// open
|
|
}
|
|
```
|
|
|
|
## 나쁜 예시 4: legacy API 사용
|
|
|
|
```java
|
|
Date now = new Date();
|
|
Timestamp expiresAt = new Timestamp(System.currentTimeMillis() + 1_800_000);
|
|
```
|
|
|
|
문제:
|
|
|
|
- 새 코드 기준으로 `java.time`보다 의미가 덜 명확하다.
|
|
- 시간 단위와 시스템 clock 의존이 코드에 흩어진다.
|
|
|
|
개선:
|
|
|
|
```java
|
|
Instant now = Instant.now(clock);
|
|
Instant expiresAt = now.plus(Duration.ofMinutes(30));
|
|
```
|
|
|
|
## 나쁜 예시 5: wall-clock 의미인데 Instant 남용
|
|
|
|
```java
|
|
public record StoreHours(
|
|
Instant opensAt,
|
|
Instant closesAt
|
|
) {
|
|
}
|
|
```
|
|
|
|
문제:
|
|
|
|
- 영업 시작/종료는 보통 지역 wall-clock 의미다.
|
|
- 절대 시점 타입이 도메인 의미를 흐린다.
|
|
|
|
개선:
|
|
|
|
```java
|
|
public record StoreHours(
|
|
LocalTime opensAt,
|
|
LocalTime closesAt
|
|
) {
|
|
}
|
|
```
|
|
|
|
## 나쁜 예시 6: Instant를 DTO에서 문자열로 직접 조립
|
|
|
|
```java
|
|
public record TokenResponse(
|
|
String expiresAt
|
|
) {
|
|
public static TokenResponse from(Instant expiresAt) {
|
|
return new TokenResponse(expiresAt.toString());
|
|
}
|
|
}
|
|
```
|
|
|
|
문제:
|
|
|
|
- DTO 조립 코드가 시간 포맷 정책을 직접 가진다.
|
|
- 응답 포맷 변경이 여러 DTO 생성 코드로 퍼질 수 있다.
|
|
|
|
개선 방향:
|
|
|
|
- response serialization 설정이나 전용 formatter 경계에서 포맷한다.
|
|
- 내부 모델과 유스케이스 결과는 `Instant` 같은 typed value를 유지한다.
|