1. 서론
기존에 서버에서 발생한 에러를 디스코드 웹훅으로 받고 있었음

여기서 불편한점
- 따로 에러가 발생하지 않고 메시지만 출력되는 내용을 굳이 디스코드를 받아야할까?
- ex) "상위 관문을 먼저 제거하여 주십시오."
- 누가, 어느 API를 호출해서 발생한 에러인지 모른다.
이 문제를 해결하고자 한다.
2. 개발
관련 커밋
fix: 기존 에러 처리 변경 · minhyeok2487/LostarktTodoBackend@b12e28d
- 간단한 에러는 webhook을 보내지 않음
github.com
fix: 에러에 헤더 수집 · minhyeok2487/LostarktTodoBackend@4046729
minhyeok2487 committed Mar 5, 2025
github.com
@RestControllerAdvice 를 이용한 글로벌 예외 핸들러
// 공통 예외 처리 메서드
private ResponseEntity<ErrorResponse> handleExceptionInternal(Exception ex, HttpServletRequest request, boolean sendWebHook) {
String requestInfo = String.format("%s %s", request.getMethod(), request.getRequestURI());
if (sendWebHook) {
// 에러 로그 먼저 호출
log.error("{} - {}", requestInfo, ex.getMessage());
// requestInfo에 헤더를 추가하기 위한 StringBuilder
StringBuilder headerDetails = new StringBuilder();
// 헤더 수집 및 로그 출력
Collections.list(request.getHeaderNames())
.forEach(headerName -> {
String headerValue = request.getHeader(headerName);
headerDetails.append(String.format("Header [%s] = %s%n", headerName, headerValue));
});
// 기존 requestInfo에 헤더 정보 추가
String updatedRequestInfo = requestInfo + "\n" + headerDetails;
// callEvent 호출
webHookService.callEvent(ex, updatedRequestInfo);
} else {
log.warn("{} - {}", requestInfo, ex.getMessage());
}
ErrorResponse errorResponse = ErrorResponse.of(
HttpStatus.BAD_REQUEST.value(), ex.getClass().getSimpleName(), ex.getLocalizedMessage()
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
먼저 공통 예외 처리 메서드를 만들었다.
sendWebHook 변수를 활용하여
치명적인 에러인 경우 API 정보와 함께 로그를 출력하고, 헤더의 내용을 더해 웹훅으로 전송한다.
그렇지 않은 경우 로그만 출력하는 형식이다.
여기서 치명적인 것과 아닌 것을 구분하는 것은
// 요구 조건이 충족되지 않아서 발생하는 예외처리 (web hook 미전송)
@ExceptionHandler(ConditionNotMetException.class)
public ResponseEntity<ErrorResponse> handlerConditionNotMetException(ConditionNotMetException ex, HttpServletRequest request) {
return handleExceptionInternal(ex, request, false);
}
// 그 외 오류 (web hook 전송)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handlerException(Exception ex, HttpServletRequest request) {
return handleExceptionInternal(ex, request, true);
}
ConditionNotMetException이라고 하는 새로 만든 Exception이다.
이 exception으로 들어가는 것들은 치명적이지 않은 에러이다.
예시
- 게시판 - "게시글 작성 후 15분이 지나 수정할 수 없습니다."
- 캐릭터 - "골드 획득 지정 캐릭터는 서버별로 6캐릭까지 가능합니다."
- 깐부 - "깐부 권한이 없습니다."
[전체 코드]
@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class ExControllerAdvice {
private final WebHookService webHookService;
// 요구 조건이 충족되지 않아서 발생하는 예외처리 (web hook 미전송)
@ExceptionHandler(ConditionNotMetException.class)
public ResponseEntity<ErrorResponse> handlerConditionNotMetException(ConditionNotMetException ex, HttpServletRequest request) {
return handleExceptionInternal(ex, request, false);
}
// 유효성 검사 예외 처리 (web hook 미전송)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorListResponse> handleValidationException(MethodArgumentNotValidException ex, HttpServletRequest request) {
String requestInfo = String.format("%s %s", request.getMethod(), request.getRequestURI());
log.warn("{} - {}", requestInfo, ex.getMessage());
List<String> messages = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> String.format("%s 입력된 값: [%s]", fieldError.getDefaultMessage(), fieldError.getRejectedValue()))
.toList();
ErrorListResponse errorListResponse = ErrorListResponse.of(
HttpStatus.BAD_REQUEST.value(), ex.getClass().getSimpleName(), messages
);
return new ResponseEntity<>(errorListResponse, HttpStatus.BAD_REQUEST);
}
// 그 외 오류 (web hook 전송)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handlerException(Exception ex, HttpServletRequest request) {
return handleExceptionInternal(ex, request, true);
}
// 공통 예외 처리 메서드
private ResponseEntity<ErrorResponse> handleExceptionInternal(Exception ex, HttpServletRequest request, boolean sendWebHook) {
String requestInfo = String.format("%s %s", request.getMethod(), request.getRequestURI());
if (sendWebHook) {
// 에러 로그 먼저 호출
log.error("{} - {}", requestInfo, ex.getMessage());
// requestInfo에 헤더를 추가하기 위한 StringBuilder
StringBuilder headerDetails = new StringBuilder();
// 헤더 수집 및 로그 출력
Collections.list(request.getHeaderNames())
.forEach(headerName -> {
String headerValue = request.getHeader(headerName);
headerDetails.append(String.format("Header [%s] = %s%n", headerName, headerValue));
});
// 기존 requestInfo에 헤더 정보 추가
String updatedRequestInfo = requestInfo + "\n" + headerDetails;
// callEvent 호출
webHookService.callEvent(ex, updatedRequestInfo);
} else {
log.warn("{} - {}", requestInfo, ex.getMessage());
}
ErrorResponse errorResponse = ErrorResponse.of(
HttpStatus.BAD_REQUEST.value(), ex.getClass().getSimpleName(), ex.getLocalizedMessage()
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
3. 결과

어떤 API에서 어떠한 문제점이 있었는지 확인가능!
아쉬운점이 있나면 RequestBody에 담긴 내용도 함께 전송했으면 하는데...
그렇게 하려면 캐시에 먼저 body값을 저장해야하기 때문에 메모리 부하가 커지게 된다.
현재 t4g.micro를 쓰고 있는 상황에서 메모리에 여유가 있지는 않기 때문에 그부분은 넘어가려고한다.