OpenFeign(MSA CURL)
OpenFeign 이란?
OpenFeign은 Netflix에서 개발한 오픈소스이며, Spring Cloud 프로젝트의 일부로서 선언적인 REST 클라이언트로서 서비스 간의 HTTP 통신을 자동화하는 데 사용됩니다.
OpenFeign은 애플리케이션 간의 서비스 호출을 단순화하고 클라이언트 코드를 간결하게 유지할 수 있도록 지원합니다.
OpenFeign 사용법
1. gradle 추가
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '3.1.7'
2. @EnableFeignClients 명시
@SpringBootApplication
@EnableFeignClients
public class SnsApiApplication {
public static void main(String[] args) {
// 타임존 셋팅
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
Locale.setDefault(Locale.KOREA);
SpringApplication.run(SnsApiApplication.class, args);
}
}
3. 인터페이스 생성
@FeignClient(name = "users", url = "https://api.example.com")
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}
4. 사용하는 곳에서 주입받아 사용
@RequiredArgsConstructor
@Service
@Transactional
public class MemberInfoService extends BaseService {
private final UserServiceClient userServiceClient;
}
5. 통신 실패 및 4xx 예외 FeignException 발생
try {
User user = feignClient.getUserById(id);
} catch (FeignException e) {
log.error(e.getMessage);
user = null;
}
- 실패에 대한 예외는 FeignException 발생
- try-catch로 예외처리 or ErrorDecoder 사용
- ErrorDecoder 란?
- Feign 클라이언트에서 발생한 오류를 처리하기 위한 사용자 정의 전략을 제공
- 특히, 서버에서 반환된 HTTP 상태 코드에 따라 다른 동작을 구현할 때 유용
- 예제 코드
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404) {
return new NotFoundException("Not Found");
}
return new RuntimeException("Generic error");
}
}
OpenFeign 장점
1. 통신 실패에 대한 재시도(Retry) 설정이 가능
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignClientConfiguration {
@Bean
public Retryer feignRetryer() {
// 최대 3번 재시도
return new Retryer.Default(100, 1000, 3);
}
}
- ex) 요청이 실패할 경우 최대 3번 재시도하도록 설정
- Retryer의 구현체로 Retryer.Default
- Retryer.Default 생성자의 매개변수 순서는 다음과 같음
- period: 재시도 간격(밀리초 단위)
- maxPeriod: 최대 재시도 간격(밀리초 단위)
- maxAttempts: 최대 재시도 횟수
- 사용하는 인터페이스에서는 configuration 속성에 해당 설정 클래스 명시 후 사용\
- 더 복잡한 설정은 Retryer 인터페이스 직접 구현하여 사용
- https://mangkyu.tistory.com/279 - (OpenFeign 타임아웃, 재시도, 로깅 설정)
2. 인터페이스 기반의 API 정의로 간단하고 직관적인 코드 작성 및 코드가 줄어듦
@FeignClient(name = "users", url = "https://api.example.com")
public interface UserServiceClient {
@GetMapping("/users/{idx}")
User getUserById(@PathVariable("idx") Long idx);
@PostMapping("/users")
User registerUser(User user);
@DeleteMapping("/users/{idx}")
int deleteUser(@PathVariable("idx") Long idx);
}
- 위의 예제와 같이 선언부만 명시하면 사용이 가능
- user와 관련된 http 통신이 하나의 인터페이스에서 심플하게 관리될 수 있는 장점 지님
3. 익숙한 Spring MVC 어노테이션으로 개발이 가능
- 2번 예제 코드와 같이 스프링 컨트롤러에서 사용하는 어노테이션을 그대로 활용할 수 있음
- 러닝 커브가 높지 않음
4. Netflix에서 개발, 지속적인 라이브러리 업데이트 및 개선
- github에서 확인 할 수 있듯이 지속적으로 업데이트 중
- https://github.com/OpenFeign/feign
- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign [Maven Repository]
@FeignClient 속성값
1. name
@FeignClient(name = "example-service")
- Feign 클라이언트의 이름을 지정합니다. 이 이름은 실제로 Feign 클라이언트의 빈 이름으로 사용되며, 이를 통해 해당 클라이언트를 식별할 수 있음
2. url
@FeignClient(url = "http://example.com")
- Feign 클라이언트가 연결할 대상 서비스의 기본 URL을 지정합니다. name 대신에 **url**을 사용하여 직접 URL을 지정할 수 있음
- @FeignClient(url = "<http://example.com>")
3. value
@FeignClient(value = "example-service")
- name과 동일한 역할을 함. Feign 클라이언트의 이름을 지정
4. configuration
@FeignClient(name = "example-service", configuration = MyFeignClientConfiguration.class)
- Feign 클라이언트의 구성을 지정하는 데 사용. 구성은 각종 Feign 관련 설정을 변경할 수 있는 빈을 참조함
5. fallback
@FeignClient(name = "example-service", fallback = MyFallback.class)
- Feign 클라이언트에서 예외가 발생할 때 사용할 Fallback 클래스를 지정함. Fallback 클래스는 특정 메서드 호출에 대한 대체 로직을 제공함
6. fallbackFactory
@FeignClient(name = "example-service", fallbackFactory = MyFallbackFactory.class)
- Fallback 클래스 대신 Fallback 팩토리를 지정함. Fallback 팩토리는 Fallback 인스턴스를 생성하는 데 사용됨
기존 WebClient 와 OpenFeign 차이
1. 같은 기능을 하는 HTTP 통신 코드 비교
- WebClient 사용 시
@SneakyThrows
private List<PetDto> getPetUuidListByCurl(PetDto petDto) {
List<PetDto> petList = new ArrayList<>(); // 리턴할 결과 값
if (Boolean.TRUE.equals(usePetWalkCurl)) {
// curl 통신
WebClient webClient = WebClient.builder()
.baseUrl(walkDomain)
.build();
String jsonString = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/v1/pet/walk/entry")
.queryParam("walkUuid", petDto.getWalkUuid())
.queryParam("memberUuid", petDto.getMemberUuid())
.build())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(throwable -> {
throw new CustomException(CustomError.WALK_UUID_ERROR); // 존재하지 않는 산책 고유아이디입니다.
})
.block();
// curl 통신 정상
if (!ObjectUtils.isEmpty(jsonString)) {
JsonParser parser = new JsonParser();
ObjectMapper mapper = new ObjectMapper();
JsonObject jsonObject = (JsonObject) parser.parse(jsonString); // json object 파싱
String dataJson = jsonObject.get("data").toString();
JsonObject listJson = (JsonObject) parser.parse(dataJson); // data 안 list 파싱
List<String> petUuidList = mapper.readValue(listJson.get("list").toString(), List.class);
if (!ObjectUtils.isEmpty(petUuidList)) {
for (String uuid : petUuidList) {
PetDto tmpPetDto = PetDto.builder()
.uuid(uuid)
.memberIdx(petDto.getMemberIdx()).build();
PetDto walkPetInfo = memberPetService.getWalkPetInfo(tmpPetDto);
if (petDto != null) {
// 상태값 text setting
memberPetService.stateText(walkPetInfo);
petList.add(walkPetInfo);
}
}
}
}
}
return petList;
}
- FeignClient 사용 시
private List<PetDto> getPetUuidListByCurl(PetDto petDto) {
if (Boolean.TRUE.equals(usePetWalkCurl)) {
List<PetDto> petList = new ArrayList<>(); // 리턴할 결과 값
// walk curl 통신
List<String> petUuidList;
try {
petUuidList = memberCurl.petUuidList(petDto.getWalkUuid(), petDto.getMemberUuid());
} catch (FeignException e) {
petUuidList = null;
}
if (!ObjectUtils.isEmpty(petUuidList)) {
for (String uuid : petUuidList) {
PetDto tmpPetDto = PetDto.builder()
.uuid(uuid)
.memberIdx(petDto.getMemberIdx()).build();
PetDto walkPetInfo = memberPetService.getWalkPetInfo(tmpPetDto);
if (petDto != null) {
// 상태값 text setting
memberPetService.stateText(walkPetInfo);
petList.add(walkPetInfo);
}
}
}
}
return petList;
}
→ 같은 동작을 하는 코드이지만 feignClient 사용 시 코드가 훨씬 간결해지는 것을 볼 수 있음
2. WebClient 와 OepnFeign 장단점
- 공통점
- 마이크로서비스 간 통신을 지원
WebClient
🟩 장점
- 비동기 지원: WebClient는 비동기 및 리액티브 프로그래밍을 지원. 이는 많은 동시 요청을 효과적으로 처리할 수 있게 해 주며, 리액티브 스타일의 프로그래밍이 필요한 경우에 적합
- 함수형 스타일: WebClient는 함수형 프로그래밍 스타일을 지원하므로, 리액티브 스트림과 같은 기능을 활용할 수 있음
- 더 많은 유연성: WebClient는 더 많은 커스터마이징이 가능하며, 여러 가지 HTTP 클라이언트 구현체를 지원
🟥 단점
- 설정 복잡성: WebClient를 설정하는 것은 상대적으로 복잡할 수 있음. 리액티브 프로그래밍과 관련된 개념을 이해해야 하며, 적절한 스케줄러 및 리액터 라이브러리를 선택해야 함
- Annotation이 없음: WebClient는 Feign과는 달리 어노테이션을 사용하지 않으므로, 메서드 호출에 대한 명시적인 URL 및 요청 구성이 필요함
FeignClient
🟩 장점
- Annotation 기반의 간편한 사용: FeignClient는 인터페이스와 어노테이션을 이용하여 클라이언트를 정의하므로, 선언적이고 간단한 사용이 가능
- 자동화된 서비스 디스커버리: Spring Cloud의 @FeignClient를 사용하면 Eureka와 같은 서비스 디스커버리와 통합되어 자동으로 서비스를 찾을 수 있음
- 설정이 간단: FeignClient는 어노테이션을 통해 간단하게 설정할 수 있음
🟥 단점
- 비동기 지원 부족: 기본적으로 FeignClient는 비동기 및 리액티브 프로그래밍을 지원하지 않음.
- 자유도 제한: FeignClient는 주로 마이크로서비스 간의 통신을 위한 목적으로 디자인되었으며, 일반적인 HTTP 클라이언트로 사용하기에는 몇몇 제약이 있을 수 있음
일반적으로 간단하게 서비스 간 통신을 구현하려면 FeignClient가 더 편리할 수 있지만, 더 복잡하거나 비동기적인 요구 사항이 있는 경우 WebClient를 사용하는 것이 더 적합할 수 있음
참고 사이트
- 우아한 형제들 feign 적용 사례
- Spring Cloud Feign document