VO와 DTO 그리고 VO로 쓰기 딱 좋은 record

제이슨·2023년 2월 17일
6
post-thumbnail

자바 17 버전을 사용하며 VO 생성 목적으로 레코드 클래스를 사용했는데, 웹 애플리케이션에서 빈번하게 사용되는 VO, DTO의 개념과 VO와 DTO 사용 시점에 대해 정리할 필요성을 느꼈습니다.

VO와 DTO와 그 사용 시점을 알아보고 JAVA 14버전부터 소개된 record 클래스를 이용해 VO 클래스를 만들어봅시다! 😀

VO란?

VO란 Value Object의 약자이며 값 객체라고 부릅니다. VO는 다음과 같은 특징을 지닙니다.

  • ‘값’ 자체를 표현하는 객체
  • equals()와 hashCode()를 오버라이딩해서 모든 속성 값이 같다면 같은 객체로 취급한다.
  • 불변성을 보장하는 객체
  • 로직을 포함할 수 있는 객체

연관개념 정리

VO와 관련된 개념으로는 DTO와 Entity가 있는데 각각의 특징을 정리한 표는 다음과 같습니다.

DTOVOEntity
용도레이어 간 데이터 전송의미 있는 값의 표현DB 테이블과 매핑되는 저장소
가변 / 불변가변객체(Mutable Object) 생성 → setter 사용 가능불변 객체(Immutable Object) 생성 → setter 사용 불가능가변객체(Mutable Object) 생성 → setter 사용 가능
로직 포함 여부로직 포함 불가능 → 메서드는 Getter, Setter만 가능로직 포함 가능로직 포함 가능

DTO와 VO의 활용에 대한 생각

위 표에 따르면 DTO는 Getter, Setter 사용이 모두 가능하다는 것인데, 한 가지 여기서 드는 생각은 “만약 Getter와 Setter를 모든 필드에 사용한다면 필드를 public으로 만드는 것과 무슨 차이가 있을까?” 입니다.

모든 필드를 public으로 선언한다는 게 굉장히 불편하게 다가왔지만, Getter, Setter를 모든 필드에 다 적용하는 경우 public 필드랑 사실상 차이가 없습니다.

마티아스 노박의 오브젝트 디자인 스타일 가이드에서는 DTO를 아래와 같이 소개하고 있기도 합니다.

“DTO는 상태를 보호하지 않으며 모든 속성을 노출하므로 획득자(getter), 설정자(setter)가 필요없다. public 속성으로 충분하다는 뜻이다.”

물론, 마티아스 노박의 의견에 상충되는 의견들도 많습니다.

그래서 결론은

저는 마티아스 노박의 DTO 정의(DTO는 public 속성으로 충분하다)를 따르기로 했습니다.

사실 제가 VO와 DTO 활용에 대한 생각을 하게 된 이유는 비즈니스 계층에서 컨트롤러 계층에 엔터티를 반환하면 안 된다는 원칙 때문이었습니다.

비즈니스 로직에서 컨트롤러 계층에 엔터티를 반환하면 엔터티에 유효성 검사 로직들이 들어가고 API 스펙이 엔터티에 종속되고 엔티티 전체 필드가 외부에 노출되니까요 😥

이러한 관점에서 본다면 DTO와 VO 활용 시점을 아래와 같이 정리할 수 있습니다.

비즈니스 계층에서 컨트롤러 계층으로 가는 방향으로만 표시했지만, 컨트롤러 계층에서 비즈니스 계층으로 가는 것도 같은 맥락에서 이해해주시면 좋을 것 같아요.

생각해 본 DTO 활용 시나리오는 아래와 같습니다.

  • 컨트롤러에서 POST 요청을 통해 가입 정보 데이터 들어왔을 때 DB에 들어가기 전 서비스 로직에서 패스워드 해싱을 해야 한다.
  • 패스워드 필드 값을 변경해야 하기 때문에 requet body를 CreateUserDto에 담아서 보내야 한다.

생각해 본 VO 활용 시나리오는 아래와 같습니다.

  • 컨트롤러에서 Get 요청을 통해 회원 전체를 조회하고 싶은데, 고객의 패스워드는 보안 정보이기 때문에 아이디와 연락처만 반환받고 싶다.
  • 필드 값이 변경될 여지가 없기 때문에 response body에 UserBasicInfoVo에 담아서 보낸다.

record의 등장

자바 14 버전 이전에는 VO를 만들기 위해 많은 보일러플레이트 코드가 작성돼야 했기 때문에 개발 생산성이 떨어지는 경우가 많았습니다.

그런데, 자바 14버전부터 ‘레코드’라는 불변 데이터 클래스를 지원하면서 VO를 간단하게 만들 수 있게 되었습니다.

before record

// 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 record

// after
public record Person(String name, int age) {
    // no additional fields or methods needed
}

참고자료

[10분 테코톡] 🎼라흐의 DTO vs VO

Java/Spring 주니어 개발자를 위한 오답노트

profile
계속 읽고 싶은 글을 쓰고 싶어요 ☺

2개의 댓글

comment-user-thumbnail
2023년 2월 22일

우와 헷갈리는 개념이었는데 정리해주셔서 감사합니다

1개의 답글