지난 시간 Redisson 분산락을 구현을 해보았습니다. 그리고 이 분산 락을 통해서 동시성 제어를 했습니다. 여기서 한 가지 더 발전을 시켜보고 싶었던 부분은 가독성과 재사용성이었습니다. 코드를 보면 좌석을 선택을 하는 비지니스 로직과 동시성을 제어를 하는 분산 락 로직이 같이 있는 것을 볼 수 있습니다. 두 로직이 같이 있다 보니 코드의 가독성도 떨어지고 만약 다른 곳에서 분산 락을 사용을 할 때, 다시 분삭 락 로직을 구현을 해야 합니다. 이에 이번 포스트에서는 가독성과 재사용성을 높혀보도록 하겠습니다.
AOP에 대해서 간단하게 알아보고자 합니다.
AOP는 관심 지향 프로그래밍이라고 합니다. AOP는 비지니스 로직을 도와주는 부가 기능 즉, 핵심 로직하고는 관련이 없지만 이 핵심 로직을 도와주는 로직을 모듈화를 시켜서 중복을 줄이고 재사용성을 높이는 것을 말합니다.

예를 들어보겠습니다. 대학교에는 다양한 학과가 있다. 이 학과는 고유한 값들이 존재를 할 수 있지만 회장, 부화장 등은 역할이 똑같고 공통적으로 존재한다는 것을 알 수 있습니다. 이렇게 역할이 똑같고 공통적인 부가 기능을 분리해서 모듈화를 해서 다른 곳에서도 똑같이 쓰일 수 있게 해주는 것입니다.
분산 락 로직을 살펴보겠습니다.
@Transactional
public SeatEntity selectSeat(Long seatId) {
String lockKey = "seat-lock:" + seatId;
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;
try {
isLocked = lock.tryLock(1, 300, TimeUnit.SECONDS);
if (!isLocked) {
throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
}
SeatEntity seatEntity = getSeatById(seatId);
if(seatEntity.getSeatStatus() == SeatStatus.SELECT) {
throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
}
seatEntity.updateSeatStatus(SeatStatus.SELECT);
return seatRepository.save(seatEntity);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Thread was interrupted while waiting for lock", e);
} finally {
if (isLocked) {
lock.unlock();
}
}
}
위 코드에는 좌석을 선택하는 로직과 동시성을 제어하는 분산 락이 한 번에 같이 있습니다. 보기에 복잡해 보이며 가독성이 떨어집니다. 또한 현재는 이 부분에서만 분산 락을 사용을 하고 있지만 추후에 분산 락을 다시 사용하게 되면 위에 분산 락 로직을 다시 작성을 해주어야 합니다. 그래서 분산 락을 분리를 해줄려고 합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key();
}
DistributedLock 어노테이션을 만들어줍니다.
이렇게 어노테이션을 만들어주면 분산 락을 사용하고자 하는 곳에 선언적을 사용이 가능하고 이로 인해 가독성과 재사용성이 높아집니다.
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {
private final RedissonClient redissonClient;
@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
String lockKey = distributedLock.key();
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;
try {
isLocked = lock.tryLock(5, 20, TimeUnit.SECONDS);
if (isLocked) {
return joinPoint.proceed();
} else {
throw new IllegalStateException("Could not acquire lock for key: " + lockKey);
}
} finally {
if (isLocked) {
lock.unlock();
}
}
}
}
Aspect 어노테이션을 통해서 DistributedLockAspect 클래스가 AOP로 실행이 될 수 있도록 설정을 합니다. 그리고 Around 어노테이션을 통해서 DistributedLock 어노테이션을 적용한 메소드에 대해서 lock 메소드가 호출이 되도록 합니다. 이 lock 메소드가 실행이 되면서 분산 락을 사용을 할 수 있겠습니다.
@Transactional
@DistributedLock(key = "seat-lock:#seatId")
public SeatEntity selectSeat(Long seatId) {
SeatEntity seatEntity = getSeatById(seatId);
if (seatEntity.getSeatStatus() == SeatStatus.SELECT) {
throw new GlobalCommonException(SeatErrorResponsive.FILL_SEAT);
}
seatEntity.updateSeatStatus(SeatStatus.SELECT);
return seatRepository.save(seatEntity);
}
좌석을 선택하는 메소드에 DistributedLock 어노테이션을 붙혀줍니다. 그리고 key의 이름을 지정해줍니다. 이렇게 되면 이 메소드를 사용할 때 DistributedLockAspect 클래스의 Lock을 사용해서 분산 락을 사용을 합니다.
이번 포스트에서는 Redisson의 분산 락을 AOP를 통해서 분리를 했습니다. 그 결과로 DistributedLock 어노테이션 하나로 어디서든 Redisson의 분산 락을 사용을 할 수 있게 되었습니다. AOP를 통해서 코드의 가독성을 높였으며 분산 락 기능 코드의 재사용성을 높일 수 있었습니다.