본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] Date vs LocalDateTime

얼마전에 Date 타입을 LocalDateTime로 바꾸었다.

가장 큰 이유는 Date 타입이 mutable 이라서 getter / setter 에서 값을 deep copy를 해서 반환하고 설정을 하라고 한다.

이유는 참조하는 곳에서 값을 바꾸면 필드의 값도 같이 바뀔 수 있기 때문이다.

// 테스트 대상 Item.java
public class Item {

    private Integer id;
    private String name;
    private String createBy;
    private Date createAt;

    public Item(Integer id, String name, String createBy, Date createAt) {
        this.id = id;
        this.name = name;
        this.createBy = createBy;
        this.createAt = createAt;
    }

    public Date getCreateAt() {
        return createAt;
    }
}

// 테스트 코드 ItemSpec.groovy
import spock.lang.Specification

class ItemTest extends Specification {

    def "getter로 가져와서 변경하면 필드의 값도 같이 바뀐다."() {
        given: "임의의 아이템을 생성한다."
        def cut = new Item(1, "sample", "namo", new Date())

        and: "getter를 이용해서 지역 변수에 담는다."
        Date date = cut.getCreateAt()

        when: "date를 하루전 날짜로 돌려놓았으면"
        date.setDate(date.getDate() - 1)

        then: "멤버의 값은 변경되지 않아야 한다."
        date != cut.getCreateAt()
    }
}

하지만 이 테스트는 실패한다.

date != cut.getCreateAt()
|    |  |   |
|    |  |   Tue May 14 10:24:25 KST 2019
|    |  <com.example.springboot.sandbox.me.Item@35cabb2a id=1 name=sample createBy=namo createAt=Tue May 14 10:24:25 KST 2019>
|    false
Tue May 14 10:24:25 KST 2019

참조 변수인 date가 실제로는 Item의 createAt과 같은 대상이기 때문이다.

 

이런 부작용을 막기 위해서는 deepCopy를 해야 한다. 다시 말해 새로운 인스턴스를 생성해서 돌려주어야 한다는 것이다. (getter / setter 모두)

수정된 코드이다. getCreateAt (Getter)에서 새로운 Date 객체를 반환을 해주었다.

public class Item {

    private Integer id;
    private String name;
    private String createBy;
    private Date createAt;

    public Item(Integer id, String name, String createBy, Date createAt) {
        this.id = id;
        this.name = name;
        this.createBy = createBy;
        this.createAt = createAt;
    }

    public Date getCreateAt() {
        return new Date(createAt.getTime());
    }

}

이렇게 하면 getter를 통해 받은 Date 객체를 수정해도 createAt에 영향을 주지 않는다.

불변 클래스 사용

위와 같이 수정을 하면 sonarqube는 다른 Technical Dept를 내놓는다. 바로 코드 중복이다.

따라서 가변 클래스인 Date를 사용하는 대신 Java 8에 추가된 LocalDateTime, LocalDate, LocalTime을 사용하는 것을 권장하고 싶다.

참고로 JSR-310에 이 클래스에 대한 스펙이 정의되어 있다.

public class Item {

    private Integer id;
    private String name;
    private String createBy;
    private LocalDateTime createAt;

    public Item(Integer id, String name, String createBy, LocalDateTime createAt) {
        this.id = id;
        this.name = name;
        this.createBy = createBy;
        this.createAt = createAt;
    }

    public LocalDateTime getCreateAt() {
        return this.createAt;
    }
}

class ItemTest extends Specification {

    def "getter로 가져와서 변경하면 필드의 값도 같이 바뀐다."() {
        given: "임의의 아이템을 생성한다."
        def cut = new Item(1, "sample", "namo", LocalDateTime.now())

        and: "getter를 이용해서 지역 변수에 담는다."
        LocalDateTime date = cut.getCreateAt()

        when: "date를 하루전 날짜로 돌려놓았으면"
        date = date.minusDays(1)

        then: "멤버의 값은 변경되지 않아야 한다."
        date != cut.getCreateAt()
    }
}

테스트 코드에서 알 수 있지만, 자바 8의 java.time 패키지의 날짜, 시간 클래스들은 불변 클래스들 이다.

테스트 코드(ItemTest)에서 볼 수 있지만 minusDays()를 호출하면 객체 자체를 바꾸는 것이 아닌 새로운 인스턴스를 돌려준다.

즉, 불변이라는 이야기이다.

 

사실 Date의 setDate 메서드는 @Deprecated 가 붙어 있다. 쓰지 말라는 이야기이다.

 

다음 글은 Date 클래스를 LocalDateTime로 바꾸었을 때 겪게 된 문제에 대해 작성할 예정이다.