티스토리 뷰

CS/공통

VO vs DTO vs Entity

bool-flower 2022. 8. 1. 15:34

DTO


Data Transfer Object(데이터 전송 객체)로서 계층간의 데이터 교환을 위해 사용되는 객체다. DTO는 로직을 가지지 않는 순수한 데이터 객체로 Getter/Setter 메서드만을 가진다. Setter 대신 생성자만 이용하면 불변 객체로 사용할 수 있다.

public class UserSaveRequestDto {

    private String userEmail;

    private String userPassword;

// Setter 대신 생성자를 사용하면 불변 객체로 사용할 수 있다.
    public UserSaveRequestDto(String userEmail, String userPassword) {
    	this.userEmail = userEmail;
        this.userPassword = userPassword;
    }

    public User toEntity() {
        return User.builder()
                .userEmail(userEmail)
                .userPassword(userPassword)
                .build();
    }

    public void setUserEmail(String userEmail) {
        this.userEmail = userEmail;
    }

    public String getUserEmail() {
        return userEmail;
    }
}

의문) DTO는 Getter/Setter 이외의 어떠한 메서드도 가지지 않는다?

우리가 자주 사용하는 패턴 중 DTO에 toEntity 메서드를 두는 것이 있다. toEntity 메서드란 DTO스스로가 자신이 가진 데이터로 Entity를 생성하는 메서드로, 데이터의 주체인 DTO에서 Getter를 통해 데이터를 빼내, 외부에서 Entity를 생성하는 것이 아닌 DTO가 직접 Entity를 생성하는 책임을 가지게 하기 위해 메서드를 생성해 사용한다.

 

하지만, 앞에서 DTO가 Getter와 Setter이외의 다른 메서드는 가질수 없다고 했는데, 이러한 방식은 잘못된 것인가?

⇒ Request와 Response용 DTO는 변경이 잦은 클래스, 편의를 위해 Request용 DTO에 toEntity를 사용 하는 것은 흔히 사용하는 패턴인 듯. (Getter로서 취급 되는 듯 하다.)

 

정리

  • 계층간 데이터 교환을 위한 객체
  • 데이터 교환만을 위해 사용하며, 로직을 가지지 않고 Getter와 Setter를 가진다.
  • Setter 대신 생성자를 이용하면 불변 객체로 사용할 수 있다.

VO


전문가와 같은 VO(Value Object)

사람의 나이를 나타대는데 어떤 유형의 변수를 사용해야 할까?

  • Integer ?
  • Boolean ?
  • String ?

정답은 세 가지 중 어느 것도 아니다 . 사람의 나이를 나타내려면 Age라는 사용자 지정한 타입을 사용해야 한다.

정수로 해도 상관 없을거라고 생각할 수 있다. 하지만, 정수에는 연령과는 다른 속성과 연산이 있다.

 

  • 두 개의 연령을 더하거나 빼는 것이 의미가 있나?
  • 두 연령을 곱하거나 나눌 필요가 있나?
  • 나이가 음수가 될 수 있나?

핵심은 Age는 정수가 아니기에 위와 같이 표현할 수 없다는 것이다. 도메인의 객체를 나타내기 위해 primitive 타입을 쓰는 좋지 않은 방법을 흔히 primitive obsession라 칭한다.

 

도메인 객체를 primitive 타입으로 사용할 수 없는 또 다른 문제가 남아있다. 먼저, 나이를 나타내는 정수 변수가 유효한 값으로 초기화되었는지 어떻게 확인할까? 이는 코드에서 할당이 발생할 때마다 할당하기 전에 명시적으로 확인해야 한다.

둘째로, 나중에 실수로 정수 값이 수정되지 않도록 하려면 어떻게 해야 할까? 변수의 불변성을 보장하는 언어를 사용하거나, 선언 시에 그렇게 하지 않는 한, 불가능하다.

 

위와 같은 이유는 primitive 타입이 도메인 객체를 모델링 하기에 부적합하다고 말할 충분한 이유가 된다. 따라서, 도메인의 의미와 완벽히 부합하는 다른 정보 단위가 필요하다. 그것을 우리는 값 객체 즉, VO라고 부른다.

VO의 특성

불변성

Value Object는 변경할 수 없다. 생성 후에 내부 값을 변경할 수 없음을 의미한다. 즉,  수정자(setter)가 허용되지 않는다.

