마이크로서비스에서 서비스간 통신을 위한 2가지 방법이 있다.
Rest Template
vs Spring Cloud OpenFeign
기존 프로젝트에서Rest Template
으로 구현되어 있던 api 호출을 Spring Cloud OpenFeign
으로 대체해보고 차이점을 알아보자.
github user 가 있는지 확인해야한다.
1. https://api.github.com/users/{githubId}
를 호출한다.
2. 정상적으로 데이터가 반환되면 user가 존재한다.
3. 에러가 던져지면 user가 존재하지 않는다.
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를 참고하자.
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);
}
}
@FeignClient 어노테이션 설정
(name ="feign client 이름 설정" , url="호출할 api url", configuration = "feignclient 설정정보가 셋팅되어 있는 클래스")
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);
}
Config 클래스를 생성하고 필요한 각 설정 정보를 아래와 같이 셋팅 가능하다.
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.
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();
}
}
나는 사용자가 없을 때, 해당 아이디를 가진 깃허브 유저가 없다
라는 커스텀 에러가 필요하다. 그래서 에러 생성후에 위에 config
에 ErrorDecoder
를 상속받은 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;
}
}
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;
...
}
feinclient 로그 정보 설정은 docs를 참고하자. 엄청 다양한 옵션들이 있다.
@Service
public class GithubFeignService {
private final GithubFeignClient githubFeignClient;
public GithubUserResponseDto getGithubUser(String githubId) {
return githubFeignClient.getGithubUser(githubId);
}
}
getGithubUser
서비스를 호출하는 Controller를 생성하여 호출하면, 결과가 잘 나온다. 깃헙 유저가 없는 경우에는 커스텀에러 또한 잘 잡아주고 있다.
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();
}
}
Rest Template
에서는 try-catch로 매번 에러 핸들링이 필요했지만, Spring Cloud OpenFeign
에서는 커스텀 에러만 작성해놓으면 알아서 잘 잡아준다.이름 | 코드 가독성 | 예외 처리 | 테스트 용이성 |
---|---|---|---|
Spring Cloud OpenFeign | 코드 가독성 | ErrorDecoder 제공 | 일반적인 인터페이스의 간편한 stubbing |
Rest Template | 가독성이 좋게 되기 위해 다른 작업 필요 | try-catch | Spring 이 구현해놓은 객체의 복잡한 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