LLMOps 11

Decision Policy로 워크플로우의 흐름을 읽고 통제하기

분기 조건은 구현하면서 if문으로 넣으면 된다고 보기 쉽다.

근거 · 교안 p29

Decision Policy로 워크플로우의 흐름을 읽고 통제하기 대표 이미지

1장: if/else가 충분하다고요, 정말로?

솔라는 방금 읽은 문장 하나에 마음이 걸렸다. 노트북 화면 한구석에 떠 있는 기술 문서의 한 구절이었다.

"Decision Policy는 다음 행동을 어떻게 결정할지, 언제 반복하고 언제 종료할지 규칙으로 정의한다."

솔라는 턱을 괸 채 중얼거렸다.

“이건 그냥 if/else 문이잖아? 조건에 따라 다음 할 일을 정하고, 반복할지 말지, 언제 끝낼지 결정하는 거. 프로그래밍의 가장 기본인데, 왜 굳이 ‘Decision Policy’라는 거창한 이름을 붙인 거지?”

솔라는 확신에 차 있었다. 이미 잘 쓰고 있는 도구에 새로운 이름을 붙여 복잡하게 만들 필요는 없다고 생각했다. 그때, 솔라의 혼잣말을 들은 루나가 조용히 다가와 노트북 화면을 들여다보았다.

“뭐가 그렇게 단호해?”

“언니, 이것 좀 봐. ‘Decision Policy’라는 게 결국 if문으로 조건 따져서 분기 처리하는 거랑 똑같은 이야기 아니야? ‘성공하면 다음 단계로’, ‘실패하면 종료’ 같은 거. 코드로 다 할 수 있는 건데.”

솔라는 자신만만하게 말했다. 루나는 잠시 생각하더니, 고개를 끄덕이는 대신 옆에 있던 메모지와 펜을 솔라 쪽으로 밀어주었다.

“그럼, 간단한 워크플로우 하나만 글로 짜볼래? 코드는 아니고, 그냥 논리 흐름만.”

“좋아. 뭔데?”

“온라인 문서 제출 시스템이라고 상상해 봐. 문서를 받으면 세 단계로 검증할 거야. 첫째, 파일 형식이 올바른지. 둘째, 내용에 필수 항목이 다 들어있는지. 셋째, 데이터베이스의 기존 정보와 일치하는지.”

솔라는 고개를 끄덕이며 듣고 있었다. 별로 복잡할 것 없다는 표정이었다.

“만약 세 단계 검증을 모두 통과하면 ‘성공’ 처리하고 다음 워크플로우로 넘겨. 그런데 셋 중 하나라도 실패하면, 처음부터 다시 시도해야 해. 단, 재시도는 총 세 번까지만 가능해. 세 번 시도했는데도 실패하면, ‘종료’ 처리하고 관리자에게 알림을 보내는 거야.”

솔라는 자신 있게 펜을 들었다. “간단하네. if문을 중첩해서 쓰면 금방이지.”

솔라는 메모지에 익숙하게 들여쓰기를 하며 논리를 써 내려가기 시작했다.

if (재시도 횟수 < 3) {
  if (파일 형식 검증 성공) {
    if (필수 항목 검증 성공) {
      if (DB 정보 일치) {
        // 성공!
      } else {
        // 재시도
      }
    } else {
      // 재시도
    }
  } else {
    // 재시도
  }
} else {
  // 종료
}

뭔가 이상했다. 솔라는 펜을 멈췄다. ‘재시도’ 로직이 여러 군데 흩어져 있었다. 그리고 재시도 횟수를 세는 변수는 어디에 두고 언제 1씩 더해야 할까? 실패할 때마다 재시도 횟수 += 1을 넣어야 하나? 코드가 금세 지저분해질 것 같았다.

솔라는 잠시 고민하다가 구조를 바꿔 다시 써보았다.

재시도 횟수 = 0
while (재시도 횟수 < 3) {
  결과 = (파일 형식 검증 && 필수 항목 검증 && DB 정보 일치)
  if (결과 == 성공) {
    // 성공! break;
  } else {
    재시도 횟수 += 1
  }
}

if (재시도 횟수 == 3) {
  // 종료
}

“어라… 이것도 좀 이상한데.”

