예외보다 먼저 확인한 것
커머스 판매 체인은 앞 회차의 판매 상태에 따라 다음 회차를 연다. 여기서 활성화 요청은 다음 회차를 열도록 기록하는 작업이다. 같은 이벤트가 다시 전달되거나 두 작업의 실행이 겹치면 그 요청도 겹칠 수 있었다.
중복 요청을 무시하는 것만으로 처리가 안전해지지는 않는다. 중복 활성화 충돌이 기존 거래 전체를 실패시키는지, 원 거래를 보존해야 하는지부터 구분해야 했다.
사례 글에서는 변경의 배경과 적용 범위를 설명했다. 이 글에서는 응답만 보고 놓치기 쉬운 상태와 검토 질문을 정리한다.
같은 요청 두 개로 생각해 보기
같은 활성화를 요청하는 A와 B가 있다고 가정해 보자. A를 처리한 뒤 B가 들어오는 경우와, A가 끝나기 전에 B가 들어오는 경우는 다르다. 순차 실행에서 기존 요청을 잘 찾더라도, 동시 실행에서는 조회 이후에 상태가 바뀔 수 있다.
기존 요청을 한 번 조회했다고 중복 처리가 안전한 것은 아니다. 아래는 이 차이를 설명하는 예시이며, 실제 운영 요청 로그를 재현한 것은 아니다.
| 상황 | 응답 외에 확인할 것 |
|---|---|
| A가 끝난 뒤 B가 도착 | 기존 요청을 식별하고 불필요한 추가 활성화를 만들지 않는가? |
| A와 B의 처리 시간이 겹침 | 조회 이후 상태가 바뀌거나 충돌해도 원 거래를 보존하는가? |
| 중복이 아닌 오류 발생 | 실패를 성공으로 숨기지 않고 업무상 필요한 실패 처리를 하는가? |
SQL 충돌과 업무 판단을 함께 보기
PostgreSQL의 UNIQUE 제약은 같은 키의 기록을 두 번 넣지 못하게 한다. 일반 INSERT가 그 제약에 부딪혀 오류를 내면, 같은 트랜잭션은 정상 쿼리를 계속 실행할 수 없는 상태가 된다. 프로그램의 catch에서 오류를 잡았다고 DB가 계속 진행 가능한 상태로 돌아오는 것은 아니다. 앞에서 기록한 원 회차 결과까지 함께 취소될 수 있었던 이유다.
예상 가능한 요청 키 충돌에는 ON CONFLICT DO NOTHING을 사용해 새 기록을 건너뛰게 했다. 아래는 관계를 설명하는 SQL 예시이며 실제 운영 스키마나 전체 코드는 아니다.
INSERT INTO activation_request (event_key, target_id)
VALUES ($1, $2)
ON CONFLICT (event_key) DO NOTHING
RETURNING id;
새 행이 없으면 ‘실패’나 ‘성공’으로 바로 끝내지 않고 기존 요청을 확인한다. 같은 이벤트의 재전송이면 기존 요청을 사용한다. 반면 서로 다른 이벤트가 같은 대상을 처리 중이면 그 사실을 남기고 건너뛴다. 이 예시의 요청 키 하나만으로 실제 업무의 모든 경쟁 조건을 표현한 것은 아니다. PostgreSQL 공식 설명도 이 구문을 충돌 시 삽입 대신 취할 동작으로 설명한다.
충돌 없는 INSERT와 기존 요청 조회를 나눠 봤다. 두 곳에서 답해야 하는 질문이 달랐다.
- SQL 쪽: 충돌 뒤에도 원 거래의 트랜잭션이 유지되는가?
- 업무 쪽: 기존 요청과 현재 거래 상태를 기준으로 계속 처리해도 되는가?
중복 충돌을 잡았다는 이유로 모든 예외를 무시하면, 다른 오류까지 성공처럼 보이게 할 수 있다. 반대로 예상 가능한 중복을 모두 거래 실패로 올리면 보존하려던 원 거래에 영향을 줄 수 있다. 어느 쪽이든 응답 코드 하나로는 판단하기 어렵다.
회귀 검토에 남길 질문
정상 응답뿐 아니라 실패 뒤의 거래 상태도 검증 대상으로 삼았다. 다음 질문은 같은 관점을 다른 변경에도 적용하기 위해 남겼다.
- 같은 요청을 다시 보내면 어떤 기존 결과를 찾는가?
- 요청 순서가 바뀌어도 원 거래의 보존 조건은 같은가?
- 조회와 처리 사이에 상태가 바뀌면 어떻게 판단하는가?
- 중복이 아닌 오류는 실패로 구분되어 남는가?
확인한 범위
2026년 7월 변경은 개발 환경 검증과 운영 배치 적용까지 확인했다. 9월 경쟁 상태 관련 작업은 구현과 테스트 보완 범위다. 두 작업을 같은 배포 성과로 묶거나 장애 감소율로 표현하지 않았다.
오류를 없앴는지만 보지 않고, 그 뒤에 보존해야 할 거래가 남았는지 확인했다.