AI

AI에게 맡길 것과 맡기지 말 것

들어가며요즘 개발하면서 코드를 직접 타이핑하는 시간이 눈에 띄게 줄었습니다. 디버깅도, 로직 작성도 상당 부분을 AI에게 맡기고 있고, 솔직히 그게 더 빠르고 정확할 때가 많습니다. 그러다 문득 이런 생각이 들었습니다. 그럼 앞으로 개발자는 뭘 하는 사람이 되는 걸까.경력이 쌓이다보니 이 질문이 남 얘기 같지 않더라고요. 그래서 제 나름대로 정리한 생각을 공유해보려 합니다. 정답은 아니고, 지금 시점에서 제가 그어놓은 선입니다.바뀐 건 역할이 아니라 작업의 단가처음엔 저도 개발자의 역할이 바뀌었다고 생각했습니다. 그런데 조금 더 파고들어 보니 정확한 표현은 아니었습니다.바뀐 건 역할이 아니라 작업의 단가입니다.코드를 타이핑하고, 스택트레이스를 눈으로 훑고, 보일러플레이트를 짜는 일. 이건 원래도 개발자의..

잡담

방통대 4학년 1학기 후기 및 기말고사 후기

컴퓨터의 이해형성평가1학년 과목이지만, 운영체제 / 데이터베이스 / 논리회로 / 보안 같은 파트를 폭 넓게 찍먹할 수 있는 과목이라고 생각합니다, 강의 내용 또한 크게 어려운 편은 아니라고 생각합니다. 쉬운 내용에 비해 성적은 크게 높지는 않지만 전반적인 컴퓨터에 대해 알아갈수있는 과목이였습니다.중간평가중간과제물 제출로 꿀이라고 생각했습니다. 다만 교재를 구입하지 않고있다가 과제물 문제가 책 페이지를 참고하라는 내용이고 서술할때 책 페이지를 작성해야해서 부랴부랴 이북을 구매해서 과제를 제출했습니다.기말평가기출문제에서 어느정도 나온다고 하길래 3개년도 문제풀이로 했고 워크북과 같이 공부했습니다. 공부 시간은 그렇게 크게 할애하지 않았는데 시험 당일 문제를 보니 다 맞을것 같다고 생각했습니다. 하지만 막상 ..

AI

장기 실행 애플리케이션 개발을 위한 하네스 설계

지난 글에는 "하네스 엔지니어링이란 AI가 삽질하지 않도록 가드레일을 치는 일"이라고 정리했습니다. 그런데 막상 직접 하네스를 짜기 시작하면 곧바로 다음 질문에 부딪힙니다. 가드레일을 어디에, 어떤 모양으로 쳐야 하는가? 한 줄짜리 규칙을 AGENTS.md에 추가하는 것과, 에이전트 여러 개를 엮어 4시간짜리 자율 빌드를 돌리는 것은 완전히 다른 문제입니다.최근 Anthropic이 공개한 장기 실행 에이전트 하네스 실험을 읽으면서, 제가 평소에 멀티 에이전트로 작업하며 어렴풋이 느끼던 것들이 깔끔하게 언어화되는 경험을 했습니다. 이번 글은 그 칼럼의 핵심 원리를, 제가 실제로 에이전트를 굴리며 겪은 일들과 겹쳐서 정리해보려고 합니다.단일 에이전트가 무너지는 두 지점AI한테 "이거 만들어줘"라고 한 번에 ..

CS/알고리즘

[알고리즘] DFS (깊이 우선 탐색)

그래프나 트리를 탐색할 때 우리는 어떤 순서로 노드를 방문해야 할까요? 미로에서 길을 찾을 때 "일단 한 길을 끝까지 가보고, 막히면 돌아와서 다른 길을 시도한다"는 생각을해볼것같아요.이것이 바로 DFS(깊이 우선 탐색, Depth-First Search)의 핵심 아이디어라고 합니다.DFS는 그래프 탐색의 가장 기본적인 알고리즘 중 하나로, 백준이나 프로그래머스 같은 코딩 테스트에서 매우 자주 등장합니다. 특히 "경로 찾기", "연결 요소 개수 구하기", "사이클 판별" 같은 문제에서 필수적으로 사용됩니다.DFS란?DFS는 한 방향으로 끝까지 탐색한 후, 더 이상 갈 곳이 없으면 되돌아와서 다른 경로를 탐색하는 알고리즘입니다.기본 원리?시작 노드를 방문하고 방문 표시를 합니다.현재 노드와 인접한 미방문 ..

CS/자료구조

Redis Sorted Set - 이중 구조, 왜 이렇게 설계했을까?

들어가며Redis는 String, List, Hash, Set, Sorted Set 이렇게 다섯 가지 기본 자료구조를 제공합니다. 이 중에서 Sorted Set은 가장 강력하면서도 오해받기 쉬운 자료구조입니다. 단순히 "정렬된 Set"이라고 생각하기 쉽지만, 실제로는 Skip List와 Hash Table이 결합된 복합 구조이며, 이 설계 덕분에 다른 자료구조로는 어려운 연산들을 효율적으로 수행할 수 있습니다.이 글에서는 Sorted Set의 정의부터 내부 구현, 다른 자료구조와의 비교, 트레이드오프까지 학습한 내용을 차근차근 정리해보겠습니다.Sorted Set이란?Sorted Set은 member(고유한 문자열)와 score(부동소수점 숫자)의 쌍으로 구성된 집합입니다.member는 유일(unique)..

AI

작은 자동화가 주는 큰 편함: CLAUDE.md 자동 생성 플러그인 만들기

Claude Code를 사용하면 CLAUDE.md 파일을 작성해야 하고 이 파일을 "잘" 작성해야 효율적인 컨텍스트 관리와 재작업을 줄일 수 있습니다.매번 새로운 프로젝트를 할 때마다 작성하는데 기술 스택은 뭔지, 커밋 컨벤션은 어떻게 할 건지, 테스트 전략은 뭔지 — 매번 손으로 작성하거나, 기존 프로젝트에서 복붙하고 조금씩 고쳐야 합니다. 규칙 파일, 스킬 파일도 마찬가지입니다.그래서 ! 어느 순간 이 반복이 피로하게 느껴졌어요.왜 귀찮은가/init 명령어도 있긴 합니다. 하지만 결과물이 너무 generic합니다. 프로젝트 특성을 거의 반영하지 못하고, 결국 손으로 다듬는 시간이 따로 발생합니다.대충 머릿속으로 원하는 방향을 떠올렸을 때 아래와 같았어요.프로젝트 구조를 스스로 읽습니다.어떤 스택인지,..

AI

AI에게 맡길 것과 맡기지 말 것

들어가며

요즘 개발하면서 코드를 직접 타이핑하는 시간이 눈에 띄게 줄었습니다. 디버깅도, 로직 작성도 상당 부분을 AI에게 맡기고 있고, 솔직히 그게 더 빠르고 정확할 때가 많습니다.

그러다 문득 이런 생각이 들었습니다. 그럼 앞으로 개발자는 뭘 하는 사람이 되는 걸까.

경력이 쌓이다보니 이 질문이 남 얘기 같지 않더라고요. 그래서 제 나름대로 정리한 생각을 공유해보려 합니다. 정답은 아니고, 지금 시점에서 제가 그어놓은 선입니다.

바뀐 건 역할이 아니라 작업의 단가

처음엔 저도 개발자의 역할이 바뀌었다고 생각했습니다. 그런데 조금 더 파고들어 보니 정확한 표현은 아니었습니다.

바뀐 건 역할이 아니라 작업의 단가입니다.

코드를 타이핑하고, 스택트레이스를 눈으로 훑고, 보일러플레이트를 짜는 일. 이건 원래도 개발자의 가치가 아니라 비용이었습니다. 다만 그 비용이 너무 커서 마치 그게 개발자의 일인 것처럼 보였을 뿐이죠. AI는 그 비용을 대폭 낮췄고, 그러자 원래 있던 진짜 일이 드러난 겁니다.

AI 코드가 장애를 내는 순간

상황을 하나 가정해보겠습니다.

사용자 정보에 등급(grade) 필드를 추가해달라는 요청을 받았습니다. 간단한 작업이라 AI에게 맡겼고, 결과물은 훌륭했습니다. 엔티티에 필드가 추가되고, 마이그레이션 스크립트가 생기고, DTO와 응답 스펙까지 깔끔하게 정리되어 나왔습니다. 제가 직접 했으면 30분 걸렸을 일이 3분 만에 끝났습니다.

그런데 여기서 아무도 말해주지 않은 게 있습니다. 그 사용자 정보는 Redis에 캐싱되고 있었습니다.

배포하면 기존 캐시에는 grade가 없는 상태로 남아 있고, 역직렬화 시점에 터지거나 조용히 null이 됩니다. AI가 짠 코드 자체는 완벽했습니다. 하지만 이 엔티티는 캐싱되고 있으니 캐시 키 버전을 올리거나 무효화해야 한다는 건 짚어주지 않았습니다.

왜냐하면 그건 코드에 안 적혀 있기 때문입니다. 캐싱 설정은 다른 모듈에 있고, 캐시 TTL 정책은 인프라 담당자와 협의한 내용이고, 예전에 비슷한 필드를 추가하다 장애가 났던 이력은 회고 문서에만 있습니다.

이 작업의 실제 가치는 코드 작성이 아니라 다음 세 가지였습니다.

  • 이 엔티티가 캐싱된다는 구조를 알고 있던 것
  • 필드 추가가 캐시 정합성에 영향을 준다는 연결고리를 떠올린 것
  • 배포 순서(캐시 무효화 → 앱 배포)까지 책임지고 챙긴 것

AI는 요청한 코드가 이렇게 완성됐다는 것까지만 알려줍니다. 그런데 이거 배포하면 캐시 때문에 터질 텐데요, 라고 말해주진 않습니다. 그 질문을 던지는 게 개발자의 일이라고 생각합니다.

