본문 바로가기
Programming & Platform/SQL

멱등(Idempotent) 처리와 UPSERT - 중복 요청에도 안전한 데이터 쓰기 완성

by 코드스니펫 2025. 12. 14.
반응형

온라인 결제 버튼을 한 번만 눌렀다고 생각했는데, 인터넷이 끊겨 다시 눌러야 했던 경험이 있습니다.

 

그런데 영수증은 하나만 나와야 합니다.

 

이렇게 “같은 요청이 여러 번 들어와도 결과는 한 번 수행한 것과 같아야 한다”는 원리를 멱등이라 합니다.

 

개발에서는 이 멱등을 데이터베이스의 UPSERT(존재하면 업데이트, 없으면 삽입)와 결합해 중복 결제, 중복 주문, 중복 포인트 적립을 막습니다.

 

이 글은 비전공자도 이해할 수 있는 비유부터 시작해, 개발자가 바로 적용할 수 있는 SQL 패턴과 시스템 설계 팁까지 한 번에 정리합니다.

 

멱등(Idempotent) 처리와 UPSERT - 중복 요청에도 안전한 데이터 쓰기 완성

 

멱등은 왜 중요한가

 

실생활 비유로 이해하는 멱등

엘리베이터 호출 버튼을 여러 번 눌러도 엘리베이터는 한 번만 옵니다.

 

호출 상태라는 “결과”가 같기 때문입니다.

 

온라인 시스템에서도 결과가 한 번만 반영되도록 만들어야 합니다.

 

버튼이 중복 클릭되거나 통신이 끊겨 재시도되더라도, 최종 결과가 “한 번 처리”와 동일해야 고객과 회계, 데이터가 모두 안전합니다.

 

멱등(Idempotent) 처리와 UPSERT
멱등(Idempotent) 처리와 UPSERT

 

개발 관점에서의 정의와 효과

멱등은 같은 입력을 여러 번 적용해도 시스템 상태가 변하지 않는 성질입니다.

 

네트워크 오류로 클라이언트·서버가 재시도해도 DB는 단 한 번만 반영됩니다.

 

이를 위해 고유한 업무 키(자연키·복합키) 혹은 Idempotency Key를 정의하고, UPSERT로 단일 트랜잭션에서 충돌을 흡수합니다.

 

결과적으로 재시도 정책을 과감하게 적용할 수 있고, 분산 환경에서 데이터 정합성이 눈에 띄게 좋아집니다.

 

개발자에게 도움 되는 사이트 TOP 5 - 활용법과 장점 총정리

텀블러 API 키 발급 완벽 가이드 - 3분만에 끝내는 텀블러 인증 절차

 

UPSERT 핵심 이해

 

 

 

UPSERT 한 문장 요약

“그 주문이 이미 있으면 업데이트하고, 없으면 새로 만든다.” 이 한 문장이 모든 상황을 단순하게 합니다.

 

데이터의 존재 여부를 애플리케이션에서 미리 확인하지 말고, 데이터베이스에게 충돌을 기준으로 분기하게 맡기는 것이 포인트입니다.

 

멱등(Idempotent) 처리와 UPSERT
멱등(Idempotent) 처리와 UPSERT

 

왜 `SELECT 후 INSERT`는 위험한가

요청이 동시에 두 번 오면 둘 다 “없음”을 보고 INSERT를 시도해 중복 행이 생길 수 있습니다.

 

반면 UPSERT는 고유 제약(UNIQUE)에 걸리면 곧바로 DO UPDATE 경로로 전환되어 단일 문장으로 경쟁 상태를 해결합니다.

 

락과 재시도를 애플리케이션이 직접 다루지 않아도 됩니다.

 

데이터베이스 별 UPSERT 요약

 

DBMS 문법 핵심 예시(업무 키: `order_no`) 비고
PostgreSQL `INSERT ... ON CONFLICT (key) DO UPDATE` `INSERT INTO orders(order_no, amount) VALUES($1,$2) ON CONFLICT(order_no) DO UPDATE SET amount=EXCLUDED.amount, updated_at=now();` 가장 명확하고 안전한 문법
MySQL `INSERT ... ON DUPLICATE KEY UPDATE` `INSERT INTO orders(order_no, amount) VALUES(?,?) ON DUPLICATE KEY UPDATE amount=VALUES(amount), updated_at=NOW();` 키는 PK/UNIQUE 필요
SQL Server `MERGE ... WHEN MATCHED THEN UPDATE WHEN NOT MATCHED THEN INSERT` `MERGE orders AS t USING (SELECT @no AS order_no,@amt AS amount) s ON t.order_no=s.order_no WHEN MATCHED THEN UPDATE SET amount=s.amount, updated_at=SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT(order_no,amount) VALUES(s.order_no,s.amount);` `MERGE`의 동시성 이슈로 적절한 힌트/락 권장
SQLite `INSERT ... ON CONFLICT(key) DO UPDATE` PostgreSQL과 유사 버전에 따라 지원 범위 확인
MongoDB `updateOne(filter, {$set:...}, {upsert:true})` `db.orders.updateOne({order_no:no}, {$set:{amount:amt, updated_at:new Date()}}, {upsert:true})` 문서지향식 업서트

 

