한 줄 정의
시스템을 논리적 컴포넌트 단위로 분해하고, 각 컴포넌트의 역할·책임·결합도를 반복적으로 개선하며 아키텍처를 설계하는 사고방식입니다.
쉽게 말하면
집을 설계할 때 주방, 침실, 욕실 같은 방 단위로 나누어 생각하듯이, 소프트웨어 시스템도 주문 접수, 결제 처리, 재고 관리 같은 기능 단위로 나누어 설계합니다.
아키텍트가 시스템을 “보는” 수준이 바로 이 컴포넌트 수준이며, 클래스 수준이 아닙니다.
왜 중요한가?
모듈(관련된 코드의 묶음)을 아키텍처 관점에서 다루면, 시스템의 기능이 어떻게 분할되고 어떻게 상호작용하는지를 구조적으로 파악할 수 있습니다.
컴포넌트 기반 사고 없이 바로 코드 수준으로 들어가면, 시스템 전체의 응집도와 결합도를 제어하기 어렵고, 유지보수·테스트·배포가 모두 어려운 비구조적인 아키텍처가 만들어집니다.
핵심 내용
논리적 컴포넌트의 정의
논리적 컴포넌트(logical component) 는 시스템에서 특정 비즈니스 기능을 수행하는 코드의 묶음입니다.
코드를 포함한 디렉터리 구조나 이름공간의 말단 노드(leaf node) 가 곧 최소의 논리적 컴포넌트이고, 그보다 상위의 디렉터리나 이름공간 노드는 시스템의 도메인과 서브도메인을 나타냅니다.
예를 들어 order_entry/ordering/payment는 Payment Processing(결제 처리) 컴포넌트를, order_entry/processing/fulfillment는 Order Fulfillment(주문 이행) 컴포넌트를 나타냅니다.
아키텍트는 이렇게 소프트웨어 시스템의 디렉터리 구조나 이름공간을 분석해서 시스템의 내부 구조, 즉 논리적 아키텍처 를 파악합니다.
논리적 아키텍처 대 물리적 아키텍처
| 구분 | 논리적 아키텍처 | 물리적 아키텍처 |
|---|---|---|
| 정의 | 시스템이 어떤 논리적 컴포넌트로 구성되며 어떻게 상호작용하는지 | 서비스, 사용자 인터페이스, 데이터베이스 같은 물리적 요소의 배치 |
| 초점 | 컴포넌트들과 그 상호작용 방식 | 배포 단위, 인프라 구성 |
| 도식화 시 포함 요소 | 논리적 컴포넌트, 데이터 저장소, 행위자 | 서비스, DB, 네트워크 |
| 독립성 | 물리적 아키텍처와 독립적으로 작성 가능 | 논리적 아키텍처와 독립적 |
논리적 아키텍처 예시