나의 생각

주관적인 저의 생각으로는, 개발자라는 직군이 통째로 사라지기보다는 그 안에서 무게중심이 옮겨갈 것 같습니다.

가장 먼저 흔들리는 건 명세를 받아 그대로 구현하던 자리라고 봅니다. 티켓을 받아 코드를 짜는 일은 AI가 제일 잘하는 구간이고, 이미 상당 부분 대체되고 있습니다. 여기에만 머물면 수요가 줄고 단가가 내려갈 수밖에 없습니다. 물론 없어지진 않겠지만, 예전만큼 안전한 자리는 아니라고 생각합니다.

반대로 문제를 정의하고 검증하는 일은 오히려 수요가 늘어날 것 같습니다. 이건 낙관론이 아니라 단순한 경제학이라고 생각하는데, 생성이 싸지면 검증이 비싸지기 때문입니다. AI가 코드를 100배 빨리 뽑을수록 이게 맞는 코드인가를 판단하는 지점이 병목이 됩니다. 하루에 PR이 3개 올라오던 팀에 30개가 올라온다고 생각해보면, 그걸 제대로 리뷰할 수 있는 사람의 가치는 올라갈 수밖에 없습니다.

그리고 가장 안전한 자리는 결국 시스템 전체를 책임지는 사람이라고 봅니다. 프로덕션 장애, 데이터 정합성, 마이그레이션, 성능 트레이드오프 같은 것들이요. AI는 이런 영역에서도 조언은 훌륭하게 해줍니다. 하지만 책임은 지지 못합니다. 새벽 3시에 장애 알림을 받고 롤백할지 말지 판단을 내리는 건 결국 사람입니다.

정리하면 코드를 만들어내는 능력보다, 그 코드가 놓일 자리를 이해하고 결과를 책임지는 능력 쪽으로 무게가 옮겨가는 흐름이라고 생각합니다.

제가 그은 선

맡기는 것

  • 구현 (명확한 스펙이 있을 때)
  • 리팩토링
  • 테스트 코드 작성
  • 라이브러리/API 문서 조회
  • 반복 작업, 보일러플레이트
  • 1차 디버깅 (스택트레이스 해석, 관련 코드 위치 찾기)

이 영역에서는 주저 없이 맡깁니다. 사람이 하는것보다 정확하고 빠르기 떄문입니다.

맡기지 않는 것

왜 이 요구사항인가에 대한 판단
AI는 요구사항 자체의 모순이나 빠진 조건을 지적하지 못합니다. 앞의 캐시 사례처럼요. 시키면 시키는 대로 잘 만들어줍니다. 그게 잘못된 방향이어도요.

틀렸을 때 알아채는 능력
여기가 진짜 위험 구간이라고 생각합니다.

AI가 짠 코드를 제대로 검토하려면, 그 코드를 직접 짤 수 있는 실력이 있어야 합니다. AI에 의존하면서 실력이 정체되면 몇 년 뒤엔 그럴듯한데 틀린 코드를 그냥 통과시키는 사람이 됩니다.

검증 능력은 구현 능력에서 나옵니다. 구현을 완전히 놓으면 검증도 못 하게 됩니다. 이건 순환 구조라서 한번 무너지면 되돌리기 어렵습니다.

시스템과 도메인에 대한 맥락
배치가 새벽에 이 테이블을 건드린다거나, 외부 협력사가 호출 중이라 이 API는 스펙을 못 바꾼다거나, 이 로직은 특정 고객사 요구로 예외 처리된 거라거나 하는 것들이요.

이런 건 코드에 안 적혀 있습니다. 회의와 장애 대응, 협력사 문의 답변에서 쌓입니다. AI가 읽을 수 있는 형태로 존재하지 않는 지식입니다.

그래서 저는 이 맥락을 최대한 문서로 남기려고 합니다. 그러면 AI도 읽을 수 있게 되고, 결과적으로 AI가 더 잘 도와줍니다. 맥락을 쥐고만 있는 것보다 훨씬 낫습니다.

그래서 구체적으로 뭘 해야 하나

두 가지 습관을 지키려고 합니다.

머지 전에 스스로에게 묻기

AI가 작성한 결과물에 대해 충분히 이해했고 처음부터 설명할 수 있나?
설명 못 하면 이해 못 한 겁니다. 이해 못 한 코드를 머지하면 그 순간부터 그 코드는 아무도 책임지지 않는 코드가 됩니다. 시간이 걸리는 일이지만, 이건 낭비가 아니라 실력 유지 비용이라고 생각합니다.

가끔은 일부러 AI 없이 짜보기

근육과 같습니다. 안 쓰면 빠진다고 생각해요.
특히 알고리즘이나 동시성처럼 어려운 부분은 의도적으로 손으로 짜봅니다. 느리고 답답하지만, 이걸 안 하면 AI가 틀렸을 때 알아챌 수 없게 됩니다.

마치며

개발자는 없어지지 않는다고 생각합니다. 다만 코드 작성자에서 시스템 설계자·검증자로 옮겨갑니다.
사실 이건 원래 시니어들이 하던 일이고, AI가 그 문턱을 아래로 내린 것뿐입니다.
가장 큰 리스크는 AI가 일자리를 뺏는 게 아니라, AI에 기대다가 판단력이 자라지 않는 것이라고 생각합니다.

요즘엔 생각이 바뀐것 같습니다. AI를 쓰는 개발자와 안 쓰는 개발자 사이의 격차보다, AI를 쓰면서 성장하는 사람과 AI에 의탁해 정체되는 사람 사이의 격차가 훨씬 클 것 같다고 생각합니다.

잡담

방통대 4학년 1학기 후기 및 기말고사 후기

컴퓨터의 이해

형성평가

1학년 과목이지만, 운영체제 / 데이터베이스 / 논리회로 / 보안 같은 파트를 폭 넓게 찍먹할 수 있는 과목이라고 생각합니다, 강의 내용 또한 크게 어려운 편은 아니라고 생각합니다. 쉬운 내용에 비해 성적은 크게 높지는 않지만 전반적인 컴퓨터에 대해 알아갈수있는 과목이였습니다.

중간평가

중간과제물 제출로 꿀이라고 생각했습니다. 다만 교재를 구입하지 않고있다가 과제물 문제가 책 페이지를 참고하라는 내용이고 서술할때 책 페이지를 작성해야해서 부랴부랴 이북을 구매해서 과제를 제출했습니다.

기말평가

기출문제에서 어느정도 나온다고 하길래 3개년도 문제풀이로 했고 워크북과 같이 공부했습니다. 공부 시간은 그렇게 크게 할애하지 않았는데 시험 당일 문제를 보니 다 맞을것 같다고 생각했습니다. 하지만 막상 결과를 보니 그건 아니였슴니다.

한줄평

컴퓨터에 대해 기초부터 다지기 좋은 과목

알고리즘

형성평가

이관용 교수님 수업은 처음 들었는데, 악명을 이전에 들었지만 개인적으로 나름 괜찮았다. 쉽게 설명해주려고 하는게 느껴졌고, 중간 그래프 강의 부터는 한번 듣고는 이해 할 수 없었지만 여러번 듣다보니 생각보다 괜찮은 수업이라고 생각했다. 그리고 무엇보다 컴퓨터 전공생이라면 들어야하는 과목이라고 생각하여 학습했는데 알고리즘에 대해 한발짝 더 알게된 것 같아 만족했던 수업입니다.

중간평가

학기초 ~ 중반 일정이 바빠서 학기초에 미리 통합수업을 신청하여 들었고 강의해주시는 분은 다른 교수님이였는데 굉장히 지루하고 산만했다. 평가는 과제형태로 나왔으며 3개정도의 문제를 주고 직접 손으로 작성하는 풀이를 요구했습니다. 어떻게 동작하는지 모르는 알고리즘을 손으로 직접 풀면서 이해하게되어 뿌듯했던 기억이 있습니다.

기말평가

알고리즘은 평균점수가 최하로 낮은 과목 중 하나라고 들었어서 공부 시간을 가장 많이 할애했습니다. 본래 현직 개발자로 일하고있지만 기초 알고리즘에 대해서는 부족했다고 생각했기에 단순 점수 획득보다 알고리즘 하나하나를 이해하려고 노력을 많이했습니다. 솔직한 심정으로 공부한 시간에 비해 A+은 못받아 아쉽지만 유명한 알고리즘 동작원리에 대해서는 설명을 할 수준이 된 것으로 만족합니다.

한줄평

무작정 암기는 절대 금물, 이해가 필요한 과목

파이썬 프로그래밍

형성평가

정재화 교수님의 수업은 언제나 준비를 많이 하시고 신경을 엄청 쓰시는게 느껴지는 강의이다. 별도 교재 없는 부분도 좋았고, 나는 프로그래밍 경험이 있는 사람이지만 비전공자나 처음 접하는 사람들도 이해하기 쉽게 강의 구성이 된 것 같다.

중간평가

학기초에 비대면 수업을 바로 신청해서 들었던 것 같고 해당 교수님도 일전에 3학년 비대면 수업때 한번 뵙던 분 같은데 평이하게 강의해주셨다. 다만 개인적으로 눈높이가 상대적으로 너무 낮게 설명 해주시다보니 도움 되는 부분은 크게 없었던것 같다. 평가는 실시간으로 링크를 주시고 풀이하여 제출했던것으로 기억한다.

기말평가

에이 파이썬 그래도 써봤고 기본적인 프로그래밍 지식이 있는데 라고 오만하게 생각하고 임했던 것 같다. 물론 공부를 아예 안한건 아니지만 상대적으로 공부량은 적었다. 물론 교재와 워크북 기출문제 같은 내용이 없었기 때문이기도 했지만 개인적으로 시험을 너무 쉽게 보았던 것 같다. 그냥 오만했다.

