지성인 프로젝트를 개발하면서 한줄평 좋아요 기능에 동시성 문제를 발견했다.
일단 한줄평 기능은 특정 도서에 대해 짧게 평가할 수 있는 기능이다.
해당 한줄평은 다른 사용자가 좋아요를 누를 수 있다.
문제 상황
현재 사이트에서는 좋아요를 1번 누르면 하트가 표시되고 다시 누르면 좋아요가 해제된다.
또한 본인의 한줄평에는 좋아요를 할 수 없다.
보기에는 문제가 없어보인다.
하지만, 좋아요를 광클해서 같은 요청이 한번에 보내진다면 ?
악의적인 사용자가 요청 로직을 알아내서 똑같은 좋아요 요청을 한번에 보낸다면 ?
서버에 심각한 에러가 발생할 수 있다.
해당 로직은 발생할 수 있는 문제 상황이다.
왜 문제가 발생하는가 ?
임계 영역(Critical Section)에 경쟁 상태(Race Condition)가 발생하기 때문이다.
임계 영역: 여러 프로세스나 스레드가 동시에 접근할 수 있는 공유 자원(코드 영역)
경쟁 상태: 여러 프로세스들이 동시에 데이터 접근할 때, 접근 순서에 따라 결과 값이 달라질 수 있는 상황
로직 테스트를 통해 직접 확인해보자.
로직을 살펴보면 사용자가 이미 좋아요을 했다면, 추가로 좋아요는 생성되면 안된다.
하지만 테스트를 보면 100개의 스레드로 진행했고, 결과는 1개가 나와야 하지만 결과는 1개가 아닌것을 확인할 수 있다.
해결 방법
처음에는 Lock이 필요한 동시성 문제인 줄 알고 비관적 락, 낙관적 락, 레디스 분산락 등 여러 방안을 찾아봤다.
낙관적 락은 트랜잭션들의 빈번한 충돌이 일어나지 않을 것이라고 가정하는 방법으로 애플리케이션 레벨에서 동시성을 제어한다.
별도의 Lock을 걸지 않기 때문에 비관적 락보다 성능상 이점이 있다.
충돌 시, ObjectOptimisticLockingFailureException 예외를 개발자가 직접 처리해야 한다.
하지만 외래키 확인을 위해 관련 테이블에 읽기 잠금을 걸리면 서로 대기하는 데드락 문제가 발생할 수 있다.
비관적 락은 트랜잭션들의 빈번한 충돌이 발생할 것이라고 가정하여 DB의 락을 사용해서 동시성을 제어한다.
만약 트랜잭션 1에서 접근하면 쓰기 잠금이 걸리기 때문에 다른 트랜잭션은 해당 레코드에 접근 자체를 할 수 없다.
그렇지만 완전히 끝나고 나서야 선점하기 때문에 데이터 정합성과 데드락 문제가 해결된다.
하지만 많은 트랜잭션이 접근하는 경우 성능에 이슈가 생길 것이다.
더 나아가 Redis 분산락도 고민해봤다.
Redis 분산락을 사용하면 분산 환경에서 여러 노드가 단일 노드인 Redis를 통해 임계 영역 접근을 제어할 수 있다.
이는 원자성을 보장하며, 주로 Redlock 알고리즘을 통해 구현된다.
Redlock 알고리즘은 여러 Redis 노드에 동일한 키를 설정하여 락을 확보하고, 일정 시간 내에 과반수 이상의 노드에서 락을 획득한 경우에만 성공적으로 락을 얻은 것으로 간주한다.
이렇게 다수의 노드에서 락을 획득하도록 하여, 단일 노드의 장애가 전체 락 시스템에 영향을 미치지 않도록 설계되어 동시성 문제를 효율적으로 해결한다.
낙관적 락과 비관적 락은 특징과 단점이 분명했다.
그래서 Redis 분산 락을 적용하려고 했지만, 내가 너무 어렵게 생각한 거 아닐까 라는 고민이 들었다.
그때 인덱스를 복합 인덱스로 설정하여 유니크 인덱스를 만들어 주는 해결 방법이 떠올랐다.
유니크 인덱스 설정을 통해 문제를 해결할 수 있었다.
해당 좋아요 중복 생성 문제를 고민하면서 중간에 굳이 Lock이 필요한가? 라는 생각이 들었다.
좋아요 중복 생성 문제는 갱신 손실 문제보다는 팬텀 리드 문제에 더 가깝다.
팬텀 리드는 트랜잭션이 특정 조건을 만족하는 레코드 집합을 여러 번 읽을 때, 다른 트랜잭션이 새로운 레코드를 삽입하거나 기존 레코드를 삭제하여 처음 읽은 레코드 집합과 나중에 읽은 레코드 집합이 달라지는 현상이다.
방지하기 위해선 Serializable 격리 수준이 필요하지만, 트랜잭션들이 독립적으로 수행되기 때문에 성능에 큰 문제를 준다.
따라서 유니크 인덱스를 사용하여 동일한 유저와 리뷰 조합에 대해 중복 삽입을 방지했다.
결론적으로 count 같은 증가하는 값에 대해 동시성 문제가 발생하는 것이 아니라, 그저 중복 생성이 핵심이었다.
추가적인 구현 리소스 없이 효율적으로 해결하는 경험을 했다. 😝
'프로젝트' 카테고리의 다른 글
CompletableFuture를 활용한 도서 베스트셀러 저장 성능 개선 (1) | 2024.10.24 |
---|---|
RESTful한 path 설계하기 (0) | 2024.10.05 |
AOP를 통한 유저 인증 정보 미리 주입하기 (0) | 2024.07.23 |
[지성인] JaCoCo 테스트 분석 도구 적용, 프로젝트 테스트 커버리지 확인 (1) | 2024.07.09 |