포스트

좋아요를 동시에 두번 누르면 어떻게 될까

좋아요를 동시에 두번 누르면 어떻게 될까

개요

진행중인 블로그 프로젝트에서 좋아요 기능을 구현했다.

UI는 일반적인 형태로 구현했다.

  • 좋아요를 누르면 좋아요 처리
  • 다시 누르면 좋아요 취소 처리

좋아요(PostLike)의 경우 게시글(Post)과 다른 Aggregate로 분리했으며, 그 이유는 이전에 적었던 포스팅 내용과 같다.

좋아요 테이블에는 다음과 같이 유니크 제약조건을 걸어두었다.

1
UNIQUE KEY uk_post_member (post_id, member_id)

서비스 로직은 아래와 같이 작성했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 게시글 좋아요 생성
*/
@Transactional
public PostLikeResponse likePost(Long postId, Long memberId) {
  if (postLikeRepository.existsByMemberIdAndPostId(memberId, postId)) {
      throw new DuplicatePostLikeException("이미 좋아요를 누른 게시글입니다.");
  }

  postLikeRepository.save(PostLike.createPostLike(postId, memberId));
  Long likeCount = postLikeRepository.countByPostId(postId);
  return PostLikeResponse.from(postId, likeCount, true);
}

유니크도 걸려있고, exists 체크도 있어, 별 문제는 없어보였다.

코드를 다 작성하고 나서 떠오른 경험

기능 구현을 마치고 테스트를 하던 중, 회사에서 겪었던 일이 떠올랐다.

당시 운영 중이던 기능에서 사용자는 버튼을 한 번 눌렀다고 인식했는데,

서버에는 동일 요청이 짧은 시간 간격으로 두번 들어온적이 있었다.

원인은 네트워크 지연이었다.

요청은 이미 서버에 도착했지만, 응답이 늦어졌다.

클라이언트는 이를 실패로 인식했고, 자동 재시도를 수행했다.

결과적으로 “의미상 동일한 요청”이 거의 동시에 두 번 서버에 도착했다.

결국 두 요청이 거의 같은 시점에 INSERT를 시도하였고, 하나는 성공 하나는 유니크 제약 조건 위반으로 오류가 발생했다.

지금 내가 만든 좋아요 기능도, 이런 상황이 충분히 발생할 수 있겠다는 생각이 들었다.

그래서 실제로 재현해봤다

그래서 실제 내가 작성한 좋아요 기능도 동일하게 문제가 발생할지 JMeter로 테스트를 진행해봤다.

(물론 당연히 생길걸 알고는 있었지만,,그래도 눈으로 확인하는게 좋으니까)

테스트 조건

  • 동일 postId = 1
  • 동일 memberId = 1
  • Threads: 2
  • Ramp-up: 1초

결과

  • DB INSERT 성공: 1건
  • 나머지 1건: SQLIntegrityConstraintViolationException

오류 로그

오류 로그

JVM 결과

JVM 결과

2건 중 1건은 성공했지만, 나머지 1건은 유니크 제약 위반으로 SQLIntegrityConstraintViolationException이 발생했다.

여기서 중요한 건 existsBy… 체크가 동시 요청을 막아주지 못했다는 점이다.

두 요청이 거의 동시에 들어오면 둘 다 exists=false를 확인한 뒤 INSERT를 시도할 수 있고,

결국 최종적으로는 DB 유니크 제약이 한 요청을 탈락시킨다.

문제는 이 상황을 정상 흐름으로 처리하지 못해, 예상 가능한 중복 요청조차 예외(500)로 터지게 된다.

해결방법

동시성의 경우 DB 단에서 처리되니, 서비스단은 예외를 처리할 수 있게 변경하였다.

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public PostLikeResponse likePost(Long postId, Long memberId) {
    try {
        postLikeRepository.save(PostLike.createPostLike(postId, memberId));
    } catch (DataIntegrityViolationException e) {
        throw new DuplicatePostLikeException("이미 좋아요를 누른 게시글입니다.");
    }

    Long likeCount = postLikeRepository.countByPostId(postId);
    return PostLikeResponse.from(postId, likeCount, true);
}

중복 요청이 와도 DB는 1건만 허용하고, API는 개발자가 예측 가능한 응답을 반환하게 수정하였다.

정리

앞서 말했듯이 이 문제를 처음 겪은 건 개인 프로젝트가 아니라 회사 업무였다.

사용자에게 이상하다는 메일을 받고 로그를 보는데, 분명히 한 번만 눌렀다고 한 요청이 동일한 파라미터로 두 번

들어와 있었다. 처음엔 프론트 쪽 문제라고 생각했다. 버튼 중복 클릭이거나, disable 처리가 빠졌겠거니 했다.

그런데 프론트는 이미 요청 중에는 다시 못 누르게 막아둔 상태였다.

뭔가 납득이 안갔다. 그래서 그 날은 몇 시간을 로그를 보면서 테스트를 해봤던거 같다.

그리고 하나씩 확인하다가, 네트워크 지연 상황에서 요청이 재전송될 수 있다는 점을 알게 되었다.

이 경험이 있어서 그런지, 개인 프로젝트에서 좋아요 기능을 구현하다가 문득 생각이 들었다.

“이 구조도 같은 상황에서 깨질 수 있지 않나?”

그래서 JMeter로 재현했고, 결과는 예상대로였다.

그 이후로 기준이 하나 생겼다.

네트워크는 우리가 통제할 수 없고, 요청은 겹칠 수 있다.

좋아요 기능은 작았지만, 이 경험 덕분에 설계에서 ‘당연하다’고 생각했던 가정들을 줄이게 됐다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.