[Lock-6][Optimization] 데이터 정합성을 보장하는 락(Lock)의 종류와 전략

2025. 12. 25. 19:48·Database/MySQL

1. 들어가며: 트랜잭션만으로 해결되지 않는 동시성 문제

 이전 포스팅에서는 트랜잭션 격리 수준(Isolation Level)을 통해 데이터 정합성을 보장하는 방법을 살펴보았다. READ COMMITTED, REPEATABLE READ 같은 격리 수준은 트랜잭션 간 읽기 일관성을 보장해 주지만, 멀티 사용자 환경에서 발생하는 모든 동시성 문제를 해결해 주지는 않는다. 대표적인 예가 바로 갱신 분실(Lost Update) 문제다. 예를 들어, 하나의 상품에 대해 동시에 두 개의 주문이 들어온 상황을 생각해 보자.

T1: 상품 재고 조회 → 10
T2: 상품 재고 조회 → 10

T1: 재고 1개 차감 → 9
T2: 재고 1개 차감 → 9

 각 트랜잭션은 모두 “정상적으로” 동작했다. 따라서 2개가 판매되었으므로 재고는 8개가 되어야 한다. 하지만 최종 결과는 재고가 9개이다. 이러한 문제가 발생하는 이유는 “읽고 → 판단하고 → 수정”하는 로직이 여러 SQL로 분리되어 있고, 그 사이를 다른 트랜잭션이 끼어들 수 있기 때문이다.

 트랜잭션은 여러 작업을 하나의 논리적 단위로 묶어 줄 뿐, 동시에 접근하는 트랜잭션들을 물리적으로 제어하지는 않는다. 이 지점에서 등장하는 개념이 바로 락(Lock)이다.


2. 락(Lock)이란 무엇인가?

 락은 쉽게 말해 “먼저 자원을 사용 중인 트랜잭션이 작업을 끝낼 때까지, 다른 트랜잭션의 접근을 제한하는 장치”라고 생각하면 된다. 조금 더 직관적인 비유를 들어보자. 공중화장실에 단 하나의 칸만 남아 있는 상황을 가정해 보자.

  • 만약 문을 잠그는 장치(Lock)가 없다면, A, B, C 세 사람이 동시에 한 칸에 들어가는 대참사가 발생할 것이다.
  • 이를 막기 위해 먼저 들어간 사람이 문을 잠그면, 나머지 사람들은 밖에서 대기해야 한다.
  • 여기서 화장실 칸은 데이터베이스의 리소스(레코드)이며, 문 잠금은 락(Lock)을 의미한다. 즉, 한 번에 하나의 트랜잭션만 특정 리소스를 안전하게 사용하도록 보호하는 기법이다.

3. 비관적 락 vs 낙관적 락

락을 어떻게 사용할지는 “충돌이 얼마나 자주 발생할 것이라고 예상하느냐”에 따라 달라진다.이 관점에서 락은 두 가지 철학으로 나뉜다.

3.1. 비관적 락 (Pessimistic Lock)

 비관적 락은 특정 데이터를 여러 트랜잭션이 동시에 수정하려고 접근하는 상황을 아예 허용하지 않는 방식이다. 하나의 트랜잭션이 데이터를 조회하는 순간, 해당 데이터에 락을 걸어 다른 트랜잭션의 접근을 차단한다. 이 락은 트랜잭션이 종료될 때까지 유지된다.

예를 들어 재고나 계좌 잔액처럼 “한 번 잘못 계산되면 바로 장애나 금전적 손실로 이어지는 데이터”의 경우, 동시에 여러 요청이 들어오는 상황을 그대로 두는 것은 매우 위험하다. 이럴 때 비관적 락을 사용하면, 먼저 접근한 트랜잭션이 작업을 끝내기 전까지 다른 트랜잭션은 해당 데이터를 읽거나 수정하지 못하고 대기하게 된다.

 

 이 방식의 핵심은 동시성보다 정합성을 우선한다는 점이다.충돌이 발생할 가능성을 애초에 열어두지 않기 때문에 갱신 분실과 같은 문제는 구조적으로 발생하지 않는다. 다만, 락을 오래 잡을수록 대기 시간이 길어지고, 여러 자원을 동시에 잠그는 상황에서는 데드락이 발생할 수 있다는 점을 함께 고려해야 한다.

SELECT * FROM stock
WHERE product_id = 1
FOR UPDATE;
장점: 갱신 분실이 구조적으로 불가능 / 정합성이 매우 중요할 때 안전함 (계좌 이체, 잔액 변경 등)
단점: 락 대기로 인한 성능 저하 / 데드락(Deadlock) 발생 가능성 / 트래픽이 많아질수록 병목이 커짐

