Architecture & Design/Software Architecture

Foundations - 04. 아키텍처 문서화

건축 도면이 거짓말이 되는 순간 — 아키텍처 문서화

건축가가 집을 지을 때 도면을 그린다. 대지 평면도로 전체를 보고, 층별 평면도로 층을, 방 상세도로 디테일을 본다. 도면은 집을 이해하고 소통하는 도구다. 하지만 도면이 집 그 자체는 아니다. 현장에서 벽을 하나 허물고 도면을 안 고치면, 도면은 거짓말이 된다. 6개월 뒤 새 직원이 그 도면을 보고 집을 이해하려다 포기한다 — 도면 속 "거실"은 이미 침실로 바뀌어 있는데 도면엔 옛날대로 그려져 있으니까.

소프트웨어 아키텍처 문서도 같다. 한때 정확했던 상자-화살표 그림이, 코드가 바뀌어도 안 고쳐지면 거짓말이 된다. 그렇다고 문서를 포기하면 결정의 이유가 사라지고 매번 같은 논쟁이 반복된다. 문제는 "문서를 쓸까 말까"가 아니라 "무엇을, 어떻게 기록하면 시스템과 함께 살아남는가"다.

이 비유의 한계: 건축 도면은 정적 구조만 다루지만, 소프트웨어는 실행 흐름(어떤 컴포넌트가 런타임에 무엇을 호출하는가)도 다뤄야 한다. 건물엔 "실행 흐름"이 없다.

한 장의 도면으로는 부족하다

아키텍처를 하나의 그림으로 요약하려는 시도가 흔하다. 하지만 한 시스템은 여러 구조(모듈·컴포넌트-커넥터·할당)로 동시에 존재한다. 각 구조를 보여주는 그림은 다르다. 하나의 그림에 이들을 억지로 욱여넣으면 누구에겐 유용하고 누구에겐 무의미한 혼란이 된다.

Philippe Kruchten은 이 문제를 4+1 뷰 모델로 정식화했다. 각 뷰는 특정 이해관계자를 위한 것이다:

다루는 것 주 이해관계자
논리 뷰(logical) 기능을 담당하는 클래스·모듈·패키지 개발자
프로세스 뷰(process) 동시성·통신·실행 단위 통합자·성능 담당
개발 뷰(development) 소스 코드의 물리적 조직(빌드·패키지·레이어) 프로그래머·빌드 담당
물리 뷰(physical) 하드웨어·노드·네트워크에 소프트웨어가 어떻게 배치되는가 운영자·인프라
시나리오(+1) 앞선 뷰들이 실제 사용 사례를 통해 일관되는지 검증 전원

"+1"인 시나리오가 중요한 이유는, 각 뷰를 따로 그리면 충돌할 수 있기 때문이다. 논리 뷰에선 깔끔해 보여도 물리 뷰에선 한 노드에 병목이 몰릴 수 있다. 시나리오(중요 사용 사례의 흐름)를 각 뷰 위에 얹어 "이 뷰들로 이 시나리오가 설명되는가"를 검증한다. Kruchten의 원 모델은 UML과 밀접해 오늘날 직접 쓰진 않지만, 아키텍처는 한 뷰가 아니라 여러 뷰의 집합이며 각 뷰는 특정 질문에 답한다는 점은 여전히 유효하다.

C4 — 줌 레벨을 가진 아키텍처 지도

Simon Brown의 C4 모델은 4+1의 정신을 현대적 코드 지도로 재해석한다. 도면의 줌 레벨처럼, 같은 시스템을 네 상세도로 본다:

flowchart LR
    C1["Context<br/>(누가 시스템을 쓰나)"] --> C2["Container<br/>(배포 단위별)"]
    C2 --> C3["Component<br/>(컨테이너 안의 모듈)"]
    C3 --> C4["Code<br/>(클래스 수준, 필요시만)"]
  • Context(수준 1) — 시스템 전체와 그것을 둘러싼 사용자·외부 시스템. 가장 추상적. 비개발자도 이해.
  • Container(수준 2) — 여기서 container는 Docker가 아니라 배포 단위(웹 앱, API 서비스, DB, 메시지 큐)다. 컨테이너들 사이의 통신과 기술 스택.
  • Component(수준 3) — 한 컨테이너 안의 주요 모듈/컴포넌트와 의존성.
  • Code(수준 4) — 클래스·인터페이스 수준. 자주 바뀌므로 자동 생성에 맡기고 수작업으로는 거의 안 그린다.

