[Java] 날짜 포멧 라이브러리 LocalDateTime 그리고 어노테이션(@DateTimeFormat vs @JsonFormat) 정리

devdo·2022년 11월 14일
0

Java

목록 보기
54/61
post-thumbnail

자바 날짜 라이브러리

자바8 Date와 Time API

자바 8에 새로운 날짜와 시간 API가 생긴 이유

  • 그전까지 사용하던 java.util.Date 클래스는 mutable하기 때문에 thead safe하지 않다.
  • 클래스 이름이 명확하지 않다. Date인데 시간까지 다룬다.
  • 버그 발생할 여지가 많다. (타입 안정성이 없고, 월이 0부터 시작한다거나..)
  • 복잡한 날짜 처리가 짜증나서 애플리케이션에서는 보통 Joda Time 라이브러리를 쓰곤했다.

현재, Joda-Time을 만든 개발자를 대려와서 JSR-310( java.time )이라는 새로운 자바 표준 날짜와 시간 라이브러리를 정의했다.

실용적인 Joda-Time에 많은 자바 커뮤니티의 의견을 반영해서 좀 더 안정적이고 표준적인 날짜와 시간 라이브러리인 java.time 패키지가 성공적으로 완성되었다.

Java 8에서 추가된 LocalDateTime 클래스는 특정 시점을 표현하는데 사용됩니다.


Date, Calendar와 비교

  • 편리성: LocalDateTime은 날짜와 시간을 직접 관리하기 때문에 java.util.Date와 java.util.Calendar와 같은 클래스보다 보다 사용하기 편리하고 정확한 결과를 제공할 수 있습니다.
    LocalDateTime은 날짜와 시간을 따로 표현하지 않고 하나의 클래스로 표현합니다. 이를 통해 날짜와 시간을 하나로 표현하고 관리하는 것이 편리합니다.

LocalDate, LocalDateTime의 메서드들이 훨씬 더 많아.

  • 보안성: LocalDateTime은 스레드에서 안정적으로 사용할 수 있는 자바 8의 새로운 날짜와 시간 API의 일부입니다. 따라서 스레드에서 동시에 날짜와 시간을 관리할 때 같은 객체를 여러 스레드에서 동시에 수정할 경우 발생할 수 있는 문제를 피할 수 있습니다.

타임존 계산

세계는 다양한 타임존으로 나뉘어 있으며, 각 타임존은 UTC(협정 세계시)로부터의 시간 차이로 정의된다. 타임존 간의 날짜와 시간 변환을 정확히 계산하는 것은 복잡하다.

타임존의 기준 UTC

UTC(협정 세계시, Universal Time Coordinated)

역사적으로 GMT가 국제적인 시간 표준으로 사용되었고, UTC가 나중에 이를 대체하기 위해 도입되었다.
둘 다 경도 0°에 위치한 영국의 그리니치 천문대를 기준으로 하며, 이로 인해 실질적으로 같은 시간대를 나타낸다. 그러나 두 시간 체계는 시간을 정의하고 유지하는 방법에서 차이가 있다.
UTC는 원자 시계를 사용하여 측정한 국제적으로 합의된 시간 체계이다. 지구의 자전 속도가 변화하는 것을 고려하여 윤초를 추가하거나 빼는 방식으로 시간을 조정함으로써, 보다 정확한 시간을 유지한다. 우리가 일반적으로 사용할 때는 GMT와 UTC는 거의 차이가 없기 때문에 GMT와 UTC가 종종 같은 의미로 사용될 수 있지만, 정밀한 시간 측정과 국제적인 표준에 관해서는 UTC가 선호된다.

✳️ MySQL > LocalDateTime vs TimeStamp 비교

시스템 종속성: Timestamp 타입은 타임존(timezone)에 종속적입니다. 따라서 타임존을 변경할 경우에도 Timestamp 타입이 영향을 받습니다.

LocalDateTime 타입은 타임존의 영향을 받지 않기 때문에, 데이터베이스와 같은 외부 시스템과의 연동에서 보다 안정적인 날짜와 시간 표현이 가능합니다.

  • 편의성: LocalDateTime 타입은 날짜와 시간을 분리하여 관리할 수 있습니다. 또한, 날짜와 시간 연산이 쉽고 간결하다는 점이 장점입니다.

  • 보안성: Timestamp 타입은 시스템 시간을 사용하기 때문에, 시스템 시간을 변경하는 공격에 취약할 수 있습니다. LocalDateTime 타입은 JVM 안에서 처리되기 때문에, 외부 공격에서는 영향을 받지 않습니다.

주요 API

참고) 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-21
    • LocalTime: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213
    • LocalDateTime: LocalDate 와 LocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213
  • 기간을 표현할 때는 Duration (시간 기반)과 Period (날짜 기반)를 사용할 수 있다.

  • DateTimeFormatter를 사용해서 일시를 특정한 문자열로 포매팅할 수 있다.

  • 앞에 Local (현지의, 특정 지역의)이 붙는 이유는 세계 시간대를 고려하지 않아서 타임존이 적용되지 않기 때문이다. 특정 지역의 날짜와 시간만 고려할 때 사용한다.
    애플리케이션 개발시 국내 서비스만 고려할 때만 사용해야 한다.
    예) 나의 생일은 2016년 8월 16일이야.

참고)


ZonedDateTime, OffsetDateTime

ZonedDateTime

"Asia/Seoul" 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로 부터 시간 차이인 오프셋 정보를 모두 포함하고 있다.

  • 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul]

  • +9:00 은 UTC(협정 세계시)로 부터의 시간대 차이이다. 오프셋이라 한다. 한국은 UTC보다 +9:00 시간이다.

  • Asia/Seoul 은 타임존이라 한다. 이 타임존을 알면 오프셋과 일광 절약 시간제에 대한 정보를 알 수 있다.

  • 일광 절약 시간제가 적용된다.

