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) 객체입니다. 실수로 값을 바꿔 버리기 쉽고, 이는 종종 버그로 이어졌습니다.
- Date와 Calendar에서 월은 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의 핵심 클래스들:
| 클래스 | 역할 | 사용 예 |
|---|---|---|
|
날짜만(연, 월, 일) | 생일(시간 없음) |
|
시간만(시, 분, 초) | 약속 시간(날짜 없음) |
|
날짜와 시간, 시간대 없음 | 로컬 이벤트 |
|
시간대가 포함된 날짜/시간 | 민스크 현지 시간으로 회의 |
|
절대 시점(UTC) | 로그 이벤트 타임스탬프 |
|
시간 간격(시, 분, 초) | 통화 지속 시간 |
|
기간(년, 월, 일) | 근속, 나이 |
예시: 새로운 방식으로 날짜 생성
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. 호환성: 레거시 코드와 함께 일하는 법
현실 세계에서는 매우 자주 Date와 Calendar를 사용하는 라이브러리나 오래된 시스템과 함께 작업해야 합니다. 다행히 새 API는 레거시에 친화적이어서, 구형 타입과 신형 타입 간 변환이 가능합니다.
Date ↔ Instant 변환
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);
Calendar ↔ ZonedDateTime 변환
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()));
표: 구형/신형 클래스 매핑
| 구형 클래스 | 신형 클래스 | 비고 |
|---|---|---|
|
|
절대 시간 |
|
|
시간대 포함 날짜/시간 |
|
|
날짜 포맷팅/파싱 |
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: LocalDate와 LocalDateTime을 혼동함.
LocalDate는 날짜(연, 월, 일)만, LocalDateTime은 시간까지 포함합니다. 시와 분까지 처리해야 한다면 혼동하지 마세요.
실수 3: 새 프로젝트에서 구형 클래스를 사용함.
가능하다면 항상 java.time을 사용하세요. 구형 클래스는 호환성 용도로만 필요합니다.
실수 4: 시간대를 잘못 처리함.
여러 지역에 중요하게 영향을 미치는 이벤트를 저장해야 한다면 ZonedDateTime이나 최소한 Instant를 사용하세요. LocalDateTime에는 시간대 정보가 없습니다!
실수 5: LocalDate와 LocalDateTime을 직접 비교하려 함.
이는 서로 다른 타입이므로 직접 비교할 수 없습니다. 먼저 동일한 타입으로 맞춘 뒤 비교하세요.
GO TO FULL VERSION