MFC기반의 CPP REST SDK 사용법(코로나앱)

오늘은 Microsoft 에서 배포한 C++ REST SDK (코드명 : 카사블랑카) 를 이용해 Http Request 를 하는 내용에 대해 다룹니다.

목표

  • 공공 데이터 포털의 시도별 코로나 정보를 REST API를 이용해 처리

 

바로 앞 예제에서 파이썬을 이용해 만들어 본 코로나 예제와 개념은 동일하지만, C++을 이용해 시도해 보았습니다.

 

간단히 전반적인 개요를 살펴보면,

1. 공공 데이터 회원 가입 (앞 게시물 참조)

2. 코로나 시도별 정보 권한(키) 획득

3. Microsoft REST SDK를 이용한 요청데이터 생성 및 전송

4. 응답 데이터 문자열(UTF-8) 수신 (XML or JSON), 신청 정보별로 다름

5. XML 파싱 후 MFC CListControl 에 출력

6. 전국 시도별 코로나 정보 획득

 

쿼리 요청 후 응답 결과는 아래와 같이 처리됩니다.

[쿼리 요청 후 응답 데이터 리스트 컨트롤 출력]

[응답 데이터 XML 문자열]


프로젝트 생성 과정은 다음과 같습니다.

1. C++, MFC 대화상자 프로젝트 생성

2. 프로젝트 생성 후 솔루션 탐색기->프로젝트 우클릭 ->NuGet 패키지 관리


3. CPPREST 검색 후 해당 패키지 설치


4. 설치 후 솔루션 폴더 아래 Packages 폴더 설치 확인


이제 Microsoft 에서 만든 Rest SDK 를 이용해 C++로 코드 작성 준비가 완료되었습니다.

 

MS REST SDK(카사블랑카)에 대한 Microsoft 소개입니다.

What's in the SDK:

  • Features - HTTP client/server, JSON, URI, asynchronous streams, WebSockets client, oAuth
  • PPL Tasks - A powerful model for composing asynchronous operations based on C++ 11 features
  • Platforms - Windows desktop, Windows Store (UWP), Linux, OS X, Unix, iOS, and Android
  • Support for Visual Studio 2015 and 2017 with debugger visualizers

 

XML 지원 클래스가 없어 아쉽지만 이는 CMarkup이나 다른 라이브러리 등 대안이 존재하므로 문제 될 것이 없습니다. 대세인 JSON 은 지원됩니다.

이 예제도 XML을 파싱하기 위해 CMarkup을 활용하였습니다.

오픈소스에다 크로스 플랫폼 지원 등 감사할 따름이죠.

사실 Python에는 REST API 에 대한 다양한 모듈, 패키지가 존재하지만 C++에서는 Qt의 QNetworkAccessManager 클래스를 이용해 REST API를 사용해 왔었습니다.

개인이 만든 C++ REST API도 존재하지만, 그냥 파이썬으로 하는게 더 편리했었죠.

저는 앞으로 C++에서 REST API를 쓰는 경우는 MS REST SDK를 사용할 것 같습니다.

보다 세부적인 내용은 MS 깃허브 링크를 참조 바랍니다.


소스코드

그럼 소스코드를 살펴보겠습니다.

제 목적은 공공데이터 포털로 코로나 정보를 요구하는 쿼리를 보내 얻은 수신 데이터(XML)를 파싱해 표시하는 것입니다.

대화상자 프로젝트로 작성되었지만, 중간 중간에 쿼리 or 수신 데이터 확인용 콘솔을 띄우기 위해 아래 코드를 pch.cpp (Precompiled header) 에 추가합니다.

VS 2015 이하는 stdafx.cpp에 추가하면 됩니다.

pch.cpp

#include "pch.h"
// 디버그 모드 콘솔 열기
#ifdef _DEBUG
#pragma comment(linker, "/entry:wWinMainCRTStartup /subsystem:console")
#endif


다음으로 쿼리를 전송 후 응답처리를 리턴하는 기능을 갖는 간단한 클래스를 하나 만듭니다.

앞에서 만들어 둔 MFC 대화상자 프로젝트에 빈 클래스를 하나 추가합니다.

