동시성 문제와 해결방법 알아보기 (synchronized, DB Locking)

나른한 개발자·2023년 8월 3일
0

CS

목록 보기
11/11
post-custom-banner

동시성 문제란?

하나의 가변 데이터에 두 개 이상의 스레드가 접근할 때 발생할 수 있는 문제

멀티 스레드 환경에서는 자원을 공유하며 작업을 동시 처리한다. 이때 하나의 스레드에서 작업이 끝나기 전에 다른 스레드에서 동일한 데이터에 접근하게 된다면 데이터의 정합성이 깨지는 문제가 발생할 수 있다.

예를 들어 상품의 재고 수량을 조정하는 경우를 생각해보자.

재고 수량이 5개가 남아있다고 할 때, A스레드에서 해당 데이터에 접근하여 5개의 재고수량을 확인하고 물품 1개를 구매 처리 하려고 한다. 이때 A스레드의 작업이 끝나기 전에 B스레드에서 재고수량을 확인하여 물품 1개를 구매 처리한다면 B스레드에서는 재고수량이 5개일때의 값을 참조했기 때문에 결과적으로 재고 수량이 4개가 된다.

두번의 구매 요청이 들어왔기에 최종적으로 재고가 3개가 되어야한다고 예상했지만 예상과는 다른 결과가 나온 것이다.

이렇게 동시성 문제는 데이터 정합성을 해치고 예상과는 다른 결과를 만들어 내기 때문에 주의해야한다.

해결 방법

이러한 동시성 문제를 해결하기 위해서는 1. synchronized 를 사용하는 방법과 2. 데이터베이스 락을 사용할 수 있다.

synchrozied와 @Transactional

synchronized는 자바에서 하나의 스레드만이 메서드에 접근할 수 있도록 락을 걸어준다.

@Transactional
public synchronized Item decreaseStock(Long itemId, int quantity){
	Item item = itemRepository.findOne(itemId);
    item.decreaseStock(quantity);
    return item;
}

하지만 @Transactional 어노테이션과 synchronized 를 함께 사용하는 경우 동시성 문제가 해결되지 않는다.

그 이유는 @Transactional 은 로직을 수행 후 마지막으로 commit을 하하는데, 이 커밋이 되기 전에 메서드에 대한 락이 풀려버려 다른 스레드가 해당 메서드에 접근할 수 있기 때문이다.

따라서 트랜잭션이 적용되기 전 컨트롤러 단에서 synchrozied를 써주거나 @Transactional 어노테이션 없이 synchrozied만 단독으로 사용해야 동시성 문제를 해결할 수 있다.

하지만 synchronized의 경우 모든 스레드 동작에 대해 락을 걸어버려 오버헤드가 발생할 수 있고, 하나의 프로세스에서만 격리성을 보장하기 때문에 두 개 이상의 서버에서는 동시성 문제를 해결할 수 없다.

DB Lock

두번째로는 데이터베이스 단에서 락을 거는 것이다. 데이터베이스의 락킹은 두가지가 있다.

비관적 락

비관적 락은 동시성 문제가 발생할 것이라고 가정하며 데이터 접근에 대해 항상 락을 거는 방식이다. 하나의 트랜잭션이 데이터를 읽는 시점에서 락을 걸고 조회 및 업데이트가 완료될 때까지 유지한다.

장점

  • 동시 접근을 방지하여 순차적인 처리가 가능하다.

단점

  • 동시성이 떨어져 대기가 길어지고 성능이 저하될 수 있따.
  • 데드락을 유발할 수 있으므로 주의해야한다.

💡 데드락?
각 트랜잭션이 자원을 서로 자원을 점유한 채, 서로 자원을 요청하며 무한정 대기하는 교착상태

낙관적 락

낙관적락은 비관적 락과는 달리 여러 트랜잭션이 동시에 값을 수정하지 않는다고 가정한다. 즉 자원에 락을 걸지 않고 충돌이 발생하면 그때 해결하는 방식이다.

@Entity
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long itemId;
    private int stockQuantity;
    
    @Version
    private long version;
    ...
}

위와 같이 version 필드를 추가하여 데이터를 읽는 당시엔 락을 걸지 않고 수정이 발생할 경우 내가 읽은 데이터의 버전이 맞는지 확인한다. 버전이 다르다면 쿼리가 실패하고, 다시 데이터를 읽은 뒤 버전을 맞추어 데이터 변경을 한다.

장점

  • 읽기 연산 시에는 락을 걸지 않아 성능이 좋다. 동시 업데이트가 없다고 판단되는 경우에는 비관적 락보다 조회 및 업데이트 성능이 좋다.

단점

  • 충돌이 발생한 경우를 직접 처리해야한다.
  • 충돌이 빈번하게 발생하는 경우 롤백 처리를 해주어야하기 때문에 비관적락이 성능이 더 좋을 수도 있다.

참고
https://velog.io/@msung99/JPA-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDOptimistic-Lock-%EA%B3%BC-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BDPessimistic-Lock-%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0#%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDoptimistic-lock
https://thalals.tistory.com/370

profile
Start fast to fail fast
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 8월 3일

많은 도움이 되었습니다, 감사합니다.

답글 달기