[Spring Cloud] OpenFeign 적용하기 - 고양이 이미지 API 구현 과제

2023. 4. 6. 21:10프로젝트/학습

과제 저장소: https://github.com/twotwobread/jwp_cat_picture_search
💡본문에 들어가기 앞서...

해당 고양이 이미지 API 구현 과제에서의 요구사항 중 하나가 Feign을 적용하여 API 호출을 하는 것이었습니다. Feign이 뭔지 공부를 해보고 적용해보려고 하고 Hype이란 프로젝트에서 Feign없이 서버에서 API를 호출해본 경험이 있어서 그 때 코드도 한 번 보면서 비교해려고 합니다아! 레츠 고도리!

※ 제가 사용하는 외부 open api는 TheCatAPI 입니다!!

OpenFeign 란?

Feign는 Netflix에서 개발된 Http Client Binder입니다. 원래 이름이 Spring Cloud Netflix Feign이었는데 현재는 오픈소스 프로젝트인 OpenFeign로 변경되었고 Spring Cloud OpenFeign로 통합되면서 SpringMVC 어노테이션에 대한 지원 및 HttpMessageConverters를 사용할 수 있게 되었습니다.

OpenFeign를 이용하면 더 쉽게 웹 서비스 클라이언트를 작성할 수 있습니다. Spring Data JPA에서 실제 쿼리를 작성하지 않고 Interface를 지정해서 쿼리실행 구현체를 자동으로 만들어 주는 것처럼 이 기술도 Interface를 작성하고 annotation을 작성하기만 하면 사용할 수 있습니다.

기존에 서비스 간 통신에서 사용하던 RestTemplate라는 기술을 OpenFeign를 이용하여 조금 더 편리하게 사용할 수 있게 됩니다.

 

의존성 설정

해당 기술을 SpringBoot에서 사용하기 위해선 의존성 설정이 필요합니다. 이 때 중요한 것은 SpringBoot와 호환되는 Spring Cloud 버전을 가져와야 하는데 저는 SpringBoot 3.0.5 버전을 사용했기에 2022.0.2 버전을 사용한 것을 확인할 수 있습니다.

dependencies {
    implementation platform("org.springframework.cloud:spring-cloud-dependencies:2022.0.2")
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    ...
}

 

OpenFeign 사용해보기

이제 OpenFeign을 사용해봅시다.  아래와 같이 SpringBootApplication이 실행될 때 @EnableFeignClients 어노테이션을 붙여줍니다. 이는 추후에 @FeignClient를 만들건데 해당 어노테이션을 찾아서 구현체를 만들어주는 역할을 수행합니다.

@EnableFeignClients
@SpringBootApplication
public class CatPictureApplication {

	public static void main(String[] args) {
		SpringApplication.run(CatPictureApplication.class, args);
	}

}
더보기

※ @JpaAuditing 사용할 때도 Application에 어노테이션을 붙여서 읽어들이게 되는데 @Configuration을 이용해서 따로 클래스로 설정파일을 빼낼 수도 있습니다. 저는 이런 방식을 사용했을 때는 스프링 부트를 돌리지 않는 경우에도 필요한 경우가 있는 경우에 예를 들면 테스트에서 스프링 부트없이 사용한다던가와 같은 경우에 따로 빼내서 사용을 했습니다.

위에서 말했듯이 OpenFeign은 웹 서비스 클라이언트이기 때문에 서비스로 접근할 클라이언트를 만들어줍니다.

@FeignClient(name = "catPictureClient", url = "${external.cat-service.host}", configuration = HeaderConfiguration.class)
public interface CatPictureClient {

	@GetMapping("/images/{id}")
	ExternalCatPictureApiResponseDto getCatPicture(@PathVariable String id);

	@ExternalApiCheck
	@GetMapping("/images/search")
	List<ExternalCatPictureApiResponseDto> getCatPicturesHavingBreeds(
		@RequestParam Integer limit,
		@RequestParam int has_breeds);

}

FeignClient를 만들어주고 url은 yml 파일을 이용해서 환경변수를 넣어줬습니다. 그리고 요구하는 API상에서 header에 특정 값을 넣어줘야 했기 때문에 HeaderConfiguration을 따로 빼서 구현했습니다.

@Configuration
public class HeaderConfiguration {

	@Bean
	public RequestInterceptor requestInterceptor(@Value("${external.cat-service.key}") String key) {
		return requestTemplate -> requestTemplate.header("x-api-key", key);
	}
}

