잘 설계된 API는 사용하기 쉬워야 하고 오용하기에는 어려워야 한다.
자바는 jdk 1.0 부터 날짜와 시간에 관련된 기능을 제공했다.
그러나 초기 날짜와 시간 API에는 문제가 많았고
jdk 1.8에서 java.time 패키지가 추가되면서 이전 문제점을 해결하고 더 직관적으로 발전시켰다.
초기의 시간 관련 API에는 어떤 문제가 있었는지, 그리고 어떻게 그 문제를 보완했고 해결하였는지 알아보자
java.util.Date 는 jdk 1.0 버전에 도입되었고
java.util.Calendar는 Date 클래스의 문제점을 보완하기 위해 jdk 1.1 버전에서 도입되었다.
그러나 두 클래스 모두 여러 가지 문제점을 가지고 있어 잘 사용되지 않는 클래스가 되었다.
두 클래스를 따로 보지 않고 한 번에 묶어서 어떠한 문제점을 가지고 있는지 간략하게 알아보자
날짜, 시간, 금액 같은 객체는 일반적으로 VO로써 불변으로 다루어져야 한다.
그러나 Date API 와 Calendar API 를 보면 setter 가 잘 정의되어 있는 걸 볼 수 있다.
변경 가능한 날짜와 시간은 다중 스레드 환경에서 안정성이 보장되지 않아 동시성 문제가 발생할 수 있다.
Calendar 클래스의 add 메소드는 이렇게 선언되어있다.

계산하고자 하는 기준 필드를 첫 번째 인자에 넣고 두 번째 인자에 들어가는 amount로 계산되는데, field의 type이 int 여서 잘못된 값이 들어갈 가능성이 있다.
또, 월 이 상수로 정의되어 있는데 1월인 January 가 0으로 선언되어 있다.

