← Back to list

MSA도, 모놀리스도 아닌 제3의 선택 — Spring Modulith

들어가며

이지희Rosie(로지)/상품개발팀 in 여기어때 기술블로그 · 2026-05-19 06:16 · 267 claps · 22.1 min read
#msa #spring-modulith #tech
Open on Medium ↗

MSA도, 모놀리스도 아닌 제3의 선택 — Spring Modulith

들어가며

안녕하세요. 여기어때 상품개발팀 로지입니다.

“MSA로 가야 하지 않을까요?”

서비스가 커지면 흔히 나오는 이야기입니다. 모놀리스에서 기능이 늘어날수록 코드 간 의존이 복잡해지고, 배포 한 번에 전체 서비스가 영향을 받는 상황이 반복되면 자연스럽게 MSA를 떠올리게 됩니다.

하지만 현실의 MSA는 다른 이야기를 들려주기도 합니다. 하나의 기능을 위해 여러 서비스를 동시에 수정·배포해야 하고, 진정한 MSA라면 독립 배포가 가능해야 함에도 점검을 걸고 새벽에 동시 배포를 해야 하는 식이죠. 저희 팀도 신규 프로젝트를 계획하며 비슷한 고민을 했습니다. Endpoint가 많지 않은 서비스를 굳이 여러 개로 쪼갤 가치가 있을까? 연관된 서비스를 매번 함께 배포해야 한다면, 그건 진정한 MSA일까?

그렇다면 모놀리스의 단순함은 유지하면서, MSA처럼 모듈 간 경계를 명확히 할 수는 없을까요?

이 질문에 대한 답이 바로 Modular Monolith, 그리고 이를 Spring 생태계에서 지원하는 Spring Modulith입니다. 결국 MSA든 모놀리스든, 응집과 결합을 다스리는 것이 먼저입니다.

모놀리스 vs MSA, 그리고 Modular Monolith

모놀리스(Monolith)

하나의 애플리케이션, 하나의 배포 단위입니다. 개발 초기에는 빠르게 기능을 만들 수 있고, 트랜잭션 관리도 간단합니다. 하지만 서비스가 성장하면 코드 간 의존이 스파게티처럼 얽히면서 이른바 Big Ball of Mud(큰 진흙 덩어리)가 될 수 있습니다.

[하나의 큰 애플리케이션]
├── 숙소 관리
├── 요금 관리
├── 재고 관리
├── 검색
└── 예약
→ 모든 것이 하나로, 경계 없이 뒤섞임

MSA(Microservices Architecture)

서비스를 작은 단위로 분리하여 독립적으로 배포합니다. 서비스 간 경계가 명확하지만, 분산 트랜잭션 처리, 서비스 간 통신, 장애 추적 등 새로운 복잡성이 생깁니다. 서비스들이 결국 서로 복잡하게 얽히면서 분산된 모놀리스(Distributed Monolith : 물리적으로는 분리됐지만 함께 배포해야 하고 서로의 변경에 영향을 받는 상태) 가 되어버리는 경우도 발생합니다.

[숙소 서비스] ←API 호출→ [요금 서비스] ←API 호출→ [재고 서비스]
     ↕                   ↕                      ↕
  [숙소 DB]            [요금 DB]               [재고 DB]
→ 완전 분리, 하지만 통신/운영 비용 증가

Modular Monolith

모놀리스와 MSA의 중간점입니다. 하나의 애플리케이션 안에서 모듈 간 경계를 명확히 정의합니다. 배포는 하나지만, 내부적으로는 각 모듈이 자신만의 도메인, 규칙, 인터페이스를 가집니다.

[하나의 애플리케이션]
├── 숙소 모듈 (독립된 도메인, 명확한 인터페이스)
├── 요금 모듈 (허용된 의존성만 참조 가능)
├── 재고 모듈 (이벤트로 다른 모듈과 통신)
├── 검색 모듈 (읽기 전용, 조합만 담당)
└── 예약 모듈
→ 하나의 배포 단위, 그러나 내부는 명확히 분리

