그림1

 

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)
  • SearchCondition 객체는 DTO가 아니라 VO로 보는 것이 적절하다.
    • persistence 저장 구조가 변경된다고 검색 조건 자체가 바뀌지는 않는다. 쿼리는 변경될 수 있어도 "어떤 속성으로 찾을 것인가"는 도메인 모델의 속성에 의존적인 정보다.

 

```java

Controller(searchConditionRequest) {

    service(searchConditionRequest.param1, searchConditionRequest.param2)

}

 

Service(param1, param2) {

    searchCondition = param1 + param2 + getTheOthers()

    repository(searchCondition)

}

 

/* 위와 동일한 개념으로 가려면 */

Controller(searchConditionRequest) {

    service(searchConditionRequest)

}

 

Service(searchConditionRequest) {

    searchCondition = searchConditionRequest + getTheOthers()

    repository(searchCondition)

}

```

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 변환 책임은?

 

Model <> DTO 변환 책임을 가진 Mapper라는 별도의 클래스를 두는 경우도 있다.

 

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)  // 이 경우 파라미터를 풀어서 받는 것은 비추. 파라미터가 많아지면 너무 지저분해진다.

}

```

 

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

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

 

참고