[Spring] 동시성 어떻게 처리할까

윤성철·2024년 12월 29일

Back-End

목록 보기
21/22
post-thumbnail

서론

사이드 프로젝트에서 좋아요 기능을 구현하면서, 동시성 이슈를 마주했다. 어떤 상황에서 발생했고 어떻게 해결했는지 과정을 기록하고자 한다.

본론

문제상황

4개의 쓰레드로 동시성 테스트를 수행했다. 당연히 4번의 좋아요 API 요청이 발생했기 때문에 좋아요는 4개가 나와야한다. 하지만 좋아요 개수 1개로 테스트가 종료되었다.

테스트 수행 로그를 확인해봤는데, 포스트에 대해 좋아요를 증가시키는 쿼리는 정상적으로 질의된 것을 알 수 있었다.

테스트 코드

동시성 문제의 원인

여러 스레드가 동시에 같은 게시글에 대해 좋아요를 누르면 likeCount(좋아요 수)를 동시에 읽고 수정하려고 한다.
예로, 사용자인 A와 B가 "인증"이라는 post에 대해서 동시에 좋아요 요청을 했다.

  1. A가 점유한 트랜잭션에서는 post를 조회했을 때 "인증"의 likeCount는 0이었는데, 1을 증가시켜 1이 되었다.
  2. B 또한, 동일하게 post를 조회해서 1을 증가시켜서 1이 되었다.

2번의 좋아요 요청이 수행되었지만, likeCount는 1이 된다.

MariaDB는 기본 트랜잭션 격리수준은 REPEATBLE_READ다.

  • 해당 전략은 다른 트랜잭션에서 데이터가 변경되더라도 현재 트랜잭션에서는 읽는 데이터는 영향을 받지 않는다.

따라서 모든 트랜잭션이 종료되고, 하나의 좋아요 요청에 대한 업데이트 쿼리만 수행된다는 것을 알 수 있다.

그렇다면 격리 수준을 높힌다면, 4만큼의 좋아요 개수가 증가하게할 수 있을까?

SERIALIZABLE 격리 수준에서, 데이터를 읽을 때, 데이터베이스는 일반적으로 공유 잠금을 설정한다.
트랜잭션이 해당 데이터를 읽을 수 있도록 허용하지만, 쓰기 작업은 차단한다.

즉, 트랜잭션이 진행하는 동안 다른 트랜잭션이 해당 데이터를 WRITE은 할 수 없지만, READ는 가능하기 때문에 위 문제가 동일하게 발생한다.

따라서, 베타 잠금을 통해 위 문제를 해결해야겠다고 생각했다.

해결방법

  1. Lock
    • 낙관적 락
    • 비관적 락
  2. likeCount 변수를 없애고 조인 쿼리를 통해서 좋아요 개수 조회

처음 좋아요 기능을 기획하고 ERD 모델링 하면서 성능에 대한 고민을 거쳤다. likeCount를 사용하지 않고, like 테이블에서 count 쿼리를 질의해 좋아요 개수를 조회로 방향성을 정했었다. 그러나, 조인 쿼리와 count 질의를 쿼리하는 것으로 성능 이슈가 필연적으로 발생할 수 밖에 없다고 생각해, 2번을 제외했었다.

낙관적 락은 실제 데이터 충돌이 발생하지 않을 것이라 가정하고 사용하는 방법이고, 또 추가적인 오류에 대한 핸들링이 필요하다는 점 때문에 비관적 락을 선택하게 되었다.

적용

@Lock 어노테이션을 사용하여, 비관적 락 옵션을 추가한다. WRITE의 경우, 데이터의 읽기/쓰기 요청 시 쓰기 락이 발생해서, 트랜잭션이 수행 중인 경우, 대기 상태에 들어가고 앞선 트랜잭션이 종료되면, 수행한다. 또한, 대기 시간이 늘어져 데드락 발생을 대비해서, 5초간의 타임아웃 시간을 지정했다.

결과

성공적으로 4개의 좋아요 요청에 대해서 4만큼의 좋아요 수가 증가했다.

한편, 소요시간이 약 1.3022초가 소요된다. 고작 4개의 요청에 대해서 이정도 성능이라면 개선이 필요하다..

뿐만 아니라, 트랜잭션의 순서, 타임아웃, 트랜잭션 최소화 이슈로 데드락이 발생할 수 있다. 추가적인 방법을 공부하고 고민해봐야겠다.

profile
내 기억보단 내가 작성한 기록을 보자..

0개의 댓글