두 번째 방식은 조금 나아 보였지만, ‘성공’과 ‘종료’라는 최종 결과가 코드의 위아래로 쪼개져 있었다. 한눈에 파악하기가 어려웠다. 만약 여기서 ‘재시도 횟수를 5번으로 바꿔주세요’라는 요청이 들어오면 while (재시도 횟수 < 3) 부분과 if (재시도 횟수 == 3) 부분을 둘 다 찾아서 고쳐야 했다. 실수가 나오기 딱 좋은 구조였다.

솔라는 머리를 긁적이며 중얼거렸다. “규칙은 분명 간단했는데, 막상 if/else로 표현하려니 왜 이렇게 헷갈리지? 실패했을 때 ‘재시도’를 해야 할지 ‘종료’를 해야 할지 결정하는 핵심 로직이 여기저기 흩어져 있잖아.”

그 모습을 지켜보던 루나가 조용히 물었다.

“그 복잡한 메모는 잠시 옆에 두고, 원래 하려던 규칙이 뭐였는지 말로만 다시 설명해볼래? 코드 말고, 그냥 우리가 지켜야 할 원칙 말이야.”

루나의 말에 솔라는 엉망이 된 메모지를 힐끗 본 뒤, 생각에 잠겼다. 그리고는 천천히 입을 열었다.

“규칙은 이거지.”

솔라는 손가락을 펴 보이며 하나씩 짚었다.

“첫째, 세 가지 검증이 모두 성공하면, 다음 행동은 ‘성공’이다.” “둘째, 검증 중 하나라도 실패하고 재시도 횟수가 3번 미만이면, 다음 행동은 ‘재시도’이다.” “셋째, 검증 중 하나라도 실패하고 재시도 횟수가 3번 이상이면, 다음 행동은 ‘종료’이다.”

말을 마친 솔라는 순간 할 말을 잃었다. 방금 자신이 말한 세 문장은 놀랄 만큼 명확하고 간결했다. 반면, if/else로 가득 찬 메모지는 누가 봐도 복잡하고 어지러웠다.

그제야 솔라는 깨달았다. 아까 읽었던 문장, "Decision Policy는 ... 규칙으로 정의한다."의 진짜 의미를. 그것은 if/else라는 코드 구현에 대한 이야기가 아니었다. 복잡한 로직의 뼈대가 되는 의사결정 규칙 그 자체를 코드와 분리해서 명확하게 정의하라는 뜻이었다. 코드는 그저 그 규칙을 실행하는 도구일 뿐이었다.

“아…”

솔라는 낮은 탄성을 내뱉었다. “규칙을 정의하는 것과, 그 규칙을 코드로 구현하는 것은 전혀 다른 차원의 이야기였구나.”

규칙은 명확해야 하고, 구현은 그 규칙을 충실히 따르기만 하면 되는 것이다. 두 개가 뒤섞이는 순간, 시스템은 길을 잃기 시작한다. 솔라는 자신이 엉망으로 만든 메모지와, 방금 입으로 말한 명료한 세 개의 규칙을 번갈아 보았다. 규칙과 구현의 분리. 그 필요성을 온몸으로 체감하는 순간이었다.

하지만 새로운 궁금증이 고개를 들었다.

‘좋아, 규칙을 코드가 아닌 별도의 문서로 먼저 정의하는 게 중요하다는 건 알겠어. 그런데 그게 다인가? 그냥 개발자들이 코딩하기 전에 문서를 잘 만들어두자는, 일종의 좋은 습관 같은 거 아닐까? 결국 시스템이 돌아갈 땐 코드가 전부인데, 규칙을 따로 적어두는 게 실제 워크플로우의 일관성이나 통제에 정말로 직접적인 영향을 줄 수 있을까?‘

2장: 워크플로우가 길을 잃지 않도록: 통제된 일관성

솔라의 머릿속에 남은 질문은 선명했다. 규칙을 코드와 분리해서 명확한 문서로 만드는 것이 중요하다는 점은 알겠다. 하지만 그게 개발자들 사이의 약속이나 좋은 습관 이상의 의미가 있을까? 결국 시스템을 움직이는 건 코드인데, 종이에 적힌 규칙이 어떻게 워크플로우를 ‘통제’한다는 걸까.

솔라의 고민을 읽은 듯, 루나는 말없이 솔라가 어지럽게 썼던 메모지 옆에 깨끗한 메모지 두 장을 더 놓았다. 그리고는 첫 번째 메모지에 솔라가 썼던 것과 미묘하게 다른 논리 흐름을, 두 번째 메모지에는 전혀 다른 방식으로 같은 규칙을 구현한 코드를 간결하게 적어 내려갔다.

