JaeWon's Devlog
article thumbnail
반응형

개발 도중 API 의 Request, Response 처리를 할 때나 다른 계층으로 데이터를 넘길 때 별도의 DTO 를 생성해야 하는 고민을 하기도 한다.

또한, 개발자마다 개발하는 과정에서 혹은 기존 소스에서 확인해보면 DTO 와 VO 는 다른 개념이지만 종종 같은 개념으로 사용하기도 합니다. 또한, Entity 와 DTO 도 한 클래스로 사용하기도 한다.

 

이것이 무조건? 틀렸다라곤 할 수는 없겠지만, 각각의 역할이 다르기 때문에 분리하여 각 역할에 맞게 사용하여야 하고 이것이 좋은 코드로 이어질 것이라고 생각한다.

 

이번 글에서는 각 Entity, DTO, VO 에 대해 정리를 해보고자 한다.


1. Entity

- 실제 DB 테이블과 매핑되는 핵심 클래스로, 데이터베이스의 테이블에 존재하는 컬럼들을 필드로 가지는 객체입니다.

상속을 받거나 구현체여서는 안되며, DB 테이블내에 존재하지 않는 컬럼을 가져서도 안됩니다.

- Entity 는 데이터베이스 영속성(persistent)의 목적으로 사용되는 객체이며, 요청(Request)이나 응답(Response) 값을 전달하는 클래스로 사용하는 것은 좋지 않습니다.

- 외부에서 Entity 클래스의 Data Field 에 접근하지 못하도록 제한해야 합니다.

- Entity 는 id 로 구분되며, 비지니스 로직을 포함할 수 있습니다.

@Entity
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class User {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;
  
   @Column(nullable = false)
   private String name;
  
   @Column(nullable = false)
   private String email;
   
   private String phoneNumber;
}

- Entity 클래스에서는 setter 메서드의 사용을 지양해야 합니다.

- 이유는 변경되면 안되는 인스턴스나 데이터에 대해서 setter 를 통해 접근이 가능해지기 때문에 객체의 일관성, 안전성을 보장하지 어렵습니다.

- 그렇기 때문에 Entity 에서는 Constructor(생성자) 또는 Builder 를 사용합니다.

2. DTO(Data Transfer Object)

- DTO 는 계층(Layer) 간 데이터 교환이 이루어질 수 있도록 하는 객체입니다.

데이터 교환만을 위해 사용하므로 비지니스 로직을 갖지 않고, getter/setter 메소드만 갖습니다.

- Controller 같은 클라이언트와 직접 마주하는 계층에서 DTO 를 사용하여 데이터를 요청/응답을 하며, 주로 View 와 Controller 사이에서 데이터를 주고받을 때 활용합니다.

@Getter
@Setter
public class MemberDto {
    private String name;
    private int age;
}

- 위 코드에서와 같이 setter 메소드를 가지는 경우 가변 객체로 활용할 수 있습니다.

- 만약, 불변 객체로 활용하고자 한다면 setter 대신 생성자를 이용해서 초기화하여 사용하실 수 있습니다.

불변 객체로 만들면 데이터를 전달하는 과정에서 데이터가 변조되지 않음을 보장할 수 있습니다.
@Getter
@AllArgsConstructor
public class MemberDto {
    private String name;
    private int age;
}

3. VO(Value Object)

- VO 는 직역과 같이 값 그 자체를 표현하는 객체라는 의미를 가지고 있습니다.

핵심 역할은 equals() 와 hashCode() 를 오버라이딩 하여 객체의 불변성을 보장합니다.
즉, 객체들의 주소 값이 달라도 데이터 값이 같으면 동일한 것으로 판단합니다.

- getter 메소드는 가질수 있지만, setter 메소드는 가지지 않습니다.(ReadOnly 특징을 가집니다)

- 비지니스 로직을 포함할 수 있습니다.

@Getter
@AllArgsConstructor
public class UserVo {

    private String name;
    private Integer age;
    private String address;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserVo userVo = (UserVo) o;
        return Objects.equals(name, userVo.name) && Objects.equals(age, userVo.age) && Objects.equals(address, userVo.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, address);
    }
}

- 테스트 코드를 작성하여 확인해보면 동일한 값으로 확인하실 수 있습니다.

public class VoTest {

    @DisplayName("VO 동등비교")
    @Test
    void isSameVo(){
        UserVo vo1 = new UserVo("개발자", 20, "인천");
        UserVo vo2 = new UserVo("개발자", 20, "인천");

        Assert.assertEquals(vo1,vo2);

        assertThat(vo1).isEqualTo(vo2);
        assertThat(vo1).hasSameHashCodeAs(vo2);
    }
}

- 만약 equals(), hashCode() 메소드가 없이 테스트를 실행하시면 테스트 통과에 실패하시는 것을 확인하실 수 있습니다.

4. 역할 분리를 해야 하는 이유

4-1. 관심사 분리

- 각각 분리를 하여 사용해야 하는 근본적인 이유는 관심사가 서로 다르기 때문입니다.

관심사의 분리(Separation of Concerns, SoC)
소프트웨어 분야의 오래된 원칙 중 하나로, 서로 다른 관심사들을 분리하여 변경 가능성을 최소화하고, 유연하며 확장 가능한 클린 아키텍처를 구축하도록 도와줍니다.

- DTO 는 Entity 객체와 달리 각 계층끼리 주고받는 데이터의 개념입니다.