각 접근 방식을 비교하면 다음과 같습니다.

잘 모듈화된 모놀리스는 단순한 시작점이 아닙니다. 추후 마이크로서비스 수준까지 발전시킬 수 있는 기반이 되며, 이것이 Modular Monolith의 핵심 가치입니다.

Spring Modulith는 어떻게 우리 문제를 풀었나

Spring Modulith는 Spring Boot 애플리케이션에서 Modular Monolith를 구현할 수 있도록 지원하는 공식 프로젝트입니다. Spring 진영에서 2022년부터 incubation 단계로 진행되었고, 1.0 GA가 2023년 11월에 출시되어 현재 2.x 안정 버전까지 나와 있습니다. 저희 팀은 Spring Boot 3.4 위에서 1.3.x 버전을 운영 중이며, Spring Boot 4 마이그레이션 시점에 Modulith 2.x로 함께 올릴 예정입니다.

Spring Modulith의 기능을 매뉴얼처럼 나열하기보다는, 저희가 실제로 마주친 문제와 그것을 어떻게 해결했는지를 중심으로 풀어 보겠습니다.

문제 1 — “이 모듈을 참조하면 안 됩니다”가 코드 리뷰에 의존하는 상황

모놀리스에서 흔히 겪는 일입니다. 아키텍처 규칙이 README나 위키 페이지에만 적혀 있어, 신규 인원이 들어오거나 시간이 지나면 자연스럽게 위반이 쌓입니다. 코드 리뷰에서 “이 모듈에선 저 모듈을 참조하면 안 돼요”를 매번 잡아내는 데에도 한계가 있죠.

Spring Modulith는 @ApplicationModule 어노테이션으로 모듈을 정의하고, allowedDependencies에 어떤 모듈을 참조할 수 있는지 명시적으로 선언합니다.

@ApplicationModule(
    allowedDependencies = ["property", "cancellationpolicy", "shared"]
)
class RatePlanModule

allowedDependencies에 명시되지 않은 모듈을 참조하면 검증 테스트에서 실패합니다. ApplicationModules.of().verify()를 CI에서 돌리면, 누군가 허용되지 않은 모듈을 참조하는 코드를 작성했을 때 빌드가 실패합니다.

class ModularityTests {
    private val modules = ApplicationModules.of(AriAutoConfig::class.java)

    @Test
    fun `모듈 구조가 allowedDependencies 규칙을 준수하는지 검증`() {
        modules.verify()
    }
}

위반이 발생하면 다음과 같이 어떤 모듈에서 어떤 내부 컴포넌트에 접근했는지 명확하게 알려줍니다.

Module 'rate' depends on non-exposed type
kr.co.gccompany.stay.product.ari.room.adapter.output.RoomQueryAdapter
within module 'room'!

아키텍처가 문서가 아니라 실행 가능한 테스트로 관리되는 것이죠.

Open vs Closed, 그리고 Named Interfaces

여기서 한 가지 짚어둘 게 있습니다. @ApplicationModule은 두 가지 타입을 가질 수 있습니다.

  • **Type.CLOSED** (기본값): 모듈 루트 패키지의 public 타입만 외부에 노출되고, 내부 패키지는 접근 시 verify()가 실패합니다.
  • **Type.OPEN*: 모듈 내부 패키지까지 외부에 모두 노출됩니다. 의존성 방향*만 강제할 뿐, 캡슐화는 강제하지 않습니다.

저희 팀은 도입 초기에 Type.OPEN을 적용해 의존성 방향만 강제하고 있습니다. 정리가 끝난 모듈부터 CLOSED로 좁혀가는 것도 자연스러운 다음 단계가 될 수 있겠죠.

