MySQL SELECT FOR UPDATE 설명: 행 잠금, NOWAIT, SKIP LOCKED 및 모범 사례

1. 소개

MySQL은 전 세계적으로 널리 사용되는 관계형 데이터베이스 관리 시스템입니다. 그 많은 기능 중에서 데이터 무결성을 유지하고 동시 업데이트로 인한 충돌을 방지하는 기술이 특히 중요합니다. 여러 사용자나 시스템이 동시에 동일한 데이터에 작업할 때, 부적절한 동시성 제어는 예상치 못한 버그나 심지어 데이터 손상을 초래할 수 있습니다.

이러한 도전 과제에 대한 가장 일반적인 해결책 중 하나는 SELECT … FOR UPDATE입니다. 이 MySQL 구문은 특정 행에 잠금(독점 제어)을 적용합니다. 이는 재고를 안전하게 감소시키거나 중복 없이 고유 일련 번호를 발급하는 등의 실제 시나리오에서 자주 사용됩니다.

이 기사에서는 SELECT … FOR UPDATE의 기본부터 실전 사용, 중요한 주의사항, 고급 사용 사례까지—명확한 예제와 샘플 SQL 코드를 통해 모든 것을 설명하겠습니다.
데이터베이스를 안전하고 효율적으로 운영하고 싶거나 동시성 제어의 모범 사례를 배우고 싶다면 끝까지 읽어보세요.

2. SELECT FOR UPDATE의 기본 및 전제 조건

SELECT … FOR UPDATE는 MySQL에서 특정 행에 독점 잠금을 적용하는 구문입니다. 주로 여러 프로세스나 사용자가 동일한 데이터를 동시에 편집할 수 있을 때 사용됩니다. 이 섹션에서는 이 기능을 안전하게 사용하기 위해 필요한 기본 개념과 전제 조건을 설명하겠습니다.

무엇보다도, SELECT … FOR UPDATE는 트랜잭션 내에서만 작동합니다. 즉, BEGIN 또는 START TRANSACTION을 사용하여 트랜잭션을 시작하고 그 범위 내에서 실행해야 합니다. 트랜잭션 밖에서 사용하면 잠금이 작동하지 않습니다.

또한, 이 구문은 InnoDB 스토리지 엔진에서만 지원됩니다. MyISAM과 같은 다른 엔진에서는 지원되지 않습니다. InnoDB는 트랜잭션과 행 수준 잠금과 같은 고급 기능을 제공하여 동시성 제어를 가능하게 합니다.

대상 테이블이나 행에 대한 적절한 권한이 있어야 합니다—일반적으로 SELECTUPDATE 권한입니다. 충분한 권한이 없으면 잠금이 실패하거나 오류가 발생할 수 있습니다.

요약

  • SELECT … FOR UPDATE는 트랜잭션 내에서만 유효합니다
  • InnoDB 엔진을 사용하는 테이블에 적용됩니다
  • 적절한 권한(SELECT 및 UPDATE)이 필요합니다

이러한 전제 조건이 충족되지 않으면 행 수준 잠금이 예상대로 작동하지 않습니다. SQL 문을 작성하기 전에 이 메커니즘을 제대로 이해하세요.

3. 작동 방식: 잠금 메커니즘 설명

SELECT … FOR UPDATE를 사용할 때, MySQL은 선택된 행에 독점 잠금(X 잠금)을 적용합니다. 독점 잠금으로 잠긴 행은 다른 트랜잭션에서 업데이트되거나 삭제될 수 없으며, 충돌과 불일치를 방지합니다. 이 섹션에서는 이것이 어떻게 작동하는지와 내부적으로 어떤 일이 발생하는지 명확히 설명하겠습니다.

행 잠금의 기본 동작

