백엔드

[스프링부트] 디스코드 웹훅 (2) - API call 알림 (Interceptor, FeignClient)

KyuminKim 2025. 1. 10. 14:45

지난 시간에는 서버 500 Internal Server Error를 디스코드 서버로 전송해줬다.

 

이번 시간에는 백엔드 API call을 디스코드로 전송해보자!


⭐️ 배경

🙋‍♀️(프론트엔드) 흠... 한 화면(페이지) 접속 시, 몇 개의 API가 호출되는지 궁금해요

🙋‍♀️(프론트엔드) 백엔드 분들, API 로그 확인해주실 수 있을까요?

 

🙋‍♀️(백엔드) 물론이죠! 잠시만 기다려주세요

 

백엔드 동료의 빠른 조치 덕분에,

API call 시 로그를 콘솔을 통해 확인할 수 있었다.

 

 

✅ 동료의 조치 방법

기존 스프링부트 실행 시, API call 발생 시 log가 발생하지 않는다.

Spring Intercept를 이용해 API Call 발생 시, HTTP 요청을 가로채 log로 남긴다

 

 

✅ 나의 발전

🙋‍♀️ (프론트엔드) 또 요청드려도 되죠,,,? 🥹

🙋‍♀️ (백엔드) 앗, 디스코드로 만들어 드릴게요!

 

그렇다.

디스코드에 API Call 발생 시 알림을 덧붙이는 작업을 해보도록 하자!


⭐️고민

동료가 만든 Spring Interceptor 방식을 살릴까? 말까? 

를 고민하기에 앞서, 

 

어떻게 해야 discord로 전송하는 것이 효율적인 방식인지를 고민해보기로 했다.

 

✅ 디스코드에 데이터를 보내는 방식 : Interceptor vs Filter

찾아보니, 보통 이 두 가지 방법으로 웹 서버 ➔ 디스코드로 데이터(알림)를 전송했다.

 

둘 다 비슷한 점은 HTTP 요청을 가로채서 ➔ 디스코드로 전송한다는 점이고,

다른 점은 언제 HTTP 요청을 가로채는지이다.

 

https://goddaehee.tistory.com/154

이 블로그에 따르면,

Filter ➔ Interceptor AOP ➔ Interceptor ➔ Filter 순서로 

사용자의 API 요청이 들어오고 처리된다고 한다.

Filter, Interceptor, AOP의 흐름 ❘ https://goddaehee.tistory.com/154

 

Filter 방식

- Spring MVC에서 관리하는 영역이 아님!

- 웹 컨테이너에서 동작

- request, response 조작 가능

 

Interceptor 방식

- Filter 이후, Spring MVC에서 관리하는 영역에 속함

- 스프링 컨텍스트에서 동작

- 인터셉터가 등록되어 있다면, Dispatcher Servlet에서 인터셉터를 순서대로 거치고 그 후 controller가 실행됨

- request, response 조작 불가능

 

두 방식을 포함해 Filter, Interceptor, AOP의 특징을 더 알아보기보다는

이 블로그에서는 

그래서 어떤 방식을 선택할지 고민해보자!

 

✅ 그래서 어떤 방식을 선택할까?

Interceptor 방식을 선택하려고 한다!

 

그런 생각을 하게 된 여정은 

 

1. request, response를 가공하지 않고, 요청이 들어올 때 '요청 들어왔어요' 라는 알림을 주고 싶다

Filter, Interceptor 방식 상관 x

 

2. 프론트엔드 분들을 위한 백엔드 API call 알림이므로, controller로 가는 요청에 대해 다루고 싶다

➔ Filter, Interceptor 방식 상관 x

    (만일, controller 를 제외한 모든 HTTP 요청을 로깅을 위함이었다면 Filter가 더 적합했을 것)

 

그렇다면 Filter, Interceptor 방식이 크게 상관이 없으므로,

이미 팀원이 구현해놓은 Interceptor 방식은 살리고,

어떻게 디스코드 서버로 전송할지를 덧붙이기로 결정했다.

 

✅ 디스코드 서버로 어떻게 전송할까? : FeignClient vs RestTemplate vs WebClient

서비스(서버) 간 HTTP 통신에 있어서