@ApplicationModule(
    type = ApplicationModule.Type.OPEN,    // 도입 초기 — 의존성 방향만 강제
    allowedDependencies = [...]
)
class RatePlanModule

CLOSED에서 외부에 명시적으로 노출하고 싶은 패키지가 있다면 @NamedInterface로 지정합니다.

// roomrate 모듈의 application/port/ 패키지에 package-info 형태로 선언
@org.springframework.modulith.NamedInterface("ports")
package kr.co.gccompany.stay.product.ari.roomrate.application.port

이렇게 하면 다른 모듈에서는 roomrate::ports라는 명명된 인터페이스로만 접근하도록 강제할 수 있습니다. MSA에서 서비스가 API만 외부에 노출하는 것과 같은 효과를 애플리케이션 내부에서 얻을 수 있는 것이 Named Interfaces의 핵심입니다.

문제 2 — 모듈을 분리한 다음, 통신은 어떻게 할 것인가

모듈 경계를 그어 놓고 나면 곧바로 다음 질문이 따라옵니다. 모듈 간 통신은 직접 호출로 할 것인가, 이벤트로 할 것인가?

저희 팀의 답은 둘 다 쓰되, 트랜잭션 경계를 기준으로 나누는 것입니다.

이벤트는 만능이 아닙니다. 같은 트랜잭션 안에서 일관된 검증이 필요한 read는 직접 호출이 정답입니다.

같은 트랜잭션 안에서 다른 모듈의 데이터를 읽어 비즈니스 로직을 수행해야 할 때는 해당 모듈의 query port를 직접 주입받아 호출합니다. 예를 들어 Rate 모듈에서 요금을 저장할 때는 RoomRate와 RatePlan의 존재 여부를 같은 트랜잭션 안에서 검증해야 하므로, RoomRateQueryPortRatePlanQueryPort를 직접 사용합니다. 이걸 이벤트로 처리하면 타이밍 문제가 생기고, 검증 자체가 불가능해집니다.

반면 변경 이후의 후속 처리를 트랜잭션에서 분리하고 싶을 때는 이벤트를 씁니다. 이벤트 패턴이 가장 잘 맞는 사례가 Search 모듈입니다. 저희 서비스의 검색 데이터는 Room, RatePlan, RoomRate, CancellationPolicy, ChildPolicy 다섯 개 모듈의 변경을 받아 갱신되는 read model입니다. 만약 이걸 port 직접 호출로 구현했다면, Search 모듈이 상위 모든 모듈에 역방향 의존을 갖게 되어 계층 구조가 무너집니다. 이벤트 덕분에 각 모듈은 “누가 내 이벤트를 수신하는지” 알 필요 없이 발행만 하면 됩니다.

// 검색 모듈 — 5개 모듈의 변경 이벤트를 한 곳에서 구독해 검색 데이터 동기화
@Component
class SearchRoomRateSyncEventListener(
    private val syncUseCase: SearchRoomRateSyncUseCase
) {
    @ApplicationModuleListener
    fun on(event: RoomChangedEvent) {
        syncUseCase.sync(SearchRoomRateByRoomIdsSync(event.ids))
    }
    @ApplicationModuleListener
    fun on(event: RatePlanChangedEvent) {
        syncUseCase.sync(SearchRoomRateByRatePlanIdsSync(event.ids))
    }
    @ApplicationModuleListener
    fun on(event: RoomRateChangedEvent) {
        syncUseCase.sync(SearchRoomRateByRoomRateIdsSync(event.ids))
    }
    @ApplicationModuleListener
    fun on(event: CancellationPolicyChangedEvent) {
        syncUseCase.sync(SearchRoomRateByCancelPolicyIdsSync(event.ids))
    }
    // ... CancellationPolicyDeleted, ChildPolicyChanged 등
}

