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

JOIN으로 늘어난 행마다 Report ID가 증가한 문제

JOIN으로 성적서 대상 행이 늘어나자 Java 반복문이 행마다 번호를 증가시켜 Report ID가 건너뛰었습니다. 발급 대상과 연결 주문을 별도 쿼리로 조회해 해결한 사례입니다.

Oracle SQLJoin CardinalityID GenerationJavaData Modeling

ID 생성기보다 먼저 입력 행 수를 확인했습니다

성적서 Report ID가 이전 번호에서 +1이 아니라 +N씩 건너뛰는 현상이 있었습니다. 번호가 비연속적으로 보이면 DB 시퀀스, 동시성 또는 채번 테이블의 충돌을 먼저 의심하기 쉽습니다.

하지만 이 사례에서 채번 값 자체는 요청받은 횟수만큼 정상적으로 증가하고 있었습니다. 문제는 한 건의 성적서를 만들면서 채번 로직이 몇 번 호출됐는가였습니다.

한 Job에 여러 주문이 연결될 수 있게 바뀌면서, 성적서 생성 대상 조회가 같은 샘플을 주문 수만큼 반환했습니다. Java 코드는 조회 행마다 현재 Report 번호를 증가시켰고, JOIN 결과 행 수가 그대로 ID 증가 폭이 됐습니다.

1:N JOIN이 처리 단위를 바꿨습니다

공개용으로 단순화한 관계는 다음과 같습니다.

아래 도식과 코드는 실제 이름을 드러내지 않도록 핵심 동작만 단순화했습니다.

BUSINESS_JOB
 └─ DOCUMENT_TARGET  1건: 문서 생성 대상
 └─ ORDER_ITEM       N건: 같은 작업에 연결된 주문 정보

성적서 생성 대상과 주문 표시 정보를 한 쿼리에서 가져오기 위해 두 영역을 JOIN하면 결과는 다음처럼 늘어납니다.

1:N JOIN이 처리 단위를 바꿨습니다 데이터 표
업무 키문서 대상 키주문 키성적서 처리 단위
작업 A대상 1주문 가대상 1
작업 A대상 1주문 나대상 1
작업 A대상 1주문 다대상 1

업무적으로는 성적서 한 건이지만 SQL 결과는 세 행입니다. Java가 행을 곧 처리 단위라고 가정하면 성적서 생성도 세 번 시도됩니다.

for (DocumentRow row : documentRows) {
    String documentKey = prefix + nextNumber++;
    createDocument(row, documentKey);
}

실제 저장 단계에서 한 건만 남거나 이후 로직이 중복을 걸러도, nextNumber++는 이미 세 번 실행됩니다. 그래서 다음 성적서는 +3부터 시작합니다.

발급 대상과 연결 주문을 따로 조회했습니다

증상만 보면 DISTINCT를 추가해 같은 샘플 행을 한 건으로 줄일 수 있습니다. 그러나 주문별로 다른 컬럼을 함께 조회하면 완전히 같은 행이 아니므로 DISTINCT로 중복을 제거하지 못할 수 있고, 필요한 주문 정보까지 잃을 수 있습니다.

Java에서 업무 대상 키를 Set으로 중복 제거하는 방법도 있습니다. 이 방식은 쿼리가 어떤 단위의 데이터를 반환하는지 모호한 상태를 유지한 채, 호출부가 우연히 중복을 보정하게 만듭니다.

근본 문제는 서로 다른 두 종류의 데이터를 한 쿼리에서 조회했다는 점이었습니다.

  • Report ID를 발급할 고유 성적서 대상 조회
  • 성적서에 표시하거나 후속 처리할 연결 주문정보 조회

두 조회를 분리하고 Report 생성은 샘플 단위 대상만 순회하도록 바꿨습니다. 주문정보는 해당 성적서를 구성할 때 별도로 조회했습니다.

reportTargets = 성적서 발급 대상 샘플 조회

for target in reportTargets:
    reportId = 다음 번호 1회 발급
    orders = target에 연결된 주문정보 조회
    report = target + orders로 성적서 구성
    report 저장

이렇게 하면 주문이 한 건이든 여러 건이든 Report ID 증가 횟수는 성적서 발급 대상 수와 일치합니다.

SQL의 행과 업무의 한 건은 같지 않습니다

SQL 결과 행이 언제나 애플리케이션의 처리 단위와 같지는 않습니다.

1:N 관계를 JOIN하면 부모 데이터가 자식 수만큼 반복되는 것은 정상적인 관계형 결과입니다. 애플리케이션이 SQL 한 행을 어떤 업무 단위로 볼지 명확하지 않았던 것이 문제였습니다.

따라서 쿼리를 검토할 때는 컬럼과 조건뿐 아니라 다음 질문이 필요합니다.

  • 이 쿼리는 한 행을 무엇으로 정의합니까?
  • Java 반복문은 한 행마다 어떤 부수효과를 실행합니까?
  • 채번, 파일 생성, 외부 전송처럼 되돌리기 어려운 처리가 행 수에 묶여 있습니까?
  • 1:N JOIN이 추가됐을 때 기존 반복문의 의미가 바뀝니까?

검증 방법

원인을 확정할 때 한 Job에 여러 주문이 연결된 조건을 만들고, 세 가지 수를 함께 비교했습니다.

  • SQL이 반환한 전체 행 수
  • 중복을 제거한 성적서 발급 대상 수
  • Java 반복문에서 Report 번호를 증가시킨 횟수

개선 후에는 ISO Report 생성과 Job 시작 내부 로직을 함께 확인했습니다.

  1. 주문 한 건이 연결된 기존 Job에서 Report ID가 한 번 증가하는지
  2. 복수 주문이 연결된 Job에서도 샘플당 한 번만 증가하는지
  3. 분리 조회한 주문정보가 성적서 구성에서 누락되지 않는지
  4. 기존 ISO Job 시작 흐름과 성적서 생성 결과가 유지되는지

SQL과 서비스 구현을 함께 변경하고 테스트한 뒤 운영에 반영했습니다.

재발급 이력 문제와는 다른 축입니다

이 사례는 최초 Report ID를 몇 번 발급하느냐의 문제입니다. 발급된 문서의 원본·재발급·폐기 관계를 어떻게 보존할지는 별도의 문서 생명주기 문제이며, 재발급 성적서를 덮어쓰지 않고 발행 순번으로 관리하기에서 따로 다룹니다.

한 번 발급할 대상을 세는 문제와 발급 후 이력을 관리하는 문제를 분리해야, 서로 다른 문제를 하나의 채번 로직으로 억지로 해결하지 않게 됩니다.

남는 조건

이 처리 방식은 “하나의 샘플이 하나의 성적서 발급 대상”이라는 당시 업무 규칙을 기준으로 합니다. 향후 주문별로 독립 성적서를 발급해야 한다면 처리 키와 채번 단위도 다시 정의해야 합니다.

번호를 증가시키는 코드 앞에서 어떤 조회가 실행되고, 그 JOIN이 업무 한 건을 몇 행으로 펼치는지까지 따라가야 합니다. 이 문제는 SQL 카디널리티와 반복문의 부수효과에서 시작됐습니다.