코딩 복습용 블로그
안녕하세여! 코딩 복습용 블로그입니다.
전체 글 (417)
인프라 개념 정리: 보안과 자동화 정리
728x90

[TIL] 보안과 자동화 정리

인프라 보안과 IaC를 정리하며

개발하다 보면 서버가 느려지거나, 외부에서 비정상적인 요청이 들어오거나, 설정을 바꾼 뒤 서비스가 달라지는 상황을 마주한다. 이때 인프라 보안과 운영 자동화의 기본 개념을 알고 있으면 문제의 원인을 더 차분하게 좁힐 수 있다.

이번 글에서는 인프라에서 자주 만나는 보안 사고와 취약점 대응 방법을 먼저 정리하고, 이어서 인프라를 코드로 관리하는 IaC의 계층과 운영 원칙을 살펴본다.

1. 보안 사고를 보는 기본 관점

보안 사고는 한 가지 기술 결함만으로 발생하지 않는다. 취약한 소프트웨어, 잘못된 설정, 탈취된 계정, 감시 공백이 겹치면서 사고 가능성과 피해가 커진다.

위협, 취약점, 위험

  • 위협: 피해를 일으킬 수 있는 행위자나 사건
  • 취약점: 위협이 악용할 수 있는 약점
  • 위험: 취약점이 악용될 가능성과 예상 피해를 함께 고려한 판단

예를 들어 외부에 공개된 오래된 서비스는 취약점이 될 수 있다. 이를 노리는 공격은 위협이고, 서비스 중단이나 정보 유출이 일어날 가능성은 위험으로 본다.

사고가 반복되는 원인

  • 패치 누락, 공개 범위 오설정, 과도한 권한처럼 사람과 절차에서 생기는 실수
  • 이미 공개된 취약점을 방치하는 문제
  • 단일 장애 지점이나 과도한 신뢰 관계처럼 설계가 피해를 키우는 경우
  • 담당자의 기억과 수동 확인에만 의존해 운영의 일관성이 무너지는 경우

제로데이 취약점은 수정 패치가 제공되기 전에 알려졌거나 악용되는 취약점을 말한다. 패치 적용을 놓친 알려진 취약점과는 구분해야 한다. 제로데이 대응은 패치만 기다리는 데 그치지 않고, 노출 범위를 줄이고 접근을 제한하며 탐지를 강화하는 방식으로 위험을 낮춘다.

2. UDP와 서비스 거부 공격

서비스 거부(DoS) 공격은 네트워크나 시스템 자원을 소진시켜 서비스를 사용할 수 없게 만드는 공격이다. 여러 감염 기기가 동시에 공격하면 분산 서비스 거부(DDoS)라고 한다.

UDP는 연결 설정 절차가 없는 데이터그램 프로토콜이다. 이 특성은 출발지 주소를 위조한 요청과 서버 응답을 이용하는 반사 공격에 악용될 수 있다. UDP 자체가 공격을 뜻하는 것은 아니며, 서버의 역할과 설정에 따라 실제 동작은 달라진다.

반사와 증폭 공격 흐름

  1. 공격자는 요청의 출발지 주소를 피해자 주소로 위조해 여러 UDP 서버에 보낸다.
  2. 요청을 받은 서버가 응답하면 그 응답이 피해자에게 전달된다. 이를 반사 공격이라고 한다.
  3. 요청보다 응답 데이터가 더 큰 경우 피해자는 공격자가 보낸 양보다 많은 트래픽을 받는다. 이를 증폭 공격이라고 한다.

DNS, NTP, SSDP 등 UDP를 사용하는 서비스는 잘못 공개되거나 설정된 경우 반사·증폭 공격에 악용될 수 있다.

DNS 서버 역할 구분

  • 권한 DNS 서버: 특정 도메인 영역의 레코드를 권위 있게 제공한다.
  • 재귀 리졸버: 클라이언트를 대신해 다른 DNS 서버를 조회하고 결과를 캐시한다.
  • 오픈 리졸버: 인터넷의 임의 사용자에게 재귀 질의를 제공하도록 노출된 DNS 리졸버를 말한다.

권한 DNS 서버라고 해서 재귀 질의를 절대 하지 않는다고 단정할 수는 없다. 두 역할은 다르지만 실제 동작은 서버 설정을 확인해야 한다.

운영에서 적용할 방어

  • DNS와 UDP 기반 서비스는 필요한 기능과 대상에만 공개한다.
  • 재귀 DNS 질의의 접근 범위를 제한해 공개 리졸버가 되지 않도록 한다.
  • 출발지 주소 위조 트래픽을 줄이는 네트워크 필터링과 클라우드 사업자의 DDoS 완화 기능을 활용한다.
  • 평소 트래픽 기준선을 기록하고 요청량, 대역폭, 오류율의 급격한 변화를 감시한다.
  • 장애 시 차단·우회·사업자 연락 절차를 미리 정하고 점검한다.

3. 웹 공격과 표적형 공격

드라이브 바이 다운로드

드라이브 바이 다운로드는 사용자가 악성 파일을 직접 실행하지 않았더라도 취약한 브라우저나 플러그인, 잘못 구성된 웹 환경을 통해 악성 코드가 내려오거나 실행되는 공격을 가리킨다.

웹 서버와 브라우저를 최신 상태로 유지하고 불필요한 플러그인을 제거하는 것이 기본이다. 여기에 엔드포인트 보호, 웹 서버 변경 감시, 의심스러운 파일 격리를 더해 피해 가능성을 낮출 수 있다.

SQL 인젝션, 세션 공격, CSRF

SQL 인젝션은 외부 입력이 SQL 구문으로 해석되어 실행되는 취약점이다. 값과 질의 구조를 분리하는 매개변수화 질의를 사용하고, 데이터베이스 계정의 권한을 필요한 범위로 제한해야 한다.

세션 탈취와 인증 우회 위험을 낮추려면 쿠키 보호 설정, 안전한 세션 수명, 다중 요소 인증, 요청마다의 권한 확인을 적용한다.

CSRF(사이트 간 요청 위조)는 사용자가 로그인한 상태를 악용해 의도하지 않은 요청을 보내게 하는 공격이다. 상태를 변경하는 요청에서 CSRF 토큰을 확인하고, 쿠키의 SameSite 설정도 함께 검토한다.

스피어 피싱과 APT

  • 스피어 피싱: 특정 사람이나 조직에 맞춘 메시지로 링크 클릭이나 계정 정보 입력을 유도한다.
  • 소셜 엔지니어링: 사람의 신뢰나 업무 관행을 악용한다.
  • APT(Advanced Persistent Threat): 고도화되고 지속적인 위협을 뜻하며, 장기간 은밀하게 접근을 유지하는 공격 캠페인을 설명할 때 사용한다.

APT는 특정 악성코드나 도구 하나를 뜻하지 않는다. 메일·웹 필터링, 다중 요소 인증, 최소 권한, 계정과 단말 로그 수집, 백업 복구 점검을 함께 적용해야 한다. 계정 탈취가 의심되면 세션과 자격 증명을 폐기하고 영향 범위를 확인한다.

4. 클라우드 보안과 책임 범위

클라우드 보안은 제공자와 고객이 나누어 맡는다. 제공자는 데이터센터와 물리 장비, 가상화 기반처럼 클라우드 자체의 보안을 담당한다. 고객은 선택한 서비스에 따라 데이터, 계정과 권한, 네트워크 규칙, 애플리케이션과 설정을 관리한다.

가상 서버를 직접 운영할 때와 완전 관리형 서비스를 사용할 때 고객이 맡는 범위는 달라진다. 따라서 서비스별 책임 경계를 확인해야 한다.

관련 표준과 관리체계

  • ISO/IEC 27017: 클라우드 서비스의 정보보호 통제를 다루는 지침이다. 클라우드 제공자와 고객의 책임을 포함한다.
  • ISMS: 정보보호 관리체계(Information Security Management System)를 뜻한다. 위험을 식별하고 정책과 통제를 적용한 뒤 점검과 개선을 반복하는 관리 방식이다.

표준과 인증 기준은 개정될 수 있으므로 실제 계약이나 감사에 적용할 때는 해당 시점의 공식 기준을 확인한다.

클라우드 기본 점검

  • 계정과 역할에 최소 권한을 적용하고 관리자 접근에는 강한 인증을 사용한다.
  • 공개 저장소와 네트워크 규칙을 검토한다.
  • 암호화 키와 토큰 같은 비밀정보를 코드 저장소에 두지 않는다.
  • 로그와 백업의 보존 기간, 접근 권한, 복구 책임자를 정한다.
  • 서비스별 제공자 책임과 고객 책임을 문서화한다.

5. 운영 중 취약점과 이상 징후 확인

운영 보안은 자산을 파악하고, 이상을 탐지하고, 영향도를 판단한 뒤, 수정과 재확인을 반복하는 과정이다. 도구 이름만 외우기보다 어떤 질문에 답하기 위해 사용하는지 이해하는 것이 중요하다.

모니터링과 점검 도구

  • SNMP: 네트워크 장비와 서버의 상태 정보를 수집하는 관리 프로토콜이다.
  • Cacti, OpenNMS: 네트워크와 시스템 모니터링에 사용할 수 있는 도구의 예다.
  • syslog, Windows Event Log: 운영체제와 서비스에서 발생한 로그를 수집하는 데 사용한다.
  • ICMP ping: 호스트의 기본 연결성을 확인하지만 애플리케이션이 정상인지까지 보장하지는 않는다. 실제 서비스는 TCP 연결이나 HTTP 요청도 확인해야 한다.
  • Nmap: 네트워크 호스트와 포트를 확인하는 도구다. 소유하거나 명시적으로 허가받은 범위에서만 실행한다.
  • OWASP ZAP: 웹 애플리케이션의 동적 보안 시험에 활용할 수 있다. 테스트 환경과 실행 범위를 먼저 정한다.
  • Greenbone/OpenVAS 계열: 취약점 점검과 결과 관리를 지원한다. 발견 결과는 자산과 버전, 노출 경로를 대조해 확인한다.

취약점 대응 순서

  1. 자산과 외부 노출 범위를 확인하고 취약점 식별자, 영향을 받는 제품과 버전을 기록한다.
  2. CVSS 점수와 벡터를 참고해 기술적 심각도를 파악한다.
  3. 인터넷 노출 여부, 실제 악용 정황, 자산 중요도, 보완 통제와 패치 가능성을 함께 고려해 우선순위를 정한다.
  4. 패치나 설정 변경을 검증 환경에서 확인하고 변경 및 롤백 계획을 세운다.
  5. 적용 후 재검사와 로그 확인으로 해결 여부를 검증한다.
  6. 예외를 둘 경우 책임자, 만료 기한, 보완 통제를 기록한다.

CVSS는 취약점 특성에 따른 심각도 척도다. 조직의 실제 위험도나 조치 순서를 단독으로 결정하는 값은 아니다.

6. 코드로 관리하는 인프라

IaC(Infrastructure as Code)는 서버와 네트워크 같은 인프라의 원하는 상태를 코드나 설정으로 표현하고, 검토·배포·재현하는 방식이다. 변경 이력을 남기고 반복 가능한 절차를 만들면 수작업으로 생기는 누락과 환경 차이를 줄일 수 있다.

다만 잘못된 설정도 빠르게 복제될 수 있다. 코드 검토와 권한 통제를 함께 운영해야 한다.

부트스트래핑 계층

운영체제나 실행 기반을 준비하는 단계다. 데이터센터에서는 OS 설치 자동화가 해당하고, 클라우드에서는 이미지 선택이나 인스턴스 초기화가 이에 해당한다.

Kickstart와 Cobbler는 OS 설치 자동화의 사례다. 클라우드 이미지와 초기화 스크립트도 부트스트래핑의 일부를 맡는다.

구성 관리 계층

OS가 준비된 뒤 패키지, 미들웨어, 사용자, 파일, 서비스 설정을 원하는 상태로 맞춘다. Ansible, Puppet, Chef 등이 구성 관리 도구의 예다.

구성 관리 도구에서 자주 쓰는 개념은 다음과 같다.

  • 선언적: 원하는 최종 상태를 표현하고, 실행 절차 일부를 도구에 맡긴다.
  • 추상화: 개별 명령보다 서비스나 패키지 같은 관리 대상을 중심으로 기술한다.
  • 멱등성: 같은 설정을 여러 번 적용해도 결과가 의도한 상태로 유지되도록 한다.
  • 수렴성: 현재 상태와 원하는 상태의 차이를 줄여 목표 상태에 도달하도록 한다.

오케스트레이션 계층

여러 서버와 서비스의 배포와 연결, 확장을 조율한다. 애플리케이션 배포, 클러스터 구성원 변경, 로드밸런서 대상 등록, 모니터링 대상 등록 등이 여기에 포함된다.