@ApplicationModuleListener는 Spring Modulith가 제공하는 어노테이션으로, 내부적으로는 @TransactionalEventListener(AFTER_COMMIT) + @Async + @Transactional(REQUIRES_NEW)의 조합과 동일합니다. 즉 발행 모듈의 트랜잭션이 커밋된 이후에 별도 스레드, 별도 트랜잭션으로 처리되도록 보장됩니다. Search 동기화가 실패해도 원래 요청에는 영향을 주지 않습니다.

이벤트의 안전망 — event_publication 테이블

Spring Modulith는 여기에 더해 이벤트 영속화도 지원합니다. spring-modulith-events-jdbc를 사용하면 이벤트 발행과 처리 완료 여부가 event_publication 테이블에 자동으로 기록되어, 장애 발생 시 미완료 이벤트를 재처리할 수 있습니다. IncompleteEventPublications 빈을 통해 미완료 이벤트를 조회·재발행할 수도 있습니다.

통신 방식 테스트 — Scenario API

이벤트 기반 통신의 흔한 의문 중 하나가 “비동기로 처리되는 흐름을 어떻게 테스트하느냐”입니다. Spring Modulith는 spring-modulith-starter-test로 이를 위한 fluent API를 제공합니다.

@ApplicationModuleTest
class RoomRateChangePropagationTest(val scenario: Scenario) {
    @Test
    fun `RoomRate 변경 시 Search 모듈로 이벤트가 전파된다`() {
        scenario.publish(RoomRateChangedEvent(listOf(1L, 2L)))
            .andWaitForEventOfType(SearchSyncCompletedEvent::class.java)
            .toArrive()
    }
}

이벤트 발행을 트리거하고 일정 시간 안에 다른 모듈에서 처리됐는지를 그대로 검증할 수 있어, 모듈 간 흐름을 통합 테스트로 묶기 수월해집니다.

문제 3 — 아키텍처 문서가 코드와 어긋나는 상황

모듈 구조와 의존 관계는 PlantUML 다이어그램과 AsciiDoc 문서로 자동 생성할 수 있습니다.

@Test
fun `모듈 문서를 자동 생성한다`() {
    Documenter(modules)
        .writeDocumentation()
        .writeIndividualModulesAsPlantUml()
}

실행하면 build/spring-modulith-docs/ 경로에 모듈 의존성 다이어그램이 생성됩니다. 모듈 구조가 변경되면 다이어그램도 자동으로 업데이트되므로, 아키텍처 문서가 코드와 괴리되는 문제를 방지할 수 있습니다.

Documenter가 자동 생성한 Rate 모듈 의존 다이어그램. uses는 다른 모듈의 port를 주입받아 호출하는 관계, depends on은 타입(VO/이벤트)을 참조하는 관계를 의미한다.

Documenter가 자동 생성한 Rate 모듈 의존 다이어그램. uses는 다른 모듈의 port를 주입받아 호출하는 관계, depends on은 타입(VO/이벤트)을 참조하는 관계를 의미한다.

문제 4 — 모듈 단위 모니터링이 안 되는 상황

Spring Modulith는 actuatorobservability 모듈만 추가하면, OpenTelemetry/Micrometer 기반 모니터링 스택과 자연스럽게 연동됩니다.

// build.gradle.kts
runtimeOnly("org.springframework.modulith:spring-modulith-actuator")
runtimeOnly("org.springframework.modulith:spring-modulith-observability")

또한 actuator를 통해 /actuator/modulith 엔드포인트가 노출됩니다. 운영 중인 인스턴스의 모듈 의존 그래프를 JSON으로 그대로 조회할 수 있어, "지금 떠있는 인스턴스에서 모듈 구조가 어떻게 보이는가"를 실시간으로 확인할 수 있습니다.

여기까지가 Spring Modulith가 제공하는 도구의 큰 그림입니다. 그렇다면 실제 도메인 위에 적용해보면 어떻게 보일까요? 저희 팀이 1년 가까이 운영해본 사례를 공유합니다.

실제 적용 사례: Stay Product Service