한줄평

믿고 듣는 정재화 교수님 강의

컴퓨터 보안

형성평가

지난 1학기 때 깔끔한 강의인 운영체제를 가르친 김진욱교수님의 과목이여서 수강하였는데, 운영체제와 비슷하게 PPT와 강의가 깔끔했다. 딥하게 들어가면 끝도없이 어려워 지는 보안과목이기에 겉핥기 수준으로만 배웠지만 어떤 방식의 암복호화 방식이 있는지 차이점이 무엇인지 옛날에는 어떻게 했고 지금은 어떻게 하는지 등 흥미롭게 들었던 강의이다.

중간평가

비대면 수업으로 다른 교수님이 지도해주셨는데 학기초에 들었어서 그런지 딱히 기억에 남지는 않는다. 과제 형식의 문제 2개를 주고 직접 흐름에 대해 손으로 작성하는 깜지 느낌이였던 과제였다. 깜지라고 표현하긴 했지만 강의 들을 때는 이해가 안되었던 부분이 손으로 직접 그리면서 확인하니 이해에 도움이 많이 되었다.

기말평가

우리가 객관식 문제를 풀다보면 1번아니면 2번인데.. 같은 상황은 분명 몇번 생기기 마련이다. 여지껏 기말고사를 보면서 가장 헷갈린 문제가 많았던 과목이다. 특히나 이번에도 운영체제와 동일하게 OX문제가 나왔고 더 헷갈렸다. 지엽적이라고 해야하나? 난이도가 이렇게 까지 어려운건가? 별에별 생각이 다 들었지만 그냥 내가 공부를 열심히 안한 탓이 더 큰 것 같다.

한줄평

깔끔한 강의 그렇지 못한 기말 시험

모바일 앱 프로그래밍

형성평가

처음 보는 교수님인데 굉장히 산만하시고 정신 없음 그 자체. 처음에는 강의를 되게 재밌게 하시네, 과목에 대해서만 말하는게 아니라 중간중간 딴 애기도 많이하시고.. 이런식으로 유쾌하게 받아드렸고 한 8강부터는 강의에 집중하는게 힘들었다. 그래서 일부로 다른 얘기할때는 일부로 바로바로 넘기면서 봤던 것 같다.

중간평가

알고리즘과 마찬가지로 통합수업으로 신청하여 학기초에 들었고, 지도하는 교수님은 차분해서 좋았다. 수업이 끝나고 바로 문제 링크를 공유해주시고 풀이 후 제출하면 끝나는걸로 기억한다. 특별히 기억에 남는 부분은 없었다.

기말평가

수업은 정신 산만하게 하는데 시험도 요상한 말도 안되는거 내시는거 아니야? 라고 생각했지만 다행히 그러진 않았고 기출 범위와 수업만 착실히 들었으면 어렵지 않은 문제들이 나왔다. 그리고 수업 중 아무리 정신없지만 강조하시는 키워드도 전부 나왔던 것 같다.

한줄평

호불호 있는 강의

세계의정치와 경제

형성평가

사실 초반 5강까지는 과제 떄문에 듣고 나머지는 그냥 틀어두기만 했던 것 같다. (무언가 한쪽으로 치우쳐진 정치 느낌을 많이 받음) 그리고 기말고사 때 필요한 강의만 2개정도 만 듣고 80%까지만 채우고 마무리했다.

중간평가

내 생각에 대해 풀어내는 과제물이었고, 주제에 대해 내 생각대로 해석하는 거라서 크게 어렵지 않았다.

기말평가

기말평가 또한 중간평가와 마찬가지로 나의 생각을 주어진 주제에 대해 작성하는 부분이였고, 초반 강의를 들을 때 정치 성향이 너무 편향되어 보여 색안경이 있으시지 않을까 싶었지만 결과를 보니 그런 부분은 없는 듯 했다.

한줄평

꿀과목 ㅇㅈ

AI

장기 실행 애플리케이션 개발을 위한 하네스 설계

지난 글에는 "하네스 엔지니어링이란 AI가 삽질하지 않도록 가드레일을 치는 일"이라고 정리했습니다. 그런데 막상 직접 하네스를 짜기 시작하면 곧바로 다음 질문에 부딪힙니다. 가드레일을 어디에, 어떤 모양으로 쳐야 하는가? 한 줄짜리 규칙을 AGENTS.md에 추가하는 것과, 에이전트 여러 개를 엮어 4시간짜리 자율 빌드를 돌리는 것은 완전히 다른 문제입니다.

최근 Anthropic이 공개한 장기 실행 에이전트 하네스 실험을 읽으면서, 제가 평소에 멀티 에이전트로 작업하며 어렴풋이 느끼던 것들이 깔끔하게 언어화되는 경험을 했습니다. 이번 글은 그 칼럼의 핵심 원리를, 제가 실제로 에이전트를 굴리며 겪은 일들과 겹쳐서 정리해보려고 합니다.

단일 에이전트가 무너지는 두 지점

AI한테 "이거 만들어줘"라고 한 번에 던지는 방식은 작은 작업에서는 잘 됩니다. 문제는 작업이 길어질 때입니다. 저는 한동안 긴 작업을 그냥 하나의 세션에서 끝까지 밀어붙이려고 했는데, 일정 규모를 넘어가면 두 가지 패턴이 반복적으로 나타났습니다.

첫 번째는 컨텍스트가 차오르면서 에이전트가 길을 잃는 것입니다. 대화가 길어질수록 모델이 앞에서 정한 규칙을 흐릿하게 기억하고, 심한 경우엔 "이쯤이면 다 된 것 같다"며 작업을 서둘러 마무리하려 듭니다. Anthropic은 이걸 컨텍스트 불안(context anxiety) 이라고 불렀습니다. 모델이 자기 컨텍스트 한계가 가까워졌다고 판단하면 남은 일을 대충 접으려 한다는 겁니다. 표현은 처음 봤지만 증상은 너무 익숙했습니다. 분명 더 할 일이 남았는데 에이전트가 "구현을 완료했습니다"라고 선언해버리는 그 순간 말입니다.

두 번째는 자기 평가의 함정입니다. 에이전트한테 "방금 만든 거 검토해봐"라고 시키면, 결과물이 명백히 평범한데도 자신만만하게 칭찬합니다. 자기가 짠 코드를 자기가 채점하니 당연합니다. 특히 디자인처럼 정답이 없는 주관적 영역에서는 통과/실패를 가를 이진 테스트가 없어서 이 편향이 더 심해집니다. 지난 글에서 LangChain이 "에이전트가 됐다고 판단하는 걸 막고 실제로 테스트를 돌리게 강제했다"고 했던 게 바로 이 문제를 건드린 것이었습니다.

이 두 지점을 어떻게 막느냐가 하네스 설계의 절반이라고 생각합니다.

첫 번째 레버: 컨텍스트 리셋

컨텍스트 불안에 대한 직관적인 해법은 컴팩션(compaction)입니다. 대화가 길어지면 앞부분을 요약해서 같은 에이전트가 계속 이어가게 하는 것이죠. Claude Code를 오래 쓰다 보면 자동으로 마주치는 그 동작입니다.

그런데 Anthropic의 관찰이 흥미로웠는데요, 컴팩션은 연속성은 유지하지만 클린 슬레이트를 주지 못한다는 겁니다. 요약을 했어도 "나는 긴 세션을 이어오고 있다"는 맥락 자체는 남아 있어서, 컨텍스트 불안이 계속 따라온다는 것이죠. 그래서 그들이 택한 건 컨텍스트 리셋입니다. 윈도우를 통째로 비우고, "이전 에이전트가 어디까지 했고 다음에 뭘 해야 하는지"를 담은 구조화된 핸드오프 문서만 들고 새 에이전트를 시작합니다. 새 에이전트 입장에선 자기가 막 시작한 것처럼 느끼니 불안이 없습니다.

이 부분에서 제 작업 방식이 떠올랐습니다. 저는 OMC 환경에서 ralph 루프(작업이 끝날 때까지 에이전트를 반복 재기동하는 자율 루프)를 쓰는데, 커뮤니티에서 흔히 "Ralph Wiggum 방식"이라 부르는 그 패턴입니다. 핵심은 결국 같습니다. 하나의 긴 세션을 무한정 늘리는 게 아니라, 상태를 파일에 적어두고 깨끗한 에이전트를 다시 띄우는 것. 핸드오프 문서가 곧 그 에이전트의 기억이 됩니다.

여기서 한 가지 단서가 중요합니다. 컨텍스트 리셋이 필수냐 아니냐는 모델 세대에 따라 달라진다는 점입니다. Anthropic은 Sonnet 4.5에서는 컨텍스트 불안이 너무 강해 리셋이 핵심이었지만, Opus 4.5부터는 이 증상이 대부분 사라져서 리셋 없이 하나의 연속 세션으로 전체 빌드를 돌릴 수 있었다고 합니다. 즉 하네스의 어떤 조각은 "모델이 아직 혼자 못 하는 것"을 메우는 임시 보철이고, 모델이 좋아지면 그 보철은 떼어내도 됩니다. 이건 뒤에서 다시 이야기하겠습니다.

두 번째 레버: 생성기와 평가기를 분리하라

자기 평가의 함정을 푸는 Anthropic의 답은 명쾌합니다. 만드는 에이전트와 평가하는 에이전트를 분리하라. GAN에서 영감을 받은 구조라고 하는데, 생성기(generator)가 결과물을 만들고 독립된 평가기(evaluator)가 회의적인 눈으로 채점합니다.