여기서고 key 값을 yml을 이용해서 넣어주는 모습입니다. 그리고 간단한 테스트 코드를 통해서 검증을 진행했습니다.

@SpringBootTest
public class GetCatCatPictureTest {

	@MockBean
	CommandLineRunner mockCommandLineRunner;

	@Autowired
	CatPictureController catPictureController;

	@Autowired
	CatPictureRepository catPictureRepository;

	private final ObjectMapper mapper = new ObjectMapper();

	@Test
	@Transactional
	void ID를_이용하여_고양이_사진을_조회할_수_있다() throws JsonProcessingException {
		// given
		CatPicture catPicture = TestUtils.createCatPicture();
		catPictureRepository.save(catPicture);
		String id = TestUtils.ID;

		// when
		ResponseEntity<ApiResponse> response = catPictureController.getCatPicture(id);
		String actualJson = mapper.writeValueAsString(Objects.requireNonNull(response.getBody()).data());
		CatPictureDetailResponseDto apiResponse = mapper.readValue(actualJson, CatPictureDetailResponseDto.class);

		// then
		assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
		assertThat(apiResponse).usingRecursiveComparison().isEqualTo(catPicture);
	}
}

 

이전에 짰던 API 호출하는 코드 살펴보기

제가 이전에 Hype이라는 프로젝트를 진행했을 때 코드 상에서 API를 호출하는 웹 서비스의 클라이언트가 되어야 하는 그런 경우가 있었는데 그 때 저는 아래와 같이 코드를 짰습니다.

@Service
@RequiredArgsConstructor
public class ItunesMusicSearchService implements MusicSearchService {

	private final ObjectMapper objectMapper;

	@Override
	public MusicSearchResponseDto search(String term) {
		HttpClient httpClient = HttpClients.createDefault();
		try {
			String requestUrl = String.format(
				"https://itunes.apple.com/search?term=%s&country=KR&media=music",
				URLEncoder.encode(term, StandardCharsets.UTF_8));

			HttpGet httpGet = new HttpGet(requestUrl);
			HttpResponse response = httpClient.execute(httpGet);
			int httpStatusCode = response.getStatusLine().getStatusCode();

			if (httpStatusCode < 200 || httpStatusCode >= 300) {
				throw new IOException(FAIL_SEARCH_MUSIC.getMessage());
			}

			JsonNode jsonNode = objectMapper.readTree(response.getEntity().getContent());
			ArrayNode results = (ArrayNode)jsonNode.get("results");
			List<MusicSearchResponseVo> convertJsonToDto = Arrays.asList(
				objectMapper.convertValue(
					results,
					MusicSearchResponseVo[].class));

			List<MusicSearchResponseVo> convertJsonToDtoNotBlankMusicUrl = convertJsonToDto.stream()
				.filter(musicSearchResponseVo ->
					Objects.nonNull(musicSearchResponseVo.previewUrl())
						&& !musicSearchResponseVo.previewUrl().isBlank()
				)
				.toList();

			return MusicSearchResponseDto.of(
				convertJsonToDtoNotBlankMusicUrl
			);
		} catch (IOException exception) {
			throw new IllegalArgumentException(NOT_VALID_TERM.getMessage());
		}
	}
}

HttpClient를 이용해서 응답에 대한 요청을 받아오고 이를 객체에 매핑 시키기 위해서 ObjectMapper를 이용해서 구현했습니다. try-catch문도 발생하고 OpenFeign으로 짠 코드보다 훨씬 가독성도 떨어지고 지저분한 것을 확인할 수 있습니다.


마무리

현업에 가면 외부 API와 연동하는 일들이 많다고 멘토님께서 말씀하셨는데 그런 것들을 경험할 수 있어서 좋았고 처음 써봤는데 확실히 다른 툴들에 비해서 OpenFeign가 익숙한 방법으로 JPA 이용하는 것처럼 짤 수 있어서 좋았던 것 같습니다.

 

Reference

https://techblog.woowahan.com/2630/

 

우아한 feign 적용기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 저는 비즈인프라개발팀에서 개발하고 있는 고정섭입니다. 이 글에서는 배달의민족 광고시스템 백엔드에서 feign 을 적용하면서 겪었던 것들에 대해서 공유 하고자 합니다

techblog.woowahan.com

https://isntyet.github.io/java/feign-client-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0/