MFC, C++ | CHttpFile, CInternetSession을 이용하여 JSON 파일 읽어오는 예제와 UTF-8 BOM 이슈

회사에서 투자정보 서버에서 서비스로 제공해 주던 데이터를 갑자기 웹 파트를 통해 URL을 이용하여 JSON 파일(.json)로 데이터를 다운 받을 수 있게 제공해 준다고 하여 웹 URL 상의 JSON 파일을 연결하여 JSON 데이터를 읽어와서 사용해야 하는 경우가 발생했다.

그래서 우리 코드 상에 잘 없는 경우인 Http URL 통신 로직을 본격적으로 추가하게 되었다.

새로운 HTTP URL 파일 읽기 함수 추가

기존 Utility 모듈(DLL)에 관련 함수가 있지만 오래된 로직이고 하니 이 참에 새로 추가하기로 한다.

추가할 기능의 내용과 예제 기록을 시작해 보자.

구현 내용

CInternetSession을 사용하여 인터넷 세션을 생성하고 이 세션으로 CHttpConnection을 얻어 웹 서버에 연결한다.

다음, CHttpConnectio 객체를 사용하여 CHttpFile 객체를 열고 URL에서 C++ JSON 라이브러리에서 처리 가능한 JSON 문자열로 JSON 데이터를 읽어 온다.

예제 코드

#include <afxinet.h> // CInternetSession, CHttpConnection, CHttpFile을 사용하기 위해 필요합니다.
#include <iostream>  // 콘솔 출력을 위해 (디버깅 목적)
#include <string>    // std::string을 사용하기 위해

// URL에서 JSON 데이터를 읽어오는 함수
std::string ReadJsonFromUrl(const CString& strUrl)
{
    std::string strJsonData;
    CInternetSession session; // 인터넷 세션을 생성합니다.
    CHttpConnection* pConnection = NULL;
    CHttpFile* pFile = NULL;

    try
    {
        // URL을 파싱하여 서버 이름, 객체 경로 등을 얻습니다.
        DWORD dwServiceType;
        CString strServer;
        CString strObject;
        INTERNET_PORT nPort;

        if (!AfxParseURL(strUrl, dwServiceType, strServer, strObject, nPort) ||
            dwServiceType != INTERNET_SERVICE_HTTP)
        {
            // URL 파싱 실패 또는 HTTP 서비스가 아닌 경우
            AfxMessageBox(_T("유효하지 않은 URL이거나 HTTP/HTTPS URL이 아닙니다."));
            return "";
        }

        // HTTP 연결을 엽니다.
        pConnection = session.GetHttpConnection(strServer, nPort);
        if (pConnection == NULL)
        {
            AfxMessageBox(_T("HTTP 연결을 설정할 수 없습니다."));
            return "";
        }

        // HTTP 파일을 엽니다 (GET 요청)
        pFile = pConnection->OpenRequest(CHttpConnection::HTTP_VERB_GET, strObject);
        if (pFile == NULL)
        {
            AfxMessageBox(_T("HTTP 요청을 열 수 없습니다."));
            return "";
        }

        // 요청을 보냅니다.
        pFile->SendRequest();

        // HTTP 상태 코드를 확인합니다. (예: 200 OK)
        DWORD dwRet;
        pFile->QueryInfoStatusCode(dwRet);
        if (dwRet != HTTP_STATUS_OK)
        {
            CString strMessage;
            strMessage.Format(_T("HTTP 요청이 실패했습니다. 상태 코드: %d"), dwRet);
            AfxMessageBox(strMessage);
            return "";
        }

        // 파일에서 데이터를 읽습니다.
        char szBuff[4096]; // 4KB 버퍼
        UINT nRead;
        while ((nRead = pFile->Read(szBuff, sizeof(szBuff) - 1)) > 0)
        {
            szBuff[nRead] = '\0'; // 널 종료
            strJsonData += szBuff;
        }

        // --- BOM 제거 로직 추가 시작 ---
        // UTF-8 BOM (0xEF, 0xBB, 0xBF) 확인 및 제거
        if (strJsonData.length() >= 3 &&
            (unsigned char)strJsonData[0] == 0xEF &&
            (unsigned char)strJsonData[1] == 0xBB &&
            (unsigned char)strJsonData[2] == 0xBF)
        {
            strJsonData = strJsonData.substr(3); // 앞 3바이트 제거
            TRACE("UTF-8 BOM이 감지되어 제거되었습니다.\n");
        }
        // --- BOM 제거 로직 추가 끝 ---

    }
    catch (CInternetException* pEx)
    {
        TCHAR szErr[1024];
        pEx->GetErrorMessage(szErr, sizeof(szErr) / sizeof(TCHAR));
        AfxMessageBox(szErr); // 예외 메시지를 표시합니다.
        pEx->Delete(); // 예외 객체를 삭제합니다.
    }

    // 리소스를 해제합니다.
    if (pFile)
    {
        pFile->Close();
        delete pFile;
    }
    if (pConnection)
    {
        pConnection->Close();
        delete pConnection;
    }
    session.Close(); // 세션을 닫습니다.

    return strJsonData;
}

