๊ธฐ์กด ์ฒ๋ฆฌ์จ ์ ํ ์ฅ์น๋ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ Spring AOP ์ ๋ ธํ ์ด์ ์ ์ ์ฉํ์ฌ ๊ณผ๋ํ ์์ฒญ์ ์ ํํ๋ ๋ฐฉ์์ด์๋ค.
๐ ใ๊ฐ์ ๋ฉด์ ์ฌ๋ก๋ก ๋ฐฐ์ฐ๋ ๋๊ท๋ชจ ์์คํ ์ค๊ณ ๊ธฐ์ดใ 4์ฅ์์ ๋ค๋ฃฌ ์ฒ๋ฆฌ์จ ์ ํ ์ฅ์น ์ค๊ณ๋ฅผ ์ฐธ๊ณ ํ์ฌ, ๋ณด๋ค ํจ์จ์ ์ธ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๊ณ ์ ํ๋ค.
ํ์ฌ ํ๋ก์ ํธ๋ ๋๊ท๋ชจ ํ๊ฒฝ์ ์๋์ง๋ง, ํด๋น ์ฑํฐ๋ฅผ ํตํด ์ฒ๋ฆฌ์จ ์ ํ ์๊ณ ๋ฆฌ์ฆ, ์บ์ ํ์ฉ๋ฒ, ์ ํ ์ฅ์น์ ์์น ์ ์ ์ ๋ํ ๊ฐ๋ ์ ํ์ตํ์๊ณ , ์ด๋ฅผ ์ ์ฉํ์ฌ ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๊ฐ์ ํ๋ ค ํ๋ค.
@Aspect
@Component
public class RateLimitAspect {
private final Cache<String, Integer> requestCountsPerIpAddress = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES) // ๊ธฐ๋ณธ 1๋ถ ๋ง๋ฃ
.build();
private final HttpServletRequest request;
public RateLimitAspect(HttpServletRequest request) {
this.request = request;
}
@Around("@annotation(rateLimit)") // @RateLimit ์ด๋
ธํ
์ด์
์ด ์๋ ๋ฉ์๋์ ์ ์ฉ
public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String clientIp = getClientIP();
int maxRequests = rateLimit.maxRequests();
int timeWindow = rateLimit.timeWindow();
// ์์ฒญ ํ์ ํ์ธ ๋ฐ ์
๋ฐ์ดํธ
Integer requests = requestCountsPerIpAddress.getIfPresent(clientIp);
if (requests == null) {
requestCountsPerIpAddress.put(clientIp, 1);
} else if (requests >= maxRequests) {
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Too many requests. Try again later.");
} else {
requestCountsPerIpAddress.put(clientIp, requests + 1);
}
// ์์ฒญ ํ์ฉ
return joinPoint.proceed();
}
// ํด๋ผ์ด์ธํธ IP ์ฃผ์ ์ถ์ถ
private String getClientIP() {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader == null) {
return request.getRemoteAddr();
}
return xfHeader.split(",")[0];
}
}
๊ธฐ์กด ๋ฐฉ์์ ๊ฒฝ์ฐ Spring AOP
๋ก ๊ตฌ์ฑ๋์๋ค. ์ด์ ๋ํ ๋ฌธ์ ๋ฅผ ์ง์ด๋ณด์.
์ฑ ์์ ์ธ๊ธํ๋ ์ฒ๋ฆฌ์จ ์ ํ ์ฅ์น์ ์์น๋ ํด๋ผ์ด์ธํธ, ์๋ฒ, ํด๋ผ์ด์ธํธ-์๋ฒ ์ฌ์ด(๋ฏธ๋ค์จ์ด)๋ฅผ ์ธ๊ธํ์๋ค.
ํด๋ผ์ด์ธํธ์์ ์ฒ๋ฆฌ์จ ์ ํ์ ์ ์ฉํ๋ฉด ์๋ฒ ๋ถํ๋ฅผ ์ค์ผ ์ ์๊ณ , ์ฌ์ฉ์๊ฐ ์ ํ์ ์ฐํํ ๊ฐ๋ฅ์ฑ์ด ํฌ๋ค. ์๋ฅผ ๋ค์ด, ๋ธ๋ผ์ฐ์ ๋ ์ฑ์์ ์์ฒญ ๋น๋๋ฅผ ์ ํํด๋, ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ์์ ํ๊ฑฐ๋ ์์ฒญ์ ์กฐ์ํ๋ฉด ๋ฌด๋ ฅํ๋ ์ ์๋ค. ๋ํ, DDoS ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ ์ ์์ผ๋ฉฐ, ๊ฐ๊ธฐ ๋ค๋ฅธ ํด๋ผ์ด์ธํธ๊ฐ ๋ค๋ฅธ ์ ํ ์ ์ฑ ์ ์ ์ฉํ ์ ์์ด ์ผ๊ด์ฑ์ด ๋ถ์กฑํ๋ค.
์๋ฒ์์ ์ฒ๋ฆฌ์จ ์ ํ์ ์ํํ๋ฉด ์ค์์์ ์์ฒญ์ ์ ์ดํ ์ ์์ง๋ง, ๋ถํ๊ฐ ๋ฐ์ํ ํ์์ผ ๊ฐ์ง๋์ด ๋ฆฌ์์ค ๋ญ๋น๊ฐ ๋ฐ์ํ ์ ์๋ค. ๋ํ, ๋ถ์ฐ ํ๊ฒฝ์์๋ ์๋ฒ๋ณ๋ก ์ ํ ์ ์ฑ ์ด ๋ค๋ฅด๊ฒ ๋์ํ ์ ์์ด ์ผ๊ด๋ ์ ํ์ ์ ์ฉํ๊ธฐ ์ด๋ ต๋ค. ์ด๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด Redis ๊ฐ์ ๋ถ์ฐ ์บ์๋ฅผ ์ฌ์ฉํด์ผ ํ์ง๋ง, ์ถ๊ฐ์ ์ธ ๊ฐ๋ฐ ๋ฐ ์ด์ ๋ถ๋ด์ด ๋ฐ์ํ๋ค.
ํด๋ผ์ฐ๋๋ฅผ ์ด์ฉ์ API GateWay๋ฅผ ์ด์ฉํ ์ ์๊ณ , Spring์ ๊ฒฝ์ฐ SpringFilter๋ก ์์ฒญ์ด dispatcherServlet
๋ก ๊ฐ๋ ๊ฒ์ ๋ง์ ์ ์๋ค.
๋ถ์ฐ ํ๊ฒฝ์ด ์๋๊ธฐ ๋๋ฌธ์ ๋ด ํ๋ก์ ํธ์์๋ API Gateway๊น์ง๋ ๊ตฌ์ฑํ์ง ์์๋ค. ๊ทธ๋ ๊ธฐ์ Filter๋ก์ ๊ตฌํ์ด ํ์ํ ๊ฒ ๊ฐ๋ค.
๊ทธ๋ ๋ค๋ฉด ํ์ฌ AOP๋ก ๊ตฌ์ฑํ ๊ฒ์ ๋ฌด์์ด ์๋ชป๋์์๊น?
์๊ตฌ์ฌํญ | API Gateway | Spring Filter | Spring AOP |
---|---|---|---|
๊ณต์ ํ ์์ฒญ ์ฒ๋ฆฌ | โ ๋ถ์ฐ ํ๊ฒฝ์์๋ ์ผ๊ด๋ ์์ฒญ ์ ํ ๊ฐ๋ฅ | โ ๋จ์ผ ์๋ฒ ๊ธฐ์ค์ผ๋ก ์ ์ฉ (๋ถ์ฐ ํ๊ฒฝ์์๋ ์ถ๊ฐ ๊ตฌ์ฑ ํ์) | โ ๊ฐ๋ณ ๋ฉ์๋ ํธ์ถ ๋จ์๋ก ์ ์ฉ๋๋ฏ๋ก ์์ฒญ ๋จ์ ์ ํ์ด ์ด๋ ค์ |
DDoS ๋ฐฉ์ด | โ WAF(Web Application Firewall) ๋ฐ Rate Limiting ๊ธฐ๋ฅ ์ ๊ณต | โ ๊ฐ๋ณ ์๋ฒ์์ ์ ์ฉ๋๋ฏ๋ก ์ ์ฒด ์์คํ ์ฐจ์์ ๋ฐฉ์ด๋ ์ด๋ ค์ | โ DDoS ๊ณต๊ฒฉ์ ํจ๊ณผ์ ์ผ๋ก ๋ฐฉ์ดํ๊ธฐ ์ด๋ ค์ |
ํจ์จ์ ์ธ ํธ๋ํฝ ์ ์ด (Rate Control) | โ Redis ๊ธฐ๋ฐ์ ๊ธ๋ก๋ฒ Rate Limit ์ง์ | โ ์์ฒญ ๋จ์๋ก ํํฐ๋ง ๊ฐ๋ฅ | โ ์์ฒญ์ด ์๋๋ผ ๋ฉ์๋ ์คํ ๋จ์๋ก ์ ํ๋จ (API ์์ค ์ ํ์ด ์๋) |
๋น ๋ฅธ ์๋ต๊ณผ ๋ฎ์ ์ค๋ฒํค๋ | โ ์ธ๋ถ ์๋น์ค์์ ์์ฒญ ์ฐจ๋จ โ ์ ํ๋ฆฌ์ผ์ด์ ๋ฆฌ์์ค ์๋น ์ต์ํ | โ ์์ฒญ ์ด๊ธฐ์ ํํฐ๋ง ๊ฐ๋ฅ โ ์ ํ๋ฆฌ์ผ์ด์ ๋ฆฌ์์ค ์ฌ์ฉ ์ต์ํ | โ ์์ฒญ์ด ์ด๋ฏธ ์ปจํธ๋กค๋ฌ์ ๋๋ฌํ ํ ์ฐจ๋จ๋๋ฏ๋ก ๋ฆฌ์์ค ๋ญ๋น ๊ฐ๋ฅ |
์ ์ฐํ ์ ์ฑ ์ ์ฉ | โ API ๋ณ, ์ฌ์ฉ์ ๋ณ, IP ๋ณ๋ก ์ค์ ๊ฐ๋ฅ | โ ํน์ URL ํจํด์ ๋ํด ์ ์ฉ ๊ฐ๋ฅ | โ ํน์ ๋ฉ์๋๋ณ ์ ์ฉ ๊ฐ๋ฅํ์ง๋ง, API ์์ค์ ์ ์ฑ ์ ์ฉ์ ์ด๋ ค์ |
์ ํ๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋จ์ผ(๊ฐ๋ณ) ์๋ฒ์ ์ ์ฉํ๋ค๋ฉด SpringFilter๋ก์ ๊ฐ๋ฐ์ด ๋์์ง ์์์ ์ ์ ์๋ค.
์ ํ๋ฅผ ์ดํดํ๊ธฐ ์ํด ํํฐ์ AOP์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ทธ๋ฆผ์ผ๋ก ์ดํดํด๋ณด์.
filter์ ๊ฒฝ์ฐ Spring์ผ๋ก ์์ฒญ์ด ์ ๋ฌ๋๊ธฐ ์ ์ ์ ํ๊ท์น์ ๋ฐ๋ผ ์ ํ๋์๋ค๋ฉด ์์ฒญ์ ์ซ์๋ธ๋ค. ํ์ง๋ง AOP์ ๊ฒฝ์ฐ ์คํ๋ง ๋ด์ ์๋น์ค ๋ฐ ๋ฆฌํฌ์งํ ๋ฆฌ ๊ณ์ธต๊น์ง์ ์ ๊ทผ์ ๋ง์์ง ๋ชฐ๋ผ๋ ์คํ๋ง๊น์ง ์์ฒญ์ด ๋ค์ด์ค๊ธฐ์ ๋น ๋ฅธ ์๋ต๊ณผ ๋ฎ์ ์ค๋ฒํค๋
์ ๊ด์ ์์ ๋ถํฉ๊ฒฉ์ด๋ค. ์ด์ ๋ฐ๋ผ ๋น์ฐํ DDOS ๋ฐฉ์ด
์๋ ๋ถ๋ง์กฑ์ค๋ฝ๋ค.
๐๏ธ ์ค์ธ๊ณ ๋น์ ๋ก ์ ๊ทผํด๋ณด์
์ด๋ ํฌ์ค์ฅ์ ํ์๋ค์ ์ผ์ผ ์ด์ฉ์ "ํ ๋ฒ"์ผ๋ก ์ ํํ๋ค.
์ผ๋ฐ์ ์ธ ๊ตฌํ ๋ฐฉ์์ด๋ผ๋ฉด, ์ ๊ตฌ์์ ํ์๋ฒํธ๋ฅผ ์ ๋ ฅํ๋ฉด ์ฆ์ ์ด์ฉ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ณ ,
์ด๋ฏธ ์ด์ฉํ ๊ฒฝ์ฐ "๊ธ์ผ ์ ์ฅ ๋ถ๊ฐ" ์๋ต(ํํฐ ์ฒ๋ฆฌ)์ ๋ฐ์ ์ ์ฅ์ด ์ฐจ๋จ๋๋ค.
ํ์ง๋ง ๊ธฐ์กด AOP ๋ฐฉ์์์๋ ๋๊ตฌ๋ ์ ๊ตฌ๋ฅผ ํต๊ณผํ ์ ์๋ค.
์ฌ์ฉ์๋ ํฌ์ค๋ณต์ผ๋ก ํ๋ณตํ๊ณ , ๋ฌผ์ ํ์ ๋ง์๊ณ , ์๋ น์ ๋ค๋ ค๊ณ ํ ๋์ผ ๋น๋ก์
"๊ธ์ผ ํฌ์ค์ฅ ์ด์ฉ ๋ถ๊ฐ" ์๋ต(AOP ์ฒ๋ฆฌ)์ ๋ฐ๋๋ค.
๐ ํต์ฌ:
AOP โ Filter๋ก ๋ณ๊ฒฝํ ์ด์ ๋ "์ด์ฐจํผ ๋๊ฐ์ผ ํ ์ฌ๋์ ์๊น์ง ๋ถ๋ฌ๋ค์ฌ ๊ฑธ๋ฌ๋ด์ง?" ๋ผ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํจ์ด๋ค.
๋ถํ์ํ ์์ ๋ญ๋น ์์ด ์ ๊ตฌ์์ ์ ํํ๋ ๊ฒ์ด ๋ ํฉ๋ฆฌ์ ์ด๋ค.
@Slf4j
@Order(1) // ์ธ์ฆ ํํฐ๋ณด๋ค ๋จผ์ ์คํ
public class RateLimitingFilter extends OncePerRequestFilter {
private static final int MAX_REQUESTS = 10; // ๋ถ๋น ์ต๋ ์์ฒญ ์
private final Cache requestCounts;
public RateLimitingFilter(CacheManager cacheManager) {
this.requestCounts = cacheManager.getCache("rateLimitCache");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String key = getRateLimitKey(request);
Integer count = requestCounts.get(key, Integer.class);
if (count == null) {
requestCounts.put(key, 1);
} else if (count >= MAX_REQUESTS) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too Many Requests");
log.warn("Rate limit exceeded for {}", key);
return;
} else {
requestCounts.put(key, count + 1);
}
filterChain.doFilter(request, response);
}
/**
* IP ๊ธฐ๋ฐ Rate Limit ํค ์์ฑ
* - `X-Forwarded-For` ํค๋๊ฐ ์กด์ฌํ๋ฉด ์ฒซ ๋ฒ์งธ IP๋ฅผ ์ฌ์ฉ (ํ๋ก์ ํ๊ฒฝ ๊ณ ๋ ค)
* - ์๋ค๋ฉด `request.getRemoteAddr()` ์ฌ์ฉ
*/
private String getRateLimitKey(HttpServletRequest request) {
String ip = getClientIp(request);
return ip + ":" + request.getRequestURI();
}
/**
* ํด๋ผ์ด์ธํธ์ ์ค์ IP ์ฃผ์ ๋ฐํ
* - `X-Forwarded-For` ํค๋๊ฐ ์กด์ฌํ๋ฉด ์ฒซ ๋ฒ์งธ IP ์ฌ์ฉ (ํ๋ก์ ํ๊ฒฝ ๊ณ ๋ ค)
* - ์๋ค๋ฉด `request.getRemoteAddr()` ์ฌ์ฉ
*/
private String getClientIp(HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isEmpty()) {
return forwardedFor.split(",")[0].trim(); // ์ฌ๋ฌ ๊ฐ์ IP๊ฐ ์์ ๊ฒฝ์ฐ ์ฒซ ๋ฒ์งธ ์ฌ์ฉ
}
return request.getRemoteAddr();
}
}
1๏ธโฃ ์์ฒญ์ด ๋ค์ด์ค๋ฉด โ ์บ์(ehcache
)์์ ์นด์ดํธ๋ฅผ +1 ์ฆ๊ฐ โฌ๏ธ
2๏ธโฃ ์ต๋ ์์ฒญ ์ โ 10๊ฐ๊น์ง ํ์ฉ
3๏ธโฃ ์บ์ ๋ง๋ฃ ์๊ฐ โ ์๋ก ์์ฑ๋ ์บ์๋ 1๋ถ๊ฐ ์ ์ง โ (๊ณ ์ ์๋์ฐ ๋ฐฉ์)
4๏ธโฃ ์์ฒญ ํ์๊ฐ 10์ ์ด๊ณผํ๋ฉด โ TOO_MANY_REQUESTS
์๋ต ๋ฐํ โ ๏ธ
โก ์ฝ 3์ด ๋์ ์ด 10๊ฐ์ ์์ฒญ์ด ์ฒ๋ฆฌ๋จ
โก ํ์ง๋ง ๋ฒํท max ๊ฐ์ 5์ด๋ฏ๋ก, ์๋๋ ํ์ฉ๋์ง ์์์ผ ํ๋ ์์ฒญ์ด ํ์ฉ๋จ
๐ ์ฒ๋ฆฌ์จ ์ ํ์ด ์์ฃผ ์ ๋ฐํ ํ์๋ ์์ผ๋ฏ๋ก, ๊ตฌํ์ด ์ฌ์ด ๊ณ ์ ์๋์ฐ ์นด์ดํฐ ๋ฐฉ์์ ์ ํ
๐ ๋ณด๋ค ์ ํํ ์ ํ์ด ํ์ํ๋ค๋ฉด ์ฌ๋ผ์ด๋ฉ ์๋์ฐ ๋ฐฉ์ ๊ณ ๋ ค ๊ฐ๋ฅ