프로젝트에서 분산 락 처리를 비즈니스 로직과 분리하기 위해Spring AOP와 커스텀 애노테이션(@RedisLock)을 활용한 구조로 구현하였다.
@RedisLock 애노테이션을 메서드에 선언하면, AOP의 @Around 어드바이스가 동작하여 다음 흐름을 수행하도록 설계하였다.
Redis 락 획득 시도
락 획득 성공 시 비즈니스 로직 실행 (joinPoint.proceed())
실행 종료 후 finally 블록에서 락 해제
이를 통해 서비스 계층에서는 락 관련 코드 없이 비즈니스 로직에만 집중할 수 있도록 관심사를 분리하였다.
분산 락은 동일 자원에 대한 동시 접근만 제어해야 하므로, 메서드마다 다른 리소스를 기준으로 락을 생성할 필요가 있다.
이를 해결하기 위해 본 프로젝트에서는 SpEL(Spring Expression Language)을 활용하여 Lock Key를 동적으로 생성하였다.
애노테이션 사용 예시는 다음과 같다.
@RedisLock(prefix = "event", key = "#eventId")
public void issueCoupon(AuthUser authUser, Long eventId)
AOP에서는 ProceedingJoinPoint를 통해 메서드의 파라미터 이름과 값을 추출한 뒤, StandardEvaluationContext와 SpelExpressionParser를 이용하여 #eventId, #request.eventId 등의 표현식을 실제 값으로 평가하였다.
최종적으로 생성되는 락 키는 다음과 같은 형태이다.
lock:event:1
lock:coupon:5
이와 같은 방식으로 리소스 단위로 락을 분리할 수 있으며, 서로 다른 이벤트나 쿠폰 요청이 불필요하게 서로를 блок킹하지 않도록 설계하였다.
AOP의 @Around 어드바이스에서는 락을 먼저 획득한 뒤 비즈니스 로직을 실행하는 구조로 구현하였다.
처리 흐름은 다음과 같다.
Redis 락 획득 시도
락 획득 성공 시 joinPoint.proceed() 호출
내부에서 @Transactional이 적용된 서비스 로직 실행
메서드 종료 후 finally 블록에서 락 해제
즉,
락 획득 → 트랜잭션 시작 및 비즈니스 로직 실행 → 트랜잭션 종료 → 락 해제
순서로 동작하도록 구성하였다.
이러한 구조를 통해 동일 자원에 대한 동시 요청을
트랜잭션 시작 이전 단계에서 차단할 수 있으며,
데이터 정합성을 보다 안정적으로 보장할 수 있다.
Redis 락은 DB 트랜잭션과 별개로 동작하기 때문에, 트랜잭션이 롤백되더라도 자동으로 해제되지 않는다.
이를 해결하기 위해 다음과 같은 방식을 적용하였다.
finally 블록에서 락 해제를 수행하여 예외 발생 여부와 관계없이 항상 해제
Lua 스크립트를 사용하여 현재 스레드가 획득한 락인지 검증 후에만 삭제
이를 통해 락 누수(lock leak) 및 잘못된 락 해제 문제를 방지하였다.