Home 수위키 - 비관적 락을 활용한 동시성 문제 해결 과정
Post
Cancel

수위키 - 비관적 락을 활용한 동시성 문제 해결 과정

강의평가 작성 시 비관적 락을 활용한 동시성 문제를 해결

문제 상황

A라는 강의 레코드에 X, Y 가 각각 값을 올리고 내리는 요청이 동시에 발생했다.

레코드의 값을 증가시키기위 위해 SELECT 하는 구문과, 값을 감소시키기 위해 SELECT 하는 구문이 동시에 발생했고

커밋되는 시점이 달라 값을 증가시킨 요청이 커밋되었음에도 값을 감소시키는 요청이 커밋되어 변경사항을 덮어 씌우는 현상이 발생했다.

동시성 문제를 해결하는 방법을 알아보는 중 DBMS에서 충돌이 발생하지 않는 상황을 가정하여 애플리케이션 단위에서 동시성을 처리하는 낙관적 락과 DBMS에서 충돌이 발생하는 상황을 가정하는 비관적 락의 차이를 학습하고 비관적 락을 적용하여 해결하기로 결정했다.

이 방법을 적용하게 된 근거로는 각 방식의 장/단점을 따져보았는데


낙관적 락의 장/단점

  • 실제로 Lock을 거는 것이 아니기 때문에 성능저하의 우려가 없다.
  • 하지만 충돌이 발생해서 롤백을 해야하는 경우 수동으로 해당 데이터의 내용을 모두 롤백하는 로직을 만들어야해서 복잡해질 수 있다.

비관적 락의 장/단점

  • Lock을 통해 데이터의 무결성을 보장할 수 있다. 그리고 그럼에도 충돌이 발생했을 경우에는 롤백연산을 한 번만 수행해도 된다.
  • 하지만 Lock을 거는 것으로 인한 리소스 경쟁으로 성능 저하가 발생할 수 있다.

왜 비관적 락을 적용하였는가?

  • 낙관적 락을 통해 성능까지 어느정도 가져갈 순 있으나 해당 데이터는 사용자가 애플리케이션에 접속하면 바로 볼 수 있는 꽤나 중요한 데이터이다.
    • 따라서 낙관적 락을 구현하기 위한 리소스를 투자하기보단, 즉시 적용할 수 있고 동시성 문제가 발생할 여지를 아예 방지하는 것이 목표였다.

비관적 락 중 어떤 락을 사용하였는가?

그리고 비관적 락에도 종류가 있는데 특정 레코드에 접근했을 때 다른 트랜잭션에서 읽기만 가능하게 하는 공유 락, 읽기 및 쓰기 모두 불가능한 베타 락이라는 종류가 있다.

이 차이점을 알아본 후 JPA가 지원하는 @Lock애노테이션의 PessimisticWrite옵션을 활용했다.

1
@Lock(value = LockModeType.PESSIMISTIC_WRITE)

이 옵션은 베타락을 지원하는 것으로 SELECT FOR UPDATE 구문을 보낼 수 있도록 하는 옵션이다.

SELECT FOR UPDATE는 어떤 트랜잭션이 데이터를 수정하기위해 SELECT를 할 때 다른 트랜잭션에서 접근하지 못하도록 Lock을 거는 쿼리 구문이다.

이 처럼 다른 트랜잭션에서 수정을 위해 접근하지 못하도록 해야하기 때문에 베타락을 사용했다.

참고로, SELECT FOR UPDATE 쿼리를 어떤 트랜잭션에서 사용 중이고, 커밋하지 않았다면 다른 트랜잭션에서 SELECT FOR UPDATE 구문을 사용할 수 없다.


락을 사용하지 않고 이 문제를 해결할 방법은 없었을까?

  • 애플리케이션 단위에서 처리하는 낙관적 락을 적용하는 것도 고려해볼만하다고 생각한다.

  • 혹은 쿼리를 특정 큐에 쌓아둔 후 순차적으로 처리될 수 있도록 아키텍처적으로 해결해볼만할 것 같다.

    • 큐에 쌓인 쿼리를 POP하면서 처리하게 되면 결국에 병목현상이 일어나는 것 아닌가? 그리고 어떤 쿼리가 오류로 인해 롤백해야한다면 그 시점을 모두 기다려야하지 않나?

DBMS의 격리수준으로는 어떤 것들이 있나?

  • Read Uncommitted
    • 커밋, 롤백 여부에 상관없이 다른 트랜잭션에서 읽을 수 있다.
    • Dirty Read 현상 발생 (다른 트랜잭션에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 트랜잭션에서 볼 수 있게 되는 현상)
  • Read Committed
    • 커밋이 완료된 트랜잭션의 변경사항만 다른 트랜잭션에서 읽을 수 있다.
    • Dirty Read 현상 해결
    • NON-REPEATABLE-READ 현상 발생
      • A 트랜잭션에서 SELECT
      • B 트랜잭션에서 UPDATE 후 커밋
      • A 트랜잭션에서 다시 SELECT
      • 위 상황에서 두 번의 SELECT가 같은 레코드의 값을 반환하지 않는다.
  • Repeatable Read
    • 트랜잭션이 시작되기 전에 커밋된 내용에 관해서만 조회할 수 있다.
    • NON-REPEATABLE-READ 현상 해결
    • Phantom Read 발생
      • A 트랜잭션에서 SELECT
      • B 트랜잭션에서 INSERT/DELETE 후 커밋
      • A 트랜잭션에서 다시 SELECT
      • 위 상황에서 두 번의 SELECT가 같은 레코드의 갯수를 결과를 반환하지 않는다.
  • Serializable
    • 테이블 내 모든 레코드에 접근 불가능
    • 모든 데이터 부정합 문제 해결

비관적 락으로 인해 데이터베이스의 부하 문제가 발생한다면 어떻게 할 것인가?

반복적이거나 자주 변하지 않는 데이터는 직접 DB에 접근하지 않고 캐싱하는 방법을 적용시킬 것 같다.

캐싱은 잘 변하지 않아야한다는 점이 포인트로 알고있는데 자주 변하게되면 캐싱에 반영이 안된 데이터를 읽게되는 데이터 정합성 문제가 발생할 수 있기 때문인 것으로 알고있다.

This post is licensed under CC BY 4.0 by the author.

수고 - @Async 애노테이션과 비동기 처리 시 주의점

수위키 - 사용자 인증 방식에 관하여 (Session vs Token)