[Spring] 유사 Modular Monolithic Architecture 적용기 (1)
기존 멀티 모듈 구조 아키텍처구조_최종_진짜최종.spring프로젝트 아키텍처 구조 지옥velog.io제가 진행중인 개인 프로젝트는 멀티 모듈 구조로 구성되어 있습니다.멀티 모듈 구조를 채택한 이유는
e4g3r.tistory.com
이전 포스팅에서 기존 레이어 별 멀티 모듈에서 도메인 별 멀티 모듈로 분리하는 작업을 진행했습니다.
하지만 비즈니스 로직을 처리하기 위해 도메인 모듈 간 의존성은 여전히 존재했습니다.
Modular Monolithic Architecture에 좀 가까운 구조로 다가가기 위해 도메인 모듈 간 의존성을 완전히 제거하는 작업을 진행했습니다.
추가로 이것저것 좀 더 다듬어서 1차적으로 완성된 프로젝트 아키텍처 구조에 대한 내용을 작성하고자 합니다.
파사드 패턴?
이전 포스팅에서 리팩토링을 막 마쳤을 때는 도메인 클래스, entity 클래스, repository 클래스 등으로 구성된 domain 모듈,
그리고 이러한 domain 모듈을 조합해서 API를 제공하는 API 모듈로 구성되었습니다.
예를 들어 결제와 관련된 API를 제공하는 qc-payment-api 모듈은 문제 결제, 포인트 결제를 처리하기 위해
question 모듈, coupon 모듈, pay 모듈, point 모듈을 의존하게 됩니다.
qc-payment-api 모듈은 4개의 도메인 모듈을 조합해서 결제라는 비즈니스 로직을 처리하는 파사드 패턴과 더 가깝게 느껴졌습니다.
이 구조도 처음에 매우 만족했지만 복잡한 API를 제공하는 모듈일수록 많은 도메인 모듈에 직접적으로 의존해야한다는 점이 아쉬웠습니다.
애매한 모듈 분리 기준
또한 기존 구조는 api 모듈 분리 기준이 실제 유저들이 인식하는 비즈니스 도메인(상점, 결제, 나의 문제집 등)으로 분리했습니다.
question이라는 도메인은 결제에 활용될 수 있고, 장바구니, 상점, 나의 문제집(library)에서 사용될 수도 있습니다.
그렇기에 question이라는 도메인은 여러 범주에서 사용될 수 있고 여러 곳에서 필요하게 됩니다.
프로젝트가 의외로 단순하기에 일반적으로 도메인 == 비즈니스 도메인이긴 합니다만
종종 question과 같은 도메인이 여러 API 모듈에서 사용되기도 했습니다.
그런데 프로젝트에서 비동기 이벤트 처리 방식이 사용되는데 이 이벤트 리스너는 어디에 위치해야하는가에 대한 고민을 하게 되었습니다.
예를 들어 문제의 판매량, 리뷰 평점과 같은 통계 데이터를 업데이트 하는 이벤트 리스너는 store API 모듈에 위치하는게 맞을까요?
하지만 store API 모듈에서는 직접적으로 통게 데이터를 사용하지 않습니다. 심지어 비즈니스 로직에선 통계 데이터의 존재를 모릅니다.
통계 데이터는 단순히 상품을 조회할 때 정렬하기 위해 domain 모듈 내의 repository에서 사용될 뿐입니다.
그렇다면 도메인 모듈에서 이를 처리해야할까요? 저는 별 문제가 없을 것 같다고 생각하긴 하지만 일반적으로 도메인 모듈은
순수해야한다고 말합니다. 이벤트를 처리하기 위해선 AWS SNS, SQS의 SDK와 같은 의존성이 필요한데 도메인은 POJO여야하므로
지양해야한다고 합니다. 근데 아직까지 저는 이 부분에 대해서 그리 큰 공감은 하지 못하고 있습니다.
아무튼 이렇게 비즈니스 로직이 위치하고 있는 qc-api 모듈의 분리 기준이 비즈니스 도메인 범주이기 때문에
부가적인 비즈니스 로직은 어디에 위치해야 하는가에 대한 문제가 발생하기도 했습니다.
그래서 저는 위 그림 구조에서 영감을 받아 api 모듈과, 도메인 모듈을 분리하지 않고
도메인 별로 분리 된 하나의 모듈에서 domain 로직과, API를 제공하는 구조로 변경하는 작업을 진행하였습니다.
그리고 도메인, 모듈 분리 기준을 여러 도메인의 조합이 아닌 단일 단위로 가져가기로 했습니다.
새로 구상한 모듈 구조
먼저 도메인 분리는 1차 리팩토링에서 진행했던 구조 그대로 사용하기로 하였습니다.
각 도메인들이 적절한 책임을 가진다고 생각하기 때문입니다.
그런데 이렇게 하나의 모듈에서 도메인 로직도 보유하고 API도 제공을 한다면 도메인 클래스를 어떻게 재사용할 수
있을까에 대한 고민을 하게 되었습니다. 제가 멀티 모듈을 선택한 이유는 도메인 모듈의 재사용이 제일 큰 목적이였기 때문입니다.
일반적으로 도메인 모듈을 재사용하게 되는 가장 큰 이유는 관리자 API 인데요.
만약 일반 유저 API 서버와 관리자 API 서버가 동일한 인스턴스에서 실행된다면 그냥 위 Controller 패키지 내부에
AdminAPIController를 추가하면 될 것입니다. 그러나 보안 혹은 망 분리를 위해 별도의 서버로 분리하게 된다면 불가능해집니다.
그렇게 저는 어떻게 하나의 모듈에서 도메인 클래스를 재사용하고, 유저 API와 관리자 API를 분리할 수 있을까에 대해 고민을 했습니다.
여러가지 방식들을 생각한 결과 제일 좋은 방식이라고 생각된 것은 하나의 도메인 모듈에서 core 모듈, api 모듈, admin-api 모듈을
분리하는 것이었습니다.
각 도메인 모듈 내에서 core 모듈은 기존 domain 모듈과 유사합니다.
재사용될 수있는 도메인 클래스, Entity 클래스, Repository등이 있습니다.
api 모듈도 기존 구조와 동일하게 일반적인 User API를 제공합니다.
그리고 api 모듈은 기존 구조와 달리 오로지 자신의 core 모듈만 의존합니다.
그런데 문제를 구매하는 로직은 이미 해당 유저가 문제를 구매했는지, 포인트는 충분한지, 확인해야 합니다.
기존에는 question 도메인 모듈과, point 도메인 모듈을 의존해서 처리했지만 이제는 의존하지 않고 API 인터페이스를 통해 처리합니다.
모듈 간 통신
결제를 처리하는 비즈니스 로직 과정에서는 유저가 이미 문제를 구매했는지 검사하기 위해 question 도메인과 협력해야하고,
포인트는 충분한지 확인 후 포인트를 차감하기 위해서는 point 도메인과 협력해야 합니다.
Modular Monolithic Architecture에서는 일반적으로 모듈 간 통신 방식은 3가지가 있는데 기존 리팩토링 구조처럼
다른 도메인 모듈에 직접 의존하거나, API 인터페이스를 참조 하거나, 이벤트 처리를 하는 방식이 있습니다.
다른 도메인 모듈에 직접 의존하는 방식을 그다지 좋다고 생각하지 않았기에 API 인터페이스를 참조하는 방식으로 구현을 진행했습니다.
프로젝트에서 모듈 간 통신하는 경우는 여러가지가 존재합니다.
1. 장바구니에 담은 상품 정보를 조회하기 위해 장바구니 모듈이 상품 모듈로부터 상품 정보를 받아오는 경우
2. 판매중인 상품에 대한 게시글을 한번에 조회하기 위해 크리에이터 모듈이 게시글 모듈로부터 데이터를 받아오는 경우
3. 결제 처리를 위해 결제 모듈에서 포인트 모듈에게 포인트 차감 요청을 하는 경우
등등..
이와 같은 상황에서 직접 모듈에 의존하지 않기 위해 외부 모듈이 필요한 경우 API 인터페이스를 참조합니다.
저는 이러한 내부 API 인터페이스를 정의해둔 모듈을 별도로 만들었습니다.
말 그대로 API 인터페이스라함은 어떤 요청을 하고자할 때 어떻게 요청을 해야하는지, 어떤 응답이 오는지 정의해 둔 인터페이스입니다.
qc-question-internal-api-interface 모듈 내부에는 Question 모듈에게 요청할 수 있는 API에 대한 정의가 존재합니다.
일반적으로 단순한 조회의 경우 저는 XXXQueryAPI라고 이름을 지었습니다.
이제 결제 도메인이나, 장바구니 도메인은 직접 Question 모듈을 의존하는 것이 아닌 단순한 Interface의 모음집
qc-question-internal-api-interface를 의존하게 됩니다.
그리고 이러한 api-interface의 구현은 해당하는 도메인의 internal-api 모듈 내부에서 구현하게 됩니다.
각 구현체는 Bean으로 등록되어 런타임시에 Spring DI를 통해 주입됩니다.
따라서 이 API를 사용하는 모듈 입장에서는 누가 어떻게 이 기능을 제공하는지에 대해서 알 필요가 없게 됩니다.
즉 느슨한 결합이 됩니다.
이렇게 장바구니에 담은 아이템을 조회하는 경우 cart-api는 questionQueryAPI를 통해 상품 정보를 조회하게 됩니다.
만약 A 모듈에서 B 모듈에 대한 특정 기능이 필요한 경우에는 internal-api-interface에 기능을 명세하고, B 모듈이 구현하도록 합니다.
이후 런타임 시 B 모듈이 구현한 API 구현체가 로딩되도록 하면 런타임 시점에서 A 모듈은 해당 기능을 사용할 수 있게 됩니다.
API-Container
이전 리팩토링과 동일하게 각 모듈들을 실행시킬 Spring Container의 역할을 하는 모듈입니다.
따라서 Spring Security 인증도 처리하고, fitler, 로깅 등을 처리합니다.
qc-api-container의 의존성을 보면 모든 모듈의 api를 주입받고 있고, api 모듈들이 서로 통신할 때 사용하는 internap-api의 구현체
또한 주입받고 있습니다.
최종 구조
클로드 MCP를 요즘 애용하고 있는데 대강 프로젝트 구조를 표현해달라고 해보았습니다.
https://github.com/Question-Cloud/qc-be
코드는 레포지토리를 보면 자세하게 보실 수 있습니다.
마무리
대체 하나의 프로젝트에서 뭐 이리 인터페이스를 만들고 복잡한 과정을 진행해야 하는가에 대한 생각이 들기도 했습니다.
A 모듈에서 B 모듈의 기능을 사용하는 데 뭐 이런 제약을 받아야 하는가..
하지만 늘 진행해온 프로젝트는 단일 모듈, 하나의 프로젝트에서 모든 걸 처리해내는 만능 프로젝트를 만들어왔기에 드는 생각이
아닐까 싶습니다.
물론 추후에 MSA 전환도 없을거고 혼자 하는 프로젝트면 굳이 라는 생각이 들 수도 있지만 언젠가는 큰 프로젝트를 하게 될 것이라고 믿고 지금부터라도 작은 프로젝트에서 해봐야 나중에 쉽게 이해되지 않을까 생각이 듭니다.
그리고 이제 다른 도메인 간 join을 사용하지 않게 되어 select query가 늘어나게 되었단 것인데,
이를 어떻게 해결할 지 고민하는 것도 재밌을 것 같습니다.
'spring' 카테고리의 다른 글
[Spring] 유사 Modular Monolithic Architecture 적용기 (1) (2) | 2025.06.17 |
---|---|
[Spring] EntityManager가 만들어지고 사용되는 과정 (feat.OSIV) (1) | 2025.06.04 |
[Spring] doDispatch 예외 처리 (1) | 2025.06.02 |
[Spring] Spring Micrometer + Grafana Tempo 찍먹 하기 (1) | 2025.05.30 |
JPA @Id 생성을 직접 하는 경우 isNew() 오버라이딩 (0) | 2025.05.22 |