배경 및 목표
플랜 다운그레이드 처리를 담당하는 워커서비스가 NestJS로 운영되고 있었습니다. 해당 서비스는 트랜잭션 경계가 없이 쿼리가 순차적으로 실행되어, 중간 실패 시 데이터 불일치가 발생했습니다.
- Executor들은 트랜잭션 경계 없이 순차 실행 → 3번째에서 실패할 경우 1, 2번은 롤백 불가
- 처리 실패 시 재처리는 메시지 재발행 → 전체 다운그레이드 로직 재실행
- 0건의 테스트 및 유지보수 어려움
왜 중요했는가
트랜잭션 경계가 없어 세 번째 스텝에서 실패하면 앞선 두 스텝은 롤백되지 않았습니다. 테스트가 0건이라 손을 대는 것 자체가 위험한 상태였습니다. 실제로 외부 연동 API 호출이 실패했을 때 트랜잭션이 없어 앞선 스텝의 변경이 그대로 남는 불일치가 발생했습니다.
제약
| 제약 | 선택지에 미친 영향 |
|---|---|
| 운영 중인 서비스 전환 | 다운그레이드는 상시 동작해야 하므로 중단 후 교체가 불가능합니다. feature toggle 무중단 전환을 전제로 설계했습니다 |
| 테스트 0건인 레거시 | 기존 동작을 검증할 기준이 없어, 포팅 전에 기대 동작을 테스트로 먼저 고정해야 했습니다 |
| Kotlin, Spring 스펙으로 통일 | 팀에서 이미 합의된 방향이라, 어떤 언어로 옮길지는 선택지가 아니었습니다 |
목표
- 부분 실패를 허용하고, 트랜잭션이 도메인 단위로 격리된 안정적인 다운그레이드 워커로 전환합니다.
- Admin API로 실패한 스텝만 재처리 가능하고, 테스트로 보호되는 유지보수 가능한 구조를 확보합니다.
맡은 역할과 판단
| 구분 | 내용 |
|---|---|
| 담당 범위 | 포팅 설계와 구현, AI 협업 파이프라인 구상까지 단독으로 담당했습니다 |
| 직접 내린 판단 | 1. 21개 Executor를 상속 기반 11개 Service로 재구성해 실패를 도메인별로 격리 2. 재처리를 전체 재실행이 아닌 실패 step 선택 재시도로 변경 3. feature toggle 무중단 전환 |
| 판단하지 않은 것 | Kotlin, Spring 스펙으로 통일하는 방향은 팀 차원에서 합의된 결정이었습니다 |
| AI 협업 | 파이프라인 스텝을 먼저 정의하고 스텝별 에이전트 역할을 지정해 산출물을 만드는 방식으로 진행했습니다 |
해결 방법과 해결 후보군
후보군 비교
| 방식 | 원리 | 한계 |
|---|---|---|
| 기존 구조 유지 + 멱등 재처리 | 순차 실행을 그대로 두고 재처리만 멱등하게 보강 | 재처리가 전체 다운그레이드 로직을 재실행 |
| 도메인별 서비스 분리 + 서비스 단위 트랜잭션 (채택) | Executor를 도메인 단위 Service로 재분류하고 서비스마다 트랜잭션 경계를 부여, 실패한 스텝(failSteps)만 선택 재처리 | 도메인 재분류, 인터페이스 설계 비용이 들지만, 실패 격리와 선택적 재시도, 테스트 보호를 확보 |
1. 상속을 이용한 도메인별 서비스 분리 + 실패 격리
각 Executor들을 도메인 단위로 재분류하여 Downgrade 인터페이스를 상속한 서비스 클래스들(트랜잭션 경계)로 분리하였습니다. 서비스 단위로 트랜잭션이 보장되도록 실행하여 하나가 실패해도 나머지는 계속 수행합니다. 전체 원자성을 포기하고 부분 성공을 허용한 선택이며, 실패한 스텝은 Admin API로 재처리해 데이터 불일치를 제거합니다.
- 새 도메인 추가 혹은 로직 변경 시 변경 범위 제한
- 실패한 스텝(failSteps)만 재처리하도록 Admin Retry API(DowngradeService 리스트 재활용)
2. AI 협업 개발 파이프라인
이번 신규 서비스 포팅 작업은 클로드를 이용하여 개발 파이프라인을 구상하고, AI와 협업을 하며 실무에 적용해 본 프로젝트입니다.
우선 어떤 파이프라인으로 문제를 해결해나갈지 정의합니다. 각 파이프라인의 스텝이 정하고, 스텝에 맞는 에이전트 역할을 정의하여 결과물을 산출합니다.
| Phase | Claude | 사람 |
|---|---|---|
| 1. 이관 분석 | 21개 Executor SQL/API/데이터 흐름 1:1 매핑, 분석 보고서 | 분석 결과 검증, 비즈니스 맥락 보정 |
| 2. 설계 | 21개 Executor →11개 Service 책임 재분류 초안 | 아키텍처 결정, feature toggle 전략 수립 |
| 3. 티켓 분할 | 11개 티켓 생성 (AS-IS↔TO-BE 쿼리 테스트 케이스 매핑, Given/When/Then TC) | 티켓 순서/우선순위 판단 |
| 4. TDD | RED: TC → Kotest 변환 / GREEN: 구현 생성 / REFACTOR: 하네스 자동 검증 | AS-IS 동작 일치 여부 검증 |
| 5. 배포 | 전환 체크리스트, 롤백 시나리오 | Flag 전환 시점, 모니터링 지표 판단 |
- 이관 분석을 통해 누락될 수 있는 요소들을 놓치지 않고, 100% 커버
- 작성된 티켓 기반의 TDD 테스트 커버리지(80%+)
- dag로 티켓 병렬 작업(티켓별 구현)을 구성하고 일정 단축 (주어진 기한 20md → 10md)
결과
| 지표 | 기존 (Node.js) | 개선 (Spring) |
|---|---|---|
| 트랜잭션 관리 | 없음 (fire-and-forget) | 서비스별 독립 트랜잭션 + 실패 격리 |
| 실패 복구 | 전체 재실행 | 실패 step만 선택적 재시도 (Admin API) |
| SQL Injection | 52건 (Raw SQL 파라미터 직접 삽입) | 0건 (QueryDSL 파라미터 바인딩) |
| 테스트 커버리지 | 0% | 80% 이상 (31개 파일) |
| 서비스 구조 | 21개 Executor (중복 포함) | 11개 Service (책임 분리, 48% 감소) |
| 기술 스택 | Node.js (NestJS) | Kotlin 단일화 |
| 일정 | - | 20md → 10md |
| 전환 방식 | - | feature toggle 무중단 전환 (중단 0건) |