날짜와 시간 라이브러리가 필요한 이유
날짜와 시간을 계산하는 것은 단순하게 생각하면 쉬워보이지만, 실제로는 매우 복잡하고 어렵습니다.
그렇다면 왜 어려울까요?
- 날짜와 시간 차이 계산
- 윤년 계산
- 일광 절약 시간(Daylight Saving Time, DST) 변환
- 타임존 계산
자바 날짜와 시간 라이브러리의 역사
자바는 날짜와 시간 라이브러리를 지속해서 업데이트 했습니다.
JDK 1.0 (java.util.Date)
문제점
- 타임존 처리 부족: 초기 Date 클래스는 타임존(time zone)을 제대로 처리하지 못했다.
- 불편한 날짜 시간 연산: 날짜 간 연산이나 시간의 증감 등을 처리하기 어려웠다.
- 불변 객체 부재: Date 객체는 변경 가능(mutable)하여, 데이터가 쉽게 변경될 수 있었고 이로 인해 버그
가 발생하기 쉬웠다.
해결책
- JDK 1.1에서 java.util.Calendar 클래스 도입으로 타임존 지원 개선.
- 날짜 시간 연산을 위한 추가 메소드 제공.
JDK 1.1 (java.util.Calendar)
문제점
- 사용성 저하: Calendar 는 사용하기 복잡하고 직관적이지 않았다.
- 성능 문제: 일부 사용 사례에서 성능이 저하되는 문제가 있었다.
- 불변 객체 부재: Calendar 객체도 변경 가능하여, 사이드 이펙트, 스레드 안전성 문제가 있었다.
해결책
- Joda-Time 오픈소스 라이브러리의 도입으로 사용성, 성능, 불변성 문제 해결.
Joda-Time
문제점
- 표준 라이브러리가 아님: Joda-Time은 외부 라이브러리
- 자바 표준에 포함되지 않아 프로젝트에 별도로 추가해야 했다.
해결책
- 자바 8에서 java.time 패키지(JSR-310)를 표준 API로 도입.
JDK 8(1.8) (java.time 패키지)
- java.time 패키지는 이전 API의 문제점을 해결하면서, 사용성, 성능, 스레드 안전성, 타임존 처리 등에서 크게
개선되었다. - 변경 불가능한 불변 객체 설계로 사이드 이펙트, 스레드 안전성 보장, 보다 직관적인 API 제공으로 날짜와 시간 연산을 단순화했다.
- LocalDate , LocalTime , LocalDateTime , ZonedDateTime , Instant 등의 클래스를 포함한다.
- Joda-Time의 많은 기능을 표준 자바 플랫폼으로 가져왔다.
자바 날짜와 시간 라이브러리 소개
자바 날짜와 시간 라이브러리는 자바 공식 문서가 제공하는 다음 표 하나로 정리할 수 있습니다.
LocalDate, LocalTime, LocalDateTime
- LocalDate: 날짜만 표현할 때 사용한다. 년, 월, 일을 다룬다. 예) 2013-11-21
- LocalTime: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213
- 초는 밀리초, 나노초 단위도 포함할 수 있다.
- LocalDateTime: LocalDate 와 LocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213
앞에 Local (현지의, 특정 지역의)이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문입니다.
특정 지역의 날짜와 시간만 고려할 때 사용합니다.
기본 날짜와 시간 - LocalDate
import java.time.LocalDate;
public class LocalDateMain {
public static void main(String[] args) {
LocalDate nowDate = LocalDate.now();
LocalDate ofDate = LocalDate.of(2013, 11, 21);
System.out.println("오늘 날짜: " + nowDate);
System.out.println("지정 날짜: " + ofDate);
// 계산(불변)
LocalDate plusDays = ofDate.plusDays(10);
System.out.println("지정 날짜+10d: " + plusDays);
}
}
오늘 날짜: 2024-04-12
지정 날짜: 2013-11-21
지정 날짜+10d: 2013-12-01
생성
- now( ): 현재 시간을 기준으로 생성한다.
- of(...): 특정 날짜를 기준으로 생성한다. (년,월,일을 입력할 수 있다.)
계산
- plusDays( ): 특정 일을 더한다. 다양한 plusXxx( ) 메서드가 존재한다.
불변
- 모든 날짜 클래스는 불변이다.
- 따라서 변경이 발생하는 경우 새로운 객체를 생성해서 반환하므로 반환값을 꼭 받아야 한다.
LocalTime
import java.time.LocalTime;
public class LocalTimeMain {
public static void main(String[] args) {
LocalTime nowTime = LocalTime.now();
LocalTime ofTime = LocalTime.of(9, 10, 30);
System.out.println("현재 시간: " + nowTime);
System.out.println("지정 시간: " + ofTime);
// 계산(불변)
LocalTime ofTimePlus = ofTime.plusSeconds(30);
System.out.println("지정 시간+30s: " + ofTimePlus);
}
}
현재 시간: 19:22:55.750747400
지정 시간: 09:10:30
지정 시간+30s: 09:11
LocalDateTime
LocalDateTime 은 LocalDate 와 LocalTime 을 내부에 가지고 날짜와 시간을 모두 표현한다
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class LocalDateTimeMain {
public static void main(String[] args) {
LocalDateTime nowDt = LocalDateTime.now();
LocalDateTime ofDt = LocalDateTime.of(2016, 8, 16, 8, 10, 1);
System.out.println("현재 날짜 시간: " + nowDt);
System.out.println("지정 날짜 시간: " + ofDt);
// 날짜와 시간 분리
LocalDate localDate = ofDt.toLocalDate();
LocalTime localTime = ofDt.toLocalTime();
System.out.println("localDate = " + localDate);
System.out.println("localTime = " + localTime);
// 날짜와 시간 합체
LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
System.out.println("localDateTime = " + localDateTime);
// 계산(불변)
LocalDateTime ofDtPlus = ofDt.plusDays(1000);
System.out.println("지정 날짜+1000d: " + ofDtPlus);
LocalDateTime ofDtPlus1Year = ofDt.plusYears(1);
System.out.println("지정 날짜+1년: " + ofDtPlus1Year);
// 비교
System.out.println("현재 날짜시간이 지정 날짜시간보다 이전인가?" + nowDt.isBefore(ofDt));
System.out.println("현재 날짜시간이 지정 날짜시간보다 이후인가?" + nowDt.isAfter(ofDt));
System.out.println("현재 날짜시간과 지정 날짜시간이 같은가?" + nowDt.isEqual(ofDt));
}
}
현재 날짜 시간: 2024-04-12T19:33:16.977548200
지정 날짜 시간: 2016-08-16T08:10:01
localDate = 2016-08-16
localTime = 08:10:01
localDateTime = 2016-08-16T08:10:01
지정 날짜+1000d: 2019-05-13T08:10:01
지정 날짜+1년: 2017-08-16T08:10:01
현재 날짜시간이 지정 날짜시간보다 이전인가? false
현재 날짜시간이 지정 날짜시간보다 이후인가? true
현재 날짜시간과 지정 날짜시간이 같은가? false
생성
- now() : 현재 날짜와 시간을 기준으로 생성한다.
- of(...) : 특정 날짜와 시간을 기준으로 생성한다.
분리
- 날짜( LocalDate )와 시간( LocalTime )을 toXxx() 메서드로 분리할 수 있다.
합체
- LocalDateTime.of(localDate, localTime)
- 날짜와 시간을 사용해서 LocalDateTime 을 만든다.
계산
- plusXxx() : 특정 날짜와 시간을 더한다. 다양한 plusXxx() 메서드가 존재한다.
비교
- isBefore(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이전이라면 true 를 반환한다.
- isAfter(): 다른 날짜시간과 비교한다. 현재 날짜와 시간이 이후라면 true 를 반환한다.
- isEquals(): 다른 날짜시간과 시간적으로 동일한지 비교한다. 시간이 같으면 true 를 반환한다.
isEquals( ) vs equals( )
- isEquals( ) 는 단순히 비교 대상이 시간적으로 같으면 true 를 반환한다. 객체가 다르고, 타임존이 달라도 시
간적으로 같으면 true 를 반환한다. 쉽게 이야기해서 시간을 계산해서 시간으로만 둘을 비교한다.
예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 true 를 반환한다. - equals( ) 객체의 타입, 타임존 등등 내부 데이터의 모든 구성요소가 같아야 true 를 반환한다.
예) 서울의 9시와 UTC의 0시는 시간적으로 같다. 이 둘을 비교하면 타임존의 데이터가 다르기 때문에
false 를 반환한다.
ZonedDateTime, OffsetDateTime
- ZonedDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이
포함된다. - OffsetDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터
의 시간대 차이인 고정된 오프셋만 포함한다.
Zoneld
자바는 타임존을 ZoneId 클래스로 제공하고 있습니다.
import java.time.ZoneId;
public class ZoneIdMain {
public static void main(String[] args) {
for (String availableZoneIds : ZoneId.getAvailableZoneIds()) {
//System.out.println("availableZoneIds = " + availableZoneIds);
//ZoneId zoneId = ZoneId.of("Asia/Seoul");
ZoneId zoneId = ZoneId.of(availableZoneIds);
System.out.println(zoneId + " | " + zoneId.getRules());
}
ZoneId zoneId = ZoneId.systemDefault();
System.out.println("zoneId.systemDefault = " + zoneId);
ZoneId seoulZoneId = ZoneId.of("Asia/Seoul");
System.out.println("seoulZoneId = " + seoulZoneId);
}
}
Asia/Aden | ZoneRules[currentStandardOffset=+03:00]
America/Cuiaba | ZoneRules[currentStandardOffset=-04:00]
Etc/GMT+9 | ZoneRules[currentStandardOffset=-09:00]
Etc/GMT+8 | ZoneRules[currentStandardOffset=-08:00]
Africa/Nairobi | ZoneRules[currentStandardOffset=+03:00]
America/Marigot | ZoneRules[currentStandardOffset=-04:00]
...
zoneId.systemDefault = Asia/Seoul
seoulZoneId = Asia/Seoul
생성
- ZoneId.systemDefault( ): 시스템이 사용하는 기본 ZoneId를 반환한다.
- ZoneId.of( ): 타임존을 직접 제공해서 ZoneId를 반환한다.
ZoneId는 내부에 일광 절약 시간 관련 정보, UTC와의 오프셋 정보를 포함하고 있습니다.
ZonedDateTime
ZonedDateTime은 LocalDateTime 에 시간대 정보인 ZoneId 가 합쳐진 것입니다.
public class ZonedDateTime {
private final LocalDateTime dateTime;
private final ZoneOffset offset;
private final ZoneId zone;
}
ZonedDateTIme: 시간대를 고려한 날짜와 시간을 표현할 때 사용합니다. 여기에 시간대를 표현하는 타임존이 포함됩니다.
- 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]
- +9:00은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라고 한다.
- Asia/Seoul은 타임존이라고 한다. 타임존을 알면 오프셋도 알 수 있다. +9:00 같은 오프셋 정보도 타임존에 포함된다.
- 추가로 ZoneId를 통해 타임존을 알면 일광 절약 시간제에 대한 정보도 알 수 있다.
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class ZonedDateTimeMain {
public static void main(String[] args) {
ZonedDateTime nowZdt = ZonedDateTime.now();
System.out.println("nowZdt = " + nowZdt);
LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50);
ZonedDateTime zdt1 = ZonedDateTime.of(ldt, ZoneId.of("Asia/Seoul"));
System.out.println("zdt1 = " + zdt1);
ZonedDateTime zdt2 = ZonedDateTime.of(2030, 1, 1, 13, 30, 50, 0, ZoneId.of("Asia/Seoul"));
System.out.println("zdt2 = " + zdt2);
ZonedDateTime utcZdt = zdt2.withZoneSameInstant(ZoneId.of("UTC"));
System.out.println("utcZdt = " + utcZdt);
}
}
nowZdt = 2024-04-15T16:38:48.957650600+09:00[Asia/Seoul]
zdt1 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
zdt2 = 2030-01-01T13:30:50+09:00[Asia/Seoul]
utcZdt = 2030-01-01T04:30:50Z[UTC]
생성
- now( ): 현재 날짜와 시간을 기준으로 생성한다. 이 때 ZoneId는 현재 시스템을 따른다.(ZoneId.systemDefault( ))
- of( ): 특정 날짜와 시간을 기준으로 생성한다. ZoneId를 추가해야 한다.
타임존 변경
- withZoneSameInstant(ZoneId): 타임존을 변경한다. 타임존에 맞추어 시간도 함께 변경된다. 이 메서드를 사용하면 지금 다른 나라는 몇 시 인지 확인할 수 있다.
OffsetDateTime
OffsetDateTime은 LocalDateTime에 UTC 오프셋 정보인 ZoneOffset 이 합쳐진 것입니다.
public class OffsetDateTime {
private final LocalDateTime dateTime;
private final ZoneOffset offset;
}
OffsetDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터의 시간대 차이인 고정된 오프셋만 포함된다.
- 에) 2013-11-21T08:20:30.213+9:00
- ZoneId가 없으므로 일광 절약 시간제가 적용되지 않는다.
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public class OffsetDateTimeMain {
public static void main(String[] args) {
OffsetDateTime nowOdt = OffsetDateTime.now();
System.out.println("nowOdt = " + nowOdt);
LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50);
System.out.println("ldt = " + ldt);
OffsetDateTime odt = OffsetDateTime.of(ldt, ZoneOffset.of("+01:00"));
System.out.println("odt = " + odt);
}
}
nowOdt = 2024-04-15T16:49:40.271583500+09:00
ldt = 2030-01-01T13:30:50
odt = 2030-01-01T13:30:50+01:00
ZoneOffset은 +01:00 처럼 UTC와의 시간 차이인 오프셋 정보만 보관합니다.
ZonedDateTime vs OffsetDateTime
- ZonedDateTime은 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다. 사용자 지정 시간대에 따른 시간 계산이 필요할 때 적합하다.
- OffsetDateTime은 UTC와의 시간 차이만을 나타날 때 사용하며, 지역 시간대의 복잡성을 고려하지 않는다. 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 때 적합하다.
Year, Month, YearMonth, MonthDay
년, 월, 년월, 달일을 각각 다룰 때 사용한다. 자주 사용하지는 않습니다.
DayOfWeek 와 같이 월, 화, 수, 목, 금, 토, 일을 나타내는 클래스도 있습니다
Instant
Instant 는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타냅니다.
Instant 는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC)를 기준으로
경과한 시간으로 계산됩니다. 쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있습니다. (나노초 포함)
따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않습니다.
public class Instant {
private final long seconds;
private final int nanos;
...
}
- UTC 기준 1970년 1월 1일 0시 0분 0초라면 seconds 에 0이 들어간다.
- UTC 기준 1970년 1월 1일 0시 0분 10초라면 seconds 에 10이 들어간다.
- UTC 기준 1970년 1월 1일 0시 1분 0초라면 seconds 에 60이 들어간다.
Instant 특징
장점
- 시간대 독립성: Instant는 UTC를 기준으로 하므로, 시간대에 영향을 받지 않는다. 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용하다.
- 고정된 기준점: 모든 Instant는 1970년 1월 1일 UTC를 기준으로 하기 때문에, 시간 계산 및 비교가 명확하고 일관된다.
단점
- 사용자 친화적이지 않음: Instant는 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다.
- 시간대 정보 부재: Instant에는 시간대 정보가 포함되지 않아, 특정 지역의 날짜와 시간으로 변환하려면 추가적인 작업이 필요하다.
사용 예
- 전 세계적인 시간 기준 필요 시 (로그 기록, 트랜잭션 타임스탬프, 서버간의 시간 동기화 등)
- 시간대 변환 없이 시간 계산 필요 시
- 데이터 저장 및 교환 (데이터의 일관성 유지)
import java.time.Instant;
import java.time.ZonedDateTime;
public class InstantMain {
public static void main(String[] args) {
// 생성
Instant now = Instant.now();
System.out.println("now = " + now);
ZonedDateTime zdt = ZonedDateTime.now();
Instant from = Instant.from(zdt);
System.out.println("from = " + from);
Instant epochStart = Instant.ofEpochSecond(0);
System.out.println("epochStart = " + epochStart);
// 계산
Instant later = epochStart.plusSeconds(3600);
System.out.println("later = " + later);
// 조회
long laterEpochSecond = later.getEpochSecond();
System.out.println("laterEpochSecond = " + laterEpochSecond);
}
}
now = 2024-04-15T11:14:13.040083800Z
from = 2024-04-15T11:14:13.067475300Z
epochStart = 1970-01-01T00:00:00Z
later = 1970-01-01T01:00:00Z
laterEpochSecond = 3600
생성
- now( ): UTC를 기준 현재 시간의 Instant를 생성한다.
- from( ): 다른 타입의 날짜와 시간을 기준으로 Instant를 생성한다. 참고로 Instant는 UTC를 기준으로 하기 때문에 시간대 정보가 필요하다. 따라서 LocalDateTime은 사용할 수 없다.
- ofEpochSecond( ): 에포크 시간을 기준으로 Instant를 생성한다. 0초로 선택하면 에포크 시간인 1970년 1월 1일 0시 0분 0초로 생성된다
계산
- plusSeconds( ): 초를 더한다.
조회
- getEpochSecond( ): 에포크 시간인 UTC를 기준으로 흐른 초를 반환한다.
- 여기서는 앞서 에포크 시간에 3600초를 더했기 때문에 3600이 반환된다.
Period, Duration
시간의 개념은 크게 2가지로 표현할 수 있습니다.
특정 시점의 시간(시각)
- 이 프로젝트는 2013년 8월 16일 까지 완료해야해
- 다음 회의는 11시 30분에 진행한다.
- 내 생일은 8월 16일이야.
시간의 간격(기간)
- 앞으로 4년은 더 공부해야 해
- 이 프로젝트는 3개월 남았어
- 라면은 3분 동안 끓어야 해
Period , Duration 은 시간의 간격(기간)을 표현하는데 사용됩니다.
시간의 간격은 영어로 amount of time(시간의 양)으로 불립니다.
Period
두 날짜 사이의 간격을 년, 월, 일 단위로 나타냅니다.
public class Period {
private final int years;
private final int months;
private final int days;
}
import java.time.LocalDate;
import java.time.Period;
public class PeriodMain {
public static void main(String[] args) {
// 생성
Period period = Period.ofDays(10);
System.out.println("period = " + period);
// 계산에 사용
LocalDate currentDate = LocalDate.of(2030, 1, 1);
LocalDate plusDate = currentDate.plus(period);
System.out.println("currentDate = " + currentDate);
System.out.println("plusDate = " + plusDate);
// 기간 차이
LocalDate startDate = LocalDate.of(2023, 1, 1);
LocalDate endDate = LocalDate.of(2023, 4, 2);
Period between = Period.between(startDate, endDate);
System.out.println("between = " + between);
System.out.println("기간: " + between.getMonths() + "개월 " + between.getDays() + "일");
}
}
period = P10D
currentDate = 2030-01-01
plusDate = 2030-01-11
between = P3M1D
기간: 3개월 1일
생성
- of( ): 특정 기간을 지정해서 Period를 생성한다.
- of(년, 월, 일)
- ofDays( )
- ofMonths( )
- ofYears( )
계산에 사용
- 2030년 1월 1일에 10일을 더하면 2030년 1월 11일이 된다. 라고 표현할 때 특정 날짜에 10일이라는 기간을 더할 수 있다.
기간 차이
- 2023년 1월 1일과 2023년 4월 2일 간의 차이는 3개월 1일이다. 라고 표현할 때 특정 날짜의 차이를 구하면 기간이 된다.
- Period.between(startDate, endDate)와 같이 특정 날짜의 차이를 구하면 Period가 반환된다.
Duration
두 시간 사이의 간격을 시, 분, 초(나노초) 단위로 나타냅니다.
public class Duration {
private final long seconds;
private final int nanos;
}
내부에서 초를 기반으로 시, 분, 초를 계산해서 사용한다.
- 1분 = 60초
- 1시간 = 3600초
import java.time.Duration;
import java.time.LocalTime;
public class DurationMain {
public static void main(String[] args) {
Duration duration = Duration.ofMinutes(30);
System.out.println("duration = " + duration);
LocalTime lt = LocalTime.of(1, 0);
System.out.println("lt = " + lt);
// 계산에 사용
LocalTime plusTime = lt.plus(duration);
System.out.println("더한 시간: " + plusTime);
// 시간 차이
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(10, 0);
Duration between = Duration.between(start, end);
System.out.println("between = " + between);
System.out.println("차이: " + between.getSeconds() + "초");
System.out.println("근무 시간: " + between.toHours() + "시간" + between.toMinutesPart() + "분");
System.out.println("근무 시간: " + between.toHours() + "시간 or " + between.toMinutes() + "분");
}
}
duration = PT30M
lt = 01:00
더한 시간: 01:30
between = PT1H
차이: 3600초
근무 시간: 1시간0분
근무 시간: 1시간 or 60분
생성
- of( ): 특정 시간을 지정해서 Period를 생성한다.
- of(지정):
- ofSeconds( ):
- ofMinutes( ):
- ofHours( ):
계산에 사용
- 1:00에 30분을 더하면 1:30이 된다. 라고 표현할 때 특정 시간에 30분이라는 시간(시간의 간격)을 더할 수 있다.
시간 차이
- 9시와 10시의 차이는 1시간이라고 표현할 때 시간의 차이를 구할 수 있다.
- Durantion(start, end) 와 같이 특정 시간의 차이를 구하면 Duration이 반환된다.
'Java' 카테고리의 다른 글
[Java] 중첩 클래스, 내부 클래스 (1) | 2024.04.18 |
---|---|
[Java] 날짜와 시간 파싱, 포맷팅 (0) | 2024.04.15 |
[Java] 타입 안전 열거형 패턴 (Type-Safe-Enum Pattern) (0) | 2024.04.11 |
[Java] Math, Random 클래스 (0) | 2024.04.11 |
[Java] System 클래스 (0) | 2024.04.11 |