상세 컨텐츠

본문 제목

[프로젝트 노트]동시성 이슈를 해결하는 여러가지 방법

Spring

by aeongiii 2024. 12. 16. 14:50

본문

1. 동시성 이슈

1.1 동시성

  • 여러 작업이 동시에 실행되는 상황을 말한다.
  • 예를 들어, 수많은 사용자가 동시에 웹 애플리케이션에 접속해 데이터를 수정하거나 읽을 경우를 말한다.

1.2 동시성 이슈

  • 여러 트랜잭션이 동시에 같은 데이터를 수정하거나 읽을 경우, 데이터의 일관성과 정합성이 깨질 수 있는 상황을 말한다.
  • Ex)
    • 은행 계좌 이체 - 두 사용자가 동시에 같은 계좌에서 출금하려고 할 때 금액이 올바르게 차감되지 않는다면?
    • 재고 관리 - 재고가 1개 남은 상황에서 두 사용자가 동시에 주문을 넣는다면?

2. 동시성 이슈 유형

 

2.1 Dirty Read (더티 리드)

  • 한 트랜잭션이 아직 커밋되지 않은 데이터를 읽어오는 경우
  • A 트랜잭션이 데이터를 수정했다가 롤백했지만, B 트랜잭션이 수정된 값을 미리 읽은 상황

2.2 Non-Repeatable Read (반복 불가능한 읽기)

  • 한 트랜잭션에서 같은 데이터를 두 번 읽었을 때, 그 사이 다른 트랜잭션이 데이터를 수정하여 결과가 달라지는 경우

2.3 Phantom Read (팬텀 리드)

  • 한 트랜잭션에서 데이터를 읽은 후 다른 트랜잭션이 새로운 데이터를 추가하면, 나중에 다시 조회할 때 결과가 달라지는 경우

3. 동시성 문제를 해결하는 방법

 

3.1 트랜잭션 격리 수준 (Transaction Isolation Levels)

  • 데이터베이스의 동시성 제어 강도를 설정하는 방법이다.
  • 격리 수준이 높을수록 엄격하게 제어할 수 있지만 그만큼 성능이 저하된다.
  • 반대로, 격리 수준이 낮을수록 성능은 높지만 동시성 문제가 발생할 수 있다.
  • 격리 수준의 종류
  Dirty Read Non-Repeatable Read Phantom Read 성능
READ UNCOMMITTED 발생 가능 발생 가능 발생 가능 높음
READ COMMITTED 방지 발생 가능 발생 가능 보통
REPEATABLE READ 방지 방지 발생 가능 낮음
SERIALIZABLE 방지 방지 방지 가장 낮음

 

3.2 데이터베이스 락 (Lock) - 비관적 락, 낙관적 락, 분산 락

 

3.2.1 비관적 락 (Pessimistic Lock)

  • 데이터를 읽거나 수정할 때 다른 트랜잭션이 접근하지 못하도록 막는 방식
  • 데이터를 사용할 때 먼저 Lock을 걸고, 트랜잭션이 종료될 때까지 다른 작업이 데이터를 수정하지 못하도록 한다.
  • 사용방법
    • Repository에 비관적 락 사용
    • @Lock(LockModeType.PESSIMISTIC_WRITE)
      @Query("SELECT p FROM Product p WHERE p.id = :productId")
      Optional<Product> findByIdWithLock(@Param("productId") Long productId);
    • Ex) 재고 수량이 1개뿐인 상품을 두 사용자가 동시에 구매할 경우 동시성 제어
    • @Transactional
      public void reduceStock(Long productId) {
          Product product = productRepository.findByIdWithLock(productId)
                  .orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다."));
          if (product.getStock() > 0) {
              product.setStock(product.getStock() - 1);
              productRepository.save(product);
          } else {
              throw new RuntimeException("재고가 부족합니다.");
          }
      }
  • 장점
    • 동시 수정을 방지하여 데이터 정합성 보장
    • 데이터 접근 제어를 직관적으로 확인할 수 있음
  • 단점
    • Lock이 실행되면 다른 트랜잭션이 기다려야 해서 성능이 저하된다.
    • 여러 트랜잭션이 서로의 락을 기다리면 DeadLock(교착 상태)에 빠질 수 있다.
  • 비관적 락을 사용하기 적절한 상황은?
    • 동시 수정 가능성이 높을 때
    • 데이터 정합성이 매우 중요한 경우 (은행 업무, 재고 관리 등)

