항해99 실전 프로젝트 3주차를 진행하며 발생한 동시성 문제 및 해결 과정을 작성해보려고 한다. 우리팀의 프로젝트 주제는 '수강신청' 이다. 많은 트래픽이 몰리는 상황에서 수강신청 시 발생할 수 있는 동시성 문제에 관한 글이다.
✔️ 수강신청 동시성 문제 발생
우리 프로젝트에서 동시성 문제는 모두 동시에 요청할 수 있는 '수강신청' 부분에서 발생하였다. Jmeter를 통해 테스트를 진행하여, 동시성 문제가 발생하는 것을 확인했다.
우리 프로젝트는 '수강신청' 전에 해당 강의를 장바구니에 담아두어야 한다. 그래서 테스트를 위한 회원 정보 및 장바구니 정보는 미리 담아둔 상태에서 수강신청에 대한 테스트를 진행했다.
💡 설정 조건
MySQL 데이터
수강 신청 테스트를 위한 로컬 MySQL에 데이터 설정을 했다.
- 학생(회원) 수 : 100명 (동시 요청 스레드 수) - Student 엔티티
- 과목 수강 신청 가능 잔여 수 : 200개 (200명 이상 수강신청이 불가능하다. 선착순 남은 자리!) - Subject 엔티티
Jmeter 설정
우리 프로젝트는 JWT를 이용하여 인가를 진행한다. 때문에 수강신청 요청을 보낼 때 쿠키에 JWT 토큰 정보를 넣어주어야 한다. 테스트 진행 순서는 아래와 같다.
- 미리 Jmeter를 이용하여 100명의 회원을 로그인 시키고 토큰 값을 csv파일로 저장해둔다.
- 100명의 장바구니에 수강신청할 과목(모두 동일하게)을 담아둔다.
- 동일한 과목에 대해서 100명이 동시에 수강신청을 요청한다.
100개의 스레드로 Ramp-up Time을 1초로 하여 1초만에 100개의 요청을 동시에 보내도록한다.
💡 테스트 결과
Jmeter 보고서에는 오류없이 100개의 요청이 정상적으로 진행됐다.
Subject 엔티티의 limit_count(남은 잔여 자리 수)는 125가 되었다. 처음 200개의 잔여 수 에서, 100명이 동시에 요청 했다면 100개의 잔여 자리 수가 남아야 하는데, 동시성 문제가 발생한 것을 확인 할 수 있다.
✔️ 동시성 문제 해결 방법 - 낙관적 락(Optimistic Lock) vs 비관적 락(Pessimistic Lock)
💡 낙관적 락(Optimistic Lock)
낙관적 락은 자원에 락을 걸지 않고, 동시성 문제가 발생하면 그 때 처리 하도록 한다.
- 대표적인 예시로 version과 같은 컬럼을 별도로 추가하여 데이터가 변경되었는지 확인
- 데이터 충돌의 발생 가능성이 적을 경우에 비관적 락보다 성능이 좋음
💡 비관적 락 (Pessimistic Lock)
비관적 락은 트랜잭션이 데이터를 사용할 때 해당 데이터에 락을 걸어서 다른 트랜잭션이 데이터를 변경할 수 없도록 한다.
- 공유 락 (Shared Lock) : 공유 락은 여러 트랜잭션이 동일한 데이터를 동시에 읽을 수 있지만, 동시에 데이터를 수정할 수는 없도록 함
- 배타 락 (Exclusive Lock) : 한 번에 하나의 트랜잭션만 해당 데이터를 읽거나 수정할 수 있음
우리의 수강신청 프로젝트는 트랜잭션 충돌이 매우 빈번한 상황이다. 낙관적 락을 적용할 경우, version 확인 및 update에 따른 루프로 서버 부하, 성능의 저하가 예상된다.
때문에 비관적 락을 적용하는 것이 부하 감소 및 성능이 낙관적 락 보다 좋을 것으로 판단하게 되었다.
✔️ 비관적 락(Pessimistic Lock) 적용 및 테스트
💡 코드 수정
과목 정보를 불러오는 메서드에 JPA에서 제공하는 @Lock 어노테이션으로 비관적 락(배타 락으로 적용)을 적용했다. MySQL에서 FOR UPDATE를 사용하여 데이터가 잠기게 된다.
service 비즈니스 로직에서 비관적 락을 이용하여 과목 정보를 가져와서 활용하도록 수정하였다.
💡동시성 제어 테스트 및 결과
비관적 락을 적용하기 전의 테스트 상황과 동일하게 테스트를 진행했다. 100명의 학생으로 Jmeter를 활용하여 수강신청 동시 요청을 보내보았다.
Jmeter에서 오류없이 잘 요청 되었고, subject의 남은 잔여 과목 수가 원하는 대로 100개가 남은 것을 확인 할 수 있다.
💡 동시성 제어 테스트 코드 작성
Jmeter로는 확인이 되는 것을 보았으니, 테스트 코드를 작성하여 검증 해보자. 테스트 코드는 H2 인메모리 DB로 테스트를 진행하였다.
📝 테스트 데이터 설정
테스트 전 @BeforeEach를 활용하여 학생 정보, 과목 정보, 장바구니 정보를 담아준다.
- 학생 수 : 100명
- 과목 수강신청 잔여 수 : 500개
학생 100명이 동시에 요청하면 500개에서 100개가 줄어든 400개가 남아야한다.
📝 테스트 코드 작성
ExecutorService와 CountDownLatch를 활용하여 테스트 코드에서 100개 요청을 한번에 보내본다.
처음 설정한 강의 잔여 수(500개)에서 100개의 요청을 뺀 400개가 정확히 남았는지 assertThat으로 검증하여 성공.
✔️ 추후 고려할 점
- 분산 락(Distributed Lock) : 여러 시스템 또는 프로세스 간에 공유된 자원에 대한 동시성을 보장하기 위해 사용되는 동기화 메커니즘
MySQL, Redis를 활용하는 분산 락에 대해 알아보고, 비관적 락과 어떤 차이점이 있는지 비교가 필요.
또한 분산 락을 적용했을 때와 비관적 락을 적용했을 때, 어느 것이 우리 프로젝트 상황에서 더 성능이 좋은지 비교하여 적용하면 좋을 것 같다.