이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • event schema 변경이 API DTO 변경보다 위험한 이유는 무엇인가?
  • backward compatibility와 forward compatibility를 event consumer 관점에서 어떻게 판단하는가?
  • 필드 추가, 필드 삭제, 의미 변경은 각각 어떤 배포 절차가 필요한가?
  • 과거 event를 replay할 때 versioning이 왜 필요한가?

개요

event는 한 번 발행되면 사라지지 않는다.

새로운 consumer가 과거 event를 replay할 수 있고, 오래된 consumer가 새로운 event를 받을 수도 있다.

그래서 event schema는 마음대로 바꾸면 안 된다.

새 필드 추가는 비교적 안전할 수 있지만, 필드 삭제와 의미 변경은 consumer와 replay를 깨뜨린다.

원리

compatibility는 producer와 consumer가 서로 다른 시점에 배포될 수 있다는 전제에서 나온다.

producer가 먼저 배포될 수도 있고, consumer가 먼저 배포될 수도 있다.

event log에는 여러 version의 event가 함께 남을 수 있다.

따라서 schema 변경은 “현재 코드가 컴파일되는가”보다 “과거와 미래 event를 모두 해석할 수 있는가”로 판단한다.

변경 유형

변경위험도기준
optional field 추가낮음consumer가 모르는 필드를 무시하면 안전
required field 추가높음과거 event에는 값이 없음
field rename높음consumer는 삭제와 추가로 인식
field 삭제높음기존 consumer가 읽을 수 없음
의미 변경매우 높음이름은 같지만 지표가 달라짐
event type 분리중간consumer routing과 문서 필요

가장 위험한 변경은 이름은 그대로 두고 의미를 바꾸는 것이다.

예를 들어 paidAmount가 gross amount에서 net amount로 바뀌면 과거 리포트와 consumer logic이 동시에 흔들린다.

호환적 추가 예시

{
  "eventType": "OrderPaid",
  "eventVersion": 1,
  "orderId": 10042,
  "paymentId": "pay-555",
  "paidAmount": "49000.00"
}
{
  "eventType": "OrderPaid",
  "eventVersion": 1,
  "orderId": 10042,
  "paymentId": "pay-555",
  "paidAmount": "49000.00",
  "couponDiscountAmount": "3000.00"
}

새 필드가 optional이고 consumer가 모르는 필드를 무시하면 비교적 안전하다.

하지만 이 필드가 없으면 계산이 틀리는 consumer가 생긴다면 required field 변경으로 봐야 한다.

새 version이 필요한 경우

{
  "eventType": "OrderPaid",
  "eventVersion": 2,
  "orderId": 10042,
  "paymentId": "pay-555",
  "grossAmount": "52000.00",
  "discountAmount": "3000.00",
  "netPaidAmount": "49000.00"
}

금액 의미가 바뀌면 새 version으로 분리하는 편이 안전하다.

consumer는 version별 parser나 adapter를 둔다.

public OrderPaidPayload parse(JsonNode node) {
    int version = node.path("eventVersion").asInt(1);
    return switch (version) {
        case 1 -> parseV1(node);
        case 2 -> parseV2(node);
        default -> throw new UnsupportedEventVersionException(version);
    };
}

unsupported version을 조용히 무시하면 누락이 된다.

명시적으로 실패시키고 DLQ나 알림으로 보내야 한다.

배포 순서

호환 변경은 보통 consumer가 먼저 넓게 읽고 producer가 나중에 쓰는 순서가 안전하다.

1. consumer가 optional field와 새 version을 읽을 수 있게 배포
2. producer가 새 field 또는 새 version을 발행
3. version 분포와 consumer 오류를 모니터링
4. 오래된 version 발행 중단
5. 보관 기간과 replay 요구를 확인한 뒤 legacy parser 제거

이 순서가 없으면 한 팀의 배포가 다른 팀의 consumer를 깨뜨릴 수 있다.

Replay와 version

replay는 과거 event를 다시 읽는 작업이다.

과거 event는 현재 schema와 다를 수 있다.

따라서 replay 가능한 consumer는 과거 version을 해석할 수 있어야 한다.

만약 legacy parser를 제거했다면 보관된 event를 다시 처리할 수 없다.

판단 축

  • 필드 이름 변경은 삭제와 추가로 본다.
  • 의미 변경은 같은 이름으로 처리하지 않는다.
  • 새 required field는 과거 event 처리 방법을 함께 정한다.
  • consumer가 모르는 필드를 무시할 수 있는지 확인한다.
  • version 제거는 보관 기간과 replay 요구를 확인한 뒤 결정한다.

실패 모델

  • producer가 paidAmount 의미를 바꿨지만 version을 올리지 않는다.
  • required field를 추가해 과거 event replay가 실패한다.
  • consumer가 unknown field에서 역직렬화 실패를 낸다.
  • DLQ에 unsupported version이 쌓이는데 알림이 없다.
  • schema registry는 통과했지만 business meaning 변경을 리뷰하지 않는다.

운영 지표

  • event_version_distribution: version별 발행 비율이다.
  • unsupported_version_count: consumer가 모르는 version 수다.
  • deserialization_failure_count: schema 호환성 실패 신호다.
  • legacy_version_replay_count: 과거 version을 다시 처리한 횟수다.
  • schema_change_incident_count: schema 변경으로 발생한 장애 수다.

개인 프로젝트 기준

개인 프로젝트에서도 version 필드는 넣어 두는 편이 좋다.

처음에는 v1만 있어도 된다.

하지만 payload 의미를 바꾸는 순간 v2를 만들고, v1 event를 읽는 테스트를 남긴다.

기업 운영 기준

기업에서는 schema 변경 PR에 producer와 consumer owner가 함께 참여해야 한다.

schema registry의 compatibility check는 구조를 도와줄 뿐 business meaning을 검증하지 못한다.

지표 의미가 바뀌면 데이터 팀과 제품 owner까지 리뷰해야 한다.

실전 팁

  • optional 추가도 consumer가 실제로 무시 가능한지 테스트한다.
  • 필드 rename 대신 새 필드 추가 후 점진적으로 migration한다.
  • event version과 producer app version을 혼동하지 않는다.
  • DLQ에는 event type, version, consumer 이름을 남긴다.
  • replay 테스트에는 가장 오래된 보관 event 샘플을 포함한다.

위험 신호!

  • “consumer가 지금 하나뿐”이라는 이유로 schema를 바로 바꾼다.
  • event version은 있는데 parser가 version을 사용하지 않는다.
  • field 의미 변경이 문서 없이 배포된다.
  • old version 제거 기준이 보관 기간과 연결되어 있지 않다.
  • schema registry 통과만으로 안전하다고 판단한다.

확인 질문

확인 질문

  • event field rename이 위험한 이유는 무엇인가?
    • consumer 입장에서는 기존 field 삭제와 새 field 추가로 보이며 과거 event replay도 깨질 수 있다.
  • 의미 변경은 왜 새 version이나 새 event type으로 분리해야 하는가?
    • 같은 이름의 field가 과거와 현재에서 다른 값을 뜻하면 지표와 consumer logic이 조용히 틀어진다.
  • legacy parser를 제거하기 전에 무엇을 확인해야 하는가?
    • 과거 event 보관 기간, replay 요구, version 분포, downstream consumer 의존성을 확인해야 한다.

참고 문서