본문 바로가기

프로젝트/taka: 대여사업 관리 서비스

[taka] dto에 관한 고찰

Dto가 필요한 이유

1. entity란 굉장히 여러 곳에서 사용되는 것이기 때문에, entity를 손댔을 때 api 스펙 자체가 변화하면 안된다. 즉, entity와 api가 1:1로 매핑되면 안된다. 또한, 실무에서는 회원가입 케이스가 많이 존재하기 때문에 (ex 사이트 자체 회원가입, 네이버 회원가입, 카카오 회원가입 등...) 하나의 entity로 여러 가지 케이스를 다 감당할 수가 없다. 그렇기 때문에 entity를 외부에 노출하여 외부에서 들어오는 entity를 @ResponseBody 로 바로 바인딩해서 쓰면 안되고, dto 를 활용하여 데이터를 받는 것이 좋다. 즉, api를 만들 때는 entity를 파라미터로 받으면 안되고, 외부에 노출해서도 안된다. 

 

또한 entity 자체를 받아버리면, api 스펙 문서를 까보기 전까지는 어떤 필드들이 넘어오는지를 알 수가 없다. 그런데 Dto를 활용하면 Entity의 어떠한 부분들을 데이터로 전달하고 있는 건지 쉽게 알 수 있다. 그래서 유지 보수 하기가 더 유용하다. 

 

2. 화면에 필요한 데이터만을 선별할 수 있다: 애플리케이션이 확장되면 상황에 따라 엔티티의 모든 필드가 필요한 것이 아니라 상황마다 다르게 사용된다. 상황에 따른 Dto를 별도로 생성해서 필요한 정보만 주고 받으면 효율성을 높일 수 있다.

 

3. 엔티티들이 양방향 참조 된 경우가 있는데, 이 때 컨트롤러에서 양방향 참조된 엔티티를 반환하면 순환 참조가 일어난다.  엔티티가 참조하고 있는 객체는 지연 로딩되고, 로딩된 객체는 또 다시 본인이 참조하고 있는 객체를 호출하며 무한 루프에 빠지게 되는 것이다. 

 

4. 엔티티에는 비즈니스 로직들이 작성되어 있는데, validation 로직까지 Dto에 작성해버리면 코드의 가독성이 떨어진다. 

 

 

Dto의 구조/위치는 어떤 식으로 설계해야 할까?

 

모든 통신에 같은 Dto를 사용하면, overfetching(사용자가 원치 않는 데이터까지 받아오게 되는 상황)이 일어날 수 있기 때문에 이 방법은 지양하는 것이 좋다. 단일 책임의 원칙을 준수하고, 유지 보수를 쉽게 하기 위해서 api 마다 RequestDto, ResponseDto를 분리하기로 하였다. 하지만 모든 api에 대해서 Dto를 생성하면 클래스가 너무 많아져서 관리하기가 힘들다. 따라서 큰 하나의 클래스를 생성한 후  inner class로 Dto를 작성하기로 했는데 - ... 

추가로 DTO의 위치는 저는 해당 DTO를 생성하는 곳에 있어야 하는게 좋다 생각합니다.
예를 들어서 DTO를 리포지토리에서 생성하면 해당 리포지토리와 같은 패키지에 DTO가 있어야 패키지 의존관계가 안전하게 유지됩니다.

 

라는 김영한 강사님의 댓글을 봐서 Dto를 사용하는 곳마다 Dto를 두고 그 안에 inner class 로 Dto 를 작성하기로 했다. 원래는 그냥 model에 다 때려넣었음;;

 

 

Entity - Dto 간 변환은 어떻게 하며, 어느 레이어에서 해야하나?

결국 이 고민은 "의존 관계를 어떻게 할 것인지" 라는 질문인 것 같다. 항상 dependency를 줄이는 것이 중요하며, 그것이 스프링 사용 목적과도 부합한다. 

 

먼저 리포지토리에서 Entity - Dto 간 변환을 하는 것은 좋지 않은데, 왜냐하면 리포지토리에서 영속성 컨텍스트에 관한 조작을 하므로 Entity - Dto 간 변환까지 맡게 되면 너무 복잡해지기 때문이다. 따라서 리포지토리에서는 그냥 Entity를 바로 받아서 조작하는 것이 일반적으로 좋다고 한다. 

 

그렇다면 남은 것은 컨트롤러와 서비스이다. 둘 중 어디에서 변환을 하는게 맞는지에 대한 정답은 없으며, 각각 장단점이 존재한다. 따라서 프로젝트 규모, 특성에 따라 알맞게 선택해야 한다.

 

 

컨트롤러에서 Dto -> Entity 변환이 일어나는 경우

 

먼저 컨트롤러에서 변환이 일어나는 경우, 컨트롤러가 입력으로 Dto를 받으면 controller 내부에서 entity로 바꾸고 service를 호출하며 이후 service가 리턴한 entity를 Dto로 바꾸어 반환한다. 

 

