[TIL] Spring AOP 동시성 제어 문제

YJin·2025년 5월 28일

[내배캠 Spring 6기_TIL]

목록 보기
42/56
post-thumbnail

AOP으로 실행 시 트랜잭션 혹은 동시성 제어가 제대로 안된다?

문제 상황

🚨 발급 제한 수량이 1000개인 쿠폰 테스트에서 1200건이 모두 발급되는 문제 발생

  • 동시에 여러 스레드가 쿠폰 수량을 조회 → 동일 수량(예: 568개)을 기준으로 중복 발급
  • 여러 스레드가 동일한 수량을 읽음 → 락이 제대로 작동하지 않음

문제가 왜 발생했고, 어떻게 해결되었는가?

프록시 패턴

Spring AOP는 기본적으로 프록시 (체인) 기반이며, 스프링 빈에만 AOP를 적용할 수 있음.
원래 객체를 프록시 객체가 감싸고 있는 형태. 따라서 접근을 제어하거나, 부가 기능을 추가할 수 있음. (관심사의 분리처럼.)

@Transactional도 AOP 기반, 즉 프록시 기반으로 동작한다.

프록시는 @Transactional, @WithRedissonLock을 감지해서 메소드 실행 전에 필요한 작업(락 획득, 트랜잭션 시작 등)을 함

프록시 체인 기대 vs 실제

초기에는 다음과 같은 AOP 실행 순서를 기대했다:

[예상]
🔵RedissonLock AOP → 🟢Transactional AOP → 🟡실제 쿠폰 서비스 메소드

그러나 실제로는 트랜잭션이 먼저 시작된 상태에서 RedissonLock AOP가 실행되고 있었다:

[실제]
🟢Transactional AOP → 🔵RedissonLock AOP → 🟡실제 쿠폰 서비스 메소드

이 사실은 아래와 같은 로그로 확인할 수 있었다:

2025-05-22T20:14:44.589+09:00  INFO 6168 --- [FourChak] [pool-3-thread-3] o.e.fourchak.aop.WithRedissonLockAspect  : REDISSON AOP TX START: true

❗즉, 락 획득하기 전에 트랜잭션이 시작되는 것이 문제였다.


락 획득 전에 트랜잭션이 시작되면 왜 위험할까?

트랜잭션 vs 락의 역할

트랜잭션: DB에 대한 일관된 작업 보장 (입장 후 처리)
: 동시 접근을 막는 선착순 입장권

  • 트랜잭션격리만 해줄 뿐, 진입 순서를 보장해주지 않음.
  • 반면에 은 먼저 들어온 한 명만 작업을 진행하게 만들 수 있음.

따라서 다음과 같은 문제의 시나리오가 발생할 수 있음.

문제 시나리오

[1] 트랜잭션 A 시작
[2] 트랜잭션 A: 쿠폰 수량 조회 → 402

[3] 트랜잭션 B 시작
[4] 트랜잭션 B: 쿠폰 수량 조회 → 402  ← 💥 트랜잭션 A와 거의 동시에 시작

[5] 트랜잭션 A: 락 획득 성공
[6] 트랜잭션 A: 수량 차감 (402401)
[7] 트랜잭션 A: 커밋
[8] 트랜잭션 A: 락 해제

[9] 트랜잭션 C 시작
[10] 트랜잭션 C: 쿠폰 수량 조회 → 401

[11] 트랜잭션 B: 락 획득 성공
[12] 트랜잭션 B: 이전에 읽은 402를 기준으로 수량 차감 → 401
[13] 트랜잭션 B: 커밋
[14] 트랜잭션 B: 락 해제

[15] 트랜잭션 C: 락 획득 성공
[16] 트랜잭션 C: 수량 차감 (401400)
[17] 트랜잭션 C: 커밋
[18] 트랜잭션 C: 락 해제

트랜잭션데이터베이스에 대한 작업(읽기, 삽입, 수정, 삭제 등)하나의 작업 단위로 묶어 일관성과 원자성을 보장하지만, 동시에 여러 스레드에서 여러 트랜잭션이 병렬로 실행될 수 있다.

반면 은 이러한 작업들이 경쟁 상태에 놓이지 않도록 제어하며, 특정 자원에 대해 한 번에 하나의 스레드만 작업할 수 있도록 보장한다.

  • 그렇기 때문에 스레드 A가 먼저 락을 획득한 다음 트랜잭션 A를 시작해 쿠폰 수량(예: 402개)을 조회해야 함.

  • 하지만 트랜잭션 A가 먼저 시작해 쿠폰 수량을 읽은 후에 락을 획득하는 구조라면
    그 사이에 트랜잭션 B도 동일한 시점의 수량(402개)을 읽을 수 있고,

  • 이후 트랜잭션 A가 수량을 차감하고 커밋하더라도, 트랜잭션 B는 여전히 과거의 402개를 기준으로 작업하게 되는 문제가 발생.


따라서 실제 수량보다 더 많은 유저 쿠폰이 발급되고, 쿠폰 수량이 제대로 차감되지 않는 문제가 발생한 것.

✅ 결론

  • 락을 먼저 획득하고, 그 안에서 트랜잭션을 시작해야 함. (제발)
  • 그래야 읽기 시점이 항상 최신 상태를 반영하며,
  • 트랜잭션 간의 경쟁 상태 없이 안전하게 작업 순서를 보장할 수 있다.
  • 혹은, RedissonLockAOP@Order(0) 명시 → 락이 트랜잭션보다 먼저 실행되도록 우선순위 지정
    • 기본적으로 @Transactional@Order가 지정되어 있지 않아서 우선순위가 Integer.MAX_VALUE (가장 낮음)
    • 따라서 @Order가 숫자가 더 작은 쪽이 먼저 실행됨




참고

profile
백엔드 개발도 락이다

0개의 댓글