트랜잭션·데이터 정합성 · Data Integrity

목록 행을 전체 주문으로 믿었을 때: 일괄 접수에서 데이터가 누락된 이유

분산된 미수주문 접수 로직을 서버에서 통합하는 과정에서 화면의 부분 객체를 전체 주문 데이터처럼 사용해 기존 업무값이 누락된 원인과, 현재 데이터를 다시 조회하도록 저장 경계를 고친 과정을 다룹니다.

JavaSpringData IntegrityRegression TestingLegacy System

문제의 출발점

2022년, CES의 미수주문 접수 로직은 서비스와 주문 상태에 따라 여러 흐름으로 나뉘어 있었다. 접수 조건과 완료 처리를 서버의 한 진입점으로 모으면 중복을 줄이고 규칙을 일관되게 적용할 수 있다고 판단했다. 통합 방향 자체는 타당했지만, 새 진입점과 기존 저장 메서드 사이의 데이터 계약을 충분히 확인하지 못했다.

배포 후 한 번의 일괄 접수에 포함된 32건에서, 접수 상태는 정상적으로 변경됐지만 저장 시 유지돼야 할 업무값 일부가 누락된 사실을 확인했다. 같은 기간 단건 접수 경로는 영향을 받지 않았다. 당시에는 이를 ‘필수값 누락’이라고 표현했지만, 정확히는 데이터베이스의 필수 제약만의 문제가 아니었다. 부가 서비스, 샘플 관련 옵션, 과금·배송·결과 수신 설정처럼 주문 처리 과정에서 그대로 보존돼야 하는 값이 빈 값으로 덮이는 문제였다.

통합 자체보다 통합 경계의 데이터 계약이 문제였다

일괄 접수 화면은 여러 주문을 표 형태로 보여 줬다. 서버로 전달된 각 행에는 주문 식별자와 화면 처리에 필요한 일부 값만 있었다. 그런데 통합 로직은 이 행 객체를 검증한 뒤, 기존의 접수 완료 메서드에 그대로 넘겼다.

for (ReceiptRow row : gridRows) {
    validateReceipt(row);
    completeByOrderRule(row);
}

문제는 기존 완료 메서드의 숨은 전제였다. 이 메서드는 전달받은 객체가 ‘화면의 한 행’이 아니라, 주문의 저장 대상 값이 충분히 채워진 전체 스냅샷이라고 가정했다. 부분 객체에 존재하지 않는 필드는 null이나 빈 값인 채로 저장 구문에 사용됐고, 그 결과 접수와 무관한 기존 값까지 덮어쓸 수 있었다.

화면에 숨은 필드를 더 추가해 모든 값을 함께 보내는 방식도 검토할 수 있다. 그러나 이 방식은 화면 컬럼과 저장 규칙을 강하게 결합한다. 새 필드가 생길 때마다 화면과 서버를 함께 수정해야 하고, 클라이언트가 보낸 값을 현재의 정합한 데이터라고 신뢰하게 된다. 화면 목록은 조회용 투영 데이터이지, 도메인 전체를 보존하는 계약이 될 수 없었다.

복구와 재발 차단을 분리했다

문제를 확인한 당일에는 먼저 일괄 접수 경로에서 같은 현상이 더 생기지 않도록 조치했다. 영향 범위를 특정한 뒤 변경 전 시점의 데이터와 대조해 32건의 값을 복원했다. 후속 수정에서는 클라이언트가 보낸 행을 저장 객체로 사용하지 않고, 주문 식별자만 명령으로 받아 서버에서 현재의 전체 주문 정보를 다시 조회하도록 바꿨다.

for (ReceiptCommand command : commands) {
    OrderSnapshot current =
        orderRepository.findReceiptDetail(command.orderId());

    validateReceipt(current);
    completeByOrderRule(current);
}

이제 일괄 접수 명령은 ‘어떤 주문을 접수할 것인가’만 전달한다. 저장에 필요한 기존 값은 서버가 현재 데이터에서 구성한다. 화면에 저장용 숨은 필드를 계속 늘릴 필요도 없어졌고, 접수 완료 메서드가 기대하는 입력과 실제로 받는 입력이 일치하게 됐다.

통과한 테스트가 놓친 것

배포 전에는 여러 서비스 유형에서 접수 상태와 다음 단계가 올바르게 변경되는지 확인했다. 하지만 검증의 중심이 ‘프로세스가 진행되는가’에 있었고, ‘이번 변경과 관계없는 기존 값이 그대로 남는가’라는 불변 조건은 포함하지 않았다. 상태 전이 테스트가 통과했어도 데이터 보존 문제를 발견하지 못한 이유다.

이후 회귀 테스트의 기준을 다음과 같이 바꿨다.

  • 단건 접수와 일괄 접수의 결과를 비교한다.
  • 접수 상태뿐 아니라 부가 서비스, 샘플, 과금·배송, 결과 수신 설정의 변경 전후 값을 비교한다.
  • 한 건이 아니라 서로 다른 조건의 여러 주문을 한 번에 처리한다.
  • 명령에 포함되지 않은 필드는 저장 전후에 같아야 한다는 불변 조건을 검증한다.

경계의 데이터 계약이 남긴 교훈

이번 경험을 통해 로직 통합은 코드 줄 수를 줄이는 작업이 아니라, 경계마다 데이터의 의미를 다시 정의하는 작업이라는 점을 배웠다. ReceiptCommandOrderSnapshot을 구분하고, 저장 메서드가 변경할 필드를 명시하면 부분 객체가 전체 엔티티처럼 사용되는 일을 막을 수 있다. 특히 레거시 시스템에서는 기존 메서드의 시그니처보다 그 안에 숨어 있는 전제부터 확인해야 안전하게 통합할 수 있다.

공개 안내: 이 글은 업무 시스템의 실제 식별자, 테이블·컬럼명, 클래스·메서드명과 원본 코드를 일반화한 회고입니다.