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

JOIN 결과 행 수가 Report ID 증가폭이 되어버린 이유

한 Job에 여러 주문이 연결되면서 성적서 대상 행이 중복되고, Java 반복문의 채번 증가가 행 수만큼 실행돼 Report ID가 +N씩 건너뛴 원인을 조회 책임 분리로 해결한 과정입니다.

Oracle SQLJoin CardinalityID GenerationJavaData Modeling

문제의 출발점

실제 테이블명, Report ID 형식과 주문 데이터는 공개용 이름으로 바꿨습니다. 이 글은 2023년에 실제 반영된 ISO17025 성적서 생성 로직 개선 사례입니다.

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

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

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

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

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

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

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

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

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

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

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

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

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

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

중복 제거보다 Query의 책임을 나눴다

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

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

근본 문제는 한 Query가 서로 다른 두 책임을 동시에 가졌다는 점이었다.

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

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

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

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

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

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

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

이 장애가 보여준 핵심은 SQL 결과 행이 언제나 애플리케이션의 처리 단위는 아니라는 점이다.

1:N 관계를 JOIN하면 부모 데이터가 자식 수만큼 반복되는 것은 정상적인 관계형 결과다. SQL이 틀린 것이 아니라, 반복된 행을 어떤 단위로 소비할지 애플리케이션 계약이 정의되지 않았던 것이 문제였다.

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

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

검증 방법

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

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

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

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

SQL과 Service 구현이 함께 변경된 이력, 테스트와 실제 반영 완료 상태를 대조해 분석 문서에만 남은 개선안이 아니라 운영에 반영된 수정임을 확인했다.

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

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

최초 발급의 처리 단위와 발급 이후의 버전 단위를 분리해야 두 문제를 하나의 채번 로직으로 억지로 해결하지 않게 된다.

남는 조건

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

ID 간격이 벌어졌다고 해서 곧바로 채번기의 결함으로 결론 내릴 수는 없다. ID를 증가시키는 코드 앞에 어떤 Query가 있고, 그 Query의 JOIN이 업무 한 건을 몇 행으로 펼치는지까지 따라가야 한다. 채번 문제처럼 보였던 현상을 데이터 카디널리티와 반복문의 부수효과 문제로 다시 정의한 것이 이 개선의 핵심이었다.