SELECT … FOR UPDATE를 사용하여 검색된 행은 현재 트랜잭션이 완료(COMMIT 또는 ROLLBACK)될 때까지 다른 트랜잭션에서 업데이트되거나 삭제되는 것이 차단됩니다. 예를 들어, 제품 테이블에서 재고를 감소시킬 때 FOR UPDATE로 대상 행을 잠그면 동일한 재고를 수정하려는 다른 프로세스가 기다려야 합니다.

다른 트랜잭션과의 상호 작용

행이 잠긴 동안 다른 트랜잭션이 해당 행을 업데이트하거나 삭제하려고 하면 작업이 잠금이 해제될 때까지 기다립니다. 그러나 일반 SELECT(읽기) 작업은 차단되지 않고 여전히 실행될 수 있습니다. 이 잠금 메커니즘의 목적은 데이터 일관성 유지쓰기 충돌 방지입니다.

갭 잠금에 대해

In InnoDB에서는 gap lock이라는 특수한 유형의 잠금도 있습니다. 이는 검색된 행이 존재하지 않거나 범위 조건이 사용될 때 지정된 범위에 새로운 데이터가 삽입되는 것을 방지하기 위해 사용됩니다. 예를 들어, id = 5를 FOR UPDATE와 함께 조회하려고 하지만 행이 존재하지 않을 경우, InnoDB는 주변 인덱스 갭을 잠글 수 있습니다. 이는 해당 범위에 다른 트랜잭션이 새로운 레코드를 삽입하는 것을 일시적으로 방지합니다.

잠금 세분화와 성능

행 수준 잠금은 최소한의 범위만 잠그도록 설계되어 데이터 일관성을 유지하면서 전체 시스템 성능을 크게 저하시키지 않습니다. 그러나 검색 조건이 복잡하거나 인덱스가 없을 경우, 잠금이 예상보다 넓은 범위에 영향을 미칠 수 있습니다. 신중한 쿼리 설계가 중요합니다.

4. 옵션 선택: NOWAIT와 SKIP LOCKED

MySQL 8.0부터 SELECT … FOR UPDATE와 함께 NOWAITSKIP LOCKED와 같은 추가 옵션을 사용할 수 있습니다. 이러한 옵션을 통해 잠금 충돌이 발생했을 때 시스템이 어떻게 동작할지를 제어할 수 있습니다. 이제 각각의 특성과 적절한 사용 사례를 살펴보겠습니다.

NOWAIT 옵션

NOWAIT가 지정되면, 다른 트랜잭션이 이미 대상 행에 대한 잠금을 보유하고 있을 경우 MySQL은 즉시 대기 없이 오류를 반환합니다.
이 동작은 빠른 응답이 필요한 시스템이나, 대기 대신 즉시 재시도하고자 하는 배치 프로세스에 유용합니다.

SELECT * FROM orders WHERE id = 1 FOR UPDATE NOWAIT;

이 예시에서 id = 1 행이 다른 트랜잭션에 의해 이미 잠겨 있다면, MySQL은 즉시 잠금 획득 오류를 반환합니다.

SKIP LOCKED 옵션

SKIP LOCKED는 현재 잠겨 있는 행을 건너뛰고 잠금되지 않은 행만 조회합니다.
이는 다수의 프로세스가 동시에 작업을 처리하는 고볼륨 데이터 처리나 큐 기반 테이블 설계에서 일반적으로 사용됩니다. 각 프로세스가 다른 프로세스를 기다리지 않고 사용 가능한 행을 계속 처리할 수 있게 합니다.

SELECT * FROM tasks WHERE status = 'pending' FOR UPDATE SKIP LOCKED;

이 예시에서는 현재 잠겨 있지 않은 status = 'pending'인 행만 조회됩니다. 이를 통해 여러 프로세스에 걸친 효율적인 병렬 작업 처리가 가능해집니다.

각 옵션을 언제 사용할까

  • NOWAIT : 즉시 성공/실패 피드백이 필요하고 대기를 감당할 수 없을 때 사용합니다.
  • SKIP LOCKED : 대규모 데이터를 병렬로 처리하면서 잠금 경쟁을 최소화하고 싶을 때 사용합니다.

