본문 바로가기
DEV/Java

[자바] java.time 패키지

by 어쩌다개발 2023. 2. 3.
반응형

자바 Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 JDK1.8부터 'java.time 패키지'가 추가되었다.
이 패키지는 아래와 같이 4개의 하위 패키지를 가지고 있다.
java.time | 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
java.time.chrono | 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
java.time.format | 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
java.time.zone | 시간대(time-zone)와 관련된 클래스들을 제공
날짜나 시간을 변경하는 메서들은 기존의 객체를 변경하는 대신 항상 변경된 새로운 객체를 반환한다. 기존 Calendar 클래스는 변경 가능하므로, 멀티 쓰레드 환경에서 안전하지 못 하다.

멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 객체에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있으며, 이를 쓰레드에 안전하지 않다고 한다. 그러나 기존 CalendarDate 호환성때문에 CalendarDate도 여전히 사용 될 것으로 보인다.

1. java.time 패키지의 핵심 클래스

날짜와 시간을 하나로 표현하는 Calendar 클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리했다.
시간을 표현할 때는 LocalTime 클래스를 사용하고, 날짜를 표현할 때는 LocalDate 클래스를 사용하면 된다.
그리고, 날짜와 시간이 모두 필요한 경우에는 LocalDateTime 클래스를 사용한다.
만약 시간대(time-zone)까지 추가해야 한다면, ZonedDateTime 클래스를 사용한다.

날짜와 시간을 초단위로 표현한 값을 타임스탬프(time-stamp)라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터베이스에서 많이 사용 된다.
Period와 Duration
날짜와 시간의 간격을 표현하기 위한 클래스도 있다.

Period : 두 날짜간의 차이를 표현하기 위한 것 (날짜 - 날짜)
Duration : 시간의 차이를 표현(시간 - 시간)

now()와 of()

java.time 패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 now()of()를 사용하는 것이다.
now()는 현재 날짜와 시간을 저장하는 객체를 생성한다.

LocalDate date = LocalDate.now(); //2023-02-02
LocalTime time = LocalTime.now(); //23:58:22.873
LocalDateTime dateTime = LocalDateTime.now(); //2023-02-02T23:58:22.873
ZonedDateTime dateTimeInKr = ZonedDatetime.now(); //23:58:22.873+09:00[Asia/Seoul]

of()는 단순히 필드의 값을 순서대로 지정해 주기만 하면 된다.

LocalDate date = LocalDate.of(2023, 02, 02); //2023년 02월 02일
LocalTime time = LocalTime.of(23, 59, 59); //23시 59분 59초

LocalDateTime dateTime = LocalDateTime.of(date, time);//2023-02-02T23:59:59
ZonedDateTime zDateTime = ZonedDateTime.of(dateTime, ZoneId,of("Asia/Seoul"));//2023-02-02T23:59:59+09:00[Asia/Seoul]

Temporal과 TemporalAmount

LocalDate, LocalTime, LocalDateTime, ZonedDateTime 등 날짜와 시간을 표현하기 위한 클래스들은 모두 Temporal, TomporalAccessor, TemporalAdjuster인터페이스를 구현했고, DurationPeriodTemporalAmount 인터페이스를 구현했다.
매개변수의 타입이 Temporal로 시작하는 것들이 자주 등장하는데 대부분 날짜와 시간을 위한 것이므로, TemporalAmount인지 아닌지 확인하면 된다.

Temporal, TemporalAccessor, TemporalAjuster를 구현한 클래스 - LocalDate, LocalTime, LocalDateTime, ZonedateTime, Instant 등 TemporalAmout를 구현한 클래스 - Period, Duration

Temporalunit과 TemporalField

날짜와 시간의 단위를 정의해 놓은 것이 TemporalUnit 인터페이스고, 이 인터페이스를 구현한 것이 열거형 ChronoUnit이다.
그리고, TemporalField는 년, 월, 일 등 날짜와 시간의 필드를 정의해 놓은 것으로 열거형 ChronoField가 이 인터페이스를 구현하였다.

LocalTime now = LocalTime.now(); //현재시간
int minute = now.getMinute();    //현재 시간에서 분(minute)만 조회
System.out.println(minute + "분");
int minute2 = now.get(ChronoField.MINUTE_OF_HOUR); //현재 시간에서 분(minute)만 조회
System.out.println(minute2 + "분");

//-------------------------------------------------------
//결과
//-------------------------------------------------------
//42분
//42분

날짜와 시간에서 특정 필드의 값만을 얻을 때는 get()이나, get으로 시작하는 이름의 메서드를 이용한다.
아래와 같이 특정 날짜와 시간에서 지정된 단위의 값을 더하거나 뺄 때는 plus() 또는 minus()에 값과 함께 열거형 ChronoUnit을 사용한다.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
System.out.println("내일 : " + tomorrow);
LocalDate tomorrow2 = today.plusDays(1);
System.out.println("내일 : " + tomorrow2);

//-------------------결과----------------------
//내일 : 2023-02-04
//내일 : 2023-02-04

참고로, get 메서드는 'int get(TemporalField field)' 로 정의되고, plus()는 'LocaDate plus(long amountToAdd, TemporalUnit unit)' 으로 정의된다.

