[Spring] Feign Client๋?
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ค Spring ์๋ฒ์์ ์ธ๋ถ API์ ํต์ ํ๋ ์๋ฒ to ์๋ฒ ํต์ ์ ํ๊ฒ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ด๋ฌํ ๊ฒฝ์ฐ์ ์ฌ์ฉํ ์ ์๋ Feign Client์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
Feign Client ๋?
Feign Client๋ฅผ ํ๋ง๋๋ก ์ ๋ฆฌํ์๋ฉด Netflix์์ ๊ฐ๋ฐ๋ Http Client Binder์ ๋๋ค. Spring Boot ํ๊ฒฝ์์ ๋ค๋ฅธ ์๋ฒ์ Api๋ฅผ ํธ์ถํ๊ธฐ ์ํด ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
์๋์ ๊ฐ์ด ๋จ์ํ๊ธฐ ๋๋ฌธ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ ์ง์คํ ์ ์๊ฒ ๋์์ค๋๋ค.
์ฅ์
- HTTP API๋ฅผ ๊ท ์ผํ๊ฒ ๋ฐ์ธ๋ฉํ์ฌ ๋ณต์ก์ฑ์ ์ค์
- RestTemplate๋ฐฉ์๊ณผ WebClient๋ฐฉ์๋ณด๋ค ๋จ์
- ์น ์๋น์ค ํด๋ผ์ด์ธํธ๋ฅผ ์ฝ๊ฒ ์์ฑํ ์ ์์
- Interface๋ฅผ ์์ฑํ๊ณ Annotaion์ ์ ์ธํ๋ฉด ๋
- Jpa๊ฐ ์ค์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ง ์๊ณ Interface๋ง ์ง์ ํ์ฌ ์ฟผ๋ฆฌ ์คํ ๊ตฌํ์ฒด๋ฅผ ์๋์ผ๋ก ๋ง๋๋๊ฒ๊ณผ ์ ์ฌํจ
์ด๋ฌํ ์ฅ์ ๋ค๋ก ์ธํด ๋น์ฆ๋์ค ๋ก์ง์ ๋ ์ง์คํ ์ ์๊ฒ ๋์์ค๋๋ค.
OkHttp
- Feign์์ ์ ๊ณตํ๋ Http Client ์ค 1๊ฐ
- ๋น๋๊ธฐ / Non-Blocking ์ฌ์ฉํ๊ธฐ ์ํด ์ค์
- ๊ธฐ๋ณธ ์ค์ ์ HttpUrlConnection
- Rest Api, Http ํต์ ์ ๊ฐํธํ๊ฒ ๊ตฌํํ ์ ์๋๋ก ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- HTTP ์์ฒญ๊ณผ ์๋ต์ ์์ฑํ๊ณ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ณธ ๊ธฐ๋ฅ์ ์ ๊ณต
- ํค๋, ๋ฐ๋ ๋ฑ์ ์ธ๋ถ ์ฌํญ์ ์๋์ผ๋ก ์ ์ดํ ์ ์์
- ์์ฒญ ๋ฐ ์๋ต์ ๋ํ ์ธ๋ฐํ ์ ์ด๊ฐ ๊ฐ๋ฅ
- Java์ ๊ฐ์ JVM ๊ธฐ๋ฐ ์์คํ ์์ ๋์
Feign Client ์์ ์ฝ๋
์ค์ Feign Client๋ฅผ ํ์ฉํด ์ธ๋ถ API๋ฅผ ํธ์ถํ๋ ๋ก์ง์ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค. ํ ์คํธ๋ฅผ ์ํด์ Json ๋ฌด๋ฃ ๊ฐ์ Rest API ์๋ฒ์ธ JSONPlaceholder๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
https://jsonplaceholder.typicode.com/
์ฐ์ build.gradleํ์ผ์ ํตํด Dependency๋ฅผ ์ค์ ํด์ผํฉ๋๋ค.
์์กด์ฑ
ext {
set('springCloudVersion', "2021.0.5")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'io.github.openfeign:feign-okhttp'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
์์ ์ฝ๋
Client Interface์ FallbackFacory Class๋ฅผ ์์ฑํฉ๋๋ค.
FallbackFacory๋?
- Feign์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ ์ค ํ๋๋ก, ์๋น์ค ๊ฐ ํต์ ์ค์ ์๋ฌ๊ฐ ๋ฐ์ํ ๋ ๋์ฒด ๋์์ ์ ์ํ๊ธฐ ์ํด ์ฌ์ฉ
- ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ํด๋น ์ธํฐํ์ด์ค์ ๋ฐฑ์๋ ๊ตฌํ ๋์ FallbackFactory์ ์ ์๋ ๋ฉ์๋ ํธ์ถ
package com.example.demofeign.api.test.feign;
import org.springframework.http.MediaType;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.Map;
@FeignClient(name = "testApi", url = "https://jsonplaceholder.typicode.com", fallbackFactory = ApiClientFallbackFactory.class)
public interface ApiClient {
@GetMapping(value = "/users/{userNo}", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
Map getTestData(@PathVariable String userNo);
}
package com.example.demofeign.api.test.feign;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
@Slf4j
@Component
public class ApiClientFallbackFactory implements FallbackFactory<ApiClient> {
@Override
public ApiClient create(Throwable cause) {
return new ApiClient() {
@Override
public Map getTestData(String userNo) {
log.error("testData Fallback reason was: " + cause.getMessage(), cause);
return null;
}
};
}
}
๋ค์์ผ๋ก ApiClient๋ฅผ ํธ์ถํ๋ TestService๋ฅผ ์์ฑํฉ๋๋ค.
package com.example.demofeign.api.test.service;
import com.example.demofeign.api.test.feign.ApiClient;
import com.example.demofeign.api.test.model.response.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
public class TestService {
private final ApiClient apiClient;
private final ObjectMapper objectMapper;
public ApiResponse getTestData(String userNo) {
Map testData = apiClient.getTestData(userNo);
ApiResponse apiResponse = objectMapper.convertValue(testData, ApiResponse.class);
log.info("TestData: {}", apiResponse);
return apiResponse;
}
}
๋ง์ง๋ง์ผ๋ก TestService๋ฅผ ํธ์ถ ํ ์คํธํด๋ณผ TestController๋ฅผ ์์ฑํฉ๋๋ค.
package com.example.demofeign.api.test.controller;
import com.example.demofeign.api.test.model.response.ApiResponse;
import com.example.demofeign.api.test.service.TestService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class TestController {
private final TestService testService;
@GetMapping("/test/{userNo}")
public ResponseEntity<ApiResponse> test(@PathVariable String userNo) {
return ResponseEntity.ok(testService.getTestData(userNo));
}
}
Postman์ผ๋ก ํธ์ถ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๋ฉด, ์์ ๋งํฌ์ ๊ฐ์ด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
ํด๋น ์์ ์์ค ๋งํฌ์ ๋๋ค.
https://github.com/Jisue/Spring-Boot---Feign-Client
[์ฐธ๊ณ ]
https://techblog.woowahan.com/2630/
https://kdhyo98.tistory.com/33#google_vignette