[Spring] Entity와 DTO 그리고 VO?

이신영·2023년 9월 7일
1

Spring

목록 보기
5/16
post-thumbnail
post-custom-banner

지금까지 DTO와 Entity를 잘 구분하냐? 하면 말못함.. 그래서 프로젝트를 새로하는 계기가 된거기도하고.. 😅 이번기회에 각잡고 정리하는거다?!


DB에서부터 Client까지의 과정

Entity : db에 저장되는 데이터 객체
DTO : 클라이언트와 서버 간 데이터 전송을 위한 객체

그림에서 보이듯 Entity는 데이터베이스와 매핑되는 객체이며EntityDTO로 변환해서 Client(View)까지 전달해야하는것이 DTO의 역할이다!!


그럼 왜 분리해야하는데?

그냥 Entity를 Client까지 전달해도되지않음? 이라고 생각할수도있다.
하지만 효과적인 개발측면에서는 문제가 발생할 수 있다. 그 문제점은 무엇일까?

1. DB와 View의 역할분리

Entity는 단순히 데이터를 담는 역할만 하지않는다. 실제 db의 테이블과 매핑되며 Entity Manager에 의해 관리되는 db와 상호작용을 위한 객체다.
즉, 전달까지 맡기기엔 역할이 많기때문에 DB와 View의 역할을 분리하기위해서 DTO를 만든다고 볼 수 있다.

2. Entity 변경 최소화

Entity는 테이블과 1:1 매핑되는 객체다. 만일 EntityDTO대신 사용할 때 요구사항이 변경되면 데이터가 변질될 수 있으며 데이터가 노출될수있기때문에 DTO를 변경시키는게 좋다.

3. 효율적인 데이터전송

Entity는 비즈니스로직이 포함될 수 있고 칼럼의 속성을 정의할 수 있다. 때문에 Entity의 로직이 무거워지면 직접 전송하기에 비효율적일 수 있으므로 데이터만 전송하는 DTO를 쓰는게 좋다.

가장 큰 이유는 DB layer와 View layer의 역할을 분리하기위함이라 생각하면된다.


스프링에 대입해보자

코드가 너무 길어지니까 lombok을 사용했다 😅

Entity

  • db와 매핑됨.
  • 비즈니스 로직을 포함함
  • 데이터의 CRUD 작업에 사용됨.
  • Setter를 가지지 않는것이 좋음(영속성)
@Entity
@Getter
@NoArgsConstructor
public class Product {
	// 실제 db와 매핑되는 칼럼만 있어야함
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;  
    @Column(name = "product_name")
    private String productName;  
    @Column(name = "price")
    private double price; 

    // 비즈니스 로직을 포함함. 예를 들어, 할인 기능이나 가격 계산 메서드를 여기에 추가할 수 있음

    // 생성자
    public Product(String productName, double price) {
        this.productName = productName;
        this.price = price;
    }

}

여담으로 Entity에 복잡한 비즈니스로직을 넣어 서비스로직이 간단하게 정의되는걸 도메인 주도 설계(Domain-Driven Design, DDD)라고 한다!

DTO (Data Transfer Object)

  • 데이터 전송에만 사용됨(getter/setter만 가짐)
  • 필요한 데이터만 가지는 가변 객체
  • 비즈니스 로직을 가지지 않음
@Getter
@Setter
public class OrderDTO {
	// 데이터 전송에만 사용됨. 
    private final Long orderId; 
    private final String product;  
    private final int quantity;  

    public OrderDTO(Long orderId, String product, int quantity) {
        this.orderId = orderId;
        this.product = product;
        this.quantity = quantity;
    }
}

VO (Value Object)

값 그자체를 나타내는 객체다. 우선 코드부터보자

  • 불변 객체
  • 데이터의 무결성을 유지
  • 비즈니스 로직을 가질 수 있음
@Getter
public class Money {
	//불변성을 가짐. 데이터 전송에 사용됨.
    private final BigDecimal amount;  
    private final Currency currency; 

    // equals()와 hashCode() : 객체의 동등성 비교를 위해 구현됨.
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return Objects.equals(amount, money.amount) &&
               Objects.equals(currency, money.currency);
    }

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

예시로 돈(Money)클래스를 생성해볼 수 있다.
돈은 변경되지 않는다. 천원이 갑자기 990원이되고 그러진않으니까? 그래서 한번 생성된 돈 객체는 불변한다. 또한 금액은 String이나 integer같이 단순한 데이터타입이 아니다. 즉, 독립적인 개념으로 표현할 때 VO를 사용한다! 이게 왜 필요한지 아직 감이 안잡힌다면?

public class MoneyExample {
    public static void main(String[] args) {
        // Money 객체 생성
        Money money1 = new Money(new BigDecimal("100.00"), Currency.getInstance("USD"));
        Money money2 = new Money(new BigDecimal("50.00"), Currency.getInstance("USD"));
        Money money3 = new Money(new BigDecimal("100.00"), Currency.getInstance("USD"));

        // Money 객체 비교
        boolean areEqual1and2 = money1.equals(money2);
        boolean areEqual1and3 = money1.equals(money3);

        System.out.println("money1 equals money2: " + areEqual1and2);
        // 출력: money1 equals money2: false
        System.out.println("money1 equals money3: " + areEqual1and3);
        // 출력: money1 equals money3: true
    }
}
        Set<Money> moneySet = new HashSet<>();
        moneySet.add(money1);
        moneySet.add(money2);
        moneySet.add(money3);

        System.out.println("HashSet size: " + moneySet.size()); 
        // 출력: HashSet size: 2

오버라이드한 equals메소드와 HashCode메소드를 사용한 예시이다. 통화(USD)는 같지만 금액이 다르거나 같을때와 중복된 경우를 구분할 수 있다.

profile
후회하지 않는 사람이 되자 🔥
post-custom-banner

0개의 댓글