Slack WebHook 활용하기 (Request Caching)

devty·2024년 6월 5일
0

SpringBoot

목록 보기
5/11

알고가야할 사항

  • 기본적인 Slack WebHook API Key 발급 관련해서는 다른 블로그들에서도 많이 나오니 넘어가도록 하겠다.
  • AOP 관련 내용들도 간단하니 넘어가도록 하겠다.
  • 그리고 다른 블로그들을 확인 했을 때, 거의 90% 이상이 Request에 대한 매핑은 없는 것을 확인할 수 있다.
  • 내가 생각하기에 에러가 발생했을시에 Request를 보는것이 중요하다고 생각한터라 Request를 어떻게 받아올지 고민하고 코드를 작성하였다.

Request를 캐싱한 코드

  • SlackNotificationAspect
    @Aspect
    @Component
    @RequiredArgsConstructor
    public class SlackNotificationAspect {
    
        private final SlackApi slackApi;
    
        @Around("@annotation(com.example.demo.global.annotation.SlackNotification)")
        public Object slackNotification(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            HttpServletRequest originalRequest = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            ContentCachingRequestWrapper request = originalRequest instanceof ContentCachingRequestWrapper
                    ? (ContentCachingRequestWrapper) originalRequest
                    : new ContentCachingRequestWrapper(originalRequest);
            String requestBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
    
            SlackAttachment slackAttachment = new SlackAttachment();
            slackAttachment.setFallback("Error");
            slackAttachment.setColor("danger");
            slackAttachment.setTitle("Error Detected");
            slackAttachment.setTitleLink(request.getContextPath());
            slackAttachment.setFields(Arrays.asList(
                    new SlackField().setTitle("Request URL").setValue(request.getRequestURL().toString()),
                    new SlackField().setTitle("Request Method").setValue(request.getMethod()),
                    new SlackField().setTitle("Request Time").setValue(new Date().toString()),
                    new SlackField().setTitle("Headers").setValue("Authorization: " + request.getHeader("Authorization")),
                    new SlackField().setTitle("Request Body").setValue(requestBody),
                    new SlackField().setTitle("Request IP").setValue(request.getRemoteAddr()),
                    new SlackField().setTitle("Request User-Agent").setValue(request.getHeader("User-Agent"))
            ));
    
            SlackMessage slackMessage = new SlackMessage();
            slackMessage.setAttachments(Collections.singletonList(slackAttachment));
            slackMessage.setIcon(":ghost:");
            slackMessage.setText("Error Detected");
            slackMessage.setUsername("DutyPark");
            slackApi.call(slackMessage);
    
            return proceedingJoinPoint.proceed();
        }
    }
    • 나의 프로젝트 관심에서 생각해봤을 때, Request를 받아오는 것이 가장 중요하다고 생각했다.
    • 그래서 간단하게 Request 정도는 어느 부분에서나 받아올 수 있다고 생각했었다.
    • 하지만, Request는 HTTP 요청 본문은 스트림으로 처리되어 한 번 읽으면 소비되어 다시 읽을 수 없었다.
    • 이러한 특성 때문에 로깅, 보안 검사, 데이터 처리 및 변환과 같은 상황에서 요청 본문을 여러 번 읽어야 할 필요가 있을 경우 본문을 캐싱해야 한다.
    • 나는 웹 프레임워크는 ContentCachingRequestWrapper와 같은 래퍼를 사용하여 요청 본문을 내부 버퍼에 저장, 요청 본문에 대한 읽기를 가능하게 하였다.
    • ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();RequestContextHolder 클래스를 이용하여 서블릿 필터나 인터셉터에서 HTTP 요청이 시작될 때 HTTP 요청과 관련된 정보(HttpServletRequest, HttpServletResponse)를 ServletRequestAttributes에 저장한다.
    • ServletRequestAttributes 은 Spring Context 안에 들어왔을 때 관리되는 객체, Spring Context 에 들어오기 전이라면 HttpServletRequest, HttpServletResponse 로 관리가 된다.
    • 나머지 부분은 기본적인 Slack WebHook을 보내기 위한 문법들이므로 넘어가도록 가도록하겠다.
  • CachingRequestBodyFilter
    @Component
    public class CachingRequestBodyFilter extends OncePerRequestFilter {
    
    	@Override
    	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
    		throws ServletException, IOException {
    		if (request instanceof ContentCachingRequestWrapper) {
    			filterChain.doFilter(request, response);
    		} else {
    			ContentCachingRequestWrapper cachingWrapper = new ContentCachingRequestWrapper(request);
    			filterChain.doFilter(cachingWrapper, response);
    		}
    	}
    }
    • OncePerRequestFilter를 상속 받아 필터가 각 요청에 대해 한번만 실행되도록 보장한다.
    • filterChain.doFilter를 사용하여 다음 필터나 디스패치 서블릿으로 넘겨준다.
    • 나의 경우에는 필터에서 캐싱을 해주었는데, 필터 말고도 인터셉터, AOP에서도 캐싱이 가능하다.
    • 하지만, 필터에서 캐싱한 이유는 다음과 같다.
      • 필터가 요청 처리 체인에서 가장 앞에 위치하기에 요청과 관련된 모든 데이터를 날 것 그대로 챙겨올 수 있기 때문이었다.