핵심 통찰은 이겁니다. "생성기를 자기 비판적으로 만드는 것보다, 독립된 평가기를 회의적으로 튜닝하는 게 훨씬 다루기 쉽다." 한 에이전트한테 "잘 만들면서 동시에 냉정하게 까기도 해"라고 요구하는 건 모순적인 주문입니다. 차라리 역할을 둘로 쪼개서, 평가기한테는 "너의 임무는 트집을 잡는 것"이라고 명확히 시키는 편이 낫습니다. 제 OMC 워크플로우로 치면 executor가 짜고 code-reviewer / verifier가 별도 패스로 검증하는 구조와 정확히 같습니다. 실제로 제 글로벌 설정에도 "작성과 리뷰는 분리된 패스로, 같은 컨텍스트에서 자기 승인 금지"라는 규칙을 박아뒀는데, 같은 결론에 다른 경로로 도달한 셈입니다.

그런데 분리만으로는 부족합니다. 평가기가 회의적으로 작동하게 만드는 게 진짜 일입니다. Anthropic은 "기본 상태의 Claude는 형편없는 QA 에이전트"라고 솔직하게 적었습니다. 정당한 이슈를 발견하고도 스스로를 설득해 "별 문제 아니다"며 통과시키고, 피상적으로만 테스트해서 미묘한 버그를 놓친다는 겁니다. 그래서 평가기 로그를 읽으며 판단이 어긋나는 사례를 찾아 QA 프롬프트를 고치는 루프를 여러 번 반복하고 나서야 합리적인 채점이 나왔다고 합니다.

이 대목이 가장 실무적이었습니다. 평가기는 한 번 만들면 끝나는 게 아니라, 로그를 읽고 튜닝하는 대상이라는 것. 검증 에이전트를 붙여놓고 "이제 검증되겠지"라고 안심했다가 똑같이 호구처럼 통과시키는 걸 본 적이 있다면 공감할 겁니다.

주관적 품질을 채점 가능하게 만들기

분리한 평가기에게 무엇을 기준으로 채점하라고 할까요. "이 디자인 예쁘니?"라고 물으면 매번 답이 흔들립니다. Anthropic의 해법이 영리합니다. 질문을 "이게 좋은 디자인 원칙을 따르는가?" 로 바꾸는 겁니다. 아름다움 자체는 점수화할 수 없지만, 디자인 원칙과 선호를 인코딩한 채점 기준(rubric)은 일관되게 적용할 수 있습니다.

그들이 생성기와 평가기 양쪽에 똑같이 준 네 가지 기준이 참고할 만합니다.

  • 디자인 품질: 색·타이포·레이아웃이 하나의 뚜렷한 정체성으로 묶이는가
  • 독창성: 커스텀 결정의 흔적이 있는가, 아니면 "보라색 그래디언트 위 흰 카드" 같은 전형적인 AI 슬롭인가
  • 완성도: 타이포 위계, 간격 일관성, 대비 비율 같은 기술적 실행
  • 기능성: 미학과 별개로, 사용자가 뭘 하는 화면인지 이해하고 주요 액션을 찾을 수 있는가

재미있는 건 가중치입니다. Claude는 완성도와 기능성에서는 원래 점수가 잘 나오는데 디자인·독창성에서 평범했기 때문에, 앞의 두 기준에 가중치를 더 줘서 모델이 미적 모험을 하도록 유도했다고 합니다. 채점 기준이 단순한 측정 도구가 아니라 결과물의 방향을 끌어당기는 핸들로 쓰인 겁니다. 실제로 "최고의 디자인은 뮤지엄 퀄리티"라는 문구 하나가 산출물의 캐릭터를 바꿨다는 대목은, 루브릭의 문구 선택 자체가 프롬프트 엔지니어링이라는 걸 보여줍니다.

저는 이 부분에서 디자인을 넘어 모든 주관적 평가에 적용 가능한 패턴을 봤습니다. 코드 리뷰도 마찬가지입니다. "이 코드 좋아?"가 아니라 "SOLID를 위반하지 않는가, 중복이 있는가, 경계 케이스를 처리하는가"라는 체크 가능한 기준으로 쪼개면, 검증 에이전트의 판단이 훨씬 안정적으로 수렴합니다.

플래너-생성기-평가기, 그리고 덜어내기

Anthropic이 풀스택 빌드에 쓴 최종 구조는 세 개의 에이전트입니다.

  • 플래너(Planner): 한두 문장짜리 프롬프트를 제대로 된 제품 스펙으로 부풀립니다. 단, 세부 기술 구현까지 미리 못박지는 않습니다. 너무 구체적으로 지정하면 그 오류가 하류로 전파되기 때문입니다. 제품 맥락과 상위 설계에 집중하라는 것이죠.
  • 생성기(Generator): 스펙에서 기능을 하나씩 집어 구현하고 git으로 버전 관리합니다.
  • 평가기(Evaluator): Playwright로 실제 사용자처럼 앱을 클릭해보며 UI·API·DB 상태를 검증하고 채점합니다.

처음엔 여기에 "스프린트"라는 단위까지 있었습니다. 생성기와 평가기가 코드 작성 전에 "이번 청크의 완료 정의가 뭔지" 계약(sprint contract)을 협상하고 들어가는 구조였죠. 그런데 이 글에서 제가 가장 좋아하는 부분은 그 다음입니다. Opus 4.6으로 모델이 좋아지자 스프린트 구조를 통째로 들어냈다는 것. 모델이 분해 없이도 2시간 넘게 일관되게 작업을 이어갈 수 있게 되니, 작업을 잘게 쪼개주던 비계가 더 이상 필요 없어진 겁니다.

대신 플래너와 평가기는 남겼습니다. 플래너가 없으면 생성기가 스펙 없이 시작해 빈약한 앱을 만들고, 평가기는 작업이 모델의 단독 능력 _경계_에 있을 때 여전히 값을 하기 때문입니다. 정리하면 이렇습니다. 하네스의 모든 조각은 "모델이 아직 혼자 못 하는 것"에 대한 가정을 담고 있고, 그 가정은 모델이 좋아지면 낡는다. 그래서 새 모델이 나올 때마다 하네스를 다시 뜯어보고, 더는 짐을 지지 않는 조각을 떼어내고, 이전엔 불가능했던 더 큰 일을 노리는 조각을 붙이는 게 옳은 작업 방식입니다.

가장 마음에 남은 한 문장

비용 이야기도 솔직합니다. 레트로 게임 메이커를 만드는 실험에서, 단일 에이전트는 20분에 9달러였고 풀 하네스는 6시간에 200달러였습니다. 20배 넘게 비쌌습니다. 대신 단일 에이전트 버전은 화면은 그럴듯한데 정작 게임이 안 돌아갔고(엔티티가 입력에 반응하지 않음), 하네스 버전은 실제로 플레이가 가능했습니다. 하네스는 공짜가 아니고, 모델이 혼자 충분히 해내는 작업이라면 순전한 오버헤드라는 것까지 분명히 합니다.

그래서 이 칼럼이 남긴 가장 중요한 문장은 결과 자랑이 아닌 아래와 같다고 생각합니다.

모델이 향상되더라도 흥미로운 하네스 조합의 공간은 줄어들지 않고 이동한다. AI 엔지니어의 과제는 다음의 새로운 조합을 계속 찾아내는 것이다.

지난 글에서 저는 "코드는 AI가 쓸 수 있지만, 무엇을 왜 어떻게 검증할지는 우리 몫"이라고 마무리했습니다. 이번 칼럼은 거기에 한 겹을 더해줍니다. 모델이 좋아진다고 우리 일이 사라지는 게 아니라, 일이 놓인 자리가 바뀔 뿐이라는 것. 어제는 스프린트를 쪼개주는 게 일이었다면, 오늘은 평가기를 회의적으로 튜닝하는 게 일이고, 내일은 또 다른 경계에서 새 조합을 설계하는 게 일이 됩니다.

결국 하네스 엔지니어링은 모델을 따라가며 끝없이 다시 그리는 지도입니다. 그 지도를 그리는 능력 — 어디서 모델이 무너지는지 트레이스를 읽어내고, 그 자리에 정확히 맞는 보철을 대는 능력 — 이야말로 모델 세대가 바뀌어도 낡지 않는 우리의 진짜 기술이라고 생각합니다.


참고 문서

CS/알고리즘

[알고리즘] DFS (깊이 우선 탐색)

그래프나 트리를 탐색할 때 우리는 어떤 순서로 노드를 방문해야 할까요? 미로에서 길을 찾을 때 "일단 한 길을 끝까지 가보고, 막히면 돌아와서 다른 길을 시도한다"는 생각을해볼것같아요.

이것이 바로 DFS(깊이 우선 탐색, Depth-First Search)의 핵심 아이디어라고 합니다.

DFS는 그래프 탐색의 가장 기본적인 알고리즘 중 하나로, 백준이나 프로그래머스 같은 코딩 테스트에서 매우 자주 등장합니다. 특히 "경로 찾기", "연결 요소 개수 구하기", "사이클 판별" 같은 문제에서 필수적으로 사용됩니다.

DFS란?

DFS는 한 방향으로 끝까지 탐색한 후, 더 이상 갈 곳이 없으면 되돌아와서 다른 경로를 탐색하는 알고리즘입니다.

기본 원리?

  1. 시작 노드를 방문하고 방문 표시를 합니다.
  2. 현재 노드와 인접한 미방문 노드 중 하나를 선택합니다.
  3. 선택한 노드로 이동하여 1~2를 반복합니다.
  4. 더 이상 갈 곳이 없으면 이전 노드로 되돌아갑니다(백트래킹).
  5. 모든 노드를 방문할 때까지 반복합니다.

실생활에 대입해보자.

미로 탐색을 생각해봅시다.

  1. 출발점에서 시작합니다.
  2. 갈림길이 나오면 한 방향을 선택합니다.
  3. 선택한 길을 끝까지 갑니다.
  4. 막다른 길에 도달하면 가장 최근 갈림길로 돌아옵니다.
  5. 다른 길을 시도합니다.