표의 문법만 외우면 대부분의 중복 쓰기 문제는 DB 단에서 해결됩니다.

 

단, 키 제약이 없으면 UPSERT는 성립하지 않으므로 업무 키에 UNIQUE 제약을 반드시 둬야 합니다.

 

ORA-12838 오류 해결법 - 병렬 작업 후 SELECT 오류? 이 한 줄이면 해결됩니다

 

언제 멱등/UPSERT가 필요한가 (체크 리스트)

 

  • 결제 승인/취소, 포인트 적립/차감, 쿠폰 1회 발급
  • 외부 웹훅·PG 콜백·메시지 큐 재전달 처리
  • 사용자 가입·프로필 동기화처럼 “하나만 존재”해야 하는 자원

 

위와 같은 작업은 실패 시 자동 재시도가 흔하고, 사용자가 연타하기 쉽습니다.

 

멱등 설계를 하지 않으면 수치가 이중 반영되어 회계와 정산이 맞지 않습니다.

 

반면 UPSERT 기반 멱등은 재시도를 안전하게 만들어 운영 복잡도를 낮춥니다.

 

특히 외부 시스템은 동일 이벤트를 여러 번 보낼 수 있으므로, 이벤트 ID를 고유 키로 저장하면 중복 수신도 문제없습니다.

 

API 레벨의 멱등 설계

 

HTTP 메서드와 멱등성

`GET`, `PUT`, `DELETE`는 원칙적으로 멱등이고, `POST`는 비멱등입니다.

 

리소스의 식별자가 명확할 때는 `PUT /orders/{order_no}`로 설계하면 클라이언트가 여러 번 전송해도 상태가 한 번만 갱신됩니다.

 

반대로 생성 의미의 `POST`가 필요하면 Idempotency-Key 헤더를 요구해 같은 키로 온 요청은 같은 응답을 재사용하도록 저장소(캐시/DB)에 기록합니다.

 

멱등(Idempotent) 처리와 UPSERT
멱등(Idempotent) 처리와 UPSERT

 

Idempotency-Key 저장 전략

  • 키: 클라이언트가 생성한 UUID, 혹은 업무 키 해시
  • 값: 최종 응답 바디·상태코드·생성된 리소스 식별자
  • 만료: 업무 특성에 맞춘 TTL(예: 결제 24시간)

 

키 저장으로 서버가 재시도 요청을 즉시 단락시켜 트래픽을 줄입니다.

 

또한 응답 재생(Replay)으로 클라이언트 구현을 단순화할 수 있습니다.

 

코드 예시로 보는 UPSERT

 

 

 

PostgreSQL

CREATE UNIQUE INDEX ux_orders_order_no ON orders(order_no);

INSERT INTO orders(order_no, user_id, amount, status, updated_at)
VALUES ($1, $2, $3, $4, now())
ON CONFLICT (order_no) DO UPDATE
SET amount = EXCLUDED.amount,
    status = EXCLUDED.status,
    updated_at = now();

 

MySQL

ALTER TABLE orders ADD UNIQUE KEY ux_order_no (order_no);