이름은 CHttpRequest로 추가해 *.h, *cpp 파일의 코드를 아래와 같이 추가합니다.

CHttpRequest.h

#pragma once
#include <vector>
#include <string>

struct _QUERY_STR
{
    std::wstring url;
    std::vector<std::pair<std::wstring, std::wstring>> tag;
};

class CHttpRequest
{
public:
    CHttpRequest(const _QUERY_STR& query);
    ~CHttpRequest();
private:
    _QUERY_STR m_query;    
public:
    std::tuple<std::vector<std::pair<std::wstring, std::wstring>>, std::wstring> SendQuery();
private:
    std::wstring Utf8ToUnicode(const std::wstring& wstr_utf8);
    std::vector<std::pair<std::wstring, std::wstring>> xmlParcer(const std::wstring str);
}; 
_QUERY_STR쿼리용 구조체 입니다. 

REST API 를 요청할 URL과 다수의 Tag를 벡터로 저장합니다.
 
쿼리를 생성하기 위해 아래와 같이 사용합니다.

Ex)
_ QUERY_STR q;
q.url = "https://google.com";
q.tag.push_back ( "태그명 1", "태그내용 1" );
q.tag.push_back ( "태그명 2", "태그내용 2" );
 
이어지는 CHttpRequest 클래스는 생성자 전달인자로 위의 _QUERY_STR 값을 받아서 SendQuery() 함수를 호출합니다.
 
이 함수는 쿼리를 전송하고 응답 결과(XML or JSON)를 파싱해 튜플(파싱데이터 벡터, 문자열)  리턴하는 용도입니다.
 
주의해야할 점은, 리턴타입이 C++11에 추가된 Tuple 타입입니다.
 
C++ 함수에서 여러개의 값을 리턴 받고자하는 경우 일반적으로 아래와 같이 진행되어 왔습니다.
  • 구조체 or 클래스를 이용한 리턴 처리
  • 함수 전달인자로 레퍼런스(&) 사용해 함수내부에서 변경

 

C++ 11에 추가된 Tuple을 이용하면 아래와 같이 사용가능합니다.

Ex)

#include <tuple>

using namespace std;

tuple<int, string> test()

{

    int a = 1;

    string  b = "튜플좋아"

    return make_tuple(a, b);

}


tuple<int, string> result;

result = test(); // 여러 개 리턴

// C++ 17은 더 편리합니다.

auto [ a, b ] = test();

나중에 리턴 받는 쪽에서 이를 간략히 처리하기 위해 C++17에 추가된 아래 auto [a,b,c,...] 문법을 활용해 구성되므로, 컴파일러 설정을 변경(C++17 이상 지원)해 주어야 에러가 발생하지 않습니다.

[VS 컴파일러 설정 변경]
 
여담으로 아직 C++11, 14, 17 표준에 추가된 내용도 익숙치 않은데 C++20 에 대한 표준안이 확정되어 적용된다고 합니다.
 
 
아래는 CHttpRequst 클래스를 이용한 쿼리 요청과 응답처리 예제 코드 입니다.

자세한 내용은 아래에 이어지는 CDlg.cpp 의 소스 코드를 참조 바랍니다.

Ex) 
// 쿼리 생성
_ QUERY_STR q;
q.url = "https://google.com";
q.tag.push_back ( "태그명 1", "태그내용 1" );
q.tag.push_back ( "태그명 2", "태그내용 2" );
 
// REST 클래스 생성
CHttpRequst req (q);
 
// 쿼리 전송 및 응답처리
auto [a, b] = req.SendQuery();
 
여기서 a는 xml 파싱된 벡터를 수신하고, b는 파싱된 문자열을 받도록 예제에서 구성하였습니다.
 
auto 키워드는 타입에 대한 명확성을 흐리게 하고 함수의 전달인자로 사용하지 못하는 등 단점도 있지만 코드를 가독성 좋게 만들어 줍니다.

개인적으로 자주 사용하지 않지만 이번에는 벡터 타입이 너무 길어서 사용해 보았습니다.
 
비공개(private) 함수인 Utf8Unicode(), xmlParcer() 는 자세히 설명하지 않겠습니다.
 