특정 TemporalFieldTemporalUnit을 사용할 수 있는지 확인하는 메서드는 다음과 같은데, 이 메서드들은 날짜와 시간을 표현하는데 사용하는 모든 크래스에 포함되어 있다.

boolean isSupported(TemporalUnit unit) //Temporal에 정의
boolean isSupported(TemporalField field) //TemporalAcessor에 정의

 

2. LocalDate와 LocalTime

LocalDateLocalTimejava.time 패키지의 가장 기본이 되는 클래스이다. 나머지 클래스들은 이들의 확장이므로 이 두 클래스만 잘 이해해도 충분하다.
객체를 생성하는 방법은 현재의/ 날짜와 시간을 LocalDateLocalTime으로 각각 반환하는 now()와 지정된 날짜와 시간으로 LocalDateLocalTime 객체를 생성하는 of()가 있고 둘 다 static 메서드이다.

//LocalDate, LocalTime
LocalDate today = LocalDate.now();  //오늘의 날짜
LocalTime now = LocalTime.now();    //현재시간

LocalDate birthDate = LocalDate.of(2022, 02, 03); //2022년 02월 03일
LocalTime birthTime = LocalTime.of(23, 59, 59);     //23시 59분 59초

of()는 다음과 같이 여러 가지 버전이 제공된다.

static LocalDate of(int year, Month month, int dayOfMonth)
static LocalDate of(int year, int month, int dayOfMonth)

static LocalTime of(int hour, int min)
static LocalTime of(int hour, int min, int sec)
static LocalTime of(int hour, int min, int sec, int nanoOfSecond)

일 단위나 초 단위로도 지정할 수 있는데, 아래의 첫 번째 문장은 1999년의 365번째 날, 즉 마지막 날을 의미하며, 두 번째 문자은 그 날의 0시 0분 0초/부터 86399초(하루는 86400초)가 지난 시간, 즉 23시 59분 59초를 의미한다.

LocalDate birthDate = LocalDate.ofYearDay(1999, 365); //1999년 12월 31일
LocalTime birthTime = LocalTime.ofSeconday(86399)	//23시 59분 59초

parse()를 이용하면 문자열을 날짜와 시간으로 변환 가능하다.

LocalDate birthDate = LocalDate.parse("1999-12-31"); //1999년 12월 31일
LocalTime birthTime = LocalTime.ofSecondDay(86399);	//23시 59분 59초

 
특정 필드의 값 가져오기 - get(). getXXX()
LocalDate와 LocalTime은 객체에서 특정 필드의 값을 가져올 때 아래의 메서드를 사용한다. 주의할 점은 Calendar와 달리 월(month)의 범위가 1~12이고, 요일은 월요일이 1부터 시작한다.

클래스메서드예시
LocalDateint getYear()년도(1999)
int getMonthValue()월(12)
Month getMonth()월(DECEMBER) get Month().getValue() = 12
int getDayOfMonth()일(31)
int getDayOfYear()같은 해의 1월 1일부터 몇번째 일(365)
DayOfWeek getDayOfWeek()요일(FRIDAY) getDayOfWeek().getValue() = 5
int lengthOfMonth()같은 달의 총 일수(31)
int lengthOfYear()같은 해의 총 일수(365), 윤년이면 366
boolean lssLeapYear()윤년여부 확인(false)
LocalTimeint get Hour()시(23)
int getMinute()분(59)
int getSecond()초(59)
int geetNano()나노초(0)

만약. int타입의 범위를 넘을 수 있다면 get() 대신 getLong()을 사용하면 된다.

int get(TemporalField field)
long getLong(TemporalField field)

해당 메서들의 매개변수로 사용할 수 있는 필드의 목록은 아래와 같다.

