지난 글에서 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를 코드에 적용하면 어노테이션만을 이용해서 간결하게 구현되는 것을 볼 수 있다!
'Java, Spring' 카테고리의 다른 글
Spring Security를 이용하여 JWT 토큰방식 로그인 구현 (0) | 2024.08.18 |
---|---|
Java Garbage Collection (0) | 2024.08.08 |
Spring AOP에 대해서 알아보자 (3) | 2024.07.13 |
무신사 블프 이벤트 상품 동시성 문제 (2) (0) | 2024.07.10 |
무신사 블프 이벤트 상품 동시성 문제 (1) (0) | 2024.07.06 |