본문 바로가기

개발기술/Web Dev

외부 API 활용 (Pure Java, Webclient, 최적화)과 직렬화/역직렬화 Parsing

API란?

Application programming Interface, 어플리케이션의 프로그래밍 통신수단

  일반적으로 OPEN API는 브라우저 웹페이지 로딩과 유사하게 HTTP프로토콜의 request와 response 방식으로 이루어진다. 단지, 전달되는 컨텐츠가 html인지 혹은 데이터(json 등)인지의 차이가 있음.

 

사용할 API 선택시 고려사항

API documentation 상세확인을 통해서 아래와 같은 질문에 답해본다.

  • API가 내가 원하는 얼만큼의 세부적인 query가 가능한가 ? (request)
  • API가 사용하기는 편한가? 유료인가 혹은 무료인가
  • API의 호출결과가 쓸만한지? (response)
    • 데이터 타입은 사용할만한지? 
    • 제공하는 데이터가 얼마나 상세한가?

 

API 요청방식(1)  Pure JAVA  :  HttpURLConnection

출처 : 기상청_단기예보 ((구)_동네예보) 조회서비스 https://www.data.go.kr/data/15084084/openapi.do

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 Main {
    public static void main(String[] args) throws IOException {
        // Stringbuilder을 사용하여 URL에다 ?를 붙이고 필요한 parameter key와 value를 붙임
        StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst"); /*URL*/
        urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=tQqvr9aPLijnRiuJq0BR2gXuUYRTFDE919UCE4MnQ4iaDl20qpXFeLPQObvamAp2YtsTPy6PSZJAGbPZ%2Fydihw%3D%3D"); /*Service Key*/
        urlBuilder.append("&" + URLEncoder.encode("pageNo","UTF-8") + "=" + URLEncoder.encode("1", "UTF-8")); /*페이지번호*/
        urlBuilder.append("&" + URLEncoder.encode("numOfRows","UTF-8") + "=" + URLEncoder.encode("1000", "UTF-8")); /*한 페이지 결과 수*/
        urlBuilder.append("&" + URLEncoder.encode("dataType","UTF-8") + "=" + URLEncoder.encode("XML", "UTF-8")); /*요청자료형식(XML/JSON) Default: XML*/
        urlBuilder.append("&" + URLEncoder.encode("base_date","UTF-8") + "=" + URLEncoder.encode("20240703", "UTF-8")); /*‘21년 6월 28일 발표*/
        urlBuilder.append("&" + URLEncoder.encode("base_time","UTF-8") + "=" + URLEncoder.encode("0600", "UTF-8")); /*06시 발표(정시단위) */
        urlBuilder.append("&" + URLEncoder.encode("nx","UTF-8") + "=" + URLEncoder.encode("55", "UTF-8")); /*예보지점의 X 좌표값*/
        urlBuilder.append("&" + URLEncoder.encode("ny","UTF-8") + "=" + URLEncoder.encode("127", "UTF-8")); /*예보지점의 Y 좌표값*/
        System.out.println(urlBuilder.toString());

        // 완성된 String을 URL class를 사용하여 url개체를 만듬
        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());
    }
}

 

  본 예시코드는 일반적인 HTTP통신(서블릿, 스프링 등) 방법과 다르게, Java의 built-in method인 java.net을 사용하였다. Java.net은 다른 http관련 package보다 추상화정도가 낮아 더욱 세세한 http 통신의 조정이 가능하다. http 개념을 복습하는 셈치고 각 라이브러리와 단계를 학습하자.

  • 1. URLEncoder.encode : 과거부터 인터넷은  ASCII을 표준으로 사용하였기때문에, 특수문자와 같은 UTF문제를 ASCII로 변환해야한다.  ASCII로 표현가능한 UTF는 그대로 유지하고 아래의 두가지 경우 변환작업을 한다. 변환시에는 % + encoded 16진수번호를 사용해서 변환한다.
    • A. special characters that have special meanings. Parameter를 표기하는 기호라면 encoding하지않고 값이라면 encoding필요함.
      • The question mark (?) is used to indicate the start of a query string.
      • The ampersand (&) is used to separate query parameters.
      • The equals sign (=) is used to assign values to parameters.
      • empty space can not be included as query parameter
      • # indicates a fragment identifier.
    • B. Character fall outside the ASCII character set such as 한국어.
      • 안녕하세요 : %EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94
  • 2. URL 클래스를 통해서 url object가 생성됨
    • Url을 특별히 Object로 관리하는 것은 url의 부분 부분마다 Sematical 의미가 다른 부분들을 규칙에 따라 Parsing하기 위해서다.
  • 3. URL 인스턴스가 url.openconnection()을 통해서 네트워크 연결을 위한 셋업 값들을 준비함. 그리고 HttpURLConnection에 객체를 담음.  
    • 프로토콜 주소, 호스트, 포트 번호 입력 등의 속성값을 셋업함. 그러나 실제로 연결을 아직 시도하지 않음.
    • HttpURLConnection에 Get method나 header값을 설정하여 request를 실행할 준비를함.
  • 4. HTTP Response 처리
    • Data Reception: The servlet reads the JSON data sent by the client.
    • JSON Parsing: Parses the JSON to extract the user data.
    • Response Creation: Creates a JSON response with a message indicating successful processing.
    • Sending Response: Sets the content type of the response to application/json and sends the JSON response back to the client.

