개인이 팀이 된 시대: 규칙은 지켜지는데 판단은 새어 나간다

개인이 팀이 된 시대: 규칙은 지켜지는데 판단은 새어 나간다


어떤 변화

작년까지 코드의 일관성은 팀 단위의 문제였다. 팀이 컨벤션을 정하고, 리뷰에서 맞추고, 린터로 강제했다. 한 사람이 쓴 코드는 그 사람 안에서는 대체로 일관됐기 때문에, 맞출 대상은 “사람과 사람 사이”였다.

지금은 다르다. 한 사람이 코딩 에이전트를 여러 개 동시에 돌린다. 저장소 A에서 세션 하나, 저장소 B에서 또 하나, 백그라운드에서 조사용 하나. 각 세션은 서로의 존재를 모른다. 그러니까 개인 한 명이 이미 하나의 작은 팀이고, 그 팀 구성원들은 서로 이야기를 나누지 않는다.

우리 조직의 저장소 활동을 실제로 뒤져 보니, 최근 90일 커밋의 대다수가 에이전트 세션에서 나오고 있었다. 사람 수로는 설명이 안 되는 커밋 수였다. 시크릿 관련 정책 문서에는 아예 이렇게 적혀 있다 — “코딩 에이전트가 주 커밋 주체가 된 지금.”

그러면 예전에 팀이 하던 일을 개인 단위에서 다시 해야 하나? 컨벤션을 더 세게 잡고, 리뷰를 더 촘촘히 하고? 몇 달 겪어 보니 그 방향은 반은 맞고 반은 틀렸다.

컨벤션은 오히려 쉬워졌다

틀린 반쪽부터. 에이전트 때문에 코딩 컨벤션이 무너질 거라는 걱정은 실제로는 반대로 나타났다.

포맷, 네이밍, import 순서, 파일 배치 — 기계가 검사할 수 있는 규칙은 에이전트가 사람보다 잘 지킨다. CLAUDE.mdAGENTS.md에 규칙을 적어 두면 매 세션이 그걸 읽고 시작한다. 린터를 훅으로 걸어 두면 어기지 않는다. 사람은 마감에 쫓기면 “이번만” 하고 건너뛰는데, 에이전트는 지치지도 않고 급하지도 않다.

지난 글에서 “컨텍스트는 세션에, 컨벤션은 저장소에”라고 썼다. 컨벤션을 저장소의 아티팩트로 외재화하면 어느 세션이 열려도 같은 바닥 위에서 시작한다는 얘기였고, 그건 지금도 잘 작동한다. 이 축은 풀린 문제다.

새어 나가는 것은 판단이다

맞는 반쪽은 여기다. 문제는 규칙이 아니라 규칙으로 적히지 않은 것, 즉 판단이다.

기계가 검사할 수 있는 것 — 쉬워졌다검사할 수 없는 것 — 어려워졌다
포맷, 네이밍, import 순서새 의존성을 끌어올 것인가
타입, 린트 규칙이 자격증명을 어디에 둘 것인가
테스트 통과 여부실패를 삼킬 것인가 올릴 것인가
 어떤 모델을 어떤 키로 어느 리전에서 부를 것인가

오른쪽 열의 결정은 세션마다 그럴듯하게 내려진다. 이 세션은 requests를, 저 세션은 httpx를 고른다. 이 세션은 API 키를 환경변수에서 읽고, 저 세션은 편의상 설정 파일에 넣는다. 이 세션은 예외를 로그만 찍고 넘기고, 저 세션은 상위로 올린다. 하나씩 보면 전부 합리적이다. 합쳐 놓으면 제각각이다.

더 나쁜 건 리뷰로 잡기 어렵다는 점이다. 각각이 틀린 코드가 아니기 때문이다. 돌아간다. 테스트도 통과한다. 리뷰어가 “왜 여기선 이 라이브러리를 썼죠?”라고 물을 근거가 그 PR 안에는 없다. 다른 저장소의 다른 세션이 뭘 골랐는지 봐야 보이는 문제라서, 저장소 단위 리뷰가 원리적으로 못 잡는다.

실제로 겪은 일

전 저장소에서 코드에 박힌 평문 시크릿을 걷어내는 작업을 한 번 끝냈다. 비밀 관리 서비스로 옮기고, 배포 시 주입하도록 바꾸고, 완료 처리했다.

