C++ 크로스 플랫폼 테스트 완벽 가이드 | CI 매트릭스·Docker·엔디안·프로덕션 패턴 [실전]

C++ 크로스 플랫폼 테스트 완벽 가이드 | CI 매트릭스·Docker·엔디안·프로덕션 패턴 [실전]

이 글의 핵심

C++ 크로스 플랫폼 테스트 입니다. CI 매트릭스 빌드, Docker 환경, 엔디안 처리, 프로덕션 패리티 테스트 방법을 실전으로 설명합니다. 크로스 플랫폼 C++ 프로젝트를 운영하면 플랫폼마다 다른 동작 때문에 "로컬에서는 통과하는데 CI에서만 실패"하는 일이 자주 발생합니다. 문제: Windows·Linux·macOS·ARM·x86 각각에서 경로 구분자, 엔디안, 정수 크기, 시스템 API가 다르고,.

들어가며: “로컬 Windows에서는 되는데 CI Linux에서만 실패해요”

실제 겪는 문제 시나리오

크로스 플랫폼 C++ 프로젝트를 운영하면 플랫폼마다 다른 동작 때문에 “로컬에서는 통과하는데 CI에서만 실패”하는 일이 자주 발생합니다. 문제: Windows·Linux·macOS·ARM·x86 각각에서 경로 구분자, 엔디안, 정수 크기, 시스템 API가 다르고, 한 플랫폼에서만 검증하면 다른 플랫폼에서 런타임 크래시가 납니다. 해결: 크로스 플랫폼 테스트 전략으로 CI 매트릭스, Docker, 플랫폼별 테스트, 엔디안 검증을 체계적으로 구성합니다.

flowchart TD
  subgraph wrong[❌ 단일 플랫폼 테스트]
    W1[Windows에서만 테스트] --> W2[Linux 배포]
    W2 --> W3[엔디안·경로 버그 발견]
    W3 --> W4[프로덕션 장애]
  end
  subgraph right[✅ 크로스 플랫폼 테스트]
    R1[CI 매트릭스 Win/Linux/macOS] --> R2[Docker로 환경 일관]
    R2 --> R3[플랫폼별·엔디안 테스트]
    R3 --> R4[배포 전 검증 완료]
  end

이 글에서 다루는 것:

  • 문제 시나리오: 크로스 플랫폼 테스트가 필요한 실제 상황
  • 완전한 CI 매트릭스: GitHub Actions·GitLab CI에서 Win/Linux/macOS 병렬 테스트
  • Docker 컨테이너: 일관된 테스트 환경 구성
  • 플랫폼별 테스트: #ifdef로 OS별 검증 분기
  • 엔디안 테스트: Little/Big Endian 바이너리 직렬화 검증
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스프로덕션 패턴

요구 환경: C++17 이상, CMake 3.16+, GTest 1.12+


실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오: 크로스 플랫폼 테스트가 필요한 순간
  2. 크로스 플랫폼 테스트 전략 개요
  3. CI 매트릭스 완전 예제
  4. Docker 컨테이너 테스트
  5. 플랫폼별 테스트
  6. 엔디안 테스트
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 구현 체크리스트

1. 문제 시나리오: 크로스 플랫폼 테스트가 필요한 순간

시나리오 1: “Windows에서는 되는데 Linux CI에서만 실패”

문제: 로컬 Windows에서 ctest는 모두 통과하는데, GitHub Actions Ubuntu runner에서 “cannot open shared object file”로 플러그인 로드 테스트가 실패합니다.

원인: Windows에는 LD_LIBRARY_PATH가 없고, Linux CI에서는 플러그인 .so 경로가 설정되지 않았습니다. 플랫폼별 환경 차이를 테스트에서 고려하지 않았습니다.

해결: CI에서 LD_LIBRARY_PATH를 설정하거나, RPATH로 실행 파일 기준 상대 경로를 지정합니다. 모든 지원 플랫폼에서 동일 테스트를 실행해 검증합니다.


시나리오 2: ARM 서버에서 바이너리 포맷 파싱 실패

문제: x86_64에서 개발한 바이너리 프로토콜이 ARM64 프로덕션 서버에서 “잘못된 헤더” 에러로 실패합니다.