3.2.2 낙관적 락 (Optimistic Lock)

  • 데이터를 읽고 수정할 때 다른 트랜잭션이 변경하지 않았을 것이라고 가정!하는 방식
  • 트랜잭션이 완료되기 전에 데이터의 버전이나 수정 시각을 확인해 충돌을 감지한다.
  • 사용 방법
    • 엔티티에 @Version 필드를 추가
    • @Entity
      public class Product {
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          private int stock;
      
          @Version
          private Long version; // 버전 관리
      }
    • Ex) 수정하는 상품 데이터의 버전이 일치하는지 확인
    • @Transactional
      public void reduceStock(Long productId) {
          Product product = productRepository.findById(productId)
                  .orElseThrow(() -> new IllegalArgumentException("상품이 존재하지 않습니다."));
          product.setStock(product.getStock() - 1);
          productRepository.save(product);
      }
  • 장점
    • 데이터를 잠그지 않기 때문에 다른 트랜잭션의 접근을 막지 않고 성능이 좋다.
    • DeadLock이 발생하지 않는다.
  • 단점
    • 동시 접근이 자주 일어나는 경우 충돌 때문에 롤백을 자주 하게 됨
    • 충돌 감지를 위해 버전 필드를 읽고 검증해야 하므로, 매번 데이터를 읽는 비용이 생김
  • 낙관적 락을 사용하기 적절한 상황은 ?
    • 동시 수정 가능성이 낮은 경우
    • 쓰기 작업보다 읽기 작업이 많은 경우

3.2.3 분산 락

  • 여러 서버 가 분산된 환경에서 Lock을 적용하는 방식
  • 사용방법
    1. Redis를 사용한다. SETNX 명령어를 사용해 락을 설정할 수 있다.
    2. Zookeeper를 사용하여 노드 기반으로 Lock을 관리한다.
      • 분산 시스템 환경에서 자주 사용되는 방법이라고 한다.

3.2.4 기타 락

  • Shared Lock (공유 락) : 읽기 작업만 가능하며, 다른 트랜잭션 접근 가능.
  • Exclusive Lock (배타적 락) : 읽기 및 쓰기 작업을 독점한다. 다른 트랜잭션은 접근 불가
  • Deadlock Detection : 여러 트랜잭션이 서로를 기다리며 교착 상태에 빠질 경우 감지하고 해결한다.

 

3.3 동시성 제어 알고리즘

3.3.1 Timestamp Ordering (타임스탬프 오더링)

  • 각 트랜잭션에 타임스탬프를 부여하고 트랜잭션의 순서에 따라 동시성을 제어한다.
  • 원칙 : 더 늦게 시작된 트랜잭션은 먼저 시작된 트랜잭션의 데이터를 덮어 쓸 수 없다.
  • 순서가 중요한 시스템에서 유용하다.

3.3.2 MVCC (Multi-Version Concurrency Control)

  • 데이터의 여러 버전을 유지하면서 동시성을 제어
  • 읽기 작업은 이전 버전의 데이터를 사용하고, 쓰기 작업은 새 버전을 만들어 사용한다.
  • 일반적으로 PostgreSQL, MySQL InnoDB 엔진에서 사용된다.
  • 장점
    • 읽기 성능이 뛰어나다.
    • 잠금이 필요하지 않다.

3.3.3 큐잉 시스템 (Queue-Based Systems)

  • 순차적으로 작업을 처리하기 위해 큐를 사용한다.
  • Ex) 주문 / 재고 관리 시스템 등 순서대로 요청을 처리하는게 중요한 업무

4. 애플리케이션 레벨 동시성 제어

 

4.1 Synchronized

  • 자바의 Synchronized 를 사용하면 단일 스레드만 특정 메서드를 실행하도록 만들 수 있다.
  • public synchronized void updateStock() {
        // 하나의 스레드만 접근 가능
        stock--;
    }
  • 장점
    • 구현이 간단하고 직관적이다.
  • 단점
    • 성능 저하와 데드락 위험이 있다.

4.2 ReenTrantLock

  • Synchronized 보다 유연하게 사용할 수 있는 ReenTrantLock 키워드를 사용한다.
  • private final ReentrantLock lock = new ReentrantLock();
    
    public void updateStock() {
        lock.lock();
        try {
            stock--;
        } finally {
            lock.unlock();
        }
    }
  • 장점
    • 타임아웃 설정 가능
    • 공정성(fairness) 옵션 사용 가능
  • 단점
    • 구현이 복잡해진다.

5. 내 프로젝트에서의 동시성 제어

  • 재입고 알림 서비스를 구현하면서 재입고 회차를 갱신하는 부분과 알림 발송 상태를 업데이트하는 부분이 동시성 문제 가능성이 있었다.
  • 낙관적 락과 비관적 락 중에서 고민 → 비관적 락 사용
    • 낙관적 락의 성능이 좋긴 하지만, 충돌이 발생할 경우 자주 롤백할 것 같았다.
    • 반면, 동시 수정될 가능성이 높고 데이터 정합성이 중요한 프로젝트라고 판단되어 비관적 락이 적절하다고 생각했다.
  •  

관련글 더보기