[Spring] Feign client 적용기

Hocaron·2022년 5월 31일
2

Spring

목록 보기
21/44

마이크로서비스에서 서비스간 통신을 위한 2가지 방법이 있다.
Rest Template vs Spring Cloud OpenFeign
기존 프로젝트에서Rest Template으로 구현되어 있던 api 호출을 Spring Cloud OpenFeign으로 대체해보고 차이점을 알아보자.

Spring Cloud OpenFeign 적용하기

요구사항

github user 가 있는지 확인해야한다.
1. https://api.github.com/users/{githubId}를 호출한다.
2. 정상적으로 데이터가 반환되면 user가 존재한다.
3. 에러가 던져지면 user가 존재하지 않는다.

의존성 추가(gradle)


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

...

dependencies {
    /* FeignClient 관련 */
    implementation "org.springframework.cloud:spring-cloud-starter-openfeign"
}

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

버전에 따른 의존성 추가 방법은 Docs를 참고하자.

@EnableFeignClients 추가

package com.comeet;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableFeignClients
@SpringBootApplication
public class CoMeetApplication {

    public static void main(String[] args) {
        SpringApplication.run(CoMeetApplication.class, args);
    }
}

@FeinClient 인터페이스 구현

  1. @FeignClient 어노테이션 설정

    (name ="feign client 이름 설정" , url="호출할 api url", configuration = "feignclient 설정정보가 셋팅되어 있는 클래스")

  2. api를 호출할 메소드 셋팅

    url이 가변 이라면, 컨트롤러에서 사용하는 것처럼 @RequestMapping 활용해 api url를 동적으로 변경 할 수 있다. 아래 처럼

메소드에 @RequestMapping 어노테이션 설정, 메소드 파라미터에서 uri에서 변경이 필요한 부분을 @PathVariable 어노테이션을 설정해주면 된다.

package com.comeet.github;

import com.comeet.config.feign.GithubFeignClientConfig;
import com.comeet.github.model.response.GithubCommitsResponseDto;
import com.comeet.github.model.response.GithubUserResponseDto;
import java.time.LocalDate;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(name = "githubFeignClient", url = "https://api.github.com", configuration = GithubFeignClientConfig.class)
public interface GithubFeignClient {

    @RequestMapping(method = RequestMethod.GET, value = "/users/{githubId}")
    GithubUserResponseDto getGithubUser(@PathVariable("githubId") String githubId);

    @RequestMapping(method = RequestMethod.GET, value = "/search/commits?q=author:{author} committer-date:{committerDate}")
    GithubCommitsResponseDto getGithubCommits(@PathVariable("author") String author,
        @PathVariable("committerDate") String committerDate);
}

@FeinClient config 정보 설정

Config 클래스를 생성하고 필요한 각 설정 정보를 아래와 같이 셋팅 가능하다.

  • Config 클래스를 따로 생성하지 않아도 아래 Bean들은 기본적으로 제공된다.

Spring Cloud OpenFeign provides the following beans by default for feign (BeanType beanName: ClassName):
Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder)
Encoder feignEncoder: SpringEncoder
Logger feignLogger: Slf4jLogger
MicrometerCapability micrometerCapability: If feign-micrometer is on the classpath and MeterRegistry is available
CachingCapability cachingCapability: If @EnableCaching annotation is used. Can be disabled via feign.cache.enabled.
Contract feignContract: SpringMvcContract
Feign.Builder feignBuilder: FeignCircuitBreaker.Builder
Client feignClient: If Spring Cloud LoadBalancer is on the classpath, FeignBlockingLoadBalancerClient is used. If none of them is on the classpath, the default feign client is used.

  • 아래 bean들은 기본으로 제공되지 않고 필요시 config 클래스에 생성해주어야 한다.

Logger.Level
Retryer
ErrorDecoder
Request.Options
Collection<RequestInterceptor>
SetterFactory
QueryMapEncoder
Capability (MicrometerCapability and CachingCapability are provided by default)

package com.comeet.config.feign;

import com.comeet.github.GithubFeignError;
import feign.Logger;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GithubFeignClientConfig {

    @Bean
    Logger.Level githubFeignClientLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return new GithubFeignError();
    }
}

