[스프링부트] 디스코드 웹훅 (2) - API call 알림 (Interceptor, FeignClient)
지난 시간에는 서버 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 방식
- 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 캐싱과 관련 있는 이유
- API 요청 결과를 상태로 관리:
- API로 데이터를 받아오면 이를 앱의 상태로 저장합니다.
- 상태 관리 도구를 사용해, 상태가 업데이트될 때 관련된 위젯만 리빌딩합니다.
- 캐싱된 데이터를 상태로 유지하면 리빌딩할 때 빠르게 데이터를 제공할 수 있습니다.
- 효율적인 UI 업데이트:
- 캐싱된 데이터를 활용하면, 위젯 리빌딩 시 네트워크 요청 없이 UI를 빠르게 업데이트할 수 있습니다.
- 예: 사용자가 화면을 이동했다가 다시 돌아왔을 때, API를 다시 호출하지 않고 캐싱된 데이터를 사용하여 위젯을 즉시 리빌딩합니다.
- 불필요한 리빌딩 방지:
- 상태 관리 도구는 상태가 바뀌었을 때만 필요한 위젯을 리빌딩하도록 돕습니다.
- 캐싱 덕분에 상태 변경이 줄어들면, 리빌딩도 줄어들어 성능이 향상됩니다.
오, 그러니까
웹 사이트의 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-로그인-예시로