여기어때 상품개발팀에서는 숙박 상품을 관리하는 Stay Product Service에 Spring Modulith를 적용하고 있습니다. 이 서비스는 숙소(Property), 객실(Room), 요금제(Rate Plan), 요금(Rate), 재고(Inventory), 검색(Search) 등의 도메인을 다루고 있습니다.

모듈 계층 설계 — Why가 먼저

모듈 간 의존 관계를 계층(Layer)으로 설계하여, 상위 계층이 하위 계층만 참조할 수 있도록 했습니다. 같은 계층 간 참조나 하위에서 상위로의 역참조는 허용되지 않습니다.

Layer 0  →  property, shared
Layer 1  →  room, cancellationpolicy, childpolicy
Layer 2  →  rateplan, inventory
Layer 3  →  roomrate
Layer 4  →  rate
Layer 5  →  search, booking (읽기 전용 조합)

계층 번호는 참조 방향변경 빈도를 함께 고려해 정했습니다. property/shared는 거의 변하지 않고 모든 모듈의 기반이 되므로 Layer 0에 두고, search/booking은 다른 모든 도메인의 변화를 받아서 동기화하므로 가장 위 Layer 5에 두었습니다. 이렇게 하면 “변경이 잦거나 다른 모듈의 변화를 받는 모듈일수록 위쪽 Layer로 올라간다”는 직관적인 규칙이 만들어집니다.

이 계층 구조는 각 모듈의 @ApplicationModule(allowedDependencies = [...]) 선언으로 강제됩니다. 만약 search 모듈에서 직접 다른 모듈을 변경하려고 하면, 역방향 의존이 되어 verify()에서 실패합니다. 이런 경우엔 이벤트로 신호를 받아 처리하는 방향으로 설계를 다시 잡습니다.

비즈니스 영역에 따라 독립적인 모듈을 구성하고, 이 모듈들이 서로 협업하도록 만드는 구조 — 이것이 DDD에서 말하는 바운디드 컨텍스트(Bounded Context)를 코드 레벨에서 구현한 것입니다.

헥사고날 아키텍처와의 조합

각 모듈 내부는 헥사고날(포트-어댑터) 아키텍처로 구성됩니다.

rate/
├── domain/              ← 도메인 모델, 비즈니스 규칙
│   ├── Rate.kt
│   ├── RateRule.kt
│   └── event/
│       ├── RateChangedEvent.kt
│       └── RateDeletedEvent.kt
│
├── application/         ← 유스케이스, 포트
│   ├── usecase/         ← 입력 포트 (인터페이스)
│   ├── port/output/     ← 출력 포트 (인터페이스)
│   └── service/         ← 유스케이스 구현체
│
├── adapter/             ← 외부 연결
│   ├── input/rest/      ← REST 컨트롤러
│   ├── input/messaging/ ← 이벤트 리스너
│   └── output/          ← DB 어댑터
│
└── RateModule.kt        ← @ApplicationModule 선언

Spring Modulith가 모듈 간 경계를 관리하고, 헥사고날 아키텍처가 모듈 내부 의존 방향을 관리합니다. 즉 모듈 외부 경계는 Spring Modulith가, 내부의 도메인 → 인프라 의존 방향은 헥사고날이 차단합니다. 두 규칙이 직교하기 때문에, “외부로 노출되는 인터페이스”와 “내부 구현 세부사항”이 자연스럽게 분리됩니다.

덕분에 새 클래스를 짤 때 “이걸 어디에 둘지” 고민하는 시간이 거의 사라졌습니다. 외부에서 호출되면 application/port, 모듈 간 신호만 주고받으면 domain/event, 외부 시스템과 연동하면 adapter/output — 이런 식으로 자리가 정해집니다.

Bean 충돌 방지 — 실제로 부딪힌 문제