이렇게 오용의 가능성이 높은 API는 당연히 개발자가 사용 시 오류가 발생할 확률이 높다.
이 부분은 너무나 잘 포스팅된 글이 있어서 참조를 하려 한다.
Naver D2: Java의 날짜와 시간 API
이 두 개의 클래스는 너무 문제가 많아서 아예 사용하지 않는다.
오류 없는 애플리케이션을 위해서는 사용하면 안 된다!!
위 클래스들의 문제를 해결하기 위해 jdk 1.8 에서 java.time 패키지를 도입했다.
기존 Date와 Calendar 클래스에 나타났던 치명적인 문제인 가변 객체라는 것을 보완하여 java.time 패키지에 속한 객체들은 불변 객체로 설계되었다.
불변 객체는 값이 변하지 않기 때문에 다중 쓰레드 환경에서 안정성이 보장된다.
밑에서 보겠지만 java.time 패키지에 있는 API들은 직관적이게 설계되어 개발자가 쉽게 사용할 수 있도록 만들었다.
이런 여러 가지 문제를 보완하고 해결한 java.time 패키지에 있는 중요한 클래스 몇 가지만 간단하게 알아보자.
이 글에서는 클래스들의 메서드를 상세하게 다루지 않고 클래스가 어떤 특징을 가지고 있고 클래스 간의 차이가 무엇인지만 간단하게 다룬다.
| 패키지 | 설명 |
|---|---|
| java.time | 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공 |
| java.time.chrono | ISO 표준 외의 달력 시스템을 위한 API 제공 |
| java.time.format | 날짜와 시간을 파싱하고 출력하는 클래스들을 제공 |
| java.time.temporal | 필드와 유닛으로 날짜와 시간에 접근하게 하는 기능 제공 |
| java.time.zone | 시간대(time-zone)와 관련된 기능 제공 |
| 클래스 | 설명 |
|---|---|
| LocalDate | 시차 정보나 위치 정보가 없는 단순한 날짜를 표현하는 클래스 |
| LocalTime | 시차 정보나 위치 정보가 없는 단순한 시간을 표현하는 클래스 |
| LocalDateTime | LocalDate + LocalTime : 날짜와 시간 둘 다 표현하는 클래스 |
| OffsetTime | 시차 정보가 포함된 시간을 나타내는 클래스 |
| OffsetDateTime | 날짜 정보가 추가된 OffsetTime |
| ZonedDateTime | 시차 정보와 위치 정보가 모두 포함된 날짜와 시간을 표현하는 클래스 |
| Period | 연, 월, 일 같은 날짜 단위의 합이나 차이 같이 날짜의 양을 표현하는 클래스 |
| Duration | Period와는 다르게 시간의 양을 나타내는 클래스 |
| Instant | Epoch를 기점으로 특정 시점의 타임 스탬프를 찍는 기능을 제공하는 클래스 |
이 3가지 클래스는 시차 정보나 위치 정보 없이 단순히 현재 시스템의 날짜와 시간 정보만을 저장한다.
LocalDate nowDate = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDateTime = LocalDateTime.now();
System.out.println(nowDate); // 2024-08-18
System.out.println(nowTime); // 13:45.30.123456789
System.out.println(nowDateTime); // 2024-08-18T13:45.30.123456789
날짜는 연-월-일 의 형태로, 시간은 시:분.초.나노초 의 형태로 출력되는 걸 볼 수 있다.
물론 시간을 직접 지정해서 객체를 생성할 수 있다.
LocalDateTime ofDateTime = LocalDateTime.of(2024, 12, 25, 8, 30);
System.out.println(ofDateTime); // 2024-12-25T08:30
Local 객체는 단순히 현재 시스템의 날짜와 시간만 나타내는 객체이다.
시간대와 위치에 대한 정보가 없어서 다른 시간대에 적용하기에는 정보가 부족하다.
이해하기 쉽게 예를 들어보자.
하나는 한국의 컴퓨터에서, 하나는 미국의 컴퓨터에서 동시에 LocalDateTime.now()를 실행했다고 가정하자.
그럼 한국 표준시 기준으로 2024-08-18T12:00-00이 저장되고
미국 표준시 기준으로는 2024-08-18T00:00-00이 저장될 것이다.
두 시간은 실제로는 같은 순간을 가리키지만, 로컬 시간만을 표현하는 LocalDateTime으로는 이를 구분하지 못한다.
이럴 때 시간대 정보나 UTC Offset이 포함된 클래스를 사용해야 한다.
LocalDateTime local = LocalDateTime.now();
OffsetDateTime offset = OffsetDateTime.now();
ZonedDateTime zoned = ZonedDateTime.now();
System.out.println(local); // 2024-08-18T15:54:55.625030700
System.out.println(offset); // 2024-08-18T15:54:55.625030700+09:00
System.out.println(zoned); // 2024-08-18T15:54:55.625030700+09:00[Asia/Seoul]
코드를 보면 차이가 딱 보이는 것처럼
OffsetDateTime은 UTC 오프셋이 포함되어 표현된다.
ZonedDateTime은 특정 시간대(time-zone) 정보를 포함하여 표현된다.
위의 예시처럼 다른 시간대를 표현해야 할 때 유용하게 사용할 수 있다.
LocalDateTime에 offset과 zoneId를 명시하여 OffsetDateTime과 ZonedDateTime을 생성할 수도 있음
Period는 날짜, Duration은 시간 기반의 간격을 표현하기 위한 클래스이다.
LocalDate date1 = LocalDate.of(2024, 8, 15);
LocalDate date2 = LocalDate.of(2025, 3, 18);
Period between = Period.between(date1, date2);
System.out.println(between); // P1Y7M3D | + 1년 7개월 3일 차이를 의미
LocalTime time1 = LocalTime.of(22, 10);
LocalTime time2 = LocalTime.of(6, 45);
Duration between = Duration.between(time1, time2);
System.out.println(between); // PT-15H-25M | - 15시간 25분 차이를 의미
Instant 클래스는 특정 시간의 타임스탬프로 사용된다.
이 클래스는 시간을 나타내기보다 특정 두 시점 간의 우선순위를 따질 때 유용하게 사용할 수 있다.
Instant instant1 = Instant.now();
Thread.sleep(3000);
Instant instant2 =Instant.now();
System.out.println(instant1.getEpochSecond()); // 1723965923
System.out.println(instant2.getEpochSecond()); // 1723965926
Instant 클래스는 Epoch 시간인 1970년 1월 1일 00:00-00 UTC 를 기준으로 경과된 초 단위의 시간을 나타낼 수 있게 한다.
그렇기 때문에 Instant 클래스는 특정 시간보다는 순차적인 이벤트의 흐름을 확인할 수 있게 로깅 같은 기능으로 사용할 수 있다.
java.time.temporal 패키지에 속해있는 클래스이다.
ChronoUnit 클래스는 시간 단위를 나타내는 유닛들이 선언되어있는 Enum 타입이다.
특정 단위에 대해서 차이를 계산하거나, 특정 단위만큼 더하거나 뺄 때 유용하게 사용할 수 있다.
LocalDate date1 = LocalDate.of(2023, 8, 17);
LocalDate date2 = LocalDate.of(2024, 8, 17);
long daysBetween = ChronoUnit.DAYS.between(date1, date2);
System.out.println(daysBetween); // 366 | 366일의 차이가 남을 의미
LocalDate date = LocalDate.of(2023, 8, 17);
LocalDate dateAfterSevenWeeks = date.plus(7, ChronoUnit.WEEKS);
System.out.println(dateAfterSevenWeeks); // 2023-10-05
java.time 패키지에 이 외의에도 여러 가지 기능을 지원하는 클래스가 많지만 이번 글에서는 중요한 클래스들과 그 클래스들의 특징 정도만 알아보았다.
더 알아보고자 한다면 java.time 패키지를 읽어보는 걸 추천!