2025. 8. 19. 21:09ㆍBackend/Spring

📝 Spring에서 외부 API를 연동하는 방법들이 궁금하여, 구현해보며 정리한 글입니다.
데이터 통신 방식은 REST, WebSocket, gRPC, MQTT, Serverless, GraphQL 등 매우 다양합니다. 그 중에서 REST(representational State Transfer) 은 HTTP 프로토콜 기반의 통신이며, 클라이언트와 서버가 통신할 수 있게 하는 통신 방식입니다.
Spring 프레임워크에서는 이러한 REST 통신을 구현할 수 있는 여러 도구를 제공합니다. 대표적으로 RestTemplate, FeignClient, 그리고 OkHttp, Retrofit 과 같은 외부 라이브러리를 활용한 방식들이 있습니다.
아래 예시는 GET 메서드를 사용하여 공공 데이터 포털(https://www.data.go.kr) 의 단기 예보 API 중 일부 정보를 조회하는 코드이며, REST 통신 방식으로 필요한 데이터만 가져오는 방식으로 최대한 간단하게 구현하였습니다(유효성 검증 X).
공공데이터 포털
국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase
www.data.go.kr
먼저, 통신을 하기 위해 엔드포인트를 확인하였고, api key를 발급 받았습니다.
application.yml 일부 (예시)
openApi:
weather:
url: https://도메인/00000/service
typeC: /sample
apiKey:
이후 데이터 요청하거나 데이터를 응답할 수 있도록 DTO 클래스를 작성하였습니다.
WeatherRequest.class (요청)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeatherRequest {
private String baseDate;
private String baseTime;
private String x;
private String y;
}
WeatherResponse.class (응답)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class WeatherResponse {
private String category;
private String fcstDate;
private String fcstTime;
private String fcstValue;
}
1. RestTemplate
WeatherController 일부
@GetMapping("/restTemplate")
public ResponseEntity<?> getWeatherByRestTemplate(WeatherRequest req) {
WeatherResponse response = weatherService.getWeatherByRestTemplate(req);
return ResponseEntity.ok(response);
}
RestTemplateConfig
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(60))
.readTimeout(Duration.ofSeconds(60))
.build();
}
}
WeatherService 일부
public WeatherResponse getWeatherByRestTemplate(WeatherRequest req) {
/* 이중 인코딩 방지 */
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
restTemplate.setUriTemplateHandler(uriBuilderFactory);
String request = UriComponentsBuilder.fromUriString(url + typeC)
.queryParam("ServiceKey", apiKey)
.queryParam("pageNo", pageNo)
.queryParam("numOfRows", numOfRows)
.queryParam("dataType", "JSON")
.queryParam("base_date", req.getBaseDate())
.queryParam("base_time", req.getBaseTime())
.queryParam("nx", req.getX())
.queryParam("ny", req.getY())
.build(false).toUriString();
ResponseEntity<JsonNode> entity = restTemplate.getForEntity(request, JsonNode.class);
JsonNode node = entity.getBody().path("response").path("body").path("items").path("item").get(0);
return WeatherResponse.builder()
.category(node.get("category").asText())
.fcstDate(node.get("fcstDate").asText())
.fcstTime(node.get("fcstTime").asText())
.fcstValue(node.get("fcstValue").asText()).build();
}
✨ 공공 API를 이용하기 위해 발급받은 api key 를 사용할 때, 인코딩 문제가 발생하였습니다. 예를 들어, %가 %25로 계속 변환되는 겁니다. 그래서 이를 방지하고자 DefaultBuilderFactory를 이용하여, 해당 문제를 해소하였습니다.
2. RestClient (spring boot 3.2~)
RestClientConfig
public class RestClientConfig {
@Bean
public RestClient restClient(RestClient.Builder builder) {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Duration.ofSeconds(60));
factory.setReadTimeout(Duration.ofSeconds(60));
return builder
.requestFactory(factory)
.build();
}
}
WeatherController 일부
@GetMapping("/restclient")
public ResponseEntity<?> getWeatherByRestClient(WeatherRequest req) {
WeatherResponse response = weatherService.getWeatherByRestClient(req);
return ResponseEntity.ok(response);
}
WeatherService 일부
public WeatherResponse getWeatherByRestClient(WeatherRequest req) {
/* 이중 인코딩 방지 */
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory();
uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
restTemplate.setUriTemplateHandler(uriBuilderFactory);
String request = UriComponentsBuilder.fromUriString(url + typeC)
.queryParam("ServiceKey", apiKey)
.queryParam("pageNo", pageNo)
.queryParam("numOfRows", numOfRows)
.queryParam("dataType", "JSON")
.queryParam("base_date", req.getBaseDate())
.queryParam("base_time", req.getBaseTime())
.queryParam("nx", req.getX())
.queryParam("ny", req.getY())
.build(false).toUriString();
URI uri = URI.create(request);
ResponseEntity<JsonNode> entity = restClient.get().uri(uri)
.accept(org.springframework.http.MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(JsonNode.class);
JsonNode node = entity.getBody().path("response").path("body").path("items").path("item").get(0);
return WeatherResponse.builder()
.category(node.get("category").asText())
.fcstDate(node.get("fcstDate").asText())
.fcstTime(node.get("fcstTime").asText())
.fcstValue(node.get("fcstValue").asText()).build();
}
3. okhttp
build.gradle
implementation "com.squareup.okhttp3:okhttp:4.12.0"
OkhttpConfig
@Configuration
public class OkHttpConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(60))
.readTimeout(Duration.ofSeconds(60))
.build();
}
}
WeatherController 일부
@GetMapping("/okhttp")
public ResponseEntity<?> getWeatherByOkHttp(WeatherRequest req) {
WeatherResponse response = weatherService.getWeatherByOkHttp(req);
return ResponseEntity.ok(response);
}
WeatherService 일부
public WeatherResponse getWeatherByOkHttp(WeatherRequest req) {
HttpUrl request = new HttpUrl.Builder()
.scheme("https").host("도메인")
.addPathSegment("00000")
.addPathSegment("service")
.addPathSegment("sample")
.addEncodedQueryParameter("ServiceKey", apiKey) //인코딩 방지
.addQueryParameter("pageNo", String.valueOf(pageNo))
.addQueryParameter("numOfRows", String.valueOf(numOfRows))
.addQueryParameter("dataType", "JSON")
.addQueryParameter("base_date", req.getBaseDate())
.addQueryParameter("base_time", req.getBaseTime())
.addQueryParameter("nx", req.getX())
.addQueryParameter("ny", req.getY()).build();
Request okHttpRequest = new Request.Builder().url(request).get().build();
WeatherResponse weatherResponse = null;
JsonNode node = null;
try(Response okHttpResponse = okHttpClient.newCall(okHttpRequest).execute()) {
JsonNode responseNode = objectMapper.readTree(okHttpResponse.body().string());
node = responseNode.path("response").path("body").path("items").path("item").get(0);
} catch (IOException e) {
log.error("[ERROR/TEST] okhttp IOException 에러 발생");
}
System.out.println("node = " + node);
return weatherResponse.builder()
.category(node.get("category").asText())
.fcstDate(node.get("fcstDate").asText())
.fcstTime(node.get("fcstTime").asText())
.fcstValue(node.get("fcstValue").asText()).build();
}
✨ 공공 API를 이용하기 위해 발급받은 api key 를 사용할 때, 인코딩 문제가 발생하였습니다. HttpUrl을 만드는 과정에서 .addEncodedQueryParameter()를 사용하여, %가 %25로 바뀌는 문제를 해소하였습니다.
4. Retrofit
build.gradle
implementation "com.squareup.retrofit2:retrofit:2.11.0"
implementation "com.squareup.retrofit2:converter-jackson:2.11.0"
WeatherController 일부
@GetMapping("/retrofit")
public ResponseEntity<?> getWeatherByRetrofit(WeatherRequest req) {
WeatherResponse response = weatherService.getWeatherByRetrofit(req);
return ResponseEntity.ok(response);
}
RetrofitConfig
@Configuration
public class RetrofitConfig {
@Bean
public Retrofit retrofit(OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.baseUrl("https://도메인/")
.addConverterFactory(JacksonConverterFactory.create())
.client(okHttpClient)
.build();
}
@Bean
public WeatherRetrofitApi weatherRetrofitApi(Retrofit retrofit) {
return retrofit.create(WeatherRetrofitApi.class);
}
}
WeatherRetrofitApi 인터페이스
public interface WeatherRetrofitApi {
@Headers("Accept: application/json")
@GET("00000/service/sample")
Call<JsonNode> getWeatherData (
@Query(value = "ServiceKey", encoded = true) String serviceKey,
@Query("pageNo") String pageNo,
@Query("numOfRows") String numOfRows,
@Query("dataType") String dataType,
@Query("base_date") String baseDate,
@Query("base_time") String baseTime,
@Query("nx") String nx,
@Query("ny") String ny
);
}
✨ 공공 API를 이용하기 위해 발급받은 api key 를 사용할 때, 인코딩 문제가 발생하였습니다. @Query(~~~, encoded = true)를 추가하여, %가 %25로 변환되는 문제를 해소하였습니다.
WeatherService 일부
public WeatherResponse getWeatherByRetrofit(WeatherRequest req) {
Call<JsonNode> call = weatherRetrofitApi.getWeatherData(
apiKey, String.valueOf(pageNo), String.valueOf(numOfRows),
"JSON", req.getBaseDate(), req.getBaseTime(), req.getX(), req.getY()
);
Request testReq = call.request();
JsonNode node = null;
try {
retrofit2.Response<JsonNode> response = call.execute();
node = response.body().path("response").path("body").path("items").path("item").get(0);
System.out.println("node = " + node);
} catch (IOException e) {
log.error("[ERROR/TEST] okhttp IOException 에러 발생");
e.printStackTrace();
}
return WeatherResponse.builder()
.category(node.get("category").asText())
.fcstDate(node.get("fcstDate").asText())
.fcstTime(node.get("fcstTime").asText())
.fcstValue(node.get("fcstValue").asText()).build();
}
5. FeignClient (Spring Cloud)
build.gradle
implementation "org.springframework.cloud:spring-cloud-starter-openfeign"
WeatherController 일부
@GetMapping("/feignclient")
public ResponseEntity<?> getWeatherByFeignClient(WeatherRequest req) {
WeatherResponse response = weatherService.getWeatherByFeignClient(req);
return ResponseEntity.ok(response);
}
WeatherFeignApi 인터페이스
@FeignClient(name = "feignClient", url = "https://도메인/")
public interface WeatherFeignApi {
@GetMapping("/00000/service/sample")
String getWeatherDataFeign (
@RequestParam("ServiceKey") String serviceKey,
@RequestParam("pageNo") String pageNo,
@RequestParam("numOfRows") String numOfRows,
@RequestParam("dataType") String dataType,
@RequestParam("base_date") String baseDate,
@RequestParam("base_time") String baseTime,
@RequestParam("nx") String nx,
@RequestParam("ny") String ny
);
}
WeatherService 일부
public WeatherResponse getWeatherByFeignClient(WeatherRequest req) {
String response = weatherFeignApi.getWeatherDataFeign(
apiKey, String.valueOf(pageNo), String.valueOf(numOfRows),
"JSON", req.getBaseDate(), req.getBaseTime(), req.getX(), req.getY()
);
JsonNode node = null;
try {
JsonNode responseNode = objectMapper.readTree(response);
node = responseNode.path("response").path("body").path("items").path("item").get(0);
} catch (IOException e) {
log.error("[ERROR/TEST] okhttp IOException 에러 발생");
}
return WeatherResponse.builder()
.category(node.get("category").asText())
.fcstDate(node.get("fcstDate").asText())
.fcstTime(node.get("fcstTime").asText())
.fcstValue(node.get("fcstValue").asText()).build();
}