본문 바로가기
📕 Spring Framework/Spring Project

[Redisson] 트랜잭션 문제 발생 및 해결

by GroovyArea 2022. 10. 1.

지난 포스트

 

[Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결

내 프로젝트의 Payment를 개발하면서 가장 기본 중에 기본이 되는 문제를 직면했었다. 그것은 바로 동시성 문제! 스프링부트의 내장 서버는 기본적으로 톰캣, 언더토우 등등의 WAS로 돌아가는데 이

sweeeetgoguma.tistory.com

지난 포스트에서 Redisson을 이용하여 동시성 문제를 해결하는 코드를 구현했다.

 

프로젝트 리팩토링이 거의 끝나가 조회 API를 구체화하여 몇 개 추가하던 도중, 스레드 100개의 동시 요청을 직접적으로 받는 과정을 확인하고 싶어졌다.

 

그래서 실험해봤다.

 

결과는??

처참하다..

 

무엇이 문제였을까

트랜잭션 처리가 씹혔다. 

@GetMapping("/test")
public void test() throws InterruptedException {
    int threadCount = 100;
    ExecutorService executorService = Executors.newFixedThreadPool(100);
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        int finalI = i;
        executorService.submit(() -> {
            try {
                log.info(finalI +"번째 일꾼 일한다.");
                kakaopayStrategyApplication.test("lala");
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();

}

이 테스트를 이용해서 100개의 요청을 동시에 요청하면,

 

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test(String id) {
    log.info("일해라!");
    Product product = productRepository.findById(1L).orElseThrow(() -> new RuntimeException("에헤이"));
    log.info("상품 전 개수:" + product.getQuantity());
    product.decreaseItemQuantity(1);
    log.info("상품 후 개수:" + product.getQuantity());
}

원래 상품 200개에서 100개가 되어야 정상이다.

하지만, 계속 이상한 재고량이 남았다..

@Transactional(propagation = Requires_New)를 하면 트랜잭션의 새로운 시작으로 인해, AOP 이용할 경우 문제가 없을 거라고 생각했다.

 

 

문제 인지

내 한줄기 빛..

이 분의 글을 읽고, 깨달았다..

temp method에 @Transactional을 사용하면 위험합니다.
내부적으로 AOP를 사용하기 때문에 unlock이 된 후 commit이 되므로 동기화가 제대로 되지 않을 수도 있습니다.
그래서 redis에 lock을 잡고 db와 redis의 transaction 처리를 따로 해줘야 합니다.

흡사 신의 목소리..

 

스프링에서 제공하는 @Transactional 애노테이션은 트랜잭션 로직을 AOP를 활용해서 타겟 메서드를 프록시로 감싼다.

그렇게 하면 비즈니스 로직 처리가 깔끔해지는 이점이 있다.

 

내가 Redisson lock을 이용하기 위해 이 또한 비즈니스 로직이 조금 섞여 들어가므로, 마찬가지로 AOP로 감쌌다. 

 

@Transactional과 @Redislocked(내가 구현한 redisson lock 애노테이션 - AOP 활용) 의 동시 사용으로 인해 AOP가 제대로 순차적으로 동작하지 않아 트랜잭션이 씹힌것이다.

 

아 맞다.. 스프링 AOP는 프록시로 감싸는 방식으로 동작하는데, 내가 이걸 왜 놓쳤을까..

순서가 중요한 법이다. 이번 기회에 다시 한번 개념을 잡고 간다.

 

 

해결 방법

트랜잭션 로직을 또 다른 Aspect로 분리하기로 결정했다.

애노테이션에 트랜잭션 옵션 추가

 

/**
 * 트랜잭션 처리 aop
 */
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
@Order(value = 2)
public class TransactionAspect {

    private final RedisFunctionProvider redisFunctionProvider;

    @Around("@annotation(com.daniel.mychickenbreastshop.global.aspect.annotation.RedisLocked)")
    public Object executeWithTransaction(ProceedingJoinPoint joinPoint) {
        if (!isTransactional(joinPoint)) {
            try {
                return joinPoint.proceed();
            } catch (Throwable e) {
                throw new InternalErrorException(e);
            }
        }

        RTransaction transaction = redisFunctionProvider.startRedisTransacton();
        TransactionStatus status = redisFunctionProvider.startDBTransacton();

        Object result;

        try {
            result = joinPoint.proceed();

            redisFunctionProvider.commitRedis(transaction);
            redisFunctionProvider.commitDB(status);
        } catch (Throwable e) {
            redisFunctionProvider.rollbackRedis(transaction);
            redisFunctionProvider.rollbackDB(status);
            throw new InternalErrorException(e);
        }
        return result;
    }

    private boolean isTransactional(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        return method.getAnnotation(RedisLocked.class).transactional();
    }
}

트랜잭션 로직으로 감싸는 AOP 추가

@Order로 AOP의 감싸지는 순서를 지정하였다. 

(숫자는 상대적 크기 비교, 숫자가 클수록 핵심 관심 모듈에 먼저 감싸짐)

 

 

실행

기존 상품 수량 20개

 

기존 100개 동시 스레드 요청 실행

 

야무진 로그들
100개가 정확하게 감소했다

 

=> 이렇게 동시 요청 건에 대한 Redisson 분산락을 정확하게 적용하게 되었다. 트랜잭션의 처리도 무척이나 중요함을 깨달았고, AOP를 적용하면서 프록시 원리를 다시 한번 깨닫는 계기가 되었다.

반응형