동시성 이슈 해결방법[2] - synchronized

Euiyeon Park·2025년 2월 17일
0
post-thumbnail

🛒📦재고시스템으로 알아보는 동시성 이슈 해결 방법

재고 감소 로직의 문제점

🚦 Race Condition

  • Race Condition여러 개의 프로세스 또는 스레드가 공유 자원에 동시 접근(변경) 할 때
    발생하는 문제로, 실행 순서에 따라 예상치 못한 결과가 발생하는 상태

해결 방법

  • 하나의 Thread가 작업이 완료된 이후에, 다른 스레드가 데이터에 접근할 수 있도록 해야 함

✨ 해결 방법 1 - Java synchronized

  • synchronized 키워드를 메서드 선언부에 붙여, 해당 메서드는 하나의 스레드만 접근 가능하도록 함

synchronized

  • synchronized는 자바에서 멀티스레드 환경에서 동기화(synchronized)를 보장하기 위한 키워드
  • 특정 코드 블럭이나 메서드가 한 번에 오직 하나의 스레드만 접근할 수 있도록
    임계 영역(critical section)을 설정하는 역할

📂 StockService.java

    @Transactional
    public synchronized void decreaseStock(Long id, Long quantity) {
        Stock stock = stockRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 id입니다."));

        stock.decreaseQuantity(quantity);
        stockRepository.saveAndFlush(stock);
    }

테스트 결과

  • 테스트 실패 ❗❗❗❗

✨ 테스트 실패 원인

  • synchronized 키워드를 사용해 문제를 해결하려고 했으나, 테스트에 실패했다.
  • 실패 원인은 @Transactional의 동작 방식에 있다.

@Transactional의 동작 방식

  • @Transactional은 Spring이 해당 메서드를 감싼 프록시(proxy) 객체를 생성해 트랜잭션을 관리한다.
    즉, StockService를 필드로 가지는 클래스를 새로 만들어서 실행
  • 트랜잭션이 시작되면 해당 메서드의 실행이 끝날 때까지 DB에 변경을 즉시 반영하지 않는다.❌
    • 트랜잭션이 끝나는 시점에서 DB에 한 번에 반영(commit)
    • 즉, decreaseStock()이 실행되어도 실제 DB에는 변경 사항이 반영되지 않은 상태
  • decreaseStock()이 완료되고 실제 DB에 반영 전에 다른 스레드가decreaseStock()을 호출하면, 다른 스레드는 갱신되기 전에 값을 가져가서 이전과 동일한 문제가 발생하게 된다.
  • 💡 @Transactional을 제거하고 테스트하면 테스트 성공

synchronized의 동작 방식

  • synchronized 키워드는 같은 인스턴스에서 실행되는 여러 스레드
    하나씩 순차적으로 메서드를 실행하도록 보장한다.
  • 그러나synchronized동일한 인스턴스를 기준으로 동기화가 작동하는데,
    @Transactional이 적용되면 프록시 객체가 개입하여 메서드를 실행하므로
    원본 인스턴스와 다른 인스턴스에서 동작
    할 수 있다.
  • 즉, synchronized가 원래 의도대로 작동하지 않을 가능성⭕

✨ 정리

@Transactional(proxy 객체)과 synchronized의 충돌

  • 스프링에서 @Transactional을 사용하면, 스프링 AOP가 개입한다.
  • 스프링은 프록시 객체를 생성해 원본 객체를 감싸고 트랜잭션을 관리한다.
  • 즉, StockService를 직접 사용하는 것이 아닌,
    스프링이 StockService를 감싼 프록시 객체를 대신 실행한다.

stockService.decreaseStock()을 호출하면

  1. 스프링이 만든 프록시 객체가 먼저 실행
  2. 프록시 객체가 원본 stockServicedecreaseStock()을 실행
  3. 그러나 synchronized는 프록시 객체가 아닌 원본 객체 기준으로 동작
  • 결과적으로 여러 스레드가 각각 다른 프록시 객체를 통해 메서드를 호출하면
    synchronized가 제대로 동작하지 않을 수 있다.
  • synchronized가 원본 객체를 기준으로 동기화하려고 하지만
    @Transactonal이 적용되면서 프록시 객체가 실행되므로
    동일한 객체가 아닐 가능성이 있어 synchronized가 깨질 수 있다.

✨ synchronized 사용의 한계

  • synchronized는 하나의 프로세스 안에서만 동기화를 보장한다.
  • 서버가 1대일 떄는 데이터 접근을 서버 1대만 해서 괜찮겠지만,
    서버가 2대 혹은 그 이상인 경우 데이터 접근을 여러 곳에서 할 수 있게 된다.
  • 실제 운영 서비스는 대부분 2대 이상의 서버를 사용하므로 synchronized 거의 사용❌

TimeServer 1StockServer 2
10:00SELECT *
FROM stock
WHERE id = 1
{id: 1, quantity: 5}
{id: 1, quantity: 5}SELECT *
FROM stock
WHERE id = 1
10:05UPDATE SET quantity = 4
FROM stock
WHERE id = 1
{id: 1, quantity: 4}
{id: 1, quantity: 4}UPDATE SET quantity = 4
FROM stock
WHERE id = 1
profile
"개발자는 해결사이자 발견자이다✨" - Michael C. Feathers

0개의 댓글