여러 Agent가 동시에 뛸 때, 중복 실행과 기억 뒤섞임은 어떻게 막는가

← 전체 목차 · 이전: 다중 Agent를 "역할 나눠 대화"로 조율하면 왜 무너지는가 · 다음: 여러 Agent가 같은 MCP 서버를 함께 써도 되는가

"조정기가 작업을 나눠주고, 실패하면 재시도한다"는 것은 작업 분배이지 일관성 보장이 아니다. 분배는 그 순간 중복 배정만 막을 뿐이고, 진짜 사고는 배정 이후 세 지점 — 의미가 겹치는 작업의 중복 실행, 부분 실패 후 재시도로 인한 부작용 중복, 그리고 병렬 세션 간 기억 오염 — 에서 터진다. 그리고 이 세 지점을 "덮어쓰기 vs 아님"이라는 제목으로만 보고 있으면 방향부터 틀린다: 진짜 사고는 대부분 "잘못 읽음"이지 "잘못 씀(덮어쓰기)"이 아니다.

결과 일관성: 실행 전에 막을 것인가, 실행 후에 대조할 것인가

실패 유형원인사후 대조의 비용
의미 중복 실행작업 분해 입도가 너무 굵어 의미가 겹침이미 두 번 실행됨
재시도 부작용 중복이미 성공한 하위 작업을 실패로 오판해 재실행이미 발생한 결제·발송이 중복됨
쓰기 경합 덮어쓰기통신 지연으로 서로 "내가 먼저"라 착각늦게 쓴 쪽이 이긴 걸 아무도 모름
조정기 다운배정 상태 유실재발송 또는 누락
이종 프로토콜결과 포맷 불일치집계 자체가 실패

기준선: 전역 작업 상태머신 + 하위 작업 멱등성 + 최종 일관성 보상을 한 세트로 삼는다.

분해 전  → 의미 지문으로 중복 판정 (문자열 해시가 아니라 의미 기반)
배정 전  → agent_task 표에 먼저 INSERT
             삽입 성공 → 배정 실행
             삽입 실패(유니크 제약 위반) → 이미 존재하는 결과를 그대로 반환
실행 중  → 상태 전이는 낙관적 락으로: 현재 상태 기준 갱신
             동시 두 번째 요청 → 영향 행수 0 → 충돌로 판정, 재조회
실행 결과 → 모든 하위 작업은 멱등 실행 지원 (재시도가 안전해지는 전제)
이종 Agent → 프로토콜 어댑터가 멱등 인터페이스와 결과 포맷을 통일, 원생 로직엔 무침투

핵심은 판정 지점을 "분배 전"으로 당기는 것이다. 의미 지문에 DB 유니크 제약을 걸면 "중복인가"라는 질문이 삽입 성공/실패라는 원자적 신호로 바뀐다 — 실행 후 사람이 대조하는 것보다 훨씬 싸다. agent_task 표는 global_task_id / sub_task_id / agent_id / task_status / self_execute_result / compensate_status 여섯 필드로 이 판정과 보상을 함께 지탱한다.

보상 ≠ 롤백이다. 이미 대외적으로 효과를 낸 동작(결제, 발송)은 되돌릴 수 없으므로 compensate_statustask_status와 별도 컬럼으로 두어 "실행 실패 + 보상 중"과 "실행 실패 + 보상도 실패"를 구분해야 보상 자체도 멱등하게 재시도할 수 있다.

기억 격리: "덮어쓰기"가 아니라 "잘못 읽는 것"이 진짜 사고다

다중 Agent가 병렬로 뛸 때 "기억이 서로 덮어써지지 않는가"라는 질문 자체가 함정이다. Agent는 기억 격리 능력을 선천적으로 타고나지 않는다 — 격리는 세 가지 설정의 산물이다.

판정 요소기본 모드위험 모드
회화 식별전용 sessionId + 프레임워크 회화 색인표로 조회·강검증저장 자원을 아끼려고 여러 경량 Agent가 하나의 저장 인스턴스를 풀링 공유
결과사설 기억은 sessionId로 격리됨sessionId 전달 오류 또는 색인 강검증 누락 시 다른 Agent의 기억을 잘못 읽음(오염된 읽기)
발현 조건단일 회화·저부하동시성이 올라가야 재현됨 — 로컬 Demo에서 절대 안 보임
사설 기억: sessionId로 격리, 읽기/쓰기 모두 허용
전역 기억: 공용 지식·전역 상태, 읽기만 허용 — 전역 측이 사설 기억을 되쓰지 않음

전역 공유 기억은 읽기 전용으로만 열면 그 자체로는 위험하지 않다. 진짜 위험은 "저장 자원을 아끼려고 여러 회화가 같은 저장 인스턴스를 공유"하는 최적화이며, 이때 sessionId 강검증이 빠지면 격리가 조용히 사라진다.

