Post

(Spring) MVC Layered Architecture - DTO 전달/변환/파라미터 설계

그림1

Controller -> Service 호출 시, Service의 메서드 파라미터 설계

1
2
3
4
5
6
7
8
9
10
@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는 전혀 변경 안해도 된다.
  • 만약 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가 business layer를 넘어 persistence layer 까지 도달하는 것도 좋아보이지 않습니다.
  • 적당한 수준을 그림으로 정리하면 아래와 같습니다.

객체 별 이동 가능 범위(영역)와 변환(화살표)

사례 1 ) 외부 데이터를 받아 별다른 작업 없이 그대로 외부 API 요청하는 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 (비즈니스에서는 의미가 없고 단순히 외부 요청 파라미터에 포함해야만 하는)
}
1
2
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 저장 구조가 변경된다고 검색 조건 자체가 바뀌지는 않는다. 쿼리는 변경될 수 있어도 “어떤 속성으로 찾을 것인가”는 도메인 모델의 속성에 의존적인 정보다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
}

searchCondition 전달/변환 예시

DTO-Model 변환 책임

Controller<>Service 구조에서 presentation DTO-Model 변환 책임은?

 ProsCons
Controller에서presentation DTO는 UI가 달라지면 변경될 수 있는 부분이므로 controller에 두는게 맞다. e.g., web 용 DTO와 native GUI 용 DTOdomain 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<>DAO(Repository) 구조에서 Model-persistence DTO 변환 책임은?

  • 가능하면 DAO(Repository)에서 한다. (시스템 내에서 데이터를 DTO로 다루는 구간은 최소한으로 유지하는 것이 좋다.)

Model <> DTO 변환은 별도의 Data Mapper 생성 도구를 이용하는 것이 좋다.

  • DTO<>Domain Model 변환 메서드를 직접 작성하게 되면, 1. 매핑 작업이 너무 많아 개발 시점에도 비효율적이고, 2. 필드/추가 삭제 시 매핑 함수가 곧 변경 포인트가 되어 유지보수하기 나쁘다.
  • DataMapper와 MapStruct 같은 Data Mapper 자동 생성 도구를 이용하는 것이 좋다.

이 이상의 너무 세부적인 원칙은 별로 의미가 없다.

원칙이 세부적일 수록, 잘 들어맞지 않는 케이스가 많아진다. 일반화 할 수 있는 몇 가지 중요한 원칙을 기반으로, 세부적인 원칙은 각자가 처한 상황과 시스템에 맞게 세워야 한다.

This post is licensed under CC BY 4.0 by the author.