서버에서 동시성 문제를 다루는 방법

fana·2023년 1월 9일
1

오늘은 서버에서 동시성 처리 문제를 다루었던 경험을 기록하려고 한다. 지금 우리 팀에서는 "따닥 이슈"라고 말하기도 한다.
문제의 현상은 API 요청이 동시에 2회 왔다고 했을 때, 특히 해당 API가 DB Insert를 처리하는 경우 똑같은 레코드가 2개 생기는 것이다. 원인은 DB에 레코드를 만들기 전 중복 방지에 대한 검증을 걸었다고 해도 DB 트랜젝션이 각각 열린 후 서로에 대해 모르기 때문에 어플리케이션 레벨에서의 중복 방지 검증에 제대로 걸리지 않기 때문이다.

상황이 급박한 경우에는 API를 요청하는 클라이언트 단에서 쓰로틀링이나 디바운스를 통해 일부 방지할 수 있기도 하겠지만, API가 Public으로 열려있는 경우에는 임시 조치일 뿐이다. 실제로 지금 일하고 있는 서비스 내 이전 출시한 프로덕트 초기에 해당 문제를 제대로 처리하지 못해 DB에 똑같은 레코드가 여러개가 생겨서 호되게 고생했던 경험이 있다.

이런 동시성 처리 문제에 대해 내가 처리했던 해결 방법은 두 가지가 있는데, 상황에 따라 맞추어서 사용하면 될 것 같다. 첫 번째 방법은, 서버를 호출하는 클라이언트로부터(브라우저든, 어떤 서버든 간에 호출하는 쪽) UUID와 같은 고유값을 받는 방법이 있다. 또 한 가지 방법으로는 처리하는 서버에서 자체로 동시성 문제를 관리하긴 위해 Redis와 같은 별도의 스토어를 사용하는 방법이 있을 수 있다.

고유값을 받는 방법

나는 UUID만 사용했던 경험이 있기 때문에 UUID로 설명을 통일하자면, 클라이언트로부터 요청당 UUID를 받은 후 DB에 Insert를 시켜버리는 방법이다. DB 컬럼 자체에 unique constraint가 걸려있기 때문에, 어떻게 보면 최후의 보루격인 DB에 일단 박고 보는 방법이라 조금은 찜찜한 기분이 들기도 한다.

이렇게 처리했던 이유는, 해당 서비스가 아래와 같은 구조를 가지고 있었기 때문이었다.

우리가 운영중인 서버가 두 가지가 있었고, 우측의 Server가 DB에 최종적으로 Insert하는 작업을 한다. 우측의 서버는 사실상 검증 외에 아무런 처리가 없다.
클라이언트는 두 명씩 1:1 대전 게임을 소켓을 통해 하고, 게임 서버가 릴레이 서버 역할을 하고 있다. 따라서 게임 서버가 현재 진행중인 게임 context에 대한 모든 state를 가지고 있었다. 게임 서버는 최종적으로 우측의 서버에 게임의 최종 결과를 gRPC를 통해 호출하면 이상이 있는지의 검증 이후 DB에 레코드가 저장된다.

사실.. 게임 서버가 gRPC 호출을 여러번 하는 이유에 대해 근본적으로 찾아서 처리하는게 맞다고 여전히 생각해서, 이 처리를 위해 클라이언트와 게임서버 간 소켓 통신을 하는 과정을 고치기 위해 여러 노력을 해보았지만 결국엔 뜻대로 잘 이루어지지 않아서 결과적으로는 게임서버 - 서버 간 UUID를 요청에 담아 보내 검증을 하도록 만들었다.

이 방식은 이전 회사에서 근무할 때 세이퍼트 서비스의 API를 호출할 때 경험했던 방법이라 금방 떠올릴 수 있었다.

Redis를 사용하는 방법

정확히는 Redis의 분산락을 사용하는 방법이라고 말하는 것이 맞을 것 같다. 위 방법처럼 UUID를 사용하지 않고 분산락을 적용한 이유는 서비스가 아래와 같이 일반적인 서버처럼 클라이언트로부터 바로 API요청을 받기 때문이었다. 와중에 이 서버도 RESTAPI로 게임을 진행하는 서비스였기 때문에, state는 DB로 관리하였는데 동시성 문제가 발생해 Redis 분산락을 적용하였다.
이 서버는 kotlin으로 만들었기 때문에, 분산락 적용에 redisson을 사용하였다.

Redis로 분산락을 적용할 때 나도 실수했던 부분이 있었는데, 반드시 하나의 분산락 안에서 "검증 - 캐시 추가"가 이루어져야 한다는 점이다.

결과로는 아주 성공적으로 따닥 이슈를 제거할 수 있었다! 친절한 동료분께서 손수 스크립트를 짜서 초당 200개 요청을 보내 분산락이 잘 적용된다는 것을 확인해주셔서 업데이트 배포를 할 수 있었다! 참고로 이 게임은 아직 프로덕션에 출시된 게임이 아니라서 처음으로 내가 만든 서비스 중 따닥 이슈를 프로덕션에 내보내기 전 처리한 서비스가 되었다...

0개의 댓글