원인: 엔디안 차이. x86은 Little Endian, 일부 ARM은 Big Endian(또는 설정에 따라 다름)입니다. 네트워크·파일 직렬화 시 바이트 순서를 고정하지 않았습니다.

해결: 엔디안 테스트를 추가해 Little/Big Endian 양쪽에서 직렬화·역직렬화를 검증합니다. 프로토콜은 항상 네트워크 바이트 순서(Big Endian)로 정의합니다.


시나리오 3: macOS에서만 “dyld: Library not loaded”

문제: Linux·Windows CI는 통과하는데, macOS runner에서 동적 라이브러리 로드 실패가 발생합니다.

원인: macOS는 @rpath, @loader_path경로 규칙이 Linux와 다릅니다. LD_LIBRARY_PATH 대신 DYLD_LIBRARY_PATH 또는 install_name_tool이 필요합니다.

해결: 플랫폼별 테스트 설정을 CI에 포함합니다. macOS 전용 RPATH·install_name_tool 설정을 CMake에 추가합니다.


시나리오 4: 경로 구분자로 인한 테스트 실패

문제: path/to/testdata/file.txt로 하드코딩한 테스트가 Windows에서 “파일을 찾을 수 없음”으로 실패합니다.

원인: Windows는 \를 경로 구분자로 사용합니다. /만 쓰면 대부분 동작하지만, std::filesystem 없이 문자열 조합으로 경로를 만들면 문제가 됩니다.

해결: 플랫폼 중립 경로 테스트를 추가합니다. std::filesystem::path를 사용하고, 테스트 픽스처 경로를 크로스 플랫폼으로 구성합니다.


시나리오 5: “로컬과 CI 컴파일러 버전이 달라요”

문제: 로컬은 GCC 11, CI는 GCC 13. 컴파일러 버전 차이로 -Werror 시 경고가 에러로 바뀌어 CI만 실패합니다.

해결: Docker로 CI와 동일한 컴파일러·라이브러리 버전을 고정합니다. 또는 CI 매트릭스에 여러 컴파일러 버전을 포함해 호환성을 검증합니다.


시나리오 6: 정수 크기·정렬 차이

문제: long을 바이너리에 4바이트로 가정해 썼는데, Linux 64비트에서는 long이 8바이트입니다. 다른 플랫폼에서 파싱 오류가 발생합니다.

해결: 고정 크기 타입(int32_t, uint64_t)을 사용하고, 플랫폼별 sizeof 검증 테스트를 추가합니다.


2. 크로스 플랫폼 테스트 전략 개요

테스트 계층과 플랫폼 커버리지

