PR 한 줄에서 시작되는 서비스 토폴로지 - 2부
2부작 중 2부입니다. 1부에서는 PR 코드리뷰부터 API 명세서 자동화까지를 다뤘습니다. 2부에서는 그 명세를 연료로 매 머지마다 갱신되는 1,852개 EP의 서비스 토폴로지를 이야기합니다.
1부에서 이어집니다
1부에서 PR 한 줄이 머지되면 60초 안에 Claude가 리뷰를 시작하고, API가 바뀌면 명세가 자동으로 따라오는 흐름을 살펴봤습니다. 3주 만에 MUST 항목이 46% 줄었고, 2,558개의 API 명세가 사람의 수작업 없이 유지되고 있다는 결과까지였습니다.
여기까지만 해도 충분한 자동화처럼 보입니다. 하지만 저희는 한 단계 더 들어갔습니다. 명세서는 사람이 읽는 부산물이고, 진짜 자산은 그 옆에 만들어진 한 개의 JSON 파일이라는 것이 이 2부의 주제입니다.
진짜 자산은 명세서가 아니라 그 옆에 만들어진 그래프입니다
자동화의 결과물 중 사람들의 시선이 가장 먼저 닿는 것은 명세 페이지입니다. 하지만 저희가 진짜 자산이라고 생각하는 것은 따로 있습니다. 매 PR 머지마다 갱신되는 한 개의 JSON 파일, ep-impact-map.json입니다.
이 파일 안에는 1,852개 엔드포인트가 각각 자기와 연결된 모든 호출 관계를 들고 있습니다. 어떤 서비스를 호출하는지(callees), 누가 자기를 호출하는지(callers), 어떤 인프라에 의존하는지(infra) — 한 EP의 의존성을 양방향으로 그려놓은 그래프입니다.
{
"ep": "GET /v1/internal/orders/eligibility",
"service": "order-service",
"method": "GET",
"path": "/v1/internal/orders/eligibility",
"handler": "InternalOrderController.checkEligibility",
"version": 3,
"updated_at": "2026-05-14T02:58:56Z",
"source_pr": "order-service#412",
"callees": [
{"service": "infra.rds:order-db-ro", "via": "jdbc"}
],
"callers": [
{"service": "payment-service", "method": "POST", "path": "/v1/payments/checkout"},
{"service": "payment-service", "method": "POST", "path": "/v1/payments/preauth"},
{"service": "cart-service", "method": "GET", "path": "/v1/cart/summary"},
{"service": "promo-service", "method": "POST", "path": "/v1/promo/apply"}
],
"infra": [
{"id": "infra.rds:order-db-ro", "via": "jdbc", "purpose": "주문 조회"}
],
"history": [
{"version": 3, "updated_at": "2026-05-14T02:58:56Z", "source_pr": "order-service#412", "changes": "할인 적용 여부 응답 필드 추가"},
{"version": 2, "updated_at": "2026-04-22T07:11:03Z", "source_pr": "order-service#388", "changes": "한도 검증 분기 추가"},
{"version": 1, "updated_at": "2026-03-09T05:24:11Z", "source_pr": "order-service#341", "changes": "최초 생성"}
]
}
이 그래프가 생긴 다음부터 회색지대가 줄었습니다.
장애가 터졌을 때 "5xx 급증, 어디서 시작됐을까요?"를 코드 grep과 메신저 수소문으로 풀던 일이, callers 역추적 한 번으로 끝나게 됐습니다. "이 시그니처를 바꾸면 어디가 깨질까?"를 사람 머리로 추정하던 일이, 영향 EP를 PR에 자동으로 댓글 다는 일로 바뀌었습니다. "이 엔드포인트 지워도 될까?"를 한 달 뜸 들이다 사고로 끝나던 일이, callers: [] 한 줄 확인으로 끝납니다.
| 시나리오 | 기존 방식 | A.I.T 이후 |
|---|---|---|
| 장애 분석 | "5xx 급증, 어디서 시작?" → 코드 grep + 메신저 수소문 | callers 역추적으로 영향 EP를 1분 안에 식별 |
| 배포 리스크 검토 | "이 시그니처 바꾸면 어디 깨지지?" → 사람 머리로 추정 | callers 자동 식별, 영향 PR 사전 공유 |
| EP Deprecation | "이거 지워도 돼?" → 한 달 뜸 들이고 지웠다가 사고 | callers: [] 면 즉시 안전 |
| 인프라 점검 공지 | "DB 점검, 누가 영향?" → DBA가 일일이 파악 | callees.infra.rds 필터로 영향 EP 전수 추출 |
| 신규 입사자 온보딩 | 아키텍처 문서는 보통 6개월 묵음 | 코드와 항상 동기화된 의존 그래프 제공 |
핵심은 신선도입니다. 6개월 묵은 다이어그램이 아니라, 오늘 아침 머지된 PR까지 반영된 그래프입니다. 코드가 바뀌면 그래프가 같이 바뀝니다.
JSON은 어떻게 생겼나요
토폴로지 데이터는 두 단계로 나뉘어 있습니다.
① EP 원본 (ep-impact/{service}/{slug}.json)
EP 한 건당 한 파일입니다. 버전과 변경 이력까지 함께 들고 있는 단일 진실 공급원입니다.
{
"ep": "GET /v1/internal/orders/eligibility",
"service": "order-service",
"method": "GET",
"path": "/v1/internal/orders/eligibility",
"handler": "InternalOrderController.checkEligibility",
"version": 3,
"updated_at": "2026-05-14T02:58:56Z",
"source_pr": "order-service#412",
"callees": [{ "service": "...", "method": "...", "path": "...", "via": "...", "purpose": "..." }],
"callers": [{ "service": "...", "method": "...", "path": "...", "via": "..." }],
"infra": [{ "id": "...", "via": "...", "purpose": "..." }],
"history": [
{ "version": 3, "updated_at": "...", "source_pr": "...", "changes": "..." },
{ "version": 2, "updated_at": "...", "source_pr": "...", "changes": "..." },
{ "version": 1, "updated_at": "...", "source_pr": "...", "changes": "최초 생성" }
]
}
history 배열 덕분에 "이 EP는 언제, 어떤 PR로, 어떻게 바뀌었는가"를 EP 자체가 자기 안에 기록합니다. 운영 중 "이 응답 필드는 언제부터 있었지?"를 더 이상 git log로 추적하지 않습니다.
② 통합 map (ep-impact-map.json)
45개 서비스의 EP 원본을 한 덩어리로 합친 조회용 파일입니다. 빠른 jq 쿼리와 시각화를 위해 버저닝 필드는 생략하고 의존 관계만 담습니다.
{
"generated_at": "2026-05-08T04:34:48Z",
"total_eps": 1852,
"total_infra": 28,
"eps": {
"{service}:{method}:{path}": {
"service": "...",
"method": "GET",
"path": "/v1/internal/orders/eligibility",
"handler": "InternalOrderController.checkEligibility",
"callees": [{ "service": "...", "method": "...", "path": "...", "via": "...", "purpose": "..." }],
"callers": [{ "service": "...", "method": "...", "path": "...", "via": "..." }],
"infra": [{ "id": "...", "via": "...", "purpose": "..." }]
}
},
"infra_index": {}
}
via 필드는 호출 방식을 구분합니다 — web-client(HTTP), jdbc, jpa, redis, kafka. 단순한 의존성이 아니라 어떤 채널로 의존하는지까지 추적합니다.
토폴로지가 만들어지는 마지막 세 단계
1부에서 코드리뷰 → 명세 생성 → 명세 검수까지의 흐름을 다뤘습니다. 2부는 그 뒤를 잇는 4~6단계입니다.
(1~3단계: 코드리뷰 → 명세 생성 → 명세 검수)
│
▼
[4] 토폴로지 갱신 봇
EP 임팩트 맵 + E2E 그래프 갱신 ← 토폴로지가 만들어지는 지점
│
│ 소스 PR 머지 시
▼ (5분 이내 감지)
[5] 머지 감시 봇
문서 레포 PR 자동 rebase + squash 머지
│
▼
[6] 배포 봇
빌드 → S3 → CDN
→ 내부 문서 사이트 반영 ✅
4단계 — 단방향 정보로 양방향 그래프를 만듭니다
이 단계가 토폴로지를 만듭니다.
명세 검수가 끝나면 두 단계가 순차로 실행됩니다.
[단건 갱신] 변경된 명세 1건 → 해당 서비스의 EP 임팩트 파일 갱신 (버저닝)
↓
[전체 통합] 서비스별 임팩트 파일 → ep-impact-map.json 재생성
개발자는 callees만 알 수 있습니다. callers는 시스템 전체가 알아야 합니다
개발자는 자기 코드에서 "어디를 호출하는지(callees)"만 압니다. "누가 나를 부르는지(callers)"는 모릅니다. 그걸 알려면 다른 서비스 코드를 전부 까봐야 합니다. 자기 모니터 안에 갇혀 있는 정보로는 시스템 전체의 그림을 그릴 수 없습니다.
토폴로지 갱신 봇이 이걸 자동으로 합니다. 2-pass 알고리즘으로 단방향 정보를 양방향 그래프로 뒤집습니다.
[1차 패스] 모든 서비스의 raw JSON을 읽으며 EP 등록 + callees + infra 추출
└─ 각 EP가 "어디를 호출하는지" 단방향으로 기록
[2차 패스] callees 정보를 거꾸로 훑으며 역방향 인덱스 구축
└─ A가 B를 호출한다는 정보 → B의 callers 필드에 A를 자동 추가
[후처리] infra_index 구성
└─ 인프라 ID → 사용 EP 목록 (top-level 역인덱스)
이게 토폴로지의 핵심입니다. 개별 서비스가 들고 있는 단편적인 정보가, 시스템 전체 관점의 양방향 의존 그래프로 자동 구성됩니다.
결과 산출물 3종
| 산출물 | 용도 |
|---|---|
ep-impact-map.json |
기계가 읽는 토폴로지 데이터 |
| E2E 콜체인 그래프 (HTML) | 사람이 보는 시각화 (Cytoscape.js) |
인프라 역인덱스 (infra_index) |
인프라 점검 영향 EP 추출 |
현재 규모
| 지표 | 값 |
|---|---|
| 추적 서비스 | 45개 |
| 추적 EP | 1,852개 |
| 인프라 컴포넌트 | 28개 |
| 양방향 의존성 매핑 EP | 227개 |
| 콜체인 평균 길이 | 3 hop 이내 |
통합 map 한 번 쿼리로 끝나는 일들
# "특정 RDS 점검 시 영향받는 EP 전수"
jq '.eps | to_entries | map(select(.value.callees[]?.service ==
"infra.rds:order-db-ro")) | length' ep-impact-map.json
# "이 EP 폐기해도 되나? callers 확인"
jq '.eps["order-service:GET:/v1/internal/orders/eligibility"].callers' ep-impact-map.json
# "특정 서비스가 호출하는 외부 서비스 전부"
jq '[.eps | to_entries[] | select(.key | startswith("order-service:"))
| .value.callees[].service] | unique' ep-impact-map.json
장애 대응 채팅방에서 "누가 영향이죠?"라는 질문이 사라집니다. JSON 쿼리 한 줄로 끝납니다.
5단계 — 문서 PR은 사람이 머지하지 않습니다
5분마다 소스 PR의 머지 여부를 폴링합니다. 머지가 감지되면 대응 문서 레포 PR 브랜치를 master 기준으로 rebase합니다. 충돌이 나면 E2E 그래프 인덱스는 master 수용으로 자동 해소합니다. 마지막으로 squash merge + 브랜치 삭제, 그리고 배포 시그널을 생성합니다.
6단계 — 배포는 시그널 하나로 시작됩니다
배포 시그널이 감지되는 즉시 빌드와 배포가 시작됩니다.
git pull origin master
→ MkDocs 빌드
→ CI/CD 배포 트리거
→ S3 동기화 → CDN invalidation
→ 내부 문서 사이트 반영
명세 페이지와 ep-impact-map.json이 동시에 배포됩니다.
예시 시나리오 — 한 PR이 토폴로지에 닿기까지
order-service 레포에 주문 사전 검증 엔드포인트가 새로 추가된다고 가정해 봅시다.
소스 PR은 POST /v3/orders/precheck 한 줄에서 시작됩니다.
자동 생성된 명세서
다음은 자동 생성된 명세서의 일부입니다.
| 항목 | 내용 |
|------|------|
| 인증 | 내부 서비스 전용 (특정 호출자만 허용) |
| 설명 | 주문 생성 전 재고/한도/할인 적용 가능성을 사전 검증하고 검증 티켓 발급 |
Response — 검증 통과 시:
{
"can_proceed": true,
"ticket_uuid": "generated-ticket-uuid",
"applied_promo": null
}
- 발급 티켓 10분 후 만료
- 검증 실패 시 사유 코드 포함 응답
소스 PR에 주석이 한 줄도 없어도, Claude는 Controller 코드, DTO, Aspect를 따라가며 위 수준의 명세를 자동으로 만들어냅니다.
문서 레포 브랜치 커밋 — 사람 0명
3a7f021 docs: API spec 초안 ← 명세 생성 봇
8b4c9e6 fix: 명세 불일치 자동 수정 ← 명세 검수 봇
c1d2e5f feat(ep-impact): EP impact 갱신 ← 토폴로지 갱신 봇
세 봇이 각자의 역할로 순차 커밋합니다. PR 오픈부터 배포까지, 사람의 개입은 코드를 쓰고 머지하는 것이 전부입니다.
토폴로지에는 무엇이 추가될까요
ep-impact-map.json에 신규 EP가 등록되면서 다음이 자동으로 따라옵니다.
- 이 EP의
callees: 외부 한도 정책 API 호출, RDS 접근 - 향후 다른 서비스가 이 EP를 호출하면 → 그 PR이 머지될 때
callers필드에 역방향 추가 - E2E 그래프에 노드 + 엣지 추가
다음에 누군가 "주문 사전 검증 흐름이 어떻게 되지?"라고 물으면, 이제는 메신저 검색이 아니라 이 그래프를 보면 됩니다.
인프라는 어떻게 묶여 있나요
17개 봇 서비스, 18개 외부 서비스 연동입니다.
| 레이어 | 구성 |
|---|---|
| AI | Claude Sonnet / Haiku (Anthropic API) |
| 제어 | 대시보드 — 봇 시작/중지/로그 통합 |
| 외부 연동 (MCP) | GitHub, Jira, Jenkins, Grafana, New Relic, AWS, Slack, Confluence 등 |
| 배포 | Jenkins → S3 → CDN |
| 문서 엔진 | MkDocs Material |
봇 간 통신은 파일 시그널 기반입니다. 각 봇이 특정 시그널 파일을 감시하는 방식으로 단계가 느슨하게 결합됩니다. 의존성이 없고, 각 봇이 독립적으로 재시작 가능합니다.
현재 단일 머신에서 동작하며, AWS EKS 이관을 검토하고 있습니다.
숫자로 보는 효과
토폴로지 산출물 규모
| 지표 | 수치 |
|---|---|
| 추적 중인 엔드포인트 | 1,852개 (45개 서비스) |
| 추적 중인 인프라 컴포넌트 | 28개 (RDS / Redis / Kafka 등) |
| 양방향 의존성 매핑 EP | 227개 (callers + callees + infra) |
운영 효율
| 지표 | 수치 |
|---|---|
| 토폴로지 갱신 주기 | PR 머지 후 약 5분 |
| 배포 성공률 (누적 20회) | 100% |
| 봇 간 통신 방식 | 파일 시그널 (의존성 없음) |
다음으로 무엇을 만들 수 있을까요
지금까지가 자동화 파이프라인의 첫 번째 챕터였다면, 다음 챕터는 토폴로지를 다른 시스템과 잇는 일입니다.
- Scala·Node.js 코드 스캐너 보강 — 일부 언어로 작성된 서비스의 내부 호출 관계가 아직 그래프에 충분히 들어오지 않았습니다. (클라이언트·외부 파트너에서 호출되는 EP는 본래 내부 스캐너 스코프 밖이므로, callers가 비어 있는 것이 정상입니다.)
- 장애 분석과의 통합 — APM·모니터링 인시던트와
ep-impact-map.json을 연결해 장애 발생 시 영향 EP가 자동으로 추출되도록 만드는 일. - 배포 PR 자동 리스크 분석 — PR diff에서 변경된 EP를 추출하고, 토폴로지에서 callers를 조회해, 영향받을 서비스에 자동 댓글이 달리도록 하는 일.
코드가 바뀌면 그래프가 같이 바뀌는 것까지는 만들었습니다. 이제는 그래프가 바뀔 때 사람에게 알려주는 일을 만들 차례입니다.
두 편을 마치며
코드는 자주 바뀝니다. 문서는 거의 안 바뀝니다. 그 간극이 운영 리스크입니다.
A.I.T는 그 간극을 메우는 시스템입니다. PR 한 줄이 머지되면, 5분 안에 명세서와 서비스 토폴로지가 같이 갱신됩니다. 코드와 문서의 시간차가 분 단위로 줄어듭니다.
두 편에 걸쳐 세 가지를 다뤘습니다.
- AI 코드리뷰가 PR 단계에서 버그·보안·성능 이슈를 사전에 잡습니다 — MUST 항목 46% 감소가 그 증거입니다. (1부)
- 2,558개의 API 명세가 자동으로 작성되어 있고, 새 엔드포인트가 추가될 때마다 늘어납니다 — 수동으로는 불가능한 규모입니다. (1부)
- 1,852 EP의 양방향 의존 그래프가 PR 머지마다 갱신됩니다 — 단방향 정보로부터 시스템 전체 토폴로지를 자동 구성합니다. (2부)
명세서는 사람이 읽는 부산물이고, 코드리뷰는 품질을 유지하는 안전망입니다. 그러나 진짜 자산은 매 머지마다 갱신되는 의존 그래프입니다. 이 그래프 위에서 장애 분석, 배포 리스크 검토, EP 폐기 결정, 인프라 점검 영향도 분석이 모두 가능해집니다.
7년 동안 풀고 싶었던 문제를 이제야 사람이 아닌 시스템의 손에 맡길 수 있게 됐습니다. PR 한 줄의 변경이 시스템 토폴로지까지 이어지는 흐름은 아직 첫 발을 내디딘 단계입니다. 앞으로도 코드가 곧 서비스 토폴로지가 되는 방향으로 자동화를 확장해 나가겠습니다.
본문 예시에 대한 안내
본문에 등장하는 서비스명(
order-service,payment-service등), API 경로, PR 번호, 커밋 해시, JSON 응답 예시는 모두 설명을 돕기 위한 가상의 예시입니다. 시스템 구조와 동작 방식은 실제와 동일하지만, 운영 데이터와 식별 가능한 정보는 노출되지 않도록 익명화했습니다.
