SpringBoot와 Redis를 활용한 중복 요청 방지 전략

대용량 트래픽 환경에서 동일한 요청이 여러 번 처리되는 문제는 시스템 안정성을 해치는 주요 원인 중 하나입니다. 이 글에서는 Redis를 활용해 효과적으로 중복 제출을 차단하는 방법을 살펴봅니다.

중복 요청이 발생하는 상황

사용자 경험 측면에서 중복 요청은 다음과 같은 경우에 발생합니다:

  • 버튼을 빠르게 여러 번 클릭
  • 네트워크 지연으로 인한 클라이언트 재시도
  • 모바일 앱에서 백그라운드 복귀 후 자동 갱신

Redis 기반 중복 방지 메커니즘

Redis의 원자적 연산과 TTL 기능을 활용하면 간단하면서도 강력한 중복 방지 계층을 구축할 수 있습니다.

핵심 원리

요청 식별자를 키로 사용하여 Redis에 임시 저장하고, 동일 키 존재 여부로 중복을 판단합니다.

의존성 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Redis 설정

spring:
  redis:
    host: localhost
    port: 6379
    lettuce:
      pool:
        max-active: 20

중복 검사 서비스 구현

@Service
public class RequestDeduplicationService {
    
    private final RedisTemplate<String, String> cacheTemplate;
    
    public RequestDeduplicationService(RedisTemplate<String, String> cacheTemplate) {
        this.cacheTemplate = cacheTemplate;
    }
    
    /**
     * 요청이 처음인지 확인하고, 첫 요청인 경우 잠금 생성
     */
    public boolean acquireProcessingLock(String requestSignature, Duration ttl) {
        Boolean isNew = cacheTemplate.opsForValue()
            .setIfAbsent(requestSignature, "PROCESSING", ttl);
        return Boolean.TRUE.equals(isNew);
    }
    
    /**
     * 처리 완료 후 상태 갱신 (선택적)
     */
    public void markAsCompleted(String requestSignature, Duration resultTtl) {
        cacheTemplate.opsForValue().set(
            requestSignature, "COMPLETED", resultTtl);
    }
}

AOP 기반 선언적 적용

어테이션을 통해 중복 방지 로직을 비즈니스 코드와 분리합니다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DuplicateGuard {
    long seconds() default 5;
    String[] keyFields() default {};
}
@Aspect
@Component
public class DuplicateGuardAspect {
    
    @Autowired
    private RequestDeduplicationService dedupService;
    
    @Around("@annotation(guard)")
    public Object intercept(ProceedingJoinPoint point, DuplicateGuard guard) throws Throwable {
        String signature = generateKey(point, guard.keyFields());
        
        boolean locked = dedupService.acquireProcessingLock(
            signature, 
            Duration.ofSeconds(guard.seconds())
        );
        
        if (!locked) {
            throw new DuplicateRequestException("동일한 요청이 처리 중입니다");
        }
        
        try {
            return point.proceed();
        } finally {
            // 필요시 완료 표시 또는 키 삭제
        }
    }
    
    private String generateKey(ProceedingJoinPoint point, String[] fields) {
        // 메서드명 + 파라미터 기반 해시 생성
        MethodSignature ms = (MethodSignature) point.getSignature();
        String base = ms.getDeclaringTypeName() + "." + ms.getName();
        
        Object[] args = point.getArgs();
        String data = Arrays.stream(args)
            .map(Object::toString)
            .collect(Collectors.joining("|"));
            
        return "dedup:" + base + ":" + DigestUtils.sha256Hex(data);
    }
}

컨트롤러 적용 예시

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    @PostMapping
    @DuplicateGuard(seconds = 10, keyFields = {"userId", "productCode"})
    public ResponseEntity<OrderResult> createOrder(@RequestBody OrderRequest req) {
        // 중복 요청은 AOP에서 자동 차단
        return ResponseEntity.ok(orderService.process(req));
    }
}

고급 고려사항

분산 환경에서의 정합성

Redisson의 RBucket 또는 RLock을 활용하면 분산 락과 원자적 연산을 더욱 안전하게 구현할 수 있습니다.

@Configuration
public class RedissonConfig {
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://localhost:6379");
        return Redisson.create(config);
    }
}

키 설계 전략

요청 유형키 구성 예시TTL 권장
주문 생성order:{userId}:{productSku}30초
결제 승인payment:{orderNo}5분
재고 차감stock:{warehouse}:{itemId}10초

예외 처리 패턴

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DuplicateRequestException.class)
    public ResponseEntity<ErrorResponse> handleDuplicate(DuplicateRequestException e) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(new ErrorResponse(
                "DUPLICATE_REQUEST", 
                "이전 요청이 처리 중입니다. 잠시 후 확인해주세요."
            ));
    }
}

Redis 기반 중복 방지는 구현이 간단하고 성능이 우수하여 대부분의 동시성 제어 시나리오에 적합합니다. 다만 비즈니스 특성에 따라 TTL 값과 키 설계를 세밀하게 조정하는 것이 중요합니다.

태그: Spring Boot Redis Idempotency Distributed Lock AOP

6월 23일 16:41에 게시됨