비즈니스 요구에 맞는 옵션을 선택함으로써 보다 유연하고 효율적인 동시성 제어를 구현할 수 있습니다.

5. 실용적인 코드 예시

이 섹션에서는 SELECT … FOR UPDATE를 실용적인 SQL 예시와 함께 설명합니다. 간단한 패턴부터 실제 비즈니스 사용 사례까지 다룹니다.

기본 사용 패턴

먼저, 특정 행을 안전하게 업데이트하기 위한 표준 패턴을 소개합니다.
예를 들어, orders 테이블에서 특정 주문을 조회하고 해당 행을 잠궈 동시 수정을 방지합니다.

예시: 특정 주문의 상태를 안전하게 업데이트하기

START TRANSACTION;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
UPDATE orders SET status = 'processed' WHERE id = 1;
COMMIT;

이 흐름에서는 id = 1 행이 FOR UPDATE로 잠겨 동시에 다른 프로세스가 업데이트하는 것을 방지합니다. 다른 트랜잭션은 해당 행을 수정하거나 삭제하기 전에 COMMIT 또는 ROLLBACK이 완료될 때까지 대기해야 합니다.

고급 예시: 고유 카운터 안전하게 발행하기

SELECT … FOR UPDATE는 순차 번호나 일련값을 안전하게 발행할 때 특히 효과적입니다.
예를 들어, 회원 ID나 주문 번호를 생성할 때, 여러 프로세스가 동일한 카운터를 조회하고 증가시키는 상황에서 경쟁 조건을 방지합니다.

예시: 중복 없이 일련 번호 발행하기

START TRANSACTION;
SELECT serial_no FROM serial_numbers WHERE type = 'member' FOR UPDATE;
UPDATE serial_numbers SET serial_no = serial_no + 1 WHERE type = 'member';
COMMIT;

이 예제에서는 serial_numbers 테이블에서 type = 'member'인 행이 잠깁니다. 현재 일련 번호를 가져와 커밋하기 전에 증가시킵니다. 여러 프로세스가 동시에 실행하더라도 중복 번호가 안전하게 방지됩니다.

참고: JOIN과 함께 FOR UPDATE 사용

FOR UPDATE는 JOIN 절과 함께 사용할 수 있지만 주의가 필요합니다. 잠금이 예상보다 넓은 범위에 적용될 수 있습니다. 대부분의 경우, 단순 SELECT 문을 사용하여 업데이트하려는 테이블의 특정 행만 잠그는 것이 더 안전합니다.

위에서와 같이, SELECT … FOR UPDATE는 간단한 업데이트뿐만 아니라 일련 번호 생성과 같은 실용적인 시나리오에도 적용할 수 있습니다. 시스템 설계에 따라 적절한 구현을 선택하십시오.

6. Gap Lock 및 Deadlock: 위험 및 대책

SELECT … FOR UPDATE는 강력한 동시성 제어 메커니즘이지만, InnoDB 엔진에는 gap lockdeadlock과 같은 특정 동작이 포함되어 있어 주의가 필요합니다. 이 섹션에서는 이러한 메커니즘과 운영상의 문제를 방지하는 방법을 설명합니다.

Gap Lock 동작 및 주의사항

gap lock은 검색된 행이 존재하지 않거나 범위 조건이 사용될 때 발생합니다. 잠금은 일치하는 행뿐만 아니라 주변 인덱스 범위(gap)에도 적용됩니다. 예를 들어 SELECT * FROM users WHERE id = 10 FOR UPDATE;를 실행했는데 id = 10인 행이 없으면, InnoDB는 인접한 gap을 잠가 다른 트랜잭션이 해당 범위에 INSERT 작업을 일시적으로 수행하지 못하게 할 수 있습니다.

