-
Service 끼리 참조 vs Service -> Repository 구조 유지프로그래밍 2025. 5. 1. 15:51반응형
MVC 구조로 개발하다 보면 Controller -> Service -> Repository 구조로 개발을 진행하게 되는데, 다른 도메인의 참조가 필요할 때가 있다.
예를 들면 Board에서 Category가 필요하거나, User 삭제 시 연관된 게시물, 댓글들을 모두 삭제하는 것 등이 있다.
이 때 두 가지 고민을 주로 하게 되는데, 다른 도메인의 Service를 참조해서 로직을 구성하는 것과 Repository를 참조해서 로직을 구성하는 것 두 개 중 하나를 선택하게 된다.
각 방법을 살펴보고 장단점을 파악해보고자 한다.1. Service -> Repository 구조
계층형 아키텍처를 보면 Controller -> Service -> Repository 순으로 접근하게 되고, Repository가 Service를 참조하는 등 하위 계층이 상위 계층을 참조하지 않는다.
이렇게 했을 때의 장점은 각 계층의 관심사에 집중할 수 있다는 것이다. Controller는 사용자와의 상호작용만을 검증하고, 비즈니스 로직 부분은 Service에 위임하는 방식이다. 똑같이 Service는 비즈니스 로직에 집중하고, 데이터 액세스나 처리는 Repository 계층에 위임할 수 있다.
여기서의 원칙은 상위 계층은 하위 계층만을 참조하는 것이다. Controller는 Service를 참조하고, Service는 Repository를 참조하게 된다.
코드로 살펴보면 다음과 같다.public class BoardController { private final BoardService boardService; @PostMapping public Mono<Boolean> save(@RequestBody BoardSaveRequestDto request) { return boardService.save(request); } } public class BoardService { private final BoardRepository boardRepository; public Mono<Boolean> save(BoardSaveRequestDto request) { return repository.save(Board.of(request)) .thenReturn(true); } }
여기서 만약 Category라고 하는 다른 도메인이 필요할 때 Repository를 참조한다면 다음과 같게 된다.public class BoardService { private final BoardRepository boardRepository; private final CategoryRepository categoryRepository; public Mono<Boolean> save(BoardSaveRequestDto request, String categoryName) { return categoryRepository.findByName(categoryName) .flatMap(category -> boardRepository.save(Board.builder() .categoryId(category.getId()) .title(request.getTitle()) .content(request.getContent()) .build())) .thenReturn(true) .as(operator::transactional); } }
Service -> Repository 구조를 지켰을 때의 장점은 순환 참조가 발생하지 않는다는 점이다. 순환 참조란 다음과 같다.public class BoardService { private final ReplyService replyService; } public class ReplyService { private final BoardService boardService; }
이렇게 되면 애초에 실행이 되지 않는다. BoardService 객체를 만드는데 ReplyService 객체가 필요한데, ReplyService 객체를 만드려면 BoardService 객체가 필요하기 때문에 오류가 발생한다.
그러나 Service -> Repository 의 경우 하위 계층을 반드시 참조하게 때문에, 순환 참조가 발생할 일이 없다.
Service -> Repository의 단점은 비즈니스 로직이 중복될 수 있다는 점이다.
예를 들어 CategoryRepository에서 id 값으로 조회할 때 없을 경우 CustomException을 발생시키고, "해당 카테고리가 존재하지 않습니다" 라는 메시지를 남긴다는 규칙이 있다고 가정하자.
그러면 다른 도메인에서 category.findById()를 부를 때마다 해당 코드가 중복되게 된다. 여기서 문제는 해당 규칙이 바뀔 경우 모든 부분을 바꿔줘야 한다는 점이다.2. Service끼리 참조
이 때 Service끼리 참조하는 것이 유리할 수 있다.
public class CategoryService { private final CategoryRepository categoryRepository; public Mono<Long> findIdByCategoryName(String name) { return categoryRepository.findByName(String name) .switchIfEmpty(Mono.error(new CustomException("해당 카테고리가 없습니다"))) .map(Category::getId); } } public class BoardService { private final BoardRepository boardRepository; private final CategoryService categoryService; public Mono<Boolean> save(BoardSaveRequestDto request, String categoryName) { return categoryService.findIdByName(categoryName) .flatMap(categoryId -> boardRepository.save(Board.builder() .categoryId(categoryId) .title(request.getTitle()) .content(request.getContent()) .build())) .thenReturn(true) .as(operator::transactional); } }
이런 식으로 BoardService에서 CategoryService를 참조함으로써 category의 비즈니스 로직은 CategoryService 내에 집중시킬 수 있고, 다른 서비스는 이를 사용하기만 하면 되는 구조로 만들 수 있다.
이 구조의 단점인 순환 참조의 경우 두 개를 사용하는 새로운 서비스를 생성해서 해결할 수 있다.
예를 들어, User를 제거할 때 Board들을 모두 제거해아 한다는 규칙이 생긴 상황에서 이미 Board가 UserService를 사용하는 상황에는 UserDeleteService와 같은 서비스를 새로 만드는 것이다.public class UserDeleteService { private final UserService userService; private final BoardService boardService; private final TransactionalOperator operator; public Mono<Boolean> deleteByEmail(String email) { return userService.findByEmail(email) .flatMap(userId -> Mono.zip(userService.deleteByUserId(userId), boardService.deleteByUserId(userId))) .thenReturn(true) .as(operator::transactional); } }
이렇게 되면 순환 참조를 막으면서 Service 끼리 참조하는 것의 장점을 가져갈 수 있다.반응형'프로그래밍' 카테고리의 다른 글
Project Reactor 연산자 정리 (0) 2025.05.13 linuxserver/ffmpeg 에서 drawtext 사용 시 한글 문제 (1) 2024.07.14 Java heap memory 늘리기 (0) 2024.06.26 Dockerfile 작성 시 주의할 점 (0) 2024.06.26 MySQL - 쿼리 작성 순서와 실제 실행 순서 (0) 2023.07.29