코드를 참조하기 바라며, UTF8로 인코딩된 문자열을 Unicode로 변환하고 CMarkUp을 이용한 XML 파싱에 사용되는 함수입니다.
 
Http Request에서 수신되는 데이터는 UTF8로 인코딩된 문자열이므로, MFC에서 UTF8을 바로 인식하지 못해 한글이 깨져서 표시되는 문제를 해결하기 위한 용도입니다.
 
XML 파싱 클래스인 CMarkUp에 대한 내용은 C++ XML Parcer 링크를 참조 바랍니다. 
 
해당 홈페이지에서 해당 코드를 *.zip 형태로 다운 받아 압축을 풀고, 프로젝트에 Markup.h, Markup.cpp 를 추가하면 됩니다.
 
이제 프로젝트 속성에서 C++ 전처리기에 MARKUP_STL을 추가 or #define 을 이용해 추가해 줍니다.
 
[CMarkup 프로젝트에 추가]
 

CHttpRequest.cpp

#include "pch.h"
#include "CHttpRequest.h"
#include "cpprest/http_client.h"
#include "Markup.h"

using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace utility;

CHttpRequest::CHttpRequest(const _QUERY_STR & query)
{    
    m_query = query;
}

CHttpRequest::~CHttpRequest()
{
}

std::wstring CHttpRequest::Utf8ToUnicode(const std::wstring & wstr_utf8)
{
    // wstring -> string 변환 (현재까지 utf8)
    std::string str_utf8;
    str_utf8.assign(wstr_utf8.begin(), wstr_utf8.end());

    // utf8 -> unicode 변환
    utility::string_t wstr_unicode = utility::conversions::to_string_t(str_utf8);

    return wstr_unicode;
}

std::tuple<std::vector<std::pair<std::wstring, std::wstring>>, std::wstring> CHttpRequest::SendQuery()
{
    http_client client(m_query.url);

    uri_builder builder(U(""));
    //std::vector<std::pair<std::wstring, std::wstring>>::iterator itr;
    auto itr = m_query.tag.begin();
    for (itr = m_query.tag.begin(); itr != m_query.tag.end(); ++itr)
    {
        builder.append_query(itr->first, itr->second, false);
        //std::wcout << itr->first << "\t" << itr->second << std::endl;
    }

    // 쿼리 문자열 생성
    utility::string_t  strQuery = builder.to_string();
    // 쿼리 문자열 전송 및 응답
    http_response response = client.request(methods::GET, strQuery).get();    

    // 응답에서 utf8 문자열 얻기
    utility::string_t wstr_utf8 = response.extract_string().get();

    // 콘솔 출력 설정 UTF8 설정
    SetConsoleOutputCP(CP_UTF8);
    std::wcout << wstr_utf8 << std::endl;    
    
    // utf8 -> unicode 변환
    utility::string_t wstr_unicode = Utf8ToUnicode(wstr_utf8);

    // xml parsing
    auto xml = xmlParcer(wstr_utf8);
    
    return std::make_tuple(xml, wstr_unicode);
}


std::vector<std::pair<std::wstring, std::wstring>> CHttpRequest::xmlParcer(const std::wstring str)
{
    // 파싱된 xml 데이터 저장 벡터
    std::vector<std::pair<std::wstring, std::wstring>> data;

    CMarkup xml;        
    if (xml.SetDoc(str))
    {
        xml.Save(L"result.xml");
        
        if(xml.FindChildElem(L"body"))
        {
            xml.IntoElem();
            if(xml.FindChildElem(L"items"))
            {
                xml.IntoElem();
                while (xml.FindChildElem(L"item"))
                {
                    xml.IntoElem();
                    while (xml.FindChildElem())
                    {
                        std::wstring tag = xml.GetChildTagName();
                        std::wstring val = xml.GetChildData();

                        // utf8 -> unicode 변환
                        utility::string_t wstr_unicode = Utf8ToUnicode(val);
                        
                        // 벡터 저장
                        data.push_back(std::make_pair(tag, wstr_unicode));
                        //std::wcout << tag << "\t" << val << std::endl;
                    }
                    xml.OutOfElem();
                }
            }
        }
    }
    return data;
} 

CPPREST SDK 관련 헤더와 namespace 를 선언합니다.

