CodeGym /행동 /JAVA 25 SELF /java.time API 개요, 기존 API와의 차이점

java.time API 개요, 기존 API와의 차이점

JAVA 25 SELF
레벨 13 , 레슨 1
사용 가능

1. 역사적 맥락: Java 8 이전의 날짜/시간 처리 방식

아주 예전(Java 8 이전)에는 java.util.Date, java.util.Calendar를 사용했고, 포맷팅에는 java.text.SimpleDateFormat을 썼습니다. 겉보기에는 간단해 보였지만, 전 세계 개발자들이 한숨을 쉬며 이를 악물고 다음과 같은 코드를 쓰곤 했습니다:

import java.util.Date;

Date now = new Date();
System.out.println(now); // "Wed Jun 05 14:15:22 MSK 2025" 같은 낯선 문자열이 출력됨

겉으론 쉬워 보이지만 실제로는 문제가 많았습니다. 오래된 API의 “즐거움” 중 몇 가지만 보면:

  • Date는 변경 가능한(mutable) 객체입니다. 실수로 값을 바꿔 버리기 쉽고, 이는 종종 버그로 이어졌습니다.
  • DateCalendar에서 월은 0부터 시작했습니다(1월은 0, 12월은 11), 반면 일(day)은 1부터 시작했습니다.
  • SimpleDateFormat은 스레드 안전하지 않았습니다. 두 스레드가 동시에 포맷팅하면 예기치 않은 결과가 나올 수 있었습니다.
  • 수많은 메서드가 @Deprecated(deprecated)로 표시되어 있었고, IDE가 계속 노란색 경고를 띄웠습니다.
  • 시간대 처리는 진짜 골칫거리였습니다. 로컬 시간과 UTC를 헷갈리기 쉽고, 서머타임 전환은 말 꺼내기도 싫은 수준이었습니다.

고통의 예

import java.util.Date;

Date date = new Date(2025, 5, 1); // 2025년, 5월(6월?), 1일
System.out.println(date); // 기대와 다름!

2. java.time의 등장: 새로운 접근

2014년 즈음에는 분명해졌습니다. 오래된 API는 불편할 뿐 아니라 위험하다는 사실이요. 그래서 JSR‑310 사양을 구현한 새로운 패키지 java.time이 Java에 도입되었습니다. 이 API는 인기 있는 Joda-Time 라이브러리에서 영감을 받았고 곧바로 사실상의 표준이 되었습니다.

주요 패키지와 클래스

  • java.time — 날짜/시간을 위한 새 클래스들이 있는 핵심 패키지.
  • java.time.format — 날짜와 시간을 포맷팅/파싱.
  • java.time.temporal — 더 고급 시간 연산을 위한 기능.
  • java.time.zone — 시간대 관련 기능.

새 API의 핵심 클래스들:

클래스 역할 사용 예
LocalDate
날짜만(연, 월, 일) 생일(시간 없음)
LocalTime
시간만(시, 분, 초) 약속 시간(날짜 없음)
LocalDateTime
날짜와 시간, 시간대 없음 로컬 이벤트
ZonedDateTime
시간대가 포함된 날짜/시간 민스크 현지 시간으로 회의
Instant
절대 시점(UTC) 로그 이벤트 타임스탬프
Duration
시간 간격(시, 분, 초) 통화 지속 시간
Period
기간(년, 월, 일) 근속, 나이

예시: 새로운 방식으로 날짜 생성

import java.time.LocalDate;

LocalDate today = LocalDate.now();
System.out.println(today); // 예: "2025-06-05"

3. 새 API의 장점

불변성(immutable)

java.time의 모든 클래스는 불변입니다. 즉, LocalDate 객체를 만들면 그 자체를 변경할 수 없습니다. 어떤 연산(예: 하루 더하기)도 항상 새로운 객체를 반환합니다.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);

System.out.println(today);    // 2025-06-05
System.out.println(tomorrow); // 2025-06-06

시간대의 명시적 처리

오래된 API에서는 날짜가 어느 시간대에 있는지 잊기 쉬웠습니다. java.time에서는 모든 것이 명시적입니다. 시간대가 필요하면 ZonedDateTime을, 필요 없으면 LocalDateTime을 사용하세요.

