ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DTO , VO - "내 돈 1000원이 달라서 핫식스를 못 먹는다고 ?"
    ProgrammingTheory/DDD 2023. 10. 27. 13:25

    이글은 테크톡의 각종 영상과 각종 블로그를 읽고 작성한다.

     

    최종 수정 : 2023 년 10월 27일 , 최초 작성일 2023.03.27

     

     

    이 글은 DTO , VO 그리고 lombok 의 @Data 를 다룬다

     

     

     DTO ,VO 둘다 데이터를 저장하는 객체(Object)이다.

     

    DTO는 데이터를 전송하기 위한 객체이며 불변일 수 도 있다.

     

    VO 는 값에 관한 객체이며 일반적으로 불변이여야 한다.

     

    DTO :   Data Transfer Object   (데이터. 전송. 객체)

     

    처음 단어를 보았을때 엥 이게 뭐야 라는 생각을 했었다. 

     

    DTO 를 DDD 의 Layerd 아키택처에서 다음 두가지 경우로 이용 할 수 있다.

     

    1.  API Layer 즉  controller 단에서 사용자와 데이터를 특정 형식으로 주고 받는 경우 .

     

    2.  Repository 단에서 jpql select 문 안에서 데이터 베이스에서 바로 dto 로 매핑을 하여 데이터를 가져올때.

     

     

    클라이언트 사이드와 사전에 약속한 형식의 데이터를 전송하는데 있어 DTO 를 사용하면 다음과 같은 장점이 있다. 

     

    클라이언트 쪽에서 생기는 요구 사항 변경의 대하여 도메인 모델에 해당하는 Entity 와 VO 에  최소한의 수정으로 요구사항을 만족하도록 구현 할 수 가 있다. 

     

    또한 우리(벡엔드)는 일관된 형식의 API Response 를  사용자의 요청에 맞춰 상대방이 예측 가능한 응답을 보내야 하는 역할과 책임이 있다.

     

    이것이 무슨말인가 하면, 어떤 api 의 ResponseBody에 header 라는 필드가 있고, 어떤 api 의 ResponseBody에 header 라는 필드가 없다면 ,  클라이언트 쪽에서는 항상 api 를 사용하기전에 확인 할 사항들이 늘어날 수 가 있다.

     

    이러한 문제에 DTO 를 사전에 api 논의시 작성하여 모델의 설계를 늦추거나 변경에 유연하게 하면서 협업시 일정한 방식으로 응답값을 전달하는 구조를 설계할 수 있다.

     

     

    DTO 는 다음과 같은 특징을 지닌다고 한다 .

     

    1. getter

     

    2 setter 

     

    3. 로직을 가지지 않는다.

     

    로직을 가지지 않는 경우는 DTO 안에 getter setter 를 제외한 "추가적인 메서드"가 없다 라고 이해를 했다.

     

    3번의 경우 많은 이견이 존재할 수 있다라고 본다 .

     

    로직을 가지지 않는다고 설명을 했지만,  빠르게 변화화는 요구사항들의 마감 날짜들을 바라보며, DTO 생성자의 간단한 로직을 담고 싶다라는 욕망과 싸우기도 한다.

     

    객체지향적인 설계 관점에서 본다면, DTO 의 "책임"은 데이터를 전달하는것이다. 

    즉 , 데이터 전달의 초점을 두기 위한 약속으로 로직을 가지지 않는다라고 이해를 하면 좋을 것 같다.

     

     

    예시를 하나 보자.

     

    Todo 프로그램에서 PrivateTodo 와 EventTodo 라는 모델이 존재하고,  클라이언트 사이드에서 완료여부를 1 또는 0 으로 달라고 요청을 한 상황이다.

     

    PrivateTodo 와 EventTodo 모델에서는 완료 여부를 나타내는 필드 값인 complete에  boolean 값을 담고 있어 다음과 같이 DTO 를 만들었다.

     

    (ResposeTodo 라는 클라스 내부에 중첩 클라스로 생성했다)

    @Data
    public static class DailyAchievement{
        private LocalDate date;
        private int done;
        
        public DailyAchievement(PrivateTodo privateTodo){
            this.date = privateTodo.getDate();
            if(privateTodo.getComplete())  this.done = 1;
            if(!privateTodo.getComplete()) this.done = 0 ;
        }
        
        public DailyAchievement(EventTodoResponseDto eventTodoResponseDto) {
            this.date=eventTodoResponseDto.getDate();
            if(eventTodoResponseDto.getComplete())  this.done = 1;
            if(!eventTodoResponseDto.getComplete()) this.done = 0 ;
        }
    }

     

     

    EventTodoResponseDto 의 getComplete 을 호출해서 변환을 시키는 코드이나, DTO 에서 변환이 아닌 모델 내부에서 변환을 해줄수 도 있을 것 같다.

     

    도메인 모델들을 견고하게 만들며 추가적인 요구사항의 대하여 예시처럼 dto 의 간단 한 로직을 두고 구현하고 , 리펙토링을 하는 방식으로 개발을 하는 방식도 좋을 것 같다.

     

     

    VO:  Value Object   "내 돈 1000원이 달라서 핫식스를 못 먹는다고 ?"

     

    VO 가 뭐야 ? 실제로도 가장 많이 고민해보고 찾아보았지만 ,  명확한 사용 방법이나 활용이 생각이 나지 않았다.

     

     VO 에 대한 설명에 앞서 DDD 에 대한 입문용 책을 먼저 추천하고 가겠다.

     

    : https://product.kyobobook.co.kr/detail/S000001810495/ 

     

    도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지 | 최범균 - 교보문고

    도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지 |

    product.kyobobook.co.kr

     

     

    VO (ValueObject) 는  비즈니스 로직에 사용되는 값으로만 비교되는 객체이다. 

    이는 다시 말하면 의하여 동등성이 판단되는 객체이다.

     

    Core J2EE Patterns 라는 책의 초판에서 위에서 설명한 DTO 라고 알려진 객체의 특징이 VO가 정의되어 있었지만, 혼동의 여지가 있어서 2판부터는 TO(Transfer Object)로 바뀌었고 이후 TO 가 DTO 로 사용이 되었다.

     

    이 블로그 글에서 설명하는 VO 는 DDD 의 DomainModel 에서 나올 수 있는 엔티티와 VO 의 그 VO 이다.

     

    VO 의 개념이 상당하게 도메인 모델링에 밀접한 관계를 맺는 개념이라는 것 을 캐치하면 좋겠다.

     

    VO는 다음과 같은 상황에서 사용이 가능하다.

     

    주문을 한다라고 가정을 하자.

     

    날짜 ,돈 등과 같은 간단한 데이터를 다룰때 사용이 가능하다.

    VO 의 사용목적은 도메인을 풍부하게 설명이 가능하게 도와준다라는 것이다.

     

    public class Order {
    	private fianal int money;
        ...
    }
    
    public class Order {
    	private fianal Money money;
        ...
    }

     

    위의 코드를 보면 다음과 같이 질문 할 수 있다.

     

    ?? :  Composition 으로 사용하는 그냥 객체 아니야 ?  차이가 뭐야 ?

     

    단순하게 객체를 만드는 것이 아니라 다음 두가지 특징을 가진다.

    1 .동일한 값을 가진 서로 다른 두 개 이상의 객체를 생성해서는 안 된다.

    1000 이라는 숫자를 가지는 객채들을 같은 객체로 판별하고 싶다.

     

    2. 일반적으로 내부 상태가 변경이 없는 특징을 가진다.

     

     

     

    예시를 들었던 Money 클라스는 다음과 같다.

    public class Money {
        private final int value;
        public Money(int value){
            this.value = value;
        }
        public int getHalfValue(){
            return  value/2;
        }
        
    }

     

     

    다음과 같이 테스트 코드를 작성하고 실행해보자.

     

    1000 을 가지는 객체 두개를 비교하는 코드이다.

     

    @Test
    void Sould_be_equal_when_comapre(){
        final  int MONEY_VALUE =10_000;
        Money money1 = new Money(MONEY_VALUE);
        Money money2 = new Money(MONEY_VALUE);
    
        assertThat(money1).isEqualTo(money2);
        assertThat(money1).hasSameHashCodeAs(money2);
    }

     

    당연하게 테스트 코드의 결과는 실패이다.

     

     

    위에서 작성한 Money class는 equals 와 hashCode  비교 모두 불일치로 나온다.

     

    이는 같은 돈의 액수인 10000 을 가지는 객체 Money가 서로 다르다고 이야기 하는것과 같다.

     

    " 아니, 내 1000원 이란, 너의 1000원이  근본적으로 사용할 수 있는 돈의 개념에서 같은 거 아니야 ?? "

     

    내 1000원이 편의점에서 핫식스 사먹는데 다른 1000원들과 다르다고 안받으면  무수한 ? 를 날릴 것 같다.

     

     

    객체 메모리의 주소값(식별자)이 달라  같은 값 10000 을 가지는 Money를 다르게 인식하는 문제를 해결 해보자.

     

    - equals 와 hashCode를 재정의 한다.

     

    동일 비교를 요청했을때 동등 비교의 결과로 알려주어야 하기 때문에 , equals() 와 hashCode() 를 오버 라이딩 하여 으로 비교하도록 해준다.

     

    VO 의 특징을 위하여 추가적으로 다음 두가지의 작업을 더 해준다.

     

    1. class 선언부에 public 에 final을 주어 상속이 불가능하게 하여 불변성을 갖게 도와준다.

     

    2. 생성자의 "도메인과 관련된" 검증 로직을 추가했다.

     

     

    VO 의 특징을 가지는 Money Class

     

    public final class Money {
        private final int value;
    
        public Money(int value) {
            if (value < 0) {
                throw new IllegalArgumentException("Money value cannot be negative");
            }
            this.value = value;
        }
    
        public int getValue() {
            return this.value;
        }
    
        public Money getHalfValue() {
            return new Money(this.value / 2);
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Money money = (Money) o;
            return value == money.value;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(value);
           // Objects.hash(amount)를 호출할 때, 내부적으로 amount 값이 Integer로 자동 박싱됨
           // 박싱된 Integer의 hashCode() 메서드로 해당 int 값을 그대로 반환
        }
    }

     

     

    아까와 같은 테스트 코드로 Money class 를 확인해보자.

    @Test
    void Sould_be_equal_when_comapre(){
        final  int MONEY_VALUE =10_000;
        Money money1 = new Money(MONEY_VALUE);
        Money money2 = new Money(MONEY_VALUE);
    
        assertThat(money1).isEqualTo(money2);
        assertThat(money1).hasSameHashCodeAs(money2);
    }

     

    테스트를 통과했다

     

     

     

    VO 의 장점은 다음과 같다.

    1. 단순 int money 가 아닌 Money type 을 만든거와 같이 도메인의 명확성이 좋아진다.

     

    2. 도메인과 관련된 제약사항을 생성자 시점에 추가하여 다른곳에서 불필요한 조건문을 제외할 수 있다.

     

    VO 는 일반적으로 불변성을 가지게 설계한다. by MartinFolwer

     

    To avoid aliasing bugs I follow a simple but important rule:  I follow a simple but important rule: value objects should be immutable.

     

    이로 인하여 다음과 같은 장/단점 이 있다.

     

    1.  값을 변경할 때마다 새로운 객체를 생성하는 방식이다. 이는 메모리를 효율적으로 관리 할 수 있다라는 것을 의미 한다.

    - 자주 변경하여 새로운 객체를 만든다고 하면 성능 저하가 있으니 주의 하자.

     

    2. 불변객체로 VO 를 만들었을 경우 여러곳에서 동시에 사용해도 안전하다.

    - 상속을 사용하기에는 문제가 있다 : ex) class 선언시 final 을 이용한 VO 선언으로 인하여 상속 불가

     

     

    마지막으로  Spring의 lombok 어노테이션의 @Data 를 보면 다음과 같이 설명이 나와 있다.

     

    @Data 는 기존에 프로젝트에서 DTO 용도로  선언하였을때 자주 사용 했었다.

     

    /**
     * Generates getters for all fields, a useful toString method, and hashCode and equals implementations that check
     * all non-transient fields. Will also generate setters for all non-final fields, as well as a constructor.
     * <p>
     * Equivalent to {@code @Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode}.
     * <p>
     * Complete documentation is found at the project lombok features page for @Data.
     * 
     * @see Getter
     * @see Setter
     * @see RequiredArgsConstructor
     * @see ToString
     * @see EqualsAndHashCode
     * @see lombok.Value
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Data {
       /**
        * If you specify a static constructor name, then the generated constructor will be private, and
        * instead a static factory method is created that other classes can use to create instances.
        * We suggest the name: "of", like so:
        * 
        * <pre>
        *     public @Data(staticConstructor = "of") class Point { final int x, y; }
        * </pre>
        * 
        * Default: No static constructor, instead the normal constructor is public.
        * 
        * @return Name of static 'constructor' method to generate (blank = generate a normal constructor).
        */
       String staticConstructor() default "";

     

    DTO 의 특징에서 이야기 한것을 제외 하면 VO 를 만드는데 도와주는 다음과 같은 어노테이션이 있다.

     

    @EqualsAndHashCode  : 마법같은 어노테이션이다.

     

    최초 작성한 Money Class에  @Data 어노테이션을 추가하고 테스트를 해보자.

    @Data
    public class Money {
        private final int value;
        public Money(int value){
            this.value = value;
        }
        public int getHalfValue(){
            return  value/2;
        }
    }

     

    다음과 같이 테스트 코드를 작성하면 

    class PrivateTodoApiTest {
            @Test
            void Sould_be_equal_when_comapre(){
                final  int MONEY_VALUE =10_000;
                Money money1 = new Money(MONEY_VALUE);
                Money money2 = new Money(MONEY_VALUE);
    
                int money1Code = money1.hashCode();
                int money2Code = money2.hashCode();
                
                assertThat(money1Code).isEqualTo(money2Code);
                assertThat(money1).isEqualTo(money2);
                assertThat(money1).hasSameHashCodeAs(money2);
            }
    }

     

    test 통과를 하는 것을 볼 수 있다

     

     

     

    DTO 와 VO 의 관하여 사용법 , 특징 , 작성법등을 알아보았다 .

     

    공통점 :

    DTO ,VO 둘다 데이터를 가지는 객체(Object)이다.

     

    중점 : 

     

    Value Object와 DTO는 비슷한 역할을 수행하지만, 서로 다른 측면에 중점을 두는 것 같다.

     

    Value Object는 개념적 측면에 중점을 둔 반면, DTO는 데이터의 물리적 측면(서버 클라이언트 통신)에 중점을 둔다고 이해를 했다.

     

    목적 :

     

    VO의 목적 동일한 값을 가진 두 개 이상의 객체를 허용하지 않고 도메인을 명확하게 나타내는것에 있다.

     

    DTO의 목적 데이터를 전송하기 위한 목적을 가지며 @Data 를 사용하여 불변성을 가지게 설계 할 수 있다.

     

    @Data 어노테이션은 DTO 를 만들거나 VO 를 만드는 상황 모두 사용이 가능하며, "상황에 따라" 필요한 경우에만 사용하는 것을 권장한다.

     

     

    TIP

     

    Java 의 record 를 사용하면 다음과 같이 DTO 이자 VO 를  간단하게 만들 수 도 있다.

    public record UserDTO(String name, int age, String email) {}

     

     

     

    ------

     

    equls() 와 hashCode() 의 관한 설명은 다음과 같으며 알고 있는 분들은 안 읽어도 될 것 같다.

     

    - String class 에서 서로 같은 문자열인지 판단할때 동일성 비교 ("==") 을 사용하지 않고 ,내부의 값을 비교할때 동등성 비교인 equals()를 사용하는 것이 기억이 날것이다.

     

    -  Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 해시코드를 만들어 리턴하기 때문에 객체 마다 다른 값을 가지고 있다.

     

    위에서 언급한 VO 의 목적 :  

    동일한 값을 갖는 두개 이상의 객체를 생성하지 않으며 내부 테이터가 변경이 없는 것을 위하여

    equals() 와 hashCode() 가 필요하다 라고 보면 된다.

     

    hashCode() 의 리턴 값이 다르다 -> 다른 객체 이다.

     

    hashCode() 의 리턴 값이 같다 -> equals() 리턴값이 다르다 -> 다른 객체이다.

     

    hashCode() 의 리턴 값이 같다 -> equals() 리턴값이 같다 -> 동등 객체이다.    (객체 내부의 값이 같다)

     

    (HashSet,HashMap,HashTree 의 비교 방식이다.)

     

     

     

    ------

     

    출처 및 참고 자료:

     

     https://www.baeldung.com/java-pojo-javabeans-dto-vo

     

    [10분 테코톡] 📍인비의 DTO vs VO:

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

     

    [개발 상식]VO, Value Object, 값 객체

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

     

    자바 VO 스펙 제안 공식 문서 : https://openjdk.org/jeps/169 

     

    벡엔드 개발자를 꿈꾸는 학생 개발자에게: https://d2.naver.com/news/3435170 

     

    자바 숫자 표현 :    https://countryxide.tistory.com/52

    'ProgrammingTheory > DDD' 카테고리의 다른 글

    DDD  (0) 2023.08.08
    Dto 에 로직이 들어가지 않는다 ?  (1) 2023.03.10
Designed by Tistory.