TemporalField(ChronoField)
* 표시는 getLong()을 사용
설명
ERA시대
YEAR_OF_ERA, YEAR
MONTH_OF_YEAR
DAY_OF_WEEK요일(1:월)
DAY_OF_MONTH
AMPM_OF_DAY오전/오후
HOUR_OF_DYA시간(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천분의 일초
MICRO_OF_SECOND *백만분의 일초
NANO_OF_SECOND *10억분의 일초
DAY_OF_YEAR그 해의 몇번째 날
EPOCH_DAY *EPOCH(1970.1.1)부터 몇번째 날
MINUTE_OF_DAY그 날의 몇 번째 분(시간을 분으로 환산)
SECOND_OF_DAY그 날의 몇 번째 초(시간을 초로 환산)
MILL_OF_DAY그 날의 몇 번째 밀리초
MICRO_OF_DAY *그 날의 몇 번째 마이크로초
NANO_OF_DAY *그 날의 몇 번째 나노초
ALIGEND_WEEK_OF_MONTH그 달의 n번째 주(1~7일 1주, 8~14일 2주, ...)
ALIGEND_WEEK_OF_YEAR그 해의 n번째 주(1월 1~7일 1주, 8~14 2주, ...)
ALIGEND_DAY_OF_WEEK_IN_MONTH요일 ( 그 달의 1일을 월요일로 간주하여 계산)
ALGEND_DAY_OF_WEEK_IN_YEAR요일 ( 그 해의 1월 1일을 월요일로 간주하여 계산)
INSTANT_SECONDS년월일을 초단위로 환산(1970-01-01 00:00:00 UTC를 0초로 계산)Instant에만 사용
OFFSET_SECONDSUTC와의 시차, ZoneOffset에만 사용 가능
PROLEPTIC_MONTH년월을 월단위로 환산(2015년 11월 = 2015 * 12 + 11)

위 표는 ChronoField에 정의된 상수를 나열한 것이므로, 사용할 수 있는 필드는 클래스마다 다르다.
예를 들어 LocalDate는 날짜를 표현하기 위한 것이므로, MINUTE_OF_HOUR와 같이 시간에 관련된 필드는 사용할 수 없다.
사용할 수 없는 필드를 사용하면 UnsupportedTemporalTypeException이 발생한다.
만약, 특정 필드가 가질 수 있는 값의 범위를 알고 싶으면 다음과 같이 사용하면 된다.

System.out.println(ChronoField.CLOCK_HOUR_OF_DAY.range()); //1~24
System.out.println(ChronoField.HOUR_OF_DAY.range()); //0 ~23

HOUR은 밤 12시를 0으로 표현하고, CLOCK은 24로 표현한다는 것을 알 수 있다.
 
필드의 값 변경하기 - with(), plus(), minus()
날짜와 시간에서 특정 필드값을 변경하려면 with로 시작하는 메서드를 사용하면 된다.

LocalDate withYear(int year)
LocalDate withMonth(it month)
LocalDate withDayOfMonth(int dayOfMonth)
LocalDate withDayOfYear(it dayOfYear)

LocalTime withHour(int hour)
LocalTime withMinute(int minute)
LocalTime withSecond(int second)
LocalTime withNano(int nanoOfSecond)
LocalDate with(TemporalField field, long newValue)

필드를 변경하는 메서드들은 항상 새로운 객체를 생성해서 반환하므로 아래와 같이 대입 연산자를 같이 사용해야 한다.

date = date.withYear(2023); //년도를 2023년으로 변경
time = time.withHour(12);	//시간을 12시로 변경

그 외에도 특정 필드에 값을 더하거나 빼는 plus()와 minus()가 있다.

LocalTime plus(TemporalAmount amountToAdd)
LocalTime plus(long amountToAdd, TemporalUnit unit)

LocalDate plus(TemporalAmout amountToAdd)
LocalDate plus(long amountToAdd, TemporalUnit unit)

//plus()로 만든 메서드
LocalDate plusYears(long yearsToAdd)
LocalDate plusMonth(long monthsToAdd)
LocalDate plusDays(long daysToAdd)
LocalDate plusWeeks(long weeksToAdd)

LocalTime plusHours(long hoursToAdd)
LocalTime plusMinutes(long minutesToAdd)
LocalTime plusSeconds(long secondsToAdd)
LocalTime plusNanos(long nanosToAdd)

//LocalTime의 truncatedTo()는 지정된 것보다 작은 단위의 필드를 0으로 만든다.
LocalTime time = LocalTime.of(12, 34, 56); // 12시 34분 56초
time = time.truncatedTo(ChronoUnit.HOURS); //시(hour)보다 작은 단위를 0으로
System.out.println(time); //12:00

LocalDate는 LocalTime과 달리 truncatedTo()가 없는데, LocalDate의 필드인 년, 월, 일은 0이 될 수 없기 때문이다.
그래서 truncatedTo()의 매개변수로는 시간과 관련된 필드만 사용가능하다.
날짜와 시간의 비교 - isAfter(), isBefore(), isEqual()
LocalDate와 LocalTimedpsms compareTo()가 오버라이딩되어 있어서 compareTo()로 비교할 수 있다.
하지만 더 편리하게 비교할 수 있는 메서드들이 추가로 제공되고 있다.

boolean isAfter(ChronoLocalDate other)
boolean isBefor(ChronoLocalDate other)
boolean isEqual(ChronoLocalDate other) //LocalDate만 제공

equals()가 있는데도, isEqual()를 제공하는 이유는 연표(chronolory)가 다른 두 날짜를 비교하기 위해서다. equals()는 모든 필드가 일치해야 하지만, isEqual()은 날짜만 비교한다.

LocalDate kDate = LocalDate.of(1999, 12, 31);
JapaneseDate jDate = JapaneseDate.of(1999, 12, 31);

System.out.println(kDate.equals(jDate)); //false -> YEAR_OF_ERA가 다름
System.out.println(kDate.isEqual(jDate)); //true
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;

public class NewTimeEx1 {
    public static void main(String[] args) {

        LocalDate today = LocalDate.now();  //오늘의 날짜
        LocalTime now = LocalTime.now();    //현재 시간

        LocalDate birthDate = LocalDate.of(2022, 02, 03);
        LocalTime birthTime = LocalTime.of(23, 59, 59);

        System.out.println("today : " + today);
        System.out.println("now : " + now);
        System.out.println("birthDate : " + birthDate);
        System.out.println("birthTime : " + birthTime);

        System.out.println(birthDate.withYear(2024)); //월, 일은 그대로두고 년도만 변경
        System.out.println(birthDate.plusDays(1));    //년, 월은 그대로두고 일만 + 1
        System.out.println(birthDate.plus(1, ChronoUnit.DAYS)); //위와 동일

        //23:59:59 -> 23:00
        System.out.println(birthTime.truncatedTo(ChronoUnit.HOURS));

        //특정 ChronoField의 범위 알아냄
        System.out.println(ChronoField.CLOCK_HOUR_OF_DAY.range()); // 1 - 24
        System.out.println(ChronoField.HOUR_OF_DAY.range()); // 0 - 23
    }
}

결과

today : 2023-02-03
now : 15:13:12.929343
birthDate : 2022-02-03
birthTime : 23:59:59
2024-02-03
2022-02-04
2022-02-04
23:00
1 - 24
0 - 23

 

조상님LOTTO - Google Play 앱

무료로 로또 당첨 확률을 높일 수 있습니다.

play.google.com

3.Instant

Instant는 에포크 타임부터 경과된 시간을 나노초 단위로 포현한다. 단일 진법으로만 다루기 때문에 사람에겐 불편하지만 계산이 쉽다. 사람이 사용하는 날짜와 시간은 여러 진법이 섞여 있어서 계산하기 어렵다.

Instant now = Instant.now();
Instant now2 = Instant.ofEpochSecond(now.getEpochSecond());
Instant now3 = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());

