--- name: deprecation-and-migration description: 지원 종료와 마이그레이션을 관리합니다. 기존 시스템, API, 기능을 제거할 때 사용하세요. 사용자를 한 구현에서 다른 구현으로 옮길 때 사용하세요. 프로덕션에서 데이터베이스 스키마를 마이그레이션할 때, 예를 들어 다운타임 없이 컬럼 이름을 바꾸거나 삭제할 때(expand/contract) 사용하세요. 기존 코드를 유지할지 종료할지 결정할 때 사용하세요. --- # Deprecation and Migration ## 개요 코드는 자산이 아니라 부채입니다. 모든 코드 줄에는 지속적인 유지보수 비용이 있습니다. 고칠 버그, 업데이트할 의존성, 적용할 보안 패치, 온보딩할 신규 엔지니어가 있습니다. 지원 종료는 더는 제 값을 하지 않는 코드를 제거하는 규율이고, 마이그레이션은 사용자를 기존 것에서 새것으로 안전하게 옮기는 과정입니다. 대부분의 엔지니어링 조직은 만드는 데는 능합니다. 제거하는 데는 능한 곳이 드뭅니다. 이 스킬은 그 간극을 다룹니다. ## 언제 사용하나 - 기존 시스템, API, 라이브러리를 새것으로 교체할 때 - 더는 필요 없는 기능을 종료할 때 - 중복된 구현을 하나로 통합할 때 - 소유자는 없지만 모두가 의존하는 죽은 코드를 제거할 때 - 새 시스템의 수명 주기를 계획할 때 (지원 종료 계획은 설계 시점에 시작합니다) - 레거시 시스템을 유지할지, 마이그레이션에 투자할지 결정할 때 ## 핵심 원칙 ### 코드는 부채입니다 모든 코드 줄에는 지속적인 비용이 있습니다. 테스트, 문서, 보안 패치, 의존성 업데이트, 그리고 근처에서 일하는 사람의 인지 부담이 필요합니다. 코드의 가치는 코드 자체가 아니라 그것이 제공하는 기능입니다. 같은 기능을 더 적은 코드, 더 낮은 복잡도, 더 나은 추상화로 제공할 수 있다면 기존 코드는 사라져야 합니다. ### Hyrum의 법칙이 제거를 어렵게 합니다 사용자가 충분히 많으면 관찰 가능한 모든 동작이 의존 대상이 됩니다. 버그, 타이밍의 특이성, 문서화되지 않은 부수 효과까지 포함합니다. 그래서 지원 종료에는 공지만으로는 부족하고 적극적인 마이그레이션이 필요합니다. 대체 구현이 재현하지 않는 동작에 의존하고 있다면, 사용자는 그냥 바꾸라는 말만으로는 옮길 수 없습니다. ### 지원 종료 계획은 설계 시점에 시작합니다 새로 만들 때는 이렇게 물어보세요. "3년 뒤에 이것을 어떻게 제거할까요?" 깔끔한 인터페이스, 피처 플래그, 최소한의 표면을 갖춘 시스템은 구현 세부 사항이 곳곳으로 새는 시스템보다 지원 종료하기 쉽습니다. ## 지원 종료 결정 무엇을 지원 종료하기 전에 다음 질문에 답하세요. ``` 1. 이 시스템이 아직 고유한 가치를 제공하나요? → 그렇다면 유지하세요. 아니라면 진행하세요. 2. 얼마나 많은 사용자/소비자가 의존하나요? → 마이그레이션 범위를 수량화하세요. 3. 대체 구현이 있나요? → 없다면 먼저 대체 구현을 만드세요. 대안 없이 지원 종료하지 마세요. 4. 각 소비자의 마이그레이션 비용은 얼마인가요? → 자동화하기 사소하면 바로 하세요. 수동이고 노력이 크면 유지보수 비용과 견주세요. 5. 지원 종료하지 않을 때의 지속적인 유지보수 비용은 얼마인가요? → 보안 위험, 엔지니어 시간, 복잡도가 가져오는 기회비용. ``` ## 권고 지원 종료와 강제 지원 종료 | 유형 | 언제 사용하나 | 방식 | |------|-------------|------| | **권고 (Advisory)** | 마이그레이션이 선택 사항이고 기존 시스템이 안정적일 때 | 경고, 문서, 넛지. 사용자는 자신의 일정에 맞춰 마이그레이션합니다. | | **강제 (Compulsory)** | 기존 시스템에 보안 문제가 있거나, 진행을 막거나, 유지보수 비용이 지속 불가능할 때 | 확정 기한. 기존 시스템은 날짜 X에 제거됩니다. 마이그레이션 도구를 제공하세요. | **기본은 권고입니다.** 유지보수 비용이나 위험이 강제 마이그레이션을 정당화할 때만 강제를 쓰세요. 강제 지원 종료는 마이그레이션 도구, 문서, 지원을 함께 제공해야 합니다. 기한만 공지하고 끝낼 수는 없습니다. ## 마이그레이션 절차 ### 1단계: 대체 구현을 만드세요 동작하는 대안 없이 지원 종료하지 마세요. 대체 구현은 다음을 만족해야 합니다. - 기존 시스템의 모든 핵심 사용 사례를 다룹니다 - 문서와 마이그레이션 가이드가 있습니다 - 프로덕션에서 검증되었습니다 ("이론상 더 낫다"만으로는 부족합니다) ### 2단계: 공지하고 문서화하세요 ```markdown ## 지원 종료 공지: OldService **상태:** 2025-03-01부터 지원 종료 **대체:** NewService (아래 마이그레이션 가이드 참조) **제거 날짜:** 권고. 아직 확정 기한 없음 **이유:** OldService는 수동 확장이 필요하고 옵저버빌리티가 없습니다. NewService는 둘 다 자동으로 처리합니다. ### 마이그레이션 가이드 1. `import { client } from 'old-service'`를 `import { client } from 'new-service'`로 바꾸세요 2. 설정을 업데이트하세요 (아래 예시 참조) 3. 마이그레이션 검증 스크립트를 실행하세요: `npx migrate-check` ``` ### 3단계: 점진적으로 마이그레이션하세요 소비자를 한 번에 모두 옮기지 말고, 한 명씩 옮기세요. 각 소비자에 대해 다음을 하세요. ``` 1. 지원 종료 시스템과 맞닿는 지점을 모두 찾습니다 2. 대체 구현을 쓰도록 업데이트합니다 3. 동작이 일치하는지 검증합니다 (테스트, 통합 확인) 4. 기존 시스템에 대한 참조를 제거합니다 5. 회귀가 없는지 확인합니다 ``` **교체 책임 규칙 (Churn Rule):** 지원 종료하는 인프라의 소유자라면, 사용자를 마이그레이션할 책임은 여러분에게 있습니다. 또는 마이그레이션이 필요 없는 하위 호환 업데이트를 제공해야 합니다. 지원 종료를 공지하고 사용자가 알아서 해결하게 두지 마세요. ### 4단계: 기존 시스템을 제거하세요 모든 소비자가 마이그레이션을 끝낸 뒤에만 제거하세요. ``` 1. 활성 사용이 0인지 검증합니다 (메트릭, 로그, 의존성 분석) 2. 코드를 제거합니다 3. 연결된 테스트, 문서, 설정을 제거합니다 4. 지원 종료 공지를 제거합니다 5. 축하하세요. 코드를 제거하는 것은 성과입니다 ``` ## 마이그레이션 패턴 ### 스트랭글러 패턴 (Strangler Pattern) 기존 시스템과 새 시스템을 병렬로 돌리세요. 트래픽을 기존에서 새것으로 점진적으로 옮기세요. 기존 시스템이 트래픽의 0%를 처리할 때 제거하세요. ``` 1단계: 새 시스템 0%, 기존 시스템 100% 2단계: 새 시스템 10% (카나리) 3단계: 새 시스템 50% 4단계: 새 시스템 100%, 기존 시스템은 유휴 5단계: 기존 시스템 제거 ``` ### 어댑터 패턴 (Adapter Pattern) 기존 인터페이스 호출을 새 구현으로 번역하는 어댑터를 만드세요. 백엔드를 마이그레이션하는 동안 소비자는 기존 인터페이스를 계속 사용합니다. ```typescript // Adapter: old interface, new implementation class LegacyTaskService implements OldTaskAPI { constructor(private newService: NewTaskService) {} // Old method signature, delegates to new implementation getTask(id: number): OldTask { const task = this.newService.findById(String(id)); return this.toOldFormat(task); } // Translate the new shape back into what old consumers expect private toOldFormat(task: Task): OldTask { return { id: Number(task.id), title: task.title, done: task.status === 'done' }; } } ``` ### 피처 플래그 마이그레이션 피처 플래그로 소비자를 기존 시스템에서 새 시스템으로 하나씩 전환하세요. ```typescript function getTaskService(userId: string): TaskService { if (featureFlags.isEnabled('new-task-service', { userId })) { return new NewTaskService(); } return new OldTaskService(); } ``` ### 데이터베이스 스키마 마이그레이션 (Expand/Contract) 스키마 변경은 가장 위험한 마이그레이션입니다. 데이터는 배포를 되돌려서 롤백할 수 있는 대상이 아니기 때문입니다. 실패 양상은 스키마 변경과 코드 변경을 묶는 것입니다. 새 이름을 쓰기 시작하는 릴리스와 같은 릴리스에서 컬럼 이름을 바꾸면, 롤아웃 구간에서 기존 코드와 새 코드가 동시에 돌 때 한쪽은 존재하지 않는 컬럼을 조회합니다. 해결책은 **컬럼을 제자리에서 절대 바꾸지 않는 것**입니다. 추가만 하는 단계로 마이그레이션해서, 매 단계에서 기존 코드와 새 코드가 모두 유효하게 하세요. ``` EXPAND ──────────────→ MIGRATE ──────────────→ CONTRACT add the new column, backfill existing rows, once no code reads the nullable, alongside dual-write old+new from old column, drop it in the old one the app a later, separate deploy ``` **예시: `name`을 `full_name`으로 바꾸기** 1. **확장 (Expand).** `full_name`을 nullable로 추가합니다. 배포합니다. (기존 코드는 무시하므로 아무것도 깨지지 않습니다.) 2. **이중 쓰기 (Dual-write).** 앱이 모든 insert/update에서 `name`과 `full_name`을 함께 씁니다. 배포합니다. 3. **백필 (Backfill).** 기존 행의 `name`을 `full_name`으로 복사합니다. 테이블 잠금을 피하도록 배치로 합니다. 4. **읽기 전환 (Switch reads).** 앱이 `full_name`을 읽게 하고, 쓰기는 둘 다 유지합니다. 배포한 뒤 안정될 때까지 둡니다. 5. **축소 (Contract).** `name` 쓰기를 멈춘 다음, *나중의 별도* 배포에서 컬럼을 삭제합니다. 각 단계는 독립적으로 배포할 수 있고 되돌릴 수 있습니다. 4단계가 이상하면 코드를 롤백해도 `full_name`은 계속 채워지고 있습니다. 각 단계를 얇은 수직 슬라이스로 다루세요. `incremental-implementation` 스킬을 참고하세요. **규칙:** - **추가는 먼저, 파괴는 마지막에 단독으로.** 추가(새 nullable 컬럼, 새 테이블, 새 인덱스)는 어떤 배포에서든 안전합니다. 삭제와 이름 변경은 어떤 코드도 기존 형태를 참조하지 않은 *뒤* 별도의 배포로 내세요. - **모든 마이그레이션에는 검증된 `down` 경로가 있습니다.** 되돌릴 수 없는 마이그레이션은 롤백할 수 없는 배포입니다. 머지하기 전에 `down`을 작성하고 실행하세요. - **백필은 배치로, 요청 경로 밖에서.** 수백만 행을 한 번의 `UPDATE`로 처리하면 테이블이 잠깁니다. 잘라서 속도를 제한하세요. - **쓰기를 막지 않고 큰 인덱스를 만드세요** (예: Postgres `CREATE INDEX CONCURRENTLY`). - **전환이 위험하면 피처 플래그로 코드와 분리하세요.** 위의 피처 플래그 마이그레이션 패턴과 같습니다. ## 좀비 코드 좀비 코드는 아무도 소유하지 않지만 모두가 의존하는 코드입니다. 적극적으로 유지되지 않고, 명확한 소유자가 없으며, 보안 취약점과 호환성 문제가 쌓입니다. 신호는 다음과 같습니다. - 6개월 이상 커밋이 없지만 활성 소비자는 있습니다 - 지정된 유지관리자나 팀이 없습니다 - 아무도 고치지 않는 실패하는 테스트가 있습니다 - 알려진 취약점이 있는 의존성을 아무도 업데이트하지 않습니다 - 문서가 더는 존재하지 않는 시스템을 가리킵니다 **대응:** 소유자를 지정하고 제대로 유지하거나, 구체적인 마이그레이션 계획과 함께 지원 종료하세요. 좀비 코드는 어정쩡한 상태로 둘 수 없습니다. 투자를 받거나 제거되거나, 둘 중 하나입니다. ## 흔한 합리화 | 합리화 | 현실 | |---|---| | "아직 동작하는데 왜 제거하나요?" | 아무도 유지하지 않는 동작하는 코드는 보안 부채와 복잡도를 쌓습니다. 유지보수 비용은 조용히 커집니다. | | "나중에 누군가 필요할 수 있습니다" | 나중에 필요하면 다시 만들 수 있습니다. 만약을 위해 쓰지 않는 코드를 두는 비용이 다시 만드는 비용보다 큽니다. | | "마이그레이션이 너무 비쌉니다" | 마이그레이션 비용을 2~3년간의 지속 유지보수 비용과 비교하세요. 장기적으로는 마이그레이션이 더 싼 경우가 많습니다. | | "새 시스템을 끝낸 뒤에 지원 종료하겠습니다" | 지원 종료 계획은 설계 시점에 시작합니다. 새 시스템이 끝날 때쯤이면 새로운 우선순위가 생깁니다. 지금 계획하세요. | | "사용자가 알아서 마이그레이션할 것입니다" | 그렇지 않습니다. 도구, 문서, 유인을 제공하거나, 마이그레이션을 직접 하세요 (교체 책임 규칙). | | "두 시스템을 무기한 유지할 수 있습니다" | 같은 일을 하는 시스템 둘은 유지보수, 테스트, 문서, 온보딩 비용이 두 배입니다. | | "컬럼 이름만 바꾸면 됩니다. 한 줄인데요" | 롤아웃 동안 기존 코드와 새 코드가 함께 돕니다. 한쪽은 더는 없는 컬럼을 조회합니다. 제자리 이름 변경은 하지 말고 expand/contract를 쓰세요. | | "같은 마이그레이션에서 컬럼을 추가하고 기존 컬럼을 삭제하겠습니다" | 안전한 추가와 파괴적인 삭제를 묶습니다. 삭제는 어떤 코드도 기존 형태를 참조하지 않은 뒤, 별도의 배포로 내세요. | | "필요해지면 롤백을 작성하겠습니다" | `down` 경로가 없는 마이그레이션은 되돌릴 수 없는 배포입니다. 머지하기 전에 `down`을 작성하고 실행하세요. | ## 레드 플래그 - 대체 구현이 없는 지원 종료 시스템 - 마이그레이션 도구나 문서가 없는 지원 종료 공지 - 몇 년째 권고 상태로 진전이 없는 "부드러운" 지원 종료 - 소유자는 없고 활성 소비자는 있는 좀비 코드 - 지원 종료 시스템에 새 기능이 추가됨 (대체 구현에 투자하세요) - 현재 사용량을 측정하지 않은 지원 종료 - 활성 소비자가 0인지 검증하지 않고 코드를 제거함 - 스키마 변경과 그에 의존하는 코드가 같은 배포로 나감 - expand/contract가 아니라 제자리에서 컬럼 이름을 바꾸거나 삭제함 - 검증된 `down` 경로 없이 머지된 마이그레이션, 또는 테이블을 잠그는 백필 ## 검증 지원 종료를 마친 뒤: - [ ] 대체 구현이 프로덕션에서 검증되었고 모든 핵심 사용 사례를 다룹니다 - [ ] 구체적인 단계와 예시가 있는 마이그레이션 가이드가 있습니다 - [ ] 모든 활성 소비자가 마이그레이션되었습니다 (메트릭/로그로 검증) - [ ] 기존 코드, 테스트, 문서, 설정이 완전히 제거되었습니다 - [ ] 코드베이스에 지원 종료 시스템에 대한 참조가 남아 있지 않습니다 - [ ] 지원 종료 공지가 제거되었습니다 (역할을 다했습니다) 데이터베이스 스키마 마이그레이션 뒤: - [ ] 변경이 추가 단계로 나갑니다 (확장 → 백필 → 축소). 한 번의 제자리 수정이 아닙니다 - [ ] 모든 배포 단계에서 기존 코드와 새 코드가 모두 스키마에 대해 유효합니다 - [ ] 각 마이그레이션에 검증된 `down` 경로가 있고, 백필은 속도를 제한한 배치로 돕니다 - [ ] 파괴적 단계(삭제/이름 변경)는 어떤 코드도 기존 형태를 참조하지 않은 뒤 별도의 배포로 나갑니다