Aggregate Model (1)

 

Aggregate Model: Bounded Context를 활용한 대규모 앱 설계

단순히 화면마다 뷰모델을 만드는 방식에서 벗어나, 앱의 규모에 따라 데이터 원천을 어떻게 논리적으로 격리하고 관리할 것인지에 대한 아키텍처적 가이드라인을 정리한다.

1. 단일 모델 구조 (Small to Medium Apps)

앱이 작거나 중간 규모일 때는 하나의 거대한 모델이 전체 상태를 관리할 수 있다. Apple의 Food Truck샘플 앱이 대표적인 사례이다.

@MainActor
public class FoodTruckModel: ObservableObject {
    @Published public var truck = Truck()
    
    @Published public var orders: [Order] = []
    @Published public var donuts = Donut.all
    @Published public var newDonut: Donut
        
    var dailyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    var monthlyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    // 생략
}

Image

  • 개념: 하나의 Food Truck Model이 트럭, 주문, 도넛, 주문 요약 등 앱의 모든 주요 엔티티(Entity)를 관리한다.
  • 특징: 모든 View가 이 하나의 중앙 모델에 직접 접근하여 데이터를 가져오며, 화면별로 뷰모델을 쪼개지 않고 전체 앱 상태를 하나로 캡슐화한다.

2. Bounded Context: 대규모 앱의 분리 기준

앱의 규모가 커지면 단일 모델(Observable Object)에 모든 것을 담을 수 없음. 이때 도메인 주도 설계의 핵심 개념인 Bounded Context가 필요하다.

Image

  • 정의: 특정 모델이나 개념이 적용되는 유효 범위 혹은 경계.
  • 핵심 원칙: 화면의 개수가 아니라, 비즈니스 로직의 경계에 따라 Store를 나눔.
  • 도메인 전문가(Domain Expert): 개발자가 임의로 나누는 것이 아니라, 비즈니스 흐름을 꿰뚫고 있는 전문가와의 소통을 통해 실제 도메인 경계를 설정해야 한다.

3. 이커머스 사례를 통한 Store 구조화

대규모 이커머스 시스템은 Bounded Context에 따라 다음과 같이 여러 개의 전문 Store로 나뉠 수 있다.

Image

Bounded Context 담당 Store 관리 엔티티 예시
카탈로그 관리 CatalogStore 아이템 목록, 카테고리 정보 등
주문 관리 OrderStore 주문 내역, 결제 정보 등
계정/고객 관리 UserAccountStore 사용자 프로필, 포인트 등
  • 유연성: 특정 화면은 OrderStore만 사용할 수도 있고, 관리자 뷰처럼 OrderStoreCatalogStore를 동시에 참조할 수도 있다.

4. 최종 아키텍처의 물리적 계층 구조

대규모 앱은 폴더나 패키지 단위로 역할을 명확히 분리한다.

Image

  • UI 계층: 도메인별 폴더(Catalog UI, Inventory UI 등)에 관련 뷰들을 모아둔다.
  • Data Store 계층: 각 도메인의 상태를 책임지는 Observable 객체(Store)들이 위치한다.
  • Network 계층: 모든 Store가 공통으로 사용하는 얇고 범용적인 HTTPClient 레이어.
  • Shared 계층: 공통 상수, 공유 UI 등 도메인 간에 공통으로 쓰이는 코드를 관리한다.

💡 핵심 요약

결국 Aggregate Model 설계의 정수는 “누구와 소통하여 경계를 칠 것인가(도메인 전문가)”“어떤 논리적 경계로 데이터를 격리할 것인가(Bounded Context)”에 있다. 단순히 화면 단위로 파일을 만드는 기계적 작업에서 벗어나, 실제 비즈니스 구조를 코드 아키텍처에 투영하는 과정이다.


Bounded Context 기반의 Store 분리와 다중 Store 활용

단일 Store 체제에서 앱의 규모가 커질 때, 도메인 경계(Bounded Context)를 따라 Store를 분리하고 뷰에서 활용하는 과정을 정리한다.

1. 단일 Store의 한계 상황 (CoffeeStore의 확장)

만약 앱이 비대해져서 주문과 직원관리를 할 수 있는 앱이 되었다고 가정을 해보자. 처음에는 OrderingStore 하나로 주문과 직원 관리(Employee)를 모두 처리할 수 있다.

CoffeStore에서 OrderingStore로 명칭 변경

하지만 직원 관리 로직이 복잡해지면 하나의 클래스가 너무 비대해지는 문제가 발생한다.

  • 현상: 한 클래스 안에 주문 관련 함수와 직원 관리 함수 10여 개가 섞이기 시작한다.
  • 판단 기준: 해당 도메인이 하나의 원천(Source of Truth)으로 남기에 너무 커졌는지 고민하고 경계를 설정해야 한다.

2. 도메인 분리: EmployeeManagementStore 구축

주문(Ordering)과 직원 관리(Employee Management)는 서로 다른 비즈니스 영역이다. 이를 Bounded Context 원칙에 따라 별도의 Store로 분리한다.

@Observable
class EmployeeManagementStore {
    
    var employees: [Employee] = []
     
     // Add employee to the store
     func addEmployee(name: String, role: String, department: String) {
         let employee = Employee(id: UUID(), name: name, role: role, department: department)
         employees.append(employee)
     }
     
     // Retrieve employees by department
     func getEmployeesByDepartment(department: String) -> [Employee] {
         return employees.filter { $0.department == department }
     }
    
}
  • 로직 요약:
    • 상태 관리: employees 배열을 통해 직원 데이터의 원천을 독립적으로 관리한다.
    • 직원 추가: 새로운 직원 인스턴스를 생성하여 배열에 직접 추가하는 addEmployee 기능을 수행한다.
    • 데이터 필터링: 전체 데이터 중 특정 부서에 해당하는 직원만 선별하여 반환하는 getEmployeesByDepartment 로직을 담당한다.

