한 줄 정의
시스템을 여러 스택으로 나눴을 때, 소비자 스택이 공급자 스택의 리소스를 어떤 방식으로 찾아내느냐가 두 스택의 결합도를 결정합니다.
쉽게 말하면
친구 집에 찾아가려면 주소를 알아야 합니다. 주소를 수첩에 그대로 적어 두면 친구가 이사하는 순간 못 찾아가고, 친구는 이사할 때마다 내 수첩까지 신경 써야 합니다.
그래서 대안이 나옵니다. “OO아파트 101동”처럼 이름 규칙으로 찾거나, 친구 회사의 인사 시스템을 열어 보거나, 둘 다 아는 공용 주소록에 적어 두거나. 셋 다 하드코딩보다는 낫지만 내가 무엇에 묶이는지는 다릅니다. 이름 규칙에 묶이느냐, 그 회사 시스템에 묶이느냐, 주소록 서비스에 묶이느냐의 차이입니다.
마지막 선택지는 아예 내가 찾지 않는 것입니다. 누군가 주소를 손에 쥐여 주면 나는 “받은 주소로 간다”만 알면 됩니다. 이것이 의존성 주입입니다.
왜 중요한가?
스택은 독립적으로 정의·프로비저닝·변경할 수 있는 가장 큰 단위입니다. 작은 스택으로 구성된 인프라가 큰 스택 하나보다 민첩한 이유는 단순합니다. 변경 범위가 작으면 더 빠르고 안전하게 바꿀 수 있고, 그래서 품질이 올라가고, 품질이 올라가니 더 자주 바꿀 수 있는 선순환이 돕니다.
그런데 나누는 순간 스택 간 통합이 필요해집니다. 문제는 이 통합에 널리 쓰이는 기술들이 대부분 긴밀한 결합을 만든다 는 점입니다. 가장 흔한 형태가 하드코딩입니다.
# shared-network-stack이 선언한 VLAN
vlan:
name: "appserver_vlan"
# application-infrastructure-stack이 이름을 그대로 참조
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: "appserver_vlan"
이러면 네트워킹 스택 인스턴스 없이는 애플리케이션 스택 코드 변경을 테스트할 수 없고, 반대로 네트워킹 스택은 이 이름을 쓰는 모든 스택에 발이 묶입니다. 복원력을 위해 VLAN을 늘리기로 했다면 양쪽을 동시에 바꿔야 하고, 그게 부담스러워 appserver_vlan, appserver_vlan_2처럼 원래 이름을 남겨 두면 이번엔 코드와 실제 인프라를 이해하기 어려워집니다.
스택을 나눴는데 함께 배포해야만 한다면, 작은 스택으로 얻으려던 이점이 사라집니다.
핵심 내용
의존성 검색 패턴 세 가지
세 패턴 모두 하드코딩을 없앤다는 목표는 같지만, 소비자 스택이 무엇에 묶이는지가 다릅니다.
flowchart LR P[공급자 스택<br/>shared-networking-stack] C[소비자 스택<br/>application-infrastructure-stack] P --> RES[인프라 리소스<br/>이름·태그 부착] P --> SD[(스택 상태 데이터<br/>export)] P --> REG[(구성 레지스트리<br/>약속된 경로)] C -. 리소스 매칭 .-> RES C -. 스택 데이터 조회 .-> SD C -. 통합 레지스트리 조회 .-> REG
리소스 매칭 — 이름·태그로 인프라를 직접 검색
소비자 스택이 이름, 태그, 그 밖의 식별 가능한 특성으로 인프라 리소스를 찾습니다. 가장 단순한 구현은 리소스 이름에 변수를 쓰는 것입니다.
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: "vlan-appserver-${ENVIRONMENT_NAME}"
다만 이름은 환경마다 다를 수 있으므로, 실무에서는 태그 매칭이 더 견고합니다. Terraform의 데이터 소스, AWS CDK의 리소스 가져오기가 이 역할을 합니다.
# 공급자
vlans:
- appserver_vlan
tags:
network_tier: "application_servers"
environment: ${ENVIRONMENT_NAME}
# 소비자
external_resource:
id: appserver_vlan
match:
tag: name == "network_tier" && value == "application_servers"
tag: name == "environment" && value == ${ENVIRONMENT_NAME}
이 패턴의 진짜 가치는 도구 결합을 만들지 않는다 는 데 있습니다. 공급자 인프라와 소비자 스택을 서로 다른 도구로 구현해도 되므로, 팀마다 다른 도구를 쓰지만 인프라 레벨에서는 통합해야 하는 대규모 조직에 잘 맞습니다. 단일 도구를 쓰는 조직에서도 도구 종속을 줄여 나중에 일부만 다른 도구로 옮길 여지를 남깁니다.
대신 이름·태그 규칙 자체가 암묵적 계약이 됩니다. 양쪽 코드를 같은 팀이 관리해 어떤 리소스가 의존 대상인지 명확히 알고 있을 때 안전하고, 팀 경계를 넘어 이 규칙이 깨지기 시작하면 다른 패턴으로 옮겨야 합니다.
스택 데이터 조회 — 공급자 도구의 상태 데이터를 읽기
원격 상태 파일 조회, 스택 참조 조회, 스택 리소스 조회라고도 부릅니다. 스택 관리 도구가 유지하는 데이터 구조에서 공급자 리소스를 찾는 방식입니다. Terraform은 원격 상태 파일에 출력값을 저장하고, Pulumi는 StackReference, CloudFormation은 스택 출력값 export/import를 씁니다.
# 공급자: 제공할 리소스를 명시적으로 선언
stack:
name: shared_network_stack
export:
- appserver_vlan_id: appserver_vlan.id
# 소비자: 공급자 스택 참조를 선언하고 export된 값을 사용
external_stack:
name: shared_network_stack
environment: ${ENVIRONMENT_NAME}
virtual_machine:
vlan: external_stack.shared_network_stack.appserver_vlan.id
공급자가 제공할 값을 명시적으로 선언한다 는 점이 리소스 매칭과의 차이입니다. 소비자가 공급자도 모르는 리소스에 종속되는 사고를 막아 줍니다.
문제는 단일 스택 관리 도구에 종속된다는 것입니다. 특히 같은 도구의 버전 차이에서 자주 깨집니다. 도구 업그레이드로 스택 데이터 구조가 바뀌면, 공급자 스택을 먼저 올렸을 때 아직 구버전인 소비자 스택이 값을 읽지 못합니다. 결국 스택 단위로 점진적 롤아웃을 할 수 없고 시스템 전체를 한 번에 업그레이드해야 합니다.
통합 레지스트리 조회 — 약속된 위치를 통해 주고받기
두 스택 모두 구성 레지스트리의 알려진 경로를 참조합니다. 공급자가 값을 쓰고 소비자가 읽습니다.
# 공급자
registry:
host: registry.shopspinner.xyz
set:
/${ENVIRONMENT_NAME}/shared-networking/appserver_vlan: appserver_vlan.id
# 소비자
registry:
id: stack_registry
host: registry.shopspinner.xyz
values:
appserver_vlan_id: /${ENVIRONMENT_NAME}/shared-networking/appserver_vlan
virtual_machine:
vlan: stack_registry.appserver_vlan_id
레지스트리를 사이에 끼우면 스택 관리 도구가 서로 분리됩니다. 같은 레지스트리 서비스와 네이밍 규칙만 합의하면 팀마다 다른 도구를 써도 되고, 한 번에 한 스택씩 도구를 업그레이드할 수 있습니다. 통합점이 레지스트리 경로로 명시되므로 공급자팀은 그 값만 유지하면 리소스 구현 방법을 자유롭게 바꿀 수 있습니다.
구현의 관건은 코드가 아니라 네이밍 규칙 입니다. 레지스트리 제품이 단순 키-값이더라도 디렉터리 같은 계층적 네임스페이스를 설계해야 합니다. 보통 아키텍처 단위(서비스·애플리케이션·아티팩트), 환경, 지리, 팀이 경로 요소로 들어갑니다.
/infrastructure/
├── au/
│ ├── shared-networking/
│ │ └── appserver_vlan=
│ └── application-infrastructure/
│ └── appserver_ip_address=
└── eu/
└── ...
레지스트리가 단일 장애점이 됩니다
이 패턴을 채택하면 구성 레지스트리는 인프라의 핵심 서비스가 됩니다. 레지스트리를 쓸 수 없으면 리소스를 프로비저닝하지도, 장애에서 복구하지도 못합니다.
파라미터 레지스트리와 무엇이 다른가
스택 인스턴스에 구성값을 넘기는 파라미터 레지스트리 패턴과 메커니즘은 본질적으로 같습니다. 차이는 목적에 있습니다. 파라미터 레지스트리는 스택 인스턴스가 쓸 설정값을 가져오는 것이고, 통합 레지스트리 조회는 스택 간 인프라 리소스를 통합하려고 명시적으로 값을 주고받는 것입니다. 이미 파라미터 레지스트리를 쓰고 있다면 같은 레지스트리를 통합에도 쓰는 편이 합리적입니다.
의존성 주입 — 검색을 스택 밖으로 빼기
앞의 세 패턴은 모두 소비자 스택 코드 안에서 의존성을 찾습니다. 대부분의 스택 관리 도구가 이를 직접 지원하지만, 리소스를 정의하는 코드와 통합할 리소스를 검색하는 코드는 분리해야 한다는 주장이 있습니다.
리소스 매칭 예제를 다시 보면, 필수 부분은 가상 머신 선언 한 줄이고 나머지 external_resource 블록은 전부 그 값을 조달하기 위한 구현 세부사항입니다. 이렇게 섞였을 때 생기는 문제는 세 가지입니다.
| 문제 | 내용 |
|---|---|
| 인지 오버헤드 | 코드를 읽을 때마다 정의와 검색을 분리해서 봐야 하는 미묘한 마찰이 생깁니다 |
| 메커니즘과의 결합 | 스택이 특정 검색 메커니즘(공급자 스택, 구성 레지스트리)에 묶여 인스턴스를 만들고 테스트하기 어려워집니다 |
| 재사용성 저하 | 검색 방식을 하드코딩하면 다른 팀이 다른 방법으로 의존성을 관리하거나 공급자 스택을 교체할 수 없습니다 |
특히 두 번째가 큽니다. 임시 인스턴스나 테스트 더블로 빠르고 빈번한 테스트를 하려던 접근이, 의존성 설정에 너무 많은 작업과 시간이 필요해지면서 무력해집니다.
해법은 스택이 스스로 발견하지 않고 받는 것입니다. 스택 프로젝트는 의존하는 리소스를 인스턴스 구성 파라미터와 동일한 방식으로 선언하고, 오케스트레이션 스크립트가 검색해서 전달합니다.
parameters:
- ENVIRONMENT_NAME
- VLAN
virtual_machine:
name: "appserver-${ENVIRONMENT_NAME}"
vlan: ${VLAN}
#!/usr/bin/env bash
ENVIRONMENT_NAME=$1
VLAN_ID=$(
stack value \
--stack_instance shared_network-${ENVIRONMENT_NAME} \
--export_name appserver_vlan_id
)
stack apply \
--stack_instance application_infrastructure-${ENVIRONMENT_NAME} \
--parameter application_server_vlan=${VLAN_ID}스택 정의 코드에는 검색 흔적이 하나도 남지 않습니다. 검색 방식은 스크립트의 관심사이므로 리소스 매칭이든 레지스트리든 자유롭게 바꿀 수 있고, 노트북에서는 원하는 VLAN 값을 직접 넘겨 로컬 모의 API나 개인 인프라 인스턴스에 적용해 볼 수 있습니다.
파이프라인 관점의 이점도 분명합니다. 초기 단계에서는 공급자 구현을 교체해 소비자 컴포넌트만 빠르게 개별 테스트하고, 이후 단계에서 실제 통합 시스템을 테스트하는 프로그레시브 테스트를 자연스럽게 구성할 수 있습니다.
비교 / 트레이드오프
| 리소스 매칭 | 스택 데이터 조회 | 통합 레지스트리 조회 | |
|---|---|---|---|
| 소비자가 읽는 대상 | 인프라 플랫폼의 리소스 | 공급자 도구의 상태 데이터 | 구성 레지스트리의 경로 |
| 무엇에 묶이는가 | 이름·태그 규칙 | 스택 관리 도구와 그 버전 | 레지스트리 서비스와 네이밍 규칙 |
| 통합점의 명시성 | 암묵적(규칙으로만 합의) | 명시적(export 선언) | 명시적(레지스트리 경로) |
| 도구 이질성 | 공급자·소비자가 달라도 됨 | 전부 동일 도구여야 함 | 달라도 됨 |
| 점진적 도구 업그레이드 | 가능 | 어려움 | 가능 |
| 잘 맞는 상황 | 양쪽 코드를 같은 팀이 관리 | 전 인프라가 단일 도구 | 팀별 기술이 다른 대규모 조직 |
| 주된 리스크 | 팀 경계를 넘으면 규칙이 깨짐 | 도구 버전 차이로 전체 동시 업그레이드 강제 | 레지스트리가 단일 장애점 |
의존성 주입은 이 세 패턴의 네 번째 대안이 아니라 직교하는 축 입니다. 검색 자체는 여전히 세 방법 중 하나로 하되, 그 코드를 스택 정의 밖으로 옮기는 것이기 때문입니다.
내 생각
- 하드코딩 이름 참조는 “함께 배포해야 하는 스택”을 만듭니다. 스택을 쪼갠 뒤에도 릴리스 순서를 조율하고 있다면 분할이 실패한 것이고, 원인은 대개 이름 하드코딩입니다.
- Terraform
remote_state대신data소스 태그 매칭을 권하는 흐름이 이 장의 결론과 같습니다. 원격 상태 직접 참조는 상태 파일 읽기 권한까지 넘겨 주는 문제도 있어서, 태그 매칭이나 SSM 파라미터 스토어 경유가 실무 기본값이 되어 가고 있습니다. - “레지스트리가 단일 장애점”은 실제로 겪으면 뼈아픕니다. 장애 복구 중에 레지스트리부터 살려야 프로비저닝이 도는 구조가 되므로, 레지스트리 가용성을 인프라 최상위 SLO로 놓고 관리해야 합니다.
- DI는 백엔드 개발자에게 가장 익숙한 개념인데 인프라에서는 오히려 덜 쓰입니다. 스프링에서
new를 직접 부르지 않는 이유와 스택 코드에서remote_state를 직접 부르지 않는 이유는 똑같이 “테스트 가능성”입니다. - 다만 DI는 오케스트레이션 스크립트라는 새 자산을 만듭니다. 스택 코드는 깨끗해지지만 배선 지식이 셸 스크립트나 파이프라인 정의로 옮겨 갈 뿐이므로, 그 스크립트도 버전 관리·테스트 대상으로 다뤄야 합니다.
관련 개념
- Ch05 코드로 인프라 스택 구축하기 — 스택의 크기와 재사용 가능한 스택 패턴
- Ch06 스택으로 환경 구축하기 — 환경별 스택 인스턴스와
ENVIRONMENT_NAME - Ch07 스택 인스턴스 구성하기 — 파라미터 레지스트리 패턴과 구성 레지스트리
- Ch09 인프라 스택 테스트하기 — 테스트 더블·임시 인스턴스와 프로그레시브 테스트
- Ch15 시스템을 작고 간단하게 빌드한다 — 응집력과 느슨한 결합의 설계 기준
- Ch16 컴포넌트에서 스택 빌드하기 — 스택 내부를 모듈·라이브러리로 나누는 문제