배포를 단순하게 생각하면:
v1 서버 종료
↓
v2 서버 시작
처럼 보인다.
하지만 실제 운영에서는 잠깐이라도:
Old Version
+
New Version
이 동시에 존재할 수 있다.
예:
Instance A
v1
Instance B
v2
또는:
Frontend
구버전 캐시
Backend
신버전
일 수도 있다.
예를 들어 Backend Response를:
{
"phone": "010..."
}
에서 갑자기:
{
"phoneNumber": "010..."
}
로 바꿨다고 하자.
신규 Frontend는 정상이다.
하지만 아직 구버전 Frontend가:
response.phone
을 사용하고 있다면 바로 깨진다.
쉽게 말하면:
새로운 버전이 기존 Client·Consumer·데이터와도 일정 기간 정상적으로 동작할 수 있는 성질
이다.
즉:
New Backend
+
Old Frontend
도 동작하고,
가능하다면:
Old Backend
+
New Frontend
도 일정 범위에서 동작하도록 설계한다.
개념적으로:
Backward Compatibility
신버전이 구버전을 이해함
Forward Compatibility
구버전이 미래 데이터를 어느 정도 견딤
으로 볼 수 있다.
실무에서는 우선 Backward Compatibility를 훨씬 자주 신경 쓴다.
서버를 끄지 않는다고 자동으로 무중단 배포가 되는 것은 아니다.
실제로는:
Old Code
+
New Code
+
Current DB Schema
조합이 모두 잠시 공존할 수 있어야 한다.
예:
orders.customer_phone
Column을:
orders.phone_number
로 Rename했다고 하자.
신버전은:
phone_number
을 읽는다.
하지만 구버전 서버는 계속:
customer_phone
을 읽는다.
Rename 순간 구버전이 깨질 수 있다.
대신:
Add
↓
Migrate
↓
Switch
↓
Remove
순서로 진행한다.
이게 0927에서 언급한:
Expand
→ Migrate
→ Contract
패턴과 연결된다.
기존 Column은 유지하고 새 Column만 추가한다.
기존:
customer_phone
추가:
phone_number
이제 DB에는 둘 다 존재한다.
구버전:
customer_phone
사용.
신버전:
phone_number
을 사용할 준비를 한다.
즉 아직 Breaking Change가 없다.
Migration 중에는 잠깐:
Old Column
+
New Column
둘 다 다뤄야 할 수 있다.
예:
phoneNumber =
row.phoneNumber ??
row.customerPhone;
처럼 Read fallback을 둔다.
새 데이터 저장 시:
customer_phone
phone_number
둘 다 동일하게 저장할 수도 있다.
예:
await tx.order.update({
data: {
customerPhone: phone,
phoneNumber: phone,
},
});
Migration 기간에만 사용하는 임시 호환 계층이다.
장기간 유지하면:
두 값 불일치
코드 복잡도 증가
어느 Column이 Source of Truth인지 혼란
이 생긴다.
예:
Migration 초기
customer_phone = Source of Truth
이후:
Migration 완료
phone_number = Source of Truth
로 전환한다.
기존 Row의 새 Column을 채운다.
예:
UPDATE orders
SET phone_number = customer_phone
WHERE phone_number IS NULL;
하지만 Production에서는 한 번에 모든 Row를 갱신하면 위험할 수 있다.
예:
1,000 rows
↓
Commit
↓
1,000 rows
↓
Commit
형태로 처리한다.
대규모 Update는:
Long Transaction
DB Lock
WAL 증가
CPU/IO 증가
API Latency 상승
을 만들 수 있다.
예:
MIGRATION_JOB
Total
1,000,000
Processed
250,000
Remaining
750,000
처럼 관리할 수 있다.
중간에 서버가 죽더라도:
0부터 다시
하지 않는다.
마지막 Cursor나 조건을 기준으로 이어간다.
예:
UPDATE orders
SET phone_number = customer_phone
WHERE phone_number IS NULL;
은 여러 번 실행해도 이미 채운 Row는 건너뛴다.
이런 형태가 안전하다.
단순히 Job이 성공했다고 끝내지 않는다.
확인:
phone_number IS NULL
0건
Old / New 불일치
0건
같은 검증이 필요하다.
Backfill이 완료되면 신버전 코드에서:
phone_number
을 Primary Read로 사용한다.
필요하다면 잠깐 fallback을 유지한다.
이후 새 Column만 쓰게 바꿀 수 있다.
단 아직 구버전 Instance가 존재한다면 Old Column Write를 너무 빨리 중단하면 안 된다.
충분히 안정화된 뒤:
customer_phone
을 제거한다.
이게 마지막 단계다.
삭제는 되돌리기 어렵다.
새 코드 문제
→ 이전 코드 Rollback
을 하려는데 Old Column이 이미 삭제됐다면 Rollback이 깨질 수 있다.
Additive Change 먼저
Destructive Change 나중
이다.
안전한 편:
새 Column 추가
새 Table 추가
새 Index 추가
새 Event Field 추가
새 API Response Field 추가
이다.
위험:
Column 삭제
Column Rename
Table 삭제
기존 API Field 제거
기존 Event Field 제거
Status 값 의미 변경
이다.
기존 API:
GET /api/orders
Response:
{
"id": "...",
"status": "WAITING"
}
새 요구사항으로 status 구조를 바꾸고 싶다고 하자.
나쁜 변경:
{
"status": {
"code": "WAITING",
"label": "대기"
}
}
기존 Client는 문자열을 기대하고 있다.
기존 Field는 유지한다.
{
"status": "WAITING",
"statusInfo": {
"code": "WAITING",
"label": "대기"
}
}
신규 Client는 statusInfo를 사용한다.
즉 API에서도:
Expand
→ Migrate Clients
→ Deprecate
→ Remove
흐름이 있다.
/v2로 만들 필요는 없다작은 Additive Change는 기존 API에 추가해도 된다.
예:
새 Optional Field 추가
정도는 Version 증가 없이 가능할 수 있다.
예:
Response 구조 대규모 변경
Field 의미 변경
필수 Request 구조 변경
행동 자체 변경
같은 Breaking Change가 있을 때다.
예:
/api/v1/orders
/api/v2/orders
가 가장 이해하기 쉬운 방식 중 하나다.
예:
Accept-Version: 2
같은 형태도 가능하지만 현재 규모에서는 URL Versioning이 더 단순할 수 있다.
예:
v1
v2
v3
v4
를 모두 장기간 지원하면:
Controller 증가
Service 분기
Test 증가
Bug Fix 중복
이 발생한다.
작은 변경마다 v2를 만드는 것보다:
Additive Change
로 해결 가능한지 먼저 본다.
기존 API를 바로 삭제하지 않고:
Deprecated
상태를 둔다.
예:
GET /api/v1/orders
는 아직 동작하지만 신규 개발에서는 사용하지 않는다.
내부 문서에:
Deprecated At
Replacement
Removal Target
Known Consumers
를 적는다.
현재 Frontend만 쓴다고 생각했는데:
Excel Script
Chrome Extension
Automation Tool
Partner Integration
이 사용할 수 있다.
삭제 전에 실제 Consumer를 확인한다.
Deprecated Endpoint 사용량을 측정할 수 있다.
예:
/api/v1/orders
최근 7일 요청
0
이면 제거 후보가 된다.
특정 월말 Batch처럼 드물게 호출되는 Consumer가 있을 수 있다.
업무 주기를 고려한다.
새 Field를 필수로 추가하면 기존 Client가 깨질 수 있다.
기존:
{
"orderId": "123"
}
새 API:
{
"orderId": "123",
"source": "NAVER"
}
에서 source를 필수로 만들면 Old Client가 실패한다.
source absent
→ DEFAULT / legacy behavior
로 처리한다.
Client가 모두 전환된 뒤 필수화를 검토한다.
예:
source 미입력
→ UNKNOWN
처럼 의미가 명확해야 한다.
잘못된 비즈니스 값을 임의 Default로 넣지 않는다.
기존:
WAITING
DONE
신규:
WAITING
PROCESSING
DONE
을 추가했다고 하자.
Old Frontend가:
switch (status) {
case 'WAITING':
case 'DONE':
}
만 처리한다면 새 값에서 UI가 깨질 수 있다.
예:
default:
return '처리 중';
처럼 안전한 fallback을 둔다.
예:
REFUNDED
CANCELLED
을 모두:
UNKNOWN
으로 숨기면 운영자가 중요한 상태를 놓칠 수 있다.
따라서 Unknown 상태를 명시적으로 보여주는 편이 낫다.
1001에서 Outbox Event를 다뤘다.
Event Producer와 Consumer는 배포 시점이 다를 수 있다.
즉:
Producer v2
Consumer v1
조합이 잠시 존재할 수 있다.
기존:
{
"version": 1,
"orderId": "123",
"status": "DONE"
}
신규:
{
"version": 1,
"orderId": "123",
"status": "DONE",
"carrier": "LGU"
}
Consumer가 Unknown Field를 무시할 수 있다면 호환 가능하다.
Consumer v1이:
status
를 요구하는데 Producer가 없애버리면 DLQ가 발생할 수 있다.
예:
ORDER_STATUS_CHANGED_V1
ORDER_STATUS_CHANGED_V2
또는:
{
"eventType": "ORDER_STATUS_CHANGED",
"version": 2
}
형태다.
예:
switch (event.version) {
case 1:
return handleV1(event);
case 2:
return handleV2(event);
default:
throw new UnsupportedEventVersionError();
}
먼저 Consumer가 v2를 이해하도록 배포한다.
즉:
Consumer First
↓
Producer Second
전략이다.
예:
1. Consumer가 V1 + V2 지원
2. Producer가 V2 발행 시작
3. V1 Event backlog/DLQ 소진 확인
4. V1 Producer 중단
5. 충분한 기간 후 V1 Consumer 제거
이다.
Schema뿐 아니라 Event에서도 동일하다.
예:
Notification
Analytics
CRM
세 Consumer 모두 v2를 지원해야 한다.
하나라도 v1만 지원하면 Producer 전환 시 깨질 수 있다.
예:
| Consumer | V1 | V2 |
|---|---|---|
| Notification | O | O |
| Analytics | O | O |
| CRM | O | X |
이 경우 아직 v2 전환이 완료된 것이 아니다.
예:
ORDER_STATUS_CHANGED
Producer
v2
Consumers
notification: v1,v2
analytics: v1,v2
crm: v1
같이 본다.
문서나 코드 상수로도 충분하다.
핵심은 Breaking Change가 누구에게 영향을 주는지 파악하는 것이다.
앱 버전:
release_20261002_03
DB Migration 상태:
migration_20261002_02
를 별도로 추적하면 좋다.
예:
{
"releaseId": "release_20261002_03",
"dbMigrationVersion": "2026100202"
}
정도다.
민감 정보를 노출할 필요는 없다.
예:
minimumDbVersion
2026100202
보다 DB가 오래되면 서버 시작을 차단할 수도 있다.
예:
DB는 destructive migration 완료
Old App rollback
하려는데 Old App이 필요한 Column이 사라졌다.
그래서 Migration과 Rollback Compatibility를 함께 봐야 한다.
DB Schema가 일정 기간:
Old App
+
New App
둘 다 지원하도록 유지한다.
이게 무중단 배포에서 중요하다.
기존 Column:
source NULL 허용
새 Requirement:
source NOT NULL
이라고 하자.
바로 Constraint를 추가하면 기존 데이터 때문에 실패할 수 있다.
1. Application에서 신규 Row에 source 저장
2. 기존 NULL Backfill
3. NULL 남은 Row 검증
4. NOT NULL Constraint 추가
이다.
처음부터 강하게 넣으면 Old App이 여전히 NULL을 넣을 수 있다.
대규모 Table에 Default + NOT NULL을 한 번에 적용하면 DB 버전/상황에 따라 비용이 클 수 있다.
현재 Table 규모와 실제 SQL을 확인한다.
대형 Table에서 일반 Index 생성이 Write를 방해할 수 있다.
Production에서는 Database가 지원하는 온라인/동시 생성 방식을 검토할 수 있다.
ORM을 사용한다고 안전성이 자동 보장되는 것은 아니다.
특히:
DROP
RENAME
NOT NULL
UNIQUE
INDEX
변경은 실제 Migration SQL을 확인한다.
Table Lock 가능성
Full Table Scan
대량 Rewrite
기존 데이터 호환
Rollback 가능성
Old App Compatibility
를 확인한다.
즉:
RENAME COLUMN
보다:
ADD new_column
Backfill
Application Switch
DROP old_column
가 더 안전할 수 있다.
기존 Table을 바로 Rename하는 대신 새 Table로 Migration 후 점진적으로 전환할 수 있다.
다만 작은 내부 프로젝트에서는 과도할 수 있으므로 실제 위험도에 맞춘다.
예:
price
INTEGER
를:
BIGINT
으로 바꾸는 건 비교적 단순할 수 있지만,
VARCHAR
→ JSON
같은 변경은 훨씬 크다.
예:
options_text
를:
options_json
으로 추가하고 Backfill한다.
기존 Column은 유지한다.
기존 데이터가 모두 정상일 거라고 가정하지 않는다.
예:
100만 건 중
57건 파싱 실패
할 수 있다.
예:
migration_errors
또는 Migration Report에:
id
reason
sourceValue
를 기록한다.
민감 데이터는 주의한다.
업무 중요도에 따라:
ALL_OR_NOTHING
BEST_EFFORT
MANUAL_REVIEW
정책이 다를 수 있다.
예:
Total
1,000,000
Processed
943,000
Failed
57
Remaining
56,943
를 볼 수 있으면 운영하기 쉽다.
Backfill만 생각하면 놓치기 쉬운 부분이다.
기존 Row Backfill 중
동시에 신규 주문 생성
이 계속 발생한다.
순서:
1. 새 Column 추가
2. 신규 Write가 Old + New 모두 기록
3. 기존 데이터 Backfill
이어야 Backfill 종료 후 다시 누락이 생기지 않는다.
Migration 기간 동안:
oldValue != newValue
인 Row를 주기적으로 찾는다.
예:
phone_column_mismatch
3
같은 Gauge를 둘 수도 있다.
0이 유지되는지 확인한다.
신규 Read Path를 실제 Response에 쓰기 전 검증할 수도 있다.
예:
실제 응답은 Old Query
백그라운드에서 New Query도 실행
결과 비교
한다.
예:
Old Result
100 orders
New Result
98 orders
라면 아직 전환하면 안 된다.
Query를 두 번 실행하므로 DB 부하가 증가한다.
일부 요청에만 적용하거나 샘플링한다.
신규 기능을 실제 사용자 결과에는 반영하지 않고 백그라운드에서 실행해 검증하는 방식이다.
Feature Flag/Canary와 연결된다.
예:
Client에는 v1 결과 반환
v2 Query도 실행
차이만 Metric 기록
할 수 있다.
이 방식은 위험한 조회 로직 변경에 유용하다.
실제 Side Effect가 두 번 발생할 수 있기 때문이다.
Write는 단순히 Shadow하지 않는다.
예:
Old Writer
New Writer
가 동시에 동작하면 중복 Side Effect가 생길 수 있다.
업무 Key를 기준으로 중복을 막는다.
새 API를 만들었다면 Consumer를 단계적으로 이동한다.
예:
관리자 v2
개발자 계정만
↓
일부 관리자
↓
전체 관리자
Feature Flag를 활용할 수 있다.
즉:
Create v2
Migrate Consumer
Observe
Deprecate v1
Remove v1
이다.
API Response가 기존 Client Contract를 깨지 않는지 테스트한다.
예:
status field 존재
id string
items array
같은 계약이다.
Response 전체 Snapshot은 작은 변경에도 자주 깨진다.
중요 Contract 중심 테스트가 더 낫다.
Consumer가:
나는 이 Field가 필요하다
라는 계약을 명시하는 방식이다.
현재 규모에서는 전문 도구까지 도입하지 않아도 된다.
예:
expect(order).toMatchObject({
id: expect.any(String),
status: expect.any(String),
});
정도로도 핵심 Field 삭제를 잡을 수 있다.
Producer가 생성하는 Event가 Consumer Schema와 호환되는지 테스트한다.
예:
ORDER_STATUS_CHANGED v2
Event가 Required Field를 모두 포함하는지 검증한다.
v1/v2 Event Sample을 각각 넣어 정상 처리되는지 확인한다.
예:
test/fixtures/events/
order-status-changed-v1.json
order-status-changed-v2.json
같이 유지할 수 있다.
현재 Producer가 더 이상 v1을 만들지 않아도 DLQ/Retry에 v1이 남아 있을 수 있다.
Consumer가 계속 처리해야 하는 기간에는 Fixture도 유지한다.
Migration 전 Sample Data를 만들어:
Migration 적용
↓
예상 Schema/Data 확인
테스트할 수 있다.
개인정보 문제도 있고 무겁다.
대표적인 형태만 익명화해서 Fixture로 만든다.
예:
Old App Query
New Schema
조합이 동작하는지 Staging에서 확인할 수 있다.
예:
| App | DB | 결과 |
|---|---|---|
| v1 | schema 1 | O |
| v1 | schema 2 | O |
| v2 | schema 2 | O |
| v2 | schema 3 | O |
| v1 | schema 3 | X |
이 경우 schema 3 적용 이후에는 v1 Rollback이 불가능하다.
0927의:
SAFE
CONDITIONAL
UNSAFE
분류에 사용할 수 있다.
예:
새 Column만 추가
했다면 이전 App은 무시하므로 Rollback이 쉽다.
예:
Old Column 삭제
후에는 이전 App이 바로 깨질 수 있다.
Release Gate에서:
파괴적 Migration인가?
Old App이 새 Schema에서 동작하는가?
Rollback 가능한가?
를 확인한다.
예:
Column Rename
Data Transform
API 변경
Event V2
를 한 번에 배포하면 문제 발생 시 원인 추적이 어렵다.
예:
새 Column 추가
Dual Write
Backfill
Read Switch
Old Column 제거
처럼 나눈다.
무중단 Migration은 보통:
한 번에 빨리
보다:
여러 번에 나눠 안전하게
하는 쪽이다.
예:
order_phone_v2_read
Flag를 두고:
개발자 계정
↓
일부 Traffic
↓
전체
로 확대할 수 있다.
DB Schema는 그대로 두고 Read만 Old Path로 돌린다.
전체 Rollback보다 빠르다.
Old/New Write가 서로 다른 데이터를 만들 수 있기 때문이다.
Write 전환은 Data Consistency 검증을 더 강하게 한다.
예:
new_phone_read_enabled
가 100% ON으로 안정화되고 Old Path를 제거했다면 Flag도 삭제한다.
0928의 Flag Lifecycle과 연결된다.
v2 Consumer에 문제가 생기면:
v2_consumer_enabled = false
로 멈출 수 있다.
단 Event는 버리지 않는다.
Consumer v2를 멈춘 동안 Event가 쌓이면 나중에 복구 시 해당 Version을 계속 이해할 수 있어야 한다.
예:
V1 Event Pending
0
V1 DLQ
0
인지 확인한다.
0930에서 Workflow Version을 Run에 고정한다고 했다.
예:
Workflow v1
3일째 WAITING
인 상태에서 코드가 v2로 배포될 수 있다.
짧은 Workflow라면 배포 전에 모두 종료시키는 방법도 있다.
긴 Workflow라면 Version Compatibility가 필요하다.
v1 Run이 모두:
SUCCESS
FAILED
CANCELLED
같은 Terminal State가 될 때까지 v1 Resume 로직을 유지할 수 있다.
예:
v1 active runs
0
확인 후 v1 Definition 제거한다.
AI Run에는:
workflowVersion
promptVersion
modelVersion
policyVersion
이 들어간다.
예:
Run 시작
prompt v4
중간에 시스템 Prompt가 v5로 바뀌었다고 Retry 시 v5를 쓰면 같은 Workflow가 다른 규칙으로 이어질 수 있다.
prompt v4
policy v7
model A
를 그대로 사용한다.
예:
Model 삭제
API 폐기
등이면 기존 Run을 새 Version으로 Migration할지 Manual 처리할지 결정한다.
Prompt/Policy 변경은 결과 의미를 바꿀 수 있다.
새 Run 생성
이 더 안전한 경우가 많다.
API Response 구조가 바뀌었는데 Old Cache가 남아 있을 수 있다.
예:
Backend v2
+
Cache v1 payload
조합이다.
예:
product:v1:{id}
product:v2:{id}
처럼 Version을 Key에 포함할 수 있다.
전환 후 v1 Cache는 TTL로 자연스럽게 사라지게 둘 수 있다.
Frontend Asset나 API Cache가 남아:
Old Frontend
+
New API
조합이 발생할 수 있다.
그래서 API Compatibility가 더 중요하다.
예:
app.abc123.js
app.def456.js
처럼 새 Asset을 별도 파일로 배포한다.
기존 Asset을 즉시 삭제하면 Old Client가 404를 볼 수 있다.
일정 기간 이전 Asset을 유지한다.
브라우저 Tab을 며칠간 열어둘 수 있다.
Old Frontend가 며칠 뒤 New API를 호출할 수도 있다.
예:
구버전 Frontend
최소 7일 지원
같은 내부 정책을 정할 수 있다.
정확한 기간은 서비스 특성에 맞춘다.
관리자가 오래된 관리자 페이지를 열어둔 채 작업할 수 있다.
Breaking API 변경 직후 저장을 누르면 실패할 수 있다.
Frontend가 현재 Release Version을 가지고:
Client Version
Server Min Supported Version
을 비교할 수도 있다.
예:
새 버전이 배포되었습니다.
페이지를 새로고침해주세요.
같은 UX를 제공할 수 있다.
즉시 새로고침보다:
현재 작업 저장
새로고침
을 유도하는 편이 낫다.
기존 Client가:
ORDER_NOT_FOUND
를 처리하고 있는데 갑자기:
NOT_FOUND
으로 바꾸면 Frontend 동작이 달라질 수 있다.
다음도 Versioning 대상이다.
HTTP Status
errorCode
error shape
validation errors
기존:
{
"code": "INVALID_ORDER"
}
신규:
{
"code": "INVALID_ORDER",
"details": {}
}
정도는 비교적 안전하다.
Frontend나 자동화에서 분기할 수 있기 때문이다.
Chrome Extension, RPA, Local Script가 호출한다면 일반 Frontend와 마찬가지다.
“내부용”이라고 Breaking Change가 안전한 것은 아니다.
예를 들어 Table 구조가 바뀌었는데 구버전 Query를 잠깐 지원하기 위해 View를 둘 수 있다.
다만 현재 규모에서는 복잡해질 수 있으므로 필요할 때만 고려한다.
예:
LegacyAdapter
DeprecatedFieldMapper
DualWrite
에는 제거 Issue와 시점을 남긴다.
몇 년 후에도:
v1
v2
legacy
temp
코드가 다 남게 된다.
삭제되지 않은 호환 코드도 기술 부채다.
예:
Deprecated API
Old Event Handler
Dual Write
Fallback Read
Legacy Column
이다.
예:
Old API Usage = 0
Old Event Pending = 0
Old Workflow Active = 0
Old Column Read = 0
이면 제거 후보가 된다.
감으로:
아마 이제 안 쓰겠지
라고 삭제하지 않는다.
실제 Usage Metric을 본다.
가능하다면:
legacy_field_read_total
같은 Metric으로 사용 여부를 추적할 수 있다.
API Endpoint 사용량, Event Version 소비량 등 큰 단위부터 본다.
절대 0ms 장애
보다 현실적으로는:
배포 중 구/신 버전 공존으로 인해
사용자 요청이 실패하지 않게 함
에 가깝다.
예:
DB Expand
↓
Backward-Compatible App
↓
Backfill
↓
Read Switch
↓
Observe
↓
DB Contract
순서다.
Old Column Delete
↓
New App Deploy
이면 Delete와 Deploy 사이에 기존 App이 깨진다.
1. 신규 Schema 추가
2. Migration 적용 확인
3. Backward-Compatible App 배포
4. Dual Write 확인
5. Existing Data Backfill
6. Consistency Check
7. New Read Path Canary
8. 전체 Read 전환
9. 일정 기간 Monitoring
10. Old Write 중단
11. Old Column 사용량 확인
12. Old Column 제거
급하게 같은 날 삭제할 필요 없다.
안정성을 위해 호환 기간을 둔다.
초반 단계일수록:
Feature Flag OFF
Old App Rollback
이 가능하도록 한다.
Migration 중 특정 지점부터 단순 Rollback이 불가능해질 수 있다.
예:
Old Column 삭제
이다.
이를 명시한다.
예:
Step 1~7
Rollback Safe
Step 8
Old Column Drop
After Step 8
Roll Forward Only
처럼 관리할 수 있다.
0927의 Auto Rollback이 무조건 안전한 것이 아닌 이유다.
Backup이 있다고:
쉽게 되돌릴 수 있음
을 의미하지 않는다.
Production 전체 DB Restore는 매우 큰 작업이다.
Schema 변경의 일반 Rollback 전략은:
Backward Compatibility
+
Roll Forward
가 더 현실적일 수 있다.
Migration 후 New App에 Bug가 있더라도 Schema가 이미 전진했다면:
Hotfix v2.1
을 배포하는 것이 이전 v1로 돌아가는 것보다 안전할 수 있다.
예:
09:00
Migration 시작
09:10
Backfill 50%
09:18
Latency 상승
09:20
Backfill Pause
같은 기록을 남긴다.
대량 Migration은:
PAUSE
RESUME
할 수 있으면 좋다.
DB 부하가 증가하면 잠시 중지한다.
예:
500 rows/sec
처럼 속도를 제한할 수 있다.
Production Traffic을 우선한다.
현재는 Metric 보면서 사람이 Batch Size를 조절하는 정도면 충분하다.
예:
PLANNED
RUNNING
PAUSED
VERIFYING
COMPLETED
FAILED
MANUAL_REQUIRED
정도로 관리할 수 있다.
Prisma Migration:
Schema 구조 변경
Backfill Job:
기존 데이터 변환
이다.
둘을 하나의 긴 SQL Transaction으로 만들지 않는 편이 안전한 경우가 많다.
가능하면:
Schema가 먼저 신버전 코드를 받아들일 준비
를 한다.
상황에 따라:
App First
가 필요한 Migration도 있다.
따라서 무조건 한 규칙이 아니라 Compatibility Matrix로 판단한다.
새 Column Read를 배포해놓고:
flag = false
로 유지한다.
Backfill/검증 후 Flag ON한다.
빠르게 이전 Read Path로 돌아갈 수 있다.
Migration 동안:
Backfill %
Mismatch Count
NULL Count
Error Count
DB CPU
Query Latency
를 본다.
예:
Backfill 100%
Mismatch 0
New Read Error 0
p95 정상
Old Path Usage 0
등이다.
Data가 실제 올바른지 검증해야 한다.
예:
v1 requests
4%
v2 requests
96%
v1 error rate
0.1%
v2 error rate
0.1%
를 보고 전환 상황을 판단한다.
V1 produced
0
V1 consumed
12
V1 DLQ
0
V2 consumed
4,291
처럼 확인한다.
예:
V1 Producer = 0
V1 Queue Pending = 0
V1 DLQ = 0
V1 Consumer Usage = 0
을 만족하면 v1 제거를 검토한다.
CI에서 최소한:
Current Client Contract
Current Event Fixtures
Migration Test
를 돌린다.
예:
prisma/
변경
→ migration compatibility test
events/
변경
→ event contract test
api/
DTO 변경
→ API contract test
를 실행한다.
AI가 Diff를 분석해 다음을 표시할 수 있다.
Field 제거
Enum 변경
Migration DROP
Rename
Event Payload 변경
Required Request Field 추가
등이다.
예:
OrderResponse.status
string → object
변경을 보고:
기존 Client Compatibility 확인 필요
라고 지적할 수 있다.
AI가:
안전해 보입니다
라고 해도 실제 Test가 실패하면 Release를 막는다.
Schema Diff
Contract Test
Migration Test
Runtime Validation
을 AI 의견보다 우선한다.
AI에게 실제 Diff 기반으로:
Expand 단계
Backfill
Read Switch
Cleanup
순서를 초안으로 만들게 할 수 있다.
특히:
DROP
DELETE
대량 UPDATE
는 Approval이 필요하다.
예:
Lock 위험
Full Table Rewrite 가능성
Rollback 위험
Old App Compatibility
를 체크리스트 형태로 분석하게 할 수 있다.
가장 먼저 다음 세 영역에 적용하면 좋다.
Prisma Migration
Admin API Response
Outbox Event Payload
이다.
주문관리 UI가 계속 발전하면:
Filter
Pagination
Status
Risk Flags
Response 구조가 바뀔 수 있다.
기존 화면과 호환 가능한 Additive Change를 우선한다.
Orders처럼 핵심 Table은:
Rename
Drop
Type Change
를 한 번에 하지 않는다.
현재 알림톡 자동화가 발전하면:
ORDER_STATUS_CHANGED
Event Payload가 늘어날 수 있다.
기존 Consumer를 깨지 않게 Field를 추가한다.
Production 수준 DB Migration까지는 아니지만:
Report Manifest
Workflow State File
Config Format
Prompt Version
변경 시 기존 Run을 읽을 수 있어야 한다.
예:
{
"version": 2,
"runId": "...",
"status": "FAILED"
}
을 둔다.
기존:
{
"model": "shn-coder"
}
신규:
{
"llm": {
"model": "shn-coder"
}
}
로 바뀐다면 기존 Config를 자동 변환하거나 Deprecated 기간을 둘 수 있다.
기존:
report generate
를 갑자기 없애면 사용자가 깨진다.
새 명령을 추가한 뒤 기존 명령을 Alias/Deprecated로 유지할 수 있다.
API만 Version 관리하는 것이 아니다.
CLI
Config
File Format
Event
Workflow
DB
모두 Contract가 있다.
모든 객체마다 Version이 필요하지는 않다.
Breaking Change 가능성이 높고 오래 보관되는 데이터에 우선 붙인다.
Integration Event
Workflow Run Definition
Persistent Config
File Format
Public/Internal API Contract
이다.
당장은:
GraphQL Schema Registry
전문 Contract Platform
복잡한 API Gateway Version Router
DB Blue/Green Cluster
Multi-region Migration
까지는 필요 없다.
1. 기존 Field 바로 삭제 금지
2. DB Column Rename 대신 Add → Migrate → Remove
3. Event Breaking Change는 Version 증가
4. Migration 전에 Rollback Compatibility 확인
5. Old Usage가 0인지 확인 후 Cleanup
이 다섯 가지가 핵심이다.
EXPAND
↓
DUAL_WRITE
↓
BACKFILL
↓
VERIFY
↓
READ_SWITCH
↓
MONITOR
↓
OLD_PATH_DISABLED
↓
CONTRACT
로 볼 수 있다.
예:
Backfill 100%
Mismatch 0
Old Path Usage 0
+
안정화 기간 통과
이다.
Old Column Usage 0
→ 즉시 DROP
까지 자동화할 필요는 없다.
삭제는 Manual Approval이 안전하다.
예:
/docs/migrations/
2026-10-order-phone-v2.md
에 계획을 기록한다.
# Migration
## 목적
## 현재 구조
## 목표 구조
## Breaking Risk
## Expand
## Dual Write
## Backfill
## Verification
## Read Switch
## Rollback Plan
## Point of No Return
## Contract
## Monitoring
예:
release_20261002_03
migration
order-phone-v2
를 연결한다.
Incident 발생 시 어떤 Migration이 함께 진행됐는지 알 수 있다.
Migration 중 Feature Flag가 바뀌면:
Release
DB Version
Config Version
Flag Version
을 같이 봐야 한다.
예:
{
"releaseId": "release_20261002_03",
"dbSchemaVersion": "2026100202",
"flagVersion": "v44"
}
처럼 주요 Context를 로그에 연결할 수 있다.
예:
release v2
Error 5%
release v1
Error 0.1%
라면 신규 Release 문제 가능성을 빠르게 볼 수 있다.
v2 API/Read Path를 일부 사용자에게만 노출하면:
Old Contract
+
New Contract
를 동시에 유지해야 한다.
예:
Internal Admin
↓
10%
↓
50%
↓
100%
로 새 Read Path를 확대한다.
Backfill:
Carrier = LGU 먼저
검증 후:
SKT
KT
순으로 확장할 수도 있다.
전체 100만 Row보다:
10,000 Row
부터 시작한다.
0925의 Reliability Test와 연결한다.
예:
Backfill 중 Worker Crash
DB Timeout
중간 Batch 실패
Old/New 값 불일치
를 테스트한다.
완료 Batch 보존
중복 Update 안전
Resume 가능
Mismatch 탐지
여야 한다.
예:
Old Frontend Payload
→ New Backend
이 여전히 정상인지 확인한다.
V1 Event
→ New Consumer
V2 Event
→ New Consumer
모두 성공해야 Migration 기간을 안전하게 운영할 수 있다.
Old App Query
→ Expanded Schema
가 성공해야 한다.
Staging에서:
Schema Expand
New App
↓
Old App Rollback
후 정상인지 확인한다.
예:
[Cleanup]
Remove customer_phone legacy column
을 생성한다.
그렇지 않으면 Legacy가 영구화될 가능성이 높다.
- Old read usage = 0
- Old write disabled
- Mismatch = 0
- 7일 안정화
같이 적는다.
조건이 충족되면:
Old API
Old Event
Old Column
Old Flag
을 순서대로 제거한다.
Cleanup이라고 위험이 없는 것이 아니다.
오히려 삭제는 Destructive Change다.
Build/Test/Migration Gate를 거친다.
Legacy Consumer 없음
Old Workflow 없음
Old Event 없음
Rollback 필요 여부
이다.
현재 NestJS + Prisma + PostgreSQL 프로젝트에
Backward Compatibility와 Zero-Downtime Migration을 고려한
기본 운영 규칙과 검증 구조를 추가해줘.
목표는 복잡한 Schema Management Platform을 만드는 것이 아니라,
API / Event / DB Schema가 변경될 때
구버전과 신버전이 잠시 공존해도
Production이 깨지지 않도록 하는 것이다.
현재 프로젝트 구조와 실제 Migration 방식을 먼저 분석하고,
기존 기능을 과도하게 재설계하지 않는다.
1. DB Migration 변경을 다음 유형으로 분류할 수 있게 한다.
- ADDITIVE
- BACKFILL_REQUIRED
- DESTRUCTIVE
- BREAKING
예:
- Column 추가 → ADDITIVE
- 기존 데이터 변환 → BACKFILL_REQUIRED
- Column 삭제 → DESTRUCTIVE
- Type 의미 변경 → BREAKING
2. Prisma Migration을 검토할 때
다음 변경을 위험 변경으로 표시할 수 있는
간단한 검증 Script 또는 Review 절차를 만든다.
- DROP TABLE
- DROP COLUMN
- RENAME
- NOT NULL 추가
- UNIQUE 추가
- 대규모 Type 변경
자동으로 Migration을 수정하지 않는다.
3. Destructive Migration은
별도 Manual Approval 없이는
Production Gate를 통과하지 않게 할 수 있는 구조를 만든다.
4. DB Migration은
Expand → Migrate → Contract 흐름을 기본 원칙으로 한다.
5. Rename이 필요한 경우
가능하다면 다음 단계로 나누는 설계안을 제시한다.
- 신규 Column 추가
- Dual Write
- Existing Data Backfill
- Consistency Verification
- New Read Switch
- Old Path Disable
- Old Column Remove
6. Dual Write가 필요한 경우
Source of Truth를 명시할 수 있게 한다.
7. Backfill Job은
한 번의 대형 Transaction으로 처리하지 않고
Batch 방식으로 실행할 수 있게 한다.
필요 기능:
- processed count
- failed count
- resume
- pause
- batch size
8. Backfill은 Idempotent하게 설계한다.
이미 Migration이 완료된 Row는
재실행 시 안전하게 Skip할 수 있어야 한다.
9. Migration Verification 구조를 만든다.
예:
- NULL count
- old/new mismatch count
- failed row count
10. Migration 중 발생한 실패 Row를
개인정보 노출 없이 추적할 수 있게 한다.
11. 새 Read Path 전환은
가능하면 Feature Flag와 연결할 수 있게 한다.
12. New Read Path를 일부 관리자 또는 일부 Traffic에서
먼저 테스트할 수 있는 구조를 유지한다.
13. Migration 완료 직후
Old Column을 자동 삭제하지 않는다.
Contract 단계는 별도 Release와
Manual Approval 대상으로 둔다.
14. API Contract 변경 시
Additive Change를 우선한다.
예:
기존 Field 유지 + 신규 Field 추가
15. 기존 Request Field를 갑자기 필수화하지 않도록 한다.
Migration 기간에는 Optional/Fallback 정책을 고려한다.
16. Enum 값 추가가
기존 Frontend에 영향을 줄 수 있다는 것을 고려한다.
Unknown Enum을 처리할 수 있는
Frontend/Consumer 정책을 점검한다.
17. Breaking API 변경이 필요한 경우
v2 Endpoint 또는 동등한 Versioning 전략을 사용할 수 있게 한다.
18. Deprecated API에 다음 정보를 관리할 수 있게 한다.
- replacement
- deprecatedAt
- removalTarget
- usage metric
19. Event Schema에도 version을 적용한다.
Event에는 최소:
- eventType
- version
- eventId
를 유지한다.
20. Breaking Event 변경은
기존 Version을 즉시 덮어쓰지 않는다.
21. Event Migration은 다음 순서를 기본으로 한다.
- Consumer가 V1 + V2 지원
- Producer가 V2 발행
- V1 backlog/DLQ 확인
- V1 Producer 중단
- V1 Consumer 제거
22. Unsupported Event Version은
무한 Retry하지 않고
기존 DLQ 흐름으로 보낸다.
23. Event Contract Test Fixture를 만든다.
예:
test/fixtures/events/
- order-status-changed-v1.json
- order-status-changed-v2.json
24. Consumer가 기존 Event Version을
Migration 기간 동안 계속 처리할 수 있는지 테스트한다.
25. API Contract Test를 추가한다.
중요 Response Field가
의도 없이 삭제되거나 Type이 변경되는 것을
테스트로 감지할 수 있게 한다.
26. Cache Payload 구조가 바뀌는 경우를 위해
Cache Key Versioning을 적용할 수 있는 구조를 고려한다.
예:
order:v1:{id}
order:v2:{id}
27. Release Metadata에 가능하다면 다음을 연결한다.
- releaseId
- commitSha
- dbSchemaVersion
- configVersion
28. Health/Internal Version Endpoint에서
민감정보 없이 다음을 확인할 수 있게 한다.
- releaseId
- commitSha
- dbSchemaVersion
29. Deployment Gate에서
Rollback Compatibility를 판단할 수 있는
체크리스트 또는 결과를 남긴다.
예:
- SAFE
- CONDITIONAL
- UNSAFE
30. Point of No Return이 있는 Migration은
명시적으로 기록한다.
예:
Legacy Column Drop 이후
Old App Rollback 불가
31. Cleanup 대상 Legacy를 추적할 수 있게 한다.
예:
- Deprecated API
- Old Event Version
- Legacy Column
- Temporary Feature Flag
32. 실제 Usage가 0인지 확인하지 않고
자동 Cleanup하지 않는다.
33. 다음 Integration Test를 작성한다.
- Old Request → New API
- New Response 추가 Field가 Old Client Contract를 깨지 않음
- V1 Event → New Consumer
- V2 Event → New Consumer
- Unsupported Event Version
- Old App Query → Expanded Schema
- Backfill 중 중단 후 Resume
- Backfill 재실행 Idempotency
- Old/New 데이터 Mismatch 검출
- Feature Flag OFF 시 Old Read Path 복귀
34. 현재 프로젝트의 실제 Prisma Schema와 API를 먼저 분석하고,
전 시스템에 Version을 무조건 추가하지 말고
Breaking Change 가능성이 높은 핵심 영역부터 적용해줘.
현재 Git Diff와 Prisma Migration을 분석해서
Zero-Downtime Migration Plan을 작성해줘.
실제 변경사항에서 확인되는 내용만 사용하고,
Production 구조를 추정해서 사실처럼 작성하지 않는다.
다음 형식으로 작성한다.
## 변경 요약
## Breaking Change 후보
## Old App 영향
## New App 영향
## DB 영향
## Expand 단계
## Dual Write 필요 여부
## Backfill 계획
## Verification Query
## Read Switch 계획
## Feature Flag 사용 여부
## Rollback 가능 범위
## Point of No Return
## Contract / Cleanup 단계
## 배포 후 Monitoring
특히 다음 변경은 강조한다.
- DROP
- RENAME
- NOT NULL
- UNIQUE
- Type 변경
- API Field 삭제
- Required Field 추가
- Enum 변경
- Event Payload Breaking Change
Migration SQL을 자동 실행하거나
Production 데이터를 직접 수정하지 않는다.
현재 변경사항이 기존 Consumer와 호환되는지 검토해줘.
검토 대상:
1. React Frontend
2. Admin Frontend
3. NestJS API
4. Prisma Schema
5. Queue/Event Consumer
6. Webhook
7. Chrome Extension 또는 내부 자동화 Script가 있다면 해당 Consumer
각 변경에 대해:
- 기존 Contract
- 신규 Contract
- Breaking 여부
- 영향을 받는 Consumer
- Backward-Compatible하게 변경하는 방법
- 필요한 Migration 단계
- 필요한 Test
를 정리한다.
결과는 다음 Priority로 구분한다.
P0
배포 시 즉시 장애 가능
P1
구버전 Consumer 깨질 가능성
P2
Cleanup/Deprecation 필요
단순 Field 추가 등 안전한 변경은
과도하게 위험하다고 분류하지 않는다.
1001에서는:
DB 변경
↓
Outbox
↓
Event
↓
Consumer
를 안전하게 연결하는 방법을 다뤘다.
하지만 실제 운영에서는 배포할 때:
Old Producer
New Producer
Old Consumer
New Consumer
Old Frontend
New Frontend
Old App
New App
이 잠시 동시에 존재할 수 있다.
그래서 1002의 핵심은:
새 버전만 정상 동작하게 만드는 것이 아니라, 전환 기간 동안 구버전과 신버전이 함께 있어도 깨지지 않게 만드는 것
이다.
가장 중요한 기본 원칙은:
Add
↓
Migrate
↓
Verify
↓
Switch
↓
Remove
이다.
DB에서는:
Expand
→ Dual Write
→ Backfill
→ Verify
→ Read Switch
→ Contract
형태가 된다.
특히:
DROP
RENAME
NOT NULL
기존 Field 삭제
같은 Destructive Change를 처음부터 수행하지 않는다.
API에서도:
기존 Field 유지
+
신규 Field 추가
형태의 Additive Change를 우선한다.
Event도 마찬가지다.
Consumer V1+V2 지원
↓
Producer V2 전환
↓
V1 backlog 제거
↓
V1 제거
순서로 진행한다.
무중단 배포에서 중요한 것은:
New App
만 테스트하는 것이 아니다.
반드시:
Old App
+
New Schema
조합도 확인해야 한다.
그래야 문제가 생겼을 때 이전 Release로 Rollback할 수 있다.
그리고 Migration에는 어느 순간:
Point of No Return
이 생길 수 있다.
예를 들어 Old Column을 삭제한 뒤에는:
Old App Rollback
이 더 이상 안전하지 않을 수 있다.
따라서 Release 전에:
Rollback Safe
Conditional
Unsafe
를 판단해야 한다.
현재 프로젝트 규모에서는 거대한 Schema Registry나 전문 Migration Platform까지 만들 필요는 없다.
우선:
Prisma Migration Review
Additive Change 원칙
Batch Backfill
Feature Flag Read Switch
API/Event Contract Test
Old Usage 확인 후 Cleanup
정도만 적용해도 Production 안정성은 크게 올라간다.
지금까지 흐름을 연결하면:
Change
↓
Expand
↓
Backward Compatibility
↓
Deploy Old + New
↓
Backfill
↓
Verify
↓
Progressive Switch
↓
Observe
↓
Contract
↓
Cleanup
이 된다.
결국 Zero-Downtime Migration의 핵심은
“한 번에 완벽하게 바꾸는 것”이 아니라, 구버전과 신버전이 잠시 공존할 수 있는 호환 구간을 의도적으로 만들고, 실제 사용량과 데이터 일관성을 확인한 뒤 마지막에 낡은 구조를 제거하는 것
이다.