무한스크롤 기능에서 동시성 이슈 해결하기
안녕하세요. 우아한테크코스 5기 BE 연어입니다.
팀 프로젝트에서 열심히 요즘카페
라는 서비스를 만들고 있습니다.
요즘카페는 스크롤 내리면서 성수동의 카페들을 하나씩 볼 수 있는 숏폼 형식의 서비스입니다. 최근에 무한 스크롤 기능을 추가하면서 예상치 못한 동시성 문제를 맞닥뜨렸는데요.
이 글에서는 ObjectOptimisticLockingFailureException
이 왜 발생했고, 관련 개념들과 해결 방법에 대해서 다뤄보고자 합니다.
목차는 아래의 순서와 같습니다.
- 문제 발생
- 배경 설명
- Entity Manager
- DB Isolation Level
- 문제 원인
- 해결 방안
- 정리
문제 발생
평소처럼 선릉 캠퍼스에 도착해서 로그를 보고 있었습니다.
그런데 처음 보는 예외가 발생해 있었고, 어떤 행동을 하면 발생하는지 알기 위해 이런저런 행동을 해봤습니다.
기대했던 동작
- 회원이 스크롤을 엄청 빠르게 내려도
- 로그에는 예외가 발생하지 않고
- 새 카페가 중복 없이 나온다
문제가 발생하는 행동
- 회원이 스크롤을 엄청 빠르게 내리면
- 로그에는 위와 같이
ObjectOptimisticLockingFailureException
이 발생해 있고 - 스크롤을 내려도 계속 똑같은 카페 화면이 보인다
왜 문제가 발생했을까요…?
배경 설명
요즘카페에서는 회원에게 아직 보여주지 않은 카페
들을 우선으로 보여주는 로직이 존재합니다.
회원마다 아직 보지 않은 카페 목록 UnViewedCafe
를 가지고 있습니다.
이는 Member:Cafe = N:M
인 다대다 관계를 중간에서 1:N, M:1
로 해결해 주는 테이블입니다.
회원이 스크롤을 내릴 때마다 새로운 카페들을 보여주게 되는데 이때 아래의 두 API가 필요합니다.
A. 회원이 아직 보지 않은 카페 목록을 조회하는 API
회원이 다음 카페를 보기 위해 스크롤을 내리면, 아직 보지 않은 카페들 중 랜덤으로 5개를 응답으로 보내줍니다.
해당 회원의 UnViewedCafe
테이블에서 5개를 조회해 와서 그대로 리턴하는 방식입니다.
B. 회원이 해당 카페를 봤다는 처리를 하기 위한 API
회원이 다음 카페를 보기 위해 스크롤을 내리면, 해당 카페 ID를 서버로 보내서 UnViewedCafe
테이블에서 제거합니다
UnViewedCafe
테이블에서 (memberId, cafeId) 조합의 데이터를 삭제하는 방식입니다.
또한 해당 회원의 UnViewedCafe
가 0개가 됐을 때는 전체 카페들을 가져와서 다시 채워줍니다. 따라서 무한 스크롤이 가능해지게 됩니다.