주로 feign client 방식, Rest template 방식, WebClient 방식을 사용하는 것을 찾을 수 있었다.

(MSA 구조에서의 마이크로 서비스간 통신에서도 이 방식을 사용한다고 한다)

 

FiegnClient

Netflix에서 개발한, Spring Cloud Framework에서 제공하는 비동기식 클라이언트 라이브러리

 

RestTemplate

Spring Framework(3~)에서 제공하는 동기식 클라이언트 라이브러리 

 

WebClient

Spring Framework(5~)에서 제공하는 비동기식/함수형 클라이언트 라이브러리

 

이중에서 FeignClient를 선택했다!

 

성능 이슈를 위한 비동기 방식 + interface를 작성하고 annotation을 선언하기만 하면 되는 간편함

으로 인해 손쉽게 구현할 수 있을 것 같아 선택했다!

(자세한 내용은 이 블로그를 참고해보라!)


⭐️ 구현 과정

1. Interceptor로 API call 가로채기 (이미 구현완료!)

2. FeignClient 만들어, API call 발생 시 디스코드로 전송

 

✅ 1. interceptor로 API call 가로채기

팀원이 이미 구현해놨지만, 코드만 살펴보자!

 

1. WebConfig.java 추가

@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final ObjectProvider<ApiLoggingInterceptor> apiLoggingInterceptorProvider;

    public WebConfig(ObjectProvider<ApiLoggingInterceptor> apiLoggingInterceptorProvider) {
        this.apiLoggingInterceptorProvider = apiLoggingInterceptorProvider;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        ApiLoggingInterceptor apiLoggingInterceptor = apiLoggingInterceptorProvider.getIfAvailable();
        if (apiLoggingInterceptor != null) {
            registry.addInterceptor(apiLoggingInterceptor)
                    .addPathPatterns("/api/**");
        }
    }
}

