Projects/HubEleven

[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (1) Synchronized

annovation 2026. 3. 6. 18:12

동시성 문제 (Race Condition)

💡해결 방법

  • 레이스 컨디션을 해결하는 방법에는 크게 3가지 관점이 있다.
  • 첫번째는 Java에서 지원하는 Synchronized 방법으로 해결하기
  • 두번째는 DB가 제공하는 Lock 을 이용하여 데이터 정합성 맞추기
  • 세번째는 Redis 를 활용하여 대표적인 라이브러리 Lettuce 와 Ressison 으로 해결하기

💡동시성 문제 (Race Condition) 이란?

https://annovation.tistory.com/508

 

[동시성 처리] Java 에서 발생하는 동시성 문제란?

Java 동시성 문제 (Race Condition, 레이스 컨디션)💡Java 동시성 문제란?Java에서 여러 스레드가 같은 데이터(필드/객체 상태) 를 공유하며 실행될 때, 올바른 제어 없이 접근하면 오류가 생긴다.이렇게

annovation.tistory.com


Application Level

  • Java에서 제공하는 Synchronized 키워드를 사용하여 한 개의 스레드만 접근 가능하도록 제한할 수 있다.

💡Synchronized

//@Transactional
public synchronized void decrease(Long id, Long quantity) {
    // Stock 조회
    // 재고를 감소시킨 뒤
    // 갱신된 값을 저장
    Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);

    stockRepository.save(stock);
}
  • synchronized 키워드를 메서드 선언부에 붙여, 해당 메서드는 하나의 스레드만 접근 가능하도록 제한한다.

✅ @Transactional 을 주석처리하는 이유 (출처)

 

1) @Transactional의 동작 원리

  • @Transactional은 Spring AOP 기반 프록시(proxy)를 통해 트랜잭션 경계를 생성한다. (공식 문서)
Spring uses AOP proxies to apply transactional behavior.
❗️개념 정리

1) Spring AOP(Aspect Oriented Programming) 란? (공식 문서)
- 관점 지향 프로그래밍이란 뜻으로, 공통 기능을 비즈니스 로직에서 분리해서 자동으로 적용하는 기술이다.
ex.
공통 기능 (ex. 로그)

비즈니스 로직 실행
// Spring은 실제 객체 대신 Proxy 객체를 먼저 호출하고, Proxy가 공통 기능을 실행한 뒤 실제 비즈니스 로직을 호출한다.

공통 기능 (ex. 로그)

2) 프록시(proxy) 란?
- Spring 에서 AOP 개념을 구현한 기능으로, 원래 객체 대신 호출을 받아서 중간에서 처리하는 객체
ex. 원래 객체 UserService를 호출했다면, Spring이 만든 UserServiceProxy가 호출된다.

3) 트랜잭션 이란?
- 여러 데이터베이스 작업(DB 안의 데이터를 읽거나 변경하는 모든 행위, CRUD)을 하나의 논리적 작업 단위로 묶는 것이다.
- 즉 여러 쿼리(데이터베이스에게 요청을 보내는 명령문)가 실행되더라도 전부 성공하거나 / 전부 실패해야 한다.
  • 즉, Spring이 해당 메서드를 감싼 프록시(proxy) 객체를 생성해 트랜잭션을 관리한다.
    예를 들면, StockService 를 필드로 가지는 클래스를 새로 만들어서 실행 (참고자료)
  • Proxy 예시
class OrderServiceProxy {

    OrderService target;

    public void order() {

        System.out.println("로그 시작");

        target.order();

        System.out.println("로그 종료");
    }
}
  • 실행 흐름
Client
 ↓
Proxy.order()
 ↓
OrderService.order()

 

2) @Transactional 을 주석처리하는 이유

  • commit(트랜잭션의 모든 작업을 최종 확정해 DB에 영구 저장하는 것)은 트랜잭션 완료 시점에 이루어진다.
  • commit 전에 DB에 기록되지만, 다른 트랜잭션에서는 그 변경사항을 볼 수 없다.
  • 따라서 재고 감소 메서드 decrease()가 완료되고 실제 DB에 반영 전에 다른 스레드가 decrease()를 호출하면, 다른 스레드는 갱신되기 전에 값을 가져가 이전과 동일한 문제가 발생한다.
  • 즉, 현재 테스트 코드에서는 여러 스레드가 동시에 실행되지만, 하나의 트랜잭션 안에서 commit이 되지 않은채 데이터에 접근하는 구조이다.

3) synchronized 동작 방식

  • synchronized 키워드는 같은 인스턴스에서 실행되는 여러 스레드 하나씩 순차적으로 메서드를 실행하도록 보장한다.
  • 같은 인스턴스에서 적용되는 synchronized Lock 예시
StockService service = new StockService();

 

두 스레드

Thread A → service.decrease()
Thread B → service.decrease()

 

이 경우

Thread A → lock(service)
Thread B → 대기

 

결과

A 실행
B 실행
  • 다른 인스턴스에서 적용되는 synchronized Lock 예시
StockService service1 = new StockService();
StockService service2 = new StockService();

 

두 스레드

Thread A → service1.decrease()
Thread B → service2.decrease()

 

이 경우

lock(service1)
lock(service2)

 

결과

동시에 실행
  • synchronized는 객체 기준으로 Lock을 잡는데 Spring이 Proxy를 사용하면 Lock이 걸리는 객체와 호출 객체가 달라질 수 있다.

4) 정리

  • @Transactional이 적용되면 Spring은 원본 StockService 대신 프록시 객체를 통해 메서드를 실행하여 트랜잭션을 관리한다.
  • 하지만 synchronized는 원본 객체 인스턴스를 기준으로 동기화되기 때문에, 프록시가 개입하는 구조에서는 의도한 대로 동기화가 보장되지 않을 가능성이 있다.

✅ synchronized 한계 (출처)

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

참고 자료

1) Spring Docs : Using @Transactional

https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html

 

Using @Transactional :: Spring Framework

The @Transactional annotation is metadata that specifies that an interface, class, or method must have transactional semantics (for example, "start a brand new read-only transaction when this method is invoked, suspending any existing transaction"). The de

docs.spring.io

2) 블로그 : 동시성 이슈 해결방법[2] - synchronized

https://velog.io/@kiteof_park/SpringBoot-java-synchronized

 

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

synchronized로 동시성 이슈를 해결하지 못하는 이유와 @Transactional과의 충돌!

velog.io