Lock

bearMin·2024년 6월 27일

들어가면서

이전 블로그 내용과 마찬가지로 Lock 역시 이번 프로젝트에서 사용을 하게 되는데, Lock의 개념만 알고 있지 사실 제대로 아는 것 같지도 않다.. 어노테이션으로 사용을 해본 적이 없기에 이번 기회에 공부를 제대로 해보자는 생각으로 블로그를 작성하게 되었다.

Lock이란?

정의는?

역시 마찬가지로 Lock Annotation에 대해 알아보기에 앞서 Lock이 무엇인지에 대해 먼저 알아볼 필요가 있다.

잠그다, 가두어 잠그다

라고 영어 사전에서는 나와있다. 어떻게 사용을 하길래 잠그다라는 영어 단어를 사용하는 것일까?

트랜잭션 처리의 순차성을 보장하기 위한 방법

이전 블로그 내용에서 나왔던 트랜잭션이 이번에도 계속 등장하게 된다. 저번 블로그에서도 얘기했지만 트랜잭션이란 동시에 이루어져야 하는 행위를 하나로 묶은 단위를 뜻한다.

만약 트랜잭션이 궁금하다면 이전에 포스팅한 Transaction 을 참고하기 바란다.


트랜잭션 처리의 순차성을 보장한다는 뜻은 무엇일까?

생각해보면 간단하다. 트랜잭션은 단위를 뜻한다고 했다. 방법이 아닌 것이다. 그렇다면 이 트랜잭션이 진행하기 위해선 어떻게 해야 하느냐! 라고 물어봤을 때 사용하는 방법이 Lock인 것이다.

조금 더 쉽게 설명을 해보자면 여러 사용자들(트랜잭션)들이 동시에 같은 데이터에 접근하려고 할 때 우리는 Lock 이라는 것을 사용해서 사용하고 있는 사용자(트랜잭션)와 다른 사용자(트랜잭션)들 간의 접근을 막아 충돌을 방지하는 것이다. 이때 이 접근을 막는 것을 데이터를 잠근다라고 표현하는 것이다.

교착상태란?

Lock에 대해서 설명 중인데 뜬금없이 이 친구는 무엇인가 싶을 것이다. 근데 또 어디선가 들어본 친구라 익숙하기도 할 것 같다.

항상 하는 말이지만 어떤 것이든 장점이 있으면 단점이 있기 마련이다. 트랜잭션의 순차성을 보장하기 위해 데이터에 Lock을 건다고 하는데 그렇다면 과연 문제는 없을까?

당연히 문제가 생길 수 있다.

그 예시가 바로 교착 상태 또는 데드락이라고 하는데 이 교착 상태란 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 아무것도 완료되지 못하는 상태를 말한다.


말로만 하면 어려우니 예시를 들어서 설명을 해보겠다.

트랜잭션 A와 트랜잭션 B가 있다고 할 때

  1. 트랜잭션 A는 Table C에 Lock을 걸어놨다.
  2. 트랜잭션 B는 Table D에 Lock을 걸어놨다.
  3. 트랜잭션 A는 Table D에 있는 데이터를 읽고 Table C를 수정해야한다.
  4. 트랜잭션 B는 Table C를 읽고 Table D를 수정해야한다.

이때 생길 수 있는 문제점은 트랜잭션 A는 Table D를 읽으려고 접근해도 트랜잭션 B가 Lock을 걸어놨기 때문에 접근이 불가능하다. 마찬가지로 트랜잭션 B도 Table C를 읽으려고 접근했지만 트랜잭션 A가 이미 Lock을 걸어놨기 때문에 접근이 불가능하다. 따라서 서로 상대방이 Lock을 풀어주길 기다리다가 영원히 처리가 되지 않게 되는 상태교착 상태라고 한다.

해결방법은?

이러한 문제점이 있으니 Lock은 사용하면 안돼! 라는 뜻은 아니다. 이러한 문제점이 생길 수도 있다는 것을 알고 사용을 해야 나중에 문제가 생기더라도 금방 해결을 할 수 있다는 뜻이다.

그렇다면 해결할 수 있는 방법에 대해서 설명을 하겠다.


우선 첫번째로 Dirty Read가 있다.

