
"팀 규모는 작은데 코드는 점점 스파게티가 되어간다..." 🍝
개발하다 보면 누구나 겪는 딜레마. 모놀리스(Monolith)로 빠르게 시작하긴 했는데, 기능이 늘어날수록 의존성이 뒤엉켜서 '거대한 진흙 덩어리(Big Ball of Mud)'가 되어버린다. 그렇다고 당장 MSA로 넘어가자니 인프라 비용이랑 관리 포인트가 너무 부담스럽고.
그래서 찾아본 현실적인 대안, Spring Modulith. 스프링 공식 프로젝트라 믿을만하고, '모듈러 모놀리스(Modular Monolith)'를 강제해 주는 도구다. 핵심만 빠르게 정리해 둔다.
쉽게 말해 "패키지 구조가 곧 아키텍처가 되게 만드는" 프레임워크다.
패키지 = 모듈: 최상위 패키지 바로 아래 있는 패키지들을 하나의 '논리적 모듈'로 본다.
경계 강제: 내 모듈의 public 클래스만 남이 쓸 수 있게 하고, 내부는 철저히 숨긴다.
느슨한 결합: 모듈끼리는 웬만하면 이벤트를 던져서 소통한다.
직접 써보니 좋았던 점은 구조 검증, 이벤트 처리, 문서화 딱 3가지다.
보통 개발하다 보면 Order에서 Inventory 부르고, Inventory가 다시 Payment 부르고 난리가 난다. Spring Modulith는 테스트 코드 한 줄로 순환 참조나 허용되지 않은 의존성을 잡아낸다.
Java
class ApplicationModulesTest {
@Test
void verifyModularity() {
// 전체 모듈 구조가 규칙을 잘 지키는지 검증
// 순환 참조 있으면 바로 테스트 실패 뜸
ApplicationModules.of(Application.class).verify();
}
}
빌드 타임에 아키텍처 깨지는 걸 막을 수 있다는 게 엄청난 장점.
서비스끼리 메서드 직접 호출(의존)하지 말고, 이벤트(Event)를 던지라는 거다. 특히 Transactional Outbox Pattern 구현이 진짜 편하다.
상황: 주문 완료되면 -> 재고 줄이기
주문 모듈: 주문 끝나면 OrderPlacedEvent 발행
재고 모듈: 이벤트 리스닝해서 처리
Java
// 재고 모듈 (Inventory)
@Component
@RequiredArgsConstructor
class InventoryEventListener {
private final InventoryService inventoryService;
// @EventListener 말고 이거 씀
// 트랜잭션 커밋 된 후에 실행됨을 보장해 줌
@ApplicationModuleListener
void on(OrderPlacedEvent event) {
inventoryService.decreaseStock(event.orderId());
}
}
@ApplicationModuleListener: 트랜잭션 성공했을 때만 실행되고, 실패하면 재시도 처리까지 지원함. 비동기 처리가 세상 편해짐.
코드 짜기도 바쁜데 문서 업데이트는 언제 함? 얘는 코드를 분석해서 모듈 간 관계도(C4 Model)를 PlantUML로 그려준다.
Java
@Test
void writeDocumentationSnippets() {
new Documenter(ApplicationModules.of(Application.class))
.writeDocumentation() // build 폴더에 puml 파일 생성됨
.writeIndividualModulesAsPlantUml();
}
코드가 바뀌면 문서도 알아서 바뀐다. 이게 진짜 문서화지.
패키지 구조가 곧 모듈 정의다.
src/main/java
└── com.example.shop
├── ShopApplication.java
├── inventory // [Inventory 모듈]
│ ├── Inventory.java
│ ├── InventoryService.java (public - 공개 API)
│ └── internal // (숨김 - 외부에서 접근 불가!)
│ └── InventoryRepository.java
├── order // [Order 모듈]
│ └── OrderService.java
└── payment // [Payment 모듈]
internal 같은 패키지에 넣어둔 건 외부 모듈에서 import 하려고 하면 컴파일러가(혹은 테스트가) 혼낸다. 강제로 캡슐화가 됨.
"지금 당장 MSA 하긴 오버인데, 나중에 쪼갤 준비는 하고 싶을 때"
배포는 하나로 퉁쳐서 운영 편의성은 챙기고, 코드 내부는 MSA처럼 깔끔하게 유지할 수 있다. 나중에 트래픽 터져서 떼어내야 할 때도, 이미 모듈 경계가 확실해서 찢어내기 쉽다.
레거시가 엉망이거나, 신규 프로젝트 깔끔하게 시작하고 싶다면 Spring Modulith, 무조건 찍먹해볼 만하다.
📚 Reference
Spring Modulith Reference Documentation