1. 락의 개념
- 데이터베이스 락이란?
- 데이터베이스에서 동시성을 제어하기 위한 기능
- 여러 트랜잭션이 동시에 동일한 데이터를 접근하려고 할 때 발생할 수 있는 데이터 손실이나 불일치 문제를 방지하기 위해 사용
락은 데이터의 무결성과 일관성을 보장하는 중요한 수단으로 만약 락이 없다면 두 트랜잭션이 같은 데이터를 동시에 수정하려 할 때 충돌이 발생하거나, 중간 상태의 데이터를 읽음으로써 예기치 못 한 결과가 나올 수 있다.
⇒ 따라서 락은 여러 트랜잭션이 데이터베이스에서 안전하게 작업할 수 있도록 보호하는 역할
동시성 문제 예시
계좌 잔액이 1,000,000원인 상황에서 두 개의 트랜잭션이 동시에 발생한다고 가정
- 트랜잭션 A: 500,000원 출금
- 트랜잭션 B: 300,000원 입금
락이 없다면:
- A와 B가 동시에 1,000,000원을 읽습니다.
- A는 500,000원을 뺀 500,000원으로 업데이트합니다.
- B는 300,000원을 더한 1,300,000원으로 업데이트합니다.
- 최종 잔액은 1,300,000원이 되어, 실제로는 800,000원이 되어야 하는 잔액이 잘못 계산됩니다.
락을 사용하면:
- A가 먼저 락을 획득하고 작업을 수행한 후 500,000원으로 업데이트합니다.
- B는 A의 작업이 끝날 때까지 기다린 후 500,000원에 300,000원을 더해 800,000원으로 정확히 업데이트합니다.
2. 락의 종류
락의 종류는 크게 공유 락과 배타적 락으로 나눌 수 있다.
1) Shared Lock (공유 락)
- 여러 트랜잭션이 동시에 데이터 조회는 가능하지만, 수정은 불가능하게 잠그는 것을 말한다.
- 즉, 동시에 데이터를 조회할 수 있는 경우 사용된다.
예시
SELECT * FROM orders WHERE id = 123 FOR SHARE;
위 쿼리는 id=123
인 행을 공유 락으로 잠급니다. 다른 트랜잭션은 이 데이터를 읽을 수 있지만 수정할 수는 없습니다.
⇒ 재고 조회 등에서는 해당 데이터를 읽어도 데이터의 정합성이 지켜지기 때문에 수정을 방지하고 조회만 가능하게 공유 락을 건다.
2) Exclusive Lock (배타적 락)
- 데이터를 변경하는 작업을 위해 잠그는 것을 말한다.
- 하나의 트랜잭션만 데이터에 접근할 수 있도록 보장한다.
- 배타적 락이 걸리면 해당 데이터를 다른 트랜잭션이 읽거나 수정할 수 없다.
- 주로 데이터 수정 작업 시 사용된다.
예시
UPDATE orders SET status = 'shipped' WHERE id = 123;
=> UPDATE에서 자동으로 적용
위 쿼리는 id=123
인 행에 배타적 락을 걸어 해당 트랜잭션이 끝날 때까지 다른 트랜잭션은 이 행에 접근할 수 없습니다.
⇒ 은행 계좌 입출금 등에서는 해당 데이터를 읽거나 쓰기를 한다면 작업 결과가 달라질 수 있기 때문에 정합성이 지켜지지 않으므로 배타 락을 건다.
3) Row-Level Lock vs Table-Level Lock
- Row-Level Lock (행 단위 락): 특정 행(row)에만 락을 걸어 다른 트랜잭션이 해당 행에만 접근을 못 하게 하는 방식이다. 테이블 전체가 아닌 일부 데이터만 수정하려는 경우에 유용하다.
- Table-Level Lock (테이블 단위 락): 테이블 전체에 락을 걸어 다른 트랜잭션이 해당 테이블에 있는 모든 데이터에 접근하지 못하게 한다. 주로 테이블 구조 변경 시 사용되며, 대규모 업데이트가 필요할 때 사용한다.
4) Optimistic Lock vs Pessimistic Lock
이 두 방식은 공유 락이나 배타적 락과 달리 구현 전략에 가깝다. 즉, 데이터베이스가 아닌 애플리케이션에서 구현되는 경우가 많다.
- Optimistic Lock (낙관적 락): 트랜잭션이 거의 충돌하지 않는다는 가정하에 락을 최소화합니다. 데이터 변경 시점에서 충돌을 검증하여 문제가 발생하면 다시 처리하는 방식이다. 주로 버전 관리(version control)를 통해 충돌 여부를 확인합니다.
- Pessimistic Lock (비관적 락): 트랜잭션 충돌이 자주 발생할 것으로 예상되는 경우, 데이터에 접근할 때부터 락을 걸어 충돌을 방지한다. 데이터에 대한 쓰기 작업이 빈번한 환경에서 자주 사용된다.
3. 데드락(Deadlock)
- 두 개 이상의 트랜잭션이 서로가 가지고 있는 락을 기다리면서 무한정 대기 상태에 빠지는 현상
- 트랜잭션 A가 자원 X에 락을 걸고 자원 Y에 락을 요청하고, 트랜잭션 B가 자원 Y에 락을 걸고 자원 X에 락을 요청하는 상황에서 두 트랜잭션 모두 자원이 반활될 때까지 기다리게 되어 교착 상태가 발생
데드락 발생 예시

