동시성 제어란?
여러 프로세스나 스레드가 동시에 같은 자원(데이터나 상태)에 접근할 때, 무결성과 일관성을 보장하기 위한 기법
왜 필요할까?
동시에 여러 사용자가 같은 데이터를 수정하면 문제가 발생할 수 있다.
아래 재고 감소 예시를 통해 살펴볼 수 있다.
- 사용자 A가 상품을 구매하려고 조회하니 1개의 재고가 남아 있다.
- 동시에 사용자 B가 조회하니 1개의 재고가 남아 있다.
- 사용자 A는 구매 가능 상태이므로 물품을 구매하고 재고를 0으로 업데이트한다.
- 사용자 B 또한 구매 가능 상태이므로 물품을 구매한다.
Synchronized란?
synchronized는 JVM 수준에서 제공되는 모니터 락(Monitor Lock)을 기반으로 작동한다.
Monitor란?
- 모든 객체(Object)는 모니터를 가지고 있음
- synchronized는 이 모니터를 획득하고 블록이 끝나면 해제
- 한 번에 하나의 스레드만 모니터를 획득할 수 있기 때문에 상호 배제가 보장
JVM 내부 작동 흐름
synchronized(lock) {
// critical section
}
이 코드가 실행되면 JVM은 아래와 같이 동작한다.
- lock 객체의 모니터를 획득하려고 시도
- 이미 다른 스레드가 가지고 있으면 대기
- 락을 획득한 스레드만 코드 블록 진입 가능
- 코드 블록이 끝나거나 예외가 나면 락 해제
- 다음 대기 중인 스레드가 락을 획득함
Synchronized를 이용한 동시성 제어
synchronized + @Transactional
@Transactional
@Synchronized
fun decreaseStockQuantity(
productId: Long,
quantity: Int
) {
// 상품 조회
val product = productRepository.findByIdOrNull(productId)
?: throw IllegalArgumentException("Product not found with id: $productId")
// 재고 감소
product.decreaseStockQuantity(quantity)
}
}
위 재고 감소 로직에 @Synchronized를 붙여주면 간단하게 동시성 제어를 할 수 있다.
val productId = 1L
val quantity = 1
val threadCount = 50
test("50명이 동시에 1개씩 물건을 구매할 때 재고가 0 남아야한다.") {
val executorService = Executors.newFixedThreadPool(threadCount)
val latch = CountDownLatch(threadCount)
repeat(threadCount) {
executorService.execute {
try {
productService.decreaseStockQuantity(productId, quantity)
} catch (e: Exception) {
println("오류 발생")
} finally {
latch.countDown()
}
}
}
latch.await()
val afterQuantity = productRepository.findByIdOrNull(productId)
?: throw IllegalArgumentException("Product not found with id: 1")
afterQuantity.stockQuantity shouldBe 0
}
쓰레드를 이용해 50명이 동시에 재고를 감소시키는 테스트 코드를 실행해보면 아래와 같이 실패한다.
분명 synchronized를 이용하면 동시에 한 명만 접근이 가능하다고 했는데 왜 이런 결과가 나오는 걸까?
@Transactional의 작동 원리
@Transactional은 Spring AOP를 통해 동작하고, 이 AOP는 인터페이스 기반 프록시(JDK 동적 프록시) 또는 클래스 기반 프록시(CGLIB)를 생성한다. 이 프록시는 메소드를 가로채서 트랜잭션의 시작과 종료를 자동으로 처리해준다.
이때 synchronized는 프록시 클래스에 복사되지 않는다. synchronized는 바이트 코드 수준에서 메소드에 플래그를 다는 것이지, Java/Kotlin 언어의 시그니처(리플렉션 가능한 메소드 정의)에는 포함되지 않는다.
아래는 @Transactional로 생성된 프록시 객체의 예시이다.
코드를 보면 프록시 객체에는 synchronized가 적용되어 있지 않고, 트랜잭션이 시작되면 원본 클래스의 메소드(여기에 synchronized가 선언됨)를 실행한다. 이로 인해 트랜잭션이 커밋되기 전에 synchronized 블록이 먼저 종료되어, 다른 스레드가 해당 메소드에 접근할 수 있게 된다. 결과적으로 원하는 동시성 제어가 제대로 이루어지지 않게 된다.
public class ProductService$$EnhancerBySpringCGLIB extends ProductService {
private final TransactionManager txManager;
@Override
public void decreaseStockQuantity(Long productId, int quantity) {
TransactionStatus tx = txManager.beginTransaction();
try {
// 실제 synchronized는 여기에 없음
super.decreaseStockQuantity(productId, quantity); // 여기만 synchronized
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
throw e;
}
}
}
synchronized without @Transactional
그럼 @Transactional 없이 사용하면 어떻게 될까?
@Synchronized
fun decreaseStockQuantity(
productId: Long,
quantity: Int
) {
// 상품 조회
val product = productRepository.findByIdOrNull(productId)
?: throw IllegalArgumentException("Product not found with id: $productId")
// 재고 감소
product.decreaseStockQuantity(quantity)
productRepository.save(product)
}
}
결과는 성공이다.
프록시 객체가 만들어지지 않으므로 한 번에 하나의 쓰레드만 접근이 가능하다.
하지만 @Transactional 없이 사용하면 아래와 같은 문제점이 발생한다.
@Transactional을 사용하지 않을 때 발생하는 문제점
JPA의 더치 체킹(Dirty Checking)을 사용할 수 없음
JPA의 더티 체킹은 영속성 컨텍스트 내에서 엔티티의 상태 변화를 감지하여 자동으로 변경 사항을 반영한다. 하지만 이 영속성 컨텍스트는 트랜잭션 범위 내에서만 활성화되므로, @Transactional이 없으면 더치 체킹이 제대로 동작하지 않는다.
롤백을 불가능함
예제에서는 간단한 로직이지만, 실무에서는 여러 단계를 포함한 복잡한 비즈니스 로직이 실행된다. 이 과정에서 예외가 발생하더라도 트랜잭션이 없다면 롤백이 불가능하여 데이터의 원자성을 보장할 수 없다.
위 두 가지 문제로 인해 @Transactional은 대부분의 경우 필수적으로 사용되어야 한다. (물론 상황에 따라 예외는 존재할 수 있다..)
하지만 더 큰 문제점은 WAS 이중화 환경에서 발생한다.
WAS 이중화
synchronized는 JVM 인스턴스 단위로 동작하는 락이기 때문에, 분산 환경에서는 동시성을 보장할 수 없다. 서로 다른 WAS 인스턴스에서 실행되는 요청은 각각의 JVM에서 개별적으로 락을 잡기 때문에, 서로 간의 동기화가 불가능하다.
결론
synchronized는 가장 간단하게 동시성을 제어할 수 있는 방법 중 하나이다.
하지만 @Transactional과 함께 사용할 수 없고, WAS 이중화 환경에서는 동시성을 보장하지 못하기 때문에 실무에서는 거의 사용할 일이 없다고 생각된다.
다음 장에서는 WAS 이중화 환경에서도 사용할 수 있는 DB 기반 락에 대해 알아보겠다.
'Programming > SpringBoot' 카테고리의 다른 글
Spring Boot 동시성 제어 (3) - 비관적 락 (0) | 2025.04.08 |
---|---|
Spring Boot 동시성 제어 (2) - 낙관적 락 (0) | 2025.04.08 |
[SpringBoot] AMQP란? (0) | 2025.01.26 |
[SpringBoot] JPA 동적 스키마 (1) | 2024.11.06 |
[SpringBoot] @ModelAttribute 작동 원리 (0) | 2024.07.17 |