프로젝트 리팩토링 도중, 이틀을 고민한 부분이 있습니다. 바로 DTO가 어느 계층까지 내려와야하는 것인지에 대한 고민이었습니다. 지나고보니 그렇게까지 오래 고민할 필요는 없는 것 같지만..? 고민했던 부분들이 누군가에겐 도움이 되었으면 하는 바람에 이 글을 쓰게 되었습니다.
결론부터 말하면, 정해진 정답은 없다는 것입니다. 개발하는 서비스마다 다르고, 같은 서비스에서도 기능마다 다를 수 있습니다.
1. DTO란?
우선 DTO란 계층간 데이터 교환을 위해 사용하는 객체입니다. 이때 계층은 Presentation(View, Controller), Business(Service), Persistence(DAO, Repository)로 나뉘게 됩니다. 스프링 프레임워크에 익숙한 개발자들은 보통 Controller - Service - Repository 계층으로 이어지는 것에 익숙할 것입니다.
DTO를 사용하는 이유
도메인의 객체를 직접 View에 전달은 할 수 있습니다. 하지만 그럼에도 DTO를 사용하는 이유는 아래와 같습니다.
우선, password와 같은 민감한 정보가 노출될 수 있다는 문제가 있습니다.
public class User {
public Long id;
public String name;
public String password; //외부에 노출 X
}
예를 들어 위와 같은 도메인 모델(Entity)이 있다고 가정했을 때, View로 password가 넘어가서는 안되는 것입니다. 이런 정보를 굳이 View에 전달해서는 안됩니다.
그리고 Model과 View 사이의 의존성이 생기기 때문입다. 도메인 모델은 초기 설계 이후에 최대한 수정되지 않는 모델입니다. 그렇기 때문에 View의 요구 사항에 따라 전달되는 데이터 구조가 바뀔 때마다 도메인도 바뀌어야하는 의존성이 생겨서는 안됩니다.
정리하자면 DTO(Data Transfer Object) 의 핵심 관심사는 이름 그대로 데이터 전달입니다. DTO는 클라이언트 요청에 포함된 데이터를 담아 서버 측에 전달하고, 서버 측 응답 데이터를 담아 클라이언트에 전달하는 역할을 수행하는 것입니다.
2. DTO의 사용 범위
위처럼 DTO와 도메인(Entity)은 엄연히 서로 다른 관심사를 가지고 있고, 분리를 통해자연스럽게 의도치 않은 데이터 변경, 노출 및 오류를 예방할 수 있습니다.
그렇다면 Entity와 DTO를 어느 계층에서 변환시키는 것인지에 대한 고민을 해볼 수 있습니다. 지금껏 찾아본 여러 포스팅에서도 의견은 매우 다양합니다. 다만 대부분 Service나 Controller의 두 가지 옵션을 고려합니다. Repository가 고려 대상이 아닌 이유는 Repository 레이어의 역할이 Entity의 영속성을 관장하는 것이기 때문입니다.
그래서 다음의 두 가지 상황을 생각해볼 수 있습니다. 컨트롤러에서 DTO와 Entity를 변환해 서비스에 전달하는 경우와, 서비스에서 DTO와 Entity를 변환하는 경우입니다.
1. Controller 에서 DTO와 Entity를 변환해 Service에 전달하는 경우
BoardController
@PostMapping
public ResponseEntity<BoardResponseDto> createArticle(@RequestBody BoardRequestDto boardRequestDto) {
...
Board board = boardRequestDto.toEntity();
Board savedBoard = boardService.createBoard(board);
BoardResponseDto boardResponseDto = BoardResponseDto.from(savedBoard);
return ResponseEntity.ok().body(boardResponseDto);
}
BoardService
public Board createBoard(Board board) {
...
return boardRepository.save(board);
}
위 코드는 Service 계층에서는 Entity만을 다루고, Controller에서 Entity와 DTO 변환을 수행하는 것을 알 수 있습니다.
2. Service에서 DTO와 Entity를 변환해 사용하는 경우
BoardController
@PostMapping
public ResponseEntity<BoardDto> createArticle(@RequestBody BoardDto boardRequestDto) {
...
BoardDto savedBoard = boardService.createBoard(boardRequestDto);
return ResponseEntity.ok().body(savedBoard);
}
BoardService
public BoardDto createBoard(BoardDto boardRequestDto) {
...
Board board = boardRequestDto.toEntity();
return BoardDto.from(boardRepository.save(board));
}
반면 위 경우는 Service레이어에서 Entity 변환을 시켜주는 것을 알 수 있습니다.
이런 저런 의견들
위 두 가지 방법에 대해 여러 포스팅을 살펴보면, Presentation(View, Controller), Business(Service)간에는 개념적으로 DTO를 전달하는게 맞고, Entity가 Controller에서 사용되어서는 안되기 때문에 Service에서 변환하는 것이 맞다는 의견이 있었습니다.
반대로, 서비스 계층에서 엔티티를 바로 받는 것이 서비스 계층의 재사용성을 높이기 때문에, Controller에서 변환되어야 한다는 의견도 있습니다. Controller에서 반환을 하면, 여러 종류의 서비스에서 해당 컨트롤러를 사용할 수 있습니다.
그리고 DTO는 계층 전달시 사용되는 데이터일뿐, 그 형식은 Entity든 String이든 Map이든 상관없고 중요한 것은 의존관계다 라는 김영한 선생님의 의견도 존재했습니다.
이런 저런 얘기를 들어보면 정해진 정답은 없고 결국, 서비스와 기능에 따라 달라질 수 있다는 것이 결론입니다. 그래서 각각의 상황에서 DTO의 위치를 어떻게 판단을 해야할지를 위해 각 방식의 문제점을 다루어 보겠습니다.
3. 각 방식의 비교
1. Controller 에서 DTO와 Entity를 변환해 Service에 전달하는 경우
BoardController
@PostMapping
public ResponseEntity<BoardResponseDto> createArticle(@RequestBody BoardRequestDto boardRequestDto) {
...
Board board = boardRequestDto.toEntity();
Board savedBoard = boardService.createBoard(board);
BoardResponseDto boardResponseDto = BoardResponseDto.from(savedBoard);
return ResponseEntity.ok().body(boardResponseDto);
}
위 방식의 문제는 View에 반환할 필요가 없는 데이터까지 Entity에 포함되어 Controller(표현 계층)까지 넘어온다는 것입니다.
그리고, Controller가 여러 Domain 객체들의 정보를 조합해서 DTO를 생성해야 하는 경우에 의존성 문제가 커지는 것입니다. 여러 Domain 객체들을 조회해야 하기 때문에 하나의 Controller가 의존하는 Service의 개수가 커지게 됩니다.
하지만 Service 계층에서는 Entity를 바로 받을 수 있기 때문에, Service 계층이 Entity에만 의존하여 재사용성이 높아지는 장점이 있습니다.
2. Service에서 DTO와 Entity를 변환해 사용하는 경우
BoardService
public BoardDto createBoard(BoardDto boardRequestDto) {
...
Board board = boardRequestDto.toEntity();
return BoardDto.from(boardRepository.save(board));
}
반면 위 경우는 controller에서 변환하며 생기는 문제를 상쇄합니다. 하지만, 서비스가 특정 DTO에 의존하기 때문에 여러 종류의 컨트롤러에서 해당 서비스를 사용하기가 어렵다는 단점이 있습니다.
또, 다양한 경로에서 DTO들이 전부 Service로 모이기 때문에, Heavy한 서비스 계층이 생길 수 있습니다.
저의 경우에는 Controller가 View에서 전달받은 DTO만으로 Entity를 구성하기가 어려워, Repository를 통해 여러 부수적인 정보들을 조회하여 Domain 객체를 구성했습니다. 그래서 Service 단에서 DTO Entity 변환을 수행했습니다.
결국 DTO를 어느 계층에서 변환해서 사용할 것인가는 프로젝트 규모나 기능따라 다르니, 의존 관계를 잘 분리하는 것에 초점을 두는 것이 중요해 보입니다.
참고 자료
[테코블 -DTO의 사용 범위에 대하여]
https://tecoble.techcourse.co.kr/post/2021-04-25-dto-layer-scope/
[인프런 - 김영한 선생님 답글]
https://www.inflearn.com/questions/139564/dto-%EC%82%AC%EC%9A%A9%EC%8B%9C%EA%B8%B0%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A7%88%EB%AC%B8
https://www.inflearn.com/questions/53023/dto%EC%9D%98-layer%EC%97%90%EB%8C%80%ED%95%B4-%EC%A7%88%EB%AC%B8-%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4
[우아한 기술 블로그]
https://techblog.woowahan.com/2711/
https://techblog.woowahan.com/2668/
[DTO를 사용해야하는 이유]
https://bnzn2426.tistory.com/137
https://hudi.blog/data-transfer-object/
DTO <-> Entity 간 변환, 어느 Layer에서 하는게 좋을까?
https://velog.io/@jsb100800/Spring-boot-DTO-Entity-%EA%B0%84-%EB%B3%80%ED%99%98-%EC%96%B4%EB%8A%90-Layer%EC%97%90%EC%84%9C-%ED%95%98%EB%8A%94%EA%B2%8C-%EC%A2%8B%EC%9D%84%EA%B9%8C
'Backend > 프로젝트' 카테고리의 다른 글
[에러 해결] docker compose 실행 시 Error while fetching server API version: HTTPConnection.request() got an unexpected keyword argument 'chunked' (0) | 2024.05.09 |
---|---|
젠킨스 설정 후 9090 포트 접속 시 연결이 안되는 상황 (0) | 2024.05.08 |
JaCoCo 적용기 (Gradle) (1) | 2024.04.16 |
[오류] Mac OS Sonoma 14.2.1로 업데이트 후 MySQL Workbench 튕김 현상 (0) | 2024.02.26 |
DBeaver 설치하기 (M1) (0) | 2024.02.20 |