
저번에 외부 API를 호출해서 검색하고자 한 지역의 대표 이미지를 가져오는 작업을 했다. 원래는 해당 API를 쓰이는 곳이 한 군데였는데, 프론트측과 이야기해보니 사용될 곳이 더 있었다. 어느 서비스 코드 하나에 종속시키고 사용하기 애매할 것 같아서 LocateUtil로 빼서 사용하기로 했다.
public class LocateUtil {
@Value("${pixabay.api.key}")
private static String apiKey;
public static String getLocateImage(Country country, City city) {
String url = "https://pixabay.com/api/";
String searchKeyword = country.toString().concat(" ").concat(city.toString());
WebClient webClient = WebClient.create(url);
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("q", searchKeyword)
.queryParam("lang","ko")
.queryParam("key", apiKey)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
return parseResponseToImageUrl(response);
}
private static String parseResponseToImageUrl(String response) {
JSONParser parser = new JSONParser();
JSONObject object;
try {
object = (JSONObject)parser.parse(response);
} catch (ParseException e) {
throw new RuntimeException(e);
}
JSONArray hits = (JSONArray)object.get("hits");
JSONObject hitBody = (JSONObject)hits.get(0);
return (String)hitBody.get("largeImageURL");
}
}
이렇게 빼내서 돌렸는데 자꾸 에러가 발생한다.
...
org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET https://pixabay.com/api/?q=%EB%B8%8C%EB%9D%BC%EC%A7%88%20%EC%83%81%ED%8C%8C%EC%9A%B8%EB%A3%A8&lang=ko&key
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:307) ~[spring-webflux-6.1.2.jar:6.1.2]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ 400 BAD_REQUEST from GET https://pixabay.com/api/ [DefaultWebClient]
...
외부 API를 호출하는데에 BAD_REQUEST가 발생했다는 내용이다. 호출된 URL을 보니 key 파라미터가 비어있어서 디버깅하니 apiKey가 null이었다. 난 @Value로 API key 주입도 해줬는데 뭐가 다르다고 안되는걸까?
Util class는 보통 다른 객체에서 공통적으로 참조하는 값을 여기에 넣음으로서 객체 생성 없이 독립적인 기능을 하는 것들에 대해 빼놓는 클래스로 생성된다.
이번에 생성한 Util class도 LocateUtil.getLocateImage() 처럼 독단적인 메서드로서 쓰이도록 static 메서드로 빼고자 했다.
getLocateImage 메서드를 static으로 빼면 참조할 수 있는 메서드 또한 static으로 선언해야 한다. 따라서 apiKey 또한 static으로 선언했다.
그런데 여기에 @Value 애노테이션을 사용한 것이 문제였다.
@Value("${pixabay.api.key}")
private static String apiKey;
static 필드는 jvm 메모리 영역 중 하나인 Class Area(Static area, Method area)에 저장된다. 해당 시점은 스프링 컨테이너인 ApplicationContext가 로드되기 전이다. @Value 애노테이션은 스프링 컨테이너에 의존적이기 때문에 static 필드인 apiKey에 정상적으로 @Value가 작동하지 않은 것이다.
자주 보이는 방법으로 setter 메서드를 사용해 static 필드에 값을 주입한다고 한다.
private static String apiKey;
@Value("${pixabay.api.key}")
public static void setApiKey(String key) {
apiKey = key;
}
근데 위처럼 하면 결국 setter 인자로 들어오는 값을 위해 외부에서 해당 API key를 넣어줘야 하는건 마찬가지이므로 내 코드상에서는 필요가 없을 것 같았다.
그래서 결국 getLocateImage 메서드의 인자로 API key를 같이 받아서 사용하는 것이 나을거라 생각했다.
public static String getLocateImage(String apiKey, Country country, City city) {
...
뭐 다른 방법들도 많긴 하던데.. 내 로직에서는 이게 최선인 것 같다. 더 좋은 방법을 알게 된다면 도입 해보고싶다.