Gap lock은 중복 등록이나 고유성 위반과 같은 문제를 방지하는 데 도움이 됩니다. 그러나 예상보다 넓은 잠금을 일으켜 INSERT 작업이 차단될 수 있습니다. 순차 ID나 범위 검색을 자주 사용하는 시스템은 특히 주의해야 합니다.

Deadlock 및 방지 방법

deadlock은 여러 트랜잭션이 서로의 잠금을 기다리면서 모두 진행할 수 없게 될 때 발생합니다. InnoDB에서는 deadlock이 감지되면 하나의 트랜잭션이 자동으로 롤백됩니다. 그러나 시스템을 설계할 때 deadlock을 최소화하는 것이 이상적입니다.

Deadlock을 방지하기 위한 주요 전략:

  • 잠금 획득 순서 표준화 트랜잭션 내에서 여러 테이블이나 행을 잠그는 경우, 모든 프로세스가 동일한 순서로 접근하도록 하여 deadlock 위험을 크게 줄입니다.
  • 트랜잭션을 짧게 유지 트랜잭션 내부 작업량을 제한하고 불필요한 대기를 피합니다.
  • 복잡한 JOIN 쿼리에 주의 LEFT JOIN이나 다중 테이블 잠금은 잠금 범위를 의도치 않게 확대할 수 있습니다. 필요에 따라 SQL 문을 단순하게 유지하고 잠금 로직을 분리하십시오.

JOIN과 결합할 때의 위험

SELECT … FOR UPDATE를 JOIN과 함께 사용할 때, 잠금이 메인 테이블을 넘어 전파될 수 있습니다. 예를 들어 orderscustomers를 FOR UPDATE와 함께 JOIN하면 두 테이블의 행이 의도치 않게 잠길 수 있습니다. 과도한 잠금을 피하려면 별도의 SELECT 문을 사용하여 실제로 필요한 특정 테이블과 행만 잠그는 것이 권장됩니다.

MySQL의 잠금 메커니즘에는 미묘한 함정이 있습니다. gap lock과 deadlock에 대한 올바른 이해는 안정적이고 신뢰할 수 있는 시스템을 구축하는 데 필수적입니다.

7. 비관적 잠금 vs 낙관적 잠금

데이터베이스에서 동시성 제어에는 두 가지 주요 접근 방식이 있습니다: 비관적 잠금낙관적 잠금. SELECT … FOR UPDATE는 비관적 잠금의 전형적인 예입니다. 실제 시스템에서는 상황에 따라 적절한 접근 방식을 선택하는 것이 중요합니다. 이 섹션에서는 각각의 특성과 선택 기준을 설명합니다.

비관적 잠금이란?

비관적 잠금은 다른 트랜잭션이 동일한 데이터를 수정할 가능성이 높다고 가정하여, 데이터에 접근할 때 미리 잠금을 걸어둡니다.
SELECT … FOR UPDATE를 사용하여 업데이트를 수행하기 전에 잠금을 적용함으로써, 동시 트랜잭션으로 인한 충돌이나 불일치를 방지합니다. 충돌이 빈번하거나 엄격한 데이터 무결성을 보장해야 하는 환경에서 효과적입니다.

일반적인 사용 사례:

  • 재고 관리 및 잔액 처리
  • 중복 주문 번호 또는 일련 번호 방지
  • 동시 다중 사용자 편집 시스템

낙관적 잠금이란?

낙관적 잠금은 충돌이 드물다고 가정하고, 데이터 검색 중에 데이터를 잠그지 않습니다.
대신, 업데이트할 때 버전 번호나 타임스탬프를 확인하여 데이터가 변경되지 않았는지 확인합니다. 다른 트랜잭션에 의해 수정된 경우 업데이트가 실패합니다.

일반적인 사용 사례:

  • 빈번한 읽기와 드문 동시 쓰기가 있는 시스템
  • 사용자가 일반적으로 독립적으로 작동하는 애플리케이션

