동시성을 제어하기 위한 쿠폰 발급 프로젝트에서
- Lettuce 를 사용한 레디스 락
- Redisson 을 사용한 레디스 락
- DB 락 (비관적 락)
방식을 사용하여 락을 구현했다.
세 락 방법의 장단점과 이론적인 성능 퍼포먼스를 조사해보고, 실제 부하 테스트 결과와 비교해본 뒤 결과분석과 우리 프로젝트에 가장 적합한 락 방식이 무엇인지 알아보자.
Lettuce
Java 진영의 redis 클라이언트 라이브러리 중 하나로, 블로킹/논블로킹 방식을 모두 지원하는 강력한 라이브러리이다. SpringBoot 2.0 이후부터 redis 클라이언트로 Lettuce 를 기본으로 지원한다.
Lettuce 는 setnx 방식으로 락을 구현할 수 있는데, setnx 는 redis 에 key-value 값을 저장하는 명령어이다. 즉 특정 key-value 저장을 시도하여 성공하면 Lock 을 획득하고, 저장에 실패하면 Lock 획득에 실패한 것이다. Lock 을 획득한 스레드는 작업 종료 후 해당 key-value 를 삭제함으로써 unLock 을 하게 되고 이후 접근하는 다른 스레드가 Lock 을 획득할 수 있는 구조이다.
이 방식에서 여러 스레드가 반복문을 돌면서 계속 Lock 획득을 시도하도록 구현하는 것이 스핀락이다. 반복을 사용하지 않으면 스레드당 한번만 Lock 획득을 시도하도록 구현할 수도 있다.(수강신청할 때 광클)
단점
- 스핀락 방식은 여러 스레드가 반복적으로 redis 에게 Lock 을 확인하는 요청을 보내기 때문에, 요청이 몰릴 경우 redis 서버에 많은 부하가 가해질 수 있다.
- 자체적인 TimeOut 이 존재하지 않기 때문에, 락을 반환하지 않는 문제에 대해서 어플리케이션 레벨에서 타임아웃을 따로 구현해야 한다.
Redisson
Lettuce 와 마찬가지로 Java 의 redis 클라이언트이지만, Lettuce 와 다르게 분산 환경에서 강점을 가지고 있다. Redisson 의 락은 pub-sub 구조를 사용한다. 재시도 로직을 직접 구현해야 하는 Lettuce 와는 다르게, Redisson 은 pub-sub 기반의 분산 락 구조를 이미 구현하여 제공해주고 있다. 이 방식은 대기중인 스레드가 지속적으로 Lock 획득을 시도하지 않고, 구독 채널로 메세지가 발행되었을 때 Lock 획득을 시도하는 방식이기 때문에 스핀락에 비해 redis 에 가해지는 부하가 적다는 장점이 있다. 그리고 Lettuce 와 달리 기본 제공되는 Lock 인터페이스에서 타임아웃을 제공하므로 어플리케이션 레벨에서 타임아웃을 직접 구현하지 않아도 된다. 즉 Lettuce 의 단점이 Redisson 에서는 장점이다.
DB Lock
- DB 락은 비관적/낙관적 락과 Named 락(MySQL에서 제공하는 분산 락) 이 있다.
- 프로젝트에는 비관적 락을 사용하여 진행하였다.(쿠폰 발급과 같이 요청이 몰리는 서비스이기 때문에 낙관적 락은 적절하지 않다고 판단하였고, Named 락은 고려하지 않았기 때문에 따로 더 공부가 필요한 부분이다)
비관적 락 방식은 충돌이 일어날 것을 예상하고, 데이터 조회와 동시에 락(공유 락 또는 베타 락)을 걸어버리는 방식이다. 해당 레코드는 수정사항이 커밋된 이후에 락을 해제하므로 데이터 정합성을 보장하지만, 레코드 자체에 락을 거는 방식이기 때문에 성능이 저해되며 다른 요청의 데이터 접근까지 막아버리기 때문에 주의하여 사용해야 한다.
단점
- 비관적 락은 DataBase 주체로 락을 거는 방식이기 때문에 기본적으로 Inmemory 저장소인 redis 를 이용한 락보다 성능이 떨어진다.(그러나 실제 테스트코드 실행 시간, Jmiter 로 확인해본 지표 상으로 비관적 락이 압도적으로 좋은 성능을 나타냈다. 물론 테스트코드 실행 시간은 객관적인 지표가 될 수는 없지만, 다른 레퍼런스나 부하테스트 지표에서도 비관적 락의 성능이 좋게 나온 것은 의아하다.)
- 비관적 락 방식은 레코드단위로 Lock 을 걸어버리기 때문에, 여러 테이블의 값을 읽어야 할 경우 다른 스레드와 데드락에 걸릴 위험이 존재한다. (하나의 레코드만 참조하는 경우에는 데드락 문제는 피해갈 수 있다.) 여러 테이블의 레코드를 참조하는 요청의 경우 비관적 락 사용 시 데드락이 발생할 확률이 높다.
- 해당 테이블의 데이터를 조회하는 다른 요청의 접근도 막아버리기 때문에 테이블을 분리하거나 하는 방법을 고려하는 것이 좋다.
- 비관적/낙관적 락 방식은 스케일 아웃된 DB 환경에서는 사용할 수 없으며, 이럴 경우 redis 를 사용한 분산 락을 고려해야 한다.
레디스 락은 무적인가?
위 글에서 정리한 내용으로 보면 레디스 락은 DB 락에 비해 장점이 많고 여러 상황에서 정답처럼 사용할 수 있는 것으로 보인다.
그러나 다음과 같은 단점이 있는데,
1. 싱글스레드 기반이므로 병목현상 발생
2. 단일 실패지점 문제
-> 클러스터링, 센티널 등 서버를 분산하는 방법으로 극복할 수 있다
(클러스터링, 페일오버, 레플리케이션 등 아직 이해가 모호한 키워드들에 대해 명확히 정리)
-> 우리는 레디스를 싱글 인스턴스로 개발 시작했으므로, 모니터링 등을 통해 cpu 부하 지점을 정하고 요청이 많아지는 시점에 클러스터링을 고려해야 함!
따라서 위 흐름으로 레디스를 사용하는 것에 대한 결점과 대응 방안을 적절하게 가져가서 안전한 아키텍쳐링을 구현하는 것이 바람직하겠다!
부하테스트 결과
결론
그렇다면 결론적으로 DB 락을 사용하는 것 보다 redis 락을 사용하는 것이 성능상으로, 그리고 분산환경에서도 유리하기 때문에 redis 락을 사용하는 것이 좋고, Lettuce 의 단점을 Redisson 이 대부분 보완했기 때문에 Redisson 으로 분산 락을 구현하는 것이 정답이다! 라고 생각할 수 있겠다. 그러나 상황에 따라서는 Redisson 보다 Lettuce 를 사용하는 것이 나을 수 있고, 레디스 락보다 DB 락을 사용하는 것이 나을 수 있다.
Redisson 은 라이브러리가 상대적으로 무겁고 별도의 의존성을 추가해 주어야 하며, 제공된 인터페이스를 사용하기 때문에 동작 방식을 알기 위해서 따로 학습이 필요하다. 실무에서도 Lock 획득을 재시도할 필요가 없는 요구사항일 경우 Lettuce 를 사용하기도 한다.
또한 분산 환경이 아닐 경우, 충돌이 자주 발생하지 않는 환경을 가정할 경우 레디스 락을 무조건 채택하기보다 낙관적 락을 사용하는 방식이 성능상 이득을 보는 경우가 많다. 또 테스트 결과에서 알 수 있듯, 요청의 수가 많지 않은 경우 비관적 락을 사용하는것이 가장 높은 성능을 보였다. 따라서 요구사항과 서비스의 규모 등을 고려하여 락 방식을 선택하는 것이 적절하다.
※ Facade 패턴?
프로젝트에서 겪었던 Transaction 커밋 시점과 unLock 시점 불일치로 인해 동시성을 보장하지 못했던 이슈를 Facade 패턴으로 많이들 해결하는 모양이다. 어떤 패턴이고 어떻게 문제를 해결할 수 있는지 공부해보자.
'Study' 카테고리의 다른 글
TCP/UDP (0) | 2024.07.24 |
---|---|
OSI 7 계층 (0) | 2024.07.24 |
[JPA(2)] - 연관관계 매핑 (0) | 2024.06.19 |
[Spring/JPA] N+1 문제와 해결 방법(fetch Join) (0) | 2024.06.03 |
[Java]가비지 컬렉션(GC) (0) | 2024.05.22 |