3. 다중 Store 주입 및 View에서의 활용

분리된 Store들은 뷰 계층의 상단에서 environment를 통해 주입하며, 뷰는 필요한 Store만 골라서 사용한다.

struct EmployeeManagementScreen: View {

    @Environment(OrderingStore.self) private var orderingStore
    @Environment(EmployeeManagementStore.self) private var employeeManagementStore
    
    var body: some View {
        List {
            Section("Orders") {
                // OrderListView(orders: orderingStore.orders)
                ForEach(orderingStore.orders) { order in
                    Text(order.name)
                }
            }
            
            Section("Employees") {
                // EmployeeListView(employees: employeeManagementStore.employees)
                ForEach(employeeManagementStore.employees) { employee in
                    Text(employee.name)
                }
            }
        }
    }
}

  • 구조: 관리자 화면은 ‘주문 현황’과 ‘직원 관리’가 모두 필요하므로 두 Store를 동시에 관측한다.
    • @Environment(OrderingStore.self) private var orderingStore
    • @Environment(EmployeeManagementStore.self) private var employeeStore
  • 데이터 활용: 각 섹션에서 orderingStore.ordersemployeeStore.employees를 각각 호출하여 렌더링한다.

4. 왜 이것이 MVVM보다 강력한가?

이 방식이 기존의 ‘화면 단위 뷰모델(MVVM)’과 결정적으로 다른 점은 데이터의 공유와 동기화 방식이다.

비교 항목 MVVM (Classical) Store 패턴 (Aggregate Model)
생성 기준 화면 개수와 1:1 대응 비즈니스 도메인(Bounded Context) 기준
데이터 공유 클로저, Combine 등으로 복잡하게 전달 필요한 Store를 Environment에서 즉시 호출
동기화 뷰모델 간 알림 로직이 필요함 Store가 단일 원천이므로 자동 갱신됨
  • 주의: 단순히 뷰를 리프레시하기 위해 불필요한 Boolean 값을 뷰모델에 넘기는 방식은 SwiftUI 설계 의도에 어긋난다.

5. 최적화 팁: Reusable View와 데이터 트래킹

뷰를 설계할 때 Store 전체를 넘기기보다, 실제로 필요한 데이터 조각만 넘기는 것이 렌더링 성능에 유리하다.

  • 권장 방식: OrderListView(orders: orderingStore.orders)와 같이 필요한 배열만 전달한다.
  • 이점: SwiftUI는 해당 배열의 변화만 정밀하게 추적하여 불필요한 리렌더링을 방지한다.

핵심 요약

Store는 화면을 위해 존재하는 것이 아니라 도메인 데이터의 원천으로서 존재한다. 대규모 앱일수록 화면 단위가 아닌 비즈니스 경계(Bounded Context)를 따라 Store를 설계하고, 뷰는 필요한 Store들로부터 데이터를 구독하는 형태를 취해야 한다.


대규모 도메인에서의 Store 간 통신 설계 (의존성 관리)

병원 시스템과 같은 거대 도메인을 사례로 들어, 분리된 Bounded Context들이 상호작용해야 하는 상황과 그에 따른 설계적 고민을 정리한다.

1. 도메인 세분화: Bounded Context의 확장

병원 도메인은 매우 방대하므로 초기에는 ‘환자 관리’ 하나로 시작하더라도, 비즈니스 로직에 따라 더 세밀하게 Store를 분리해야 한다.

Image

  • 환자 관리(Patient Management): 환자 기본 정보, 등록 등 담당.
  • 진료 기록 관리(Medical Records): 진단, 치료, 처방전 기록 담당.
  • 예약 관리(Appointments): 진료 예약 일정 담당.
  • 판단 근거: 환자 관리 Store 하나에 예약과 진료 기록 로직을 다 넣으면 수천 줄의 코드가 발생하므로, 도메인 전문가와의 소통을 통해 이를 별도의 데이터 스토어(Observable Object)로 격리한다.

2. 데이터 흐름의 정석: View와 Network Layer

각각의 독립된 Store는 화면(View)에서 직접 접근하며, 보안이 중요한 의료 데이터 특성상 서버와의 통신을 통해 데이터를 유지한다.

Image

  • 흐름: View -> Store -> Network Layer -> Server 순으로 요청이 흐른다.
  • 보안: 환자 정보와 같은 민감 데이터는 디바이스에 저장하지 않고 서버에서 실시간으로 가져오는 것을 원칙으로 한다.

3. 핵심 문제 제기: Store 간의 통신(Communication)

도메인이 분리되어 있더라도 실제 비즈니스 로직에서는 각 Store 간의 정보 공유가 반드시 필요하다.

Image

  • 상황 예시:
    • ‘진료 기록 관리’에서 처방을 내리기 위해 ‘예약 관리’의 특정 진료 세션 정보가 필요한 경우.
    • ‘예약 관리’에서 예약을 확정하기 위해 ‘환자 관리’의 환자 식별 정보가 필요한 경우.
  • 설계적 고민: “Store 내부에 다른 Store를 중첩(Nested)해서 선언해야 하는가?”, “어떤 방식으로 이 통신 채널을 구축해야 데이터 무결성을 지키면서도 결합도를 낮출 수 있는가?”

핵심 요약

분리된 Bounded Context는 독립적으로 존재할 때 가장 안전하지만, 실제 앱은 이들의 협업으로 작동한다.