아래에 설명하는 클래스는 SDK에서 제공하는 클래스입니다.

  • http_client : 쿼리 전송 및 응답 리턴 담당
  • uri_builder : 쿼리 생성 담당
  • http_response : 쿼리 응답처리 담당
  • utility::string_t : 문자열, char, wchar_t 를 개발 환경에 맞춰 자동 생성
  • utility::conversions : 문자열 인코딩, 디코딩 등 유용

 

SDK 사용 방법은 아래와 같습니다.

1. http_client 생성 (URL 전달)

2. uri_builder 로 쿼리 생성

3. http_client.request() 쿼리 전송, http_response 리턴

4. http_response 로 수신된 응답 처리

5. CMarkup을 이용, XML 파싱 후 리턴

 

CDlg.h

위에서 만든 CHttpRequest Class를 인스턴스화 시켜 사용하는 대화상자쪽 코드입니다.

리소스에 CListControl을 추가하고, m_list라는 이름으로 컨트롤 멤버 변수를 생성해 응답 데이터를 출력하는 방식입니다.

class CCPPRESTDlg : public CDialogEx
{
// 생성입니다.
public:
    CCPPRESTDlg(CWnd* pParent = nullptr);
    
...생략...

private:
    CListCtrl m_list;
};

CDlg.cpp

...추가...
#include "CHttpRequest.h"
#include <iostream>

using namespace std;

...생략...

BOOL CCPPRESTDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    
    SetIcon(m_hIcon, TRUE);
    SetIcon(m_hIcon, FALSE);

    // http request 쿼리 구조체 생성 (url, key 등)
    _QUERY_STR query;
    query.url = L"http://openapi.data.go.kr/openapi/service/rest/Covid19/getCovid19SidoInfStateJson?";

    query.tag.push_back(std::make_pair(L"serviceKey", L"여기에 키 입력"));
    query.tag.push_back(std::make_pair(L"pageNo", L"1"));
    query.tag.push_back(std::make_pair(L"numOfRows", L"10"));
    query.tag.push_back(std::make_pair(L"startCreateDt", L"20200830"));
    query.tag.push_back(std::make_pair(L"endCreateDt", L"20200830"));    

    // Http request 클래스 생성
    CHttpRequest request(query);
    // query 전송 후 리턴 (tuple type, C++17 compile ON)
    auto [xml, str] = request.SendQuery();    

    // 리스트 컨트롤 값 추가
    int row = xml.size();
    int col = 2;

    CRect rect;
    GetClientRect(&rect);
    m_list.InsertColumn(0, _T("Tag"), LVCFMT_CENTER, int(rect.Width()*0.5));
    m_list.InsertColumn(1, _T("Value"), LVCFMT_CENTER, int(rect.Width()*0.5));

    m_list.SetExtendedStyle(m_list.GetExtendedStyle() | LVS_EX_GRIDLINES);

    int r = 0;
    for (auto itr = xml.begin(); itr!=xml.end(); ++itr)
    {
        m_list.InsertItem(r, itr->first.c_str());
        m_list.SetItem(r, 1, LVIF_TEXT, itr->second.c_str(), 0, 0, 0, NULL);
        r++;
    }

    return TRUE;
}

제가 수행한 쿼리는 공공데이터에서 제공해 주는 시,도별 코로나 현황입니다. 

해당 코드의 수행결과는 auto[ xml, str ] 로 저장되며 결과는 아래와 같습니다.

[str로 수신된 응답 XML문자열]

[xml로 수신된 파싱 벡터 데이터를 리스트 컨트롤 출력]

 

이상으로 코드 설명을 마칩니다.

위에 링크된 MS 깃 허브 예제를 참조해 만들었으며, PPL Task람다식이 많아 초보자들의 접근이 어려울 것 같아 최소한의 기능으로 Client 만 구현하였습니다.


Task, std::async 를 이용한  비동기(Asynchronous)에 대해 더 공부하고 싶다면 아래 게시물을 참조 바랍니다.

동기 vs 비동기 게시물


감사합니다.

 

[개발 환경]

  • Windows 10 Pro, Visual Studio 2017 
  • C++, MFC 대화상자 

  • NuGet Package cpprest SDK v141
  • CMarkUp

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기