쩨이엠 개발 블로그

OpenFeign 적용기 ( spring boot 3.0.x ) 본문

개발/JAVA

OpenFeign 적용기 ( spring boot 3.0.x )

쩨이엠 2023. 6. 13. 16:48
728x90
반응형

RestTemplate 을 대신하여 OpenFeign을 적용해보기로 했다

 

1. Gradle 적용

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.6'
    id 'io.spring.dependency-management' version '1.1.0'
}

...

dependencies {
...

    // OpenFeign
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.3'
    
}

ext {
    set('springCloudVersion', "2022.0.3")
}

dependencyManagement {

    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }

}

 

Springboot가 3.0.x 버전이어서 springCloudVersion과 OpenFeign 버전 또한 제일 최신으로 맞춰주었다

아니면 빌드 실패 난다

 

밑은 그 에러내용

 

[Spring cloud] Could not find org.springframework.cloud:spring-cloud-starter-openfeign Required by: project 빌드 실패

1. 현상 OpenFeign을 사용하기 위해 dependency들을 추가했다 dependencies { ... implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-depend

gogo-jjm.tistory.com

 

 

2. OpenFeign 소스 적용

OpenFeign을 적용하기 위해서는 Config 파일과 Client 파일을 만들어줘야한다

 

OpenFeignConfig.java

import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackages = {"com.gate.common"})
public class OpenFeignConfig {

}

basePackages 안의 client에 글로벌하게 Config 설정을 할 수 있다

 

SearchApi.java

...
import feign.codec.ErrorDecoder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

@FeignClient(value= "search", url = "http://localhost:8080", configuration = SearchApi.FeignConfig.class)
public interface SearchApi {

    @GetMapping(value = "/books",produces = MediaType.APPLICATION_JSON_VALUE)
    BookResponse getBookItems(@RequestParam("title") String title,
                                    @RequestParam("page") int page,
                                    @RequestParam("size") int size
    );


    class FeignConfig {
        @Bean
        ErrorDecoder errorDecoder() {
            return new SearchErrorDecoder();
        }
    }
}

OpenFeign은 client를 interface로 만들어주면 그 안의 url과 실제 RestController mapping을 연결하여 RestTemplate을 사용하듯이 보내주면서도 깔끔하게 소스를 확인할 수 있다

 

예제 : 외부의 도서리스트 불러오기

- url : http://localhost:8080/books GET

(yaml 파일에서 세팅한 값을 profile마다 다르게 가져올 수도 있다)

 

- configuration : SearchApi 안에서만의 설정이 필요하다면 직접 밑에 FeignConfig를 구현하는 것도 가능하다

 

- FeignConfig : ErrorDecoder를 새로 구현하여 검색에서 난 에러를 관리해 줄 수 있다

 

주의 받는쪽의 MediaType을 꼭 확인할 것

 

 

SearchService.java

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class SearchService {

    private final SearchApi searchApi;

...

    public SearchResponse getBookList(SearchDto dto){
        BookResponse res = searchApi.getBookItems(dto.getSearch(), dto.getPage(), dto.getSize());

        List<SearchResponse.Item> list = res.getItems().stream()
                .map(item -> new SearchResponse.Item(item.getShopName(), item.getTitle(), item.getPrice(), item.getImg())).toList();

        return new SearchResponse(res.getTotal(), list);
    }

    
}

실제 사용은 간단하다

위에 선언 후 그대로 SearchApi에 맞게 넘겨주면 끝!

 

 


Exception 처리하기

 

OpenFeign에서는 HttpStatus가 200이 아닐때 FeignException을 던지기 때문에 그에 대한 대비를 해놓으면 좋다

feign.FeignException$BadRequest: [400] during [GET] to [http://URL] [SearchApi#getBookItems(String,int,int)]: [{"code":1,"message":"Not match field","fieldErrors":[{"field":"search","value":"","message":"must not be blank"}],"timestamps":"2023-06-13 13:49:31"}]

외부 API에서는 search가 null일 때 HttpStatus 400과 message를 내려주었다

 

[{"code":1,"message":"Not match field","fieldErrors":[{"field":"q","value":"","message":"must not be blank"}],"timestamps":"2023-06-13 13:49:31"}]

 

이 부분을 받아서 client에 전달하기 위해서는 Decoder와 Exception 관련 객체들이 추가되어야한다

+ ExceptionAdvice

 

SearchErrorResponse.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
public class SearchErrorResponse {
    private int code;
    private String message;
    private List<FieldError> fieldErrors;
    private String timestamps;


    @Data
    public static class FieldError{
        private String field;
        private String value;
        private String message;
    }
}

Exception에 담을 Response를 설정해준다

 

SearchFeignException.java

import org.springframework.web.server.ResponseStatusException;

public class SearchFeignException extends ResponseStatusException {
    private final ApiResponseType apiResponseType;
    private final SearchErrorResponse errorResponse;

    public SearchFeignException(ApiResponseType apiResponseType) {
        super(apiResponseType.getHttpStatus(), apiResponseType.getMessage());
        this.apiResponseType = apiResponseType;
        this.errorResponse = null;
    }

    public SearchFeignException(ApiResponseType apiResponseType, SearchErrorResponse errorResponse) {
        super(apiResponseType.getHttpStatus(), apiResponseType.getMessage());
        this.apiResponseType = apiResponseType;
        this.errorResponse = errorResponse;
    }

    public ApiResponseType getApiResponseType() {
        return apiResponseType;
    }

    public SearchErrorResponse getErrorResponse() {
        return errorResponse;
    }
}

필요한 Exception을 만들어준다

 

SearchDecoder.java

import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import feign.codec.StringDecoder;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SearchErrorDecoder implements ErrorDecoder {
    private final StringDecoder stringDecoder = new StringDecoder();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    @SneakyThrows
    public SearchFeignException decode(String methodKey, Response response) {

        String message = stringDecoder.decode(response, String.class).toString();
        SearchErrorResponse searchErrorResponse = objectMapper.readValue(message, SearchErrorResponse.class);

        return new SearchFeignException(ApiResponseType.SEARCH_KEYWORD_NULL, searchErrorResponse);
    }

}

ErrorDecoder에서 decode 부분을 override해 변경한다

여러개의 method가 이 decode를 통과한다면 methodKey로 ExceptionType을 변경해 내려줄 수 있다

 

내 경우에는 한개뿐이라 methodKey는 사용하지 않고 response만 사용했다

위의 stringdecoder와 objectMapper로 바로 ErrorResponse로 변환이 가능하다

 

그럼 이제 Advice에 적용해본다

 

ExceptionControllerAdvice.java

@RestControllerAdvice
public class ExceptionControllerAdvice {

    ...
    
    @ExceptionHandler(SearchFeignException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<ErrorResponse> handleException(SearchFeignException e) {
        log.error("global handleException SearchFeignException", e);

        ErrorResponse errorResponse = ErrorResponse.of(e.getApiResponseType().getCode(), e.getErrorResponse());

        return new ResponseEntity<>(errorResponse, e.getApiResponseType().getHttpStatus());

    }

}

SearchFeignException일 때 ExceptionAdvice를 타도록 @ExceptionHandler 어노테이션을 붙여준다

그리고 받아온 e에서 객체를 꺼내서 원하는 ResponseEntity에 담을 수 있도록 가공한 후 내보내면

 

원하는 대로 보낼 수 있다

728x90
반응형
Comments