결과

  • Slack 채팅 결과
    • Compact하게 우리가 간단하게 필요한 정보들만 가져온 것을 확인할 수 있다.
    • 거기에 Request로 보낸 요청들도 확인할 수 있다.
  • 추가적으로 걸린 시간에 대해서도 확인해보겠다.
    • 응답시간이 0.951초가 나온 것을 확인할 수 있다.
    • 지금 우리는 비즈니스 로직이 없는데도 이정도로 느리다면 문제가 있을 것이다.
    • 그래서 Slack WebHook을 비즈니스 로직과 별개로 두어 처리를 해보겠다. → 비동기로 처리

비동기 처리를 한 코드

  • SlackNotificationAspect
    @Aspect
    @Component
    @RequiredArgsConstructor
    public class SlackNotificationAspect {
    
        private final SlackApi slackApi;
        private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
        @Around("@annotation(com.example.demo.global.annotation.SlackNotification)")
        public Object slackNotification(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            HttpServletRequest originalRequest = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            ContentCachingRequestWrapper request = originalRequest instanceof ContentCachingRequestWrapper
                    ? (ContentCachingRequestWrapper) originalRequest
                    : new ContentCachingRequestWrapper(originalRequest);
            String requestBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8);
    
            SlackAttachment slackAttachment = new SlackAttachment();
            slackAttachment.setFallback("Error");
            slackAttachment.setColor("danger");
            slackAttachment.setTitle("Error Detected");
            slackAttachment.setTitleLink(request.getContextPath());
            slackAttachment.setFields(Arrays.asList(
                    new SlackField().setTitle("Request URL").setValue(request.getRequestURL().toString()),
                    new SlackField().setTitle("Request Method").setValue(request.getMethod()),
                    new SlackField().setTitle("Request Time").setValue(new Date().toString()),
                    new SlackField().setTitle("Headers").setValue("Authorization: " + request.getHeader("Authorization")),
                    new SlackField().setTitle("Request Body").setValue(requestBody),
                    new SlackField().setTitle("Request IP").setValue(request.getRemoteAddr()),
                    new SlackField().setTitle("Request User-Agent").setValue(request.getHeader("User-Agent"))
            ));
    
            SlackMessage slackMessage = new SlackMessage();
            slackMessage.setAttachments(Collections.singletonList(slackAttachment));
            slackMessage.setIcon(":ghost:");
            slackMessage.setText("Error Detected");
            slackMessage.setUsername("DutyPark");
    
            threadPoolTaskExecutor.execute(() -> slackApi.call(slackMessage));
            // slackApi.call(slackMessage);
    
            return proceedingJoinPoint.proceed();
        }
    }
    • 지금 추가 된 부분만 확인한다면 다음과 같다.
    • ThreadPoolTaskExecutor → 스레드 풀 기반으로 동시에 여러 태스크를 비동기적으로 처리할 수 있도록 스레드를 관리함.
    • execute() → 주어진 Runnable 객체(여기서는 람다 표현식으로 둠)를 비동기적으로 실행하는 역할을 함.
  • ThreadPoolConfig
    @EnableAsync
    @Configuration
    public class ThreadPoolConfig {
    
    	@Bean
    	public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
    		ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    		threadPoolTaskExecutor.setMaxPoolSize(5); //최대 스레드 수
    		threadPoolTaskExecutor.setCorePoolSize(5); //기본 스레드 수
    		threadPoolTaskExecutor.initialize();
    		return threadPoolTaskExecutor;
    	}
    }
    • 간단한 스레드 풀 설정을 해주었다.

결과

  • 비동기 처리 후 걸린 시간
    • 기존 0.9초에서 0.16초로 줄은 걸을 파악할 수 있다.
    • 우리는 이로서 비즈니스 로직과 별개로 Slack webHook을 처리할 수 있게 되었다.
profile
지나가는 개발자

0개의 댓글