Elevare Digital의 AI 에이전트가 유휴 상태(idle)가 된 이유는 새로 추가된 PostgreSQL 행 수준 보안(RLS) 정책이 모든 작업(job) 행을 필터링하여 큐가 비어 있는 것처럼 보이게 만들었기 때문입니다. 이 실수는 작업이 쌓일 때까지 발견되지 않았고, 결국 팀은 오케스트레이터가 빈 큐를 감지하는 방식을 재설계해야 했습니다.
숨겨진 사각지대
Elevare의 자율 AI 시스템인 ARIA는 대기 중인 작업을 확인하기 위해 PostgreSQL 테이블을 폴링(poll)합니다. 쿼리는 성공했고, 0개의 행을 반환했으며, 에이전트는 휴면 상태로 들어갔습니다. 실제로는 테이블이 가득 차 있었습니다. RLS 정책이 특정 사용자 세트에 대해서만 SELECT 액세스를 제한했기 때문입니다. 오케스트레이터는 bypass 권한이 없는 서비스 역할(service role)로 연결되었고, 데이터베이스는 결과 세트에서 모든 행을 조용히 제거했습니다. PostgreSQL은 필터링된 읽기를 빈 테이블과 동일하게 처리하므로 오류, 경고 또는 실패 코드가 나타나지 않았습니다. 유휴 상태인 에이전트의 정상적인 하트비트(heartbeat)는 아무런 문제가 있다는 징후를 보여주지 않았습니다.
RLS가 어떻게 가득 찬 큐를 침묵으로 바꿨는가
RLS는 SELECT 중에 각 행에 서술어(predicate)를 추가합니다. 서술어가 거짓(false)이면 해당 행은 결과에서 사라집니다. 클라이언트는 정책을 충족하는 행만 볼 수 있으며, 행이 숨겨졌다는 사실을 전혀 알 수 없습니다. 큐 워커(queue worker)에게 빈 결과 세트는 실제로 비어 있는 큐와 똑같이 보입니다. 오케스트레이터는 "행 없음 = 작업 없음"이라고 가정하고 유휴 루프(idle loop)에 진입하는 동안, 보이지 않는 곳에서 작업이 계속 쌓였습니다.
팀은 개별 사용자로 읽기 범위를 제한하려던 정책이 의도치 않게 서비스 역할 자체까지 포함했다는 것을 발견했습니다. 해당 역할에 "bypass RLS"라는 특별한 속성이 없었기 때문에, 오케스트레이터가 발행하는 모든 쿼리에 정책이 적용되었습니다. 이는 보안과 관찰 가능성(observability) 사이의 전형적인 트레이드오프를 보여줍니다. RLS는 권한이 없는 사용자로부터 데이터를 보호하지만, 가시성에 의존하는 시스템 구성 요소에 유용한 실패 신호를 제거하기도 합니다.
카나리 체크 패턴
조용한 빈 결과에 대한 의존성을 깨기 위해 Elevare는 "카나리(canary)" 체크를 추가했습니다. 새로운 흐름은 다음과 같습니다:
- 대기 작업 테이블을 쿼리합니다.
- 행이 반환되면 이전과 같이 처리합니다.
- 결과가 비어 있으면, 항상 존재해야 하는 전용 카나리 행에 대해 두 번째 쿼리를 실행합니다.
- 카나리 쿼리가 예상된 행을 반환하면 큐가 정말로 비어 있는 것입니다. 유휴 하트비트를 기록합니다.
- 카나리 쿼리도 아무것도 반환하지 않으면 에이전트가 보지 못하는 상태(blind)입니다. 즉시 경고를 발생시킵니다.
이제 오케스트레이터는 세 가지 상태를 구분합니다:
- 작업 발견 (Jobs found) – 정상 처리.
- 작업 없음, 카나리 정상 (No jobs, canary OK) – 실제 유휴 기간.
- 작업 없음, 카나리 실패 (No jobs, canary failed) – 숨겨진 RLS 차단, 경고 트리거.
카나리 테이블은 절대 변하지 않는 단일 행입니다. 설정하는 데 약 한 시간 정도 걸렸지만, 이로써 조용한 실패의 한 부류를 완전히 제거할 수 있었습니다.
팀이 해야 할 일
PostgreSQL 또는 이를 기반으로 구축된 호스팅 서비스(Supabase 등)에서 큐 워커를 운영하는 경우 다음 단계를 따르십시오:
- "bypass RLS" 플래그가 있는 서비스 역할 자격 증명(service-role credential)을 사용하십시오. 이를 통해 시스템 구성 요소가 사용자 수준 정책과 관계없이 모든 행을 볼 수 있습니다.
- 서비스 역할에 대한 bypass 권한 누락 여부를 확인하기 위해 RLS 정책을 감사(audit)하십시오. 최종 사용자에게는 올바르게 보이는 정책이 의도치 않게 내부 서비스를 가둘 수 있습니다.
- 카나리 테이블(또는 그에 상응하는 항상 존재하는 행)을 추가하고 워커의 유휴 로직에 카나리 체크를 포함하십시오. 추가 쿼리는 비용이 저렴하며 명확한 안전망을 제공합니다.
트레이드오프
RLS는 세밀한 데이터 액세스를 강제하는 강력한 도구로 남아 있습니다. 이는 코드베이스 전체에 애플리케이션 수준의 필터를 뿌리지 않고도 실수로 인한 데이터 유출을 방지하고 멀티 테넌트(multi-tenant) 아키텍처를 지원합니다. 단점은 "행 없음" 신호를 "할 일 없음"으로 예상하는 구성 요소로부터 실패를 숨길 수 있다는 점입니다. 카나리 패턴은 RLS를 약화시키지 않습니다. 대신 관찰 가능성을 복구하는 가벼운 검증 단계를 추가합니다.
요점
숨겨진 RLS 정책은 바쁜 큐를 조용한 막다른 길로 바꿀 수 있으며, 작업이 밀려드는 동안 AI 에이전트를 유휴 상태로 방치할 수 있습니다. 서비스 역할에 적절한 bypass 권한을 부여하고 모든 빈 큐 읽기에 카나리 체크를 결합하십시오. 그러면 팀은 자율 워커의 신뢰성을 유지하고 비용이 많이 드는 사각지대를 피할 수 있습니다.
