티스토리 뷰

728x90
반응형

들어가기전

최근에 알게된 기술로 상당히 편리한 기술이기에 공유하고자 블로그로 포스팅하려고 합니다. 본 내용은 OpenFeign에 대한 개념 정리와 실습에 적용했던 사례로 작성되었습니다.


OpenFeign이란?

OpenFeign은 Netflix에 의해 처음 만들어진 HttpClient 도구입니다. 외부 API 호출을 쉽게 할 수 있도록 도와주는 역할을 합니다. OpenFeign은 인터페이스에 어노테이션들만을 이용하여 구현하게 됩니다. 이러한 방식은 SpringBoot 와 유사하여 상당히 편리하게 개발을 할 수 있습니다.

 

짦막한 OpenFeign의 역사

Feign 모델 초기는 Netflix에 의해 만들어져 공개가 되었지만 추후 Spring Cloud 진영에서 Spring Cloud Netflix라는 프로젝트를 통해 Netflix OSS를 받아 탄생하게 되었습니다. 이후 Netflix에서 개발 중단을 결정하게 되었고, Spring Cloud는 OpenFeign이라는 생태계로 통합하게 되었습니다.

 

 

장점

  • 인터페이스와 어노테이션을 통한 구현으로 코드 효율성 극대화
  • Spring의 어노테이션 기반과 동일하게 개발이 가능하여 편리함
  • Spring Cloud에 속해있기에 여러 기술들을 통합하여 사용 가능

 

사용해보면서 제일 좋다고 느낀것이 코드 길이가 줄어들고 단순화하여 코드 가독성이 편해지는 효과가 있었습니다.

아래는 HttpURLConnection을 이용한 처리 방법입니다. 해당 내용은 실적 조회에 대한 처리입니다.

URL apiurl = new URL(requrl.toString());
conn = (HttpURLConnection)apiurl.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-Type","application/json");

//실적조회 response
br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = "";
StringBuffer readline = new StringBuffer();
List<Object> list = new ArrayList<Object>();
while((line = br.readLine()) != null) {
    readline.append(line);
}

JSONParser parser = new JSONParser();
JSONObject obj = (JSONObject) parser.parse(readline.toString());
String result = (String) obj.get("result");			
JSONArray array = (JSONArray) obj.get("order_list");
map.put("result",result);

 

외부 통신을 위한 코드가 지속적으로 반복되는 부분이 있고, 코드 길이가 길어진다는 것은 매우 보기가 불편한것은 현실입니다. 외부 통신에 대한 처리가 많아질 경우 유지보수 또는 MSA 시대에서는 비효율적인 방법이라고 볼 수 있습니다.

 

위 코드를 OpenFeign을 이용하여 작성해보겠습니다. 아래 코드로 정말 단순하게 표현할 수 있고 직관적으로 표현할 수 있어 가독성이 높게 느껴질 것입니다.

@FeignClient(name = "LinkPriceMerchantAllOpenFeign", url = "${linkprice.host}", configuration = LinkPriceConfiguration.class)
public interface LinkPriceClient {

    @GetMapping("/test/all/list")
    String searchMerchantAll();

    @GetMapping("/test/all/cps/detail")
    String searchMerchantDetailAll();

}

 

