지난 글에서 AOP를 이용하는 방법에 대해서 알아보았다.

 

Spring AOP를 이용하여 부가 기능을 핵심 기능으로부터 분리할 수 있는데, 락을 거는 것은 부가 기능으로 볼 수 있다.

따라서 이번에는 기존에 동시성 처리를 위해 만들어 놓은 분산락을 AOP 기능을 통해 분리를 해보려고 한다.

 

기존 구현 방식

@Component
@Slf4j
@RequiredArgsConstructor
public class RedissonLockHandler {

    private final RedissonClient client;

    public void execute(String name, long waitTime, long leaseTime, Runnable runnable) {
        RLock lock = client.getLock(name);

        try {
            if (!lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS)) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("락 획득 실패");
            }
            runnable.run();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
        } finally {
            try {
                lock.unlock();
            } catch (Exception e) {
                log.error("락 반납 오류", e);
            }
        }
    }
}

 

기존에는 분산락을 RedissonLockHandler 파일에 분리하여 구현하였고, 이를 사용하는 곳에서 Composition을 이용하여 사용하도록 하였다. 물론 Composition으로도 충분히 부가 기능을 분리할 수 있다. 하지만 사용하는 클래스에서 의존성을 주입하여야 하기 때문에 코드의 결합도가 올라간다. 또한 코드의 중복이 발생한다.

 

AOP를 이용한 구현

@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    long waitTime() default 5L;
    long leaseTime() default 3L;
}

 

우선 분산락 어노테이션을 만들어주었다. 위 어노테이션이 붙은 곳에 분산락을 적용하는 식으로 코드를 작성하려고 한다.

어노테이션을 지정할 때 락 생성에 필요한 key, timeUnit, waitTime, leaseTime을 설정할 수 있다.

 

@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAop {

    private final RedissonClient redissonClient;

    @Around(value = "@annotation(com.example.blackfriday.annotation.DistributedLock)")
    public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        String[] parameterNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();

        String lockName = distributedLock.key();

        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i].equals("eventProductId")) {
                lockName = lockName.concat(args[i].toString());
            }
        }

        RLock lock = redissonClient.getLock(lockName);

        try {
            if (!lock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit())) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("락 획득 실패");
            }

            return joinPoint.proceed();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
            throw new InterruptedException();
        } finally {
            try {
                lock.unlock();
            } catch (Exception e) {
                log.error("락 반납 오류", e);
            }
        }
    }
}

 

@Around 어노테이션을 이용해 분산락이 적용된 메서드 실행 전과 실행 후를 모두 컨트롤한다. value를 이용하여 포인트컷을 지정할 수 있는데 이때 어노테션으로도 포인트컷을 지정이 가능하다. 따라서 위에서 만들어준 DistributedLock 어노테이션을 포인트컷으로 지정해주었다.

 

ProceedingJoinPoint를 이용하여 메서드에 대한 정보를 가져올 수 있는데, DistributedLock의 key를 통해 지정해 준 값과, 메서드의 파라미터인 eventProductId 값을 이용하여 각 이벤트상품 ID에 대한 유니크한 락을 생성하도록 하였다.

 

try-catch 구문을 통해 마지막으로 메서드가 실행된 후 락이 제대로 반환되지 않거나 비즈니스 로직 상 오류나 났을 경우 오류에 대한 핸들링을 제공해 준다.

 

결과

@DistributedLock(key = "redisson_lock:")
@Override
public void processEventProduct(OrderDto.EventOrderRequest req, Long eventProductId, LocalDateTime currentTime) throws InterruptedException {
    defaultEventProductService.decreaseQuantity(eventProductId, currentTime);
}

 

AOP를 코드에 적용하면 어노테이션만을 이용해서 간결하게 구현되는 것을 볼 수 있다!

green_dev