서버를 직접 지정하는 방법은 변경 범위가 분명하지만 반복 작업이 생긴다. 서비스 디스커버리나 클러스터 관리 도구를 통한 자동 등록은 규모 확장에 유리하지만 자동화 규칙과 상태 감시가 필요하다.

Capistrano와 Fabric은 원격 작업이나 배포 자동화에 사용할 수 있다. Serf와 Consul은 클러스터 구성원 정보와 서비스 디스커버리 같은 분산 환경 기능의 사례다. 도구별 기능과 지원 상태는 바뀔 수 있으므로 도입 전 공식 문서를 확인한다.

IaC를 안전하게 운영하기

  • 코드를 버전 관리에 저장하고 동료 검토와 자동 검증을 거쳐 적용한다.
  • 비밀번호와 토큰, 개인 키를 코드나 상태 파일에 평문으로 저장하지 않는다.
  • 개발·검증·운영 환경의 차이를 명시적으로 관리한다.
  • 실제 인프라와 코드 사이의 설정 편차(drift)를 주기적으로 확인한다.
  • 변경 권한을 최소화하고 실패 시 되돌릴 방법을 준비한다.
  • 큰 변경은 일부 자원에 먼저 적용해 영향을 확인한다.

후기

이번 내용을 정리하면서 인프라 보안은 보안 도구를 설치하는 것만으로 끝나는 일이 아니라는 점을 알게 됐다. 안전한 설정과 최소 권한을 바탕으로 로그와 지표를 계속 확인하고, 취약점이 실제 서비스에 미치는 영향까지 살펴야 대응 우선순위를 제대로 정할 수 있다.

또 IaC를 사용하면 인프라 변경을 반복하고 재현하기 쉬워지지만, 잘못된 설정도 여러 환경에 빠르게 퍼질 수 있다는 점을 배웠다. 그래서 코드를 검토하고 비밀정보를 보호하며 실제 설정과 코드가 일치하는지 확인하는 과정이 자동화만큼 중요하다. 앞으로 인프라 문제를 만나면 도구부터 찾기보다 어떤 자산이 영향을 받는지, 책임 범위와 변경 이력이 무엇인지 먼저 확인해야겠다.

핵심만 다시 정리

  • 사고 원인은 기술 결함뿐 아니라 설정 실수, 계정 문제, 변경 절차, 감시 공백에서도 생긴다.
  • UDP 반사·증폭 공격은 주소 위조와 공개 서비스의 설정 문제를 악용할 수 있다.
  • 클라우드 책임 범위는 사용하는 서비스 형태에 따라 달라진다.
  • 취약점 점수와 스캐너 결과는 판단을 돕는 자료이며 실제 위험을 함께 따져야 한다.
  • IaC를 안전하게 쓰려면 변경 검토, 비밀정보 보호, 편차 확인이 필요하다.

참고 자료

728x90
  Comments,     Trackbacks
하네스 엔지니어링이란? - (end)
728x90

Claude Code 하네스로 디버깅 / RCA 팀 꾸리기 — "로컬에서는 잘 되는데요"

리뷰 팀은 있는 코드를 평가했고, 구현 팀은 없는 기능을 만들었고, 마이그레이션 팀은 같은 변환을 수천 번 반복했다. 이번엔 원인을 모르는 문제를 다룬다. 디버깅은 추측이 가장 쉽게 끼어드는 일이라, 이번 하네스의 목표는 하나다. 추측을 증거로 바꾸는 순서를 강제하는 것.
참고 저장소: https://github.com/revfactory/harness-engineering-with-cc (ex-14-10-application-guide)


1. 디버깅이 왜 하네스가 필요한 일인가

혼자 디버깅할 때를 떠올려 보면 이렇다. 버그 보고를 받고, 코드를 훑고, "아마 이거겠지" 하고 고치고, 테스트가 통과하면 끝. 재현도, 가설 검증도, 같은 실수가 다른 곳에 있는지 확인도 안 한다. 운 좋게 맞으면 다행이고, 틀리면 버그는 다른 모양으로 돌아온다.

Claude에게 "이 버그 고쳐 줘"라고 시켜도 똑같다. 오히려 더 빠르게 추측하고 더 자신 있게 고친다. 그래서 이번 하네스는 네 가지 장치로 그 지름길을 막는다.

장치 막는 것
단계별 담당 분리 재현·분석·가설·수정이 한 머릿속에서 뒤섞이는 것
결정론 게이트 재현도 안 하고 가설로 넘어가는 것
하이브리드 구조 탐색은 느려지고, 수정 루프는 끝없이 도는 것
피시본 분석 떠오르는 원인 하나에 꽂혀 나머지를 안 보는 것

2. 저장소에서 가져온 것, 직접 설계한 것

저장소의 디버깅 예제에는 응용 가이드 폴더에 프로덕션 장애용 reviewer.md 하나만 있었다. 핵심 4인 에이전트, 오케스트레이터, 샘플 버그는 없었다. 대신 reviewer.md가 검토하는 산출물 이름(reproduction.sh, hypotheses.md, fix.patch, completion-checklist.md)이 나와 있어서, 그 이름과 5단계 워크플로에 맞춰 나머지를 직접 설계했다.


3. 시나리오: 로컬에서는 통과, CI에서는 실패

실습용 버그는 타임존 버그로 만들었다.