/api/** 경로로 들어오는 모든 요청에 대해 ApiLoggingInterceptor를 적용하도록 인터셉터를 등록시키자

 

 

2. ApiLoggingInterceptor.java 추가

@RequiredArgsConstructor
@Component
public class ApiLoggingInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(ApiLoggingInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String logMessage = String.format("API Called: [%s %s]", method, uri);
        logger.info(logMessage);
        return true;
    }
}

/api/** 경로로 요청이 들어오면 preHandle이 실행되는데,

preHandle에서는 요청 메소드, URI를 로깅한다

 

 

✅ 2. FeignClient 만들어, API call 발생 시 디스코드로 전송

 

1. 디스코드 채널 및 웹훅 생성

지난시간과 똑같이 디스코드 채널, 웹훅을 만들어주자!

 

 

2. application.yml 변경 (디스코드 웹훅 추가)

logging:
  discord-api-call:
    webhook-url: (디스코드 웹훅 URL)
    name: discord-feign-client

1번에서 생성한 디스코드 웹훅 주소를 넣어준다 !

 

 

3. DiscordFiegnClient.java 인터페이스 생성

@FeignClient(name = "${logging.discord-api-call.name}", url = "${logging.discord-api-call.webhook-url}")
public interface DiscordFeignClient {
    @PostMapping
    void sendMessage(@RequestBody DiscordMessage discordMessage);
}

디스코드 웹훅으로 메시지를 전송하는 코드를 생성한다!

 

 

4. DiscordMessage.java DTO 만들기 

public record DiscordMessage (
    String content
){
    public static DiscordMessage createDiscordMessage(String content) {
        return new DiscordMessage(content);
    }
}

주의할 점은, 메시지 필드를 content라고 해야 한다는 점이다 !

 

만일 content가 아닌 값으로 필드 명을 선택하면

feign.FeignException$BadRequest: [400 Bad Request] during [POST] to [디스코드웹훅주소] [DiscordFeignClient#sendMessage(DiscordMessage)]: [{"message": "Cannot send an empty message", "code": 50006}]

다음과 같은 400 Bad Request : "Cannot send an empty message" 가 뜰 것이다..... (경험담)

 

 

5. DiscordMessageProvider.java 만들기

@RequiredArgsConstructor
@Component
public class DiscordMessageProvider {
    private final DiscordFeignClient discordFeignClient;

    public void sendMessage(String message) {
        DiscordMessage discordMessage = createDiscordMessage(message);
        sendMessageToDiscord(discordMessage);
    }

    private void sendMessageToDiscord(DiscordMessage discordMessage) {
        discordFeignClient.sendMessage(discordMessage);
    }
}

sendMessage: String 형태의 메시지를 (로그 데이터) ➔ discordMessage DTO로 변환하고

sendMessageToDiscord: 실제로 디스코드로 전송하는 코드를 작성한다

 

 

6. 기존 인터셉터 코드(ApiLoggingInterceptor.java) 변경 (Interceptor - feign client 연결)

@RequiredArgsConstructor
@Component
public class ApiLoggingInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(ApiLoggingInterceptor.class);
    private final DiscordMessageProvider discordMessageProvider;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String logMessage = String.format("API Called: [%s %s]", method, uri);
        logger.info(logMessage);

        // discord trigger
        discordMessageProvider.sendMessage(logMessage);
        return true;
    }
}

 

필드로 discordMessageProvider를 추가하고,

Intercept한 API method과 URI를 디스코드로 전송한다 !


⭐️ 결과

디스코드로 API call이 잘 오는 것을 확인할 수 있다 ☺️


⭐️ 여담

'프론트엔드에서 이 정보가 왜 궁금하지?' 라는 생각부터 들었다.

 

이유를 여쭤보니,

프론트엔드에 적용한 API 캐싱이 제대로 동작하는지 테스트하기 위함이라고 하셨다.

(앱 프론트엔드에서 API call 수를 줄여 더 나은 UX를 만들기 위함이라고 한다)

 

동시에

플러터 위젯리빌딩 / 상태 관리도 언급하셨다

 

gpt를 통해 이 개념이 API 캐싱과 무슨 연관이 있는지 찾아보았다.

 

(gpt) 플러터 위젯 리빌딩과 상태 관리가 API 캐싱과 관련 있는 이유

  1. API 요청 결과를 상태로 관리:
    • API로 데이터를 받아오면 이를 앱의 상태로 저장합니다.
    • 상태 관리 도구를 사용해, 상태가 업데이트될 때 관련된 위젯만 리빌딩합니다.
    • 캐싱된 데이터를 상태로 유지하면 리빌딩할 때 빠르게 데이터를 제공할 수 있습니다.
  2. 효율적인 UI 업데이트:
    • 캐싱된 데이터를 활용하면, 위젯 리빌딩 시 네트워크 요청 없이 UI를 빠르게 업데이트할 수 있습니다.
    • 예: 사용자가 화면을 이동했다가 다시 돌아왔을 때, API를 다시 호출하지 않고 캐싱된 데이터를 사용하여 위젯을 즉시 리빌딩합니다.
  3. 불필요한 리빌딩 방지:
    • 상태 관리 도구는 상태가 바뀌었을 때만 필요한 위젯을 리빌딩하도록 돕습니다.
    • 캐싱 덕분에 상태 변경이 줄어들면, 리빌딩도 줄어들어 성능이 향상됩니다.

오, 그러니까 

웹 사이트의 UI는 상태 관리를 통해 각 위젯이 항상 새로운 상태임을 보장하고,

특정 위젯의 상태가 바뀔 때에만 해당 위젯에 대해 새 상태를 요청하도록 하는 것이구나!

(신기하다)

프론트엔드, 특히 웹은 잘 몰랐는데 이번 기회에 알게 되어 매우 흥미롭다.

 

백엔드에서도 이번 경험으로 화면당 API call 수를 확인할 수 있었는데,

한 화면에서 특히 API call 수가 많은 것 같으니 새로운 API를 만드는 것을 고려해야겠다.


⭐️ 참고자료

filter, intercept 비교 (*이용사례, 메소드 종류 포함) https://dev-coco.tistory.com/173

 

filter, interceptor, AOP 비교와 흐름 https://goddaehee.tistory.com/154

 

RestTemplate vs FeignClient (*특징, 비교 포함) https://velog.io/@choiyunh/MSA-서비스간-통신시-RestTemplate-vs-FeignClient

 

RestTemplate vs FeignClient vs WebClient https://velog.io/@wlsgur1533/RestTemplate-WebClient-FeignClient-를-비교-OAuth-로그인-예시로