Spring Project : 공공 데이터 API 요청 + XML Parsing 하기

    공공 데이터 API 요청 

    공공 데이터 API 요청은 공공 데이터 API 홈페이지 가입 후, 요청하고 승인 받으면 API를 요청해서 데이터를 받을 수 있다. (https://www.data.go.kr)

     

    공공 데이터 API 요청

    데이터를 요청하려면, 사용 설명서를 봐야하는데 나는 개발계정 상세보기의 '미리보기'를 이용했다. 미리보기를 클릭하면 요청변수에 어떤 값을 넣어줘야하는지가 뜬다. 여기서 Service key는 공란인데, 발급받은 Service Key를 넣으라는 의미다.

    요청해야 할 URL은 END Point다. EndPoint를 살펴보면 "?"로 이미 쿼리 파라미터가 들어간 것을 볼 수 있다 따라서 &을 이용해서 위의 쿼리파라미터를 추가해준다.

    위의 요청변수를 넣고 Post Man으로 테스트를 했다. 테스트 결과 정상적으로 나오는 것을 확인했다. Post Man으로는 XML / JSON으로 모두 전달이 되는 것 같았는데, 스프링에서 요청 시에는 XML로만 응답이 오는 것 같았다. 

     

    StringBuilder sb = new StringBuilder();
    sb.append("http://openapi.molit.go.kr/OpenAPI_ToolInstallPackage/service/rest/RTMSOBJSvc/getRTMSDataSvcAptTradeDev?");
    sb.append("ServiceKey=");
    sb.append("...");
    sb.append("&pageNo=" + 1);
    sb.append("&numOfRows=" + 1000000);
    sb.append("&LAWD_CD=" + APART_CD_LIST[i]);
    sb.append("&DEAL_YMD=" + 202112);
    
    
    // URL 연결
    URL url = new URL(sb.toString());
    HttpURLConnection conn = (HttpURLConnection)url.openConnection();
    
    conn.setRequestProperty("Content-Type","application/xml");
    conn.setRequestMethod("get");
    conn.connect();​

    API 요청을 위해서 다음과 같이 코드를 작성했다. 스트링 빌더를 활용해서 기본 URL + 쿼리 파라미터를 넣어주는 방식으로 했다. 한번에 수십만 줄도 받아와지는 것 같아, 수십만 줄을 받도록 했다. 이유는 하루에 요청할 수 있는 횟수가 1000번이기 때문이다. LAWD_CD는 행정번호(?) 같은 것이었는데, 빈 값으로 요청을 할 수 없었다. 그래서 APART_CD_LIST라는 배열을 만들어서 For문을 돌면서 여러번 요청을 하는 방식으로 요청을 했다. 

    쿼리 파라미터를 다 넣은 다음에는 문자열로 변환하고, HTTP 연결을 했다. 이 때 요청 메서드나, 요청 형식을 지정해줬는데 테스트 해본 결과 사실 안해줘도 크게 문제는 없었다. 요청 후, connect()로 데이터를 요청했다. API를 요청하고 난 다음, 데이터는 conn 객체에 저장된다.

    implementation group: 'org.jdom', name: 'jdom2', version: '2.0.6.1'

    가져온 데이터는 jdom2 라이브러리를 이용해 Parsing 했다. 나는 Gradle을 사용하는데, Gradle에서 JDOM2 라이브러리 의존성 추가는 위와 같이 할 수 있다. 

    SAXBuilder builder = new SAXBuilder();
    Document document = builder.build(conn.getInputStream());
    
    Element root = document.getRootElement();
    Element body = root.getChild("body");
    Element items = body.getChild("items");
    List<Element> item = items.getChildren("item");
    
    System.out.println("NOW LAW_CD = " + APART_CD_LIST[i]);
    
    for (Element element : item) {
        ApartXmlParser apartXmlParser = transferXmlToParser(element);
        System.out.println("apartXmlParser = " + apartXmlParser);
    }

    JDOM에서 SAXBuilder를 만들고, SAXBuilder에 전달받은 conn의 InputStream을 넘겨주면 XML Document 객체가 만들어진다.

    요청한 XML 파일은 이렇게 되는데, getRootElement는 <Response> 이하의 영역을 다 가져온다는 이야기다. 그리고 <Response> 밑으로는 여러 자식들이 있는데, 거기서 데이터는 모두 Body에 모여있기 때문에 getChild("body")를 이용해 Body의 데이터를 가져왔다. 이런 식으로 <item>까지 내려갔다. <item>부터는 이제 각각의 정보가 담겨져 있기 때문에 For문을 통해 객체로 Parsing하고자 했다. 이 때, trasnferXmltoParser 메서드를 만들어서 돌렸다.

    private ApartXmlParser transferXmlToParser(Element item) {
    
        ApartXmlParser.ApartXmlParserBuilder builder = ApartXmlParser.builder();
    
        List<Element> children = item.getChildren();
        for (Element child : children) {
            mappingFromItemToParser(builder, child);
        }
    
        return builder.build();
    
    }

    transferXmlToParser 메서드는 다음과 같이 작성했다. Lombok의 Builder를 이용해서 하나씩 만들기로 했는데, mappingFromitemToParser 메서드를 for를 돌리면서 한번 더 했다. 이유는 모든 정보가 동일한 형태로 올라오는게 아니었기 때문이다. 어떤 정보는 하나의 객체가 26개의 정보를 가지는데, 어떤 객체는 21개를 가진다. 즉, 순서가 다르기 때문에 순서대로 값을 파싱할 수가 없었다. 복잡해지는 것 같아 메서드를 하나 더 만들었다.

    private void mappingFromItemToParser(ApartXmlParser.ApartXmlParserBuilder builder, Element item) {
    
        String value = item.getContent(0).getValue().trim();
        String name = item.getName();
    
        if(value.equals("")){
            return;
        }
    
        if(name.equals("거래금액")){
            builder.price(Integer.valueOf(value.replace(",", "")));
        }
    
        if(name.equals("거래유형")){
            builder.tradeType(value);
        }
    
        if(name.equals("건축년도")){
            builder.buildYear(value);
        }
    
        if(name.equals("년")){
            builder.year(value);
        }
    
        if(name.equals("도로명")){
            builder.streetName(value);
        }
    
        if(name.equals("법정동")){
            builder.lawCdStreetName(value);
        }
    
    
        if(name.equals("아파트")){
            builder.apartName(value);
        }
    
        if(name.equals("월")){
            builder.tradeMonth(value);
        }
    
        if(name.equals("일")){
            builder.tradeDay(value);
        }
    
        if(name.equals("층")){
            builder.floor(value);
        }
    
        if(name.equals("전용면적")){
            builder.area(Double.valueOf(value));
        }
    
        if(name.equals("해제사유발생일")){
            builder.cancelDate(LocalDate.parse(value.replace(".",""), DateTimeFormatter.ofPattern("yyMMdd")));
        }
    
        if(name.equals("해제여부")){
            builder.isCancel(value);
        }
    
        if(name.equals("지역코드")){
            builder.lawCd(value);
        }
    
        if(name.equals("지번")){
            builder.addressNumber(value);
        }
    
        if(name.equals("도로명코드")){
            builder.streetNameCode(value);
        }
    }

    실제 객체 맵핑은 여기서 하나씩 이루어진다. 처음에 공백값이 들어오면 바로 돌아가도록 했다. null값이 들어오는 건데, 값을 정수나 날짜 형식으로 변경해야하는 경우 null 처리할 경우 코드가 복잡해졌다. 어차피 공백값은 다른 Column에도 필요없기 때문에 공백이 들어오는 순간 바로 Return 처리를 해버렸다.

     

    실행 결과

    100개의 동에 대해서 API 요청을 하면서, 즉시 값을 Parsing 했다. 그리고 Parsing한 값을 하나씩 바로 출력하도록 했다. 예외가 발생하지 않았고, 값도 정상적으로 맵핑 되는 것을 확인할 수 있었다. 

     

    TODO 

    테스트 컨트롤러를 만들었다고는 하지만, 컨트롤러에서 모든 일을 처리한다. 따라서 다음에는 컨트롤러에서 받아온 데이터를 서비스 계층으로 넘겨주고, 서비스 계층에서 비즈니스 로직을 처리하고 DB에 중복 케이스를 발라내고 밀어넣는 일을 처리해야한다. 

     

    댓글

    Designed by JB FACTORY