Spring

[Spring]Web - Service Layer 간 상호 의존 문제(Circular Dependency)

Woonys 2022. 7. 16. 15:27
반응형

Introduction

지난주에 첫 PR을 올렸다! <스프링부트와 AWS로 혼자 구현하는 웹서비스> 책을 참고해 작성한 POST 로직 관련 기능이었다. 기대 반 걱정 반으로 PR을 올렸는데 아니나 다를까 피드백이 줄줄이 달리기 시작했다. 그 중에서 꽤나 인상적인 리뷰가 있었다.

 

nit: How about thinking this? 🙇‍♂️ but, You don't need to fix it now

(N)ot (I)mportant, (T)hough

  • Controller(package-web) has a dependency on Service(business logic/package-service)
  • Service(business logic/package-service) has a dependency on DTO(package-web)

what is the Issue?

먼저 코드부터 살펴보자. 해당 이슈는 책에서 동일하게 발생하였기에 책의 코드를 참조해 설명하겠다.

PostsService(/service)

package springboot.service.posts;
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) { // DTO에 의존
        return postsRepository.save(requestDto.toEntity()).getId();
    }
	...
}

PostsService의 8번째 줄 public Long save(PostsSaveRequestDto requestDto) 에서 PostsService는 Web 레이어에 있는 requestDTO에 의존한다.

PostsSaveRequestDto(/web/dto)

package springboot.web.dto;
...
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto { // Controller와 Service에서 사용할 DTO 클래스!
    private String title;
    private String content;
    private String author;
    @Builder
    public PostsSaveRequestDto(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntity() {
        return Posts.builder()
            .title(title)
            .content(content)
            .author(author)
            .build();
    }
}

이번에는 Web 레이어에 있는 Controller 클래스로 가보자. Web 레이어에 있는 PostsController는 서비스 레이어의 PostsService에 의존한다.

 

PostsController(/web)

package springboot.web;
...

public class PostsApiController {
    private final PostsService postsService; // PostsService에 의존

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto); // 등록 기능! -> web에서 post API로 관리.
    }
...
}

 

정리하면

  • Controller(웹 패키지)는 Service(비즈니스로직/서비스 패키지)에 의존한다.
  • 서비스(서비스 패키지)는 DTO(웹 패키지)에 의존한다.

이렇게만 보면 Controller → 서비스 → DTO 방향으로 의존하고 있으니 잘 설계된 것 같아보인다. 하지만 상위 레이어 관점에서 살펴 보면 웹(Controller) → 서비스(Service)이자 동시에 서비스(Service) → 웹(DTO)으로 레이어 간에 상호 의존이 발생한다. 이렇게 서로가 서로를 참조하는 상황을 상호 의존성(Circular Dependency)라고 한다.

 

이게 왜 문제가 되는 걸까?

위키피디아에 Circular dependency를 검색하면 다음과 같이 여러 문제가 있음을 알 수 있다.

*상호 참조(순환 의존)는 소프트웨어 프로그램에 원치 않는 여러 영향을 야기할 수 있다. 소프트웨어 설계 관점에서 대부분의 문제는 서로 독립적인 모듈 간에 강한 결합이 있을 때 발생하는데, 이는 개별적인 단일 모듈을 재사용하기 어렵게 하거나 혹은 불가능하게 만든다.

 

상호 참조는 도미노 효과를 일으킬 수 있는데, 하나의 모듈에서 작은 국소적 변화가 있다고 해보자. 이는 다른 모듈까지 영향을 미쳐 결과적으로는 전역적으로 원치 않는 문제를 일으키게 된다(프로그램 에러 혹은 컴파일 에러 등). 또한 상호 참조는 무한 재귀 또는 예상치 못한 실패를 만들기도 한다.  

 

