대충 넘어가지 않는 습관을 위한 기록

API 요청 방식 변경 : RestTemplate -> FeignClient

uhyvn 2024. 11. 12. 15:25

전에 RestTemplate으로 연동했던 Gemini api 포스팅에 이어서 리팩토링 기록을 적어보려고 한다.

 

 

먼저, FeignClient로 변경하면 어떤 점이 좋은지를 알아보자면 아래와 같다.

1. 코드 가독성과 간결성

  • 더 간단한 코드: FeignClient는 REST 클라이언트 인터페이스를 선언형으로 정의할 수 있어 코드가 간결하고 가독성이 좋습니다. HTTP 요청을 수동으로 구성할 필요가 없어 RestTemplate보다 직관적입니다.
  • 직접 매핑: 인터페이스에 어노테이션을 통해 HTTP 메서드 및 엔드포인트를 명시함으로써 코드의 의도를 쉽게 파악할 수 있습니다.

2. 자동화된 HTTP 요청 처리

  • 부가 작업 감소: FeignClient는 자동으로 HTTP 요청을 처리하고, 복잡한 설정이나 객체 매핑을 간단하게 수행할 수 있습니다. RestTemplate에서는 수동으로 객체를 매핑하거나 설정을 추가해야 하는 경우가 많습니다.
  • 헤더 설정의 단순화: 인증이나 공통 헤더 추가와 같은 기능도 Feign 인터셉터를 통해 쉽게 구현할 수 있습니다.

3. 마이크로서비스 아키텍처(MSA)와의 시너지

  • Spring Cloud 통합: FeignClient는 Spring Cloud와 완벽하게 통합됩니다. 특히, 서비스 디스커버리(e.g., Eureka), 로드 밸런싱(e.g., Ribbon), 회로 차단기(e.g., Hystrix) 등과 손쉽게 연동됩니다.
  • 로깅 및 디버깅: FeignClient는 설정을 통해 로깅을 활성화할 수 있으며, 요청 및 응답을 쉽게 추적할 수 있어 디버깅에 유리합니다.
  • 유연한 설정: application.yml 또는 application.properties에서 Feign과 관련된 설정을 통해 타임아웃, 재시도 정책 등을 쉽게 관리할 수 있습니다.

4. 확장성과 유연성

  • 플러그인 가능: Feign은 다양한 플러그인과 연동할 수 있어, 사용자 정의 디코더, 인코더, 인터셉터 등을 쉽게 추가할 수 있습니다.
  • 애노테이션 기반 확장: 커스텀 애노테이션을 사용해 공통 로직을 쉽게 재사용할 수 있습니다.

5. 에러 핸들링 및 복구

  • 회로 차단기 통합: FeignClient는 Resilience4j 또는 Hystrix 같은 라이브러리와 연동해 서비스 장애 시 회로 차단기 패턴을 적용할 수 있습니다. 이는 MSA 환경에서 서비스 복원력에 큰 도움이 됩니다.
  • Failover 처리: 다양한 재시도 전략과 예외 핸들링을 구성해 서비스 안정성을 높일 수 있습니다.

MSA에서의 이점

  • 서비스 호출의 일관성: MSA에서 여러 마이크로서비스 간의 통신이 빈번한데, FeignClient는 일관성 있는 API 호출 방식을 제공하여 팀 간 협업과 유지보수성을 향상시킵니다.
  • 로드 밸런싱: Feign은 Ribbon과 함께 기본적인 클라이언트 측 로드 밸런싱 기능을 제공합니다. 이를 통해 서비스 인스턴스 간의 부하를 분산시켜 성능을 최적화할 수 있습니다.
  • 서비스 디스커버리: Eureka 같은 서비스 레지스트리와 통합하여 자동으로 다른 서비스의 위치를 탐색하고 호출할 수 있습니다.

요약

FeignClient는 RestTemplate보다 선언형 접근 방식을 통해 코드를 간결하게 만들며, Spring Cloud 생태계와의 통합성을 높여 MSA에서 특히 유용합니다. 자동화된 설정과 다양한 기능 지원 덕분에 마이크로서비스 간의 통신이 보다 효율적이고 안정적으로 이루어집니다.

 


 

 

 

위 정리는 검색을 통해 알아낸 정보들이다. 그렇지만 사실 나는 새로운 기술 스택 공부와 더 나은 가독성, 그리고 다음 프로젝트에서는 MSA를 사용하여 개발할 예정인데, MSA에서는 위에 적어놓은 이유들을 바탕으로 RestTemplate과의 확연한 차이를 보여서이다.

 

 

그러면 이제 RestTemplate에서 FeignClient로 변경하면서 바뀐 코드, 그리고 추가된 코드들을 기록하겠다.

 

첫 번째로, FeignClient 의존성을 추가해준다. - build.gradle

// feign-client
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2'

 

 

그리고 FeignClient 설정을 위해 @EnableFeignClients 어노테이션을 추가해줘야 하는데,

복잡한 설정이 필요없이 위 어노테이션만 필요한 상황이라면 맨 앞단의 Application의 상단에 붙여주면 된다.

@SpringBootApplication
@EnableFeignClients // 이곳에 추가
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}

 

 

 

GeminiFeignClient를 생성해준다.

@FeignClient(value = "geminiClient", url = "${gemini.api.url}")
public interface GeminiFeignClient {
	@PostMapping
	ChatResponse createChat(@RequestParam("key") String apiKey, @RequestBody ChatRequest chatRequest);
}

 

