한 줄 정의
자주 쓰는 서버 설계 패턴 6가지(MVC·계층형·DDD 전술 패턴·마이크로서비스·이벤트 기반·CQRS)는 모두 관심사를 분리해서 변경의 영향 범위를 줄이는 방법입니다.
쉽게 말하면
성장하는 식당의 조직도라고 생각해 봅시다. 처음엔 한 사람이 주문받고 요리하고 서빙까지 다 하지만, 규모가 커지면 역할을 나눠야 합니다.
MVC는 홀 매니저(컨트롤러)가 주문을 받아 주방(모델)에 전달하고, 플레이팅 담당(뷰)이 손님에게 낼 모양을 만드는 기본 분업입니다. 계층형 아키텍처는 홀 → 주방 → 재료 창고 방향으로만 요청이 흐르게 하는 동선 규칙입니다.
DDD 전술 패턴은 레시피(도메인 로직)를 각자 머릿속이나 창고 메모에 흩어 두지 않고 주방 한 곳에 모으는 것이고, 마이크로서비스는 장사가 커졌을 때 한 주방을 쪼개 메뉴별 분점(서비스별 독립 주방 + 창고)을 내는 것입니다.
이벤트 기반 아키텍처는 분점끼리 직접 전화하는 대신 게시판(브로커)에 “결제 완료됨” 쪽지를 남기면 관심 있는 분점이 알아서 가져가는 방식이고, CQRS는 주문서(명령)와 메뉴판(조회)을 같은 종이 한 장으로 겸용하지 않는 것입니다.
공통점은 하나입니다. 역할을 나누면 한쪽이 바뀌어도 다른 쪽이 흔들리지 않습니다.
왜 중요한가?
패턴 없이 개발하면 로직이 아무 데나 자리 잡습니다. 상태 전이 규칙이 SQL 쿼리 조건절에 숨어 있으면, 어떤 조건에서 상태가 바뀌는지 알기 위해 쿼리를 뒤져야 합니다.
반대로 패턴을 비용 계산 없이 도입해도 문제입니다. 유행이라서 도입한 마이크로서비스는 분산 모놀리식이 되고, 단순한 모델에 적용한 CQRS는 작업량만 늘립니다.
각 패턴이 무엇을 분리하고 그 대가로 무엇을 치르는지 알아야, 지금 내 시스템에 맞는 구조를 고를 수 있습니다.
핵심 내용
MVC 패턴
스프링과 Express.js가 사용하는 전형적인 패턴으로, 3개 요소로 구성됩니다.
| 구성 요소 | 역할 |
|---|---|
| 모델 | 회원 가입, 암호 변경 같은 비즈니스 로직 처리 |
| 뷰 | 사용자가 볼 결과 생성 (HTML, JSON 응답) |
| 컨트롤러 | 사용자 입력 처리와 흐름 제어 |
flowchart LR U[사용자] -- "1\. 요청" --> C[컨트롤러] C -- "2\. 로직 실행" --> M[모델] C -- "3\. 뷰 선택" --> V[뷰] V -- "4\. 응답" --> U
핵심은 두 가지입니다. 비즈니스 로직(모델)과 결과 생성(뷰)을 분리하고, 흐름 제어와 요청 처리는 컨트롤러에 집중시킵니다.
의존 방향이 중요합니다. 컨트롤러만 모델과 뷰에 의존하고, 모델과 뷰는 서로를 모릅니다. 그래서 모델 내부 구현이 바뀌어도 뷰는 영향을 거의 받지 않고, 뷰 기술을 JSP에서 타임리프로 바꿔도 모델은 그대로입니다.
컨트롤러가 실제로 하는 일은 입력 값 형식 검증, 쿼리 문자열의 데이터 모델 변환, 그리고 비즈니스 로직에 속하지 않는 사용자 인증 처리입니다.
계층형 아키텍처
각 계층이 특정 역할을 맡고, 상위 → 하위 방향으로만 의존하는 구조입니다. 하위 계층은 상위 계층을 모릅니다. 바로 아래 계층에만 의존을 허용하는 엄격한 변형과, 하위 계층 전체에 의존을 허용하는 느슨한 변형이 있습니다.
웹 애플리케이션은 일반적으로 4계층으로 구성합니다.
| 계층 | 역할 | 대표 구성 요소 |
|---|---|---|
| 표현(UI) | 사용자 상호 작용, 요청을 응용 계층에 위임 | MVC의 컨트롤러·뷰 |
| 응용 | 요청을 실제 처리, 도메인·인프라를 조합해 기능 구현 | 서비스 |
| 도메인(모델) | 도메인 로직 (주문 취소 제약 조건, 상태 변경 규칙) | 도메인 모델 |
| 인프라(영속) | DB 연동, 문자 발송 같은 구현 기술 | DAO |
구조가 단순하고 규칙이 명확해서 코드 실행 흐름을 추적하기 쉽다는 것이 장점입니다.
흩어지는 도메인 로직
실무에서는 도메인 계층 없이 서비스/DAO만으로 구현한 3계층 구조가 흔합니다. 이때 도메인 로직이 응용 계층과 인프라 계층으로 분산되는 경향이 생깁니다.
전형적인 예가
update member set status = 20 where member_id = ? and status = 10같은 쿼리입니다. “status가 10일 때만 20으로 바꾼다”는 상태 전이 규칙이 쿼리 조건절에 숨어 있어서, 코드만 봐서는 도메인 로직을 알 수 없고 쿼리를 뒤져야 합니다. 이를 막으려면 도메인 로직을 최대한 한 계층으로 모아야 합니다.
DDD와 전술 패턴
로직이 복잡한 도메인에서는 DDD(Domain-Driven Design) 의 전술 패턴이 도메인 로직을 도메인 영역 한 곳에 집중시키는 데 도움이 됩니다.
| 구성 요소 | 핵심 |
|---|---|
| 엔티티(Entity) | 고유 식별자로 구분. 내부 상태가 바뀌어도 식별자는 불변 (예: 주문번호) |
| 밸류(Value) | 식별자 없는 개념적 값 (금액, 배송 주소). 불변 구현 권장 |
| 애그리거트(Aggregate) | 관련 객체를 묶은 개념적 단위 (Order 엔티티 + OrderLine 밸류 집합 + ShippingAddress 밸류). 모델 일관성 관리의 단위 |
| 리포지토리(Repository) | 도메인 객체와 물리 저장소를 연결하는 저장·조회 인터페이스. 애그리거트 단위로 존재 |
| 도메인 서비스(Domain Service) | 특정 애그리거트에 속하지 않는 로직, 외부 연동이 필요한 도메인 로직 |
| 도메인 이벤트(Domain Event) | 도메인 상태 변경 시 발생, 다른 부분에 변화를 알리는 용도 |
응용 서비스는 리포지토리로 애그리거트를 조회한 뒤 로직 실행을 위임할 뿐, 주문 취소 로직 자체는 Order 애그리거트의 cancel() 메서드에 위치합니다.
@Transactional
public void cancel(OrderNumber orderNum) {
Order order = orderRepository.findById(orderNum)
.orElseThrow(() -> new NoOrderException());
order.cancel(); // 취소 로직은 애그리거트 안에
}복잡한 모델을 애그리거트 단위로 관리하면 복잡도가 낮아지고, 관련 로직이 모여 응집도가 높아집니다.
전술 패턴 외에 바운디드 컨텍스트(bounded context)는 도메인 간의 경계를 설정해서 상위 수준에서 복잡한 도메인을 관리하게 해 주며, 마이크로서비스 아키텍처의 분리 단위와도 잘 어울립니다.
마이크로서비스 아키텍처
하나의 애플리케이션에 모든 것을 구현하는 모놀리식과 달리, 더 작은 단위로 서비스를 분리하고 각 서비스가 연동되는 구조입니다. 각 마이크로서비스는 자신의 DB를 따로 갖습니다.
유행이라서 도입하는 경우가 적지 않은데, 모놀리식과 장단점을 비교해서 효과가 분명할 때 도입해야 합니다. 판단 기준이 되는 6가지 핵심 개념은 다음과 같습니다.
- 독립적 배포: 다른 서비스를 배포하지 않고도 변경·배포·출시할 수 있어야 합니다. 6가지 중 가장 중요하며, 이를 위해 서비스 간 결합도를 최대한 낮춰야 합니다
- 도메인 중심 모델링: 도메인 기준으로 서비스를 구분합니다. 한 도메인의 기능이 여러 서비스에 걸치면 출시 비용이 증가합니다
- 자신의 상태를 가짐: DB를 공유하지 않습니다. 다른 서비스의 데이터는 DB 직접 접근이 아니라 API로 접근합니다
- 크기: 절대적 기준은 없습니다. 조직이 감당할 수 있는 수준과 경계 정의에 집중합니다
- 유연함: 비용을 들여 기술·확장·견고함의 유연함을 얻는 구조입니다. 비용을 감당할 수 있는 수준에서 도입합니다
- 아키텍처와 조직 맞춤: 조직 구조는 아키텍처에 영향을 줍니다(콘웨이 법칙). 비즈니스 도메인이 아키텍처를 주도하도록 설계합니다
모놀리식이 나쁜 건가?
모놀리식 아키텍처 자체가 잘못된 경우는 드뭅니다. 대부분은 모놀리식 안의 설계와 코드 품질이 문제입니다. 품질이 낮은 모놀리식을 그대로 마이크로서비스로 쪼개면 더 복잡한 구조만 남습니다. 구조와 품질에 신경 쓴다면 모듈라 모놀리식(Modular Monolithic)으로도 모듈 간 의존을 줄이면서 짧은 배포 주기를 유지할 수 있습니다.
이벤트 기반 아키텍처
두 시스템 간 통신에 이벤트를 사용하는 구조입니다. 여기서 이벤트는 과거에 발생한 사실이므로 ‘주문함’, ‘인증에 실패함’처럼 과거형으로 표현합니다.
flowchart LR P[이벤트 생산자] -- 이벤트 --> B["이벤트 브로커<br/>(라우터)"] B -- 이벤트 --> C1[이벤트 소비자] B -- 이벤트 --> C2[이벤트 소비자]
- 데이터 변경 전파: 주문 시스템의 ‘결제 완료’ 이벤트를 배송 시스템이 받아 배송을 시작합니다
- 알림: 인증 시스템의 ‘인증 실패’ 이벤트를 이상 감지 시스템이 받아 연속 실패 횟수로 비정상 접근을 판단합니다
대표적인 이벤트 브로커가 카프카입니다. 생산자와 소비자가 직접 연결되지 않고 브로커를 통해 간접 연결되므로 서로 간섭 없이 독립적으로 배포할 수 있고 새 소비자 추가도 쉽습니다. 반면 이벤트가 중간 브로커를 거치기 때문에 처리 상태를 추적하려면 별도 수단이 필요합니다.
이벤트 브로커와 단일 진실 공급원
카프카는 메시지를 삭제하지 않고 저장할 수 있어서 브로커를 단일 진실 공급원(Single Source of Truth)으로 확장할 수 있습니다. 시스템의 모든 상태 변화가 이벤트로 발생한다면 이벤트 기록만으로 상태 변화를 추적할 수 있고, 나아가 브로커를 이벤트 소싱 패턴의 이벤트 저장소로도 활용할 수 있습니다.
CQRS 패턴
CQRS(Command Query Responsibility Segregation)는 상태를 변경하는 명령 모델과 상태를 읽는 조회 모델을 분리하는 패턴입니다.
조금만 복잡해져도 명령과 조회가 쓰는 데이터는 달라집니다. 주문 생성에는 주문 ID·회원 ID·배송지 주소·상품 ID를 쓰지만, 주문 조회에는 여기에 회원 이름·상품명이 추가됩니다. 이를 하나의 모델로 구현하면 상태 변경 코드에 조회 전용 속성이 섞여 분석이 어려워지고, 기능이 추가될수록 모델이 비대해집니다.
- 이점: 기능별로 맞는 모델을 구현해 명령·조회 간 상호 영향을 최소화하고, 조회 모델에 캐시를 적용하거나 조회 전용 DB를 확장하는 식으로 조회 성능을 올리기 쉽습니다
- 비용: 모델을 따로 만들어야 하므로 코드가 늘고, 명령·조회를 다른 기술로 구현하면 구현 기술도 늘어납니다. 특히 조회 전용 DB를 분리하면 DB 간 데이터 동기화를 위한 메시징 수단이 추가로 필요합니다
단순한 모델에 적용하면 작업량만 늘고 이점은 적습니다. 모델이 복잡할 때 이점이 커집니다.
비교 / 트레이드오프
모놀리식 vs 마이크로서비스
| 모놀리식 | 마이크로서비스 | |
|---|---|---|
| 배포 | 단순하지만 작은 변경도 전체 재배포 | 독립적·지속적 배포 용이 |
| 확장 | 복잡한 구조 없이도 성능 확보 | 서비스 단위 성능 확장 용이 |
| 장애 영향 | 한 기능의 문제가 전체에 영향 | 서비스 단위로 격리 |
| 기술 선택 | 구현 기술 변경 어려움 | 기술 유연성 확보 |
| 개발 경험 | 코드 관리·테스트·디버깅 쉬움, 규모 커지면 개발 속도 저하 | 테스트·디버깅 어려움, (보통) 개발자 만족도 높음 |
| 인프라·소통 | 단순 | 인프라 복잡, 소통 부하 증가, 무분별하면 분산 모놀리식 |
패턴별로 무엇을 분리하고 무엇을 치르는가
| 패턴 | 분리하는 것 | 치르는 비용 |
|---|---|---|
| MVC | 로직 / 결과 생성 / 흐름 제어 | 거의 없음 (기본기) |
| 계층형 | 계층별 역할, 단방향 의존 | 도메인 계층을 안 두면 로직 분산 |
| DDD 전술 패턴 | 도메인 로직을 애그리거트로 집중 | 모델링 난이도 |
| 마이크로서비스 | 배포 단위 | 인프라·소통 복잡도 |
| 이벤트 기반 | 시스템 간 직접 결합 | 처리 상태 추적 수단 |
| CQRS | 명령 모델 / 조회 모델 | 코드량·구현 기술·동기화 |
내 생각
- 실무에서 가장 흔한 문제는 도메인 계층의 부재입니다. 컨트롤러-서비스-리포지토리 3계층 Spring 프로젝트에서 서비스가 수천 줄이 되는 이유가 정확히 “흩어지는 도메인 로직”입니다.
order.cancel()처럼 규칙을 객체 안으로 넣는 것만으로도 쿼리·서비스에 숨은 로직 상당수가 드러납니다. - 마이크로서비스 도입 판단은 결국 “독립적 배포가 필요한가” 하나로 수렴합니다. 서비스를 쪼개 놓고도 항상 같이 배포해야 한다면 분산 모놀리식이라는 최악의 조합만 남습니다. 팀 단위로 배포 주기를 분리할 필요가 생기기 전까지는 모듈라 모놀리식이 현실적인 답입니다.
- CQRS는 풀 도입 전에 조회 전용 DTO·쿼리 분리부터 시작해도 됩니다. 명령은 JPA 엔티티, 조회는 별도 DTO 프로젝션으로만 나눠도 “조회 속성이 명령 모델을 오염시키는” 문제 대부분이 해결됩니다. 별도 조회 DB와 동기화 메시징은 트래픽이 증명된 뒤의 선택지입니다.
- 이벤트 이름이 과거형인지가 설계 상태를 드러내는 리트머스입니다. ‘배송하라’(명령)를 이벤트로 발행하고 있다면 생산자가 소비자의 행동을 알고 있다는 뜻이고, 그 순간 브로커를 써도 결합은 그대로 남습니다.
관련 개념
- Ch05 비동기 연동, 언제 어떻게 써야 할까 — 이벤트·메시징의 설계와 처리 시 고려 사항
- Ch10 계층형 아키텍처 — 계층형 아키텍처 심화 (소프트웨어 아키텍처 The Basics)
- Ch11 모듈형 모놀리스 아키텍처 — 모듈라 모놀리식 심화 (소프트웨어 아키텍처 The Basics)
- Ch15 이벤트 주도 아키텍처 — 이벤트 기반 아키텍처 심화 (소프트웨어 아키텍처 The Basics)
- Ch18 마이크로서비스 아키텍처 — 마이크로서비스 심화 (소프트웨어 아키텍처 The Basics)