1. 동시성 제어발생 가능 시나리오
1-1. 포인트 충전 및 차감
서버 딜레이로 인해 화면이 바로 변경되지 않아 사용자가 포인트 충전요청을 여러번 보내거나 여러번 사용요청을 보낸다.
중복호출 (aka. 따닥이슈)이 발생했을 경우 어떤 이슈가 발생할 수 있을까?
- 발생 가능성 : 中
- 위험도 : 高 -> 고객에게 크나큰 혼란을 줄 수 있다..!!
- 문제 : 같은 요청을 중복으로 보내게 되면 의도치 않은 포인트가 충전되거나 사용될 수 있다.
- 재시도 필요 유무 : 無 -> 한개의 요청만 실행되면 됨.
1-2. 재고 조회
상품 주문 시 재고 조회를 하는데, 여러개의 주문이 한꺼번에 들어와 재고를 조회해야 하는 경우 동시성 이슈가 발생할 수 있다.
- 발생 가능성 : 높음
- 문제 : 재고 관리가 최신의 상태로 조회되지 않으면 품절된 상품도 구매처리가 이루어 질 수 있다.
- 재시도 필요 유무 : 無
1-3. 랭킹 조회
랭킹은 구매 테이블을 조회하는데 내 주문 내역을 보는 API + 탑 랭킹을 조회하는 API 등 여러 요청이 구매 테이블 하나를 조회할 경우 동시성 이슈가 발생할 수 있다.
- 발생 가능성 : 낮음
- 문제 : 동시성 이슈를 처리하지 못한다고 해서 랭킹이라는 기능자체 때문에 크리티컬한 비즈니스 이슈가 있진 않지만 동시에 다수의 접근이 이루어진다면 정확한 판매 순위를 조회할 수 없다.
2. 고려할 만한 DB Lock의 종류
2-1. 🔐 Named-Lock (네임드 락)
Named-Lock?
이름을 가진 metatdata lock
이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션은 이 Lock을 획득할 수 없도록 함.
👉데이터 삽입시에 정합성을 맞춰야 하는 경우 사용 가능.
Pessimistic, Optimistic Lock은 item에 대해서 lock을 걸고
named Lock은 별도 MySql 서버 메모리 공간에 lock을 걺.
네임드 락을 사용하여 동시성을 처리할 때는 DB나 다른 시스템 내부에서 자동적으로 동시성을 처리하기보다는 애플리케이션 코드 단에서 개발자가 지정한 이름의 Lock을 사용하게 함으로써 의도적으로 동시성을 보장하기 위해 개발자가 Lock을 설정한다.
-
⚠주의할 점
transaction이 종료될 때 lock이 자동으로 해제되지 않기때문에 별도의 명령어로 해제해주거나 선점 시간이 끝나야 해제된다.
네임드 락을 설정하는 부분과 비즈니스 로직의 트랜잭션을 분리해야한다.
-
장점
- MySql에서 제공하는 NAMED LOCK을 이용해 Lock에 이름을 지정할 수 있기 때문에 해당 Lock의 이름을 이용하여 애플리케이션단에서 제어가 가능하다.
- UPDATE 작업이 아닌 INSERT 작업의 경우에는 기준을 잡을 레코드가 존재하지 않아 비관적 락을 사용할 수 없는데, 이때 Named Lock을 사용할 수 있다.
* 분산 락을 구현할 수 있다.
-
단점
* 트랜잭션 종료 시에 Lock 해제, 세션 관리 등을 수동으로 처리해야 하기 때문에 구현이 복잡할 수 있다.
2-2. 🔐 Optimistic Lock (낙관적 락)
DB단에 실제 Lock을 설정하지 않고, Version을 관리하는 컬럼을 테이블에 추가해서 데이터 수정 시 마다 맞는 버전의 데이터를 수정하는지 판단하는 방식.
- 장점
- DB단에서 별도의 Lock을 설정하지 않기 때문에 하나의 트랜잭션 작업이 길어질 때 다른 작업이 영향받지 않아서 성능이 좋을 수 있다.
- 충돌이 자주 발생하지 않는다고 가정하므로, 많은 사용자가 동시에 데이터에 접근할 수 있도록 한다. < 처리량을 향상시킬 수 있다.
- 단점
- 버전이 맞지 않는 일이 여러번 발생한다면 재시도를 여러번 거치니 성능이 좋지 않다.
- 버전이 맞지 않아서 예외가 발생할 때 재시도 로직을 구현해야함.
2-3. 🔐 Pessimistic Lock (비관적 락)
DB단에 X-Lock을 설정해서 동시성제어를 함.
DB단에서 해당 자원의 점유는 트랜잭션 단위로 수행되며, 해당 트랜잭션이 종료되기 전까지는 다른 트랜잭션에서 해당 데이터를 수정할 수 없음.
- 장점
Race Condition이 빈번하게 일어난다면 낙관적 락보다 성능이 좋다.
DB단의 Lock을 통해서 동시성을 제어하기 때문에 확실하게 데이터 정합성이 보장된다.
- 단점
- 한 트랜잭션 작업이 정상적으로 끝나지 않으면 다른 트랜잭션 작업들이 대기해야하므로 성능이 감소할 수 있다.
2-4. Redis를 이용한 분산 락
주로 key-value형태의 데이터 저장소로 사용됨.
고속의 읽기 및 쓰기 성능을 자랑한다.
- 단점
추가적인 인프라 구축비용이나 유지보수가 발생.
3. 고려해야 할 요소들
3-1. 구현 복잡성
🔅코드가 얼마나 이해하기 쉽고 관리하기 쉬운지 평가하는 요소.
- 구성 요소 :
- 코드 가독성 : 코드가 직관적이고 명확하게 작성되었는지
- 논리의 단순화 : 복잡한 조건문, 중첩 루프, 중복 코드 등이 최소화 되었는지
- 모듈화 : 기능이 분리되어있고 각각의 모듈이 독립적인지
- 평가 지표 :
- 코드 길이
- 코드 중복도
- 복잡도 지표
유지보수가 용이하고 확장성이 뛰어난 구조를 가지도록 하는 것이 목표.
3-2. 성능
🔅 코드가 얼마나 빠르게 실행되는지, 시스템 리소스를 얼마나 효율적으로 사용하는지.
- 구성 요소 :
- 시간 복잡도 : 코드가 작업을 완료하는데 걸리는 시간.
- 공간 복잡도 : 코드가 사용하는 메모리 양
- 네트워크 및 I/O성능 : 네트워크 요청, DB쿼리 등 I/O작업이 필요한 경우에 얼마나 최적화 돼있는지.
- 평가 지표 :
- 실행시간
코드가 빠르고, 자원을 적게 사용하도록 최적화하는 것이 중요.
3-3. 효율성
🔅코드가 성능과 자원을 얼마나 균형있게 사용하여 주어진 작업을 최적화하는지를 평가.
요구사항 만족하면서 자원을 최대한 절약하는 것을 목표.
-
구성 요소:
- 자원 최적화: CPU, 메모리, 네트워크 대역폭 등 시스템 자원을 효과적으로 사용하는지.
- 실행 가능성: 주어진 자원과 제한된 환경에서 목표 성능을 달성하는지.
- 코드 최적화: 성능에 영향을 주지 않으면서 구현 복잡성을 최소화하는지.
-
평가 지표:
- 자원 대비 성능비(메모리 대비 속도, CPU대비 처리량)
- 주어진 제약조건 내에서 성능 달성 여부
자원을 효율적으로 사용하면서 충분한 성능을 확보하는 것이 목표.
4. DB락 별 분석
4-1. Named Lock
4-1-1. 구현 복잡성 : 下
- 구현이 간단하고 Redis, ZooKeeper 등에서 기본기능으로 제공되는 경우가 많다.
- 유지보수성 : 락을 걸 리소스의 이름만 정하면 돼서 코드의 복잡도가 낮다. 유지보수에 유리.
4-1-2. 성능 : 中
- 리소스 접근시 락이 필요한 경우 빠른 처리속도제공, 하지만 고성능을 유지하려면 락 수와 지속시간을 최적화 해야함.
- 락 충돌이 잦아지면 대기시간 늘어날 수 있음.
- 이름으로 고정되어있어 충돌이 빈번히 발생할 가능성이 있으며 높은 트래픽에서는 병목이 발생할 수 있다.
4-1-3. 효용성 : 中
- 특정 리소스에만 락을 적용할 수 있어 자원절약
- 잦은 충돌이 일어나면 효율이 떨어질 수 있으므로 트랜잭션 요구가 큰 작업에는 적합하지 x
4-2. Optimistic Lock
4-2-1. 구현복잡성 : 中
- 충돌 발생 시 재시도 로직이 필요하여 구현이 다소 복잡할 수 있다.
- 데이터 일관성을 보장하려면 버전 관리가 필요하다.
- 유지보수성 : 충돌이 발생할 수 있는 경로를 미리 설계해야하므로, 복잡한 트랜잭션 로직에서 적용이 까다롭다.
4-2-2. 성능 : 上
- 락을 사용하지 않기 때문에 락 대기시간이 없고, 충돌이 빈번하지 않다면 성능상의 이점이 크다.
- 충돌 발생 시 재시도로 인해 일부 성능 저하가 발생할 수 있다.
4-2-3. 효율성 : 上
- 자원을 락으로 고정하지 않아 자원 사용 측면에서 효율적.
- 잦은 충돌 발생 시 재시도로 인한 성능 저하가 생길 수 있다.
4-3. Pessimistic Lock
4-3-1. 구현 복잡성 : 中
- 기본적 락 설정은 쉬운 편, 충돌이 잦은 경우 복잡도가 증가
- 유지보수성 : 락을 해제하지 못하는 상황이나 데드락을 방지해야하므로 신중한 설계가 필요.
4-3-2. 성능 : 下
- 모든 접근에 락을 걸기 때문에 병목 발생할 가능성 있음.
동시에 처리할 수 있는 요청 수가 줄어듦.
- 락을 통해 데이터 일관성은 보장되나 성능이.. ㅠ
4-3-3. 효율성 : 中
- 자원을 오랜시간 점유하여 낭비될 수 있음.
- 데이터 충돌 가능성이 높은 경우에는 안정적인 효율을 기대할 수 있다.
4-4. Distributed Lock
4-4-1. 구현 복잡성 : 上
- 분산 환경에서 락을 관리하려면 트랜잭션 일관성을 보장하기 위한 추가 설정과 구현이 필요함.
- 노드 간 동기화와 락 만료시간 등을 설계해야 하므로 복잡도가 높고 유지보수가 어려울 수 있음.
4-4-2. 성능 : 中
- 분산 환경에서의 락이므로 네트워크 지연과 여러 인스턴스 간의 동기화로 인해 성능이 약간 저하.
- 장애 발생 시에도 복원 가능한 락을 사용할 수 있어 고가용성 애플리케이션에 적합.
4-4-3. 효율성 : 上
- 여러 인스턴스간 자원 접근을 효율적으로 관리할 수 있어 확장성과 안정성이 요구되는 분산환경에서 유용.
- 단일 애플리케이션 환경에서는 효율적이지 않을 수 있다.
5. 시나리오에 DB Lock 적용해보기.
5-1. 포인트 충전에서 생기는 동시성 문제.
포인트 충전에 관한 분석은 사람마다 달랐는데, 이번 이커머스 환경에서는 분산 환경 이라는 전제가 있긴 하나
"외부 결제시스템"에서 결제가 이루어 지는 점.
"결제 수단이 포인트"인 점.
"충전 또한 내부 API"에서 이루어 지는 점 등을 미루어보아
포인트 차감은 애초 주문 시점에서 동시성 제어가 이루어 질 것이므로 동시성 제어를 할 필요가 없다고 생각했다.
포인트 충전 시 '고객'이 충전 버튼을 여러번 누르는 "따닥이슈"로 같은 요청이 여러번 오는 경우가 있을 것이다.
👉🏻 최초 1회 요청을 받아들이고 나선 뒤의 요청들은 재시도 할 필요 ❌
👉🏻 재시도 로직 구현 불필요.
👉🏻 따닥이슈 제어 후 다시 요청하는 것은 프론트에서 제어가 가능하므로 충돌이 잦지 않을 것 같다.
때문에 "낙관적 락" 을 선택했다.
5-2. 상품 주문에서 생기는 동시성 문제
같은 상품을 구매하는 요청을 동시에 여러개 보낼 경우 재고가 없는데도 결제가 되는 동시성 문제가 생길 수 있다.
재고 차감문제는 높은 일관성이 보장되어야 하므로 성능이 조금 떨어지더라도 "비관적 락"을 사용하는 방식으로 처리할 것이다.
대신 비관적 락 만으로는 분산환경에서 한계가 있을 수 있으므로 Redis분산 락을 병행하여 처리하는 것을 고려해봐야 할 것이다.
[Database] MySQL의 네임드 락을 이용한 분산 락 구현하기
분산락을 이용한 중복예약 동시성 문제 해결
[Spring] 스프링 동시성 처리 방법(feat. 비관적 락, 낙관적 락, 네임드 락)