백엔드 데이터 인프라 13편. UPDATE 데이터 수정의 표준 패턴 — SET·FROM·RETURNING·CASE 표현식·WHERE 안전 가드 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 13편이에요. 11편 SELECT ·12편 JOIN 까지 "조회" 를 다뤘으니, 이번 13편은 데이터 수정 — UPDATE.
UPDATE 기본 형태
UPDATE 테이블
SET 컬럼1 = 값1, 컬럼2 = 값2, ...
WHERE 조건
RETURNING *;
세 가지 절: - SET = 무엇을 어떻게 바꿀지 - WHERE = 어느 행을 (절대 빠뜨리지 말 것) - RETURNING = 수정된 행을 결과로 (PG 특별)
단건·여러 컬럼
-- 한 컬럼
UPDATE users SET name = 'Alice Smith' WHERE id = 1;
-- 여러 컬럼
UPDATE users
SET name = 'Alice Smith',
email = 'alice.smith@example.com',
updated_at = NOW()
WHERE id = 1;
여러 컬럼 동시 = 한 트랜잭션 안. INSERT 보다 자연스러운 묶음.
표현식 — 단순 값이 아닌
-- 현재 값에 더하기
UPDATE products SET price = price * 1.1 WHERE category = 'electronics';
-- 다른 컬럼 참조
UPDATE orders SET total = amount + shipping_fee;
-- NULL 처리
UPDATE users SET email = COALESCE(email, 'no-email@example.com');
SET col = 표현식 어디에 박는 어떤 표현식도 OK.
CASE — 조건부 수정
UPDATE products
SET status = CASE
WHEN stock = 0 THEN 'SOLDOUT'
WHEN stock < 10 THEN 'LOW_STOCK'
ELSE 'IN_STOCK'
END;
한 SQL로 여러 케이스 분기. 대량 일괄 수정에 표준.
FROM — 다른 테이블 값으로 (PG 강점)
UPDATE users u
SET last_login = a.last_at
FROM activities a
WHERE u.id = a.user_id;
UPDATE ... FROM = MySQL엔 없는 PG 강력 기능. "JOIN해서 다른 테이블 값으로 갱신".
서브쿼리로
UPDATE users
SET order_count = (
SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id
);
각 행마다 서브쿼리 실행 — 큰 테이블엔 느림. FROM 절 형태가 보통 더 빠름.
RETURNING — 수정된 행 받기
UPDATE users
SET name = 'Alice Smith'
WHERE id = 1
RETURNING id, name, updated_at;
수정 후 "실제로 무엇이 바뀌었나" 즉시 확인. 운영 환경에서 매우 유용.
조건부 RETURNING
UPDATE products
SET price = price * 1.1
WHERE category = 'electronics'
RETURNING id, name, price AS new_price;
수정된 행 + 새 값을 명시.
WHERE 없는 UPDATE — 무서운 함정
UPDATE users SET name = 'Test'; -- ❌ 모든 행 수정
운영 환경에서 가장 무서운 사고. 한 번 실행되면 — 모든 사용자 이름이 'Test'로. PG 안전 가드 패턴:
BEGIN;
-- 1. SELECT로 영향 행 확인
SELECT COUNT(*) FROM users WHERE created_at < '2020-01-01';
-- 100 출력 — OK
-- 2. UPDATE
UPDATE users SET status = 'INACTIVE' WHERE created_at < '2020-01-01';
-- UPDATE 100 — 예상치와 일치
-- 3. 검증
SELECT COUNT(*) FROM users WHERE status = 'INACTIVE';
-- 4. 의도와 같으면 COMMIT, 다르면 ROLLBACK
COMMIT;
-- 또는 ROLLBACK;
이 5단계 흐름이 운영 안전 표준.
운영 안전 추가 기법
sql_safe_updates (옵션)
SET session_user_safe_updates = on; -- 세션 수준 안전 모드
WHERE 없는 UPDATE·DELETE 거부. PG에 직접 기능은 없고 — 클라이언트(DBeaver 등) 안전 모드 또는 트리거로 구현.
LIMIT — 안전 제한
-- PG 에 표준 UPDATE LIMIT 없음. 단 — CTE로 우회 가능
WITH targets AS (
SELECT id FROM users WHERE created_at < '2020-01-01' LIMIT 100
)
UPDATE users SET status = 'INACTIVE'
WHERE id IN (SELECT id FROM targets);
대량 UPDATE를 작은 단위로 chunk 처리 — 락 시간 단축·트랜잭션 부담 감소.
UPDATE 와 트랜잭션·MVCC
자바 백엔드 입문 43편 @Transactional 의 트랜잭션이 — UPDATE의 안전망. PG는 38편 MVCC 로 "내 UPDATE가 다른 트랜잭션의 SELECT를 막지 않음" 보장.
-- 트랜잭션 A
BEGIN;
UPDATE users SET name = 'New' WHERE id = 1;
-- 아직 COMMIT 안 함
-- 트랜잭션 B (다른 세션)
SELECT name FROM users WHERE id = 1;
-- 'Old' 보임 — 아직 A의 변경이 B에 안 보임 (MVCC)
Spring JPA 의 UPDATE
자바 백엔드 입문 47편 영속성 컨텍스트 의 변경 감지(Dirty Checking) 가 — UPDATE를 자동 생성.
@Transactional
public void updateName(Long id, String newName) {
User user = userRepository.findById(id).orElseThrow();
user.setName(newName);
// 트랜잭션 종료 시 Hibernate가 자동 감지 → UPDATE
}
→ 자동 생성 SQL:
UPDATE users SET name = ?, updated_at = ? WHERE id = ?;
JPA의 마법이지만 — "실제 어떤 UPDATE가 박히나" 모르면 디버깅 불가.
Bulk UPDATE — JPQL
@Modifying
@Query("UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLoginAt < :date")
int markInactiveOldUsers(@Param("date") LocalDateTime date);
큰 데이터의 일괄 변경 — JPQL의 UPDATE. 1차 캐시 무효화 주의.
운영 시나리오 — 5가지
(1) 단일 행 수정 (가장 흔함)
UPDATE users SET email = 'new@example.com' WHERE id = 1;
(2) 상태 변경
UPDATE orders SET status = 'SHIPPED' WHERE id = 100 AND status = 'PAID';
조건에 "현재 상태" 도 박아 — 잘못된 전이 차단.
(3) 일괄 갱신 (chunk)
WITH chunk AS (
SELECT id FROM users WHERE status = 'PENDING' LIMIT 1000 FOR UPDATE
)
UPDATE users SET status = 'ACTIVE' WHERE id IN (SELECT id FROM chunk);
(4) 통계 갱신
UPDATE products p
SET order_count = (SELECT COUNT(*) FROM orders WHERE product_id = p.id);
(5) 마이그레이션
UPDATE users SET name = TRIM(name); -- 공백 정리
함정 5가지
(1) WHERE 누락 — 모든 행 수정
가장 무서운 사고. BEGIN 후 SELECT COUNT 검증 → UPDATE → COMMIT.
(2) UPDATE 한꺼번에 1억 건
락·WAL·메모리 폭발. chunk 1000~10000건 단위.
(3) updated_at 자동 갱신 잊음
UPDATE users SET name = 'New' WHERE id = 1;
-- updated_at 안 박힘 — 디버깅 불가
표준 = 항상 함께. 또는 트리거.
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
이 트리거 박으면 — UPDATE 마다 자동 갱신.
(4) JPA Dirty Checking 의 환상
User user = userRepository.findById(id).get();
user.setName("New");
// → UPDATE 자동 발행되지만, 트랜잭션 안에서만
트랜잭션 밖에서 setName 하면 — UPDATE 안 박힘. @Transactional 필수.
(5) 잘못된 컬럼명·오타
PG는 에러 출력하지만 — 큰 SQL 안에서 못 잡을 수 있음. 운영 = SQL을 별도 검토 + EXPLAIN 한 번.
(1) BEGIN, (2) SELECT COUNT 검증, (3) UPDATE 실행 + 행수 확인, (4) RETURNING으로 결과 검증, (5) COMMIT 또는 ROLLBACK. 이 5단계가 운영 사고를 막아요.
한 줄 정리 — UPDATE SET·WHERE·RETURNING 3절 + FROM·CASE 표현식. WHERE 누락이 가장 무서운 함정. 운영 = BEGIN-SELECT-UPDATE-검증-COMMIT 5단계. JPA는 Dirty Checking, Bulk는 JPQL @Modifying. updated_at 트리거가 표준.
시험 직전 한 번 더 — UPDATE 입문자가 매번 헷갈리는 것
UPDATE t SET col=val WHERE 조건- 여러 컬럼 = 한 SET 안에 콤마
- 표현식 SET =
price = price * 1.1 - CASE 표현식 = 조건부 수정
- FROM = 다른 테이블 값으로 (PG 강점)
- RETURNING = 수정된 행 결과
- WHERE 누락 = 모든 행 (가장 무서운 함정)
- 운영 5단계 = BEGIN → SELECT → UPDATE → 검증 → COMMIT/ROLLBACK
- chunk UPDATE = CTE LIMIT + IN
FOR UPDATE= 행 락- updated_at 자동 갱신 = 트리거
- JPA Dirty Checking = 트랜잭션 안에서만 작동
- @Modifying @Query = JPA Bulk UPDATE
- MVCC = 내 UPDATE가 다른 SELECT 안 막음
- 잘못된 상태 전이 차단 = WHERE에 현재 상태 함께
- 큰 UPDATE = chunk 1000~10000건
- COALESCE·NULLIF = NULL 처리
- 상태 컬럼 = ENUM 또는 CHECK
- COUNT 검증 = 영향 행 사전 확인
- ROLLBACK = 결과 의심스러우면
- JPQL UPDATE = 1차 캐시 무효화 주의
- 한국 백엔드 = 단일 행 UPDATE 가 99%
시리즈 다른 편
- Part 1 PostgreSQL 튜토리얼: 1편 · 9편 CREATE TABLE · 10편 INSERT · 11편 SELECT · 12편 JOIN · 13편 (현재 글)
시리즈 다음 글
다음 글(14편)에서는 DELETE 데이터 삭제 — 안전 가드와 TRUNCATE·소프트 삭제 패턴.
공식 문서: PostgreSQL 18 — Tutorial: Updates에서 더 자세한 사양을 확인할 수 있어요.