Architecture & Design/Software Architecture

Principles - 08. 복잡성의 원천

흙은 없앨 수 없지만 잡초는 뽑을 수 있다 — 복잡성의 원천

정원을 가꾼다고 하자. 흙이 있다 — 식물이 자라려면 흙이 필요하고, 흙의 종류·배수성·영양분이 복잡하다. 이건 정원의 본질이다, 없앨 수 없다. 하지만 잡초도 있다 — 우리가 심지 않았는데 자라난 것, 영양분을 빼앗고 정원을 어지럽힌다. 이건 뽑을 수 있다. "정원이 복잡하다"고만 말하면 흙과 잡초를 구분 못 한다 — 무엇이 본질이고 무엇이 우리가 만든 것인가를 가려야 한다.

소프트웨어 복잡성도 두 종류다. Fred Brooks는 (1986)에서 본질적 복잡성(essential complexity)과 우발적 복잡성(accidental complexity)을 나눴다. 본질적 복잡성은 문제 자체에서 온다 — 결제 도메인의 복잡한 규칙, 동시성 요구, 분산 합의. 우발적 복잡성은 우리가 선택한 도구·기법·구조에서 온다 — 과도한 간접층, 불필요한 추상화, 잘못 고른 프레임워크. 둘을 구분하지 못하면, 갚을 수 있는 부채(잡초)를 그냥 두거나, 갚을 수 없는 부채(흙)를 없애려 허우적댄다.

Ousterhout의 복잡성 정의

John Ousterhout는 에서 복잡성을 "시스템을 이해하고 수정하기 어ulse 만드는 모든 것"이라 정의하고, 세 가지 형태로 나눴다.

형태 설명 정원 비유
변경 증폭(change amplification) 한 변경이 여러 곳에 영향 잡초가 번식하면 정원 전체를 덮음
인지 부하(cognitive load) 개발자가 알아야 할 것이 너무 많음 흙의 성분을 전부 외워야 하는 것
알 수 없는 미지(unknown unknowns) 무엇을 변경해야 하는지조차 모름 어디에 잡초가 있는지 모르는 것

이 중 "알 수 없는 미지"가 가장 위험하다 — 변경 증폭과 인지 부하는 적어도 "무엇을 고쳐야 하는지"는 안다. 미지는 "무엇을 모르는지조차 모르는" 상태라, 변경이 의도치 않은 곳에서 부작용을 낸다.

본질적 복잡성 — 줄일 수 없다

본질적 복잡성은 문제 자체의 어려움이다. 결제 시스템에서 "같은 카드로 동시에 두 번 결제되는 것을 막아야 한다"는 규칙은, 아키텍처를 어떻게 짜든 존재한다 — 동시성 제어가 필요하고, 그건 본질적으로 어렵다. 이 복잡성을 "단순화"하려 하면 문제 자체를 단순화하는 것(동시성 포기)이 되어 비즈니스 요구를 충족 못 한다.

본질적 복잡성에 대처하는 방법은 "줄이는 것"이 아니라 "잘 격리하는 것"이다 — 복잡한 규칙을 한 모듈(애그리거트) 안에 가두어, 시스템의 나머지 부분이 그 복잡성에 노출되지 않게 한다.

우발적 복잡성 — 뽑을 수 있다

우발적 복잡성은 우리가 만든 것이다. 그래서 제거할 수 있다.

// 우발적 복잡성 1: 과도한 간접층
class OrderController {
    OrderFacade facade;   // Facade
    // Facade가 Service를 호출하고, Service가 UseCase를 호출하고,
    // UseCase가 DomainService를 호출하고... → 한 변경이 4계층에 영향
}

// 우발적 복잡성 2: 설정 지옥
new OrderService(
    new OrderValidator(config.getRules()),
    new OrderProcessor(mapper, converter, formatter),
    new OrderNotifier(emailConfig, smsConfig, pushConfig)
);   // 10개 매개변수 — 무엇을 넣어야 하는지 파악만 30분

// 우발적 복잡성 3: 불필요한 동시성
class OrderService {
    synchronized void place(Order order) {  // 모든 주문에 동기화
        repo.save(order);                   // 실제로는 경합이 없는데 lock 비용만 발생
    }
}

설계 사례 — 우발적 복잡성 제거

