모든 테스트를 통과했지만 실제 데이터에서 실패한 워크플로우: n8n에서의 프로덕션 페이로드 형태
요약
n8n에서 LLM 워크플로우를 프로덕션 환경에 적용하는 과정에서 테스트 통과에도 불구하고 실제 데이터의 '형태(shape)' 때문에 실패한 사례들을 분석합니다. 필드 누락, 데이터 타입 변동, 극단적인 규모의 예외 케이스 등 세 가지 문제점을 제시하며, 경계 지점에서의 '계약' 및 초기 정규화가 필수적임을 강조합니다.
핵심 포인트
- 테스트는 로직이 아닌 실제 데이터의 '형태(shape)'를 검증해야 합니다.
- 필드 누락 시 계약(contract)을 설정하여 흐름을 중단하고 검토 단계로 보내야 합니다.
- 데이터 타입 변동에 대비해 수집 단계에서 모든 데이터를 정규화하는 것이 중요합니다.
- 테스트 데이터는 실제 운영 환경의 극단적인 예외 케이스를 포함해야 합니다.
이 글은 제가 n8n에서 LLM(대규모 언어 모델) 워크플로우를 프로덕션 환경에서 운영하는 과정에 대한 '공개 개발 기록(build-in-public)' 시리즈의 일부입니다. 여기에 언급된 모든 내용은 가설이 아니라, 실제로 목격했던 실패 사례들입니다.
해당 워크플로우는 제가 작성한 모든 테스트를 통과했습니다. 정상 경로에서는 녹색이었고, 빈 입력 값에서도 녹색이었으며, 재시도(retry) 과정에서도 녹색이었습니다. 그러다가 처음으로 실제 데이터를 만나자 한 시간 만에 실패했습니다. 이는 로직상의 버그 때문이 아니라, 제 테스트가 결코 보여주지 못했던 데이터의 '형태(shape)' 때문에 발생한 것이었습니다. 테스트 자체가 틀린 것은 아니었습니다. 단지 너무 순진했을 뿐입니다.
실제 데이터는 세 가지 특정 방식으로 무례합니다. 그리고 각각은 다른 가정(assumption)을 깨뜨립니다.
첫째, 픽스처(fixtures)에서는 항상 존재하지만 프로덕션 환경에서는 때때로 누락되는 필드들입니다. 제가 테스트 행들을 작성할 때는 모두 전화번호, 이메일, 회사 이름이 있었습니다. 하지만 실제 행들은 빈 셀, 누락된 키, 그리고 주소 대신 'Priya에게 문의'라는 단어가 포함된 이메일을 가진 연락처를 가지고 있었습니다. {{$json.email}}을 읽는 노드는 키가 누락되었다고 실패하지 않습니다. 대신 빈 값(empty value)을 다운스트림으로 전달하며, 전송 단계(send step)는 세 노드 후에 실패하거나, 더 나쁜 경우, 쓰레기 데이터와 함께 성공합니다. 해결책은 경계 지점에서의 '계약(contract)'입니다. 즉, 수집(ingestion) 후 첫 번째 노드가 어떤 필드가 반드시 존재해야 하고 어떤 형태를 갖춰야 하는지 단언해야 하며, 계약을 위반하는 모든 것은 흐름을 계속하기보다 검토(review) 단계로 보내져야 합니다. '이 행은 잘못되었다'고 말할 수 없는 워크플로우는 결국 고객에게 더 나쁜 무언가를 말하게 될 것입니다.
둘째, 데이터 타입 변동(type drift): 동일한 필드가 다른 타입으로 도착하는 경우입니다. 테스트에서는 숫자로 인식되었던 수량이 양식(form)에서는 문자열 "12"로, 스프레드시트에서는 "12 units"로 도착했습니다. 하나의 항목을 객체(object)로 보낸 웹훅(webhook)이 두 개의 항목을 배열(array)로 보내는 경우처럼, 단일 항목 케이스가 루프를 완전히 건너뛰어 워크플로우가 아무것도 처리하지 못하고 성공적으로 종료되는 경우가 있습니다. n8n 노드는 조용히 강제 변환합니다: 숫자 비교에서 문자열이 사용되거나, 배열이 예상된 곳에 객체가 들어가는 경우에도 실행은 정상으로 유지되지만 결과는 잘못됩니다. 제가 사용하는 규칙은 이렇습니다. 수집 단계(ingestion)에서 정규화하고, 절대로 후속 단계에서 처리하지 않는 것입니다. 변환(Convert), 공백 제거(trim), 형변환(cast)을 한 곳에서, 트리거 직후에 수행하여 이후 모든 노드가 정규화된 형태를 가정하도록 만드세요. 값이 정규화될 수 없다면, 그것은 최선을 다한 결과가 아니라 검토가 필요한 항목입니다.
셋째, 테스트 환경의 고정 데이터(fixtures)가 절대 포함하지 못하는 규모 및 콘텐츠 극단값들입니다. 10개 행으로 구성된 테스트 시트는 메모 필드에 붙여넣어진 4,000자 길이의 HTML을 숨기고, 하위 CSV를 깨뜨리는 이모지가 포함된 이름을 숨기며, 분당 두 번씩 도착하는 중복 제출을 숨깁니다. 이들 중 어느 것도 실제 운영 환경에서는 예외 케이스가 아닙니다. 그것들은 평범한 화요일입니다. 이제 제 테스트 데이터는 실제 페이로드에서 가져옵니다. 개인 데이터를 제거하고 실제 입력 샘플을 가져와, 빈 값, 특이 타입, 긴 문자열, 중복 제출 같은 '형태(shapes)'만을 유지합니다. 제가 상상해서 만든 데이터만 사용하는 테스트는 저의 상상력만을 시험할 뿐, 워크플로우를 시험하지 못합니다.
경계에 있어야 할 가드
이 세 가지 실패 사례들은 공통적으로 하나의 근본 원인을 가지고 있습니다: 워크플로우가 자신의 입력값을 신뢰했다는 것입니다. 이 '가드(guard)'는 트리거와 모든 다른 요소 사이에 위치하는 작은 검증 단계이며, 세 가지 역할을 수행합니다. 필수 필드가 존재하는지 확인하고 예상되는 타입과 일치하는지 확인합니다. 정규화 가능한 것은 정규화하며—공백을 제거하고, 숫자 문자열로 형변환하며, 단일 객체를 단일 항목 배열로 감쌉니다. 그리고 고칠 수 없는 것은 원래 페이로드와 함께 격리(quarantine)하여, 저하된 형태로 하위 단계로 흘러가게 하는 대신 처리합니다.
그 격리(quarantine) 단계는 대부분의 사람들이 포기하는 것처럼 느껴져 건너뛰는 부분입니다. 하지만 실제로는 정반대입니다. 이는 워크플로우가 자신이 모르는 것을 인정하고, 3단계 후에 조용히 실패하거나 잘못된 결과로 성공하는 대신, 사람이 개입할 수 있는 지점에서 멈추는 것입니다. 검토 단계에서 발견되는 데이터 오류 행(malformed row)은 평범한 화요일의 과제일 뿐입니다. 하지만 고객에게 도달한 데이터 오류 행은 사과문이나 다름없습니다.
정제된 데이터(polite shapes)가 아닌, 실제 데이터 형태(production shapes)로 테스트하기
실제 데이터에 접근하는 워크플로우를 라이브(live)로 배포하기 전에, 저는 그 워크플로우가 당황하도록 설계된 데이터셋을 사용하여 테스트합니다. 예를 들어, 모든 선택적 필드가 누락된 행, 코드가 리스트를 예상하지만 단일 항목인 경우, 단일 항목을 예상하는 곳에 리스트가 있는 경우, 예상보다 10배 긴 값, 중복 배송 건, 그리고 숫자가 들어가야 할 필드에 텍스트가 포함된 경우 등이 있습니다.
저는 단순히 성공 여부뿐만 아니라 카운트(counts)에 대해 단언합니다(assert). 즉, 올바른 수의 행이 처리되었는지, 올바른 수의 행이 격리되었는지, 그리고 조용히 버려진 행이 0개인지 확인하는 것입니다. 이 중 어떤 단언이라도 실패하면, 워크플로우는 아직 라이브로 배포되지 않으며 — 계약(contract)이 유지될 때까지 경계 단계(boundary step)로 되돌아갑니다.
제가 계속 되새기는 규칙은 이렇습니다. 오직 사용자가 직접 만든 데이터에서만 통과하는 워크플로우는 테스트된 것이 아닙니다. 그것은 리허설(rehearsed)했을 뿐입니다. 실제 운영 데이터(Production data)는 즉흥적으로 변할 것이며, 문제는 당신의 워크플로우가 그 즉흥성에 계약으로 대응하는지, 아니면 녹색 실행 로그와 잘못된 결과로 대응하는지에 달려 있습니다.
이 시리즈의 무료 워크플로우 (MIT): [https://github.com/zhp910318/n8n-ai-automation-lab]
제 스토어의 프로덕션 템플릿: [https://payhip.com/haopengailab]
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기