자바 8에 새로운 날짜와 시간 API가 생긴 이유
mutable
하기 때문에 thead safe하지 않다.타입 안정성
이 없고, 월이 0부터 시작한다거나..
)현재, Joda-Time을 만든 개발자를 대려와서 JSR-310( java.time )이라는 새로운 자바 표준 날짜와 시간 라이브러리를 정의했다.
실용적인 Joda-Time에 많은 자바 커뮤니티의 의견을 반영해서 좀 더 안정적이고 표준적인 날짜와 시간 라이브러리인 java.time 패키지가 성공적으로 완성되었다.
Java 8에서 추가된 LocalDateTime 클래스는 특정 시점을 표현하는데 사용됩니다.
LocalDate, LocalDateTime의 메서드들이 훨씬 더 많아.
세계는 다양한 타임존으로 나뉘어 있으며, 각 타임존은 UTC(협정 세계시)로부터의 시간 차이로 정의된다. 타임존 간의 날짜와 시간 변환을 정확히 계산하는 것은 복잡하다.
UTC(협정 세계시, Universal Time Coordinated)
역사적으로 GMT가 국제적인 시간 표준으로 사용되었고, UTC가 나중에 이를 대체하기 위해 도입되었다.
둘 다 경도 0°에 위치한 영국의 그리니치 천문대를 기준으로 하며, 이로 인해 실질적으로 같은 시간대를 나타낸다. 그러나 두 시간 체계는 시간을 정의하고 유지하는 방법에서 차이가 있다.
UTC는 원자 시계를 사용하여 측정한 국제적으로 합의된 시간 체계이다. 지구의 자전 속도가 변화하는 것을 고려하여 윤초를 추가하거나 빼는 방식으로 시간을 조정함으로써, 보다 정확한 시간을 유지한다. 우리가 일반적으로 사용할 때는 GMT와 UTC는 거의 차이가 없기 때문에 GMT와 UTC가 종종 같은 의미로 사용될 수 있지만, 정밀한 시간 측정과 국제적인 표준에 관해서는 UTC가 선호된다.
시스템 종속성: Timestamp 타입은 타임존(timezone)에 종속적
입니다. 따라서 타임존을 변경할 경우에도 Timestamp 타입이 영향을 받습니다.
LocalDateTime 타입은 타임존의 영향을 받지 않기 때문에
, 데이터베이스와 같은 외부 시스템과의 연동에서 보다 안정적인 날짜와 시간 표현이 가능합니다.
편의성: LocalDateTime 타입은 날짜와 시간을 분리하여 관리할 수 있습니다. 또한, 날짜와 시간 연산이 쉽고 간결하다는 점이 장점입니다.
보안성: Timestamp 타입은 시스템 시간을 사용하기 때문에, 시스템 시간을 변경하는 공격에 취약할 수 있습니다. LocalDateTime 타입은 JVM 안에서 처리되기 때문에, 외부 공격에서는 영향을 받지 않습니다.
참고) https://docs.oracle.com/javase/tutorial/datetime/iso/overview.html
기계용 시간 (machine time)
과 인류용 시간(human time)
으로 나눌 수 있다.
기계용 시간은 EPOCK (1970년 1월 1일 0시 0분 0초)부터 현재까지의 타임스탬프를 표현한다.
인류용 시간은 우리가 흔히 사용하는 연,월,일,시,분,초
등을 표현한다.
타임스탬프는 Instant를 사용한다.
특정 날짜(LocalDate), 시간(LocalTime), 일시(LocalDateTime)를 사용할 수 있다.
LocalDate
: : 날짜만 표현할 때 사용한다. 년, 월, 일을 다룬다. 예) 2013-11-21LocalTime
: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213LocalDateTime
: LocalDate 와 LocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213기간을 표현할 때는 Duration (시간 기반)과 Period (날짜 기반)를 사용할 수 있다.
DateTimeFormatter를 사용해서 일시를 특정한 문자열로 포매팅할 수 있다.
앞에 Local (현지의, 특정 지역의)
이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문이다. 특정 지역의 날짜와 시간만 고려할 때 사용한다.
애플리케이션 개발시 국내 서비스만 고려할 때만 사용해야 한다.
예) 나의 생일은 2016년 8월 16일이야.
참고)
"Asia/Seoul" 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로 부터 시간 차이인 오프셋 정보를 모두 포함하고 있다.
예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]
+9:00 은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라 한다. 한국은 UTC보다 +9:00 시간이다.
Asia/Seoul 은 타임존이라 한다. 이 타임존을 알면 오프셋과 일광 절약 시간제에 대한 정보를 알 수 있다.
일광 절약 시간제가 적용된다.
시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터
의 시간대 차이인 고정된 오프셋만 포함한다.
Asia/Seoul 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로 부터 시간 차이인 오프셋 정보를 모두 포함하고 있다.
일광 절약 시간제(DST, 썸머타임)을 알려면 타임존을 알아야 한다. 따라서 ZonedDateTime 은 일광 절약 시간제를함께 처리한다. 반면에 타임존을 알 수 없는 OffsetDateTime 는 일광 절약 시간제를 처리하지 못한다.
ZonedDateTime 은 시간대를 고려해야 할 때 실제 사용하는 날짜와 시간 정보를 나타내는 데 더 적합하고, OffsetDateTime 은 UTC로부터의 고정된 오프셋만을 고려해야 할 때 유용하다
Instant 는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타낸다. Instant 는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC 기준)를 기준으로 경과한 시간으로 계산된다.
쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다. (나노초 포함)
따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않다
전 세계적인 시간 기준 필요 시
Instant 는 UTC를 기준으로 하므로, 전 세계적으로 일관된 시점을 표현할 때사용하기 좋다. 예를 들어, 로그 기록이나, 트랜잭션 타임스탬프, 서버 간의 시간 동기화 등이 이에 해당한다.
시간대 변환 없이 시간 계산 필요 시
시간대의 변화 없이 순수하게 시간의 흐름(예: 지속 시간 계산)만을 다루고
싶을 때 Instant 가 적합하다. 이는 시간대 변환의 복잡성 없이 시간 계산을 할 수 있게 해준다.
데이터 저장 및 교환
데이터베이스에 날짜와 시간 정보를 저장하거나, 다른 시스템과 날짜와 시간 정보를 교환할
때 Instant 를 사용하면, 모든 시스템에서 동일한 기준점(UTC)을 사용하게 되므로 데이터의 일관성을 유지하기 쉽다.
일반적으로 날짜와 시간을 사용할 때는 LocalDateTime , ZonedDateTime 등을 사용하면 된다. Instant 는 날짜를 계산하기 어렵기 때문에 앞서 사용 예와 같은 특별한 경우에 한정해서 사용하면 된다.
자바8 이전에 Date API와 포멧 라이브러리 SimpleDateFormat 그리고 Calendlar를 살펴보자.
: String <-> Date
// String <-> Date
Date nowDate = new Date();
System.out.println("포맷 지정 전 : " + nowDate);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy년 MM월 dd일");
//원하는 데이터 포맷 지정
String strNowDate = simpleDateFormat.format(nowDate);
System.out.println("포맷 지정 후 : " + strNowDate);
// 일반 현재 날짜 및 시간
//long 타입으로 System.currentTimeMillis() 데이터를 받아야합니다
long time = System.currentTimeMillis();
SimpleDateFormat dayTime = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss E요일");
String str = dayTime.format(new Date(time));
// 24시 형태 현재 날짜 및 시간
long time = System.currentTimeMillis();
SimpleDateFormat dayTime = new SimpleDateFormat("yyyy.MM.dd kk:mm:ss E요일");
String str = dayTime.format(new Date(time));
: String에서 <-> Calendar
SimpleDateFormat -> Calendar 클래스 -> String 를 이용하여 타입 변환을 할 수 있다.
✔ 주의사항)
년, 월, 일 -1 해줘야 한다!
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class DateAdd {
public static void main(String[] args) throws Exception{
String date = "20200801";
//1년 후 날짜
String addYear = AddDate(date, 1, 0, 0);
//1달 후 날짜
String addMonth = AddDate(date, 0, 1, 0);
//1일 후 날짜
String addDay = AddDate(date, 0, 0, 1);
System.out.println(addYear); //20210801
System.out.println(addMonth); //20200901
System.out.println(addDay); //20200802
}
private static String AddDate(String strDate, int year, int month, int day) throws Exception {
SimpleDateFormat dtFormat = new SimpleDateFormat("yyyyMMdd");
Calendar cal = Calendar.getInstance();
Date dt = dtFormat.parse(strDate);
cal.setTime(dt);
cal.add(Calendar.YEAR, year);
cal.add(Calendar.MONTH, month);
cal.add(Calendar.DATE, day);
return dtFormat.format(cal.getTime());
}
}
위의 예제와 같이 Calendar 객체의 add() 메서드를 사용하면 원하는 날짜를 계산할 수 있다.
만약 특정일 기준으로 원하는 날짜만큼 빼려면
원하는 만큼의 년, 월, 일에 -(마이너스)를 붙여 add 메서드 파라미터에 세팅한다.
//year 년 전
cal.add(Calendar.YEAR, -year);
//month 월 전
cal.add(Calendar.MONTH, -month);
//month 일 전
cal.add(Calendar.DATE, -day);
Or
//위의 메소드에 적용하면 다음과 같이 파라메터를 세팅하면된다.
//일년 전
AddDate(date, -1, 0, 0);
//한달 전
AddDate(date, 0, -1, 0);
//하루 전
AddDate(date, 0, 0, -1);
private String getFolderYesterDay() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
String str = sdf.format(cal.getTime());
return str.replace("-", File.separator);
}
: String <-> LocalDateTime
// String -> LocalDateTime
// @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
// @JsonFormat -> POST 방식일때!
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@PastOrPresent // 현재까지 입력가능
private LocalDateTime startAt;
// @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
@PastOrPresent // 현재까지 입력가능
private LocalDateTime endAt;
private String startDate;
private String endDate;
// Stirng -> LocalDateTime
public LocalDateTime getStartDate() {
if (StringUtils.hasText(startDate)) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
return LocalDateTime.parse(startDate + "T00:00:00", formatter);
}
return null;
}
public LocalDateTime getEndDate() {
if (StringUtils.hasText(endDate)) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
return LocalDateTime.parse(endDate + "T23:59:59", formatter);
}
return null;
}
간혹 시간 필드에서 응답시 이런 값을 붙이면서 보내주는 경우가 있다.
이부분은 Date와 Time 구분값
이라 생각해주면 된다!
단순 띄어쓰기에서 더 확실한 구분값을 짓기 위해서다.
pattern = "yyyy-MM-dd'T'HH:mm:ss"
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
이 어노테이션이 적용된 필드의 값은 현재 또는 과거(or미래)의 시점에 해당되는 날짜 또는 시간
이어야 하는 validation이다!
@PastOrPresent
private LocalDateTime startTime;
LocalDate a = LocalDateTime.of(2012, 6, 30, 12, 00);
LocalDate b = LocalDateTime.of(2012, 7, 1, 12, 00);
a.isBefore(b) == true // b 이전이냐?
a.isBefore(a) == false
b.isBefore(a) == false
// 2. 쿠폰 유효기간 체크
if(now.isBefore(formatterLocalDateTime(couponDto.getBeginDt())) ||
now.isAfter(formatterLocalDateTime(couponDto.getEndDt()))) {
return CouponDto.CouponDetails.builder()
.couponStatus(CouponDto.CouponStatus.UNABLE)
.build();
}
간격을 두고 사이에 있는 조회하고 싶으면 startDay에 00:00:00
, endDay에 23:59:59
를 두고 설정해주어야 한다. 설정해주는 코드는 다음과 같다.
public LocalDateTime getParsedSearchDate1() {
return LocalDateTime.parse(searchDate1 + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
public LocalDateTime getParsedSearchDate2() {
return LocalDateTime.parse(searchDate2 + " 23:59:59", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
참고 - https://hianna.tistory.com/613
Date를 LocalDate 또는 LocalDateTime 객체로 변환하기 위해서 다음의 3단계를 거칩니다.
1) Date -> Instant
2) Instant -> ZonedDateTime
3) ZonedDateTime -> LocalDate, LocalDateTime
// 날짜 변환
// Date -> LocalDateTime
Date date = new Date();
LocalDateTime dateTime = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
System.out.println(dateTime); // 2022-12-09T20:28:36.436
// LocalDateTime -> Date
LocalDateTime localDateTime = LocalDateTime.now();
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date date1 = Date.from(instant);
System.out.println(date1); // Fri Dec 09 20:28:36 KST 2022
ZonedDateTime 은 LocalDateTime 에 시간대 정보인 ZoneId 가 합쳐진 것이다
ZonedDateTime 클래스
public class ZonedDateTime {
private final LocalDateTime dateTime;
private final ZoneOffset offset;
private final ZoneId zone;
}
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);
// LocalDateTime에 타임존 추가하여 ZonedDateTime 생성
ZonedDateTime zdt1 = ZonedDateTime.of(ldt, ZoneId.of("Asia/Seoul"));
System.out.println("zdt1 = " + zdt1);
// ZonedDateTime 생성 (LocalDateTime 사용하지 않고 직접 생성)
ZonedDateTime zdt2 = ZonedDateTime.of(2030, 1, 1, 13, 30, 50, 0, ZoneId.of("Asia/Seoul"));
System.out.println("zdt2 = " + zdt2);
// 동일한 시간을 UTC 타임존으로 변환
ZonedDateTime utcZdt = zdt2.withZoneSameInstant(ZoneId.of("UTC"));
System.out.println("utcZdt = " + utcZdt);
}
}
결과
nowZdt = 2024-02-09T12:02:13.457712+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]
스프링에서 어노테이션(@JsonFormat, @DateTimeFormat)을 이용한 JSON 객체 Date타입을 String으로 직렬화하는 어노테이션들이다. 둘이 비슷한 기능인데, 이번 기회에 정리해 볼려고 한다.
사용 설명에 앞서 @JsonFormat, @DateTimeFormat에 대해 정확한 정보를 찾다가 아래에 포스팅을 찾아는데 설명이 잘 되어있어서 들어가서 한번 정독하고 사용하면 더 도움이 될 듯 하다.
[참고] https://jojoldu.tistory.com/361
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startDate;
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endDate;
Spring MVC에서는 @DateTimeFormat, Spring REST API시에는 @JsonFormat을 사용해야 되는 것 같은데?
Spring Boot 2.0에서는 JSR 310이 의존성 기본적으로 되어있음
그래서 아래와 같이 따로 의존성을 추가 할 필요가 없다.
compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310')
@JsonFormat은 Jackson의 어노테이션 > 우선순위가 더 높아
@DateTimeFormat은 Spring의 어노테이션
- (GET) @DateTimeFormat
- (POST) @DateTimeFormat or @JsonFormat 사용하자
날짜 어노테이션