시간데이터 다루기
개요
시간데이터 다룰 때마다 지피티한테 물어봤는데 시간이 너무 소요되어 이번 기회에 정리해본다.
옛날 방식
java.util.Date와 java.util.Calendar
옛날에는 이 클래스들을 이용하여 시간데이터를 다뤘다.
예를 들어 다음과 같다.
Date now = new Date();
System.out.println(now); // Fri Oct 23 20:00:00 KST 2025
이 방식은 자바의 시작과 함께 등장했고 사용되었다. 자바 버전으로 따지면 1.0 ~ 1.1이 된다.
그러나 여러가지 문제점이 있었는데
- 불변(immutable)이 아니다.
- 월(month)은 0부터 시작해서 헷갈린다.
- 스레드 세이프하지 않다.
- 포맷팅이 복잡하다 (SimpleDateFormat).
그러다 시간이 지난 자바 8버전부터 java.time 패키지가 등장했다. 비교적 근대화된 방식이라고 말할 수 있다.
구체적으로는 다음과 같은 클래스들이 속해있다.
LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant
LocalDateTime now = LocalDateTime.now();
LocalDateTime tomorrow = now.plusDays(1);
Duration duration = Duration.between(now, tomorrow);
System.out.println(duration.toHours()); // 24
- LocalDate: 날짜만 나옴
- LocalTime: 시간만 나옴
- LocalDateTime: 날짜와 시간이 나옴.
- ZonedDateTime: 날짜, 시간, 타임존이 나옴
- Instant: 기준일인 1970년 부터 경과한 초/밀리초
또 코드들을 보면 자주 나오는 Timestamp가 있다.
Timestamp는 java 1.1 버전에 나온 java.sql.Timestamp의 패키지 구조를 가지고 SQL 전용 날짜/시간 데이터다. 초기의 자바는 java.util.date만 있었다. 그러나 JDBC를 사용하기 시작하면서 문제가 생겼다.
DB에는 DATE, TIME, TIMESTAP와 같은 타입들이 존재하는데 자바에는 java.util.Date만 있었기 때문이다.
따라서 자바는 DB의 날짜/시간 데이터와 매핑할 수 있는 타입을 java.sql이라는 패키지 안에 모아놓은 것이다.
타임스탬프는 과거엔 많이 쓰였지만 기본적으로 java.util.Date 기반으로 만들어져있다. 따라서 날짜와 시간만 표현이 가능했다.
또한 Date의 하위 클래스라 시간대의 개념이 없다. 이로인해 서버 지역이 바뀌면 데이터가 어긋나는 일이 생긴다.
반면에 LocalDateTime의 경우는 타임존을 명시적으로 처리할 수 있어 이런 문제가 생기지 않는다.
Date는 위에서 봤듯이 출력 형태가 보기 불편하게 되어있다. 날짜 조작이 불가능한 문제도 있는데 예를 들어 1일 뒤를 표현한다거나 할 때 calendar를 같이 사용해야했다.
실무에서 멀티스레드 환경일 경우 동일한 객체를 공유하게 될 수 있다. Timestampsk Date는 내부 값을 변경할 수 있다. 이렇게 되니 의도치않은 변경이 일어날 수 있다는 문제가 생긴다.
타임스탬프는 java.sql 패키지에 속하기 때문에 DB전용 API다 이로 인해 비즈니스 로직에서 DB 의존성이 생긴다.
time 패키지는 다음과 같은 장점을 갖는다.
- 불변
- 명확한 시간 단위
- 타임존 지원
- 편리한 연산
- 포매팅 간소화
예를 들어 이번에 작성한 시간대별 요금 차등 적용을 살펴보자.
private BigDecimal getDiscountRate(LocalTime time) {
if (!time.isBefore(LocalTime.MIDNIGHT) && time.isBefore(LocalTime.of(9, 0))) {
return NIGHT_DISCOUNT_PRICE_PER_HALF_HOUR; // 00:00 ~ 08:59:59
}
if (!time.isBefore(LocalTime.of(18, 0)) && time.isBefore(LocalTime.MAX)) {
return EVENING_DISCOUNT_PRICE_PER_HALF_HOUR; // 18:00 ~ 23:59:59
}
return this.roomInfo.getHalfHrPrice();
}
이런 식으로 여러 메소드를 제공한다.
예를 들어 자정부터 오전 9시 까지는 50% 할인된 금액으로 제공한다고 했을 때
isBefore를 사용하여 대소 연산자를 사용하듯 할 수 있다.
그리고 LocalTime.MAX는 하루의 끝을 의미한다. 이런 식으로 연산이 간편하다.
| 메서드 | 설명 | 예시 |
|---|---|---|
plusDays(long days) |
n일 뒤 | now.plusDays(1) |
minusDays(long days) |
n일 전 | now.minusDays(7) |
plusHours(long hours) |
n시간 뒤 | now.plusHours(3) |
plusMinutes(long minutes) |
n분 뒤 | now.plusMinutes(30) |
plusWeeks(long weeks) |
n주 뒤 | now.plusWeeks(2) |
plusMonths(long months) |
n개월 뒤 | now.plusMonths(1) |
plusYears(long years) |
n년 뒤 | now.plusYears(1) |
| 메서드 | 설명 | 예시 |
|---|---|---|
withYear(int year) |
연도 변경 | now.withYear(2030) |
withMonth(int month) |
월 변경 | now.withMonth(12) |
withDayOfMonth(int day) |
일 변경 | now.withDayOfMonth(15) |
withHour(int hour) |
시 변경 | now.withHour(9) |
withMinute(int minute) |
분 변경 | now.withMinute(0) |
with(TemporalAdjuster adjuster) |
날짜 조정기 사용 | now.with(TemporalAdjusters.lastDayOfMonth()) |
그리고 많이 쓰이는 두 날짜 사이의 차이는 between이나 ChronoUnit을 사용한다. 혹은 Duration도 시간 차를 구할 때 자주 쓰인다.
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 0, 0);
LocalDateTime end = LocalDateTime.now();
long days = ChronoUnit.DAYS.between(start, end);
long hours = ChronoUnit.HOURS.between(start, end);
System.out.println("총 " + days + "일, " + hours + "시간 지남");
Duration duration = Duration.between(start, end);
duration.toMinutes(); // 분 단위 반환
| 메서드 | 설명 | 예시 |
|---|---|---|
isBefore(LocalDateTime other) |
이전 시각인가 | now.isBefore(meetingTime) |
isAfter(LocalDateTime other) |
이후 시각인가 | now.isAfter(meetingTime) |
isEqual(LocalDateTime other) |
같은 시각인가 | now.isEqual(otherTime) |