Dirty Read란 데이터가 변경되었지만 아직 커밋이 되지 않은 상황에서 다른 트랜잭션이 해당 변경 사항을 조회할 수 있는 문제를 뜻한다. 문제인데 오히려 해결 방법이 된다? 데이터의 정확도가 중요하지 않다면 빠르게 해결할 수 있는 방법이다.


두번째로는 예방 방법이 있다.

트랜잭션이 실행되기 전에 필요한 데이터를 모두 lock을 하고 모두 lock을 하지 못한다면 unlock을 통해 반납하는 방식을 사용하여 확실하게 실행할 수 있을 때만 트랜잭션을 실행하는 것이다. 대신 어떤 트랜잭션은 무한정 수행되지 않는 기아 현상이 발생할 수 있다.

또는 LOCK_TIMEOUT 설정을 사용하여 예방할 수 있다.

Lock의 최대 시간을 설정하여 시간이 지나면 Lock을 해제하도록 해 데드락을 예방할 수 있다. 다만 이 역시 근본적인 해결방법은 아니다.


세번째로는 회피 방법이 있다.

Time Stamp를 사용하여 데드락을 회피할 수 있는데, Wait-Die와 Wound-Wait 방법이 있다.

Wait-Die비선점 기법으로 Time Stamp가 더 빠른 트랜잭션이 Time Stamp가 느린 트랙잭션이 점유한 자원에 lock을 걸면 기다린다. 그러나 Time Stamp가 느린 트랜잭션이 Time Stamp가 더 빠른 트랜잭션이 점유한 자원에 lock을 걸면 롤백 후 재시도를 한다.

Wound-Wait이란 선점 기법으로 Time Stamp가 더 빠른 트랜잭션은 기다리지 않는다. 만약 빠른 트랜잭션이 느린 트랜잭션이 점유한 lock을 요청하면 더 느린 트랜잭션의 lock을 뺏고 롤백시킨다. 더 느린 트랜잭션이 더 빠른 트랜잭션이 점유한 자원에 lock을 요청하면 기다린다.

Lock 처리방법은?

구현방법은?

Lock이란건 무엇이고 어떻게 사용을 해야한다는 걸 알았으니 이젠 직접 코드 구현으로 들어가보자

Lock 역시 어노테이션으로 처리를 할 수 있는데 주의할 점은 Lock에는 낙관적 잠금비관적 잠금이 있다는 것이다.

낙관적 잠금(Optimistic Lock)이란?

정의는?

충돌이 발생하지 않을 것이라 가정하고 Lock을 거는 방식

의미를 몰라도 충분히 유추할 수 있다. 말 그대로 낙관적으로 생각하면 된다.

충돌 안 나겠지~ 라고 생각하면서 Lock을 걸어주는 방식인데 트랜잭션을 커밋하는 시점에서 충돌을 알 수 있다. 이때 충돌은 다중 트랜잭션이 데이터를 동시에 수정하지 않는다고 가정하는 것이다. 따라서 데이터를 읽을 때 Lock을 설정하지 않는다.


장점은 락의 잠금이 없기 때문에 성능이 좋다. 따라서 동시 업데이트가 없는 경우에 이 방법을 사용하면 이후에 나올 비관적 잠금보다 빠르게 조회 및 업데이트를 할 수 있다.

하지만 단점으로는 여러 트랜잭션들이 작업을 하고 있을 때 하나의 트랜잭션이 데이터를 변경한다면 다른 트랜잭션들의 작업이 거부되어 오류 혹은 재시도 처리를 해야한다.

이때, 낙관적 잠금의 경우 UPDATE가 실패해도 자동으로 예외를 던지지 않기 때문에 개발자가 직접 예외처리를 해주어야 한다.

구현 방식은?

@Version
private int version;

Version 어노테이션을 사용해서 적용할 수도 있고 이후 아래에서 나올 LockModeType에서 명시적으로 선언해서 적용할 수도 있다.

주의해야할 점은 @Version을 사용할 경우 해당 테이블에 version 필드를 생성해주어야 한다!

위의 어노테이션을 사용할 경우 수정이 될 때 자동으로 버전을 상승시키는데 그 버전이 저장되는 필드가 필요하기 때문이다. 조회시점과 버전이 다른 경우 OptimisticLockException 예외를 발생시킨다.