// 예제 사용법
void ExampleUsageOfReadJsonFromUrl()
{
    // 예시 JSON URL (실제 작동하는 JSON URL로 변경하세요)
    // 이 예제에서는 더미 JSON 데이터를 제공하는 API를 사용했습니다.
    // https://jsonplaceholder.typicode.com/posts/1
    CString url = _T("http://jsonplaceholder.typicode.com/posts/1");

    std::string jsonContent = ReadJsonFromUrl(url);

    if (!jsonContent.empty())
    {
        // JSON 데이터가 성공적으로 읽혔을 때
        AfxMessageBox(_T("JSON 데이터를 성공적으로 읽었습니다. 디버그 창을 확인하세요."));
        // 디버그 출력 (Visual Studio의 출력 창에서 확인 가능)
        TRACE("읽어온 JSON 데이터:\n%s\n", jsonContent.c_str());

        // 여기서 jsonContent를 JSON 파싱 라이브러리(예: nlohmann/json, rapidjson 등)를 사용하여 파싱할 수 있습니다.
        // 예를 들어:
        // #include "nlohmann/json.hpp"
        // json::json parsedJson = json::json::parse(jsonContent);
        // TRACE("JSON 파싱 예시 - userId: %d\n", parsedJson["userId"].get<int>());
    }
    else
    {
        AfxMessageBox(_T("JSON 데이터를 읽는 데 실패했습니다."));
    }
}

이슈 : BOM(Byte Order Mark)

잘 되나 싶었는데 웹 파트에서 올려 놓은 JSON 파일의 제일 앞 3바이트에 알 수 없는 값이 들어 있어 JSON Parsing이 안되었다.

한참을 씨름하다가 결국 알아낸 내용이 BOM(Byte Order Mark)에 관한 내용이다.

BOM은 유니코드 텍스트 파일의 시작 부분에 붙는 선택적인 바이트 시퀀스이다. 이 마크는 해당 텍스트 파일이 어떤 유니코드 인코딩으로 되어 있는지, 멀바이트의 경우 바이트 오더링이 어떻게 되는지 알려주는 역할을 한다.

우리 회사에서 발생한 문제는 UTF-8 BOM 이었다. 그래서 위 예제 코드와 같은 코드에 BOM 제거 코드를 추가해 주어야 했다.

결과

다행히 BOM(Byte Order Mark) 이슈까지 잘 해결하였는데 웹 파트에서 HTTP URL에 올려 놓은 파일 자체를 조치하여 BOM 바이트 3개를 제거하기로 했다.

오늘도 덕분에 열코딩으로 시간이 잘 가는 하루였다. 😂


참고

플러터 네비게이션

플러터 라이브러리

댓글 남기기