Instant를 생성할 때는 위와 같이 now()와 ofEpochSecond()를 사용한다.
필드에 저장된 값을 가져올 때는 다음과 같이 한다.

long epochSec = now.getEpochSecond();
int nano = now.getNano();

Instant는 시간을 초 단위와 나노초 단위로 나누어 저장한다. 오라클 데이터베이스의 타임스탬프(timestamp)처럼 밀리초 단위의 EPOCH TIME을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있다.

long toEpochMilli()

Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있다. 시간대를 고려해야하는 경우 OffsetDateTime을 사용하는 것이 더 나은 선택이다.
UTC는 '세계 협정시(Coordinated Universal Time)'라고 하며, 1972년 1월 1일부터 시행된 국제 표준시이다. 이전에 사용되던 GMT(Greenwich Mean Time)와 UTC는 거의 같지만, UTC가 좀 더 정확하다.
Instant는 기존의 java.util.Date를 대체하기 위한 것이며 JDK1.8부터 Date에 Instant로 변환할 수 있는 새로운 메서드가 추가되었다.

static Date from(Instant instant); //Instant -> Date
static toInstant()	//Date -> Instant

 
4. LoclaDateTime 과 ZonedDateTime
LocalDateTime은 LocalDate와 LocalTime을 합쳐놓은 것이다. ZonedDateTime은 LocalDateTime에 시간대(time zone)을 추가한 것이다.
 
LocalDate와 LocalTime으로 LocalDateTime 만들기

LocalDate date = LocalDate.of(2015, 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(2015, 12, 31));
LocalDateTiem dt6 = date.atStartOfDay(); //dt6 = date.atTime(0, 0, 0);

 
LocalDateTime으로 날짜와 시간 지정

LocalDateTime dateTime = LocalDateTime.of(2015, 12, 31, 12, 34, 56); //2015년 12월 31일 12시 34분 56초
LocalDateTime today = LocalDateTime.now();

 
LocalDateTime을 LocalDateTime 또는 LocalDate로 변환

LocalDateTime dt = LocalDateTime.of(2015, 12, 31, 12, 34, 56);
LocalDate date = dt.toLocalDate(); //LocalDateTime -> LocalDate
LocalTime time = dt.toLocalTime(); //LocalDateTime -> LocalTime

 
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");
ZoneDateTime zdt = dateTime.atZone(zid);
System.out.println(zdt); //2023-02-03T17:47:50.451+09:00[Asia/seoul]

LocalDate에 atStartOfDay()라는 메서드가 있는데 이 메서드에 매개변수로 ZoneId를 지정해도 ZonedDateTime을 얻을 수 있다.

ZonedDateTime zdt = LocalDate.now().atStartOfDay(zid);
System.out.println(zdt); //2023-02-03T00:00+09:00[Asia/Seoul]

메서드이름(startOfDay)에서 알 수 있듯이 시간이 0시 0분 0초로 되어 있다.
만약 특정 시간대의 나라 Zone을 알고 싶다면 다음과 같이 변환하면 된다.

ZoneId nyId = ZoneId.of("Amareica/New_York");
ZoneDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

now() 대신 of()를 사용하면 날짜와 시간을 지정할 수 있다.
 
ZoneOffset
UTC로부터 얼마만큼 떨어져 있는지는 ZoneOffset으로 표현한다. 서울은 UTC보다 9시간 빠른걸 알 수 있다.

ZoneOffSet krOffset = ZonedDateTime.now().getOffset();
//ZoneOffSet krOffset = ZonedDateTime.of("+9"); //h, hh, hhmm, hh:mm
int krOffsetInSec = krOffset.get(ChronoField.OFFSET_SECONDS); // 324000초(60*60*9) 즉, +9

 
OffsetDateTime
ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffsetDateTime이다.
ZoneOffset은 단지 시간대를 시간의 차이로만 구분한다. 컴퓨터에게 일광절약시간(ZoneId)처럼 계절별로 시간을 더했다 뺐다하는 것과 같은 행위는 위험하다. 아무런 변화 없이 일관된 시간체계를 유지하는 것이 안전하다. 같은 지역 내의 컴퓨터간에 데이터를 주고받을 때, 전송시간을 표현하기에 LocalDateTime이면 충분하겠지만. 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요하다.