OffsetDateTime

시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 타임존은 없고, UTC로 부터
의 시간대 차이인 고정된 오프셋만 포함한다.

  • 예) 2013-11-21T08:20:30.213+9:00
  • 일광 절약 시간제가 적용되지 않는다.

Asia/Seoul 같은 타임존 안에는 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로 부터 시간 차이인 오프셋 정보를 모두 포함하고 있다.
일광 절약 시간제(DST, 썸머타임)을 알려면 타임존을 알아야 한다. 따라서 ZonedDateTime 은 일광 절약 시간제를함께 처리한다. 반면에 타임존을 알 수 없는 OffsetDateTime 는 일광 절약 시간제를 처리하지 못한다.
ZonedDateTime 은 시간대를 고려해야 할 때 실제 사용하는 날짜와 시간 정보를 나타내는 데 더 적합하고, OffsetDateTime 은 UTC로부터의 고정된 오프셋만을 고려해야 할 때 유용하다


기계 중심의 시간 - Instant

Instant 는 UTC(협정 세계시)를 기준으로 하는, 시간의 한 지점을 나타낸다. Instant 는 날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC 기준)를 기준으로 경과한 시간으로 계산된다.

쉽게 이야기해서 Instant 내부에는 초 데이터만 들어있다. (나노초 포함)
따라서 날짜와 시간을 계산에 사용할 때는 적합하지 않다

사용 예

  • 전 세계적인 시간 기준 필요 시
    Instant 는 UTC를 기준으로 하므로, 전 세계적으로 일관된 시점을 표현할 때사용하기 좋다. 예를 들어, 로그 기록이나, 트랜잭션 타임스탬프, 서버 간의 시간 동기화 등이 이에 해당한다.

  • 시간대 변환 없이 시간 계산 필요 시
    시간대의 변화 없이 순수하게 시간의 흐름(예: 지속 시간 계산)만을 다루고
    싶을 때 Instant 가 적합하다. 이는 시간대 변환의 복잡성 없이 시간 계산을 할 수 있게 해준다.

  • 데이터 저장 및 교환
    데이터베이스에 날짜와 시간 정보를 저장하거나, 다른 시스템과 날짜와 시간 정보를 교환할
    때 Instant 를 사용하면, 모든 시스템에서 동일한 기준점(UTC)을 사용하게 되므로 데이터의 일관성을 유지하기 쉽다.

일반적으로 날짜와 시간을 사용할 때는 LocalDateTime , ZonedDateTime 등을 사용하면 된다. Instant 는 날짜를 계산하기 어렵기 때문에 앞서 사용 예와 같은 특별한 경우에 한정해서 사용하면 된다.



Code

자바8 이전에 Date API와 포멧 라이브러리 SimpleDateFormat 그리고 Calendlar를 살펴보자.

SimpleDateFormat

: 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));

Calendar 활용

: 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);

활용 예시
https://github.com/mooh2jj/board_mybatis/blob/master/src/main/java/com/example/board_springboot/common/scheduler/FileCheckTask.java

    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);
    }

LocalDateTime

: String <-> LocalDateTime

Request: Parsing 메서드 구현(String)

  • 최신 방식(업데이트)
    // 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;
    }

Response: T 붙이는 건 머야?

간혹 시간 필드에서 응답시 이런 값을 붙이면서 보내주는 경우가 있다.
이부분은 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;

@PastOrPresent or @FutureOrPresent 는 머야?

이 어노테이션이 적용된 필드의 값은 현재 또는 과거(or미래)의 시점에 해당되는 날짜 또는 시간이어야 하는 validation이다!

@PastOrPresent 
private LocalDateTime startTime;

유효기간 체크(isBefore() & isAfter())

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();
      }

between (startDay: 00:00:00 ~ endDay: 23:59:59)

간격을 두고 사이에 있는 조회하고 싶으면 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"));
    }

Date <-> LocalDateTime

참고 - 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

  • ZonedDateTime 은 LocalDateTime 에 시간대 정보인 ZoneId 가 합쳐진 것이다

  • ZonedDateTime 클래스

public class ZonedDateTime {
 private final LocalDateTime dateTime;
 private final ZoneOffset offset;
 private final ZoneId zone;
}
  • ZoneId 는 내부에 일광 절약 시간 관련 정보, UTC와의 오프셋 정보를 포함하고 있다
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]
  • 타임존 변경
    withZoneSameInstant(ZoneId) : 타임존을 변경한다. 타임존에 맞추어 시간도 함께 변경된다. 이 메서드를 사용하면 지금 다른 나라는 몇 시 인지 확인일 수 있다. 예를 들어서 서울이 지금 9시라면, UTC 타임존으로 변경하면 0시를 확인할 수 있다.

@JsonFormat vs @DateTimeFormat

스프링에서 어노테이션(@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;
  • 서버 Get 요청 @ModelAttribute에는 @DateTimeFormat 사용
  • 서버 Post 요청 @RequestBody에는 JSON 객체를 @DateTimeFormat과 @JsonFormat 로 모두 사용 가능
  • 2개의 어노테이션 모두가 있으면 선언 시에는 @JsonFormat이 먼저 적용
  • 서버 응답 시 ResponseBody에서는 JSON으로 직렬화하는 @JsonFormat 만 적용
    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 사용하자


참고

날짜 어노테이션

profile
배운 것을 기록합니다.

0개의 댓글