C4의 미덕은 적절한 상세도를 골라 쓴다는 점이다. 모든 것을 한 그림에 욱여넣지 않고, 청중에 따라 줌 레벨을 바꾼다 — 경영진에겐 Context, 신규 개발자에겐 Container+Component. Brown은 그림을 짧은 설명과 함께 코드 가까이 두어야 한다고 강조한다. 그래야 코드가 바뀌어도 그림이 따라가서, 거짓말 도면이 덜 생긴다. 줌 레벨을 헷갈려 한 그림에 Context와 Component를 섞으면 다시 혼란이 돌아오니, 한 그림은 한 줌 레벨만 담는다.

구조가 아니라 결정을 기록한다 — ADR

여기까지는 "구조"를 그리는 법이었다. 하지만 아키텍처의 본질은 결정이다. 결정에서 가장 빨리 잃히는 건 "왜 그렇게 결정했는가"다. 6개월 뒤엔 "왜 동기 대신 이벤트를 택했지?"를 아무도 기억 못 한다. 그래서 같은 논쟁이 반복된다.

이걸 막는 도구가 아키텍처 결정 기록(ADR, Architecture Decision Record)이다. Michael Nygard가 2012년에 제안한 형식으로, 각 결정을 짧은 문서 하나로 남긴다. 전형적 구조는 간결하다:

# ADR-007: 주문 처리를 이벤트 기반으로 전환한다

## 상태(status)
수락됨 (2026-06-10)

## 맥락(context)
피크 트래픽 시 결제 지연이 주문 전체를 블록시킨다(사례: 5월 프로모션 장애).
동기식 구조에서는 결제 서비스 회복 전까지 주문 수용 불가.

## 결정(decision)
주문 접수는 이벤트 발행까지만 동기로 처리, 결제·재고·배송은 비동기 구독으로 전환.
결제 완료는 '최종 일관성'으로 타협 — 사용자에게는 "결제 대기 중" 상태 노출.

## 결과(consequences)
- 얻는 것: 결제 장애 시에도 주문 수용 가능, 서비스별 독립 확장.
- 잃는 것: 강일관성 포기, 메시지 브로커 의존성 추가, 디버깅 복잡도 증가.
- 후속 필요: outbox 패턴으로 이벤트 발행 신뢰성 확보(별도 ADR).

"결과(consequences)"를 적는 것이 ADR의 핵심이다. 결정을 기록하되 무엇을 포기했는가를 함께 적는다 — 이게 없으면 결정의 트레이드오프가 안 보이고 "왜 이렇게 했지?"에 답하지 못한다. ADR은 불변이다. 결정을 번복하면 새 ADR을 쓰고 이전 것에 "superseded by ADR-012"를 붙인다. 아키텍처의 결정사가(history of decisions)가 쌓이는 것이다. 논쟁이 재발하면 "ADR-007을 보세요, 거기서 트레이드오프를 다뤘습니다"로 닫힌다. 매번 처음부터 다시 논쟁할 필요가 없다.

문서는 어디에, 얼마나 둘 것인가

문서의 가장 큰 적은 부패(decay)다. 시스템은 계속 바뀌고, 문서는 가만히 있어 진실에서 멀어진다. 현대 담론이 모으는 원칙은 대체로 이렇다:

  • 코드 가까이에 — 별도 위키보다 저장소 안(코드 옆)에. docs/architecture/나 ADR 디렉토리를 코드와 같은 저장소에 두어 변경이 함께 일어나게.
  • 자동 생성되는 건 자동으로 — 코드 수준 그림(의존성 그래프, 컴포넌트 맵)은 수작업 말고 도구로. 사람이 그리면 곧 부패한다.
  • 결정은 쓰고, 구조는 짧게 — 거대 구조 문서는 금방 부패한다. 반면 ADR(결정)은 부패에 강하다 — 결정은 역사적 사실이라 바뀌지 않기 때문. 그래서 현대 팀은 구조 문서보다 ADR에 더 많은 에너지를 쏟는 경향이 있다.
  • 청중에 맞춰 — 개발자용과 비개발자용을 구분. C4의 Context는 비개발자에게, Component는 개발자에게.