ZonedDateTime zdt = ZondedDateTime.of(date, time, zid);
OffsetDateTime odt = OffsetDateTime.of(date, time, krOffset);

//ZonedDatetime -> OffsetDateTime
OffsetDateTime odt = zdt.toOffsetDateTime();

OffsetDateTime은 ZonedDateTime처럼, LocalDate와 LocalTime에 ZoneOffset을 더하거나, ZonedDateTime에 toOffsetDateTime()을 호출해서 얻을 수도 있다.
ZonedDateTime의 변환
ZonedDateTime도 LocalDateTime 처럼 날짜와 시간에 관련된 다른 클래스로 변환하는 메서드를 가지고 있다.

LocalDate toLocalDate()
LocalTime tolocalTime()
LocalDateTime toLocalDateTime()
OffsetDateTime toOffsetDateTime()
long toEpochSecond()
Instant toInstant()

GregorianCalenda와 가장 유사한 것이 ZonedDateTime 이다. GregorianCalendar와 ZonedDateTime 간의 변환방법만 알면, 위에 나열한 메서드를 이용해서 다른 날짜와 시간 클래스들로 변환할 수 있다.

GregorianCalendar from(ZonedDateTime zdt); //ZonedDateTime -> GregorianCalendar
ZonedDateTime toZonedDateTime(); //GregorianCalendar -> ZonedDateTime
import java.time.*;

public class NewTimeEx2 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2023, 02, 03); // 2023년 02월 03일
        LocalTime time = LocalTime.of(12, 34, 56);  //12시 34분 56초

        LocalDateTime dt = LocalDateTime.of(date, time);

        ZoneId zid = ZoneId.of("Asia/Seoul");
        ZonedDateTime zdt = dt.atZone(zid);
        String strZid = zdt.getZone().getId();

        ZonedDateTime seoulTime = ZonedDateTime.now();
        ZoneId nyId = ZoneId.of("America/New_York");
        ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

        //ZonedDatetime -> OffsetDateTime
        OffsetDateTime odt = zdt.toOffsetDateTime();

        System.out.println(dt);
        System.out.println(zid);
        System.out.println(zdt);
        System.out.println(seoulTime);
        System.out.println(nyTime);
        System.out.println(odt);
        
    }
}

//결과
//2023-02-03T12:34:56
//Asia/Seoul
//2023-02-03T12:34:56+09:00[Asia/Seoul]
//2023-02-03T16:45:42.141431+09:00[Asia/Seoul]
//2023-02-03T02:45:42.143678-05:00[America/New_York]
//2023-02-03T12:34:56+09:00

 

5. TemporalAdjusters

plus(), minus()와 같은 메서드로 날짜와 시간을 계산할 수 있지만 지난 주 토요일이 며칠인지, 또는 이번 달의 3번째 금요일이 며칠인지와 같은 날짜계산을 하기에는 불편하다.
그래서 자주 쓰일만한 날짜 계산들을 대신 해주는 메서드를 정의해놓은 것이 있는데 TemporalAdjusters 클래스이다.

LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));

 
 

메서드설명
firstDayOfNextYear()다음 해의 첫 날
firstDayOfNextMonth()다음 달의 첫 날
firstDayOfYear()올 해의 첫 날
firstDayOfMonth()이번 달의 첫 날
lastDayOfYear()올 해의 마지막 날
lastDayOfMonth()이번 달의 마지막 날
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, Instent 등 대부분의 날짜와 시간에 관련된 클래스에 포함되어 있다.
TemporalAdjuster인터페이스는 다음과 같이 추상 메서드 하나만 정의되어 있으며, 이 메서드만 구현하면 된다.

@FunctionalInterface
publi interface TemporalAdjuster {
	Temporal adjustInto(Temporal temporal);
}

실제로 구현해야하는 것은 adjustInto()지만, TemporalAdjuster와 같이 사용해야 하는 메서드는 with()다.
with()와 adjustInto() 중에서 어느 쪽을 사용해도 상관없지만, adjustInto()는 내부적으로만 사용할 의도로 작성된 것이기 때문에 with()를 사용하면 된다.
날짜와 시간에 관련된 대부분의 클래스는 Temporal 인터페이스를 구현하였으므로, adjustInto()의 매개변수가 될 수 있다.
예를 들어 특정 날짜로부터 2일후의 날짜를 계산하는 DayAfterTomorrow는 다음과 같이 작성할 수 있다.

class DayAfterTomorrow implements TemporalAdjuster {
	@Override
    public Temporal adjustinto(Temporal temporal) {
    	return temporal.plus(2, Chronounit.DAYS); 	//2일 더함
    }
}
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;

import static java.time.DayOfWeek.TUESDAY;
import static java.time.temporal.TemporalAdjusters.*;

class DayAfterTomorrow implements TemporalAdjuster {

    @Override
    public Temporal adjustInto(Temporal temporal) {
        return temporal.plus(2, ChronoUnit.DAYS);
    }
}

public class NewTimeEx3 {

    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate date = today.with(new DayAfterTomorrow());

