도메인 경계와 영향 범위 CI: 작은 변경을 작게 검증하기
업무의 경계를 먼저 긋고, 바뀐 곳과 그에 딸린 곳만 골라 테스트하며, 경로 필터로 건너뛴 테스트가 병합을 막는 함정을 피하는 검증 설계의 뼈대를 정리한 개념 글이다.
고친 내용: 초판
문제: 커질수록 테스트가 무거워진다
저장소가 커지면 모든 테스트를 매번 돌리기가 버거워진다. 다시 테스트하고, 다시 빌드하고, 다시 린트를 도는 일이 쌓여 병합까지의 기다림이 길어진다. 기다림이 길어지면 작은 고침도 미루게 되고, 미룬 고침이 쌓여 병합은 더 무거워진다.
흔한 첫 반응은 테스트를 나누는 일이다. 폴더마다 다른 테스트를 걸어 바뀌지 않은 곳은 건너뛰는 것이다. 발상은 맞지만 마감이 허술하면 다른 종류의 고장이 난다. 건너뛴 테스트가 병합을 멈춰 세우거나, 로컬의 통과가 병합의 통과를 보장하지 못하는 일이 생긴다. 이 글은 나누기의 원리부터 함정까지를 정리한다.
개념: 경계는 폴더가 아니다
마이크로서비스 안내가 강조하는 출발점은 업무 역량 중심의 설계다. 자료 접근이나 메시지 같은 수평 층이 아니라, 장바구니·결제·배송 같은 업무 능력 단위로 나눈다. 나눈 조각은 느슨하게 결합하고 안으로는 응집한다. 느슨한 결합의 잣대는 분명하다. 한 조각을 고칠 때 다른 조각을 함께 고치지 않아도 되면 느슨한 것이다.
도메인 주도 설계의 말로는 경계 컨텍스트다. 하나의 도메인 모형이 통하는 범위가 곧 경계이며, 같은 말도 경계마다 뜻이 달라질 수 있다. 그래서 경계마다 유비쿼터스 언어(공유 언어)를 따로 세우고, 경계 사이의 만남은 컨텍스트 맵으로 적는다. 폴더 나누기는 이 결과물을 담는 그릇일 뿐, 경계 자체가 아니다. 폴더만 나누고 말이 따로 놀면 경계는 그어지지 않은 셈이다.
이 절의 근거 S27

