본문 바로가기
개발 이야기

슬랙 알림 최적화 - 배치처리, 버퍼링

by 신재권 2024. 8. 25.

많은 기업에서 슬랙을 단순한 커뮤니케이션 도구 이상으로 활용하고 있을 것이다. 우리 팀은 애플리케이션에서 발생하는 중요한 에러 로깅 알림을 슬랙을 통해 관리하고 있다.

 

우리 앱은 이미 ELK(Elasticsearch, Logstash, Kibana)를 사용하여 로그를 수집하고 있으며, 여기에는 operation time 정보도 포함된다. ELK 검색을 통해 긴 operation time을 가진 요청들을 추적할 수 있지만, 슬랙 알림을 통해 이를 더욱 신속하게 모니터링할 수 있다고 판단하여 새로운 기능을 구현하였다.

 

구현은 간단했다. 로깅 필터에서 ELK에 로그를 쌓을 때, 특정 시간(15,000ms) 이상의 작업이 발생하면 비동기로 슬랙 알림을 발송하도록 했다. 이를 통해 별도의 검색 없이도 긴 시간이 소요되는 작업을 즉시 추적할 수 있게 되었다.

 

하지만 예상 외로 15,000ms 이상 소요되는 작업이 많았고, 이에 따라 슬랙 알림도 빈번하게 발생했다. 슬랙의 API 사용 제한(분당 약 20개 메시지)으로 인해 429 에러가 발생하기 시작했고, 이는 다른 중요한 에러 로깅 알림이 무시될 수 있는 위험을 초래했다.

 

이 문제를 해결하기 위해 배치 버퍼 기능을 구현했다. Redis List를 사용하여 슬랙 알림 메시지를 버퍼에 모아두고, 스케줄러를 통해 일정 간격으로 버퍼의 메시지를 발송하도록 했다.

fun sendMessage() {
    val key = KeyManager.getLongOperationBufferKey()
    val message = longOperationMessageBufferService.getMessagesBySize(key, MESSAGE_BATCH_LIMIT)

    if (message.isNotEmpty()) {
        val joinMessage = message.joinToString("\n")
        asyncSlackSender.sendMessage(SlackChannel.LONG_OPERATION_API, joinMessage)
    }
}

key를 통해 큐에 쌓여있는 메시지를 가져오고, 메시지가 비어있지 않다면 슬랙 알림을 보내는 코드이다. 이 메서드를 스케줄링을 통해 1분마다 호출하도록 구현하였다.

override fun getMessagesBySize(
    key: String,
    size: Long,
): List<String> {
    val messages = redisTemplate.opsForList().range(key, 0, size - 1)
    if (!messages.isNullOrEmpty()) {
        redisTemplate.opsForList().trim(key, messages.size.toLong(), -1)
    }
    return messages ?: emptyList()
}

주어진 size 만큼 큐에 쌓인 메시지들을 가져온다. 쌓인 메시지의 수 보다 큰 size를 전달해도, 알아서 최종 메시지까지 가져온다.

그 후 trim을 통해 읽은 메시지를 지워준다. 읽는 작업 <-> 지우는 작업 사이에 새로운 메시지가 추가되도 손실되지 않는다.

 

이 방식을 통해 슬랙 API의 사용 제한을 우회하면서도 중요한 에러 알림이 누락되지 않도록 할 수 있었다. 현재는 하나의 슬랙 채널에만 적용했지만, 필요에 따라 다른 채널에도 쉽게 확장할 수 있다.

최종 목적은 리소스 사용을 최적화하고, 외부 시스템과의 상호작용을 효율적으로 관리하는 것이다.

 

만약 레디스 장애 상황 발생 시에도 메세지가 제대로 전달되어야 한다면 장애 발생 시 fallback 로직으로 메시지를 외부 저장소에 저장하고, 외부 저장소를 별도로 탐색하는 스케줄링을 만들 것 같다. 현재 시나리오에서는 유실되어도 큰 문제는 없다고 생각해서 장애 상황까지는 고려하지 않았다.

 

또한 위 코드에서 BATCH_MESSAGE_LIMIT는 200이다. 이는 한 번에 보낼 수 있는 슬랙 메시지 길이를 계산하여 추정한 값이다.

한 번에 보낼 수 있는 슬랙 메시지 길이에도 제한이 있기 떄문이다.

즉, 큐에서 최대 200개의 메시지를 가져오고, 삭제한다. -> 즉 가져오고 삭제하는데 각각 O(200) 시간복잡도가 든다.

 

레디스는 싱글 스레드로 동작하기 때문에, 긴 작업을 조심해야 한다.

지금 구현에서는 200 크기 밖에 안되어서 큰 상관은 없으나, 숫자 크기가 더 커진다면 조심해야 할 것이다.

 

마지막으로 스케줄러로 구현하려면 스케줄러 서버가 단일 서버여야 중복요청이 가지 않는다. 만약 여러 서버가 올라간다면 API화 시켜 Jenkins와 같은 도구로 스케줄링하는 방식도 고려할 수 있을 것이다.