1. 문제의 근본 원인 이해하기
Qt 애플리케이션에서 네트워크 작업을 메인 스레드(UI 스레드)에서 직접 수행하면 심각한 반응성 저하가 발생한다. 이는 Qt의 이벤트 루프가 단일 스레드에서 실행되기 때문이다. HTTP 요청처럼 I/O 대기 시간이 긴 작업은 이벤트 처리를 방해하여 사용자 인터페이스가 멈춘 것처럼 보이게 하며, 장시간 지속될 경우 운영체제가 "응답 없음" 상태로 판단할 수 있다.
또한, 백그라운드 스레드에서 받은 응답 데이터를 UI 업데이트에 바로 사용하는 것은 위험하다. Qt의 GUI 요소는 메인 스레드 외부에서 접근 시 정의되지 않은 동작을 유발할 수 있으며, 여러 스레드가 동시에 공유 자원에 접근할 경우 데이터 경합(race condition)도 발생할 수 있다.
2. 안정적인 솔루션 설계: QThread 기반 비동기 아키텍처
효율적인 해결책은 네트워크 통신 로직을 별도의 작업 스레드로 분리하고, 결과는 신호(signal)와 슬롯(slot)을 통해 메인 스레드로 안전하게 전달하는 것이다. 이 방식은 Qt의 메타오브젝트 시스템이 제공하는 스레드 간 통신 메커니즘을 활용한다.
2.1 작업 객체 설계
다음은 독립된 QObject 파생 클래스로, 네트워크 요청을 담당하며 내부적으로 QNetworkAccessManager를 관리한다.
// apirequesthandler.h
#ifndef APIREQUESTHANDLER_H
#define APIREQUESTHANDLER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonObject>
class ApiRequestHandler : public QObject
{
Q_OBJECT
public:
explicit ApiRequestHandler(QObject *parent = nullptr);
public slots:
void sendGetRequest(const QString& targetUrl);
void sendPostRequest(const QString& targetUrl, const QJsonObject& payload);
signals:
void responseReceived(const QByteArray& rawData, bool success);
void transferProgress(qint64 current, qint64 total);
void operationError(const QString& message);
private slots:
void handleResponseReady(QNetworkReply* reply);
void updateTransferProgress(qint64 bytesDone, qint64 bytesTotal);
private:
QNetworkAccessManager* manager;
};
#endif // APIREQUESTHANDLER_H
// apirequesthandler.cpp
#include "apirequesthandler.h"
#include <QNetworkRequest>
#include <QUrl>
#include <QJsonDocument>
#include <QDebug>
ApiRequestHandler::ApiRequestHandler(QObject *parent)
: QObject(parent), manager(new QNetworkAccessManager(this))
{
connect(manager, &QNetworkAccessManager::finished,
this, &ApiRequestHandler::handleResponseReady);
}
void ApiRequestHandler::sendGetRequest(const QString &targetUrl)
{
QUrl serviceEndpoint(targetUrl);
if (!serviceEndpoint.isValid()) {
emit operationError("잘못된 URL 형식입니다.");
return;
}
QNetworkRequest request(serviceEndpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply* reply = manager->get(request);
connect(reply, &QNetworkReply::downloadProgress,
this, &ApiRequestHandler::updateTransferProgress);
}
void ApiRequestHandler::sendPostRequest(const QString &targetUrl, const QJsonObject &payload)
{
QUrl serviceEndpoint(targetUrl);
if (!serviceEndpoint.isValid()) {
emit operationError("유효하지 않은 요청 주소");
return;
}
QNetworkRequest request(serviceEndpoint);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QByteArray jsonData = QJsonDocument(payload).toJson();
QNetworkReply* reply = manager->post(request, jsonData);
connect(reply, &QNetworkReply::uploadProgress,
this, &ApiRequestHandler::updateTransferProgress);
}
void ApiRequestHandler::handleResponseReady(QNetworkReply* reply)
{
bool isSuccess = reply->error() == QNetworkReply::NoError;
QByteArray resultData;
if (isSuccess) {
resultData = reply->readAll();
} else {
emit operationError(reply->errorString());
}
emit responseReceived(resultData, isSuccess);
reply->deleteLater(); // 반드시 수동으로 삭제해야 함
}
void ApiRequestHandler::updateTransferProgress(qint64 done, qint64 total)
{
emit transferProgress(done, total);
}
2.2 스레드 분리 및 생명주기 관리
UI 쓰레드와 작업 쓰레드를 명확히 분리하기 위해 QThread 인스턴스를 생성하고, 작업 객체를 해당 스레드에 이동시킨다.
// mainwindow.cpp 일부
#include <QThread>
// ...
void MainWindow::initializeNetworkService()
{
QThread* workerThread = new QThread(this);
ApiRequestHandler* handler = new ApiRequestHandler();
// 객체를 별도의 스레드로 이동
handler->moveToThread(workerThread);
// 시그널 연결 설정
connect(this, &MainWindow::startFetch, handler, &ApiRequestHandler::sendGetRequest);
connect(handler, &ApiRequestHandler::responseReceived,
this, &MainWindow::onDataArrived);
connect(handler, &ApiRequestHandler::operationError,
this, &MainWindow::showErrorMessage);
connect(handler, &ApiRequestHandler::transferProgress,
this, &MainWindow::updateProgressBar);
// 스레드 시작
workerThread->start();
// 생명주기 정리 연결
connect(workerThread, &QThread::finished, handler, &QObject::deleteLater);
}
3. 실용적 고려사항 및 최적화 팁
- 메모리 누수 방지: QNetworkReply 객체는 수동으로 deleteLater() 호출이 필요하다. 그렇지 않으면 메모리 누수가 발생한다.
- 요청 취소 지원: 장시간 다운로드 시 reply->abort()를 통해 중단 가능하도록 UI 제어 추가.
- 재시도 로직: 일시적인 네트워크 오류에 대비하여 재시도 정책(exponential backoff)을 신호 핸들러 내에 구현 가능.
- 타임아웃 처리: QTimer를 활용해 응답 지연 시 강제 종료 및 에러 처리.
- 공통 헤더 관리: Authorization 토큰 등은 QNetworkAccessManager의 defaultRequestHeaders에 미리 설정.
4. 결론
Qt에서 네트워크 작업을 안정적으로 처리하려면 반드시 메인 UI 스레드로부터 분리되어야 한다. QObject 기반의 작업 클래스와 QThread 조합은 직관적이면서도 강력한 멀티스레딩 패턴을 제공하며, 신호-슬롯 메커니즘을 통해 스레드 간 통신을 자연스럽고 안전하게 구현할 수 있다. 이를 통해 부드러운 사용자 경험과 견고한 시스템 안정성을 동시에 확보할 수 있다.