Spring Boot Slack 적용(with ThreadPoolTaskExecutor)

최민길(Gale)·2023년 1월 16일
1

Spring Boot 적용기

목록 보기
13/46

안녕하세요 오늘은 에러 메시지를 Slack에서 확인하는 내용에 대해서 포스팅해보도록 하겠습니다. 기존 투다 서비스의 경우 메일로 에러를 확잏나고 있었는데 메일 자동 동기화 속도가 느려 빠르게 받아볼 수 없어서 Slack 연동을 결정했습니다.

우선 Slack에 로그인하신 후 새로운 채널을 만들어 우클릭을 눌러 "채널 세부정보 보기" 항목을 클릭합니다. 그 후 "통합" 탭에 들어가 "앱 추가" 버튼을 누른 후 "imcoming-webhook"을 설치합니다. 그럼 URL을 반환하는데 이 URL을 이용해서 Slack으로 메시지를 전송할 예정입니다.

    // Slack
    implementation "net.gpedro.integrations.slack:slack-webhook:1.4.0"

build.gradle에 다음과 같은 dependency를 추가합니다.

#Slack
slack.webhook-uri={위에서 발급받은 URL}

이어서 application.properties에 위에서 발급받은 URL을 넣어줍니다.

package com.example.test.config;

import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackAttachment;
import net.gpedro.integrations.slack.SlackMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SlackConfig {

    @Value("${slack.webhook-uri}")
    private String slackToken;

    @Bean
    public SlackApi slackApi(){
        return new SlackApi(slackToken);
    }

    @Bean
    public SlackAttachment slackAttachment(){
        SlackAttachment slackAttachment = new SlackAttachment();

        slackAttachment.setFallback("Error");
        slackAttachment.setColor("danger");
        slackAttachment.setTitle("Error Directory");

        return slackAttachment;
    }

    @Bean
    public SlackMessage slackMessage(){
        SlackMessage slackMessage = new SlackMessage();

        slackMessage.setIcon(":ghost:");
        slackMessage.setText("Error Detected");
        slackMessage.setUsername("TODA Error Catcher");
        return slackMessage;
    }

}

다음으로 SlackConfig 클래스를 생성하여 Slack 클래스들의 의존성을 주입하겠습니다.

package com.example.test.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class ThreadPoolConfig {

    // ThreadPool 설정값

    // 서버 코어 수만큼 할당
    @Value("${spring.task.execution.pool.core-size}")
    int CORE_POOL_SIZE;

    // 스레드 풀 최대 스레드 할당 수
    @Value("${spring.task.execution.pool.max-size}")
    int MAX_POOL_SIZE;

    @Bean
    public TaskExecutor taskExecutor(){

        // ThreadPoolTaskExecutor 생성
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.initialize();
        return executor;
    }
}

이어서 ThreadPoolConfig 클래스를 통해 Slack 메시지 발송을 비동기로 처리하도록 설정합니다. Spring Boot에서는 독립적으로 실행한 가능한 작업인 task들을 다양하게 실행하도록 추상화하여 TaskExecutor라는 인터페이스를 제공합니다. 주로 독립적인 스레드에 의해 비동기로 실행될 때 사용하며, ThreadPoolExecutor를 통해 요청을 지정한 크기의 스레드 풀에서 처리하여 무거운 작업을 비동기로 빠르게 처리가 가능합니다.

package com.example.test.error;

import jakarta.servlet.http.HttpServletRequest;
import net.gpedro.integrations.slack.SlackApi;
import net.gpedro.integrations.slack.SlackAttachment;
import net.gpedro.integrations.slack.SlackField;
import net.gpedro.integrations.slack.SlackMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

@ControllerAdvice
public class ErrorDetectAdvisor {

    @Autowired
    private SlackApi slackApi;

    @Autowired
    private SlackAttachment slackAttachment;

    @Autowired
    private SlackMessage slackMessage;

    @Autowired
    private TaskExecutor taskExecutor;

    @ExceptionHandler(Exception.class)
    public void handleException(HttpServletRequest request, Exception e) throws Exception{
        slackAttachment.setTitleLink(request.getContextPath());
        slackAttachment.setText(Arrays.toString(e.getStackTrace()));

        slackAttachment.setFields(
                List.of(
                        new SlackField().setTitle("Request URL").setValue(String.valueOf(request.getRequestURL())),
                        new SlackField().setTitle("Request Method").setValue(request.getMethod()),
                        new SlackField().setTitle("Request Time").setValue(new Date().toString()),
                        new SlackField().setTitle("Request IP").setValue(request.getRemoteAddr()),
                        new SlackField().setTitle("Request User-Agent").setValue(request.getHeader("User-Agent"))
                )
        );

        slackMessage.setAttachments(Collections.singletonList(slackAttachment));

        // ThreadPool에서 비동기로 메시지 발송
        taskExecutor.execute(new Runnable() {
            @Override
            public void run() {
                slackApi.call(slackMessage);
            }
        });
        throw e;
    }
}

이제 Slack으로 메시지 전송 준비는 끝났고, 서비스에서 발생하는 예외를 넘겨주는 세팅을 진행합니다. @ControllerAdvice를 통해서 모든 @Controller에서 발생할 수 있는 예외를 잡아 처리합니다. 그 후 @ExceptionHandler를 통해 @Controller, @RestController가 적용된 Bean내에서 발생하는 예외를 하나의 메서드에서 처리해줍니다. 이 때 내부에 Slack 메시지 발송 준비를 진행하여 slackApi.call(slackMessage) 메소드를 통해 메시지를 발송합니다.

실제로 서비스에서 예외를 발생시키면 다음과 같이 Slack에 메시지가 발송되는 것을 확인할 수 있습니다. 이상으로 오늘의 포스팅 마치도록 하겠습니다!

출처
: https://shanepark.tistory.com/430
: https://jeong-pro.tistory.com/195
: https://hyeonyeee.tistory.com/73

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글