생성자에 하나 이상의 매개변수를 삽입해서 값을 할당한 후에는 되돌릴 수 없다. 해당 객체는 GC에 의해 폐기될 때까지 어떤 일이 있어도 변하지 않음을 보장한다.

 

불변성은 공유를 더욱 간편하게 하고 의미를 좀 더 명확하게 만든다. 

1) 간편한 공유

Value Object는 변경할 수 없음으로 코드의 다른 부분에서 참조로 모든 Value Object를 공유할 수 있다. Value Object가 불변성을 보장하고 있기 때문이다. 이는 버그를 방지하는 데 필요한 복잡성과 부하를 크게 줄인다. 특히 코드가 Multi-thread 환경에서 실행될 때 극대화 된다.

// 생성된 이후에는, 수정자(setter)를 통해 수정되지 않습니다.
final class Name {
    private String value;

    public Name(String value) {
        this.value = value;
    }
}

2) 더욱 명확해진 의미 (본문에선 향상된 의미 체계라고 한다.)

무의미한 Getter를 Value Object에 추가하지 않는다.

초기 클래스는 생성자와 private접근자인 속성만을 가지고 있어야 한다. 나중에 필요할 수 있다고 해서 미리 메서드를 추가하면 안된다. 이렇게 하면 Value Object의 용도를 이해하고, 메서드가 정말로 필요하다고 생각하게 되었을 때 메서드를 결정(생성)할 수 있다. 이는 무의미한 인터페이스를 피하고 Value Object에 대해 의미 있는 이름과 동작을 정의하여 모델을 개선할 수 있다.

 

일반적으로 Value Object를 조작하는 방법은 다음과 같다.

  • 생성자 또는 정적 메서드를 통해 새 인스턴스를 만들기
  • 현재 Value Object 통해 변경된 또 다른 Value Object를 만들기
  • 내부 데이터를 추출하여 다른 유형으로 변환
final class ComplexNumber {
    private float realPart;
    private float complexPart;

    // 정적 메소드를 통해 새 인스턴스 만들기
    public static ComplexNumber zero() {
        return new ComplexNumber(0, 0);
    }

    // 생성자를 통해 새 인스턴스를 만들기
    public ComplexNumber(float realPart, float complexPart) {
        this.realPart = realPart;
        this.complexPart = complexPart;
    }

    // 정적 팩토리 메소드 패턴을 활용하여 생성하기 (생성자를 private하게 바꿈)
    public static ComplexNumber of(float realPart, float complexPart) {
    	return new ComplexNumber(realPart, complexPart);
    }

    // 현재 객체에서 다른 객체를 생성하기
    public ComplexNumber add(ComplexNumber anotherComplexNumber) {
        return new ComplexNumber(
            realPart + anotherComplexNumber.realPart,
            complexPart + anotherComplexNumber.complexPart
        );
    }

    // 내부 데이터를 추출하여 다른 유형으로 변환하기
    public String toString() {
        return String.format("%f + %f i", realPart, complexPart);
    }
}

값 동등성

Value Object는 내부의 값이 동일한 두 객체는 동일한 것으로 판단한다. 즉, 내부 값이 모두 각각 동일한지 확인하여 동등성을 확인 할 수 있다.

final class Card {
    private Rank rank;
    private Suit suit;

    public Card(Rank rank, Suit suit) {
        this.rank = rank;
        this.suit = suit;
    }

	// 동등성 보장
    public boolean sameAs(Card anotherCard) {
        return rank.sameAs(anotherCard.rank) && suit.sameAs(anotherCard.suit);
    }
}
💡 Java개발자 참고 사항
무의미한(나중에 필요하겠지란 생각으로) equals()와 hashCode()는 사용하면 안된다.

자체 검증

Value Object는 컨텍스트에서 의미가 있는 값만 허용합니다. 이는 유효하지 않은 값으로 Value Object를 생성할 수 없음을 의미한다.

생성자에 값이 주입될 때 값의 일관성을 확인해야 한다. 값 중 하나가 유효하지 않으면 의미 있는 예외가 발생해야 한다. 이것은 객체의 인스턴스 주변에 더 이상 if 가 없다는 것을 의미한다. 모든 공식적인 유효성 검사는 생성 할 때 발생한다.

 

