Redis를 사용하여 동시성 문제 해결하기

mujik-tigers·2024년 2월 14일

수강 신청 상황에서 발생한 동시성 문제를 해결한 과정을 기록한 글입니다.

프로젝트 중간 점검을 마치고 기능 개선을 시작한 첫 주였습니다.
그 중에서 수강 신청 검증 단계에서 발생한 동시성 문제를 해결한 과정을 소개합니다. 😊


(1) 강의 정원을 초과해서 수강할 수 있다고?

현재 수강 신청을 위한 검증 단계에서 정원을 초과하여 신청하려고 하는지 확인하는 코드는 다음과 같습니다.

enrollment 는 강의 수강 신청에 성공하면 생기는 데이터입니다. 강의의 enrollment 개수를 count 해서 요청한 학생이 강의를 수강해도 정원을 초과하지 않는지 확인합니다.

위 코드가 제대로 된 검증을 수행하는지 테스트를 돌려서 확인해 보겠습니다.

정원이 30명인 강의에 대한 100개의 수강 신청 요청 스레드를 생성하여 수행한 결과,

위와 같이 테스트가 실패하는 것으로 나타났습니다.

즉, Race condition 문제가 발생하며 30명보다 더 많은 학생이 수강 신청에 성공하게 된 것입니다.


(2) 정원에 맞춰서 신청할 수 있도록 제대로 된 검증 구현하기

Race condition 문제를 해결하기 위해 데이터베이스에 Lock을 걸어서 순차적으로 요청을 처리할 수 있지만,
이 방법은 Lock이 걸린 범위에 대한 모든 접근이 불가하므로 수많은 요청을 처리하는 데 다소 많은 시간이 소요됩니다.

수강 신청이라는 단기간에 많은 사용자가 몰리는 이벤트 상황에 적합하지 않다고 판단하여 Redis를 사용해서 Lock을 구현하기로 합니다. 데이터베이스에 직접 Lock을 거는 것이 아니라 외부에서 Lock을 관리하기 때문에 해당 Lock 때문에 다른 비즈니스 로직에 부수 효과가 발생하는 것을 방지할 수 있습니다.

Spring Boot Redis의 기본 클라이언트인 Lettuce는 Spin Lock 방식으로 동작하기 때문에 많은 재요청이 필요한 수강 신청 상황에서는 Redis에 예상치 못한 부하를 발생시킬 수 있습니다. 따라서 최종적으로 pub-sub 방식의 Lock을 제공하는 Redisson 라이브러리를 사용하기로 결정했습니다.

Redisson GitHub


Lock의 key는 강의의 PK 값인 lectureId 로 지정하였습니다. 또한 lectureId or lectureNumber 를 사용하여 수강 신청 요청을 받고 있었기 때문에 lectureNumber 로 들어온 요청도 findByNumber() 를 통해 lectureId 를 읽어와서 Lock의 key로 사용하도록 하여 서로 다른 요청에도 정합성 문제가 발생하지 않도록 했습니다.


수정 전 EnrollmentService

EnrollmentService에서 lectureIdlectureNumber 각각의 요청에 따른 전처리를 한 다음 공통된 private enroll() 메서드를 사용하여 작업을 수행합니다.

새로 생성한 RedissonEnrollmentLockFacade

RedissonEnrollmentLockFacade를 생성하여 lectureIdlectureNumber 각각의 Lock 획득 Facade를 구현하고 EnrollmentService에게 enroll() 수행을 요청합니다.

수정 후 EnrollmentService

기존 private enroll() 메서드를 public으로 개방하고 lectureIdlectureNumber 각각의 요청에 대한 인터페이스는 제거했습니다.


(3) Lock 구현 후 테스트 결과 확인하기

이제 수정된 코드로 동일한 테스트를 수행합니다.

try 내부의 enrollmentService.enrollLecture()redissonEnrollmentLockFacade.enrollLecture() 로 변경한 다음 테스트를 수행한 결과,

AssertionError가 발생하지 않고 정상적으로 종료되는 것을 확인할 수 있었습니다.


lectureIdlectureNumber 요청이 함께 들어온 경우에도 동시성 문제가 발생하지 않는지 확인하기 위한 테스트도 작성해보았습니다.
번갈아가면서 lectureIdlectureNumber 를 사용하여 수강 신청 요청을 보내도록 했고 테스트 결과,

문제없이 동작하는 것을 확인할 수 있었습니다.


마무리

Lock을 구현하는 과정에서 Facade라는 레이어를 추가하면서 JPA Transaction 범위에 따른 초기화 문제가 생기기도 했습니다.
해당 문제에 대해 아티클을 참고하였고, 필요한 연관 관계를 Fetch Join을 사용하여 한 번에 조회함으로써 해결하였습니다.

또한 동시성 테스트를 통해 정합성 문제가 발생하는 것을 직접 확인함으로써 멀티 스레딩 프로그램에서 자원에 대한 접근을 관리하는 것의 중요성을 실감할 수 있었습니다.

이로써 과정을 모두 소개했습니다. 이어서 Redis를 캐시로 활용하여 성능을 개선해보는 작업을 해보려고 합니다.

긴 글 읽어주셔서 감사합니다 🙂


작성자 : 김서연

profile
mujik-tigers 프로젝트 블로그

0개의 댓글