[Spring] Rest 통신으로 공공 API 연결해보기

2025. 8. 19. 21:09Backend/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();

}