본문 바로가기
Database

DB | DB Locking과 Optimistic Lock/Pessimistic Lock

by Hoya324 2023. 9. 20.

  • 락은 데이터의 일관성과 무결성을 유지하기 위해 DBMS가 사용하는 공통적인 방법이다.
  • DB와 같은 시스템은 같은 데이터를 동시에 접근하는 경우가 생길 수밖에 없는데, 이럴 경우 데이터가 오염될 수 있다.

락의 크기

  • 락의 크기는 어느 정도의 범위를 잠글 것인지 나타낸다.
    • 로우 락(Row Lock), 페이지 락(Page Lock), 테이블 락(Table Lock)이 있다.
  • 만약 락을 걸어야 하는 페이지가 너무 많다면, 차라리 테이블 전체에 락을 거는 것이 잠금 비용의 손실을 줄일 수 있다.
    • 잠금 비용: 잠금을 거는 과정에서 발생하는 성능 손실을 말한다.
  • 페이지 락 여러 개를 테이블 하나로 대체한다면 잠금 비용은 낮아지겠지만, 대신 동시성 비용은 높아질 것이다.
    • 동시성 비용: 락을 걸면서 동시성이 낮아져서 발생하는 성능 손실을 의미한다.

락의 종류

공유잠금

  • 공유잠금(Shared Lock)은 가장 낮은 강도의 잠금으로, 데이터를 읽을 때 사용되는 락이다.
  • 일반적으로 SELECT를 할 때 공유잠금이 발생하며, SELECT가 완료되면 즉시 공유잠금은 해제된다.
  • 이런 공유잠금은 공유잠금끼리 동시에 접근이 가능하다.
  • 즉, 하나의 데이터를 읽는 것은 여러 사용자가 동시에 할 수 있다.
  • 하지만 공유잠금이 설정된 데이터에 배타잠금을 사용할 수는 없다.

배타잠금

  • 배타잠금(Exclusive Lock)은 가장 높은 강도의 잠금이다.
  • 데이터를 변경하고자 할 때 사용되며, 트랜잭션이 완료될 때까지 유지된다.
  • 배타잠금이 해제될 때까지 다른 트랜잭션은 해당 리소스를 접근할 수 없다.
  • 또한 해당 락은 다른 트랜잭션이 수행되고 있는 데이터에 대해서는 접근하여 함께 락을 설정할 수 없다.
  • 배타잠금은 다른 모든 종류의 잠금과 호환되지 않는다.
  • 즉, 어떠한 약한 잠금이라도 걸려 있는 레코드에 대해서는 Update가 불가능하며, 반대로 Update가 진행 중인 레코드에 대해서는 SELECT를 포함한 어떠한 작업도 불가능하다.

갱신잠금

  • 갱신잠금(Update Lock)은 공유잠금과 배타잠금의 중간 강도의 잠금이다.
  • 공유잠금과는 호환되지만 다른 갱신잠금이나 배타잠금과는 호환되지 않는다.
  • 일반적으로는 Update의 Filter(Where절)가 수행되는 단계에서 갱신잠금이 걸리며, Filter 된 결과에 대해 실제로 Update를 시도할 때 갱신잠금은 배타잠금으로 전환된다.
  • 공유잠금과 배타잠금이 서로 호환되지 않기 때문에 공유잠금에서 배타잠금으로 변환될 때 락 대기시간(Lock Wait)이 발생한다.
  • 이 대기 시간은 공유잠금이 해제될 때까지의 시간을 의미하는데, 이때 잠재적인 교착상태에 빠질 가능성이 생긴다.
  • 예를 들어 업데이트(Update)를 위해 공유잠금 모드에서 배타잠금 모드로 변환하려고 하는데, 또 다른 트랜잭션이 업데이트를 위해 공유잠금을 필요로 하는 경우, 공유잠금을 해제하려는 트랜잭션과 필요로 하는 트랜잭션 사이에 교착 상태가 된다.
  • 이런 교착 상태를 해결하기 위해서 사용되는 락이 갱신잠금 모드이다.
  • 갱신잠금은 잠금 힌트를 통해 업데이트문이 아닌 SELECT문에도 걸 수 있다. 보통 컨버전 데드락을 방지하기 위해 SELECT문에 갱신잠금을 거는 경우가 많다.

