이전 글
2023.11.07 - [Database] - [Redis] Redisson 분산 락을 간단하게 적용해 보기
분산 락 AOP 이용하기
build.gradle
아래 implementation을 추가한다.
dependencies {
//...
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.redisson:redisson-spring-boot-starter:3.17.0'
}
@DistributedLock
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
/**
* 락의 이름
*/
String key();
/**
* 락의 시간 단위
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 락 획득을 위해 기다리는 시간
*/
long waitTime() default 5L;
/**
* 락 임대 시간
*/
long leaseTime() default 3L;
}
DistributedLockAop
@DistributedLock 어노테이션이 붙은 메서드 전후로 실행되는 클래스입니다.
@Slf4j
@Order(1)
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAop {
private static final String REDISSON_LOCK_PREFIX = "LOCK:";
private final RedissonClient redissonClient;
private final AopForTransaction aopForTransaction;
@Around("@annotation(com.example.fitnessrecord.global.redis.redisson.lock.DistributedLock)")
public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
String key = REDISSON_LOCK_PREFIX +
CustomSpringELParser.getDynamicValue(
signature.getParameterNames(),
joinPoint.getArgs(),
distributedLock.key());
log.info("lock on [method:{}] [key:{}]", method, key);
RLock rLock = redissonClient.getLock(key);
String lockName = rLock.getName();
try {
boolean available =
rLock.tryLock(
distributedLock.waitTime(),
distributedLock.leaseTime(),
distributedLock.timeUnit());
if (!available) {
throw new MyException(ErrorCode.LOCK_NOT_AVAILABLE);
}
return aopForTransaction.proceed(joinPoint);
} catch (InterruptedException e) {//락을 얻으려고 시도하다가 인터럽트를 받았을 때 발생
throw new MyException(ErrorCode.LOCK_INTERRUPTED_ERROR);
} finally {
try {
rLock.unlock();
log.info("unlock complete [Lock:{}] ", lockName);
} catch (IllegalMonitorStateException e) {//락이 이미 종료되었을때 발생
log.info("Redisson Lock Already Unlocked");
}
}
}
}
이 클래스에서는 CustomSpringELParser 클래스와 aopForTransaction 클래스가 쓰인다.
CustomSpringELParser
@DistributedLock과 함께 사용되는 값을 파싱하기 위한 클래스이다.
전달받은 Lock의 이름을 파싱 한다.
public class CustomSpringELParser {
private CustomSpringELParser(){
}
public static Object getDynamicValue(String[] parameterNames, Object[] args, String key){
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
return parser.parseExpression(key).getValue(context, Object.class);
}
}
AopForTransaction
@DistributedLock이 적용된 부분은 부모트랜잭션 유무에 상관없이 별도의 트랜잭션으로 동작하도록 설정한다.
propagation = Propagation.REQUIRES_NEW로 지정한다.
@Component
public class AopForTransaction {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
@DistributedLock 사용
@Override
@DistributedLock(key = "T(java.lang.String).format('Record%d', #id)")
public RecordDto updateRecord(Long id, Long userId, RecordInput input) {
//update로직
}
위와 같이 메서드에 @DistributedLock을 설정하고, key 값을 설정해 두면,
key=Record{id} 값으로 Redis에 값이 들어가고,
해당 로직이 종료되면 해당 key를 가진 k-v 쌍이 삭제된다.
알게 된 점
처음엔 아래와 같이 실제 update가 진행되는 부분을 메서드로 분리하여 @DistributedLock을 분리한 private 메서드에 붙였다.
@Override
public RecordDto updateRecord(Long id, Long userId, UpdateRecordInput input) {
//매개변수로 데이터 수집, 변경할 엔티티 불러옴
Record record = repository.findById(id);
//update 로직 시작
this.update(record, input);
//update 완료
}
@DistributedLock(key = "T(java.lang.String).format('Record%d', #id)")
private void update(Record record, UpdateRecordInput input){
//update 로직 수행
}
AOP는 public 메서드에서 동작한다.
AOP는 private 또는 final 메서드에서는 동작하지 않는다.
AOP가 적용된 메서드를 직접 호출해야 한다.
위의 메서드는 Controller - Service - Repository 구조에서 Service 클래스에 존재한다.
Controller에서 해당 updateRecord 메서드를 호출하고, updateRecord()에서 update() 메서드를 호출하면 AOP는 동작하지 않는다. (update()가 public 메서드이더라도 마찬가지)
이 이유는 AOP가 동작할 때 Proxy를 이용하여 동작을 하는데, Proxy를 이용하려면 같은 bean 안에서 호출하면 안 된다고 한다. update()를 호출하는 곳이 외부에 있어야 update를 감싼 proxy를 한번 거친 뒤 update 함수로 가는데, 같은 클래스 내에서 호출하면 해당 proxy, aop 부분이 동작하지 않는다고 한다.
아래 스프링 공식 문서에서 AOP에 대한 설명이 잘 나와있다.
https://docs.spring.io/spring-framework/reference/core/aop/proxying.html#aop-understanding-aop-proxies
comment
아직 proxy, AOP는 제대로 공부해보지 못한 상황에서 redis도 사용해 보고, aop도 사용해 보고자 이 부분을 적용해 봤다.
그러나 잘 알지 못하니 동작 원리도 제대로 이해되지 않았다. AOP를 추가로 공부해봐야 잘 이해될 것 같다.
아래 글을 참고하였습니다.
https://helloworld.kurly.com/blog/distributed-redisson-lock/
'Database' 카테고리의 다른 글
[Database / SQLD] 데이터 모델과 SQL _ 정규화, 반정규화, 트랜잭션 (Part1 - Ch02) (0) | 2024.04.24 |
---|---|
[Database / SQLD] 데이터 모델링의 이해 (Part1 - Ch01) (0) | 2024.04.19 |
[Redis] Redisson 분산 락을 간단하게 적용해보기 (0) | 2023.11.07 |
[Redis] 레디스 설치 및 기본 기능 (0) | 2023.11.07 |
[H2 DB] 인메모리 방식 연결 방법 (미설치) (1) | 2023.08.18 |