- 데이터를 담고 있는 점에서 유사하지만, DTO의 목적 자체는 전달이므로 읽고, 쓰는 것이 가능하고 일회성으로 사용되는 성격이 강합니다.

- Entity 는 주로 JPA 에서 사용되는데 단순히 데이터를 담는 객체가 아니라 DB 와 관련된 중요한 역할을 하고, 데이터의 핵심 비지니스 로직 담는 객체입니다.

4-2. Validation 로직 및 불필요한 코드의 분리

- 데이터 검증을 위한 @Valid 어노테이션이나, JPA를 사용하기 위한 Entity 에서도 @Id, @Column 등 어노테이션을 사용하여 각 객체를 만들게 됩니다.

- 만약 Entity, DTO 를 분리하지 않고 같이 사용한다면 해당 클래스의 코드가 상당히 복잡해집니다.

- 예를 들어, Entity 와 DTO를 같이 사용한다면 아래와 같이 알아보기 힘든 클래스가 생성되게 됩니다.

@Entity
@Table
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @NotNull
    @Size(min = 0)
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(nullable = false)
    private Long id;

    @NotBlank
    @Column(nullable = false)
    private String userId;

    @NotNull
    @Size(min = 0)
    @Setter
    @Column(nullable = false)
    @ColumnDefault("0")
    private Integer age;

    @CreationTimestamp
    @Column(nullable = false, length = 20, updatable = false)
    private LocalDateTime createTime;

    @UpdateTimestamp
    @Column(length = 20)
    private LocalDateTime updateTime;

}

- 위와 같이 어노테이션들이 잔뜩 있는 클래스는 보기만 해도 유지보수가 힘들어짐을 느낄 수 있습니다.

- 또한 위 Entity 클래스의 생성일자(createTime)나 수정 일자(updateTime)는 API 요청 및 응답 데이터로는 필요 없을 수 있습니다.

- 이렇게 한 클래스로 Entity 와 DTO 를 같이 사용하게 되면 핵심 비지니스 도메인 코드를 포함하여 요청/응답을 위한 값, 유효성 검증을 위한 값 등이 추가되면서 지나치게 비대해지고, 확장 및 유지보수 등에서 어려움을 겪게 될 것입니다.

4-3. API 스펙의 유지

- Entity 클래스를 통해서 API 를 응답한다고 예를 들어 본다면

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
public class Todo {

    @Id
    @GeneratedValue
    @Column(name = "todo_id")
    private Long id;

    private String item;

    private String date;

    private boolean completed;

    private String time;

    private LocalDateTime writeDate;
    private LocalDateTime updateDate;

    @Column(columnDefinition = "varchar(1) default 'Y'")
    private String useYn;
}

- API 호출 시 응답 값으로 아래와 같이 전달받게 됩니다.

{
    "id" : "15",
    "item" : "TODO 1",
    "date" : "2022-10-15",
    "completed" : false,
    "time" : "13:33:05",
    "createDate" : "20221015",
    "updateDate" : "20221015",
    "useYn" : "Y"
}

- 이때 만약 어떠한 이유로 한 파라미터의 이름을 변경해야 하는 상황이 있을 경우, API를 사용하고 있는 모든 사용자들이 장애를 겪게 되고 모두 수정 작업이 필요하게 될 것입니다.

- 물론 @JsonProperty 를 이용해 반환되는 이름의 값을 변경할 수도 있겠지만, 위 2번에서와 같이 클래스 자체를 무겁게 만들기 때문에 근본적인 해결책이라곤 할 수 없습니다.

- 그러므로 DTO 를 이용하여 분리해 독립성을 높이고 변경이 전파되는 것을 방지해야 합니다.

- DTO 를 사용한다면 내부에서 Entity 가 변경되어도 API 스펙이 변경되지 않으므로 안정성을 확보할 수 있습니다.

5. 정리

- 개발을 하다보면 시간에 쫓겨 역할 분리를 안하고 하나의 파일을 통해서 통신에서도 사용하고 DB 에서도 사용 하는 일이 있을 수 있습니다.

- 서비스는 동작하겠지만, 추후 유지보수 과정이나 변경사항이 있을 경우 엄청난 작업으로 이어질 수 있기 때문에 각 객체의 목적에 맞게 생성하여 사용하는 것이 좋다고 생각합니다.

- 이것이 무조건의 정답은 아니지만 조금더 좋은 아키텍처를 구성하는 하나의 방법으로 생각하시면 좋을 것 같습니다.

- 간단하게 표로 정리해보면 아래와 같습니다.

  Entity DTO VO
  목적(역할) JPA 사용시 DB 와 직접적으로 매핑하여 컬럼의 필드를 가지는 객체 계층(Layer) 간 데이터 송/수신 용도의 객체 객체안의 값을 통해서 비교해야하는 중요 로직에서 사용하는 Data 객체
  getter 사용 O O O
  setter 사용 X O X
  equals(), hashCode() 사용 O O X
  Spring Tier 사용 범위 Repository <-> Service Controller <-> Service
Client <-> Controller
Controller <-> Service


참고

- https://mangkyu.tistory.com/192

- https://tecoble.techcourse.co.kr/post/2021-05-16-dto-vs-vo-vs-entity/

- https://wildeveloperetrain.tistory.com/m/101

- https://velog.io/@boo105/Entitiy-%EC%99%80-DTO-%EC%9D%98-%EB%B6%84%EB%A6%AC

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!