영향 범위 계산: 그래프와 두 커밋
영향 범위 테스트의 재료는 두 가지다. 어느 파일이 바뀌었는지를 아는 버전 기록, 그리고 그 파일이 어느 프로젝트에 속하고 누가 누구에 의존하는지를 아는 프로젝트 그래프다. 바뀐 쪽과 그에 의존하는 쪽을 함께 묶어 최소 집합을 뽑는다. 영향 집합이 작을 때는 그 집합에만 빌드·테스트·린트를 돌려 계산을 아끼고, 집합이 전체에 가까워지면 전부 돌리는 편이 정직하다.
Nx 문서가 적는 운용법은 구체적이다. 영향 명령은 비교의 밑(base)과 끝(head)을 받으며, 로컬의 기본 밑은 main 브랜치다. CI에서는 값을 직접 정해야 하며, 권장되는 밑은 main 브랜치의 최근 성공 커밋이다. 마지막으로 성공한 실행 이후의 바뀜을 전부 품기 위해서다. 이때 전체 기록을 받아야 밑 커밋을 찾을 수 있으므로, 얕은 받기는 금물이다. GitHub에서는 마지막 성공 실행을 찾아주는 도구가 있고, 다른 CI에서는 각자의 변수로 같은 값을 맞춘다.
이 절의 근거 S28
그림을 글로 읽기
여섯 상자를 잇는 흐름도. 1 금액 계산 조각의 바뀜이 결제 도메인과 검색 도메인으로 화살표를 뻗는다. 2 결제 도메인과 검색 도메인이 함께 영향 집합에 든다. 3 화면 문구의 바뀜은 결제 도메인으로만 화살표를 뻗는다. 4 영향 명령이 밑과 끝 두 커밋을 받아 집합을 뽑는다. 이 그림의 상점은 이 글을 위해 지은 가상이다.
경로 필터의 함정: 멈춰 선 테스트
GitHub Actions의 경로 필터는 테스트 나누기의 또 다른 수단이다. push와 pull_request 사건에 paths나 paths-ignore를 걸어 바뀐 경로에 따라 흐름을 돌리거나 말린다. 브랜치 필터와 함께 쓰면 두 조건을 모두 만족할 때만 돈다. 여기까지는 편리한 도구다.
함정은 문서에 또렷이 적혀 있다. 브랜치 필터나 경로 필터, 혹은 커밋 메시지로 흐름이 건너뛰어지면, 그 흐름에 딸린 테스트는 Pending 상태로 남는다. 그리고 그 테스트를 요구 조건으로 건 보호 브랜치는 병합을 막는다. 테스트가 실패해서가 아니라 보고되지 않아서 막히는 것이다. 바뀌지 않은 곳을 테스트하지 않겠다는 선택이, 아무도 병합하지 못하는 멈춤으로 돌아오는 셈이다. 완화책의 선택은 팀의 몫이지만, 함정의 존재 자체는 문서의 서술이다.
이 절의 근거 S29
| 갈래 | 고르는 기준 | 지켜야 할 것 |
|---|---|---|
| 경로 필터 | 바뀐 경로와 가지 조건의 동시 만족 | 건너뛴 검사가 Pending으로 남아 막는지 |
| 영향 범위 | 프로젝트 그래프와 밑·끝 두 커밋 | 집합이 전체에 가까워지면 전부 도는지 |
| 건너뛴 테스트 | 보고되지 않은 검사는 없는 검사와 같다 | 요구 조건과의 짝을 주기적으로 다시 맞추기 |
표를 글로 읽기
세 행의 표. 경로 필터는 바뀐 경로와 가지 조건에 따라 흐름을 돌리거나 말리고, 영향 범위는 프로젝트 그래프와 밑·끝 두 커밋으로 묶음을 뽑으며, 건너뛴 검사는 요구 조건에 걸리면 병합을 막으므로 보고 여부를 따로 지킨다.
캐시: 다시 받기와 다시 쓰기의 사이
깃허브가 주는 실행기(GitHub-hosted runners)는 매번 깨끗한 판에서 시작하므로 의존성을 매번 다시 받는다. 의존성 캐시는 자주 쓰는 파일을 깃허브가 맡아두어 다시 만드는 시간을 아끼는 수단이다. 산출물과는 쓰임이 다르다. 캐시는 실행 사이에 다시 쓰는 재료용이고, 산출물은 실행이 끝난 뒤에 보거나 다음 일에 넘기는 결과물용이다.
캐시의 주의도 분명하다. 되살린 캐시는 있는 그대로 믿지 말고 믿을 수 없는 입력으로 다룬다. 브랜치나 태그 단위로 공유되며, 낮은 신뢰의 계기로 돌린 흐름이 민감한 캐시를 읽을 수 있기 때문이다. 비밀은 캐시에 넣지 않는다. 캐시는 빨라지는 길이지, 테스트를 대신하는 길이 아니다.
이 절의 근거 S30
TDD와의 관계: 빠른 테스트와 넓은 테스트
테스트 주도 개발의 단위 테스트는 가장 빠른 피드백이다. 원문이 적는 셋을 반복한다. 다음에 넣을 기능의 테스트를 먼저 쓰고(레드), 테스트가 통과할 때까지 기능 코드를 쓰며(그린), 새 코드와 묵은 코드를 다듬는다(리팩터). 테스트를 먼저 쓰면 쓰는 쪽의 겉모습(인터페이스)을 먼저 생각하게 되어, 겉모습과 속을 가르는 설계에 힘이 된다. 이 빠른 피드백은 로컬의 한 조각을 다듬는 데 쓰며, 도메인 사이의 약속이 지켜지는지를 증명하지는 않는다.
그래서 층을 나눈다. 단위 테스트는 한 조각 안의 뜻을 보고, 컨텍스트 맵은 경계 사이의 만남과 관계를 적는 지도이며, 약속한 대로 도는지는 통합 테스트와 계약 테스트의 몫이다. 영향 범위 테스트는 바뀜의 파급이 어느 조각에 닿는지를 가린다. 이 층 나누기는 이 글의 편집자 관점이며, 원문들이 직접 한 줄로 적은 서술은 아니다. 로컬의 단위 테스트는 매 고침마다, 영향 범위 테스트는 매 제안마다, 전체 통합 테스트는 정해진 때마다 돈다. 어느 층을 어디에 두는지는 팀의 선택이지만, 층을 없애는 선택은 권장하지 않는다. 특히 공유 조각의 변경은 여러 경계에 파급되므로, 빠른 통과 하나로 병합을 결정하지 않는다.
| 층 | 도는 때 | 묻는 것 |
|---|---|---|
| 로컬 단위 | 매 고침마다 | 조각 안의 뜻이 맞는가 |
| 영향 범위 | 매 제안마다 | 바뀜의 파급이 닿는가 |
| 전체 통합 | 정해진 때 | 합쳐서 도는가 |
| 멈춤 감시 | 주기적으로 | 건너뛴 테스트가 막는가 |
표를 글로 읽기
네 행의 표. 로컬 단위는 매 고침마다 뜻을 보고, 영향 범위는 매 제안마다 파급을 보고, 전체 통합은 정해진 때 합침을 보고, 멈춤 감시는 Pending 병합을 본다.
따라가기: 가상의 상점 저장소
아래는 실행하지 않은 가상의 따라가기다. 어떤 상점 저장소에 결제와 검색 두 도메인이 있고, 두 도메인이 함께 쓰는 금액 계산 조각이 있다고 하자. 금액 계산을 고치면 그래프는 결제와 검색을 함께 영향 집합에 넣는다. 결제 화면의 문구만 고치면 영향 집합은 결제뿐이다. 이 판단은 폴더 이름이 아니라 그래프의 의존 관계가 내린다.
CI에서는 밑을 최근 성공 커밋으로, 끝을 이번 풀 리퀘스트(pull request, PR)의 끝으로 정하고 영향 명령에 테스트 작업을 맡긴다. 셸 발췌는 아래와 같으며, 두 변수는 팀 CI가 준다. 흐름 전체가 아니라 발췌이므로 그대로 붙여 넣으면 돌지 않는다. 브랜치 이름 비교(origin/main 같은)는 손쉬운 출발점이며, main의 최근 성공 커밋을 밑으로 두는 편이 권장되는 운용이다. 경로 필터를 쓰는 흐름이 있다면, 건너뛴 테스트가 보호 브랜치의 요구 조건에 걸려 있는지 먼저 확인한다. 분 단위 단축 수치는 이 글에 없다. 재는 법은 정해져 있다. 같은 제안 뭉치를 전후로 견주어 기다림과 계산량을 견주는 것이다.
# 팀 CI가 두 변수를 준다. 전체 흐름이 아니라 발췌이다.
: "${NX_BASE:?set NX_BASE to last successful main commit SHA}"
: "${NX_HEAD:?set NX_HEAD to PR head SHA}"
nx affected -t test --base="$NX_BASE" --head="$NX_HEAD"실패 양상과 한계
영향 테스트가 놓치는 자리는 그래프 바깥이다. 그래프에 잡히지 않는 동적 불러오기와 손으로 돌리는 배포 각본, 문서만으로 남은 약속은 바뀌어도 집합에 들지 않는다. 공유 조각의 변경도 한계다. 집합이 넓어져 절약이 사라지는 때가 있으며, 그때는 전부 돌리는 편이 정직하다.
밑 커밋의 설정 오류도 흔한 실패다. 밑이 필요보다 이르면(너무 앞선 기준) 그 뒤의 바뀜을 전부 품어 집합이 부풀고, 변경 뒤의 너무 늦은 기준으로 두면 그 앞의 바뀜을 빠뜨린다. 얕은 받기로 밑을 잃는 일도 같은 부류다. 그리고 로컬 통과는 병합 통과가 아니다. 로컬에는 없는 경로 필터와 요구 조건이 병합 앞에는 있다. 이 간극을 잊으면 통과한 줄 알았던 제안이 문 앞에서 멈춘다.
- 집합이 부풀면 밑을 당긴다
필요보다 이른 밑은 그 뒤의 바뀜을 전부 품는다. 밑을 main 가지의 최근 성공 커밋으로 당긴다.
- 바뀜을 빠뜨리면 밑을 앞당긴다
변경 뒤의 너무 늦은 기준은 그 앞의 바뀜을 빠뜨린다. 마지막 성공 실행 이후의 바뀜을 전부 품게 밑을 둔다.
- 밑을 못 찾으면 기록을 받는다
얕은 받기로는 밑 커밋을 찾을 수 없다. 전체 기록을 받아 밑을 찾고, GitHub에서는 성공 실행 찾기 도구를 쓴다.
단계를 글로 읽기
세 단계 순서. 집합이 부풀면 밑이 필요보다 이른지 보고 최근 성공 커밋으로 당긴다. 바뀜을 빠뜨리면 변경 뒤의 너무 늦은 기준인지 보고 밑을 앞당긴다. 밑을 못 찾으면 얕은 받기인지 보고 전체 기록을 받는다.
측정과 검증 계획
도입하는 팀은 다음을 잰다. 병합까지의 기다림, 실행당 계산량, 건너뛴 테스트의 보고 여부, 그리고 놓친 파급의 수다. 특히 놓친 파급, 즉 영향 집합 밖에 있던 고장은 따로 센다. 그 수가 늘면 그래프가 거짓말을 하는 것이므로 경계와 그래프를 손본다.
검증의 또 다른 축은 멈춤의 감시다. Pending으로 멈춘 병합이 있는지를 주기적으로 살핀다. 멈춤이 반복되면 경로 필터와 요구 조건의 짝을 다시 맞춘다. 테스트를 나누는 일의 끝은 나누는 일이 아니라, 나눈 뒤에도 병합이 멈추지 않게 지키는 일이다.
결론: 경계를 긋고, 범위를 재고, 멈춤을 본다
이 글의 원칙은 세 마디다. 업무의 경계를 말과 모형으로 먼저 긋고, 바뀜의 영향 범위를 그래프와 두 커밋으로 계산하고, 건너뛴 테스트가 병합을 막지 않는지 지킨다. 빠른 로컬 테스트는 이 위에 얹는 피드백이다.
어떤 도구를 쓰고 밑을 어디에 둘지는 팀의 선택이다. 서가가 줄 수 있는 것은 선택지가 아니라 선택의 기준뿐이다. 병합 앞의 문지기는 검토 게이트 글에, 밤사이 도는 테스트는 예약 실행 글에 있으니 함께 읽기를 권한다.
현재 판 출처 (이 개정의 출처 보관본 아님) 5건
아래는 현재 판의 출처 목록을 고리 풀이용으로 그대로 둔 참조이며, 이 개정의 출처 보관본이 아니다.