3.2. 낙관적 락 (Optimistic Lock)

 낙관적 락은 데이터를 조회하는 시점에는 아무런 락도 걸지 않고, 실제로 수정하는 순간(최종 커밋 시점)에만 충돌 여부를 확인하고 충돌 발생 시 재시도 하는 방식이다. 즉, 여러 트랜잭션이 동시에 같은 데이터를 읽는 것은 허용하되, “누가 먼저 수정했는지”를 업데이트 시점에 판단한다.

 

 보통은 버전(version) 컬럼을 두고, 내가 읽었던 시점의 버전과 현재 DB에 저장된 버전이 같은 경우에만 업데이트를 허용한다. 만약 그 사이에 다른 트랜잭션이 먼저 데이터를 수정했다면, 업데이트는 실패하고 애플리케이션은 이를 충돌로 인식한다.

 

 이 방식은 읽기 요청이 많고, 실제 충돌은 드문 경우에 매우 효율적이다. 락을 잡지 않기 때문에 대기 시간이 거의 없고, 시스템 전체의 처리량도 높게 유지할 수 있다. 다만 충돌이 발생하면 트랜잭션을 다시 시도해야 하므로, 충돌 빈도가 높아질수록 오히려 성능이 나빠질 수 있다.

UPDATE product
SET price = 10000,
    version = version + 1
WHERE id = 1
  AND version = 3;
장점: 락 대기가 없어 동시성 성능이 뛰어남 / 읽기 위주의 시스템에 적합
단점: 충돌 시 재시도 로직 필요 / 충돌이 잦아지면 오히려 성능 저하

3.3. 선택의 본질: 기술의 우열이 아닌 도메인의 책임

 동시성 제어를 공부하는 초보 개발자들은 흔히 "비관적 락과 낙관적 락 중 무엇이 더 뛰어난 기술인가?"라는 질문에 매몰되곤 한다. 성능 지표나 구현의 복잡도를 따지며 기술적인 우위를 가리려 하지만, 실무의 세계에서 정답을 결정하는 기준은 기술 그 자체가 아니다. 오히려 우리가 마주한 '데이터의 무게'와 '실패했을 때의 감당 가능성'이 그 기준이 된다.

 

 가장 먼저 스스로에게 던져야 할 질문은 "이 데이터가 일시적으로 어긋났을 때, 우리 시스템과 비즈니스가 그것을 감당할 수 있는가?"이다. 만약 데이터가 충돌하더라도 단순히 사용자에게 다시 시도해달라는 메시지를 보여주면 그만인 영역, 즉 실패의 복구 비용이 저렴한 데이터라면 낙관적 락(Optimistic Lock)이 가장 효율적인 선택이 된다. 이는 시스템의 자원을 미리 점유하지 않으면서도 최소한의 정합성을 보장하는 영리한 접근 방식이다.

 

 반대로 단 한 번의 데이터 정합성 오류가 금전적 손실이나 시스템 전체의 신뢰도 하락으로 이어지는 영역, 즉 복구 비용이 막대한 데이터라면 성능적 손해를 감수하고서라도 비관적 락(Pessimistic Lock)이나 그보다 강력한 보호 장치를 선택해야 한다. 데이터가 오염된 후에 수습하는 것보다, 데이터에 접근하는 입구를 잠가버리는 비용이 훨씬 저렴하기 때문이다.

 

 결국 동시성 제어 전략은 단순한 기술 선택의 문제가 아니라, 해당 데이터가 비즈니스에서 가지는 가치와 책임을 정의하는 도메인 설계의 문제로 바라봐야 한다. 우리가 보호하려는 것이 무엇인지 명확히 정의될 때, 비로소 최적의 락 전략도 그 모습을 드러내게 된다.

3.4. 실무형 대안: 원자적 업데이트 (Atomic Update ⭐⭐)

 비관적 락은 너무 무겁고, 낙관적 락은 재시도 로직을 작성하기 번거로울 때 실무에서 가장 먼저 고려되는 제3의 길이 있다. 바로 '원자적 업데이트' 방식이다. 이는 별도의 락을 명시적으로 요청하는 대신, 단일 SQL 문의 원자성(Atomicity)을 이용한다. 앞서 살펴본 재고 차감 문제를 이 방식으로 해결하면 다음과 같다.

UPDATE stock
SET quantity = quantity - 1
WHERE product_id = 1 AND quantity >= 1;

