본문 바로가기
프로젝트/Recipository

[Dev] 22.12.16. Dto와 Entity

by 규글 2022. 12. 16.

Dto와 Entity

 Dto는 무엇인지 Entity는 무엇인지 간단히 알아보고, JPA와 함께하는 Entity를 client와의 request나 response에 사용하지 않아야 하는 이유에 대해 고찰해보고자 한다.

 

DTO

 DTO는 Data Transfer Object의 줄임말로, 글자 그대로 data를 옮기는 목적의 객체이다. Data를 객체에 setting 하거나, 객체로부터 get 할 수 있다. VO (Value Object) 와는 조금 다른데, VO는 글자 그대로 값(value, data)을 지닌 객체로, 그 값을 조회만 할 수 있다(read only)는 차이가 있다.

 

Entity

 이미지는 이전에 JPA 강의를 들으면서 그렸던 것이다.

 

 Database(DB)는 data를 저장하는 창고같은 공간이라고 할 수 있다. 그리고 Application에서는 DB에서 data를 받아 client에 출력하거나, 중간에 가공하여 의미있는 data를 만들어낼 수도 있다. Application에서는 Object(객체)가 객체 지향 프로그래밍 언어인 Java에서 사용되는 한 단위라고 말할 수 있는데, 이 Object와 DB의 table 사이에서 관계를 mapping 해주는 것이 ORM (Object Relational Mapping) 이라고 한다. 이때 관계를 맺는 Object가 바로 Entity 가 되는 것이며, 현재 JPA는 ORM의 표준으로 채택되어 있다.

 

 좀 더 구체화 시킨 이미지이다.

 

 Application과 DB를 연결해주는 것이 ORM이라는 개념이고, 이 ORM의 표준을 JPA 로 정의해서 interface 로 제공하고 있다. 그리고 ORM 표준인 JPA를 실제로 구현(implements) 한 것이 Hibernate이고, 그 중에 자주 쓰는 기능들을 사용하기 쉽도록 Spring framework에서 한 번 더 묶어놓은 것이 Spring Data JPA 이다.

 

 즉 Application의 Entity Object는 JPA를 통해서 DB와 data를 주고 받을 때 mapping 되는 것인데, 왜 data 를 가지고 있는 이 Entity를 직접 return 하는 방식을 지양하라는 말이 있는 것일까?

 

 

왜 DTO와 Entity를 구분해야 하는가?

 이 게시글을 작성하는 의의가 바로 여기에 있다. 이미 관련한 여러 문제를 직접 몸으로 맞이해왔으며, 관련 언급을 지인에게서도 들은 적이 있다.

 

역할 분담

 Signin 기능에 대한 작업을 하던 중에 Entity 객체에 그대로 Validation annotation을 사용했다가 문제를 마주한 적이 있다.[각주:1] 당시 번거롭다는 이유 하나만으로 Entity 객체를 Controller에서도 활용해서 client로부터 data를 받으면서 동시에 validation까지 수행하도록 작업했었고, 그 때문에 password encoding 후의 값이 validation에 걸려서 오류 마주했었다.

 

 객체는 각각이 할 수 있는 고유의 역할이 있는 것이다. Entity는 ORM을 기준으로 DB와 맞은 편에 서서 mapping 하기 위한 data와 그 설정을 보유하는 역할을 한다. 이 Entity에 이외의 역할을 부여하려고 하지 말고 다른 객체가 그 역할을 대신하도록 해야하는 것이며, 앞서 언급한 에러 상황 시 data를 주고 받을 역할은 DTO 객체가 하도록 했다.

 

순환 참조 방지

 위 에러 상황을 맞이하고 얼마 되지 않아서 Entity 간의 양방향 연관 관계에 대한 공부를 하면서 맞이했던 순환 참조 문제를 해결하기 위한 방법으로 DTO 객체를 다루는 것을 제안하는 블로그를 발견했었다.[각주:2] 물론 필자는 당시에 Entity 객체를 Controller 단으로 넘기는 경우는 아니었지만, A에서 B를 참조하고 B에서 다시 A를 참조하는 순환 참조를 하는 Entity를 그대로 return 하는 것이 아닌 필요한 data만을 field로 담을 수 있도록 DTO를 다뤄서 data를 return 하는 방식을 언급했었다.

 

캡슐화 및 필요 데이터 선별[각주:3]

 Entity를 그대로 return 하는 것은 UI layer에 table을 설계한 사항을 노출하는 것이다. 때문에 보안 상으로도 바람직하지 못한 구조인 것이라고 블로그는 언급하고 있다. 따라서 Entity의 구현을 UI 노출하지 않고, 그 대신 client와 data를 주고 받는 역할을 DTO가 하도록 해야하는 것은 당연한 것이라고도 할 수 있겠다.

 게다가 Entity의 field로 구성한 모든 data를 return 하지 않을 수도 있는 것이다. 규모가 작은 경우라면 동일할 수도 있겠으나, 규모가 커지고 data가 많아지는 상황에서 Entity를 그대로 return 한다면 화면 구성에 전혀 필요하지 않은 내용들까지도 함께 노출하게 되는 것이며 많아진 만큼 속도도 더뎌질 것이다. 따라서 주고 받을 data만을 선별하여 field로 만든 DTO를 사용하는 것이 좋다고 말할 수 있다.

 

참고[각주:4]

 해당 블로그에서는 DTO와 Entity 사이에 VO를 두고 있는 경우를 언급하고 있다.

 

