자바 17 버전을 사용하며 VO 생성 목적으로 레코드 클래스를 사용했는데, 웹 애플리케이션에서 빈번하게 사용되는 VO, DTO의 개념과 VO와 DTO 사용 시점에 대해 정리할 필요성을 느꼈습니다.
VO와 DTO와 그 사용 시점을 알아보고 JAVA 14버전부터 소개된 record 클래스를 이용해 VO 클래스를 만들어봅시다! 😀
VO란 Value Object의 약자이며 값 객체라고 부릅니다. VO는 다음과 같은 특징을 지닙니다.
VO와 관련된 개념으로는 DTO와 Entity가 있는데 각각의 특징을 정리한 표는 다음과 같습니다.
DTO | VO | Entity | |
---|---|---|---|
용도 | 레이어 간 데이터 전송 | 의미 있는 값의 표현 | DB 테이블과 매핑되는 저장소 |
가변 / 불변 | 가변객체(Mutable Object) 생성 → setter 사용 가능 | 불변 객체(Immutable Object) 생성 → setter 사용 불가능 | 가변객체(Mutable Object) 생성 → setter 사용 가능 |
로직 포함 여부 | 로직 포함 불가능 → 메서드는 Getter, Setter만 가능 | 로직 포함 가능 | 로직 포함 가능 |
위 표에 따르면 DTO는 Getter, Setter 사용이 모두 가능하다는 것인데, 한 가지 여기서 드는 생각은 “만약 Getter와 Setter를 모든 필드에 사용한다면 필드를 public으로 만드는 것과 무슨 차이가 있을까?” 입니다.
모든 필드를 public으로 선언한다는 게 굉장히 불편하게 다가왔지만, Getter, Setter를 모든 필드에 다 적용하는 경우 public 필드랑 사실상 차이가 없습니다.
마티아스 노박의 오브젝트 디자인 스타일 가이드에서는 DTO를 아래와 같이 소개하고 있기도 합니다.
“DTO는 상태를 보호하지 않으며 모든 속성을 노출하므로 획득자(getter), 설정자(setter)가 필요없다. public 속성으로 충분하다는 뜻이다.”
물론, 마티아스 노박의 의견에 상충되는 의견들도 많습니다.
저는 마티아스 노박의 DTO 정의(DTO는 public 속성으로 충분하다)를 따르기로 했습니다.
사실 제가 VO와 DTO 활용에 대한 생각을 하게 된 이유는 비즈니스 계층에서 컨트롤러 계층에 엔터티를 반환하면 안 된다는 원칙 때문이었습니다.
비즈니스 로직에서 컨트롤러 계층에 엔터티를 반환하면 엔터티에 유효성 검사 로직들이 들어가고 API 스펙이 엔터티에 종속되고 엔티티 전체 필드가 외부에 노출되니까요 😥
이러한 관점에서 본다면 DTO와 VO 활용 시점을 아래와 같이 정리할 수 있습니다.
비즈니스 계층에서 컨트롤러 계층으로 가는 방향으로만 표시했지만, 컨트롤러 계층에서 비즈니스 계층으로 가는 것도 같은 맥락에서 이해해주시면 좋을 것 같아요.
생각해 본 DTO 활용 시나리오는 아래와 같습니다.
생각해 본 VO 활용 시나리오는 아래와 같습니다.
자바 14 버전 이전에는 VO를 만들기 위해 많은 보일러플레이트 코드가 작성돼야 했기 때문에 개발 생산성이 떨어지는 경우가 많았습니다.
그런데, 자바 14버전부터 ‘레코드’라는 불변 데이터 클래스를 지원하면서 VO를 간단하게 만들 수 있게 되었습니다.
// before
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
// equals() and hashCode() methods
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// toString() method
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// after
public record Person(String name, int age) {
// no additional fields or methods needed
}
우와 헷갈리는 개념이었는데 정리해주셔서 감사합니다