이 상태 모델은 언제 쓸 만하고 언제 과할까
Categories:
앞 글까지 정리하고 나면 상태 모델이 꽤 그럴듯해 보인다. queue가 있고, lease가 있고, 대상 단위 lock이 있고, pipeline 단계마다 성공과 실패를 기록한다.
이번 글의 질문은 도입 기준이다. 어디까지 만들면 충분하고, 어디부터는 과한가.
하지만 여기서 바로 경계해야 할 질문이 있다.
이걸 모든 데이터 작업에 넣어야 할까
답은 아니다. 단순한 cron job이라면 과하다. 매일 한 번 실행하고, 실패하면 로그를 보고 사람이 다시 돌려도 되는 작업이라면 queue와 lease, 상태 전이까지 넣는 것이 오히려 운영 부담이 될 수 있다.
예를 들어 이런 작업은 단순한 구조로 충분할 수 있다.
매일 새벽 report 하나 생성
실패해도 다음 날 다시 생성 가능
중복 실행돼도 큰 문제 없음
실행 시간이 짧음
이 경우에는 cron과 script, 간단한 로그만으로도 충분하다.
반대로 다음 조건이 붙기 시작하면 상태 모델의 가치가 커진다.
- 실행 시간이 길다
- 중복 실행되면 데이터나 파일이 깨질 수 있다
- 실패 후 어디까지 진행됐는지 알아야 한다
- 취소, 재시도, 복구가 필요하다
- 여러 worker가 병렬로 작업을 처리한다
- 외부 시스템 또는 파일 시스템에 side effect를 남긴다
- 작업 결과를 운영 화면이나 API로 조회해야 한다
이 조건들은 데이터 엔지니어링에서 자주 나온다.
ETL
ELT
CDC ingestion
파일 수집
외부 API sync
retention
compaction
data quality check
feature pipeline
backfill
이런 작업들은 대부분 단순한 함수 호출이 아니다. 오래 걸리고, 실패하고, 다시 실행되고, 중간 결과가 남는다. 그래서 상태를 명시하지 않으면 운영자가 결국 로그와 감으로 상태를 추측하게 된다.
상태 모델을 도입하면 이런 질문에 답할 수 있다.
지금 대기 중인 작업은 무엇인가
어떤 worker가 가져갔는가
실행 중인 작업은 무엇인가
같은 dataset을 동시에 처리하고 있지는 않은가
취소 요청이 반영됐는가
죽은 worker의 작업은 어떻게 복구됐는가
이 실패는 재시도 가능한가
여기서 중요한 건 FSM이라는 이름이 아니다. 이름보다 중요한 건 상태 전이를 명시적으로 제한하는 것이다.
예를 들어 이런 전이는 허용하지 않아야 한다.
QUEUED -> SUCCEEDED # 실행 기록 없이 성공 처리하면 안 된다
FAILED -> RUNNING # 같은 attempt를 되살리면 실패 이력이 흐려진다
SUCCEEDED -> FAILED # terminal 결과를 뒤집으면 감사가 어려워진다
RUNNING -> QUEUED # side effect가 있었을 수 있어 단순 대기로 돌리기 어렵다
반대로 이런 전이는 상황에 따라 허용할 수 있다.
QUEUED -> LEASED
LEASED -> RUNNING
RUNNING -> SUCCEEDED
RUNNING -> FAILED
RUNNING -> CANCELLED
LEASED -> QUEUED
RUNNING -> STALE
여기서도 예외는 있다. 예를 들어 실패한 작업을 재시도하지 말라는 뜻은 아니다. 보통은 기존 attempt를 FAILED로 남기고 새 attempt나 새 job을 만든다. 또는 RETRYING, REQUEUED 같은 별도 상태를 둔다. 중요한 건 terminal 상태를 조용히 덮어써서 과거 실행을 지우지 않는 것이다.
LEASED -> QUEUED 역시 실행 전이라는 경계가 보장될 때 안전하다. 장시간 실행 작업이라면 heartbeat나 lease renewal 없이 단순히 시간이 지났다는 이유만으로 죽었다고 판단하면 안 된다.
이 전이를 DB update 조건으로 막으면 여러 worker가 동시에 떠 있어도 상태가 덜 꼬인다.
update jobs
set status = 'RUNNING'
where id = ?
and status = 'LEASED'
and lease_token = ?
이런 식의 조건부 전이는 단순해 보이지만, 운영에서는 꽤 강력하다. 상태를 바꿀 자격이 있는 worker만 성공하고, 나머지는 실패한다.
여기까지 남는 기준은 하나다. 상태 전이는 정답표가 아니라, 이 시스템에서 어떤 애매함을 허용하지 않을지 정하는 운영 규칙이다.
그렇다고 이 구조가 공짜는 아니다.
- 상태 이름을 설계해야 한다
- timeout과 lease 시간을 정해야 한다
- heartbeat가 필요한지 판단해야 한다
- retry와 stale 처리 기준을 정해야 한다
- terminal 상태를 다시 바꿀 수 없게 해야 한다
- 운영자가 이해할 수 있는 메시지를 남겨야 한다
그래서 도입 기준은 단순하다.
실패, 중복 실행, 복구를 명시적으로 다뤄야 하면 상태 모델이 필요하다.
반대로 말하면, 실패해도 그냥 다시 실행하면 되고 중복 실행도 크게 위험하지 않다면 굳이 크게 만들 필요가 없다.
이번 고민에서 얻은 결론은 이것이다.
상태 모델은 데이터 엔지니어링에서 필수 알고리즘이라기보다, 오래 걸리고 실패할 수 있는 작업을 운영 가능한 단위로 바꾸는 설계 패턴이다.
처음부터 거창한 FSM 라이브러리를 도입할 필요는 없다. 오히려 작은 DB table과 명시적인 상태 전이부터 시작하는 편이 현실적인 경우가 많다.
job table
worker pool
lease
resource lock
pipeline step
recovery rule
이 정도만 있어도 많은 데이터 작업은 훨씬 설명 가능해진다.
결국 핵심은 하나다.
데이터 작업이 오래 걸리고, 외부 side effect를 남기며, 중복 실행이나 복구 판단이 위험하다면 실행 명령보다 먼저 상태를 설계해야 한다.
이 관점이 생기면 cron과 script를 버릴지 유지할지도 더 차분하게 판단할 수 있다. script는 계속 쓸 수 있다. 다만 전체 흐름을 script가 암묵적으로 끌고 가게 둘지, controller가 상태를 보고 관리하게 할지는 별개의 문제다.
반대로 오래 걸리더라도 입력이 명확하고, 재실행이 idempotent하며, scheduler retry만으로 충분하다면 상태 모델은 과할 수 있다. 길고 실패할 수 있다는 사실만으로는 충분하지 않다. 같은 입력을 다시 읽을 수 있는지, 중복 실행이 위험한지, 사람이 개입하지 않아도 복구 판단을 해야 하는지가 함께 중요하다.
이 차이를 구분하는 순간, 데이터 작업은 단순 실행에서 운영 가능한 시스템으로 넘어가기 시작한다.
그래서 이 시리즈의 결론은 거창한 FSM 도입론이 아니다. 오히려 반대에 가깝다. 상태 모델은 작업을 더 복잡하게 만들기 위한 구조가 아니라, 이미 복잡해진 운영 현실을 더 이상 로그와 감으로만 다루지 않기 위한 최소한의 언어다.
마지막 판단은 결국 현실적인 질문으로 돌아온다.
이 작업은 정말 상태 모델을 둘 만큼 위험한가
이 질문에 아니다라고 답할 수 있다면 단순한 script와 cron을 유지하는 편이 낫다. 하지만 실패 지점, 중복 실행, 동일 입력 재시도, 복구 기준, 취소 의미를 매번 사람이 추측하고 있다면, 그때는 이미 상태 모델이 필요한 신호가 나온 것이다.
그 순간부터 중요한 것은 어떤 라이브러리를 쓸지가 아니다. 먼저 이 작업이 가질 수 있는 상태를 적고, 어떤 전이를 허용하지 않을지 정하고, worker가 죽었을 때 무엇을 다시 실행해도 되는지 구분하는 일이다.
그 정도의 작은 상태 설계만으로도 데이터 작업은 훨씬 설명 가능해진다. 그리고 설명 가능한 작업만이 운영 가능한 작업이 된다.