2024년 7월 7일
보안
조회 : 168|3분 읽기
Redis를 활용한 DDoS 방어 기능 모듈화
최근 프로젝트에서 DDoS 공격 방어를 위한 개선 작업을 진행했습니다.
🔍 직면한 문제
기존 시스템에서는 Spring Session Data Redis를 사용하여 세션을 Redis에서 관리했습니다.
Spring Session Redis를 사용하여 요청 수를 관리했으나 동시 요청 시 세션 값이 제대로 갱신되기 전에 다음 요청이 처리되는 문제가 있었습니다.
이는 동기화가 보장되지 않아 DDoS 방어 로직이 효과적으로 동작하지 못하게 했습니다.
spring-session-data-redis를 사용한 세션 서버 블로그는 잘 정리되어 있는 포스트를 가져왔습니다.
HTTP에서 세션 사용 문제점
-
세션 관리의 비효율성: HTTP 세션을 통해 쿠키를 사용하여 요청을 관리하면, 동시 요청이 많은 상황에서 세션 관리의 부하가 증가합니다.
-
서블릿의 동시성 처리: 서블릿은 동시 요청을 처리할 때, 여러 요청을 병렬로 처리하지만, 요청 간의 동기화 문제가 발생할 수 있습니다.
문제 설명
동시 다발적으로 요청이 들어올 때, 세션을 통해 Redis에서 카운트를 관리하는 방식은 세션에 카운트를 저장하기 전에 모든 요청이 처리되어 버립니다.
이로 인해 카운트가 쌓이는 기능이 지연되어 제대로 동작하지 않습니다.
일반적인 세션 관리
현재의 문제점
🔧 개선된 해결책
이 문제를 해결하기 위해 메시징 기능 전체를 컴포넌트로 만든 후 RedisTemplate을 사용해 명시적으로 요청을 처리하고 카운트를 관리하도록 개선했습니다.
이를 통해 성능을 향상시키고 모듈화하여 재사용성을 높였습니다.
또한 Redis는 비즈니스 로직의 코드를 줄여주는데도 기여했습니다. 기존에는 세션에 최초 요청의 시각 값을 담아 다음 요청의 시각과 비교하여 5분 후면 카운트를 초기화하는 로직도 함께 코드상에 존재했지만, expire를 사용하며 더 간결하고 안전한 코드가 되었습니다.
🛠️ 개선된 코드 예시
1번 해결책 synchronized 사용
java1@Component 2public class RequestLimiter { 3 4 @Autowired 5 private RedisTemplate<String, Integer> redisTemplate; 6 7 private static final int MAX_REQUESTS = 10; 8 private static final long EXPIRE_DURATION = 1L; // duration in minutes 9 10 public void synchronized protectRateLimit(String redisKey) { 11 // 현재 요청 수를 먼저 조회 12 Integer currentCount = redisTemplate.opsForValue().get(redisKey); 13 14 // 이미 최대 요청 수를 초과했는지 확인 15 if (currentCount != null && currentCount >= MAX_REQUESTS) { 16 throw new RuntimeException("MAX_REQUESTS 초과"); 17 } 18 19 // 요청 카운트를 증가시키고, 처음이라면 만료 시간 설정 20 Long incremented = redisTemplate.opsForValue().increment(redisKey); 21 22 if (incremented != null && incremented == 1) { 23 redisTemplate.expire(redisKey, EXPIRE_DURATION, TimeUnit.MINUTES); 24 } 25 } 26 27 public boolean isRequestAllowed(String sessionId) { 28 try { 29 protectRateLimit(sessionId); 30 return true; 31 } catch (RuntimeException e) { 32 return false; 33 } 34 } 35}
synchronized 괜찮을까?
- 목표가 단순 카운팅: 요청을 카운트하고 특정 횟수를 넘으면 막기 위한 로직.
- 카운팅은 증가 연산: INCR 같은 Redis의 명령어는 원자적(atomic)으로 동작
- 최대 요청 수 초과만 확인: 이미 MAX_REQUESTS를 넘었다면 추가 요청을 막을 수 있음.
2번 해결책 분산락 사용
java1 2@Component 3public class RequestLimiter { 4 5 @Autowired 6 private RedisTemplate<String, Integer> redisTemplate; 7 8 private static final int MAX_REQUESTS = 10; // 최대 요청 수 9 private static final long EXPIRE_DURATION = 1L; // 만료 시간 (단위: 분) 10 11 /** 12 * 요청 제한 로직 13 */ 14 private synchronized void protectRateLimit(String redisKey) { 15 // Redis 키가 없으면 초기화하고 만료 시간 설정 16 Boolean isNewKey = redisTemplate.opsForValue().setIfAbsent(redisKey, 0, EXPIRE_DURATION, TimeUnit.MINUTES); 17 18 if (Boolean.TRUE.equals(isNewKey)) { 19 // 새로운 키가 생성되었으므로 요청 카운트를 초기화 후 처리 20 redisTemplate.opsForValue().increment(redisKey); 21 return; 22 } 23 24 // 현재 요청 수를 조회 25 Integer currentCount = redisTemplate.opsForValue().get(redisKey); 26 27 // 최대 요청 수 초과 여부 확인 28 if (currentCount != null && currentCount >= MAX_REQUESTS) { 29 throw new RuntimeException("MAX_REQUESTS 초과"); 30 } 31 32 // 요청 카운트를 증가 33 redisTemplate.opsForValue().increment(redisKey); 34 } 35 36 /** 37 * 요청이 허용되는지 확인하는 메서드 38 */ 39 public boolean isRequestAllowed(String sessionId) { 40 try { 41 protectRateLimit(sessionId); // 요청 제한 체크 42 return true; // 요청 허용 43 } catch (RuntimeException e) { 44 return false; // 요청 제한 초과 45 } 46 } 47}
최적화
Redis의 setIfAbsent(SETNX) 명령을 사용하여 키가 없는 경우 초기화와 만료 시간을 동시에 처리합니다. 키가 없을 때만 초기화하므로 초기화 과정에서의 경쟁 조건을 줄입니다
분산락은 왜 사용할까
분산락이 가지고 있는 장점은 하나의 상태에 앱이 여러개 연결되어있을때 나타납니다.
경쟁 상태 방지: 예를 들어, 여러 요청이 동시에 동일한 Redis 키를 수정하려고 하면 경합 상태가 발생할 수 있는데, 분산락으로 이를 방지할 수 있습니다.
데이터 무결성: 키의 초기화, 증가, 만료 등의 작업이 원자적으로 처리됩니다.
현재 구현하는 기능이 단순한 카운팅이라면 Redis의 원자적 연산으로 충분히 동시성을 보장할 수 있어 분산락이 필요하지 않을 가능성이 큽니다.
하지만 여러 유저가 하나의 상태를 공유하고, 동시에 상태를 읽고 쓰는 작업이 많아 데이터의 무결성을 보장해야 한다면 분산락을 사용하는 것이 타당합니다.
작은 서비스에서는 락으로 인한 성능 저하가 크지 않아 분산락을 쉽게 사용할 수 있지만, 트래픽이 많아질 경우 성능 병목을 고려해 다른 대안을 검토할 필요가 있습니다.
✅ 결과
RedisTemplate을 사용한 개선 후, 요청을 더 빠르게 처리할 수 있었고 동시 요청 시에도 정확한 카운팅이 가능해졌습니다. 이를 통해 DDoS 공격을 효과적으로 방어할 수 있었습니다.
🏁 결론
RedisTemplate을 활용한 DDoS 방어 최적화는 시스템 성능과 안정성을 모두 향상시켰습니다. 이를 통해 동시 요청 처리 능력을 강화하고, 시스템의 보안을 한층 더 높일 수 있었습니다.