Human-in-the-Loop 데이터베이스 마이그레이션: 에이전트가 실행하려는 내용을 승인하기
요약
AI 에이전트가 데이터베이스 마이그레이션을 수행할 때, 단순한 승인 버튼을 넘어 실제 실행될 구문, 잠금 동작, 영향 범위, 롤백 계획을 포함한 정교한 Human-in-the-loop 시스템을 구축해야 함을 강조합니다.
핵심 포인트
- 단순한 '예/아니오' 승인은 불충분하며 구체적인 데이터 차이(diff)를 제공해야 함
- 에이전트가 작성한 요약에 의존하지 말고 실제 실행될 SQL 구문을 검토해야 함
- 잠금(lock) 동작과 영향받는 행(rows)에 대한 가시성 확보가 필수적임
- 제어 평면은 에이전트의 프롬프트가 아닌 데이터 경로 하위 계층에 존재해야 함
요약 (TL;DR): "데이터베이스 변경에는 인간의 승인이 필요해야 한다"는 말은 좋은 조언이지만, 그 관문(gate)을 어떻게 구축해야 하는지에 대해서는 아무것도 알려주지 않습니다. 진정한 승인 인터페이스는 정확한 구문(statement), 잠금 동작(lock behavior), 영향을 받는 행(rows) 또는 문서(documents), 그리고 롤백 계획(rollback plan)을 보여주어야 합니다. 이는 에이전트가 준수하기로 선택하는 프롬프트(prompt)나 도구(tool) 내에 머무는 것이 아니라, 데이터 경로(data path) 상에서 에이전트보다 하위 계층에 존재해야 합니다. 그래야만 에이전트가 해킹당하거나 실수하더라도 살아남을 수 있습니다. 보류된 마이그레이션을 적절한 승인자에게 전달하고, 정확한 차이(diff)가 비준될 때까지 차단하며, 모든 결정을 추가 전용 로그(append-only log)에 기록하십시오. 제어 평면(control plane)은 Postgres, MySQL, MongoDB 전반에 걸쳐 동일한 형태를 가집니다.
조언이 멈추는 지점
AI 에이전트가 운영 데이터(production data)를 다루도록 허용하는 방법에 대한 지침을 검색해 보면 어디에서나 똑같은 문장을 발견하게 될 것입니다: "데이터베이스 변경에는 인간의 승인이 필요해야 한다." 이 말은 옳지만, 그 자체로는 쓸모가 없습니다. 원하는 결과가 무엇인지는 알려주지만, 그것을 어떻게 구축해야 하는지는 알려주지 않기 때문입니다.
이 간극은 매우 중요합니다. 스키마 변경(schema changes)은 잘못된 호출이 발생했을 때 비용이 많이 들고 되돌리기 어려운 작업의 전형이기 때문입니다. 마이그레이션은 다시 실행할 수 있는 SELECT 문이 아닙니다. 이는 다른 시스템이 의존하는 구조를 다시 작성하며, 사용량이 많은 테이블에서는 뒤따르는 모든 쿼리를 중단시키는 잠금(locks)을 유발할 수 있습니다. 프롬프트 인젝션(prompt injection)에 대한 OWASP의 2025년 지침은 이 원칙에 대해 단호합니다: "권한이 없는 작업을 방지하기 위해 권한이 있는 작업에 대해 'Human-in-the-loop 제어'를 구현하고, 핵심 기능은 '모델에 제공하기보다 코드 내에서' 처리하십시오." 이것이 올바른 직관입니다. 이 글은 그 직관을 작동하는 관문으로 바꾸는 것에 관한 것입니다.
어려운 부분은 인간이 내릴 수 있는 결정을 렌더링하는 것입니다
에이전트에 승인 기능을 덧붙이는 대부분의 팀은 "에이전트가 마이그레이션을 실행하려고 합니다. 승인하시겠습니까? [예] [아니오]"라고 적힌 Slack 메시지 단계에서 멈춥니다. 거기서 '예'를 클릭하는 것은 승인이 아닙니다. 그것은 UI를 곁들인 동전 던지기에 불과합니다.
진정한 승인이란 검토자가 승인하기 전에, 자신이 판단할 수 있는 용어로 곧 어떤 일이 일어날지를 볼 수 있어야 함을 의미합니다. 어려운 점은 버튼이 아닙니다. 대기 중인 마이그레이션(migration)을 시니어 엔지니어가 30초 안에 읽고 신뢰하거나 거부할 수 있는 형태로 조립하는 것입니다. 그 조립 단계가 바로 대부분의 자체 구축 시스템이 조용히 실패하는 지점입니다. 왜냐하면 마이그레이션을 작성한 에이전트(agent)가 그것을 요약하는 주체이기도 하며, 행위자가 작성한 요약은 독립적인 통제 수단이 될 수 없기 때문입니다.
승인 가능한 마이그레이션 뷰(view)에 필요한 요소
검토자에게는 네 가지가 필요하며, 의도에 대한 모호한 자연어 설명은 그중 어디에도 해당하지 않습니다.
- 정확한 디프(diff). "주문 쿼리 속도를 높이기 위해 인덱스를 추가합니다"와 같은 식이 아닙니다. 대상 객체의 이름과 함께, 바이트 단위로 실행될 실제 구문(statement)이어야 합니다. 만약 에이전트가 사람이 첫 번째 버전을 읽은 후에 구문을 다시 작성했다면, 그 승인은 무효입니다.
- 예상 락(lock) 동작. 검토자는 해당 구문이 어떤 락을 획득할 것이며 무엇을 차단할지를 볼 수 있어야 합니다. Postgres의 경우,
ACCESS EXCLUSIVE락은 지속 시간 동안 읽기와 쓰기를 차단하며, 락을 기다리는 모든 마이그레이션은 새로운 쿼리들을 그 뒤에 대기하게 만듭니다. 따라서 "작은" 변경이라도 테이블 전체를 멈추게 할 수 있습니다 (Xata). MySQL의 경우,ALGORITHM=INPLACE를 사용하는 온라인 DDL도 최종 완료를 위해 짧은 배타적 메타데이터 락(exclusive metadata lock)이 필요하며, 메타데이터 락을 보유한 장시간 실행되는 트랜잭션이 이를 차단할 수 있습니다 (MySQL docs). 뷰는 이를 구체적으로 보여주어야 합니다. - 영향을 받는 행(rows) 또는 문서(documents) 수. 천 개의 행을 건드리는 백필(backfill)과 1억 개의 행을 건드리는 백필은 SQL 형태는 동일하지만 위험도는 완전히 다릅니다. 검토자에게는 그 규모(scale)가 필요합니다.
- 롤백(rollback) 계획. 작업이 잘못되었을 때 무엇이 이를 되돌릴 수 있는지, 그리고 그 되돌리는 작업 자체가 부하가 걸린 상태에서 실행하기에 안전한지 알아야 합니다. "백업에서 복구하겠습니다"라는 말은 화요일 오후 2시에 실행된 마이그레이션에 대한 롤백 계획이 아닙니다.
만약 승인 인터페이스(approval surface)가 이 네 가지 요소를 모두 보여줄 수 없다면, 인간은 단순히 형식적인 승인(rubber-stamping)을 하는 것에 불과하며, 당신은 통제(control)가 아닌 절차(ceremony)만을 구축한 것입니다.
게이트(Gate)가 위치해야 하는 곳
이 모든 것이 유효한지 결정하는 부분은 바로 여기입니다. 게이트는 에이전트 내부에 존재해서는 안 됩니다.
만약 "DDL을 실행하기 전에 일시 중지하고 인간의 승인을 기다려라"라는 규칙이 시스템 프롬프트(system prompt)의 한 줄이거나 에이전트가 선택하여 호출하는 도구(tool)라면, 그 규칙을 강제하는 것은 결국 에이전트의 추론(reasoning)입니다. 이는 에이전트가 추론을 통해 이를 우회하거나, 우회하도록 유도하는 콘텐츠를 흡수하기 전까지는 괜찮습니다. 간접 프롬프트 주입(Indirect prompt injection)은 작동하기 위해 인간이 읽을 수 있을 필요는 없으며, 단지 모델에 의해 파싱(parsing)될 수만 있으면 됩니다 (OWASP). 에이전트가 스스로를 게이트할 수 있다고 신뢰하는 마이그레이션 도구는 제안(suggestions)으로 만들어진 가드레일(guardrail)에 불과합니다. 읽기 전용(Read-only) 액세스만으로는 충분하지 않으며, 지시 사항 수준의 제어(instruction-level controls)는 적대적 입력(adversarial input) 하에서 실패합니다.
게이트는 에이전트보다 아래, 즉 데이터 경로(data path)에 위치해야 하며, 에이전트의 의도와 상관없이 데이터베이스로 향하는 모든 문장이 반드시 이를 통과해야 합니다. 에이전트가 마이그레이션을 생성하면, 제어 평면(control plane)이 이를 가로채어 스키마 변경(schema change)으로 분류하고 대기시킵니다. 에이전트는 지시 사항을 듣지 않는 컴포넌트를 말로 설득하여 통과할 수 없습니다. 핵심 원칙은 프롬프트 내부가 아니라, 연결 경로(connection path) 상에서 대역 외(out-of-band)로 강제하는 것입니다.
SDK 인터럽트(interrupt)는 제안일 뿐이지만, 대역 외 게이트(out-of-band gate)는 벽입니다.
현재 에이전트 프레임워크들은 Human-in-the-loop 프리미티브(primitives)를 제공합니다. LangGraph의 interrupt()는 도구 노드(tool node) 이전에 그래프를 일시 중지하고 인간이 응답하면 재개합니다. 이것들은 좋은 에이전트 UX를 구축하는 데 진정으로 유용하며, 적극적으로 사용해야 합니다. 하지만 그것들이 무엇인지 이해해야 합니다. 그것들은 에이전트 자체의 런타임(runtime) 내부에서 이루어지는 협력적인 일시 중지(cooperative pause)입니다. 그것들이 작동하는 이유는 에이전트가 해당 노드에 도달했고 인터럽트를 준수하기로 선택했기 때문입니다.
대역외 게이트(Out-of-band gate)는 성격 자체가 다릅니다. 이는 에이전트가 특정 노드에 도달하거나 무언가를 준수하는 여부에 의존하지 않습니다. 이 게이트는 데이터베이스 연결(database connection) 상에 존재하며, 실제 문장(statement)을 확인하고 결정이 비준(ratified)될 때까지 이를 전달하기를 거부합니다. 만약 프롬프트 인젝션(prompt injection)이 에이전트를 설득하여 인터럽트(interrupt)를 건너뛰게 하더라도, 프레임워크 게이트는 사라질 수 있지만 데이터 경로 게이트(data-path gate)는 여전히 남아 있습니다. 하나는 에이전트가 거부할 수 있는 제안인 반면, 다른 하나는 에이전트가 우회할 수 없는 벽입니다. 두 가지 모두 필요하지만, 컴플라이언스(compliance) 문서에 명시할 수 있는 통제 수단은 오직 하나뿐입니다. 동일한 논리가 일반적으로 프로덕션 데이터베이스에 접근하는 에이전트에게도 적용됩니다.
적절한 승인자에게 라우팅하고 비준될 때까지 차단하기
보류된 마이그레이션은 실제로 이를 판단할 수 있는 사람에게 도달할 때만 유용하며, 당직을 서는 주니어 엔지니어가 항상 그 적임자인 것은 아닙니다. 게이트는 단순히 '변경 사항'이라는 이유만으로 작동하는 것이 아니라, '어떤 변경'인지에 따라 라우팅되어야 합니다. 기본값이 없는 Nullable 컬럼을 추가하는 작업은 당직 엔지니어에게 라우팅될 수 있습니다. 하지만 컬럼을 삭제(drop)하거나, 테이블을 재작성(rewrite)하거나, 수백만 개의 행을 백필(backfill)하는 문장은 데이터베이스 소유자나 지정된 승인자 그룹으로 라우팅되어야 하며, 영향 범위(blast-radius)가 큰 변경은 하나 이상의 승인을 요구할 수 있습니다.
요청이 대기열(queue)에 머무는 동안 실행은 차단(blocked)됩니다. 우선순위가 낮아지는 것이 아니라 차단되는 것입니다. 권한이 있는 누군가가 제시된 정확한 차이점(diff)을 비준할 때까지 해당 문장은 데이터베이스에 도달하지 않습니다. 만약 에이전트가 마이그레이션을 다시 생성하면, 시간은 초기화되며 새 버전은 다시 게이트를 통과해야 합니다. 승인은
기록을 남기지 않는 승인은 나중에 방어할 수 없는 승인입니다. 모든 결정, 누가 검토했는지, 그들이 본 정확한 문구는 무엇인지, 언제 승인(ratify)했는지, 그리고 실제로 무엇이 실행되었는지는 에이전트나 엔지니어가 사후에 몰래 수정할 수 없는 추가 전용 로그(append-only log)에 기록되어야 합니다. 6개월 후에 누군가가 왜 특정 컬럼이 삭제되었는지 묻는다면, 그 답변은 삭제되었을지도 모르는 Slack 대화 기록이 아니라 명확한 기록이어야 합니다. 결정 사항과 실행된 문구는 함께 캡처되어 구조적으로 위변조 방지(tamper-evident)가 되어야 합니다.
Postgres, MySQL, MongoDB 전반에 걸친 동일한 게이트
잠금(locking)의 세부 사항은 엔진마다 다르지만, 제어 평면(control plane)의 형태는 모든 엔진에서 동일하며, 이것이 핵심입니다. 스키마 변경(schema change)은 그것을 실행하는 주체가 무엇이든 간에 권한이 필요하며 되돌리기 어려운 작업입니다.
- Postgres.
ALTER TABLE변형 및 인덱스 빌드는ACCESS EXCLUSIVE잠금을 가질 수 있습니다. 게이트는 잠금 클래스(lock class)와 대기열 위험(queue risk)을 표시하여, 검토자가 승인하기 전에 영향 범위(blast radius)를 확인할 수 있도록 합니다. - MySQL. 온라인 DDL은 잠금을 줄여주지만, 커밋을 위해서는 여전히 짧은 배타적 메타데이터 잠금(exclusive metadata lock)이 필요하며, 열려 있는 트랜잭션이 이를 지연시킬 수 있습니다 (MySQL docs). 게이트는 알고리즘과 차단 조건(blocking conditions)을 드러냅니다.
- MongoDB. 이전 버전의 MongoDB에서는 포그라운드(foreground) 인덱스 빌드가 진행되는 동안 컬렉션 수준의 쓰기 잠금(write lock)을 유지할 수 있었습니다. 최신 버전은 덜 제한적인 잠금 체계 하에서 인덱스를 빌드하며, 시작과 끝에만 잠시 더 강력한 잠금을 사용합니다 (MongoDB docs). 게이트는 인덱스 빌드를 그 본질에 맞게 스키마 변경으로 취급합니다.
검토자가 각 엔진의 잠금 매트릭스(lock matrix)를 모두 외울 필요는 없습니다. 게이트는 위험을 엔진별 용어로 표현하고 동일한 정책을 적용합니다: 변경 사항 분류, 차이점(diff) 및 영향도 표시, 적절한 담당자에게 전달, 승인될 때까지 차단, 결과 기록.
출처
출처
- OWASP Gen AI Security Project, LLM01:2025 Prompt Injection: https://genai.owasp.org/llmrisk/llm01-prompt-injection/
- Xata, 스키마 변경 및 Postgres 잠금 큐: https://xata.io/blog/migrations-and-exclusive-locks
- MySQL 8.0 참조 매뉴얼, 온라인 DDL 성능 및 동시성: https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-performance.html
- MongoDB 매뉴얼, FAQ: 동시성: https://www.mongodb.com/docs/manual/faq/concurrency/
- LangChain Docs, Human-in-the-loop: https://docs.langchain.com/oss/python/deepagents/human-in-the-loop
원래는 Datapace 블로그에 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기