flowchart LR
    subgraph Before["Before: 우발적 복잡성"]
        B1["OrderController"] --> B2["OrderFacade"] --> B3["OrderService"] --> B4["OrderUseCase"] --> B5["OrderDomain"]
        Note1["4계층 간접 → 한 변경이 4곳에 영향 (변경 증폭)"]
    end
    subgraph After["After: 본질만 남김"]
        A1["OrderController"] --> A2["OrderService"] --> A3["Order (도메인)"]
        Note2["2계층 → 변경 영향 최소화"]
    end
// Before — 우발적 복잡성: 4계층 통과 (변경 증폭)
// "주문에 할인 적용"을 추가하려면:
orderFacade.applyDiscount(orderId, discountRate);     // Facade
orderService.applyDiscount(orderId, discountRate);    // Service
orderUseCase.execute(new ApplyDiscountCommand(...));  // UseCase
order.applyDiscount(discountRate);                    // Domain (실제 로직)
// 같은 변경이 4곳을 건드림 — 변경 증폭

// After — 본질만 남김: 2계층 (변경 국소화)
// "주문에 할인 적용":
orderService.applyDiscount(orderId, discountRate);
//   → 내부에서 order.applyDiscount(discountRate) 호출
// 한 곳만 변경 — 변경 증폭 없음

Before에서 "할인 적용"이라는 변경이 Facade → Service → UseCase → Domain 4곳을 건드린다. 각 계층은 "구조를 위한 구조"였다 — 비즈니스 가치를 더하지 않고 통과만 시킨다. 이 계층들이 존재하는 이유가 "아키텍처 원칙"이라면, 그건 원칙을 위한 원칙이다 — 우발적 복잡성이다. After에서 불필요한 계층을 제거하면, 변경이 한 곳에 머문다.

복잡성을 측정하는 지표

"복잡하다"는 감각을 수치로 잡으면, 퇴보를 감지할 수 있다.

지표 측정 방법 위험 임계치 복잡성 종류
순환 복잡도 (cyclomatic complexity) 분기(if/for/while) 수 메서드당 10 초과 인지 부하
응집 결여 (LCOM) 클래스 내 메서드 간 필드 공유도 높을수록 낮은 응집 인지 부하
들어오는 의존성 수 (fan-in) 나를 import하는 파일 수 너무 많으면 변경 시 파급 변경 증폭
나가는 의존성 수 (fan-out) 내가 import하는 파일 수 20 초과 시 주의 인지 부하
동일 변경에 닿는 파일 수 "기능 X 추가" 시 수정되는 파일 수 5개 초과 시 주의 변경 증폭

마지막 지표("동일 변경에 닿는 파일 수")가 Ousterhout이 말한 변경 증폭의 직접적 측정이다. "할인 추가"에 1개 파일만 닿으면 낮은 복잡성, 5개 파일이 닿으면 높은 복잡성이다. 이 수치를 배포 주기별로 추적하면, 복잡성이 자라는 시점을 감지할 수 있다.

복잡성 줄이기 — 구체적 기법

기법 복잡성 형태 실천
깊은 모듈 (02번) 인지 부하 인터페이스를 작게, 구현을 풍부하게
주석 (설계 메모) 미지(unknown unknowns) "왜 이 구조인가"를 코드 옆에 기록
불필요한 계층 제거 변경 증폭 pass-through 계층(Facade/Wrapper) 제거
전역 상태 제거 미지 전역 변수 → 주입(의존성 역전)
작은 애그리거트 변경 증폭 큰 클래스 → 작은 애그리거트로 분리 (05-ddd 03번)

주석이 복잡성 도구라는 점이 의외일 수 있다. Ousterhout은 "주석이 없는 코드는 복잡하다"고 본다 — "왜 이렇게 했는가"를 모르면, 변경할 때 미지(unknown unknowns)에 부딪힌다. 주석은 구현을 설명하는 게 아니라, 설계 결정의 이유와 제약을 기록한다. "여기서 분산 락을 쓰는 이유: 동시 주문 시 재고 음수 방지 (ADR-003 참조)" — 이 한 줄이 다음 개발자가 안전하게 변경할 수 있게 한다.

흙과 잡초를 구분하는 습관

"이 복잡성이 문제에서 온 것인가, 우리가 만든 것인가"를 묻는 습관이 중요하다. 동시성 제어는 흙이다 — 뽑을 수 없다. 하지만 그 동시성 제어를 4계층의 간접층으로 감싼 건 잡초다 — 뽑을 수 있다. 매 코드 리뷰에서 "이 코드는 본질적 복잡성을 다루고 있는가, 우발적 복잡성을 만들고 있는가"를 묻는 것 — 그것이 복잡성을 관리하는 출발점이다.


참고