[개발자 A의 코드]

// 문서 검증 로직 A
let attempts = 0;
while (attempts <= 3) {
  if (run_all_checks()) {
    return 'SUCCESS';
  }
  attempts++;
}
return 'FAILURE';

[개발자 B의 코드]

// 문서 검증 로직 B
for (let i = 0; i < 3; i++) {
  const result = run_all_checks();
  if (result.is_ok) {
    break; // 성공 시 반복 중단
  }
  if (i == 2) { // 마지막 시도였으면
    return 'FAILURE';
  }
}
return 'SUCCESS';

이제 책상 위에는 솔라가 처음 썼던 복잡한 if 중첩 메모지까지 총 세 개의 각기 다른 ‘구현’이 나란히 놓였다. 모두 ‘3회 재시도’라는 동일한 규칙을 따르려 했지만, 그 모습은 제각각이었다.

루나가 조용히 상황을 설정했다. “자, 솔라. 우리 시스템에 큰 문제가 생겼어. 어떤 사용자들의 문서가 검증 과정에서 무한정 멈춰있다는 불만이 폭주하고 있어. 그리고 우리 시스템의 문서 검증 로직은 이 세 명의 개발자가 각자 다른 부분에 구현해 뒀지. 원인이 뭘까? 어디부터 봐야 할까?”

솔라는 눈앞에 놓인 세 개의 메모지를 번갈아 보았다. 머리가 지끈거렸다.

“일단… A 개발자 코드를 볼까? while (attempts <= 3) 이니까, 실제로는 네 번 시도하는 거네. 규칙은 세 번이었는데. 여기서 문제가 생길 수도 있겠다. 아니, B 개발자 코드는 더 헷갈려. 성공했을 땐 break로 빠져나가는데, 실패했을 때 return 'FAILURE'로 가는 조건이 i == 2 일 때만 있네? 만약 run_all_checks() 함수가 에러를 뱉어서 result.is_ok가 아예 없는 상황이 되면 어떻게 되지? 이 반복문은 그냥 끝나버리고 ‘SUCCESS’를 반환할 수도 있겠는데?”

솔라는 이내 고개를 저었다. “아니, 이건 추측일 뿐이야. 진짜 문제는 따로 있어.”

“어떤 문제?”

“세 코드 모두 ‘3회 재시도’라는 규칙을 자기 방식대로 해석해서 구현했다는 게 문제야. A는 4번, B는 특정 상황에서 실패를 놓칠 수도 있고, 내가 처음 썼던 코드는 너무 복잡해서 또 다른 실수가 숨어있을지 몰라. ‘재시도 횟수를 5회로 늘려주세요’라는 간단한 요청이 들어오면, 이 세 군데를 모두 찾아서 고쳐야 해. 그러다 하나라도 빼먹거나 또 다르게 고치면, 시스템은 영원히 혼란에서 벗어날 수 없을 거야.”

솔라는 마침내 핵심을 짚었다. “진짜 원인은 코드가 아니라, ‘3회 재시도’라는 규칙의 ‘진실’이 세 군데로 쪼개져 있다는 거야. 규칙이 하나로 정해져 있고, 모든 코드가 그 하나의 진실을 참조하도록 강제되었다면 이런 일은 없었을 텐데.”

바로 그 순간, 솔라는 ‘통제’라는 단어의 무게를 실감했다. Decision Policy 문서는 단순히 개발자에게 ‘이렇게 만드세요’라고 알려주는 가이드라인이 아니었다. 그것은 시스템 전체에 흩어져 있는 분기(Branch)와 반복(Loop) 로직이 따라야 할 유일한 ‘진리의 원천(Single Source of Truth)‘이었다.

"Decision Policy는 Workflow의 Branch와 Loop를 일관되게 통제한다."

이제 이 문장은 다르게 읽혔다. ‘통제한다’는 것은, 제각각 해석될 여지를 없애고 모든 의사결정이 단 하나의 기준을 따르도록 강제한다는 의미였다. 정책이 코드와 분리되어 문서로 존재할 때, 우리는 비로소 버그가 발생하면 코드의 복잡한 논리를 파헤치는 대신 ‘실행 기록이 정책을 따랐는가?‘라는 명확한 질문 하나로 원인을 추적할 수 있게 된다. 이것이 바로 정책 분리가 가져다주는 ‘일관성’과 ‘통제 가능성’의 진짜 가치였다.

