항해 2주차에는 특강 신청 서비스를 개발하면서 신청 시에 발생할 수 있는 동시성 이슈를 잡는게 목표였다.
정확하게 30명의 사용자에게만 특강을 제공할 방법을 고민해보자.
같은 사용자에게 여러 번의 특강 슬롯이 제공되지 않도록 제한할 방법을 고민해보자.
위 2가지가 이번 주차의 KEY POINT 이므로 이를 지키도록 하는 리팩토링을 하면서 새롭게 알게된 기술과
고민했던 부분을 적어보려고 한다.
목차
- 동시성 제어 후보
- 낙관적 락과 비관적 락 중 선택
- 비관적 락 구현 방법 및 종류
- 락 적용 전과 후
- 비관적락 사용 시 고려 해야할 점
- 회고
동시성 제어 후보
낙관적 락(Optimistic Lock)
데이터를 업데이트하기 전 다른 사용자가 데이터를 변경했는지 확인하는 방식이다.
- 버전 번호나 타임 스탬프를 사용한다.
- 데이터 읽기 시 락을 걸지 않고, 업데이트 시점에서 데이터의 버전을 비교하여 충돌 여부를 확인한다.
- 충돌이 발생하면 애플리케이션에서 처리한다.
- 장점 : 락을 걸지 않아 읽기 성능이 뛰어나고, 충돌 가능성이 낮은 시나리오에서 효과적이다.
- 단점 : 데이터 충돌이 잦을 경우 처리 비용이 높아질 수 있다.
비관적 락(Pessimisic Lock)
데이터를 접근하는 동안 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 락을 걸어 충돌을 방지하는 방식이다.
- 데이터베이스 수준에서 SELECT ... FOR UPDATE 또는 LOCK INSHARE MODE 같은 쿼리를 사용한다.
- 락이 해제될 때마다 다른 트랜잭션은 대기하거나 실패한다.
- 장점 : 데이터 충돌 가능성이 높은 경우 충돌을 효과적으로 방지한다.
- 단점 : 트랜잭션이 오래 지속되거나 많은 사용자가 동시 접근할 경우 데드락(교착 상태) 위험이 있고, 성능이 저하될 수 있다.
낙관적 락과 비관적 락 중 선택
요구사항 분석
동시에 여러 사용자가 신청할 경우 30명까지만 허용, 같은 사용자가 여러번 신청할 수 없어야 한다.
낙관적 락과 비관적 락 비교
- 낙관적 락 : 동시에 여러 사용자가 신청하면 충돌이 발생할 가능성이 높고, 이후 처리를 애플리케이션 레벨에서 해야하므로 복잡도가 증가한다.
- 비관적 락 : 동시성 문제를 데이터베이스 레벨에서 해결하여 일관성을 보장할 수 있다.
선택
데이터의 충돌 가능성이 높고, 데이터 일관성 보장이 중요하므로 비관적 락이 더 적합하다.
비관적 락 구현 방법 및 종류
- JPA를 이용하여 비관적 락을 거는 방식이 있는데, 이는 Spring Data JPA에서 @Lock 어노테이션을 사용하여 비관적락을 구현한다.
- 공유락(Shared Lock)
- 읽기 작업(Read)는 허용하지만, 쓰기 작업(Write)는 허용되지 않는다.
@Lock(LockModeType.PESSIMISTIC_READ)
- 배타락(Exclusive Lock)
- 데이터에 대한 읽기 작업(Read)와 쓰기 작업(Write) 모두 허용되지 않는다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
- 락을 거는 시점
- 특강을 신청하기 전 조회를 시도하는데 읽는 시점이 다르게 신청하게 되면 데이터 정합성이 깨질 수 있기 때문에 데이터를 조회할 때 부터 걸어줘야 한다.
- 예시
1. 유저1, A 특강 인원 조회 -> 현재 인원 수 20명
2. 유저2, A 특강 인원 조회 -> 현재 인원 수 20명
3. 유저1, A 특강 신청 -> 기존에 읽은 인원 수 20명 + 1명 = 21명
4. 유저2, A 특강 신청 -> 기존에 읽은 인원 수 20명 + 1명 = 21명
- ➡️ A 특강을 신청한 총 인원 수가 22명으로 나와야 하는데,
유저1, 유저2가 각각 신청한 뒤에 총 인원 수가 21명으로 나와서 예상한 데이터와 결과가 상이한 것을 확인할 수 있다.
그러므로 락이 걸리면 읽기/쓰기 작업을 모두 할 수 없게 하는 배타락을 이용해야 한다.
락 적용 전과 후
비관적 락 적용 전
//then
assertEquals(30, successfulRequests.get(), "성공한 요청은 30개여야 합니다.");
assertEquals(10, failedRequests.get(), "실패한 요청은 10개여야 합니다.");
assertEquals(30, registrationRepository.countCompletedRegistrationByLectureId(lecture.getId()), "데이터베이스에 저장된 신청은 30개여야 합니다.");
특강 신청 인원 30명으로 제한되어 있는 상태에서 40명이 동시에 요청하였을 때의 통합테스트 결과다.
특강에 대해 조회 시 비관적 락을 적용시키 전이여서 예상 결과와 차이가 나는 것을 확인할 수있다.
비관적 락 적용 후
락 적용 후에는 assertEquals 로 성공 / 실패 요청 횟수(30건 , 10건)가 일치하고,
신청 성공한 데이터 건수를 DB에서 조회했을때 30건으로 예상값과 일치하여 통합테스트에 성공하는 것을 확인할 수 있다.
비관적락 사용 시 고려 해야 할 점
데드락
- 여러 트랜잭션이 동시에 데이터에 락을 걸려고하면 데드락이 발생할 수 있다.
트랜잭션 범위 처리
- 트랜잭션 범위가 넓으면 다른 트랜잭션의 대기가 길어져 성능이 저하될 수 있다.
가능한 최소 범위로 트랜잭션을 설정해야 한다.
타임아웃 설정
- 락 대기로 인해 시스템이 멈추지 않도록 타임아웃을 설정한다.
회고
동시성 제어를 하는 방법도 다양하고, 어느 기술을 사용하는 것이 진행하는 프로젝트 의도와 적합할지 고민하는 시간을 많이 가지게 되었다.
그리고 락을 거는 시점도 조회할 때 인지, 저장할 때 인지 의문이 들었는데, 공유락과 배타락에 대해 공부하면서 왜 조회하는 시점에 락을 걸어야 하는지 알게 되었다.
이번 주차에는 시간을 적절하게 분배하지 못해서 결국 작업을 완수하지 못하였는데, 이를 경험 삼아 3주 차부터는 계획을 차근차근 세우고 시간 안에 목표한 기능을 완성해야겠다.
'항해 플러스 백엔드 > TDD & 클린 아키텍처' 카테고리의 다른 글
[항해] 2주차, 클린 아키텍처 (2) | 2025.01.05 |
---|---|
[항해] 1주차, 동시성 제어하기(순서 보장 포함) (2) | 2024.12.22 |
[항해] 1주차, TDD 방법론을 사용하여 개발하기 (2) | 2024.12.21 |