해당 프로젝트를 시작하면서 달성하고자 했던 핵심 목표 중 하나는, 동시성 문제를 직접 겪어보고 이를 해결하는 방법론에 대한 학습과 이를 실제로 해결해본 경험을 갖는 것이었습니다.
저희 프로젝트 내에서 동시성 문제가 발생하는 지점은 바로 티케팅을 수행하는 로직입니다. 서버 상에는 한정된 수의 티켓이 존재하게 되는데, 이에 대해 티켓보다 더 많은 고객이 동시다발적으로 티켓을 획득하려는 과정에서 데이터 정합성 문제가 발생하는 것이죠. 저희 팀은 이 문제를 다양한 방법으로 접근해보기 위해 각자 비관적 락(P-lock), 낙관적 락(O-lock), 분산 락(D-lock)을 이용하여 문제를 해결해보는 과정을 거쳤습니다.
그리고 최종적으로 저희 상황에 가장 부합하는 하나의 방법론을 확정하기 위한 테스트 및 결과 분석 단계를 앞두고 있습니다.
저희는 자동화된 테스트를 원했고, 이를 위해 추가적인 개별 코드 수정 없이 코드 컨텍스트 외부에서 어떤 방법론을 사용하여 락을 수행할지 결정하고, 주입해주기를 원했습니다.
이러한 요구사항은 결국 객체지향 설계 원칙의 ‘개방-폐쇄 원칙’에 해당되는 문제이고, 따라서 확장 가능한 객체지향 설계가 필요한 상황이었습니다. 이를 위해 추가적인 아키텍쳐 미팅을 진행하고 결정된 아키텍쳐에 맞게 각자 로직을 구성하였습니다.
하지만 이 과정에서 다양한 문제들이 발생하여 최종적으로는 초기 논의했던 아키텍쳐와는 사뭇 다른 방식으로 개발이 진행되었습니다.
해당 글에서는 개발 중에 겪은 다양한 문제점들과 더불어 문제가 왜 발생했는지에 대한 고찰을 진행하고, 최종적으로는 문제 상황들을 고려한 새로운 아키텍쳐 개선안을 도출해내고자 합니다.
글 내용에 대해 쉽게 이해하기 위해서는 락을 구현하는 방법론들의 종류와 그 특징을 알고 있는 편이 좋습니다.
이를 위해 각 방법론에 대해 간단히 다루면 아래와 같습니다.
낙관적 락이라고 일컬어지는 방식입니다.
테이블 내 버전 칼럼을 추가하고 해당 레코드가 수정될 때마다 이 값을 함께 수정하는 방식입니다.
@Version
어노테이션을 통해 간단히 설정할 수 있습니다.로직 내에서 업데이트를 위해 읽어온 값을 DB로 다시 동기화하는 과정에서 초기 읽어왔던 버전 값과 동기화하는 순간의 버전 값이 다를 경우 발생하는 예외를 따로 처리하는 식으로 구현합니다.
실행 쿼리 예시
UPDATE users
SET name = "Jamal", version = version + 1
WHERE id = 10 and version = ?; // 버전 일치 여부 판정
애플리케이션 레벨에서 동시성에 대해 다루지 않고, 일단 쿼리를 날린 뒤 예외가 발생할 경우 그제서야 동시성 핸들링을 하는 방식으로 조회 시 어떠한 락도 사용하지 않기 때문에 성능 저하가 없지만 만약 동시성 문제가 발생한 상황이라면 네트워크 비용 + 예외 처리 비용 + 재요청 비용이 추가되어 성능적으로 큰 저하를 겪을 수 있습니다.
SELECT ~ FOR UPDATE
문을 이용하여, 타겟 레코드에 배타락을 거는 방식으로 동작합니다.SELECT ~ FOR UPDATE
문을 통해서는 동시 접근이 불가능해집니다. 이로 인해 성능의 저하가 발생할 수 있습니다.