API 요청방식 (2) Spring WebFlux :  WebClient

  Spring에서 17년이후 Restemplate은 deprecated되었으며 가장 최신이며 보편적으로 사용하는 WebClient를 사용해보자. WebClient를 사용하기 위해서는 'spring-boot-starter-webflux',를 추가해야한다. 

 

WebClient 사용

public class HeritageApi {

    public String externalAPI() {

        WebClient client = WebClient.builder()
            .baseUrl("https://www.khs.go.kr")
            .defaultHeader("User-Agent", "MyApp")  // Default header example
            .build();

        String response = client.get()
            .uri(uriBuilder -> uriBuilder
                .path("/cha/SearchKindOpenapiList.do")
                .queryParam("param1", "value1")  // Example query parameter
                .queryParam("param2", "value2")
                .build())
            .header("Custom-Header", "CustomHeaderValue")  // Add custom header
            .retrieve()
            .bodyToMono(String.class)
            .block();  // Synchronous call, use `block()` in non-reactive code

        return response;
    }
}

 

WebClient Method

  • client.get(): Specifies that this will be a GET request.
  • .uri("/cha/SearchKindOpenapiList.do"): Appends the URI path to the base URL.
    • "?pageUnit=20&pageIndex=600&ccbaCncl=N"과 같이 queryParam을 사용하는 API 호출은 uri Builder를 사용하는 것이 가시성이 좋다. 
    • uriBuilder : uribuilder은 spring에서 제공하는 기능으로 queryParameter을 포함하는 uri를 좀 더 가독성이 좋도록 분리하는 기능을 제공한다.
.uri(uriBuilder -> uriBuilder
    .path("/cha/SearchKindOpenapiList.do")
    .queryParam("pageUnit", 300)
    .queryParam("pageIndex", pageNum)
    .queryParam("ccbaCncl", "N")
    .build())

 

  • .retrieve(): Sends the request and starts retrieving the response.
  •  
  • .bodyToMono(String.class): 응답데이터가 String인 것을 명시하며 Mono방식으로 수신할 것을 명시
    • Why use Mono? : A Mono allows non-blocking processing, meaning that WebClient will continue with other work while it waits for the response to arrive. The actual value is only emitted when the response completes.
  • .block(): This is a blocking operation that waits for the response to complete and returns the body as a String. Since this is used in synchronous code, it's necessary here, but if you're working in a reactive context, you would avoid using block().
CompletableFuture.supplyAsync(() -> "Hello, Async!")
    .thenAccept(result -> {
    // 콜백: 비동기 작업이 끝난 후 결과를 출력
    System.out.println("Received result: " + result);
    });

 

// Creating a Mono to send a notification
Mono<Void> notificationMono = notificationService.sendNotification(userId);

