AOP와 Redis Lock으로 따닥 문제 해결해보기

김현교·2023년 1월 5일
0

개요

보드게임 형식의 턴 제 팀 게임인 The-knight 프로젝트 중, 공격 선택, 의심 선택 등 옵션을 선택하면 로직 처리 후 다음 화면으로 넘어가는 방식으로 게임을 구현했다. 그런데 동시에 여러 번 버튼을 누르면 모든 요청을 수행하기 때문에 화면 전환 후에 해당 옵션이 선택된 것처럼 동작하는 문제가 가끔 발생하였다. (사용자가 버튼을 동시에 여러 번 클릭하는 문제를 ‘따닥’ 문제라고 하겠다.)

마침 프로젝트에서 Spring AOP를 활용해서 로깅을 하고 Redis의 redisson Lock을 활용하여 동시성을 제어하고 있었는데, 이 2가지를 활용하여 특정 로직에 대한 따닥 문제를 해결할 수 있겠다는 생각을 하여서 적용해보았다.

아이디어

먼저 Redisson이 제공하는 Lock 기능에 대해 확인해보자. 아래는 Lock을 점유하려 시도할 때 사용하는 메소드이다. (Redisson Lock에 대한 내용은 별도의 포스팅에서 다룬다.)

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

첫 번째 파라미터 waitTime은 Lock을 획득하기 위해 대기하는 시간,

두 번째 파라미터 leaseTime은 Lock을 점유하는 최대 시간,

세 번째 파라미터 unit은 시간의 단위를 의미한다.

Lock을 점유하면 true를 리턴한다.

아이디어는 간단하다. 먼저 waitTime을 짧게, leaseTime을 길게 설정한다. 그리고 Spring AOP를 이용하여 특정 로직이 수행되기 전에 Redisson Lock을 잡는다. 만약 Lock을 점유하지 못한다면 해당 로직이 이미 호출된 경우이므로 로직을 호출하지 않는다.

Lock을 잡는 key값은 GameId(게임 식별자)와 MemberId(클라이언트 식별자), 호출 메소드명(로직 식별자)을 조합한 문자열로 설정했다. 따라서 특정 게임 내의 특정 플레이어의 특정 로직에만 Lock을 걸기 때문에 Lock으로 인한 다른 로직에 문제가 발생할 일은 없다.

구현 코드

다음은 Spring AOP의 코드이다.

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

    private final GameLockUtil gameLockUtil;

    @Around("@annotation(com.a301.theknight.global.aop.annotation.PreventClick)")
    public Object preventMultiClick(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
        String[] parameterNames = codeSignature.getParameterNames();
        Object[] args = joinPoint.getArgs();

        long gameId = 0L;
        long memberId = 0L;
        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i].equals("gameId") && args[i] != null) {
                gameId = (long) args[i];
            } else if (parameterNames[i].equals("memberId") && args[i] != null) {
                memberId = (long) args[i];
            }
        }

        **if (gameLockUtil.clickLock(gameId, memberId, methodName)) {
            return joinPoint.proceed();
        }**
        return null;
    }
}

핵심 부분은 clickLock() 메소드를 호출하여 Lock을 점유하면 proceed()로 원래 메소드를 호출하고 아니면 호출하지 않는 부분이다.

GameLockUtil은 Redisson Lock에 대한 기능을 구현해둔 클래스이다. clickLock 메소드는 다음과 같이 구현했다.

waitTime은 1초, leaseTime은 5초로 설정하였기 때문에, 가장 처음 요청이 Lock을 점유하면 5초 간 Lock을 해제하지 않는다. 그리고 이어서 들어온 요청들은 1초 간 Lock 대기 후 점유 실패를 하기 때문에 로직이 수행되지 않는다.

AOP 적용 부분 설정하기

위에서 구현한 AOP의 따닥 방지 기능을 특정 기능에만 적용하기 위해 커스텀한 어노테이션을 만들었다. 아래는 어노테이션의 구현부이다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PreventClick {
}

그리고 Spring AOP 코드 중 @Around의 값을 어노테이션의 풀 패키지 경로로 설정해준다. @PreventClick 어노테이션이 붙은 메소드는 해당 AOP의 로직이 수행된다.

이제 따닥 문제가 발생하지 않기를 바라는 Controller Layer 메소드에 해당 어노테이션을 붙여주면 된다. (@MessageMapping은 Spring Websocket에서 URL를 매핑하는 어노테이션이다.)

profile
백엔드 개발자로 취업 준비 중입니다.

0개의 댓글