이전 글을 참고하면 알겠지만, 미리 만들어둔 ChatRequest에 맞춰서 요청하고, ChatResponse 응답값에 맞춰서 응답을 받아와준다.

https://hyunco.tistory.com/70

 

Gemini api pro 연동 기록 - RestTemplate

이번 프로젝트에서 음식점 업주가 우리 서비스에 매장 상품을 등록할 때,상품 설명을 기입하는 과정에서 ai를 사용할 수 있게 연동을 해보려고 한다!      우선 아래 링크에서 구글 api 키를

hyunco.tistory.com

 

 

 

 

 

그리고 마지막 Service 코드 수정인데, 아래는 이전 RestTemplate을 사용한 서비스 로직이다.

@Service
@RequiredArgsConstructor
public class AIService {

	private final AIJpaRepository aiJpaRepository;
	private final StoreJpaRepository storeJpaRepository;
	private final RestTemplate restTemplate;

	@Value("${gemini.api.url}")
	private String apiUrl;

	@Value("${gemini.api.key}")
	private String apiKey;

	private static final String STORE_NAME_PROMPT = "우리 매장의 이름은 ";
	private static final String PRODUCT_DESCRIPTION_PROMPT = " 입니다. 배달 서비스에 상품 등록을 하려고 하는데, 내가 다음 문장에 주는 정보들을 이용해서 상품 설명 작성을 도와줘.";
	private static final String MAX_LENGTH_HINT = "답변은 최대한 간결하게 50자 이하로 작성해줘.";

	public AIResponse createProductDescription(String storeId, AIRequest request, User user) {
		validateOwner(user.getUserRole());
		Store store = storeJpaRepository.findById(UUID.fromString(storeId))
			.orElseThrow(() -> new IllegalArgumentException(ErrorCode.NOT_FOUND_STORE.getMessage()));

		String requestText =
			STORE_NAME_PROMPT + store.getName() + PRODUCT_DESCRIPTION_PROMPT + request.text() + MAX_LENGTH_HINT;

		String requestUrl = apiUrl + "?key=" + apiKey;
		ChatRequest chatRequest = new ChatRequest(requestText);
		ChatResponse chatResponse = restTemplate.postForObject(requestUrl, chatRequest, ChatResponse.class);

		if (chatResponse == null) {
			throw new IllegalStateException(ErrorCode.INTERNAL_SERVER_ERROR.getMessage());
		}
		String message = chatResponse.candidates().get(0).content().parts().get(0).text();

		aiJpaRepository.save(AIRequestLog.create(store, request.text(), message, user.getId()));

		return new AIResponse(message);
	}
}

 

 

그리고 바뀐 코드를 보자면

@Service
@RequiredArgsConstructor
public class AIService {

	private final AIJpaRepository aiJpaRepository;
	private final StoreJpaRepository storeJpaRepository;
	private final GeminiFeignClient geminiFeignClient;

	@Value("${gemini.api.key}")
	private String apiKey;

	private static final String STORE_NAME_PROMPT = "우리 매장의 이름은 ";
	private static final String PRODUCT_DESCRIPTION_PROMPT = " 입니다. 배달 서비스에 상품 등록을 하려고 하는데, 내가 다음 문장에 주는 정보들을 이용해서 상품 설명 작성을 도와줘.";
	private static final String MAX_LENGTH_HINT = "답변은 최대한 간결하게 50자 이하로 작성해줘.";

	public AIResponse createProductDescription(String storeId, AIRequest request, User user) {
		validateOwner(user.getUserRole());
		Store store = storeJpaRepository.findById(UUID.fromString(storeId))
			.orElseThrow(() -> new IllegalArgumentException(ErrorCode.NOT_FOUND_STORE.getMessage()));

		String requestText =
			STORE_NAME_PROMPT + store.getName() + PRODUCT_DESCRIPTION_PROMPT + request.text() + MAX_LENGTH_HINT;
		ChatResponse chatResponse = geminiFeignClient.createChat(apiKey, new ChatRequest(requestText));

		if (chatResponse == null)
			throw new IllegalStateException(ErrorCode.INTERNAL_SERVER_ERROR.getMessage());
		String message = chatResponse.candidates().get(0).content().parts().get(0).text();

		aiJpaRepository.save(AIRequestLog.create(store, request.text(), message, user.getId()));

		return new AIResponse(message);
	}
}

 

 

 

 

큰 변경사항은 아래와 같다.

 

  • apiUrl 선언 위치를 Service 레이어에서 FeignClient 로 변경
  • ChatResponse 생성 방식 변경
restTemplate.postForObject(requestUrl, chatRequest, ChatResponse.class);
geminiFeignClient.createChat(apiKey, new ChatRequest(requestText));

 

ChatRequest 선언 방식은 내 가독성을 위해 변경한 코드이니 이 부분 말고,

restTemplate.postForObject() -> geminiFeignClient.createChat() 부분을 보면 된다.

 

이전에는 직접 Url에 key를 넣어서 전달한 반면, FeignClient를 사용하면 그냥 @Value로 받아온 key값만 전달해주면 된다.

당연하겠지만, class 선언도 필요없다.

 

 

 

 

이렇게 FeignClient로 방식 변경 끝~

다음 기록은 rate limit 적용기가 되겠다.