Develop/Database

동시성과 정합성을 어떻게 관리할 수 있을까?

코딩의성지 2023. 3. 19. 20:03

이전에 동시성에 대한 내용을 다룬적 있다.

https://devkingdom.tistory.com/303

 

동시성 프로그래밍에 대하여

백엔드 개발자라면 동시성을 고려한 프로그래밍을 할 줄 알아야한다. 다만 아직 학생이거나 주니어 레벨에서는 이러한 동시성을 이해하기가 쉽지는 않다. 프론트 단의 개발과는 다르게 백엔드

devkingdom.tistory.com

관련한 내용을 옛자료를 보고 정리를 해놔서... 요즘 대체적으로 많이 쓴느 개발 환경인 Spring JPA 에서는 어떻게 적용하면 될지를 정리해야겠다는 생각을 했었다.

 

우선 상황을 가정해보자.

상품 구매 시스템을 만든다고 가정하고, 인기 있는 상품을 조회하는 기능을 개발한다고 가정하자.

 

상품 구매 -> 이미 구매된 동일 상품이 있는지 조회 -> 상품 정보 추가 -> 상품 구매 수 업데이트

 

우리가 구현해야할 상황은 위와 같다.

 

보통 JPA 에서 업데이트 관련 로직은 JPA의 더티 체킹(변경감지)를 이용하여 처리하는 경우가 많다. 더티 체킹 (변경감지)는 커밋종료 시점 혹은 영속성 컨텍스트의 flush가 일어나는 시점에 엔티티의 스냅샷을 비교해 변경된 컬럼이 있는지 확인해 업데이트 쿼리를 실행하는 방식인데, 직접 쿼리를 작성하지 않아도 되는 장점은 있지만 동시성 부분에서 문제가 존재한다.

 

예를들어 A라는 고객이 I1 아이템을 구매하고, 구매 카운트가 하나 올라가는 트랜잭션이 시행되고, 

B라는 고객도 I1 아이템을 구매하고, 구매 카운트가 하나 올가는 트랜잭션이 수행 된다고 할때, 이 트랜잭션이 각각 실행된다고 할때, 하나의 허점이 존재한다.

 

DB 내부에서 카운트를 업데이트 하는 것이 아니고 엔티티의 스냅샷을 비교하는 것이기에 ,  현재 카운트가 10이라 가정하면

한 트랜잭션이 카운트 10인 상태에서 트랜잭션을 시작해 카운트 1을 올린 상태인  11로 커밋이 되고, 마찬가지로 다른 트랜잭션도 카운트가 10인 상태에서 트랜잭션을 시작해 카운트 1을 올린 11로 커밋한다면, 두번의 트랜잭션에 의해 12가 되어야 맞지만 결과는 11이될 수도 있다는 것이다. 동시성에 문제가 생겨 데이터 자체의 정합성에 문제를 줘버리는 것이다.

 

이러한 문제를 해결하기 위해 몇가지 방법이 있다.

 

1.낙관적 락 (Optimistic Lock) 

일단 낙관적 락을 사용하는 방법을 고려해보자. 그동안 소규모의 프로젝트를 개발할때는 나는 간단하게 이 낙관적 락을 이용해 특정 정보 조회 시 배타락 (Exclusive Lock) 을 얻게 만들어, 이 트랜잭션이 끝나기전에 다른 트랜잭션이 어떠한 락도 가져올 수 없도록 만들었다. 이렇게 처리해주면 트랜잭션이 끝날때까지 다른 트랜잭션은 대기하게 되니, 정합성 문제도, 데드락 문제도 해결된다.

 

하지만 이 낙관적 락의 경우 트랜잭션이 끝날때까지 기다려야 하기 때문에 , 대기 시간이 생겨 트랜잭션이 늘어다면 늘어 날수록 API의 콜 대기 시간이 늘어나는 문제가 발생한다. 최근에 프로젝트를 진행하며 다른 개발자분께서 이러 저러한 이유로 이 낙관적 락 사용을 피한다는 이야기를 들었다.

 

2. 비관적 락 (Pessimistic Lock)

어떻게 보면 JPA 와 비관적 락은 찰떡 궁합이다. 업데이트 쿼리 호출 시 버전이 일치하는 경우만 커밋을 하고 버전을 업데이트 해주는 방식인데, 버전이 일치하지 않으면 롤백되게 만든다. 카운트 10인 상태에서 트랜잭션을 시작해 카운트 1을 올린 상태인  11로 커밋이 되는 트랜잭션이 수행되고, 또 다른 트랜잭션이 카운트가 10인 상태에서 트랜잭션을 시작해 11로 업데이트 하려고 할 때, 버전정보가 맞지 않아 실패하게 된다. 이러한 경우 분명 정합성은 맞추었지만... 결과적으로 서비스가 실패하는 현상이 생긴다. 

 

3. 쿼리 직접 실행

마지막 방법은 더티 체킹을 포기하고 레코드 자체의 값을 통해 통계를 계산해주는 방법이다. 엔티티의 값을 변경하는 것이아닌 서비스 레이어에서 직접 레퍼지토리의 메서드를  호출해 정합성을 맞춘다. 이렇게 할 수 있는 이유는 update 쿼리가 데이터베이스 자체적으로 배타락을 걸기때문에 가능한 것이다.

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = "update Product p set p.sellingCnt = p.sellingCnt + 1 where p.id = :productId)
void increase(Long productId);

이렇게 하면 비지니스 로직도 간소화되어 훨씬 빨라지고, 또한 배타락 덕분에 정합성도 보장된다. 사실 이러한 방식은 JPA 의 더티 체킹 기능을 사용하지 않기에, 객체지향적인 관점에서는 옳지 않은 방식일 수도 있다.

 

하지만 확실한 성능을 보장하고, 데이터 정합성을 지키는데 좋아 실무에서 많이 사용하는 방식이니 잘알아두도록 하자.

 

반응형