세 개의 머리를 가진 Status Field (그리고 우리가 해결한 방법)

발행: (2026년 1월 15일 오전 11:16 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

Source:

원죄

제가 처음 import/export 설정을 만들었을 때, 상태는 명확했습니다:

type ImportConfig struct {
    Status string `json:"status"` // "draft" | "active" | "paused"
}

깨끗하고. 간단하고. 끝났습니다.

그런 다음 사용자가 import를 실행하기 시작했습니다. 마지막 실행이 성공했는지 실패했는지 알아야 했습니다:

type ImportConfig struct {
    Status        string `json:"status"`        // "draft" | "active" | "paused"
    LastRunStatus string `json:"lastRunStatus"` // "success" | "failed" | "running"
}

여전히 관리 가능했습니다. 두 개의 필드, 두 개의 고민.

그 후 마법사를 만들었습니다. 사용자는 흐름 중간에 진행 상황을 저장해야 했지만, 깨진 설정을 만들고 싶지는 않았습니다. 간단한 해결책:

type ImportConfig struct {
    Status        string `json:"status"`
    LastRunStatus string `json:"lastRunStatus"`
    IsDraft       bool   `json:"isDraft"` // wizard in progress
}

세 개의 머리. 하나의 괴물.

Source:

조합론 악몽

조합론 악몽

세 개의 필드가 만들고 있는 문제: 상태 매트릭스를 생성한다.

Status:        draft | active | paused | disabled | completed
LastRunStatus: success | failed | running | (empty)
IsDraft:       true | false

이는 5 × 4 × 2 = 40개의 가능한 상태를 의미한다. 대부분은 의미가 없었다. status=active, lastRunStatus=running, isDraft=true는 무슨 뜻일까?

프론트엔드에 있던 이 보석:

function getDisplayStatus(config: ImportConfig): string {
  if (config.isDraft) return 'draft';
  if (config.lastRunStatus === 'running') return 'running';
  if (config.status === 'paused') return 'paused';
  if (config.lastRunStatus === 'failed') return 'failed';
  if (config.status === 'active') return 'ready';
  return 'draft'; // ¯\_(ツ)_/¯
}

그 함수는 잘못되었다. 어느 경우가 잘못됐는지 나는 몰랐다.

인식의 순간

새로운 기능을 추가하고 있었는데, 기본적인 질문에 답할 수 없다는 것을 깨달았다:

“이 가져오기가 실행 준비가 되었나요?”

답을 찾으려면 세 개의 필드를 확인하고, 그 우선순위를 이해하며, 논리가 맞는지 확인해야 했다.

그때 나는 상태를 모델링하지 못했다는 것을 알았다. 나는 증상만 쌓아두고 있었다.

해결책: 모든 것을 통제하는 하나의 필드

리팩터는 개념적으로는 간단했습니다:

Before: 겹치는 관심사를 추적하는 3개의 필드
After: 5개의 상호 배타적 상태를 갖는 1개의 필드

const (
    StatusDraft   = "draft"   // Wizard incomplete, missing required fields
    StatusReady   = "ready"   // Complete config, can be run
    StatusRunning = "running" // Currently executing
    StatusPaused  = "paused"  // User paused execution
    StatusFailed  = "failed"  // Last run failed, needs attention
)

type ImportConfig struct {
    Status string `json:"status"` // One of the above. That's it.
}

매트릭스도, 우선순위도, 추측도 없습니다.

“이 가져오기가 실행 준비가 되었나요?”config.Status == StatusReady

But Wait, What About the Wizard?

isDraft 필드는 마법사가 진행 중인 부분을 저장해야 하기 때문에 존재했습니다. 이를 제거한다는 것은 다른 문제를 해결한다는 의미였습니다: 마법사 상태는 어디에 저장되는가?

Answer: Status == StatusDraft 일 때만 존재하는 DraftData 필드에 있습니다:

type ImportConfig struct {
    Status    string     `json:"status"`
    DraftData *DraftData `json:"draftData,omitempty"` // nil unless Status=draft
}

type DraftData struct {
    CurrentStep     string          `json:"currentStep"`
    PendingSchema   *PendingSchema  `json:"pendingSchema,omitempty"`
    PendingDataType *PendingDataType `json:"pendingDataType,omitempty"`
}

핵심 인사이트: 대기 중인 리소스는 실제 테이블이 아니라 초안 데이터에 존재합니다. 마법사 진행 중에 스키마를 만들고 있다면, 그 스키마는 아직 존재하지 않습니다. 이는 DraftData에 저장된 대기 중인 스키마입니다. 최종 확정할 때 비로소 실제 스키마가 됩니다. 이제 포기된 마법사 때문에 남는 고아 스키마가 없습니다.

피해 보고서

수정으로 영향을 받은:

  • 193개의 파일
  • 11,786개의 추가
  • 4,179개의 삭제
  • 백엔드 모델, 서비스, 핸들러
  • 프론트엔드 타입, 훅, 마법사
  • 양쪽 모두에 대한 테스트

그럴 가치가 있었을까? 이제 getDisplayStatus 함수는 다음과 같다:

function getDisplayStatus(config: ImportConfig): string {
  return config.status;
}

네. 그럴 가치가 있었다.

Lessons

  1. State fields multiply. One becomes two becomes three. Each addition feels small. The complexity is combinatorial.
    상태 필드가 늘어난다. 하나가 두 개가 되고, 세 개가 된다. 각 추가는 작게 느껴진다. 복잡성은 조합적이다.

  2. “What state is this?” should be trivial to answer. If you need a flowchart, you have a modeling problem.
    ”이 상태가 뭐지?” 라는 질문은 쉽게 답할 수 있어야 한다. 흐름도를 만들어야 한다면 모델링 문제가 있다.

  3. Pending resources aren’t real resources. Wizard state is draft data, not partially‑created entities.
    대기 중인 리소스는 실제 리소스가 아니다. 마법사 상태는 초안 데이터이며, 부분적으로 생성된 엔터티가 아니다.

  4. Big refactors are just many small changes. 193 files sounds scary. It was really “update Status constant” × 193.
    대규모 리팩터링은 많은 작은 변경들의 집합일 뿐이다. 193개의 파일은 무섭게 들리지만, 실제로는 “Status 상수 업데이트” × 193이었다.

Building Flywheel – data pipelines for startups. If you’ve ever watched a “simple” field grow three heads, I’d love to hear your war stories.
Flywheel을 구축하고 있습니다 – 스타트업을 위한 데이터 파이프라인. 만약 당신이 “간단한” 필드가 세 갈래로 성장하는 모습을 본 적이 있다면, 전쟁 이야기를 듣고 싶습니다.

Back to Blog

관련 글

더 보기 »

Go에서 우아한 도메인 주도 설계 객체

❓ Go에서 도메인 객체를 어떻게 정의하시나요? Go는 전형적인 객체‑지향 언어가 아닙니다. Domain‑Driven Design(DDD) 같은 개념을 구현하려고 할 때, 예를 들어 En…

사랑과 증오의 편지 do Json

JSON을 구성 형식으로 사용하는 것은 실수다. 예, 실수다. 선호도가 아니다. 스타일 선택도 아니다. 구조적인 실수다. 그 진술은 …

Go의 비밀스러운 삶: 패키지와 구조

Chapter 11: The Architect's Blueprint 금요일 오후의 햇살이 아카이브 창문을 통해 낮게 비스듬히 들어와, 공중에서 춤추는 먼지 입자를 비추었다. 이든은…