이 강제 유효성 검사는 의미 있고 명시적인 방식으로 도메인 제약 조건을 표현하는 데에도 유용하다.

final class Rank {
    private int value;

    public Rank(int value) {
        // if문을 통한 자체 유효성 검사
        if (value < 1 || value > 13) {
        	throw new InvalidRankValue(value);
        }
        this.value = value;
        validateValue();
    }

    public static Rankof(int value) {
        return new Rank(value);
    }

    // 방어적 복사 기법
    private void validateValue() {
        if (value < 1 || value > 13) {
        	throw new throw new InvalidRankValue(value);
        }
    }
}
💡 현재 예제에선 값 주입 후 validate를 진행했는데. 이는 방어적 복사 기법이라 합니다. (Multi-Thread에 안전)

정리

  • Value Object는 불변, 수정자(setter)를 가질 수 없다.
  • Value Object는 도메인의 의미를 반영한다.
  • 런타임 동안 정보의 흐름과 변환을 보여줍니다. 
  • 의미없는(나중에 필요하겠지란 생각으로) getter와 메서드를 가져선 안된다.
  • Value Object는 속성을 직접 다른 Value Object와 비교할 수 있어야 한다.
  • DDD(도메인주도설계)에서 식별자를 가지지 않는 도메인 객체를 Value Object라고 한다.

Entity


Entity는 실제 DB 테이블과 매핑되는 객체다. DB테이블과 마찬가지로 ID값을 통해 구분되고, 내부에 로직을 포함 시킬 수 있다. Entity를 기준으로 DB테이블이 생성되고 변경되기 때문에 절대로 Entity를 요청이나 응답값을 전달하는 클래스로 사용해서는 안된다.

@DynamicInsert
@DynamicUpdate
@Entity
@NoArgsConstructor
@Table(name = "user")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userIdx;

    private String userEmail;
    private String userPassword;

    @Builder
    public UserString userEmail, String userPassword) {
        this.userEmail = userEmail;
        this.userPassword = userPassword;
    }

    @Override
    public String getPassword() {
        return this.userPassword;
    }

    @Override
    public String getUserEmail() {
        return this.userEmail;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

정리

  • 실제 DB테이블과 매핑되는 객체로 Id로 구분된다.
  • 내부에 로직을 포함 시킬 수 있다.
  • 불변을 기본으로 하지만 Setter를 가질 경우 가변 객체로도 활용할 수 있다. (비추천)
  • DDD(도메인주도설계)에서 식별자를 가진 도메인 객체를 Entity라고 한다.

DTO, Entity 클래스들을 분리하는 이유


Entity와 DTO를 분리해서 관리하는 이유는 DB 레이어, 비즈니스 레이어, 프론트 레이어의 레이어 아키텍쳐 구성에서 분리하기 위해서다. DTO 클래스는 클라이언트가 던진 데이터를 받는 그릇으로, 요청에 따라 데이터들이 자주 변경될 수 있다. 하지만 Entity는 영속성 모델을 표현한 객체로, 영속성 모델의 표현만을 위해서 사용되어야 하기 때문에 변경되지 않고 사용되어야 한다. 그렇기에 Entity 클래스에선 setter 같은 수정자 메서드를 사용하지 않는 것을 원칙으로 한다.

참조


 

Value Objects Like a Pro

How to build the perfect class for your domain.

medium.com

 

[Java] VO(Value Object)란?

개발을 하다 자주 VO라는 개념을 들은적이 있습니다. 대략적으로 값 객체 패턴(Value object pattern) 즉, 객체를 값처럼 쓸 수 있고, 제약사항 중 하나는 객체의 인스턴스 변수가 생성자를 통해서 일단

velog.io

 

DTO vs VO vs Entity 비교

쿼리 결과값을 리턴받을 때 사용, 프로젝트 때마다 통신시 보내줄 때 사용계층간 데이터 교환을 위한 객체클래스 맴버변수들의 값 그 자체를 가진다. equals()와 hashcode() 메서드를 오버라이딩 하

velog.io

https://www.youtube.com/watch?v=J_Dr6R0Ov8E

 

'CS > 공통' 카테고리의 다른 글

Web Server와 WAS(Web Application Server)  (0) 2022.03.18
객체지향설계의 5대 원칙  (0) 2022.02.22
싱글톤(Singleton) 패턴  (0) 2022.02.20
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday