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); };
- 구조체 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();
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)에 대해 더 공부하고 싶다면 아래 게시물을 참조 바랍니다.
감사합니다.
[개발 환경]
- Windows 10 Pro, Visual Studio 2017
- C++, MFC 대화상자
- NuGet Package cpprest SDK v141
- CMarkUp
댓글
댓글 쓰기