단점

  • Spring Cloud 의존성을 추가해야 합니다. Spring Cloud는 사용하지 않고 Open Feign만 사용하는데도 의존성을 추가하여 관리해야 합니다.
  • 공식적으로 비동기를 위한 Reactive 모델을 지원하지 않습니다. (비공식 오픈소스 라이브러리를 사용해야합니다.)
  • HttpClient가 Http2를 지원하지 않습니다.(추가적인 설정이 필요합니다.

 

OpenFeign 적용하기

1. OpenFeign 의존성 추가

 

Spring Cloud를 우선 의존성을 추가해야 합니다.

아래 예시는 SpringBoot 3 기준에 해당합니다. 만약 SpringBoot 2일 경우 2023.0.1은 지원하지 않습니다. 이보다 아래 버전으로 적용해주시면 됩니다.

dependencyManagement {
	imports {
		//spring cloud
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1"
	}
}

 

아래 코드는 dependencies 아래에 작성합니다.

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

 

2.OpenFeign 환경 설정하기

 

OpenFeign을 활성화하기 위해서는 @EnableFeignClients 어노테이션을 선언해줘야합니다.

  • @SpringBootApplication이 선언된 main 전체에 선언하기
  • Config 파일을 생성하여 @EnableFeignClients를 선언하기

Main 클래스에 선언해도 되겠지만, SpringBoot의 테스트하는 부분에 영향을 줄 수 있기 때문에, Config 파일을 생성하여 별도로 지정해주는 방식이 좋습니다.

 

@EnableFeignClients에 compoentScan 같이 baseBackes를 설정하여 OpenfFeign이 적용되는 부분에만 선언해줄 수 있습니다.

@Configuration
@EnableFeignClients("cashnamu.cashnamu_v2.www.connect.openfeign")
public class OpenFeignConfig {
    
    //OpenFeign 요청에 대한 로그 처리
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

 

3.OpenFeign 구현하기

 

API 호출을 위한 OpenFeign을 만들기 위해서는 인터페이스에 @FeignClient를 선언해줘야합니다. name 과 url을 설정해줍니다. name은 FeingClient의 명칭이 되겠고, url은 호출할 주소에 해당하게 됩니다.

@FeignClient(name = "GiftLetterOpenFeign", url = "https://wapi.gift-n.net",configuration = GiftLetterConfiguration.class)
public interface GiftLetterClient {

@GetMapping("/getGoodsInfoList")
List<GiftLetterGoodsInfoSummaryResponse> getGoodsInfoList(@RequestParam("cid") String cid,
                                                              @RequestParam("brand_id") String brandId,
                                                              @RequestParam("enc") String enc);                                                             
}

 

Configuration을 설정하지 않는다면 @FeingClient에서 default로 설정을 하게된다. 하지만 @FeignClient에 대한 환경설정을 커스텀 할 수 있습니다.

 

GiftLetterConfiguration.class

아래와 같이 API 호출 방식을 설정할 수 있고 requestTemplate안에 직접적으로 설정할 수 있습니다.

X-Method-Key는 현재 호출하는 url 정보에 해당합니다.

public class GiftLetterConfiguration {

    @Bean
    public RequestInterceptor requestInterceptor(){
        return requestTemplate -> {
            // HTTP 메서드(GET, POST 등)와 URL을 조합해 methodKey 생성
            String methodKey = requestTemplate.method() + " " + requestTemplate.url();
            // 헤더에 methodKey 추가
            requestTemplate.header("X-Method-Key", methodKey);
        };
    }
}

 

 

4.Decoder / ErrorDecoder 설정하기

 

Decoder

 

@FeingClient를 선언하여 처리하다 보면 JSON 값에 대하여 Response 처리하여 파싱하는 경우가 있습니다. 하지만 명확하게 파싱이 되는 경우도 있지만 그렇지 않은 경우도 있습니다. 예를 들어 응답 결과이 Response로 설정된 값이 아닌 다른 값으로 전달 주는 경우 파싱 불일치로 에러가 발생할 수 있습니다. 이러한 부분들을 처리하기 위해 Decoder를 통해 파싱 전 커스텀을 할 수 있습니다.

 

public class GiftLetterDecoder implements Decoder {

private final ObjectMapper objectMapper;

public GiftLetterDecoder(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
}

@Override
public Object decode(Response response, Type type) throws IOException, FeignException {

    // 응답 Body를 String으로 변환
    String body = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);

    try{
        GiftLetterErrorResponse errorResponse = objectMapper.readValue(body, GiftLetterErrorResponse.class);
        if(!"N".equals(errorResponse.getStatus()) && !"Y".equals(errorResponse.getStatus())) {
            throw FeignException.errorStatus("기프트 레터 접근 에러 errorResponse = " + errorResponse.toString(),
                    Response.builder()
                            .status(500)  // 상태 코드를 500으로 설정
                            .request(response.request())
                            .body(body, StandardCharsets.UTF_8)
                            .build());
        }
    }catch (MismatchedInputException e) {
        //TODO errorResponse 값이 존재하지 않을 경우 Exception을 예외처리합니다.
    }

    // 정상 처리
    return objectMapper.readValue(body, objectMapper.constructType(type));
}

}

 

위 코드를 보게되면 @FeingClient로 호출한 메소드에 대한 결과 값을 파싱하는 구간에 해당하겠습니다. OpenFeing에서는 이렇게 발생하는 메소드 결과에 대해 Decoder를 제공하여 결과 값을 도출하기 전에 커스텀을 할 수 있습니다.

 

위의 코드 내용은 만약 결과 값이 error에 해당한다면 강제로 FeingException을 통해 예외를 처리하는 구간에 해당하겠습니다. Decoder 같은 경우 결과 값이 200에 해당하는 경우에 대한 데이터를 처리하게 됩니다. 실질적으로 외부 API를 받는 경우 실패한 데이터에 대해 HTTP 상태코드 400 또는 404등 에러 상태값으로 하여 전달 주는 곳이 대부분이겠지만, 가끔식 응답 상태값은 200이지만 JSON 결과값이 실패에 대한 값으로 전달하여 전혀 다른 파싱 값을 전달하는 경우가 있습니다.

 

이러한 경우 Decoder를 통해 위와 같이 예외처리 또는 다른 커스텀 방식을 적용할 수 있습니다.

 

ErrorDecoder

 

ErrorDecoder는 Decoder와 반대로 HTTP 상태코드가 4xx, 5xx 인 경우에만 호출을 합니다. 이러한 경우 기존 설정된 Response에 대한 값이 아닌 다른 값이 전달 될 수 있기 때문에 그에 대한 예외처리를 해줘야합니다. 그러한 경우 ErrorDecoder를 통해 해결할 수 있습니다.

 

public class GiftLetterErrorDecoder implements ErrorDecoder {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final ObjectMapper objectMapper;

    public GiftLetterErrorDecoder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Exception decode(String methodKey, Response response) {
        try{
            String body = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
            GiftLetterErrorResponse errorResponse = objectMapper.readValue(body,GiftLetterErrorResponse.class);
            throw new GiftLetterException(errorResponse.getStatus(), HttpStatus.valueOf(response.status()));
        }catch (Exception e) {
            logger.error("[Gift-Letter] 에러 메세지 파싱 에러 code={}, request={}, methodKey={}, errorMessage={}",response.status(),response.request(),methodKey,e.getMessage());
            throw new GiftLetterException("기프트레터 메세지 파싱에러",HttpStatus.valueOf(response.status()));
        }
    }
}

 

위 코드는 ErroDecoder를 통해 HTTP 상태코드 값이 200이 아닐 경우 에러에 대한 대처를 하는 방법입니다. 만약 ErrorDecoder를 탈 경우 GiftLetterErrorResponse로 파싱을 하여 그 결과값을 예외로 던지게 되는 구조입니다.

 

이를 통해 만약 GlobalExceptionHandler에 GiftLetterExcepton을 설정해 두었다면 예외 발생한 결과값을 클라이언트에게 전달하게 됩니다.

 

Decoder / ErrorDecoder 선언하기

 

아래 코드같이 FeingClient에 선언한 환경설정 class에 Decoder 와 ErrorDecoder를 Bean으로 선언해야지만 사용할 수 있습니다.

public class GiftLetterConfiguration {

  	....
    
    @Bean
    public GiftLetterErrorDecoder giftLetterErrorDecoder(ObjectMapper objectMapper) {
        return new GiftLetterErrorDecoder(objectMapper);
    }

    @Bean
    public  GiftLetterDecoder giftLetterDecoder(ObjectMapper objectMapper) {
        return new GiftLetterDecoder(objectMapper);
    }


}

 


 

마치며..

실질적으로 운영하는 사이트에서 외부 API와의 협업을 통해 처리하는 시스템이 많아 호출 처리에 있어서 HttpUrlConnection 이나 RestTemplate, WebClient 등을 통해 처리해 왔었습니다. 하지만 이러한 코드 처리는 많이 적용될 수록 난잡함이 있었고 코드를 읽는데 있어서 효율적이지 못해 아쉬워하고 있었습니다. 그러한 와중에 OpenFeign이라는 기술을 찾게 되었는데 너무 좋은 기술이었습니다. 무엇보다 코드의 길이가 줄어들고 API 호출과 같이 직관적이어서 코드 읽기가 편했습니다.

 

모든 기술에는 단점이 존재하지만 워낙 장점이 좋아서 자주 애용하게 될 기술로 보입니다. 만약 저처럼 외부 API 호출에 대해 고민하고 계시다면 OpenFeing을 적용해보는것을 추천합니다 ^^

 

 

참고

https://spring.io/projects/spring-cloud-openfeign#overview 

 

Spring Cloud OpenFeign

 

spring.io

 

728x90
반응형
250x250
반응형
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함