이 방식은 우리가 본능적으로 미로를 탐색할 때 사용하는 방법과 매우 유사합니다.


복잡도

시간 복잡도

  • V: 정점(Vertex)의 개수
  • E: 간선(Edge)의 개수
  • O(V + E)

모든 정점을 한 번씩 방문하고, 모든 간선을 한 번씩 확인하므로 O(V + E)의 시간 복잡도를 가집니다.

공간 복잡도

  • 재귀 구현: O(V) - 재귀 호출 스택
  • 스택 구현: O(V) - 명시적 스택 + 방문 배열

DFS 구현 방법

DFS는 크게 두 가지 방법 재귀, 스택으로 구현할 수 있습니다. 말로 설명하는것보다 코드로 바로 작성해보도록 하겠습니다.

1. 재귀를 이용한 구현

def dfs_recursive(graph, node, visited):
    """
    재귀를 이용한 DFS

    Args:
        graph: 인접 리스트로 표현된 그래프
        node: 현재 방문 중인 노드
        visited: 방문 여부를 저장하는 리스트
    """
    # 현재 노드 방문 처리
    visited[node] = True
    print(node, end=' ')

    # 인접한 노드들을 재귀적으로 방문
    for neighbor in graph[node]:
        if not visited[neighbor]:
            dfs_recursive(graph, neighbor, visited)

# 사용 예제
if __name__ == "__main__":
    # 그래프를 인접 리스트로 표현
    graph = {
        1: [2, 3],
        2: [1, 4, 5],
        3: [1, 6, 7],
        4: [2],
        5: [2],
        6: [3],
        7: [3]
    }

    # 방문 여부를 저장할 딕셔너리 (노드 번호가 1부터 시작)
    visited = {i: False for i in range(1, 8)}

    print("DFS 탐색 순서:")
    dfs_recursive(graph, 1, visited)

실행 결과

DFS 탐색 순서:
1 2 4 5 3 6 7

2. 스택을 이용한 구현

명시적인 스택을 사용하여 재귀 없이 구현할 수 있습니다.

def dfs_stack(graph, start):
    """
    스택을 이용한 DFS

    Args:
        graph: 인접 리스트로 표현된 그래프
        start: 시작 노드
    """
    visited = {i: False for i in graph.keys()}
    stack = [start]
    result = []

    while stack:
        # 스택에서 노드 꺼내기
        node = stack.pop()

        # 아직 방문하지 않은 노드라면
        if not visited[node]:
            visited[node] = True
            result.append(node)

            # 인접 노드를 스택에 추가 (역순으로 추가하여 작은 번호부터 방문)
            for neighbor in reversed(graph[node]):
                if not visited[neighbor]:
                    stack.append(neighbor)

    return result

# 사용 예제
if __name__ == "__main__":
    graph = {
        1: [2, 3],
        2: [1, 4, 5],
        3: [1, 6, 7],
        4: [2],
        5: [2],
        6: [3],
        7: [3]
    }

    result = dfs_stack(graph, 1)
    print("DFS 탐색 순서:", ' '.join(map(str, result)))

실행 결과

DFS 탐색 순서: 1 2 4 5 3 6 7

재귀 vs 스택 구현 비교

재귀 구현의 장점

  • 코드가 간결하고 이해하기 쉽습니다.
  • 백트래킹 로직이 자동으로 처리됩니다.
  • 대부분의 코딩 테스트에서 선호됩니다.

재귀 구현의 단점

  • 재귀 깊이가 너무 깊으면 스택 오버플로우 발생 가능
  • Python의 기본 재귀 한도는 약 1000

스택 구현의 장점

  • 재귀 한도 제한이 없습니다.
  • 메모리 사용을 더 세밀하게 제어할 수 있습니다.

스택 구현의 단점

  • 코드가 재귀보다 복잡합니다.
  • 방문 순서가 재귀와 다를 수 있습니다.

DFS 동작 과정 시각화

다음 그래프를 DFS로 탐색하는 과정을 단계별로 살펴봅시다.

그래프 구조:
    1
   / \
  2   3
 / \  / \
4  5 6  7
def dfs_with_visualization(graph, node, visited, depth=0):
    """
    DFS 동작 과정을 시각화하는 함수
    """
    # 들여쓰기로 깊이 표현
    indent = "  " * depth
    print(f"{indent}→ 노드 {node} 방문")

    visited[node] = True

    for neighbor in graph[node]:
        if not visited[neighbor]:
            print(f"{indent}  노드 {node}에서 노드 {neighbor}로 이동")
            dfs_with_visualization(graph, neighbor, visited, depth + 1)
            print(f"{indent}  노드 {neighbor}에서 노드 {node}로 복귀")

# 사용 예제
if __name__ == "__main__":
    graph = {
        1: [2, 3],
        2: [1, 4, 5],
        3: [1, 6, 7],
        4: [2],
        5: [2],
        6: [3],
        7: [3]
    }

    visited = {i: False for i in range(1, 8)}
    print("=== DFS 동작 과정 ===")
    dfs_with_visualization(graph, 1, visited)

실행 결과

=== DFS 동작 과정 ===
→ 노드 1 방문
  노드 1에서 노드 2로 이동
  → 노드 2 방문
    노드 2에서 노드 4로 이동
    → 노드 4 방문
    노드 4에서 노드 2로 복귀
    노드 2에서 노드 5로 이동
    → 노드 5 방문
    노드 5에서 노드 2로 복귀
  노드 2에서 노드 1로 복귀
  노드 1에서 노드 3으로 이동
  → 노드 3 방문
    노드 3에서 노드 6으로 이동
    → 노드 6 방문
    노드 6에서 노드 3으로 복귀
    노드 3에서 노드 7로 이동
    → 노드 7 방문
    노드 7에서 노드 3으로 복귀
  노드 3에서 노드 1로 복귀

이 출력을 보면 DFS가 어떻게 한 방향으로 끝까지 탐색하고, 되돌아와서 다른 경로를 탐색하는지 명확히 알 수 있습니다.


그래프 표현 방법

DFS를 구현하기 전에 그래프를 어떻게 표현할지 결정해야 합니다.

1. 인접 리스트 (Adjacency List) - 추천

각 노드에 연결된 노드들을 리스트로 저장합니다.

# 딕셔너리를 이용한 인접 리스트
graph_dict = {
    1: [2, 3],
    2: [1, 4, 5],
    3: [1, 6, 7],
    4: [2],
    5: [2],
    6: [3],
    7: [3]
}

# 리스트를 이용한 인접 리스트 (노드 번호가 0부터 시작)
graph_list = [
    [],           # 0번 노드 (사용 안 함)
    [2, 3],       # 1번 노드
    [1, 4, 5],    # 2번 노드
    [1, 6, 7],    # 3번 노드
    [2],          # 4번 노드
    [2],          # 5번 노드
    [3],          # 6번 노드
    [3]           # 7번 노드
]

장점

  • 메모리 효율적 (O(V + E))
  • 인접 노드 순회가 빠름
  • 희소 그래프(sparse graph)에 적합

단점

  • 두 노드의 연결 여부 확인이 느림 (O(V))

2. 인접 행렬 (Adjacency Matrix)

2차원 배열로 노드 간 연결을 표현합니다.

# 7개 노드 (1~7번)
INF = float('inf')
graph_matrix = [
    [0, 0, 0, 0, 0, 0, 0, 0],  # 0번 (사용 안 함)
    [0, 0, 1, 1, 0, 0, 0, 0],  # 1번 노드
    [0, 1, 0, 0, 1, 1, 0, 0],  # 2번 노드
    [0, 1, 0, 0, 0, 0, 1, 1],  # 3번 노드
    [0, 0, 1, 0, 0, 0, 0, 0],  # 4번 노드
    [0, 0, 1, 0, 0, 0, 0, 0],  # 5번 노드
    [0, 0, 0, 1, 0, 0, 0, 0],  # 6번 노드
    [0, 0, 0, 1, 0, 0, 0, 0]   # 7번 노드
]

장점

  • 두 노드의 연결 여부를 O(1)에 확인 가능
  • 가중치 그래프 표현이 직관적
  • 밀집 그래프(dense graph)에 적합

단점

  • 메모리 낭비 (O(V²))
  • 인접 노드를 찾으려면 모든 노드를 확인해야 함

어떤 방법을 선택해야 할까?

대부분의 경우 인접 리스트를 사용하는 것이 좋습니다.

  • 코딩 테스트에서는 보통 희소 그래프가 주어집니다.
  • 메모리와 시간 효율이 더 좋습니다.
  • Python의 딕셔너리나 리스트로 쉽게 구현 가능합니다.

DFS 실전 예제

예제 1: 연결 요소의 개수 구하기

그래프에서 서로 연결되어 있지 않은 부분 그래프의 개수를 구하는 문제입니다.

def count_connected_components(n, edges):
    """
    연결 요소의 개수를 구하는 함수

    Args:
        n: 노드의 개수 (1부터 n까지)
        edges: 간선 정보 [(u, v), ...]

    Returns:
        연결 요소의 개수
    """
    # 그래프 생성 (인접 리스트)
    graph = {i: [] for i in range(1, n + 1)}
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = {i: False for i in range(1, n + 1)}
    count = 0

    def dfs(node):
        visited[node] = True
        for neighbor in graph[node]:
            if not visited[neighbor]:
                dfs(neighbor)

    # 모든 노드에 대해 DFS 수행
    for node in range(1, n + 1):
        if not visited[node]:
            dfs(node)
            count += 1

    return count

# 사용 예제
if __name__ == "__main__":
    n = 6
    edges = [(1, 2), (2, 5), (3, 4)]

    result = count_connected_components(n, edges)
    print(f"연결 요소의 개수: {result}")

    # 그래프 시각화
    print("\n그래프 구조:")
    print("1 - 2 - 5")
    print("3 - 4")
    print("6 (독립)")