DTO 내에서도 구분을?

 DTO도 request에 관여하는 RequestDTO, response에 관여하는 ResponseDTO로 나눌 수 있다. 필자는 지금까지의 작업을 request와 response의 구분 없이 사용해왔는데, DTO와 Entity를 구분했던 것처럼 request와 response에 관여하는 DTO 또한 역할을 나누는 것이 좋을 것이라고 생각했다.

 당장 댓글을 추가하고 화면에 출력하는 작업을 예로 들수 있다. 댓글을 추가하기 위해서 client로부터 받는 data는 게시글의 id와 작성한 댓글의 내용, 그리고 대댓글인지 여부를 확인할 수 있는 group id까지 세 가지를 받고 있다. 하지만 화면에 출력하기 위한 data는 댓글의 id, 작성자, 댓글의 내용, group id, 작성 시간의 다섯 가지이다. 서로 다루는 data가 다른 만큼 DTO 또한 구분하는 것이 좋은 방향성이지 않을까?

 

 만약 작업에 쓰던 CommentDto를 CommentReqeustDto와 CommentResponseDto로 구분하면 어떻게 될까?

 

 

DTO 구분 작업

CommentController.java

 Request를 처리하는 controller의 변수를 RequestDto로 바꿔주었다.

 

CommentRequestDto.java

 이때의 RequestDto의 field에는 앞서 언급했던 댓글 내용(comment), 게시글의 id(targetId), 대댓글인지 여부를 확인하면서 동시에 댓글을 grouping 할 group id(groupId)의 세 field로 구성했다.

 이 RequestDto를 Comment entity로 변환하기 위해서는 원래도 method를 사용했지만, 차이가 있다면 Dto의 field에 setter method로 setting 했던 것을 Entity로 변환하는 method에 인자로 전달했다는 정도이다.

 

CommentServiceImpl.java

 기존 댓글을 추가하던 service logic이다. 앞서 변경한 RequestDto에서 없앤 field에 setter method를 적용할 수 없어서 Entity로의 변환 method에 전달한 부분에 대해 수정해주었다.

 

Comment.java / CommentResponseDto.java

 DB에서 data를 받은 entity를 client로 전달하는 ResponseDto로 return type을 수정해주었다. 이때 Response 역시 앞서 언급했던 다섯 field 만을 남겼으며, 기존에 Dto로부터 Entity로의 변환 method는 ResponseDto 입장에서는 필요가 없으므로 작성하지 않았다.

 

Recipe.java / RecipeServiceImpl.java

 역시 Recipe entity에서 Comment를 CommentDto로 바꿔서 Dto List로 변환하는 method에서도 type을 바꿔주었다.

 

 이렇게 Dto를 Request의 경우와 Response의 경우로 구분했지만, 동작은 동일한 것을 확인했다.

 

 

DTO가 많아진다면?

 Comment에 대해서만 RequestDto와 ResponseDto로 나눈 것인데 나머지 세 가지도 구분하고, 또 이외에도 여러 항목의 DTO를 생성해야한다고 생각해보면 개수가 점점 늘어나고, directory도 깔끔해보이지 않을 것이다. 그럴 때에는 domain 별로 따로 구분하는 방식을 취한다고 하지만, 그 외에도 DTO 자체를 묶어서 관리하는 방식을 취한다고 한다.[각주:5] [각주:6]

 

 다음을 보자.

 

CommentDto.java

package com.example.recipository.dto;

import com.example.recipository.domain.Comment;
import com.example.recipository.domain.Recipe;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

public class CommentDto {
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class CommentRequestDto {
        private String comment;
        private Long targetId;
        private Long groupId;

        public Comment toEntity(Long id, String writer){
            Recipe recipe = Recipe.builder()
                    .contentId(targetId)
                    .build();

            return Comment.builder()
                    .commentId(id)
                    .writer(writer)
                    .comment(this.comment)
                    .recipe(recipe)
                    .groupId(this.groupId)
                    .build();
        }
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    public static class CommentResponseDto {
        private Long commentId;
        private String writer;
        private String comment;
        private Long groupId;
        private String regDate;
    }
}

 앞서 구분했던 RequestDto와 ResponseDto를 CommentDto의 inner static class로 작성했다. Static으로 작성했기 때문에 상위 class인 CommentDto 객체를 생성하지 않고도 각 Dto를 생성할 수 있다.

 이렇게 inner class를 static으로 작성하는 것에 대한 이유에 대해 언급한 블로그를 찾을 수 있었다.[각주:7] 다음과 같이 언급하고 있다.

  • 단순히 inner class로 작성한다면 각 Dto class는 Outer class인 CommentDto를 instance화 시킨 뒤에 instance화 할 수 있기 때문에 생성 시간이 더 걸린다.
  • Outer class와 inner class의 관계 정보는 inner class 의 instance에 만들어져서 더 많은 메모리를 차지한다.
  • 두 class의 관계 정보가 inner class에 있다는 것은 inner class에서 Outer class를 참조하고 있다고 볼 수 있는데, 이때문에 Garbage Collection에서 Outer class를 garbage 수거 대상에서 제외한다. 그러므로 Outer class의 메모리를 빼앗지 못하기 때문에 Outer class를 참조할 일이 없는 경우는 inner class를 static으로 선언하는 것이 좋다.

 

 이대로 작업했을 때의 이미지는 Controller의 이미지만 가져왔다. 나머지들도 수정해주었으며, 동작도 동일하게 되는 것을 확인했다.

 

Reference

댓글