Database Lock

코린이·2025년 9월 12일

해당 글을 읽기 전 트랜잭션에 대해 잘 모른다면 트랜잭션 포스트를 읽고 와주세요!

데이터베이스는 항상 데이터의 일관성과 무결성이 보장되어야한다. 하지만 동시에 많은 세션이 DB에 접근한다면 이런 일관성과 무결성이 깨지느 동시성 문제가 발생할 수 있다.

예를 들어 고객 2명이 상품이 구매하는 경우를 가정해보자.
그러면 고객 1과 고객 2 모두 상품의 재고를 조회하고 상품의 재고를 1 차감하는 트랜잭션이 발생할 것이다.
여기서 고객 1의 트랜잭션을 T1, 고객 2의 트랜잭션을 T2라고 하겠다.

1. (T1) 고객 1이 상품의 재고를 조회한다. 
조회 결과 5개로 확인되었다.

2. (T1) 고객 1이 상품을 구매하여 재고가 1 차감된다.
(현재 재고 4)

3. (T2) 고객 2가 상품의 개수를 조회한다.
조회 결과 5개로 확인되었다.

4. (T1) 트랜잭션 1이 커밋된다.

5. (T2) 고객 2가 상품을 구매하여 재고가 1 차감된다.
(현재 재고 4)

6. (T2) 트랜잭션 2이 커밋된다.

결과적으로 두명의 고객이 상품을 구매했지만 재고는 3개가 아닌 4개로 수정되었다. 이유는 T2가 T1의 트랜잭션이 커밋되기 전에 실행되었기 때문이다.
이와 같이 트랜잭션끼리 서로 충돌이 발생하면 데이터의 불일치가 발생하여 무결성과 일관성이 깨지게 된다.

이러한 동시성 문제를 해결하기 위해 우리는 Lock을 사용할 수 있다.

락(Lock)?

데이터베이스의 일관성과 무결성을 유지하기 위해 트랜잭션 처리의 순차성을 보장하는 방법이다.
즉, 여러 개의 트랜잭션이 동시에 접근했을 때 일관성이 깨질 수 있으므로 잠금을 하여 다른 트랜잭션이 접근하지 못하도록 한다.

Lock은 크게 낙관적 락과 비관적 락으로 구분할 수 있다.

낙관적 락(Optimistic Lock)

낙관적 락은 대부분의 트랜잭션이 충돌이 발생하지 않을 것이라고 낙관적으로 가정하는 방법이다. 따라서 서로 다른 트랜잭션이 동시에 접근하여 트랜잭션을 처리할 수 있다.

그렇다고 완전히 내버려두면 충돌이 발생하였을 때 데이터의 일관성이 무너질 수 있다.
그래서 낙관적 락에서는 DB 단이 아닌 어플리케이션 단에서 엔티티 버전을 통해 동시성을 제어하고 이때 version과 같은 구분 칼럼을 이용하여 충돌을 방지한다.
(hashCode, timestamp 같은 방법도 있다)

JPA에서 낙관적 락을 적용하는 방법

가장 간단한 방법은 @Version 어노테이션을 사용하는 것이다.

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    private String name;

    private Integer price;
    
    @Version
    private Long version;
   
}

위와 같이 version 필드를 엔티티에 추가하면 JPA에서 자동적으로 버전 값을 하나씩 증가시킨다.

UPDATE PRODUCT
SET
  name = ?,
  version = ? # 버전 + 1 증가
WHERE
  id = ?,
  and version = ? # 버전 비교

이렇게 하면 위와 같이 엔티티를 업데이트 할 때 엔티티 조회 시점의 version과 일치하는 엔티티를 찾고 많약 존재하지 않으면 예외를 발생한다. 그러면 개발자 직접 수동으로 롤백 처리를 하거나 이전 트랜잭션의 커밋이 완료되고 다시 예외가 발생한 트랜잭션을 재시도하는 방식으로 접근해야한다.
(어찌보면 좀 까다로운 방법이다😣)

주의할 점은 해당 version 필드는 JPA가 자동으로 관리하므로 개발자가 임의로 수정해서는 안된다.

이외에도 LockModeType 통해 락 옵션을 변경할 수 있는데 내용이 너무 길어지니 다음에 기회가 되면 포스팅 하겠다.

비관적 락(Pessimistic Lock)

비관적 락은 데이터를 수정하기 전에 해당 데이터에 대한 접근을 미리 제한하는 방식이다.
즉, 트랜잭션 작업 간에 충돌이 발생할 것이라고 미리 예상하고 락을 거는 방법이다.

비관적 락은 크게 공유 락(Shared Lock)과 배타 락(Exclusive Lock)이 있으며 트랜잭션이 시작될 때 해당 락을 걸고 시작한다.

공유 락은 읽기 작업 시, 배타락은 쓰기 작업 시에 사용된다고 일단은 알아두자.

공유 락(Shared Lock)

공유 락은 데이터를 변경하지 않은 읽기 작업을 할 때 사용한다. 그렇기 때문에 다음과 같은 특징을 가지고 있다.

  • 하나의 트랜잭션이 읽기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 읽더라도 문제가 발생하지 않으므로 다른 공유 락을 막을 필요가 없다.
  • 하나의 트랜잭션이 읽기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 수정한다면 데이터 정합성이 지켜지지 않을 수 있으므로 막을 필요가 있다.