낙관적 잠금 구현 예시:

-- Store the version number when retrieving data
SELECT id, value, version FROM items WHERE id = 1;

-- Update only if the version has not changed
UPDATE items SET value = 'new', version = version + 1 
WHERE id = 1 AND version = 2;
-- If another transaction already updated the version,
-- this UPDATE statement will fail

둘 중 선택하는 방법

  • 비관적 잠금 : 충돌이 빈번하거나 데이터 일관성이 절대적으로 중요한 경우 사용.
  • 낙관적 잠금 : 충돌이 드물고 성능을 우선시할 때 사용.

실제로는 시스템이 작업에 따라 두 접근 방식을 모두 사용합니다. 예를 들어, 주문 처리나 재고 할당은 일반적으로 비관적 잠금을 사용하며, 프로필 업데이트나 구성 변경은 낙관적 잠금을 사용할 수 있습니다.

비관적 잠금과 낙관적 잠금의 차이점을 이해하면 애플리케이션에 가장 적합한 동시성 제어 전략을 선택할 수 있습니다.

8. 성능 고려사항

SELECT … FOR UPDATE는 강력한 동시성 제어를 제공하지만, 부적절한 사용은 전체 시스템 성능에 부정적인 영향을 미칠 수 있습니다. 이 섹션에서는 주요 성능 고려사항과 일반적인 함정을 설명합니다.

인덱스 누락으로 인한 테이블 수준 잠금

SELECT … FOR UPDATE는 행 수준 잠금을 위해 설계되었지만, 검색 조건에 적절한 인덱스가 없거나 조건이 모호한 경우 MySQL은 테이블의 훨씬 더 큰 부분을 효과적으로 잠글 수 있습니다.
예를 들어, 인덱싱되지 않은 열에 대한 WHERE 절을 사용하거나 비효율적인 패턴(예: 선행 와일드카드 LIKE 검색)을 사용하면 MySQL이 정확한 행 잠금을 적용하지 못하게 되어 더 넓은 범위의 잠금이 발생할 수 있습니다.

이로 인해 다른 트랜잭션이 불필요하게 대기하게 되어 응답성 저하와 데드락 빈도 증가를 초래할 수 있습니다.

장기 실행 트랜잭션 피하기

트랜잭션이 SELECT … FOR UPDATE로부터 잠금을 장기간 유지하면, 다른 사용자와 시스템은 잠금이 해제될 때까지 대기해야 합니다.
이는 잠금을 유지하면서 사용자 입력을 기다리는 등의 애플리케이션 설계 실수로 인해 자주 발생하며, 시스템 성능을 심각하게 저하시킬 수 있습니다.

주요 대책:

  • 잠긴 범위 최소화 (WHERE 조건 최적화 및 적절한 인덱싱 사용)
  • 트랜잭션을 가능한 한 짧게 유지 (사용자 상호작용이나 불필요한 처리를 트랜잭션 외부로 이동)
  • 예상치 못한 장기 잠금을 방지하기 위해 타임아웃 및 적절한 예외 처리 구현

잠금 충돌에 대한 재시도 처리

고트래픽 시스템이나 대량 배치 처리가 많은 환경에서 잠금 충돌과 대기 오류가 자주 발생할 수 있습니다.
이러한 경우 잠금 획득 실패 시 재시도 로직을 구현하는 것을 고려하고, 적절한 곳에서 NOWAIT 또는 SKIP LOCKED를 효과적으로 사용하세요.

Without careful performance planning, even well-designed concurrency control can lead to processing delays or system bottlenecks. From the design phase onward, always consider both lock behavior and performance impact to ensure stable system operation.

9. FAQ (Frequently Asked Questions)

This section summarizes common questions and practical issues related to SELECT … FOR UPDATE in a Q&A format. Understanding these frequently misunderstood points will help you avoid common pitfalls in real-world implementations.