// Subscribing to confirm completion
notificationMono.subscribe(
    unused -> System.out.println("Notification sent successfully"),
error -> System.err.println("Failed to send notification: " + error)
);
// Creating a Flux to retrieve all orders for a user
Flux<Order> ordersFlux = orderService.findAllOrdersByUserId(userId);

// Subscribing to process each order
ordersFlux.subscribe(
    order -> System.out.println("Order: " + order),                // onNext
error -> System.err.println("Error: " + error),                // onError
    () -> System.out.println("Completed retrieving orders")        // onComplete
);

 

 

API 연결방식 선택 :  Performance Consideration 

1. Blocking vs 멀티스레드 비동기 vs Non-Blocking 비동기

개념 설명:

  • Blocking I/O 서버에서는 각 클라이언트 요청마다 스레드 풀에서 스레드를 할당하여 처리합니다. API를 호출하면 메인스레드는 API 호출이 완료될 때까지 차단 상태를 유지합니다.
  • 멀티스레드 비동기 서버에서는 메인스레드가 API 호출을 대기하지 않고 별도의 스레드를 할당하여 API 호출을 대기한 후에 후속작업을 비동기적으로 처리합니다. 
    • 비동기 처리에서는 콜백이나 프로미스(Promise, Java에서는 CompletableFuture 등)를 활용하여, 백그라운드에서 API 호출이 완료될 때 후속 작업을 비동기적으로 처리합니다.
  • NonBlocking 비동기 서버 아키텍처에서는 클라이언트 요청마다 별도의 스레드를 할당하지 않고, 소수의 스레드 풀로 여러 연결을 동시에 처리합니다. 
    • Java에서는 webFlux 에서 후속작업을 비동기적으로 처리합니다. 

 

  • 1. non-blocking 비동기 방식 (전체 reactive 기반일 때)
public Mono<ResponseDto> callSomeApiAsync() {
    return webClient.get()
            .uri("...")
            .retrieve()
            .bodyToMono(ResponseDto.class);
}
  • 2. blocking 방 (일반 Spring MVC 기반일 때)
public ResponseDto callSomeApiBlocking() {
    return webClient.get()
            .uri("...")
            .retrieve()
            .bodyToMono(ResponseDto.class)
            .block();  // ⚠️ blocking
}
  • 3. 멀티스레드 비동기 방식 (일반 Spring MVC 기반일 때)
public CompletableFuture<ResponseDto> callSomeApiAsync() {
    return webClient.get()
            .uri("...")
            .retrieve()
            .bodyToMono(ResponseDto.class)
            .toFuture();  // WebClient는 Mono -> CompletableFuture로 변환 가능
}

 

  • 단순히 Mono를 CompletableFuture로 감싼다고 , Mono → Blocking으로 바꾼 건 아님. 그러므로 기본적으로 reactive하게 동작한다.
    • 단, completableFuture.get()을 사용하면 스레드풀에서 스레드가 할당되어 blocking이 되며 병렬동기 실행으로 변하게 된다. 
    • 단, 여기서 subscribe를 사용해서 callback 형식으로 처리하게 된다면 순수하게 비동기로 처리가된다. 

 

2. 커넥션 풀링 (Connection Pooling)

커넥션 재사용: 매 요청마다 새로운 HTTP 연결을 설정하면 시간이 오래 걸리고 자원이 소모됩니다. 커넥션 풀링을 사용하면 동일한 TCP 연결을 여러 요청이 재사용할 수 있어, 연결 설정과 종료에 드는 오버헤드를 줄임으로써 성능을 크게 개선할 수 있습니다.

 

 

외부 API사이트

  • 공공데이터포탈 www.data.go.kr

 

 

직렬화 및 역직렬화

직렬화 / 역직렬화란?

용어 의미 방향 예시
직렬화 (Serialization) 자바 객체 → 문자열(JSON/XML 등) Java → JSON API 응답 만들기
역직렬화 (Deserialization) 문자열 → 자바 객체 JSON → Java API 요청 받기

 

 

직렬화/역직렬화는 언제 발생하나?

