🌈 0. 들어가기 전
이번에 Controller와 Service 레이어에서 Dto를 주고받는 과정에 대해 생각해보게 되었다.
이전에는 관성적으로 Controller와 Service 간의 요청, 응답 Dto 각각 하나로 관리했었다.
이때, Layered Architecture와 유지보수성의 관점에서 두 레이어 간의 Dto 사용을 다시 생각해보게 되었다.
Web 요청 & 응답 시에 Controller와 Service 간의 Dto 사용에 대해 개인적인 생각을 기록하고자 한다.
🎯 1. Web 응답 : Service에서 Response Dto 생성 후 Controller에서 반환?
일반적으로, Web 응답에 Service가 Response Dto를 Controller에 반환하는 경우가 대부분일 것이다.
✅ Service
public StationSaveResponse saveStation(StationRequest stationRequest) {
...
return stationSaveResponse;
}
✅ Controller
@PostMapping
public ResponseEntity<StationSaveResponse> createStation(@RequestBody StationRequest stationRequest) {
StationSaveResponse stationSaveResponse = stationSaveService.saveStation(stationRequest);
...
}
이런 식으로 Controller에서 Service의 비즈니스 로직 메소드를 호출하여 Response Dto를 사용한다.
이렇게만 사용해오다가, 주변에서 Service에서 Response Dto가 아닌 도메인 자체를 넘기는 코드를 봤었다.
그리고, 컨트롤러에서 도메인을 Response Dto로 변환하여 사용했다.
✅ Service
public Station saveStation(StationRequest stationRequest) {
...
return station;
}
✅ Controller
@PostMapping
public ResponseEntity<StationSaveResponse> createStation(@RequestBody StationRequest stationRequest) {
Station station = stationSaveService.saveStation(stationRequest);
// Station 도메인 -> StationSaveResponse 변환 로직
...
}
정리해보면, Web 응답을 처리할 때 2가지 관점이 존재한다.
1. Service에서 Response Dto 생성 후 Controller에서 반환
2. Service에서 도메인 자체를 반환하여 Controller에서 Response Dto로 변환 후 사용
그렇다면, 관성적으로 사용하던 1번이 아닌 2번을 사용한 이유가 뭘까?
주변의 얘기들을 크게 종합해보면 다음과 같은 이유들이 존재했다.
1. 비즈니스 로직을 담당하는 Service 계층에서 View에 종속적인 Response Dto를 반환하는 것은
Service의 책임이 아니라 Controller의 책임이다.
이러한 이유로, 요청 Format이 변할 때 Service 계층까지 변경 영향이 미치는 것이 좋지 않다.
2. Service의 반환이 Response Dto일때, Service 간 의존을 한다면
Dto보다는 도메인을 반환하는 것이 좋을 것이다.
충분히 타당한 이유들로 보인다.
특히 1번 같은 경우에는 Layered Architecture를 사용하는 주 목적인 '관심사의 분리'를 잘 지키는 방향같다.
그래서 더욱 이유가 공감이 갔다.
하지만 나는 그대로 1번 방법을 사용하기로 현재는 결정했다.
그 이유는 2번 방법의 치명적인 단점 때문이었다.
바로, 컨트롤러에 도메인이 노출된다는 점이다.
2번 방법을 선호하는 사람들은 이에 대해 다음과 같은 방안? 해결책? 들을 말한다.
1. 도메인이 노출되는 것이 별로라면, Service -> Controller 간의 Dto를 만들어서 사용하자.
2-1. Controller에서 도메인 로직을 실행시키는 개발자와 일을 하지 않으면 된다.
2-2. 현업에서는 많은 코드리뷰를 통해 Approve된 코드들이 main 브랜치에 머지되므로 오염 가능성이 적다.
1번 방안에 대해서는 충분히 해결할 수 있는 방법같다.
하지만 결국 해당 Dto도 Service에서 View에 의존하는 것이라고 생각한다.
View에 응답할 정보가 변경된다면 해당 Dto도 변경될 것이기 때문이다.
또, 규모가 커진다면 Dto 관리가 엄청 어려워 질 것 같아서 선호하지 않을 것 같다.
2번에 대해서는 동의하지 않는다.
Service에서 Response Dto를 생성해서 Controller에 넘기면 도메인 로직이 실행될 가능성이 0%지만,
Service에서 도메인을 넘긴다면 도메인 로직이 실행될 가능성이 0%가 아니기 때문이다.
결국 코드 리뷰도 사람이 하는 것이기 때문에 휴먼 에러가 발생할 수 있고,
개발한 내가 제어할 수 없는 것이기 때문이다.
그래서 결과적으로 지금은 1번 방법으로 개발을 할 것 같다.
✅ 유지보수성 VS 위험성
각 방법의 trade-off를 요약해보면 위와 같다.
Service에서 Response Dto를 생성하여 Controller에 전달하면
Service가 View에 종속적이게 되어 응답의 Format 변경 시 Service도 변경되어 유지보수성이 좋지 않다.
또한, Service 계층의 책임 관점에서도 올바르지 않은 것 같다.
하지만 이러한 유지보수성과 책임 관점을 따라서 Service에서 도메인을 Controller에 반환한다면
컨트롤러에서 도메인 로직이 실행되는 치명적인 위험이 발생할 수 있다.
물론 작성하는 지금도 이러한 경우는 상당히 드물 것 같긴하지만 가능성이 있다는 것이 좀 찝찝했다.
그래서 유지보수성과 위험성 중 어느 것을 더 중요하게 보느냐에 따라 달라진다고 생각했다.
나는 유지보수성과 위험성의 적용 대상에 집중해서 생각했다.
유지보수성은 개발자에게 적용되지만, 위험성은 일반 사용자들에게 적용될 것이다.
나는 서비스가 사용자에게 친화적이어야 한다고 생각하기 때문에
일반 사용자들이 잘못된 정보를 보게될 위험성을 중요하게 생각한다.
그래서 현재 생각으로는 Service에서 Response Dto를 생성해서 Controller에 전달할 것 같다.
🎯 2. Web 요청 : Controller의 Request Dto를 그대로 Service에 전달?
Web 응답과 비슷하게, 관성적으로 구현했던 것이 있다.
바로 Controller의 Request Dto를 그대로 Service에 전달하는 것이다.
✅ Controller
@PostMapping
public ResponseEntity<StationSaveResponse> createStation(@RequestBody StationRequest stationRequest) {
StationSaveResponse stationSaveResponse = stationSaveService.saveStation(stationRequest);
...
}
✅ Service
public StationSaveResponse saveStation(StationRequest stationRequest) {
// StationRequest 사용하여 비즈니스 로직 수행
...
}
이런식으로 Controller가 Service에 Request Dto를 그대로 전달하여 Service가 사용했었다.
이렇게만 사용해오다가, 주변에서 Service가 비즈니스 로직에 사용할 Service Dto를 따로 만들고,
Controller에서 Request Dto를 Service Dto로 변환하여 Service에 전달하는 코드를 봤었다.
✅ Controller
@PostMapping
public ResponseEntity<StationSaveResponse> createStation(@RequestBody StationRequest stationRequest) {
// StationRequest -> StationCommand 변환
StationCommand stationCommand = stationRequest.toCommand(...);
StationSaveResponse stationSaveResponse = stationSaveService.saveStation(stationCommand);
...
}
✅ Service
public StationSaveResponse saveStation(StationCommand stationCommand) {
// StationCommand 사용하여 비즈니스 로직 수행
...
}
이때, Service에서 사용하는 Dto의 네이밍을 Command로 많이 하는 것 같다.
정리해보면, Web 요청을 처리할 때 2가지 관점이 존재한다.
1. Controller에서 Request Dto를 그대로 Service에 전달하여 사용
2. Service Dto를 따로 만들고, Controller에서 Request Dto를 Service Dto로 변환 후 전달하여 사용
2번 방법을 사용하는 가장 큰 이유는 다음과 같다.
1. View에 Service가 종속적이게 된다.
View의 요청 정보가 변경되면 Service 계층까지 영향을 줄 수 있다.
그래서, Controller와 Service가 강한 의존을 가질 수 있다.
이러한 이유에는 동의한다.
그러나 Service Dto를 생성해야하므로 Dto 관리 범위가 더 커진다는 단점이 있을 것 같다.
또, 정보가 같은데 굳이 Service Dto를 분리해서 할 이유가 있을까?
불필요한 Dto 생성이 아닐까? 라는 생각을 처음 들었을 때 가졌었다.
그런데 다음과 같은 이유가 더 있었다.
Controller에서 원하는 포맷과 Service에서 원하는 포맷이 다르다!
이러한 상황이 있다면, Service Dto를 따로 두는 것이 이해가 된다.
들어본 예시 상황으로는 외부 API가 컨트롤러 단에 존재할 때, Web에서 들어온 Request Dto와
외부 API 호출 후에 Service로 전달될 Dto의 포맷이 달라진다는 상황이었다.
https://techblog.woowahan.com/2711/
우아한형제들 기술 블로그의 'Controller와 Service 레이어의 강한 결합' 챕터 부분에 나와있다!
현재 생각으로는 2번 방법이 Web 응답 부분의 2번 방법의 Controller에서 도메인이 노출된다는 치명적인 단점은 없고,
이론적으로 컨트롤러와 서비스의 의존을 줄이는 방법으로 충분히 공감하지만
관리해야 할 Dto의 수가 늘어난다는 점이 별로라고 느껴지는 것 같다.
그리고 Controller와 Service가 원하는 포맷이 다르다는 상황을 아직 겪어보지 못했고,
일반적으로 쉽게 상상할 수 없어서 더 와닿지 않는 것 같다.
그래서 내린 결론은,
일반적으로는 Controller에서 Service로 Request Dto를 그대로 전달하고
Controller와 Service가 원하는 포맷이 달라지는 상황이 오면 그때 Service Dto를 만들어서 리팩토링 할 것 같다.
이후에 해당 상황을 겪고, 설계 시에 부분적으로 적용하거나 전체적으로 적용할 기준이 생기면 그때 적용할 것 같다!
🎯 3. 결론
Web 요청 & 응답에서 Controller <-> Service 간의 Dto 사용에 대해서 알아보았다.
요약하면 다음과 같은 관점들이 있었다.
* Web 응답
1. Service에서 Response Dto 생성 후 Controller에서 반환
2. Service에서 도메인 자체를 반환하여 Controller에서 Response Dto로 변환 후 사용
---
* Web 요청
1. Controller에서 Request Dto를 그대로 Service에 전달하여 사용
2. Service Dto를 따로 만들고, Controller에서 Request Dto를 Service Dto로 변환 후 전달하여 사용
해당 관점들에서, 모두 일반적인 방법인 1번을 선택했다.
결론적으로는 구현하는 방식에 변함은 없을 것 같지만, 새로운 2번 방식들을 알게됐다는 점에서 너무 좋았다!
또, 현재의 생각이니 추후에 많은 개발 경험을 쌓았을 때는 다른 방식을 선호할 수도 있을 것 같다.
Controller와 Service간 Dto 사용 방식에 관해서 여러 방식의 장단점을 공부했고
내가 1번을 선호하는 주관을 가졌다는 것에 의의가 있을 것 같다!
'아키텍쳐' 카테고리의 다른 글
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기 (1) | 2024.02.02 |
---|---|
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란? (0) | 2024.02.01 |
[아키텍쳐] 패키지 구조 : 계층형 VS 도메인형 어떤 것을 선택할까? (4) | 2023.05.06 |
[아키텍쳐] Service, Repository가 왜 필요할까? (0) | 2023.04.24 |
[아키텍쳐] Layered Architecture란? (feat. 자동차 경주 미션 예시) (7) | 2023.04.21 |