이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
INSERT,UPDATE,DELETE는 조회보다 왜 더 조심해야 하는가?- 운영 DB에서 데이터 변경 전 어떤 검증이 필요한가?
- 동시성에 안전한 변경 SQL은 어떻게 작성해야 하는가?
개요
쓰기 SQL은 데이터를 바꾼다. 잘못된 UPDATE나 DELETE는 성능 문제를 넘어 데이터 사고가 된다. 또한 쓰기 SQL은 row lock, 인덱스 갱신, WAL/binlog, 제약 검사 비용을 만든다.
백엔드 개발자는 쓰기 SQL을 결과뿐 아니라 영향 범위와 복구 가능성으로 평가해야 한다.
원리
쓰기 SQL의 원리는 “row를 바꾼다”에서 끝나지 않는다. DB는 변경 대상 row를 찾고, 필요한 lock을 잡고, 제약을 검사하고, 인덱스를 갱신하고, WAL/binlog를 남기며, 실패하면 rollback할 정보를 유지한다. 그래서 같은 조건의 SELECT보다 UPDATE와 DELETE는 장애 반경이 훨씬 크다.
백엔드 개발자는 쓰기 SQL을 영향 row 수, lock 보유 시간, 트랜잭션 길이, 재시도 가능성, idempotency, 복구 방법으로 읽어야 한다. API 코드의 save() 한 줄도 실제로는 여러 인덱스와 제약, trigger, replication에 영향을 줄 수 있다.
INSERT
INSERT는 제약과 인덱스를 함께 통과한다.
INSERT INTO members (email, name, created_at)
VALUES ('joseph@example.com', 'Joseph', now());이메일 Unique 제약이 있으면 중복 시 실패한다. 애플리케이션은 이 실패를 도메인 예외로 변환해야 한다.
대량 insert는 batch size와 transaction 크기를 조절한다.
Spring/JPA에서는 saveAll()이 항상 효율적인 batch insert를 의미하지 않는다. JDBC batch 설정, 식별자 생성 전략, flush/clear 주기, 트랜잭션 크기에 따라 실제 SQL 개수와 메모리 사용량이 달라진다.
UPDATE
UPDATE는 조건이 핵심이다.
UPDATE orders
SET status = 'CANCELED'
WHERE id = :orderId
AND status = 'CREATED';현재 상태 조건을 함께 두면 이미 결제된 주문을 취소하는 실수를 막을 수 있다. 영향 row 수가 0이면 상태가 이미 바뀌었거나 존재하지 않는 것이다.
재고 차감은 조건부 update로 DB 안에서 판단과 변경을 묶는 편이 안전하다.
UPDATE products
SET stock = stock - 1
WHERE id = :productId
AND stock > 0;이 쿼리는 “읽고 판단한 뒤 쓰기”가 아니라 DB 안에서 조건 확인과 변경을 한 번에 수행한다. 영향 row 수가 1이면 성공, 0이면 재고 부족 또는 대상 없음으로 해석할 수 있다. 별도의 SELECT 후 UPDATE보다 race condition에 강하다.
DELETE
DELETE는 복구 가능성을 먼저 본다.
DELETE FROM members
WHERE id = :memberId;회원 삭제가 주문, 결제, 포인트 이력에 어떤 영향을 주는지 확인해야 한다. 물리 삭제, 소프트 삭제, 익명화 중 어떤 정책인지 정한다.
대량 delete는 batch로 나누거나 partition을 검토한다.
대량 삭제는 삭제 자체보다 인덱스 갱신, vacuum/purge, replica lag, 백업 크기, rollback 시간이 더 큰 문제가 될 수 있다. 오래 보관한 로그성 데이터라면 기간 partition을 두고 partition drop으로 처리하는 편이 나을 수 있다.
운영 변경 전 체크
운영에서 직접 변경하기 전에는 다음을 확인한다.
SELECT count(*)
FROM orders
WHERE status = 'CREATED'
AND created_at < timestamp '2026-01-01';- 대상 건수가 예상과 맞는가?
- 실행 계획이 안전한가?
- 백업과 복구 지점이 있는가?
- 트랜잭션으로 묶을 것인가?
- batch size는 얼마인가?
- 롤백 SQL이나 forward fix가 있는가?
PostgreSQL에서는 UPDATE ... RETURNING으로 변경된 row를 바로 기록할 수 있다. MySQL은 버전별 지원과 문법 차이가 있으므로 같은 방식이 가능한지 확인해야 한다.
UPDATE orders
SET status = 'EXPIRED'
WHERE status = 'CREATED'
AND created_at < timestamp '2026-01-01'
RETURNING id, status, updated_at;운영 정정에서는 DBMS별 기능보다 “변경한 대상을 나중에 증명할 수 있는가”가 핵심이다.
실전 팁
UPDATE와DELETE는 먼저 같은 조건의SELECT count(*)로 확인한다.- 상태 변경 SQL에는 현재 상태 조건을 넣는다.
- 대량 변경은 작은 batch와 진행률 기록을 둔다.
- 운영 쿼리는 동료 리뷰를 받는다.
- 변경 후 검증 쿼리를 준비한다.
- 조건부 update는 영향 row 수를 도메인 결과로 해석한다.
- 외부 API 호출, 파일 처리, 긴 계산을 DB transaction 안에 넣지 않는다.
- 대량 DML 전에는 lock timeout, statement timeout, replica lag 중단 기준을 정한다.
위험 신호!
WHERE없는 UPDATE/DELETE를 실행한다.- 운영에서 대량 변경을 한 트랜잭션으로 수행한다.
- 영향 row 수를 확인하지 않는다.
- 백업 상태를 확인하지 않고 destructive 작업을 한다.
- 상태 전이 조건 없이 status를 덮어쓴다.
saveAll()을 쓰면 DB batch가 자동으로 최적화된다고 믿는다.- 대량 delete 후 vacuum/purge, replica lag, 디스크 회수 시점을 고려하지 않는다.
- 외부 결제 API 호출을 DB transaction 안에서 수행해 lock과 connection을 오래 잡는다.
확인 질문
- UPDATE에 현재 상태 조건을 넣는 이유는 무엇인가?
- 동시 변경이나 잘못된 상태 전이를 막고, 영향 row 수로 성공 여부를 판단하기 위해서다.
- 운영 DELETE 전 확인해야 할 것은 무엇인가?
- 대상 건수, 참조 관계, 백업과 복구 가능성, 실행 계획, batch 전략, 검증 쿼리다.
- 쓰기 SQL이 조회보다 운영 비용이 큰 이유는 무엇인가?
- 데이터 변경, lock, 인덱스 갱신, 제약 검사, WAL/binlog 기록, rollback 비용을 만들기 때문이다.
- 재고 차감에 조건부 update가 유용한 이유는 무엇인가?
- 조건 확인과 변경을 DB 안에서 원자적으로 수행하고, 영향 row 수로 성공/실패를 판단할 수 있어 동시성 경쟁에 더 강하기 때문이다.