“그렇구나… 규칙을 별도 문서로 분리하는 건 그냥 깔끔하게 정리하자는 게 아니었어. 시스템 전체의 혼란을 막고, 문제가 생겼을 때 길을 잃지 않게 해주는 나침반 같은 거였네.”

솔라는 정책을 코드에서 떼어내는 것이 시스템의 신뢰성을 높이는 핵심적인 설계 원칙임을 깨달았다. 하지만 곧바로 또 다른 의문이 피어올랐다.

‘좋아, 재시도 횟수처럼 고정된 규칙은 이렇게 통제하는 게 맞겠어. 그런데 만약 규칙 자체가 동적으로 변해야 한다면 어떨까? 예를 들어, ‘일반 사용자는 3번, 프리미엄 사용자는 10번 재시도’라거나, ‘두 번 실패하면 자동으로 사람에게 검토를 요청(HITL)하고, 그 후에 한 번 더 시도’하는 것처럼 복잡한 조건이 붙는다면? 이런 것까지 간단한 정책 문서로 다룰 수 있을까? 결국 다시 복잡한 if문이 필요해지는 거 아닐까?‘

3장: 복잡한 결정의 나침반: 재시도, HITL, 그리고 명확성

정책을 코드에서 분리하는 것이 시스템 전체의 혼란을 막는 나침반 역할을 한다는 것을 깨달은 솔라. 하지만 그 나침반이 모든 길을 알려줄 수 있을지는 여전히 의문이었다. 고정된 규칙이라면 몰라도, 상황에 따라 동적으로 변하는 복잡한 규칙 앞에서도 과연 유용할까?

솔라의 이런 고민을 알아차렸는지, 루나는 조용히 새 메모지를 한 장 꺼내 들었다. 이전 장에서 솔라가 겨우 명료하게 정리했던 세 가지 규칙 위에 새로운 조건을 덧붙이기 시작했다.

[새로운 요구사항]

  1. 사용자 등급이 ‘프리미엄’이면 재시도 횟수는 10번까지 허용한다. ‘일반’ 사용자는 기존처럼 3번이다.
  2. 사용자 등급과 관계없이, 검증에 2번 연속 실패하면 다음 행동은 ‘재시도’가 아니라 ‘사람에게 검토 요청(HITL)‘으로 바뀐다.

루나는 펜을 내려놓고 솔라를 바라보았다. “자, 이 새로운 규칙들을 네가 아까 짰던 코드에 한번 반영해 볼래?”

솔라는 자신 없이 메모지를 받아 들었다. 머릿속이 다시 복잡해지기 시작했다. ‘일단 사용자 등급을 확인하는 if문이 추가되어야겠지. 그리고 연속 실패 횟수를 세는 변수도 필요하고…’

솔라는 펜을 들고 끙끙거리며 새로운 논리를 엮어보려 애썼다.

// ... 이전 코드 ...
consecutive_failures = 0;

while (attempts < (user.isPremium ? 10 : 3)) {
  if (run_all_checks()) {
    // 성공
    break;
  } else {
    attempts++;
    consecutive_failures++;

    if (consecutive_failures == 2) {
      // 사람에게 검토 요청 (HITL)
      // 근데 여기서 반복문을 끝내야 하나?
      // 아니면 다른 상태로 바꿔야 하나?
      // ... 혼란 ...
      break; 
    }
  }
}
// ...

몇 줄 쓰지 않아 솔라의 손이 멈췄다. 코드는 순식간에 누더기가 되었다. 사용자 등급에 따라 바뀌는 재시도 횟수, 그와 별개로 작동하는 연속 실패 카운트, 그리고 새로운 상태인 ‘사람에게 검토 요청(HITL)’. 이 모든 조건이 하나의 반복문 안에서 뒤엉켜 있었다. ‘재시도’를 할지, ‘HITL’로 넘길지, ‘종료’할지를 결정하는 핵심 로직이 어디에 있는지 한눈에 파악하기 불가능했다.

“이건 아니야…” 솔라는 결국 펜을 내려놓았다. “규칙 몇 개 추가했을 뿐인데, 거의 새로 짜는 수준이잖아. 게다가 나중에 ‘프리미엄 사용자는 HITL 없이 바로 10번 재시도’ 같은 예외 규칙이라도 추가되면… 상상하기도 싫다.”

더 큰 문제는 따로 있었다.

