대부분의 서비스는 여러 사용자가 동시에 데이터에 접근한다. 이 과정에서 같은 데이터를 동시에 수정하려 할 경우 문제가 발생할 수 있다. 이러한 문제를 동시성 문제(Concurrency Issue) 라고 하며 데이터의 정합성과 무결성을 해칠 수 있다.
데이터베이스의 동시성 문제를 해결하기 위한 대표적인 방법으로는 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)이 있다.
낙관적 락은 대부분의 트랜잭션이 충돌하지 않을 것이라고 "낙관적으로" 가정하고 설계된 방법이다. 미리 데이터베이스에 락을 걸지 않고, 최종적으로 트랜잭션을 커밋할 때 버전 충돌을 확인하여 동시성을 제어한다. 충돌이 감지되면 예외를 발생시키고 해당 트랜잭션을 롤백한다.
낙관적 락은 데이터베이스의 락 기능이 아니라 애플리케이션 단에서 버전(version)을 통해 충돌을 감지한다.
JPA에서 제공하는 @Version
어노테이션을 활용하여 구현할 수 있다.
@Entity
public class Board {
@Id
private Long id;
private String title;
@Version
private Integer version;
}
version
필드는 엔티티가 변경될 때마다 자동으로 증가한다. 업데이트 시 JPA는 이 버전을 확인하여 충돌 여부를 판단한다.
업데이트 시 버전을 체크하여 최초 조회 시의 버전과 현재 데이터베이스에 있는 버전을 비교하여 다를 경우 예외가 발생한다.
UPDATE board
SET title = ?, version = version + 1
WHERE id = ? AND version = ?
만약 버전이 일치하지 않는다면 OptimisticLockException
예외가 발생한다.
NONE
별도의 옵션 없이 @Version
필드가 존재하면 자동으로 적용된다.
OPTIMISTIC
데이터 읽기 시점부터 버전을 체크하며 트랜잭션 종료까지 버전 충돌을 검사한다.
OPTIMISTIC_FORCE_INCREMENT
데이터를 변경하지 않아도 버전을 강제로 증가시켜 충돌 발생 가능성을 높인다. 주로 읽기 작업만 수행하는 상황에서도 충돌을 명시적으로 관리하고 싶을 때 사용한다.
낙관적 락은 충돌이 발생한 시점에만 롤백을 수행한다. 충돌이 감지되면 OptimisticLockException
예외를 발생시키고 현재 트랜잭션의 변경사항을 모두 롤백하여 이전 상태로 되돌린다.
이후 충돌한 트랜잭션은 개발자가 직접 다시 실행시키거나 사용자에게 충돌 상황을 알려 재시도를 요청하는 방식을 취한다.
비관적 락은 동시성 충돌이 자주 발생할 것으로 "비관적으로" 가정하고 설계된 방식이다. 데이터를 접근할 때 미리 락을 걸어 다른 트랜잭션의 접근을 차단한다. 즉, 충돌 가능성을 사전에 방지한다.
비관적 락은 데이터 접근 시점부터 락을 걸기 때문에 데이터의 일관성과 무결성이 매우 중요한 환경에서 주로 사용된다.
PESSIMISTIC_READ
공유 잠금(Shared Lock)을 획득한다. 다른 트랜잭션에서는 읽기 작업만 가능하고 수정 작업은 불가능하다.
PESSIMISTIC_WRITE
배타적 잠금(Exclusive Lock)을 획득한다. 다른 트랜잭션에서는 읽기 및 수정 작업이 모두 불가능하다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Coupon c WHERE c.id = :id")
Coupon findByIdForUpdate(@Param("id") Long id);
이렇게 하면 해당 데이터에 락을 걸고 다른 트랜잭션이 접근하지 못하도록 한다.
PessimisticLockException
락을 획득하는 데 실패했을 때 발생하는 예외
LockTimeoutException
락 획득을 기다리는 시간이 설정된 시간을 초과했을 때 발생하는 예외
비관적 락은 충돌이 아닌 락 획득 실패나 락 대기 초과 시 트랜잭션을 즉시 롤백한다. 락이 획득되지 않으면 해당 트랜잭션이 즉시 종료되고 이전 상태로 복원된다. 따라서 개발자는 타임아웃 시간을 적절히 설정하고 락 획득 실패 시 재시도하거나 사용자에게 안내하는 처리를 해줘야 한다.
어느 한 가지 방식이 무조건 좋다고 할 수는 없다. 즉, 서비스의 특성에 맞게 선택해야 한다.
서비스의 동시성 충돌 빈도, 성능 요구 사항, 데이터 일관성의 중요도를 고려하여 적절한 락 방식을 결정하는 것이 핵심이다.