날짜와 시간
Calendar와 Date
Date는 날짜와 시간을 다룰 목적으로 JDK 1.0부터 제공되어온 클래스입니다. JDK 1.0이 제공하는 클래스의 수와 기능은 지금과 비교할 수 없을 정도로 빈약했습니다. Date클래스 역시 기능이 부족했기 때문에, 서둘러 Calendar라는 새로운 클래스를 그 다음 버전인 JDK 1.1부터 제공하기 시작했습니다. Calendar는 Date보다는 훨씬 나았지만 몇 가지 단점들이 발견되었습니다. 그래서 늦은 감이 있지만 Java 8부터 'java.time 패키지'로 기존의 단점들을 개선한 새로운 클래스들이 추가되었습니다.
여기서 말하는 Date클래스는 java.sql 패키지가 아닌 java.util 패키지의 Date클래스를 의미합니다.
Calendar와 GregorianCalendar
Calendar는 추상클래스이기 때문에 직접 객체를 생성할 수 없고, 메서드를 통해서 완전히 구현된 클래스의 인스턴스를 얻어야 합니다.
Calendar cal = Calendar.getInstance();
Calendar를 상속받아 완전히 구현한 클래스로는 GregorianCalendar와 BuddhistCalendar가 있는데, getInstance()는 시스템의 국가와 지역설정을 확인해서 태국인 경우에는 BuddhistCalendar의 인스턴스를 반환하고, 그 외에는 GregorianCalendar의 인스턴스를 반환합니다.
GregorianCalendar는 Calendar를 상속받아 오늘날 전세계 공통으로 사용하고 있는 그레고리력에 맞게 구현한 것으로 태국을 제외한 나머지 국가에서는 GregorianCalendar를 사용하면 됩니다.
Calendar의 구현체로 JapaneseImperialCalendar도 있지만, 이는 연호 체계의 불안정성으로 인해 public 접근 제한자를 가지지 않습니다.
Date와 Calendar간의 변환
Calendar가 새로 추가되면서 Date는 대부분의 메서드가 'deprecated'되었으므로 잘 사용되지 않습니다. 그럼에도 불구하고 여전히 Date를 필요로 하는 메서드들이 있기 때문에 Calendar를 Date로 또는 그 반대로 변환할 일이 생깁니다.
// Calendar -> Date
Calendar cal = Calendar.getInstance();
Date d = new Date(cal.getTimeInMillis());
// Date -> Calendar
Date d = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(d);
getInstance()를 통해서 얻은 Calendar 인스턴스는 기본적으로 현재 시스템의 날짜와 시간에 대한 정보를 담고 있습니다. 원하는 날짜나 시간으로 설정하려면 set 메서드를 사용하면 됩니다. 반대로 원하는 값을 얻어오기 위해서는 `int get(int field)` 를 이용하면 됩니다.
이때 get 메서드의 매개변수로 사용되는 int값들은 Claendar에 정의된 static상수입니다.
한 가지 주의해야할 것은 `get(Calendar.MONTH)`로 얻어오는 값의 범위가 1~12가 아닌 0~11이라는 것입니다.
날짜 계산
두 날짜간의 차이를 구하기 위해서는 두 날짜를 최소단위인 초단위로 변경한 다음 그 차이를 구하면 됩니다. 우선 set 메서드를 사용해 날짜와 시간을 원하는 값으로 변경하고, getTimeInMillis()를 사용해 초단위로 바꿔줍니다. getTimeInMillis()는 1/1000초 단위로 값을 반환하기 때문에 초단위로 얻기 위해서는 추가로 '1000'으로 나눠 주어야 하고, 일단위로 얻기 위해서는 '24*60*60*1000'으로 나누어야 합니다.
단순히 시간상의 전후를 알고 싶을 때는 'boolean after(Object when)' 또는 'boolean before(Object when)'을 사용하면 됩니다.
형식화 클래스
자바에서는 숫자와 날짜를 쉽게 형식화 할 수 있는 형식화 클래스를 제공합니다. 이 클래스는 java.text 패키지에 포함되어 있으며 숫자, 날짜, 텍스트 데이터를 일정한 형식에 맞게 표현할 수 있는 방법을 객체지향적으로 설계하여 표준화하였습니다.
형식화 클래스는 형식화에 사용될 패턴을 정의하는데, 데이터를 정의된 패턴에 맞춰 형식화할 수 있을 뿐만 아니라, 역으로 형식화된 데이터에서 원래의 데이터를 얻어낼 수도 있습니다.
DecimalFormat
형식화 클래스 중에서 숫자를 형식화 하는데 사용되는 것이 DecimalFormat입니다. DecimalFormat을 사용하면 숫자 데이터를 정수, 부종소수점, 금액 등의 다양한 형식으로 표현할 수 있으며, 반대로 일정한 형식의 텍스트 데이터를 숫자로 쉽게 변환하는 것도 가능합니다.
형식화 클래스에서는 원하는 형식으로 표현 또는 변환하기 위해서 패턴을 정의하는데, 이 패턴 정의가 전부라고 해도 과언이 아닙니다. 다음은 DecimalFormat 패턴에 자주 사용되는 기호들을 정리한 표입니다.
| 기호 | 의미 | 패턴 |
| 0 | 10진수 (값이 없으면 0) | 0 0.0 000000000.00000 |
| # | 10진수 | # #.# #########.##### |
| . | 소수점 | #.# |
| - | 음수부호 | #.#- -#.# |
| , | 단위 구분자 (콤마) | #,###.### #,####.## |
| E | 지수기호 | #E0 0E0 ##E0 00E0 ###E 0000E0 0.0E0 00.000000000E0 ###.########E0 |
| ; | 패턴 구분자 | #,###.##+;#,###.##- |
| % | 퍼센트 | #.#% |
| \u2030 | 퍼밀 (퍼센트 x 10) | #.#\u2030 |
| \u00A4 | 통화 | \u00A4 #,### |
| ' | escape 문자 (작은 따옴표) | '#'#.### |
DecimalFormat을 사용하는 방법은 간단합니다. 먼저 원하는 출력형식의 패턴을 작성하여 DecimalFormat인스턴스를 생성한 다음, 출력하고자 하는 문자열로 format메서드를 호출하면 원하는 패턴에 맞게 변환된 문자열을 얻게 됩니다.
double number = 1234567.89;
DecimalFormat df = new DecimalFormat("#.#E0");
String result = df.format(number);
그리고 parse 메서드를 사용하면 기호와 문자가 포함된 문자열을 숫자로 쉽게 변환할 수도 있습니다. 이 메서드는 DecimalFormat의 조상인 NumberFormat에 정의된 메서드이며, 선언부는 다음과 같습니다.
public Number parse(String source) throws ParseExcepion
SimpleDateFormat
Date와 Calendar만으로 날짜 데이터를 원하는 형태로 다양하게 출력하는 것은 불편하고 복잡합니다. 그러나 SimpleDateFormat을 사용하면 이러한 문제들이 간단히 해결됩니다.
| 기호 | 의미 |
| G | 연대(BC, AD) |
| y | 년도 |
| M | 월(1~12 또는 1월~12월) |
| w | 년의 몇 번째 주(1~53) |
| W | 월의 몇 번째 주(1~5) |
| D | 년의 몇 번째 일(1~366) |
| d | 월의 몇 번째 일(1~31) |
| F | 월의 몇 번째 요일(1~5) |
| E | 요일 |
| a | 오전/오후(AM, PM) |
| H | 시간(0~23) |
| k | 시간(1~24) |
| K | 시간(0~11) |
| h | 시간(1~12) |
| m | 분(0~59) |
| s | 초(0~59) |
| S | 천분의 일초(0~999) |
| z | Time zone(General time zone) |
| Z | Time zone(RFC 822 time zone) |
| ' | escape 문자(작은 따옴표) |
SimpleDateFormat을 사용하는 방법은 간단합니다. 먼저 원하는 출력형식의 패턴을 작성하여 SimpleDateFormat인스턴스를 생성한 다음, 출력하고자 하는 Date인스턴스를 가지고 format(Date d)를 호출하면 지정한 출력형식에 맞게 변환된 문자열을 얻게 됩니다.
Date today = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String result = df.format(today);
Date인스턴스만 format메서드에 사용될 수 있기 때문에, Calendar인스턴스를 Date인스턴스로 변환 후 사용해야 합니다.
그리고 parse 메서드를 사용하면 문자열을 날짜 Date인스턴스로 변환할 수 있습니다. 이 메서드는 SimpleDateFormat의 조상인 DateFormat에 정의되어 있습니다.
public Date parse(String source) throws ParseException
ChoiceFormat
ChoiceFormat은 특정 범위에 속하는 값(숫자: double)을 문자열로 변환해줍니다. 연속적 또는 불연속적인 범위의 값들을 처리하는 데 있어서 if문이나 switch문은 적절하지 못한 경우가 많습니다. 이럴 때 ChoiceFormat을 잘 사용하면 복잡하게 처리될 수밖에 없었던 코드를 간단하고 직관적으로 만들 수 있습니다.
ChoiceFormat의 생성자는 다음과 같이 2개가 있습니다.
public ChoiceFormat(double[] limits, String[] formats) {
setChoices(limits, formats);
}
public ChoiceFormat(String newPattern) {
applyPattern(newPattern);
}
첫 번째는 직접 숫자 배열과 문자열 배열을 인자로 넘기는 방식이고, 두 번째는 패턴을 인자로 넘기는 방식입니다. 패턴은 구분자로 '#'과 '<' 두 가지를 제공하는데 'limit#value'의 형태로 사용합니다. '#'은 경계값을 범위에 포함하고, '<'는 경계값을 범위에 포함하지 않습니다.
경계값(limits)은 반드시 모두 오름차순으로 정렬되어 있어야 하며, 치환 될 문자열의 개수는 경계값에 의해 정의된 범위의 개수와 일치해야 합니다. 그렇지 않으면 IllegalArgumentException이 발생합니다.
다음과 같이 사용할 수 있습니다.
double[] limits = {60, 70, 80, 90};
String[] grades = {"D", "C", "B", "A"};
ChoiceFormat form = new ChoiceFormat(limits, grades);
String result = form.format(77);
MessageFormat
MessageFormat은 데이터를 정해진 양식에 맞게 출력할 수 있도록 도와줍니다. 데이터가 들어갈 자리를 마련해 놓은 양식을 미리 작성하고 프로그램을 이용해서 다수의 데이터를 같은 양식으로 출력할 때 사용하면 좋습니다. 그리고 SimpleDateFormat의 parse처럼 MessageFormat의 parse를 이용하면 손 쉽게 지정된 양식에서 필요한 데이터만을 추출해 낼 수도 있습니다.
String msg = "Name: {0} \nTel: {1} \nAge:{2} \n Birth:{3}";
Object[] argumets = {"홍길동", "02-1111-2222", "27", "07-09"};
String result = MessageFormat.format(msg, arguments);
MessageFormat에 사용할 양식인 문자열 msg를 작성할 때 '{숫자}'로 표시된 부분이 데이터가 출력될 자리입니다. 이 자리는 순차적일 필요는 없고 여러 번 반복해서 사용할 수도 있습니다. 양식에 들어갈 데이터는 객체 배열인 arguments에 지정되어 있으며, 숫자는 배열처럼 인덱스가 0부터 시작합니다.
그리고 parse 메서드를 사용하면 출력된 데이터로부터 필요한 데이터만을 뽑아낼 수도 있습니다.
public Object[] parse(String source) throws ParseException
java.time 패키지
Java의 탄생부터 지금까지 날짜와 시간을 다루는데 사용해왔던, Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 드디어 Java 8부터 'java.time 패키지' 가 추가되었습니다. 이 패키지는 다음과 같이 4개의 하위 패키지를 갖고 있습니다.
| 패키지 | 설 명 |
| java.time | 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공 |
| java.time.chrono | 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공 |
| java.time.format | 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공 |
| java.time.temporal | 날짜와 시간의 필드(field)와 단위(unit)를 위한 클래스들을 제공 |
| java.time.zone | 시간대(time-zone)와 관련된 클래스들을 제공 |
위의 패키지들에 속한 클래스들의 가장 큰 특징은 String클래스처럼 '불변(immutable)'이라는 것입니다. 그래서 날짜나 시간을 변경하는 메서드들은 기존의 객체를 변경하는 대신 항상 새로운 객체를 반환합니다. 기존 Calendar클래스는 변경가능하므로, 멀티 쓰레드 환경에서 안전하지 못합니다.
핵심 클래스
날짜와 시간을 하나로 표현하는 Calendar클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리해 놓았습니다. 시간을 표현할 때는 LocalTime클래스를 사용하고, 날짜를 표현할 때는 LocalDate클래스를 사용합니다. 그리고 날짜와 시간이 모두 필요할 때는 LocalDateTime클래스를 사용하면 됩니다. 여기에 추가적으로 시간대(time-zone)까지 다뤄야 한다면 ZonedDateTime클래스를 사용하면 됩니다.