Spring Modulith를 적용하면서 도입 초기에 실제로 만난 문제는 Bean 이름 충돌이었습니다. Modular Monolith에서는 여러 모듈이 하나의 Spring Context를 공유하기 때문에, 서로 다른 모듈에서 같은 클래스명을 쓸 경우 Bean 등록 단계에서 부팅이 실패합니다. 예를 들어 ARI 모듈과 Overseas 모듈 모두 RoomRateCommandService라는 이름의 Bean을 가질 수 있죠.

저희 팀은 이를 커스텀 BeanNameGenerator로 해결했습니다.

@Modulithic
@ComponentScan(nameGenerator = AriBeanNameGenerator::class)
@Configuration
class AriAutoConfig

class AriBeanNameGenerator : BeanNameGenerator {
    override fun generateBeanName(
        definition: BeanDefinition,
        registry: BeanDefinitionRegistry
    ): String {
        val defaultName = defaultGenerator.generateBeanName(definition, registry)
        return if (definition.beanClassName!!.contains(".ari.")) {
            "ari${defaultName.replaceFirstChar { it.uppercaseChar() }}"
        } else defaultName
    }
}

위 코드는 핵심 흐름만 추린 단순화 버전이며, 실제 구현에는 명시적으로 지정한 Bean 이름을 우회하는 분기가 포함되어 있습니다.

이렇게 하면 ARI 모듈의 Bean은 ariRoomRateCommandService, Overseas 모듈은 overseasRoomRateCommandService로 등록되어 충돌을 피할 수 있습니다. 여러 도메인을 하나의 Context에 담는 Modular Monolith 특성상, 이런 이름 충돌은 모듈 수가 늘수록 자연스럽게 발생하므로 초기에 규칙을 정해두는 게 좋습니다.

도입 시 고려할 점

Spring Modulith를 도입하면서 느낀 점들을 정리합니다.

좋았던 점

  • 아키텍처가 코드로 강제된다: allowedDependenciesverify() 테스트 덕분에 "이 모듈은 저 모듈을 참조하면 안 된다"는 규칙이 문서가 아니라 CI에서 검증됩니다. 더 이상 코드 리뷰에서 아키텍처 위반을 눈으로 찾을 필요가 없습니다.
  • 점진적 전환이 가능하다: 기존 모놀리스에 모듈 경계만 추가하는 것부터 시작할 수 있습니다. 한 번에 MSA로 전환하는 빅뱅 방식이 아니라, 단계적으로 구조를 개선할 수 있습니다.
  • 자동 문서화가 실용적이다: 모듈 구조가 변경되면 다이어그램도 자동으로 업데이트되므로, 아키텍처 문서가 코드와 괴리되는 문제를 방지할 수 있습니다.

알고 시작하면 좋을 점

  • 모듈 경계 설계가 가장 어렵다: Spring Modulith는 도구일 뿐, 모듈을 어떻게 나눌지는 도메인 이해의 영역입니다. 잘게 쪼갤수록 좋은 게 아니라, 변경의 단위와 일치하는 경계를 찾는 게 핵심입니다.
  • 공유 모듈은 작게 유지한다: shared는 모든 곳이 의존하기 때문에 비대해지면 글로벌 변수와 다를 바 없어집니다. 공통 enum/VO에 한정하고, 도메인 로직은 두지 않습니다.
  • Bean 네이밍 정책은 1일차에 정한다: 같은 클래스명을 가진 Bean 충돌은 모듈이 늘어나면 거의 확정적으로 발생합니다. 모듈이 쌓인 뒤에 도입하면 일괄 변경 비용이 큽니다.

시작하려면 — 의존성 한 블록

Spring Modulith를 도입하려면 BOM 한 줄과 starter 몇 개만 있으면 충분합니다. 저희 팀에서 사용하는 구성을 그대로 공유합니다.

// build.gradle.kts
dependencyManagement {
    imports {
        mavenBom("org.springframework.modulith:spring-modulith-bom:1.3.4")
    }
}

