Search

7주차 - Redis를 통한 분산 락 구현 시 구조 설계 변경 고민

생성일
2025/04/22 14:32
태그
기존 코드
import org.aspectj.lang.JoinPoint import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.reflect.MethodSignature import org.redisson.api.RedissonClient import org.slf4j.LoggerFactory import org.springframework.core.annotation.Order import org.springframework.expression.spel.standard.SpelExpressionParser import org.springframework.expression.spel.support.StandardEvaluationContext import org.springframework.stereotype.Component import java.util.concurrent.TimeUnit @Aspect @Order(0) @Component class DistributedLockAspect( private val redissonClient: RedissonClient ) { private val log = LoggerFactory.getLogger(javaClass) @Around("@annotation(DistributedLock)") fun lockByRedisson(joinPoint: JoinPoint): Any { val signature = joinPoint.signature as MethodSignature val distributedLock = signature.method.getAnnotation(DistributedLock::class.java) val lockKey = distributedLock.prefix + parseExpressionLanguage(signature.parameterNames, joinPoint.args, distributedLock.key) val lock = redissonClient.getLock(lockKey) try { val available = lock.tryLock(distributedLock.starvationTime, distributedLock.lockTimeToLive, TimeUnit.MILLISECONDS) require(available) { return false } return (joinPoint as ProceedingJoinPoint).proceed() } finally { runCatching { lock.unlock() } .onFailure { log.warn("Redis distributed lock is already unlocked : key={}", lockKey) } .getOrNull() } } fun parseExpressionLanguage(parameterNames: Array<String>, args: Array<Any>, keyEL: String): Any? { val parser = SpelExpressionParser() val context = StandardEvaluationContext() parameterNames.forEachIndexed { index, param -> context.setVariable(param, args[index]) } return parser.parseExpression(keyEL).getValue(context, Any::class) } }
Kotlin
복사
현재 구조가 레이어드 아키텍처 형태를 갖추고 있지만, Repository DIP로 클린 아키텍처를 지향 → 도메인과 서비스가 핵심
분산 락 적용
AOP를 통해 서비스 앞뒤로 적용해, 트랜잭션보다 먼저 획득 - 더 나중에 반환
그렇다면 분산 락을 불러오는게 서비스의 책임인가?
현재 구조
분산 락 적용
분산 락 획득과 반환의 시점 자체는 AOP 일 수 있지만, 그 획득하는 과정과 반환하는 과정 자체는 Service 혹은 Repository가 적절한 위치가 아닐까?
각 용어 및 책임 정의
FacadeService : 여러 Service의 기능을 호출하여 그 순서나 흐름을 제어, 각 데이터를 Client가 원하는 응답을 반환할 수 있게 조립(비즈니스 로직은 들어가지 않는다)
Service : 특정한 행동/동작(비즈니스 로직)의 단일 책임을 맡아 이를 처리하는 기능 단위
Repository : 데이터베이스에서 어떤 방법(쿼리)을 통해 데이터를 꺼내서 특정 형태(엔티티, 도메인 모델)로 반환해주는것
내 애플리케이션에서의 분산 락 : Redis를 통해 특정 Lock에 대한 Key를 설정하여, 해당 Key가 없다면 해당 자원에 다른 스레드(프로세스)에서 접근하지 못하도록 막는 것
분산 락 획득 : Redis에서 Lock으로 사용할 특정 Key 값에 대해 setnx를 통해 저장하는 것 / 만약 해당 Key값이 있다면 대기
분산 락 반환 : 획득한 Lock으로 사용하는 Key를 삭제(remove)하는 것
위 정의를 생각했을 때, 분산 락을 획득하는 것과 반환하는 것의 적절한 위치는?
현재 클린 아키텍처 구조 → 분산 락을 Redis가 아닌 다른 매개체를 통해 구현 가능하도록 분리할 필요 O
지금은 Redisson 라이브러리를 통해 단순히 getLock을 하면 위 동작 일괄적으로 처리가 됨
RedissonClient가 Repository 역할을 대체
분산 락 획득과 반납 자체는 Service가 맞을 듯 - Service에서 RedissonClient를 호출하기
추가적으로 Redis를 통해 대기열을 구현하면 문제가 없을까?
책임이 다르니 Service의 분리는 필요할 지도(ex. DistributedLockService // TokenQueueService)
추가적으로 Redis에 직접 접근해 데이터를 조작할 필요 O → Repository로 구현
Repository는 그 책임에 맞게 key를 전달받아 Redis에서 값을 꺼내서 반환
그렇다면 분산 락을 적용하는 AOP의 책임은?
1.
분산 락 적용 시점 결정하기
2.
트랜잭션보다 분산 락을 먼저 적용할 수 있도록 하기
3.
리플랙션을 이용해 필요한 데이터 가공(메서드 필드, 애노테이션 필드 등)
위 구조 적용 시도
AOP라는 독특한 시점 때문에 분산 락을 적용하기 위해서는 불필요한 래핑이 너무 많아짐
DIP를 통해 분산 락을 분리하기 위해서는 RLock을 별개의 객체(CustomLock)로 감싸서 반환
CustomLock을 서비스에 넘겨서 다시 RLock으로 캐스팅 및 try Lock
락 해제 시에도 동일
추가적으로 하면 안되는 이유를 나열해 보자면,
AOP를 통해 분산 락 구현 자체를 분리했고, 해당 AOP를 적용하는 시점에는 내부가 어떤 분산 락인지 몰라도 됨 → 이미 책임 분리가 완료된 것 아닐까?
이후 Redis를 통한 다른 분산 락으로 변경 시, 코드에서 바꾸어야하는 부분은 AOP 한 곳 → 단일 책임을 잘 지키는 듯
AOP를 통해 호출되는 시점은 애노테이션이 적용된 메서드 → 해당 애노테이션을 위 애플리케이션 구조의 설계를 잘 따라 상식적으로 사용한다면 Service에서만 사용할 것 → 그렇다면 Facade에서 호출되는 서비스의 앞단과 뒷단에서 처리되는 별개의 서비스로 봐도 되지않을까? → 그렇다면 서비스 로직을 가져도 되지 않을까?