“만약 1년 뒤에 ‘이 프리미엄 사용자의 문서가 왜 두 번 만에 사람에게 넘어갔죠?‘라는 질문을 받는다면, 나는 이 엉망인 코드를 뒤져가며 consecutive_failures 변수가 어떻게 변했는지, if문의 어떤 분기를 탔는지 일일이 설명해야 해. 아무도 이해 못 할 거야.”

의사결정의 이유를 추적하고 증명하는 것이 불가능에 가까웠다.

그때 루나가 솔라의 어지러운 메모 옆에, 전혀 다른 형태의 표 하나를 그려 보였다.

“이게 바로 Decision Policy 문서의 실제 모습이야.”

정책명판단 기준 (State)선택지 (Next Action)조건 (Condition)설명
RetryPolicyVerificationFailedRetry(user.grade == ‘Premium’ AND attempts < 10) OR (user.grade == ‘Normal’ AND attempts < 3)사용자 등급별 재시도 횟수 차등 적용
EscalationPolicyVerificationFailedEscalateToHumanconsecutive_failures >= 22회 연속 실패 시, 사람에게 검토 요청
TerminationPolicyVerificationFailedTerminateOtherwise재시도 및 HITL 조건에 해당하지 않을 경우 종료

표는 놀랍도록 명확했다. 복잡하게 얽혀 있던 조건들이 각자의 정책 이름 아래 질서정연하게 분리되어 있었다. 재시도 규칙, HITL 전환 규칙, 종료 규칙이 서로를 침범하지 않고 독립적으로 존재했다.

솔라는 이 표가 단순한 코드 명세서가 아님을 직감했다.

“아…!”

솔라의 눈이 커졌다. “이 표가 있으면, 아까 그 질문에 바로 답할 수 있어. ‘왜 사람에게 넘어갔죠?’ 라고 물으면, 그냥 ‘EscalationPolicy가 발동했고, 조건은 2회 연속 실패였기 때문입니다.‘라고 말하면 끝나는 거구나. 코드를 한 줄도 보여줄 필요 없이.”

복잡한 if/else의 미로 속에서는 불가능했던 ‘설명 가능성’과 ‘감사 가능성’이 이 표 하나로 확보되는 순간이었다. 코드는 그저 이 정책표를 읽고 실행하는 엔진 역할만 하면 되었다. 정책이 바뀌면 개발자는 코드를 고치는 게 아니라, 이 정책표의 내용을 수정하고 시스템은 그 즉시 새로운 규칙에 따라 움직인다.

‘반복, 종료, 재시도, HITL 전환 같은 정책을 문서로 분리해야 원인 추적과 통제가 쉬워진다.’

이제 솔라는 이 문장의 진정한 의미를 체득했다. Decision Policy는 복잡한 결정을 위한 나침반일 뿐만 아니라, 그 결정의 모든 과정을 기록하고 증명하는 항해일지이기도 했다. 정책을 구조화된 문서로 분리하는 것은 선택이 아닌, 복잡하고 중요한 시스템을 위한 필수적인 설계 원칙이었다. 이것이야말로 진정한 의미의 ‘의사결정 감사 도구’였다.

잠시 후, 솔라는 자신의 노트북을 열었다. 지금 참여하고 있는 프로젝트의 코드베이스에서 가장 골치 아픈 부분, 바로 쿠폰 할인 로직이 떠올랐기 때문이다. 사용자 등급, 이벤트 기간, 쿠폰 종류, 다른 할인과의 중복 여부 등 수많은 조건이 if/else 지옥을 이루고 있어 아무도 선뜻 수정에 나서지 못하는 영역이었다.

솔라는 새로운 페이지를 열고, 방금 루나가 보여준 표를 따라 자신만의 Decision Policy 문서 초안을 작성하기 시작했다.

정책명판단 기준 (State)선택지 (Next Action)조건 (Condition)
CouponPolicyCouponAppliedApplyDiscountuser.grade == ‘VIP’ AND is_weekend()

더 이상 코드로 먼저 생각하지 않았다. 복잡하게 얽힌 규칙들을 하나씩 분리하여, 누가 보아도 명확하게 이해할 수 있는 정책으로 정의하는 것. 그것이 혼란스러운 시스템에 질서를 부여하는 첫걸음임을 솔라는 이제 알게 되었다. 옆에서 그 모습을 지켜보던 루나의 입가에 옅은 미소가 번졌다.