동시성 문제 (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
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
'Projects > HubEleven' 카테고리의 다른 글
| [동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (3) Redis Distributed Lock (0) | 2026.03.11 |
|---|---|
| [동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (2) Database Lock (0) | 2026.03.09 |
| [동시성 처리] 재고 감소 통합 테스트 코드 6 - Optimistic Lock(낙관적 락) 멀티 스레드 테스트 코드 (1) | 2026.02.24 |
| [동시성 처리] Optimistic Lock (낙관적 락) 재시도 로직 (0) | 2026.02.23 |
| [동시성 처리] DB Lock : Optimistic Lock (낙관적 락) - 실습 (0) | 2026.02.20 |