Q1. SELECT … FOR UPDATE가 활성화된 동안 다른 세션이 동일한 행을 SELECT 할 수 있나요?

A. 예. SELECT … FOR UPDATE에 의해 적용되는 잠금은 업데이트 및 삭제 작업에만 영향을 미칩니다. 일반 SELECT(읽기 전용) 쿼리는 다른 세션에서도 차단되지 않고 여전히 해당 행을 조회할 수 있습니다.

Q2. FOR UPDATE와 함께 존재하지 않는 행을 SELECT 하려고 하면 어떻게 되나요?

A. 이 경우 InnoDB는 검색된 범위에 gap lock을 적용할 수 있습니다. 이는 다른 트랜잭션이 해당 범위에 INSERT하는 것을 방지합니다. 이로 인해 새로운 레코드 삽입이 의도치 않게 차단될 수 있으니 주의하십시오.

Q3. LEFT JOIN과 같은 JOIN 절과 함께 FOR UPDATE를 사용하는 것이 안전한가요?

A. 일반적으로 권장되지 않습니다. JOIN을 사용하면 잠금 범위가 여러 테이블이나 의도보다 많은 행으로 확대될 수 있습니다. 정확한 잠금이 필요하다면, 필요한 특정 테이블과 행만 잠그는 간단한 SELECT를 사용하십시오.

Q4. NOWAIT와 SKIP LOCKED 중 어느 것을 선택해야 할까요?

A. NOWAIT는 잠금을 획득할 수 없을 경우 즉시 오류를 반환합니다. SKIP LOCKED는 잠금되지 않은 행만 반환합니다. 즉시 성공/실패 결과가 필요할 때는 NOWAIT를 선택하고, 대규모 데이터셋을 병렬로 처리할 때는 SKIP LOCKED을 선택하십시오.

Q5. 언제 낙관적 잠금이 더 적합한가요?

A. 충돌이 드물거나 높은 처리량이 요구될 때 낙관적 잠금이 효과적입니다. 충돌이 빈번하거나 엄격한 데이터 무결성이 필수적인 경우에는 비관적 잠금(FOR UPDATE)을 사용해야 합니다.

By addressing these common questions in advance, you can improve the reliability and practical value of your system design and troubleshooting process.

10. 결론

SELECT … FOR UPDATE는 MySQL에서 가장 강력하고 유연한 동시성 제어 메커니즘 중 하나입니다. 여러 사용자나 프로세스가 동시에 동일한 데이터를 접근하는 시스템에서 데이터 일관성과 안전성을 유지하는 데 중요한 역할을 합니다.

This article covered the fundamentals, practical usage, available options, advanced scenarios, gap locks, deadlocks, pessimistic vs optimistic locking, and performance considerations. These insights are valuable for both daily operations and troubleshooting in real-world environments.

핵심 요점:

  • SELECT … FOR UPDATE는 트랜잭션 내에서만 작동합니다
  • 행 수준 잠금은 동시 업데이트와 데이터 충돌을 방지합니다
  • gap lock 및 JOIN에 의한 잠금 확장과 같은 MySQL 고유 동작을 인지하십시오
  • NOWAIT와 SKIP LOCKED과 같은 옵션을 적절히 사용하십시오
  • 비관적 잠금과 낙관적 잠금의 차이를 이해하십시오
  • 적절한 인덱싱, 트랜잭션 관리, 그리고 성능 계획이 필수적입니다

SELECT … FOR UPDATE는 매우 유용하지만, 그 동작이나 부작용을 오해하면 예상치 못한 문제가 발생할 수 있습니다. 항상 잠금 전략을 시스템 설계와 운영 목표에 맞추십시오.
보다 고급 데이터베이스 시스템이나 애플리케이션을 구축하려는 경우, 여기서 설명한 개념을 활용하여 환경에 가장 적합한 동시성 제어 전략을 선택하십시오.