이 방식이 강력한 이유:

  1. 암시적 배타 락(X-Lock) 활용: DB 엔진은 UPDATE 문을 실행하는 찰나에 해당 로우에 배타 락을 건다. 하지만 SELECT ... FOR UPDATE처럼 트랜잭션 내내 락을 유지하는 것이 아니라, 수정을 완료하는 아주 짧은 순간에만 락을 유지한다.
  2. 조건 확인과 수정의 결합: WHERE quantity >= 1 조건을 통해 "조회(Check)"와 "수정(Use)"을 단일 연산으로 묶어버린다. 이를 통해 갱신 분실 문제를 락의 오버헤드 없이 깔끔하게 해결할 수 있다.
  3. 성능과 안전의 균형: 락 유지 시간을 극단적으로 줄이면서도 데이터 정합성을 완벽하게 보장한다. 일반적인 이커머스의 재고 관리 시스템에서 가장 널리 쓰이는 패턴이다.
💡 일반적인 JPA 업데이트(Dirty Checking)와의 결정적 차이
실무에서 흔히 사용하는 Spring Data JPA의 더티 체킹(Dirty Checking) 방식과 원자적 업데이트는 데이터를 수정하는 메커니즘에서 본질적인 차이를 보인다.

[1] JPA 더티 체킹

-- 절대적인 값으로 덮어쓰기
UPDATE stock SET quantity = 9 WHERE id = 1;​


[2] 원자적 업데이트

-- SQL이 실행되는 바로 그 순간의 실제 DB 값
UPDATE stock SET quantity = quantity - 1 WHERE id = 1 AND quantity >= 1;​

 

즉, 더티 체킹은 "내가 읽었던 시점의 데이터"를 기준으로 계산된 결과값을 DB에 덮어씌운다. 만약 조회를 하고 업데이트를 하기 직전에 다른 트랜잭션이 값을 바꿔버렸다면, 나중에 수행된 트랜잭션이 앞선 수정을 무시하고 덮어쓰는 '갱신 분실(Lost Update)' 문제가 발생한다. 그러나 원자적 업데이트는 "SQL이 실행되는 바로 그 순간의 실제 DB 값"을 기준으로 연산을 수행하므로 동시성 문제에서 자유롭다.
✅ 원자적 업데이트는낙관적 락과 비관적 락의 단점들을 해결할 수 있는 실무 대안이므로, 실제 Spring 코드로 어떻게 작성되는지도 알아야 한다. 해당 내용은 이후 다른 포스팅에서 다룰 예정이다.

3.5. 전략과 도구의 연결고리

 앞서 살펴본 비관적 락, 낙관적 락, 원자적 업데이트는 동시성 문제를 대하는 개발자의 '전략' 혹은 '철학'에 해당한다. 반면, 이어질 섹션에서 설명할 배타 락, 공유 락, 갭 락 등은 데이터베이스 엔진이 그 전략을 실현하기 위해 제공하는 구체적인 '물리적 장치(도구)'이다.

 

 초보 개발자들이 가장 많이 혼동하는 지점이 바로 이 둘의 관계다. 결론부터 말하자면, 우리가 선택한 '전략'에 따라 데이터베이스가 사용하는 '도구'가 결정된다.

  • 비관적 락 전략을 선택하고 FOR UPDATE 쿼리를 실행하면, DB는 내부적으로 배타 락(X-Lock)이라는 도구를 사용하여 데이터를 잠근다. 만약 LOCK IN SHARE MODE를 쓴다면 공유 락(S-Lock)이라는 도구를 꺼내 드는 것이다.
  • 원자적 업데이트 전략은 별도의 락을 명시하지 않더라도, UPDATE 문이 실행되는 아주 짧은 찰나에 DB가 자동으로 배타 락(X-Lock)을 사용하여 원자성을 보장한다.
  • 낙관적 락 전략은 조회 시점에는 DB의 물리적인 락 도구를 전혀 사용하지 않다가, 마지막 업데이트 시점에만 잠깐 배타 락(X-Lock)을 활용한다.

즉, 이들은 서로 배타적인 개념이 아니라 "어떤 철학(전략)으로 어떤 연장(도구)을 휘두를 것인가"의 문제로 이해해야 한다. 이제부터는 MySQL이 우리에게 제공하는 그 구체적인 연장들이 각각 무엇인지 하나씩 살펴보자.


4. MySQL InnoDB의 다양한 락 종류와 실무적 활용

 MySQL의 InnoDB 엔진은 데이터 무결성을 보장하기 위해 상황에 따른 다양한 락(Lock) 기능을 제공한다. 단순히 전체를 잠그는 방식부터 특정 레코드 사이의 빈 공간을 제어하는 방식까지, 각 락의 메커니즘을 이해하는 것이 고성능 데이터베이스 설계의 첫걸음이다.