물리적 아키텍처 예시
핵심은 이 둘이 독립적 이라는 점입니다.
논리적 아키텍처를 작성할 때는 시스템의 물리적 구조보다는 시스템이 무엇을 하는지, 그 기능이 어떻게 분할되는지, 기능적 부분들이 어떻게 상호작용하는지에 더 초점을 맞춥니다.
논리적 아키텍처의 모든 컴포넌트를 하나의 모놀리스 아키텍처(단일 배포 단위)에 몰아넣을지 아니면 개별 서비스로 만들어서 배포할지를 결정하지 않고도, 심지어 이 시스템에 어떤 종류의 아키텍처 스타일이 가장 적합할지 결정하지 않은 상태에서도 논리적 아키텍처를 작성할 수 있습니다.
반면 물리적 아키텍처만으로 개발을 진행하면 유지보수, 테스트, 배포가 어려운 비구조적인 아키텍처가 만들어지기 쉽습니다. 물리적 아키텍처만으로는 시스템의 기능이 어디에 있으며 그것이 전체와 어떻게 결합되는지 파악하기 어렵기 때문입니다.
논리적 아키텍처의 작성
논리적 아키텍처를 작성하는 과정은 논리적 컴포넌트들을 식별하고 재구성하는 작업의 반복으로 이루어집니다.
컴포넌트 식별(component identification) 은 반복적인 프로세스로 진행할 때 가장 효과적입니다. 피드백 루프를 통해 개선해 나갑니다.
graph LR A[초기 핵심 컴포넌트를<br/>식별한다] --> B[사용자 스토리/요구사항을<br/>컴포넌트에 배정한다] B --> C[역할과 책임을<br/>분석한다] C --> D[아키텍처 특성들을<br/>분석한다] D --> E[필요에 따라 컴포넌트를<br/>리팩터링하거나 추가한다] E --> B
핵심 컴포넌트의 식별
초기 핵심 컴포넌트를 처음부터 완벽하게 만들려고 너무 많은 노력을 기울이는 것은 수입니다.
시스템의 핵심 기능을 바탕으로 초기 핵심 컴포넌트가 어떤 모습일지 최선으로 추측하고, 이후 반복적 작업흐름을 통해서 점차 개선하는 것이 더 낫습니다.
초기 핵심 컴포넌트는 ‘빈 양동이(empty bucket)‘에 비유할 수 있습니다. 아키텍트가 “채우기” 시작해야, 즉 컴포넌트에 사용자 스토리나 요구사항을 배정하기 시작해야 비로소 의미를 가집니다. 초기 핵심 컴포넌트는 본질적으로 하나의 자리표(placeholder), 즉 기능적 구성요소에 대한 최선의 추측일 뿐입니다.
초기 핵심 컴포넌트를 생성하는 세 가지 일반적인 접근법이 있습니다.
작업흐름 접근법
사용자가 시스템을 정상적이고 효과적으로 사용하는 경로에 해당하는 작업흐름(또는 시스템의 주된 요청 처리 작업흐름)을 활용합니다.
예를 들어 주문 입력 시스템의 작업흐름 각 단계에서 컴포넌트를 도출할 수 있습니다:
- 사용자가 품목 카탈로그를 둘러본다 → Item Browser(품목 탐색기)
- 사용자가 주문한다 → Order Placement(주문 제출)
- 사용자가 주문 대금을 결제한다 → Order Payment(주문 결제)
- 사용자에게 주문 상세 정보를 이메일로 보낸다 → Customer Notification(고객 알림)
- 주문을 준비한다 → Order Fulfillment(주문 이행)
- 주문을 배송한다 → Order Shipment
- 주문이 배송되었음을 고객에게 이메일로 알린다 → Customer Notification
- 배송을 추적한다 → Order Tracking
작업흐름의 모든 단계에 대해 새 컴포넌트를 만들어야 하는 것은 아닙니다. 위의 단계 4와 단계 7은 둘 다 Customer Notification 컴포넌트를 사용합니다. 아키텍트는 시스템의 주요 작업흐름이나 사용자 여정(user journey)을 가능한 한 많이 모델링해서 해당 단계에서 상응하는 컴포넌트를 식별합니다.
행위자/행동 접근법
행위자/행동(Actor/Action) 접근법 은 시스템의 행위자가 여럿일 때 특히 유용합니다.
아키텍트는 사용자가 시스템에서 수행할 수 있는 주요 행동(예: 주문하기)을 식별합니다. 시스템 자체도 행위자임을 기억하여, 시스템은 결제 및 재고 보충과 같은 자동화된 기능(행동)을 수행합니다.
예를 들어 주문 입력 시스템에서 고객, 주문 포장 담당자, 시스템이라는 세 행위자를 식별하고 각 행위자의 주요 행동을 정리하면:
- 고객 행위자 — 품목 검색(Item Search), 품목 상세 정보(Item Details), 주문(Order Placement), 주문 취소(Order Cancel), 신규 고객 등록(Customer Registration), 고객 정보 갱신(Customer Profile)
- 주문 포장 행위자 — 상자 크기 선택(Order Fulfillment), 배송 준비 완료 표시(Order Fulfillment), 고객에게 주문 배송(Order Shipment)
- 시스템 행위자 — 재고 조정(Inventory Management), 공급업체에 추가 재고 주문(Supplier Ordering), 결제 적용(Order Payment)
일반적으로 행위자/행동 접근법은 작업흐름 접근법보다 더 많은 컴포넌트를 생성합니다.
엔티티 함정
아키텍트는 시스템과 관련된 엔티티들을 집중적으로 분석해서 엔티티들로부터 컴포넌트를 도출하려는 유혹에 빠지기가 매우 쉽습니다. 이것을 엔티티 함정(Entity Trap) 이라고 부르며, 피해야 할 안티패턴입니다.
이 안티패턴의 문제점은 세 가지입니다:
- 이런 식으로 도출한 논리적 컴포넌트의 이름은 컴포넌트의 역할을 잘 설명하지 못합니다. 예를 들어 “Order Manager(주문 관리자)“라는 이름에서는 이것이 주문을 관리하는 컴포넌트라는 점만 알 수 있을 뿐, 시스템에서 구체적으로 어떤 역할을 하고 무엇을 책임지는지는 알 수 없습니다. 컴포넌트 이름이 Manager, Supervisor, Engine, Processor 같은 단어가 있다면 엔티티 함정에 걸렸을 가능성이 큽니다.
- 이런 식으로 도출한 컴포넌트는 도메인 관련 기능들의 ‘쓰레기 하치장(dumping ground)‘이 됩니다. 예를 들어 Order Manager 같은 엔티티 기반 컴포넌트 이름을 생각해 보면, 주문 검증, 주문 접수, 주문 내역, 주문 처리, 주문 배송, 주문 추적 등 모든 주문 기능이 이 컴포넌트 하나에 들어갈 것입니다. 이를 개발자라면 누구나 한 번은 보았을 ‘kitchen sink’ 유틸리티 클래스와 같습니다.
- 너무 거친(coarse-grained) 컴포넌트가 만들어질 수 있습니다. 세분도가 낮은 컴포넌트는 너무 많은 일을 하게 되어서 애초의 목적에서 벗어납니다. 단일 목적(single-purpose) 컴포넌트(예: Validate Order)와 달리 유지보수, 테스트, 배포가 어렵고, 따라서 신뢰성이 높지 않습니다.
시스템 자체가 실제로 엔티티 기반이고 단순히 해당 엔티티에 대해 CRUD(생성, 읽기, 갱신, 삭제) 작업만 수행하면 되는 경우도 있습니다. 그런 시스템에는 굳이 아키텍트가 필요하지 않습니다. 개발자가 해당 엔티티에 작용하는 소스 코드 대부분을 생성할 수 있도록 하는 CRUD 기반 프레임워크나 도구, 노코드/로우코드 환경이 요긴할 것입니다.
사용자 스토리를 컴포넌트에 배정
논리적 아키텍처를 만드는 다음 단계는 사용자 스토리(user story)나 요구사항을 논리적 컴포넌트에 배정하는 것입니다. 대부분의 경우 사용자 스토리나 요구사항을 미리 완전히 알 수는 없으므로, 이러한 배정 작업은 반복적인 과정이며, 시스템이 진화함에 따라 사용자 스토리나 요구사항도 함께 진화합니다.
이 단계의 목표는 빈 양동이(컴포넌트)에 구체적인 역할과 책임을 부여함으로써 채우기 시작하는 것입니다. 다음 세 가지 사용자 스토리를 통해 이 과정이 어떻게 진행되는지 살펴봅니다.
고객 #1 — “주문 내용을 정확하게 입력했는지 확인받고 싶다”
이 사용자 스토리는 Order Placement 컴포넌트에 배정하는 것이 합리적입니다. 사용자가 주문하기 위해 상호작용하는 컴포넌트이기 때문입니다.
주문을 검증한다(고객 #1 사용자 스토리) → Order Placement
주문 준비 담당자 — “어떤 크기의 상자를 사용해야 하는지 알고 싶다”
상자 크기 결정은 아마 Order Fulfillment 컴포넌트가 처리해야 할 것입니다. 이 컴포넌트가 주문을 준비하고 상자에 포장하는 데 필요한 모든 시스템 로직을 담당하기 때문입니다.
상자 크기를 결정한다(주문 준비 담당자 사용자 스토리) → Order Fulfillment
고객 #2 — “주문 상태가 변경될 때마다 이메일을 받고 싶다”
나열된 네 가지 컴포넌트 중 주문이 접수되었을 때와 배송 준비가 되었을 때, 그리고 배송이 완료되었을 때 고객에게 이메일을 보내야 하는 것은 Order Placement, Order Fulfillment, Order Shipment 세 가지입니다. 그런데 사용자 스토리는 소스 코드를 통해 구현되며, 그 코드는 특정 디렉터리나 이름공간의 특정 노드에 위치해야 한다는 점을 명심해야 합니다. 이메일 전송 코드를 세 컴포넌트 모두에 복제하는 것은 좋은 생각이 아니므로, 아키텍트는 이 사용자 스토리를 처리할 새로운 컴포넌트를 정의해야 합니다.
고객에게 이메일을 보낸다(고객 #2 사용자 스토리) → Customer Notification(새로 추가된 컴포넌트)
기존의 Order Placement, Order Fulfillment, Order Shipment 컴포넌트는 새로 추가된 Customer Notification 컴포넌트와 통신해서 이메일 전송을 요청해야 합니다.
다음은 새 컴포넌트가 추가된 논리적 아키텍처의 모습입니다.

역할과 책임의 분석
논리적 컴포넌트를 다듬는 다음 단계는 각 컴포넌트의 역할과 책임을 분석하는 것입니다. 이를 통해 아키텍트는 앞 단계에서 해당 컴포넌트에 배정한 요구사항이나 사용자 스토리가 실제로 그 컴포넌트에 적합한지, 컴포넌트가 너무 많은 일을 하지는 않는지 등을 확인합니다.
이 단계에서 아키텍트가 중요하게 생각하는 것은 응집(cohesion) 입니다. 즉, 컴포넌트의 작업들이 서로 얼마나 관련되어 있는가를 중점적으로 살펴보게 됩니다.
이 단계가 어떻게 진행되는지 설명하기 위해, 아키텍트가 Order Placement 컴포넌트에 다음과 같은 요구사항들을 배정했다고 가정합니다:
- 모든 필드가 입력되었고 올바른지 확인하기 위해 주문 검증을 수행한다
- 품목 설명, 수량, 가격과 함께 장바구니를 표시한다
- 올바른 배송 주소를 결정한다
- 결제 정보를 수집한다
- 고유한 주문 ID를 생성한다
- 주문에 대해 결제를 적용한다
- 주문된 품목의 재고 수량을 조정한다
- 고객에게 주문 요약 이메일을 보낸다
아키텍트가 이 컴포넌트의 역할과 책임을 다음과 같이 정의했다고 합니다:
“이 컴포넌트는 주문의 유효성을 검사하고 품목 사진, 설명, 수량, 가격이 모두 포함된 유효한 장바구니를 표시하는 역할을 담당합니다. 이 컴포넌트는 또한 주문의 올바른 배송 주소를 결정하고 고객으로부터 모든 결제 정보를 수집하는 책임도 집니다. 이에 더해, 결제를 적용하고 재고를 조정하며 고객에게 주문 요약을 이메일로 보내는 역할도 담당합니다.”
이 문장을 보면 이 컴포넌트가 너무 많은 책임을 지고 있음이 분명합니다. 특히 결제 적용, 재고 조정, 고객 이메일 발송까지 담당하고 있습니다.
컴포넌트가 과도한 역할을 담당하는지 확인하는 한 가지 방법은 역할 기술문에서 “그리고나, 또한, 더불어, 그와 함께” 같은 연결 구문을 찾아보는 것입니다. 또한 쉼표가 너무 많은 것도 역할 및 책임이 과도하다는 힌트가 됩니다.
논리적 컴포넌트는 코드 저장소에서 이름공간 또는 디렉터리로 표현됩니다. Order Placement 컴포넌트의 경우 이 컴포넌트를 나타내는 모든 소스 코드는 com/app/order/placement나 com.app.order.placement 같은 하나의 디렉터리 또는 이름공간에 있게 됩니다. 이 컴포넌트는 상당히 많은 기능을 담당하므로 코드의 분량이 너무 많을 가능성이 큽니다.
따라서 결제 처리, 재고 관리, 이메일 통신을 위한 클래스들을 별도의 디렉터리로 분리하는 것이 타당합니다. 이것이 바로 논리적 컴포넌트 분리의 핵심입니다.
분리 후 컴포넌트들은 각각 좀 더 명확하고 구분되는 역할과 책임을 갖게 됩니다:
- Order Placement — 주문 검증 수행, 장바구니 표시, 배송 주소 결정, 결제 정보 수집, 고유한 주문 ID 생성
- Payment Processing — 결제를 적용한다
- Inventory Management — 주문된 품목의 재고 수량을 조정한다
- Customer Notification — 고객에게 주문 요약 이메일을 보낸다
아키텍처 특성들의 분석
마지막 분석 단계는 시스템에 필요한 아키텍처 특성을 고찰하는 것입니다. 확장성, 내결함성, 탄력성, 그리고 민첩성(변화에 빠르게 대응하는 능력) 같은 일부 아키텍처 특성은 논리적 컴포넌트의 크기에 영향을 줄 수 있습니다.
예를 들어 큰 컴포넌트(책임이 많은 컴포넌트)를 더 작은 컴포넌트들로 분할하면 각 컴포넌트의 유지보수와 테스트가 더 쉬워지고(이것이 민첩성입니다), 확장성과 탄력성, 내결함성도 좋아집니다.
두 부분에 필요한 아키텍처 특성이 서로 다를 가능성이 크다면, 그 두 부분을 하나의 컴포넌트로 설계하면 기능적 관점에서는 괜찮을 수 있지만, 아키텍처 특성의 관점에서 컴포넌트를 분석하면 좀 더 세분화된 컴포넌트들이 만들어질 것입니다.
컴포넌트 재구성
소프트웨어 설계에서 피드백은 매우 중요합니다. 아키텍트는 개발자와 협력하며 컴포넌트 설계 작업을 지속적으로 반복해야 합니다. 컴포넌트 설계에는 반복적인 접근법이 필수인 이유:
- 작업을 진행하다 보면 새로운 사실이 발견되거나 예외 상황이 발생하기 마련인데, 이들을 미리 모두 예측할 수 없으며 이들 중 하나라도 설계를 바꾸는 계기가 될 수 있습니다
- 아키텍트와 개발자는 애플리케이션 구축에 더 깊이 파고들수록, 시스템의 행동방식과 역할을 어디에 두어야 할지를 좀 더 정교하게 이해하게 됩니다
아키텍트는 시스템 또는 제품의 수명 주기 전반에 걸쳐 컴포넌트를 자주 재구성하게 될 잠재적인 변경을 예상하고 대비해야 합니다. 이는 신규 시스템뿐만 아니라 빈번한 유지보수가 이루어지는 모든 시스템에 해당합니다.
컴포넌트 결합
만일 컴포넌트들이 서로 통신하거나 한 컴포넌트의 변경이 다른 컴포넌트들에 영향을 미칠 수 있다면 그 컴포넌트들은 결합(coupled) 된 것입니다. 컴포넌트들의 결합도(coupling) 가 높을수록 시스템을 유지보수하고 테스트하기가 더 어려워지므로 결합도에 세심한 주의를 기울이는 것이 중요합니다.

정적 결합
정적 결합(static coupling) 은 컴포넌트들이 서로 동기적으로 통신할 때 발생합니다. 주목해야 할 정적 결합의 유형은 구심 결합과 원심 결합 두 가지입니다.
구심 결합 (Afferent Coupling)
들어오는(incoming) 결합 또는 팬인(fan-in) 결합이라고도 부릅니다. 다른 컴포넌트들이 대상 컴포넌트에 어느 정도나 의존하는지를 나타냅니다.
예를 들어 Customer Notification 컴포넌트를 생각해 보면, Order Placement와 Order Shipment 컴포넌트는 고객에게 이메일을 보내기 위해 Customer Notification 컴포넌트와 통신해야 합니다. 이 둘은 Customer Notification 컴포넌트와 구심적으로 결합(afferently coupled) 되었다고 말합니다. 이 경우 구심 결합 수준은 2입니다(2는 들어오는 의존성의 개수). 구심 결합은 일반적으로 CA로 표기합니다.
graph LR subgraph "구심 결합" A1[주문 접수] --> B1[고객 알림] A2[주문 배송] --> B1 end
원심 결합 (Efferent Coupling)
나가는(outgoing) 결합 또는 팬아웃(fan-out) 결합이라고도 부릅니다. 대상 컴포넌트가 다른 컴포넌트들에 의존하는 정도를 나타냅니다.
예를 들어 Order Placement 컴포넌트는 Order Fulfillment 컴포넌트에 의존합니다. 따라서 Order Placement 컴포넌트는 Order Fulfillment 컴포넌트와 원심적으로 결합(efferently coupled) 되었습니다. 이 경우 원심 결합 수준은 1입니다(1은 나가는 의존성의 개수). 원심 결합은 일반적으로 CE로 표기합니다.
graph LR subgraph "원심 결합" C1[주문 접수] --> C2[주문 이행] end
직접 통신하지 않는 컴포넌트들도 결합될 수 있다는 점도 중요합니다. 한쪽의 변경이 다른 쪽에 영향을 주로 결합되는 경우가 있습니다.
시간적 결합
시간적 결합(temporal coupling) 은 비정적(nonstatic) 의존성을 설명합니다. 일반적으로 타이밍이나 트랜잭션(transaction) (단일 작업 단위)에 기반한 의존성은 비정적입니다.
예를 들어 Order Placement 컴포넌트와 Order Shipment 컴포넌트의 결합이 있습니다. 주문 접수가 주문 배송보다 먼저 이루어져야 하므로, 두 컴포넌트는 시간적으로 결합됩니다.
시간적 결합의 문제점은 현재 시중에 나와 있는 도구로는 탐지하기 어렵다는 것입니다. 대부분의 경우 이러한 유형의 결합은 설계 문서를 통해 식별되거나 오류 조건을 통해 발견됩니다.
데메테르의 법칙
느슨하게 결합된 시스템을 만드는 기법으로 데메테르의 법칙(Law of Demeter) 이라는 것이 있습니다. 최소 지식의 원칙(Principle of Least Knowledge) 으로도 불립니다.
데메테르의 법칙은 “컴포넌트나 서비스는 다른 컴포넌트나 서비스에 대한 제한된 지식만을 가져야 한다”는 것입니다. 여기서 지식 이란 “어떤 일이 발생해야 한다는 사실을 아는 것”을 뜻합니다. 컴포넌트가 알고 있는 지식이 많을수록 시스템의 나머지 부분과 강하게 결합됩니다.
적용 전: 주문 제출이 너무 많이 아는 상태
주문 제출 컴포넌트가 주문 접수 과정에서 다음과 같은 지식을 모두 갖고 있다고 합니다:
- 주문을 접수하면 재고를 감소 시켜야 한다 → 재고 관리를 호출
- 재고가 부족해지면 공급자에게 추가 재고를 주문 해야 한다 → 공급자 주문을 호출
- 공급이 제한되면 품목 가격을 조정 해야 한다 → 품목 가격 책정을 호출
- 주문 접수가 완료되면 고객에게 이메일 을 보내야 한다 → 이메일 알림을 호출
이 상태를 도식화하면 주문 제출 컴포넌트가 시스템의 나머지 부분과 강하게 결합되어 있습니다:
graph LR OP[주문 제출] -->|재고 감소| IM[재고 관리] OP -->|재고가 부족한가?| SO[공급자 주문] OP -->|단가 인상| IP[품목 가격 책정] OP -->|고객에게 알림| EN[이메일 알림]
주문 제출이 재고 감소뿐만 아니라 “재고가 부족하면 추가 주문을 넣어야 한다”, “공급이 제한되면 가격을 올려야 한다”는 후속 로직까지 전부 알고 있습니다. 이 지식들이 모두 결합 지점이 됩니다.
잘못된 접근: 중간 컴포넌트를 끼워넣기
이 지식을 분산시키려고 주문 제출과 재고 관리 사이에 다른 컴포넌트를 추가해서 “재고를 감소시켜야 한다”는 지식을 위임한다면 어떨까요? 주문 제출 컴포넌트의 원심 결합(나가는 결합) 수는 변하지 않습니다. 결합 대상이 바뀌었을 뿐 결합 지점 자체는 그대로이므로, 시스템은 더 느슨해지지 않습니다.
올바른 접근: 지식 자체를 제거하기
“재고가 부족하면 추가 주문을 넣어야 한다”는 지식과 “공급이 제한되면 가격을 조정해야 한다”는 지식은 주문 제출이 알 필요가 없습니다. 이 두 지식을 재고 관리 컴포넌트에 위임할 수 있습니다. 재고 관리가 재고 감소를 처리하면서 스스로 부족 여부를 판단하고 공급자 주문과 품목 가격 책정을 호출하면 됩니다.
graph LR OP[주문 제출] -->|재고 감소| IM[재고 관리] IM -->|재고가 부족한가?| SO[공급자 주문] IM -->|단가 인상| IP[품목 가격 책정] OP -->|고객에게 알림| EN[이메일 알림]
주문 제출 컴포넌트는 이제 “재고를 감소시켜야 한다”와 “이메일을 보내야 한다”만 알면 됩니다. 특정 기능이 발동되어야 한다는 지식을 제거 한 덕분에 주문 제출 컴포넌트가 시스템의 나머지 부분과 결합하는 정도가 낮아졌습니다.
주의: 결합은 사라지지 않고 이동한다
데메테르의 법칙을 적용해서 주문 제출 컴포넌트의 결합 수준을 줄이긴 했지만, 재고 관리 컴포넌트의 결합 수준이 증가 했다는 점을 주목할 필요가 있습니다. 데메테르의 법칙을 적용해도 시스템 전체의 결합 수준이 반드시 낮아지는 것은 아닙니다. 보통은 결합이 시스템의 다른 부분으로 재배포되기만 합니다.
핵심은 어디에 결합을 집중시킬 것인지를 의도적으로 결정하는 것 입니다.
사례 연구: GGG(Going, Going, Gone) — 컴포넌트의 발견
GGG(Going, Going, Gone) 라는 가상의 온라인 경매 시스템을 예로 행위자/행동 접근법을 적용해 봅니다.
세 가지 주요 행위자를 식별할 수 있습니다: 입찰자(bidder), 경매 진행자(auctioneer), 그리고 시스템 입니다. 이 모델링 기법에서 시스템은 내부 행동들을 수행하는 행위자로서 자주 등장합니다.
각 행위자의 주요 행동:
- 입찰자 — 라이브 동영상 스트림을 시청한다, 라이브 입찰 스트림을 시청한다, 입찰한다
- 경매 진행자 — 라이브 입찰들을 시스템에 입력한다, 온라인 입찰들을 받는다, 품목이 팔렸음을 표시한다
- 시스템 — 경매를 시작한다, 결제를 처리한다, 입찰자의 활동을 추적한다
이러한 행동들을 바탕으로 도출한 초기 컴포넌트 집합:
| 컴포넌트 | 역할 |
|---|---|
| Video Streamer | 사용자에게 라이브 경매를 스트리밍합니다 |
| Bid Streamer | 입찰이 입력될 때마다 사용자에게 입찰 정보를 스트리밍합니다. Video Streamer와 Bid Streamer 둘 다 입찰자에게 경매에 대한 읽기 전용 뷰를 제공합니다 |
| Bid Capture | 경매 진행자와 입찰자로부터 입찰 정보를 캡처합니다 |
| Auction Session | 경매를 시작하고 낙찰 시 결제 및 결정(resolution) 단계를 촉발합니다. 다음 입찰 품목을 입찰자들에게 알리는 작업도 포함됩니다 |
| Bid Tracker | 입찰 정보를 추적하며, 시스템의 기록 저장소 역할을 합니다 |
| Payment | 신용카드 결제를 위한 서드파티 결제 컴포넌트입니다 |
| ![[Pasted image 20260315211923.png | 748]] |
| 초기 컴포넌트 식별 단계를 거친 후 아키텍트는 식별된 아키텍처 특성들을 분석해서 컴포넌트 설계에 영향을 미칠 만한 특성이 있는지 파악합니다. |
예를 들어 현재 설계에는 입찰자와 경매 진행자 모두로부터 입찰을 캡처하는 Bid Capture 컴포넌트가 있습니다. 입찰이 누구로부터 오든 입찰 캡처는 동일하므로, 입찰 캡처를 하나의 컴포넌트가 담당하게 한 것은 기능적으로 합당한 선택입니다. 하지만 아키텍트는 입찰 이전에 식별된 아키텍처 특성들 중 어떤 것이 필요한 것인지 고찰해야 합니다.
경매당 입찰자는 수백, 수천 명일 수 있지만 경매 진행자는 한 명이므로, 입찰자만큼의 확장성이나 탄력성이 필요하지 않습니다. 마찬가지로 시스템의 다른 부분에 비해 경매 진행자에게 특별히 요구되는 아키텍처 특성들도 있습니다. 신뢰성(연결이 끊어지지 않도록 보장하는 것)이나 가용성(시스템이 계속 작동하도록 보장하는 것)이 좋은 예입니다. 입찰자가 사이트에 로그인할 수 없거나 연결이 끊기는 것은 사업상 좋지는 않겠지만 치명적이지는 않습니다. 하지만 경매 진행자에게 그런 일이 발생한다면 치명적입니다.
같은 아키텍처 특성에 대해 입찰자와 경매 진행자의 요구 수준이 다르므로 아키텍트는 Bid Capture 컴포넌트를 Bid Capture(입찰 캡처) 와 Auctioneer Capture(경매 진행자 캡처) 라는 두 개의 컴포넌트로 분할하기로 결정합니다.

새 Auctioneer Capture는 Auctioneer Capture에서 Bid Streamer로의 정보 링크(온라인 입찰자에게 라이브 입찰을 보여주기 위한)와 Bid Tracker로의 정보 링크(입찰 스트림을 관리하기 위한)를 갱신합니다. Bid Tracker는 이제 시스템 기록 저장소인 동시에, 경매 진행자의 단일 정보 스트림과 입찰자들의 다중 스트림이라는 다른 두 정보 스트림을 통합하는 컴포넌트가 됩니다.
이 설계가 최종 설계가 될 가능성은 낮습니다. 사용자가 새 계정을 등록하는 방법이나 시스템이 결제 기능을 관리하는 방법 등 더 많은 요구사항이 밝혀져야 합니다. 하지만 이 설계는 추가적인 반복 작업을 출발점으로 유용할 것입니다.
모든 설계는 트레이드오프를 다릅니다. 아키텍트가 ‘유일하고 올바른 설계’를 찾는 데 집착해서는 안 됩니다. 충분히 잘 작동하는 설계는 얼마든지 많습니다. 다양한 설계 결정 사이의 트레이드오프를 가능한 한 객관적으로 평가하고, ‘가장 덜 나쁜’ 트레이드오프 집합을 가진 설계를 선택하는 데 주력해야 합니다.
정리
| 접근법 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| 작업흐름 접근법 | 사용자 여정 기반으로 직관적, 비교적 적은 수의 컴포넌트 도출 | 행위자가 여럿일 때 모든 관점을 포착하기 어려움 | 요구사항이 불명확하지만 전반적인 작업흐름은 파악한 경우 |
| 행위자/행동 접근법 | 다양한 행위자의 관점 반영, 더 세분화된 컴포넌트 도출 | 컴포넌트 수가 많아질 수 있음 | 행위자가 여럿이고 각각 다른 행동을 수행하는 시스템 |
| 엔티티 함정 (안티패턴) | - | 역할 불명확, kitchen sink 컴포넌트 생성, 과도한 세분도 | 피해야 할 접근법 (CRUD 전용 시스템 제외) |
내 생각
- 논리적 아키텍처를 물리적 아키텍처와 분리해서 먼저 고민하는 습관이 중요합니다. 실무에서는 “이건 마이크로서비스로 할까, 모놀리스로 할까?”라는 물리적 결정을 먼저 하고 논리적 구조는 그 안에서 즉흥적으로 만드는 경우가 많은데, 이러면 컴포넌트 간 경계가 흐릿해지고 나중에 분리하기가 매우 어려워집니다.
- 엔티티 함정은 실제로 매우 흔합니다.
UserService,OrderManager같은 이름의 클래스가 수천 줄짜리 God Object로 커지는 것을 한 번쯤 경험해 봤을 것입니다. 컴포넌트 이름에 Manager, Service, Handler 같은 범용 접미사가 붙는 순간 경계해야 합니다. - 데메테르의 법칙은 시스템 전체 결합을 낮추는 게 아니라 재배치 하는 것이라는 점이 핵심입니다. 어디에 결합을 집중시킬 것인지를 의도적으로 결정하는 것이 아키텍트의 역할입니다.
관련 개념
출처
- Fundamentals of Software Architecture (Mark Richards, Neal Ford) — Chapter 8