- 트랜잭션 A가 Resource 1을 잠금.
- 트랜잭션 B가 Resource 2를 잠금.
- 트랜잭션 A는 Resource 2를 요청하지만, B가 소유 중이므로 대기.
- 트랜잭션 B는 Resource 1을 요청하지만, A가 소유 중이므로 대기.
데드락 방지 및 해결 방법
- 타임아웃(Timeouts): 일정 시간이 지나면 트랜잭션을 강제로 롤백시키는 방법이다. 데드락 상태를 오래 유지하지 않도록 트랜잭션 대기 시간을 제한한다. ⇒ MySQL 지원
- 락 순서 보장: 트랜잭션이 자원을 잠글 때 일정한 순서를 정해 충돌을 방지하는 방법입니다. 예를 들어, 모든 트랜잭션이 자원 1을 먼저 락한 후 자원 2를 락하는 방식으로 데드락을 방지합니다. . ⇒ 개발자가 제어
- 사이클 감지: 그래프 기반으로 트랜잭션과 락 간의 대기 관계를 분석하여 데드락 사이클을 발견하면 해당 트랜잭션을 롤백하는 방법이다. . ⇒ MySQL 지원
그 외에도 낙관적 락, 대기 그래프, 은행원 알고리즘 등이 있다.
4. 락 대기와 성능 이슈
락 경합(Contention)
- 여러 트랜잭션이 동시에 동일한 자원에 접근하려고 할 때 발생
- 락 경합이 자주 발생하면 데이터베이스 성능이 급격히 저하될 수 있다.
락 경합이 발생하는 주요 원인
- 트랜잭션이 너무 오래 락을 유지하는 경우
- 특정 데이터에 접근하는 트랜잭션이 많아 락 대기가 길어지는 경우
- 잘못된 트랜잭션 설계로 인해 불필요하게 많은 락을 사용하는 경우
락 에스컬레이션(Lock Escalation)
- 다수의 행에 락이 걸릴 경우 테이블 전체에 락을 거는 현상과 그로 인한 성능 저하 문제.
- 다수의 행에 락이 걸렸을 때 성능 최적화를 위해 자동으로 테이블 전체 락이 걸리지만 다른 트랜잭션의 접근이 차단될 수 있다.
락 에스컬레이션 문제 해결 방안
- 트랜잭션 크기를 최소화하고, 필요한 범위에서만 락을 거는 방법을 사용
- 인덱스 활용을 통해 행 단위로 락을 거는 범위를 줄이는 방법
- 애플리케이션 레벨에서 락을 줄이는 설계 ⇒ Redis, 데이터 분리 등
정리
데이터베이스 레벨에서의 락은 기본적으로 동시성 문제를 해결하는 중요한 방법이지만, 락 에스컬레이션과 같이 성능 저하나 많은 요청으로 인한 병목 현상이 발생할 수 있고 특히 대규모 환경과 같이 분산 환경에서는 더 큰 문제가 될 수 있다.
⇒ 이러한 문제를 해결하기 위해서는 Redis와 같은 인메모리 데이터 저장소를 활용하여 락을 중앙에서 관리하는 방식이 성능과 확장성 측면에서는 더 나은 선택이 될 수 있다.