Spring Boot 예외 처리와 보안 알림
봇이 발생시키는 404, 405는 Slack 알림을 보내면 노이즈가 된다.
@RestControllerAdvice로 예외를 분류하고, 진짜 서버 오류(5xx)만 알림을 보내는 패턴이다.
예외 분류 기준
| 예외 | HTTP | 원인 | 알림 |
|---|---|---|---|
NoResourceFoundException | 404 | 봇 스캔, 잘못된 URL | 없음 |
HttpRequestMethodNotSupportedException | 405 | GET/POST 메서드 불일치 | 없음 |
MethodArgumentNotValidException | 400 | 입력값 검증 실패 | 없음 |
Exception (그 외) | 500 | 진짜 서버 오류 | Slack 알림 |
전역 예외 처리 구현
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
private final SlackNotifier slackNotifier; // 선택 사항 — 없으면 null 주입 가능
public GlobalExceptionHandler(@Autowired(required = false) SlackNotifier slackNotifier) {
this.slackNotifier = slackNotifier;
}
// ────────────────────────────────────────────
// 봇 스캔 / 잘못된 경로 → 404, 알림 없음
// ────────────────────────────────────────────
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<Void> handleNoResource(NoResourceFoundException e,
HttpServletRequest request) {
// WARN 레벨로만 기록, Slack 알림 없음
log.warn("404 Not Found: method={} uri={}", request.getMethod(), request.getRequestURI());
return ResponseEntity.notFound().build();
}
// ────────────────────────────────────────────
// 메서드 불일치 → 405, 알림 없음
// ────────────────────────────────────────────
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Map<String, String>> handleMethodNotSupported(
HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
log.warn("405 Method Not Allowed: method={} uri={}", request.getMethod(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(Map.of("error", "Method not allowed"));
}
// ────────────────────────────────────────────
// 입력값 검증 실패 → 400, 알림 없음
// ────────────────────────────────────────────
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(
MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.toList());
log.warn("400 Validation failed: {}", errors);
return ResponseEntity.badRequest()
.body(Map.of("error", "Validation failed", "details", errors));
}
// ────────────────────────────────────────────
// 그 외 모든 예외 → 500, Slack 알림
// ────────────────────────────────────────────
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleException(
Exception e, HttpServletRequest request) {
log.error("500 Internal Server Error: method={} uri={}",
request.getMethod(), request.getRequestURI(), e);
// 진짜 서버 오류만 Slack 알림
notifySlack(e, request);
return ResponseEntity.internalServerError()
.body(Map.of("error", "Internal server error"));
}
private void notifySlack(Exception e, HttpServletRequest request) {
if (slackNotifier == null) return;
try {
slackNotifier.sendError(
"[서버 오류] " + e.getClass().getSimpleName(),
request.getMethod() + " " + request.getRequestURI(),
e.getMessage()
);
} catch (Exception ex) {
log.warn("Slack 알림 전송 실패", ex);
}
}
}
SlackNotifier 구현 예시
@Component
@ConditionalOnProperty(name = "slack.webhook-url") // 설정 없으면 Bean 미등록
public class SlackNotifier {
private static final Logger log = LoggerFactory.getLogger(SlackNotifier.class);
@Value("${slack.webhook-url}")
private String webhookUrl;
private final RestTemplate restTemplate = new RestTemplate();
public void sendError(String title, String location, String message) {
String payload = """
{
"text": "*%s*\\n위치: `%s`\\n메시지: %s"
}
""".formatted(title, location, message);
try {
restTemplate.postForEntity(webhookUrl,
new HttpEntity<>(payload,
buildHeaders()),
String.class);
} catch (Exception e) {
log.warn("Slack 전송 실패: {}", e.getMessage());
}
}
private HttpHeaders buildHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
}
# application.yaml — Slack Webhook URL 설정
slack:
webhook-url: ${SLACK_WEBHOOK_URL:} # 환경변수 미설정 시 Bean 미등록
봇 스캔 로그 레벨 조정
봇이 발생시키는 404 로그가 많아 INFO 레벨이 오염되는 경우 application.yaml에서 레벨을 올린다.
logging:
level:
org.springframework.web.servlet.resource: WARN
org.springframework.web.servlet.DispatcherServlet: WARN
적용 확인
# 존재하지 않는 경로 호출 → 404, 알림 없음
curl -s -o /dev/null -w "%{http_code}" https://example.com/wp-login.php
# 잘못된 메서드 → 405, 알림 없음
curl -s -o /dev/null -w "%{http_code}" -X DELETE https://example.com/api/user/get
# 로그에서 봇 스캔 패턴 확인
grep "404 Not Found" /var/log/your-app/app.log | tail -20