Calendar는 ZonedDateTime처럼, 날짜와 시간 그리고 시간대까지 모두 갖고 있습니다. Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 나노초단위로 표현합니다. 날짜와 시간을 초단위로 표현한 값을 타임스탬프(time-stamp)라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터베이스에 많이 사용됩니다.
이외에도 날짜를 더 세부적으로 다룰 수 있는 Year, YearMonth, MonthDay와 같은 클래스도 있습니다.
Period와 Duration
날짜와 시간의 간격을 표현하기 위한 클래스도 있는데, Period는 두 날짜간의 차이를 표현하기 위한 것이고, Duration은 시간의 차이를 표현하기 위한 것입니다.
객체 생성하기 - now(), of()
java.time 패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 static 팩토리 메서드인 now()와 of()를 사용하는 것입니다. now()는 현재 날짜와 시간을 저장하는 객체를 생성합니다.
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zDateTime = ZonedDateTime.now();
of()는 단순히 해당 필드의 값을 순서대로 지정해 주기만 하면 됩니다.
LocalDate date = LocalDate.of(2025, 12, 31);
LocalTime time = LocalTime.of(23, 59, 59);
LocalDateTime dateTime = LocalDateTime.of(date, time);
ZonedDateTime zDateTime = ZonedDateTime.of(dateTime, ZoneId.of("Asia/Seoul"));
Temporal과 TemporalAmount
LocalDate, LocalTime, LocalDateTime, ZonedDateTime등 날짜와 시간을 표현하기 위한 클래스들은 모두 Temporal, TemporalAccessor, TemporalAdjuster인터페이스를 구현했고, Duration과 Period는 TemporalAmount인터페이스를 구현했습니다. 앞으로 소개할 메서드 중에서 매개변수 타입이 Temporal로 시작하는 것들이 자주 등장하는데, 이때 TemporalAmount인지 아닌지만 확인하면 됩니다.
TemporalUnit과 TemporalField
날짜와 시간의 단위를 정의해 놓은 것이 TemporalUnit인터페이스이고, 이 인터페이스를 구현한 것이 열거형 ChronoUnit입니다. 그리고 TemporalField인터페이스는 년, 월, 일 등 날짜와 시간의 필드를 정의해 놓은 것으로, 열거형 ChronoField가 이를 구현했습니다.
날짜와 시간에서 특정 필드의 값만을 얻을 때는 get()이나, get으로 시작하는 이름의 메서드를 이용합니다.
LocalTime now = LocalTime.now();
int minute = now.getMinute();
int minute = now.get(ChronoField.MINUTE_OF_HOUR);
특정 날짜와 시간에서 지정된 단위의 값을 더하거나 뺄 때는 plus(), minus()나 plus, minus로 시작하는 이름의 메서드를 이용합니다.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
또한 다음 메서드를 이용해 특정 TemporalField나 TemporalUnit을 사용할 수 있는지 확인할 수 있습니다.
boolean isSupported(TemporalUnit unit)
boolean isSupported(TemporalField field)
LocalDate와 LocalTime
LocalDate와 LocalTime은 java.time 패키지의 가장 기본이 되는 클래스이며, 나머지는 이 둘의 확장입니다.
특정 필드의 값 가져오기 - get(), getXxx()
LocalDate와 LocalTime의 객체에서 특정 필드의 값을 가져올 때는 아래의 표에 있는 메서드를 사용합니다. 주의할 점은 Calendar와 달리 월(Month)의 범위가 1~12이고, 요일은 1~7(일~토)의 범위를 갖습니다.
| 클래스 | 메서드 | 설명 |
| LocalDate | int getYear() | 년도 |
| int getMonthValue() | 월(숫자) | |
| Month getMonth() | 월(영어) | |
| int getDayOfMonth() | 일 | |
| int get DayOfYear() | 같은 해의 1월 1일부터 몇 번째 일(365일 중) | |
| DayOfWeek getDayOrWeek() | 요일(영어) | |
| int lengthOfMonth() | 같은 달의 총 일 수(31일 중) | |
| int lengthOfYear() | 같은 해의 총 일 수(365 or 366) | |
| boolean isLeapYear() | 윤년 여부 확인 | |
| LocalTime | int getHour() | 시 |
| int getMinute() | 분 | |
| int getSecond() | 초 | |
| int getNano() | 나노초 |
위의 표에 소개된 메서드 외에도 get()이 있는데, 원하는 필드를 직접 지정할 수 있습니다. 대부분의 필드는 int타입의 범위에 속하지만, 몇몇 필드는 int타입의 범위를 넘을 수 있습니다. 그럴때 getLong()을 사용합니다.
int get(TemporalField field)
Long getLong(TemporalField field)
이 메서드들의 매개변수로 사용할 수 있는 필드들은 TemporalField인터페이스를 구현한 열거형 ChronoField에 있습니다. 물론 사용할 수 있는 필드는 클래스마다 다르며, 만일 해당 클래스가 지원하지 않는 필드를 사용하면 UnsupportedTemporalTypeException이 발생합니다.
그리고 다음과 같이 해당 필드가 가질 수 있는 값의 범위를 알고 싶으면 range()를 사용하면 됩니다.
System.out.println(ChronoField.HOUR_OF_DAY.rang()); // 0 - 23
필드의 값 변경하기 - with(), plus(), minus()
날짜와 시간에서 특정 필드 값을 변경하려면, 다음과 같이 with로 시작하는 메서드를 사용하면 됩니다.
| 클래스 | 메서드 |
| LocalDate | withYear(int year) |
| withMonth(int month) | |
| withDayOfMonth(int dayOfMonth) | |
| withDayOfYear(int dayOfYear) | |
| LocalTime | withHour(int hour) |
| withMinute(int minute) | |
| withSecond(int second) | |
| withNano(int nanoOfSecond) |
위 메서드들 대신 with()를 사용하면, 원하는 필드를 직접 지정할 수 있습니다.
LocalDate with(TemporalField field, long newValue)
LocalTime with(TemporalField field, long newValue)
주의해야 할 점은 java.time 패키지의 클래스들은 불변이기 때문에 항상 새로운 객체를 생성해서 반환하므로, 다음과 같이 대입연산자를 같이 사용해야합니다.
date = date.withYear(2025);
time = time.withHour(12);
이 외에도 특정 필드에 값을 더하거나 빼는 plus()와 minus()가 있고, 이 메서드들로 만든 plusXxx(), minusXxx() 같은 메서드들도 있습니다.
그리고 LocalTime의 truncatedTo()는 지정된 것보다 작은 단위의 필드를 0으로 만듭니다.
LocalTime time = LocalTime.of(12, 34, 56); // 12시 34분 56초
time = time.truncatedTo(ChronoUnit.HOURS); // 12시 00분 00초
LocalDate에는 truncatedTo()가 없는데, 그 이유는 년, 월, 일은 0이 될 수 없기 때문입니다.
날짜와 시간 비교 - isAfter(), isBefore(), isEqual()
LocalDate와 LocalTime도 compareTo()가 적절히 오버라이딩되어 있어서, 아래와 같이 compareTo()로 비교할 수 있습니다.
int result = date1.compareTo(date2); // -1, 0, 1
그리고 보다 편리하게 비교할 수 있는 메서드들이 추가로 제공됩니다.
boolean isAfter(ChronoLocalDate other)
boolean isBefore(ChronoLocalDate other)
boolean isEqual(ChronoLocalDate other)
equals()가 있는데도, isEqual()을 제공하는 이유는 연표(chronology)가 다른 두 날짜를 비교하기 위해서입니다. 그래서 대부분의 경우 equals()와 isEqual()의 결과는 같습니다.
Instant
Instant는 에포크 타임(epoch time, 1970-01-01 00:00:00 UTC)부터 경과된 시간을 나노초 단위로 표현합니다. 사람이 사용하는 날짜와 시간은 여러 진법이 섞여있어서 계산하기 어렵지만, Instant는 단일 진법으로만 다루기 때문에 계산하기 쉽습니다.
Instant를 생성할 때는 다음과 같이 now()와 ofEpochSecond()를 사용합니다.
Instant now = Instant.now();
Instant now2 = Instant.ofEpochSecond(now.getEpochSecond());
Instant now3 = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());
그리고 필드에 저장된 값을 가져올 때는 다음과 같이 합니다.
long epochSec = now.getEpochSecond(); // 초
int nano = now.getNano(); // 나노초
long milli = now.toEpochMilli(); // 밀리초
위 코드에서 짐작할 수 있듯이, Instant는 시간을 초 단위와 나노초 단위로 나누어 저장합니다. 데이터베이스의 타임스탬프(timestamp)처럼 밀리초 단위의 에포크 타임을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있습니다.
Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있습니다. 예를 들어 한국은 시간대가 '+09:00' 이므로 Instant와 LocalTime간에는 9시간의 차이가 있습니다. 이렇게 시간대를 고려해야 하는 경우 OffsetDateTime을 사용하는 것이 더 나은 선택일 수 있습니다.
UTC는 'Cooordinated Universal Time'의 약어로 '세계 협정시'라고 하며, 1972년 1월 1일 부터 시행된 국제 표준시입니다. 이전에 사용되던 GMT(Greenwich Mean Time)와 거의 같지만, UTC가 좀 더 정확합니다.
Instant와 Date간의 변환
Instant는 기존의 java.util.Date를 대체하기 위한 것이며, Java 8부터 Date에 Instant로 변환할 수 있는 새로운 메서드가 추가되었습니다.
static Date from(Instant instant) // Instant -> Date
Instant toInstant() // Date -> Instant
LocalDateTime과 ZonedDateTime
앞서 언급한 것과 같이 LocalDate와 LocalTime을 합쳐 놓은 것이 LocalDateTime이고, LocalDateTime에 시간대(time zone)을 추가한 것이 ZonedDateTime입니다.
LocalDate와 LocalTime으로 LocalDateTime 만들기
다음과 같이 LocalDate와 LocalTime을 합쳐서 하나의 LocalDateTime을 만들 수 있는 다양한 방법들이 있습니다.
LocalDate date = LocalDate.of(2025, 12, 31);
LocalTime time = LocalTime.of(12, 34, 56);
LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt2 = date.atTime(time);
LocalDateTime dt3 = time.atDate(date);
LocalDateTime dt4 = date.atTime(12, 34, 56);
LocalDateTime dt5 = time.atDate(LocalDate.of(2025, 12, 31));
LocalDateTime dt6 = date.atStartOfDay(); // dt6 = date.atTime(0, 0, 0);
물론 LocalDateTime에도 날짜와 시간을 직접 지정할 수 있는 다양한 버전의 of()와 now()가 정의되어 있습니다.
LocalDateTime dateTime = LocalDateTime.of(2025, 12, 31, 12, 34, 56);
LocalDateTime today = LocalDateTime.now();
LocalDateTime의 변환
반대로 LocalDateTime을 LocalDate 또는 LocalTime으로 변환할 수 있습니다.
LocalDateTime dt = LocalDateTime.of(2024, 12, 31, 12, 34, 56);
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();
LocalDateTime으로 ZonedDateTime만들기
LocalDateTime에 시간대(time-zone)를 추가하면, ZonedDateTime이 됩니다. 기존에는 TimeZone클래스로 시간대를 다뤘지만 새로운 시간 패키지에서는 ZoneId라는 클래스를 사용합니다. ZoneId는 일광 절약시간(DST, Daylight Saving Time)을 자동적으로 처리해주므로 더 편리합니다.
LocalDate에 시간 정보를 추가하는 atTime()을 쓰면 LocalDateTime을 얻을 수 있는 것처럼, LocalDateTime에 atZone()으로 시간대 정보를 추가하면, ZonedDateTime을 얻을 수 있습니다.
ZoneId = zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dateTime.atZone(zid); // LocalDateTime + ZoneId
사용 가능한 ZoneId의 목록은 ZoneId.getAvailableZoneIds()로 얻을 수 있습니다.
LocalDate에 atStartOfDay()라는 메서드가 있는데, 이 메서드에 매개변수로 ZoneId를 지정해도 ZoneDateTime을 얻을 수 있습니다.
ZonedDateTime zdt = LocalDate.now().atStartOfDay(zid); // LocalDate + startOfDay + ZoneId
만일 현재 특정 시간대의 시간, 예를 들어 뉴욕을 알고 싶다면 다음과 같이 하면 됩니다.
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
물론 위 코드에서 now() 대신에 of()를 사용하여 날짜를 직접 지정할 수도 있습니다.
ZoneOffset
UTC로부터 얼마만큼 떨어져 있는지를 ZoneOffset으로 표현합니다. 예를 들어 서울은 '+9' 입니다. 즉, UTC보다 9시간(32400초=60x60x9)이 빠릅니다.
ZoneOffset krOffset = ZonedDateTime.now.getOffset();
ZoneOffset krOffset = ZoneOffset.of("+9");
int krOffsetInSec = krOffset.get(ChronoField.OFFSET_SECONDS); // 32400
OffsetDateTime
ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffsetDateTime입니다. ZoneId는 일광 절약 시간처럼 시간대와 관련된 규칙들을 포함하고 있는데, ZoneOffset은 단지 시간대를 시간의 차이로만 구분합니다. 컴퓨터에게 일광 절약 시간처럼 계절별로 시간을 더했다 뺐다하는 것과 같은 행위는 위험하기 때문에 아무런 변화 없이 일관된 시간체계를 유지하는 것이 더 안전합니다.
같은 지역 내의 컴퓨터 간에 데이터를 주고받을 때, 전송 시간을 표현하기에는 LocalDateTime이면 충분하겠지만, 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요합니다.
ZonedDateTime zdt = ZonedDateTime.of(date, time, zid);
OffsetDateTime odt = OffsetDateTime.of(date, time, krOffset);
OffsetDateTime은 ZonedDateTime처럼, LocalDateTime에 ZoneOffset을 더하거나, ZonedDateTime에 toOffsetDateTIme()을 호출해서 얻을 수도 있습니다.
ZoneOffset krOffset = ZoneOffset.of("+9");
dateTime.atOffset(krOffset); // LocalDateTime + ZoneOffset
OffsetDateTime odt = zdt.toOffsetDateTime(); // ZonedDateTime -> OffsetDateTime
ZonedDateTime의 변환
ZonedDateTime도 LocalDateTime처럼 날자와 시간에 관련된 다른 클래스로 변환하는 메서드를 갖고 있습니다.
LocalDate toLocalDate()
LocalTime toLocalTime()
LocalDateTime toLocalDateTime()
OffsetDateTime toOffsetDateTime()
long toEpochSecond()
Instant toInstant()
GregorianCalendar와 가장 유사한 것이 ZonedDateTime입니다. GregorianCalendar와 ZonedDateTime간의 변환방법만 알면, 그 다음엔 위의 나열한 메서드를 이용해서 다른 날짜와 시간 클래스들로 변환할 수 있습니다.
// ZonedDateTime -> GregorianCalendar
GregorianCalendar from(ZonedDateTime zdt)
// GregorianCalendar -> ZonedDateTime
ZonedDateTime toZonedDateTime()
TemporalAdjusters
앞서 보았던 plus(), minus()와 같은 메서드로 날짜와 시간을 계산할 수 있지만, 지난주 토요일이 며칠인지, 또는 이번달의 3번째 금요일은 며칠인지와 같은 날짜 계산을 하기에는 불편합니다. 그래서 자주 쓰일만한 날짜 계산들을 대신 해주는 메서드를 정의해놓은 것이 ZonedDateTime클래스입니다.
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
위의 코드는 다음 주 월요일의 날짜를 계산할 때 TemporalAdjusters에 정의된 next()를 사용하였습니다. 이 외에도 다음과 같은 많은 유용한 메서드들이 정의되어 있습니다.
| 메서드 | 설명 |
| firstDayOfNextYear() | 다음 해의 첫 날 |
| firstDayOfNextMonth() | 다음 달의 첫 날 |
| firstDayOfYear() | 올 해의 첫 날 |
| firstDayOfMonth() | 이번 달의 첫 날 |
| lastDayOfYear() | 올 해의 마지막 날 |
| lastDayOfMont() | 이번 달의 마지막 날 |
| firstInMonth(DayOfWeek dayOfWeek) | 이번 달의 첫 번째 요일 |
| lastInMonth(DayOfWeek dayOfWeek) | 이번 달의 마지막 요일 |
| previous(DayOfWeek dayOfWeek) | 지난 요일(당일 미포함) |
| previousOrSame(DayOfWeek dayOfWeek) | 지난 요일(당일 포함) |
| next(DayOfWeek dayOfWeek) | 다음 요일(당일 미포함) |
| nextOrSame(DayOfWeek dayOfWeek) | 다음 요일(당일 포함) |
| dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) | 이번 달의 n번째 요일 |
TemporalAdjuster 직접 구현하기
보통은 TemporalAdjusters에 정의된 메서드로 충분하겠지만, 필요하면 자주 사용되는 날짜 계산을 해주는 메서드를 직접 만들 수도 있습니다. LocalDate의 with()는 다음과 같이 정의되어있으며, TemporalAdjuster인터페이스를 구현한 클래스의 객체를 매개변수로 제공해야 합니다.
LocalDate with(TemporalAdjuster adjuster)
with()는 LocalTime, LocalDateTime, ZonedDateTime, Instant 등 대부분의 날짜와 시간에 관련된 클래스에 포함되어 있습니다.
TemporalAdjuster인터페이스는 다음과 같이 추상 메서드 하나만 정의되어 있으며, 이 메서드만 구현하면 됩니다.
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
앞서 언급한 것과 같이 날짜와 시간에 관련된 대부분의 클래스는 Temporal인터페이스를 구현하였으므로 adjustInto()의 매개변수가 될 수 있습니다. 예를 들어, 특정 날짜로부터 2일 후의 날짜를 계산하는 DayAfterTomorrow는 다음과 같이 작성할 수 있습니다.
class DayAfterTomorrow implements TemporalAdjuster {
@Override
public Temporal adjustInto(Temporal temporal) {
return temporal.plus(2, ChronoUnit.DAYS);
}
}
LocalDate today = LocalDate.now();
LocalDate d = today.with(new DayAfterTomorrow());
Period와 Duration
Period는 날짜의 차이를, Duration은 시간의 차이를 계산하기 위한 것입니다.
between()
두 날짜 date1과 date2의 차이를 내타내는 Period는 between()으로 얻을 수 있습니다. 이때 날짜 상으로 이전으면 양수로, 이후면 음수로 Period에 저장됩니다.
LocalDate date1 = LocalDate.of(2024, 1, 1);
LocalDate date2 = LocalDate.of(2025, 12, 31);
Period pe = Period.between(date1, date2);
그리고 시간차이를 구할 때는 Duration을 사용합니다.
LocalTime time1 = LocalTime.of(00, 00, 00);
LocalTime time2 = LocalTime.of(12, 34, 56);
Duration du = Duration.between(time1, time2);
Period와 Duration에서 특정 필드의 값을 얻을 때는 get()을 사용합니다.
| Period | year | long get(ChronoUnit.YEARS) | int getYears() |
| month | long get(ChronoUnit.MONTHS) | int getMonths() | |
| day | long get(ChronoUnit.DAYS) | int getDays() | |
| Duration | sec | long get(ChronoUnit.SECONDS) | int getSeconds() |
| nano | long get(ChronoUnit.NANOS) | int getNano() |
한 가지 특이한 점은 Period와 달리 Duration에는 getHours(), getMinutes() 같은 메서드가 없습니다. 불편하지만 hour와 minute를 구하기 위해서는 Duration을 LocalTime으로 변환한 다음에 LocalTime으로부터 구해야합니다.
LocalTime time = LocalTime.of(0, 0).plusSeconds(du.getSeconds());
int hour = time.getHour();
int minute = time.getMinute();
between()과 until()
until()은 between()과 거의 같은 일을 합니다. between()은 static메서드이고, until()은 인스턴스 메서드라는 차이가 있습니다.
Period pe = Period.between(today, mybirthDay);
Period pe = today.until(myBirthDay);
Period는 년월일을 분리해서 저장하기 때문에, D-day를 구하려는 경우에는 두 개의 매개변수를 받는 until()을 사용하는 것이 낫습니다. 그리고 날짜가 아닌 시간에도 until()을 사용할 수 있지만, Duration을 반환하는 until()은 없습니다.
long dDay = today.until(myBirthDay, ChronoUnit.DAYS);
long sec = LocalTime.now().until(endTime, ChronoUnit.SECONDS);
of(), with()
Period에는 of(), ofYears(), ofMonths(), ofWeeks(), ofDays()가 있고, Duration에는 of(), ofDays(), ofHours(), ofMinutes(), ofSeconds() 등이 있습니다. 사용법은 LocalDate나 LocalTime의 of()와 동일합니다.
Period pe = Period.of(1, 12, 31);
Period pe1 = Period.ofYears(2);
Duration du = Duration.of(60, ChronoUnit.SECONDS);
Duration du1 = Duration.ofSeconds(60);
특정 필드의 값을 변경하는 with()도 있습니다.
pe = pe.withYears(2);
du = du.withSeconds(120);
사칙연산, 비교연산, 기타 메서드
기본적인 plus(), minus()외에 곱셈과 나눗셈을 위한 메서드도 있습니다. 참고로 나눗셈은 Duration만 있습니다.
pe = pe.minusYears(1).multipliedBy(2);
du = du.plusHours(1).dividedBy(60);
그리고 음수인지 확인하는 메서드와, 0인지 확인하는 메서드도 있습니다.
boolean sameDate = Period.between(date1, date2).isZero();
boolean isBefore = Duration.between(time1, time2).isNegative();
부호를 반대로 변경하는 negated()와 부호를 없애는 abs()도 있습니다. 참고로 abs()는 Duration에만 있습니다.
du = du.abs();
du = du.negated();
추가로 Period에는 normalized()라는 메서드가 있는데, 이 메서드는 월(month)의 값이 12를 넘지 않게, 즉 13개월을 1년 1개월로 바꿔줍니다. 하지만 일(day)의 길이는 일정하지 않으므로 그대로 유지됩니다.
pe = Period.of(1, 13, 32).normalized(); // -> 2년 1개월 32일
다른 단위로 변환
이름이 'to'로 시작하는 메서드들이 있는데, 이 들은 Period와 Duration을 다른 단위의 값으로 변환한는데 사용됩니다. get()은 특정 필드의 값을 그대로 가져오는 것이지만, 아래의 메서드들은 특정 단위로 변환한 결과를 반환한다는 차이가 있습니다.
| 클래스 | 메서드 | 설 명 |
| Period | long toTotalMonths() | 년월일을 월 단위로 변환해서 반환(일 단위는 무시) |
| Duration | long toDays() | 일 단위로 변환해서 반환 |
| long toHours() | 시간 단위로 변환해서 반환 | |
| long toMinutes() | 분 단위로 변환해서 반환 | |
| long toMillis() | 밀리초 단위로 변환해서 반환 | |
| long toNanos() | 나노초 단위로 변환해서 반환 |
이 메서드들의 반환타입은 모두 long 타입인데, 이것은 지정된 단위 이하의 값들은 버려진다는 뜻입니다.
참고로 LocalDate의 toEpochDay()라는 메서드는 에포크 데이(epoch day)인 '1970-01-01'부터 날짜를 세어서 반환합니다. 이 메서드를 이용하면 Period를 사용하지 않고도 두 날짜간의 일수를 편리하게 계산할 수 있습니다.
LocalDate date1 = LocalDate.of(2025, 8, 22);
LocalDate date2 = LocalDate.of(2025, 8, 23);
long period = date2.toEpochDay() - date1.toEpochDay();
마찬가지로 LocalTime에도 toSecondOfDay(), toNanoOfDay()라는 메서드가 있어서 Duration을 사용하지 않고도 위와 같이 뺄셈으로 시간 차이를 계산할 수 있습니다.
파싱과 포맷
형식화(formattin)와 관련된 클래스들은 java.time.format 패키지에 들어있는데, 이 중에서 DateTimeFormatter가 핵심입니다. 이 클래스에는 자주 쓰이는 다양한 형식들을 기본적으로 정의하고 있으며, 그 외의 형식이 필요하다면 직접 정의해서 사용할 수도 있습니다.
LocalDate date = LocalDate.of(2025, 1, 2);
String yyyymmdd = DateTimeFormatter.ISO_LOCAL_DATE.format(date); // "2025-01-02"
String yyyymmdd = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // "2025-01-02"
날짜와 시간의 형식화에는 위와 같이 format()이 사용되는데, 이 메서드는 DateTimeFormatter뿐만아니라 LocalDate나 LocalTime같은 클래스에도 있습니다.
로케일에 종속된 형식화
DateTimeFormatter의 static메서드 ofLocalizedDate(), ofLocalizedTime(), ofLocalizedDateTime()은 로케일(locale)에 종속적인 포맷터를 생성합니다.
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
String shortFormat = formatter.format(LocalDate.now());
FormatSytle의 종류에 따른 출력 형태는 다음과 같습니다.
| FormatStyle | 날짜 | 시간 |
| FULL | 2025년 12월 25일 수요일 | N/A |
| LONG | 2025년 12월 25일 (수) | 오후 11시 22분 33초 |
| MEDIUM | 2025.12.25 | 오후 11:22:33 |
| SHORT | 25.12.25 | 오후 11:22 |
출력형식 직접 정의
DateTimeFormatter의 ofPattern()으로 원하는 출력형식을 직접 작성할 수도 있습니다.
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
문자열을 날짜와 시간으로 파싱
문자열을 날짜 또는 시간으로 변환하려면 static메서드 parse()를 사용하면 됩니다. 날짜와 시간을 표현하는데 사용되는 클래스에는 이 메서드가 거의 다 포함되어 있습니다. parse()는 오버로딩된 메서드가 여러 개 있는데, 그중에서 다음의 2개가 자주 사용됩니다.
static LocalDateTime parse(CharSequence text)
static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)
DateTimeFormatter에 상수로 정의된 형식을 사용할 때는 다음과 같이 합니다.
LocalDate date = LocalDate.parse("2025-01-02", DateTimeFormatter.ISO_LOCAL_DATE);'Lang > Java' 카테고리의 다른 글
| [Java 21] (12) - modern Java features (0) | 2025.11.05 |
|---|---|
| [Java 21] (11) - collections framework (0) | 2025.11.03 |
| [Java 21] (9) - java.lang package & util classes (0) | 2025.10.24 |
| [Java 21] (8) - exception handling (0) | 2025.10.20 |
| [Java 21] (7) - Object-oriented Programming 2 (0) | 2025.10.03 |