소개

  • Spring: 자바 플랫폼을 위한 오픈 소스 프레임워크로, 2003년에 Rod Johnson 의해 처음 발표되었다. 
  • SpringBoot: Spring Framework 위에서 구축된 독립적인(stand-alone), 생산 수준의 Spring 기반 애플리케이션을 쉽게 만들 있도록 돕는 도구

공통점

  • Java로 개발된 애플리케이션을 만들기 위한 프레임워크

라이브러리: 개발자가 코드의 흐름을 제어하며 필요한 시점에서 호출하여 사용한다.
프레임워크: 개발자가 아닌 프레임워크가 애플리케이션의 흐름을 제어하며, 애플리케이션의 전반적인 구조를 제공하며 개발자는 정해진 구조 위에서 기능을 추가하게 된다. 

차이점

    • 설정과 구성
      • Spring Framework: Spring 애플리케이션을 설정하고 구성하는 데에 XML 기반 또는 주석 시반 설정으로 복잡한 부분이 있어 많은 시간이 소요된다.
      • Spring Boot: opinionated defaults 기법을 통해 기본 설정을 제공받아 설정을 자동화하여 개발자가 별도의 설정을 하지 않아도 바로 애플리케이션을 실행할 수 있게 해준다. 상대적으로 적은 시간이 소요된다.
    • 서버 설정
      • Spring Framework: 별도의 내장 서버를 포함하지 않기 때문에 개발자가 별도로 웹 서버를 설치하고 구성해야 한다.
      • Spring Boot: 내장 Tomcat, Jetty, Undertow 등의 서버를 제공하므로 웹 서버를 따로 설치하거나 구성할 필요가 없다.
    • 의존성 관리
      • Spring Framework: 개발자가 필요한 모든 라이브러리의 의존성을 수동으로 관리해야 한다.
      • Spring Boot: 스타터(starter)종속성을 제공하여, 프레임워크 자체에서 관련된 의존성 그룹을 자동으로 가지고 와서 설치해준다.
    • 프로젝트 배포
      • Spring Framework: WAR 파일로 패키지화하여 별도의 서버에 배포하여야 한다.
      • Spring Boot: JAR 파일로 패키지화하여 자체 내장 서버를 통해 쉽게 실행하고 배포할 수 있다. 
      • ( https://suhanlim.tistory.com/192 )배포 및 WAR, JAR 파일 설명 글
    • 개발 및 시장 출시 시간
      • Spring Framework: 설정, 의존성 관리 등으로 인해 개발 및 배포 시간이 상대적으로 길 수 있다.
      • Spring Boot: 자동 설정, 내장 서버, 스타터 종속성 등으로 인해 빠르게 애플리케이션을 개발하고 배포 할 수 있다.
    • SpringBoot에서 추가된 기능
      • Actuator: Spring Boot Actuator 애플리케이션의 상태를 모니터링하고 관리하는 기능을 제공한다. HTTP 엔드포인트나 JMX 통해 애플리케이션의 다양한 정보(: 메트릭, 헬스 체크, 환경 설정 ) 확인할 있다.
      • ( https://suhanlim.tistory.com/193 )사용 방법 정리 글
      • YAML: YAML형식의 설정 파일을 지원한다.

언제 사용해야 하는가?

  • Spring Framework: 복잡한 애플리케이션에 대해 많은 제어가 필요하거나 특정 설정을 최적화하려는 경우나, 학습 목적으로 Spring 내부 동작을 이해하려는 경우에 사용하는게 적합하다.
  • Spring Boot: 빠른 프로토타이핑이나 마이크로서비스 아키텍처와 같이 빠른 개발과 배포가 중요한 상황에서 적합하다. 설정의 복잡성을 최소화하고 빠르게 실행 가능한 애플리케이션을 만들 때 사용하면 좋다.

공공 데이터 포털이란?

공공데이터포털은 공공기관이 생성 또는 취득하여 관리하고 있는 공공데이터를 한 곳에서 제공하는 통합 창구이다.

공공데이터포털에서는 국민이 쉽고편리하게 공공데이터를 이용할 수 있도록 파일데이터, 오픈API, 시각화 등 다양한 방식으로 제공하고 있으며, 누구라도 쉽고 편리한 검색을통해 원하는 공공데이터를 빠르고 정확하게 찾을 수 있다. 

https://www.data.go.kr/index.do

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

사용하기 위한 필요 과정

정말 간단하다. 사이트에 회원 가입 후 사용하길 희망하는 오픈 API에 활용신청 하여 API 키를 발급받으면 된다. 
주의할 점이 있다면 대부분 API는 상시 허가로 되어 있어 신청하자마자 바로 사용할 수 있지만 몇몇 API와 데이터 같은 경우에는 신청 후 승인까지의 대기시간이 있을 수 있다는 점이다.

 

사용하기 전 미리보기 기능

마이페이지 -> API 신청 -> 신청한 API -> 미리보기 버튼을 활용하여 Rest API 방식으로 요청을 보내는 파라미터들을 임의로 조작하여 미리 어떤 결과가 나오는지 확인이 가능하다.

만약 공공데이터 사용이 처음이라면 미리보기 기능을 통해 어디까지 활용이 가능하고 어떻게 활용해야 겠다 계획을 세우고 하는 것을 추천한다.

<미리 보기 한 결과>

 

데이터 포털에서 제공해주는 Java 1.8 샘플 코드

import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.io.BufferedReader;
import java.io.IOException;

public class ApiExplorer {
    public static void main(String[] args) throws IOException {
        StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList"); /*URL*/
        urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=서비스키"); /*Service Key*/
        urlBuilder.append("&" + URLEncoder.encode("pageNo","UTF-8") + "=" + URLEncoder.encode("1", "UTF-8")); /*페이지번호*/
        urlBuilder.append("&" + URLEncoder.encode("numOfRows","UTF-8") + "=" + URLEncoder.encode("3", "UTF-8")); /*한 페이지 결과 수*/
        urlBuilder.append("&" + URLEncoder.encode("entpName","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*업체명*/
        urlBuilder.append("&" + URLEncoder.encode("itemName","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*제품명*/
        urlBuilder.append("&" + URLEncoder.encode("itemSeq","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*품목기준코드*/
        urlBuilder.append("&" + URLEncoder.encode("efcyQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약의 효능은 무엇입니까?*/
        urlBuilder.append("&" + URLEncoder.encode("useMethodQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약은 어떻게 사용합니까?*/
        urlBuilder.append("&" + URLEncoder.encode("atpnWarnQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약을 사용하기 전에 반드시 알아야 할 내용은 무엇입니까?*/
        urlBuilder.append("&" + URLEncoder.encode("atpnQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약의 사용상 주의사항은 무엇입니까?*/
        urlBuilder.append("&" + URLEncoder.encode("intrcQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약을 사용하는 동안 주의해야 할 약 또는 음식은 무엇입니까?*/
        urlBuilder.append("&" + URLEncoder.encode("seQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약은 어떤 이상반응이 나타날 수 있습니까?*/
        urlBuilder.append("&" + URLEncoder.encode("depositMethodQesitm","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*이 약은 어떻게 보관해야 합니까?*/
        urlBuilder.append("&" + URLEncoder.encode("openDe","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*공개일자*/
        urlBuilder.append("&" + URLEncoder.encode("updateDe","UTF-8") + "=" + URLEncoder.encode("", "UTF-8")); /*수정일자*/
        urlBuilder.append("&" + URLEncoder.encode("type","UTF-8") + "=" + URLEncoder.encode("xml", "UTF-8")); /*응답데이터 형식(xml/json) Default:xml*/
        URL url = new URL(urlBuilder.toString());
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Content-type", "application/json");
        System.out.println("Response code: " + conn.getResponseCode());
        BufferedReader rd;
        if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
            rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        } else {
            rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
        }
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = rd.readLine()) != null) {
            sb.append(line);
        }
        rd.close();
        conn.disconnect();
        System.out.println(sb.toString());
    }
}

해당 코드를 그대로 가져와서 수정하여 사용하여도 되지만 가급적 최신 자바 코드 라이브러리와 스프링 부트의 라이브러리를 활용하는 방법으로 코드를 수정하고자 한다.

 

최신 자바 라이브러리(HttpClient) 사용 버전

@RestController
public class DrugOpenAPIController {
    @GetMapping("/publicData")
    public String getDrugInfo() throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        String url = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
        String charset = StandardCharsets.UTF_8.name();
        String query = String.format("serviceKey=%s&pageNo=%s&numOfRows=%s&entpName=%s&itemName=%s&itemSeq=%s&efcyQesitm=%s&useMethodQesitm=%s&atpnWarnQesitm=%s&atpnQesitm=%s&intrcQesitm=%s&seQesitm=%s&depositMethodQesitm=%s&openDe=%s&updateDe=%s&type=%s",
                URLEncoder.encode("Decoding 인증키", charset),
                URLEncoder.encode("1", charset),
                URLEncoder.encode("1", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("", charset),
                URLEncoder.encode("json", charset));

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url + "?" + query))
                .GET()
                .header("Content-Type", "application/json")
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("Response code: " + response.statusCode());
        HttpHeaders headers = response.headers();
        headers.map().forEach((k, v) -> System.out.println(k + ":" + v));

        String responseBody = response.body();

        // Jackson 객체 생성
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);

        // JSON 문자열을 Map 객체로 변환
        Map<String, Object> map = mapper.readValue(responseBody, Map.class);

        // 'body' key에 해당하는 부분을 추출
        Map<String, Object> bodyMap = (Map<String, Object>) map.get("body");

        // 'items' key에 해당하는 부분을 추출
        List<Map<String, Object>> items = (List<Map<String, Object>>) bodyMap.get("items");

        // items를 JSON 문자열로 변환
        String prettyItemsJson = mapper.writeValueAsString(items);

        System.out.println(prettyItemsJson); // 수정된 JSON 출력

        return prettyItemsJson;
    }
}

해당 코드에서는 Jackson 객체를 사용하여 응답 body 부분의 item 항목의 데이터를 파싱 하는 것 까지 추가해보았다.

 

스프링 부트에서 권장하는 라이브러리(WebClient) 사용 버전

<필요한 의존성> https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux/3.1.0

implementation 'org.springframework.boot:spring-boot-starter-webflux:3.1.0'

<블록킹 방식>

@RestController
public class DrugOpenAPIController {
    private final static String BASE_URL = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
    private final String API_KEY = "encoding API키";

    @GetMapping(value = "/publicData", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> getDrugInfo() {
        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        WebClient webClient = WebClient.builder()
                .uriBuilderFactory(factory)
                .baseUrl(BASE_URL)
                .build();

        ResponseEntity<String> response = webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .queryParam("serviceKey", API_KEY)
                        .queryParam("pageNo", "1")
                        .queryParam("numOfRows", "1")
                        .queryParam("entpName", "")
                        .queryParam("itemName", "")
                        .queryParam("itemSeq", "")
                        .queryParam("efcyQesitm", "")
                        .queryParam("useMethodQesitm", "")
                        .queryParam("atpnWarnQesitm", "")
                        .queryParam("atpnQesitm", "")
                        .queryParam("intrcQesitm", "")
                        .queryParam("seQesitm", "")
                        .queryParam("depositMethodQesitm", "")
                        .queryParam("openDe", "")
                        .queryParam("updateDe", "")
                        .queryParam("type", "json")
                        .build())
                .retrieve()
                .toEntity(String.class).block();

        return response;
    }
}

<논 블록킹 방식>

@RestController
public class DrugOpenAPIController {
    private final static String BASE_URL = "http://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
    private final String API_KEY = "encoding API키";

    @GetMapping(value = "/publicData", produces = MediaType.APPLICATION_JSON_VALUE)
    public Mono<String> getDrugInfo() {
        DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

        WebClient webClient = WebClient.builder()
                .uriBuilderFactory(factory)
                .baseUrl(BASE_URL)
                .build();

        Mono<String> response = webClient.get()
                .uri(uriBuilder -> uriBuilder
                        .queryParam("serviceKey", API_KEY)
                        .queryParam("pageNo", "1")
                        .queryParam("numOfRows", "1")
                        .queryParam("entpName", "")
                        .queryParam("itemName", "")
                        .queryParam("itemSeq", "")
                        .queryParam("efcyQesitm", "")
                        .queryParam("useMethodQesitm", "")
                        .queryParam("atpnWarnQesitm", "")
                        .queryParam("atpnQesitm", "")
                        .queryParam("intrcQesitm", "")
                        .queryParam("seQesitm", "")
                        .queryParam("depositMethodQesitm", "")
                        .queryParam("openDe", "")
                        .queryParam("updateDe", "")
                        .queryParam("type", "json")
                        .build())
                .retrieve()
                .bodyToMono(String.class);

        return response;
    }
}
첫 번째 방식(ResponseEntity<String>반환)에서
.toEntity(String.class).block(); 구문을 사용하고 있는데, 이는 호출한 스레드가 데이터를 수신할 때까지 대기하게 하며, 데이터가 도착하면 작업을 계속 진행합니다.
이 방식은 쉽고 직관적인 프로그래밍을 가능하게 하지만, I/O 작업이 많거나 네트워크 지연이 발생할 경우 대기 시간이 길어지고, 이로 인해 전체적인 시스템 성능이 저하될 수 있습니다.
두 번째 방식(Mono<String>반환)은
논블로킹 방식으로, 호출한 스레드가 결과를 기다리지 않고 바로 반환합니다.
이 방식은 리액티브 프로그래밍 패러다임을 따르며, 데이터가 준비될 때까지 다른 작업을 계속 수행할 수 있습니다.
이로 인해 리소스를 효율적으로 사용하고, 시스템 성능을 개선할 수 있습니다.
단, 이 방식은 프로그래밍이 복잡해질 수 있으며, 비동기 프로그래밍에 익숙하지 않은 개발자들에게는 이해하기 어려울 수 있습니다.

 

결론: 블로킹 방식은 쉽고 직관적이지만, 대기 시간이 길어질 수 있으며, 논블로킹 방식은 복잡하지만, 성능 향상과 리소스 효율성을 얻을 수 있다.

주의 사항 (Content-Type 체크)

스프링 프레임워크는 기본적으로 application/json 타입으로 받는 것을 기대하기 때문에

아래와 같이 Content-Type이 application/json 이면 상관 없으나 

해당 사진 처럼 간혹 공공포털 api 에서 text/json으로 반환 해주는 경우가 있어 

해당 경우에는 추가적으로 다음 로직을 추가하여야 한다.

	ExchangeStrategies strategies = ExchangeStrategies
                .builder()
                .codecs(configurer -> configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.valueOf("text/json"))))
                .build();

        WebClient webClient = WebClient.builder()
                .baseUrl(BASE_URL)
                .exchangeStrategies(strategies)
                .build();

 

JSON Viwer 확장 플러그인을 사용하여 깔끔하게 표시되는 결과

WebClient란?

WebClient는 Spring 5에서 도입된 비동기적인 방식의 HTTP 프로토콜을 통해 요청을 보낼 때 사용하는 클래스로 기존에 사용되었던 동기적 방식의 RestTemplate를 대체하여 사용되며 블로킹, 논블로킹 I/O 작업을 지원한다. 

(위에 코드에서는 block(); 메소드를 통해 블로킹 I/O 작업을 사용하였다.)

 

<정리가 잘 되어있는 링크>

https://velog.io/@dnrwhddk1/Spring-Web-Client-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EC%82%AC%EC%9A%A9

+ Recent posts