즉, 공유 락이 걸어진 상황에서 다른 공유 락이 접근해도 되지만 배타 락은 접근하면 안된다.

공유 락 - 공유 락 (O)
공유 락 - 배타 락 (X)

공유 락(Exclusive Lock)

배타 락은 데이터를 변경하는 쓰기 작업을 할 때 사용된다. 해당 락도 특징을 살펴보자

  • 하나의 트랜잭션이 쓰기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 읽기 작업을 한다면 데이터 정합성 지켜지지 않을 수 있으므로 막을 필요가 있다.
  • 하나의 트랜잭션이 쓰기 작업을 수행할 때, 다른 트랜잭션이 해당 데이터를 쓰기 작업을 한다면 데이터 정합성이 지켜지지 않을 수 있으므로 막을 필요가 있다.

즉. 배타 락이 걸어진 상황에서는 다른 공유 락이나 배타 락이 접근해서는 안된다.

베타 락 - 공유 락 (X)
베타 락 - 배타 락 (X)

블로킹(Blocking)

그렇다면 앞서 말한 접근하면 안되는 Lock들 끼리 서로 충돌이 발생하면 어떻게 될까?
이럴 경우 먼저 Lock을 설정한 트랜잭션이 끝날 때까지 다른 트랜잭션은 대기를 해야한다.

이런식으로 Lock간의 경합이 발생하여 특정 트랜잭션이 작업을 진행하지 못하고 대기하는 상황을 블로킹이라고 한다.

위 그림은 보면 트랜잭션 A가 먼저 공유 락을 설정하였기 때문에 배타 락을 건 트랜잭션 B가 대기하며 블로킹 상태에 빠진 것을 알 수 있다.

데드락(Dead Lock)

그렇다면 두 개의 트랜잭션에 모두 블로킹이 발생하면 어떻게 될까?

트랜잭션 A와 B 각각 Coupon 데이터에 X-L을 Member 데이터에 X-L을 걸었다. 그러면 이제 트랜잭선이 끝날 때까지 어떠한 S-L, X-L도 해당 데이터에 접근할 수 없다. 근데 트랜잭션 A와 B가 이미 락이 걸린 데이터에 S-L을 걸려고 한다. 이로 이로 인해 트랜잭션 A와 B 모두 블로킹에 빠져 무한대기가 발생한다.

이와 같이 A와 B 모두 블로킹이 발생하여 무한대기에 빠진 상태를 데드락이라고 한다.

보통 이런 경우 DBMS에서 자동으로 데드락을 감지하여 롤백을 진행하여 문제를 해결한다.

낙관적 락 VS 비관적 락

지금까지 낙관적 락, 비관적 락에 대해 알아보았다. 그러면 어떤 걸 쓰는 것이 좋을까?

낙관적 락은 한 트랜잭션이 데이터를 선점하고 있는 상황에서도 다른 트랜잭션이 접근할 수 있기 때문에 상황에 따라 조금 더 빠른 조회가 가능할 거 같다. 하지만 충돌이 발생했을 경우 트랜잭션을 재시도하는 로직을 작성해야하며 만약 재시도가 불가능한 로직이라면 개발자 직접 수동으로 롤백을 진행해야 한다는 불편한 점이 있다.

낙관적 락은 충돌이 빈번하게 발생하지 않고 성능이 중요한 상황에서 사용하는 것이 좋을 거 같다.

비관적 락은 한 트랜잭션에서 락을 건 상황에서 다른 트랜잭션이 락을 걸고자 할 때 블로킹이 발생하여 대기하는 시간이 존재한다. 또한 트랜잭션 모두가 블로킹이 발생한 경우 무한대기라는 데드락이 발생할 수 있다. 하지만 충돌이 발생할 경우 DB 단에서 블로킹을 통해 데이터의 정합성과 일관성을 유지해주기 때문에 개발자 입장에서는 편하다고 볼 수 있다.

비관적 락은 충돌이 빈번하게 발생하는 상황에서 데이터의 일관성과 정합성이 중요한 상황에서 사용되는 것이 좋다고 생각된다.

이 둘 중 뭐가 더 좋다기 보다는 상황에 따라 적절하게 쓰는 것이 좋다고 생각한다.

마무리

여담이지만 해당 포스트는 다른 사람들의 포트폴리오를 구경하던 중 동시성 이슈와 데드락에 대한 내용이 자주 등장해 궁금증을 해소하고자 데드락을 공부하고 정리하며 시작됐다.

데드락 하나를 제대로 이해하기 위해 트랜잭션부터 Lock 개념까지 다양한 내용을 습득하는 시간을 가졌다. 이틀이라는 시간동안 짬짬히 시간을 내어 공부하니 오랜만에 Deep하게 CS 공부를 한 거 같아 나름 뿌듯하다.

나의 공부기록이 다른 사람들에게도 도움이 되었으면 좋겠다!😊

짧지만 어려운 내용 봐주셔서 감사합니다. 모두들 파이팅!💪🏻

출처

https://gyeongsuuuu.tistory.com/68
https://ksh-coding.tistory.com/121
https://chaewsscode.tistory.com/181

profile
호기심이 많고, 문제를 끝까지 해결하려는 집념이 강한 개발자입니다.

0개의 댓글