DateTime은 “값(Value)“이다
“2026년 3월 29일 오후 3시”라는 시점은 숫자 42와 같은 성격을 가진다. 42에 1을 더하면 42가 43으로 변하는 게 아니라 새로운 값 43이 만들어진다. DateTime도 마찬가지다 - “3일 후”를 구하면 원래 날짜가 바뀌는 게 아니라 새로운 DateTime이 나와야 자연스럽다.
이걸 Value Semantics라고 한다. 값 타입은 그 자체로 의미를 가지며, 한 번 생성되면 바뀌지 않는다. 정수, 문자열, 좌표 - 그리고 날짜/시간이 여기에 속한다.
문제는 이 원칙을 어겼을 때 벌어지는 일이다.
Mutable DateTime이 만드는 문제들
참조 공유 부수효과
mutable DateTime의 가장 흔한 사고 시나리오다.
// Java의 java.util.Date는 mutable이었다
Date meetingStart = new Date();
Date meetingEnd = meetingStart; // 같은 객체를 참조
meetingEnd.setHours(meetingStart.getHours() + 1);
// meetingStart도 같이 바뀌어 버린다meetingEnd를 수정했을 뿐인데 meetingStart까지 바뀐다. 두 변수가 같은 객체를 참조하고 있기 때문이다. immutable이면 이런 일이 원천 차단된다 - plusHours(1)은 새 객체를 리턴하므로 원본은 절대 바뀌지 않는다.
Thread Safety
mutable DateTime을 여러 스레드가 공유하면, 한 스레드가 연도를 바꾸는 사이에 다른 스레드가 월을 읽는 식의 race condition이 발생할 수 있다. immutable이면 생성 이후 상태가 절대 바뀌지 않으므로 lock 없이도 안전하게 공유할 수 있다.
Hash 기반 컬렉션 키 손상
HashMap이나 HashSet의 키로 DateTime을 사용할 때, 키의 값이 바뀌면 해시값도 달라진다. 이미 저장된 엔트리를 더 이상 찾을 수 없게 되면서 컬렉션이 조용히 깨진다. immutable이면 이 걱정이 없다.
되돌릴 수 없는 날짜 연산
날짜 연산에는 직관과 다르게 동작하는 함정이 있다.
1월 31일 + 1개월 = 2월 28일 (또는 29일)
2월 28일 - 1개월 = 1월 28일 (31일이 아니다!)“1개월 더하기”의 역연산이 원래 값을 복원하지 못한다. 2월에는 31일이 없으므로 28일로 clamping되고, 1월에는 28일이 유효하므로 그대로 28일이 된다. mutable에서 이 연산이 원본 객체를 직접 변경한다면, plusMonths(1) 후 minusMonths(1)을 했을 때 원래 값으로 돌아오지 않는 버그가 생기고, 디버깅은 극도로 어려워진다. immutable이면 각 연산이 독립적인 새 객체를 만들기 때문에 원본은 항상 보존된다.
업계가 배운 교훈
Java - 가장 유명한 실패 사례
Java의 java.util.Date와 Calendar는 mutable이었고, 수많은 버그의 온상이었다. 문제는 mutability만이 아니었다 - java.util.Date와 java.sql.Date가 같은 이름에 다른 패키지, Date 클래스에 시간까지 포함, thread-safe하지 않음 등. 하지만 mutability가 가장 근본적인 설계 결함이었다.
Stephen Colebourne이 만든 Joda-Time 라이브러리가 이 문제를 먼저 해결했다. 핵심 원칙은 immutability. 이 라이브러리가 사실상 표준처럼 쓰이면서, Java 8(2014)에서 java.time 패키지(JSR-310)가 탄생했다. Colebourne은 JSR-310의 공동 스펙 리드이기도 했다.
// java.time - 모든 클래스가 immutable
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusWeeks(1); // today는 변하지 않는다JavaScript - 현재 진행형
JavaScript의 Date도 mutable이다.
const date = new Date('2026-03-29');
date.setMonth(0); // 원본이 1월로 바뀌어 버린다이 문제를 해결하기 위해 TC39의 Temporal 제안이 진행되어 Stage 4에 도달했다. Temporal의 모든 타입은 immutable이며, Java의 java.time과 유사한 설계 철학을 따른다.
// Temporal - immutable
const date = Temporal.PlainDate.from('2026-03-29');
const nextMonth = date.add({ months: 1 }); // date는 변하지 않는다언어별 비교
| 언어 | Mutable API | Immutable API | 비고 |
|---|---|---|---|
| Java | java.util.Date (1996) | java.time (2014) | JSR-310, Joda-Time에서 발전 |
| JavaScript | Date (1995) | Temporal (2026) | TC39 Stage 4 |
| C# | - | DateTime (struct) | .NET 1.0부터 immutable 설계 |
| Dart | - | DateTime | 처음부터 immutable, setter 없음 |
| Python | - | datetime | 처음부터 immutable, 개별 필드 저장 |
| Rust | - | chrono | 언어 자체가 기본 immutable |
초기 언어들(Java, JavaScript)은 mutable로 시작해서 고통을 겪은 뒤 immutable API를 새로 만들었고, 이후 언어들(C#, Dart, Rust)은 처음부터 immutable로 설계했다. 업계의 학습 곡선이 그대로 보인다.
Duration과 Period - 시간의 두 가지 의미
DateTime 불변성과 함께 알아둘 만한 설계 포인트가 하나 더 있다. “시간의 양”에는 두 가지 서로 다른 의미가 있다.
Duration - 절대적 시간량
“3600초”, “86400초”처럼 물리적으로 정확한 시간량이다. 캘린더 규칙을 몰라도 단순한 정수 연산으로 계산할 수 있다.
Period - 캘린더 상대적 시간량
“1개월”, “1년”은 상황에 따라 실제 길이가 달라진다. 1개월은 28일일 수도, 31일일 수도 있다. 계산하려면 반드시 캘린더 규칙(윤년, 월별 일수 등)을 알아야 한다.
Java의 java.time과 JavaScript의 Temporal은 이 둘을 명시적으로 분리한다.
// Duration - 절대 시간
Duration d = Duration.ofHours(24); // 정확히 86400초
// Period - 캘린더 상대 시간
Period p = Period.ofMonths(1); // 28~31일, 상황에 따라 다름이 구분이 실질적으로 중요해지는 순간은 DST(일광 절약 시간) 전환일이다. “24시간 후”와 “1일 후”가 다른 결과를 낳는다.
- Duration(24시간)을 더하면 물리적 시간을 더하므로, DST 전환으로 인해 로컬 시간이 달라질 수 있다
- Period(1일)를 더하면 캘린더상의 날짜를 더하므로, 로컬 시간은 보존된다
대부분의 비즈니스 로직에서 “내일”은 “24시간 후”가 아니라 “캘린더상 다음 날”을 의미한다. Duration과 Period를 혼용하면 DST 전환일에 1시간씩 어긋나는 버그가 조용히 발생할 수 있다.
정리
DateTime을 immutable로 만드는 건 단순한 코딩 컨벤션이 아니다.
- “날짜/시간은 값이다”라는 도메인 특성에 맞는 설계
- 참조 공유, 스레드 안전성, 해시 무결성 문제의 원천 차단
- 수십 년간 mutable API가 만든 실제 버그에서 학습한 결과
Java와 JavaScript가 mutable에서 immutable로 전환하는 데 각각 18년, 31년이 걸렸다. 그 사이에 발생한 버그의 양을 생각하면, 이후 언어들이 처음부터 immutable을 선택한 건 당연한 귀결이다.