Python + Web

레거시 3개 시스템을 하나로 옮기며 배운 것 — 스키마 불일치, 이중 저장소, 타임존, 그리고 “새 연결을 먼저”(1)

내인생왜이래 2026. 7. 21. 13:43
반응형

에너지·입찰 도메인의 레거시 웹·인증·WAS를 Next.js 모노레포 하나로 옮기는 작업을 했다.  
겉으로는 “프론트/백엔드를 Next로 포팅한다”처럼 보이지만, 실무의 핵심은 UI 이식이 아니라 데이터 모델·경계·진실의 원천을 다시 설계하는 일이었다.

아래는 그때 붙잡고 있던 이론과, 그걸 어떻게 코드·운영에 심었는지다.

---
1. 스트랭글러 피그(Strangler Fig): 한 번에 끊지 않는다

### 이론
마틴 파울러의 **스트랭글러 피그**는 레거시를 일괄 교체하지 않고, 새 시스템이 기능을 하나씩 감싼 뒤 오래된 경로를 점진적으로 제거하는 방식이다.

### 적용
작업 순서를 이렇게 고정했다.

1. 신규 DB·신규 서비스로의 **연결을 먼저** 만든다  
2. end-to-end로 **검증**한다  
3. 그다음에야 레거시 URI·플레이스홀더·중복 환경변수를 **제거**한다  

첫 화면부터 레거시 WAS URL을 끊지 않았다. 같은 오리진의 **Self API**를 먼저 붙이고, 그 뒤에서 새 저장소·외부 서비스를 호출하게 한 뒤, 옛 배선 제거는 맨 마지막에 했다.

**교훈:** 마이그레이션에서 가장 위험한 순간은, 새 경로가 아직 증명되지 않았는데 옛 배선을 지우는 때다.

---

2. 안티커럽션 레이어(ACL): 스키마가 다르면 번역이 제품이다

### 이론
DDD의 **Anti-Corruption Layer**는 외부(레거시) 모델을 우리 도메인으로 오염시키지 않도록, 경계에 번역층을 두는 패턴이다.

### 적용
레거시는 MySQL의 `tbl_*` 중심이었고, 신규는 PostgreSQL 계열의 다중 스키마·다른 엔티티 구조였다. 컬럼명이 비슷해 보여도 **조인 키가 달랐고**, 어떤 테이블은 A 식별자, 어떤 테이블은 B 식별자를 썼다.

그래서 화면마다 감으로 조인하지 않고, **테이블·컬럼 단위 매핑표**를 먼저 두고 조회했다. 대응이 없으면 “없음”으로 명시했다. 그 문서가 곧 ACL의.

**교훈:** 스키마 이전에서 버그의 상당수는 비즈니스 로직이 아니라 **잘못된 키로 조인한 결과**다. 코드보다 매핑표가 먼저다.

---

3. BFF + Self API: 브라우저가 모르는 시스템을 모른다

### 이론
**BFF(Backend for Frontend)**는 클라이언트가 여러 백엔드를 직접 치지 않게 하고, 화면용 API면으로 오케스트레이션한다.

### 적용
프론트는 **같은 오리진의 `/api/v1/**`만** 호출한다.  
기존 인증·업무 서버는 Next.js Route Handlers + ORM으로 재구현했다.  
외부 관리형 서비스 호출, 서비스 간 토큰 발급, 스키마 번역은 전부 서버에 두었다.

브라우저는 다운스트림 IAM을 직접 다루지 않는다. 서비스 간은 OIDC ID 토큰, 사용자 세션은 (전환기에는) 앱 JWT — 이후 엣지 IAM으로 옮길 전제.

**교훈:** 프론트가 레거시 WAS를 직접 치는 구조를 유지하면, 스택을 바꿔도 경계는 영원히 새지 않는다.

---

4. 도메인 모델 변경: 같은 화면이라도 의미가 바뀐다

### 이론
마이그레이션은 테이블 복사가 아니라 **유비쿼터스 언어의 재정의**에 가깝다. 표현 단위가 바뀌면 계산식·검증·선택 UI·권한까지 바뀐다.

### 적용
레거시에서 “A − B”로 쓰이던 값이, 신규에서는 “용량 × 비율”처럼 **다른 단위·다른 API**로 바뀌었다. 등록과 수정이 같은 엔드포인트로 합쳐지고, “해제/복구”는 별도 의미의 연산이 되었다.

권한·드롭다운 스코프를 옆 도메인용 조인 테이블에 묶었다가, **실제 업무 단위의 뷰**로 고친 적도 있다. 기능은 살아 있는데 “이 자원은 접근 불가”로 보이는 전형적인 증상이다.

**교훈:** UI를 픽셀 단위로 포팅해도, 도메인 단위가 바뀌면 **권한·선택지·검증**부터 다시 짜야 한다.

---

5. Source of Truth 분리: 상태 이력 ≠ 업무 등록 이력

### 이론
하나의 저장소가 서로 다른 두 질문에 동시에 답하려 하면 망가진다.

- “시스템 상태가 언제 어떤 값이었나?”  
- “누가 언제 무엇을 등록·수정·삭제했나?”

### 적용
외부 서비스가 쌓는 **상태 이력**은 겹침 처리 때문에 구간이 쪼개져, 사용자가 등록한 “한 건”과 모양이 달라졌다. 그 조각만으로는 수정·삭제 대상을 안정적으로 집기 어려웠다.

그래서 저장소를 나눴다.

| 저장소 | 역할 |
| --- | --- |
| 앱 소유 append-only 테이블 | 등록 원본(메타데이터·기간·행위자) |
| 외부 서비스 → 상태 이력 | 실제 운영 값 |

목록·이력 화면은 **등록 원본**을, 세밀한 시계열 뷰만 상태 API를 보게 했다.  
거창한 CQRS 도입이 아니라, **질의 종류가 다르면 저장 모델도 나눈다**는 원칙의 현실형이다.

**교훈:** “이미 상태 이력이 있으니 화면 소스로 쓰자”는 유혹이 강하다. 화면이 ‘등록건’을 다루면 SoT는 상태 이력이 아니라 **등록 이벤트**여야 한다.


반응형