서버 구조 리팩토링 하기
서론
이번년도부터 본격적으로 취준에 들어가기 위해서 포트폴리오를 정리를 하면서 현재 서버 클래스 간 종속성을 클래스 다이어그램을 그리면서 파악해봤다.

개발 당시에는 데드라인까지 시간이 여유롭지 않았기 때문에 구조까지 디테일하게 신경쓰기 어려웠기 때문에 복잡도를 포기하고 빠른 개발 생산성을 택했었다. 나름대로 깔끔한 구조를 위해서 도메인별 패키지를 나누어서 작업했지만 도메인 간 경계없이 서로 다른 도메인에 대해서 자유롭게 참조하고 있기 때문에 도메인이 다른 객체끼리도 높은 결합도를 가지게 됐다.
누군가는 “뭐 이런 작은 프로젝트에 의존성까지 신경써?”라는 생각을 할 수도 있을 것 같다. 하지만 언젠가 대규모 프로젝트에서 개발을 한다면 서로 의존성이 엉켜있는 구조를 다시 좋은 구조로 리팩토링할 줄도 알아야 한다고 생각한다.
정말 핵심 비즈니스 로직을 온전히 순수하게 유지하려면 헥사고날 아키텍처를 생각해볼 수도 있지만 아무래도 외부와의 연결이 그렇게 많은 프로젝트는 아니기에 계층형 구조를 유지하면서 의존성을 줄이기로 했다. 먼저 UseCase Diagram을 작성하면서 어떤 기능이 필요한지 더 명확히 알아보고 더 나은 구조를 만들기 개선할 수 있는 부분을 생각해보자

도메인간의 관계를 고려하기
UseCase를 보면 알 수 있드시 우리가 해결해야할 비즈니스 문제는 관리자에겐 식당, 메뉴, 식단 정보를 등록하고 필요 시에 알림을 보낼 수 있게 하고 일반 사용자는 식당, 식단 정보, 알림을 제공받을 수 있게 하는 것이다. 이 상황에서 가장 메인이 되는 도메인인 식당, 메뉴, 식단에서 어떤 역할을 해야하는지 정리해보자
식당
식당 정보에 대한 CRUD를 제공하고 각 식당마다 메뉴를 등록하고 삭제할 수 있는 기능을 제공해야한다.
메뉴
식당의 하위 도메인으로 각 식당의 메뉴에 대해서 CRD를 제공해야하고 동일한 이름의 메뉴는 등록할 수 없도록 해야한다.
식단
이 역시도 식당의 하위 도메인으로 각 식당의 식단을 등록할 수 있고, 식단 마다 메뉴의 변동을 관리하고 휴무, 품절 정보를 제공한다.
이렇게 정리해보니 도메인간 상하관계가 좀 더 직접적으로 드러나며 도메인간 의존성을 단방향으로 더 단순화시킬 수 있을 것 같다.

구조 개선
그럼 이제부터 구체적으로 의존성 하나하나를 구체적으로 살펴보며 개선해보자. 먼저 외부 도메인으로 의존성이 가장 적은 menu가 cafeteria에 대해 의존성을 갖는 부분을 알아보자
menu 도메인
- MenuController -> CafeteriaQueryService
- MenuQueryService -> CafeteriaRepository
- MenuCommandService -> CafeteriaQueryService
위 세 경우를 하나씩 봐보자

식당 식별자를 받아서 메뉴를 조회하는 API이다. 식당의 식별자를 받아 식당을 조회하는 부분에서 cafeteria에 의존하게 됐다.
식당과 연관된 요청이기 때문에 cafeteria의 Controller로 변경시킬 수 있고, 오히려 이 구조가 비즈니스 흐름을 더 직관적으로 나타낼 수 있을 것 같다.
따라서 cafeteria의 Admin용 Controller로 옮기고 기존 API Path인 “/menu?cafeteriaId=식당 식별자 번호” 를 “/cafeteria/{cafeteraId}/menus”로 변경했다.

그 다음은 MenuQueryService의 의존성이다.

Menu를 조회하기 위한 findByCafeteriaAndName 메서드에서 식당 식별자와 메뉴 이름으로 조건으로 두는데 이 과정에서 식당 프록시 객체를 생성하기 위해 cafeteriaRepository가 쓰인 것이었다. findByCafeteriaId 이런식으로 Spring Data JPA 메서드를 통해 쿼리를 만들면 조인이 일어나서 JpaRepository에서 제공하는 getReferenceById 메서드로 엔티티를 생성했던 것인데 이를 그냥 builder로 생성하는 것으로 변경해야겠다.