import java.time.ZonedDateTime;
import java.time.ZoneId;

ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
System.out.println(MinskTime); // 2025-06-05T14:23:45+03:00[Europe/Minsk]

연산과 비교를 위한 편리한 메서드들

LocalDate today = LocalDate.now();
LocalDate nextMonth = today.plusMonths(1);
boolean isAfter = LocalDate.now().plusDays(1).isAfter(today); // true

포맷팅과 파싱

포맷팅과 파싱은 DateTimeFormatter를 통해 수행합니다(자세한 내용은 다음 강의에서 다룹니다):

import java.time.format.DateTimeFormatter;

LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
String formatted = today.format(formatter); // "05.08.2025"

4. 호환성: 레거시 코드와 함께 일하는 법

현실 세계에서는 매우 자주 DateCalendar를 사용하는 라이브러리나 오래된 시스템과 함께 작업해야 합니다. 다행히 새 API는 레거시에 친화적이어서, 구형 타입과 신형 타입 간 변환이 가능합니다.

DateInstant 변환

import java.util.Date;
import java.time.Instant;

// Date → Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();

// Instant → Date
Date dateBack = Date.from(instant);

CalendarZonedDateTime 변환

import java.util.Calendar;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Date;

// Calendar → ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(
        calendar.toInstant(),
        calendar.getTimeZone().toZoneId()
);

// ZonedDateTime → Calendar
Calendar calBack = Calendar.getInstance();
calBack.setTime(Date.from(zdt.toInstant()));

표: 구형/신형 클래스 매핑

구형 클래스 신형 클래스 비고
java.util.Date
Instant
절대 시간
java.util.Calendar
ZonedDateTime
시간대 포함 날짜/시간
java.text.SimpleDateFormat
DateTimeFormatter
날짜 포맷팅/파싱

5. 실습: java.time 첫걸음

사용자의 생년월일이 있다고 가정해 봅시다. 이 날짜를 저장하고 출력해 봅시다:

import java.time.LocalDate;

public class UserProfile {
    private String name;
    private LocalDate birthDate;

    public UserProfile(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }

    public void printProfile() {
        System.out.println("이름: " + name);
        System.out.println("생년월일: " + birthDate);
    }
}

public class Main {
    public static void main(String[] args) {
        UserProfile user = new UserProfile("알리사", LocalDate.of(1998, 12, 25));
        user.printProfile();
    }
}

출력:

이름: 알리사
생년월일: 1998-12-25

6. 비교: 구형 API vs 신형 API

예시: 생일에 1주를 더하기

구형 방식 (Date/Calendar):

import java.util.Calendar;

Calendar cal = Calendar.getInstance();
cal.set(1998, Calendar.DECEMBER, 25);
cal.add(Calendar.WEEK_OF_YEAR, 1);
System.out.println(cal.getTime()); // 번거롭고 직관적이지 않음

신형 방식 (java.time):

import java.time.LocalDate;

LocalDate birthDate = LocalDate.of(1998, 12, 25);
LocalDate nextWeek = birthDate.plusWeeks(1);
System.out.println(nextWeek); // 1999-01-01

새 API에서는 코드가 더 짧고, 더 단순하며, 더 안전합니다.

7. java.time 사용 시 흔한 실수

실수 1: 객체가 불변이라는 사실을 잊음.
date.plusDays(1);를 호출하고 결과를 저장하지 않으면, 원래 날짜는 그대로입니다.

실수 2: LocalDateLocalDateTime을 혼동함.
LocalDate는 날짜(연, 월, 일)만, LocalDateTime은 시간까지 포함합니다. 시와 분까지 처리해야 한다면 혼동하지 마세요.

실수 3: 새 프로젝트에서 구형 클래스를 사용함.
가능하다면 항상 java.time을 사용하세요. 구형 클래스는 호환성 용도로만 필요합니다.

실수 4: 시간대를 잘못 처리함.
여러 지역에 중요하게 영향을 미치는 이벤트를 저장해야 한다면 ZonedDateTime이나 최소한 Instant를 사용하세요. LocalDateTime에는 시간대 정보가 없습니다!

실수 5: LocalDateLocalDateTime을 직접 비교하려 함.
이는 서로 다른 타입이므로 직접 비교할 수 없습니다. 먼저 동일한 타입으로 맞춘 뒤 비교하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION