스프링부트 예외 발생 시, Slack으로 알림 보내기 (feat. Slack WebHooks)

 

 

스프링부트 with Slack

슬랙 WebHooks 사용하여 에러 로깅해보기

Slack

 
 
서버를 개발하다보면 미처 처리하지 못한 알 수 없는 에러가 발생하는 경우가 종종 발생한다.
 
이런 경우 로컬 환경이라면 금방 디버깅이 가능하지만, 서버를 배포해놓은 상황이라면
서버의 로그 파일을 통해 발생한 예외를 확인하거나, 모니터링 기능을 추가하여 해결하는 등 다양한 방법을 통해 해결해야한다.
 
이와 마찬가지로 슬랙을 통해서도 문제에 대한 해결책을 던져볼 수 있다.
예외가 발생하면 슬랙의 채널에 에러 로그가 전달되어 환경에 구분없이 빠른 조치와 해결이 가능하다!
 
꼭 한번 사용해보는 것을 추천한다.
 


패키지 구조

Package Structure

 

플로우 🌊

  1. SlackTestController에서 요청을 받아 Exception을 발생시킨다.
  2. ControllerExceptionAdvice에서 예외를 캐치하여 처리되어있지 않은 에러라면 500에러를 발생시키고 ApiResponse를 return함과 동시에,
    SlackApi를 사용하여 발생한 예외에 대한 내용을 작성하여 Slack webhooks 를 통해 설정한 채널로 전송
  3. 받은 예외에 대한 내용을 통하여 지정한 슬랙 채널에 에러 로깅을 수행
  4. 에러 로깅을 통한 트러블슈팅

 


1. 슬랙에 Webhook 앱 추가 ➕

슬랙에 요청이 들어오면 특정 채널에 전달받은 요청에 담긴 메시지를 전달하기 위하여 Webhook 앱을 설치해야 한다.
 

webhook 추가를 원하는 슬랙 워크스페이스에 접속하여 좌측 채널 탭, 가장 하단에 있는 앱 추가 버튼 클릭
 
 

우리는 요청을 수신받아 메시지를 출력해줄 것이기 때문에 Incoming WebHooks 추가
 
 

Slack에 추가 버튼을 클릭한 후, WebHook URL을 통해 보낸 메시지를 출력하기 원하는 채널을 선택
 
 

설정이 끝나면 WebHooks URL을 받을 수 있다.
 
 


2. SpringBoot에 적용 ✨

 

2-1. build.gradle에 의존성 추가

// Slack Webhook
	implementation 'com.slack.api:slack-api-client:1.28.0'
	implementation 'com.google.code.gson:gson:2.10.1'
	implementation 'com.squareup.okhttp3:okhttp:4.10.0'
	implementation 'com.slack.api:slack-app-backend:1.28.0'
	implementation 'com.slack.api:slack-api-model:1.28.0'

 

2-2. application.yml 에 slack 관련 환경설정

slack:
  webhook:
    url: {webhook-URL}

 
 

2-3. RestController 생성

@RestController
@RequestMapping("/test")
public class SlackTestController {

    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public ApiResponse test() {
        throw new IllegalArgumentException();
    }
}

/test 라는 경로로 요청이 들어오면 IllegalArgumentException 을 발생시키는 API 생성

 

2-4. Error처리 Enum 클래스 생성

 
Error.java

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum Error {

/**
     * 500 INTERNAL SERVER ERROR
     */
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류"),
    ;

    private final HttpStatus httpStatus;
    private final String message;

    public int getHttpStatusCode() {
        return httpStatus.value();
    }
}

상태코드와 ErrorMessage에 대한 정보를 담은 Error enum클래스 생성
필드로는 HttpStatus를 담는 httpStatus와 String형태의 message가 있다.

 

2-5. 요청에 대한 응답결과 Response를 위한 ApiResponse 클래스 생성

@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiResponse<T> {

    private final int status;
    private final String message;
    private T data;

    public static ApiResponse error(Error error) {
        return new ApiResponse<>(error.getHttpStatusCode(), error.getMessage());
    }
}

위에서 생성한 Error enum클래스를 입력받아, 이를 통해 응답결과를 생성해줄 ApiResponse 클래스이며 정적 메서드로 구성된다.
Error 외에도 요청 성공에 대한 응답도 추가하여 사용가능하다.

 

2-6. 예외를 catch해줄 ControllerExceptionAdvice 클래스 생성

@RestControllerAdvice
@Component
@RequiredArgsConstructor
public class ControllerExceptionAdvice {

    private final SlackUtil slackUtil;

/**
     * 500 Internal Server
     */
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    protected ApiResponse<Object> handleException(final Exception error, final HttpServletRequest request) throws IOException {
        slackUtil.sendAlert(error,request);
        return ApiResponse.error(Error.INTERNAL_SERVER_ERROR);
    }
}

@RestControllerAdvice는 스프링에서 제공하는 기능으로 요청에 대하여 @ExceptionHandler에 등록해놓은 예외가 발생하면 해당 예외를 잡은 핸들러 메서드를 실행한다.

이처럼 이곳에서 서버 개발자 측에서 예상한 예외들을 핸들링할 수 있는데, 예상한 예외상황에서 벗어나 예외 처리가 되지 않은 내용에 대해서는
예외 클래스의 최상위 클래스인 Exception 클래스에 대한 handleException 메서드가 이를 캐치하게 되고 그로 인해 slackApi의 에러 로깅 메서드를 실행시키는 것이다.

 

