비관적 락과 낙관적 락, 둘 중에 어떤것을 적용할까?

2025. 2. 28. 10:58Project/SNS

SNS프로젝트를 하다가, 특정 인기 게시물에 대한 좋아요 개수가 multi-thread환경에서 올바르게 증가되지 않는 현상을 발견하였다.

인기 게시물의 경우, 수십명이 좋아요를 누르게 될 것이다. 여러 Thread가 좋아요 컬럼에 update를 하다가 여러 경합이 발생했을 것이라고 판단하였다.

이 문제를 해결하고자 비관적락을 도입하여 좋아요 개수가 올바르게 증가하도록 하였다.

이번 글에서는 비관적 락과 낙관적 락의 차이에 대해 설명하고, 왜 비관적 락을 도입했는지 설명하고자 한다.

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

DB에 접근해서 데이터를 수정할 때, 위 SNS프로젝트처럼, 동시에 수정이 일어나 충돌이 일어날 수 있다.

이를 해결할 수 있는 방법으로 크게 2가지를 제시한다.

첫번째, 테이블의 Row에 접근시, Lock을 걸고 다른 Lock이 걸려 있지 않을 경우에만 수정을 가능하게 할 수 있다. → 비관적 락

두번째, 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것이다. → 낙관적 락


비관적 락

비관적 락은 Repeatable Read 또는 Serializable 정도의 격리 수준을 제공한다.

비관적 락이란 트랜잭션이 시작될 때, Shared Lock 또는 Exclusive Lock을 걸고 시작하는 방법이다. 즉, Shared Lock을 걸게 되면, write를 하기 위해 Excluesize Lock을 얻어야하는데, Shared Lock이 다른 트랜잭션에 의해 걸려있으면, 해당 Lock을 얻지 못해 업데이트를 할 수 없다. 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료되어야한다.

 

Exclusive Lock

  • 쓰기 잠금이라고 불린다.
  • 어떤 트랜잭션에 데이터를 변경하고자 할때, 해당 트랜잭션이 완료될때까지, 해당 테이블 혹은 Row를 다른 트랜잭션에 읽거나 쓰지 못하게 하기 위해 Exclusize Lock을 걸고 트랜잭션을 진행시키는 것이다.
  • Exclusive Lock은 SELECE, FOR UPDATE, DELETE 등의 수정 쿼리를 날릴때, 각 Row에 걸리는 Lock이다.
  • Exclusize Lock이 걸리면 Shared Lock을 걸 수 없다.
  • Exclusize Lock이 걸린 테이블, Row 등의 자원에 대해 다른 트랜잭션이 Exclusize Lock을 걸 수 없다.
Shared Lock
  • 읽기 잠금이라고 불린다. ****- 어떤 트랜잭션에서 데이터를 읽고자 할때 Shared Lock은 허용이 되지만, Exclusive Lock은 불가능하다. → 리소스를 동시에 다른 사용자가 읽을 수 있지만, 변경을 불가능하게 하는 것이다.
  • 어떤 자원에 Shared Lock이 동시에 여러개 적용될 수 있다.
  • 어떤 자원에 Shared Lock이 하나라도 걸려있으면 Exclusive Lock을 걸 수 없다.

만약 Lock을 걸어야할 페이지가 많다면, 그럴바에 테이블 전체에 Lock을 걸어버리는 편이 한번에 처리하니까 잠금 비용이 낮아져 효율적이다. 하지만, Lock의 범위가 넓어질 수록 동시에 접근할 수 없는 자원이 많아지므로, 동시성 비용이 높아져 효율이 떨어진다.

 

위의 도식도를 보면서 비관적락에 대해 알아보자.

  1. Transaction 1에서 table의 ID2번을 읽었다. (name = karol)
  2. Transaction 2에서 table의 ID2번을 읽었다. (name = karol)
  3. Transaction 2에서 table의 ID2번의 name을 karol2로 변경 요청을 하였으나,
  4. 하지만 Transaction 1에서 이미 Shared Lock을 걸고 있기때문에 변경 불가능, Blocking처리됨.
  5. Transaction 1에서 트랜잭션 해제 (commit)
  6. Blocking되어있던 Transaction2의 update요청을 정상 처리할 수 있었다.

