본문 바로가기

Backend/Database

[동시성 제어] JPA와 PostgreSQL을 활용한 동시성 제어 전략 (낙관적 락 vs 비관적 락)

 

 

Why?

  • 동시에 같은 데이터를 접근하거나 변경하는 상황은 자주 발생한다. ex) 재고 수량의 변경 등
  • 이 상황에서 데이터를 안전하게 관리하기 위한 2가지 방법
    • 낙관적 락 (Optimistic Lock)
    • 비관적 락 (Pessimistic Lock)

1. 낙관적 락 (Optimistic Lock)

낙관적 락은 각 버전에 대한 정보를 가지고, 트랜젝션이 완료될 때 데이터 변경의 충돌을 감지한다. (별도로 DB 자체에 락을 걸지 않는다.)

JPA에서는 @Version 어노테이션으로 사용

  • 장점
    • 락 대기 시간이 없어 동시성 성능 UP
    • 충돌이 없으면 빠르게 병행 처리가 가능
    • JPA가 버전 관리를 자동으로 처리해 구현이 간편
  • 단점
    • 충돌 발생 시 트랜잭션이 롤백되며, 재시도나 사용자 알림 처리가 필요 (충돌 발생 시 별도의 로직 필요)
    • 빈번한 충돌 상황에서는 성능 저하를 초래
  • 사용하기 적합한 상황
    • 동시 수정이 적은 게시물이나 프로필 관리 등 일반적인 웹 환경에 적합
[예제 코드]

엔티티
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int stock;

    @Version  // 자동으로 0부터 시작하여 증가
    private Long version;
    
}​



충돌 처리

@Service
public class ProductService {
    @Autowired
    private ProductRepository productRepository;

    @Transactional
    public void updateProductName(Long id, String newName) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException());
        product.setName(newName);
        try {
            productRepository.save(product);  // @Version 체크
        } catch (ObjectOptimisticLockingFailureException e) {
            // 충돌 발생: 재시도 또는 예외 처리
            throw new ConcurrentModificationException("다른 요청에서 먼저 수정되었습니다.");
        }
    }
}

 

2. 비관적 락 (Pessimistic Lock)

비관적 락은 충돌 가능성을 미리 방지하기 위해 데이터를 잠금 처리한다. JPA에서는 PESSIMISTIC_WRITE 등의 옵션을 통해 행 단위로 락을 설정 가능하다.

  • 장점
    • 동시 수정 충돌을 원천적으로 방지
    • 데이터 정합성 보장
  • 단점
    • 락으로 인해 동시성 성능이 저하되고 대기 시간이 늘어남
    • 데드락 발생 가능성이 존재
  • 사용하기 적합한 상황
    • 재고 관리, 은행 계좌 이체처럼 데이터 정합성이 매우 중요하고 동시 업데이트가 빈번한 경우 유리
[예제 코드]

Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    // 비관적 락으로 행 잠금
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Optional<Account> findByIdForUpdate(@Param("id") Long id);
}



Service

@Service
public class AccountService {
    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void withdraw(Long accountId, BigDecimal amount) {
        // 데이터를 가져오며 비관적 락 잠금
        Account account = accountRepository.findByIdForUpdate(accountId)
                          .orElseThrow(() -> new EntityNotFoundException());
        // 비즈니스 로직
        account.setBalance( account.getBalance().subtract(amount));
    }
}

 

3. PostgreSQL의 MVCC와 트랜잭션 격리 수준

PostgreSQL은 MVCC(다중 버전 동시성 제어) 방식으로 동시성 문제를 처리하며, 기본 격리 수준은 Read Committed이다.

  • Read Committed (기본값) : 성능과 일관성의 균형
  • Repeatable Read : 트랜잭션 동안 동일한 스냅샷으로 데이터를 일관되게 유지할 때 유리
  • Serializable : 완벽한 데이터 일관성이 필수적인 금융 시스템 등 제한적 상황

[실 적용]

  • 일반적으로 낙관적 락과 기본 격리 수준(Read Committed)을 사용하는 것이 안전
  • 충돌이 빈번하거나 데이터 정합성이 필수인 로직에 한정하여 비관적 락을 사용
  • 장기 트랜잭션을 피하고, 락 범위를 최소화하여 성능 저하를 방지 (트랜잭션 관리)

요약

  • 기본적으로는 구현난이도와 성능이 뛰어난 낙관적 락을 활용한다.
  • 필요시에만 (충돌이 일어나서는 안되는 경우) 비관적 락을 사용해 활용한다.

'Backend > Database' 카테고리의 다른 글

H2 관리 웹페이지에서 h2 설정이 사라졌을 때  (0) 2021.11.15