UMC/study

[UMC_study] 버튼을 연타하는 걸 막아라!

sunm2n 2025. 9. 21. 04:35

버튼을 빠르게 여러 번 눌렀을 때 여러 가지 이유(비동기 로직 등)로 요청이 지연되어 완전히 처리하기 전 두 번 요청이 들어갈 수 있습니다.

이를 해결할 수 있는 방법에 대해 작성하여 주세요.

 
유저의 손가락을...
 
어떤 방법이 있는지 알아보자!

 

## 1. 프론트엔드 (클라이언트 측) 해결 방안: 사용자의 액션 제어

가장 먼저 사용자에게 피드백을 주고 추가적인 입력을 막는 방법입니다.

방법: 버튼 비활성화 및 로딩 상태 표시

사용자가 "미션 도전!" 버튼을 한 번 클릭하면, 즉시 버튼을 비활성화(disabled) 처리하고 로딩 스피너와 같은 시각적 피드백을 보여줍니다. 그리고 서버로부터 성공 또는 실패 응답을 받은 후에야 버튼을 다시 활성화하거나 다른 상태(예: '도전 중')로 변경합니다.

JavaScript
 
// 간단한 Javascript 예시
async function handleChallengeClick() {
  const challengeButton = document.getElementById('challenge-btn');

  // 1. 버튼 비활성화 및 로딩 표시
  challengeButton.disabled = true;
  challengeButton.innerText = '처리 중...';

  try {
    // 2. 서버에 API 요청
    await api.challengeMission();
    // 3. 성공 시 UI 변경
    challengeButton.innerText = '도전 완료!';
  } catch (error) {
    // 4. 실패 시 버튼 다시 활성화
    challengeButton.disabled = false;
    challengeButton.innerText = '미션 도전!';
    alert('오류가 발생했습니다. 다시 시도해주세요.');
  }
}
  • 장점:
    • 사용자 경험(UX)이 향상됩니다. 현재 요청이 처리 중임을 명확히 인지시켜 혼란을 막습니다.
    • 대부분의 순수한 실수로 인한 중복 클릭을 효과적으로 방지할 수 있습니다.
  • 단점:
    • 완벽한 해결책이 아닙니다. 악의적인 사용자나 네트워크 지연 등 비정상적인 상황에서는 프론트엔드 제어를 우회하여 중복 요청을 보낼 수 있습니다.

## 2. 백엔드 (서버 측) 해결 방안: 요청 처리 제어

프론트엔드의 방어선을 뚫고 들어온 중복 요청을 서버 로직 단에서 처리하는 방법입니다.

방법: 분산 락(Distributed Lock) 활용 (feat. Redis)

여러 서버 인스턴스가 동작하는 분산 환경에서 가장 효과적인 방법 중 하나입니다. 공유 저장소인 Redis의 SETNX(SET if Not eXists) 명령어를 활용해 특정 요청에 대한 '락(Lock)'을 설정합니다.

  1. 요청 접수: (사용자 ID, 미션 ID) 조합으로 고유한 키를 만듭니다. (예: lock:member1:mission101)
  2. 락 획득 시도: Redis에 SETNX 명령어로 해당 키가 존재하는지 확인하고, 없다면 키를 생성합니다. (보통 짧은 만료 시간(TTL)을 함께 설정하여 락이 영원히 남는 것을 방지합니다)
  3. 로직 분기:
    • 락 획득 성공 (키 생성 성공): 첫 번째 요청이므로, 비즈니스 로직(데이터베이스에 member_mission 데이터 삽입 등)을 수행합니다. 처리가 끝나면 락을 해제(DEL)합니다.
    • 락 획득 실패 (키가 이미 존재): 직전의 요청이 아직 처리 중이거나 이미 처리된 것이므로, 현재 요청은 즉시 "이미 처리 중인 요청입니다" 와 같은 오류 응답을 반환합니다.
  • 장점:
    • 여러 서버에서도 동시성 제어가 가능하여 매우 안정적입니다.
    • 데이터베이스에 도달하기 전에 중복 요청을 차단하므로, 불필요한 DB 부하를 줄일 수 있습니다.
  • 단점:
    • Redis와 같은 별도의 시스템에 대한 의존성이 생기고, 아키텍처가 복잡해집니다.

## 3. 데이터베이스 (최후의 방어선) 해결 방안: 데이터 제약 조건

모든 방어선을 뚫고 중복된 데이터 생성 요청이 DB에 도달하더라도, 데이터베이스 스스로가 중복을 허용하지 않도록 막는 가장 강력하고 확실한 최후의 보루입니다.

방법: UNIQUE 제약 조건 설정

member_mission 테이블에서 한 명의 사용자가 동일한 미션에 두 번 참여할 수 없도록 데이터베이스 레벨에서 제약 조건을 거는 것입니다. member_id와 misson_id 두 컬럼을 조합하여 고유키(Unique Key)로 설정합니다.

SQL
 
-- 이미 테이블이 존재할 경우 제약조건 추가
ALTER TABLE member_mission
ADD CONSTRAINT uk_member_id_misson_id UNIQUE (member_id, misson_id);
  • 동작 방식:
    1. 첫 번째 "미션 도전" 요청이 들어와 INSERT 쿼리가 성공적으로 실행됩니다.
    2. 직후 두 번째 중복 요청이 들어와 동일한 (member_id, misson_id) 값으로 INSERT를 시도합니다.
    3. 데이터베이스는 UNIQUE 제약 조건 위반으로 판단하고, INSERT를 거부하며 오류를 발생시킵니다.
    4. 백엔드 애플리케이션은 이 DB 오류를 감지하고 "이미 도전한 미션입니다."와 같은 적절한 응답을 사용자에게 보냅니다.
  • 장점:
    • 데이터 무결성을 100% 보장하는 가장 확실하고 강력한 방법입니다.
    • 애플리케이션 로직의 실수나 버그가 있더라도 데이터 중복을 원천적으로 차단합니다.
  • 단점:
    • 이미 발생한 요청을 '처리'하는 과정에서 오류를 내는 것이므로, 예방적이라기보다는 방어적인 성격이 강합니다.

## 결론: 다계층 방어 (Defense in Depth) 🛡️

세 가지 방법 중 하나만 선택하는 것이 아니라, 모두 적용하는 것이 가장 이상적인 아키텍처입니다.

  1. 프론트엔드에서 실수로 인한 중복 클릭을 막아 사용자 경험을 개선하고,
  2. 백엔드에서 분산 락으로 불필요한 비즈니스 로직 실행과 DB 부하를 방지하며,
  3. 데이터베이스에서 UNIQUE 제약 조건으로 데이터 정합성을 최종적으로 보장하는 것입니다.

이렇게 여러 계층에 걸쳐 방어 체계를 구축하면 안정적이고 신뢰도 높은 시스템을 설계할 수 있습니다.
 
 
백엔드에서 분산 락 시스템을 구축하는 것은 저번 프로젝트에서 사용한 방법이였기 때문에 비교적 이해하는데 어려움이 덜 했던거 같다.
실제로 이 부분을 초기에 고려하는 것이 제일 좋은거 같다.
저번 프로젝트의 경우 처음에는 이런 부분을 전혀 고려하지 않았다가 구현 중간에 깨닫게 되어 수정하느라 하루 정도 러닝커브가 발생했다.
물론 실력자분께서 이 문제를 해결하셔서 하루였을 뿐 나였다면 2~3일은 걸렸을 거 같다.
항상 구현전에 이러한 부분까지 충분한 설계를 마치고 구현을 들어가는 것이 가장 빠른 구현이다.
복습 또 복습!