본문 바로가기

Spring

동시성 처리

 

 

자바 코드에서 Thread Safe하도록 만드는 방법으로 synchronized 키워드 사용하는 것이 있다.

 

하지만 Thread Safe와 동시성 이슈로 인한 DB 데이터 정합성과는 또 다른 이야기이다.

즉 synchronized 키워드로는 모든 동시성 이슈를 해결할 수는 없다.

 

이유는 다음과 같다.

Synchronized 의 문제점

1. Synchronized 는 하나의(각각의) 프로세스 안에서만 보장이 된다.

서버가 여러 대일 경우에는 각 서버에서 돌아가는 프로세스에서 각각 하나씩의 synchronized 메소드를 실행할 수 있게 된다.

즉 두 개 이상의 프로세스에서 동일한 DB값에 접근해서 데이터를 수정할 수 있게 된다는 것이다.

 

둘 이상의 프로세스가 동일한 DB값에 접근해서 데이터를 수정한다면 synchronized 한 속성을 보장할 수 없다.

즉, race condition을 보장할 수 없다.

 

실제 서비스에서는 여러 대의 서버에서 같은 DB를 바라보기 때문에, synchronized는 거의 사용되지 않는다.

 

이런 문제를 해결하기 위해서 DBMS의 기능을 활용해서 데이터의 정합성을 맞추는 방법이 필요하다.

 

2. @Transactional 어노테이션과 같이 사용할 경우 의도한 대로 동작하지 않는다.

이는 @Transactional 어노테이션이 AOP 방식으로 동작하기 때문에 발생한 문제이다.

AOP 동작 방식은 다음과 같다.

해당 클래스를 상속받은 프록시 객체(클래스)를 생성하고 그 안에서 [트랜잭션을 시작 → 원하는 메소드 실행 → 트랜잭션 종료] 의 과정을 실행한다.

생성된 프록시 객체에는 synchronized 키워드가 상속되지 않기 때문에 여러 스레드에서 그 메소드를 실행시킬 수 있는 것이다.

 

즉, synchronized 키워드와 트랜잭션을 같이 사용하고 싶다면 트랜잭션을 직접 구현하는 코드를 사용해야 할 것이다.

 

MySQL의 LOCK 활용

 

1. Pessimistic Lock (비관적 락)

 

실제로 데이터에 Lock 을 검으로써 데이터의 정합성을 맞추는 방법이다.

 

exclusive lock을 걸면 lock이 풀리기 전에는 다른 트랜잭션에서는 데이터에 접근을 할 수 없어진다.

하지만 데드락이 발생할 수 있기 때문에 주의해야 한다.

 

2. Optimistic Lock (낙관적 락)

실제로 Lock을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법이다.

데이터를 읽은 후에 update를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하며 업데이트를 한다.

내가 읽은 버전에서 수정사항이 생겼을 경우 application에서 다시 읽은 후에 작업을 수행해야 한다.

 

3. Named Lock (네임드 락)

이름을 가진 metadata locking이다.

이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없도록 해야 한다.

주의할 점은 transaction이 종료될 때 lock이 자동으로 해제되지 않는다.

별도의 명령어로 해제를 해주거나 선점시간이 끝나야 해제가 된다.

 

  • Pessimistic lock과 Named Lock의 차이점
    • named lock은 row나 table이 아니라 metadata를 가지고 locking을 하는 것이라는 차이가 있다.
    • pessimistic lock은 row나 table 단위로 걸지만,

1. Pessimistic Lock (비관적 락)

방법

Lock을 걸고자하는 쿼리에 @Lock 어노테이션을 사용한다.

public interface StockRepository extends JpaRepository<Stock, Long> {

    Optional<Stock> findByProductId(Long productId);

    **@Lock(LockModeType.PESSIMISTIC_WRITE)**
    @Query("select s from Stock s where s.productId = :productId")
    Stock findByProductIdWithPessimisticLock(Long productId);

}

‘for update’라고 되어있는 부분이 Lock을 거는 부분이다. 즉 이 쿼리는 Lock을 걸고 데이터를 조회해오는 쿼리이다.

 

이렇게 하면 100개의 스레드에서 각각 재고를 감소시키는 테스트에서 성공을 한다.

장점

  1. 충돌이 빈번하게 일어나는 경우라면 Optimistic Lock보다 성능이 좋을 수 있다.
  2. Lock을 통해 업데이트를 제어하기 때문에 데이터 정합성이 보장된다.

단점

DB에서 별도의 Lock을 잡기 때문에 성능 감소가 발생할 수 있다.

 

참고)

@Lock 어노테이션에서 Pessimistic lock 모드를 PESSIMISTIC_READ 로 했을 때에는 쿼리가 아래와 같이 for share 가 뜨고 100개의 동시 요청에 대한 테스트는 실패한다.

public interface StockRepository extends JpaRepository<Stock, Long> {

    Optional<Stock> findByProductId(Long productId);

    **@Lock(LockModeType.PESSIMISTIC_READ)**
    @Query("select s from Stock s where s.productId = :productId")
    Stock findByProductIdWithPessimisticLock(Long productId);

}

 

 


2. 낙관적 락 (Optimistic Lock)

실제 DB의 Lock을 사용하지 않고 버전을 사용함으로써 정합성을 보장하는 방법이다.

 

Optimistic Lock을 사용하기 위해서 테이블(entity)에 ‘버전’을 의미하는 컬럼을 추가해주어야 한다.

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class StockForOptimisticLock {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long productId;
    private Long quantity;
    
    **@Version   // persistence 패키지의 어노테이션
    private Long version;  // 추가된 컬럼**
}

Optimistic Lock은 update를 시도 후 실패했을 때 재시도를 해야 한다.

 

 

장점

별도의 Lock을 잡지 않으므로 Pessimistic Lock보다 성능상 이점이 있다.

단점

업데이트가 실패했을 때 재시도 로직을 개발자가 직접 작성해주어야 하므로 번거롭다는 점이 있다.

 

충돌이 빈번하게 일어난다면 혹은 충돌이 빈번하게 일어날 것이라고 예상이 된다면 Pessimistic Lock을, 그러지 않다면 Optimistic Lock을 추천한다.


Named Lock

이름을 가진 metadata lock이다.

이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 lock을 획득할 수 없다.

 

주의할 점은 트랜잭션이 종료될 때 lock이 자동으로 해제되지 않기 때문에, 별도의 명령어로 해제를 수행해주거나 선점 시간이 끝나야 lock이 해제된다.

 

mysql의 경우에는 get_lock 명령어를 통해 named lock을 획득할 수 있고 release_lock 명령어를 통해 lock을 해제할 수 있다.

 

 

pessimistic lock과는 다르게, 직접 테이블이나 row에 lock을 거는 것이 아니라 별도의 공간에 lock을 걸게 된다.

 

Named Lock은 주로 분산 락을 구현할 때 사용한다.

pessimistic lock은 타임 아웃을 구현하기 힘들지만 named lock은 타임아웃을 손쉽게 구현할 수 있다.

 

데이터 삽입 시에 정합성을 맞춰야 하는 경우에도 named lock을 사용할 수 있다.

 

하지만 lock 해제와 session 관리를 잘 해주어야 하기 때문에 주의해야 하고, 실제로 사용할 때에는 구현 방법이 복잡할 수 있다.

 

Reference

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

'Spring' 카테고리의 다른 글

[JPA] 1차 캐시 작동 조건과 적용 범위  (0) 2024.09.22
Not Found QClass  (0) 2023.11.28