트랜잭션보다 빠른 속도로 요청이 들어올 때 해결방안

HwangDo·2023년 7월 27일
0

SpringBoot

목록 보기
5/14

문제 사항

현재 소프트웨어 공모전으로 심사중인 우리 프로젝트엔 북마크 기능이 있다.


메뉴마다 달린 이 작은 버튼을 누르면 북마크가 등록되고, 다시 누르면 해제된다.
설계상 북마크 리스트는 백엔드 쪽에서 DB를 통해 담당하고, 그 부분 로직의 일부는 다음과 같다.

public void addBookmark(BookmarkForm bookmarkForm){
    validateDuplicate(bookmarkForm);
    bookmarkRepository.save(bookmarkForm);
}

private void validateDuplicate(BookmarkForm bookmarkForm){
    bookmarkRepository.findBookmarkByUniqueIdAndMenuId(bookmarkForm).ifPresent(b -> {
        throw new IllegalStateException("이미 북마크된 메뉴입니다.");
    });
}

북마크 추가 전, 나름의 중복 검사 과정이 존재한다.
Repository에 있는 메서드 이름이 뭔가 이상한 것 같더라도, 나는 DATA JPA가 아닌 그냥 JPA와 JPQL을 통해 메서드를 직접 짜서 그렇다고 넘어가자.

그런데, 위 로직을 사용하면 문제가 발생 할 수 있다.

북마크 추가 요청을 아주 빠른 시간 내에 반복해서 보내면, 중복 검사를 걸러내지 못하고 DB에 복수의 데이터가 저장된다.
위 그림이 예시 상황이다. 첫 데이터가 저장되기 전 수행되는 중복 검사는 모두 통과하기 때문에, 데이터가 복수로 들어가게 된다.

게다가 북마크 삭제 로직에선, 중복이 없음을 상정하고 getSingleResult를 사용하기 때문에 삭제 과정에서도 문제가 생긴다.

해결법

찾아본 바로는 해결법은 크게 두 가지가 있다.

1. 동기 사용

스프링부트에서 멀티 쓰레드는 정말 간략히 말하면

  1. 쓰레드 풀을 톰캣이 부팅시 생성
  2. 요청이 들어오면, 쓰레드 풀에서 여유 쓰레드 할당
  3. 작업 종료시 쓰레드 반환

식으로 이루어진다.
만약 메소드에 synchronized 키워드를 붙이게 된다면, 해당 메소드는 하나의 쓰레드만 접근이 가능하다. 즉, 이미 해당 메소드가 실행 중일 때 다른 쓰레드에서 해당 메소드를 쓰고자 한다면 기다려야 한다는 것이다.

private synchronized void validateDuplicate(BookmarkForm bookmarkForm){
    bookmarkRepository.findBookmarkByUniqueIdAndMenuId(bookmarkForm).ifPresent(b -> {
        throw new IllegalStateException("이미 북마크된 메뉴입니다.");
    });
}

이런식으로 사용 가능하다. 그런데 정말 당연하게도 이러면 성능이 떨어질것이다. 직접 테스트해보지는 않았지만, 메소드를 강제로 동기시키는건 좋은 접근 방법이라고 생각되지 않았다.

2. UNIQUE 사용

내가 채택한 방법으로, 그냥 사용중인 MySql 상에서 유니크 인덱스를 추가한다.

CREATE UNIQUE INDEX unique_userid_menuid ON bookmarks (user_id, menu_id);

정말 간단하게도 이렇게 하면 중복된 데이터를 받지 않는다. 스프링부트를 건든 해결책은 아니지만, 1번의 동기보단 더 나은 선택지라고 생각했다.

3. Redis등 인메모리 캐시 사용

대표적인 방법이지만, 사용하지 않았다.

profile
제가 배워가는 내용과, 실수한 부분을 정리합니다

0개의 댓글