본문 바로가기

Hub Development/Java

[Java] DTO와 VO 객체의 차이

728x90

📌 DTO(Data Transfer Object)


🔹 DTO(Data Transfer Object)는 데이터 전송(이동) 객체(Java Beans)라는 의미로 순수하게 데이터를 담아 계층 간으로 전달하는 객체이다.

 

DTO 특징

 

◈  DTO는 데이터 접근 메서드 외에 기능을 가지지 않는다. (getter, setter 메서드 외에 비지니스 로직을 가지지않음)

      정렬, 직렬화 등 데이터 표현을 위한 기능은 가질 수 있음

 

  값을 유연하게 변경할 수 있다. (가변성, mutable)

 

  데이터 캡슐화를 통해 유연한 대응이 가능하다.

      데이터 요청 수 감소 효

 

여기서 getter/setter 이외에 다른 로직이 필요 없는 이유는 뭘까? 만약, DTO가 데이터 전달 만을 위한 객체라고 한다면 완전히 데이터 '전달' 용도로만 사용하기 때문에 getter/setter로직만이 필요하다는 이유로 다른 로직을 사용하지 않는다.

public class StudentDTO {

	private int grade;
	private String name;
    
	public int getGrade() {
		return grade;
	}
    
	public void setGread(int grade) {
		this.grade = grade;
	}
    
	public String getName() {
		return name;
	}
    
	public void setName(String name) {
		this.name = name;
	}
}

 

이를 통해 보내는 쪽에서는 setter를 이용하여 값을 담아 전달하고, 받는 쪽에서는 getter를 이용하여 값을 꺼내 쓰게 된다.

이때, setter의 경우 데이터의 변형이 일어날 수 있으니 생성자를 활용한다면 데이터가 변하지 않는 불변 객체를 만들 수 있다.

//생성자를 이용한 불변 객체

public class StudentDTO {

    private int grade;
    private String name;

    public UserDTO(int grade, String name) {
        this.grade = grade;
        this.name = name;
    }

    public int getGrade() {
        return grade;
    }
    
    public String getName() {
        return name;
    }
}

 

📑 VO(Value Object)


🔹VO는 값 그 자체를 나태는 객체이다. DTO와 반대로 로직을 포함할 수 있으며, VO의 경우 특정 값 자체를 표현하기 때문에 불변성의 보장을 위해 생성자를 사용하여야 한다. 오직 read만 가능하며 setter메소드는 갖지 않는다.

 

VO 특징

 

◈  변하지 않는 값을 가지는 객체(불변성, immutable)

    ◈  값이 변하지 않음을 보장하며 코드의 안정성과 생산성을 높임

 

◈  값이 같다면 동일한 객체

    ◈  각 객체를 비교하는데 사용되는 ID가 없음

    ◈  같은 객체인지 판단하기 위해 각 속성들의 값을 비교함

    ◈  equals() 메서드와 hashCode() 메서드를 오버라이드해서 객체 비교를 구현한다.

 

VO는 서로 다른 이름을 갖는 VO 인스턴스라도 모든 속성 값이 같다면 두 인스턴스는 같은 객체인 것이 핵심이다.

예를 들어 모두 똑같은 자동차가 색깔만 다르다고 하더라도 이를 별개의 객체로 보는 것이 아니라 하나의 객체(자동차)로 보는것과 같다.

//생성자를 이용한 불변 객체public class CarVO {

    private final String color;

    public CarVO(String color) {
        this.color = color;
    }

}

자동차의 색깔의 값을 가지고 있는 VO를 하나 만들어서 객체 두개를 아래와 같이 비교 테스트로 돌려보았다.

class CarVOTest {

    @Test
    void CarVOEqualTest() {
        final String color = "red";

        CarVO car1 = new CarVO(color);
        CarVO car2 = new CarVO(color);

        assertThat(car1).isEqualTo(car2);
        assertThat(car1).hasSameHashCodeAs(car2);

    }

}

당연하게도 객체의 주소값을 비교하기 때문에 값이 같은 것과는 상관없이 테스트에서 에러가 난다.

이러한 문제가 있기 때문에 값만을 비교하기 위해서 equals()와 hashCode()를 오버 라이딩(재정의) 해주어야 한다.

public class CarVO {

    private final String color;

    public CarVO(String color) {
        this.color = color;
    }

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

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

 

즉, VO는 값 그 자체를 나타내기 때문에 setter 같은 성격의 변조 가능성이 있는 메서드가 존재하면 안되며, 두 객체의 '필드 값이 같다면' 모두 같은 각체로 만드는 것이 핵심이다

 

👻 DTO vs VO


🔹 DTO와 VO의 차이점을 정리해보면 아래와 같다.

 

DTO와 VO의 경우 혼용해서 사용하는 경우도 있고, 엄격하게 나눠 사용하는 경우도 있어 정확히 언제 사용해야 할지 어렵게 느껴진다. DTO와 VO에 대해서 알아보며 느꼈던 점은 Data를 전달하는 객체로 동일한 개념을 갖고 있지만, VO는 값 자체를, DTO는 Data를 전달한다는 차이점을 알게 되었다. 즉, DTO는 getter/setter 두 가지의 메서드만 존재해야 하지만 VO의 경우는 불변성을 위해서 setter가 없어야 하고 다른 로직이 들어가도 된다고 이해했다. 그렇다면 상황에 따라 VO를 통해 내부적으로 validate와 같은 유효성 검사 로직을 구현해도 좋고, DTO를 활용한다면 외부에 Validator 클래스를 두고 호출을 통해 검증하는 과정을 만든다면 어느 방식을 사용해도 괜찮다는 결론을 내렸다. (즉, 내부에 다른 로직을 넣을지 말지에 따라 구분해서 사용하기)

 

🧪 핵심정리


DTO는 계층간의 데이터를 전송하기 위한 객체로써, getter/setter를 사용하여 보내는 사람은 setter를 사용하여 값을 담고, 받는 사람은 getter를 사용하여 값을 꺼내 쓴다. 이때 값의 변조를 막고 싶다면 생성자로 불변 객체로 만들어 버리면 된다. 또한 DTO의 특성상 데이터 전달만은 목적으로 사용하기 때문에 getter/setter를 제외한 다른 로직이 필요가 없다.

 

  VO의 경우 값 그 자체를 나타내는 객체로써, 핵심은 필드 값이 같다면 두 객체를 같은 객체로 본다는 것이다. 이때 당연하게도 주소값을 비교하는 경우의 문제점을 위해서 hashcode()와 equals()를 재정의하여 필드 값이 같다면 같은 객체로 인식될 수 있도록 해주어야 한다.

 

📸 참조


https://maenco.tistory.com/entry/Java-DTO%EC%99%80-VO%EC%9D%98-%EC%B0%A8%EC%9D%B4