실행 결과

연결 요소의 개수: 3

그래프 구조:
1 - 2 - 5
3 - 4
6 (독립)

동작 과정

1. 노드 1부터 시작 → DFS로 1-2-5 탐색 → count = 1
2. 노드 3 시작 (미방문) → DFS로 3-4 탐색 → count = 2
3. 노드 6 시작 (미방문) → DFS로 6만 탐색 → count = 3

예제 2: 경로 존재 여부 확인

두 노드 사이에 경로가 존재하는지 확인하는 문제입니다.

def has_path(graph, start, end, visited=None):
    """
    두 노드 사이의 경로 존재 여부를 확인

    Args:
        graph: 인접 리스트
        start: 시작 노드
        end: 목표 노드
        visited: 방문 여부 (재귀용)

    Returns:
        경로가 존재하면 True, 아니면 False
    """
    if visited is None:
        visited = set()

    # 목표 노드에 도달
    if start == end:
        return True

    # 이미 방문한 노드
    if start in visited:
        return False

    visited.add(start)

    # 인접 노드 탐색
    for neighbor in graph[start]:
        if has_path(graph, neighbor, end, visited):
            return True

    return False

# 사용 예제
if __name__ == "__main__":
    graph = {
        'A': ['B', 'C'],
        'B': ['A', 'D'],
        'C': ['A', 'E'],
        'D': ['B'],
        'E': ['C'],
        'F': ['G'],
        'G': ['F']
    }

    print(f"A에서 E로 가는 경로: {has_path(graph, 'A', 'E')}")  # True
    print(f"A에서 F로 가는 경로: {has_path(graph, 'A', 'F')}")  # False
    print(f"F에서 G로 가는 경로: {has_path(graph, 'F', 'G')}")  # True

실행 결과

A에서 E로 가는 경로: True
A에서 F로 가는 경로: False
F에서 G로 가는 경로: True

예제 3: 미로 탐색

2차원 배열로 표현된 미로에서 출발점부터 도착점까지의 경로를 찾는 문제입니다.

def solve_maze(maze, start, end):
    """
    미로 탐색 (DFS)

    Args:
        maze: 2차원 리스트 (0: 길, 1: 벽)
        start: 시작 좌표 (row, col)
        end: 도착 좌표 (row, col)

    Returns:
        경로가 존재하면 True, 경로 정보 반환
    """
    rows, cols = len(maze), len(maze[0])
    visited = [[False] * cols for _ in range(rows)]
    path = []

    def dfs(row, col):
        # 범위 체크
        if row < 0 or row >= rows or col < 0 or col >= cols:
            return False

        # 벽이거나 이미 방문한 경우
        if maze[row][col] == 1 or visited[row][col]:
            return False

        # 현재 위치 방문 처리
        visited[row][col] = True
        path.append((row, col))

        # 도착 지점 도달
        if (row, col) == end:
            return True

        # 상하좌우 탐색
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        for dr, dc in directions:
            if dfs(row + dr, col + dc):
                return True

        # 백트래킹 (막다른 길)
        path.pop()
        return False

    found = dfs(start[0], start[1])
    return found, path if found else []

# 사용 예제
if __name__ == "__main__":
    # 0: 길, 1: 벽
    maze = [
        [0, 0, 0, 1, 0],
        [1, 1, 0, 1, 0],
        [0, 0, 0, 0, 0],
        [0, 1, 1, 1, 0],
        [0, 0, 0, 0, 0]
    ]

    start = (0, 0)
    end = (4, 4)

    found, path = solve_maze(maze, start, end)

    if found:
        print("경로를 찾았습니다!")
        print(f"경로: {path}")
        print(f"경로 길이: {len(path)}")

        # 미로 시각화 (경로 표시)
        print("\n미로 (경로는 *로 표시):")
        for i in range(len(maze)):
            for j in range(len(maze[0])):
                if (i, j) in path:
                    print("* ", end="")
                elif maze[i][j] == 1:
                    print("■ ", end="")
                else:
                    print(". ", end="")
            print()
    else:
        print("경로를 찾을 수 없습니다.")

실행 결과

경로를 찾았습니다!
경로: [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3), (2, 4), (3, 4), (4, 4)]
경로 길이: 9

미로 (경로는 *로 표시):
* * * ■ .
■ ■ * ■ .
. . * * *
. ■ ■ ■ *
. . . . *

DFS 활용 분야

1. 경로 찾기

  • 두 노드 사이의 경로 존재 여부
  • 모든 경로 찾기
  • 최단 경로 (가중치 없는 그래프에서는 BFS가 더 적합)

2. 사이클 탐지

  • 무향 그래프의 사이클
  • 유향 그래프의 사이클
  • 위상 정렬
def has_cycle(graph, n):
    """
    무향 그래프에서 사이클 존재 여부 확인
    """
    visited = [False] * (n + 1)

    def dfs(node, parent):
        visited[node] = True

        for neighbor in graph[node]:
            if not visited[neighbor]:
                if dfs(neighbor, node):
                    return True
            elif neighbor != parent:
                # 부모가 아닌 방문한 노드 → 사이클
                return True

        return False

    for i in range(1, n + 1):
        if not visited[i]:
            if dfs(i, -1):
                return True

    return False

3. 위상 정렬

선행 관계가 있는 작업들의 순서를 결정하는 문제입니다.

def topological_sort(graph, n):
    """
    위상 정렬 (DFS 기반)
    """
    visited = [False] * (n + 1)
    stack = []

    def dfs(node):
        visited[node] = True

        for neighbor in graph[node]:
            if not visited[neighbor]:
                dfs(neighbor)

        # 후위 순회: 현재 노드를 스택에 추가
        stack.append(node)

    for i in range(1, n + 1):
        if not visited[i]:
            dfs(i)

    # 스택을 역순으로 반환
    return stack[::-1]

4. 연결 요소

  • 연결 요소의 개수
  • 각 연결 요소의 크기
  • 강한 연결 요소 (Strongly Connected Components)

5. 미로/퍼즐 탐색

  • 미로 찾기
  • 스도쿠
  • N-Queen 문제

DFS의 장단점

장점

  1. 구현이 간단합니다 (재귀 사용 시)
  2. 현재 경로의 노드만 기억하므로 메모리 효율적
  3. 백트래킹과 자연스럽게 결합됨
  4. 모든 경로를 탐색해야 하는 경우 유용

단점

  1. 최단 경로를 보장하지 않음
  2. 재귀 깊이가 깊어지면 스택 오버플로우 위험
  3. 무한 루프 가능성 (사이클이 있고 방문 체크를 안 하면)

DFS 체크 포인트

1. 방문 체크 누락

DFS에서 가장 흔한 실수는 방문 체크를 빼먹는 것입니다.

# 잘못된 코드 (무한 루프)
def wrong_dfs(graph, node):
    print(node)
    for neighbor in graph[node]:
        wrong_dfs(graph, neighbor)  # 방문 체크 없음!

# 올바른 코드
def correct_dfs(graph, node, visited):
    if visited[node]:
        return

    visited[node] = True
    print(node)

    for neighbor in graph[node]:
        if not visited[neighbor]:
            correct_dfs(graph, neighbor, visited)

2. 재귀 깊이 제한

Python의 기본 재귀 한도는 약 1000입니다.

import sys

# 재귀 한도 증가 (필요한 경우)
sys.setrecursionlimit(10000)

# 또는 스택을 사용한 반복 구현으로 변경

3. 양방향 간선 처리

무향 그래프에서는 양방향으로 간선을 추가해야 합니다.

# 무향 그래프
edges = [(1, 2), (2, 3), (3, 4)]
graph = {i: [] for i in range(1, 5)}

for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)  # 양방향!

4. 방문 배열 초기화

여러 번 DFS를 수행할 때는 방문 배열을 초기화해야 합니다.

# 방법 1: 새로운 방문 배열 생성
visited = [False] * n

# 방법 2: 기존 배열 초기화
visited = [False] * n
# DFS 수행...
visited = [False] * n  # 재초기화

DFS vs BFS 비교

특징 DFS BFS
탐색 방식 깊이 우선 너비 우선
구현 재귀 or 스택
메모리 O(h) - 높이만큼 O(w) - 너비만큼
최단 경로 보장 안 됨 보장됨
적합한 문제 경로 존재 여부, 백트래킹 최단 거리, 레벨 탐색

언제 DFS를 사용해야 할까?

  • 모든 경로를 탐색해야 할 때
  • 백트래킹이 필요할 때
  • 메모리가 제한적일 때
  • 경로의 특징을 찾아야 할 때

언제 BFS를 사용해야 할까?

  • 최단 경로를 찾아야 할 때
  • 레벨별로 탐색해야 할 때
  • 가까운 노드부터 확인해야 할 때

마치며

DFS를 처음 배웠을 때는 이름부터 생소하고 단순 "재귀가 어렵다"는 생각이 들었습니다. 하지만 직접 손으로 그래프를 그려보고, 코드를 작성하면서 "한 방향으로 끝까지 가고, 막히면 돌아온다"는 단순한 원리를 이해하게 되었습니다.

특히 미로 탐색 예제를 직접 구현해보면서, 우리가 본능적으로 미로를 탐색하는 방식과 DFS가 얼마나 유사한지 깨달았습니다. 알고리즘은 결국 우리의 직관을 체계화한 것이라는 생각이 들었습니다.

실무에서는 DFS를 직접 구현하기보다는, 그래프 라이브러리나 프레임워크를 사용하는 경우가 많습니다. 하지만 DFS의 원리를 이해하면 복잡한 문제를 분해하고 해결하는 사고방식을 기를 수 있습니다.

코딩 테스트에서는 DFS가 매우 자주 등장합니다. 특히 "연결 요소 개수", "경로 찾기", "사이클 판별" 같은 문제는 DFS의 기본 패턴만 익히면 쉽게 풀 수 있습니다.

