이상을 꿈꾸는 몽상가.. 프로그래밍을 좋아함..


Java Date - Instant, LocalDateTime, ZonedDateTime

https://openjdk.java.net
( 이미지 출처 : https://openjdk.java.net )

JDK 8부터는 Instant, LocalDateTime , ZonedDateTime 등이 추가됐다. 이 들은 immutable하고 thread-safe 하기 때문에 더 편하고 안전하게 날짜와 시간을 다룰 수 있게 되었습니다.

Date의 대부분의 기능은 JDK 1.1부터 deprecated 되었고 JDK 7까지는 Calendar 혹은 GregorianCalendar를 이용해서 날짜와 시간을 다뤄왔지만 JDK 8부터는 그럴 필요가 없어졌습니다.

java.time package in JDK 8

JDK 8 에서는 dates, times, instants과 durations을 위해 java.time 패키지를 추가했다. 이 패키지에서 제공하는 모든 class들은 immutable하기 때문에 thread-safe합니다.

  • Instant : timestamp(UTC 1970-01-01T00:00:00Z 로부터 흐른 시간)를 다룹니다.

  • LocalDateTime : time-zone 을 제외한 date-time 값을 다룹니다.
  • LocalDate : time-zone 을 제외한 date 을 다룹니다.
  • LocalTime : time-zone 을 제외한 time 을 다룹니다.

  • ZonedDateTime : time-zone 을 포함한 date-time 값을 다룬다.

java.util.Date 의 문제점

d2.naver.com - Java의 날짜와 시간 API 글에서 java.util.Date 클래스의 문제점을 세세하게 설명해주고 있습니다.

  • 불변 객체가 아니다
  • int 상수 필드의 남용
  • 헷갈리는 월 지정
  • 일관성 없는 요일 상수
  • Date와 Calendar의 불편한 역할 분담
  • 오류에 둔감한 시간대 ID지정
  • java.util.Date 하위 클래스의 문제

그 중 가장 “불변 객체가 아니다”는 불편함을 넘어서 예상하기 힘든 기능 오류를 발생시킬 수 있습니다.

Mutable 객체

아래 코드의 출처는 “Effective Java 3/E 한글판 303 page, 아이템50” 입니다.

기간을 표현하는 아래와 같은 Period 클래스가 있습니다.

public final class Period {
  
    private final Date start;
    private final Date end;

    pulbic Period(Date start, Date end) {
        if (start.compareTo(end)>0) {
            throw new IllegalArgumentException(start + " is later than " + end);
        }

        this.start = start;
        this.end = end;
    }

    public Date start() {
        return start;
    }


    public Date end() {
        return end;
    }
    
    ...(생략)

}

Period 객체가 가진 Date 객체는 final도 적용되고 캡슐화 되어있어서 얼핏 불변처럼 보이고 시작 시각이 종료시각보다 늦을 수 없다는 불변식이 지켜질 수 있을 것 처럼 보입니다. 하지만 mutable한 Date 객체가 client에게 제공되었기 때문에 그 불변식은 깨질 수 있습니다.

    Date start = new Date();
    Date end = new Date();
    Period period = new Period(start, end);
    end.setYear(78);

Period의 생성자에서 시작시각과 종료시각에 대한 validation이 진행됐지만 그 이 후 client가 종료시각의 year을 수정함으로써 Period의 불변식도 깨지게 되었습니다.

이와 같은 상황은 client의 의도가 있었다면 해킹으로 이어질 수 있고 의도가 없었다면 원인을 쉽게 찾기 힘든 기능 오류로 이어질 수 있습니다.

해결방법

Immutable 객체 사용

앞서 이야기 드렸듯이 JDK 8 부터는 Instant, LocalDateTime , ZonedDateTime 등의 immutable 객체가 제공되기 때문에 쉽게 해결가능합니다.

public final class Period {
  
    private final LocalDateTime start;
    private final LocalDateTime end;

    pulbic Period(LocalDateTime start, LocalDateTime end) {
        if (start.isAfter(end)) {
            throw new IllegalArgumentException(start + " is later than " + end);
        }

        this.start = start;
        this.end = end;
    }

    public LocalDateTime start() {
        return start;
    }


    public LocalDateTime end() {
        return end;
    }
    
    ...(생략)

}

LocalDateTime 객체는 불변이기 때문에 end 의 값을 변경할 방법이 없습니다. 결국 Period 객체도 불변 객체가 됩니다.

    LocalDateTime start = LocalDateTime.now();
    LocalDateTime end = LocalDateTime.now();
    Period period = new Period(start, end);
    // end 를 변경할 방법 이 없습니다.  

Defensive Copy

JDK 7 이전에는 방어적으로 Date의 복사본을 사용함으로써 Period 객체를 불변 객체로 만들 수 있습니다.

public final class Period {
  
    private final Date start;
    private final Date end;

    pulbic Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

        if (this.start.compareTo(this.end)>0) {
            throw new IllegalArgumentException(this.start + " is later than " + this.end);
        }
    }

    public Date start() {
        return new Date(start.getTime());
    }


    public Date end() {
        return new Date(end.getTime());
    }
    
    ...(생략)

}

아래와 같이 client가 end 와 period.end()의 값을 변경해도 Period가 가진 속성은 변경되지 않습니다.

    Date start = new Date();
    Date end = new Date();
    Period period = new Period(start, end);
    end.setYear(78); 
    period.end().setYear(78);

Period 생성자에서 start와 end의 복사본을 먼저 생성 후 validation을 한 것도 이유가 있습니다.
아래처럼 validation이 먼저 이뤄진다면, validation이 끝난 시점에 다른 쓰레드가 start, end를 변경하고 변경된 start, end 값이 복제되어 Period의 valiation이 무효화 될 수 있습니다.

    pulbic Period(Date start, Date end) {
        if (start.compareTo(end)>0) {
            throw new IllegalArgumentException(start + " is later than " + end);
        }

        // validation이 끝난 시점에 다른 쓰레드가 start, end 를 바꾸면 불변식이 깨집니다.  

        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());

    }

이러한 공격을 검사시점/사용시점(time-of-check/time-of-use) 공격이라고 하며 줄여서 TOCTOU 공격이라고 합니다.


Associated Posts

관련된 주제를 살펴볼 수 있도록 동일한 Tag를 가진 글들을 모아뒀습니다. 제목을 눌러주세요.

i