[AvAb] Spring + Discord Webhook으로 에러 상황 알림 받기

엄기훈·2024년 2월 20일
3

AvAb

목록 보기
4/4
post-thumbnail

아브아브의 데모데이도 얼마 남지 않았습니다.
서버는 얼추 기능 개발이 종료되고 리팩터링할 것들을 찾아보고 있는데요.
그 중에서 프런트엔드와 소통하면서 겪었던 문제 상황을 자동화해 보려 합니다.

🤔 문제 상황

서버에 알 수 없는 예외가 생길 경우 500 상태 코드와 함께 간략한 오류 원인을 응답하도록 ExceptionHandler를 구성해두었습니다.

또한 디버깅 용도로 Stack Trace를 로그로 찍도록 설정해두었습니다.

@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
	e.printStackTrace();

	return handleExceptionInternalFalse(
    	e,
        ErrorStatus._INTERNAL_SERVER_ERROR,
        HttpHeaders.EMPTY,
        ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),
        request,
        e.getMessage());
}

프런트에서 API 연동을 하면서 500 응답이 왔을 때 프런트에서는 자신이 원인인지 서버의 문제인지 헷갈려하는 경우가 많았고, 혼자 고민하다가 뒤늦게 담당자에게 연락을 하는 경우가 발생했습니다.
서버 담당자도 API 응답에 적힌 오류 원인을 봐서 어떤 오류인지 알 수 없었고 결국 Beanstalk이나 EC2에서 로그를 찍어봐야 하는 번거로움이 존재했습니다.

또한, 데모데이에서 시현하는 도중 에러가 발생하면 즉각적으로 반응하기 위한 도구가 필요했습니다.

  1. 에러 발생 시 부가적인 로그를 포함시켜 에러 원인을 1차적으로 인지하는 것의 필요성
  2. 에러 발생 시 즉각적인 대응을 위한 수단의 필요성

기존에도 이슈 트래킹을 위해 Discord Webhook을 Github와 연동해 두었는데요.
이처럼 Webhook을 활용해 에러 상황을 Discord로 받아보도록 자동화하기로 했습니다.

🤖 Discord Webhook 설정

먼저 디스코드 Webhook을 생성해야 합니다.

채널 편집 - 연동 - 웹후크에 들어가면 웹후크를 생성할 수 있습니다.
저는 디스코드에서 서버 에러 상황만 볼 수 있도록 새 채널에 웹후크를 생성했습니다.
이후 웹후크 URL 복사 버튼을 눌러 URL을 복사해둡니다.

✉️ Spring에서 Webhook으로 메시지 보내기

Feign Client 구성하기

Discord Webhook에 메시지 요청을 보내기 위해 Feign Client 인터페이스를 작성합니다.

@FeignClient(
        name = "discord-client",
        url = "{discord-webhook-url}",
        configuration = DiscordFeignConfiguration.class)
public interface DiscordClient {

    @PostMapping()
    void sendAlarm(@RequestBody DiscordMessage message);
}

이 때 url은 웹후크 생성할 때 복사한 url을 붙여 넣어주면 됩니다.
요청 바디는 Discord 공식 문서를 참고해서 아래와 같이 DTO 클래스를 작성합니다.

{
  "content": "string"
  "embeds": [
  	{
  		"title": "string"
  		"description": "string"
	},
	...
  ]
}
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class DiscordMessage {

    private String content;
    private List<Embed> embeds;

    @Builder
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Getter
    public static class Embed {

        private String title;
        private String description;
    }
}

Feign Client에서 JSON 직렬화를 위해서는 클래스에 반드시 Getter 메소드가 있어야 합니다.

ExceptionHandler 구성하기

이후 RestControllerAdvice 클래스에 아래의 메소드들을 작성합니다.

private void sendDiscordAlarm(Exception e, WebRequest request) {
	discordClient.sendAlarm(createMessage(e, request));
}