컨트롤러는 DTO, entity, service에 의존하고 / 서비스는 entity와 repository에 의존하게 된다. 서비스에서는 비즈니스 로직을 다루는데, 비즈니스 로직을 다루는 부분이 Dto 에 의존하지 않으므로 서비스의 재사용성이 높다. 하지만 이 경우 비즈니스 로직을 다루지 않는 컨트롤러에 비즈니스 로직이 섞일 가능성이 생긴다.

 

서비스에서 Dto -> Entity 변환이 일어나는 경우

 

다음으로 서비스에서 Dto -> Entity 변환이 일어나는 경우를 살펴보자. 이 경우 컨트롤러는 Dto, service에 의존하고, 서비스는 Dto, entity, service에 의존한다. 이 경우 서비스가 Dto에 의존하고 있으므로, 해당 Dto 가 아니면 서비스를 이용할 수 없다는 점에서 서비스의 재사용성이 굉장히 떨어진다. 

 

컨트롤러의 복잡성을 최소화할 수 있기 때문에 서비스단에서 변환을 하는게 나은 것 같다.

 

ㅋㅋㅋㅋ 여담으로 얼마 전에 스택오버플로우에 질문했는데... 답답해서 대문자로 썼더니 누가 소리지르지 말라고 되게 무례한거라면서 댓글 담;; ㅠㅠ 상처받아서 바로 글 삭제

uppercase가 진짜 무례하냐는 내 질문에 대한 친구의 대답

 


Entity - Dto 간 변환은 어떻게 해야할까?

찾아본 결과, entity - dto 변환에 사용되는 방법에는 크게 세 가지가 있다.

 

1. 자바코드 매핑

- 라이브러리를 사용하지 않고 직접 객체 상태 간의 매핑 로직을 구현하는 방법

- 장점은 안전하지만 단점으로는 코드가 길어진다. 

 

2. MapStruct

- 간결한 객체 간의 변환을 위해 사용되는 라이브러리이다. 코드가 깔끔해진다!

- 컴파일 시점에서 어노테이션을 읽어 구현체를 만들어내기 때문에 리플렉션이 발생하지 않는다. 

- 롬복 어노테이션과의 충돌 가능성: Lombok annotation processor가 getter나 builder 등을 만들기 전에 mapstruct annotation processor가 동작하여 매핑할 수 있는 방법을 찾지 못하게 될 수 있다.  

 

3. ModelMapper

- 변환하려는 대상은 Getter, 변환되어 만들어지는 대상은 Setter가 필요하다. Entity가 DTO로 변환된다고 한다면 Entity에는 각 필드값을 읽을 수 있는 Getter가 존재해야되고, DTO는 필드값을 넣을 수 있는 Setter들이 존재해야 한다. 역으로 Dto를 Entity로 변환한다고 하는 경우 Entity에 Setter이 있어야 한다는 뜻인데, 앞선 게시글에서 알아보았듯 entity에 setter을 쓰는 것은 entity의 데이터를 여러 군데에서 변화시킬 수 있으므로 좋지가 않다. 

- 런타임 시 자바의 reflection api를 이용해서 매핑이 이루어지기 때문에, 컴파일 시점에서 오류를 잡기가 힘들고, 컴파일 시 매핑이 일어나는 MapStruct 보다 성능이 떨어진다. 

-  동시성 성능 이슈가 있다. 수천 TPS의 리엑티브 모델에서는 이 부분이 명확하게 병목으로 나왔다고 한다. (물론 수천 TPS가 안되는 상황에서는 상관이 없다.)

 

 

매핑 라이브러리를 사용하면 장점도 많지만, 결국 수동으로 하는 방법이 디버깅에 더 용이할 것 같아서 그러기로 했다. 


엔티티의 사용은 어느 계층까지 허용할 것인가?

서비스에서는 dto에 의존하지 않도록 하기 위해서 컨트롤러에서 dto -> entity 변환을 하고 그걸 서비스단으로 넘겨준다. 엔티티는 최대한 서비스단에서만 활용하기로 했다. 사실 엔티티가 api 엔드포인트에 노출되지 않도록 하는 것이 중요하지 엔티티를 전 계층에서 사용하는 것은 큰 문제는 없다고 한다. 그럼에도 불구하고 엔티티를 서비스 계층에서만 사용하는 것이 좋은 이유는, 엔티티의 지연 로딩이 가능한 범위 때문이다. 엔티티를 지연 로딩하려면 영속성 컨텍스트가 필요한데, 이 영속성 컨텍스트는 보통 transaction을 시작할 때 영속성 컨텍스트를 시작하고, transaction이 끝날 때 영속성 컨텍스트도 종료된다. 즉 영속성 컨텍스트는 transaction의 범위에 맞춰서 사용되는 것이다. 그런데 만약 서비스 계층을 벗어나서 엔티티를 사용하게 되면, transaction이 종료된 이후에는 지연로딩을 할 수가 없게 된다. (OSIV, OEIV를 써서 영속성 컨텍스트의 생존 범위를 ui 계층까지도 가져갈 수 있다곤 하는데 이 부분에 대해서는 다음에 공부하고 써보겠다.)