4.1. 테이블 락 (Table Lock)

테이블 락은 행 단위가 아닌 테이블 전체를 잠그는 방식이다. 개별 레코드를 다루는 행 단위 락에 비해 동시성은 크게 떨어지지만, 데이터 전체의 정합성을 강력하게 확보해야 할 때 사용된다.

  • 동작 원리: 테이블 락이 걸리면 다른 트랜잭션은 해당 테이블에 대해 어떠한 수정 연산도 수행할 수 없다.
  • 실무 활용: 주로 전체 데이터를 복제하는 백업이나, 데이터 구조를 대대적으로 변경하는 마이그레이션 작업 시 활용된다.
-- 읽기 전용 락: 다른 세션에서 읽기는 가능하나 수정은 불가
LOCK TABLES book READ;

-- 쓰기 전용 락: 다른 세션에서 읽기와 수정 모두 불가
LOCK TABLES book WRITE;

-- 작업 완료 후 반드시 해제 필요
UNLOCK TABLES;

4.2. 배타 락 (Exclusive Lock, X-Lock)

배타 락은 "내가 이 데이터를 곧 수정할 예정이니, 작업이 끝날 때까지 아무도 건드리지 마라"는 선언이다. 특정 트랜잭션이 데이터를 수정하는 동안 다른 트랜잭션이 동시에 수정하는 것을 방지한다.

  • 동작: 배타 락이 걸린 레코드는 다른 트랜잭션이 추가적인 배타 락이나 공유 락을 획득할 수 없으며, 수정(UPDATE/DELETE) 시도 시 대기 상태에 빠진다.
  • 특이사항: MySQL의 MVCC(Multi-Version Concurrency Control) 특성상, 락이 없는 일반적인 SELECT 쿼리는 대기 없이 수행 가능하다.
-- Session A
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- X-Lock 적용
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- Session B
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- X-Lock 적용
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
Session A에서 id=1인 row에 X-Lock을 흭득한 순간부터 Session B에서 아무리 id=1인 row에 X-Lock을 흭득하려고 하더라도 무한정 대기 상태에 빠지게 됨

4.3. 공유 락 (Shared Lock, S-Lock)

공유 락은 "우리가 이 데이터를 읽고 있는 동안에는 누구도 이 값을 변경해서는 안 된다"는 의도를 가진다. 이름 그대로 '공유'가 가능하여, 여러 트랜잭션이 동시에 같은 리소스에 대해 공유 락을 획득하고 읽을 수 있다.

  • 동작: 여러 트랜잭션이 동시에 읽는 것은 허용하지만, 누군가 공유 락을 쥐고 있다면 다른 트랜잭션의 수정 작업은 차단된다.
  • 실무 활용: 보고서 생성이나 집계 작업 중, 읽기 작업이 진행되는 동안 데이터가 중간에 변하지 않도록 보장해야 할 때 사용한다.
-- Session A
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;  -- S-Lock 적용
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- Session B
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;  -- S-Lock 적용
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

위에서 SessionA와 SessionB 모두 `SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE`를 실행할 수 있다. 즉, 공유락은 여럿이서 걸 수 있다. 다만, S-Lock을 흭득한 시점부터 그 밑에 있는 UPDATE문은 실행이 되지 않고 무한정 대기 상태에 빠지게 된다.

✅ SELECT와 공유락은 다르다. 그리고 MySQL InnoDB는 SELECT시 공유락을 걸지 않고 조회한다. 배타락을 그 레코드에 걸었든, 말든, MySQL InnoDB엔진은 SELECT시 공유락을 걸지 않으니 조회가 가능하다. 즉, S락은 읽는 동안 수정이 발생하지 않게 잠그는 것이고, X락은 쓰는 동안 수정이 발생하지 않게 잠그는 것이다.
💡 공유 락과 배타 락의 결정적인 차이는 락 사이의 호환성에 있다.
[1] 공유 락(S) + 공유 락(S): 가능. 여러 명이 동시에 읽는 것은 정합성을 해치지 않는다.
[2] 공유 락(S) + 배타 락(X): 불가능. 누군가 읽고 있거나 쓰고 있다면 반대쪽은 대기해야 한다.
[3] 배타 락(X) + 배타 락(X): 불가능. 오직 한 트랜잭션만 수정 권한을 가진다.

4.4. 갭 락 (Gap Lock)

