DB Lock 이란?
DB 락(Database Lock)은 데이터베이스에서 여러 트랜잭션이 동시에 같은 데이터에 접근할 때, 데이터의 무결성(일관성)을 보장하기 위해 사용되는 메커니즘입니다.
Lock이 없을 때
- Dirty Read (더티 리드): 한 트랜잭션이 데이터를 수정 중일 때 다른 트랜잭션이 그 데이터를 읽는 상황. 만약 첫 번째 트랜잭션이 롤백된다면, 두 번째 트랜잭션은 잘못된 데이터를 읽은 것이 됩니다.
- 트랜잭션 A가 고객의 은행 계좌 잔액을 수정하고 1000원을 더합니다. 잔액이 5000원에서 6000원으로 변경됩니다. 하지만 트랜잭션 A는 아직 이 변경을 커밋하지 않았습니다.
- 트랜잭션 B가 같은 고객의 계좌 잔액을 조회하여 6000원이라고 읽습니다. 트랜잭션 B는 이 잔액을 기반으로 다른 계산을 수행합니다.
- 트랜잭션 A가 예기치 않은 오류로 인해 롤백되어, 잔액은 다시 5000원으로 되돌아갑니다.
- 그러나 트랜잭션 B는 이미 잘못된 6000원을 읽었으므로, 이로 인해 부정확한 계산이 발생할 수 있습니다.
- Non-repeatable Read (반복 불가능한 읽기): 한 트랜잭션이 데이터를 읽은 후, 다른 트랜잭션이 그 데이터를 수정하고 커밋하여 첫 번째 트랜잭션이 동일한 데이터를 다시 읽을 때 값이 달라지는 상황입니다.
- 트랜잭션 A가 고객의 계좌 잔액을 읽습니다. 이때 계좌 잔액은 5000원입니다.
- 트랜잭션 B가 같은 고객의 계좌 잔액을 7000원으로 수정하고 커밋합니다.
- 트랜잭션 A가 동일한 고객의 계좌 잔액을 다시 읽습니다. 이번에는 잔액이 7000원으로 표시됩니다.
- 트랜잭션 A는 같은 트랜잭션 내에서 동일한 데이터를 두 번 읽었지만, 그 값이 일관되지 않게 변경된 것을 확인합니다.
- Lost Update (업데이트 손실): 두 개의 트랜잭션이 동시에 같은 데이터를 수정하려고 할 때, 한 트랜잭션의 수정 내용이 다른 트랜잭션에 의해 덮어쓰여져 사라지는 상황입니다.
- 트랜잭션 A가 고객의 계좌 잔액을 5000원에서 6000원으로 수정합니다. 하지만 아직 커밋하지 않았습니다.
- 트랜잭션 B도 같은 고객의 계좌 잔액을 5000원에서 7000원으로 수정하고, 즉시 커밋합니다.
- 트랜잭션 A가 자신의 변경 사항을 커밋합니다.
- 결과적으로 계좌 잔액은 6000원으로 저장됩니다. 그러나 트랜잭션 B가 먼저 커밋한 7000원으로의 변경은 사라져 버렸습니다. 이 상황을 업데이트 손실이라고 합니다.
Lock의 종류
-
공유 락 (Shared Lock, S Lock)
- 공유 락은 데이터베이스에서 데이터를 읽을 때 사용됩니다. 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있지만, 공유 락이 걸린 동안에는 데이터를 수정할 수 없습니다.
- 상황: 고객 정보 시스템에서 여러 직원이 동시에 같은 고객의 정보를 조회할 수 있지만, 조회 중에는 그 정보를 수정할 수 없습니다.
- 예시:
- 트랜잭션 A가 고객 A의 정보를 조회합니다. 이때 고객 A의 정보에 대해 공유 락이 걸립니다.
- 트랜잭션 B도 동시에 고객 A의 정보를 조회합니다. 공유 락이 이미 걸려 있으므로, 트랜잭션 B는 정상적으로 고객 정보를 읽을 수 있습니다.
- 트랜잭션 C가 고객 A의 정보를 수정하려고 시도합니다. 하지만 공유 락 때문에 수정 작업이 차단되거나 대기 상태가 됩니다.
- 트랜잭션 A와 B가 조회를 끝내고 공유 락이 해제된 후에야 트랜잭션 C가 고객 정보를 수정할 수 있습니다.
-
배타 락 (Exclusive Lock, X Lock)
- 배타 락은 데이터를 수정할 때 사용됩니다. 배타 락이 걸린 데이터는 다른 트랜잭션이 읽거나 수정할 수 없습니다. 한 트랜잭션이 배타 락을 획득하면 다른 모든 트랜잭션은 해당 데이터에 접근할 수 없습니다.
- 상황: 재고 관리 시스템에서 한 직원이 특정 상품의 재고를 수정하고 있을 때, 다른 직원이 그 상품의 재고를 조회하거나 수정하지 못하게 하는 상황.
- 예시:
- 트랜잭션 A가 상품 A의 재고를 100에서 150으로 수정하려고 합니다. 이때 상품 A에 대해 배타 락이 걸립니다.
- 트랜잭션 B가 상품 A의 재고를 조회하려고 하지만, 배타 락 때문에 대기 상태가 됩니다.
- 트랜잭션 C가 상품 A의 재고를 150에서 200으로 수정하려고 하지만, 마찬가지로 대기 상태가 됩니다.
- 트랜잭션 A가 재고 수정 작업을 완료하고 커밋한 후, 락이 해제되면 트랜잭션 B와 C가 차례로 진행될 수 있습니다.
-
비관적 락 (Pessimistic Locking)
- 비관적 락은 데이터를 읽을 때부터 락을 걸어 다른 트랜잭션이 접근하지 못하도록 하는 방식입니다. 데이터의 충돌 가능성이 높을 때 유용합니다.
- 상황: 은행 시스템에서 한 사용자가 특정 계좌의 잔액을 조회하고 수정하려는 시나리오에서, 다른 사용자가 이 계좌에 접근하지 못하게 하는 방식.
- 예시:
- 트랜잭션 A가 고객 A의 계좌 잔액을 조회하고 수정하려고 합니다. 이때 비관적 락을 사용하여 잔액에 대한 락을 걸고, 다른 트랜잭션의 접근을 차단합니다.
- 트랜잭션 B가 같은 계좌의 잔액을 조회하려고 하지만, 비관적 락 때문에 대기 상태가 됩니다.
- 트랜잭션 A가 계좌 잔액을 수정하고 커밋한 후, 락이 해제되면 트랜잭션 B가 잔액을 조회할 수 있습니다.
- 쓰기가 작업이 많고 충돌가능성이 높은 환경에서 사용
-
낙관적 락 (Optimistic Locking)
- 낙관적 락은 데이터를 수정하기 전까지 락을 걸지 않고, 수정 시점에만 충돌을 확인하는 방식입니다. 주로 데이터의 버전 번호를 사용하여 동시성 문제를 해결합니다.
- 상황: 온라인 쇼핑몰에서 여러 사용자가 동시에 동일한 상품의 정보를 수정할 수 있는 상황에서, 수정 시점에 충돌을 감지하여 해결하는 방식.
- 예시:
- 트랜잭션 A와 트랜잭션 B가 동시에 상품 A의 가격을 수정하려고 합니다. 상품 A의 현재 버전은 1입니다.
- 트랜잭션 A는 상품 가격을 5만원으로 수정하고, 버전을 2로 증가시켜 저장합니다.
- 트랜잭션 B는 상품 가격을 6만원으로 수정하려고 하지만, 저장 시점에 버전 충돌이 발생합니다. 트랜잭션 B는 버전 1이 아닌 2를 발견하므로, 예외를 발생시키거나 변경 작업을 재시도해야 합니다.
- 읽기작업이 쓰기작업에 비해 많은 데이터에 사용
-
명명된 락 (Named Lock)
- 명명된 락은 데이터베이스에서 특정 이름으로 락을 설정하여, 동시에 하나의 프로세스만 특정 리소스에 접근하도록 하는 방식입니다. 주로 특정 리소스나 작업에 대한 접근을 직관적으로 제어하기 위해 사용됩니다.
- 상황: PostgreSQL에서 특정 보고서를 생성하는 작업에 대해 이름 기반으로 락을 걸어, 두 명의 사용자가 동시에 같은 보고서를 생성하지 못하게 하는 상황.
- 예시:
- 트랜잭션 A가 “월간 보고서 생성”이라는 이름으로 명명된 락을 설정하고 보고서 생성을 시작합니다.
- 트랜잭션 B도 “월간 보고서 생성”을 시도하지만, 트랜잭션 A가 락을 걸고 있으므로 대기 상태가 됩니다.
- 트랜잭션 A가 보고서 생성을 완료하고 명명된 락을 해제하면, 트랜잭션 B가 보고서 생성을 시작할 수 있습니다.
-
분산 락 (Distributed Lock)
- 분산 락은 여러 시스템이나 인스턴스에서 동시에 동일한 자원에 접근할 때, 자원의 일관성을 유지하기 위해 사용되는 락입니다. Redis와 같은 분산 시스템을 사용하여 구현됩니다.
- 상황: 온라인 예약 시스템에서 여러 서버 인스턴스가 동일한 좌석을 동시에 예약하지 못하도록 하는 상황.
- 예시:
- 인스턴스 A가 공연 좌석 10번에 대해 예약을 시도하고, Redis를 사용하여 분산 락을 설정합니다.
- 인스턴스 B도 동일한 좌석 10번을 예약하려고 시도하지만, 인스턴스 A가 락을 걸고 있으므로 예약이 실패하거나 대기 상태가 됩니다.
- 인스턴스 A가 예약을 완료하고 락을 해제하면, 인스턴스 B는 다음 예약을 시도할 수 있습니다.
낙관적 락
-
낙관적 락의 동작방식
- 낙관적 락(Optimistic Lock)은 트랜잭션 간의 충돌을 최소화하고 성능을 향상시키기 위해 사용되는 동시성 제어 메커니즘입니다.
- 비관적 락이 데이터베이스 레벨에서 락을 걸어 다른 트랜잭션의 접근을 차단하는 방식이라면, 낙관적 락은 데이터베이스 락을 사용하지 않고, 대신 데이터가 변경되었는지 확인하여 충돌을 처리하는 방식입니다.
- 버전 관리:
- 낙관적 락에서는 보통 version이라는 필드를 엔티티에 추가합니다. 이 필드는 해당 엔티티의 수정 횟수를 추적하는 역할을 합니다.
- 트랜잭션이 엔티티를 읽을 때, 현재의 버전 번호가 함께 읽혀옵니다.
- 트랜잭션이 엔티티를 수정하고 저장하려고 할 때, 현재 데이터베이스에 저장된 버전 번호와 트랜잭션이 처음 읽어온 버전 번호를 비교합니다.
- 데이터 충돌 검출:
- 트랜잭션이 데이터를 저장할 때, 데이터베이스에 저장된 버전 번호가 트랜잭션이 처음 읽어온 버전 번호와 동일하다면, 데이터가 수정되지 않았다고 간주하고 업데이트를 수행합니다. 이때 버전 번호는 증가합니다.
- 반면, 버전 번호가 다르면, 다른 트랜잭션이 데이터를 수정한 것으로 간주하고, 현재 트랜잭션을 롤백하거나 재시도하도록 합니다.
-
낙관적 락의 장단점
- 장점
- 성능: 비관적 락에 비해 성능이 뛰어납니다. 데이터베이스에서 락을 걸지 않으므로 병행 처리 성능이 향상됩니다.
- 유연성: 충돌이 발생했을 때, 비즈니스 로직에 따라 트랜잭션을 재시도하거나 롤백할 수 있습니다.
- 단점
- 충돌 가능성: 데이터가 자주 변경되는 경우, 충돌이 자주 발생할 수 있습니다. 이로 인해 여러 번의 재시도가 필요할 수 있습니다.
- 복잡성: 충돌을 처리하기 위한 로직이 추가로 필요할 수 있습니다.
-
낙관적 락은 데이터베이스 락을 최소화하면서도 데이터 일관성을 유지할 수 있는 효과적인 방법입니다.
-
특히, 데이터 충돌이 적고 병행 처리가 많은 시스템에서 유용하게 사용할 수 있습니다.
데이터의 변경이 많을 경우는 레디스에서 메인으로 처리를 하고 데이터 변경사항을 DB에 기록하는 방법을 사용할 수 있다.
-
하지만 충돌이 발생했을 때의 처리 로직을 잘 설계해야 하며, 특정 상황에서는 비관적 락보다 복잡할 수 있습니다.
비관적 락
- 비관적 락의 동작 방식
- 락의 개념: 비관적 락은 데이터에 대한 접근을 제어하기 위해 사용됩니다. 데이터베이스에서 특정 행(row)이나 테이블에 대해 락을 걸어, 다른 트랜잭션이 동시에 동일한 데이터에 접근하거나 수정하지 못하도록 합니다.
- 락의 종류:
- PESSIMISTIC_READ: 읽기 락(Shared Lock)을 설정하여 다른 트랜잭션이 해당 데이터를 읽을 수는 있지만, 수정은 할 수 없도록 합니다.
- PESSIMISTIC_WRITE: 쓰기 락(Exclusive Lock)을 설정하여 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하도록 합니다.
- DB 레벨에서의 락 동작:
- 락 설정: 비관적 락을 사용하면 SQL 쿼리나 트랜잭션이 데이터베이스에 접근할 때 락이 설정됩니다. 예를 들어, PESSIMISTIC_WRITE 락을 설정하면, 해당 데이터에 대한 모든 읽기 및 쓰기 작업이 락이 해제될 때까지 대기하게 됩니다.
- 락 해제: 락은 일반적으로 트랜잭션이 종료되거나 커밋될 때 해제됩니다. 트랜잭션이 커밋되면 락이 해제되어 다른 트랜잭션이 해당 데이터에 접근할 수 있게 됩니다. 트랜잭션이 롤백되는 경우에도 락이 해제됩니다.
- 비관적 락은 주로 데이터베이스 레벨에서 동작하며, 데이터의 무결성을 보장하는 데 매우 유용합니다.
- 그러나 성능에 영향을 미칠 수 있으므로, 데이터 충돌 가능성이 높은 환경에서 신중하게 사용해야 합니다.
- 스프링 부트와 같은 애플리케이션에서 비관적 락을 설정하면, 데이터베이스가 이 락을 처리하고 관리하게 됩니다.