๐ ๋ค์ด๊ฐ๋ฉฐ
๋ฐฑ์๋ API ๊ฐ๋ฐ์ ํ๋ค๋ณด๋ฉด ์ธ๋ถ ์๋น์ค์ ์ฐ๋ํด์ผ ํ๋ ์ผ์ด ๋ง์ด ์๊น๋๋ค. ํนํ ํ์ฌ ์ฃผ๋ฌธ/๊ฒฐ์ ๋๋ฉ์ธํ์ ์๋ค๋ณด๋ ๋๋์ฑ ๋น์ฆ๋์ค ๋ก์ง์์ ์ธ๋ถํต์ ์ ํด์ผํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ด ์๊ธฐ๋ ๊ฒ ๊ฐ์ต๋๋ค. ์ด๋ฒ ํฌ์คํ ์์๋ ์ธ๋ถํต์ ์ ๋์์ฃผ๋ ๋ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค(HttpClient, WebClient) ์ค Feign Client ์ ๋ํด ์์๋ณด๊ณ ์ ํฉ๋๋ค.
๐ Feign Client
Feign Client๋ Netflix์์ ๊ฐ๋ฐํ Http Client์ ๋๋ค.
(HttpClient๋ Http ์์ฒญ์ ๊ฐํธํ๊ฒ ๋ง๋ค์ด์ ๋ณด๋ผ ์ ์๋๋ก ๋๋ ๊ฐ์ฒด)
์ฒ์์๋ Netflix์์ ์์ฒด์ ์ผ๋ก ๊ฐ๋ฐ์ ์งํ(Spring Cloud Netflix Feign)ํ์ง๋ง ํ์ฌ๋ ์คํ์์ค ํ๋ก์ ํธ์ธ OpenFeign์ผ๋ก ์ ํํ์ผ๋ฉฐ SpringCloud ํ๋ ์์ํฌ์ Spring Cloud OpenFeign์ ํตํฉ๋์์ต๋๋ค.
์ด๋ฌํ Feign Client์ ์ฌ์ฉํ๊ฒ ๋๋ ์ด์ ๋ฅผ ๊ทธ ํน์ง๊ณผ ํจ๊ป ์์๋ณด๋ฉด
- SpringMvc์์ ์ ๊ณต๋๋ ์ด๋ ธํ ์ด์ ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค. (Spring Cloud์ starter-openfeign์ ์ฌ์ฉํ ๊ฒฝ์ฐ)
- RestTemplate ๋ณด๋ค ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ(interface ์ annotation๋ง ์ ์ธํ๋ฉด ๊ตฌํ์ฒด๊ฐ ์๊น) ๊ฐ๋ ์ฑ์ด ์ข์ต๋๋ค.
- ๋๊ธฐ์ ์ผ๋ก ์๋ํ๊ธฐ ๋๋ฌธ์ ์ธ๋ถ ์๋น์ค์ ์๋ต์ด ๋น์ฆ๋์ค ๋ก์ง์ ํฌํจ๋์ด ๋ฐ๋์ ์๋ต์ ๋ฐ์์ผ API๋ฅผ ์ฑ๊ณต ์ฒ๋ฆฌ ํ ์ ์๋ ๊ฒฝ์ฐ ์ฌ์ฉํ๊ธฐ์ ์ ํฉํฉ๋๋ค.
๐ Feign Client ์์ํ๊ธฐ
์ฐ์ Spring Cloud ๊ด๋ จ ํจํค์ง๋ค์ ๋ฒ์ ์ ๋ง๋ ์์กด์ฑ ์๋ ์ค์ ์ ์ํด spring-cloud-dependencies ๋ฅผ ๋ฑ๋กํ๊ณ openfeign dependency๋ ์ถ๊ฐํด์ค๋๋ค.
ext {
// [2021-07-16] Dependency management for Spring Cloud AWS.
springCloudVersion = '2021.0.5'
// spring cloud version (Spring Boot 2.7.x compatibility)
springCloudAwsVersion = '2.4.2'
}
dependencyManagement {
imports {
mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:${springCloudAwsVersion}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
//########## OpenFeign ##########//
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.10.0'
testImplementation 'org.springframework.cloud:spring-cloud-contract-wiremock'
}
์คํ๋ง ๋ถํธ ๋ฒ์ ์ ๋ง๋ spring-cloud ๋ฒ์ ์ ์ฌ์ฉํด์ผ ํ๋ฏ๋ก ์์ ์ ์คํ๋ง๋ถํธ ๋ฒ์ ์ ๋ง๋ depnedency ์ค์ ์ ํด์ค๋๋ค.
https://spring.io/projects/spring-cloud
Spring | Home
Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.
spring.io
๊ฐ์์ ํ๋ก์ ํธ์ ์ ํํ๊ฒ ๋ง๋ ๋ฒ์ ์ ๋ณด๋ฅผ ์ป๊ณ ์ ํ๋ค๋ฉด ์์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ๋ถํ๋๋ฆฝ๋๋ค.
build.gradle์ ์์ ํ์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ค์ด ๋ฐ์๋ค๋ฉด OpenFeign ๊ด๋ จ ์ปดํฌ๋ํธ ์ค์บ์ ์ํด
Application์ @EnableFeignClients ๋ฅผ ๋ถ์ฌ์ค๋๋ค.
@EnableFeignClients()
@ServletComponentScan
@SpringBootApplication
public class SearchApplication {
public static void main(String[] args) {
SpringApplication.run(SearchApplication.class, args);
}
}
๊ทธ ๋ค์ FeignClient ๋ฅผ ํตํด ์์ฒญํ ์ธ๋ถํต์ ์คํ์ ๋ง๊ฒ interface๋ฅผ ์์ฑํ๋ฉด ๋ฉ๋๋ค.
@FeignClient(name = "SearchNaverFeignClient", url = ENDPOINT_NAVER_URL)
public interface SearchNaverFeignClient {
/**
*
* @param naverClientId
* @param naverClientSecret
* @param query
* @param display
* @param start
* @param sort
* @return
*/
@GetMapping("/v1/search/blog.json")
NaverResultDto getSearchResult(
@RequestHeader(NAVER_CLIENT_ID) String naverClientId,
@RequestHeader(NAVER_CLIENT_SECRET) String naverClientSecret,
@RequestParam("query") String query,
@RequestParam("display") Integer display,
@RequestParam("start") Integer start,
@RequestParam("sort") String sort);
}
EndPoint url์ application.yaml ํ์ผ์์ ๊ฐ์ ธ์ ์ ์ฉํ๋ ๊ฒ๋ ๊ฐ๋ฅํฉ๋๋ค.
external:
user-service:
host: 'https://user.me'
@FeignClient(name = "userClient", url = "${external.user-service.host}")
๋ํ feignClient์ ์ฅ์ ์ค ํ๋๋ก Client์ ์ปค์คํ Configuration์ ์ ์ฉํ ์ ์์ต๋๋ค.
์๋ฅผ๋ค์ด ํด๋น ์ธ๋ถ ์๋น์ค์ API ๋ฅผ ํธ์ถํ ๋ ๋ฌด์กฐ๊ฑด ๊ณตํต์ผ๋ก ๋ค์ด๊ฐ์ผํ๋ header๊ฐ ์๊ฑฐ๋
๊ณตํต Response๊ฐ ์์ด์ ํ์ํ ํ๋๋ง decodeํด์ค๊ฑฐ๋ ๋ฑ์ ํ์๋ค์ ํ ์ ์์ต๋๋ค.
ex) ํด๋น client์์ api ํธ์ถ ํ ๋๋ง๋ค header์ ๊ฐ ๋ฃ๊ธฐ
public class HumanFeignClientConfig {
@Bean
public RequestInterceptor requestInterceptor() throws InterruptedException {
return requestTemplate -> requestTemplate.header("header-name", "header-value");
}
}
์ด๋ฅผ feignClient interface์ ์ ์ฉํ๊ธฐ
@FeignClient(name = "humanClient", url = "${external.human-service.host}", configuration = HumanFeignClientConfig.class)
public interface HumanClient {
@GetMapping(value = "/human/list")
List<HumanInfo> getHumans(@RequestParam("name") String name);
}
์ด์ธ์๋ ๋ค์ํ ์ค์ ๋ฒ์ด ์์ต๋๋ค. (Decoder, Encoder, Logger ๋ฑ)
๐ Feign Client readTimeout connectionTimeout
readTimeout ๊ณผ connectionTimeout์ ์ธ๋ถ ํต์ ์ฐ๋ ์
๊ฐ์ฅ ์ ๊ฒฝ์จ์ผ ํ๋ ํ์์ ์ธ ์ค์ ๋ถ๋ถ์ด๋ผ๊ณ ํ ์ ์์ต๋๋ค.
FeignClient์์๋ ์ด๋ฅผ ๊ฐ๋จํ๊ฒ ์ค์ ํ ์ ์์ต๋๋ค.
default๋ก ๋ชจ๋ FeingClient interface์ ๊ณตํต์ ์ผ๋ก ์ ์ฉํ๊ฑฐ๋
์๋์ ๊ฐ์ด ํน์ feignClient interface ๋ง๋ค ๊ฐ์ ๋ค๋ฅด๊ฒ ์ ์ฉํ ์ ์์ต๋๋ค.
feign:
httpclient:
enabled: true
maxConnections: 1000
maxConnectionsPerRoute: 1000
client:
config:
default:
retry-period: 500
retry-max-period: 1000
max-attempts: 3
connectTimeout: 3000
readTimeout: 30000
inicis:
retry-period: 500
retry-max-period: 1000
max-attempts: 1 #(์ค์ ์๋ + ์ฌ์๋ ํ์์ด๋ฏ๋ก ์ฌ์๋ ์์)
naver:
retry-period: 500
retry-max-period: 1000
max-attempts: 2
CustomRetryer ์์ฑ ํ์ฌ readTimeout ๋ฐ์ ์์๋ retry ํ์ง ์๊ณ
connectionTimeout ๋ฐ์ ์์๋ง retry ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
public interface CustomRetry extends Retryer {
@Slf4j
class CustomRetryer implements CustomRetry {
private final int readTimeoutRetries;
private final int connectTimeoutRetries;
private int attempt;
public CustomRetryer(int readTimeoutRetries, int connectTimeoutRetries) {
this.readTimeoutRetries = readTimeoutRetries;
this.connectTimeoutRetries = connectTimeoutRetries;
}
@Override
public void continueOrPropagate(RetryableException e) {
if (e.getCause() instanceof IOException && e.getMessage().toLowerCase(Locale.ROOT).contains("read")) {
log.error("Read timed out occurred. count: {}", attempt);
throw new ApiException(ResponseCode.READ_TIMEOUT);
} else if (e.getCause() instanceof IOException && e.getMessage().toLowerCase(Locale.ROOT).contains("connect")) {
log.error("Connect timed out occurred. init: {}", attempt);
if (attempt++ < connectTimeoutRetries) {
log.error("Connect timed out occurred. Retry count: {}", attempt);
return;
}
}
throw e;
}
public Retryer clone() {
return new CustomRetryer(readTimeoutRetries, connectTimeoutRetries);
}
}
}
@Slf4j
@Configuration
public class FeignConfig {
@Value("${feign.client.config.max-attempts}")
private int maxAttempts;
/**
* Debug log level
*/
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor requestInterceptor(){
return requestTemplate -> {
requestTemplate.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
requestTemplate.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
};
}
/**
* retry ์ค์
*/
@Bean
public Retryer retryer(){
return new CustomRetry.CustomRetryer(MAX_READ_TIMEOUT_TRY_COUNT, maxAttempts);
}
}
์ด ์ธ์๋ FeingClient ์ฌ์ฉ์ ์ํ ๋ค์ํ ์ค์ ๋ฐฉ๋ฒ๋ค์ ๋ํด์๋ ๊ธฐํ๊ฐ ๋๋๋๋ก ์ ๋ฆฌํ์ฌ ํฌ์คํ ํ๋๋ก ํ๊ฒ ์ต๋๋ค.
์ฐธ๊ณ :
https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/
'BackEnd > SpringBoot' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring Transaction ์ ๋ํ ์ดํด (0) | 2023.09.20 |
---|---|
[Spring] ์ด๋ ธํ ์ด์ ์ ์ ์์ ๋์์๋ฆฌ (0) | 2023.09.18 |
์คํ๋ง ๋ถํธ ๋ก์ปฌ ์บ์ ์ ์ฉํ๊ธฐ (0) | 2023.06.25 |
String ํํ๊ฐ ์๋ LocalDateTime๊ทธ๋๋ก ๋ฐ์๋ณด์! (1) | 2023.01.01 |
SpringBoot @Converter @Convert ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ํ์ฉ (1) | 2022.12.11 |