그렇다면 생각해야할 것은 만일 동시에 여러 번의 변경사항이 생겼을 경우 어떻게 되는가? 이다.

이에 대한 대답으로는 조회시점과 버전이 다른 경우에는 예외를 발생시키기 때문에 동시에 데이터의 변경을 요청했을 경우 최초의 커밋만 성공하고 나머지는 실패한다.

만약 5라는 데이터에 1 감소하는 요청을 동시에 5번 보낸다고 했을 때 최초의 커밋만 성공하고 나머지는 실패하기 때문에 조회되는 값은 4가 될 것이다.

비관적 잠금(Pessimisitic Lock)이란?

정의는?

트랜잭션끼리 충돌이 발생한다고 가정하고 우선적으로 Lock을 거는 방식

위의 낙관적의 반대로 생각하면 된다. 무조건 트랜잭션끼리 충돌이 발생할거야! 라고 비관적으로 생각하여 우선적으로 Lock을 걸어주는 방식으로 특정 데이터 항목을 읽거나 수정하기 전에 해당 데이터에 대한 잠금을 요청하고 트랜잭션이 완료될 때 잠금을 해지한다.


장점은 한 번에 하나의 트랜잭션만이 데이터에 접근하고 수정할 수 있으므로 데이터 불일치 및 데이터 충돌 문제를 해소할 수 있다. 또한 트랜잭션이 시작된 순간부터 충돌을 방지할 수 있다.

하지만 단점으로는 교착 상태에 빠질 우려가 있으니 주의를 기울여서 적용시켜야한다. 또한 잠금 시간이 길어질수록 성능에 대한 문제가 발생할 수 있다.

구현 방식은?

@Lock(LockModeType.PESSIMISTIC_WRITE)
public void test() {
			// 로직 구현
}

Lock 어노테이션을 사용하여 적용할 수 있다.

Lock 어노테이션은 LockModeType을 설정할 수 있는데 각각의 옵션들을 살펴보면

  • NONE
    ⇒ 잠금 모드를 지정하지 않으며 다른 트랜잭션이 데이터에 접근할 수 있다.

  • OPTIMISTIC
    ⇒ 낙관적 잠금을 수행하며 엔티티를 조회할 때 잠금을 설정하지 않는다. 트랜잭션 커밋 시에 충돌을 감지한다.

  • OPTIMISTIC_FORCE_INCREMENT
    ⇒ 낙관적 잠금을 수행하면서, 버전의 정보를 강제로 증가시킨다.

  • PESSIMISTIC_READ
    ⇒ 비관적 읽기 잠금을 수행한다. 다른 트랜잭션에서 읽기 작업은 가능하지만 변경 작업은 불가능하다. 공유 잠금(Share Lock)이라고도 한다.

  • PESSIMISTIC_WRITE
    ⇒ 일반적인 옵션이다. 비관적 쓰기 잠금을 수행한다. 다른 트랜잭션에서 데이터를 읽거나 수정하는 것을 차단한다. 배타적 잠금(Exclusive Lock)이라고도 한다.

  • PESSIMISTIC_FORCE_INCREMENT
    ⇒ 비관적 쓰기 잠금을 수행하면서, 버전 관리 컬럼을 증가시킨다. 비관적 락이지만 버전의 정보를 강제로 증가시킨다.

그래서 결론이 뭐야?

Lock은 사용자들(트랜잭션)들이 동시에 같은 데이터에 접근하려고 할 때 사용하고 있는 사용자(트랜잭션)와 다른 사용자(트랜잭션)들 간의 접근을 막아 충돌을 방지하는 것이다!

Lock은 데이터의 일관성과 무결성을 유지할 수 있지만 무작정 Lock을 걸어둔다면 성능 저하와 교착상태에 이르게 되므로 주의해서 사용해야한다!

@Version은 낙관적 잠금이고, @Lock은 비관적 잠금일 때 주로 사용한다!

낙관적 잠금동시성이 중요하고 충돌이 잘 발생하지 않을 것으로 예상될 때,

비관적 잠금데이터의 일관성과 동시성이 중요하고 충돌이 발생할 가능성이 높은 경우에 적합하다!

profile
소소한 공부기록

0개의 댓글