과정을 즐기자

각 계층이 가지고 있어야 할 코드에 대한 생각 - Controller, Service, Domain 본문

Spring

각 계층이 가지고 있어야 할 코드에 대한 생각 - Controller, Service, Domain

320Hwany 2023. 1. 19. 14:35

컨트롤러, 서비스, 도메인 각 계층이 가지고 있어야 할 코드에 대해 고민한 내용을 정리하고자 한다.  

 

먼저 어떻게 왜 고민을 하게되었는지 배경부터 말해보면 프로젝트를 진행하면서

만화를 수정할 수 있도록 기능을 추가하려고 하였다.  만화를 수정하려면 먼저

작가로 로그인한 상태에서 만화의 id와 변경할 정보를 CartoonUpdate로 전달해줘야 한다.  

겪은 문제점

Controller

 @PatchMapping("/cartoon/{cartoonId}")
 public ResponseEntity<CartoonResponse> update(@LoginForAuthor AuthorSession authorSession,
                                               @PathVariable Long cartoonId,
                                        @RequestBody @Valid CartoonUpdate cartoonUpdate) {
     Cartoon cartoon = cartoonService.update(cartoonUpdate, cartoonId, authorSession);
     CartoonResponse cartoonResponse = CartoonResponse.getFromCartoon(cartoon);
     return ResponseEntity.ok(cartoonResponse);
 }

컨트롤러에서는 클라이언트로부터 요청을 받고 그 결과를 반환을 해준다. 

 

Service

@Transactional
public Cartoon update(CartoonUpdate cartoonUpdate, Long cartoonId, 
						AuthorSession authorSession) {
    Cartoon cartoon = cartoonRepository.getById(cartoonId);
    Cartoon.checkEnumTypeValid(
    	cartoonUpdate.getDayOfTheWeek(), cartoonUpdate.getProgress());
    cartoon.checkAuthorityForCartoon(authorSession);
    cartoon.update(cartoonUpdate);
    return cartoon;
}

update 메소드가 하는 일은 

1. 서비스에서는 요청 받은 만화의 id로 부터 만화가 존재하는지 확인하고

2. cartoonUpdate로 입력한 정보가 잘못되지는 않았는지 확인하고 

3. 찾은 만화가 지금 로그인한 작가의 만화인지 확인한 후

4 .만화 정보를 바꿔주었다.

 

update 메소드가 너무 많은 역할을 한다는 생각이 들었다. 컨트롤러를 너무 길게 만들면 안된다고 생각을 해서 

cartoonService.update의 인자를 3개씩이나 받아서 처리를 했다. 그 결과 4가지 역할을 하는 메소드를 만들었다.   

분명히 잘못되었다.  따라서 다음과 같이 리팩토링하였다.  

=====================================

OSIV 설정을 False로 변경하려면 이러한 방법은 좋지 않습니다.

밑에 링크를 첨부하겠습니다!

https://320hwany.tistory.com/65

 

OSIV 설정을 False로 변경하기위한 리팩토링 과정

OSIV란 Open-Session-In-View 의 약자로 영속성 컨텍스트를 뷰까지 열어두는 기능을 말합니다. 스프링에서는 기본적으로 OSIV 설정이 true입니다. 설정을 어떻게 하느냐에 따라서 각 계층에서 영속 상태

320hwany.tistory.com

======================================

Controller

@PatchMapping("/cartoon/{cartoonId}")
public ResponseEntity<CartoonResponse> update(
		@LoginForAuthor AuthorSession authorSession, @PathVariable Long cartoonId,
          	@RequestBody @Valid CartoonUpdate cartoonUpdate) {
    CartoonEnumField cartoonEnumField = 
    		CartoonEnumField.getFromCartoonUpdate(cartoonUpdate);
    Cartoon.validateEnumTypeValid(cartoonEnumField);
    cartoonService.validateAuthorityForCartoon(authorSession, cartoonId);
    Cartoon afterUpdateCartoon = cartoonService.update(cartoonId, cartoonUpdate);
    CartoonResponse cartoonResponse = CartoonResponse.getFromCartoon(afterUpdateCartoon);

    return ResponseEntity.ok(cartoonResponse);
}

길이가 길어진 것을 볼 수 있다.  하지만 컨트롤러가 하는 역할은 같다. 클라이언트로부터 요청을 받고 그 결과를 반환해준다. 

 

Service

@Transactional
public Cartoon update(Long cartoonId, CartoonUpdate cartoonUpdate) {
    Cartoon cartoon = cartoonRepository.getById(cartoonId);
    cartoon.update(cartoonUpdate);
    return cartoon;
}

public void validateAuthorityForCartoon(AuthorSession authorSession, Long cartoonId) {
    Cartoon cartoon = cartoonRepository.getById(cartoonId);
    cartoon.checkAuthorityForCartoon(authorSession);
}

