개발놀이터

온라인 쇼핑몰 ver.2 (4) : 동시성 문제 해결하기 본문

사이드 프로젝트/온라인 쇼핑몰 ver.2

온라인 쇼핑몰 ver.2 (4) : 동시성 문제 해결하기

마늘냄새폴폴 2023. 5. 21. 15:04

기존 동시성 문제

  • 기존에도 동시성 문제가 발생한다는 것을 느낌적으로 알아차렸습니다. 
  • 이에 Synchronized 키워드를 이용해 동시성 문제를 해결했습니다.

 

기존 동시성 문제의 문제점

  • Synchronized 키워드는 다른 동시성 문제를 해결하는 방법보다 성능상 좋지못합니다.

 

ver.2에서 개선한 동시성 문제

  • JPA의 특징 때문에 Synchronized 키워드는 어울리지 않는다고 판단하여 JPA와 어울리는 낙관적 락 / 비관적 락 중 하나를 선택하였습니다. 

 

 

우선 테스트 코드를 통해 "동시성 문제가 발생합니다" 라고 증명하는 것부터가 문제였습니다. 

그 때 마침 "단위 테스트" 라는 것에 꽂혀서 어디선가 DB를 불러오는 단위 테스트는 안티 패턴이다 라는 말이 생각났습니다. 그래서 Mock 객체를 이용한 단위 테스트에 대해 공부했습니다. 

Mockito 프레임워크에 대해 공부하게 되었고 처음엔 조금 헤맸지만 이내 원리를 파악하고 곧잘 하기 시작했습니다. 하지만 단위 테스트를 작성하고 보니 문제가 생겼습니다. 

'동시성 문제가 발생한다는 것을 단위 테스트로는 검증할 수가 없네..?'

어차피 Mock을 이용한 단위 테스트는 지금 하려는 동시성 문제를 해결하고 다음에 해야할 과제였기 때문에 좋은게 좋은거지 싶어서 넘어갔습니다. 

이제 동시성 문제가 발생한다는 것을 테스트로 증명해야한다. 라는 과제는 저에게 큰 시련을 주었습니다. 우선 테스트 코드를 작성하는 것을 많이 해보지 않아서 쉽지 않았는데 거기다 동시성 문제를 검증한다니...

구글링을 통해 동시성 문제가 발생했고 해결했다는 포스팅을 미친듯이 검색해서 어떤 패턴이 동시성 문제를 일으키는지에 대해 공부했습니다. 

제가 겪은 동시성 문제는 재고가 1개밖에 남지 않은 상황에서 동시에 두세개씩 요청이 들어오는 상황입니다. 

 

단일 스레드로는 이를 클라이언트 사이드에서 막을 수 있었습니다. 결제를 할 때마다 재고가 남아있는지 확인하는 로직이 들어있기 때문입니다. 

 

하지만 이런 상황은 굉장히 골치아팠습니다. 

 

몇번의 고민 끝에 저와 같은 상황에서 동시성 문제를 해결하기 위해서는 세가지 방법이 존재했습니다. 

 

  1. synchronized 키워드
  2. 데이터베이스 격리 수준을 Serializable로 격상
  3. 낙관적 락, 비관적 락

이 세가지에 대해서 알아보면서 기존에 알고 있던 CS 지식이 많은 도움이 됐습니다. 

 

JPA 전파 단계, 데이터베이스 격리수준, 프로세스 동기화, 뮤텍스와 세마포어, 낙관적 락 / 비관적 락, 교착상태와 기아상태

 

다양한 분야가 제가 동시성 문제를 해결할 때까지 많은 도움이 되었습니다. 

 

1. synchronized 키워드

synchronized 키워드만큼 동시성 문제를 간결하고 빠르게 해결할 수 있는건 없을 것입니다. 

 

하지만 JPA는 기본적으로 각각의 스레드에 다른 트랜잭션이 부여되고 이 트랜잭션들은 ACID를 준수하며 데이터베이스 격리수준에 기반해 웬만해선 동시성 문제가 발생하지 않습니다. 

 