2-7. Slack 에 에러 로깅을 수행하는 SlackApi 클래스 생성

@Component
@RequiredArgsConstructor
@Slf4j
public class SlackUtil {

		//application.yml 에 등록해놓은 webhookUrl
    @Value("${slack.webhook.url}")
    private String webhookUrl;

		private StringBuilder sb= new StringBuilder();

    // Slack으로 알림 보내기
    public void sendAlert(Exception error, HttpServletRequest request) throws IOException {

        // 현재 프로파일이 특정 프로파일이 아니면 알림보내지 않기
//        if (!env.getActiveProfiles()[0].equals("set1")) {
//            return;
//        }
				

				// 메시지 내용인 LayoutBlock List 생성
        ListlayoutBlocks= generateLayoutBlock(error,request);
				
				// 슬랙의 send API과 webhookURL을 통해 생성한 메시지 내용 전송
        Slack.getInstance().send(webhookUrl, WebhookPayloads
                .payload(p->
												// 메시지 전송 유저명
												p.username("Exception is detected 🚨")
												// 메시지 전송 유저 아이콘 이미지 URL
                        .iconUrl("<https://yt3.googleusercontent.com/ytc/AGIKgqMVUzRrhoo1gDQcqvPo0PxaJz7e0gqDXT0D78R5VQ=s900-c-k-c0x00ffffff-no-rj>")
												// 메시지 내용
                        .blocks(layoutBlocks)));
    }

    // 전체 메시지가 담긴 LayoutBlock 생성
    private List generateLayoutBlock(Exception error, HttpServletRequest request) {
        return Blocks.asBlocks(
                getHeader("서버 측 오류로 예상되는 예외 상황이 발생하였습니다."),
                Blocks.divider(),
                getSection(generateErrorMessage(error)),
                Blocks.divider(),
                getSection(generateErrorPointMessage(request)),
                Blocks.divider(),
								// 이슈 생성을 위해 프로젝트의 Issue URL을 입력하여 바로가기 링크를 생성
                getSection("<github_issue_url)|이슈 생성하러="" 가기="">")
        );
    }

		// 예외 정보 메시지 생성
		private String generateErrorMessage(Exception error) {
        sb.setLength(0);
        sb.append("*[🔥 Exception]*" + NEW_LINE + error.toString() + DOUBLE_NEW_LINE);
        sb.append("*[📩 From]*" + NEW_LINE + readRootStackTrace(error) + DOUBLE_NEW_LINE);

        return sb.toString();
    }

    // HttpServletRequest를 사용하여 예외발생 요청에 대한 정보 메시지 생성
    private String generateErrorPointMessage(HttpServletRequest request) {
        sb.setLength(0);
        sb.append("*[🧾세부정보]*" + NEW_LINE);
        sb.append("Request URL : " + request.getRequestURL().toString() + NEW_LINE);
        sb.append("Request Method : " + request.getMethod() + NEW_LINE);
        sb.append("Request Time : " + new Date() + NEW_LINE);

        return sb.toString();
    }
		
		// 예외발생 클래스 정보 return
    private String readRootStackTrace(Exception error) {
        return error.getStackTrace()[0].toString();
    }
		// 에러 로그 메시지의 제목 return
    private LayoutBlock getHeader(String text) {
        return Blocks.header(h->h.text(
plainText(pt->pt.emoji(true)
                        .text(text))));
    }
		
		// 에러 로그 메시지 내용 return
    private LayoutBlock getSection(String message) {
        return Blocks.section(s->
s.text(BlockCompositions.markdownText(message)));
    }
</github_issue_url)|이슈>

해당 클래스에서는 Slack API를 사용하여 슬랙 채널로 전송할 메시지를 생성 및 전송한다.
자세한 내용은 주석을 통해 알아보자!
 
 

짚고 넘어가기🔥

현재 SlackApi 클래스에서 메시지 내용을 입력받아 LayoutBlock을 return하는 getSection, getHeader 등의 메시지를 통해
슬랙 메시지가 LayoutBlock 단위로 구성되는 것을 알 수 있다!

블럭을 구성하기 위해선 의존성에 추가했던 slack.api.model 에서 제공하는
Blocks 클래스의 정적 메서드를 사용하여 구현이 가능하다.

 
슬랙에서 제공하는 Block Kit Builder 를 사용하면 어떤 블럭이 존재하는 지, 이름과 형태를 확인할 수 있다.
이를 활용하여 원하는 블럭을 구성하여 메시지를 만들어보자

 

Slack

nav.top { position: relative; } #page_contents > h1 { width: 920px; margin-right: auto; margin-left: auto; } h2, .align_margin { padding-left: 50px; } .card { width: 920px; margin: 0 auto; .card { width: 880px; } } .linux_col { display: none; } .platform_i

app.slack.com

 
 


3. 테스트 ✅

이제 모든 설정이 끝났으니 예외 발생 시, 실제 슬랙 채널로 에러 로깅 메시지가 전송되는 지 확인하자
 

postman 테스트

Postman을 통한 결과는 성공적으로 출력되었으며 상태코드 500이 return 되었다.
 

슬랙에도 마찬가지로 위와 같이 정상적으로 예외에 대한 정보를 담은 메시지가 출력되었다!