        p(today); //오늘
        p(date);  //2일 뒤
        p(today.with(firstDayOfNextMonth())); //다음 달의 첫 날
        p(today.with(firstDayOfMonth()));   //이 달의 첫 날
        p(today.with(lastDayOfMonth()));    //이 달의 마지막 날
        p(today.with(firstInMonth(TUESDAY)));   //이 달의 첫번째 화요일
        p(today.with(lastInMonth(TUESDAY)));   //이 달의 마지막 화요일
        p(today.with(previous(TUESDAY)));   //지난주 화요일
        p(today.with(previousOrSame(TUESDAY)));   //지난주 화요일(오늘 포함)
        p(today.with(next(TUESDAY)));   //다음주 화요일
        p(today.with(nextOrSame(TUESDAY)));   //다음주 화요일(오늘 포함)
        p(today.with(dayOfWeekInMonth(4, TUESDAY)));   //이 달의 4번째 화요일
    }

    static void p(Object obj) {
        System.out.println(obj);
    }
}

 

6. Period 와 Duration

Period : 날짜 차이
Duration : 시간 차이
between()
두 날짜 차이를 나타내는 period는 between()으로 값을 얻을 수 있다.

LocalDate date1 = LocalDate.of(2023, 1, 1);
LocalDate date2 = LocalDate.of(2024, 12, 31);

Period pe = Period.between(date1, date2);

날짜 상으로 이전이면 양수, 이후면 음수로 period에 저장된다.
시간차이를 구할 때는 Duration을 사용한다는것만 빼면 똑같다.

LocalDate date1 = LocalDate.of(00, 00, 00);
LocalDate date2 = LocalDate.of(12, 34, 56); //12시 34분 56초

Period pe = Duration.between(date1, date2);

Period, Duration에서 특정 필드의 값을 얻을 때는 get()을 사용한다.

long year = pe.get(ChronoUnit.YEARS);	//int getYears()
long month = pe.get(ChronoUnit.MONTHS);	//int getMonths()
long day = pe.get(ChronoUnit.DAYS);		//int getDays()

long sec = pe.get(ChronoUnit.SECONDS);	//long getSeconds()
long nano = pe.get(ChronoUnit.NANOS);	//int getNano()

Period와 달리 Duration에는 getHours() 같은 메서드가 없다. Chrono.SECONDS와 Chrono.NANOS 밖에 사용할 수 없다. 직접 계산하는 방법이 있지만 불편하다. Duration을 LocalTime으로 변환한 다음에, LocalTime이 가지고 있는 get 메서드들을 사용하면 직접 계산보다는 조금 간단하다.

LocalTime tmpTime = localTime.of(0,0).plusSeconds(du.getSeconds());

int hour = tmpTime.getHour();
int min = tmpTime.getMinute();
int sec = tmpTime.getSecond();
int nano = tmpTime.getNono();

 
between()과 until()
until()은 between()과 거의 같은 일을 한다. between()은 static 메서드이고, untill()은 인스턴스 메서드라는 차이가 있다.

//Period pe = Period.between(today, myBirthDay);
Period pe = today.until(myBirthDay);
long dday = today.until(myBirthDay, ChronoUnit.DAYS);

Period는 년월일을 분리해서 저장하기 때문에, D-day를 구하려는 경우에는 두 개의 메개변수를 받는 until()을 사용하는 것이 낫다.
날짜가 아닌 시간에도 until()을 사용할 수 있지만, Duration을 반환하는 until()은 없다.

long sec = Localtime.now().until(endTime, ChronoUnit.SECONDS);

 
사칙연산, 비교연산, 기타 메서드
plus(), minus()외에 곱셈과 나눗셈을 위한 메서드도 있다.

pe.minusYears(1).multipledBy(2);	//1년을 빼고, 2배를 곱한다.
du.plusHours(1).dividedBy(60);		//1시간을 더하고 60으로 나눈다.

음수인지 확인하는 isNegative()와 0인지 확인하는 isZero()가 있다. 두 날짜 또는 시간을 비교할 때 사용하면 어느쪽이 앞인지 또는 같은지 알아낼 수 있다.

boolean sameDate = Period.between(date1, date2).isZero();
boolean isBefore = Duration.between(time1, time2).isNegative();

부호를 반대로 변경하는 negate()와 부호를 없애는 abs()가 있다. 아래 두 소스는 같으나 Period에는 abs()가 없어서 negated()를 사용해야 한다.

du = du.abs();

if(du.isNegatiove()) du = du.negated(); //위와 같다.

Period에 normalized()라는 메서드가 있는데, 이 메서드는 월의 값이 12를 넘지않게 바꿔준다. 일의 길이는 일정하지 않다보니 계산해주지 않는다.

pe = Period.of(1, 13, 32).normalized(); //1년 13개월 32일 -> 2년 1개월 32일

 
다른 단위로 변환 - toTotalMonths(), toDays(), tohours(), toMinutes()
이름이 'to'로 시작하는 메서들들은 Peroid와 Duration을 다른 단위의 값으로 변환하는데 주로 사용된다. get()은 특정 필드의 값을 그대로 가져오지만, 아래의 메서드들은 특정 단위로 변환한 결과를 반환한다는 차이가 있다.
참고) 이 메서드들의  반환타입은 모두 정수(long타입)인데, 지정된 단위 이하의 값들은 버려진다.