// src/settlement.js — 정산 마감일 계산
export function businessDate(isoString) {
  const d = new Date(isoString);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

getDate()는 코드를 실행하는 컴퓨터의 시간대 기준이다. 한국 시간 11월 1일 새벽 0시 30분은 UTC로는 10월 31일 15시 30분. 그래서 한국 PC에서는 2026-11-01, UTC로 도는 CI 서버에서는 2026-10-31이 나온다. 코드만 봐서는 잘못이 안 보이고, 개발자 PC에서는 테스트가 전부 통과한다. "로컬에서는 잘 되는데요"의 교과서 같은 사례다.

재료는 세 가지를 뒀다.

  • 버그 보고서 reports/BUG-001.md: 증상, "로컬에서는 잘 되는데요"라는 개발자 코멘트, 발생·정상 환경
  • CI 로그 reports/ci.log: 실패 메시지와 함께 Node 버전, LANG=C.UTF-8, TZ=(빈 값). 진짜 단서와 헷갈리게 하는 단서가 섞여 있다
  • 같은 패턴의 두 번째 파일 src/invoice.js: 청구서 발행일도 같은 방식으로 날짜를 뽑는다. 마지막 단계의 "유사 패턴 검색"이 이걸 찾아내는지 보기 위한 함정이다

하네스를 돌리기 전에 정답부터 손으로 확인해 뒀다.

npm test                 # 통과 (로컬 한국 시간)
TZ=UTC npm test          # 실패 (CI와 같은 조건)

4. 팀 구성: 4명

에이전트 맡은 일 model 핵심 권한
reproducer 재현 스크립트, 3회 연속 재현 게이트, 수정 후 재검증 sonnet Bash, Write / Edit 없음
log-analyzer 정상·실패 환경 비교, 타임라인 sonnet Bash, Write / Edit 없음
hypothesis-analyst 피시본 분석, 가설 5개, 반증 실험 설계 opus Write / Bash 없음
fixer 최소 수정, 회귀 테스트, fix.patch sonnet Edit, Bash

지난 실습들에서 배운 대로 역할 경계는 tools 한 줄로 정했다.

코드를 고칠 수 있는 건 fixer뿐이다. 재현 담당이 재현하다가 "이거 고치면 되겠네" 하고 손대는 순간, 재현이 증거에서 추측으로 바뀐다.

별도 verifier가 없다. 수정 후 검증은 reproducer가 맡는다. 처음에 버그를 잡아낸 그 재현 스크립트로 "이제 안 일어난다"를 보여 준다. 같은 자로 재야 전후 비교가 된다.

가설 담당은 실험을 못 한다. Bash가 없어서 반증 실험을 설계만 하고 실행은 reproducer가 한다. 가설을 세운 사람이 자기 가설을 검증하면 확증 편향이 끼어든다. 구현 팀에서 "만든 사람이 검증하면 자기 확증"이라고 정리했던 원리가 여기서는 "가설 세운 사람이 실험하면 자기 확증"으로 나타난다.

opus는 가설 담당 하나다. 여러 증거를 동시에 놓고 범주별로 원인을 따지는, 판단이 가장 무거운 자리라서다.


5. 워크플로: 5단계와 두 개의 게이트

단계 하는 일 모드 다음으로 가는 문
1. 준비 보고서 파싱, _workspace/bug-001/ 생성 메인 세션 report.md 정리
2. 관찰 재현 + 로그 분석 병렬 팀 게이트 1: 재현 3회 연속
3. 가설 피시본 → 가설 5개 → 우선순위 → 반증 실험 서브 1순위 가설이 실험으로 지지됨
4. 수정-검증 수정 → 재현 스크립트 확인, 최대 3회 서브 게이트 2: 수정 후 3회 연속 통과
5. 정리 포스트모템 + 유사 패턴 검색 메인 세션 완료 체크리스트

6. 결정론 게이트: "3회 연속"의 의미

reproducer의 정의 파일에서 가장 중요한 줄은 이것이다.

3회 중 결과가 갈리면 "비결정적"으로 보고하고 게이트를 통과시키지 않는다.

한 번 재현은 우연일 수 있다. 두 번 실패, 한 번 성공이면 그건 재현이 아니라 운이다. 그 상태로 가설 단계에 가면 가설을 확인할 방법이 없다. 실험 결과가 바뀌어도 그게 가설 때문인지 운 때문인지 모르니까.

그리고 재현 스크립트에는 대조군이 같이 들어간다. "UTC에서 실패"만으로는 부족하고, 같은 스크립트 안에서 "한국 시간에서는 통과"까지 보여야 한다. 그래야 시간대가 원인의 경계선이라는 게 증명된다.

reproduction.sh 3회 실행
  1회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
  2회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
  3회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
→ 게이트 1 통과

같은 스크립트가 수정 후에는 반대 방향의 게이트가 된다. 3회 모두 양쪽 다 통과해야 수정이 인정된다. 게이트를 사람의 느낌이 아니라 반복 실행 결과로 정한다는 것, 이게 이번 실습에서 배운 첫 번째 지식이다.


7. 하이브리드 구조: 왜 관찰은 팀, 수정은 서브에이전트인가

지금까지 세 팀은 모드가 하나였다. 리뷰 팀과 마이그레이션 팀은 서브에이전트, 구현 팀은 팀 모드. 디버깅 팀은 한 워크플로 안에서 둘을 섞는다.

관찰은 탐색이다. log-analyzer가 CI 로그에서 TZ=가 비어 있는 걸 발견하는 순간, reproducer가 바로 TZ=UTC로 돌려 봐야 빠르다. 메인 세션을 거쳐 전달하면 느리고 맥락이 끊긴다. 그래서 둘을 한 팀으로 묶어 직접 대화하게 둔다.

수정-검증은 통제다. 고치고, 재고, 실패하면 다시. 이 루프의 횟수를 메인 세션이 정확히 세고 있어야 3회 상한이 지켜진다. fixer와 reproducer를 한 팀으로 묶어 직접 대화하게 두면 저희끼리 몇 번이고 주고받으며 상한이 무너진다. 그래서 메인 세션이 한 명씩 부른다.

탐색 구간은 팀으로, 통제 구간은 서브에이전트로.

모드를 "이 하네스 전체에 하나"로 고르는 게 아니라 구간마다 일의 성격을 보고 고른다. 구현 팀에서 패턴을 Phase마다 조합했던 것처럼, 이번엔 실행 모드를 단계마다 조합한 셈이다.

관찰 단계에 팀 모드가 들어가니 이번엔 환경 변수가 필요하다.

export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude

8. 피시본: 가설을 떠올리지 말고 펼친다

가설 담당에게는 원인 범주 여섯 개를 줬다.

코드 / 환경 / 의존성 / 데이터 / 테스트 / 프로세스

규칙은 범주마다 최소 하나씩 요인을 적고, 해당 없으면 "해당 없음 + 이유"를 적는 것. 이렇게 하면 처음 눈에 띈 원인에 꽂혀 나머지를 건너뛰는 일이 막힌다. 이번 버그로 펼치면 대략 이런 모양이 나와야 한다.

범주 요인
코드 실행 환경 시간대에 의존하는 날짜 getter
환경 CI의 TZ 미설정(UTC), 로컬은 한국 시간
의존성 Node 버전 차이 (로컬 vs CI v20)
데이터 자정 직후 타임스탬프 (시간대 경계)
테스트 로컬 시간대에서만 실행, 시간대 고정 없음
프로세스 CI에 시간대 매트릭스 없음

여기서 가설 5개를 고르고, 가설마다 반증 실험을 붙인다.

  • 시간대 가설이 맞다면 → TZ=Asia/Seoul로 돌리면 통과해야 한다
  • Node 버전 가설이 맞다면 → 시간대는 그대로 두고 Node 버전만 바꿔도 결과가 바뀌어야 한다

반증 실험은 reproducer가 돌린다. 결과가 나오면 가설끼리 말싸움할 필요가 없다. 틀린 가설은 실험으로 지워진다. 이게 "체계적으로 좁힌다"의 실제 모습이었다.

한 가지 더. 가설 담당에게 "단일 원인으로 환원하지 않는다, 직접 원인과 기여 요인을 구분한다"를 줬다. 이 버그의 직접 원인은 코드지만, 기여 요인은 테스트(한국 시간에서만 돎)와 프로세스(CI에 시간대 고정 없음)다. 코드만 고치면 다음 시간대 버그도 또 "로컬에서는 잘 되는" 채로 들어온다.


9. 범위를 지키는 두 줄

이번 하네스에서 의외로 중요했던 건 "하지 않는 일"을 정한 두 줄이다.

fixer: "기여 요인은 고치지 않고 포스트모템 권고로 넘긴다."
fixer가 신나서 CI 설정까지 바꾸기 시작하면 수정 범위가 커지고, 검증이 실패했을 때 무엇 때문인지 가려낼 수 없다. 코드 수정은 최소로, 구조 개선은 포스트모템으로 분리한다.

5단계: "유사 패턴은 목록화만 하고 고치지 않는다."
invoice.js에도 같은 버그가 있다. 하지만 여기서 같이 고치면 이번 수정의 검증 범위가 흐려진다. 유사 패턴 목록은 다음 사고 후보 목록이고, 고치는 건 별도 버그로 다룬다.


10. 하네스 구조

debug_rca/
├── CLAUDE.md                          원칙: 재현 없이 가설 없음, 3회 게이트, blameless
├── reports/
│   ├── BUG-001.md                     버그 보고서
│   └── ci.log                         CI 실패 로그
├── src/settlement.js, invoice.js      버그가 숨은 코드
├── tests/settlement.test.js           로컬에서만 통과하는 테스트
├── .claude/
│   ├── agents/
│   │   ├── reproducer.md
│   │   ├── log-analyzer.md
│   │   ├── hypothesis-analyst.md
│   │   └── fixer.md
│   └── skills/debug-rca/SKILL.md      5단계 오케스트레이터
└── _workspace/bug-001/                실행 시 생성
    ├── report.md
    ├── observations/repro.md, logs.md
    ├── reproduction.sh
    ├── hypotheses.md
    ├── fix.patch
    ├── verification.md
    ├── postmortem.md
    ├── similar-patterns.md
    └── completion-checklist.md

실행은 한 줄이다.

BUG-001 디버깅해 줘

11. 응용: 같은 뼈대, 바꾸는 곳만 다르다

재현 → 가설 → 수정-검증 → 정리라는 뼈대는 그대로 두고, 상황에 따라 게이트의 의미와 담당자의 초점만 바꾼다.

응용 게이트 1 (재현) 바뀌는 담당 게이트 2 (수정 확인)
프로덕션 장애 프로덕션 말고 스테이징·로컬에 재현 환경 구축 log-analyzer → 알람·배포 타임라인 중심. 저장소의 reviewer 추가 핫픽스는 사람이 결정, 하네스는 근본 수정만
플래키 테스트 3회 연속 대신 N회(예: 50회) 실패율 측정 피시본 범주 교체: 실행 순서, 타이밍, 공유 상태, 난수, 네트워크 같은 N회에서 실패 0회
성능 저하 3회 측정 모두 기준선 대비 임계값 이상 저하 + 편차 작음 log-analyzer → 프로파일러 역할 3회 측정 모두 기준선 ± 허용 오차
1인 개발자 그대로 3회 팀 모드 없이 reproducer + fixer 둘만 남김. 로그·가설은 메인 세션이 겸함 그대로 3회, 루프 3회 상한 유지

몇 가지는 짚어 둘 만하다.

프로덕션 장애의 reviewer는 저장소에 있던 유일한 파일인데, 도구가 Read 하나뿐이다. 포스트모템 검토자는 아무것도 실행하거나 고치지 못하고 읽기만 한다. 그리고 "누구 잘못인가" 대신 "시스템이 왜 그걸 허용했나"를 묻고, 권고마다 자동화된 가드(린터, 테스트, 알람 임계값)를 붙이게 되어 있다. 장애 회고가 사람 탓으로 흐르는 걸 구조로 막는다. 이 blameless 원칙은 기본 하네스의 CLAUDE.md에도 헌법으로 넣어 뒀다.

플래키 테스트는 reproducer의 "3회 중 결과가 갈리면 통과시키지 않는다"에서 갈라지는 경로다. 결정론 게이트를 통과하지 못하는 버그는 버리는 게 아니라 통계 게이트로 갈아탄다.

1인 개발자 버전에서 아무리 줄여도 reproducer와 fixer는 반드시 분리한다. 둘이 합쳐지면 "고친 사람이 고쳐졌다고 판정"하게 되어 하네스의 존재 이유가 사라진다. 비용을 줄일 때 지켜야 할 최소 단위가 이 둘이다.


12. 네 팀을 이어 보면 (최종 정리)

리뷰 팀 구현 팀 마이그레이션 팀 디버깅 팀
다루는 일 있는 코드 평가 없는 기능 만들기 같은 변환 반복 모르는 원인 찾기
주 패턴 팬아웃 Phase별 조합 감독자 게이트 + 하이브리드
넘어가는 기준 없음 Phase 산출물 검토 배치 검증 재현 3회 연속
실행 모드 서브 팀 서브 팀 + 서브 혼합
검증 기준 정의 파일 기준 설계 문서 기준선 테스트 재현 스크립트
마지막 산출물 리뷰 보고서 통합 보고서 상태 파일 포스트모템

디버깅 팀에서 새로 얻은 건 두 가지다.

  1. "넘어가도 되는가"를 사람의 느낌이 아니라 반복 실행 결과로 정하는 게이트. 그리고 그 게이트가 통과되지 않을 때 갈아탈 경로(플래키, 성능)까지 미리 설계해 두는 것.
  2. 한 워크플로 안에서도 탐색 구간은 팀으로, 통제 구간은 서브에이전트로 섞는 설계. 패턴을 조합했듯 모드도 조합한다.
728x90

'인공지능' 카테고리의 다른 글

하네스 엔지니어링이란? - (3)  (0) 2026.11.03
하네스 엔지니어링이란? - (2)  (0) 2026.11.02
하네스 엔지니어링이란? - (1)  (0) 2026.11.01
  Comments,     Trackbacks
하네스 엔지니어링이란? - (3)
728x90

레거시 파일 수천 개를 옮기는 하네스 직접 쌓아 보기 — 감독자 패턴 실습기

리뷰 팀은 "동시에 보고 끝내는" 팀이었고, 구현 팀은 "Phase마다 팀을 갈아끼우는" 팀이었다. 세 번째로 만든 건 같은 작은 일이 수천 번 반복되는 일을 맡는 팀이다. 레거시 .jsx 파일들을 TypeScript로 옮기는 마이그레이션. 이번엔 감독자 패턴을 처음으로 직접 짜 봤다.

항목 내용
실습일 2026-11-01
실습 위치 ~/Desktop/side/harness/legacy_migration
만든 것 에이전트 4명 + 감독자 스킬 1개 + CLAUDE.md + 상태 파일 구조
직접 설계한 것 baseline-tester, migration-worker, migration-verifier, legacy-migration 스킬
가져다 쓴 것 dependency-mapper 정의, 샘플 레거시 코드 17개
진행 상황 하네스 구축·검사 완료, 본 실행 결과는 다음 글

1. 시작하며: 이번엔 일의 모양이 다르다

앞의 두 팀을 만들면서 "일의 모양이 패턴을 정한다"는 걸 배웠다. 그래서 이번에도 먼저 일의 모양부터 따져 봤다. 레거시 3,500개 파일을 바꾼다고 치면 성질이 네 가지다.

  1. 반복: 파일 하나 바꾸는 일 자체는 작고 비슷하다. 그게 수천 번.
  2. 순서: UserCard → useAuth → api → http. 아래부터 바꿔야 위가 안 깨진다.
  3. 불균형: 같은 개수여도 어떤 묶음은 쉽고 어떤 묶음은 어렵다.
  4. 중단: 이 정도 양이면 중간에 반드시 끊긴다.

1-1. 처음 떠올린 두 방법이 왜 안 되는가

Claude 하나에게 다 시키기. 제일 먼저 떠오르지만 바로 접었다.

  • 맥락이 넘쳐서 100번째와 3,000번째 파일의 변환 방식이 달라진다.
  • 어디까지 했는지가 대화 속에만 있어서 끊기면 처음부터다.
  • 한 파일이 깨져도 다음으로 넘어가 버린다.

리뷰 팀처럼 처음에 똑같이 나누기(고정 팬아웃). 리뷰 팀에서 잘 썼으니 이번에도 될까 싶었는데 아니었다.

  • 워커 A가 UserCard를 바꾸는 동안 그게 쓰는 http는 워커 C가 아직 손도 안 댔을 수 있다. 순서가 찢어진다.
  • 쉬운 묶음 받은 워커는 놀고, 어려운 묶음 받은 워커는 한참 걸린다.
  • 이미 다 나눠 준 뒤라 실패한 일을 다시 나눌 수가 없다.

1-2. 그래서 감독자

정리하고 보니 필요한 건 대기열을 쥔 사람이었다. 주방장이 주문서를 들고 있다가, 요리사 손이 비면 지금 바로 만들 수 있는 주문 하나를 건넨다. 소스가 아직 안 끓었으면 그 소스가 들어가는 요리는 내주지 않는다. 일을 미는 게 아니라 워커가 가져가는 구조다.

  혼자 다 하기 처음에 나누기 감독자
분배 시점 없음 처음 한 번 워커가 빌 때마다
의존 순서 머릿속 찢어짐 대기열이 지킴
일의 불균형 — 노는 워커 자동 흡수
실패 처리 묻힘 못 옮김 재배정, 상한 넘으면 사람
중단 후 재개 처음부터 어려움 상태 파일에서 이어서

2. 팀 설계: 4단계, 4명 + 감독자

예제 저장소를 열어 보니 이번 폴더엔 dependency-mapper 에이전트 하나와 샘플 코드 17개뿐이었다. README에 나오는 mock-mapper.sh, verify.sh도 없었다. 리뷰 팀 때 "저장소는 단계별 스냅샷"이라는 걸 한 번 겪어서 이번엔 덜 당황했다. 나머지 팀원은 직접 설계하기로 했다.

먼저 워크플로를 4단계로 잡았다.

단계 담당 하는 일 패턴
1. 의존 매핑 dependency-mapper import 그래프 → 배치와 순서 —
2. 기준선 테스트 baseline-tester 변환 전 동작을 테스트로 박제 —
3. 점진 변환 감독자 + migration-worker 여럿 시작 가능한 배치를 하나씩 가져감 감독자
4. 검증 migration-verifier 배치마다 PASS/FAIL 판정 생성-검증

구현 팀에서 배운 "패턴의 층"이 여기서도 그대로 보였다. 바깥 뼈대는 파이프라인(1→2→3·4), 안쪽 엔진은 감독자와 생성-검증. 다만 이번엔 3단계와 4단계가 배치 단위로 맞물려 계속 돈다.

[1. 의존 매핑] ──▶ [2. 기준선 테스트] ──▶ ┌──────── 3·4단계 루프 (배치 단위) ────────┐
 import 그래프       변환 전 동작 박제       │ 감독자(메인 세션) ── 대기열(batches.json) │
 → 배치·순서         전부 통과 못 하면 정지  │   │ 시작 가능 배치 출고 (최대 3 동시)      │
                                          │   ▼                                    │
                                          │ migration-worker ×N ──▶ migration-verifier│
                                          │        ▲   FAIL(사유)       │ PASS        │
                                          │        └── pending 복귀 ◀───┘ → verified  │
                                          │  3회째 FAIL → 되돌림 + failed + 위층 차단  │
                                          └──────────────────────────────────────────┘

감독자는 에이전트로 만들지 않았다. 구현 팀의 PM처럼 스킬을 읽은 메인 세션이 감독자다. 워커끼리 대화할 일도 없어서 에이전트 팀 모드도 안 켰다. 배치 사이의 정보는 "이미 변환된 파일의 타입을 읽는다"로 파일을 통해 오간다. 일반 서브에이전트로 충분하다.

변환 대상은 샘플이 .jsx라서 JavaScript → TypeScript로 정했다.

2-1. 권한 설계: tools 칸이 곧 직무 분리

담당 모델 tools 일부러 뺀 것
dependency-mapper (원본 유지) Read, Grep, Bash Write — 저장은 감독자가
baseline-tester sonnet Read, Grep, Write, Bash Edit — src는 읽기만
migration-worker sonnet Read, Grep, Write, Edit, Bash (본문에서 tests/·남의 배치·commit 금지)
migration-verifier sonnet Read, Grep, Bash Edit, Write — 판정만

표를 채우면서 의도한 건 세 가지다.

  • 테스트를 쓰는 사람은 코드를 못 고친다 → 테스트 통과시키려고 코드를 바꾸는 일이 불가능.
  • 판정하는 사람은 아무것도 못 고친다 → 보다가 편한 쪽을 슬쩍 고쳐 버리는 일이 불가능. 구현 팀 boundary-verifier에서 배운 그대로다.
  • 아무도 커밋하지 않는다 → 커밋은 내가 한다.

2-2. 모델 설계: 이번엔 전원 sonnet

구현 팀 때 boundary-verifier는 opus였다. 두 파일을 동시에 놓고 "구현 탓이냐 설계 탓이냐"를 추론해야 했으니까. 이번 verifier는 다르다. tsc, vitest, git diff가 내놓는 결과로 판정이 거의 다 정해지고, 판정도 PASS/FAIL 둘뿐이다. 판단이 도구 출력에 있으면 모델은 한 단계 낮춰도 된다고 봤다. 워커도 규칙이 명확하고 배치가 작으니 sonnet. 반복 횟수가 많아서 비용 면에서도 이게 맞다.


3. 판 깔기

mkdir -p ~/Desktop/side/harness/legacy_migration
cd ~/Desktop/side/harness/legacy_migration

git clone --depth 1 https://github.com/revfactory/harness-engineering-with-cc.git /tmp/hewcc
cp -r /tmp/hewcc/ex-13-01-dependency-mapper/sample-src ./src
find src -type f | sort

샘플 17개를 훑어보다가 일부러 심어 둔 함정 두 개를 발견했다.

파일 함정 하네스가 해야 할 일
cyclic-a.jsx, cyclic-b.jsx 서로를 import하는 순환 의존 한 배치로 묶고 cyclic 경고
truncated.jsx 중간에 잘린 파일 (파싱 실패) 배치에서 빼고 manual-queue로

나머지 15개로 의존 층을 직접 그려 봤다. 실행했을 때 맞는지 비교할 정답지다.

1층 (의존 없음)   utils/http, utils/format, components/Footer, standalone
2층              utils/api
3층              hooks/useAuth, hooks/useFetch, components/Header
4층              context/SessionContext, components/UserCard
5층              components/Profile, components/UserList
6층              pages/Dashboard, pages/Settings
따로             cyclic-a + cyclic-b (한 배치, cyclic 경고)
제외             truncated (manual-queue)

되돌리기를 git으로 할 거라 레거시 상태를 먼저 커밋해 두고, TypeScript와 vitest를 깔았다.

git init
git add src
git -c user.name=practice -c user.email=practice@local commit -m "legacy baseline"

npm init -y
npm install -D typescript vitest
npm pkg set scripts.test="vitest run" scripts.typecheck="tsc --noEmit"

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
EOF

allowJs: true가 중요했다. 마이그레이션 도중엔 .tsx와 .jsx가 섞여 있다. 이 상태에서도 타입 체크가 돌아야 배치마다 검증할 수 있다.


4. CLAUDE.md: 원칙 네 줄

cat > CLAUDE.md << 'EOF'
# legacy_migration — 레거시 마이그레이션 하네스

## 개요
- 진입점: legacy-migration 스킬 ("src를 TypeScript로 마이그레이션해 줘")
- 메인 세션이 감독자. 워커는 서브에이전트
- 상태 파일: _workspace/migration/batches.json (진행 상태의 유일한 기준)

## 워크플로
1. 의존 매핑 → 2. 기준선 테스트 → 3. 점진 변환 → 4. 배치별 검증

## 원칙
- 마이그레이션은 동작을 바꾸지 않는다. 버그도 그대로 옮긴다. 고치는 건 별도 작업
- 기준선 테스트는 변환 중 절대 수정하지 않는다
- 배치 실패가 상한을 넘으면 그 배치 파일을 git으로 되돌리고 manual-queue에 기록
- 누구도 git commit 하지 않는다. 커밋은 사람이 한다
EOF

"버그도 그대로 옮긴다"를 넣을지 한참 고민했다. 바꾸는 김에 고치면 좋지 않나? 그런데 그러면 테스트가 깨졌을 때 변환이 잘못된 건지 일부러 고친 건지 구분이 안 된다. 기준이 흔들리면 검증 자체가 의미가 없어진다. 그래서 고치는 건 별도 작업으로 뺐다.


5. 팀원 명부

5-1. dependency-mapper: 가져와서 배치 크기만 줄였다

mkdir -p .claude/agents
cp /tmp/hewcc/ex-13-01-dependency-mapper/.claude/agents/dependency-mapper.md .claude/agents/
sed -i 's/10~50/2~4/' .claude/agents/dependency-mapper.md

정의를 열어 보니 tools: Read, Grep, Bash로 Write가 없고, 복잡도 공식과 배치 필드 8개가 들어 있었다.

필드 의미
id 배치 식별자
files 배치에 속한 파일
depends_on 먼저 verified 돼야 하는 배치
complexity 복잡도 점수 (감독자의 우선순위)
pattern 변환 유형
status pending → in_progress → converted → verified | failed
attempts / max_attempts 시도 횟수 / 상한

배치 크기는 원래 10

50이다. 그런데 파일이 17개뿐이라 그대로 두면 배치가 한두 개밖에 안 나오고, 감독자가 일을 나눠 주는 모습을 볼 수가 없다. **분배가 눈에 보이도록 2

4로 줄였다.**

5-2. baseline-tester: "올바른 동작"이 아니라 "지금의 동작"

cat > .claude/agents/baseline-tester.md << 'EOF'
---
name: baseline-tester
description: 마이그레이션 전에 레거시 코드의 현재 동작을 테스트로 박제한다. 변환 전 코드에서 전부 통과해야 한다. 기준선, 박제, 회귀 테스트 키워드에서 사용.
model: sonnet
tools: Read, Grep, Write, Bash
---

## 역할
- src의 export마다 현재 동작을 그대로 기록하는 테스트를 쓴다.
- 기대값은 "올바른 동작"이 아니라 "지금의 동작"이다. 버그도 그대로 박제한다.

## 입력
- src/ 전체
- batches.json (manual-queue에 들어간 파일은 제외)

## 절차
1. 파일마다 export 목록을 뽑는다.
2. 외부 호출(fetch 등)은 vi.fn으로 막고, 무엇이 어떤 인자로 호출되는지까지 기록한다.
3. tests/baseline/에 테스트 작성. import 경로에 확장자를 쓰지 않는다 (.jsx → .tsx 후에도 그대로 동작하도록).
4. npm test 실행. 레거시 코드에서 전부 통과해야 한다.

## 출력
- tests/baseline/*.test.js
- 통과 결과 요약 (파일별 테스트 수)

## 에러 핸들링
- src 파일은 읽기만 한다. Edit이 없다.
- 실행하면 문제가 생기는 동작(예: 무한 재귀)은 "그 문제가 일어난다"를 기대값으로 박제한다.
- 레거시에서 통과하지 않는 테스트는 기준선이 아니다. 통과할 때까지 테스트를 고친다.
EOF

이 에이전트를 쓰면서 마이그레이션 검증의 핵심이 뭔지 정리됐다. "동작이 안 바뀌었다"를 확인하려면, 바꾸기 전 동작을 먼저 사진 찍어 둬야 한다. 순환 의존 파일이 스택을 터뜨리면 "스택이 터진다"가 정답이다.

작은 요령 하나: import 경로에 확장자를 안 쓰게 했다. 그래야 .jsx가 .tsx로 바뀌어도 테스트를 한 글자도 안 고쳐도 된다.

5-3. migration-worker: 유일하게 손을 가진 팀원

cat > .claude/agents/migration-worker.md << 'EOF'
---
name: migration-worker
description: 감독자에게 배치 하나를 받아 그 배치의 파일만 JavaScript에서 TypeScript로 변환한다. 여러 명이 동시에 일한다. 변환, 마이그레이션 워커 키워드에서 사용.
model: sonnet
tools: Read, Grep, Write, Edit, Bash
---

## 역할
- 받은 배치의 파일만 변환한다. 다른 배치 파일은 건드리지 않는다.
- 동작은 바꾸지 않는다. 버그도 그대로 옮긴다.

## 입력
- 배치 하나 (id, files, pattern)
- 재시도라면 이전 검증 실패 사유

## 변환 규칙
1. .jsx → .tsx (JSX가 없으면 .ts). git mv로 이름을 바꿔 이력을 남긴다.
2. 함수 인자와 반환값에 타입을 붙인다. 모르면 unknown으로 두고 any는 쓰지 않는다.
3. import 경로는 확장자 없이 유지한다.
4. 로직, 문자열, 호출 순서는 그대로 둔다.

## 절차
1. 배치 파일과, 그 파일이 import하는 이미 변환된 파일의 타입을 읽는다.
2. 변환 규칙대로 바꾼다.
3. npx tsc --noEmit으로 이 배치 파일에 오류가 없는지 확인한다.
4. 감독자에게 변환한 파일 목록과 특이사항을 돌려준다.

## 에러 핸들링
- tests/ 아래 파일은 절대 수정하지 않는다.
- 동작을 바꿔야만 타입이 맞는 경우 변환을 멈추고 감독자에게 사유를 보고한다.
- git commit 금지.
EOF

리뷰 팀 refactorer 때처럼, 손을 가진 팀원일수록 제약을 많이 걸었다. 특히 마지막 탈출구 — "동작을 바꿔야만 타입이 맞으면 멈추고 보고" — 를 꼭 넣고 싶었다. 억지로 타입을 맞추다 로직을 건드리는 게 마이그레이션에서 제일 조용히 틀리는 방식이라서다.

5-4. migration-verifier: 판정 순서를 고정했다

cat > .claude/agents/migration-verifier.md << 'EOF'
---
name: migration-verifier
description: 변환이 끝난 배치 하나를 검증해 PASS 또는 FAIL만 판정한다. 직접 고치지 않는다. 검증, 배치 검증 키워드에서 사용.
model: sonnet
tools: Read, Grep, Bash
---

## 역할
- 배치 하나가 변환 규칙을 지켰고 동작이 그대로인지 판정한다.

## 판정 순서 (반드시 이 순서로)
1. npm run typecheck — 이 배치 파일에서 타입 오류가 있으면 FAIL
2. npm test — 기준선 테스트 중 하나라도 실패하면 FAIL (동작이 바뀜)
3. git diff로 이 배치 외 파일이나 tests/가 바뀌었으면 FAIL (경계 침범)
4. any 사용, 로직 변경 흔적이 있으면 FAIL
5. 모두 통과하면 PASS

## 출력
- verdict: PASS | FAIL
- 실패 단계 번호, 파일:라인, 근거 (도구 출력 인용)

## 에러 핸들링
- Edit, Write가 없다. 직접 고치지 않는다.
- 도구 출력 없이 판정하지 않는다.
EOF

boundary-verifier를 고치면서 얻은 교훈 — 판정 에이전트에는 판정의 뜻이 아니라 순서를 준다 — 를 처음부터 적용했다. 그리고 boundary-verifier와 비교해 보니 차이가 분명했다.

  boundary-verifier (구현 팀) migration-verifier (이번)
판정 PASS / FIX / REDO PASS / FAIL
되돌아갈 곳 구현 또는 설계 워커뿐 (설계 문서가 없음)
기준 설계 문서 두 개 기준선 테스트 하나
모델 opus sonnet

REDO가 없는 이유는 간단하다. 되돌아갈 설계가 없다. 기준이 기준선 테스트 하나라 PASS/FAIL이면 충분하다. "도구 출력 없이 판정하지 않는다"는 리뷰 팀에서 haiku 담당이 근거 없이 지적하던 걸 보고 넣은 줄이다.


6. 감독자 스킬: 이번 실습의 핵심

mkdir -p .claude/skills/legacy-migration
cat > .claude/skills/legacy-migration/SKILL.md << 'EOF'
---
name: legacy-migration
description: 레거시 코드 대규모 마이그레이션을 감독자 패턴으로 진행한다. "마이그레이션해 줘", "TypeScript로 바꿔 줘" 요청에서 사용.
---

# 레거시 마이그레이션 감독자

메인 세션이 감독자다. 직접 변환하지 않는다. 대기열을 관리하고 일을 나눠 주고 판정을 받는다.

## 상태 파일
- _workspace/migration/batches.json — 진행 상태의 유일한 기준. 상태가 바뀔 때마다 즉시 저장한다.
- _workspace/migration/manual-queue.md — 사람이 봐야 할 파일과 사유
- 배치 status: pending → in_progress → converted → verified | failed

## 1단계: 의존 매핑
- dependency-mapper에게 target_glob "src/**/*.jsx"를 넘긴다.
- 돌려받은 batches.json 초안을 저장한다. max_attempts가 비어 있으면 3.
- 파싱 실패 파일은 manual-queue.md에 기록한다.
- cyclic 경고가 붙은 배치를 사용자에게 알린다.

## 2단계: 기준선 테스트
- baseline-tester를 부른다.
- npm test가 레거시 코드에서 전부 통과해야 3단계로 간다. 하나라도 실패하면 멈춘다.

## 3단계: 점진 변환 (감독자 루프)
반복한다, pending 배치가 없어질 때까지:
1. 시작 가능 배치 = status가 pending이고 depends_on의 모든 배치가 verified인 것
2. 비어 있는 워커 자리(최대 3)만큼 시작 가능 배치를 꺼낸다. complexity가 높은 것부터.
3. 꺼낸 배치를 in_progress로 바꾸고 저장한 뒤 migration-worker에게 하나씩 넘긴다.
4. 워커가 돌아오면 converted로 바꾸고 4단계 검증으로 보낸다.
5. 워커 자리가 비면 바로 1로 돌아가 다음 배치를 꺼낸다. 다른 워커를 기다리지 않는다.

## 4단계: 배치별 검증 (생성-검증 루프)
- migration-verifier에게 배치를 넘긴다.
- PASS → verified로 저장. 이 배치를 기다리던 배치들이 시작 가능해진다.
- FAIL → attempts + 1
  - attempts < max_attempts: 실패 사유를 붙여 pending으로 되돌린다 (다시 대기열로)
  - attempts = max_attempts: 그 배치 파일을 git checkout으로 되돌리고 failed로 저장, manual-queue.md에 기록.
    이 배치에 의존하는 배치들도 failed로 막고 사유를 남긴다.

## 끝
- verified / failed / manual-queue 건수를 사용자에게 보고한다.
- 커밋은 하지 않는다.
EOF

쓰고 나서 다시 읽어 보니 감독자 패턴을 결정하는 건 결국 세 줄이었다.

  1. "다른 워커를 기다리지 않는다." 이 한 줄이 팬아웃과 감독자를 가른다. 팬아웃은 셋이 다 끝나야 다음 라운드, 감독자는 끝난 자리부터 바로 채운다. 이 줄이 빠지면 그냥 라운드제 팬아웃이 된다.
  2. 시작 조건은 converted가 아니라 verified. 처음엔 converted로 썼다가 고쳤다. 검수 안 된 기초 위에 2층을 올리면, 아래가 나중에 FAIL 났을 때 위까지 다 다시 해야 한다.
  3. FAIL은 재배정, 상한이면 되돌리고 위층까지 막는다. 반쯤 바뀐 파일을 남기지 않고, 깨진 기초 위에 쌓지 않는다.

complexity 높은 것부터 꺼내는 것도 의도가 있다. 오래 걸리는 일을 먼저 출발시켜야 마지막에 한 워커만 혼자 붙잡고 있는 꼬리가 짧아진다.

6-1. 상태 파일 하나로 끝까지

구현 팀 때까지는 진행 상태가 Phase 산출물에 흩어져 있었다. 이번엔 수천 번 반복이라 그러면 안 된다. 모든 진행 상태를 batches.json 하나에 두고, 바뀔 때마다 즉시 저장하게 했다. 세션이 끊기면 새 세션이 이 파일만 읽고 이어 가면 되고, 나도 이 파일 하나만 열면 진행률을 안다.

pending ──▶ in_progress ──▶ converted ──▶ verified
   ▲                                          
   └──── FAIL (3회 미만, 실패 사유 첨부) ───────┘
                    FAIL 3회째 ──▶ failed (되돌림 + manual-queue + 위층 차단)

7. 결재함 만들고 검사하기

mkdir -p _workspace/migration tests/baseline
echo "_workspace/" >> .gitignore
echo "node_modules/" >> .gitignore
find CLAUDE.md .claude tsconfig.json -type f | sort
ls package.json tsconfig.json
claude plugin validate .claude/agents

여기서 하나 당황한 게 있었다. 에이전트가 제대로 등록됐는지 보려고 /agents를 쳤는데 안 먹었다. 이 버전에선 /agents 마법사가 빠졌다. /list-agents는 에이전트가 아니라 세션 목록이었다. 결국 이렇게 확인했다.

  • 세션에서 @mig, @dep, @base를 치면 자동완성에 (agent) 표시가 붙은 항목 4개가 뜨는지
  • 파일 형식은 claude plugin validate .claude/agents로

그리고 리뷰 팀 때 데인 기억이 있어서 이번엔 처음부터 지켰다. 정의 파일이 있는 legacy_migration 폴더에서 새 세션을 연다. 세션 중에 폴더를 옮겼다가 에이전트가 정식 로드되지 않아 tools 제한이 무시됐던 사건 — verifier의 "Edit 없음"이 그냥 부탁이 돼 버리면 이 하네스는 의미가 없다.

cd ~/Desktop/side/harness/legacy_migration
claude

처음 돌릴 땐 shift+tab으로 auto mode를 꺼서 단계마다 눈으로 확인하고, 진행 중엔 /tasks로 워커와 모델을 볼 생각이다.

src를 TypeScript로 마이그레이션해 줘

8. 돌리기 전에 짚어 본 것들

설계를 마치고 다시 훑어보니 걸리는 데가 몇 군데 있었다. 실행하면서 실제로 터지는지 같이 보려고 적어 둔다.

# 걸리는 점 왜 대응 후보
1 병렬 워커 간 오판정 워커 3명이 같은 작업 트리에서 일한다. verifier의 git diff에 다른 워커의 변경이 섞이면 "경계 침범" FAIL이 잘못 나올 수 있다. npm test도 남의 반쯤 바뀐 파일 때문에 깨질 수 있다 diff를 git diff -- <배치 파일>로 한정, 또는 검증만 직렬화
2 truncated.jsx와 타입 체크 allowJs: true라 tsc가 잘린 JS도 파싱한다. 워커가 돌리는 npx tsc --noEmit이 매번 구문 오류를 볼 수 있다 manual-queue 파일을 tsconfig.exclude에 추가
3 git mv 뒤 되돌리기 이름을 바꾼 뒤 git checkout만으로는 원래 .jsx가 안 돌아오고 .tsx가 남을 수 있다 원래 경로 restore + 새 파일 삭제를 절차로 명시
4 컴포넌트 테스트 의존성 깐 건 typescript, vitest뿐. 컴포넌트를 렌더링하는 기준선이라면 react 등이 더 필요할 수 있다 샘플 import 보고 사전 설치
5 차단 범위 "의존 배치도 failed"가 바로 위층만인지 그 위 전부인지 애매하다 그래프 상위 전체를 막는다고 명시

9. 실행하면 확인할 것

단계 확인 항목 결과
1. 의존 매핑 내가 그린 층 구조와 맞는가 / 순환 두 파일이 한 배치 + 경고 / truncated → manual-queue ☐
2. 기준선 레거시에서 전부 통과 / 순환 파일의 "스택 터짐"이 박제되는가 ☐
3. 점진 변환 1층부터 나가는가 / 한 워커가 끝나자마자 다음 배치가 나가는가 / 남의 배치 파일을 안 건드리는가 ☐
4. 검증 FAIL이 사유와 함께 대기열로 돌아가는가 / 3회째면 되돌려지고 위층이 막히는가 ☐
전체 batches.json만 보고 진행 상황을 알 수 있는가 ☐
8장 걸리는 점 1~5 중 실제로 터진 것 ☐

10. 세 팀을 나란히 놓고 보니

  리뷰 팀 구현 팀 마이그레이션 팀
일의 모양 한 번 보고 끝 단계마다 다른 일 같은 일 수천 번
주 패턴 팬아웃 & 팬인 Phase별 조합 감독자 + 생성-검증
분배 처음에 한 번 Phase마다 팀 교체 워커가 빌 때마다
진행 상태 보고서 Phase 산출물 상태 파일 하나
검증 기준 정의 파일의 기준 설계 문서 기준선 테스트
워커 간 대화 없음 필요 (팀 모드) 없음 (서브에이전트)
verifier 모델 — opus sonnet

리뷰 팀이 한 장면을 잘 찍는 법, 구현 팀이 장면을 이어 붙여 영화를 만드는 법이었다면, 이번 팀은 같은 장면을 수천 번 찍어도 흔들리지 않게 촬영장을 운영하는 법이었다.

11. 정리

  1. 일의 모양이 패턴을 정한다. 반복·순서·불균형·중단, 이 네 가지가 감독자를 불렀다.
  2. 감독자의 본질은 대기열 + 당겨 가기 + 상태 파일이다. 처음에 나누지 않고, 빌 때마다 시작 가능한 일을 준다. "다른 워커를 기다리지 않는다" 한 줄이 팬아웃과의 차이다.
  3. 합격 기준을 기계가 판정할 수 있게 만든다. 지금의 동작을 버그까지 기준선 테스트로 박제하니 verifier가 sonnet + PASS/FAIL로 충분해졌다.
  4. 검수 끝난 것 위에만 쌓는다. 시작 조건은 verified.
  5. 실패는 멈추고 기록하고 사람에게. 상한 → 되돌림 → 위층 차단 → manual-queue.
  6. 앞에서 배운 건 그대로 재사용된다. 같은 뼈대의 정의 파일, tools로 권한 분리, 판정 순서 명시, 커밋은 사람, 정의 파일 위치에서 새 세션.

다음엔 실제로 돌려서 batches.json과 manual-queue.md를 들고 9장 표를 채우고, 8장에서 걸렸던 점들이 실제로 터지는지 확인할 생각이다. 일부러 FAIL을 유도해서 3회 차단까지 가는지도 볼 예정이다.


참고

728x90

'인공지능' 카테고리의 다른 글

하네스 엔지니어링이란? - (end)  (0) 2026.11.04
하네스 엔지니어링이란? - (2)  (0) 2026.11.02
하네스 엔지니어링이란? - (1)  (0) 2026.11.01
  Comments,     Trackbacks
하네스 엔지니어링이란? - (2)
728x90

리뷰 팀 다음에 구현 팀을 만들며 알게 된 것: 에이전트 설계에서 워크플로 설계로

리뷰 팀을 만들 때는 "에이전트 한 명을 어떻게 잘 정의하는가"를 배웠다. 구현 팀을 쌓아 보니 질문이 바뀌었다. "여러 팀을 시간 순서대로 어떻게 굴리는가." 이번 실습에서 얻은 하네스 엔지니어링 지식은 세 가지다.

  1. 패턴 4개를 Phase별로 조합하는 법
  2. 경계면만 보는 boundary-verifier가 왜 따로 있어야 하고, 어떻게 판정해야 하는가
  3. feature-pm이 Phase마다 역할을 바꾸고 팀을 갈아끼우는 타이밍


1. 패턴은 고르는 게 아니라 Phase마다 조합한다

리뷰 팀은 패턴 하나로 충분했다. 네 명이 동시에 읽고(팬아웃) 보고서를 모으면(팬인) 끝. 리뷰라는 일이 처음부터 끝까지 같은 모양이었기 때문이다.

구현은 다르다. 요구사항을 쪼개는 일, 설계하는 일, 코드를 짜는 일, 채점하는 일은 모양이 전부 다르다. 그래서 Phase마다 "지금 이 일은 어떤 모양인가"를 묻고 거기 맞는 패턴을 꺼낸다.

이 일에 대한 질문 답이 "예"면 이번 팀에서 쓰인 곳
일 전체를 누가 쪼개고 책임지나? 계층적 위임 PM이 처음부터 끝까지 꼭대기
앞 단계 결과가 있어야 다음을 할 수 있나? 파이프라인 Phase 0 → 1 → 2 → 3 → 4
여러 사람이 서로 기다리지 않고 동시에 할 수 있나? 팬아웃 & 팬인 Phase 1 설계 3인
만든 걸 바로바로 따로 검증해야 하나? 생성-검증 Phase 2·3 구현 ↔ verifier
독립된 판정 하나만 필요한가? 단독 서브에이전트 Phase 4 test-suite

여기서 얻은 핵심은 패턴에 층이 있다는 것이다.

  • 바깥 층(뼈대): 계층적 위임과 파이프라인. 기능 하나가 시작해서 끝날 때까지 전체를 감싼다.
  • 안쪽 층(엔진): 팬아웃과 생성-검증. 특정 Phase 안에서만 돈다.

Phase 경계가 곧 안쪽 엔진이 갈아끼워지는 지점이다. Phase 1에서 팬아웃이 돌다가, 설계가 확정되는 순간 Phase 2로 넘어가며 생성-검증으로 바뀐다. 리뷰 팀 때는 패턴을 "하나 고르는 것"이라 생각했는데, 실제로는 뼈대 위에 Phase별 엔진을 끼우는 설계였다.

조합이 교과서처럼 깔끔하지만도 않다. Phase 1은 팬아웃이지만 ui-designer와 db-migrator는 서로 직접 말하지 않고 api-designer를 거친다. API가 화면과 DB 사이의 계약이라서다. 패턴은 이름표가 아니라 일의 의존 관계를 그대로 옮긴 모양이다.


2. boundary-verifier: 왜 따로 있어야 하고, 어떻게 판정해야 하나

2-1. 리뷰 실습에서 이미 본 장면

지난 리뷰 실습에서 design-reviewer가 잡은 가장 좋은 지적은 두 파일 사이에 있었다. 같은 failed라는 이름이 실행 엔진, 결과 집계, 화면에서 서로 다른 뜻으로 쓰여 한 화면에 "0 실패"와 "1개 실패"가 동시에 뜰 수 있다는 것. 파일 하나만 보면 절대 안 보이는 버그였다.

구현 팀은 이 관점을 사후 리뷰가 아니라 만드는 도중에 넣는다. 그럼 왜 backend-impl이나 frontend-impl이 스스로 확인하지 않고 한 명을 더 둘까.

2-2. 별도 에이전트여야 하는 이유

① 경계면은 주인이 없는 땅이다.
backend-impl은 자기 API가 설계대로면 됐다고 믿고, frontend-impl은 자기 훅이 설계대로면 됐다고 믿는다. 둘이 맞물리는지는 누구의 일도 아니다. 주인 없는 곳에는 전담자를 둔다.

② 만든 사람이 검증하면 자기 확증이 된다.
생성-검증은 주체가 둘이어야 성립한다. 같은 머리가 만들고 검사하면 "내가 의도한 대로 됐다"를 확인할 뿐이다.

③ 검증자에게 손이 있으면 판정이 흐려진다.
verifier에게 Edit이 있으면 어긋남을 보자마자 편한 쪽을 고쳐 버린다. 그러면 "누가 설계를 안 따랐는지", "설계 자체가 모순인지"가 기록되지 않고 묻힌다. 손 없이 판정만 하게 해야 고치는 사람과 기준이 분명해진다.

이 제약은 정식 에이전트여야만 걸린다. 리뷰 실습에서 정의 파일을 일반 에이전트에게 글로 읽혀 흉내 내게 했더니 Write 금지가 무시됐다. tools 제한은 에이전트 단위로만 걸린다. 구현자 안에 "검증 모드"를 넣으면 Edit을 뺄 방법이 없다.

④ 필요한 두뇌가 다르다.
구현자는 확정된 설계로 한 파일씩 짜니 sonnet이면 된다. verifier는 두 파일을 동시에 놓고 추론해야 해서 opus다. 모델 배정 단위도 에이전트다.

⑤ 판정이 한 곳에 모여야 루프가 끝난다.
같은 경계면 REDO가 2회면 사람을 부른다. 이 카운트가 의미 있으려면 판정자가 한 명이어야 한다.

2-3. 정의를 직접 고치며 배운 것: FIX가 기본, REDO는 예외

처음 정의 파일에는 PASS / FIX / REDO의 뜻만 나열되어 있었다. 그런데 읽다 보니 걸리는 게 있었다. 구현이 안 맞으면 구현이 잘못된 건데, 왜 설계로 되돌리는 REDO가 있지?

따져 보니 판정의 순서가 빠져 있었다. 어긋남을 본 verifier가 "설계가 이상하네" 하고 곧장 REDO를 내면, 구현자가 설계를 안 따른 것까지 설계 탓이 된다. 그래서 boundary-verifier.md에 판정 순서 칸을 새로 만들었다.

  1. 양쪽 구현이 맞물리는가? → 맞물리면 PASS
  2. 안 맞물리면, 각 구현을 자기 설계 문서와 대조한다. 설계와 다른 쪽이 있으면 FIX → 그 구현자에게. 기본은 FIX다.
  3. 양쪽 다 자기 설계대로인데도 안 맞을 때만 REDO → api-designer에게.
상황 원인 판정
API 설계는 { user, accessToken }인데 백엔드가 { data: { user } }로 짰다 구현이 설계를 안 따름 FIX → backend-impl
API 설계는 객체를 주는데 UI 설계의 훅은 배열을 기대한다. 양쪽 다 자기 설계대로 짰다 설계 둘이 서로 모순 REDO → api-designer
설계에 에러 응답 형식이 없어 양쪽이 각자 다르게 짰다 설계 누락 REDO → api-designer

REDO가 필요한 이유는 아래 두 경우 때문이다. 설계끼리 모순이거나 설계에 내용이 없으면, 어느 쪽 구현을 고쳐도 설계 문서는 틀린 채 남는다. 그런데 Phase 4의 test-suite는 그 설계와 요구사항을 보고 E2E를 짠다. 코드만 맞춰 두면 테스트가 엉뚱한 기준으로 실패한다. 그래서 기준(설계)부터 바로잡고, 고쳐진 설계 기준으로 다시 FIX가 나가게 했다. REDO도 결국 구현을 맞추는 걸로 끝난다. 한 단계를 더 거칠 뿐이다.

함께 넣은 장치가 두 개 더 있다.

  • 판정 메시지에 근거 칸: "어느 설계 문서의 어느 부분과 어떻게 다른지"를 반드시 적는다. 받는 구현자가 무엇에 맞출지 바로 알고, 나중에 판정이 옳았는지 추적할 수 있다.
  • 금지 한 줄: "설계 문서와 대조하지 않고 REDO를 내지 않는다." 대조를 건너뛰고 재설계로 도망가는 걸 막는다.

2-4. 여기서 얻은 지식

리뷰 팀 때는 에이전트를 관점(보안, 설계, 정적 분석)으로 나눴다. 구현 팀에서 배운 건 두 가지다.

권한이 다르거나, 필요한 모델이 다르거나, 책임의 단위가 다르면 별도 에이전트로 뺀다.

판정을 맡기는 에이전트에는 판정의 뜻만이 아니라 판정의 순서를 줘야 한다.

뜻만 주면 에이전트가 그때그때 편한 판정을 고른다. 순서를 주면 같은 어긋남에 언제나 같은 판정이 나온다. 리뷰 팀 security-auditor에 심각도 기준을 줬던 것과 같은 원리인데, 이번엔 기준이 아니라 결정 순서였다.


3. feature-pm은 Phase마다 다른 사람이 된다

3-1. 리더의 역할이 고정이 아니다

리뷰 팀의 오케스트레이터는 한 가지 모드였다. 부르고, 기다리고, 모으기만 했다. feature-pm은 Phase마다 하는 일이 바뀐다.

Phase PM의 모드 하는 일
0 작성자 요구사항 json을 직접 쓴다. 이 기능의 채점표
1 검토자 / 문지기 설계 문서 3종에 [NOTE.]·[BLOCKER.]·[Q.] 주석, 최대 3회, 통과해야 다음으로
2·3 감독자 워커끼리 직접 대화하게 두고 중계하지 않는다. 결과와 사람 호출 신호만 받는다
4 호출자 / 통합자 test-suite를 직접 불러 채점시키고, 통합 보고서를 쓰고, 팀을 해체한다

PM이 처음부터 끝까지 능동적이면 두 가지가 터진다. 병목: 구현자 둘과 verifier의 모든 대화가 PM을 거치면 PM이 막힌다. 맥락 오염: "5회 실패 후 60초 잠금" 같은 구현 세부가 PM 머릿속에 쌓여 정작 Phase 게이트 판단이 흐려진다.

그래서 PM만 할 수 있는 일(채점표 작성, 게이트 판정, 최종 채점)은 직접 하고, 워커끼리 풀 수 있는 일에서는 물러난다. 리더는 늘 같은 강도로 개입하지 않고, 단계에 따라 개입의 깊이를 조절한다.

3-2. 팀은 언제 갈아끼우나

전환 팀 교체? 이유
Phase 0 → 1 설계 팀 생성 일이 "요구사항 작성"에서 "설계"로 바뀜
Phase 1 → 2 교체 산출물이 문서에서 코드로 바뀜. 설계자가 남아 있으면 구현 중에도 설계를 계속 바꾸려 들고, 동시 4명 상한도 넘는다
Phase 2 → 3 유지 일의 종류가 같음. 남은 FIX·REDO 정리라 쌓인 맥락을 이어 가는 게 이득
Phase 3 → 4 해체 후 단독 채점은 만든 사람들과 섞이면 안 된다
REDO 발생 팀 교체 없이 api-designer만 재호출 설계 전체가 아니라 모순된 계약 부분만 부분 복귀

일의 종류가 바뀌면 팀을 바꾸고, 같은 일이 이어지면 팀을 유지한다. 교체 신호는 Phase 게이트다.

Phase 2와 3의 구성이 같은데 굳이 나눈 이유도 여기서 이해됐다. 팀은 같아도 목표가 다르다. Phase 2는 만들면서 검증, Phase 3은 남은 결함을 0으로. 종료 조건이 다르니 단계를 나눈다.

REDO가 팀 교체 없이 api-designer 한 명만 다시 부르는 것도 2-3절과 이어진다. REDO는 "설계 전체가 틀렸다"가 아니라 "이 경계면의 계약이 모순이다"라서, 그 부분만 고치고 바로 FIX로 돌아온다.


4. 리뷰 팀과 나란히 놓고 보면

리뷰 팀에서 배운 것 구현 팀에서 새로 배운 것
설계 대상 에이전트 한 명 여러 팀이 시간 순서로 도는 워크플로
패턴 하나를 고른다 뼈대 위에 Phase별 엔진을 끼운다
에이전트 분리 기준 관점 관점 + 권한 + 모델 + 책임 단위
판정 설계 심각도 기준을 준다 판정 순서를 준다 (FIX 기본, REDO 예외)
검증 시점 다 만든 뒤 만드는 도중
리더 한 가지 모드 Phase마다 모드 전환
팀 한 번 꾸리고 끝 일의 종류가 바뀔 때 교체
실패 처리 정직한 표기 상한을 넘으면 멈추고 사람을 부름

리뷰 팀이 한 장면을 잘 찍는 법이었다면, 구현 팀은 장면을 이어 붙여 영화를 만드는 법이다. 에이전트 하나하나의 뼈대는 그대로고, 그 위에 언제 깨우고, 언제 해산시키고, 누가 어떤 순서로 판정하고, 어디서 멈추는지를 설계하는 층이 하나 더 생겼다.


5. 실제로 돌리며 확인할 것

  • 패턴 조합: Phase 1에서 설계 3인이 동시에 뜨고, Phase 2로 넘어가며 생성-검증으로 바뀌는지
  • verifier 판정 순서: 어긋남마다 설계 대조를 먼저 하는지, FIX와 REDO의 비율이 어떻게 나오는지, 근거 칸이 채워지는지
  • verifier 권한: 정말 한 줄도 고치지 않는지
  • PM 모드 전환: Phase 2·3에서 PM이 대화에 끼어들지 않고 결과만 받는지
  • 팀 교체 타이밍: 설계 팀이 Phase 2 시작 시점에 해산되고, Phase 2→3에서는 유지되는지
728x90

'인공지능' 카테고리의 다른 글

하네스 엔지니어링이란? - (end)  (0) 2026.11.04
하네스 엔지니어링이란? - (3)  (0) 2026.11.03
하네스 엔지니어링이란? - (1)  (0) 2026.11.01
  Comments,     Trackbacks
하네스 엔지니어링이란? - (1)
728x90

Claude Code 하네스로 코드 리뷰 자동화 팀 만들어 보기

Claude Code에 "코드 리뷰해 줘"라고만 하면 그날그날 보는 관점도, 결과 형식도 달라진다. 그래서 리뷰 담당자를 넷으로 나누고, 각자 권한과 역할을 파일로 못 박는 하네스를 직접 구성해 봤다.


1. 하네스가 뭔지부터

하네스(harness)는 원래 말에 채우는 마구다. 말의 힘은 그대로인데 그 힘을 원하는 방향으로 쓰게 해 준다.

Claude Code에서 말은 Claude, 마구는 프로젝트의 .claude/ 폴더다. "누가, 무엇을 기준으로, 어떤 순서로 일하라"를 마크다운 파일로 미리 적어 두는 것. 프로그램을 짜는 게 아니라 조직 문서를 쓰는 일에 가깝다.

.claude/
├── agents/   ← 담당자 소개서 (누가 일하는가)
└── skills/   ← 업무 매뉴얼, 팀장 지침 (어떻게, 어떤 순서로)

이번에 만든 건 코드 리뷰를 맡는 4인 팀이다.


2. 처음에 헷갈렸던 것: 예제 저장소 구조

참고한 예제 저장소를 열었을 때 첫 폴더에는 에이전트가 static-analyzer.md 하나뿐이었다. 나머지 담당자는 어디 갔지? 한참 찾았다.

알고 보니 저장소는 완성된 프로젝트 하나가 아니라 진행 단계마다 찍어 둔 스냅샷 모음이었다. 레고 설명서의 단계별 그림처럼, 다음 폴더에 가야 나머지 셋이 합류해 있고 static-analyzer는 그대로 복사되어 같이 들어 있다.

단계 하는 일 이 시점의 .claude/
1 대표 에이전트 1명 정의 파일 뜯어보기 agents 1개
2 나머지 3명 합류, 4인 팀 완성 agents 4개
3 팀장(오케스트레이터) 스킬 작성 agents 4개 + skill 1개
4 버그 심은 PR에 팀 적용 위와 동일

이걸 이해하고 나니 나머지가 술술 읽혔다.


3. 4인 리뷰 팀 구성

에이전트 모델 도구 맡은 일 산출물
static-analyzer haiku Read, Grep, Glob, Bash 린트·타입·복잡도·중복 등 규칙 기반 분석 _workspace/review/01_static.md
design-reviewer opus Read, Grep, Glob 책임 분리, 경계면, 의존성 방향, 명명 _workspace/review/02_design.md
security-auditor sonnet Read, Grep, Glob, Bash OWASP Top 10, 시크릿 노출, 의존성 취약점 _workspace/review/03_security.md
refactorer opus Read, Grep, Glob, Edit 앞 세 보고서를 종합해 수정안(patch) 생성 _workspace/review/04_refactor.md + _workspace/patches/*.diff

표를 만들어 놓고 보니 설계 의도가 두 가지로 보였다.

3-1. 모델 차등: 일의 난이도에 맞춰 급을 나눈다

도구 출력을 옮기는 기계적인 일은 가벼운 haiku, 보안 판단은 sonnet, 두 파일을 교차해 설계를 따지거나 상충하는 의견을 조율해 수정안을 만드는 깊은 판단은 opus. 비용과 품질을 담당자 단위로 조절하는 구조다.

3-2. 권한 차등: tools 한 줄로 행동을 물리적으로 막는다

  • Bash는 분석 도구를 실제로 돌려야 하는 static-analyzer, security-auditor만 받는다.
  • design-reviewer는 Bash조차 없다. 오직 읽고 판단한다.
  • Edit은 refactorer만 받는다. 그것도 _workspace/patches/*.diff만 고치도록 본문에서 한정한다.
  • 아무도 git 권한이 없다. 자동 커밋은 구조적으로 불가능하다.

"리뷰어는 코드를 고치지 마라"를 부탁하는 게 아니라 아예 도구를 안 줘서 못 하게 만든다. 고객사 서버 계정에 최소 권한만 주는 것과 똑같은 원리라 바로 와닿았다.


4. 에이전트 파일은 같은 뼈대에 칸만 갈아끼운다

네 파일을 나란히 펼쳐 보니 생김새가 전부 같았다. 양식 하나만 이해하면 나머지는 칸 속 내용만 읽으면 된다.

---
name:        # 담당자 이름 (호출할 때 쓰는 식별자)
description: # 무엇을 하는지 + 트리거 키워드 (언제 불러야 하는지)
model:       # haiku / sonnet / opus
tools:       # 쓸 수 있는 도구 목록 (권한)
---

# 핵심 역할           ← 한 문장 정체성과 범위
## 작업 원칙          ← 판단 기준
## 입출력             ← 무엇을 받아서 어디에 어떤 형식으로 내놓는가
## 팀 통신 프로토콜    ← 누구에게 받고 누구에게 넘기는가
## 에러 핸들링         ← 도구나 입력이 없을 때 어떻게 하는가
## 자체 검증 체크리스트 ← 제출 전 셀프 점검
# 경계                ← 절대 하지 않는 일

칸마다 왜 있는지 정리해 봤다.

  • description: 사람이 아니라 Claude가 읽는다. 메인 세션은 이 설명과 트리거 키워드("보안 감사", "취약점" 등)를 보고 일을 누구에게 맡길지 고른다.
  • 작업 원칙: 판단이 매번 흔들리지 않게 고정한다. static-analyzer는 "추론보다 도구 출력을 신뢰한다", "확신 없으면 P2로 낮춘다"가 원칙이다.
  • 입출력: 출력 경로와 형식을 못 박아야 다음 사람이 받아서 합칠 수 있다. 넷 다 P0/P1/P2 등급, 파일·라인, 근거, 권장 형식을 쓴다.
  • 에러 핸들링: "도구가 없으면 지어내지 말고 '도구 부재'라고 적어라." 조용히 틀리는 걸 막는 칸이다.
  • 자체 검증 체크리스트: "모든 발견에 파일·행 번호가 있는가", "Edit·Write 호출 0건인가" 같은 제출 전 점검.
  • 경계: 마지막에 굵게 한 번 더. "코드를 편집하지 않는다."

5. 담당자별 관전 포인트

static-analyzer — 사실을 깔아 주는 담당

tsc --noEmit, eslint, npm run lint의 원본 출력을 근거로 인용한다. PR diff에 포함된 파일·라인만 보고 범위를 넓히지 않는다. 수정 아이디어가 떠올라도 직접 고치지 않고 refactorer에게 넘긴다.

security-auditor — 근거 번호를 붙이는 감사관

모든 발견에 CWE 번호를 붙인다(SQL 인젝션 = CWE-89). npm audit, semgrep을 쓰되 semgrep이 없으면 시크릿 패턴 정규식 Grep으로 대체하고 "도구 부재"를 명시한다. 네트워크가 막혀 npm audit이 실패하면 "오프라인 — 의존성 취약점 미확인"으로 적는다. 폐쇄망 고객사 환경을 생각하면 꼭 필요한 처리다.

design-reviewer — 두 파일을 동시에 보는 설계 검토자

핵심은 "두 파일 동시 읽기"다. API 핸들러와 그걸 호출하는 훅을 같이 열어 계약이 맞는지 본다. 파일 하나만 보면 절대 안 보이는 버그가 있어서다. 점검 기준은 경계면 7패턴.

  1. 응답 래핑 불일치 ({ user } vs user)
  2. 케이스 변환 불일치 (user_id ↔ userId)
  3. 파일 경로 ↔ 링크 변환 누락
  4. 상태 전이 누락 (loading / error / success)
  5. API ↔ 훅 매핑
  6. 즉시 응답 ↔ 비동기 (동기인 줄 알았는데 Promise 반환)
  7. 옵셔널 필드 처리 (?:인데 직접 접근)

refactorer — 유일하게 손대는 담당, 그래서 제약도 가장 많다

  • 앞 세 보고서를 종합해 P0에만 patch를 만든다. P1·P2는 "다음 PR 권고"로 글만 남긴다.
  • 리뷰어끼리 의견이 충돌하면 trade-off를 한 줄로 적고 한쪽을 고른다.
  • 생성-검증 루프 3회 상한: patch를 만들면 리뷰어가 재검증, 거절당하면 최대 2회 재시도, 3회째도 거절이면 사람에게 넘긴다.
  • 앞 보고서가 하나라도 없으면 추측으로 patch를 만들지 않는다.

6. 팀장: code-review-team 스킬의 6 Phase

네 명을 지휘하는 팀장은 .claude/skills/code-review-team/SKILL.md로 만든다. 팀장이 에이전트가 아니라 스킬인 게 처음엔 이상했는데, 다른 에이전트를 부르고 팀을 꾸리는 일은 메인 세션이 해야 하기 때문이다. 메인 세션이 이 스킬을 읽는 순간 팀장이 된다.

Phase 하는 일
0. 입력 수집 gh pr diff로 PR diff를 받아 _workspace/input/pr-{N}.diff에 저장
1. 팀 생성 TeamCreate로 팀을 만들고 4명 합류 (worktree로 작업 공간 격리)
2. 작업 배정 TaskCreate로 작업 4개 생성. 리팩토링은 앞 3개에 depends_on
3. 팬아웃 리뷰어 3명 병렬 실행. 서로 SendMessage로 교차 발견 공유 (리더 미경유)
4. 생성-검증 루프 refactorer가 patch 생성, 리뷰어 재검증, 최대 3회
5. 통합·게시·정리 보고서 4개를 P0→P1→P2 순으로 합쳐 review_report.md 작성, PR 코멘트 게시, 팀 해체 (_workspace/는 보존)

원칙 세 가지가 인상적이었다.

  • 리더 무발화: 팀장은 부르고 모으기만 한다. 리뷰 내용은 전부 워커가 쓴다.
  • 자동 커밋 금지: PR 코멘트까지만. 커밋·머지는 안 한다.
  • workspace 보존: 팀은 해체해도 중간 보고서는 남겨 사람이 다시 볼 수 있게 한다.

주의할 점도 있다. 예제의 SKILL.md에서 parseDiff, waitForTeamCompletion, mergeReports, $ 네 가지는 실제 도구가 아니라 의사 함수다. 그대로 복사해 쓰려면 이 부분은 직접 구현해야 한다.


7. 버그를 심은 PR로 시험하기

팀이 제 역할을 하는지 보려면 정답지가 있는 코드가 필요하다. 예제에서는 JWT 리프레시 기능 PR에 버그 4종을 일부러 심는다.

버그 주로 잡아야 할 담당
SQL 인젝션 security-auditor
N+1 쿼리 여러 담당 (교차 발견)
경계면 불일치 (API는 객체를 주는데 훅은 배열 기대) design-reviewer
테스트 0건 static-analyzer / refactorer

볼 포인트는 각 담당자가 자기 몫을 잡는지, 그리고 남의 영역을 침범하지 않는지다. 특히 경계면 불일치는 파일 하나만 보는 리뷰로는 놓치기 쉬워서 design-reviewer의 존재 이유가 드러나는 버그다.


8. 실제 프로젝트에 돌려 보기

예제 저장소에는 .claude/ 정의 파일과 문서만 있고 리뷰 대상 코드와 결과 파일은 없었다. 그래서 내가 만들고 있던 테스트 자동화 도구 D:\common_tester(Python API + React 화면 + Playwright, 약 2,500줄)를 리뷰 대상으로 삼았다.

8-1. 첫 번째 삽질: 다른 드라이브 폴더 붙이기

하네스 폴더에서 Claude Code를 켠 상태로 claude --add-dir /d/common_tester를 입력했는데 아무 일도 안 일어났다. 이건 새 세션을 시작할 때 터미널에서 쓰는 옵션이라, 이미 열린 세션의 입력창에 치면 그냥 대화 글로 취급된다.

# 터미널에서 새로 시작할 때
cd 하네스_폴더
claude --add-dir /d/common_tester
# 이미 열린 세션 안에서는 슬래시 명령으로
/add-dir D:/common_tester

8-2. 1차 리뷰: 한 줄로 시켰더니 알아서 병렬로 돌았다

폴더를 붙이고 이렇게만 입력했다.

에이전트를 이용해서 해당 프로젝트를 리뷰해줘

메인 세션이 리뷰 범위를 먼저 훑어서(이전 버전 스냅샷 폴더는 알아서 제외) static-analyzer, design-reviewer, security-auditor 세 명을 백그라운드로 동시에 띄웠다. 각자 1분 남짓 걸렸고 전체는 3분 정도. 결과를 모아 우선순위별로 정리해 줬다.

등급 건수 대표 내용
P0 1 LLM이 생성한 테스트 코드를 격리 없이 서버 권한으로 실행
P1 4 대상 URL 검증 부족(SSRF), API 인증 없음, 같은 판정 로직이 두 곳에서 다르게 동작, 실행 폴더 경로를 여러 모듈이 각자 조립
P2 8 부분 문자열 비교로 인한 오탐, 조회(GET) 요청이 파일을 씀, 제품 전용 규칙이 범용 코드에 박힘 등
P3 8 shell=True 사용, 안 쓰는 함수, README 링크 오류 등

인상적이었던 건 메인 세션이 리뷰어 보고서를 그대로 믿지 않고 핵심 지적을 코드로 다시 읽어 교차 확인했다는 점이다. 그리고 static-analyzer의 지적 두 개를 걸러냈다.

  • "이런 OpenAI 모델명은 존재하지 않는다" → 도구 근거 없이 모델의 오래된 지식으로 판단한 것이라 보류
  • "UUID 충돌 위험" → 확률상 무의미하다고 기각

static-analyzer는 model: haiku로 지정되어 있었는데, 셋 중 보고서가 유독 얕았다. 정의 파일의 "도구 우선, 추론보다 도구 출력 신뢰" 원칙과 달리 도구를 거의 안 돌리고 추론으로 채운 결과였다. 싼 모델에 맡길수록 원칙이 지켜지는지 검증하는 단계가 꼭 필요하다는 걸 체감했다.

8-3. 두 번째 삽질: 세션 중에 폴더를 옮기면 에이전트가 사라진다

정리하려고 하네스 파일들을 harness/code-review/ 아래로 옮겼더니, 그 세션에서는 리뷰 에이전트를 더 이상 부를 수 없게 됐다. 에이전트 정의는 세션 시작 시점에 읽어 들이기 때문이다. 옮긴 위치에서 새 세션을 열어야 한다.

cd 하네스_폴더/code-review
claude --add-dir /d/common_tester

8-4. 2차 리뷰: 이번엔 도구를 실제로 돌렸다

같은 세션에서 "다시 한 번 리뷰해봐"라고 하자, 메인 세션은 커스텀 에이전트를 못 부르는 대신 일반 에이전트에게 각 정의 파일 내용을 그대로 읽혀 같은 역할을 맡기는 우회로를 택했다. PR diff가 없으니 리뷰 대상 파일 목록(_workspace/input/scope.md)을 만들어 입력으로 넘겼고, 보고서는 정의대로 _workspace/review/01~03_*.md에 저장했다.

이번 결과는 정의 파일의 칸들이 실제로 작동하는 게 눈에 보였다.

  • static-analyzer: py_compile, tsc --noEmit을 실제로 돌려 둘 다 통과. eslint와 pyflakes는 설치가 안 되어 있어 "도구 부재"로 정직하게 표기했다. 에러 핸들링 칸이 그대로 작동한 것이다. 1차 때의 근거 없는 지적도 사라졌다.
  • security-auditor: npm audit을 돌려 의존성 취약점 0건 확인. pip-audit과 semgrep은 없어서 "도구 부재"로 적고, 정의대로 정규식 Grep과 코드 읽기로 대체했다. 모든 발견에 CWE 번호가 붙었고, 이상 없음을 확인한 항목도 "거짓 양성 방지용 기록"으로 따로 남겼다.
  • design-reviewer: P1 3건, P2 11건. 1차 때보다 깊었다. 특히 두 파일 동시 읽기로만 잡히는 문제를 찾아냈다. 실행 엔진, 결과 집계, 화면 세 곳에서 같은 failed라는 이름이 서로 다른 의미로 쓰여서 한 화면에 "0 실패"와 "1개 실패"가 동시에 뜰 수 있다는 것, 그리고 건너뛴(skipped) 테스트가 통과(passed)로 집계될 수 있다는 상태 전이 누락. 메인 세션이 후자를 코드로 직접 확인해 사실로 판정했다.
  • 보고서마다 [cross:design], [cross:static] 같은 교차 도메인 태그가 붙었다. 팀 모드가 아니라 실제 SendMessage는 오가지 않았지만, 정의 파일의 "팀 통신 프로토콜" 칸이 다른 담당자에게 넘길 거리를 표시하는 형태로 나타났다.

8-5. 가장 큰 배움: 권한 제한은 "정식 등록"됐을 때만 작동한다

2차 리뷰에서 static-analyzer가 01_static.md를 스스로 파일로 써 버렸다. 정의 파일의 tools에는 Write가 없는데도.

이유는 8-4의 우회로에 있었다. 커스텀 에이전트로 정식 로드된 게 아니라 일반 에이전트가 정의 파일을 글로 읽고 흉내만 낸 것이라, tools 줄이 실제 권한 제한으로 적용되지 않은 것이다. 본문의 "Write 금지"는 그저 부탁이 되어 버렸다.

앞에서 정리한 "권한은 지시가 아니라 도구 제한으로 막는다"는 원칙을 반대 방향에서 확인한 셈이다. tools 제한이 힘을 가지려면 에이전트가 .claude/agents/에서 정식으로 로드되어야 하고, 그러려면 정의 파일이 있는 위치에서 세션을 새로 열어야 한다.

8-6. 기타 팁

  • 리뷰어들은 정의대로 코드를 한 줄도 고치지 않았다. 보고서만 냈다.
  • 대화 기록은 Claude Code에서 /export로 텍스트 파일로 내보낼 수 있다. 다만 리뷰 대화에는 프로젝트 경로와 취약점 내용이 그대로 들어 있으니 외부 공유 전에 꼭 훑어봐야 한다.
  • refactorer 단계(P0 patch 생성)는 이번 글을 쓰는 시점에 아직 진행 중이었다. 결과는 다음 글에서 정리할 예정이다.

9. 정리

  1. 하네스는 조직 설계다. 한 명에게 다 시키지 않고 관점별로 담당자를 나눈다. 정적 분석, 설계, 보안, 수정.
  2. 에이전트 파일은 같은 뼈대에 칸만 갈아끼운다. 하나를 제대로 이해하면 나머지는 읽힌다.
  3. model과 tools 두 줄이 담당자의 급과 권한을 정한다. 권한은 지시가 아니라 도구 제한으로 막는다.
  4. 팀장은 부르고, 기다리고, 모으기만 한다. 순서(depends_on), 병렬(팬아웃), 반복 상한(3회), 결과 보존까지가 팀장의 일이다.
  5. 실패를 드러내게 설계한다. 도구 부재 표기, 추측 금지, 애매하면 P2. 조용히 틀리는 게 가장 위험하다.
  6. 권한 제한은 정식 로드돼야 힘을 갖는다. 정의 파일을 글로 읽혀 흉내 내게 하면 tools 제한은 부탁으로 전락한다. 정의 파일 위치에서 세션을 새로 열자.
  7. 싼 모델일수록 검증 단계가 필요하다. haiku 담당자는 원칙을 건너뛰기 쉬웠고, 메인 세션의 교차 확인이 그걸 걸러 줬다.

다음엔 refactorer의 patch 결과를 확인하고, static-analyzer 모델을 올렸을 때 보고서가 얼마나 달라지는지 비교해 볼 생각이다.


참고

728x90

'인공지능' 카테고리의 다른 글

하네스 엔지니어링이란? - (end)  (0) 2026.11.04
하네스 엔지니어링이란? - (3)  (0) 2026.11.03
하네스 엔지니어링이란? - (2)  (0) 2026.11.02
  Comments,     Trackbacks
[TIL]SFTP로 받은 tar 이미지를 Nexus에 올리고 Kubernetes에 적용하기
728x90

보기에 앞서 저번에 여러툴로 서버에 파일을 올리는 여러 방법을 알아봤었다. 이번에는 올리고 나서 쿠버네티스를 이용하여 이미지 패치를 하는 방법을 알아보고자 한다. PC에 이미지화된 압축파일이 존재하고 그걸 서버에 올리는것부터 시작한다.


SFTP로 받은 tar 이미지를 Nexus에 올리고 Kubernetes에 적용하기

컨테이너 이미지가 tar 파일로 전달되면 서버에 파일을 전송하고, nerdctl로 이미지를 적재한 뒤 레지스트리에 올린다. 마지막으로 Deployment의 이미지 주소를 변경하고 새 Pod가 정상 기동하는지 확인한다.

1. 압축 해제

분할 압축파일은 모든 조각을 같은 폴더에 둔다. .7z.001 파일을 7-Zip으로 압축 해제하면 나머지 조각을 이어서 읽는다. 아래에서는 압축 해제로 생성된 파일을 pipeline-api.tar, react-app.tar라고 가정한다.

2. SFTP로 tar 파일 전송

SSH로 deploy 서버에 접속해 업로드할 폴더를 만든다.

mkdir -p /root/smg

그다음 SFTP 세션을 열어 파일을 전송한다. SSH 터미널에서 실행하는 서버 명령과 SFTP 세션에서 실행하는 파일 전송 명령은 구분해야 한다.

명령형 SFTP 세션에서는 다음 순서로 작업한다.

pwd
cd /root/smg
lcd "C:\path\to\image-files"
lls
put "pipeline-api.tar"
put "react-app.tar"
ls

cd는 서버의 업로드 폴더로 이동하고, lcd는 내 PC에서 tar 파일이 있는 폴더로 이동한다. lls로 PC의 파일을 확인한 뒤 put으로 서버에 올린다. 마지막 ls는 서버의 /root/smg에 두 파일이 보이는지 확인하는 명령이다.

파일 창을 사용하는 SFTP 도구라면 서버 쪽 폴더를 /root/smg로 연 다음 PC의 tar 파일 두 개를 그 폴더로 전송한다. 도구별 SFTP 화면을 여는 방법은 SSH와 SFTP 사용법에 정리돼 있다.

전송이 끝나면 SSH 터미널에서 파일명과 크기를 다시 확인한다.

ls -lh /root/smg/pipeline-api.tar /root/smg/react-app.tar

3. 변경 전 이미지 주소 기록

kubectl config current-context

kubectl get deployment -n trb-app \
  trb-app-pipeline-api trb-app-react-app \
  -o yaml > /root/smg/deployment-before-patch.yaml

저장한 YAML의 spec.template.spec.containers[].image 값은 복구할 때 사용한다.

4. tar 이미지 적재

cd /root/smg

nerdctl load -i pipeline-api.tar
nerdctl load -i react-app.tar

nerdctl images | grep 20261006

nerdctl images에 표시된 이미지 이름과 태그를 확인한다. tar 파일명과 이미지 이름은 별개이므로, 다음 명령의 원본 이미지 주소는 이 출력값을 기준으로 작성한다.

5. Nexus에 이미지 업로드

적재된 이미지에 Nexus push 주소를 붙인다. 아래 예시에서 push 주소는 :50000이다.

nerdctl tag harbor.console.bok-trb.okestro.cloud/trb/pipeline-api:20261006-164837 \
  nexus.okestro-k8s.com:50000/trb-app/pipeline-api:20261006-164837

nerdctl tag harbor.console.bok-trb.okestro.cloud/trb/react-app:20261006-164836 \
  nexus.okestro-k8s.com:50000/trb-app/react-app:20261006-164836

nerdctl login nexus.okestro-k8s.com:50000

nerdctl push nexus.okestro-k8s.com:50000/trb-app/pipeline-api:20261006-164837
nerdctl push nexus.okestro-k8s.com:50000/trb-app/react-app:20261006-164836

push가 완료되면 Nexus에서 두 이미지의 이름과 태그를 확인한다.

6. Deployment 이미지 변경

Deployment에는 Kubernetes가 이미지를 가져오는 pull 주소 :55000을 지정한다.

kubectl set image deployment/trb-app-pipeline-api \
  pipeline-api=nexus.okestro-k8s.com:55000/trb-app/pipeline-api:20261006-164837 \
  -n trb-app

kubectl set image deployment/trb-app-react-app \
  react-app=nexus.okestro-k8s.com:55000/trb-app/react-app:20261006-164836 \
  -n trb-app

7. 배포 확인

kubectl rollout status deployment/trb-app-pipeline-api -n trb-app
kubectl rollout status deployment/trb-app-react-app -n trb-app

kubectl get deployment -n trb-app \
  trb-app-pipeline-api trb-app-react-app \
  -o 'custom-columns=NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image'

kubectl get pods -n trb-app -o wide

Deployment의 이미지 주소가 의도한 태그인지 확인하고, 새 Pod의 READY 상태와 로그를 확인한다.

kubectl logs -n trb-app deployment/trb-app-pipeline-api --tail=100
kubectl logs -n trb-app deployment/trb-app-react-app --tail=100

이미지 변경으로 대상 Deployment의 Pod는 롤링 업데이트된다. 추가 재기동이 필요한 경우에는 영향 범위를 확인한 뒤 실행한다. 다음 명령은 trb-app 네임스페이스의 모든 Deployment를 재기동한다.

kubectl rollout restart deployment -n trb-app

문제가 발생하면 앞서 저장한 YAML에서 기존 이미지 주소를 확인하고 해당 Deployment에 다시 지정한다. 특히 추가 재기동 후에는 단순 rollout undo의 직전 리비전이 이미지 변경 전 상태와 다를 수 있으므로, 기록해 둔 이미지 주소를 기준으로 복구한다.

728x90
  Comments,     Trackbacks