집중잠금

  • 집중잠금(Intent Lock)은 SQL 서버에서 데이터베이스나 테이블 내의 일부 데이터 영역에 이미 공유잠금이나 배타잠금이 걸려 있다는 것을 다른 커넥션에게 알리기 위해서 사용한다.

DB 충돌 상황을 개선할 수 있는 방법

  • 첫번째, 테이블의 row에 접근시 Lock을 걸고 다른 Lock이 걸려 있지 않을 경우에만 수정을 가능하게 할 수 있다.
  • 두번째, 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것이다.

비관적 락(pessimistic lock)

  • 비관적 락은 Repeatable Read 또는 Serializable 정도의 격리성 수준을 제공한다.
    • SERIALIZABLE은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시킨다.
    • REPEATABLE READMVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)를 이용해 한 트랜잭션 내에서 동일한 결과를 보장하지만, 새로운 레코드가 추가되는 경우에 부정합이 생길 수 있다.
  • 비관적 락이란 트랜잭션이 시작될 때 Shared Lock(공유락) 또는 Exclusive Lock(배타락)을 걸고 시작하는 방법이다.
  • 즉, Shared Lock(공유락)을 걸게 되면 write를 하기위해서는 Exclucive Lock(배타락)을 얻어야하는데 Shared Lock(공유락)이 다른 트랜잭션에 의해서 걸려 있으면 해당 Lock을 얻지 못해서 업데이트를 할 수 없다.
  • 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(commit) 되어야합니다.

  1. Transaction_1 에서 table의 Id 2를 읽음 ( name = Karol )
  2. Transaction_2 에서 table의 Id 2를 읽음 ( name = Karol )
  3. Transaction_2 에서 table의 Id 2의 name을 Karol2로 변경 요청 ( name = Karol )
    • 하지만 Transaction 1에서 이미 shared Lock을 잡고 있기 때문에 Blocking
  4. Transaction_1 에서 트랜잭션 해제 (commit)
  5. Blocking 되어있었던 Transaction_2의 update 요청 정상 처리

이렇듯 Transaction을 이용하여 충돌을 예방하는 것이 바로 비관적 락(Pessimistic Lock)입니다.

낙관적 락(optimistic lock)

  • 낙관적 락은 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것입니다.
  • 이 특징은 DB에서 제공해주는 특징을 이용하는 것이 아닌 Application Level에서 잡아주는 Lock입니다.

  1. A가 table의 Id 2를 읽음 ( name = Karol, version = 1 )
  2. B가 table의 Id 2를 읽음 ( name = Karol, version = 1 )
  3. B가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol2, version = 2 ) 성공
  4. A가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol1, version = 2 ) 실패
    • Id 2번은 이미 version이 2로 업데이트 되었기 때문에 A는 해당 row를 갱신하지 못함
  • 위 flow를 통해서 같은 row에 대해서 각기 다른 2개의 수정 요청이 있었지만 1개가 업데이트 됨에 따라 version이 변경되었기 때문에 뒤의 수정 요청은 반영되지 않게 되었다.
  • 이렇게 낙관적 락은 version과 같은 별도의 컬럼을 추가하여 충돌적인 업데이트를 막는다.
  • version 뿐만 아니라 hashcode 또는 timestamp를 이용하기도 한다.

낙관적 락은 version 등의 구분 컬럼을 이용해서 충돌을 예방한다.

롤백(Rollback)

  • 만약 업데이트를 하는 테이블이 1개가 아니라 2개의 테이블이고, 2번째 테이블을 업데이트하다 이와같은 충돌이 발생했다면 하나의 수정 요청에 대해서는 롤백이 필요하게된다.

2개의 테이블을 수정하는 비관적 락의 수도코드

  • 하나의 트랜잭션으로 묶여있기 때문에 수정이 하나 실패하면 database 단에서 전체 Rollback이 일어나게된다.
  • 만약 theTable이 실패할 때. Transaction이 실패한 것이기 때문에 트랜잭션 전체에 자동으로 rollback이 일어나게 된다.
SELECT id, `name`
     FROM theTable
     WHERE id = 2;
{새로운 값으로 연산하는 코드}
BEGIN TRANSACTION;
UPDATE anotherTable
     SET col1 = @newCol1,
         col2 = @newCol2
     WHERE id = 2;