갭 락은 존재하는 레코드 자체가 아니라, 레코드와 레코드 사이의 빈 공간(Gap)에 락을 거는 독특한 방식이다. 이는 이미 있는 데이터를 보호하는 것이 아니라, 새로운 데이터의 삽입(INSERT)을 방지하기 위해 사용된다.

  • 실무 활용: 예약 시스템에서 특정 시간대 구간에 중복된 예약이 들어오지 못하게 막아야 할 때 필수적이다. WHERE 조건에 맞는 행이 현재 없더라도 그 범위 자체를 잠가버린다.
-- 15시부터 18시 사이의 빈 공간에 갭 락 적용
SELECT * FROM reservations
WHERE space_id = 1
AND start_time > '2025-03-01 15:00:00'
AND end_time < '2025-03-01 18:00:00'
FOR UPDATE;

-- 이 락이 유지되는 동안 다른 세션은 해당 범위 내에 INSERT를 시도하면 대기하게 된다.

4.5. 네임드 락 (Named Lock) - 행동에 대한 락

네임드 락은 DB의 물리적인 레코드가 아니라, 사용자가 지정한 특정 문자열(Key)에 락을 거는 기능이다. 이는 데이터베이스 외부의 논리적인 작업 단위를 동기화할 때 유용하다.

  • 실무 활용: "유저당 이벤트 참여는 최대 3개까지만 가능"과 같은 로직은 단순 행 락으로 제어하기 어렵다. 새로운 참여 데이터가 쌓이기 전에 현재 개수를 확인하고 삽입하는 전체 과정을 보호해야 하기 때문이다. 이때 유저 식별자를 키로 하는 네임드 락을 사용하여 해당 과정을 원자적으로 보호할 수 있다.
-- 1️⃣ 특정 유저에 대한 네임드 락 획득 (10초 동안 대기)
SELECT GET_LOCK(CONCAT('user_concert_lock_', 1001), 10);
user_concert_lock_1001 이라는 락을 획득하고 있어야지만 유저 1001 이 콘서트 참여하는 행위에 대해 결정지을 수 있음.

-- 2️⃣ 현재 참가한 콘서트 개수 확인 & 참가 가능한 경우에만 INSERT 실행 (java code)
if (concertCount < 3) {
                concertParticipantRepository.save(new ConcertParticipant(userId, concertId));
}
-- 3️⃣  네임드 락 해제
SELECT RELEASE_LOCK(CONCAT('user_concert_lock_', 1001));
주의: 네임드 락은 MySQL 서버 자원을 직접 사용하므로 부하를 초래할 수 있다. 대규모 트래픽 환경에서는 이를 대체하기 위해 Redis 분산 락을 더 많이 사용한다.

5. 결론 및 요약

락은 데이터 정합성을 유지하기 위한 최후의 보루이다.

  1. 충돌이 잦다면 비관적 락을 통해 안전을 확보해야 한다.
  2. 충돌이 드물다면 낙관적 락을 통해 성능을 챙기는 것이 유리하다.
  3. 조회 시 일관성이 필요하면 S-Lock을, 수정 전 선점이 필요하면 X-Lock을 활용한다.
  4. 범위 보호가 필요할 때는 갭 락을, 논리적 제어가 필요할 때는 네임드 락을 고려한다.

각 도메인의 특성과 트래픽 상황에 맞는 적절한 락 전략을 선택하는 것이 유능한 데이터베이스 설계의 시작이다.

 

'Database > MySQL' 카테고리의 다른 글

[Lock-8][Optimization] 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) 구현 및 비교  (0) 2025.12.26
[Lock-7][Optimization] 데드락(Deadlock)의 유령과 갱신 분실(Lost Update)의 함정  (0) 2025.12.25
[Lock-5][Optimization]트랜잭션 실습을 통한 동시성 문제 인식  (0) 2025.12.25
[Lock-4][Optimization] Redo/Undo 로그와 MVCC 원리  (0) 2025.12.25
[Lock-3][Optimization] Lock: 격리성 수준(Isolation Level) 심층 분석 및 실습  (0) 2025.12.25
'Database/MySQL' 카테고리의 다른 글
  • [Lock-8][Optimization] 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) 구현 및 비교
  • [Lock-7][Optimization] 데드락(Deadlock)의 유령과 갱신 분실(Lost Update)의 함정
  • [Lock-5][Optimization]트랜잭션 실습을 통한 동시성 문제 인식
  • [Lock-4][Optimization] Redo/Undo 로그와 MVCC 원리
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[Lock-6][Optimization] 데이터 정합성을 보장하는 락(Lock)의 종류와 전략
상단으로

티스토리툴바