다음 포스팅에서는 DFS와 함께 그래프 탐색의 양대 산맥인 BFS(너비 우선 탐색) 에 대해 포스팅하려고합니다. DFS가 "깊이"를 우선한다면, BFS는 "너비"를 우선합니다. 두 알고리즘을 비교하면서 공부하면 더욱 명확하게 이해할 수 있을 것입니다!!

CS/자료구조

Redis Sorted Set - 이중 구조, 왜 이렇게 설계했을까?

들어가며

Redis는 String, List, Hash, Set, Sorted Set 이렇게 다섯 가지 기본 자료구조를 제공합니다. 이 중에서 Sorted Set은 가장 강력하면서도 오해받기 쉬운 자료구조입니다. 단순히 "정렬된 Set"이라고 생각하기 쉽지만, 실제로는 Skip List와 Hash Table이 결합된 복합 구조이며, 이 설계 덕분에 다른 자료구조로는 어려운 연산들을 효율적으로 수행할 수 있습니다.

이 글에서는 Sorted Set의 정의부터 내부 구현, 다른 자료구조와의 비교, 트레이드오프까지 학습한 내용을 차근차근 정리해보겠습니다.


Sorted Set이란?

Sorted Set은 member(고유한 문자열)와 score(부동소수점 숫자)의 쌍으로 구성된 집합입니다.

  • member는 유일(unique) 값이며 같은 값을 다시 추가하면 score만 갱신
  • score 기준으로 자동 정렬되며 score가 같으면 member의 사전순(lexicographical)
  • score는 IEEE 754 64bit double 값으로 음수, 양수, 무한대 가능
member: "alice"  score: 100
member: "bob"    score: 85
member: "carol"  score: 95

위 데이터를 ZADD로 넣으면 내부적으로 score 오름차순으로 정렬되어 저장됩니다.

[bob:85] → [carol:95] → [alice:100]

이 한 줄 정의에서 Sorted Set의 모든 특성이 파생됩니다.

특성 파생되는 동작
member uniqueness 같은 값 재추가 시 score 업데이트 (Set의 성질)
score 정렬 범위 쿼리, 순위 조회 가능
양방향 매핑 member로 score 조회 O(1), score 범위로 member 조회 O(log N)

내부 구현 - 왜 두 자료구조를 같이 쓰일까?

Sorted Set은 단일 자료구조가 아닙니다. Skip ListHash Table 두 개를 동시에 운영합니다.

이중 구조

    ┌──────────────────────────────────────────┐
    │           Sorted Set                     │
    │                                          │
    │  ┌─────────────────┐  ┌───────────────┐  │
    │  │  Hash Table     │  │  Skip List    │  │
    │  │                 │  │               │  │
    │  │ member -> score │  │ score 순 정렬   │  │
    │  │   O(1) 조회      │  │    범위 쿼리    │  │
    │  └─────────────────┘  └───────────────┘  │
    └──────────────────────────────────────────┘
  • Hash Table: member → score 매핑. ZSCORE alice 같은 단건 조회를 O(1)로 처리
  • Skip List: score 순서로 노드를 정렬 보관. ZRANGEBYSCORE 80 100 같은 범위 쿼리를 O(log N + M)로 처리

두 구조는 항상 동기화된 상태를 유지합니다. ZADD 한 번에 양쪽 모두 갱신되므로 메모리는 더 쓰지만 모든 연산이 빨라집니다.

Skip List 구조

Skip List는 여러 레벨의 연결 리스트를 쌓아 만든 확률적 자료구조입니다.

Level 3:  HEAD ──────────────────→ [100] ──→ NIL
Level 2:  HEAD ──────→ [85] ─────→ [100] ──→ NIL
Level 1:  HEAD ──→ [70] → [85] ─→ [100] ──→ NIL
Level 0:  HEAD ──→ [70] → [85] → [95] → [100] → NIL
  • 각 노드는 무작위로 레벨을 부여받음 (보통 평균 1.33 레벨)
  • 상위 레벨에서 빠르게 점프해 검색 → 평균 O(log N)
  • Balanced Tree와 달리 회전(rotation) 같은 복잡한 연산 불필요

listpack ↔ skiplist 자동 전환

데이터가 적을 때는 Skip List가 오히려 비효율적입니다. Redis는 작은 Sorted Set을 listpack(이전 버전의 ziplist)이라는 압축된 단일 메모리 블록에 저장하다가, 임계값을 넘으면 자동으로 skiplist 구조로 전환합니다.

설정 기본값 의미
zset-max-listpack-entries 128 항목 수가 이 값 이하면 listpack 사용
zset-max-listpack-value 64 한 member 크기(byte)가 이 값 이하면 listpack 사용

둘 중 하나라도 초과하면 skiplist로 전환되며, 한 번 전환된 후에는 다시 줄어들어도 listpack으로 돌아가지 않습니다.

Skip List를 선택한 이유

Redis 저자 antirez는 Balanced Tree(예: Red-Black Tree) 대신 Skip List를 선택한 이유를 다음과 같이 설명했습니다.

  1. 메모리 효율: 메모리 효율 파라미터를 조정 가능 (확률 p, 평균 레벨)
  2. 범위 쿼리 친화적: 가장 낮은 레벨이 그대로 정렬된 연결 리스트라 ZRANGE가 자연스럽게 빠름
  3. 구현 단순성: RB-Tree의 회전 로직 없이 무작위 레벨링으로 균형 유지
  4. lock-free 친화적: 동시성 확장이 비교적 단순

주요 연산 시간복잡도

연산 복잡도 설명
ZADD O(log N) Skip List에 삽입
ZREM O(log N) Skip List에서 제거
ZSCORE O(1) Hash Table 조회
ZRANGE O(log N + M) M = 반환 항목 수
ZRANGEBYSCORE O(log N + M) score 범위 조회
ZRANK O(log N) 순위 조회
ZINCRBY O(log N) score 증감 (재정렬 포함)
ZREMRANGEBYRANK O(log N + M) 범위로 일괄 삭제
ZCARD O(1) 전체 개수

다른 자료구조와의 비교

Sorted Set이 강력한 이유는 다른 자료구조의 한계를 보면 명확해집니다.

요구사항 String List Hash Set Sorted Set Stream
순서 유지 X O (삽입순) X X O (score순) O (시간순)
범위 쿼리 X 인덱스만 X X O (score) O (ID)
중복 제거 - X key 단위 O O X
점수 기반 정렬 X X X X O X
임의 위치 효율 삭제 - O(N) O(1) O(1) O(log N) 제한적
영속성/Consumer X X X X X O

각 자료구조가 어디서 무너지는지 살펴보면 아래와 같습니다.

  • List: 삽입순 보장은 되지만 "score 80 이상" 같은 조건 검색이 안 됨. 전체 스캔 필요
  • Hash: 빠른 조회는 되지만 정렬·범위 개념 자체가 없음
  • Set: 중복 제거는 되지만 순서가 없음
  • Stream: ID 기반 시간순 조회는 가능하지만 임의 점수 기반 정렬, 점수 갱신 같은 동작은 부적합

"정렬된 상태로 무언가를 유지하고, 범위 또는 순위로 조회하고 싶다." 이 요구가 있으면 Sorted Set 외에는 답이 없습니다.


주요 연산과 사용 패턴

Sorted Set의 활용은 결국 score를 무엇으로 정의하느냐에 따라 달라집니다. 언제 사용하는지 사례를 통해 살펴봅시다.

1. 실시간 랭킹 (게임 리더보드)

  • score = 점수, member = 유저 ID
ZADD leaderboard 1500 user:alice
ZADD leaderboard 2200 user:bob
ZADD leaderboard 1800 user:carol

ZREVRANGE leaderboard 0 9 WITHSCORES   # 상위 10명
ZREVRANK leaderboard user:alice        # alice의 순위
ZINCRBY leaderboard 100 user:alice     # alice 점수 +100, 자동 재정렬

수십만 명의 랭킹도 모든 연산이 O(log N)에 동작합니다.

2. 우선순위 큐 (Delayed Job Queue)

  • score = 실행 예정 시각(unix timestamp), member = job ID
ZADD jobs 1735689600 job:001
ZADD jobs 1735689660 job:002

# 워커가 "지금 실행해야 할 작업" 조회
ZRANGEBYSCORE jobs -inf <현재시각>

가장 오래된 작업부터 자연스럽게 가져옵니다. RDB 폴링 방식보다 훨씬 가볍습니다.

3. 시계열 인덱싱

  • score = timestamp, member = 이벤트 ID 또는 직렬화된 데이터
ZRANGEBYSCORE events <from_ts> <to_ts>
ZREMRANGEBYSCORE events -inf <만료_ts>   # 오래된 이벤트 정리

특정 시간 구간 이벤트를 빠르게 조회하거나, 오래된 데이터만 일괄 삭제할 때 유용합니다.

4. 슬라이딩 윈도우 Rate Limiter

  • score = 요청 시각, member = 요청 ID(또는 timestamp 자체)
# 1분 동안 100회 제한
ZADD rate:user:123 <now> <now>
ZREMRANGEBYSCORE rate:user:123 -inf <now - 60>
ZCARD rate:user:123   # 100 초과면 거부

고정 윈도우 카운터의 경계 문제를 피할 수 있습니다.

5. 최근 N개 캐시

  • score = sequence 또는 timestamp, member = 데이터
ZADD recent:session:42 17 "<event_payload_json>"
ZREMRANGEBYRANK recent:session:42 0 -51   # 최신 50개만 유지

ZADD로 추가 후 ZREMRANGEBYRANK로 오래된 항목을 잘라내면 항상 최신 N개만 유지됩니다.


트레이드오프와 함정

그렇다면, 실제로 어떤 트레이드오프가 있고 주의할점이 있을지 살펴보도록 합시다.

