발급 시각은 새 값인데 만료 시각은 과거 쿠폰과 같은 행이 생겼다고 하자. 새 정책으로 발급한 것일 수도 있지만 사용했던 행을 복제했을 가능성도 조사해야 한다. Java 발급 메서드에서 중복 경로를 못 찾았다는 사실만으로 다른 INSERT까지 배제할 수는 없다.

과거 코드 조사에서는 Java 정상 발급이 기존 이력을 건너뛰고 스케줄러는 재발급하지 않는 경로와, 레거시 Python 정산 제외 처리가 사용 쿠폰을 새 발급 행으로 복제하는 경로가 구분됐다. 후속 수정은 새 행 생성 대신 기존 쿠폰 복원 방향이었다. 코드의 쓰기 경로를 찾은 뒤에는 운영 DB의 제약과 기존 중복, 배포된 버전에서도 같은 규칙이 지켜지는지 확인할 차례다.

이 글은 누가 같은 테이블에 어떤 권리를 쓰는가를 행의 단서부터 복원하는 방법을 설명한다.

생성 규칙이 다른 컬럼을 나란히 본다

비교할 값좁힐 수 있는 가설
새 발급 시각과 과거 만료 시각원본 행 복제 여부
정산·보정 같은 사유인접 도메인의 복원 경로
과거 쿠폰·정산 참조새 요청보다 이전 거래에 기반한 쓰기
작성 주체·요청 식별자서비스·배치·관리 경로 구분

이 지문은 작성자를 확정하는 증거가 아니다. 테이블명·ORM 모델·raw SQL·상태·메모 문구로 검색할 범위를 좁힌다. Java뿐 아니라 레거시 서비스, 관리자 API, 취소·통합·정산 보상, 배치, 일회성 보정 SQL을 각각 목록으로 만든다.

현재 실행 중인 writer는 pg_stat_activity의 연결 사용자·application_name·client 주소·query를 참고할 수 있다. 다만 이미 끝난 거래를 복원하는 감사 로그는 아니다. PostgreSQL 통계 문서의 관측 범위를 구분한다.

Java 발급 API, 레거시 서비스, 관리·보정 경로가 같은 쿠폰 테이블을 직접 쓰는 구조

한 경로의 잠금에 다른 writer가 참여하지 않으면 전체 발급 규칙은 직렬화되지 않는다. 도식의 각 경로가 실제로 존재하는지 코드·배포·DB 계정으로 확인한다.

복원과 새 발급을 다른 명령으로 정의한다

정산 제외 뒤 쿠폰을 다시 쓸 수 있게 한다는 요구에도 두 선택이 있다. 기존 권리를 복원하거나 새 권리를 만들 수 있다. 원본 관계, 만료, 회수 상태와 동일 요청 재시도의 결과가 다르다.

Settlement Service
  → requestCouponRestore(settlementExclusionId, originalGrantId)
  → Coupon Domain
      정책 검사 → 중복 요청 검사 → 상태 전이 → 감사 기록

이것은 쓰기 책임을 모으는 설계안이다. 서비스 분리가 어렵다면 공유 명령·DB 절차·제한된 권한으로 통로를 좁힐 수 있다. 이름만 restore로 바꾸는 대신 어떤 권리가 몇 번 생성돼야 하는지 정책을 정의한다.

권리의 키와 요청의 키를 분리한다

같은 회원이 다음 캠페인 회차에 같은 쿠폰을 받을 수 있다면 member_id + coupon_id의 유일성이 항상 맞지는 않는다.

CREATE TABLE coupon_grant (
    id bigint PRIMARY KEY,
    member_id bigint NOT NULL,
    coupon_policy_id bigint NOT NULL,
    entitlement_key varchar(100) NOT NULL,
    status varchar(30) NOT NULL,
    predecessor_id bigint REFERENCES coupon_grant(id),
    source_operation varchar(50) NOT NULL,
    source_request_id varchar(100) NOT NULL,
    issued_at timestamptz NOT NULL,
    expires_at timestamptz NOT NULL,
    UNIQUE (member_id, coupon_policy_id, entitlement_key),
    UNIQUE (source_operation, source_request_id)
);

첫 키는 어떤 사건에서 권리가 한 번 생기는지, 둘째 키는 같은 명령의 재전송인지를 표현하는 설계 예시다. 서비스별로 요청 ID 공간이 다르면 서비스 식별자도 업무 키에 포함해야 한다. 적용할 때는 실제 발급·복원 정책에 맞춰 키의 범위를 정한다.

PostgreSQL 제약조건 문서는 복합 UNIQUE의 유일성을 설명한다. 애플리케이션의 사전 조회와 달리 동시 쓰기도 DB 제약의 판정을 받는다. 유효한 후속 발급까지 막지 않도록 원본·후속 권리와 상태 이력을 정의한 뒤 제약을 고른다.

모든 writer가 참여하는 검증으로 닫는다

공통 업무 키·DB 제약, 명령의 멱등성, 쓰기 책임 집중은 서로 보완한다. provenance에는 서비스·작업·요청·actor를 남길 수 있다. 실제 저장 여부와 로그 보존 범위는 따로 확인한다.

한 API의 단위 테스트만으로 전체 중복 방지를 확인하지 않는다. 정상 발급 뒤 정산 제외 재시도, 응답 유실, Java와 레거시의 동시 쓰기, 다른 유효 캠페인을 각각 검사한다. 기대 결과도 동일 grant, 한 번의 복원, 별도 회차 허용처럼 정책별로 정한다.

기존 중복은 사용·정산·외래키 이력을 비교한 뒤 보존·병합·회수를 판단한다. 큰 ID를 일괄 삭제하는 방식은 사용 이력을 끊을 수 있다. 다음 조사에서는 수정 코드 확인과 운영 DB 제약·배포·기존 데이터 확인을 분리해 숨은 쓰기 경로가 닫혔는지 판단한다.