DTO와 VO의 차이

DTO와 VO의 차이

초보자들이 가장 많이 실수하는 DTO(Data Transfer Object)VO(Value Object)에 대한 정리

~DTO, ~VO 라는 클래스들을 꽤 보았을 법 하다. 무슨 차이인지 알아보자.

DTO (Data Transfer Object)


직역하자면 데이터를 옮기기 위한 객체라는 뜻이다.

이 DTO는 흔히 우리가 자바빈즈(Java Beans)라고 부르는 형태와 똑같다고 볼 수 있다.

public class Money {
    private String currency;
    private int amount;
    
    public Money(String currency, int amount){
        this.currency = currency;
        this.amount = amount;
    }

    public String getCurrency() {
        return currency;
    }

    public void setCurrency(String currency) {
        this.currency = currency;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }
}

Money라는 데이터를 주고받기 위해 인스턴스 변수 통화(currency)값(amount)을 정의했고

인스턴스 변수들을 초기화시켜줄 수 있는 생성자(constructor)

이에 접근하기 위한 수정자(setter), 접근자(getter)를 정의한다.

그러면 이 객체를 매개로 각 계층 간에 데이터를 주고받을 수 있을 것이다.

정말 단순하게 위의 형태를 갖는 것은 DTO라고 봐도 무방하다.

주로 쿼리의 결과를 바인딩하거나, 계층간 데이터 전달을 위해 사용된다.

VO(Value Object)


직역하면 값 객체라는 뜻인데 데이터 전달에 목적을 두는 DTO와 다르게 VO는 객체 자체를 어떠한 값(Value)으로서 사용하는데 목적을 두기 때문에 DTO와 차별화되는 점이 몇 가지 있다.

하나는 equals()hashCode()를 반드시 재정의(Override)해서 각 객체의 동등성을 판별할 수 있어야 한다는 것이고,

다른 하나는 객체의 인스턴스 변수가 생성자에 의해 한번 초기화되면 이 값이 불변(Immutable)해야 한다는 것이다.

💡 프로그래밍에서의 동등성? 동일성?

  • 동등성 - 두 개체의 가치가 동일함을 의미. 즉, 두 개체가 참조하는 메모리 주소는 다를 수 있다.

    10,000원짜리 지폐 두장은 서로 독립적인 별개의 개체이지만, 동일한 가치를 갖는다.

  • 동일성 - 두 개체가 참조하는 메모리 주소가 동일함을 의미.

    이에 대해 궁금하다면 얕은복사, 깊은복사를 공부해보면 도움이 될 것이다.

예를 들어 $1000, $2000, $3000 라는 세개의 달러를 VO로 만들 수 있다.

 

public final class Dollar {
    private final String amount;

    public Dollar(String amount) {
        this.amount = amount;
    }

    public BigDecimal amount() {
        return new BigDecimal(amount);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Dollar that = (Dollar) o;
        return Objects.equals(this.amount, that.amount);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.amount);
    }
}

이 객체는 생성자에 의해 amount가 초기화되면 절대 바뀌지 않아야 한다.

예를 들자면 식당에서 $70 짜리 식사를 마치고 $100를 건넸는데 주인이 돈이 부족해서 결제가 안된다고 말하면 이는 말이 되지 않는 상황이다.

즉, 이러한 말이 안되는 상황이 VO를 가변으로 설계하면 일어날 수 있는 상황이다.

$100 짜리 지폐는 그 어떤 상황에서도 $100의 가치를 해야만 한다. (불변성, Immutable)

그리고 $100짜리 지폐 두장이 있을 경우 두 지폐의 가치는 동일해야 한다. (동등성)

정말 간단한 테스트를 해보자

class DollarTests {
    @Test
    void test() {
        Dollar oneDollar1 = new Dollar("1");
        Dollar oneDollar2 = new Dollar("1");
        Dollar twoDollar = new Dollar("2");
        
        assertTrue(oneDollar1.equals(oneDollar2));
        assertFalse(oneDollar1, oneDollar2);
        assertFalse(oneDollar1.equals(twoDollar));
        assertFalse(oneDollar2.equals(twoDollar));
    }
}

oneDollar1oneDollar2는 서로 메모리 주소를 참조하는 별개의 객체임을 알 수 있다. (동일성 비교, 비교 연산자를 이용한 비교 결과 false)

하지만 이 두 객체는 같은 가치를 갖는다. (동등성 비교, equals를 이용한 비교 결과 true)

oneDollar1, oneDollar2twoDollar의 가치는 서로 같지 않다.

즉, $1와 $1는 같은 가치를 갖지만, $1와 $2의 가치는 다르다. (너무 당연한 이야기이다.)

일견 보기에 끝난 것 같지만 이 상태로 oneDollar1oneDollar2해시 컬렉션에서 사용한다면 어떤 결과가 나올까?

해시 컬렉션은 해시코드도 활용해 동등성 검사를 하기 때문에 두 객체는 다른 가치를 갖는 객체라는 결과가 나올수도 있다.

즉, Set에 $1가 두개 들어갈수도 있게 된다.

이는 잘못된 결과일수 있으며, 객체가 해시 컬렉션에서 사용되더라도 각 객체간의 가치판단은 항상 정확해야 한다.

그렇기 때문에 Object.equals()Object.hashCode()를 반드시 함께 재정의(Override) 해준다.

다행히 인텔리제이 같은 IDE에서는 재정의를 자동으로 해주는 기능을 이용하면 대부분 이 두 메서드가 함께 재정의된다.

또한 이 내용은 모두 📜 자바 레퍼런스에 명시되어 있다.

📜 equals()


  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
    • 반사성: null이 아닌 모든 참조 값 x에 대해 x.equals(x) == true를 만족해야한다.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
    • 대칭성: null이 아닌 모든 참조 값 x, y에 대해 x.equals(y) == true면, y.equals(x) == true도 만족해야 한다.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
    • 이행성: null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y) == true이고, y.equals(z) == true이면 x.equals(z)도 true여야 한다.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
    • 일관성: null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
  • For any non-null reference value x, x.equals(null) should return false.
    • null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.

📜 hashCode()


  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
    • equals 비교에 사용되는 정보가 변경되지 않았다면 객체의 hashCode 메서드는 몇 번을 호출해도 항상 일관된 값을 반환해야 한다. 단, Application을 다시 실행한다면 메모리 주소또한 달라지기 때문에 값이 달라져도 상관없다.
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
    • equals를 통해 두 개의 객체가 같다고 판단했다면 두 객체는 똑같은 해시코드를 반환해야 한다.
  • It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
    • equals가 두 개의 객체를 다르다고 판단했다 하더라도 두 객체의 해시코드가 반드시 서로 다른 값을 가질 필요는 없다. 이는 해시 알고리즘에 의한 특성이다. 하지만 가급적이면 서로 다른 객체라면 다른 해시코드를 반환해야 해시 테이블의 성능이 좋아진다.

© 2022. All rights reserved.