레거시 리팩토링 - 비교 알람을 활용한 안전한 마이그레이션
배경
금융 기관에 일일 데이터를 전송하는 배치 태스크를 리팩토링했다. 금융 데이터 전송이라 정확성이 생명이고, 한 건이라도 틀리면 규제 이슈로 이어질 수 있는 시스템이다.
기존 시스템의 문제점
flowchart TD
subgraph problems["기존 시스템의 6가지 문제"]
A["1. 단일 함수에 모든 로직 혼재<br>(전처리→직렬화→전송→검증)"]
B["2. 잘못된 클래스 사용<br>(타입 A에 타입 B용 처리기 사용)"]
C["3. 임시 파일 미삭제<br>→ 디스크 용량 부족"]
D["4. 재시도 불가<br>→ 부분 실패 시 전체 재실행"]
E["5. 간헐적 미실행<br>→ 원인 불명"]
F["6. 로깅 부족<br>→ 실패 원인 추적 불가"]
end
특히 문제 4번(재시도 불가)이 운영 부담이 컸다. 새벽에 배치가 부분 실패하면, 이미 성공한 건까지 다시 전송해야 했고, 중복 전송 위험도 있었다.
해결: 4계층 아키텍처
기존 단일 함수를 4개 계층으로 분리했다.
flowchart TD
subgraph layer1["Model Layer"]
A[전송 데이터 영속화 모델]
end
subgraph layer2["Query Layer"]
B[데이터 조회 로직 분리]
end
subgraph layer3["Sender Layer - Strategy 패턴"]
C[BaseSender]
C --> D[TypeA Sender]
C --> E[TypeB Sender]
C --> F[TypeC Sender]
end
subgraph layer4["Comparison Layer"]
G[기존 vs 신규 결과 비교]
G --> H[불일치 시 Slack 알람]
end
layer1 --> layer2 --> layer3 --> layer4
Strategy 패턴 Sender
전송 타입마다 전처리 방식이 다른데, 기존에는 if-else 분기로 처리하고 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# AS-IS: if-else 분기 (확장할 때마다 기존 코드 수정)
def send_daily():
if record_type == 'A':
data = process_type_a(records) # 실은 타입 B 처리기를 쓰고 있었음 (버그)
elif record_type == 'B':
data = process_type_b(records)
elif record_type == 'C':
...
# TO-BE: Strategy 패턴 (새 타입은 새 클래스만 추가)
class TypeASender(BaseSender):
record_info_class = TypeARecordInfo # 올바른 처리기
def preprocess(self, records):
...
def serialize(self, data):
...
새 전송 타입이 추가되면 기존 코드 변경 없이 새 Sender 클래스만 만들면 된다.
멱등성과 부분 실패 복구
flowchart LR
subgraph before["AS-IS: 전체 재실행"]
A1["A 성공"] --> A2["B 성공"] --> A3["C 실패"] --> A4["D 미실행"]
A4 --> A5["→ A,B,C,D 전체 재실행<br>(A,B 중복 전송 위험)"]
end
flowchart LR
subgraph after["TO-BE: 실패분만 재처리"]
B1["A 성공 ✓<br>is_shared=True"] --> B2["B 성공 ✓<br>is_shared=True"]
B2 --> B3["C 실패 ✗"] --> B4["D 미실행"]
B4 --> B5["→ C,D만 재처리<br>(A,B는 스킵)"]
end
성공한 전송에 is_shared=True 플래그를 마킹하여, 재실행 시 실패한 건만 재처리한다. 이 플래그 하나가 새벽 장애 대응의 스트레스를 크게 줄여줬다.
핵심 전략: 비교 알람 시스템
레거시를 한 번에 교체하는 것은 위험하다. 특히 금융 데이터에서 “아마 맞을 것이다”는 통하지 않는다.
기존 시스템과 새 시스템을 병렬로 운영하면서 결과를 비교하는 방식으로 안전하게 마이그레이션했다.
flowchart TD
A[같은 입력 데이터] --> B[기존 시스템]
A --> C[신규 시스템]
B --> D[결과 A]
C --> E[결과 B]
D --> F{Comparison Engine}
E --> F
F -->|일치 ✓| G[로그 기록]
F -->|불일치 ✗| H[Slack 알람 + 상세 diff]
마이그레이션 4단계
flowchart TD
P1["Phase 1: 병렬 운영 시작<br>신규 시스템 결과만 생성, 전송은 기존 시스템<br>비교 알람으로 차이 수집"]
P2["Phase 2: 비교 정밀도 강화<br>레코드 단위 비교, 필드별 diff"]
P3["Phase 3: 불일치 0 달성<br>엣지 케이스 해결<br>충분한 기간 동안 불일치 0 확인"]
P4["Phase 4: 기존 시스템 제거<br>신규 시스템으로 전송 전환<br>레거시 코드 삭제"]
P1 --> P2 --> P3 --> P4
비교 알람이 찾아낸 엣지 케이스
비교 과정에서 예상치 못한 케이스를 발견했다:
1
2
3
4
5
6
7
09:00 대출 등록 (등록 레코드 생성)
15:00 당일 상환 (상환 레코드 생성)
23:00 일일 배치 실행
→ 기존 시스템: 등록/상환을 별도로 처리
→ 신규 시스템: 하나의 트랜잭션으로 처리
→ 결과 불일치 → Slack 알람 → 원인 분석 → 로직 수정
비교 알람이 없었다면 이 엣지 케이스는 프로덕션에서 발견됐을 것이다. 테스트 코드만으로는 실제 데이터의 모든 조합을 커버할 수 없기 때문이다.
느낀 점
비교 알람은 대규모 리팩토링의 보험이다
“새 코드가 기존과 동일한 결과를 내는가?”를 자동으로 검증함으로써 리팩토링에 대한 확신을 가질 수 있었다. 금융 시스템에서는 특히 이 방법이 효과적이다.
Strategy 패턴은 “같은 일을 다른 방식으로”에 잘 맞는다
전송 타입별로 전처리가 다른 경우, if-else 분기보다 구현체 분리가 유지보수에 유리하다. 기존 코드에서 타입 A에 타입 B 처리기를 쓰는 버그도 이 리팩토링으로 발견됐다.
멱등성은 배치 시스템의 핵심이다
부분 실패 시 “그냥 재실행하면 된다”라고 말할 수 있게 되면, 새벽 장애 대응이 완전히 달라진다.
점진적 마이그레이션 > 빅뱅 전환
빅뱅 전환의 유혹은 강하다 — “어차피 두 시스템 운영하는 게 더 복잡하잖아.” 하지만 정확성이 중요한 시스템에서 빅뱅은 도박이다. 병렬 운영 기간을 충분히 갖는 것이 결국 더 빠른 길이었다.