간단한 Qt 네트워크 프로그램을 개발하던 중, 문자열 외에 사용자 정의 구조체를 전송해야 하는 상황에 직면했습니다. 이를 해결하기 위해 memcpy() 함수를 사용해 데이터를 BYTE 배열로 직렬화하여 네트워크 전송을 구현했습니다.
직렬화는 Java에서 유래한 개념으로, C에는 존재하지 않았지만 C++에서는 후에 직렬화 및 역직렬화 개념이 도입되었습니다. 직렬화란 시스템 타입이 아닌 클래스 객체를 기본 데이터 형식으로 변환하는 과정으로, 주로 네트워크 전송이나 파일 입출력을 용이하게 하기 위해 사용됩니다. 역직렬화는 이의 반대 과정입니다.
참고로 C/C++에서 BYTE와 Char는 메모리 내에서 동일한 데이터를 가지며, Windef.h에는 다음과 같은 정의가 있습니다:
typedef unsigned char BYTE;
따라서 char와 BYTE는 메모리에서 동일하며, 직렬화의 목표는 BYTE나 char 형식으로 변환하는 것입니다.
QUdpSocket은 char 형식의 네트워크 전송을 요구하므로, 직렬화 결과도 char 형식이어야 합니다. 이때 C/C++의 메모리 정렬 문제를 고려해야 합니다. 경험상 char, BYTE와 같은 고정 길이 타입을 주로 사용하고 int, double과 같은 가변 길이 타입은 최소화하는 것이 좋습니다. 이는 다양한 시스템에서 가변 길이 타입이 다른 메모리 크기로 정의될 수 있으며, 시스템이 메모리 정렬을 수행하면서 역직렬화 시 문제를 일으킬 수 있기 때문입니다.
만약 특정 구조체나 타입을 사용해야 한다면, 변수를 분리하고 다양한 길이의 타입으로 분리하여 메모리가 항상 적절한 위치에 정렬되도록 보장해야 합니다. 또한, 상대적으로 "큰" 타입의 변수를 앞쪽에 배치하는 것이 좋습니다.
문제 상황
요구사항: Qt 인터페이스에서 작업 정보를 수집하여 구조체에 저장하고, UDPSocket 객체를 초기화한 뒤 구조체를 char 형식으로 직렬화하여 네트워크로 전송합니다.
구현 방식: Qt 인터페이스에서 QImage 클래스 객체를 통해 이미지 정보와 작업 데이터를 가져옵니다. 사용자 정의 구조체는 다음과 같습니다:
typedef struct tagDrawInfo
{
TOOLS m_Tool; // 그리기 도구; TOOLS는 사용자 정의 enum입니다.
int width; // 펜 너비;
QColor lineColor; // 선 색상;
QColor fillColor; // 채우기 색상;
Qt::BrushStyle
brushStyle; // 브러스 스타일;
QPoint startPoint; // 시작점;
QPoint endPoint; // 종료점;
} DRAWINFO;
memcpy() 함수를 사용해 메모리 복사를 통해 직렬화를 구현했습니다.
문제점
수신 측에서 받은 DRAWINFO 객체의 값이 예상과 달랐으며, 이로 인해 프로그램이 해당 객체를 사용한 추가 작업을 수행할 수 없었습니다.
해결 방안
- 직접 직렬화 함수를 정의하여 특정 객체를 지정된 위치에 배치하고, 역직렬화 시 데이터 위치가 예상과 같도록 보장합니다. 구조체의 각 부분을 개별적으로 직렬화 및 역직렬화하며, 각 타입의 생성자를 사용해 객체를 초기화합니다.
- DRAWINFO 정의 순서를 조정하고 가변 길이 타입 객체를 분리하여 시스템 메모리의 자동 정렬이 데이터 위치에 영향을 주지 않도록 합니다. 수정된 구조체 정의는 다음과 같습니다:
typedef struct tagDrawInfo
{
Qt::BrushStyle
brushStyle; // 브러스 스타일;
QPoint endPoint; // 종료점;
QColor fillColor; // 채우기 색상;
QColor lineColor; // 선 색상;
TOOLS m_Tool; // 그리기 도구; TOOLS는 사용자 정의 enum입니다.
QPoint startPoint; // 시작점;
int width; // 펜 너비;
} DRAWINFO;
결과
정의 순서를 조정한 후, 수신 측 DRAWINFO 객체의 역직렬화 기능이 정상적으로 작동했습니다.
분석
C++ 컴파일러는 int, bool 등 고정 길이 타입에 대해 메모리 정렬을 수행하여 메모리 사용 효율을 높이고, CPU가 한 작업 주기에 처리할 수 있는 명령어 수를 증가시켜 프로그램 실행 효율을 향상시킵니다. 그러나 이 자동 기능은 예기치 않은 문제를 일으킬 수 있으므로 적절한 조정이 필요합니다.
메모리 정렬 문제에 대한 더 자세한 내용은 추후 별도로 다루도록 하겠습니다.