클래스메서드설명
Periodlong to TotalMonths()년월일을 월단위로 변환해서 반환(일 단위는 무시)
Durationlong toDays()일단위로 변환해서 반환
long toHours()시간단위로 변환해서 반환
long toMinutes()분단위로 변환해서 반환
long toMillis()천분의 일초 단위로 변환해서 반환
long toNonos()나노초 단위로 변환해서 반환

LocalDate의 toEpochDay()라는 메서드는 Epoch Day인 '1970-01-01'부터 날짜를 세어 반환한다.
두 날짜 모두 Epoch Day 이후라면, Period를 사용하지 않고도 두 날짜간의 일수를 편리하게 계산할 수 있다.

LocalDate date1 = LocalDate.of(2023, 01, 01);
LocalDate date2 = LocalDate.of(2023, 02, 03);

long period = date2.toEpochDay() - date1.toEpochDay();

LocalTime에도 다음과 같은 메서드가 있어서, Duration을 사용하지 않고도 뺄셈으로 시간차이를 계산할 수 있다.

int toSecondOfDay()
long toNanoOfDay()
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;

public class NewTimeEx4 {
    public static void main(String[] args) {
        LocalDate date1 = LocalDate.of(2022, 1, 1);
        LocalDate date2 = LocalDate.of(2023, 12, 31);

        Period pe = Period.between(date1, date2);

        System.out.println("date1 : " + date1);
        System.out.println("date2 : " + date2);
        System.out.println("pe : " + pe);

        System.out.println("YEAR : " + pe.get(ChronoUnit.YEARS));
        System.out.println("MONTH : " + pe.get(ChronoUnit.MONTHS));
        System.out.println("DAY : " + pe.get(ChronoUnit.DAYS));

        LocalTime time1 = LocalTime.of(2, 12, 50);
        LocalTime time2 = LocalTime.of(12, 34, 56);

        Duration du = Duration.between(time1, time2);

        System.out.println("time1 : " + time1);
        System.out.println("time2 : " + time2);
        System.out.println("du : " + du);

        LocalTime tempTime = LocalTime.of(0, 0).plusSeconds(du.getSeconds());

        System.out.println("HOUR : " + tempTime.getHour());
        System.out.println("MINUTE : " + tempTime.getMinute());
        System.out.println("SECOND : " + tempTime.getSecond());
        System.out.println("NANO : " + tempTime.getNano());

    }
}

//결과
//date1 : 2022-01-01
//date2 : 2023-12-31
//pe : P1Y11M30D
//YEAR : 1
//MONTH : 11
//DAY : 30
//time1 : 02:12:50
//time2 : 12:34:56
//du : PT10H22M6S
//HOUR : 10
//MINUTE : 22
//SECOND : 6
//NANO : 0

 

뽀모도로 타이머(Pomodoro Timer) - Google Play 앱

뽀모도로(Pomodoro)공부법은 단시간 집중력 향상을 위한 자기주도 학습법입니다.

play.google.com

7. 파싱과 포맷

형식화(formatiing)와 관련된 클래스들은 java.time.format 패키지에 들어있는데, 이 중에서 DateTimeFormatter가 가장 핵심이다.
자주 쓰이는 다양한 형식들을 기본적으로 정의하고 있으며, 필요하다면 직접 정의해서 사용할 수도 있다.

LocalDate date = LocalDate.of(2023, 1, 2);

String yyyymmdd1 = DateTimeFormatter.ISO_LOCAL_DATE.format(date); //2023-01-02
String yyyymmdd2 = date.format(DateTimeForatter.ISO_LOCAL_DATE);  //2023-01-02

위와 같이 format()을 사용하여 날짜와 시간을 형식화하는데, 이 메서드는 DateTimeFormatter 뿐만 아니라 LocalDate나 LocalTime 같은 클래스에도 있다. 같은 기능을 하니까 상황에 따라 편한 쪽을 선택해서 사용하면 된다.

DateTimeFormatter설명보기
ISO_DATE_TIMEDate and time with ZoneId2023-12-03T10:15:30+01:00[Europe/Paris]
ISO_LOCAL_DATEISO Local Date2023-12-03
ISO_LOCAL_TIMETime without offset10:15:30
ISO_LOCAL_DATE_TIMEISO Local Date and Time2023-12-03T10:15:30
ISO_OFFSET_DATETime with offset2023-12-03+01:00
ISO_OFFSET_TIMEDate with offset10:15:30+01:00
ISO_OFFSET_DATE_TIMEDate Time with Offset2023-12-03T10:15:30+01:00
ISO_ZONED_DATE_TIMEZoned Date Time2023-12-03T10:15:30+01:00[Eupope/Paris]
ISO_INSTANTDate and Time of an Instant2023-12-03T10:15:30Z
BASIC_ISO_DATEBasic ISO date20231203
ISO_DATEISO Date with or without offset2023-12-03+01:00
2023-12-03
ISO_TIMETime with or without offset10:15:30+01:00
10:15:30
ISO_ORDINAL_DATEYear and day of year2023-337
ISO_WEEK_DATEYear and Week2023-W48-6
RFC_1123_DATE_TIMERFC 1123 / RFC 822Tue, 3 Jun 2023 11:05:30 GMT

 
로케일에 종속된 형식화
DateTimeFormatter의 static 메서드 ofLocalzedDate(), ofLocalizedTime(), ofLocalized DateTime() 로케일(locale)에 종속적인 formmater를 생성한다.