과소 문서화와 과잉 문서화 사이의 정답은 없다. 아무 문서도 없으면 결정의 이유가 사라지고, 너무 많으면 아무도 안 읽어 똑같이 쓸모가 없다. 대체로 "구조는 가볍게(자동 생성 우선), 결정은 꼼꼼히(ADR)" 쪽이 부패에 강하다.

설계 사례 — C4 Container로 시스템 그리기 + ADR로 결정 추적하기

주문 시스템을 C4 Container 수준에서 그리면, 한눈에 배포 단위와 통신이 보인다. Context 수준(시스템 전체)에서는 안 보이던 기술 스택과 통신 방식이 Container에서 드러난다.

flowchart LR
    User["사용자<br/>(브라우저)"] -->|HTTPS| Web["웹 앱<br/>(React SPA)"]
    Web -->|REST API| API["주문 API 서비스<br/>(Spring Boot)"]
    Web -->|REST API| Catalog["카탈로그 서비스<br/>(Spring Boot)"]
    API -->|JPA| DB[("주문 DB<br/>(PostgreSQL)")]
    API -->|이벤트 발행| MQ[("메시지 큐<br/>(Kafka)")]
    MQ -. 구독 .-> Ship["배송 서비스<br/>(Spring Boot)"]
    MQ -. 구독 .-> Notify["알림 서비스<br/>(Node.js)"]

이 Container 다이어그램이 있으면, 새 개발자가 "배송은 어떻게 트리거되나?"라고 물을 때 "Kafka의 order-placed 토픽을 배송 서비스가 구독한다"로 바로 답할 수 있다. 다이어그램이 없다면 코드를 뒤져야 한다.

하지만 다이어그램만으로는 "왜 Kafka를 썼는가?"에 답하지 못한다. 그 답은 ADR에 있다:

# ADR-003: 서비스 간 통신을 동기 HTTP에서 Kafka 이벤트로 전환한다

## 상태
수락됨 (2026-05-20), ADR-001(결제 분리)의 후속

## 맥락
결제 서비스를 분리(ADR-001)한 뒤에도 동기 HTTP로 호출 중.
피크 시 결제 지연이 주문 API로 전파되어 p99 타임아웃 발생.

## 결정
서비스 간 비동기 통신을 Kafka 이벤트로 전환.
주문 API는 OrderPlaced 이벤트만 발행, 배송·알림은 구독.

## 결과
- 얻는 것: 결제 지연이 주문 API에 영향 없음, 서비스별 독립 확장.
- 잃는 것: 최종 일관성(배송 처리까지 시차), Kafka 운영 부담.
- 전제: 배송 지연을 사용자가 수용할 수 있어야 함(프론트엔드에서 '처리 중' 표시).

이 ADR이 있으면, 6개월 뒤 "왜 Kafka를 썼지, 그냥 HTTP 호출이 낫지 않아?"라는 질문에 ADR-003을 가리키면 끝난다 — 맥락(동기 호출의 장애 전파), 결정(비동기 이벤트), 결과(최종 일관성 수용)가 한 문서에 있다. 다이어그램은 "무엇이 연결되어 있는가"를, ADR은 "왜 그렇게 연결했는가"를 답한다. 두 가지가 함께 있어야 아키텍처 문서가 살아있다.

도면이 아니라 결정을 남기는 일

아키텍처를 "문서화"하라는 임무를 받으면, 예쁜 그림부터 그리지 않는 편이 낫다. 먼저 내린 결정과 그 트레이드오프를 ADR로 한 줄씩 적는다. 그것이 6개월 뒤에도 살아있는 유일한 문서가 될 확률이 가장 높다. 그림은 코드 옆에, 자동 생성으로, 짧게. 건축가가 도면을 버리지는 않겠지만, 도면이 집이 아니듯 다이어그램도 아키텍처가 아니다 — 살아있는 것은 코드와 결정 기록이다.


참고