[Spring] MVC Layered Architecture : DTO 전달/변환/파라미터 설계
Controller -> Service 호출 시, Service의 메서드 파라미터 설계
```java
@PostMapping("/some-path")
public ResponseEntity<...> foo(@RequestBody @Valid FooControllerRequest request) {
varService.doSomething(request.getGroupId(), request.getUserId()); ...
}
/* foo API에 변경이 발생했다 */
@DeleteMapping("/some-path/groups/{groupId}/users/{userId}")
public ResponseEntity<...> foo(@PathVariable String groupId, @PathVariable String userId) {
varService.doSomething(groupId, userId); ...
}
```
- Controller가 변경되었지만 Service 메서드 파라미터는 그대로. => Service는 전혀 변경 안해도 된다.
- 만약 ``java varService.doSomething(FooControllerRequest)`` 이었다면, doSomething 내부에서 파라미터 사용하는 부분을 수정해주어야 한다. (변경 부담이 크지 않을지라도, 가능하면 의존성은 줄이는 것이 낫다)
파라미터가 2-3개 정도라면 이렇게 풀어서 넘기면 되니 문제가 없다.
하지만 파라미터가 5개 이상, 더 많아진다면 어떻게 해야할까?
layer 분리와 그에 따른 DTO 분리는 필요하지만, 예를 들어 presentation DTO를 반드시 presentation layer에서만 사용 할 필요는 없습니다.
- presentation DTO가 presentaion layer 바깥으로 나가지 못하게 엄격하게 제한하는 것은 DTO를 목적에 맞지 않게 사용할 가능성을 차단 할 수는 있습니다만, 오히려 시스템의 유연성을 떨어트리고 불필요한 매핑 작업을 초래합니다.
- "presentation DTO와 Domain Model이 잘 분리되어 있는 시스템" 이라면, presentation DTO가 service layer로 전달되더라도 변경 부담이 크지 않습니다.
- 중요한 것은 어느 layer로 전달되느냐 보다, 목적에 맞게 사용하느냐 입니다.
- 그래서 사용할 layer를 한정하지는 않되, 패키지 정도는 꼭 나눠주는 것이 좋아보입니다.
- 하지만 그렇다고 presentation DTO가 persistence layer 까지 도달하는 것도 좋아보이지 않습니다.
- 적당한 수준을 그림으로 정리하면 아래와 같습니다.
사례 1 ) 외부 데이터를 받아 별다른 작업 없이 그대로 외부 API 요청하는 경우
```java
FooControllerRequest {
@swagger
@NotEmpty
String clientIP
@swagger
boolean mobile
... 기타 8개 필드 ...
}
FooRequest { // 굳이 필요한가?
String clientIP
boolean mobile
... 기타 8개 필드 ...
}
FooExternalRequest {
/* 클라이언트에서 받아야 되는 파라미터 */
@JsonProperty(clnt_ip)
String clientIP
@JsonProperty(mobile_yn)
String mobile // Y or N
... 기타 8개 필드 ...
/* 서버에서 세팅해야 하는 파라미터 */
String serverCode (비즈니스에서는 의미가 없고 단순히 외부 요청 파라미터에 포함해야만 하는)
}
```
```java
presentation DTO -> external DTO (실용주의)
presentation DTO -> VO -> external DTO (엄격한 layering)
```
- DTO를 분리하는 이유를 생각해보면, '수평 방향의 책임을 나눠 god class를 예방하자' 인데, 위와 같은 경우 FooRequest가 단순히 'Business layer에서 사용되는 용도의 객체' 이외의 다른 의미나 책임을 가지지 못한다. 즉 VO가 어느 한쪽으로 혼합되는 나쁜 케이스가 아니라 VO 자체가 필요가 없는 케이스다.
- 이런 경우에도 VO를 만들면 구조적 통일성을 지킬 수는 있겠지만, 그 보다는 불필요한 Mapping 작업에서 오는 피로도가 더 클 것으로 보인다. 이런 케이스는 실용주의적으로 접근하는게 낫다.
- presentation DTO와 external DTO가 패키지로 잘 나뉘어져 있다면, VO가 필요해지는 시점에 눈치채고 VO를 분리할 수 있을 것으로 보인다.
- 보통은 service 호출처가 Controller 하나라서 분리 하지 않는게 편하지만, 같은 service layer 내에서도 호출되어야 한다면 VO 분리를 고려해볼만 하다.
- 파라미터를 넘기기 위해 service layer 내에서 FooControllerRequest를 만드는 것은 애매할 수 있기 때문.
사례 2 ) SearchCondition
- [컨트롤러 검색 조건 파라미터 받아서 ➞ DB에서 꺼내서 ➞ Domain Model 반환] 하는 케이스를 생각해보자.
- selectOne이라면 보통 pk로 검색하고, 조건이 추가로 있다고 해도 많지 않아서 파라미터를 쪼개서 전달해도 되며, find* 메서드의 개수도 부담되지 않는 수준이다.
- 반면, selectMany의 경우 조건이 많아진다. 예를 들어 파라미터가 5개 이상이라면, 필요한 findAllBy* 메서드의 개수는
\\[{}_5 \mathrm{ C }_1 + {}_5 \mathrm{ C }_2 + ... + {}_5 \mathrm{ C }_5 = 2^5 - 1\\]
- 이런 상황 때문에 보통 파라미터 조건을 if로 처리하는 select 쿼리 1개를 범용적으로 사용하게 되고, 이 때 파라미터로 넘겨 받을 검색 조건을 명시용 클래스가 필요하다. (SearchCondition)
- 참고) JPA 에서도 SearchCondition 받아서 QueryDSL 사용하는 경우 있다. (link) (또는 Criteria)
- SearchCondition 객체는 DTO가 아니라 VO로 보는 것이 적절하다.
- persistence 저장 구조가 변경된다고 검색 조건 자체가 바뀌지는 않는다. 쿼리는 변경될 수 있어도 "어떤 속성으로 찾을 것인가"는 도메인 모델의 속성에 의존적인 정보다.
```java
Controller(searchRequest) {
service(searchRequest.param1, searchRequest.param2)
}
Service(param1, param2) {
searchCondition = param1 + param2 + getTheOthers()
repository(searchCondition)
}
/* 위와 동일한 개념으로 가려면 */
Controller(searchRequest) {
service(searchRequest)
}
Service(searchRequest) {
searchCondition = searchRequest + getTheOthers()
repository(searchCondition)
}
```
Controller<>Service 구조에서 presentation DTO-Model 변환 책임은?
Pros | Cons | |
Controller에서 | presentation DTO는 UI가 달라지면 변경될 수 있는 부분이므로 controller에 두는게 맞다. e.g., web 용 DTO와 native GUI 용 DTO |
domain model을 controller에서 만들고, controller로 반환하게 되니 controller에서 domain model에 접근이 가능하다. controller에서 DTO 변환 이외의 다른 작업을 수행할 여지가 있다. (실수로) |
Service에서 | service input/output이 애초에 DTO이면, controller에서 domain model에 접근할 여지가 없다. controller가 domain model을 조작하는 실수를 막을 수 있다. | service layer에서 presentation DTO에 대해 알고 있어야 한다. |
- 애초에, SearchCondition 계열이나, presentation DTO -> external DTO로 바로 매핑해야 하는 케이스는 presentation DTO가 service layer 파라미터로 전달 되는 것이 자연스럽다.
- 따라서 변환에 대한 책임을 강제하기 보다는, 상황에 맞게 유연하게 선택하는 것이 좋아보인다.
Service<>Repository<>Mapper 구조에서 Model-persistence DTO 변환 책임은?
- Repository와 DataMapper의 차이, 개념
- 요약하면, Repository에서 한다.
Model <> DTO 변환 책임을 가진 Mapper라는 별도의 클래스를 두는 경우도 있다.
- 이는 Repository와 DataMapper의 차이, 개념 글에서의 DataMapper와는 다르다.
- 위 글에서 DataMapper의 역할은 DTO <> Data Source 변환 (Mybatis @Mapper)
- 여기서 의미하는 Mapper의 역할은 Domain Model <> DTO 변환 (위 글에서는 Repository에서 변환)
- https://www.baeldung.com/java-dto-pattern
- https://proandroiddev.com/the-real-repository-pattern-in-android-efba8662b754
- https://mapstruct.org/ 의 Mapper interface 참조
Mapper 클래스를 별도로 두기 보다는, DTO 메서드로 Domain Model을 생성하는 것은 어떨까?
DTO <> Model 변환 코드. 어디에?
- DTO에 두는 경우
- pros ) Domain model에는 비즈로직이 들어가야 해서, 지저분한 변환 로직은 DTO에 두는 것이 낫다. 어떤 데이터를 Domain Model에 맞게 어떻게 변환할 것인지는 data src 측인 DTO가 알고 있는 것이 더 타당하다.
- cons ) 2개의 DTO가 1개의 Domain Model이 되는 경우는 커버가 안된다
- 서비스에 두는 경우
- Mapper라는 변환용 클래스를 두는 경우를 생각해보면, DTO, Model 외부의 Service에서 하는 것도 적절해 보인다.
- Domain Model에 두는 경우
- 도메인 모델은 비즈니스 로직을 담고 있어야 해서 매핑로직이 들어가는 것은 가독성을 해치고 좋지 않아 보인다.
웬만하면 DTO에 두고, 2개의 DTO를 조합해 Domain Model을 만들어야 하는 경우 각 서비스 또는 Domain Model에서 처리
```java
class DTO {
public DomainModelVO toVO() // 이 경우 DomainModel의 빌더가 유용 할 수 있다.
public DomainModelEntity toEntity()
public static DTO from(DomainModel) // 또는 생성자
}
// domain model에서 처리해야 한다면
class DomainModel {
public DomainModel from(DTO) // 이 경우 파라미터를 풀어서 받는 것은 비추. 파라미터가 많아지면 너무 지저분해진다.
}
```
이 이상의 너무 세부적인 원칙은 별로 의미가 없다.
원칙이 세부적일 수록, 잘 들어맞지 않는 케이스가 증가하게 된다. 일반화 할 수 있는 몇 가지 중요한 원칙을 기반으로, 세부적인 원칙은 각자가 처한 상황과 시스템에 맞게 세워야 한다.
참고
'System Design > Layered Arch' 카테고리의 다른 글
[Spring] MVC Layered Architecture : DTO와 Domain Model을 분리해야 하는 이유 (0) | 2022.03.14 |
---|---|
[마틴파울러] Layering 관련 글 모음 (0) | 2022.03.13 |
Domain Model에 대해서 (0) | 2022.03.11 |
Repository와 DataMapper의 책임 (w/o ORM) (0) | 2022.03.09 |
[Spring] MVC Layered Architecture : Controller와 Service의 책임 나누기 (0) | 2020.07.06 |