VO, DTO를 사용하는 이유

Socra·2025년 2월 9일
0
post-thumbnail

VO, DTO, ENTITY가 필요한 이유

Case1) DB 컬럼, 요청, 응답 등 필요한 모든 속성을 하나의 클래스로 만들면?

@Entity
@Table(indexes = {
		@Index(columnList = "createdBy"),
		@Index(columnList = "title")
})
@ApiModel(value = "Post", description = "포스트")
public class Post {
		@Column("id)
		private Integer id;
		
		@ApiModelProperty("포스트 제목")
		@Column("title")
		private String title;
		
		@JsonIgnore
		private List<Member> subscribers;
}

JPA, Jackson, Swagger의 애너테이션이 하나의 클래스에 모두 붙어 있음

  • 어떤게 어디에 쓰이는지 알기 어렵다

Case2) 연관된 모든 테이블의 데이터를 담은 클래스

public class Issue {
     private Repo repo;
     private List<Comment> comments;
     private List<Label> labels;
     private Milestone milestone;
     private List<Account> partipants;
}
  • 의존성으로 인한 부작용, N+1 쿼리 발생 가능성

성능 저하

  • 항상 연관된 객체를 다 조회해 불필요한 쿼리 발생
  • N+1 쿼리 발생 가능성 높음
  • Lazy Loading을 쓰지 않고 직접 값을 채울 때
    • getComments()에 값이 채워질지는 DAO 내부까지 까봐야 알 수 있다
    • 비슷한 메서드가 여러개 생긴다
      • findIssueById(), findIssueByWithComments() …

뷰까지 클래스가 전달된다

  • 객체 참조 관계를 바꾸기 매우 힘들어진다
  • 깊은 객체 탐색이 이루어진다

JSP에서 사용하면 이렇게 된다 🤮

<div>${issue.milestone.creator.email}</div>

결론: 이렇게 만들지말고, Java Beans / VO / DTO / ENTITY 를 사용하자



VO / DTO 살펴보기

VO(Value Object)

  • 값이 같으면 동일하다고 간주되는 식별성이 없는 객체(Money, Color)
    (마틴 파울러의 정의, 위키피디아)
  • DTO와 혼용해서 쓰여왔다

VO를 쓰는 이유

VO를 사용하지 않은 경우

public class Order {
    private int price;

    public Order(int price) {
        this.price = price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}

외부에서 setPrice()로 값이 쉽게 변경되어 문제가 발생하기 쉽다.

VO 사용한 경우

public class Order {
    private final Money price;

    public Order(Money price) {
        this.price = price;
    }
}
public class Money {
    private final int amount; // 필드를 final로 선언

    public Money(int amount) { // 검증 로직(도메인 로직)
        if (amount < 0) {
            throw new IllegalArgumentException("금액은 0 이상이어야 합니다.");
        }
        this.amount = amount;
    }

    public int getAmount() {
        return amount;
    }
    
    public Money add(Money other) { // 값을 변경하지 않고 새로운 객체를 반환
        return new Money(this.amount + other.amount);
    }
    
    public Money multiply(int factor) { // 도메인 로직
		    return new Money(this.amount * factor);
		}

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

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

얻을 수 있는 장점

  • 불변성: 주문의 가격이 Money VO 객체를 통해 불변성을 유지하면서 변경 불가능해졌다.
  • 값 기반 동등성 비교: equals(), hashCode()를 재정의해 값으로 비교할 수 있다.
  • 도메인 로직 응집도 증가: Money 객체가 금액 연산을 직접 담당하므로, ServiceEntity에 불필요한 로직을 구현할 필요가 없다


DTO(Data Transfer Object)

  • 원격 호출을 효율화하기 위해 나온 패턴
    마틴 파울러의 페이지에서 용어 역사까지 설명되어 있다
  • 네트워크 전송 시의 Data holder 역할, 최근엔 폭넓게 쓰인다
  • 맥락이 달라졌지만 ‘레이어 간 경계를 넘어 데이터를 전달’ 한다는 역할은 동일하다

DDD에서의 정의

  • ENTITY: 연속성과 식별성의 맥락에서 정의되는 객체
  • Value Obejct: 식별성 없이 속성만으로 동일성을 판단하는 객체

DTO를 쓰는 이유

ENTITY를 뷰, API 응답으로 그냥 노출시키면 안되나?

  • 캡슐화를 지키기 어렵다
    • 필요하지 않는 속성도 외부로 노출되어 수정하기 어려워진다
  • 뷰(JSP, Freemarker)에서 객체 참조
    • 클래스를 수정하면 뷰의 에러가 뒤늦게 발견된다
    • JPA를 쓴다면 LazyLoading을 할 때 JPA가 관리하는 세션 사이클에 포함되어 있을 때만 LazyLoading이 되어 쿼리를 추가로 날릴 수 있다
  • JSON 응답
    • @JsonIgnore, @JsonView 등을 사용하면서 클래스로 JSON의 형태를 알아보기 어렵다

DTO를 사용하면 장점

  • ENTITY에서 DTO로 변환 오류가 컴파일 시점에 발견된다
  • 단순한 JSON 응답, View에서 쓰기 편한 구조로 만들기 쉽다
  • DTO 자체가 외부 인터페이스 - 문서화가 쉽다 (Swagger 스펙 활용)
  • 여러 ENTITY의 데이터를 조합할 수 있다

⇒ 결론: ENTITY를 외부에 감추자!

0개의 댓글

관련 채용 정보