마지막으로 MenuCommandService이다.

메뉴를 저장하는 과정에서 cafeteriaId를 직접 받기 때문에 이를 조회하는 과정에서 cafeteria에 대한 의존이 생겼다. 앞에서 정리했듯이 cafeteria를 찾는 것은 menu 도메인의 역할이 아니다. cafeteria 도메인에서 수행할 역할이다. 따라서 현재 cafeteria를 받아 조회하는 부분을 미리 조회된 cafeteria를 받는 것으로 변경해주자.
그런데 이렇게 cafeteria를 직접 받는 것으로 바꾸다 보니 이미 식당에 존재하는 메뉴인지를 확인하는 MenuQueryService의 메서드에서 조건문으로 사용했던 cafeteriaId를 직접 cafeteria로 받아야 했다.
곰곰이 생각해보니 애초에 menu도메인에서 순수한 cafeteria가 아닌 cafeteriaId를 받는다는 것 자체가 cafeteria와 관련해서 repository 레이어에서 신경쓰게될 원인을 제공하는 것이기 때문에 이전에 변경했던 MenuQueryService역시 cafeteria를 받도록 변경하는 것이 더 바람직해보인다.
이렇게 cafeteria를 받도록 변경했더니 cafeteriaId 정보를 담은 cafeteria를 만드는 역할이 MenuService에 의존하고 있는 MenuController와 AdminDietController에 넘어가게 됐다.
Controller의 역할
여기서 MenuController와 AdminController의 역할에 대해서도 한번 생각해보자. Controller는 API Path와 이를 작업할 메서드를 연결해주는 핸들러 역할을 한다. 근데 현재 menu와 diet 도메인에선 모두 도메인 내에서 자체적으로만 처리하여 결과를 넘겨주는 API는 존재하지 않는다. 모두 식당과 관련된 작업뿐이다. 따라서 Cafeteria 도메인의 Controller으로 넘겨줄 수 있고 이것이 오히려 요청하는 입장에서도 더 직관적일 것이다. 다만 Controller가 너무 비대해질 수 있다는 단점도 존재한다. 여기서 비대해진다는 것은 단순히 코드의 양이 많아진다는 것을 의미하는 것보단 CafeteriaController의 역할에 맞지 않는 API까지 매핑한다는 의미에 가까울 것 같다. 후에 독자적인 Controller로 나눌 필요가 생길 수도 있겠지만 현재로 봤을 때는 그렇게 필요하지 않은 것 같다.
표로 정리하자면 아래와 같다.
구분 | 도메인별 Controller | 하나의 Controller (도메인별 서비스 분리) |
---|---|---|
장점 | - 각 도메인에 집중하여 책임 분산 가능 - 도메인별로 독립적인 개발 및 테스트 용이 - 코드가 단순하고 모듈화된 설계 | - 비즈니스 흐름을 하나의 컨트롤러에서 관리하여 전체 구조가 명확 - 도메인 간 의존성을 깔끔하게 관리 가능 - 도메인별 서비스에 로직 분리로 유지보수 및 확장성 증가 |
단점 | - 요청에서 도메인 간 관계가 명시적으로 드러나지 않음 - 비즈니스 로직이 여러 컨트롤러에 분산되어 흐름이 산만할 수 있음 - 도메인 간의 협력 로직 관리가 복잡해질 가능성 있음 | - 컨트롤러가 비대해질 가능성 (도메인 간 협력이 많을수록 복잡도 증가) - 상위 컨트롤러가 너무 많은 책임을 질 경우 설계가 무거워질 수 있음 |
적용에 적합한 상황 | - 도메인의 독립성이 강하고, 도메인 간 관계가 적은 경우 - 도메인별로 작업을 분리하여 독립적으로 개발해야 하는 경우 | - 요청 처리에서 도메인 간의 관계를 명확히 드러내야 하는 경우 - 비즈니스 흐름이 도메인 간 협력으로 구성된 경우 - 유지보수와 확장성을 중요시하는 프로젝트 |
현재 모든 메뉴, 식단 API들은 모두 식당의 식별자를 같이 받아서 식당 현재 식당, 메뉴, 식단 3가지 도메인은 서로 밀접한 관계를 가지며 협력을 하기 때문에 하나의 상위 도메인인 cafeteria에만 컨트롤러를 두기로 했다.
이전에 진행했던 ‘탐식당’ 프로젝트를