이전 이슈를 통해 동시 요청시 중복 데이터 삽입 문제를 해결할 수 있었습니다.
그러나 아직 몇 가지 개선해야 할 점이 남아있습니다.
- 하드코딩된 lockname 파라미터
- 메서드 분리와 추상화
하드코딩된 lockname 파라미터 개선
현재 작성된 분산락 어드바이저는 다음과 같은 방식으로 lockname을 가져옵니다.
String lockName = joinPoint.getArgs()[0].toString();
위 방식은 분산락 AOP를 사용하는 경우 항상 첫번째 파라미터를 lockname 파라미터로 고정시키는 방식입니다.
하드코딩은 의미를 파악하기 어려우며 유지보수가 어렵다는 단점이 있으므로 분명히 개선해야할 문제입니다.
따라서 기존에 선언한 @DistributedLock 적용한 DistributedLockPrefix처럼 lockname 속성을 추가해 보았습니다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DistributedLock {
DistributedLockPrefix prefix();
String lockName; //추가!!
}
하지만 스프링부트는 CGLIB를 이용해 컴파일이 끝난 직후 프록시 객체를 생성합니다.
런타임에서 사용자의 요청을 받기 전에 lockname을 알고 애노테이션 속성으로 전달할 수 없습니다.
따라서 lockname 파라미터에 적용할 커스텀한 애노테이션을 생성하여 분산락이 실행될 때 마다 해당 어노테이션이 적용된 파라미터를 가져와 lockname으로 사용하도록 개선했습니다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface LockName {
}
@Slf4j
@Aspect
@Component
@Order(1)
@RequiredArgsConstructor
public class DistributedLockAspect2 {
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(3);
private final DistributedLockManager distributedLockManager;
//생략
@Around("@annotation(distributedLock)")
public Object handleDistributedLock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
String lockName = getLockName(joinPoint, distributedLock);
//생략
}
private String getLockName(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
String lockName = distributedLock.prefix().getValue();
boolean hasLockName = false;
Annotation[][] annotations = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterAnnotations();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
for (Annotation annotation : annotations[i]) {
if (annotation instanceof LockName) {
hasLockName = true;
lockName += args[i];
break;
}
}
}
if (!hasLockName) {
throw new DistributedLockException(ErrorCode.NO_LOCK_NAME_SET);
}
return lockName;
}
private void checkResultSetSuccess(ResultSet rs) throws SQLException, DistributedLockException {
//생략
}
}
동작 방식은 다음과 같습니다.
- getLock() 메서드는 JoinPoint의 MethodSignature를 사용해 실행될 비즈니스 로직의 파라미터와 애노테이션들을 가져옵니다.
- 반복문을 이용하여 애노테이션 배열을 순회하며 @LockName을 탐색합니다.
- @LockName을 발견하면 해당 파라미터의 value를 반납하고, @LockName을 발견하지 못한 경우 예외를 던집니다.
위 방식으로 LockName 파라미터의 선언을 원하는 위치에 선언할 수 있었으며, long 과 int 의 값들을 별도로 타입변환해야 하는 번거로움도 해결할 수 있었습니다.
분산락의 추상화
현재 가독성이 떨어지는 코드의 개선과 분산락 설계 시 고려했던 Redis, Zookeeper를 사용한 분산락으로의 변경용을 고려하여 추상화를 적용했습니다.
그러나 레디스를 사용한 분산락은 별도의 커넥션을 사용하지 않고 비즈니스 로직이 수행되는 커넥션 하나만으로 분산락의 기능을 수행할 수 있었습니다. 즉 DistributedLockManager 인터페이스에서 다음과 같이 커넥션을 파라미터로 전달할 수 없었습니다.
public interface DistributedLockManager {
Connection getLock(String lockName, Duration timeout);
void releaseLock(String lockName, Connection connection);
}
따라서 파라미터를 사용하지 않고 커넥션을 전달하는 방법을 고민하다, 이전에 적용한 Spring Security의 Authorization의 동작 원리를 자세히 살펴보았습니다. Spring Security는 ThreadLocal을 사용하여 SecurityContextHolder에 Authorization 인스턴스를 저장하고 사용합니다.
위 방식을 도입해 커넥션을 저장하는 ThreadLocalConnectionHolder를 생성했습니다.
public class ThreadLocalConnectionHolder {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static void save(Connection connection) {
connectionHolder.set(connection);
}
public static Connection get() {
return connectionHolder.get();
}
public static void clear() {
connectionHolder.remove();
}
}
ThreadLocalConnectionHolder와 DistributedLockManger를 적용한 코드는 다음과 같습니다.
@Around("@annotation(distributedLock)")
public Object handleDistributedLock(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
String lockName = getLockName(joinPoint, distributedLock);
distributedLockManager.getLock(lockName, DEFAULT_TIMEOUT);
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw (BusinessException) throwable;
} finally {
distributedLockManager.releaseLock(lockName);
}
}
private String getLockName(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
String lockName = distributedLock.prefix().getValue();
boolean hasLockName = false;
Annotation[][] annotations = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterAnnotations();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
for (Annotation annotation : annotations[i]) {
if (annotation instanceof LockName) {
hasLockName = true;
lockName += args[i];
break;
}
}
}
if (!hasLockName) {
throw new DistributedLockException(ErrorCode.NO_LOCK_NAME_SET);
}
return lockName;
}
}
추상화를 적용하기 전 코드와 비교했을 때, 약 200라인의 코드를 줄이고 분리할 수 있었습니다.
'[Project] > [Momo]' 카테고리의 다른 글
[이슈 #5] Spring Life Cycle Event를 이용한 런타임에러를 컴파일 시점으로 변경하기 (0) | 2022.08.05 |
---|---|
[이슈 #4] 분산락을 활용한 중복 데이터 삽입 이슈 해결3 (0) | 2022.08.02 |
[이슈 #2] 분산락을 활용한 중복 데이터 삽입 이슈 해결1 (0) | 2022.08.02 |
[이슈 #1] Spring Transaction의 Self Invocation 이슈 (0) | 2022.03.04 |