상황 동작 설명
Controller에서 @RequestBody 파라미터 받을 때 역직렬화 JSON 요청 → DTO
Controller에서 DTO를 리턴할 때 직렬화 DTO → JSON 응답
외부 API 응답(WebClient 등)을 DTO로 받을 때 역직렬화 외부 JSON → DTO

 

백엔드 개발자가 꼭 알아야 할 특성

  • 필드 이름이 다르면 역직렬화시에 매핑되지 않는다
  • Jackson은 기본 생성자 + Setter 기반 (기본 방식)으로 맵핑하기 때문에 기본 생성자, getter/setter 없으면 동작 안 할 수 있다
  • 타입이 맞지 않으면 역직렬화 실패

 

 

JSON (JavaScript Object Notation) and XML (Extensible Markup Language) are two popular formats for data interchange between systems.  

XML 데이터 가공 

  JackSon Library 사용.  Json뿐만아니라 XML타입에도 호환이되며 성능이 준수하고 기능이 다양하여 여러 기업에서 선호되는 라이브러리임.

 

의존성 도입하기 

// xml to json
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'

 

XmlMapper을 생성

  • DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES: When set to false, it allows the mapper to ignore unknown properties in the XML data during deserialization 
@Configuration
public class Config {

  @Bean
  public static XmlMapper xmlMapper() {
    XmlMapper xmlMapper = new XmlMapper();
    xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    return xmlMapper;
  }

 

 XML 데이터를 맵핑할 객체 JavaBean생성

  • List형태인 경우 List element도 JavaBean으로 생성이 필요
  • Java Bean에 Getter와 Setter 생성하기, field를 annotation으로 mapping 시키기
  • Jackson은 자동적으로 Class와 Xml데이터 포맷을 자동적으로 맵핑하나 Class나 field 이름이 다르면 xmlProperty annotation으로 맵핑이 필요
    • @JacksonXmlRootElement(localName = "root_element"): map root element of the XML to annotated class.
    • @JacksonXmlProperty(localName = "element_name"): map XML elements to fields to annotated field of class when the names differ s.
    • @JacksonXmlElementWrapper(useWrapping = false) :
      • @JacksonXmlElementWrapper(useWrapping = false)는 컬렉션 필드에 붙여 wrapping 태그를 자동으로 생성할지 설정합니다.  
      • Jackson XML은 컬렉션(Collection) 을 XML로 변환할 때 다음처럼 중간에 하나의 wrapper 태그를 만들어줍니다. 본 xml에는 그런 존재가 필요없다는 것을 명시적으로 Jackson에게 알려주는 기능을 함.

@JacksonXmlElementWrapper(useWrapping = true)

<WrapperExample>
    <items>  <!-- 이게 바로 wrapper -->
        <item>apple</item>
        <item>banana</item>
    </items>
</WrapperExample>

 

@JacksonXmlElementWrapper(useWrapping = false)

<WrapperExample>
<item>apple</item>
<item>banana</item>
</WrapperExample>

 

@JacksonXmlRootElement(localName = "result")
public class HeritageApiResult {

  @JacksonXmlElementWrapper(useWrapping = false)
  @JacksonXmlProperty(localName = "item")
  List<HeritageApiItem> heritageApiItemList;

}
public class HeritageApiItem {

  @JacksonXmlProperty(localName = "ccbaCpno")
  private String heritageId; //
  @JacksonXmlProperty(localName = "ccbaMnm1")
  private String heritageName;
  private Double longitude;
  private Double latitude;
  @JacksonXmlProperty(localName = "ccmaName")
  private String heritageGrade;

  //ccbaKdcd, ccbaAsno, ccbaCtcd는 다른 APi 호출을 위해 필요한 para값
  private String ccbaKdcd;
  private String ccbaAsno;
  private String ccbaCtcd;

}
 

 

 

  • - XML Deserialization / readValue: This method is part of Jackson's XmlMapper (which extends ObjectMapper) and is used to convert a string (or other input) containing XML data into a Java object. In this case, the XML data is mapped to the HeritageApiResult class.
  try {
    log.info("Successfully parsed XML to HeritageApiResult");
    return xmlMapper.readValue(xmlResponse, HeritageApiResult.class);
  } catch (JsonProcessingException e) {
    log.error(e.getMessage());
    throw new CustomExcpetion(ErrorCode.EXTERNALAPI_NOT_FOUND, "Heritage API Not Found");
  }
}