<참고 자료>

- Dto 작성법: https://dukcode.github.io/spring/spring-dto/

 

[개발고민] Spring DTO는 어떻게 작성하고 변환해야 할까?

💡 개발고민은 개발을 공부하며 했던 저의 생각들 입니다. 정답이 아니며 정답을 찾아가는 과정이라고 봐주시면 감사하겠습니다. Github Repository Spring DTO는 어떻게 작성하고 변환해야 할까?

dukcode.github.io

 

- Dto <-> Entity 변환은 어디서? : https://blog.hyelie.com/entry/Spring-DTO%EC%99%80-Entity%EC%9D%98-%EB%B3%80%ED%99%98%EC%9D%80-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EB%84%88%EB%AC%B4-%EB%B3%B5%EC%9E%A1%ED%95%9C-Query%EC%9D%B8-%EA%B2%BD%EC%9A%B0-Overfetching-Underfetching

 

[Spring] DTO와 Entity 간의 변환

Spring을 쓴다면 MVC 구조를 사용한다는 것을 전제로 깔고 갈 것이다. 따라서 Controller, Service, Repositoy, DB 순으로 flow가 이동하며, 이 과정에서 entity라는 객체와 DTO라는 객체를 사용한다. 정의를 먼저

blog.hyelie.com

 

- Dto의 필요성: https://tecoble.techcourse.co.kr/post/2020-08-31-dto-vs-entity/

 

요청과 응답으로 엔티티(Entity) 대신 DTO를 사용하자

tecoble.techcourse.co.kr

 

- Dto 위치 : 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

 

Dto 사용시기에 대한 질문 - 인프런

안녕하세요. 항상 강의 잘 듣고있습니다 ! 질문이 두가지 있습니다. 첫째, '어느 레이어에서 DTO로 반환하는가?' 입니다. 현재 강의에서는 controller 에서 repository 를 바로 di 해서 사용하고 있으므로

www.inflearn.com

 

- Dto 구조 : https://velog.io/@ausg/Spring-Boot%EC%97%90%EC%84%9C-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-DTO-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0

 

- https://stackoverflow.com/questions/21554977/should-services-always-return-dtos-or-can-they-also-return-domain-models

 

Should services always return DTOs, or can they also return domain models?

I'm (re)designing large-scale application, we use multi-layer architecture based on DDD. We have MVC with data layer (implementation of repositories), domain layer (definition of domain model and

stackoverflow.com

 

- https://buildplease.com/pages/repositories-dto/

 

A Better Way to Project Domain Entities into DTOs

Imagine you have a nicely designed Domain Layer that uses Repositories to handle getting Domain Entities from your database with an ORM, e.g. Entity Framework, into an MVC view or a Web API controller. Problem is, the Presentation Layer needs objects of a

buildplease.com

 

- https://www.slipp.net/questions/93

 

DTO는 어느 레이어까지 사용하는 것이 맞을까?

지금까지 나는 Data Transfer Object(이하 DTO)를 다음과 같이 사용했다. 예를 들어 slipp.net의 질문 데이터를 QuestionDto에 담는다고 가정할 경우 다음과 같이 처리했다. QuestionController.create(QuestionDto question

www.slipp.net

 

- Dto에 관한 유효성 검사: https://7357.tistory.com/327

 

삽질 기록(19) DTO에서 멤버 객체 유효성 검사하기

지난 프로젝트에서는 모든 요청을 날것(?)으로 받았었는데, 이번 프로젝트는 도메인이 예약이라 그런지 단순 커뮤니티 때보다 요청사항이 복잡해서 객체 형태로 연관된 데이터끼리 모아서 받고

7357.tistory.com

 

- https://lob-dev.tistory.com/entry/%EA%B0%9D%EC%B2%B4-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0-%EC%9E%90%EB%B0%94-%EC%BD%94%EB%93%9C-%EB%A7%A4%ED%95%91-vs-MapStruct-vs-ModelMapper

 

객체 변환하기. 자바 코드 매핑 vs MapStruct vs ModelMapper?

해당 글은 MapStruct Library를 실무에서 사용하기 이전에 학습했던 예제와 장, 단점을 옮겨온 글입니다. (2022-10-26 수정 Benchmark 게시물 링크 추가) 현재 저는 약간의 수고로움을 감수하며 Java Code 기반

lob-dev.tistory.com

 

https://www.inflearn.com/questions/30618/%EA%B6%81%EA%B8%88%ED%95%A9%EB%8B%88%EB%8B%A4

 

궁금합니다. - 인프런

안녕하세요 진짜 좋은 강의와 선생님의 답변으로 많이 배우고 있습니다. 강의를 따라 하다보니 몇가지 궁금증이 생겼습니다. 1. controller에서 responseEntity 를 안쓰시던데 딱히 이유가 있을까요? 2.

www.inflearn.com