배경 및 목표

플랜 다운그레이드 처리를 담당하는 워커서비스가 NestJS로 운영되고 있었습니다. 해당 서비스는 트랜잭션 경계가 없이 쿼리가 순차적으로 실행되어, 중간 실패 시 데이터 불일치가 발생했고 메시지도 유실되는 구조였습니다.

graph LR
    subgraph AS_IS["기존: Node.js 워커"]
        K["Kafka"] -->|"fire-and-forget"| C["Consumer"]
        C --> E1["Executor 1"]
        E1 --> E2["Executor 2"]
        E2 --> E3["..."]
        E3 --> E21["Executor 21"]
        E21 -->|"Raw SQL 52건"| DB["DB"]
    end
    subgraph TO_BE["개선: Spring 워커"]
        K2["Kafka"] -->|"Manual ACK"| C2["Consumer"]
        C2 --> D["PlanDowngrader"]
        D --> S1["Service 1"]
        D --> S2["Service 2"]
        D --> S11["Service 11"]
        S1 -->|"QueryDSL"| DB2["DB"]
        S2 -->|"QueryDSL"| DB2
        S11 -->|"QueryDSL"| DB2
    end
    AS_IS -.->|"포팅"| TO_BE
  • Executor들은 트랜잭션 경계 없이 순차 실행 → 3번째에서 실패할 경우 1, 2번은 롤백 불가
  • 처리 실패 시 재처리는 메시지 재발행 → 전체 다운그레이드 로직 재실행
  • 0건의 테스트 및 유지보수 어려움

목표

  • 중간 실패 시 데이터 불일치·메시지 유실이 없는 안정적인 다운그레이드 워커로 전환한다.
  • 실패한 스텝만 재처리 가능하고, 테스트로 보호되는 유지보수 가능한 구조를 확보한다.

해결 방법과 해결 후보군

1. 상속을 이용한 도메인별 서비스 분리 + 실패 격리

각 Executor들을 도메인 단위로 재분류하여 Downgrade 인터페이스를 상속한 서비스 클래스들(트랜잭션 경계)로 분리하였습니다. 서비스 단위로 트랜잭션이 보장되도록 실행하여 하나가 실패해도 나머지는 계속 수행합니다.

classDiagram
		class Downgrader {
        -services: List~DowngradeService~
        +downgrade(id, changedPlan)
        -executeServices()
    }
    note for PlanDowngrader "sortedBy order → forEach try-catch 실패해도 계속 실행, failedSteps 기록"
    class DowngradeService {
        <<interface>>
        +order: Int
        +stepName: Step
        +executeBy(id, changedPlan)
    }
    class ADowngradeService {
        +order = 1
        +executeBy()
    }
    class BDowngradeService {
        +order = 3
        +executeBy()
    }
    class CDowngradeService {
        +order = 5
        +executeBy()
    }
    Downgrader --> DowngradeService : sortedBy order forEach try-catch
    DowngradeService <|.. ADowngradeService
    DowngradeService <|.. BDowngradeService
    DowngradeService <|.. CDowngradeService

  • 새 도메인 추가 혹은 로직 변경 시 변경 범위 제한
  • 실패한 스텝(failSteps)만 재처리하도록 Admin Retry API(DowngradeService 리스트 재활용)

2. AI 협업 개발 파이프라인

이번 신규 서비스 포팅 작업은 클로드를 이용하여 개발 파이프라인을 구상하고, AI와 협업을 하며 실무에 적용해 본 프로젝트입니다.

우선 어떤 파이프라인으로 문제를 해결해나갈지 정의합니다. 각 파이프라인의 스텝이 정하고, 스텝에 맞는 에이전트 역할을 정의하여 결과물을 산출합니다.

graph LR
    P1["① 이관 분석"] --> P2["② 설계 + 문서"]
    P2 --> P3["③ 티켓 + TC 작성"]
    P3 --> P4["④ TDD 구현"]
    P4 --> P5["⑤ PR 리뷰 + 배포"]

    style P1 fill:#e8f4f8
    style P3 fill:#e8f4f8
    style P4 fill:#e8f4f8
PhaseClaude사람
1. 이관 분석21개 Executor SQL/API/데이터 흐름 1:1 매핑, 분석 보고서분석 결과 검증, 비즈니스 맥락 보정
2. 설계21개 Executor →11개 Service 책임 재분류 초안아키텍처 결정, Feature Flag 전략 수립
3. 티켓 분할11개 티켓 생성 (AS-IS↔TO-BE 쿼리 테스트 케이스 매핑, Given/When/Then TC)티켓 순서/우선순위 판단
4. TDDRED: TC → Kotest 변환 / GREEN: 구현 생성 / REFACTOR: 하네스 자동 검증AS-IS 동작 일치 여부 검증
5. 배포전환 체크리스트, 롤백 시나리오Flag 전환 시점, 모니터링 지표 판단
  • 이관 분석을 통해 누락될 수 있는 요소들을 놓치지 않고, 100% 커버
  • 작성된 티켓 기반의 TDD 테스트 커버리지(80%+)
  • 병렬 작업(티켓별 구현)으로 인한 일정 단축 (주어진 기한 20md → 10md)

결과

지표기존 (Node.js)개선 (Spring)
트랜잭션 관리없음 (fire-and-forget)서비스별 독립 트랜잭션 + 실패 격리
실패 복구전체 재실행실패 step만 선택적 재시도 (Admin API)
SQL Injection52건 (Raw SQL 파라미터 직접 삽입)0건 (QueryDSL 파라미터 바인딩)
테스트0건31개 파일, 커버리지 80%+
서비스 구조21개 Executor (중복 포함)11개 Service (책임 분리, 48% 감소)
기술 스택Node.js (NestJS)Kotlin 단일화
일정-20md → 10md
전환 방식-Feature Flag 무중단 전환 (중단 0건)

모니터링

  • 다운그레이드 처리 성공/실패 건수와 failedSteps 기록을 관측하고, 재시도 3회 초과 건은 알림으로 escalation한다.
  • Feature Flag 전환 전후 처리 성공률·에러율을 비교해 무중단 전환을 검증한다.