INSERT INTO orders(order_no, user_id, amount, status, updated_at)
VALUES (?, ?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
amount = VALUES(amount),
status = VALUES(status),
updated_at = NOW();

 

MongoDB

db.orders.updateOne(
  { order_no: no },
  { $set: { user_id: uid, amount: amt, status: st, updated_at: new Date() } },
  { upsert: true }
);

 

세 예시는 모두 “같은 `order_no`”로 들어오면 중복 생성 대신 갱신됩니다.

 

트랜잭션을 수반해야 하는 복합 작업이라면 DB의 트랜잭션 기능을 활용하거나, 아웃박스 패턴으로 이벤트 발행과 저장을 한 번에 보장합니다.

 

실수 포인트와 방어 전략 목록

 

  • 업무 키가 없다: 자연키(예: `yyyymmdd+driver_id`, `provider_event_id`)를 정의하고 UNIQUE 제약을 건다. 키가 없으면 UPSERT는 의미가 없다.
  • 경합 중 덮어쓰기: “나중에 온 요청이 이전 상태를 덮어써도 되는가?”가 불분명하면 `updated_at` 비교나 낙관적 락(버전 컬럼)으로 최신성 규칙을 강제한다.
  • MERGE 남용: SQL Server의 `MERGE`는 동시성 버그 이슈가 보고되어, 최신 가이드에서는 개별 `INSERT...ON CONFLICT` 스타일 또는 잠금 힌트를 권장한다.
  • 부분 갱신 누락: UPSERT 시 업데이트 컬럼 범위를 명시적으로 적어야 한다. 미지정 컬럼은 이전 값이 남아 데이터가 일관되지 않을 수 있다.

 

이 목록을 릴리스 체크리스트로 사용하면 중복 처리 관련 사고를 크게 줄일 수 있습니다.

 

특히 키 설계와 최신성 규칙은 초기에 합의해두어야 추후 운영 정책이 흔들리지 않습니다.

 

운영 시나리오로 확인하는 멱등 설계

 

재시도(오토 리트라이)가 켜져 있는 메시지 큐

메시지가 최대 한 번 이상 전달될 수 있습니다.

 

메시지 ID를 UNIQUE로 저장하고 UPSERT로 처리하면 소비자가 재시도를 자유롭게 수행해도 안전합니다.

 

실패 시 롤백·재큐잉을 하더라도 최종 상태는 한 번 처리와 같습니다.

 

멱등(Idempotent) 처리와 UPSERT
멱등(Idempotent) 처리와 UPSERT

 

사용자가 결제 버튼을 연타

프런트엔드는 버튼을 비활성화하지만, 백엔드는 항상 최악을 가정합니다.

 

결제 시도 키를 헤더로 받고, 승인 결과를 키 기준으로 저장·재사용합니다.

 

성공 응답을 받은 뒤 동일 키가 오면 DB에서 저장된 응답을 그대로 돌려주면 됩니다.

 

개발자용 빠른 레시피

 

1) 키를 고른다

  • 자연키(업무 조합), 대체키(UUID). 가능하면 사람이 추적하기 쉬운 키를 선호한다.

 

2) DB 제약을 건다

  • `UNIQUE(order_no)` 같이 스키마 차원에서 중복을 물리적으로 금지한다.

 

3) 단일 문장 UPSERT를 사용한다

  • Postgres/SQLite: `ON CONFLICT DO UPDATE`
  • MySQL: `ON DUPLICATE KEY UPDATE`
  • SQL Server: `MERGE` 대신 대안 고려 또는 잠금 힌트

 

4) API는 멱등 키를 받는다

  • `Idempotency-Key` 저장소로 재시도 차단과 응답 재생을 구현한다.

 

이 4단계만 지켜도 대부분의 중복 쓰기 이슈는 사라집니다.

 

추가로 관측성을 위해 “업서트 여부(INSERT/UPDATE)”를 로그에 남기면 운영 시 원인 분석이 쉬워집니다.

 

 

멱등/UPSERT 설계 체크 테이블

 

항목 필수 여부 점검 질문
업무 키 UNIQUE 필수 이 리소스를 유일하게 식별하는 키가 스키마에 있는가
UPSERT 문장 필수 단일 SQL/명령으로 충돌을 해결하는가
멱등 키 저장 권장 POST 재시도에 같은 응답을 보낼 수 있는가
최신성 규칙 상황별 나중 요청이 이전 요청을 덮어써도 되는가(버전/타임스탬프)
관측성 권장 INSERT/UPDATE 분기, 충돌 횟수, 키별 상태가 기록되는가

 

테이블을 스프린트 종료 전에 한번 훑어보면 설계 누락을 빠르게 발견합니다.

 

특히 “최신성 규칙”이 불명확하면 예외 케이스에서 데이터가 흔들리니, 제품·운영과 함께 정책을 수립하십시오.

 

마무리

 

멱등은 사용자에게는 “여러 번 눌러도 안심할 수 있는 버튼”, 운영자에게는 “재시도가 두렵지 않은 시스템”, 개발자에게는 “단순한 오류 처리와 높은 정합성”을 선물합니다.

 

핵심은 업무 키 + UNIQUE 제약 + UPSERT + 멱등 키의 조합입니다.

 

오늘 소개한 패턴과 체크리스트를 바로 적용해, 실패에도 안전하고 재시도에 강한 쓰기 경로를 완성해 보시기 바랍니다.

 

▼ 함께 보면 좋은 글 ▼

변화된 데이터만 골라내는 'CDC 솔루션' - 생산성 향상의 열쇠
정렬 알고리즘, 데이터 정렬의 다양한 종류의 개념과 예시 소개
Oracle SQL에서 상위 N개 데이터만 선택하는 방법 - ROWNUM과 LIMIT의 차이점