이번 담당 기능에 슬랙 알림 기능 구현을 맡게 되었다.
슬랙 알림은 주요 메서드가 실행될 때 로그 같은 형식으로 알림을 보내는 용도이다.
바로 구현을 해보자
// slack api gradle
implementation 'com.slack.api:slack-api-client:1.27.2'
먼저 gradle에 추가해 준다.
제일 처음으로 생각한 방법은 이벤트를 사용하는 방식이었다.
1. 사용자가 요청을 해서 메서드를 실행시킨다.
2. 메서드에서 슬랙 알림을 보내는 이벤트를 발행한다.
3. 이벤트 처리에서 슬랙에 알림을 보낸다.
그럼 이벤트는 어떤 걸로 처리를 해야 할까? 여러 가지 이벤트 핸들링 방법이 있다. 예를 들어, RabbitMQ, 레디스 PubSub, 카프카 등등 하지만 우리의 프로젝트는 규모가 작은 프로젝트이고, 또한 비동기로 처리를 굳이 안 해도 성능 상 이슈가 없을 것 같아서 스프링에서 기본적으로 지원하는 ApplicationEvent를 사용하기로 했다. 추가적으로 설치하고 손댈 것 없이 간편하게 쓸 수 있다.
이를 구현하기 위해 먼저 슬랙 클라이언트를 만들었다.
@Slf4j
@Component
public class SlackClient {
@Value("${DEFAULT_SLACK_WEBHOOK_URL}")
private String slackAlertWebhookUrl;
private final ObjectMapper mapper = new ObjectMapper();
public void notify(String webhookUrl, String payload) {
try {
Slack slack = Slack.getInstance();
Map<String, String> map = new HashMap<>();
map.put("text", payload);
WebhookResponse response = slack.send(webhookUrl,
mapper.writeValueAsString(map));
SlackResponse slackResponse = new SlackResponse(response.getCode(),
response.getMessage(),
response.getBody());
String json = mapper.writeValueAsString(slackResponse);
log.error("[ slack notify result ] {}", json);
} catch (Exception e) {
log.error("Slack Message Send Failed!!");
}
}
public void notify(String payload) {
try {
Slack slack = Slack.getInstance();
Map<String, String> map = new HashMap<>();
map.put("text", payload);
WebhookResponse response = slack.send(slackAlertWebhookUrl,
mapper.writeValueAsString(map));
SlackResponse slackResponse = new SlackResponse(response.getCode(),
response.getMessage(),
response.getBody());
String json = mapper.writeValueAsString(slackResponse);
log.error("[ slack notify result ] {}", json);
} catch (Exception e) {
log.error("Slack Message Send Failed!!");
}
}
private record SlackResponse(int code, String message, String body){ }
}
슬랙에서만 쓸 반환 DTO를 내부에 네스티드 클래스로 만들 두었다. 슬랙 사용법은 되게 간단했다. Slack.getInstance()로 가져와서 사용하면 됐다.
그다음 Event클래스를 만들었다.
public class SlackNotifyEvent extends ApplicationEvent {
public SlackNotifyEvent(String message) {
super(message);
}
public String getMessage() {
return (String) super.getSource();
}
}
다음은 Slack이벤트를 구독하는 EventListener클래스를 만들었다.
@Component
@RequiredArgsConstructor
public class SlackNotifyEventListener {
private final SlackClient slackClient;
/**
* SlackNotifyEvent가 발행되면 실행되는 메서드
* @param event
*/
@EventListener
public void onSlackNotify(SlackNotifyEvent event) {
String message = event.getMessage();
slackClient.notify(message);
}
}
스프링에서 기본 제공이라서 사용법도 간단했다.
@RequiredArgsConstructor
class Test {
// 주입을 받는다.
private final ApplicationEventPublisher publisher;
public void logic() {
publisher.publishEvent(new SlackNotifyEvent("message"));
}
}
ApplicationEventPublisher를 주입받아서 publishEvent에 SlackNotifyEvent를 발행시켜 주면 완성이다!
잘 오는 것을 확인했다.
기본적인 구현을 마친 후 좀 더 개선할 수 없을까?? 해서 생각해 낸 것은 카프카나 RabbitMQ나 카프카를 사용하면 어노테이션으로 처리해 주는 것을 공부했었다. 이에 영감을 받아서 AOP로 @SlackAlert 어노테이션을 달아주면 슬랙 알림을 보낼 수 있게 만들 것이다.
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SlackAlert {
String onSuccess() default "";
String onFailure() default "";
String hookUrl() default "";
}
hookUrl은 Default hook url 말고 다른 것을 사용할 때 값을 넣어주는 용도이고 onSuccess랑 onFailure은 메서드가 성공, 실패했을 때의 메시지를 적어주는 용도이다.
AOP는 Around를 이용해서 메서드가 성공했을 때 실패했을 때 메시지를 다르게 해 주었다.
@Component
@Aspect
@RequiredArgsConstructor
public class SlackAlertAop {
@Value("${DEFAULT_SLACK_WEBHOOK_URL}")
private String defaultSlackWebhookUrl; // 여기에서 설정 값 주입
private final SlackClient slackClient;
@Pointcut("@annotation(com.trelloproject.common.annotations.SlackAlert)")
public void annotaionPc(){}
@Around("annotaionPc()")
public Object slackAlertAround(ProceedingJoinPoint joinPoint) throws Throwable {
boolean isExceptionOccurred = false;
long startTime = System.currentTimeMillis();
Exception exception = null;
try {
return joinPoint.proceed();
} catch (Exception e) {
isExceptionOccurred = true;
exception = e;
throw e;
} finally {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
SlackAlert annotation = method.getAnnotation(SlackAlert.class);
String webhookUrl = StringUtils.hasText(annotation.hookUrl()) ? annotation.hookUrl() : defaultSlackWebhookUrl;
String msg = isExceptionOccurred
? "["+ exception.getClass().getSimpleName() + "] " + (StringUtils.hasText(annotation.onFailure()) ? annotation.onFailure() : exception.getMessage())
: (StringUtils.hasText(annotation.onSuccess())? annotation.onSuccess() : "Success Alert");
long executionTime = System.currentTimeMillis() - startTime;
// Auth user 가져오기
AuthUser auth = null;
JwtAuthenticationToken authentication = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
auth = authentication == null ? null : (AuthUser) authentication.getPrincipal();
String authInfo = auth == null
? "NO AUTH"
: "{ "+MessageFormat.format("Id: {0} || Email: {1} || ROLE: {2}" ,auth.getUserId(), auth.getEmail(), Arrays.toString(auth.getAuthorities().toArray())) + " }";
String payload = MessageFormat.format(
"\n"+"""
```
🔔 [Slack Alert] 🔔
*────────────────────────────────────────────────────────*
👤 Auth: {0}
📌 Method: {1}
✉️ Message: {2}
{6} Result: {3}
⏳ ExecutionTime: {4}ms
🕒 Timestamp: {5}
*────────────────────────────────────────────────────────*```
""" +"\n",
authInfo,
joinPoint.getSignature().toShortString(),
msg,
isExceptionOccurred ? "FAILED" : "SUCCESS",
executionTime,
LocalDateTime.now().toString(),
isExceptionOccurred ? "🔴" : "🟢"
);
slackClient.notify(webhookUrl, payload);
}
}
}
이제 알림을 받을 메서드 위에 @SlackAlert를 달아주면 정상적으로 동작한다.
로그인을 한 유저가 어떤 메서드를 실행할 때 Auth도 제대로 나오는 것을 볼 수 있다.
AOP를 한 개 더 만들었는데, 바로 Exception이 났을 때에만 알림이 오는 기능이다.
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface SlackExceptionAlert {
String value() default "";
}
package com.trelloproject.common.aop;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.trelloproject.common.annotations.SlackAlert;
import com.trelloproject.common.annotations.SlackExceptionAlert;
import com.trelloproject.domain.notification.SlackClient;
import com.trelloproject.security.AuthUser;
import com.trelloproject.security.JwtAuthenticationToken;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Slf4j
@Component
@Aspect
@RequiredArgsConstructor
public class SlackExceptionAlertAop {
@Value("${DEFAULT_SLACK_WEBHOOK_URL}")
private String defaultSlackWebhookUrl; // 여기에서 설정 값 주입
private final SlackClient slackClient;
@Pointcut("@annotation(com.trelloproject.common.annotations.SlackExceptionAlert)")
public void annotaionPc(){}
@AfterThrowing(value = "annotaionPc()", throwing = "e")
public void slackAlert(JoinPoint joinPoint, RuntimeException e) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
SlackExceptionAlert annotation = method.getAnnotation(SlackExceptionAlert.class);
String webhookUrl = annotation != null && StringUtils.hasText(annotation.value()) ? annotation.value() : defaultSlackWebhookUrl;
// Auth user 가져오기
AuthUser auth = null;
JwtAuthenticationToken authentication = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
auth = authentication == null ? null : (AuthUser) authentication.getPrincipal();
String authInfo = auth == null
? "NO AUTH"
: "{ "+MessageFormat.format("Id: {0} || Email: {1} || ROLE: {2}" ,auth.getUserId(), auth.getEmail(), Arrays.toString(auth.getAuthorities().toArray())) + " }";
String message = MessageFormat.format(
"\n"+"""
```
🚨 [Exception Alert]* 🚨
*────────────────────────────────────────────────────────*
👤 Auth: {0}
📌 Service: {1}
⚠️ Exception: {2}
💥 Message: {3}
🕒 Timestamp: {4}
*────────────────────────────────────────────────────────*```
""" +"\n",
authInfo,
joinPoint.getSignature().toShortString(),
e.getClass().getSimpleName(),
e.getMessage(),
LocalDateTime.now().toString()
);
slackClient.notify(webhookUrl,message);
}
}
이번에는 Around 말고 AfterThrowing을 사용해서 구현했다.
잘 나오는 것을 볼 수 있다.
'부트캠프 > Dev' 카테고리의 다른 글
통합 검색 구현 (0) | 2024.11.25 |
---|---|
최종 프로젝트 중간점검 (0) | 2024.11.04 |
최종 전.. 팀 프로젝트 시작 (1) | 2024.10.15 |
스프링으로 RabbitMQ를 사용해보자 (1) | 2024.09.27 |
Rabbit Mq (3) | 2024.09.27 |