목차


DTO란?

An object that carries data between processes in order to reduce the number of method calls.

  • 즉, 메서드 호출 횟수를 줄이기 위한 목적으로 프로세스들 간의 데이터를 전송하는 객체이다.
  • 메서드 호출을 클라이언트-서버의 네트워크 통신으로 바꾸면 네트워크 통신 비용을 줄이는 목적으로도 생각할 수 있다.
  • 예를 들어 Artist와 Album 정보를 얻기 위해 REST API를 두 번 호출하는 것보다 Album정보에 Artist가 포함된 DTO를 한번에 응답 하는게 하는게 통신 비용을 줄일 수 있겠다.
  • 최근에는 단순히 네트워크/메서드 호출 횟수를 줄이는 목적 보다 더 넓게 데이터 전송 역할로 많이 사용되는 것 같다.
  • 단순히 같은 하나의 엔티티를 조회하는데 도메인 엔티티가 Presentation 레이어로 노출되지 않아야 하니 DTO를 사용하는 경우도 왕왕 있다.
  • 그래서 더 적합한 용례로 MVVM 패턴의 ViewModel로 부르자는 목소리도 있다.
  • Presentation 레이어만 생각하면 좋아보이긴 하는데, 복잡한 통계용 쿼리 결과를 반환하는 Repository 레이어에서 DTO를 사용한다. (QueryDSL 공식 메뉴얼 예제에서 DTO 사용 등)
  • 결론적으로 DTO는 레이어간 데이터 전송 객체로써 역할을 함과 동시에 원래의 목적 또는 이점은 메서드/통신 호출 비용을 줄이는 것으로 이해하면 된다.

또 다른 이점

  • 메서드 호출 횟수를 줄이는 것 이외에도 직렬화(serialization) 매커니즘을 캡슐화하는 장점이 있다.
  • 그럼 직렬화란 무엇인가?

직렬화란

In computing, serialization is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, over a computer network) and reconstructed later (possibly in a different computer environment).

  • 컴퓨터 공학에서 직렬화는 데이터 구조체 또는 객체 상태를 저장할 수 있는 포맷(파일, 메모리 데이터 버퍼 등)또는 전송된(예, 컴퓨터 네트워크) 후 재구성(아마 다른 환경의 컴퓨터에서)할 수 있는 형식으로 변환하는 프로세스를 말한다.
  • Java에서는 Object(객체)를 Byte Stream으로 변환하는 것을 말한다. Deserialization은 그 반대이다.
  • Object는 메모리에 올려져 있는 Data Buffer이기 때문에 파일이나 네트워크 통신을 위해선 Byte Stream으로 변환이 필요한데, 이 때 직렬화가 필요하다.
  • 더 자세한 것은 추후에 다루고 여기선 DTO와 연관되어 얘기해보려 한다.

다시 돌아가 DTO의 직렬화 이점

  • 클라이언트에서 들어온 값을 서버에서 사용하기 위해 직렬화 과정이 필요한데, 각 컨트롤러(Facade)에서 직렬화를 처리해야한다.
  • DTO를 사용한다면 이 직렬화 처리를 캡슐화해서 가질 수 있어 컨트롤러에서 직렬화하는 책임을 없앨 수 있다.

DTO와 VO의 차이


VO란?

  • Value Object의 약자이다.
  • 과거 VO는 DTO와 비슷한 용례로 사용이 되곤 했다.
  • 마틴 파울러의 정의에 따르면 VO는 다음과 같다.

A small simple object, like money or a date range, whose equality isn't based on identity.

  • 동일성을 기반으로 하지 않는 동등성을 가진 돈, 날짜와 같은 작고 단순한 객체

동일성(Identity)과 동등성(Equality)

  • 동일성은 실제 같은 인스턴스(레퍼런스 주소가 같음)이며 Java에서는 == 연산자로 비교한다.
  • 동등성은 실제 인스턴스는 다르지만 같은 정보를 가진다는 뜻으로 eqauls() 메서드를 오버라이드 하여 구현한다.

다시 돌아가 VO

  • 동일성을 기반으로 하지 않는 다는 것은 레퍼런스 주소를 비교하지 않는다는 것이다.
  • 즉, VO는 eqauls() 메서드를 오버라이드 하여 같은 값을 가진 객체들끼리는 동등하게 취급하는 객체이다.
  • VO는 마치 원시 타입처럼 취급하여 레퍼런스 타입과 구분하기 용이하기 위해 사용한다.
  • DDD(Domain Driven Design)에서는 VO에 불변성(Imuutable) 특징을 더 한다. 즉, VO는 수정할 경우 VO의 값을 변경하는 것이 아닌 수정된 값을 인자로 인스턴스를 새로 생성해야 한다.

그래서 DTO와 VO 차이는?

  • 예전처럼 VO를 DTO처럼 쓰는게 아닌 VO는 불변한 값 동등성을 가진 객체로 사용하고, DTO는 데이터 전송 객체로 사용하는 차이가 있다.

DTO 활용

클라이언트 요청 파라미터 받기

@RequestMapping("/boards")
@RestController
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
  }

    @GetMapping
    public ResponseEntity<List<BoardResponse>> getBoards(BoardQueryRequest boardQueryRequest) {
        return ResponseEntity.ok(boardService.getBoards(boardQueryRequest));
    }
}

public class BoardQueryRequest {
    private String title;
    private int parentBoardId;
    private LocalDate createDateStart;
    private LocalDate createDateEnd;
}
  • Board 목록을 조회하는 컨트롤러 및 검색용 Request DTO 객체(BoardQueryRequest)

클라이언트에 응답하기


@Service
public class BoardServiceImpl implements BoardService {

    private final BoardRepository boardRepository;

    public BoardService(BoardRepository boardRepository) {
        this.boardRepository = boardRepository;
    }

    public List<BoardResponse> getBoards(BoardQueryRequest boardQueryRequest) {
        return BoardResponse.ofs(boardRepository.findAllBy(boardQueryRequest));
    }

}

public class BoardResponse implements Serializable {

    // serial uid

    private String title;
    private String category;

    // getters...
    // builder

    public static List<BoardResponse> ofs(List<Board> boards) {
        return boards.stream()
                    .map(BoardResponse::of)
                    .collect(toList());
  }

    public static BoardResponse of(Board board) {
        return BoardResponse.builder()
                    .title(board.getTitle())
                    .category(board.getCategoryName())
                    .build();
  }

}

DTO 너무 귀찮아!?

  • 처음에 애플리케이션이 단순할 때, Entity를 도메인 레이어 경계 밖으로 데이터 전송하는 역할로 DTO를 사용할 경우 Entity의 속성과 DTO의 속성이 1:1로 매핑된다.
  • 이 때, Entity ↔ DTO의 변환 로직을 일일히 만드는 것이 귀찮고, 지루해 보일 수 있다.
  • 심지어는 코드 중복으로 생각될 수도 있다.
  • 그럴 때 ModelMapper 같은 라이브러리를 사용하면 편리하게 변환이 가능하다.
  • 개인적으로는 처음부터 분리하는게 여러모로 좋은 경우가 많아서 아주 단순한 CRUD라도 DTO를 꼭 사용하는 편이다.

참고자료

+ Recent posts