몇 주 뒤 다른 브랜치에서 같은 평문 키가 되살아난 것이 실측됐다. 누가 악의로 넣은 게 아니다. 어느 세션이 “설정값이 필요하네” 하고 가장 손에 잡히는 방법을 골랐을 뿐이다. 그 세션 입장에서는 그럴듯한 판단이었다.

그래서 “지우는 것”과 별개로 “다시 들어오지 못하게 막는 것”을 따로 설계해야 했는데, 그 과정에서 계층이 이렇게 나뉘었다.

계층수단성질
L1AGENTS.md · CLAUDE.md에 중단 프로토콜 명시확률적 — 모델이 무시할 수 있다
L2에이전트 훅(PreToolUse)에서 도구 실행 차단결정론적 — 모델 준수와 무관
L3pre-commit + gitleaks결정론적 — --no-verify로 우회 가능
L4조직 ruleset + CI 시크릿 스캔으로 머지 차단결정론적 — 신규 저장소에 자동 적용

이 표를 만들고 나서야 문제가 또렷해졌다. 문서로 시키는 것은 확률적이고, 시스템으로 거는 것은 결정론적이다. L1은 있어야 하지만 L1만으로는 안 된다. 에이전트에게 “이러지 마세요”라고 적어 두는 건 부탁이고, 부탁은 지켜질 수도 있고 아닐 수도 있다.

컨벤션이 저장소로 옮겨져서 잘 지켜진 이유도 이제 설명된다. 컨벤션은 대부분 L2~L4에 해당하는 도구(포매터, 린터, 타입체커)가 이미 있었다. 판단은 그런 도구가 없어서 L1에만 머물러 있었던 것이다.

판단을 코드 밖으로 꺼낸다

그렇다면 할 일은 하나다. 각 세션이 매번 내리던 판단 중 일관돼야 하는 것을 골라 코드 밖으로 꺼내고, 시스템이 소유하게 만든다. 판단할 여지 자체를 없애는 것이다.

돌아보니 지난 1년 동안 플랫폼 쪽에서 한 일이 대부분 이 패턴이었다.

  • 자격증명 — 코드에서 뺀다. 비밀 관리 서비스에 두고 배포가 주입한다. 세션은 “어디에 둘까”를 판단할 필요가 없다.
  • 접근 — 공용 계정을 없애고 개인 계정으로, 개인 토큰을 없애고 조직 발급 인증으로. “어떤 계정으로 할까”를 판단할 필요가 없다.
  • 배포 — 재사용 워크플로와 공용 차트. “어떻게 배포할까”를 판단할 필요가 없다.
  • 관측 — 계측 표준 하나. “어디에 어떻게 로그를 남길까”를 판단할 필요가 없다.
  • LLM 호출 — 이 글의 주제.

마지막 축이 가장 늦게 왔다. 이유는 단순한데, LLM을 부르는 코드가 저장소마다 생기기 시작한 게 비교적 최근이기 때문이다. 그리고 이 축이 판단의 밀도가 제일 높다.

LLM 호출에는 판단이 몇 개나 숨어 있나

애플리케이션이 LLM을 한 번 부를 때 세션이 내리는 판단을 세어 봤다.

  1. 어떤 제공자(Bedrock, Anthropic 직접, OpenAI, 중개 업체)로 갈 것인가
  2. 어느 리전으로 갈 것인가 — 리전마다 단가가 다르고 지연이 다르다
  3. 어떤 자격증명을 쓸 것인가 — 그 키는 어디에 둘 것인가
  4. 실패하면 어떻게 할 것인가 — 재시도? 다른 리전? 다른 제공자?
  5. 얼마나 기다릴 것인가
  6. 비용과 사용량을 어디에 기록할 것인가

여섯 개 판단이 저장소 수만큼 곱해진다. 제공자가 셋이고 리전이 셋이면 관리할 조합은 덧셈이 아니라 곱셈으로 늘어난다. 그리고 이 판단들은 전부 “돌아가는 코드”를 만든다. 어느 조합을 골라도 응답은 온다. 리뷰에서 걸리지 않는다.

그래서 이 여섯 판단을 통째로 밖으로 꺼내기로 했다. LLM 게이트웨이다.

게이트웨이: 판단을 설정으로 바꾼다

구조는 이렇다.

관리 경로:  사용자 ──▶ 셀프서비스 웹 ──▶ 게이트웨이 관리 API
호출 경로:  애플리케이션 ─────────────▶ 게이트웨이 ──▶ LLM 제공자

