JSON-OBJECT Software Engineering Blog

Professional Senior Backend Engineer. Specializing in high volume traffic and distributed processing with Kotlin and Spring Boot as core technologies.

View on GitHub
26 September 2023

Spring Boot, Slack 채널에 알림 메시지 전송하기

by Taehyeong Lee

개요

Slack 채널 생성 및 WebHook URL 획득

https://api.slack.com/apps 접속

# Your Apps[Create an App] 클릭
 
# Create an apps[From scratch] 클릭
 
# Name apps & choose workspace
→ App Name: (채널 이름 입력)
→ Pick an workspace to develop your apps in: (채널이 속한 워크스페이스 선택)[Create App] 클릭
 
# Basic Information[Incoming Webhooks] 클릭
 
# Incoming Webhooks
→ Activate Incoming Webhooks: On (선택)[Add New Webhooks to Workspace] 클릭
 
→ 어디에 게시해야 합니까? (채널 이름 입력)[허용] 클릭
→ 생성된 WebHook URL을 복사

환경 설정 추가

slack:
  webhook-url:
    info: {url}
    warn: {url}
    error: {url}

build.gradle.kts

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.11.0")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.3")
}

OkHttpConfig 작성

@Configuration
class OkHttpConfig {

    @Bean("okHttpClient")
    fun okHttpClient(): OkHttpClient {

        return OkHttpClient()
            .newBuilder().apply {
                connectionSpecs(
                    listOf(
                        ConnectionSpec.CLEARTEXT,
                        ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                            .allEnabledTlsVersions()
                            .allEnabledCipherSuites()
                            .build()
                    )
                )
                connectTimeout(10, TimeUnit.SECONDS)
                writeTimeout(10, TimeUnit.SECONDS)
                readTimeout(10, TimeUnit.SECONDS)
            }.build()
    }
}

JsonConfig 작성

@Configuration
class JsonConfig {

    @Bean("objectMapper")
    fun objectMapper(): ObjectMapper {

        return jacksonObjectMapper().apply {
            setSerializationInclusion(JsonInclude.Include.ALWAYS)
            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            disable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            registerModules(JavaTimeModule())
        }
    }
}

AsyncConfig 작성

@Configuration
@EnableAsync
class AsyncConfig {

    @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    fun asyncTaskExecutor(): AsyncTaskExecutor {

        val taskExecutor = TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor())

        return taskExecutor
    }
}

Slack 메시지 송신 레벨 정의

enum class SlackMessageLevel(val colorCode: String) {

    INFO("#2EB67D"),
    WARN("#ECB22E"),
    ERROR("#E01E5A")
}

Slack 메시지 전송 서비스 작성

interface SlackService {

    fun sendMessage(fieldMap: Map<String, String>, messageLevel: SlackMessageLevel = SlackMessageLevel.INFO): Future<Boolean>
}
@Service
@Async("taskExecutor")
class SlackServiceImpl(
    private val okHttpClient: OkHttpClient
    private val objectMapper: ObjectMapper
) : SlackService {

    @Value("\${slack.webhook-url.info}")
    private lateinit var SLACK_WEBHOOK_URL_INFO: String

    @Value("\${slack.webhook-url.warn}")
    private lateinit var SLACK_WEBHOOK_URL_WARN: String

    @Value("\${slack.webhook-url.error}")
    private lateinit var SLACK_WEBHOOK_URL_ERROR: String

    @Async(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    override fun sendMessage(fieldMap: Map<String, String?>, messageLevel: SlackMessageLevel): Future<Boolean> {

        val webHookUrl = when (messageLevel) {
            SlackMessageLevel.INFO -> SLACK_WEBHOOK_URL_INFO
            SlackMessageLevel.WARN -> SLACK_WEBHOOK_URL_WARN
            SlackMessageLevel.ERROR -> SLACK_WEBHOOK_URL_ERROR
        }
        
        val requestBody = objectMapper.writeValueAsString(
            mapOf(
                "attachments" to
                        arrayOf(
                            mapOf(
                                "color" to color,
                                "blocks" to arrayOf(
                                    mapOf(
                                        "type" to "section",
                                        "text" to mapOf(
                                            "type" to "mrkdwn",
                                            "text" to StringBuilder().apply {
                                                fieldMap.forEach {
                                                    if (it.value != null) {
                                                        append("*${it.key}*")
                                                        append("\n    ${it.value}\n\n")
                                                    }
                                                }
                                            }.toString()
                                        ),
                                    )
                                )
                            )
                        )
            )
        )

        val httpResponse = try {
            okHttpClient.newCall(
                Request.Builder()
                    .url(webHookUrl)
                    .post(requestBody.toRequestBody("application/json; charset=utf-8".toMediaType()))
                    .build()
            ).execute()
        } catch (ex: Exception) {
            return CompletableFuture.completedFuture(false)
        }

        val statusCode: Int = httpResponse.code
        val responseBody: String? = httpResponse.body?.string()

        return CompletableFuture.completedFuture(statusCode == 200)
    }
}

Slack 메시지 전송 예

slackService.sendMessage(
    mapOf(
        "foo" to "bar",
        "alpha" to "bravo"
    ),
    SlackMessageLevel.INFO
)

Spring Boot 애플리케이션 이벤트 연동

@Component
class SlackEventListener(
    private val slackService: SlackService
) {
    @EventListener(ApplicationReadyEvent::class)
    fun afterApplicationReady() {

        slackService.sendMessage(
            mapOf(
                "event" to "APPLICATION_STARTED",
                "node_ip_address" to InetAddress.getLocalHost().hostAddress
            ),
            SlackMessageLevel.INFO
        )
    }

    @EventListener(ApplicationFailedEvent::class)
    fun afterApplicationFailed() {

        slackService.sendMessage(
            mapOf(
                "event" to "APPLICATION_STARTUP_FAILED",
                "node_ip_address" to InetAddress.getLocalHost().hostAddress
            ),
            SlackMessageLevel.ERROR
        )
    }
}

참고 글

tags: Spring Boot