소프트웨어 개발에서 날짜와 시간의 처리는 생각보다 까다로운 문제입니다. 시간대(Time Zone), 윤년(leap year), 월의 길이 차이 등 다양한 예외 상황이 존재하기 때문입니다. Java는 이러한 복잡성을 감안하여 일찍이 날짜와 시간을 다루기 위한 표준 클래스를 제공해 왔습니다.
가장 먼저 등장한 클래스는 Date입니다. Date 클래스는 JDK 1.0부터 제공된 기본 클래스이며, 날짜와 시간의 표현 및 연산을 위한 수단을 제공하였습니다. 그러나 Date는 설계상의 한계로 인해 다음과 같은 문제점을 안고 있었습니다:
내부적으로 날짜를 표현하는 방식이 직관적이지 않고, 월(month)의 인덱스가 0부터 시작하는 등 혼란을 유발
불변(immutable)하지 않아 동시성 처리에 적합하지 않음
생성자 및 메서드 중 일부가 deprecated 처리되며 유지보수에 비효율적
이러한 한계를 극복하기 위해, JDK 1.1부터 Calendar 클래스가 도입되었습니다. Calendar는 다양한 국가와 문화권의 달력 시스템을 지원하며, 시간 계산과 조작을 보다 유연하게 수행할 수 있도록 설계되었습니다. 예를 들어 특정 날짜에 일(day)을 더하거나 빼는 작업은 Calendar를 통해 훨씬 수월하게 구현할 수 있습니다. 하지만 Calendar 역시 완벽하지는 않았습니다.
이러한 문제점을 개선하기 위해 JDK 1.8부터 java.time 패키지가 새롭게 도입되었습니다. 이 패키지는 LocalDate, LocalTime, ZonedDateTime 등 불변 객체 기반의 현대적인 날짜/시간 API를 제공하며, 기존 Date 및 Calendar의 단점을 효과적으로 보완하였습니다.
그럼에도 불구하고, Calendar는 여전히 많은 레거시 코드와 시스템에서 사용되고 있으며, 과거와의 호환성 측면에서도 여전히 학습할 가치가 있는 클래스입니다. 이번 글에서는 Calendar 클래스의 기본 구조와 주요 기능들을 중심으로 그 활용 방법을 알아보겠습니다.
자바에서 날짜와 시간을 보다 유연하게 다루기 위해 도입된 Calendar 클래스는 Date 클래스의 한계를 보완하기 위해 JDK 1.1부터 제공된 추상 클래스입니다. Calendar는 시간 연산, 구성 요소 분리, 시간대 설정 등 다양한 기능을 제공하지만, 추상 클래스이기 때문에 직접 객체를 생성할 수는 없습니다.
Calendar 객체를 생성하려면, 정적 메서드 Calendar.getInstance()를 사용해야 합니다. 이 메서드는 현재 시스템의 기본 로케일과 시간대를 기준으로 적절한 하위 클래스의 인스턴스를 반환합니다.
Calendar cal = Calendar.getInstance();
이때 반환되는 실제 클래스는 상황에 따라 다릅니다.
| 반환되는 클래스 | 설명 |
|---|---|
GregorianCalendar | 대부분의 시스템에서 기본적으로 반환됨. 서양식 그레고리력을 따름 |
BuddhistCalendar | 태국처럼 불기(Buddhist Era)를 사용하는 경우 반환 |
그레고리력(Gregorian calendar)은 1582년 교황 그레고리오 13세가 율리우스력의 오차를 보완하여 제정한 현재 전 세계에서 가장 널리 쓰이는 달력 체계입니다. 윤년 계산법과 월의 길이가 명확히 정의되어 있어 컴퓨터 시스템에서도 표준 달력으로 채택됩니다.
Calendar 객체는 다양한 날짜/시간 필드를 포함하고 있으며, 이 값들을 get(int field) 메서드와 함께 정적 상수를 통해 읽을 수 있습니다.
Calendar today = Calendar.getInstance();
System.out.println("현재 연도: " + today.get(Calendar.YEAR));
System.out.println("현재 월 (0~11): " + today.get(Calendar.MONTH));
| 상수명 | 설명 | 반환값 예시 |
|---|---|---|
YEAR | 연도 | 2025 |
MONTH | 월 (0부터 시작) | 0: 1월, 11: 12월 |
DATE, DAY_OF_MONTH | 해당 월의 일 | 1 ~ 31 |
DAY_OF_YEAR | 연 중 몇 번째 날 | 1 ~ 366 |
DAY_OF_WEEK | 요일 (1: 일요일 ~ 7: 토요일) | 1 ~ 7 |
HOUR | 시간 (12시간제) | 0 ~ 11 |
HOUR_OF_DAY | 시간 (24시간제) | 0 ~ 23 |
MINUTE | 분 | 0 ~ 59 |
SECOND | 초 | 0 ~ 59 |
MILLISECOND | 밀리초 | 0 ~ 999 |
ZONE_OFFSET | 표준 시간대 오프셋 (밀리초 단위) | -43200000 ~ +43200000 |
import java.util.*;
class Ex10_1 {
public static void main(String[] args)
{
// 기본적으로 현재날짜와 시간으로 설정된다.
Calendar today = Calendar.getInstance();
System.out.println("이 해의 년도 : " + today.get(Calendar.YEAR));
System.out.println("월(0~11) : " + today.get(Calendar.MONTH));
System.out.println("이 해의 몇 째 : " + today.get(Calendar.WEEK_OF_YEAR));
System.out.println("이 달의 몇 째 : " + today.get(Calendar.WEEK_OF_MONTH));
// DATE와 DAY_OF_MONTH는 같다.
System.out.println("이 달의 몇 일: " + today.get(Calendar.DATE));
System.out.println("이 달의 몇 일: " + today.get(Calendar.DAY_OF_MONTH));
System.out.println("이 해의 몇 일: " + today.get(Calendar.DAY_OF_YEAR));
System.out.println("요일(1~7, 1:일요일): " + today.get(Calendar.DAY_OF_WEEK));
System.out.println("이 달의 몇 째 요일: " + today.get(Calendar.DAY_OF_WEEK_IN_MONTH));
System.out.println("오전_오후(0:오전, 1:오후): " + today.get(Calendar.AM_PM));
System.out.println("시간(0~11): " + today.get(Calendar.HOUR));
System.out.println("시간(0~23): " + today.get(Calendar.HOUR_OF_DAY));
System.out.println("분(0~59): " + today.get(Calendar.MINUTE));
System.out.println("초(0~59): " + today.get(Calendar.SECOND));
System.out.println("1000분의 1초(0~999): " + today.get(Calendar.MILLISECOND));
// 천분의 1초를 시간으로 표시하기 위해 3600000으로 나누었다.(1시간 = 60 * 60초)
System.out.println("TimeZone(-12~+12): " + (today.get(Calendar.ZONE_OFFSET)/(60*60*1000)));
System.out.println("이 달의 마지막 : " + today.getActualMaximum(Calendar.DATE));
}
}
Calendar는 기본적으로 현재 시각으로 초기화되지만, 필요에 따라 특정 날짜나 시간으로 변경할 수 있습니다. 이때는 set() 메서드를 활용합니다.
void set(int field, int value)
void set(int year, int month, int date)
void set(int year, int month, int date, int hourOfDay, int minute)
void set(int year, int month, int date, int hourOfDay, int minute, int second)
import java.util.*;
class Ex10_2 {
public static void main(String[] args) {
// 요일은 1부터 시작
final String[] DAY_OF_WEEK = {"", "일", "월", "화", "수", "목", "금", "토"};
Calendar date1 = Calendar.getInstance();
Calendar date2 = Calendar.getInstance();
// Calendar.APRIL = 3, 2019년 4월 29일
date1.set(2019, Calendar.APRIL, 29);
System.out.println("date1은 " + toString(date1)
+ DAY_OF_WEEK[date1.get(Calendar.DAY_OF_WEEK)] + "요일이고,");
System.out.println("오늘(date2)은 " + toString(date2)
+ DAY_OF_WEEK[date2.get(Calendar.DAY_OF_WEEK)] + "요일입니다.");
long difference = (date2.getTimeInMillis() - date1.getTimeInMillis()) / 1000;
System.out.println("그 날(date1)부터 지금(date2)까지 " + difference + "초가 지났습니다.");
System.out.println("일(day)로 계산하면 " + difference/(24 * 60 * 60) + "일입니다.");
}
public static String toString(Calendar date) {
return date.get(Calendar.YEAR) + "년 "+ (date.get(Calendar.MONTH) + 1) + "월 "
+ date.get(Calendar.DATE) + "일 ";
}
}
Calendar는 가변 객체(Mutable Object)이므로, 공유되는 환경에서는 주의해서 사용해야 합니다. 가능하다면 java.time 패키지의 LocalDateTime과 같은 불변 클래스 사용을 권장합니다.
날짜 간 차이를 계산하거나 포맷팅하려면 SimpleDateFormat이나 DateFormat을 함께 사용하는 것이 좋습니다.
로케일이나 시간대(TimeZone) 변경이 필요한 경우, Calendar.getInstance(TimeZone, Locale) 메서드를 사용할 수 있습니다.
실무에서는 특정 날짜를 기준으로 며칠 전, 혹은 몇 개월 후의 날짜를 계산해야 하는 경우가 자주 발생합니다. 예를 들어, “오늘로부터 7일 뒤 마감일 설정”이나 “6개월 전 가입자의 데이터 조회” 같은 시나리오가 해당합니다. Java의 Calendar 클래스는 이러한 날짜 연산을 쉽게 처리할 수 있도록 add() 와 roll() 이라는 메서드를 제공합니다.
add() 메서드는 지정한 날짜/시간 필드를 원하는 만큼 증가시키거나 감소시킵니다. 이 메서드의 특징은 필드 간의 연관성을 반영하여 변화가 전파된다는 것입니다.
예를 들어, 1월 31일에서 일(day) 필드를 1만큼 증가시키면, 결과는 2월 1일이 됩니다. 즉, 일 필드의 변화가 월 필드에도 영향을 미칩니다.
Calendar date = Calendar.getInstance();
date.set(2019, Calendar.JULY, 31); // 2019년 8월 31일
date.add(Calendar.DATE, 1); // 하루 후
반면, roll() 메서드는 지정한 필드만을 독립적으로 증가 또는 감소시킵니다. 다른 필드에는 영향을 미치지 않는다는 점이 가장 큰 차이점입니다.
예를 들어, 8월 31일에서 DATE 필드를 1만큼 roll하면 결과는 9월 1일이 아닌 8월 내에서의 날짜로 롤링됩니다.
date.roll(Calendar.DATE, 31); // 월은 그대로, 날짜만 순환
| 항목 | add() | roll() |
|---|---|---|
| 필드 간 영향 | 해당 필드 변화가 상위 필드(월, 연도 등)에 영향을 줌 | 지정한 필드만 영향을 받고 다른 필드는 그대로 유지 |
| 월말 처리 | 1월 31일 + 1일 → 2월 1일 | 1월 31일 roll +1 → 1월 내 날짜로 롤링 |
| 사용 목적 | 정확한 날짜 계산이 필요한 경우 | 단순 반복(예: UI 달력 표시 등)에서 날짜를 순환시킬 때 적합 |
| 예외 상황 | 필드 간 자연스러운 연산 수행 | 월말 처리 시 예상치 못한 결과가 나올 수 있음 |
Calendar date = Calendar.getInstance();
date.set(2019, 7, 31); // 2019년 8월 31일
System.out.println(toString(date));
date.add(Calendar.DATE, 1);
System.out.println("= 1일 후(add) = " + toString(date)); // 2019년 9월 1일
date.add(Calendar.MONTH, -6);
System.out.println("= 6달 전(add) = " + toString(date)); // 2019년 3월 1일
date.roll(Calendar.DATE, 31);
System.out.println("= 31일 후(roll) = " + toString(date)); // 2019년 3월 1일
date.add(Calendar.DATE, 31);
System.out.println("= 31일 후(add) = " + toString(date)); // 2019년 4월 1일
public static String toString(Calendar date) {
return date.get(Calendar.YEAR)+"년 " + (date.get(Calendar.MONTH)+1)
+ "월 " + date.get(Calendar.DATE) + "일";
}
실행 결과를 통해 add()는 실제 날짜 기준으로 계산되어 월과 연도가 변경되지만, roll()은 같은 월 내에서만 날짜가 순환되는 것을 확인할 수 있습니다.
roll() 은 단독으로 사용할 때 유용하지만, 월말(end of month) 처리 시 예외가 발생할 수 있습니다. 예를 들어, DATE 필드가 31일인데 MONTH를 roll하면 해당 월의 말일에 따라 DATE 값도 변경될 수 있습니다.
날짜 간 계산이 정확하게 필요하다면 가급적 add() 를 사용하는 것이 안전합니다.
java.time 패키지의 LocalDate.plusDays() , minusMonths() 등은 이와 같은 불확실성을 줄인 더 직관적인 대안이 될 수 있습니다.
자바에서는 시간이 지남에 따라 날짜와 시간을 다루는 방식이 진화해 왔으며, 그 과정에서 Date와 Calendar라는 두 가지 대표적인 클래스가 공존하게 되었습니다. Date는 JDK 1.0부터, Calendar는 JDK 1.1부터 제공되면서 역할이 분화되었지만, 레거시 코드 또는 외부 API와의 호환성 때문에 여전히 두 클래스를 오가는 상황이 자주 발생합니다.
Calendar 객체는 내부적으로 시간을 long 값으로 저장하고 있으며, getTimeInMillis() 메서드를 통해 해당 값을 추출할 수 있습니다. 이 값을 사용하여 Date 객체를 생성할 수 있습니다.
Calendar cal = Calendar.getInstance();
Date date = new Date(cal.getTimeInMillis()); // Date(long date)
✅ Date(long date) 생성자는 1970년 1월 1일 00:00:00 GMT로부터의 밀리초 값을 기준으로 Date 객체를 생성합니다.
반대로, Date 객체를 Calendar로 변환하려면 Calendar.getInstance() 로 객체를 생성한 후, setTime(Date date) 메서드를 사용하여 날짜 값을 설정하면 됩니다.
Date date = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(date);
✅ Calendar는 Date 객체의 시간을 기반으로 내부 필드를 자동으로 갱신합니다 (YEAR, MONTH, DATE 등).
외부 API 사용 시 호환성 확보: JDBC, JSON 직렬화 등 많은 라이브러리가 여전히 Date 객체를 사용하기 때문에 변환이 필요할 수 있습니다.
시간 정밀도 주의: Calendar는 밀리초 단위까지 포함하지만, 일부 시스템은 초 단위까지만 처리하기 때문에 정밀도가 손실될 수 있습니다.
java.time 패키지와의 변환도 가능하며, JDK 8 이상에서는 Date, Calendar를 LocalDateTime 또는 ZonedDateTime으로 변환해 사용하는 것이 점점 더 일반적입니다.
이번 글에서는 Calendar 클래스의 구조와 주요 메서드들을 중심으로 날짜와 시간을 다루는 방법을 살펴보았습니다. Date 클래스의 한계를 보완하기 위해 등장한 Calendar는 다양한 날짜 필드 접근, 연산 메서드(add, roll) 및 유연한 설정 기능을 제공함으로써 자바의 날짜/시간 처리를 보다 강력하게 만들어 주었습니다.
비록 JDK 8 이후 java.time 패키지(LocalDate, ZonedDateTime 등)의 도입으로 인해 새로운 API가 주류로 자리잡았지만, 여전히 많은 레거시 코드와 외부 라이브러리에서는 Calendar와 Date 기반의 API가 사용되고 있습니다. 이러한 상황에서 Calendar 클래스의 원리를 정확히 이해하고 자유롭게 활용할 수 있는 능력은 실무에서 큰 자산이 됩니다.
앞으로는 SimpleDateFormat을 이용한 날짜 포맷 처리, 또는 Calendar와 java.time API 간의 변환 방법 등을 함께 익혀두면 더욱 강력하고 유연한 시간 처리가 가능해질 것입니다.