1. 개요
토비님의 스프링부트 강의를 보고
자주 쓸만한게 있다면 커스텀 어노테이션을 만들어보자 생각이 들어서
간단하게 슬랙 메세지를 보내는 기능을 만들어보기로 했습니다.
이번 포스팅에서는 따로 개념적인 부분을 크게 언급하지 않고 어떤 흐름으로 작업했는지에 대한 내용이므로
주요 키워드는 Annotation, AOP, Async, DI 이니 따로 해당 개념을 어느정도 보시고 이 포스팅을 보시면 편하실거라 생각됩니다.
2. Slack Bot Part
이 기능에서는 코드 작업 외에 슬랙 메세지를 작성해줄 봇이 필요합니다.
그렇기에 먼저 Slack Bot 을 만드는 내용을 다루기에 이미 가지고 계시다면 아래 3번 부분으로 넘어가주세요.
a. Create App(Bot)
먼저 slack api 사이트에 접속하고
우측 상단에 [Your apps] 탭에서 [Create your first app] 을 클릭해줍니다.
(로그인이 안되어 있다면 로그인부터 진행해주세요!)
이미 만들어진 App이 있어서 안나온다면
[Manage your apps] 를 눌러나오는 첫화면에 Create App 버튼이 있으니 그 쪽에서 진행해주시면 됩니다.
(이미 있으신 분들은 활용하고 계실테니 자세한 설명은 생략하겠습니다)
페이지가 넘어가면 Create an app 이라는 모달창이 나올텐데,
[From scratch] 를 눌러주고
[App Name] 입력창에는 만들어줄 앱(봇)의 이름
[Work Space] 부분은 앱을 추가해줄 워크스페이스를 선택해주시고 [Create App] 버튼 클릭
다음 나오는 페이지인 App 세부 설정 페이지 왼쪽 탭을 보면
[Basic Infomation] 에서 [App features and functionality] 항목을 볼 수 있는데 [Bots] 부분이 비활성화 상태일겁니다.
해당 부분을 누르면 OAuth 권한 및 토큰 이야기가 나오는데
그 부분은 아래 권한과 토큰 부분을 진행하면 [Bots] 부분도 자동으로 활성화 되기에 우선 알아두고만 넘어가도록 하겠습니다.
b. 권한 설정 및 토큰 받기
권한 및 토큰은 왼쪽 [OAuth & Permissions] 탭에서 설정이 가능합니다.
우선 상단 쪽에는 당장 할게 없으니 스크롤을 내려보면 Bot 에 대한 권한을 설정하는 부분이 나옵니다.
쭉 내리다보면 [Scopes] 항목이 나오는데,
처음에는 [Bot Token Scopes] 부분이 비어있으므로
[Add an OAuth Scope] 버튼을 눌러 [channels:read], [chat:write] 권한을 추가하도록 하겠습니다.
(원하신다면 다른 권한들도 목록으로 보여주니 추가하셔도 됩니다.)
권한 설정이 되셨다면 다시 위로 스크롤 하면
[OAuth Tokens for Your Workspace] 항목에서 [Install to Workspace] 버튼이 활성화 된 것을 볼 수 있습니다.
버튼을 클릭하면
[Workspace 설치에 동의하는지]에 대한 페이지 이동이 되고 [Allow] 버튼을 누르면 [xoxb-] 로 시작하는 토큰을 발급 받게 됩니다.
발급 받은 토큰으로 애플리케이션과 Slack 간의 인증 과정을 담당하기 때문에 개발 부분에서 해당 토큰을 가져다 사용해야합니다.
해당 작업이 완료 되었다면 봇이 워크스페이스 Apps 부분에 자동으로 추가되는 모습을 볼 수 있습니다.
(저는 [Back App Boot] 으로 작업하였기에 앞으로 작업될 내용에 참고해주세요)
c. Invite Channel
다음으로 워크스페이스 내에 등록된 App(Bot)을 [메세지를 받을 채널] 내에 초대를 진행합니다.
초대는 다른 사람을 채널에 초대하는것과 같으므로
채널 채팅창에 [invite @{bot-name}] 으로 입력해도 초대가 됩니다.
3. Spring Boot Application Part
Slack SDK for Java 여기서 기능에 대한 정보를 얻을 수 있으니 참고!
a. build.gradle
우선 Slack SDK 에 대한 의존성 주입을 받아야합니다.
dependencies {
// Slack API
implementation "com.slack.api:slack-api-client:1.36.1"
// Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
// Configration Properties 용
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
주입 받은 라이브러리 중 SlackAPI, AOP, Configuration Processor 는 필수 나머지는 선택입니다.
b. application.yaml
slack:
token: ${SLACK_TOKEN} # ex, xoxb-~
아래 작업 할 SlackConfig 의 token에 바인딩 될 값을 application.yaml 파일에 작성해줍니다.
토큰은 앞서 Slack App을 만들때 생성된 토큰을 가져와주시면 됩니다.
c. SlackConfig
@Configuration
// Configuration Property File 을 읽어들이기 위한 작업
@ConfigurationProperties(prefix = "slack")
@Getter
@Setter
// Slack 라이브러리가 Import 되었는지
// Slack 라이브러리가 없으면 코드 자체가 실행이 안되니 굳이 필요없지만...
@ConditionalOnClass(name = "com.slack.api.Slack")
@Slf4j
public class SlackConfig {
private String token;
/**
* 토큰 값을 적용한 MethodsClient Bean 등록
* @return
*/
@Bean
@ConditionalOnMissingClass
public MethodsClient methodsClient() {
log.info("Slack Init!");
MethodsClient methodsClient = Slack.getInstance().methods(token);
return methodsClient;
}
}
SlackConfig 파일을 만들어주고
@ConfigurationProperties
Properties로 token을 받도록 했습니다.
(이 부분은 찾아보면 금방 이해하실 수 있을겁니다.)
@ConditionalOnClass
굳이 없어도 되는 부분이지만,
나중에 Slack SDK나 확장 SDK인 Bolt를 쓰게 되는 경우 선택적으로 통합할 수 있게 놔둔 부분이라 크게 신경쓰지 않으셔도 됩니다.
methodClient()
기본 토큰을 적용하고 메세지를 보내게끔 하기 위해서 MethodClient 를 Bean으로 등록하는 작업이 되었습니다.
d. SlackMessaging
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlackMessaging {
String channel() default "#back_sys_noti";
String title() default "Running";
String message() default "";
ColorType color() default ColorType.GREEN;
}
슬랙 메세지를 Annotation이 달린 메소드가 실행된 후에 보내도록 하기 위해서 @Target을 Method로 설정하였습니다.
channel()
default 값을 설정하므로 값을 넣지 않은 경우 세팅한 채널로 알람이 가도록 만들었습니다.
(채널의 경우 고정되는 경우도 많지 않을까 해서 Property 로 빼려했는데 Spring Bean 을 벗어나는 순간 사용하기 복잡해져서 추후 수정해보면서 적용해볼 예정입니다.)
title(), message(), color()
아래 Slack Message 형태를 만들때
Attach 부분에 타이틀, 메세지 내용, 컬러 등 설정이 가능합니다. 그 부분에 들어갈 파라메터입니다.
e. ColorType Enum
@Getter
public enum ColorType {
GREEN(Color.GREEN),
BLUE(Color.BLUE),
RED(Color.RED);
private Color color;
ColorType(Color color) {
this.color = color;
}
public String getHexCode() {
return "#"+Integer.toHexString(this.getColor().getRGB()).substring(2);
}
}
컬러부분을 빼면 굳이 만들 필요는 없지만 Default 값을 상수로 적용해야하기 때문에 만들어진 Enum 입니다.
java.awt.Color 를 파라메터로 가지며,
Slack Message 에서 Color 부분을
Hex(= HTML Color Code) 로 나타내야하기 때문에 getHexCode() 메소드를 만들어두었습니다.
f. SlackMessageDto
@Getter
@Setter
public class SlackMessageDto {
private String channel;
private String methodName;
private String title;
private String message;
private ColorType color;
@Builder
public SlackMessageDto(String channel, String methodName, String title, String message, ColorType color) {
this.channel = channel;
this.methodName = StringUtils.hasLength(methodName) ? methodName : "NO_NAME";
this.title = title;
this.message = message;
this.color = color;
}
/**
* ChatPostMessageRequest > Attachment > Field
* 내부 요소를 포함하고 있다.
* @return
*/
public Field generateSlackFiled() {
return Field.builder()
.title(this.title)
.value(this.message)
.valueShortEnough(false)
.build();
}
public ChatPostMessageRequest generatePostMessage() {
Attachment attachment = Attachment.builder()
.color(this.color.getHexCode())
.fields(List.of(generateSlackFiled()))
.build();
return ChatPostMessageRequest.builder()
.channel(this.channel)
.text(this.methodName)
.attachments(List.of(attachment))
.build();
}
/**
* SlackMessageDto by ProceedingJoinPoint
* TODO 실행한 Method 명이 필요없다면 SlackMessaging Annotation 에서 바로 매핑하도록 함
* @param proceedingJoinPoint
* @return
*/
public static SlackMessageDto toDto(ProceedingJoinPoint proceedingJoinPoint) {
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
SlackMessaging slackAnno = methodSignature.getMethod().getAnnotation(SlackMessaging.class);
String channel = slackAnno.channel();
String title = slackAnno.title();
String message = slackAnno.message();
ColorType color = slackAnno.color();
String methodName = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getName();
return SlackMessageDto.builder()
.channel(channel)
.methodName(methodName)
.title(title)
.message(message)
.color(color)
.build();
}
}
Message 폼을 정형화 하고 Slack에서 메세지를 보내는 폼과 매핑을 담당할 DTO 클래스를 만들어주었습니다.
ChatPostMessageRequest 라는 객체를 만들어 보내는데
Attach, Field 형태를 만들어주고 내용에 포함하도록 메소드 구성을 하였습니다.
ProceedJoinPoint 는 AOP에서 나오는 개념입니다.
toDto() 메소드에서 실행하고자 하는 Method 명을 추가적으로 가져오기 위해서 파라메터로 받았습니다.
(맘에 드는 구조가 아니라서 굳이 필요 없다면 Anntation -> Dto 구조로 바로 매핑되도록 하는게 더 나아보입니다.)
g. SlackService
@Service
@RequiredArgsConstructor
@Slf4j
public class SlackService {
private final MethodsClient methodsClient;
/**
* Send Message to Slack by SlackMessageDto Obj
*
* @param dto
* @throws SlackApiException
* @throws IOException
*/
@Async
public void sendSlackMessage(SlackMessageDto dto) throws SlackApiException, IOException {
// Generate ChatPostMessageRequest Object
ChatPostMessageRequest request = dto.generatePostMessage();
// send Message to Slack
methodsClient.chatPostMessage(request);
}
}
Dto 에서 매핑 부분을 담당하기에 Service Layer에는 methodsClient 를 주입 받아서 메세지를 보내는 부분 정도만 적용되어있지만 @Async 를 적용하여 비동기 처리를 진행하였습니다.
(Web API에 포함된 메소드인 경우, 동기 처리라면 슬랙 메세지도 보내져야 답변이 보내지기 때문에 비동기 처리를 넣어줬습니다.)
추후 비동기지만 Queue 에 담아서 순차처리하던가 Thread Pool을 제한해서 적용하면 애플리케이션 성능 상에 미치는 영향을 줄여주면 좋을 것 같습니다.
h. SlackAspect
@Aspect
@Order(1)
@Component
@RequiredArgsConstructor
@Slf4j
public class SlackAspect {
private final SlackService slackService;
/**
* AOP 작업
* SlackMessaging Annotation 이 적용된 Method 가 실행 될때,
* SlackService의 sendSlackMessage() 가 실행 되도록
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("@annotation(SlackMessaging)")
public Object processCustomAnnotation(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// Run Method
Object proceedReturnValue = proceedingJoinPoint.proceed();
// Slack Message Dto Builder
SlackMessageDto slackMessageDto = SlackMessageDto.toDto(proceedingJoinPoint);
// Send Message;
slackService.sendSlackMessage(slackMessageDto);
return proceedReturnValue;
}
}
AOP를 활용한 기능에 대한 정의 부분입니다.
@Around()
앞서 만든 @SlackMessaging 가 붙어 있는 경우, 정의된 메소드가 사용되도록 하는 부분입니다.
processCustomAnnotation()
프록시 역할을 할 메소드이며, proceedingJoinPoint.proceed() 부분이 @SlackMessaging Annotation이 붙은 메소드를 실행시키는 역할을 하고 메소드 실행이 끝난 후 sendSlackMessage() 메소드가 실행되도록 하였습니다.
i. main
@SpringBootApplication
@EnableAsync
@EnableAspectJAutoProxy
@Slf4j
public class CustomAnnotationApplication {
public static void main(String[] args) {
SpringApplication.run(CustomAnnotationApplication.class, args);
}
}
SpringApplication 이 실행되는 메인 쪽 클래스에 추가해줘야할 Annotation이 두 개가 있는데,
- AOP - @EnableAspectJAutoProxy (Aspect 를 인식하도록 함)
- Async - @EnableAsync (Async 기능이 동작 가능하도록 함)
이렇게 두 가지를 추가해주면 됩니다.
4. Test
a. TestController
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/slack")
@SlackMessaging(message = "슬랙 테스트를 하였다.", color = ColorType.BLUE)
public ResponseEntity sendSlackTest() {
return ResponseEntity.ok().body("send Slack message!");
}
}
간단하게 테스트용 컨트롤러를 만들고 @SlackMessaging 을 달아두었습니다.
(테스트용이니 이렇게 요청이 들어오면 메세지를 보내는 방식은 상용 서비스에는 맞지 않을 수 있습니다!)
b. Run
4. 마무리
기존 슬랙 서비스로 구현해서 코드 상에 메소드를 호출하는 경우가 많았는데 AOP와 커스텀 어노테이션을 활용해서 의존성을 낮추는 작업을 해보니 좀 더 Spring Boot 구조나 작동 방법에 대해서나 객체지향적으로 어떻게 구성하면 좋을지 고민하게 되는거 같습니다.
지금 만들어진 코드는 샘플정도이니 각자에 맞게 리팩토링해봐도 좋을듯합니다.
(마음에 안드는 부분이 꽤 보여서 개선작업을 해도 재밌을거 같습니다.)
5. GitHub
https://github.com/mk1-p/custom-annotation
GitHub - mk1-p/custom-annotation: SpringBoot Custom Annotations by mk1-p
SpringBoot Custom Annotations by mk1-p. Contribute to mk1-p/custom-annotation development by creating an account on GitHub.
github.com
6. 참고자료
Slack은 생산성 플랫폼입니다
Slack은 팀과 커뮤니케이션할 수 있는 새로운 방법입니다. 이메일보다 빠르고, 더 조직적이며, 훨씬 안전합니다.
slack.com
https://dev-jwblog.tistory.com/114
[SpringBoot] 스프링부트 + 슬랙(slack bot) 연동하기
예전에는 서비스를 운영하다 보면 직원들에게 알림(장애, 배치, 반영 등)을 알려주기 위해 메시지를 보내거나 하였지만, 최근에는 대부분 슬랙(Slack)이란 메신저를 사용한다. 대부분의 회사가 초
dev-jwblog.tistory.com
'DEV > Spring' 카테고리의 다른 글
[Spring] GlobalExceptionHandler 만들기 (with. Kotlin) (1) | 2024.11.09 |
---|---|
[JPA] QueryDSL 페이징 적용 (0) | 2023.11.11 |
[JPA] QueryDSL 사용자 정의 레포지토리 적용 (0) | 2023.11.06 |
[JPA] QueryDSL 동적 쿼리 적용 (0) | 2023.10.26 |
[JPA] QueryDSL DTO 반환 방법 (0) | 2023.10.23 |