오늘은 입사 2개월 차에 일어났던 동시성 이슈에 대해서 글을 써보려고 합니다.
동시성 이슈란 소프트웨어 개발과 실행 과정에서 여러 스레드, 프로세스 또는 작업이 동시에 실행될 때 발생하는 문제들을 가리키는 용어입니다.
이번에 회사에서 서비스하고 있는 앱에서 새로 쿠폰 뽑기 이벤트를 개발하던 중에 일어났던 일이었습니다. 회원이 이벤트 페이지에 접근하여 쿠폰 뽑기 버튼을 눌렀을 때 계정당 1회만 미리 DB에 저장된 쿠폰 번호를 발급해 주는 기능을 개발하였는데 새로 개발한 쿠폰 뽑기 기능을 QA 테스트하는 도중 비정상적인 결과가 나왔습니다.
하나의 계정으로 두 개의 디바이스에서 로그인하고 두 디바이스 모두 이벤트 페이지에 접근한 다음 동시에 쿠폰 뽑기 버튼을 클릭하여 쿠폰을 발급받는 테스트였는데 저희가 기대했던 것과 다르게 2개의 쿠폰이 발급된 상황이었습니다.
서버 로그를 확인해 보니 0.01초 동안 2개의 로직이 동시에 실행되어서 처음 들어온 쿠폰 뽑기 로직이 끝나지 않은 상태로 다시 쿠폰 뽑기 로직이 실행되어 회원 한 명에게 두 개의 쿠폰이 발급되는 동시성 이슈가 발생한 것이었습니다.
동시성 문제를 해결하기 위한 여러 방법들이 있는데 그중에서 저는 @Lock 어노테이션을 사용하여 문제를 해결했습니다.
@Lock 어노테이션이란?
스프링 프레임워크에서 @Lock 어노테이션은 데이터베이스 레코드에 대한 DB 락을 사용하여 메소드 레벨에서 동시성
제어에 사용되는 사용자 정의 어노테이션입니다. 또한, @Lock 어노테이션을 사용하여 JPA 엔티티에 대한 잠금 전략도
정의할 수 있습니다. 이를 통해, 데이터베이스 레코드의 동시 업데이트 문제를 해결할 수 있습니다.
@Lock 어노테이션을 사용하려면 메소드에 적용하고, 어노테이션의 파라미터로 락을 설정하는 방식을 정의해야 합니다.
JPA에는 낙관적 잠금과 비관적 잠금이라는 두 가지 기본 잠금 유형이 정의되어 있습니다.
낙관적 잠금 모드 ( LockModeType.OPTMISITC )
낙관적 잠금은 여러 트랜잭션 간에 충돌이 발생하지 않을 것이라고 가정하고 락을 설정합니다.
트랜잭션이 엔티티를 즉시 잠그지 않고 대신 트랜잭션은 일반적으로 엔티티에 할당된 버전 번호를 사용하여 엔티티의
상태를 저장합니다.
비관적 잠금 모드 ( LockModeType.PESSMISTIC_WRITE )
비관적 잠금은 트랜잭션에서 엔티티에 엑세스를 하면 트랜잭션을 커밋 하거나 롤백 하기 전까지는 다른 사용자가 읽기,
수정, 삭제를 불가능하게 하도록 다른 트랜잭션이 해당 레코드에 대한 읽고 쓰기를 모두 막습니다.
이번 상황은 쿠폰 코드가 이미 DB에 저장되어 있고 쿠폰 코드가 저장된 레코드에 유저 아이디를 저장하여 발급하는 방식이기 때문에 처음 조회한 쿠폰 코드를 다른 트랜잭션에서 조회할 수 없도록 비관적 잠금 모드를 사용하겠습니다.
다음 코드처럼 @Lock 어노테이션과 비관적 잠금 모드를 적용할 수 있습니다.
@Repository
public interface CouponRepository extends JpaRepository<Coupon, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Coupon findByCouponCode(String couponCode);
}
위에서 설명한 잠금 모드를 제외하고도 여러 종류의 잠금 모드를 지원하고 있습니다.
LockModeType.OPTIMISTIC_FORCE_INCREMENT
- 낙관적 잠금 모드, 데이터 수정 시 버전 필드를 증가시켜 충돌을 검사합니다.
- 다른 트랜잭션이 같은 데이터를 수정하면 버전 번호가 변경되므로 충돌이 발생합니다.
LockModeType.PESSIMISTIC_READ
- 비관적 읽기 잠금 모드, 데이터를 읽을 때 다른 트랜잭션과 충돌하지 않도록 데이터를 읽습니다.
- 다른 트랜잭션이 읽기 및 쓰기 잠금을 설정하지 못하도록 합니다.
LockModeType.PESSIMISTIC_FORCE_INCREMENT
- 비관적 쓰기 잠금 모드, 데이터를 읽고 쓸 때 다른 트랜잭션과 충돌하지 않도록 데이터를 잠급니다.
- 동시에 버전 필드를 증가시켜 충돌을 검사합니다.
여러 상황과 요구 사항에 따라서 어떤 잠금 모드를 사용할지 선택하여 정하시면 되겠습니다.
신입 개발자로서 성장할 수 있도록 동시성 이슈를 겪게 해준 QA팀에게도 감사합니다!