애플리케이션이 아는 것은 두 개로 줄어든다. base_urlapi_key. 모델 이름은 claude-haiku-4-5 같은 별칭 하나만 부른다. 그 별칭 뒤에 어느 제공자의 어느 리전이 1순위이고, 그게 실패하면 어디로 넘어가고, 몇 번 재시도하고, 얼마나 기다리는지는 게이트웨이의 설정 파일에 있다. 여섯 판단이 설정 한 곳으로 모였다.

게이트웨이는 LiteLLM을 그대로 썼다. 100개 넘는 제공자를 OpenAI 호환 인터페이스 하나로 부르게 해 주는 오픈소스다. 포크하지 않는다는 원칙을 세웠는데, 이유는 앞 절과 같다. 우리가 소유해야 하는 것은 설정(판단)이지 코드가 아니다. 업스트림 이미지와 헬름 차트를 그대로 쓰고, 우리 변경분은 values 파일 하나다.

앞에 얇은 셀프서비스 웹을 하나 얹었다. 직원이 SSO로 로그인해 자기 키를 발급하고 사용량을 보는 화면이다. 이건 우리가 직접 만들었는데, 기능이 부족해서가 아니라 오픈소스 버전의 SSO 연동이 소규모 팀을 넘으면 유료 기능이었기 때문이다. 관리 화면만 우리 것이고, 실제 호출은 이 웹을 거치지 않는다. 그래서 이 웹이 죽어도 이미 발급된 키의 호출은 영향을 받지 않는다.

판단을 밖으로 꺼내면 생기는 이득

설정으로 바뀐 판단은 한 곳에서 바꿀 수 있다. 실제로 있었던 두 사례.

임베딩 리전. 앱마다 임베딩 모델을 미국 리전으로 부르고 있었다. 게이트웨이 설정에서 같은 모델의 진입 리전을 서울로 바꿨다. 앱 코드 변경 없이 응답 시간이 5분의 1로 줄었다. 그 전에도 각 앱이 각자 바꿀 수 있었지만, 저장소 수만큼의 PR·배포·검증이 필요했고 그래서 아무도 안 했다. 싸게 바꿀 수 있는가가 실제로 바뀌는가를 결정한다.

제공자 상관 끊기. Claude 계열 별칭의 대체 경로가 전부 한 클라우드 안에 있었다. 그 클라우드의 전역 장애나 계정 한도 소진이면 다 같이 죽는 구조였다. 설정에 4순위로 Anthropic 직접 호출을 추가했다. 단가가 같아서 넣는 비용이 없었다. 이런 판단을 각 앱이 각자 하고 있었다면, 어느 앱은 했고 어느 앱은 안 했을 것이다.

그리고 관측. 앱마다 각자 계측하면 각자의 사각지대가 생기고, 사각지대는 누가 말해 주지 않으면 영원히 모른다. 실제로 앱 계측만 믿었을 때 한 호출 경로가 통째로 장부에서 빠져 있었고, 게이트웨이를 지나가게 하고 나서야 드러났다.

반박시 님 말이 다 맞음

여기까지 읽으면 반론이 세 개 떠오를 것이다. 전부 맞는 반론이고, 나도 도입하면서 같은 걱정을 했다.

“단일 장애점이 생기는 것 아닌가”

맞다. 게이트웨이는 특정 제공자의 장애는 흡수하지만 자기 자신이 멈췄을 때 대신 우회해 줄 수는 없다.

그래서 세운 원칙이 “기존 경로를 지우지 않는다”다. 앱은 게이트웨이 호출이 실패하면 원래 쓰던 직접 호출로 돌아가게 해 두고 전환한다. 갈아치우는 게 아니라 앞에 한 겹 얹는 것이다. 그리고 언제 그 겹을 걷어낼지는 앱 팀이 정한다. 플랫폼이 기한을 요구하지 않는다. 지우지 않고 계속 두기로 해도 된다.

이 원칙은 실제로 한 번 작동했다. 첫 파일럿 앱을 전환한 직후 게이트웨이 파드 하나가 메모리 문제로 죽었는데, 그 파드로 가던 요청은 앱이 직접 호출로 전환해 정상 완료했고 나머지는 살아 있는 파드가 받았다. 우회 경로가 없었다면 그 요청들은 그냥 실패였다.

