유민우 · Tech Notes


드래프트 표준: DraftIR ↔ DraftVariant 불변성 아키텍처

🗓️ 9/20/2025 👤 Role: Backend Developer
FastAPIContextPropagationImmutableDesign

Problem & Condition

Definition of Problem Context

마케팅 팀이 캠페인을 진행하면서 게시하는 여러 콘텐츠는 같은 시간대에 같은 토픽을 공유한다.
하지만 플랫폼 정책 제약, 각 플랫폼의 톤앤매너, 표현 방식의 차이 때문에 완전히 동일한 콘텐츠를 그대로 쓸 수는 없다.

그럼에도 불구하고 이들 콘텐츠는 분명히 같은 맥락을 공유한다. 따라서 단일 진실 공급원(Single Source of Truth, SoT)을 정의하고, 이를 표준화하는 구조가 필요하다.

만약 이러한 경계가 없다면,

  • 사용자 입력(Draft)이 시스템 내부 처리 과정에서 변조되거나,
  • 플랫폼별 변환 과정이 추적 불가능해져,
  • 누가 무엇을 수정했는지 책임소재가 불분명해진다.

이는 운영 안정성과 회귀(traceability)를 크게 해치게 된다.

Definition of Solution DTOs and Relationships

이를 해결하기 위해, PostgreSQL 스키마 설계와 RESTful API, AsyncIO, Celery 워커 등 모든 경로에서 In-Out Schema 불변성을 유지해야 한다.

따라서 필요한 요건은 다음과 같다:

  1. Celery와 AsyncIO 간 Context 전파
    (X-Request-ID, X-Idempotency-Key, X-Trace-Parent 등)
  2. IR(DTO) 스키마 정의
    사용자 입력과 시스템 산출물을 구분
  3. Injector/Policy 정의
    다양한 컨텍스트(페르소나, 트렌드, 플레이북 등)를 안전하게 주입

Solution

DTOs and Responsibilities

  • DraftIR

    • 범 플랫폼적 DTO
    • 오직 사용자 Action에 의해 Write되며, 내부 시스템은 수정 불가
  • DraftVariant

    • Adapter에 의해 생성되는 결과물
    • 플랫폼별 Rendered Blocks와 불변 필드를 포함
    • 사용자는 직접 수정할 수 없음

즉,

  • 사용자 책임: DraftIR 작성 및 CRUD
  • 시스템 책임: DraftVariant 컴파일 및 영속화

이렇게 경계를 분리하면,

  • User Action은 DraftIR CRUD 기록에서 항상 확인 가능
  • 최종 렌더링 결과는 DraftVariant에서 추적 가능

→ 결과적으로 책임소재와 불변성이 명확히 보장된다.

Responsibilities of Injectors and Adapters

  • Adapter

    • 추상 클래스
    • 각 플랫폼별 어댑터는 고유한 규칙에 따라 DraftVariant를 생성
    • 컴파일러로서 최소한의 변환 책임만 수행
    • Service Layer에서 결과는 영속화
  • Injector

    • InjectorContext 내에서 연쇄적으로 적용
    • finalize() 시점에 InjectedResult 스키마 생성
    • Adapter로 전달할 컨텍스트를 준비하되, 최종 결과에는 직접 관여하지 않음

시각적으로 표현하면:

InjectorContext(Injector1 -> Injector2 -> ...) -> Adapter -> DraftVariant(RenderedBlocks)

Context Propagation 경로는 다음과 같다:

Service Layer -> Capture Context -> Celery Worker -> Apply Context -> Adapter -> Persist

Effects

  1. Context Propagation

    • Injector와 Adapter는 모두 ContextVars 기반 캡처/복원 방식을 사용
    • X-Request-ID, X-Trace-Parent, X-Idempotency-Key 등이 end-to-end로 보존
  2. 독립적 주입 (Isolation)

    • 각 Injector는 독립적으로 작동 → 부작용 최소화
    • Context 주입 충돌을 예방
  3. 책임소재 분리 (Accountability)

    • DraftIR = 사용자 입력의 SoT
    • DraftVariant = 시스템 산출물의 SoT
    • 두 레이어 사이 불변성 보장
  4. 안정성 & 운영성

    • Adapter.compile은 항상 Celery Worker에서 실행
    • 멱등키 기반 재시도/회로 차단기 적용 가능
    • 추후 장애 추적과 롤백 용이

Conclusion

DraftIR ↔ DraftVariant의 경계를 명확히 하면,

  • 사용자 입력은 시스템이 손대지 않는다
  • 시스템 결과물은 사용자 손을 타지 않는다

이 단순한 원칙 덕분에 콘텐츠 파이프라인 전반이 추적 가능하고 안정적이며, 운영 비용이 낮은 구조로 유지된다.