범용적인 관점에서 상호 의존은 피하는 게 좋다. 상호 의존을 하면 의존하는 클래스들 간에 강한 결합이 발생한다. 만약 특정 클래스 A가 변한다면, A에 의존하고 있는 B는 아무런 문제가 없음에도 같이 변해야 하는 상황이 발생하게 된다. 여기서 문제는 그 변화가 다시 A에 영향을 끼친다는 것. 고작 A-B 두 모듈 간의 상호 참조면 그나마 나은데 이게 C, D, E, … 계속해서 이어진다고 생각해보자. 의존도가 높을수록 유지보수에 매우 큰 악영향을 끼칠 우려가 있다. 이를 어떻게 해결할 수 있을까?  

 

How to solve it?

Service 레이어가 Web layer에 의존해야 하는 이유를 생각해보자. 왜 의존해야 하지? 반대로 Web layer가 Service에 의존해야 하는 이유 역시 고민해보자. 대체 왜?  

 

Web 레이어의 경우 의존의 목적이 명확하다. 웹 레이어는 클라이언트로부터 요청 및 응답에 대한 처리를 해주는 계층이다. 내부 비즈니스 로직은 여기서 처리하지 않는다. 따라서 서버 내부에서 작업한 비즈니스 로직을 전달해주기 위해서는 웹이 서비스에 의존해야 한다. 그렇다면 서비스는? 웹에서 요청한 게 무엇이든 간에 자신이 해야 할 일만 처리하면 되지 않나? 어차피 웹에서는 서비스를 의존하고 있으니 서비스에 맞게 데이터를 제공해줄텐데? 따라서 의존성의 방향을 한쪽 방향으로 바꿔줄 필요가 있다. 의존성의 방향을 한쪽으로만 통제하면 변경에 영향을 받는 부분을 명확하게 이해할 수 있어진다.  

 

현재 모양새를 보면 아래와 같다.

  • Web: Controller / DTO
  • Biz logic: Service / Entity

웹 레이어는 Controller/DTO가, 비즈니스 로직은 Service/Entity 레이어에서 처리하고 있다. 그리고 의존성의 방향은 Web → Service → Entity로 흐르도록 하는 것이 좋다. 따라서 현재 DTO에 의존하고 있는 Service를 끊고 이를 Entity에 의존하도록 의존성 방향을 바꾼다.  

 

Controller는 서비스에, 서비스는 기존에 DTO를 가리켰던 의존성 방향을 Entity로 옮긴다. 어라, 그러면 여기서 의문이 생긴다.  

 

분명히 책에서는 서비스 레이어가 Entity에 의존하지 말라 그랬는데요!!

 

여기서 <스프링부트와 AWS로 혼자 구현하는 웹 서비스>에 대한 비판점이 등장한다. 꼭 이 말이 정답은 아니라는 것(물론 향로님은 개인적으로 엄청 존경하는 분이기에 이 글과 별개로 향로님을 향한 리스펙은 변함이 없음을 밝힌다). 아래 글은 <언제 DTO를 쓰지 말아야 할까?>로 따로 아티클을 하나 배포할 예정.  

 

영한님 스프링부트 강의에서 Q&A에 이런 글이 올라와있다.  

 

Dto 사용시기에 대한 질문

먼저 DTO를 간단하게 생각해야 합니다. DTO는 단순히 계층간 데이터를 전달할 때 사용하지만, 그것이 필수는 아닙니다. 물론 레이어간 데이터 이동이 필요하면 DTO를 이동해도 되지만, Entity를 이용하셔도 되고, 단순히 String, Map등을 이용해도 됩니다. 정말 중요한 것은 의존관계라는 관점이 중요합니다.

  

ex) 특히 다음과 같은 로직은 의존관계 관점에서 문제가 있습니다.

 

XXDto found = XXService.findById(id)

XX foundToEntity = XXDto.toEntity() // <-- 서비스레이어에서 dto 를 반환할때, 이부분이 너무 불편합니다.

 

Reference

https://brunch.co.kr/@fishz/143

https://hyper-cube.io/2018/03/30/circular_dependency/

https://en.wikipedia.org/wiki/Circular_dependency

https://www.inflearn.com/questions/139564

반응형