메모리 오버헤드

Skip List는 평균 1.33개의 포인터를 노드마다 저장하고, Hash Table까지 함께 운영하므로 단순 List/Hash 대비 메모리를 더 사용합니다. 데이터가 작을 때는 listpack으로 자동 최적화되지만, 임계값을 넘는 순간 메모리 사용량이 급증합니다.

멀티 명령은 원자적이지 않음

ZADDZREMRANGEBYRANK로 윈도우를 유지하는 패턴은 두 번의 네트워크 왕복이며, 그 사이에 다른 클라이언트가 끼어들 수 있습니다. 엄격한 원자성이 필요하면 MULTI/EXEC 또는 Lua 스크립트로 묶어야 합니다.

-- 원자적으로 추가 + 윈도우 유지
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[2])
redis.call('ZREMRANGEBYRANK', KEYS[1], 0, -ARGV[3]-1)

score double 정밀도 한계

score는 IEEE 754 double이므로 정수는 2^53(약 9×10^15)까지만 정확히 표현됩니다. timestamp(밀리초 단위)나 일반적인 sequence라면 충분하지만, 나노초 단위 + 큰 base를 더하는 식으로 score를 만들면 정밀도 손실이 발생할 수 있습니다.

Cluster 환경의 단일 키 hot spot

Redis Cluster는 키를 슬롯에 분산시키지만, 하나의 Sorted Set은 하나의 슬롯에만 집중됩니다. 단일 키에 트래픽이 몰리면 해당 노드가 병목이 됩니다.

해결 방법

  • 키를 도메인 단위로 분할 (예: leaderboard:region:KR, leaderboard:region:US)
  • 읽기 부하면 Replica로 분산
  • 키 자체가 핫하면 샤딩 키 설계 재검토

동일 score 처리

score가 같으면 member의 사전순(lexicographical)으로 정렬됩니다. 이를 활용한 ZRANGEBYLEX 연산도 있지만, 의도하지 않은 정렬 동작에 주의해야 합니다. 시간순을 보장하고 싶다면 score에 timestamp를 사용하거나 member에 정렬 가능한 prefix를 붙이는 방식이 안전합니다.


전체적으로 정리를 해보자면..

구분 내용
본질 member(unique) + score(double) 쌍의 정렬된 집합
내부 구현 Hash Table + Skip List 이중 구조 (작을 때는 listpack)
강점 정렬 유지, 범위 쿼리, 순위 조회를 O(log N)에 처리
약점 메모리 오버헤드, 멀티 명령 비원자성, 단일 키 hot spot

Sorted Set을 고려해볼 상황

  • 정렬된 상태로 데이터를 유지해야 함
  • score(점수, 시각, 우선순위) 기반 범위 쿼리가 필요
  • 순위 조회나 점수 갱신이 빈번
  • 항목 중복은 막되 score는 갱신해야 함

Sorted Set 굳이? 싶은 상황

  • 단순 캐시 (String/Hash로 충분)
  • FIFO/LIFO 큐 (List가 더 가벼움)
  • 영속 메시지 큐 (Stream/Kafka가 적합)
  • 단순 중복 체크 (Set으로 충분)

참고문서

AI

작은 자동화가 주는 큰 편함: CLAUDE.md 자동 생성 플러그인 만들기

Claude Code를 사용하면 CLAUDE.md 파일을 작성해야 하고 이 파일을 "잘" 작성해야 효율적인 컨텍스트 관리와 재작업을 줄일 수 있습니다.

매번 새로운 프로젝트를 할 때마다 작성하는데 기술 스택은 뭔지, 커밋 컨벤션은 어떻게 할 건지, 테스트 전략은 뭔지 — 매번 손으로 작성하거나, 기존 프로젝트에서 복붙하고 조금씩 고쳐야 합니다. 규칙 파일, 스킬 파일도 마찬가지입니다.

그래서 ! 어느 순간 이 반복이 피로하게 느껴졌어요.

왜 귀찮은가

/init 명령어도 있긴 합니다. 하지만 결과물이 너무 generic합니다. 프로젝트 특성을 거의 반영하지 못하고, 결국 손으로 다듬는 시간이 따로 발생합니다.

대충 머릿속으로 원하는 방향을 떠올렸을 때 아래와 같았어요.

  • 프로젝트 구조를 스스로 읽습니다.
  • 어떤 스택인지, 어떤 패턴인지 파악합니다.
  • 모호한 부분을 사용자에게 질문합니다.
  • 곧바로 쓸 수 있는 CLAUDE.md와 규칙들을 만들어 줍니다.

만들자

그래서 만든 게 claude-auto-setup-plugin입니다. Claude Code 플러그인으로 /auto-setup 명령어 하나로 실행되며 내부는 4개의 에이전트가 순서대로 돌아가는 파이프라인입니다.

setup-analyzer (Haiku) : 프로젝트 구조, 언어, 프레임워크 분석

setup-interviewer (Sonnet) : 분석 결과 기반으로 맞춤 질문 (사용자와 대화)

setup-generator (Sonnet) : CLAUDE.md, rules, skills 생성

setup-reviewer (Sonnet) : 품질 검증, 미흡하면 재생성 루프

첫 번째 에이전트(Haiku)가 코드베이스를 빠르게 훑어 전반적인 프로젝트를 파악하고, 그 결과를 가지고 두 번째 에이전트가 사용자에게 의미 있는 질문만 던집니다. 예로 인증 관련 내용이 프로젝트에 있다면 "어디에 인증 코드가 있나요?" 같은 걸 묻는 게 아니라, "JWT 말고 세션 방식도 고려하고 있나요?" 수준의 질문이 나오길 원했습니다.

하네스 엔지니어링, 에이전트를 믿게 만드는 구조

멀티 에이전트를 만들면서 단순히 "에이전트를 연결"하는 것 이상이 필요하다는 걸 느꼈습니다.

에이전트는 자율적으로 움직이고 행동하지만 그 자율성이 의도한 방향으로 흐르도록 하는 하네스가 강제되어야 한다고 생각합니다. 하네스란 에이전트의 입출력, 실행 순서, 실패 처리, 품질 기준을 정의하는 안전한 가드레일이라고 보시면 됩니다.

이 프로젝트에서 하네스 엔지니어링 관점에서 신경 쓴 부분은 세 가지가 있습니다.

1. 역할에 맞는 모델 선택

모든 에이전트에 같은 모델을 쓸 필요가 없습니다. 탐색(분석)은 속도가 중요하고, 생성·검증은 품질이 중요합니다. 따라서 아래와 같이 구성했습니다.

에이전트 모델 이유
setup-analyzer Haiku 빠른 코드 탐색, 비용 최소화
setup-interviewer Sonnet 맥락 이해 기반 질문 생성
setup-generator Sonnet 품질 높은 문서 생성
setup-reviewer Sonnet 세밀한 검증 판단

Haiku로 사실을 수집하고, Sonnet으로 판단하는 구조. 이 조합만으로 비용과 품질의 균형이 잘 잡히도록 했습니다.

2. 품질이 안 되면 다시

setup-reviewer 가 결과물을 검토해서 기준 미달이면 setup-generator로 다시 돌립니다. 사람이 "이거 좀 이상한데?"라고 느끼는 순간을 에이전트가 대신 잡아내는 구조입니다.

이 루프가 없다면 나쁜 결과물이 그냥 사용자한테 넘어오고 그걸 또 재작업하는 형태가 됩니다. 제가 생각하는 하네스의 핵심은 실패를 조용히 넘기지 않는 것입니다.

3. 앞 에이전트의 결과가 다음 에이전트의 입력

각 에이전트는 이전 에이전트의 결과를 컨텍스트로 받습니다. setup-analyzer가 "이 프로젝트는 Spring Boot + JPA 기반"이라는 사실을 파악하면, setup-interviewer는 그 사실을 전제로 질문을 구성합니다.

에이전트를 단순히 병렬로 풀어놓는 게 아니라, 정보가 축적되면서 흐르는 시퀀셜 파이프라인으로 설계한 이유입니다.

그래서 뭐가 달라졌나?

클로드 코드에서 세팅 후 실행하면 출력물은 세 가지입니다.

  • .claude/CLAUDE.md — 프로젝트 규칙, 아키텍처, 패턴 정리
  • .claude/rules/ — 코딩 제약사항
  • .claude/skills/ — 반복 작업 워크플로우

기존 30분~1시간 걸리던 설정 작업이, 인터뷰 5분 포함해서 10분 안에 끝납니다. 그리고 결과물의 품질이 제가 대충 복붙했을 때보다 훨씬 구체적이라고 생각합니다.

느낀 점

혼자만의 불편함에서 시작한 작은 프로젝트지만, 이걸 만들면서 느낀 게 있습니다.

멀티 에이전트 파이프라인은 대규모 프로젝트를 만들 때만 쓰는 게 아니라고 생각합니다. 단순한 반복 작업, 특히 분석 → 질문 → 생성 → 검증처럼 단계가 명확하게 나뉘는 작업이라면 오히려 더 잘 맞습니다.

AI가 코드를 생성하더라도 결국 사람이 확인하고 검증을 합니다. 하지만 하네스를 잘 짜두면 에이전트를 믿고 맡길 수 있습니다. 결과를 매번 확인하지 않아도 되는 구조, 그게 자동화의 진짜 의미인 것 같습니다.

Repository

https://github.com/yhyuk/claude-auto-setup-plugin

 

GitHub - yhyuk/claude-auto-setup-plugin: 프로젝트 분석 후 맞춤 인터뷰를 통해 .claude/ 설정을 자동 생성하

프로젝트 분석 후 맞춤 인터뷰를 통해 .claude/ 설정을 자동 생성하는 멀티 에이전트 파이프라인 - yhyuk/claude-auto-setup-plugin

github.com