핵심 통찰

  1. 작업 분배는 "누가 하는가"만 풀고, 일관성은 "다 하고 나서 맞는가"를 푼다 — 조정기가 중복 배정을 막는 것은 배정 순간뿐이다. 진짜 중복 실행은 의미 중복, 실패 재시도, 쓰기 경합이라는 배정 이후 세 지점에서 발생한다. 조정기를 일관성 방안으로 착각하는 것은 라우터의 능력으로 트랜잭션의 책임을 대신하려는 것과 같다.
  2. 판정 지점을 앞당길수록 보상 범위가 줄어든다 — 의미 지문 + DB 유니크 제약은 "중복인가"를 실행 후 인간 대조가 아니라 삽입 한 번의 성공/실패라는 원자적 판정으로 바꾼다. 이 설계 원칙은 다른 중복 방지 문제에도 그대로 이식된다.
  3. 멱등성은 재시도가 성립하기 위한 전제이지 부가 기능이 아니다 — 멱등하지 않은 시스템에서 "재시도 붙이면 되지"는 위험한 말이다. 이미 대외적으로 효과를 낸 쓰기·결제를 한 번 더 실행하게 만든다. 먼저 멱등성이 있어야 재시도가 열린다.
  4. 상태머신 + 낙관적 락은 "상태 비동기화"를 "갱신 실패"로 퇴화시킨다 — 매 변경이 현재 상태 기준이어야 하고, 두 번째 동시 요청은 영향 행수 0을 받는다. 관측 불가능한 분산 비일관성이 재조회·재계산·경보가 가능한 로컬 신호로 바뀐다.
  5. "덮어쓰기"가 아니라 "잘못 읽음"이 기억 사고의 본질이다 — 저장 공간을 공유하는 풀링이 원인이지, 두 Agent가 서로의 기억을 망가뜨리며 쓰는 게 아니다. 이 구분을 못 하면 방어 설계가 쓰기 잠금 쪽으로 잘못 향한다.
  6. 격리성은 컴포넌트의 타고난 속성이 아니라 설정과 검증이 함께 만든 결과다 — sessionId, 회화 색인표, 프레임워크 강검증 이 셋이 함께 작동해야 "격리됨"이 성립한다. 저장 자원을 아끼려고 풀링을 켜는 순간 이 중 하나가 빠지면 결론이 뒤집힌다.
  7. 동시성은 격리 결함의 현상액이다 — 두 사고(중복 실행, 기억 오염) 모두 동시성이 올라가야 재현된다. 로컬 Demo가 저부하·단일 세션으로 도는 한 "안 터졌다"는 것은 반증이 아니라 조건 미충족일 뿐이다.
  8. 자원 최적화는 암묵적으로 격리성과 맞바꾼다 — "여러 경량 논리 개체가 하나의 물리 인스턴스를 공유한다" 형태의 모든 최적화(저장 풀링, KV Cache 공유 등)는 격리 리스크 지점으로 재검토돼야 한다. 수익은 즉시 보이고 손실은 부하가 오른 뒤에야 드러난다.

엔지니어링 실전 Tips

  • 중복 판정 지문은 "요청" 대신 "의미"에 묶을 것. 문자열 해시로 지문을 만들면 같은 뜻을 다르게 표현한 하위 작업이 중복 판정을 피해간다.
  • 유니크 제약이 가장 값싼 분산 락이다. 자체 분산 락보다 컴포넌트 하나 적고, 트랜잭션 롤백에 자연히 딸려 원자성을 얻는다.
  • 낙관적 락 실패는 예외가 아니라 신호로 처리할 것. 영향 행수 0이면 누군가 이미 상태를 진행시켰다는 뜻 — 그대로 재시도하지 말고 상태를 다시 읽고 계속 실행할지부터 판단한다.
  • compensate_statustask_status와 별도 컬럼으로 둘 것. 그래야 "실패 + 보상 중"과 "실패 + 보상도 실패"라는 서로 다른 종단 상태를 표현하고, 보상 자체도 멱등하게 재시도할 수 있다.
  • sessionId는 선택 인자가 아니라 읽기/쓰기 강제 필수 인자로 만들 것. 모든 사고가 "전달 오류 또는 누락"에서 시작된다.
  • 회화 색인표는 사후 반증 자료로도 쓸 것. "이 기억이 실제로 누구 것인가"를 되짚을 수 있는 유일한 물증이므로 쓰기 시점뿐 아니라 사고 조사 시점에도 조회 가능해야 한다.
  • 격리성 검증은 반드시 부하 테스트에 묶을 것. 기능 테스트만으로는 이 계열 결함이 전혀 드러나지 않는다. sessionId가 호출 체인에서 오류 없이 전달되는지를 동시성을 올린 상태에서 확인해야 한다.
  • 저장 풀링·공유 최적화를 도입할 때마다 격리 위험 체크리스트를 강제할 것. "물리 자원 하나를 논리 개체 여럿이 공유하는가"라는 질문에 예가 나오면, 그 지점에 강검증이 있는지부터 확인한다.