트랜잭션·데이터 정합성 · Java Design

조회 메서드가 인자를 바꾼다면: 반복 처리에서 첫 번째 고객 값이 재사용된 이유

고객 이메일 조회 메서드의 숨은 입력값 변경이 반복 처리와 결합해 이전 고객의 상태를 다음 주문에 남긴 원인과, 영향 범위를 줄인 수정 및 장기적인 무부수효과 설계를 다룹니다.

JavaSide EffectMutable ObjectBatch ProcessingRegression Testing

문제와 확인 범위

2024년 바코드 주문등록 프로세스를 개선하던 중, 서로 다른 고객의 주문을 연속으로 처리하면 두 번째 고객부터 추가 수신 이메일 값이 잘못 설정될 수 있는 문제를 확인했다. 첫 번째 고객의 대표 이메일이 이후 고객의 주문 데이터에도 남을 수 있는 형태였다.

확인된 범위는 주문 데이터의 추가 수신 이메일 값이 잘못 설정될 가능성이었다. 잘못 설정된 주소로 실제 결과가 전송됐거나 개인정보 사고가 발생했다고 단정할 근거는 없었기 때문에, 현상과 확인되지 않은 영향은 구분해서 다뤘다.

문제는 조회 결과가 아니라 입력 객체에 남은 상태였다

처음에는 고객별 이메일 조회 조건이나 원천 데이터를 의심했다. 하지만 고객을 한 건씩 처리하면 정상이고, 서로 다른 고객을 연속으로 처리할 때만 문제가 나타났다.

원인은 조회 메서드의 숨은 부수효과였다. 이 메서드는 전달받은 객체를 조회 조건으로만 읽지 않고, 고객 식별 값이 비어 있으면 내부에서 값을 찾아 같은 객체에 다시 설정했다.

Email findCustomerEmail(Context context) {
    if (context.customerId().isEmpty()) {
        context.setCustomerId(findCustomerId(context));
    }

    return findEmail(context.customerId());
}

호출부에서는 하나의 변경 가능한 객체를 반복 처리 과정에서 재사용하고 있었다.

for (Order order : orders) {
    context.apply(order);

    Email email = findCustomerEmail(context);
    order.setAdditionalEmail(email);
}

첫 번째 주문을 처리하면서 객체에 고객 식별 값이 채워지면, 다음 주문에서도 그 값이 남았다. 두 번째 주문이 다른 고객의 것이어도 조회 메서드는 고객 값이 이미 존재한다고 판단할 수 있었고, 결국 첫 번째 고객을 기준으로 이메일을 조회하게 됐다.

메서드 이름만 보면 단순한 조회처럼 보이지만 실제 계약은 ‘필요하면 입력 객체의 상태도 바꾼다’였다. 호출자가 알기 어려운 계약이 반복 처리와 결합하면서 주문 사이의 상태가 섞인 것이다.

영향 범위를 줄인 우선 조치

문제가 확인된 시점에는 같은 조회 메서드를 사용하는 다른 흐름도 있었다. 메서드 자체의 동작을 즉시 바꾸면 기존 호출부에 예상하지 못한 영향을 줄 수 있었다. 우선 문제가 발생한 호출부에서 이메일을 조회하기 직전에 고객 식별 값을 초기화했다.

for (Order order : orders) {
    context.apply(order);
    context.clearCustomerId();

    Email email = findCustomerEmail(context);
    order.setAdditionalEmail(email);
}

변경 범위를 제한하면서 각 반복이 현재 주문을 기준으로 고객을 다시 결정하게 한 조치다. 운영 안정화를 위한 현실적인 선택이었지만, 호출자가 매번 초기화를 기억해야 한다는 점에서 구조적인 해결은 아니었다.

회귀 테스트는 서로 다른 고객의 연속 처리로 구성했다

한 고객만 처리하는 테스트로는 이 문제를 재현할 수 없었다. 그래서 최소 두 명 이상의 서로 다른 고객에게 바코드 주문을 등록하고, 두 번째 이후 고객의 추가 수신 이메일에 첫 번째 고객의 대표 이메일이 지정되지 않는지 확인했다.

검증 질문도 ‘이메일 조회가 성공했는가’에서 ‘앞선 반복의 상태가 다음 반복에 남지 않았는가’로 바꿨다. 반복 처리에서는 각 건의 결과뿐 아니라 건과 건 사이의 상태 격리가 중요한 테스트 조건이다.

단기 수정과 장기 설계를 구분하기

장기적으로는 조회 메서드가 전달받은 객체를 변경하지 않도록 만드는 편이 안전하다. 조회에 필요한 값을 불변 입력으로 전달하고, 계산된 값은 반환값으로만 돌려주면 호출자가 숨은 상태 변화를 추적할 필요가 없다.

CustomerEmailQuery query =
    new CustomerEmailQuery(order.customerKey());

Email email = customerEmailReader.find(query);

주문 변경 명령과 고객 정보 조회를 분리하면 메서드의 책임과 데이터 흐름도 명확해진다. 입력 객체를 공유해야 한다면 반복마다 새 객체를 생성하거나 복사해 상태 격리를 보장할 수 있다.

이 결함의 수정은 같은 달 후반에 진행된 더 큰 범위의 주문등록 구조 개편보다 먼저 반영된 별도 안정화 작업이었다. 따라서 이후의 구조 개편이 이 문제를 만들었다거나, 개편 뒤 같은 문제가 다시 발생했다고 해석해서는 안 된다.

같은 전환 기간에 별도로 진행한 서비스·바코드 유형 검증, 요청번호별 중복 방지와 결과 수신자 범위 보완은 바코드 발행 주체가 바뀌었을 때: SAP 연동 주문등록의 정합성을 지킨 방법에서 선후관계와 역할을 나눠 정리했다.

반복 처리에서 확인해야 할 것

이번 경험에서 얻은 가장 큰 교훈은 변경 가능한 파라미터도 사실상 공유 상태가 될 수 있다는 점이다. 특히 조회 메서드가 입력값을 변경하면 단건 처리에서는 드러나지 않던 문제가 반복·배치 처리에서 나타난다. 메서드 이름과 실제 부수효과를 일치시키고, 반복 간 상태가 격리되는지를 회귀 테스트로 확인해야 같은 유형의 문제를 예방할 수 있다.

공개 안내: 이 글은 업무 시스템의 실제 고객 정보, 클래스·메서드·필드명과 원본 코드를 일반화한 회고입니다.

TECHNICAL SERIES

SAP 연동 주문등록 정합성

바코드 발행 경계가 바뀐 주문등록 흐름과 반복 처리에서 발견한 숨은 상태 변경을 함께 살펴봅니다.

시리즈 전체 보기

현재 2/2

  1. 1바코드 발행 주체가 바뀌었을 때: SAP 연동 주문등록의 정합성을 지킨 방법
  2. 2조회 메서드가 인자를 바꾼다면: 반복 처리에서 첫 번째 고객 값이 재사용된 이유