나는 사용자가 없을 때, 해당 아이디를 가진 깃허브 유저가 없다라는 커스텀 에러가 필요하다. 그래서 에러 생성후에 위에 configErrorDecoder를 상속받은 GithubFeignError를 추가로 작성해주었다.

Feign 의 장점 중 하나는 Microservice 에서 내부적으로 API 호출을 수행했을 때, 예외 처리를 핸들링하는 방법을 ErrorDecoder로 제공한다.

package com.comeet.github;

import com.comeet.member.exception.GithubUserNotFoundException;
import feign.Response;
import feign.codec.ErrorDecoder;

public class GithubFeignError implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 404:
                return new GithubUserNotFoundException();
        }
        return null;
    }
}

Dto 생성 (api response mapping)

package com.comeet.github.model.response;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class GithubUserResponseDto {

    String login;

    Long id;

    String node_id;

    String avatar_url;

    String gravatar_id;
...

}

application.properties 설정

feinclient 로그 정보 설정은 docs를 참고하자. 엄청 다양한 옵션들이 있다.

호출해보자


@Service
public class GithubFeignService {

    private final GithubFeignClient githubFeignClient;

    public GithubUserResponseDto getGithubUser(String githubId) {
        return githubFeignClient.getGithubUser(githubId);
    }
}

getGithubUser 서비스를 호출하는 Controller를 생성하여 호출하면, 결과가 잘 나온다. 깃헙 유저가 없는 경우에는 커스텀에러 또한 잘 잡아주고 있다.

Rest Template을 사용하여 api를 호출했을 때는?

    public String checkGithubId(String githubId) {
        /**
         * TODO 외부 API 호출은 추후 리팩토링
         */
        String githubUrl = "https://api.github.com/users";
        RestTemplate restTemplate = new RestTemplate();

        Map<String, Object> params = new HashMap<>();
        params.put("id", githubId);
        try {
            restTemplate.getForObject(githubUrl + "/{id}", Object.class,
                params);
            return "해당 깃허브 아이디를 사용하는 유저가 존재합니다.";
        } catch (HttpStatusCodeException e) {
            throw new GithubUserNotFoundException();
        }
    }
  • 가독성이 떨어진다.
  • 에러 핸들링을 매번 해야한다.
    • 프로젝트에서는 github 유저 유무 확인 외에도 커밋 / 이슈 / pr 내역을 확인하는 api 호출이 필요했다. 기존에 Rest Template에서는 try-catch로 매번 에러 핸들링이 필요했지만, Spring Cloud OpenFeign에서는 커스텀 에러만 작성해놓으면 알아서 잘 잡아준다.

정리해보자.

이름코드 가독성예외 처리테스트 용이성
Spring Cloud OpenFeign코드 가독성ErrorDecoder 제공일반적인 인터페이스의 간편한 stubbing
Rest Template가독성이 좋게 되기 위해 다른 작업 필요try-catchSpring 이 구현해놓은 객체의 복잡한 stubbing

관심사의 분리를 고려하면

Service 의 행동에 대한 관심사는 github API에 호출을 보내는 것으로 Feign 이나 RestTemplate이나 동일하다.

하지만 Uri 에 대한 직접적인 설정 정보는 Service가 가져야 하는게 맞을까?
책임의 관심사로 본다면 어떻게 될까?
만약 github API의 호출 경로가 달라졌다면 그에 대한 책임은 Service 가 아니라 호출을 하는 로직 자체에 존재한다.
하지만 RestTemplate 에서는 설정 정보가 Service.class 내에 있기 때문에 Service가 그 책임을 지고 있다.

그에 반해서 Feign은 어떨까?
아예 Feign을 사용하기 위해서는 호출에 관한 설정을 다 FeignClient.interface 에서 수행하도록 강제화되어 있기 때문에 관심사가 분리되어있다.
결국 이를 가져다 쓰는 Service 에서는 반환에 대한 결과만을 책임으로 갖고 있는 것으로 적절하다고 할 수 있다. 변경에 대해서 유연하게 대처할 수 있다.
가독성은 이야기 하지 않더라도 Feign 이 좋다고 생각한다.

적용한 코드는 여기

https://github.com/Co-Meet/co-meet-server

References

profile
기록을 통한 성장을

0개의 댓글