QA Agent 실행 모델을 DAG 스케줄러로 바꾼 날

26년 03월 31일

정적 그룹의 한계

나는 며칠째 이어지던 QA Agent 구조 개편에서 실행 모델을 갈아없은 날이었다.

정적 그룹 기반 실행을 DAG 기반 의존성 스케줄링으로 바꿨다.

17개 파일을 수정했고 버그 4개를 잡았다.

실제 QA 시트로 검증까지 마쳤다.

그 전까지는 테스트 케이스를 정적 그룹으로 묶어서 실행했다.

근데 이 방식은 케이스 사이의 의존성을 정확히 표현하지 못했다.

QA 케이스는 서로 의존성을 가진다.

예를 들어 “관리자 코스 생성” 케이스가 성공해야 “그 코스 수정” 케이스를 돌릴 수 있다.

전자가 실패했는데 후자를 돌리면 당연히 실패한다.

근데 정적 그룹으로는 이런 의존성을 정확히 표현할 수 없었다.

그룹 단위로만 묶을 수 있었지, 케이스 간의 세밀한 의존 관계는 표현이 안 됐다.

그래서 실행 모델을 DAG로 바꿨다.

DAG 기반 의존성 스케줄링

DAG는 유향 비순환 그래프다.

케이스들을 노드로, 의존성을 간선으로 표현한다.

위상 정렬을 돌려서 의존성을 만족하면서 최대한 병렬로 실행할 수 있는 순서를 찾는다.

핵심은 의존성을 명시적으로 모델링하는 거다.

각 케이스가 어떤 케이스의 뒤에 와야 하는지, 어디까지 병렬로 돌려도 되는지를 그래프로 표현한다.

그러니까 정확한 순서를 보장하면서 동시에 최대한 많은 케이스를 병렬로 돌릴 수 있다.

하드 의존성만 남기고 소프트 의존성은 버렸다

의존성을 정리할 때 중요한 결정을 했다.

소프트 의존성을 아예 없애고 하드 의존성만 남긴 거다.

소프트 의존성은 “이 케이스가 먼저 돌면 도움이 된다” 정도의 느슨한 관계였다.

이론적으로는 실행 효율을 올릴 수 있을 것 같았는데, 실제로는 효과가 불분명했다.

그리고 디버깅할 때 복잡도만 늘렸다.

소프트 의존성 때문에 실행 순서를 예측하기가 어려워서, 문제가 생겼을 때 원인을 추적하기 힘들었다.

그래서 “반드시 먼저 성공해야 하는” 하드 의존성만 남겼다.

복잡도를 제거하는 게 더 나은 설계라는 판단이었다.

하위 케이스 자동 차단

의존성과 함께 하나 더 만든 게 하위 단계 차단 설정 플래그다.

케이스가 실패하면, 그 케이스에 의존하는 모든 하위 케이스를 자동으로 막는다.

선행 케이스가 실패했는데 후행 케이스를 돌리는 건 의미가 없다.

무조건 실패하니까. 그러니까

실패가 감지되면 하위 케이스는 재시도하지 않고 바로 차단 표시를 한다.

이렇게 하니까 불필요한 실행이 줄었다.

그리고 실패가 연쇄적으로 전파되는 구조가 명확해졌다.

Lane 기반 병렬 실행

위상 정렬로 실행 가능한 Lane을 찾는다.

Lane은 병렬로 실행할 수 있는 케이스 묶음이다.

Planner가 케이스 특성에 맞춰 1~5개 사이에서 Lane 개수를 동적으로 정한다.

케이스들이 서로 독립적이면 최대한 병렬로 돌리고 의존성이 엮여 있으면 순차에 가깝게 돌린다.

고정된 병렬도가 아니라, 의존성 그래프 형태에 맞춰 동시성을 조절하는 셈이다.

2단계 재시도와 맞물리다

이 DAG 스케줄러는 며칠 전에 만든 2단계 재시도랑 맞물려서 돌아간다.

1단계에서는 DAG 기반으로 전체 케이스를 병렬로 돌린다.

통과, 실패, 막힘을 수집한다.

2단계에서는 막힌 케이스만 다시 돌린다.

이때 Inspector가 분석한 막힌 원인이랑 통과한 이웃 케이스의 컨텍스트를 주입한다.

DAG가 의존성을 정확히 표현하니까, 재시도할 때도 어떤 케이스가 선행되어야 하는지 알 수 있다.

막힌 케이스만 다시 돌리되, 그 케이스가 의존하는 케이스가 통과한 상태인지 확인하고 돌린다.

버그 4개 수정

구조를 바꾸면서 남아 있던 버그도 같이 잡았다.

executionGroups에서 cases로 바꾸는 과정에서 남은 잔재를 정리했다.

Executor 타임아웃 구조를 다듬었다.

막힌 케이스 원인 분석 로직을 보강했다.

그리고 source-lookups라는 기능을 완전히 걷어냈다.

구조 전환할 때 옛날 코드 잔재가 남으면 이상한 곳에서 버그가 터진다.

그래서 새 구조로 옮기면서 옛날 코드를 확실히 지우는 게 중요했다.

실제 시트로 검증

마지막으로 실제 QA 시트로 검증했다.

목이 아니라 진짜 Notion이랑 Google Sheets에 있는 테스트 케이스를 끝까지 돌렸다.

실제 데이터로 돌려야 진짜 문제가 드러난다.

목 테스트로는 잡을 수 없는 의존성 케이스나, 실제 시트 구조에서만 나오는 엣지 케이스가 있으니까. 다행히 검증이 통과했다.

마무리

정적 그룹에서 DAG로 바꾸니까 두 가지를 동시에 잡았다.

의존성을 정확히 표현하면서 동시에 병렬 실행을 극대화했다.

소프트 의존성을 버린 결정이 특히 잘 먹혔다.

복잡도를 줄이니까 실행 순서가 예측 가능해졌고 디버깅도 쉬워졌다.

“있으면 좋을 것 같은” 기능을 과감히 빼는 게 더 나은 설계로 이어지는 경우가 많다.

이 DAG 기반 병렬 설계는 약 두 달 반 뒤에 다시 검토 대상이 됐다. 병렬 Executor들이 같은 계정으로 동시에 로그인하면서 중복 로그인 문제가 생겼기 때문이다.

결국 순차 실행 단일 에이전트 방향으로 일부 선회했다.

하지만 이날 만든 의존성 모델이랑 위상 정렬 접근은 그 이후에도 참고가 됐다.