통합대비

초기엔 Modulith 구조로 시작했습니다. 한 애플리케이션 안에서 order, inventory를 패키지로 명확히 분리하되 같은 JVM에서 직접 호출하도록 했어요. 처음부터 MSA로 가면 인프라 부담과 분산 트랜잭션 문제로 개발 속도가 떨어진다고 판단했습니다. 다만 나중에 분리할 가능성을 염두에 두고, 인벤토리 호출은 InventoryClient 인터페이스로 추상화하고 Spring Profile로 구현체를 주입받게 설계했습니다. 모놀리식 단계에선 LocalInventoryClient가 같은 JVM의 InventoryService를 직접 호출하는 구현체였고, 비즈니스 로직 클래스인 OrderService는 인터페이스에만 의존했어요. 인벤토리 도메인의 트래픽이 늘고 독립 배포 필요성이 생기면서 별도 서비스로 분리했는데, 이때 OrderService 코드는 한 줄도 안 바꿨습니다. LocalInventoryClient를 HttpInventoryClient로 교체하고 Profile만 prod로 바꾸면 됐어요. 분리 후엔 RestClient로 HTTP 호출하면서 Resilience4j Circuit Breaker로 장애 전파를 막았습니다.”

면접관 추가 질문 대비

이 답변을 했을 때 면접관이 깊이 파고들 가능성이 높은 질문들이에요.

Q1: “분리 시점은 어떻게 판단했나요?”

“단순히 트래픽 수치로 정한 게 아니라 몇 가지 신호를 봤습니다. 인벤토리 도메인의 배포 빈도가 다른 도메인보다 훨씬 잦아져서 함께 배포되는 게 부담이 됐고, 인벤토리 로직 변경 시 다른 도메인의 회귀 테스트까지 돌려야 하는 비용이 컸어요. 또 인벤토리만 별도로 스케일링하고 싶은 요구가 생겼고요. 이런 신호가 누적됐을 때 분리했습니다.”

Q2: “Modulith에서 다른 모듈 직접 호출하면 결합도가 강한데 괜찮았나요?”

“맞아요, 그래서 두 가지 안전장치를 뒀습니다. 첫째, 인벤토리 모듈은 InventoryService만 public이고 내부는 internal 패키지로 캡슐화해서 외부 모듈이 함부로 접근 못 하게 했습니다. 둘째, 호출 측에서 InventoryClient 인터페이스로 한 번 더 추상화해서 실제 의존성이 인터페이스로만 향하게 했어요. 결합도는 명확하면서 분리 가능성은 열어둔 구조였습니다.”

Q3: “왜 처음부터 MSA로 안 갔나요?”

“팀 규모와 도메인 성숙도가 맞지 않았습니다. 도메인 경계가 확정되지 않은 상태에서 서비스를 쪼개면 잘못된 경계로 나뉠 위험이 컸고, 한 번 쪼개면 되돌리기 어렵습니다. 또 분산 트랜잭션, 모니터링, 인프라 비용이 초기에 부담이 컸어요. Modulith로 시작하면 도메인을 충분히 관찰한 후 자연스러운 경계에서 분리할 수 있어서 이쪽을 택했습니다.”

Q4: “분리할 때 DB는 어떻게 했나요?”

“초기엔 같은 DB의 다른 스키마를 썼습니다. inventory 모듈은 inventory 스키마만 접근하도록 강제했고, 다른 모듈이 인벤토리 테이블을 직접 조회하는 일이 없도록 코드 리뷰에서 확인했어요. 서비스 분리 시점에 inventory 스키마를 별도 DB로 옮겼고, 데이터 일관성이 필요한 곳은 이벤트 기반으로 동기화했습니다.”

Q5: “분리 후 트랜잭션 처리는 어떻게 바뀌었나요?”

“분리 전엔 @Transactional 하나로 주문과 재고를 묶을 수 있었는데, 분리 후엔 분산 트랜잭션 문제가 생겼습니다. 처음엔 동기 호출이라 재고 차감 후 주문 저장이 실패하면 보상 호출로 재고를 복구했고, 나중에 Kafka 기반으로 전환하면서 Saga 패턴을 적용했습니다. 주문은 PENDING으로 일단 저장하고 이벤트로 재고 처리 후 결과 이벤트로 확정/취소하는 방식이에요.”

추가 가산점 멘트

답변 끝에 이런 마무리를 더하면 좋아요.

“이 경험을 통해 배운 건, MSA가 목적이 아니라 도구라는 점입니다. Modulith로 도메인을 충분히 관찰하면 어디를 어떻게 분리해야 할지 자연스럽게 보이고, 분리 비용도 인터페이스 추상화로 미리 줄여둘 수 있다는 걸 느꼈어요.”

이런 메타 인사이트가 들어가면 “기술을 도구로 보는 시니어 마인드” 라는 인상을 줍니다.