Controller의 Request 객체를 Service에게 넘기는 게 좋을까?

kdkdhoho·2023년 4월 23일
1

Spring

목록 보기
26/26

개요

웹 자동차 경주 미션 1단계 피드백 중에 이런 리뷰가 있었습니다.

이 리뷰를 받고 페어와 함께 이야기를 나눠보았을 때, 둘 다 동시에 "오.. 이 부분에 대해 전혀 생각하지 못했다" 고 말했습니다.

아마 이 내용에 대해 많은 사람들도 고민해보지 못했을 것이라고 생각합니다. (아님 말고)
따라서 먼저 고민하고 내린 결론을 모두에게 공유하고 싶어서 이 글을 작성합니다.

(좋은 고민 거리를 만들어주신 조앤 감사합니다. 🙇‍♂️)

Request 객체를 Service에 넘겼을 때

예시 코드

@Controller
public class WebController {

    @Autowired
    private final GameService gameService;

    @PostMapping("/plays")
    public ResponseEntity<PlayResponse> plays(@RequestBody @Valid final PlayRequest playRequest) {
        PlayResponse playResponse = gameService.playRacing(playRequest);
        return ResponseEntity.ok().body(playResponse);
    }
}
@Service
public class GameService {

    public PlayResponse playRacing(final PlayRequest playRequest) {
        Cars cars = racingGame.createCars(playRequest.getNames());
        TrialCount trialCount = racingGame.createTrialCount(playRequest.getTrialCount());

        // other logic ..
    }
}

장점

1. Request 객체를 전달함으로써, 값 전달이 끝납니다.

2. Service에서 필요한 정보를 선택적으로 사용할 때 유용합니다.

단점

1. 서비스 계층의 재사용성이 떨어집니다.

Controller와 Service의 메서드는 항상 1:1 관계가 아닙니다.
코드로 조금 더 쉽게 이해해보겠습니다.

만약 웹 자동차 경주가 한국에서 흥하여 미국까지 진출했다고 가정해봅시다. (오..)
우선 미국인들의 이름에 따라, 아래와 같은 UsaPlayRequest 를 만들어 보겠습니다.

@Getter
@AllArgsConstructor
public class UsaPlayRequest {

    @NotBlank(message = "Please Input First Name")
    private final String firstName;
    
    @NotBlank(message = "Please Input Middle Name")
    private final String middleName;

    @NotBlank(message = "Please Input Last Name")
    private final String lastName;

    @Min(value = 1, message = "The trialCount must be greater than 1")
    private final TrialCount trialCount;
}

그리고 미국에서 오는 요청을 따로 처리해주기 위해 아래와 같은 컨트롤러를 만들었다고 가정해보겠습니다.
이때 기존 컨트롤러와 마찬가지로 프로그램의 핵심 도메인 객체인 GameService를 의존합니다.

@Controller
public class UsaController {

    @Autowired
    private final GameService gameService;

    @PostMapping("/plays/usa")
    public ResponseEntity<PlayResponse> plays(@Valid @RequestBody final UsaPlayRequest usaPlayRequest) {
        PlayResponse playResponse = gameService.playRacing(PlayRequest.from(usaPlayRequest));
        
        return ResponseEntity.ok().body(playResponse);
    }
}

이처럼, 하나의 Request 객체를 파라미터로 받는 서비스를, 많은 컨트롤러가 사용하게 된다면 별도의 변환 과정이 필요합니다.

2. Request 객체의 변경이 Service 계층에 영향을 미칠 수 있다.

Service가 Request 객체를 의존합니다. 이때 Request 객체가 변경한다면 그 영향은 Service Layer까지 미칠 것입니다.

결국 이렇게 정리할 수 있을 것 같습니다.

편리성 vs 유연성


대안

다른 방법은 없을까 생각해보았습니다.

1. Service에서 필요한 데이터만 파라미터로 받는다.

public PlayResponse playRacing(final String names, final int count) {
    // logic
}

만약 playRacing() 메서드를 위처럼 선언한다면, 어느 컨트롤러에서 어떤 요청 객체가 생기든 간에, 필요한 값만 get() 하여 건네주면 됩니다.

따라서 서비스 계층의 유연성도 챙기고 별도의 변환 과정도 필요없게 됩니다.

2. Dto를 이용한다.

만약, Service 계층의 메서드에서 필요한 데이터가 10개라면 위의 방법을 사용하는 것은 분명 무리입니다.
메서드의 파라미터가 10개가 되어야 하니까요.
이럴 때는 메서드가 필요로 하는 데이터만을 담은 Dto 객체를 만들고, 해당 객체만을 파라미터로 받습니다.

아래 코드처럼 할 수 있을 것입니다.

public class PlayDataDto {
    private final String names;
    private final int trialCount;
    
    public static PlayDataDto from(final UsaPlayRequest) { .. }
    
    public static PlayDataDto from(final KoreaPlayRequest) { .. }
public PlayResponse playRacing(final PlayDataDto playDataDto) {
    // logic
}

컨트롤러에서는 요청 객체를 Dto로 변환하여 전달하면 됩니다.
변환하는 데에 비용이 드는 것은 사실이지만, 요청 객체의 변경이 Service Layer까지 미치는 것은 막을 수 있습니다.

개인적으로 Dto를 이용하는 기준은 Service 메서드에서 필요한 파라미터 수가 4개 이상인 경우에 사용하면 된다고 판단합니다.

결론

개인적으로 판단했을 때, Request 객체가 Service Layer까지 침투하는 것은 그리 좋아보이지 않습니다.

만약 구현하는 서비스 메서드가 재사용될 일이 없다는 예측이 간다면 필요한 Request 객체를 받아도 좋을 것 같습니다.

하지만 그렇지 않은 메서드에서는 생각해본 대안을 통해 불필요한 의존성을 끊고 '보다 유연한 코드'를 짜는 것이 중요하다고 생각합니다.

profile
newBlog == https://kdkdhoho.github.io

4개의 댓글

comment-user-thumbnail
2023년 4월 23일

서비스가 Presentation Layer의 객체에 의존하므로 재사용성이 떨어지고, 상위 레이어의 변경사항이 전파될 수 있는 문제였군요.
의존관계를 끊어서 해결한 점이 좋은 것 같습니다.

자동차경주가 미국에 진출한다는 가정도 너무 재밌었는데, 읽다보니 하나 궁금한 점이 생겼어요.
미국에 진출해 다른 형식의 이름도 지원해야 하는 것은 입출력 요구사항만 변화된 것일까요?
아니면 비즈니스 요구사항 자체가 변화한 것일까요?

생각하고, 풀어내기 나름이겠지만
저는 사용자 풀이 변했고, 그에 따라 이름이라는 모델 자체가 firstName, middleName, lastName 구조로 확장되었으므로 후자라고 생각했어요.
또한 이름을 잘 담고, 저장하고, 두 형식의 이름을 일관된 방식으로 다루기 위해 서비스 레이어를 변화시킬 것 같아요.
예를 들어 다음처럼 이름을 대표하는 값 객체를 만들고 이용할 것 같습니다:

class Name {
  ...
  public String getFirstName();
  public String getMiddleName();
  public String getLastName();
  // eq&hc
}

도기는 지금의 구조를 유지하실 건가요?

1개의 답글
comment-user-thumbnail
2023년 7월 27일

블로그에 등장하다니 영광이에요!!

1개의 답글