이렇듯 Transaction을 이용하여 충돌을 예방하는 것이 비관적 락이다.


낙관적 락

낙관적 락은 DB 충돌 상황을 개선할 수 있는 방법 중 2번째인 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 동일한 조건으로 값을 수정할 수 없게 하는 것이다. 그런데 잘 보면, 이 특징은 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 = karol2, version = 2 → 실패)
  5. → ID 2번은 이미 version 2로 업데이트 되었기때문에 A가 version=1을 기준으로 조건을 걸어 업데이트를 하면 row을 갱신하지 못함.

위 Flow를 통해서 같은 Row에 대해 각기 다른 2개의 수정요청이 있었지만, 1개가 업데이트 됨에 따라 version이 변경되었기 때문에 뒤의 수정요청은 반영되지 않게 되었다. 이렇게 낙관적락은 version과 같은 별도의 컬럼을 추가하여 충돌적인 업데이트를 막을 수 있다. version 뿐만 아니라 hashcode 또는 timestamp를 이용하기도 한다.

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


비관적 락 Rollback

만약 업데이트를 하는 테이블이 1개가 아니라 2개의 테이블이며, 2번째 테이블을 업데이트하다 이와 같은 충돌이 발생했다면 하나의 수정 요청에 대해서는 롤백이 필요하게 된다. 비관적 락과 낙관적 락이 각각 어떻게 롤백하는지 알아보도록 하자.

 - 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}

위는 2개의 테이블을 수정하는 비관적 락의 수도코드이다. 하나의 트랜잭션으로 묶여있기 때문에 수정이 하나 실패하면 DB단에서 전체 Rollback이 일어나게 된다. 만약 TheTable이 실패한다고 생각해보자.

그렇다면 Transaction이 실패한 것이기 때문에 Transaction전체에 자동으로 Rollback이 일어나게 된다.


낙관적 락 Rollback

낙관적 락의 수도코드는 아래와 같다. 코드를 보면 알겠지만, Transaction으로 잡지 않는다. 그렇기 때문에 충돌이 발생하여 수정을 못한 부분에 대해서는 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}

각각 어떤 경우에 효과적일까?

낙관적 락과 비관적 락이 어떤 개념이며 어떤 롤백 처리방식을 가지고 있는지 알아보았다.

그렇다면 어느 경우에 낙관적 락을 사용하고 또 어떤 경우에 비관적 락을 사용하면 좋을까?

낙관적 락은 데이터를 업데이트 하기 전, 조회하면서 Lock을 거는 작업이 필요없다. 따라서 비관적락보다 성능면에서 좋다. 그리고 낙관적 락은 트랜잭션을 필요로 하지 않는다. (트랜잭션이 필요하면 아무래도, DB커넥션을 오래 점유하지 않게 되어, 동시 처리 능력이 향상이 된다. 또, DeadLock위험이 감소하게 된다.) 이 두가지가 비관적 락에 비해 가지는 낙관적 락의 최대 강점이다.

하지만, 낙관적 락의 최대 단점은 롤백이다. 만약 충돌이 났다고 하면, 이를 해결하려면 개발자가 수동으로 롤백처리를 한땀한땀 해줘야한다. (비관적락이라면 트랜잭션을 롤백하면 끝나는 작업임.)

이러한 단점때문에 낙관적 락은 충돌이 많이 예상되거나 충돌이 발생했을 때 비용이 많이 들것이라고 판단되는 곳에서는 사용하지 않는 것이 좋을 것이다.


그래서 어떤거 적용할까?

인기 게시물의 경우, 수십명이 좋아요를 누르게 될 것이기때문에 충돌이 많이 발생할 것이라고 판단이 들었다. 충돌이 많이 발생했을 때, 개발자가 수동으로 롤백처리를 해줘야하기 때문에 비용이 많이 들것이라고 판단이 들었다. 이에 따라, 비관적 락을 도입하게 되었다.