UPDATE theTable
     SET `name` = 'Karol2',
     WHERE id = 2;
{if AffectedRows == 1 }
    COMMIT TRANSACTION;
    {정상 처리}
{else}
    ROLLBACK TRANSACTION;
    {DB 롤백 이후 처리}
{endif}

낙관적 락의 수도코드

  • 비관적 락과 달리 Transaction으로 잡지 않는다.
  • 만약 충돌이 발생하여 수정을 못한 부분에 대해서는 롤백에 대한 책임을 Application 단에서지며 Application에서 롤백을 수동으로 해줘야 한다.
SELECT id, `name`, `version`
     FROM theTable
     WHERE iD = 2;
{새로운 값으로 연산하는 코드}
UPDATE theTable
     SET val1 = @newVal1,
         `version` = `version` + 1
     WHERE iD = 2
         AND version = @oldversion;
{if AffectedRows == 1 }
    {정상 처리}
{else}
    {롤백 처리}
{endif}

낙관적 락과 비관적 락을 언제 사용할까?

  • 낙관적 락은 트랜잭션을 필요로하지 않는다. 따라서 성능적으로 비관적 락보다 더 좋다. → 낙관적 락의 장점
    • 트랜잭션을 필요로 하지 않기 때문에 아래와 같은 로직의 흐름을 가질때도 충돌 감지를 할 수 있다.
    1. 클라이언트가 서버에 정보를 요청
    2. 서버에서는 정보를 반환
    3. 클라이언트에서 이 정보를 이용하여 수정 요청
    4. 서버에서는 수정 적용 ( 충돌 감지 가능 )
  • 반면 비관적 락은 1~3에서 트랜잭션을 유지할 수 없다.

따라서 충돌이 많이 일어나지 않을 것이라고 보여지는 곳에 사용하면 좋은 성능을 기대할 수 있다.

  • 하지만 낙관적 락의 최대 단점은 롤백이다.
  • 만약 충돌이 났을 때,이를 해결하려면 개발자가 수동으로 롤백처리를 해줘야합니다.
  • 비관적 락이라면 트랜잭션을 롤백하면 끝나는 작업이지만 낙관적 락은 그렇지 않다.
  • 수동으로 롤백처리는 구현하기도 까다롭고, 성능적으로 볼 때, update를 한번씩 더 해줘야한다.
  • 결론적으로 충돌 발생 확률이 낮고 성능 저하를 예방하려면 낙관적 락을 사용하면 되고, 충돌을 미연에 방지하고 데이터의 일관성을 유지하려면 비관적 락을 사용하면 된다.

JPA에서의 낙관적 락과 비관적 락

낙관적 락

@Entity
public class SampleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private Long version;

    private String data;
}
  • @Version 어노테이션을 이용하여 version 필드로 지정
  • 데이터를 수정할 때 같은 id 값이지만 다른 사용자에 의한 변경이 발생하면 version 값이 다르게 되고, 이때 예외가 발생하므로 충돌로부터 안전하게 처리할 수 있다.

비관적 락

@Service
public class SampleEntityService {
    @Autowired
    private SampleEntityRepository sampleEntityRepository;

    @Autowired
    private EntityManager entityManager;

    @Transactional
    public SampleEntity updateDataWithPessimisticLock(Long id, String newData) {
        SampleEntity sampleEntity = entityManager.find(SampleEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
        sampleEntity.setData(newData);
        entityManager.flush();
        return sampleEntity;
    }
}
  • EntityManager의 find 메소드에 락 타입(LockModeType.PESSIMISTIC_WRITE)을 지정하여 데이터에 락을 걸어두고, 변경 작업이 끝난 후에 락을 해제한다.
  • 이를 통해 다른 트랜잭션이 동시에 수정할 수 없어 동시성 처리 이슈를 방지할 수 있게 된다.

Reference

https://mangkyu.tistory.com/299

https://mozzi-devlog.tistory.com/37

https://hudi.blog/jpa-concurrency-control-optimistic-lock-and-pessimistic-lock/

https://mangkyu.tistory.com/299

https://sabarada.tistory.com/175

'Database' 카테고리의 다른 글

DB | Join  (0) 2023.09.20
DB | 트랜잭션, 동시성 제어, 회복  (1) 2023.08.28
DB | 정규화  (0) 2023.08.28
DB | 데이터 모델링  (1) 2023.08.28
DB | 데이터베이스 프로그래밍  (1) 2023.08.27