레거시 3개 시스템을 하나로 옮기며 배운 것 — 스키마 불일치, 이중 저장소, 타임존, 그리고 “새 연결을 먼저”(2)
지난 글에 이어서..
---
6. Append-only 이벤트: 삭제도 사실을 추가한다
### 이론
이벤트 소싱의 핵심은 “현재 상태는 이벤트의 투영”이다. 물리 DELETE 대신 사실을 추가한다.
### 적용
하나의 업무 건(`reg_no` 같은 논리 ID)에 등록 → 수정 → 삭제 이벤트가 쌓이고, **최신 이벤트가 현재 상태**다. 삭제도 삭제 이벤트다.
이력 화면은 모든 이벤트, 운영 화면은 삭제되지 않은 최신 건만 본다.
채번 경쟁은 UNIQUE만으로 막기 어려워 **advisory lock**으로 직렬화했다.
같은 행 안에서도 “사용자에게 보이는 식별자의 날짜 규약”과 “감사 시각의 저장 규약”이 다를 수 있음을 문서화했다.
**교훈:** append-only는 감사에 강하지만, 현재 상태 조회는 항상 **최신 이벤트 투영 규칙**을 명확히 해야 한다.
---
7. 분산 정합성: DB 트랜잭션으로 HTTP를 감싸는 타협
### 이론
외부 HTTP는 로컬 DB 트랜잭션에 들어가지 않는다. 이상은 사가·아웃박스·보상 트랜잭션이다.
실무에서는 먼저 **어느 불일치를 더 못 참는지**를 고른다.
### 적용
쓰기 경로에서:
1. DB 트랜잭션 시작
2. 이력 이벤트 INSERT
3. 같은 흐름에서 외부 API 호출
4. 외부 실패 시 throw → 이력 INSERT 롤백
외부 왕복을 위해 트랜잭션 timeout을 늘렸다.
한계도 남겼다.
- DB 커밋이 나중에 실패하면 외부는 이미 반영될 수 있다
- 외부가 여러 번 나눠 쓰면 중간 실패 시 앞선 호출은 남는다
완벽한 2PC는 아니다. 다만 “외부는 실패했는데 이력만 남는” 쪽 — 운영자가 더 자주 보는 불일치 — 를 우선 막았다.
**교훈:** 분산 트랜잭션을 완벽히 풀기 전에, **참을 수 있는 불일치의 방향**을 정해라.
---
8. 타임존: 변환은 경계에서만
### 이론
타임존 버그의 고전은 한 시스템에 여러 의미의 datetime이 섞이는 것이다.
규약은 저장 포맷이 아니라 **경계 계약**으로 정의해야 한다.
### 적용
경계를 나눴다.
| 경계 | 규약 |
| --- | --- |
| 화면 ↔ BFF | 사용자 지역 벽시계 문자열 |
| BFF 내부 | UTC instant |
| BFF ↔ 외부 API / DB(naive timestamp) | UTC 벽시계(오프셋 없음) |
외부 서비스는 “naive = UTC”를 불변식으로 강제하고, 오프셋이 붙은 입력을 거절했다.
naive에 지역 시각을 넣으면 **조용히 N시간 밀려** 저장되고, 같은 테이블을 UTC로 읽는 다른 서비스가 깨진다.
우리 앱 안에서는 쓰기·읽기가 같이 밀려 “맞아 보이는” 함정이 있었다.
변환 함수는 경계에만 두고, 예전에 지역 시각을 넣던 경로를 UTC 계약으로 고쳤다.
**교훈:** 기능 테스트가 통과해도 **다른 소비자**가 있으면 틀린다. 교차 서비스 검증이 필수다.
---
9. 소유권 경계: 공용 테이블에 PoC를 얹지 않는다
### 이론
공유 커널은 비용이 크다. 제품이 팀 공용 마스터에 직접 쓰면 스키마·데이터 lifecycle이 얽힌다.
### 적용
사용자·자격증명을 팀 공용 유저 테이블에 얹었다가, **앱 소유 스키마의 단일 테이블**로 옮겼다.
업무 이력·앱 전용 코드도 앱 소유 스키마에 두었다. DB 계정에 DDL이 없어, 앱 소유 테이블 생성은 레포 밖·앱은 DML만 하는 제약도 문서화했다.
**교훈:** “이미 있는 공용 테이블에 조금 얹자”는 단기 이득이 크고, 장기 결합 비용이 더 크다.
---
10. 검증 문화: 초록불이 거짓말을 할 때
통합 테스트가 DB URL 없이 “DB 없음 → 통과”로 처리되던 함정도 만났다.
리포트는 초록인데 실제 신규 DB 경로는 한 번도 안 탄다.
이론적으로는 테스트 신뢰성 문제이고, 실무적으로는 **스킵을 성공으로 세지 말 것**, **통합 테스트는 환경 주입을 명시할 것**이다.
---
한 줄로 정리하면
> 새 연결을 먼저 증명하고, 스키마는 번역하며, 질문이 다르면 저장소를 나누고, 시간은 경계에서만 바꾸고, 불일치는 우선순위를 정해 줄인다.
레거시 포팅은 “같은 화면을 새 스택으로 옮기는 일”이 아니다.
같은 업무를 새 진실 구조 위에 다시 올려놓는 일에 가깝다.
그 차이를 인정하는 순간부터, 마이그레이션은 이관 프로젝트가 아니라 설계 프로젝트가 된다.
---