
@RestControllerAdvice는 스프링의 전역 예외 처리와 응답 바디 직렬화 기능을 한 번에 제공하는 어노테이션으로 @ControllerAdvice와 @ResponseBody를 합친 어노테이션이다.
@ControllerAdvice
@ResponseBody
주요 특징
전역 예외 핸들링
일관된 에러 응답
응답 직렬화 자동 적용
사용했을 때 이점
핸들러를 한 번만 정의하면 모든 @RestController에서 던지는 예외를 일관된 방식으로 처리할 수 있다.
예외에 대해 동일한 응답 형태를 보장한다.
에러 로깅, Sentry 전송, Webhook 전송 등에 대한 로직을 한 곳에 모아둘 수 있다.
예외처리, 응답 변환, 로깅, 알림에 대한 로직을 분리하기 때문에 유지보수가 쉬워지고 컨트롤러는 비즈니스 로직에만 집중할 수 있다.
RestControllerAdvice를 사용하지 않으면 어떻게 될까?
각 컨트롤러마다 try~catch를 직접 구현해야 하고, 별도의 에러 발생 메시지와 HTTP 상태 코드를 세팅해야 한다.
응답 형태에 일관성이 없어진다.
로깅, 모니터링 코드가 분산되어 유지보수성이 떨어진다.
어노테이션 기반으로 코드를 자동 생성해주는 라이브러리
주요 어노테이션
@Getter/@Setter
@NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor
@Data
@Getter + @Setter + @ToString + @EqualsHashCode + @RequiredArgsConstructor
@Setter가 들어가니 주의!
toString(), equals(), hashCode() 자동 생성
@Builder
@Slf4j
애플리케이션 간에 실시간으로 이벤트나 데이터를 푸시하기 위한 HTTP 콜백 메커니즘
서버에 특정 이벤트가 발생했을 때 미리 등록해 둔 URL으로 자동으로 HTTP 요청을 보낸다.
동작 원리
클라이언트가 서버에 웹훅 URL을 등록
서버에서 특정 이벤트가 발생
서버가 등록된 URL로 HTTP POST 요청을 보내는데, 보통 JSON payload에 이벤트 관련 데이터를 담는다.
수신자 측 어플리케이션이 엔드포인트에서 요청을 받아 비즈니스 로직 수행
적절한 응답을 반환
장점
실시간 이벤트 알림이 가능하다.
불필요한 polling이 없다.
여러 수신자에게 동일 이벤트를 푸싱할 수 있다.
단점
누락, 중복 방지를 위한 재시도와 서명 검증 등의 추가 구현이 필요하다.
엔드포인트 노출 위험이 있어 별도의 보안 조치가 필요하다.
디버깅이 어렵다.
주요 사용 사례
결제 승인/실패 이벤트를 webhook 으로 받아 주문 상태 업데이트
깃허브의 푸시나 PR 이벤트를 웹훅으로 받아 자동 빌드,배포 트리거
슬랙, 디스코드와 같은 채팅 서비스에 시스템 이벤트를 실시간으로 알려줌
S3 업로드 완료와 같은 알림을 웹훅으로 받아 후속 처리할 수 있다.
1. 디스코드/슬랙 웹훅(Webhook) 설정
다음과 같이 에러 메시지를 전송하기 위한 별도의 디스코드 채널을 설정했다.
채널 설정에 들어가 웹후크 항목에 들어간 뒤,
다음과 같이 새로운 웹 후크를 생성해주었고, '웹후크 URL 복사' 버튼을 눌러 URL을 복사해주었다.
application.yml 파일에 들어가 다음과 같이 주소를 붙여넣기 해주어 디스코드 웹후크 기본 설정을 완료해주었다.
discord:
webhook-url: 복사한 url 경로
2. 디스코드/슬랙 웹훅(Webhook) 설정
디스코드에 메시지를 보내기 위해 별도의 메시지 전송 객체를 생성해주었다.
public record DiscordWebhookPayload(
String content,
List<Embed> embeds
) {
public record Embed(
String title,
String description,
String timestamp,
List<Field> fields
) {
public record Field(String name, String value, boolean inline) {}
}
}
기존 미션 코드에서 예외 처리를 담당하는 부분이 ExceptionAdvice.class 파일이기 때문에 거기에 Discord 메시지를 보내는 코드를 추가하기로 하였다.
먼저 application.yml 파일에 적어놓은 웹훅 url을 가져오기 위해 @Value 어노테이션을 사용해 url을 가져오도록 설정했고, 미션에서 로컬 환경과 운영 환경을 분리하도록 요청했기 때문에, @Profile 값을 받아올 수 있는 env 변수를 선언해주었다.
@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${discord.webhook-url}")
private String discordWebhookUrl;
@Autowired
private Environment env;
...
}
다음으로 디스코드에 메시지를 보낼 수 있도록 sendToDiscord 메서드를 생성해주었다. (미션 요구사항을 만족하기 위해 프로필이 "prod"일 때만 메시지를 보내도록 구현했다.) 위에서 만들었던 DiscordWebhokPayload 객체에 메시지를 적절하게 담아 보내도록 코드가 구현되었다.
private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) {
if (!env.acceptsProfiles(Profiles.of("prod"))) {
log.debug("현재 active 프로파일이 prod가 아니므로 Discord 알림을 보내지 않습니다.");
return;
}
String path = ((ServletWebRequest) request).getRequest().getRequestURI();
String timestamp = Instant.now().toString();
// Embed 필드 구성
DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed(
"🚨 서버 에러 발생",
"```" + ex.getMessage() + "```",
timestamp,
List.of(
new DiscordWebhookPayload.Embed.Field("URL", path, false),
new DiscordWebhookPayload.Embed.Field("Status", status.toString(), true),
new DiscordWebhookPayload.Embed.Field("Time", timestamp, true),
new DiscordWebhookPayload.Embed.Field("Exception", ex.getClass().getSimpleName(), true)
)
);
DiscordWebhookPayload payload = new DiscordWebhookPayload(null, List.of(embed));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
try {
restTemplate.postForEntity(
discordWebhookUrl,
new HttpEntity<>(payload, headers),
String.class
);
log.info("Discord Webhook 전송 완료");
} catch (Exception sendEx) {
log.warn("Discord Webhook 전송 실패", sendEx);
}
}
이후, 다음과 같이 예외를 처리하는 메서드에 sendToDiscord() 메서드를 통해 메시지를 디스코드에 전달하도록 코드를 추가해주었다.
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
WebRequest webRequest = new ServletWebRequest(request);
sendToDiscord(generalException, webRequest, errorReasonHttpStatus.getHttpStatus());
return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
}
3.에러 발생 시 알림 테스트
실제로 예외에 대해 메시지가 적절하게 발생하는지 확인하기 위해 기존에 작성되어 있던 TempRestController에 에러 전용 api 엔드포인트를 추가해주었다.
@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {
private final TempQueryService tempQueryService;
@GetMapping("/test")
public ApiResponse<TempResponse.TempTestDTO> testAPI(){
return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
}
@GetMapping("/exception")
public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag){
tempQueryService.CheckFlag(flag);
return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
}
@GetMapping("/error")
public void errorAPI(){
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}
}
이후, 다음과 같이 postman을 통해 요청을 보내보았고,
메시지가 Discord에 잘 보내지는 것을 확인했다!
추가적으로, 다음과 같이 application.yml의 프로필을 prod가 아닌 다른 값으로 변경하면, 요청을 보내도 디스코드에 알림이 가지 않는다!
spring:
profiles:
active: local