한 줄 정의
스택 테스트는 빠른 오프라인 검사에서 온라인 검증으로 넓혀가는 파이프라인을 만들고, 테스트 픽스처 로 다른 스택 없이도 스택 하나를 홀로 테스트할 수 있게 하는 활동입니다.
쉽게 말하면
변속기 공장에서 변속기 하나를 검사할 때 자동차 한 대를 통째로 조립하지 않습니다. 엔진 역할을 하는 시험용 모터와 바퀴 역할을 하는 부하 장치를 물려서 돌려봅니다. 이 시험 장비가 테스트 픽스처입니다.
그리고 모터에 물리기 전에 도면과 치수부터 검사합니다. 이게 오프라인 테스트입니다.
시험대를 매번 새로 조립할지, 조립된 채로 계속 쓸지는 검사 속도와 깨끗한 환경 사이의 선택입니다. 이게 테스트 인스턴스의 생명 주기 패턴입니다.
왜 중요한가?
온라인 스택 테스트는 느립니다. 의존하는 다른 스택까지 프로비저닝하면 더 느려지고, 공유 인스턴스에 의존하면 신뢰성도 떨어집니다. 테스트 픽스처가 없으면 복잡한 테스트 인프라 모음을 만들고 유지보수해야 합니다.
테스트 인스턴스의 생명 주기를 관리하지 않으면, 실패한 변경이 남긴 손상된 인스턴스를 수동으로 제거하는 데 팀의 시간이 계속 새어 나갑니다.
반대로 픽스처로 의존성을 분리하면 스택 간 느슨한 결합이 유지되고, 공급자 스택 프로젝트가 바뀌어도 소비자 스택의 작업에 영향을 주지 않습니다.
핵심 내용
예제 스택과 파이프라인
ShopSpinner는 고객별 애플리케이션 인프라(앱 서버 VM, 네트워킹, DBaaS 데이터베이스)를 재사용 가능한 하나의 스택으로 정의합니다. 공유 웹 서버 컨테이너 클러스터는 스택 밖에 있는 의존성입니다.
파이프라인은 스택 빌드 스테이지(오프라인 테스트) → 온라인 스택 테스트 단계 → 고객별 프로덕션 단계 순서입니다. 초기 단계일수록 빠른 실행이 필요하므로 첫 단계는 오프라인 테스트로 실행합니다.
오프라인 테스트 — 플랫폼 없이 빠르게
오프라인 테스트는 인프라를 프로비저닝하지 않고 에이전트 노드에서 로컬로 실행됩니다. 엄격한 오프라인 테스트는 외부 서비스에 아예 연결하지 않고, 완화된 오프라인 테스트는 클라우드 API에 연결은 하되 실제 스택 인프라를 사용하지 않습니다.
목표는 세 가지입니다. 빠른 피드백, 분리된 컴포넌트의 정확성 검증, 컴포넌트가 완전히 분리되었다는 증명.
| 유형 | 확인하는 것 | 도구 예 | 한계 |
|---|---|---|---|
| 구문 검사 | 오타·구문 오류 (예: virtual_mahcine 오타) | terraform validate, aws cloudformation validate-template | 구문 외 오류는 누락 |
| 오프라인 정적 분석(린트) | 코딩 스타일, 정책 준수, 잠재 보안 문제 (예: Name 태그 누락 규칙) | TFLint, CloudFormation Linter, cfn_nag, tfsec, checkov | 플랫폼과의 충돌은 못 봄 |
| API를 사용한 정적 분석 | 코드가 참조하는 인스턴스 유형·서버 이미지가 플랫폼에 실제 존재하는지 | TFLint (일부 검사) | 특정 스택 인스턴스가 아니라 코드를 검사 |
| 모의 API 테스트 | 로컬 모의 플랫폼에 적용해 구문·정적 분석이 못 잡는 오류 발견 | Localstack, Azurite | 선언형 코드에는 가치가 낮음. 명령형 코드·라이브러리 단위 테스트에 유용 |
온라인 테스트 — 인스턴스를 만들어 검증
온라인 단계는 인프라 플랫폼으로 스택 인스턴스를 만들고 상호작용합니다. 느리지만 오프라인보다 의미 있는 테스트입니다.
핵심 설계 원칙은 다른 스택의 인스턴스와 통합할 필요 없이 스택 인스턴스를 만들고 테스트할 수 있도록 인프라·스택·테스트를 설계하는 것입니다.
온라인 테스트는 변경 프리뷰, 변경 적용 확인(어서션), 결과 증명 세 가지를 포함합니다.
프리뷰 — 변경 미리보기
Terraform의 plan처럼 스택 코드를 인스턴스와 비교해 변경을 적용 없이 나열합니다. 보통 프로덕션 적용 전 안전 조치로 사람이 검토하지만, 사람의 검토는 신뢰하기 어렵습니다. 문제 있는 변경을 오해하거나 알아차리지 못하기 때문입니다.
대신 프리뷰 출력을 확인하는 자동화된 테스트를 작성할 수 있습니다. 사용되지 않는 리소스 유형을 생성하거나 데이터베이스 인스턴스를 리빌드·삭제하는 코드가 있으면 실패하게 만듭니다.
프리뷰는 꼼꼼하지 않다
프리뷰는 “신규 서버를 생성한다”는 알려주지만,
base_image로 지정한 서버 이미지가 존재하지 않아apply가 실패할 것이라는 사실은 알려주지 않을 수 있습니다. 프리뷰는 제한된 위험 확인용이지 코드 검증 수단이 아닙니다.
검증 — 인프라 리소스에 대한 어서션
Awspec, Clarity, Inspec, Taskcat, Terratest 같은 프레임워크로 스택 인스턴스의 리소스에 어서션을 겁니다.
given virtual_machine(name: "appserver-testcustomerA-staging") {
it { exists }
it { is_running }
it { passes_healthcheck }
it { has_attached storage_volume(name: "app-storage-testcustomerA-staging") }
}
어서션마다 가치가 다릅니다. 어디에 걸어야 하는지가 이 절의 핵심입니다.
| 어서션 | 가치 |
|---|---|
exists 같은 기본 어서션 | 코드 되읽기라 낮지만, 적용 성공 여부와 파이프라인·테스트 설정의 기본 문제를 빠르게 식별 |
is_running, passes_healthcheck | 스택 도구는 성공했지만 VM이 충돌한 경우 같은 근본 문제를 알려 디버깅 시간 단축 |
| RAM 크기 등 구성 항목 되읽기 | 가치가 거의 없고 오버헤드만 추가 |
has_attached storage_volume() 같은 조합 검증 | 여러 선언의 조합 이 올바르게 작동하는지 확인. 적용은 성공해도 서버-볼륨 연결이 깨진 상태일 수 있음 |
| 동적 코드의 결과 어서션 | network_access 파라미터에 따라 public/internal 주소 블록이 갈리는 것처럼, 파라미터별로 결과가 달라지는 지점 검증 |
동적 분기가 복잡해지면 모듈이나 라이브러리로 이동시키고 스택 코드와 별도로 테스트합니다. 그러면 스택 코드 테스트가 다시 단순해집니다.
결과 — 인프라가 제대로 작동하는지 입증하기
가장 가치 있는 테스트는 생성된 리소스가 해야 할 일을 한다 는 증명입니다. 웹 세그먼트에서 애플리케이션 세그먼트로 네트워크가 연결되는가, 클러스터에 애플리케이션을 배포·실행할 수 있는가, 서버 리빌드 시 스토리지 볼륨을 안전하게 재연결할 수 있는가, 로드 밸런서가 인스턴스 추가·제거를 처리하는가.
given stack_instance(stack: "shopspinner_networking", instance: "online_test") {
can_connect(ip_address: stack_instance.appserver_ip_address, port:443)
http_request(ip_address: stack_instance.appserver_ip_address,
port:443, url: '/').response.code is('200')
}
테스트 픽스처로 의존성 분리
테스트 픽스처 는 다른 스택의 인스턴스 없이 스택을 홀로 프로비저닝·테스트할 수 있도록 특별히 생성하는 인프라 리소스입니다. 테스트 더블은 픽스처의 한 유형입니다. 픽스처는 테스트 중인 스택의 일부가 아니라, 스택의 의존성을 대신하는 추가 인프라입니다.
의존성에는 방향이 있습니다. 리소스를 제공하는 스택이 공급자(provider), 다른 스택의 리소스를 사용하는 스택이 소비자(consumer) 입니다.
| 방향 | 테스트 대상 | 픽스처의 역할 | 예 |
|---|---|---|---|
| 업스트림 의존성 (테스트 더블) | 소비자 스택 (애플리케이션 스택) | 공급자가 제공할 리소스를 테스트 구성이 직접 생성 | 네트워크 스택 인스턴스 전체 대신 주소 블록만 생성 |
| 다운스트림 의존성 (테스트 픽스처) | 공급자 스택 (네트워킹 스택) | 리소스를 사용할 소비자 역할의 리소스 배치 | 각 네트워크 세그먼트에 컨테이너 인스턴스 한 쌍을 두고 can_connect(from: ..., to: ...)로 연결 검증 |
업스트림 더블이 전체 공급자 스택 인스턴스보다 나은 이유는 두 가지입니다.
첫째, 네트워크 스택에는 테스트에 필요 없는 프로덕션용 과잉 인프라(네트워크 정책, 감사 등)가 포함될 수 있습니다.
둘째, 소비자 스택이 공급자 스택에서 분리(decoupling) 됩니다. 누군가 네트워킹 스택 프로젝트를 변경해도 애플리케이션 스택 작업에 영향이 없고, PCI 준수용·비준수용 네트워킹 스택을 따로 만들어도 애플리케이션 스택은 그와 무관하게 테스트할 수 있어 재사용과 조립이 쉬워집니다.
분리를 위한 리팩터링
독립적으로 테스트하기 어려운 컴포넌트는 설계에 문제가 있다는 신호입니다. 잘 설계된 시스템은 컴포넌트가 느슨하게 결합되어 있습니다. 의존성이 하드코딩되어 있다면 스트랭글러 애플리케이션 같은 리팩터링 전략으로 설계 자체를 수정해야 합니다.
테스트 인스턴스의 생명 주기 패턴
| 패턴 | 방식 | 동기 | 함정 |
|---|---|---|---|
| 영구 테스트 스택 | 항상 실행 중인 인스턴스에 변경만 적용 | 신규 생성보다 훨씬 빨라 파이프라인 전체의 피드백이 빠름 | 실패 후 결함 있는 인스턴스가 남고, 수동 제거에 시간을 소비 |
| 임시 테스트 스택 | 테스트 단계마다 인스턴스 생성 → 테스트 → 삭제 | 매번 깨끗한 환경. 이전 실행의 데이터·픽스처·결함 위험 없음 | 프로비저닝이 느려 피드백·딜리버리 주기가 느려짐 |
| 이중 스택 단계 (안티패턴) | 각 변경을 영구·임시 두 단계로 동시에 보냄 | 두 패턴의 단점을 서로 상쇄하려는 시도 | 단점도 결합됨. 수동 수정 시간 + 느린 단계 대기 + 리소스 2배 비용. 과도기 솔루션으로만 |
| 주기적인 스택 리빌드 | 영구 인스턴스를 일정에 따라 삭제·리빌드 (퇴근 시 삭제, 아침 생성) | 비용 절감, 테스트 반복으로 누적되는 메모리·스토리지 사용량 해소 | 근본 문제·설계 문제를 감추는 임시방편이 될 수 있음. 다른 시간대 작업자에게 환경이 없을 수 있음 |
| 지속적인 스택 재설정 | 테스트 단계가 완료될 때마다 백그라운드에서 삭제·리빌드 | 매번 새 환경이면서 생성·삭제 시간이 피드백 주기 밖. 프로덕션 코드 버전에서의 업데이트 프로세스도 테스트 가능 | 백그라운드 실패가 눈에 안 보임. 다음 변경이 실패했을 때 원인이 리빌드 작업임을 깨닫는 데 시간이 걸림 |
임시 테스트 스택의 “느림”의 기준은 팀이 필요로 하는 피드백 주기입니다. 커밋마다 발생하는 빈번한 변경에는 부적합하지만, OS 패치처럼 덜 빈번한 변경은 완전한 리빌드로 테스트할 수 있습니다. 실패를 디버깅할 수 있도록 테스트가 실패하면 삭제 전에 중지하게 단계를 구성할 수 있습니다.
지속적인 스택 재설정에서 리빌드에 쓸 코드 버전은 두 가지 중 선택합니다. 방금 단계를 통과한 버전을 쓰거나, 프로덕션에 적용된 마지막 버전을 가져옵니다. 후자는 각 버전이 현재 프로덕션 버전에서 업데이트되며 테스트되므로 프로덕션 업그레이드 프로세스를 더 정확하게 표현합니다.
테스트 오케스트레이션
픽스처 생성, 테스트 데이터 로딩, 인스턴스 생명 주기 관리, 도구 파라미터 전달·실행, 결과 통합, 정리를 조합하는 일입니다. 대부분의 팀은 스택 관리 스크립트와 유사한 스크립트(bash, Make, Rake, Python 등)로 조정합니다. 전용 도구로 Test Kitchen(Chef 계열)과 Molecule(Ansible용)이 있지만 특정 워크플로를 전제해 수정이 어렵습니다.
지켜야 할 원칙은 두 가지입니다.
- 로컬 테스트 지원: 코드를 공유 파이프라인에 푸시하기 전에 작업자가 직접 테스트를 실행할 수 있어야 합니다. 핵심은 로컬 작업과 파이프라인 단계가 동일한 오케스트레이션 스크립트 를 쓰는 것입니다.
- 파이프라인 도구에 대한 결합 회피: CI 도구의 테스트 기능·플러그인에 의존하면 파이프라인 외부에서 일관되게 실행하기 어려워집니다. 오케스트레이션은 별도 스크립트로 구현하고, 테스트 단계는 최소한의 파라미터만 넘겨 호출합니다.
비교 / 트레이드오프
| 축 | 한쪽 | 다른 쪽 | 기준 |
|---|---|---|---|
| 프리뷰 vs 테스트 인스턴스 | 프리뷰: 적용 직전 제한된 위험 확인 | 테스트 인스턴스: 의미 있는 코드 검증 | 복사-붙여넣기 환경은 프리뷰가 환경별 최소 테스트, 재사용 가능한 스택은 테스트 인스턴스가 필요 |
| 픽스처 vs 공급자 스택 인스턴스 | 픽스처(주소 블록만): 빠르고 공급자에서 분리 | 전체 인스턴스: 실제에 가깝지만 과잉 인프라 포함 | 테스트에 필요한 최소한만 생성 |
| 단순 어서션 vs 결과 테스트 | 어서션: 기본 문제의 빠른 식별 | 결과 테스트: 인프라가 할 일을 한다는 증명 | 어서션은 스모크 수준으로 최소화하고, 투자의 중심은 결과 테스트 |
내 생각
- 어서션 가치의 그라데이션이 Ch08 “받아쓰기 금지”의 실전 보정입니다. 선언마다 어서션을 거는 게 문제지,
exists·is_running수준의 스모크 어서션은 디버깅 시간을 줄이는 장치라 되레 유용합니다. 걸 곳은 조합과 동적 분기입니다. - 테스트 픽스처는 설계 품질의 리트머스입니다. 픽스처로 분리가 안 되는 스택은 의존성이 하드코딩됐다는 뜻입니다. 애플리케이션 코드에서 mock을 못 끼우는 클래스가 설계 문제인 것과 같은 신호입니다.
- 지속적인 스택 재설정의 백그라운드 실패가 실무에서 가장 아픈 함정입니다. 파이프라인은 그린인데 뒤에서 인스턴스가 깨져 있으면, 다음 변경의 실패를 엉뚱한 곳에서 디버깅하게 됩니다. 백그라운드 리빌드 작업에도 알림·모니터링이 필요합니다.
- 오케스트레이션을 CI 도구 밖 스크립트로 빼라는 원칙은 애플리케이션 CI와 동일합니다. Jenkins 파이프라인에 로직을 심지 않고 Makefile·Gradle 태스크를 호출하게 얇게 유지하는 것과 같은 이유로, 로컬과 파이프라인의 실행 일관성이 목적입니다.
관련 개념
- Ch05 코드로 인프라 스택 구축하기 — 테스트 대상 단위인 스택의 정의
- Ch06 스택으로 환경 구축하기 — 재사용 가능한 스택 vs 복사-붙여넣기 환경, 프리뷰 활용도가 갈리는 지점
- Ch07 스택 인스턴스 구성하기 — 테스트 인스턴스에 값을 전달하는 스택 파라미터
- Ch08 코드를 지속적으로 테스트하고 딜리버리한다 — 프로그레시브 테스트, 오프라인/온라인 구분, 테스트 더블의 이론 배경