flowchart TB
  subgraph layers[테스트 계층]
    L1[단위 테스트\n플랫폼 독립 로직]
    L2[플랫폼별 테스트\n#ifdef 분기 검증]
    L3[통합 테스트\n경로·동적 로딩·파일]
    L4[엔디안 테스트\n직렬화·프로토콜]
  end

  subgraph platforms[플랫폼 커버리지]
    P1[Windows x64]
    P2[Linux x64]
    P3[macOS arm64/x64]
    P4[Linux ARM64]
  end

  L1 --> P1
  L1 --> P2
  L1 --> P3
  L2 --> P1
  L2 --> P2
  L2 --> P3
  L3 --> P1
  L3 --> P2
  L3 --> P3
  L4 --> P2
  L4 --> P4
계층목적플랫폼CI 실행
단위순수 로직, 플랫폼 독립모든 플랫폼매트릭스
플랫폼별#ifdef 분기, OS API해당 OS만조건부
통합경로, dlopen, 파일모든 플랫폼매트릭스
엔디안직렬화, 프로토콜Little+Big Endian별도 job

3. CI 매트릭스 완전 예제

3.1 GitHub Actions: Windows·Linux·macOS 매트릭스

# .github/workflows/cross-platform-test.yml
name: Cross-Platform Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            cc: gcc
            cxx: g++
            cmake_args: -DCMAKE_BUILD_TYPE=Release
          - os: ubuntu-latest
            cc: clang
            cxx: clang++
            cmake_args: -DCMAKE_BUILD_TYPE=Release
          - os: windows-latest
            cc: cl
            cxx: cl
            cmake_args: -DCMAKE_BUILD_TYPE=Release -G "Visual Studio 17 2022" -A x64
          - os: macos-latest
            cc: clang
            cxx: clang++
            cmake_args: -DCMAKE_BUILD_TYPE=Release
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies (Ubuntu)
        if: matrix.os == 'ubuntu-latest'
        run: |
          sudo apt-get update
          sudo apt-get install -y cmake build-essential libgtest-dev

      - name: Configure
        run: |
          if [ "${{ matrix.os }}" = "windows-latest" ]; then
            cmake -B build ${{ matrix.cmake_args }}
          else
            cmake -B build -DCMAKE_C_COMPILER=${{ matrix.cc }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} ${{ matrix.cmake_args }}
          fi

      - name: Build
        run: cmake --build build --config Release

      - name: Run tests
        run: |
          if [ "${{ matrix.os }}" = "windows-latest" ]; then
            ctest --test-dir build -C Release --output-on-failure
          else
            cd build && ctest --output-on-failure
          fi

핵심: fail-fast: false로 한 플랫폼 실패 시 다른 플랫폼 결과도 확인할 수 있습니다.

3.2 GitLab CI 매트릭스

# .gitlab-ci.yml
stages:
  - build
  - test

variables:
  GIT_SUBMODULE_STRATEGY: recursive

.build_template: &build_template
  stage: build
  script:
    - cmake -B build -DCMAKE_BUILD_TYPE=Release $CMAKE_EXTRA
    - cmake --build build
  artifacts:
    paths:
      - build/

test:linux-gcc:
  <<: *build_template
  image: ubuntu:22.04
  variables:
    CMAKE_EXTRA: "-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++"
  script:
    - apt-get update && apt-get install -y cmake g++ libgtest-dev
    - cmake -B build -DCMAKE_BUILD_TYPE=Release
    - cmake --build build
    - cd build && ctest --output-on-failure

test:linux-clang:
  <<: *build_template
  image: ubuntu:22.04
  variables:
    CMAKE_EXTRA: "-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++"
  script:
    - apt-get update && apt-get install -y cmake clang libgtest-dev
    - cmake -B build -DCMAKE_BUILD_TYPE=Release
    - cmake --build build
    - cd build && ctest --output-on-failure

test:windows:
  <<: *build_template
  image: mcr.microsoft.com/windows:ltsc2022
  before_script:
    - choco install cmake visualstudio2022buildtools -y
  script:
    - cmake -B build -G "Visual Studio 17 2022" -A x64
    - cmake --build build --config Release
    - ctest --test-dir build -C Release --output-on-failure

3.3 CMake 테스트 등록 (크로스 플랫폼)

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(CrossPlatformApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

enable_testing()

# 단위 테스트 — 모든 플랫폼
add_executable(unit_tests
  test_calculator.cpp
  test_endian.cpp
)
target_link_libraries(unit_tests PRIVATE GTest::gtest_main)
add_test(NAME unit_tests COMMAND unit_tests)

# 통합 테스트 — 플러그인 경로 플랫폼별 설정
add_executable(integration_tests test_plugin_integration.cpp)
target_link_libraries(integration_tests PRIVATE GTest::gtest_main)
add_test(NAME integration_tests COMMAND integration_tests)

# 플랫폼별 환경 변수 설정
if(UNIX AND NOT APPLE)
  set_tests_properties(integration_tests PROPERTIES
    ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins:$ENV{LD_LIBRARY_PATH}"
  )
elseif(APPLE)
  set_tests_properties(integration_tests PROPERTIES
    ENVIRONMENT "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins:$ENV{DYLD_LIBRARY_PATH}"
  )
endif()

4. Docker 컨테이너 테스트

4.1 멀티 스테이지 Dockerfile (테스트용)

# Dockerfile.test
FROM ubuntu:22.04 AS builder

RUN apt-get update && apt-get install -y \
    cmake \
    g++ \
    libgtest-dev \
    git \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY . .

RUN cmake -B build -DCMAKE_BUILD_TYPE=Release && \
    cmake --build build

# 테스트 실행 스테이지
FROM ubuntu:22.04 AS test-runner

RUN apt-get update && apt-get install -y \
    libstdc++6 \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /src/build /src/build
WORKDIR /src/build

CMD ["ctest", "--output-on-failure"]

4.2 Docker Compose로 여러 환경 테스트

# docker-compose.test.yml
version: '3.8'

services:
  test-ubuntu-gcc:
    build:
      context: .
      dockerfile: Dockerfile.test
    image: myapp-test:ubuntu-gcc

  test-ubuntu-clang:
    build:
      context: .
      dockerfile: Dockerfile.test.clang
    image: myapp-test:ubuntu-clang

  test-alpine:
    build:
      context: .
      dockerfile: Dockerfile.test.alpine
    image: myapp-test:alpine

4.3 CI에서 Docker 테스트 실행

# .github/workflows/docker-test.yml
name: Docker Test

on: [push, pull_request]

jobs:
  docker-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and run tests in Docker
        run: |
          docker build -f Dockerfile.test -t myapp-test .
          docker run --rm myapp-test

4.4 ARM64 에뮬레이션 (QEMU)으로 크로스 아키텍처 테스트

# ARM64 테스트 (GitHub Actions)
  test-arm64:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and test on ARM64
        run: |
          docker build --platform linux/arm64 -f Dockerfile.test -t myapp-test-arm64 .
          docker run --rm --platform linux/arm64 myapp-test-arm64

5. 플랫폼별 테스트

5.1 플랫폼 감지 및 조건부 테스트

// platform_detect.hpp
#pragma once

#if defined(_WIN32) || defined(_WIN64)
  #define PLATFORM_WINDOWS 1
  #define PLATFORM_NAME "Windows"
#elif defined(__APPLE__)
  #include <TargetConditionals.h>
  #if TARGET_OS_IPHONE
    #define PLATFORM_IOS 1
    #define PLATFORM_NAME "iOS"
  #else
    #define PLATFORM_MACOS 1
    #define PLATFORM_NAME "macOS"
  #endif
#elif defined(__linux__)
  #if defined(__ANDROID__)
    #define PLATFORM_ANDROID 1
    #define PLATFORM_NAME "Android"
  #else
    #define PLATFORM_LINUX 1
    #define PLATFORM_NAME "Linux"
  #endif
#else
  #define PLATFORM_UNKNOWN 1
  #define PLATFORM_NAME "Unknown"
#endif

5.2 플랫폼별 테스트 케이스 (GTest)

// test_platform_path.cpp
#include <gtest/gtest.h>
#include <filesystem>
#include <string>
#include "platform_detect.hpp"

TEST(PlatformPathTest, PathSeparator) {
  // std::filesystem::path는 모든 플랫폼에서 / 지원
  auto p = std::filesystem::path("config") / "settings.json";
  EXPECT_FALSE(p.string().empty());

#if defined(PLATFORM_WINDOWS)
  EXPECT_TRUE(p.string().find('\\') != std::string::npos ||
              p.string().find('/') != std::string::npos);
#else
  EXPECT_TRUE(p.string().find('/') != std::string::npos);
#endif
}

TEST(PlatformPathTest, CurrentPathExists) {
  auto cwd = std::filesystem::current_path();
  EXPECT_TRUE(std::filesystem::exists(cwd));
}

#if defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS)
TEST(PlatformPathTest, HomeDirectory) {
  const char* home = std::getenv("HOME");
  if (home) {
    std::filesystem::path homePath(home);
    EXPECT_TRUE(std::filesystem::exists(homePath));
  }
}
#endif

#if defined(PLATFORM_WINDOWS)
TEST(PlatformPathTest, WindowsUserProfile) {
  const char* userProfile = std::getenv("USERPROFILE");
  if (userProfile) {
    std::filesystem::path profilePath(userProfile);
    EXPECT_TRUE(std::filesystem::exists(profilePath));
  }
}
#endif

5.3 동적 라이브러리 확장자 테스트

// test_platform_dlopen.cpp
#include <gtest/gtest.h>
#include <string>

std::string getSharedLibExtension() {
#if defined(_WIN32)
  return ".dll";
#elif defined(__APPLE__)
  return ".dylib";
#else
  return ".so";
#endif
}

std::string getSharedLibPrefix() {
#if defined(_WIN32)
  return "";
#else
  return "lib";
#endif
}

TEST(PlatformDlopenTest, ExtensionMatchesPlatform) {
  auto ext = getSharedLibExtension();
#if defined(_WIN32)
  EXPECT_EQ(ext, ".dll");
#elif defined(__APPLE__)
  EXPECT_EQ(ext, ".dylib");
#else
  EXPECT_EQ(ext, ".so");
#endif
}

TEST(PlatformDlopenTest, PrefixMatchesPlatform) {
  auto prefix = getSharedLibPrefix();
#if defined(_WIN32)
  EXPECT_EQ(prefix, "");
#else
  EXPECT_EQ(prefix, "lib");
#endif
}

5.4 정수 크기 검증 테스트

// test_integer_sizes.cpp
#include <gtest/gtest.h>
#include <cstdint>
#include <cstddef>

TEST(IntegerSizeTest, FixedWidthTypes) {
  EXPECT_EQ(sizeof(int8_t), 1u);
  EXPECT_EQ(sizeof(int16_t), 2u);
  EXPECT_EQ(sizeof(int32_t), 4u);
  EXPECT_EQ(sizeof(int64_t), 8u);
  EXPECT_EQ(sizeof(uint8_t), 1u);
  EXPECT_EQ(sizeof(uint32_t), 4u);
  EXPECT_EQ(sizeof(uint64_t), 8u);
}

TEST(IntegerSizeTest, SizeTMatchesPointer) {
  EXPECT_EQ(sizeof(size_t), sizeof(void*));
}

// long은 플랫폼마다 다를 수 있음 — 경고
TEST(IntegerSizeTest, LongSizeDocumented) {
  // Windows 64: 4, Linux/macOS 64: 8
  EXPECT_GE(sizeof(long), 4u);
  EXPECT_LE(sizeof(long), 8u);
}

6. 엔디안 테스트

6.1 엔디안 유틸리티

// endian_utils.hpp
#pragma once

#include <cstdint>
#include <cstring>

namespace endian {

inline bool isLittleEndian() {
  uint16_t x = 0x0001;
  return *reinterpret_cast<uint8_t*>(&x) == 1;
}

inline bool isBigEndian() {
  return !isLittleEndian();
}

inline uint16_t swap16(uint16_t x) {
  return ((x >> 8) & 0xFF) | ((x << 8) & 0xFF00);
}

inline uint32_t swap32(uint32_t x) {
  return ((x >> 24) & 0xFF) | ((x >> 8) & 0xFF00) |
         ((x << 8) & 0xFF0000) | ((x << 24) & 0xFF000000);
}

inline uint64_t swap64(uint64_t x) {
  return ((x >> 56) & 0xFF) | ((x >> 40) & 0xFF00) |
         ((x >> 24) & 0xFF0000) | ((x >> 8) & 0xFF000000) |
         ((x << 8) & 0xFF00000000) | ((x << 24) & 0xFF0000000000) |
         ((x << 40) & 0xFF000000000000) | ((x << 56) & 0xFF00000000000000);
}

// 네트워크 바이트 순서(Big Endian)로 변환
inline uint32_t toNetworkOrder(uint32_t host) {
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
  return host;
#else
  return swap32(host);
#endif
}

inline uint32_t fromNetworkOrder(uint32_t net) {
  return toNetworkOrder(net);  // 대칭
}

}  // namespace endian

6.2 엔디안 테스트 케이스

// test_endian.cpp
#include <gtest/gtest.h>
#include "endian_utils.hpp"
#include <cstring>

TEST(EndianTest, DetectEndianness) {
  bool little = endian::isLittleEndian();
  bool big = endian::isBigEndian();
  EXPECT_NE(little, big);
  EXPECT_TRUE(little || big);
}

TEST(EndianTest, Swap16RoundTrip) {
  uint16_t original = 0x1234;
  uint16_t swapped = endian::swap16(original);
  uint16_t back = endian::swap16(swapped);
  EXPECT_EQ(original, back);
}

TEST(EndianTest, Swap32RoundTrip) {
  uint32_t original = 0x12345678;
  uint32_t swapped = endian::swap32(original);
  uint32_t back = endian::swap32(swapped);
  EXPECT_EQ(original, back);
}

TEST(EndianTest, Swap32ChangesByteOrder) {
  uint32_t x = 0x01020304;
  uint32_t s = endian::swap32(x);
  // Little: 04 03 02 01, Big: 01 02 03 04
  EXPECT_NE(x, s);
  EXPECT_EQ(endian::swap32(s), x);
}

TEST(EndianTest, ToNetworkOrderRoundTrip) {
  uint32_t host = 0x12345678;
  uint32_t net = endian::toNetworkOrder(host);
  uint32_t back = endian::fromNetworkOrder(net);
  EXPECT_EQ(host, back);
}

TEST(EndianTest, BinarySerializationConsistency) {
  uint32_t value = 0xDEADBEEF;
  uint8_t buf[4];

  // 호스트 순서로 쓰고 읽기
  std::memcpy(buf, &value, 4);
  uint32_t read;
  std::memcpy(&read, buf, 4);
  EXPECT_EQ(value, read);

  // 네트워크 순서로 쓰고 읽기 (플랫폼 독립)
  uint32_t net = endian::toNetworkOrder(value);
  std::memcpy(buf, &net, 4);
  uint32_t netRead;
  std::memcpy(&netRead, buf, 4);
  EXPECT_EQ(endian::fromNetworkOrder(netRead), value);
}

6.3 프로토콜 헤더 직렬화 테스트

// protocol.hpp
#pragma once

#include <cstdint>
#include "endian_utils.hpp"

struct PacketHeader {
  uint32_t magic;
  uint32_t length;
  uint16_t version;
  uint16_t flags;

  void toNetwork() {
    magic = endian::toNetworkOrder(magic);
    length = endian::toNetworkOrder(length);
    version = endian::swap16(version);
    flags = endian::swap16(flags);
  }

  void fromNetwork() {
    magic = endian::fromNetworkOrder(magic);
    length = endian::fromNetworkOrder(length);
    version = endian::swap16(version);
    flags = endian::swap16(flags);
  }
};
// test_protocol.cpp
#include <gtest/gtest.h>
#include "protocol.hpp"
#include <cstring>

TEST(ProtocolTest, HeaderSerializationRoundTrip) {
  PacketHeader orig{0xCAFEBABE, 1024, 1, 0};
  PacketHeader sent = orig;
  sent.toNetwork();

  uint8_t buf[sizeof(PacketHeader)];
  std::memcpy(buf, &sent, sizeof(PacketHeader));

  PacketHeader recv;
  std::memcpy(&recv, buf, sizeof(PacketHeader));
  recv.fromNetwork();

  EXPECT_EQ(recv.magic, orig.magic);
  EXPECT_EQ(recv.length, orig.length);
  EXPECT_EQ(recv.version, orig.version);
  EXPECT_EQ(recv.flags, orig.flags);
}

7. 자주 발생하는 에러와 해결법

에러 1: “cannot open shared object file” (Linux CI)

증상: 로컬에서는 통과하는데 CI에서 플러그인 로드 테스트 실패

원인: LD_LIBRARY_PATH 미설정

해결:

# CMakeLists.txt
set_tests_properties(integration_tests PROPERTIES
  ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins:$ENV{LD_LIBRARY_PATH}"
)
# 또는 실행 전 수동 설정
export LD_LIBRARY_PATH=$PWD/build/plugins:$LD_LIBRARY_PATH
./integration_tests

에러 2: “dyld: Library not loaded” (macOS)

증상: macOS에서만 동적 라이브러리 로드 실패

원인: DYLD_LIBRARY_PATH 또는 @rpath 미설정

해결:

if(APPLE)
  set_tests_properties(integration_tests PROPERTIES
    ENVIRONMENT "DYLD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins:$ENV{DYLD_LIBRARY_PATH}"
  )
  set_target_properties(integration_tests PROPERTIES
    BUILD_RPATH "${CMAKE_BINARY_DIR}/plugins"
    INSTALL_RPATH "@loader_path/../plugins"
  )
endif()

에러 3: Windows에서 “DLL not found”

증상: LoadLibrary 실패, 테스트 실행 시 DLL을 찾을 수 없음

원인: DLL이 실행 파일과 같은 디렉터리에 없거나 PATH에 없음

해결:

# 테스트 실행 디렉터리에 DLL 복사
add_test(NAME integration_tests COMMAND integration_tests)
set_tests_properties(integration_tests PROPERTIES
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
# 또는 POST_BUILD로 DLL 복사
add_custom_command(TARGET integration_tests POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
    $<TARGET_FILE:test_plugin>
    $<TARGET_FILE_DIR:integration_tests>
)

에러 4: “파일을 찾을 수 없음” (경로 구분자)

증상: path/to/file이 Windows에서 실패

원인: 하드코딩된 / 경로

해결:

// ❌ 잘못된 예
std::string path = "testdata" + std::string("/") + "input.txt";

// ✅ 올바른 예
auto path = std::filesystem::path("testdata") / "input.txt";
std::ifstream f(path);

에러 5: 엔디안으로 인한 파싱 오류

증상: x86에서는 되는데 ARM에서 바이너리 파싱 실패

원인: 호스트 바이트 순서로 직접 저장

해결:

// ❌ 잘못된 예 — 호스트 순서 그대로 저장
uint32_t len = 1000;
file.write(reinterpret_cast<char*>(&len), 4);

// ✅ 올바른 예 — 네트워크 바이트 순서로 저장
uint32_t len = 1000;
uint32_t netLen = endian::toNetworkOrder(len);
file.write(reinterpret_cast<char*>(&netLen), 4);

에러 6: “unistd.h not found” (Windows CI)

증상: Windows 빌드에서 POSIX 헤더 에러

원인: 플랫폼별 #include 누락

해결:

// ❌ 잘못된 예
#include <unistd.h>

// ✅ 올바른 예
#if defined(_WIN32)
  #include <io.h>
  #include <process.h>
#else
  #include <unistd.h>
#endif

에러 7: 테스트 순서에 따른 플래키

증상: 단독 실행 시 통과, 전체 실행 시 실패

원인: 전역 상태, 환경 변수 공유

해결:

class IsolatedTest : public ::testing::Test {
protected:
  void SetUp() override {
    savedEnv = std::getenv("LD_LIBRARY_PATH");
  }
  void TearDown() override {
    if (savedEnv)
      setenv("LD_LIBRARY_PATH", savedEnv, 1);
  }
  const char* savedEnv = nullptr;
};

에러 8: Docker 내부에서 테스트 타임아웃

증상: CI Docker job이 타임아웃으로 종료

원인: QEMU 에뮬레이션 등으로 실행이 느림

해결:

# GitHub Actions
  test-arm64:
    timeout-minutes: 30
    steps: ...
// 느린 테스트는 환경 변수로 스킵
TEST(IntegrationTest, SlowNetwork) {
  if (!std::getenv("RUN_SLOW_TESTS")) {
    GTEST_SKIP() << "Set RUN_SLOW_TESTS=1 to run";
  }
  // ...
}

8. 베스트 프랙티스

8.1 모든 지원 플랫폼에서 CI 실행

# 최소한 Windows, Linux, macOS는 매트릭스로
matrix:
  os: [ubuntu-latest, windows-latest, macos-latest]

8.2 std::filesystem으로 경로 처리

// ✅ 크로스 플랫폼 경로
auto path = std::filesystem::path("config") / "settings.json";

8.3 고정 크기 정수 타입 사용

#include <cstdint>
uint32_t length;  // 항상 4바이트
int64_t offset;   // 항상 8바이트

8.4 직렬화는 네트워크 바이트 순서

// 파일·네트워크 프로토콜은 Big Endian 고정
uint32_t net = endian::toNetworkOrder(value);

8.5 플랫폼별 테스트는 해당 OS에서만 실행

#if defined(PLATFORM_LINUX)
TEST(PlatformTest, LinuxSpecific) { ... }
#endif

8.6 Docker로 로컬-CI 환경 일치

docker build -f Dockerfile.test -t myapp-test .
docker run --rm myapp-test

8.7 테스트 픽스처 경로는 환경 변수 허용

std::string getTestDataDir() {
  const char* env = std::getenv("TEST_DATA_DIR");
  if (env) return env;
  return std::filesystem::path(BINARY_DIR) / "testdata";
}

9. 프로덕션 패턴

9.1 통합 크로스 플랫폼 CI 워크플로우

sequenceDiagram
  participant Dev as 개발자
  participant GH as GitHub Actions
  participant Win as Windows Runner
  participant Linux as Linux Runner
  participant Mac as macOS Runner

  Dev->>GH: push
  GH->>Win: build + test
  GH->>Linux: build + test
  GH->>Mac: build + test
  Win-->>GH: result
  Linux-->>GH: result
  Mac-->>GH: result
  GH->>Dev: 모든 플랫폼 통과 시 merge 가능

9.2 단계별 테스트 (빠른 것 먼저)

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - run: cmake -B build && cmake --build build
      - run: ctest --test-dir build -R unit_ -R endian_

  integration:
    needs: unit
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - run: cmake -B build && cmake --build build
      - run: ctest --test-dir build -R integration_

9.3 아티팩트로 플랫폼별 빌드 보존

- name: Upload artifacts
  uses: actions/upload-artifact@v4
  with:
    name: build-${{ matrix.os }}
    path: build/

9.4 커버리지는 Linux에서만 (비용 절감)

coverage:
  runs-on: ubuntu-latest
  steps:
    - run: cmake -B build -DENABLE_COVERAGE=ON
    - run: cmake --build build
    - run: ctest --test-dir build
    - run: lcov --capture --directory build --output-file coverage.info

9.5 플랫폼별 릴리스 태그

release:
  if: startsWith(github.ref, 'refs/tags/')
  strategy:
    matrix:
      os: [ubuntu-latest, windows-latest, macos-latest]
  steps:
    - run: cmake -B build -DCMAKE_BUILD_TYPE=Release
    - run: cmake --build build
    - run: cpack -B dist
    - uses: actions/upload-artifact@v4
      with:
        name: ${{ matrix.os }}-release
        path: build/dist/

10. 구현 체크리스트

  • CI 매트릭스에 Windows·Linux·macOS 포함
  • LD_LIBRARY_PATH(Linux), DYLD_LIBRARY_PATH(macOS) 테스트 설정
  • std::filesystem::path로 경로 처리
  • 플랫폼별 #ifdef 테스트 케이스 추가
  • 엔디안 유틸리티 및 직렬화 테스트
  • 고정 크기 타입(int32_t 등) 사용
  • Docker로 로컬-CI 환경 일치 (선택)
  • fail-fast: false로 매트릭스 결과 전체 확인
  • 테스트 픽스처 경로는 TEST_DATA_DIR 환경 변수 지원

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 멀티 플랫폼 라이브러리, Windows·Linux·macOS 동시 지원 앱, 임베디드·서버 크로스 컴파일 시 활용합니다. 본문의 CI 매트릭스와 Docker 예제를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 크로스 플랫폼 빌드(#55-4), 테스트 전략(#55-7), GTest 단위 테스트(#18-1)를 먼저 읽으면 이해가 쉽습니다.

Q. CI에서 모든 플랫폼을 테스트하려면?

A. GitHub Actions·GitLab CI 매트릭스 빌드로 Windows·Linux·macOS를 병렬 실행합니다. Docker로 일관된 환경을 구성할 수 있습니다. 본문의 완전한 예제를 참고하세요.

Q. Big Endian 환경을 로컬에서 어떻게 테스트하나요?

A. QEMU로 qemu-system-arm -M virt 등 Big Endian 머신을 에뮬레이션하거나, CI에서 ARM64/빅엔디안 Docker 이미지를 사용합니다. 또는 엔디안 스왑 함수를 직접 호출하는 단위 테스트로 검증합니다.

Q. 프로덕션에서 주의할 점은?

A. (1) 직렬화·프로토콜은 항상 네트워크 바이트 순서를 사용하고, (2) long·size_t 대신 int32_t·uint64_t 등 고정 크기 타입을 쓰며, (3) CI에서 모든 지원 플랫폼을 매트릭스로 테스트합니다.


참고 자료


한 줄 요약: CI 매트릭스·Docker·플랫폼별·엔디안 테스트를 체계적으로 구성하면, “Windows에서는 되는데 Linux에서만 실패” 문제를 배포 전에 잡을 수 있습니다.


관련 글

  • C++ 크로스 플랫폼 기초 완벽 가이드 | 플랫폼 감지·std::filesystem
  • C++ 크로스 플랫폼 빌드 완벽 가이드 | CMake 툴체인·CPack·ABI 안정성·프로덕션 패턴
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3