private DiscordMessage createMessage(Exception e, WebRequest request) {
	return DiscordMessage.builder()
    	.content("# 🚨 에러 발생 비이이이이사아아아앙")
        .embeds(
        	List.of(
            	Embed.builder()
            		 .title("ℹ️ 에러 정보")
            	 	 .description(
                 		"### 🕖 발생 시간\n"
                    	+ LocalDateTime.now()
                    	+ "\n"
                    	+ "### 🔗 요청 URL\n"
                    	+ createRequestFullPath(request)
                    	+ "\n"
                    	+ "### 📄 Stack Trace\n"
                    	+ "```\n"
                    	+ getStackTrace(e).substring(0, 1000)
                    	+ "\n```")
                    .build()
                    )
				)
                .build();
}

private String createRequestFullPath(WebRequest webRequest) {
	HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest();
    String fullPath = request.getMethod() + " " + request.getRequestURL();

	String queryString = request.getQueryString();
    if (queryString != null) {
    	fullPath += "?" + queryString;
    }

	return fullPath;
}

private String getStackTrace(Exception e) {
	StringWriter stringWriter = new StringWriter();
    e.printStackTrace(new PrintWriter(stringWriter));
    return stringWriter.toString();
}

sendDiscordAlarm은 발생한 예외와 WebReqeust 요청 객체를 받아 메시지를 만들고 FeignClient를 통해 웹후크로 메시지 전송 요청을 보냅니다.

createMessage는 예외와 요청 객체를 받아 메시지 DTO 객체를 생성합니다.
마크다운 문법에 맞춰서 메시지를 작성하면 디스코드에 반영이 돼서 아름답게 표시됩니다!!

Stack Trace가 너무 길면 Discord Webhook 서버에서 Bad Reqeust 응답이 옵니다...
이를 위해서 적절히 Stack Trace를 잘라줍시다.

createReqeustFullPath는 클라이언트에서 요청한 URL의 전체 경로 문자열을 생성합니다.
이 때, WebRequestServletWebRequest로 타입 캐스트해 HttpServletRequest를 받아와야 합니다.
이렇게 하면 요청 메소드, 요청 URL, 쿼리 스트링까지 접근할 수 있게 됩니다.

getStackTrace는 Stack Trace를 문자열로 받아옵니다.
Stack Trace를 바로 문자열로 가져오게 해달라!

이후 모든 예외를 잡는 ExceptionHandler 메소드에서 sendDiscordAlarm 메소드를 호출합니다.

@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
	e.printStackTrace();

    sendDiscordAlarm(e, request);

	return handleExceptionInternalFalse(
    	e,
        ErrorStatus._INTERNAL_SERVER_ERROR,
        HttpHeaders.EMPTY,
        ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),
        request,
        e.getMessage());
}

이제 알 수 없는 예외(500 에러)가 발생하면 디스코드로 알림이 옵니다.

배포 환경에서만 알림 가도록 하기

로컬 환경에서 개발을 하다가 테스트 도중 500 에러가 발생한다면 동일하게 디스코드로 알림이 오기 때문에 실제 에러가 아닌데도 황급하게 달려가는 일이 발생하겠죠?
이를 위해서 배포 환경에서만 디스코드 알림이 가도록 로직을 수정 해야합니다.

아브아브 서버는 local 프로파일과 dev 프로파일을 나누어서 운영하고 있는데요
local은 자신의 컴퓨터에서 개발할 때 사용하는 프로파일이고, dev는 개발 서버에서 사용하는 프로파일입니다.

local 환경에서는 디스코드 알림이 가지 않도록 코드를 작성 해야합니다.
Environment 객체를 주입 받은 뒤 로컬 환경인지 확인하는 코드를 추가해주면 됩니다.

if (!Arrays.asList(environment.getActiveProfiles()).contains("local")) {
	sendDiscordAlarm(e, request);
}

😎 결론

이제 500 에러가 발생하면 디스코드로 바로 알림이 와 즉각적인 대응이 가능하게 되었습니다.
(물론 이런 에러가 발생하지 않는게 제일 좋지만요)

예외 발생 시 알림 기능 말고도 Webhook를 이용하면 자동화할 수 있는 것들이 많습니다.
가령 토스에서는 PR이 올라오면 자동으로 리뷰어를 정하고 리마인드 알림이 가도록 하는 Github Actions를 구성해서 사용하고 있다고 합니다.
Webhook를 이용한 자신만의 서비스를 제공하는 것도 재미있을 것 같네요.

profile
한 번 더 고민해보는 개발자

0개의 댓글