버튼을 빠르게 여러 번 눌렀을 때 여러 가지 이유(비동기 로직 등)로 요청이 지연되어 완전히 처리하기 전 두 번 요청이 들어갈 수 있습니다.
이를 해결할 수 있는 방법에 대해 작성하여 주세요.
유저의 손가락을...
어떤 방법이 있는지 알아보자!
## 1. 프론트엔드 (클라이언트 측) 해결 방안: 사용자의 액션 제어
가장 먼저 사용자에게 피드백을 주고 추가적인 입력을 막는 방법입니다.
방법: 버튼 비활성화 및 로딩 상태 표시
사용자가 "미션 도전!" 버튼을 한 번 클릭하면, 즉시 버튼을 비활성화(disabled) 처리하고 로딩 스피너와 같은 시각적 피드백을 보여줍니다. 그리고 서버로부터 성공 또는 실패 응답을 받은 후에야 버튼을 다시 활성화하거나 다른 상태(예: '도전 중')로 변경합니다.
// 간단한 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)'을 설정합니다.
- 요청 접수: (사용자 ID, 미션 ID) 조합으로 고유한 키를 만듭니다. (예: lock:member1:mission101)
- 락 획득 시도: Redis에 SETNX 명령어로 해당 키가 존재하는지 확인하고, 없다면 키를 생성합니다. (보통 짧은 만료 시간(TTL)을 함께 설정하여 락이 영원히 남는 것을 방지합니다)
- 로직 분기:
- 락 획득 성공 (키 생성 성공): 첫 번째 요청이므로, 비즈니스 로직(데이터베이스에 member_mission 데이터 삽입 등)을 수행합니다. 처리가 끝나면 락을 해제(DEL)합니다.
- 락 획득 실패 (키가 이미 존재): 직전의 요청이 아직 처리 중이거나 이미 처리된 것이므로, 현재 요청은 즉시 "이미 처리 중인 요청입니다" 와 같은 오류 응답을 반환합니다.
- 장점:
- 여러 서버에서도 동시성 제어가 가능하여 매우 안정적입니다.
- 데이터베이스에 도달하기 전에 중복 요청을 차단하므로, 불필요한 DB 부하를 줄일 수 있습니다.
- 단점:
- Redis와 같은 별도의 시스템에 대한 의존성이 생기고, 아키텍처가 복잡해집니다.
## 3. 데이터베이스 (최후의 방어선) 해결 방안: 데이터 제약 조건
모든 방어선을 뚫고 중복된 데이터 생성 요청이 DB에 도달하더라도, 데이터베이스 스스로가 중복을 허용하지 않도록 막는 가장 강력하고 확실한 최후의 보루입니다.
방법: UNIQUE 제약 조건 설정
member_mission 테이블에서 한 명의 사용자가 동일한 미션에 두 번 참여할 수 없도록 데이터베이스 레벨에서 제약 조건을 거는 것입니다. member_id와 misson_id 두 컬럼을 조합하여 고유키(Unique Key)로 설정합니다.
-- 이미 테이블이 존재할 경우 제약조건 추가
ALTER TABLE member_mission
ADD CONSTRAINT uk_member_id_misson_id UNIQUE (member_id, misson_id);
- 동작 방식:
- 첫 번째 "미션 도전" 요청이 들어와 INSERT 쿼리가 성공적으로 실행됩니다.
- 직후 두 번째 중복 요청이 들어와 동일한 (member_id, misson_id) 값으로 INSERT를 시도합니다.
- 데이터베이스는 UNIQUE 제약 조건 위반으로 판단하고, INSERT를 거부하며 오류를 발생시킵니다.
- 백엔드 애플리케이션은 이 DB 오류를 감지하고 "이미 도전한 미션입니다."와 같은 적절한 응답을 사용자에게 보냅니다.
- 장점:
- 데이터 무결성을 100% 보장하는 가장 확실하고 강력한 방법입니다.
- 애플리케이션 로직의 실수나 버그가 있더라도 데이터 중복을 원천적으로 차단합니다.
- 단점:
- 이미 발생한 요청을 '처리'하는 과정에서 오류를 내는 것이므로, 예방적이라기보다는 방어적인 성격이 강합니다.
## 결론: 다계층 방어 (Defense in Depth) 🛡️
세 가지 방법 중 하나만 선택하는 것이 아니라, 모두 적용하는 것이 가장 이상적인 아키텍처입니다.
- 프론트엔드에서 실수로 인한 중복 클릭을 막아 사용자 경험을 개선하고,
- 백엔드에서 분산 락으로 불필요한 비즈니스 로직 실행과 DB 부하를 방지하며,
- 데이터베이스에서 UNIQUE 제약 조건으로 데이터 정합성을 최종적으로 보장하는 것입니다.
이렇게 여러 계층에 걸쳐 방어 체계를 구축하면 안정적이고 신뢰도 높은 시스템을 설계할 수 있습니다.
백엔드에서 분산 락 시스템을 구축하는 것은 저번 프로젝트에서 사용한 방법이였기 때문에 비교적 이해하는데 어려움이 덜 했던거 같다.
실제로 이 부분을 초기에 고려하는 것이 제일 좋은거 같다.
저번 프로젝트의 경우 처음에는 이런 부분을 전혀 고려하지 않았다가 구현 중간에 깨닫게 되어 수정하느라 하루 정도 러닝커브가 발생했다.
물론 실력자분께서 이 문제를 해결하셔서 하루였을 뿐 나였다면 2~3일은 걸렸을 거 같다.
항상 구현전에 이러한 부분까지 충분한 설계를 마치고 구현을 들어가는 것이 가장 빠른 구현이다.
복습 또 복습!
'UMC > study' 카테고리의 다른 글
| [UMC_study] Soft Delete 란? (0) | 2025.10.05 |
|---|---|
| [UMC_study] AOP(Aspect-Oriented Programming) 원리 탐구 (0) | 2025.09.28 |
| [UMC_study] 서블릿 vs Spring MVC 비교 (0) | 2025.09.28 |
| [UMC_study] 함수 기반 인덱스와 복합 인덱스 (0) | 2025.09.21 |
| [UMC_study] 트렌젝션의 상태와 전파에 대하여 (0) | 2025.09.21 |