dependencies {
    // 이벤트 영속화가 필요하면 starter-jdbc, 그렇지 않으면 starter-core
    implementation("org.springframework.modulith:spring-modulith-starter-jdbc")
    // 운영 가시성
    runtimeOnly("org.springframework.modulith:spring-modulith-actuator")
    runtimeOnly("org.springframework.modulith:spring-modulith-observability")
    // verify() / Documenter / Scenario API
    testImplementation("org.springframework.modulith:spring-modulith-starter-test")
}

event_publication 테이블은 spring-modulith-events-jdbc(starter-jdbc에 포함)가 자동으로 생성·관리합니다. 외부 브로커로 이벤트를 함께 발행하고 싶다면 spring-modulith-events-kafka 등을 추가하면 됩니다.

검색 모듈처럼 미래에 분리할 가능성이 있는 모듈에 대해서는, 이벤트에 @Externalized 어노테이션을 붙여 두면 In-Process 발행과 Kafka 발행을 동시에 보낼 수 있습니다.

@Externalized("room-rate-changed::#{roomRateId}")
data class RoomRateChangedEvent(val roomRateId: Long)

@Externalized 한 줄과 spring-modulith-events-kafka 의존성만 추가하면, 같은 이벤트가 In-Process와 Kafka 양쪽에 동시에 흐르게 됩니다. 검색 모듈을 별도 서비스로 분리할 시점이 와도, 이벤트 발행 로직은 그대로 두고 구독 측만 새 서비스로 옮기면 됩니다. 모듈 경계가 코드 레벨에서 명확히 정의되어 있기 때문에, "어디를 잘라야 하는가"에 대한 답이 코드에 이미 존재하는 셈이죠.

마치며

더 좋거나 나쁜 아키텍처는 없습니다. 서비스의 규모, 팀의 크기, 운영 역량, 비즈니스 목표에 따라 적절한 아키텍처는 달라집니다.

글 처음에 던졌던 질문을 다시 떠올려 봅니다. “Endpoint가 많지 않은 서비스를 굳이 여러 개로 쪼갤 가치가 있을까?” 1년 가까이 Modular Monolith를 운영해본 저희 팀의 답은 “쪼개는 대신 경계만 명확히 그었다” 입니다.

모듈 간 위반은 더 이상 코드 리뷰에서 눈으로 잡지 않고 CI에서 자동으로 차단됩니다. 검색 모듈을 떼어낼 시점이 오더라도, 이벤트 기반 구조 위에서 분리할 준비가 되어 있다는 점이 가장 든든한 부분입니다.

Modular Monolith의 가장 좋은 점은 — 이런 결정을 나중에 내릴 수 있다는 것 자체인 것 같습니다.

“지금은 모놀리스가 맞지만, 나중에는 분리가 필요할 수도 있다”고 생각하신다면, Modular Monolith를 한번 고려해보시는 건 어떨까요. 이게 저희가 찾은 제3의 답입니다.

참고 자료


메타데이터
post_id
5f6d1de6399a
slug
msa도-모놀리스도-아닌-제3의-선택-spring-modulith-5f6d1de6399a
url
https://techblog.gccompany.co.kr/msa%EB%8F%84-%EB%AA%A8%EB%86%80%EB%A6%AC%EC%8A%A4%EB%8F%84-%EC%95%84%EB%8B%8C-%EC%A0%9C3%EC%9D%98-%EC%84%A0%ED%83%9D-spring-modulith-5f6d1de6399a
canonical_url
https://techblog.gccompany.co.kr/msa%EB%8F%84-%EB%AA%A8%EB%86%80%EB%A6%AC%EC%8A%A4%EB%8F%84-%EC%95%84%EB%8B%8C-%EC%A0%9C3%EC%9D%98-%EC%84%A0%ED%83%9D-spring-modulith-5f6d1de6399a
author_url
https://medium.com/@rosie_lee
status
ok
fetched_at
2026-06-10 15:53:41