Engineering Note

[SW Engineering] Redis 분산락을 통한 동시성 문제 해결 과정 본문

SW Engineering

[SW Engineering] Redis 분산락을 통한 동시성 문제 해결 과정

Software Engineer Kim 2025. 12. 25. 15:16

작성 예정
redis 동시성 제어 단계적 해결 과정과 해결과정중 발생한 원인 분석과 facade 패턴을 통한 최종 해결책
초기 코드의 문제는 프록시 객체를 통해 호출된 메서드가클래스 레벨의 @Transactional(readOnly = true) 트랜잭션에 참여하면서내부 메서드 호출 역시 해당 readOnly 트랜잭션의 영향을 받은 점이다.이로 인해 엔티티 변경과 더티 체킹은 발생했지만flush가 수행되지 않아 DB에는 UPDATE가 반영되지 않았다.이를 해결하기 위해 프록시 객체가 호출하는 메서드에@Transactional을 직접 선언하여 정상적인 쓰기 트랜잭션을 적용했으나,Redis 락 해제를 트랜잭션 커밋 이전에 수행하면서커밋되지 않은 상태에서 락이 해제되어결과적으로 Lost Update 문제가 발생했다.
그래서 최종 해결로 이거 맞아? facade 패턴으로 트랜잭션 외부에서 레디스락,해제를 적용해 해결
 
문제 해결과정 중 알게된 사실
Sping AOP, @Transactional 어노테이션 작동 방식, SQL auto commit, JPA 더티체킹, flush 적용 시점.
 
Redis 역할
Redis 분산락을 통해 DB 접근 가능 여부를 선제적으로 제어하여, 동시 요청 상황에서 DB의 트랜잭션 및 락 관리 부담을 효과적으로 완화하는 역할. 비관적락을 통해 발생하는 성능 저하이슈해결
 효과

DB 커넥션 풀 부담도 줄여서 다른 요청을 처리할 수 있게 자원을 효율적으로 관리했다.
정리하면 분산락을 통해 동시성 제어를 DB 밖으로 이동시켜, DB 커넥션 풀과 트랜잭션 자원을 보호하고 전체 요청 처리량을 안정적으로 유지하는 구조로 설계했다.

 

 

알고 있어야 할 개념

1️⃣ @Transactional은 “어노테이션”이 아니라 “프록시 기반 경계”다

새로 정리된 포인트

  • @Transactional은 메서드에 붙어 있다고 자동으로 동작하지 않는다
  • Spring 프록시를 거쳐 호출될 때만 트랜잭션이 시작된다
  • 같은 클래스 내부 메서드 호출(self-invocation)은 프록시를 타지 않는다

결과

  • 내부 메서드에 @Transactional이 있어도 트랜잭션이 시작되지 않을 수 있다
  • 트랜잭션은 “어디서 호출되느냐”가 핵심

📌 참고
Spring 공식 문서 – Declarative Transaction Management
https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html


2️⃣ “트랜잭션이 없다”는 말은 “SQL이 실행되지 않는다”는 뜻이 아니다

새로 정리된 포인트

  • SQL은 트랜잭션 없이도 실행된다
  • MySQL 기본 설정은 autocommit = ON
  • 이 상태에서는 각 SQL이 개별 트랜잭션으로 실행된다

정확한 표현

  • ❌ 트랜잭션이 없어서 SQL이 안 나간다
  • ✅ 트랜잭션 경계 없이, SQL 하나당 즉시 커밋된다

📌 참고
MySQL 8.0 Reference – Autocommit
https://dev.mysql.com/doc/refman/8.0/en/commit.html


3️⃣ @Transactional의 진짜 역할은 “여러 SQL을 하나의 생명주기로 묶는 것”

새로 정리된 포인트

  • @Transactional이 하는 일
    • auto-commit을 끄고
    • 여러 SQL을 하나의 논리적 작업 단위로 묶는다
  • 예외 발생 시 전체 롤백
  • DB 락을 트랜잭션 종료 시점까지 유지

반대로 없으면

  • SQL1 → 즉시 커밋
  • SQL2 → 즉시 커밋
  • 중간 실패 시 되돌릴 방법 ❌

📌 참고
Spring Transaction Propagation
https://docs.spring.io/spring-framework/reference/data-access/transaction/propagation.html


4️⃣ DB 락은 “트랜잭션 생명주기”에 종속된다

새로 정리된 포인트

  • DB 락(비관락, 낙관락)은 트랜잭션 안에서만 의미가 있다
  • 트랜잭션이 끝나면:
    • COMMIT → 락 해제
    • ROLLBACK → 락 해제

그래서 생긴 깨달음

  • 트랜잭션이 없으면
    • 락이 걸려도 쿼리 종료와 동시에 해제
    • 동시성 보호 효과 없음

📌 참고
MySQL InnoDB Locking
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html


5️⃣ Redis 락과 DB 락은 “역할 레이어가 다르다”

새로 정리된 포인트

  • Redis 락
    • 애플리케이션 레벨
    • “누가 먼저 들어오느냐”를 제어
  • DB 락
    • 데이터 레벨
    • “데이터를 누가 수정 중이냐”를 제어

이번 코드에서의 실제 상태

  • Redis 락 ✅ 정상
  • DB 트랜잭션 ❌
  • DB 락 ❌ 의미 없음

👉 진입은 막았지만, 데이터 보호는 못 한 구조


6️⃣ MySQL에서 트랜잭션을 명시적으로 시작하는 방법

새로 정리된 포인트

  • 트랜잭션 시작 쿼리
  • START TRANSACTION; BEGIN;
  • 트랜잭션 종료
  • COMMIT; ROLLBACK;
  • BEGIN … END는 트랜잭션이 아니라 블록 문법

📌 참고
MySQL START TRANSACTION
https://dev.mysql.com/doc/refman/8.0/en/commit.html


7️⃣ 최종적으로 정리된 “핵심 문장 3개”

이 세 문장은 지금 단계에서 정확히 머리에 들어가 있으면 된다.

  1. @Transactional은 프록시를 거쳐야만 동작한다
  2. 트랜잭션이 없으면 SQL은 실행되지만 보호되지 않는다
  3. DB 락은 트랜잭션 안에서만 의미를 가진다

 

Comments