때문에 데이터베이스 레벨에서 락을 걸던가 낙관적, 비관적 락을 사용하거나, 데이터베이스 격리수준을 격상하는 방법이 JPA를 사용한다면 조금 더 적합해 보였습니다. 

 

2. 데이터베이스 격리 수준을 Serializable로 격상

이것도 고려해보고 테스트를 진행했지만 만족스럽지 못한 결과물이 나왔기 때문에 채택되지 못했습니다. 

 

왜냐하면 기존 synchronized 로 동기화를 했다면 생각하지 않아도 되는 문제인 데드락이 발생했기 때문입니다. 

 

물론 List를 sort()메서드를 이용해 정렬해서 락을 획득하는 우선순위를 정해주면 '국지적으로' 데드락이 발생하지 않는 사실을 알게 되었지만 어디까지나 국지적으로 데드락이 예방되기 때문에 채택하지 못했습니다. 

 

또한 Serializable로 격상하는 것은 단순히 synchronized 키워드를 사용하는 것과 성능상 큰 차이를 보여주지 못했기 때문에 채택하지 못했습니다. 

 

3. 낙관적 락, 비관적 락

낙관적 락과 비관적 락에 대해서 이론적으로만 알고 있다가 이번에 실전에서 써먹어 봤습니다. 

 

낙관적 락과 비관적 락에 대해서 제가 알게된 내용은 다음과 같습니다. 

 

  • 낙관적 락 (Optimistic Lock) : 낙관적 락은 충돌이 빈번하게 일어나지 않는 경우에 비관적 락보다 성능상 이점이 있을 수 있고 충돌이 일어났을 경우 개발자가 다시 요청하는 로직을 try / catch 문을 이용해 구현해야 합니다. 
  • 비관적 락 (Pessimistic Lock) : 비관적 락은 충돌이 빈번하게 일어나는 경우 낙관적 락보다 성능상 이점이 있을 수 잇고 락을 통해 업데이트를 제어하기 때문에 데이터의 정합성이 어느정도 보장됩니다. 하지만 락에 의한 성능 저하는 감수해야 합니다. 

 

제 프로젝트에는 비관적 락을 선택하는 것이 올바르다고 판단했습니다. 

 

그 이유는 다음과 같습니다. 

 

  • 재고가 1개 밖에 없는데 세명이 동시에 결제를 요청했다면 뒤에 두사람은 결제가 일어나지 않아야합니다. 
  • 재고가 0개인데 주문이 들어가는 상황은 반드시 막아야할만큼 중요한 상황입니다. 
  • 낙관적으로 JPA에 의해 커밋이 되는 순간 충돌을 판단하여 OptimisticLockingFailuerException을 이용해 처리하는 방법도 있겠지만 재고가 1개인 상황에서 처음 사용자를 제외하고 모든 사용자를 통과하게 두면 안됩니다. 

때문에 저는 비관적 락을 이용해 동시성 문제를 해결했습니다. 이에 따르는 테스트 케이스도 작성을 완료했고 정상적으로 통과하는 것을 확인했습니다. 

 

 

느낀점

동시성 문제는 직관적이지 않지만 특정한 패턴이 있기 때문에 이번 프로젝트를 통해 동시성 문제에 대한 냄새(?)를 맡을 수 있게 된 것 같습니다.

 

또한 동시성 문제를 확인할 때 고려해야 하는 상황이 정말 많고 많은 클래스들이 꼬여있는 상황에선 정말 발견하기 힘들겠다는 생각도 했습니다. 

 

하지만 이번 경험을 통해 JPA가 어떤 방식으로 동시성 문제를 해결하기위해 방법론들을 준비해 뒀는지에 대해서 학습할 수 있었습니다. 

 

또한 멀티 스레드 환경의 테스트를 구축하는 방법도 터득하게 되어 추후에 멀티스레드 환경을 테스트하는 일이 있으면 요긴하게 사용할 것 같습니다.