[자바의 정석] Chapter 10 날짜와 시간 & 형식화
1. 날짜와 시간
1.1 Calendar와 Date
Date: 날짜와 시간을 다룰 목적으로 JDK 1.0부터 제공된 클래스.
Calendar: Date클래스를 보완하기 위해 JDK 1.1부터 제공된 클래스.
Calendar와 GregorianCalendar
Calendar는 추상클래스이기 때문에 직접 객체를 생성할 수 없고 메서드를 통해서 완전히 구현된 클래스의 인스턴스를 얻어야 한다.
Calendar cal = new Calendar(); // 에러
Calendar cal = Calendar.getInstance(); // 이게 맏다.
getInstance()메서드는 Calendar클래스를 구현한 클래스의 인스턴스를 반환한다. Calendar를 상속받아 완전히 구현한 클래스로는 GregorianCalendar, Buddhist Calendar가 있다. getInstance()는 해당 시스템을 사용하는 국가와 지역설정을 확인해서 태국이라면 Buddhist Calendar의 인스턴스를, 그 외에는 GregorianCalendar의 인스턴스를 반환한다.
인스턴스를 직접 생성해서 사용하지 않고 getInstance()를 통해서 반환받는 이유는 최소한의 변경으로 프로그램이 동작하도록 하기 위함이다. 만약 특정 인스턴스를 생성하도록 프로그램이 작성되어 있다면 [다른 종류의 역법(calendar)을 사용하는 국가에서 실행하거나 새로운 역법이 추가된다면] 즉, 다른 종류의 인스턴스를 필요로 하는 경우 해당 프로그램을 변경해야 한다. 하지만 getInstance()를 이용해 인스턴스를 얻어오도록 하면 해당 프로그램을 변경하지 않아도 된다. getInstance()의 내용이 달라질 수 있지만 프로그램을 변경하지 않아도 된다는 것이 중요하다.
getInstance()가 static인 이유
- 메서드 내의 코드에서 인스턴스 변수를 사용하거나 인스턴스 메서드를 호출하지 않기 때문.
- getInstance()가 static이 아니라면 객체를 생성한 다음에 호출해야 하는데 Calendar는 추상클래스라서 객체를 생성할 수 없기 때문.
Date와 Calendar간의 변환
Calendar가 새로 추가되면서 Date의 대부분의 메서드가 deprecated*되어 잘 사용되지 않는다. 그럼에도 여전히 Date를 필요로 하는 메서드들이 있기 때문에 Calendar와 Date를 서로서로 변환할 일이 생긴다.
*deprecated: 중요도가 떨어져 더 이상 사용되지 않고 사라지게 될(컴퓨터공학). Java API문서를 보면 더 이상 사용을 권장하지 않는 대상에 deprecated가 붙어있다.
1. Calendar를 Date로 변환
Calendar cal = Calendar.getInstance(); Date date = new Date(cal.getTimeInMillis()); // Date(long date)
2. Date를 Calendar로 변환
Date date = new Date(); Calendar cal = Calendar.getInstance(); cal.setTime(date);
int get(int field) 메서드를 이용해 원하는 필드의 값을 얻어올 수 있다. get메서드의 매개변수로 사용되는 int값들은 Calendar에 정의된 static 상수이다.
Calendar today = Calendar.getInstance();
// 오늘 날짜: 2023/04/05
int yearOfToday = today.get(Calendar.YEAR); // yearOfToday = 2023
int monthOfToday = today.get(Calendar.MONTH); // monthOfToday = 3(0이 1월임)
int dayOfToday = today.get(Calendar.DATE); // dayOfToday = 5
int dayOfToday2 = today.get(Calendar.DAY_OF_MONTH); // 위와 같음
이런 식으로 사용할 수 있다. 실제로 훨씬 더 많은 필드들이 정의되어 있으니 Java API를 참고하면 된다. 한가지 주의할 점은 상수 MONTH는 값의 범위가 0부터 11이라서 위 예제처럼 값을 1 적게 출력한다.
만약 원하는 날짜나 시간으로 설정하려면 set()메서드를 사용해야 한다.
Calendar date1 = Calendar.getInstance();
Calendar today = date1.set(2023, 3, 30); // 2023/4/30
Calendar today2 = date1.set(2023. Calendar.APRIL, 30); // 2023/4/30
두 날짜간의 차이를 얻으려면 getTimeInMills()로 1/1000초 단위로 변환한 후 두 시간의 차를 구해 단위를 바꾸면 된다.
long difference = (date2.getTimeInMillis() - date1.getTimeInMillis()) / 1000; // 초단위로 계산
int dayDifference = difference / (24 * 60 * 60); // 일단위로 계산
시간 상의 전후를 알고 싶을 때에는 두 날짜간의 차이가 양수인지 음수인지를 판단하면 되고, 앞뒤 생각하지 않고 차이를 알고 싶다면 Math.abs()를 이용하믄 된다.
add(int field, int amount)를 사용하면 지정한 필드의 값을 원하는 만큼 증가(amount값을 양수로) 또는 감소(amount값을 음수로)시켜 특정 날짜(시간)를 기점으로 일저이각 전후의 날짜(시간)을 알 수 있다. field값으로 Calendar의 상수필드를 사용하면 된다.
roll(int field, int amount)도 지정한 필드의 값을 증가 또는 감소시킬 수 있는데 add()와의 차이점은 다른 필드에 영향을 미치지 않는다는 것이다. 예를 들어, add메서드로 일자 필드(Calendar.DATE)의 값을 31만큼 증가시켰다면 다음 달로 넘어가 월 필드(Calendar.MONTH)의 값도 1 증가하지만, roll()은 월 필드의 값은 변하지 않고 일자의 값만 바뀐다.
단 한가지 예외가 있는데 일자필드가 말일일 경우 roll()을 이용해 월 필드를 변경하면 일자 필드에 영향을 미칠 수 있다.
문자열을 parseInt를 이용해 int값으로 바꿔서 이용할 수 있다.
int year = Integer.parseInt("2021");
int month = Integer.parseInt("03");
Calendar cal = Calendar.getInstance();
Calendar today = cal.set(year, month, 3); // today = 2023/03/3 이 됨
2. 형식화 클래스
성적처리 프로그램을 작성할 대 각 점수의 평균을 소수점 2자리로 맞춰서 출력하거나 날짜를 형식에 맞게 출력할 때 매번 복잡한 연산을 작성하는 과정을 거쳐야 하는데 이런 번거로움을 해결하는 방법이 형식화 클래스이다. 이 클래스는 java.text패키지에 포함되어 있고 숫자, 날짜, 텍스트 데이터를 일정한 형식에 맞게 표현할 수 있는 방법을 표준화하여 제공한다. 데이터의 패턴만 정의해주면 복잡한 문자열에서도 쉽게 원하는 값을 얻어낼 수 있다.
2.1 DecimalFormat (숫자를 형식화)
형식화 클래스들 중에서 숫자를 형식화하는데 사용되는 클래스이다. DecimalFormat을 이용하면 숫자 데이터를 정수, 부동소수점, 금액 등 다양한 형식으로 표현할 수 있고 텍스트 데이터를 숫자로 변환하기도 쉽다. 형식화 클래스에서는 원하는 형식으로 표현 또는 변환하기 위해 패턴을 정의하는데 이 패턴을 정의하는 것이 전부라고 해도 과언이 아니다. 아래는 패턴을 정의하는데 사용되는 기호와 그 예시이다.
기호 | 의미 | 패턴 정의 예시 | 예시 - 1234567.89 |
0 | 10진수(값이 없을 때는 0으로) | 0 0.0 0000000000.0000 |
1234568 1234567.9 0001234567.8900 |
# | 10진수 | # #.# ##########.#### |
1234568 1234567.9 1234567.89 |
. | 소수점 | #.# | 123457.9 |
- | 음수부호 | #.#- -#.# |
1234567.9- -1234567.9 |
, | 단위 구분자 | #,###.# #,####.# |
1,234,567.9 123,4567.9 |
E | 지수기호 | #E0 0E0 ##E0 00E0 ####E0 0000E0 #.#E0 0.0E0 0.0000000000E0 00.000000000E0 000.00000000E0 #.#########E0 ##.########E0 ###.#######E0 |
.1E7 1E6 1.2E6 12E5 123.5E4 1235E3 1.2E6 1.2E6 1.234567890E6 12.34567890E5 123.4567890E4 1.23456789E6 1.23456789E6 1.23456789E6 |
; | 패턴구분자 | #,###.##+;#,###.##- | 1,234,567.89+ 1,234,567.89- |
% | 퍼센트 | #.#% | 123456789% |
\u2030 | 퍼밀(퍼센트 x 10) | #.#\u2030 | 123456789‰ |
\u00A4 | 통화 | \u00A4 #,### | ₩ 1,234,568 |
' | escape문자 | '#'#,### ''#,### |
#1,234,568 '1,234,568 |
DecimalFormat을 사용하는 방법
- 원하는 출력형식의 패턴을 작성하여 Decimal인스턴스를 생성한다
- 출력하고자 하는 문자열로 format메서드를 호출한다.
- 원하는 패턴에 맞게 변환된 문자열을 얻게 된다.
double number = 1234567.89;
DecimalFormat df = new DecimalFormat("#.#E0");
String result = df.format(number); // result = 1.2E6
Integer.parseInt에서는 기호나 공백이 포함된 문자열을 숫자로 변환하지 못하는데 DecimalFormat의 parse메서드는 기호와 문자가 포함된 문자열을 숫자로 쉽게 변환할 수 있다.
2.2 SimpleDateFormat (날짜를 형식화)
Date와 Calendar로 날짜를 계산하는 방법을 배웠고 이 데이터들을 원하는 형태로 출력할 때에는 SimpleDateFormat을 사용한다. Date와 Calendar를 이용해 날짜 데이터를 다양한 형태로 출력하는 것은 복잡하기 때문이다.
SimpleDateFormat 역시 패턴을 정의해 사용한다. 아래는 패턴을 정의하는데 사용되는 기호와 그 예시이다.
기호 | 의미 | 출력 형태 |
G | 기원(AD, BC) | AD |
y | 연도 | 2006 |
M | 월(1~12 또는 1월~12월) | 10 또는 10월, OCT |
w | 연의 주수 | 52 |
W | 월의 주수 | 2 |
D | 연의 일수(1~366) | 100 |
d | 월의 일수(1~31) | 21 |
F | 월의 요일수(1~5) | 3 |
E | 요일 | 월 |
a | 오전/오후(AM, PM) | AM |
H | 시간(0~23) | 0 |
h | 시간(1~12) | 12 |
k | 시간(1~24) | 24 |
K | 시간(0~11) | 0 |
m | 분(0~59) | 32 |
s | 초(0~59) | 32 |
S | 천분의 1초(0~999) | 234 |
z | Time zone(General time zone) | GMT +09:00 |
Z | Time zone(RFC 822 zone) | +0900 |
' | escape문자(특수문자를 표현하는데 사용) | 없음 |
SimpleDateFormat의 사용방법
- 원하는 출력형식의 패턴을 작성해 SimpleDateFormat인스턴스 생성
- 출력하고자 하는 Date인스턴스를 가지고 format(Date d)를 호출한다
- 그럼 지정한 출력형식에 맞게 변환된 문자열을 얻게 된다
Date today = new Date();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String result = df.format(today);
SimpleDateFormat의 parse(String source)는 문자열 source를 Date인스턴스로 변환해주기 때문에 매우 유용하다.
DateFormat df = new SimpleDateFormat("yyyy/MM/dd");
Date inDate = df.parse("2013/12/21"); // inDate에 2013년 12월 21일 저장
2.3 ChoiceFormat (범위값 변환)
ChoiceFormat은 특정 범위에 속하는 값을 문자열로 변환해준다. 연속적 또는 불연속적인 범위의 값들을 처리할 때 if문이나 switch문보다 간단하고 직관적으로 만들 수 있다.
double[] limits = { 60, 70, 80, 90 };
String[] grades = { "D", "C", "B", "A" };
int[] scores = { 100, 95, 88, 70, 52, 60, 70 };
ChoiceFormat cf = new ChoiceFormat(limits, grades);
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i] + ": " + cf.format(scores[i]);
}
/*결과*
* 100: A
* 95: A
* 88: B
* 70: C
* 52: D
* 60: D
* 70: C
*/
limits는 범위의 경계값을 저장하는데 사용되었고, grades는 범위에 포함된 값을 치환할 문자열을 저장하는 배열이다. 경계값은 double 타입으로 반드시 오름차순 정렬되어야 하며 치환될 문자열의 개수는 경계값 범위의 개수와 일치해야 한다(limits.length == grades.length). 그렇지 않으면 IllegalArgumentException이 발생한다.
String pattern = "60#D|70#C|80#B|90#A";
int[] scores = { 91, 90, 80, 88, 70, 52, 60 };
ChoiceFormat cf = new ChoiceFormat(pattern);
for (int i = 0; i < scores.length; i++) {
System.out.println(scores[i] + ": " + cf.format(scores[i]));
}
/*결과*
* 91: A
* 90: A
* 80: C
* 88: B
* 70: C
* 52: B
* 60: B
*/
배열대신 패턴을 사용할 수 있다. 패턴은 구분자로 '#'과 '<' 두 가지를 제공하는데 'limit#value'의 형태로 사용된다. '#'은 경계값을 범위에 포함시키지만 '<'는 포함시키지 않는다. 위 결과에서 90은 A지만, 80은 B가 아닌 C인 것에 주목하자.
2.4 MessageFormat (양식에 맞게 출력)
MessageFormat은 데이터를 정해진 양식에 맞게 출력하는 클래스. 데이터가 들어갈 자리를 마련해놓은 양식을 미리 작성하고 프로그램을 이요해서 다수의 데이터를 같은 양식으로 출력할 때 사용하면 좋다. MessageFormat 역시 parse를 이용하면 지정된 양식에서 필요한 데이터만을 손쉽게 추출할 수 있다.
String msg = "Name: {0} \nTel: {1} \nAge: {2} \nBirthday: {3}";
Object[] arguments = {
"이자바", "02-123-1234", "23", "03/31"
};
String result = MessageFormat.format(msg, arguments);
/*결과*
* Name: 이자바
* Tel: 02-123-1234
* Age: 23
* Birthday: 03/31
*/
양식은 문자열(msg)을 정의할 때 {숫자}로 입력한 부분이 데이터가 출력될 자리이다. 이 자리는 순차적일 필요는 없고 여러 번 반복해서 사용할 수도 있다. 여기에 사용되는 숫자는 배열처럼 인덱스가 0부터 시작하며 양식에 들어갈 데이터는 객체배열인 arguments에 지정되어 있음을 알 수 있다.
3. java.time패키지
Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 JDK1.8부터 java.time패키지가 추가되었다. 이 패키지는 4개의 하위 패키지를 가지고 있다.
패키지 | 설명 |
java.time | 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공 |
java.time.chrono | 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공 |
java.time.format | 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공 |
java.time.temporal | 날짜와 시간의 필드(field)와 단위(unit)를 위한 클래스들을 제공 |
java.time.zon | 시간대(time zone)와 관련된 클래스들을 제공 |
위 패키지에 속한 클래스들의 가장 큰 특징은 String클래스처럼 불변이라는 것이다. 그래서 날짜나 시간을 변경하는 메서드들은 기존의 객체를 변경하는 대신 항상 변경된 새로운 객체를 반환한다. 기존 Calendar클래스는 변경 가능하므로, 멀티 쓰레드 환경에서 안전하지 못하다. 멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 객체에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있으며 이를 쓰레드에 안전(thread-safe)하지 않다고 한다.
3.1 java.time패키지의 핵심 클래스
날짜와 시간을 하나로 표현하는 Calendar클래스와 달리 java.time패키지는 날짜와 시간을 별도로 분리했다. 시간을 표현할 때에는 LocalTime, 날짜를 표현할 때에는 LocalDate, 날짜와 시간을 모두 표현할 때에는 LocalDateTime클래스를 사용한다. LocalDateTime에 시간대까지 다뤄야 한다면 ZonedDateTime을 사용한다. Calendar는 날짜와 시간 그리고 시간대까지 모두 가지고 있다.
Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 나노초(1/10억) 단위로 표현한다. 날짜(시간)을 초단위로 표현한 값을 타임스탬프라고 부르는데, 이 값은 날짜(시간)을 하나의 정수로 표현할 수 있으므로 날짜(시간)의 차이를 계산하거나 순서를 비교하는데 유리해 데이터베이스에 많이 사용된다. 이 외에 날짜를 세부적으로 다루기 위한 Year, YearMonth, MonthDay 등이 있다.
Period와 Duration
날짜(시간)의 간격을 표현하기 위한 클래스로, Period는 두 날짜간의 차이를 표현하고 Duration은 시간의 차이를 표현한다.
객체 생성하기 - now(), of()
java.time패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 now()와 of()를 사용하는 것이다.
- now(): 현재 날짜(시간)을 저장하는 객체를 생성
- of(): 해당 필드의 값을 순서대로 지정한다. 각 클래스마다 다양한 종류의 of()가 정의되어 있다.
// now
LocalDate nowDate = LocalDate.now(); // nowDate = 2023-04-05
LocalTime nowTime = LocalTime.now(); // nowTime = 16:26:42.523
LocalDateTime nowDateTime = LocalDateTime.now(); // nowDateTime = 2023-04-05T16:26:53.312
ZonedDateTime nowZonedDateTime = ZonedDateTime.now();
// nowZonedDateTime = 2023-04-05T16:26:54.234+09:00[Asia/Seoul]
// of
LocalDate date = LocalDate.of(2001, 03, 12); // date = 2001-03-12
LocalTime time = LocalTime.of(23, 42, 12); // time = 23:42:12
LocalDateTime dateTime = LocalDateTime.of(date, time);
ZonedDateTime zonedDateTime = ZonedDateTime.of(dateTime, ZonedId.of("Asia/Seoul"));
Temporal과 TemporalAmount
LocalDate, LocalTime, LocalDateTime, ZonedDateTime 등 날짜(시간)을 표현하기 위한 클래스들은 모두 Temporal, TemporalAccessor, TemporalAdjuster인터페이스를 구현한다.
Duration과 Period는 TemporalAmount인터페이스를 구현하였다.
앞으로 소개할 메서드 중 매개변수의 타입이 Temporal로 시작하는 것들이 자주 등장할텐데 대부분 날짜와 시간을 위한 것이므로 TemporalAmount인지 아닌지만 확인하면 된다.
TemporalUnit과 TemporalField
TemporalUnit: 날짜와 시간의 단위를 정의한 인터페이스, 이 인터페이스를 구현한 클래스가 열거형 ChronoUnit
TemporalField: 날짜와 시간의 필드를 정의한 인터페이스, 이 인터페이스를 구현한 클래스가 열거형 ChronoField
※ temporal과 chrono 모두 시간을 의미하는데 time이 아닌 이들을 이용하는 이유는 time의 범위는 시분초 정도이나 이들의 범위는 년월일시분초 정도의 큰 개념을 의미하기 때문이다.
날짜(시간)에서 특정 필드의 값만을 얻을 때는 get()이나 get으로 시작하는 이름의 메서드를 이용한다.
int get(TemporalField field)
LocalTime now = LocalTime.now(); // 현재시간
int minute = now.getMinute(); // 현재 시간에서 분만 뽑는다.
int minute = now.get(ChronoField.MINUTE_OF_HOUR); // 위 문장과 동일
특정 날짜와 시간에서 지정된 단위의 값을 더하거나 뺄 때는 plus() 또는 minus()에 값과 함께 열거형 ChronoUnit을 사용한다.
LocalDate plus(long amountToAdd, TemporalUnit unit)
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS); // 오늘 날짜에 1(1)일(ChronoUnit.DAYS) 더한다(plus())
= today.plusDays(1);
3.2 LocalDate와 LocalTime
두 클래스는 java.time패키지의 가장 기본이 되는 클래스이다. 객체를 생성하는 방법은 현재의 날짜와 시간을 LocalDate와 LocalTime으로 각각 반환하는 now()와 지정된 날짜와 시간으로 LocalDate(LocalTime)의 객체를 생성하는 of()가 있다. 둘 다 static메서드이다. of()는 매개변수의 개수에 따라 여러 종류로 제공된다.
날짜를 일 단위나 초 단위로 지정할 수 있다.
LocalDate birthDate = LocalDate.ofYearDay(1999, 365); // 1999년의 365번째 날짜 지정(12/31)
LocalTime birthTime = LocalTime.ofSecondDay(86399); // 00:00:00부터 83699초 이후(23:59:59)
또한 parse()를 이용해 문자열을 날짜와 시간으로 변환할 수 있다.
특정 필의 값 가져오기 - get(), get필드명()
LocalDate(LocalTime)의 객체에서 특정 필드의 값을 가져올 때는 아래의 표에 있는 메서드를 사용한다.
클래스 | 메서드 | 사용예(1999-12-31 23:59:59) |
LocalDate | int getYear() | 1999(연) |
int getMonthValue() | 12(월) | |
Month getMonth() | DECEMBER(월) | |
int getDayOfMonth() | 31(일) | |
int getDayOfYear() | 365(1999/1/1부터 d+) | |
DayOfWeek getDayOfWeek() | FRIDAY(요일) | |
int lengthOfMonth() | 31(12월의 총 일수 = 말일) | |
int lengthOfYear() | 365(1999년의 총 일수, 윤년이면 366) | |
boolean isLeapYear() | false(윤년이면 true) | |
LocalTime | int getHour() | 23(시) |
int getMinute() | 59(분) | |
int getSecond() | 59(초) | |
int getNano() | 0(나노초) |
위 표의 메서드 외에도 get()과 getLong()이 있다. get(TemporalField field)은 매개변수에 원하는 필드를 직접 지정할 수 있다. 필드가 int타입일 경우 get()을 사용하고 long일 경우 getLong(TemporalField field)을 사용한다. 아래는 get()과 getLong()의 매개변수로 사용할 수 있는 필드의 목록이다. 이 목록은 ChronoField에 정의된 모든 상수를 나열한 것일 뿐, 사용할 수 있는 필드는 클래스마다 다르다.
TemporalField(ChronoField) | 설명 |
ERA | 시대 |
YEAR_OF_ERA, YEAR | 연 |
MONTH_OF_YEAR | 월 |
DAY_OF_WEEK | 요일(1이 월) |
DAY_OF_MONTH | 일 |
AMPM_OF_DAY | 오전/오후 |
HOUR_OF_DAY | 시간(0~23) |
CLOCK_HOUR_OF_DAY | 시간(1~24) |
HOUR_OF_AMPM | 시간(0~11) |
CLOCK_HOUR_OF_AMPM | 시간(1~12) |
MINUTE_OF_HOUR | 분 |
SECOND_OF_MINUTE | 초 |
MILLI_OF_SECOND | 1/1000초(밀리초) |
MICRO_OF_SECOND | 1/백만초(마이크로초) |
NANO_OF_SECOND | 1/10억초(나노초) |
DAY_OF_YEAR | 1/1부터 d+ |
EPOCH_DAY | epoch(1970/1/1) 이후 d+ |
MINUTE_OF_DAY | 그 날의 몇 번째 분(시간을 분으로) |
SECOND_OF_DAY | 그 날의 몇 번째 초(시간을 초로) |
MILLI_OF_DAY | 그 날의 몇 번째 밀리초 |
MICRO_OF_DAY | 그 날의 몇 번째 마이크로초 |
NANO_OF_DAY | 그 날의 몇 번째 나노초 |
ALIGNED_WEEK_OF_MONTH | 그 날의 n번째 주(1~7일 1주, 8~14일 2주, ...) |
ALIGNED_WEEK_OF_YEAR | 그 해의 n번째 주 |
ALIGNED_DAY_OF_WEEK_IN_MONTH | 요일(그 달의 1일을 월요일로 간주) |
ALIGNED_DAY_OF_WEEK_IN_YEAR | 요일(그 해의 1월 1일을 월요일로 간주) |
INSTANT_SECONDS | 연월일을 초단위로 환산(1970/1/1 00:00:00UTC가 0초), Instant에만 사용가능 |
OFFSET_SECONDS | UTC와의 시차, ZoneOffset에만 사용가능 |
PROLEPTIC_MONTH | 연월을 월단위로 환산(ex. 2015년 11월 = 2015 * 12 + 11) |
특정 필드가 가질 수 있는 값의 범위를 알고 싶다면 range() 메서드를 사용한다.
System.out.println(ChronoField.CLOCK_HOUR_OF_DAY.range()); // 1-24
System.out.println(ChronoField.HOUR_OF_DAY.range()); // 0-23
필드의 값 변경하기 - with(), plus(), minus()
날짜(시간)에서 특정 필드 값을 변경하려면, 다음과 같이 with(필드)나 with로 시작하는 메서드를 사용하면 된다.
LocalDate with(TemporalField field, long newValue)
LocalDate withYear(int year)
LocalDate withMonth(int month)
LocalDate withDayOfMonth(int dayOfMonth)
LocalDate withDayOfYear(int dayOfYear)
LocalTime with(TemporalField field, long newValue)
LocalTime withHour(int hour)
LocalTime withMinute(int minute)
LocalTime withSecond(int second)
LocalTime withNano(int nanoOfSecond)
필드를 변경하는 메서드들은 항상 새로운 객체를 생성해 반환하므로 대입연산자를 같이 사용해야 한다는 것을 잊으면 안된다.
이 외에도 특정 필드에 값을 더하거나 빼는 plus()와 plus로 시작하는 메서드, minus()와 minus로 시작하는 메서드가 있다.
그리고 LocalTime의 truncatedTo(TemporalUnit unit)는 지정된 것보다 작은 단위의 필드를 0으로 만든다.
LocalTime time = LocalTime.of(23, 49, 12); // 23시 49분 12초
time = time.truncatedTo(ChronoUnit.HOURS); // 시(hour)보다 작은 단위를 0으로
System.out.println(time); // 23:00
LocalDate에는 truncatedTo()가 없는데 필드인 연, 월, 일은 0이 될 수 없기 때문이다.
아래 표는 truncatedTo()의 매개변수로 받을 수 있는 값들이다.
매개변수 필드값 | 설명 |
FOREVER | Long.MAX_VALUE초(약 3천억년) |
ERAS | 10억년 |
MILLENNIA | 천년 |
CENTURIES | 백년 |
DECADES | 10년 |
YEARS | 1년 |
MONTHS | 월 |
WEEKS | 주 |
DAYS | 일 |
HALF_DAYS | 반나절 |
HOURS | 시 |
MINUTES | 분 |
SECONDS | 초 |
MILLLS | 밀리초 |
MICROS | 마이크로초 |
NANOS | 나노초 |
날짜와 시간의 비교 - isAfter(), isBefore(), isEqual()
LocalTime(LocalDate)도 compareTo()가 적절히 오버라이딩 되어 있어서 아래와 같이 비교할 수 있다.
int result = date1.compareTo(date2);
/*결과*
* 같으면 result = 0
* date1이 이전이면 result = -1
* date1이 이후면 result = 1
*/
compareTo()가 있어도 더 편리하게 비교할 수 있는 메서드들이 추가로 제공된다.
boolean isAfter(ChronoLocalDate other)
boolean isAfter(ChronoLocalDate other)
boolean isEqual(ChronoLocalDate other) // LocalDate에만 있음
equals()가 있는데도 isEqual()을 제공하는 이유는 연표가 다른 두 날짜를 비교하기 위해서이다. 모든 필드가 일치해야하는 equals()와는 달리 isEqual()은 오직 날짜만 비교한다.
3.3 Instant
Instant는 에포크 타임(EPOCH TIME, 1970-01-01 00:00:00 UTC)부터 경과된 시간을 나노초 단위로 표현한다. 사람에겐 불편하지만, 단일 진법으로만 다루기 때문에 계산을 하기 쉽다. 사람이 사용하는 날짜와 시간은 여러 진법이 섞여 있어서 계산하기 어렵다.
Instant를 생성할 대는 now()와 ofEpochSecond()를 사용한다. 그리고 필드에 저장된 값을 가져올 때는 get으로 시작하는 메서드를 이용한다.
// Instant 생성
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();
Instant는 시간을 초 단위와 나노초 단위로 나누어 저장한다. 밀리초 단위의 EPOCH TIME을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있다.
Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있다. UTC는 Coordinated Universal Time의 약어로 세계 협정시라고 하며, 1972/01/01부터 시행된 국제 표준시이다. 이전에 사용되던 GMT(Greenwich Mean Time)와 UTC는 거의 같지만 UTC가 좀 더 정확하다.
Instant와 Date간의 변환
Instant는 기존의 Date클래스를 대체하기 위한 것으로 JDK1.8부터 Date에 Instant로 변환할 수 있는 새로운 메서드가 추가되었다.
static Date from(Instant instant) // Instant -> Date
Instant toInstant() // Date -> Instant
3.4 LocalDateTime과 ZonedDateTime
LocalDate + LocalTime = LocalDateTime
LocalDateTime + 시간대 = ZonedDateTime
LocalDate와 LocalTime으로 LocalDateTime 만들기
아래는 LocalDate와 LocalTime을 합쳐서 LocalDateTime을 만들 수 있는 대표적인 방법들이다.
LocalDate date = LocalDate.of(2021, 12, 14);
LocalTime time = LocalTime.of(12, 31, 55);
LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt2 = date.atTime(time);
LocalDateTime dt3 = time.atDate(date);
LocalDateTime dt4 = date.atTime(12, 59, 12);
LocalDateTime dt5 = time.atDate(LocalDAte.of(2001, 03, 11);
LocalDateTime dt6 = date.atStartOfDay() // = date.atTime(0, 0, 0);
LocalDateTime에도 다향한 버전의 of()와 now()가 정의되어 있다.
LocalDateTime의 변환
LocalDateTime을 LocalDate(LocalTime)으로 바꿀 수 있다.
LocalDateTime ldt = LocalDateTime.of(2015, 12, 31, 23, 44, 52);
LocalDate ld = ldt.toLocalDate(); // ld = 2015/12/31
LocalTime lt = ldt.toLocalTime(); // lt = 23:44:52
LocalDateTime으로 ZonedDateTime만들기
LocalDate에 시간 정보를 추가하는 atTime()을 쓰면 LocalDateTime을 얻을 수 있는 것처럼, LocalDateTime에 atZone()으로 시간대 정보를 추가하면 ZonedDateTime을 얻을 수 있다. 그리고 LocalDate의 atStartOfDay()메서드에 매개변수로 ZoneId를 지정해도 ZonedDateTime을 얻을 수 있다.
ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = LocalDateTime.of(2015, 8, 14, 15, 31, 33).atZone(zid);
ZonedDateTime zdt2 = LocalDate.now().atStartOfDay(zid);
/*결과*
* zdt = 2015-08-14T15:31:33+09:00[Asia/Seoul]
* zdt2 = 2023-04-06T00:00+09:00[Asia/Seoul]
*/
만약 현재 다른 나라의 시간 예를 들어 뉴욕을 알고 싶다면 다음처럼 한다.
ZoneId nyID = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
ZoneOffSet
UTC로부터 얼마나 떨어져 있는지를 ZoneOffSet으로 표현한다. 서울은 +9이므로 ZoneOffset.of("+9")처럼 표현한다. 매개변수의 문자열의 형태로 ±h, ±hh, ±hhmm, ±hh:mm 등으로 표현할 수 있다.
OffsetDateTime
ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffsetDateTime이다. ZoneId는 일광절약시간(DST)처럼 시간대와 관련된 규칙들을 포함하지만 ZoneOffset은 시간대를 시간의 차이로만 구분한다. 같은 지역 내의 컴퓨터간에 데이터를 주고받을 때, 전송시간을 표현하기에 LocalDateTime이면 충분하겠지만, 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요하다.
OffsetDateTime은 LocalDate와 LocalTime에 ZoneOffset을 더하거나, ZonedDataTime에 toOffsetDataTime()을 호출해서 얻을 수 있다.
ZonedDateTime의 변환
ZonedDateTime은 날짜와 시간에 관련된 다른 클래스로 변환하는 메서드들을 가지고 있다.
LocalDate toLocalDate()
LocalTime toLocalTime()
LocalDateTime toLocalDateTime()
OffsetDateTime toOffsetDateTime()
long toEpochSecond()
Instant toInstant()
GregorianCalendar와 가장 유사한 것이 ZonedDateTime이다. GregorianCalendar와 ZonedDateTime간의 변환방법만 알면 위의 메서드를 이용해 다른 날짜와 시간 클래스들로 변환할 수 있다.
GregorianCalendar from(ZonedDateTime zdt) // ZonedDateTime -> GregorianCalendar
ZonedDateTime toZonedDateTime() // GregorianCalendar -> ZonedDateTime
3.5 TemporalAdjusters
앞서 plus(), minus()와 같은 메서드로 날짜(시간)을 계산할 수 있음을 알았다. 그러나 지난 주 토요일이 며칠인지, 이번 달 세번재 금요일은 며칠인지를 plus, minus로 계산하기는 쉽지 않다. 그래서 자주 쓰일만한 날짜 계산을 해주는 메서드를 정의한 클래스가 TemporalAdjusters이다.
TemporalAdjusters의 메서드 | 설명 |
firstDayOfNextYear() | 다음 해의 첫 날 |
firstDayOfNextMonth() | 다음 달의 첫 날 |
firstDayOfYear() | 올 해의 첫 날 |
firstDayOfMonth() | 이번 달의 첫 날 |
lastDayOfYear() | 올 해의 마지막 날 |
lastDayOfMonth() | 이번 달의 마지막 날 |
firstInMonth(DayOfWeek dayOfWeek) | 이번 달의 첫번째 n요일 |
lastInMonth(DayOfWeek dayOfWeek) | 이번 달의 마지막 n요일 |
previous(DayOfWeek dayOfWeek) | 지난 n요일(당일 미포함) |
previousOrSame(DayOfWeek dayOfWeek) | 지난 n요일(당일 포함) |
next(DayOfWeek dayOfWeek) | 다음 n요일(당일 미포함) |
nextOrSame(DayOfWeek dayOfWeek) | 다음 n요일(당일 포함) |
dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) | 이번 달의 n번째 요일 |
TemporalAdjuster 직접 구현하기
보통은 TemporalAdjusters에 정의된 메서드로 충분하겠지만 with메서드에 TemporalAdjuster인터페이스를 구현한 클래스의 객체를 매개변수로 입력해 사용할 수 있다. with메서드는 LocalTime, LocalDateTime, ZonedDateTime, Instant 등 대부분의 날짜, 시간과 관련된 클래스에 포함되어 있다.
TemporalAdjuster인터페이스는 adjustInfo(Temporal temporal)라는 추상 메서드만 정의되어 있으므로 이 메서드만 구현하면 된다. 실제로 구현해야 하는 것은 adjustInfo()지만 TemporalAdjuster과 사용해야 하는 메서드는 with()이다. with()와 adjustInfo() 둘 다 사용 가능하지만 후자는 내부적으로만 사용하려고 작성된 것이기 때문에 with() 사용을 권장한다. 앞서 언급한 것과 같이 날짜(시간)에 관련된 대부분의 클래스는 Temporal인터페이스를 구현하였으므로 adjustInfo()의 매개변수가 될 수 있다.
3.6 Period와 Duration
Period는 날짜의 차이를, Duration은 시간의 차이를 계산하기 위한 클래스이다.
between()
두 날짜 date1과 date2의 차이를 나타내는 Period는 between()으로 얻을 수 있다. 시간을 계산한다는 부분에서 Duration도 마찬가지이다.
LocalDate date1 = LocalDate.of(2023, 3, 14);
LocalDate date2 = LocalDate.of(2023, 3, 16);
Period period = Period.between(date1, date2);
// date1이 date2보다 날짜 상으로 이전이면 양수, 이후면 음수로 저장된다.
Period(Duration)에서 특정 필드의 값을 얻을 때는 get() 또는 get으로 시작하는 메서드를 사용한다.
각 클래스의 인스턴스에 getUnits()메서드를 호출하면 get()에 사용할 수 있는 ChronoUnit의 종류를 확인할 수 있는데 Period는 [Years, Months, Days]가, Duration에는 [Seconds, Nanos]가 반환된다. Period는 날짜와 관련된 단위를 모두 사용할 수 있으나 Duration에는 Chrono.SECONDS와 Chrono.NANOS만 사용할 수 있다. 만약 Duration으로 시분초 단위까지 사용하고 싶다면 Duration을 LocalTime으로 변환한 다음에 LocalTime의 get메서드를 사용하면 더 간단한다.
between()과 until()
until()은 between()과 거의 같은 일을 하지만 between()은 static메서드이고, until()은 인스턴스 메서드이다. Period는 연월일을 분리해서 저장하기 때문에 디데이를 구하려면 두 개의 매개변수를 받는 until()을 사용하는 것이 낫다. 시간에도 until()을 사용할 수 있지만 Duration을 반환하는 until()은 없다.
of(), with()
<LocalTime(LocalDate)와 유사하므로 생략>
사칙연산, 비교연산, 기타 메서드
plus(), minus() 외에 multipliedBy()(곱셈), dividedBy()(나눗셈)도 있다. Period는 dividedBy()가 없다. 그리고 음수인지 확인하는 isNegative()와 0인지 확인하는 isZero()가 있다. 부호를 반대로 변경하는 negate(), 부호를 없애는 abs()가 있다. Period에는 abs()가 없으므로 if문을 이용해야 한다.
Period에 normalized()가 있는데 이 메서드는 월의 값이 12를 넘지 않게(1년 13개월 -> 2년 1개월) 바꿔준다. 일의 길이는 일정하지 않으므로 둔다.
다른 단위로 변환 - toTotalMonths(), toDays(), toHours(), toMinutes()
이름이 to로 시작하는 메서드들이 있는데 이들은 Period(Duration)를 다른 단위의 값으로 변환하는 데 사용된다.
클래스 | 메서드 | 설명 |
Period | long toTotalMonths() | 연월일을 월단위로 변환후 반환(일단위는 무시) |
Duration | long toDays() | 일단위로 변환해서 반환 |
long toHours() | 시간단위로 변환해서 반환 | |
long toMinutes() | 분단위로 변환해서 반환 | |
long toMillis() | 밀리초로 변환해서 반환 | |
long toNanos() | 나노초 단위로 변환해서 반환 |
LocalDate의 toEpochDay()메서드는 Epoch Day부터 날짜를 세어서 반환한다. 이 메서드를 이용하면 두 날짜간의 일수를 편리하게 계산할 수 있다. 단 두 날짜 모두 Epoch Day 이후여야 한다. LocalTime도 toSecondOfDays(), toNanoOfDays()가 있어 쉽게 시간차이를 계산할 수 있다.
3.7 파싱과 포맷
날짜와 시간을 원하는 방식으로 출력하고 해성하는 파싱하는 방법에 대해 알아보고자 한다. 형식화와 관련된 클래스는 java.time.format패키지에 있는데 이 중 DAteTimeFormatter가 핵심이다. 날짜와 시간의 형식화에는 format()이 사용되는데 이 메서드는 LocalDate와 같은 클래스에도 있다.
로케일에 종속된 형식화
DateTimeFormatter의 static메서드 ofLocalizedDate(), ofLocalizedTime(), ofLocalizedDateTime()은 로케일에 종속된 포맷터를 생성한다. 매개변수에 열거형 FormatStyle에 정의된 상수를 입력한다.
FormatStyle | 날짜 | 시간 |
FULL | 2015년 11월 28일 토요일 | N/A |
LONG | 2015년 11월 28일 (토) | 오후 9시 15분 13초 |
MEDIUM | 2015.11.28 | 오후 9:15:13 |
SHORT | 15.11.28 | 오후 9:15 |
출력형식 직접 정의하기
DateTimeFormatter의 ofPattern()으로 원하는 출력형식을 문자열로 직접 작성할 수 있다.
기호 목록은 책에 있다 ..
문자열을 날짜와 시간으로 파싱하기
문자열을 날짜(시간)으로 변환하려면 parse()를 사용하면 된다.
<하략>