JSON 데이터 가공 

DTO 생성

 

Jackson does not care about your class names like Document, Meta, RoadAddress, etc. Instead, it matches:

 

  • JSON field names (keys like meta, documents, road_address, etc.)
  • to Java field names (like private Meta meta;, private List<Document> documents;)

 

@Getter
public static class KakaoCoord2AddressResponse {

    private Meta meta;

    private List<Document> documents;

    @Getter
    public static class Meta {
        @JsonProperty("total_count")
        private int totalCount;
    }

    @Getter
    public static class Document {

        @JsonProperty("road_address")
        private RoadAddress roadAddress;

        private Address address;
    }

    @Getter
    public static class RoadAddress {

        @JsonProperty("address_name")
        private String addressName;

        @JsonProperty("region_1depth_name")
        private String region1DepthName;

        @JsonProperty("region_2depth_name")
        private String region2DepthName;

        @JsonProperty("region_3depth_name")
        private String region3DepthName;

        @JsonProperty("road_name")
        private String roadName;

        @JsonProperty("underground_yn")
        private String undergroundYn;

        @JsonProperty("main_building_no")
        private String mainBuildingNo;

        @JsonProperty("sub_building_no")
        private String subBuildingNo;

        @JsonProperty("building_name")
        private String buildingName;

        @JsonProperty("zone_no")
        private String zoneNo;
    }

    @Getter
    public static class Address {

        @JsonProperty("address_name")
        private String addressName;

        @JsonProperty("region_1depth_name")
        private String region1DepthName;

        @JsonProperty("region_2depth_name")
        private String region2DepthName;

        @JsonProperty("region_3depth_name")
        private String region3DepthName;

        @JsonProperty("mountain_yn")
        private String mountainYn;

        @JsonProperty("main_address_no")
        private String mainAddressNo;

        @JsonProperty("sub_address_no")
        private String subAddressNo;

        @JsonProperty("zip_code")
        private String zipCode;
    }
}

 

 

DTO.class를 WebClient에 제공

  Json 가공에도 jackson library를 사용하면 되지만, webclient에서는 json에 대해서 jackson 기능이 내장되어있어서 DTO를 생성해서 아래와 같이 webclient의 input으로 설정해주면 된다.

result = webClient.get()
        .uri(urlWithQueryParams)
        .headers(httpHeaders -> headers.forEach(httpHeaders::add))
        .retrieve()
        .bodyToMono(responseType)
        .block();

 

 

 

그 외 Jackson Library 사용

  • @JsonInclude : Jackson에서 직렬화 시  값에 따라 포함 여부 결정.
    • DTO 클래스나 응답 객체에서 필드가 null이거나 특정 값일 때 JSON 응답에서 제외하고 싶을 때 사용됨
    • NON_EMPTY – null, "", [], {} 도 제외
    • NON_NULL – null이면 제외
    • NON_DEFAULT – 기본값 제외
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDto {
    private String name;
    private String email; // null이면 JSON 응답에 포함되지 않음
}

 

  • @JsonProperty : JSON에서의 필드명 지정.
    • Java 객체의 필드명과 JSON 필드명이 다를 때 매핑을 도와주는 Jackson 애노테이션
    • 외부 API 호출(WebClient, RestTemplate 등)과 JSON ↔ Java 변환이 필요한 경우 snake_case ↔ camelCase 매핑에 자주 쓰입니다.
public class UserDto {
    @JsonProperty("user_name")
    private String userName;

    @JsonProperty("email_address")
    private String email;
}
  • @JsonIgnore : 해당 필드 무시

'개발기술 > Web Dev' 카테고리의 다른 글

NGNIX 사용  (0) 2025.03.07
RESTFUL API 설계  (0) 2024.12.15
Interception , Filter  (0) 2024.09.05
프론트엔드 연계 관련 지식  (0) 2024.07.08
자바 웹프로그래밍의 입문 - 서블릿, JSP, 톰캣, 스프링 소개  (0) 2024.06.29