DateTimeFormatter. formatter = DateTimeFormatter.ofLocaizedDate(FormatStyle.SHORT);
String shortFormat = formatter.format(LocalDate.now());

FormatStyle의 종류에 따른 출력 형태는 다음과 같다.

FormatStyle날짜시간
FULL2023년 02월 03일N/A
LONG2023년 02년 03일 (금)오후 9시 15분 13초
MEDIUM2023. 02. 03오후 9:15:13
SHORT23. 02. 03오후 9:15

 
출력형식 직접 정의하기
DateTimeFormatter의 ofPattern()으로 원하는 출력형식을 직접 작성할 수 있다.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
기호의미보기
G연대(BC, AD)서기 또는 AD
y 또는 u년도2023
M 또는 L월(1~12 또는 1월~12월)11
Q 또는 q분기(quarter)4
w년의 몇 번째 주(1~53)48
W월의 몇 번째 주(1~5)4
D년의 몇 번째 일(1~366)332
d월의 몇 번째 일(1~31)28
F월의 몇 번째 요일(1~5)4
E 또는 e요일토 또는 7
a오전/오후(AM, PM)오후
H시간(0~23)22
k시간(1~24)22
K시간(0~11)10
h시간(1~12)10
m분(0~59)12
s초(0~59)35
S천분의 일초(0~999)7
A천분의 일초(그 날의 0시 0분 0초의 부터의 시간)80263808
n나노초(0~999999999)475000000
N나노초(그 날의 0시 0분 0초 부터의 시간)81069992000000
V시간대 ID(VV)Asia/Seoul
z시간대(time-zone) 이름KST
O지역화된 zone-offsetGMT+9
Zzone-offset+0900
X 또는 xzone-offset(Z는 +00:00를 의미)+09
'escape문자(특수문자를 표현하는데 사용) 
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

public class DateFormatterEx1 {
    public static void main(String[] args) {
        ZonedDateTime zDateTime = ZonedDateTime.now();

        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-mm-dd HH:mm:ss")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("''yy년 MMM dd일 E요일")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS z VV")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 올 해의 D번째 날")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 d번째 날")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 올 해의 w번째 주")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 W번째 주")));
        System.out.println(zDateTime.format(DateTimeFormatter.ofPattern("오늘은 이 달의 W번째 E요일")));
    }
}

//결과
//2023-19-03 23:19:50
//'23년 2월 03일 금요일
//2023-02-03 23:19:50.698 KST Asia/Seoul
//2023-02-03 11:19:50 오후
//오늘은 올 해의 34번째 날
//오늘은 이 달의 3번째 날
//오늘은 올 해의 5번째 주
//오늘은 이 달의 1번째 주
//오늘은 이 달의 1번째 금요일

 
문자열을 날짜와 시간으로 파싱하기
static 메서드 parse()를 사용하면 된다. 날짜와 시간을 표현하는데 사용되는 클래스에는 parse() 메서드가 거의 다 포함되어 있다.
parse()는 오버로딩 된 메서드가 여러 개 있는데 그 중 아래 2개가 자주 쓰인다.

static LocalDateTime parse(CharSequence text)
static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)

DateTimeFormatter에 상수로 정의된 형식을 사용할 때는 다음과 같이 표현한다.

LocalDate date = LocalDate.parse("2023-01-02", DateTimeFormatter.ISO_LOCAL_DATE);

아래와 같이 상수를 사용하지 않고도 파싱이 가능하다.

LocalDate newDate = LocalDate.parse("2023-01-01");
LocalTime newTime = LocalTime.parse("23:59:59");
LocalDateTime newDateTime = LocalDateTime.parse("2023-01-01T23:59:59");

//ofPattern() 사용
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime endOfYear = LocalDateTime.parse("2023-01-01 23:59:59", pattern)";
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class DateFormatterEx2 {
    public static void main(String[] args) {
        LocalDate newYear = LocalDate.parse("2023-01-01", DateTimeFormatter.ISO_LOCAL_DATE);
        System.out.println(newYear); //2023-01-01

        LocalDate date = LocalDate.parse(("2023-01-01"));
        System.out.println(date); //2023-01-01

        LocalTime time = LocalTime.parse("23:59:59");
        System.out.println(time); //23:59:59

        LocalDateTime dateTime = LocalDateTime.parse("2023-01-10T23:59:59");
        System.out.println(dateTime); //2023-01-10T23:59:59

        DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        LocalDateTime endOfYear = LocalDateTime.parse("2023-01-01 23:59:59", pattern);
        System.out.println(endOfYear); //2023-01-01T23:59:59

    }
}

 
 

조상님LOTTO - Google Play 앱

무료로 로또 당첨 확률을 높일 수 있습니다.

play.google.com


 
참고: 자바의 정석

반응형

'DEV > Java' 카테고리의 다른 글

[자바]ArrayList  (6) 2023.02.06
[자바] 컬렉션 프레임워크(Collections Framework)  (4) 2023.02.05
[자바] MessageFormat  (3) 2023.02.02
[자바] ChoiceFormat  (1) 2023.02.02
[자바] SimpleDateFormat  (2) 2023.02.02

댓글