“한 홉 더 타면 느려지지 않나”

구조적으로는 클러스터 내부 서비스 주소 한 홉이고, 제공자 쪽 DNS와 연결은 게이트웨이가 캐시·재사용한다(aiohttp 기준 DNS 캐시 300초, keepalive 120초 기본). LLM 호출 지연의 지배 항은 제공자 왕복이라, 프록시 홉이 그보다 크기는 어렵다.

다만 정직하게 적으면, 같은 조건의 직접 호출과 나란히 재 본 수치는 아직 없다. 게이트웨이 경유로 가장 작은 모델을 짧게 부르면 왕복 0.4초대, 첫 토큰까지 0.4초 남짓이라는 실측은 있는데, 비교 대상이 없는 숫자다. 파일럿에서 같은 작업으로 양쪽을 재기로 했다. “안 느려집니다”라고 말하고 싶었지만, 근거 없이 말하면 첫 질문에 무너진다.

“스트리밍은 어떻게 되나”

첫 토큰까지의 시간(TTFT)이 중요한 서비스는 여기가 진짜 쟁점이다. 두 가지를 먼저 정해야 한다.

  • 첫 청크 경계 계약. 첫 청크가 나오기 전 실패는 직접 호출로 전환하고, 첫 청크 이후 실패는 fallback하지 않고 응답을 실패시켜 상위에서 재시도한다. 이미 내보낸 청크 뒤에 다른 응답을 이어 붙이면 문장이 섞이기 때문이다. 이건 새 제약이 아니라 직접 호출에서도 이미 그런 동작이다.
  • stream_timeout. 게이트웨이 설정에 전체 응답 타임아웃만 있고 첫 청크 타임아웃이 없으면, 첫 청크가 매달렸을 때 전체 타임아웃까지 fallback이 돌지 않는다. 우리 설정에 이게 빠져 있었다. 첫 토큰 전 총 예산(stream_timeout × 재시도 × 체인 깊이)이 앱의 TTFT 목표 안에 들어오게 잡아야 한다.

이걸 처음 설계할 때 “게이트웨이 스트림을 끝까지 모아 두고 성공하면 내보낸다”는 안이 있었다. 실패 시 깔끔하게 직접 호출로 넘어갈 수 있어서다. 그런데 그건 스트리밍을 끄는 것과 같다. 스트리밍 앱에는 첫 청크 경계 계약이 맞다.

원칙 네 개

도입하면서 앱 팀과 합의한 원칙이다. 하나씩 “지켜졌는지 확인하는 방법”을 붙였다. 약속만 적으면 약속으로 끝난다.

원칙확인 방법
공통 운영은 플랫폼이 맡는다 — 제공자 키, 한도, 경로, 장애 알림과 복구각 팀이 반복하던 작업이 실제로 줄었는가
앱의 업무 계약은 유지한다 — 모델·출력 변경은 앱 소유자가 검증같은 품질·형식으로 작업이 끝나는가
복구 방식은 작업 성격에 맞춘다 — 배치는 큐·재시도, 실시간은 검증된 우회정해둔 시간 안에 감지·복구되는가
검증 전에 기존 경로를 제거하지 않는다속도와 비용을 각 팀이 직접 확인한 뒤 결정

첫 파일럿에서는 호출 경로만 바꾸고 모델은 그대로 둔다. 경로 전환과 모델 교체를 동시에 하면 뭐가 문제인지 알 수 없다.

정리

지난 글의 결론이 “컨텍스트는 세션에, 컨벤션은 저장소에”였다면, 이번 글은 그 다음 줄이다.

규칙은 저장소에, 판단은 시스템에.

에이전트는 규칙을 잘 지킨다. 못 지키는 건 규칙이 아닌 것들이다. 그 판단들 중 일관돼야 하는 것을 골라 코드 밖으로 꺼내고, 문서로 부탁하지 말고 시스템이 소유하게 하는 것. 시크릿이 그랬고, 접근이 그랬고, 배포와 관측이 그랬다. LLM 호출은 그 다섯 번째였고, 판단의 밀도가 가장 높은 축이었다.

개인이 팀이 된 시대에 팀이 하던 일을 개인이 다시 할 수는 없다. 대신 팀이 하던 일 중 시스템이 할 수 있는 것을 시스템에 넘기면, 개인은 — 그리고 그 개인이 돌리는 에이전트들은 — 남은 판단에 집중할 수 있다.