다음과 같이 메소드를 서비스에서 2가지로 분류하였다. 이렇게 만드니 테스트하기도 훨씬 쉬워졌다.  

사실 테스트해야 하는 내용은 같으나 하나의 메소드에 같이 있으므로 당연히 더 어려웠던 것이다.  

update 메소드가 하던 나머지 역할들은 도메인 계층에서 처리하였다.

 

도메인, 리포지토리 계층에서 세세한 기능을 테스트하고 서비스 계층에서는 전체 묶은 기능을 테스트하는게 

좋은 방법일 거 같습니다. 

각 계층이 가지고 있어야 할 코드에 대한 생각

 

이렇게 리팩토링을 하고나서 각 계층이 가지고 있어야 할 코드에 대해 생각한 점을 적어보려고 한다.  

일단 계층은 4가지정도로 나눠볼 수 있을 것이다.  Controller, Service, Repository, Domain이다.

 

일단 Controller의 테스트 코드는 위와 같은 리팩토링을 했어도 변경하지 않았다. 

Controller의 역할은 클라이언트의 요청을 받고 결과를 반환하는 역할이다.

 

다음으로 비즈니스 로직을 처리하는 곳은 어디일까?

바로 Domain이다. 위에서 볼 수 있듯이 Service 계층에서의 메소드들은 비즈니스 로직을 처리하는 것이 아니라 

단순히 Repository계층에 있는 데이터를 가져오고 조작하는 것, Domain 계층에 있는 비즈니스 로직들을

연결해주는 역할만 한다. 

 

조금 더 생각해보기 위해 의존관계를 표현해보자 경우의 수로 나누어 표현해보자

1. Controller -> Service -> Domain

2. Controller -> Service -> Repository

3. Controller -> Service -> (Domain, Repository)

 

3가지 경우가 있을 수 있다. 

1, 2, 3번 모두 비즈니스 로직은 Domain, 데이터를 가져오고 조작하는 것은 

Repository를 통해서 이어진다. 

 

위 예시로 다시 생각해보자. 

만화를 수정한다면 만화 id를 통해서 만화를 찾는 로직은 리포지토리에서 이루어진다.  

이 찾은 만화를 수정하는 비즈니스 로직은 도메인에서 이루어진다.  

이 두가지 로직은 서비스 계층에서 합쳐지고 컨트롤러에서는 서비스 계층을 이용한다.

이렇게 함으로써 Cotroller에 의존관계를 주입할 때 Service만 주입할 수 있게 된다.

 

3번의 경우 Domain, Repository의 로직을 합쳐서 컨트롤러에서 사용하기 때문에

Service 계층의 필요성을 느낀다. 하지만 1, 2번의 경우 굳이 필요할까 라는 생각이 들 수 있다.  

 

1번의 경우는 다음과 같이 정적 팩토리 메소드를 사용하는 경우이고 

public static Cartoon getFromCartoonSaveAndAuthor(CartoonSave cartoonSave,
						Author author) {
    return builder()
            .title(cartoonSave.getTitle())
            .author(author)
            .dayOfTheWeek(DayOfTheWeek.valueOf(cartoonSave.getDayOfTheWeek()))
            .progress(Progress.valueOf(cartoonSave.getProgress()))
            .genre(Genre.valueOf(cartoonSave.getGenre()))
            .build();
}

 

2번은 오직 Domain 없이 Service가 오직 Repository 의존하는 경우이다

public List<Cartoon> findAllByTitle(CartoonSearch cartoonSearch) {
    return cartoonRepository.findAllByTitle(cartoonSearch);
}

1번은 서비스 계층이 없어도 될 것 같고 없는 것이 더 좋은 거 같다.

사용하는 것이 오히려 서비스 계층에 메소드만 늘리는 것 같다.

하지만 2번은 사용하는 것이 좋다고 생각한다.

그래야 Controller가 Repository에 의존하지 않고 Service에만 의존하기 때문이다. 

1번은 정적 팩토리 메소드이기 때문에 의존 관계를 주입할 필요가 없다.  

 

정리

정리하자면 비즈니스 로직은 Domain, 데이터를 가져오고 조작하는 것은 Repository에서만 처리하고

Service는 이를 합쳐서 전달하는 역할만 한다.

이때 Repository는 항상 Service를 통해 전달하는 것이 좋고 Domain은 정적 팩토리 메소드의 경우

바로 Controller에서 사용해도 좋을 것 같다. 

물론.. 이게 좋은 방법이라고 단정할 수는 없다. 실무를 해본 것도 아니라... 하지만 나름대로의 결론은 이렇다.

더 좋은 방법이 얼마든지 있다고 생각한다.