본문 바로가기
프로젝트/FitTrip

[트러블슈팅] API Gateway OpenFeign 사용 오류

by 진꿈청 2024. 6. 25.

🐞 버그 설명

상황

 

API Gateway는 다른 서비스들과 마찬가지로 서비스 디스커버리에 Eureka Client로 등록되어있습니다.
따라서, 사용자의 로그아웃 여부를 처리하는 과정에 있어 다른 기타 서비스와 마찬가지로 OpenFeign을 사용하려고 하였지만,
해당 부분에서 버그가 발생하였습니다.

발생한 버그

block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3

 

위의 버그는 아래 코드의 doNotLogout에서 발생했습니다.
doNotLogout은 OpenFeign을 활용해 유저 서비스에 유저의 로그아웃 여부를 확인하는 메서드입니다.

 

doNotLogout을 활용하는 GatewayFilter 코드

try {
    if (StringUtils.hasText(accessToken) 
    && jwtTokenProvider.validateToken(accessToken)
    && doNotLogout(accessToken)) 
    {
        return chain.filter(exchange); // Token is valid, continue to the next filter
    }
}

 

doNotLogout 메소드

private boolean doNotLogout(String accessToken) {
    return authClient.isValidToken(accessToken).getData().equals("true");
}

 

AuthClient

@FeignClient(name = "user-service", configuration = FeignConfiguration.class)
@Qualifier("AuthFeignClient")
public interface AuthFeignClient extends AuthClient{
    @Override
    @PostMapping("/isLogin")
    DataResponseDto isValidToken(String accessToken);
}

 

위와 같은 오류는 처음 만나보게 되어 정보를 찾아본 결과,
해당 오류는 OpenFeign이 기본적으로 동기 방식으로 작동되기 때문에 발생한 오류로 확인이 되었습니다.

 

Spring Cloud Gateway 애플리케이션을 구동하게 되면 기존의 임베디드 톰켓 기반의 Spring Boot Web 애플리케이션과는
다른 Netty 기반의 비동기 통신을 지원하는 형태의 웹 애플리케이션으로 실행이 됩니다.

 

따라서, Netty의 비동기 방식에 대해 동기적인 OpenFeign의 요청으로 버그가 생기게 되었습니다.

 

해결 방안

 

OpenFeign 작업시 CompletableFuture라는 반환 타입을 사용하면 비동기 처리가 가능하다고 합니다.
하지만, 저는 WebClient를 사용하기로 하였는데요. 그 이유는, Spring WebFlux 환경에서 WebClient를 사용하여 비동기적이고,
논블로킹 방식으로 외부 서비스와 통신하는 것이 리액티브 프로그래밍 모델에 적합하다고 합니다.

 

따라서, FitTrip API Gateway 기존의 OpenFeign 코드를 아래와 같은 WebClient 코드로 수정하였습니다.

 

OpenFeign -> WebClient

@Autowired
private WebClient.Builder webClientBuilder;

private Mono<Boolean> doNotLogout(String accessToken) {
    return webClientBuilder.build().post()
            .uri("http://user-service/isLogin")
            .body(BodyInserters.fromValue(accessToken))
            .retrieve()
            .bodyToMono(DataResponseDto.class)
            .map(response -> "true".equals(response.getData()));
}

하지만, 이 방법도 버그가 발생하였습니다..

API Gateway - AccessToken validation error: Failed to resolve 'user-service' [A(1)] after 4 queries

 

위 오류는 API Gateway가 'user-service'라는 서비스를 찾는 과정에서 DNS 조회 실패가 발생하였다는 것을 의미합니다.

 

즉, user-service를 호출할 때 Spring Cloud Gateway와 함께 사용되는 서비스 발견 메커니즘을 통해 자동으로 찾아야하는
서비스 이름을 직접 DNS 이름으로 해석하려고 하기 때문에 발생한 오류라고 볼 수 있습니다.

 

따라서, uri를 지정하는 부분을 API Gateway의 application.yml에 routes에 지정해준 것과 같이
"lb://USER-SERVICE/isLogin"로 설정해주었습니다.

 

application.yml

      routes:
        - id: user-service
          uri: lb://USER-SERVICE 
          predicates:
            - Path=/api/user/** 
          filters:
            - RewritePath=/api/user/?(?<segment>.*), /$\{segment}

 

 

 

최종적인 OpenFeign -> WebClient 변환 코드는 다음과 같습니다.

 

WebClient 최종 코드

    private Mono<Boolean> doNotLogout(String accessToken, ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        // accessToken을 JSON 객체로 변환
        String requestBody = accessToken;
        return webClientBuilder.build().post()
                .uri("lb://USER-SERVICE/isLogin")
                .headers(httpHeaders -> httpHeaders.addAll(headers))
                .contentType(MediaType.APPLICATION_JSON) // JSON 컨텐츠 타입 명시
                .body(BodyInserters.fromValue(requestBody)) // JSON 객체로 변환된 본문 사용
                .retrieve()
                .bodyToMono(DataResponseDto.class)
                .map(response -> Boolean.TRUE.equals(response.getData()));
    }

위처럼, WebClient를 사용했을 때 로그아웃 확인 여부를 유저 서비스에게 묻는 것이 정상적으로 작동하는 것을 확인할 수 있었습니다.

 

 

참고
https://velog.io/@akfls221/%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-NonBlocking-Blocking%EA%B3%BC-%EC%9B%B9-%EC%84%9C%EB%B2%84%EC%9D%98-%EA%B4%80%EA%B3%84

 

동기/비동기 & NonBlocking Blocking과 Spring의 관계

동기 / 비동기 & NonBlocking/Blocking 과 Srping의 관계를 알아보자

velog.io

 

https://ykh6242.tistory.com/entry/Spring-Cloud-Gateway%EA%B0%80-netty-%EA%B8%B0%EB%B0%98-reactive-web-application%EC%9C%BC%EB%A1%9C-%EA%B5%AC%EB%8F%99%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

Spring Cloud Gateway가 netty 기반 reactive web application으로 구동되는 이유

개요 Spring Cloud Gateway 애플리케이션을 구동하게 되면 기존의 임베디드 톰켓 기반의 Spring Boot Web 애플리케이션과는 다르게 Netty 기반의 비동기 통신을 지원하는 형태의 웹 애플리케이션으로 실행

ykh6242.tistory.com

 

🙋🏻 More

 

하지만, WebClient 사용하여 통신할 때 클라이언트의 요청 헤더를 전부 넣는 아래 코드가

                .headers(httpHeaders -> httpHeaders.addAll(headers))

 

SSE 사용시 문제가 되었습니다. 관련 버그는 또 다른 이슈로 등록하겠습니다.