날짜와 시간의 메서드는 너무 많기 때문에 적당히 훑어보고 필요할 때 찾아보면 된다. (PDF 참고)
1. 열거형 - ENUM
문자열과 타입 안정성
a. 문자열
예를 들어 String으로 선언된 회원 등급에 따라 할인 정책을 다르게 적용한다고 가정해 보자. 이때, String 타입을 사용하면 타입 안정성 부족 문제가 발생할 수 있다.
- 값의 제한 부족
- String으로 상태나 카테고리를 표현하면, 오타나 잘못된 값이 입력될 수 있다.
- 컴파일 시 오류 감지 불가
- 위에서 말한 오류를 런타임에서만 발생할 수 있어 디버깅이 어려워질 수 있다.
public class User {
private String name;
private String grade;
public User(String name, String grade) {
this.name = name;
this.grade = grade;
}
}
b. 문자열 상수
위의 방법 대신 문자열 상수를 사용하는 방식을 사용하면, 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 그대로 사용하는 것보단 안전하게 사용할 수 있다.
- 실수로 상수의 이름을 잘못 입력하더라도 컴파일 시점에 오류가 발생하기 때문에 빠르게 처리할 수 있다.
- 다른 개발자가 StringGrade 클래스에 있는 문자열 상수를 사용하지 않고 직접 다른 문자열을 사용하면 막을 수 있는 방법이 없다. 따라서 의도대로 개발하게 하려면 주석이라도 적어서 StringGrade에 있는 문자열 상수를 사용하도록 해야 한다.
public class StringGrade {
public static final String SILVER = "SILVER";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
c. 타입 안전 열거형 패턴
타입 안전 열거형 패턴(Type-Safe Enum Pattern)을 사용하면 위에서 말한 문제를 해결할 수 있다. 여기서 Enum은 Enumeration의 줄임말로, 어떤 항목을 나열하는 것을 뜻한다.
- 타입 안정성 향상
- 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 예를 들어, 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다.
- 데이터 일관성
- 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.
- 제한된 인스턴스 생성
- 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
public class ClassGrade {
public static final ClassGrade SILVER = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
private ClassGrade() {} // 인스턴스 생성 방지
}
열거형 - Enum Type
자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.
- Enumeration은 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.
- 열거형도 클래스지만, 정의할 때는 class 대신 enum을 사용한다. 안에 원하는 상수의 이름을 나열하면 된다.
- 자동으로 java.lang.Enum을 상속받으며, 외부에서 임의로 생성할 수 없다.
- 열거형은 toString()을 오버라이딩하기 때문에 참조값을 직접 확인할 수 없다. 참조값을 구하고 싶다면 이전 섹션을 참고하자.
public enum Grade {
SILVER, GOLD, DIAMOND
}
열거형(ENUM)의 장점은 다음과 같다.
- 타입 안정성 향상
- 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 발생하는 경우엔 컴파일 오류가 발생한다.
- 간결성 및 일관성
- 코드가 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성
- 새로운 값을 추가하고 싶을 때, ENUM에 ,(콤마, comma)를 사용해 추가하면 된다.
참고
열거형은 switch문에서 사용할 수 있다.
참고
열거형을 사용하는 경우, static import를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
주요 메서드
모든 열거형은 java.lang.Enum 클래스를 자동으로 상속받는다. 따라서 해당 클래스가 제공하는 기능을 사용할 수 있다.
- values()
- 모든 ENUM 상수를 포함하는 배열 반환
- valueOf(String name)
- 주어진 이름과 일치하는 ENUM 상수 반환
- name()
- ENUM 상수의 이름을 문자열로 반환
- ordinal()
- ENUM 상수의 선언 순서(0부터 시작) 반환
- toString()
- ENUM 상수의 이름을 문자열로 반환 (직접 오버라이드 가능)
public class EnumMethodMain {
public static void main(String[] args) {
// 모든 ENUM 반환
Grade[] values = Grade.values();
System.out.println("values = " + Arrays.toString(values));
for (Grade value : values) {
System.out.println("name = " + value.name() +
", ordinal = " + value.ordinal());
}
// String -> ENUM 변환
// 잘못된 문자면 IllegalArgumentException 발생
String input = "GOLD";
Grade gold = Grade.ValueOf(input);
System.out.println("gold = " + gold); // toString() 오버라이딩 가능
}
}
참고
Arrays.toString()은 배열의 참조값이 아니라 배열 내부의 값을 출력할 때 사용한다.
참고
ordinal()은 가급적 사용하지 말자. 값이 추가되거나 빠지면서 오류가 발생할 수 있다.
예시 - 리팩토링
위에 간단하게 적은 예제 코드를 리팩터링해 본다.
- discountPercent 필드를 추가하고, 생성자를 통해 필드에 값을 저장한다.
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로는 생성할 수 없다. 따라서 생성자에 접근 제어자를 선언할 수 없게 막혀 있다. 기본값은 private라고 생각하면 된다.
- SILVER(10)처럼 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
- 열거형도 클래스이므로 메서드를 추가할 수 있다.
public enum Grade {
SILVER(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
public int discount(int price) {
return price * discountPercent / 100;
}
}
2. 날짜와 시간
자바 날짜와 시간 라이브러리 소개
다음과 같은 이유 때문에 날짜와 시간을 계산하는 것은 매우 어렵고 복잡하다. 이러한 복잡성 때문에 대부분의 현대 개발 환경에서는 날짜와 시간을 처리하기 위해 잘 설계된 라이브러리를 사용해야 한다.
- 날짜와 시간 차이 계산
- 윤년 계산
- 일광 절약 시간(DST, Daylight Saving Time) 변환
- 타임존(UTC로부터의 시간 차이) 계산
참고
자바 날짜와 시간 라이브러리의 역사는 스킵한다.
자바 날짜와 시간 라이브러리는 자바 공식 문서가 제공하는 다음 표 하나로 정리할 수 있다. 자세한 내용은 아래에서 하나씩 알아본다.
- *
- 초는 나노초 단위의 정밀도로 캡처된다. (밀리초, 나노초 가능)
- **
- 이 클래스는 이 정보를 저장하지는 않지만, 이러한 단위로 시간을 제공하는 메서드가 있다.
- ***
- ZonedDateTime에 Period를 추가하면 서머타임 또는 기타 현지 시간 차이를 준수한다.
참고
모든 날짜 클래스는 불변 객체이다. 따라서 변경이 발생하는 경우, 새로운 객체를 생성해서 반환하므로 반환값을 꼭 받아야 한다.
기본 날짜와 시간 - LocalDateTime
가장 기본이 되는 날짜와 시간 클래스는 LocalDate, LocalTime, LocalDateTime이다. Local이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문이다. 따라서 특정 지역의 날짜와 시간만 고려할 때 사용한다.
a. LocalDate
날짜만 표현할 때 사용하며 년, 월, 일을 다룬다.
- ex. 2021-05-12
public class LocalDateMain {
public static void main(String[] args) {
LocalDate nowDate = LocalDate.now();
LocalDate ofDate = LocalDate.of(2021, 5, 29);
// 계산 (불변)
LocalDate plusDays = ofDate.plusDays(10);
}
}
b. LocalTime
시간만을 표현할 때 사용하며 시, 분, 초를 다룬다.
- 초는 밀리초, 나노초 단위도 포함할 수 있다.
- ex. 07:21:59.235
public class LocalTimeMain {
public static void main(String[] args) {
LocalTime nowTime = LocalTime.now();
LocalTime ofTime = LocalTime.of(7, 34, 20);
// 계산 (불변)
LocalTime ofTimePlus = ofTime.plusSeconds(30);
}
}
c. LocalDateTime
LocalDate와 LocalTime을 합한 개념이다.
- ex. 2021-05-12T07:21:59.235
public class LocalDateTimeMain {
public static void main(String[] args) {
LocalDateTime nowDt = LocalDateTime.now();
LocalDateTime ofDt = LocalDateTime.of(2014, 2, 22, 9, 56, 3);
// 날짜와 시간 분리
LocalDate localDate = ofDt.toLocalDate();
LocalTime localTime = ofDt.toLocalTime();
// 날짜와 시간 합체
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
// 계산 (불변)
LocalDateTime ofDtPlusDays = ofDt.plusDays(1000);
LocalDateTime ofDtPlusYear = ofDt.plusYears(1);
// 비교
System.out.println(nowDt.isBefore(ofDt));
System.out.println(nowDt.isAfter(ofDt));
System.out.println(nowDt.isEqual(ofDt));
}
}
참고
isEqual()은 단순히 비교 대상이 시간적으로 같으면 true를 반환한다. 객체가 다르고, 타임존이 달라도 시간적으로 같으면 true를 반환한다.
equals()는 객체의 타입, 타임존 등 내부 데이터의 모든 구성 요소가 같아야 true를 반환한다.
ex. 서울의 9시와 UTC의 0시는 시간적으론 동일하지만 타임존의 데이터가 다르기 때문에 두 메서드의 결과가 다르게 나온다.
타임존 - ZonedDateTime
"Asia/Seoul +09:00" 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로부터의 시간 차이인 오프셋 정보를 모두 포함하고 있다.
a. ZoneId
자바는 타임존을 ZoneId 클래스로 제공한다.
public class ZoneIdMain {
public static void main(String[] args) {
for (String availableZoneId : ZoneId.getAvailableZoneIds()) {
ZoneId zoneId = ZoneId.of(availableZoneId);
System.out.println(zoneId + " | " + zoneId.getRules());
}
ZoneId zoneId = ZoneId.systemDefault();
ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
}
}
b. ZonedDateTime
LocalDateTime에 시간대 정보인 ZoneId가 합쳐진 것으로, 시간대를 고려한 날짜와 시간을 표현할 때 사용한다.
- ex. 2021-05-29T08:21:45.235+9:00[Asia/Seoul]
- +9:00
- UTC(협정 세계시)로부터의 시간대 차이이며, 오프셋이라고 한다.
- Asia/Seoul
- 타임존을 나타내며, 타임존을 알면 오프셋도 알 수 있다. 오프셋 정보도 타임존에 포함된다.
- 여기에 추가로 ZoneId를 통해 타임존을 알면, 일광 절약 시간에 대한 정보도 알 수 있다. 따라서 일광 절약 시간제가 적용된다.
- +9:00
- withZoneSameInstant(ZoneId zoneId)
- 타임존을 변경하고, 그에 맞춰 시간도 함께 변경한다.
- 이 메서드를 사용하면 지금 다른 나라는 몇 시인지 확인할 수 있다.
public class ZonedDateTime {
private final LocalDateTime dateTime;
private final ZoneOffset offset;
private final ZoneId zone;
...
}
///
public class ZonedDateTimeMain {
ZonedDateTime nowZdt = ZonedDateTime.now();
LocalDateTime ldt = LocalDateTime.of(2040, 2, 3, 19, 32, 4);
ZonedDateTime zdt1 = ZonedDateTime.of(ldt, ZoneId.of("Asia/Seoul"));
ZonedDateTime zdt2 = ZonedDateTime.of(2040, 2, 3, 19, 32, 4, ZoneId.of("Asia/Seoul"));
ZonedDateTime utcZdt = zdt2.withZoneSameInstant(ZoneId.of("UTC"));
}
c. OffsetDateTime
LocalDateTime에 UTC 오프셋 정보인 ZoneOffset이 합쳐진 것으로, 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존이 없고, UTC로부터의 시간대 차이인 고정된 오프셋만 포함한다.
- ex. 2021-05-29T08:21:45.235+9:00
- ZoneId가 없으므로 일광 절약 시간제가 적용되지 않는다.
public class OffsetDateTime {
private final LocalDateTime dateTime;
private final ZoneOffset offset;
...
}
///
public class OffsetDateTimeMain {
public static void main(String[] args) {
OffsetDateTime nowOdt = OffsetDateTime.now();
LocalDateTime ldt = LocalDateTime.of(2040, 1, 4, 19, 2, 12);
OffsetDateTime odt = OffsetDateTime.of(ldt, ZoneOffset.of("+01:00"));
}
}
참고
ZonedDateTime은 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다. 사용자 지정 시간대에 따른 시간 계산이 필요할 때 적합하다.
OffsetDateTime은 UTC와의 시간 차이만을 나타낼 때 사용하며, 지역 시간대의 복잡성을 고려하지 않는다. 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 때 적합하다.
기계 중심의 시간 - Instant
Instant는 UTC를 기준으로 하는, 시간의 한 지점을 나타낸다.
- 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC 기준)를 기준으로 경과한 시간으로 계산된다.
- 쉽게 말하면 Instant 내부에는 초 데이터만 들어 있다. 따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않다.
public class Instant {
private final long second;
private final int nanos;
...
}
참고
Epoch time(에포크 시간), 또는 Unix timestamp는 컴퓨터 시스템에서 시간을 나타내는 방법 중 하나이다. 이는 1970년 1월 1일 00:00:00 UTC로부터 현재까지 경과된 시간을 초 단위로 표현한 것으로, 시간대에 영향을 받지 않는 절대적인 시간 표현 방식이다.
Instant는 바로 이 Epoch 시간을 다루는 클래스이다.
Instant의 특징은 다음과 같다.
- 장점
- 시간대 독립성
- Instant는 UTC를 기준으로 하므로, 시간대에 영향받지 않는다. 따라서 전 세계 어디서나 동일한 시점을 가리키는 데 유용하다.
- 고정된 기준점
- 모든 Instant는 1970년 1월 1일 UTC를 기준으로 하기 때문에, 시간 계산 및 비교가 명확하고 일관된다.
- 시간대 독립성
- 단점
- 사용자 친화적이지 않음
- Instant는 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다. 예를 들어, 날짜와 시간을 계산하고 사용하는 데 필요한 기능이 부족하다.
- 시간대 정보 부재
- Instant에는 시간대 정보가 포함돼 있지 않아, 특정 지역의 날짜와 시간으로 변환하려면 추가적인 작업이 필요하다.
- 사용자 친화적이지 않음
- 사용 예시
- 전 세계적인 시간 기준이 필요한 경우
- ex. 로그 기록이나 트랜잭션 타임스탬프, 서버 간의 시간 동기화
- 시간대 변환 없이 시간 계산이 필요한 경우
- ex. 시간대의 변화 없이 순수하게 시간의 흐름(ex. 지속 시간 계산)만을 다루고 싶을 때
- 데이터 저장 및 교환
- ex. DB에 날짜와 시간 정보를 저장하거나, 다른 시스템과 날짜와 시간 정보를 교환할 때
- 전 세계적인 시간 기준이 필요한 경우
일반적으로 날짜와 시간을 사용할 때는 LocalDateTime, ZonedDateTime 등을 사용하면 된다. Instant는 날짜를 계산하기 어렵기 때문에 특별한 경우에 한정해서 사용하면 된다.
public class InstantMain {
public static void main(String[] args) {
Instant now = Instant.now();
ZonedDateTime zdt = ZonedDateTime.now();
Instant from = Instant.from(zdt);
Instant epochStart = Instant.ofEpochSecond(0);
Instant later = epochStart.plusSeconds(3600);
long laterEpochSecond = later.getEpochSecond();
}
}
기간, 시간의 간격 - Duration, Period
시간의 개념은 크게 특정 시점의 시간(시각)과 시간의 간격(기간), 이 두 가지로 표현할 수 있다.
- Period와 Duration은 시간의 간격(기간)을 표현하는 데 사용된다. 시간의 간격은 영어로 amount of time(시간의 양)으로 불린다.
구분 | Period | Duration |
단위 | 년, 월, 일 | 시간, 분, 초, 나노초 |
사용 대상 | 날짜 | 시간 |
주요 메서드 | getYears(), getMonths(), getDays() | toHours(), toMinutes(), getSeconds(), getNano() |
a. Period
두 날짜 사이의 간격을 날짜(년, 월, 일) 단위로 나타낸다.
public class Period {
private final int years;
private final int months;
private final int days;
...
}
///
public class PeriodMain {
public static void main(String[] args) {
Period period = period.ofDays(10);
LocalDate currentDate = LocalDate.of(2030, 1, 1);
LocalDate plusDate = currentDate.plus(period);
LocalDate startDate = LocalDate.of(2023, 1, 1);
LocalDate endDate = LocalDate.of(2023, 4, 2);
Period between = Period.between(startDate, endDate);
}
}
b. Duration
두 시간 사이의 간격을 시간(시, 분, 초;나노초) 단위로 나타낸다.
- 내부에서 초를 기반으로 시, 분, 초를 계산해서 사용한다.
public class Duration {
private final long seconds;
private final int nanos;
...
}
///
public class DurationMain {
public static void main(String[] args) {
Duration duration = Duration.ofMinutes(30);
LocalTime lt = LocalTime.of(1, 0);
LocalTime plusTime = lt.plus(duration);
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(10, 0);
Duration between = Duration.between(start, end);
}
}
날짜와 시간의 핵심 인터페이스
위에서 말했듯이 날짜와 시간은 특정 시점의 시간(시각)과 시간의 간격(기간)으로 나눌 수 있다.
- 특정 시점의 시간(시각)
- TemporalAccessor를 포함한 Temporal 인터페이스를 구현한다.
- 구현으로 LocalDateTime, LocalDate, LocalTime, ZonedDateTime, OffsetDateTime, Instant 등이 있다.
- 시간의 간격(기간)
- TemporalAmount 인터페이스를 구현한다.
- 구현으로 Period, Duration이 있다.
a. 특정 시점의 시간(시각)과 시간의 간격(기간, 시간의 양)
시각과 기간에 관련된 인터페이스 정보는 다음과 같다.
- TemporalAccessor 인터페이스
- 날짜와 시간을 읽기 위한 기본 인터페이스로, 특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공한다.
- Temporal 인터페이스
- TemporalAccessor의 하위 인터페이스로, 날짜와 시간을 조작하기 위한 기능을 제공한다. 이를 통해 날짜와 시간을 변경하거나 조정할 수 있다.
- TemporalAmount 인터페이스
- 시간의 간격(시간의 양, 기간)을 나타내며, 날짜와 시간 객체에 적용해 그 객체를 조정할 수 있다. 예를 들어, 특정 날짜에 일정 기간을 더하거나 빼는 데 사용된다.
b. 시간의 단위와 시간 필드
시간의 단위를 뜻하는 TemporalUnit(ChronoUnit)과 시간의 각 필드를 뜻하는 TemporalField(ChronoField)도 날짜와 시간에 관련된 핵심 인터페이스다.
시간의 단위 - TemporalUnit, ChronoUnit
- TemporalUnit 인터페이스는 날짜와 시간을 측정하는 단위를 나타내며, 주로 사용되는 구현체는 java.time.temporal.ChronoUnit 열거형으로 구성돼 있다.
- ChronoUnit은 다양한 시간 단위를 제공한다.
- 시간 단위
- NANOS = 나노초 단위
- MICROS = 마이크로초 단위
- MILLIS = 밀리초 단위
- SECONDS = 초 단위
- MINUTES = 분 단위
- HOURS = 시간 단위
- 날짜 단위
- DAYS = 일 단위
- WEEKS = 주 단위
- MONTHS = 월 단위
- YEARS = 년 단위
- DECADES = 10년 단위
- CENTURIES = 세기 단위
- MILLENNIA = 천년 단위
- 기타 단위
- ERAS = 시대 단위
- FOREVER = 무한대의 시간 단위
- 주요 메서드
- between(Temporal, Temporal) = 두 Temporal 객체 사이의 시간을 현재 ChronoUnit 단위로 측정해 반환
- isDateBased() = 현재 ChronoUnit이 날짜 기반 단위인지(ex. 일, 주, 월, 년) 여부 반환
- isTimeBased() = 현재 ChronoUnit이 시간 기반 단위인지(ex. 시, 분, 초) 여부 반환
- isSupportedBy(Temporal) = 주어진 Temporal 객체가 현재 ChronoUnit 단위를 지원하는지 여부 반환
- getDuration() = 현재 ChronoUnit의 기간을 Duration 객체로 반환
- 시간 단위
시간 필드 - TemporalField, ChronoField
- TemporalField 인터페이스는 날짜와 시간을 나타내는 데 사용되며, 주로 사용되는 구현체는 java.time.temporal.ChronoField 열거형으로 구성돼 있다.
- ChronoField는 날짜 및 시간을 나타내는 데 사용되는 열거형으로, 다양한 필드를 통해 연도, 월, 일, 시간, 분 등 날짜와 시간의 특정 부분을 나타낸다.
- 연도 관련 필드
- ERA = 연대 ex. 서기(AD) || 기원전(BC)
- YEAR_OF_ERA = 연대 내의 연도
- YEAR = 연도
- EPOCH_DAY = 1970-01-01로부터의 일 수
- 월 관련 필드
- MONTH_OF_YEAR = 월 (1월 = 1)
- PROLEPTIC_MONTH = 연도를 월로 확장한 값
- 주 및 일 관련 필드
- DAY_OF_WEEK = 요일 (월요일 = 1)
- ALIGNED_DAY_OF_WEEK_IN_MONTH = 월의 첫 번째 요일을 기준으로 정렬된 요일
- ALIGNED_DAY_OF_WEEK_IN_YEAR = 연의 첫 번째 요일을 기준으로 정렬된 요일
- DAY_OF_MONTH = 월의 일 (1일 = 1)
- DAY_OF_YEAR = 연의 일 (1월 1일 = 1)
- EPOCH_DAY = 유닉스 에폭(1970-01-01)으로부터의 일 수
- 시간 관련 필드
- 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 = 분 (0~59)
- SECOND_OF_MINUTE = 초 (0~59)
- NANO_OF_SECOND = 초의 나노초 (0~999,999,999)
- MICRO_OF_SECOND = 초의 마이크로초 (0~999,999)
- MILLI_OF_SECOND = 초의 밀리초 (0~999)
- 기타 필드
- AMPM_OF_DAY = 하루의 AM/PM 부분
- INSTANT_SECONDS = 초를 기준으로 한 시간
- OFFSET_SECONDS = UTC/GMT에서의 시간 오프셋 초
- 주요 메서드
- getBaseUnit() = 필드의 기본 단위 반환
- getRangeUnit() = 필드의 범위 단위 반환
- isDateBased() = 필드가 주로 날짜를 기반으로 하는지 여부 반환
- isTimeBased() = 필드가 주로 시간을 기반으로 하는지 여부 반환
- range() = 필드가 가질 수 있는 값의 유효 범위를 ValueRange 객체로 반환 (최솟값과 최댓값 제공)
- 연도 관련 필드
날짜와 시간 조회하고 조작하기
a. 날짜와 시간 조회하기
날짜와 시간을 조회하려면 날짜와 시간 항목 중에 어떤 필드를 조회할지 선택해야 한다. 이때 날짜와 시간의 필드를 뜻하는 ChronoField가 사용된다.
TemporalAccessor.get(TemporalField field)
- LocalDateTime을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 TemporalAccessor 인터페이스를 구현한다.
- TemporalAccessor는 특정 시점의 시간을 조회하는 기능을 제공한다.
- get(TemporalField field)을 호출할 때, 어떤 날짜와 시간 필드를 조회할지 TemporalField의 구현체인 ChronoField를 인수로 전달하면 된다.
편의 메서드 사용
- get(TemporalField field)을 사용하면 코드가 복잡해지기 때문에 자주 사용하는 조회 필드는 간단한 편의 메서드를 제공한다.
- ex. dt.get(ChronoField.DAY_OF_MONTH) → dt.getDayOfMonth()
- 자주 사용하지 않는 특별한 기능은 편의 메서드를 제공하지 않는다. 이런 경우엔 get(TemporalField field)을 사용하면 된다.
b. 날짜와 시간 조작하기
날짜와 시간을 조작하려면 어떤 시간 단위(Unit)를 변경할지 선택해야 한다. 이때 날짜와 시간의 단위를 뜻하는 ChronoUnit이 사용된다.
Temporal plus(long amountToAdd, TemporalUnit unit)
- LocalDateTime을 포함한 특정 시점의 시간을 제공하는 클래스는 모두 Temporal 인터페이스를 구현한다.
- Temporal은 특정 시점의 시간을 조작하는 기능을 제공한다.
- plus(long amountToAdd, TemporalUnit unit)를 호출할 때, 더할 숫자와 TemporalUnit의 구현체인 ChronoUnit을 전달하면 된다.
- 불변이므로 반환 값을 받아야 한다.
- 참고로 minus()도 존재한다.
편의 메서드 사용
- 자주 사용되는 메서드는 편의 메서드가 제공된다.
- ex. dt.plus(10, ChronoUnit.YEARS) → dt.plusYears(10)
Period를 사용한 조작
- Period나 Duration은 기간(시간의 간격)을 뜻한다. 특정 시점의 시간에 기간을 더할 수 있다.
c. 필드 사용 여부 확인
인터페이스 덕분에 특정 구현 클래스와 무관하게 일관성 있는 시간 조회 및 조작 기능을 사용할 수 있다. 그러나 모든 시간 필드를 조회할 수 있는 것은 아니다.
- 예를 들어 아래 코드에서 LocalDate는 날짜 정보만 갖고 있기 때문에 분에 대한 정보를 조회하려고 하면 예외가 발생한다.
public class IsSupportedMain {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
int minute = now.get(ChronoField.SECOND_OF_MINUTE);
System.out.println("minute = " + minute);
// 예외 발생 UnsupportedTemporalTypeException
}
}
이런 문제를 예방하기 위해 TemporalAccessor과 Temporal 인터페이스는 현재 타입에서 특정 시간 단위나 필드를 사용할 수 있는지 확인할 수 있는 메서드를 제공한다.
// TemporalAccessor
boolean isSupported(TemporalField field);
// Temporal
boolean isSupported(TemporalUnit unit);
///
public class IsSupportedMain {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
boolean supported = now.isSupported(ChronoField.SECOND_OF_MINUTE);
if (supported) {
int minute = now.get(ChronoField.SECOND_OF_MINUTE);
System.out.println("minute = " + minute); // false
}
}
}
d. with() 메서드
날짜와 시간을 조작하는 with() 메서드에 대해 알아보자.
Temporal with(TemporalField field, long newValue)
- Temporal.with()를 사용하면 날짜와 시간의 특정 필드 값만 변경할 수 있다.
- 불변이므로 반환값을 받아야 한다.
편의 메서드 제공
- 자주 사용하는 메서드는 편의 메서드가 제공된다.
- ex. dt.with(ChronoField.YEAR, 2020) → dt.withYear(2020)
e. TemporalAdjuster 사용
with() 메서드는 아주 단순한 날짜만 변경할 수 있다. 다음 금요일, 이번 달의 마지막 일요일 같은 복잡한 날짜를 계산하고 싶다면 TemporalAdjuster를 사용하면 된다.
- 원래대로 하면 아래 인터페이스를 직접 구현해야 하겠지만, 자바에서 TemporalAdjusters에 다 만들어뒀다.
- ex. TemporalAdjusters.next(DayOfWeek.FRIDAY), TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY)
- DayOfWeek = 월~일을 나타내는 열거형
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
주요 메서드
- dayOfWeekInMonth = 주어진 요일이 몇 번째인지에 따라 날짜 조정
- firstDayOfMonth / lastDayOfMonth = 해당 월의 첫째/마지막 날로 조정
- firstDayOfNextMonth / lastDayOfNextMonth = 다음 월의 첫째/마지막 날로 조정
- firstDayOfNextYear / lastDayOfNextYear = 다음 해의 첫째/마지막 날로 조정
- firstDayOfYear / lastDayOfYear = 해당 해의 첫째/마지막 날로 조정
- firstInMonth / lastInMonth = 주어진 요일 중 해당 월의 첫째/마지막 요일로 조정
- next / previous = 주어진 요일 이후/이전의 가장 가까운 요일로 조정
- nextOrSame / previousOrSame = 주어진 요일 이후/이전의 가장 가까운 요일로 조정하되, 현재 날짜가 주어진 요일인 경우 현재 날짜 반환
날짜와 시간 문자열 파싱과 포맷팅
포맷팅은 날짜와 시간 데이터를 원하는 포맷의 문자열로 변경하는 것을 말하며, 파싱은 문자열을 날짜와 시간 데이터로 변경하는 것을 말한다.
a. 포맷팅
LocalDate와 같은 날짜 객체를 원하는 형태의 문자로 변경(포맷팅)하려면 DateTimeFormatter를 사용하면 된다. 이후 ofPattern() 메서드로 원하는 포맷을 지정할 수 있다.
- DateTimeFormatter의 패턴은 공식 사이트를 참고하자.
public class FormattingMain {
pubic static void main(String[] args) {
LocalDate date = LocalDate.of(2025, 1, 2);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
String formattedDate = date.format(formatter);
}
}
b. 파싱
LocalDateTime과 같은 날짜와 시간 객체를 원하는 형태의 문자로 변경(파싱)하려면 DateTimeFormatter를 사용하면 된다. 이후 ofPattern() 메서드로 원하는 포맷을 지정할 수 있다.
- 문자열을 읽어서 날짜와 시간으로 파싱 할 때는 년, 월, 일, 시, 분, 초의 위치를 정해서 읽어야 한다.
public class FormattingMain {
pubic static void main(String[] args) {
LocalDateTime now = LocalDateTime.of(2023, 1, 2, 19, 24, 29);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateTimeString = "2023-02-02 12:40:00";
LocalDateTime parsedDateTime = LocalDateTime.parse(dateTimeString, formatter);
}
}