# 자동화된 빌드와 테스트(간단 CI 개념)

#### 개념 이해

지속적 통합(Continuous Integration, CI)은 소프트웨어를 개발하는 과정에서 코드가 변경될 때마다 자동으로 빌드, 테스트, 품질 검사 등을 수행하는 기법이다. 일반적으로 CI 서버(또는 서비스)를 운영하여 변경 사항이 저장소에 push되었을 때 즉시 빌드와 테스트가 진행된다. 이는 다음과 같은 장점이 있다.

* **코드 결함 조기 발견**: 작은 단위로 자주 테스트하므로 오류나 품질 저하를 빠르게 파악 가능
* **일관성 있는 품질 유지**: 매 빌드마다 동일한 프로세스로 테스트하여 프로젝트 품질을 일정 수준 이상으로 유지
* **빌드 자동화**: 개발자가 직접 로컬에서 빌드하지 않아도, 서버에서 자동으로 빌드 진행
* **테스트 자동화**: 여러 플랫폼이나 여러 구성에서 테스트를 자동으로 수행

ROS2 Humble 환경에서도 CI를 적극 도입하면, 메시지 타입 변경이나 새로운 패키지 추가 시 빌드와 관련된 문제를 빠르게 확인할 수 있다.

#### CI 파이프라인의 기본 구성 요소

CI 파이프라인은 일반적으로 다음 단계를 거친다.

코드 체크아웃(Checkout):

* 저장소(GitHub, GitLab 등)에 push된 최신 코드를 CI 서버에서 가져온다.

의존성 설치(Dependency Installation):

* 빌드와 테스트를 하기 위한 사전 준비 단계이다.
* ROS2 Humble의 경우 필요한 패키지, 의존성 등을 apt, pip 등을 통해 설치하거나, rosdep 등을 이용해 의존성을 만족시킨다.

빌드(Build):

* ROS2 빌드 도구(colcon 등)를 이용하여 패키지를 컴파일하고 링크한다.
* 예:

  ```bash
  colcon build --symlink-install
  ```

테스트(Test):

* 빌드가 완료되면 단위 테스트, 통합 테스트, 로직 테스트 등을 자동으로 실행한다.
* 예:

  ```bash
  colcon test
  colcon test-result --all
  ```

결과 보고(Report):

* 빌드와 테스트 결과를 CI 콘솔이나 협업 도구(Slack, 이메일 등)로 알려준다.
* 테스트 실패 시 로그나 리포트를 바로 확인하고 수정할 수 있다.

#### CI 단계별 작업

ROS2 Humble 프로젝트에서 CI 설정 파일(예: GitHub Actions에서는 `*.yml` 파일, GitLab CI에서는 `.gitlab-ci.yml`, Jenkins에서는 Jenkinsfile 등)을 구성할 때 다음을 주의한다.

ROS2 Humble용 Docker 이미지 활용:

* CI 환경이 매번 깨끗한 상태에서 실행되도록 Docker 이미지를 사용한다.
* 이미지 안에 ROS2 Humble과 관련 의존성이 미리 깔려 있으면 빌드 시간을 단축할 수 있다.

colcon + CTest 활용:

* CMake 기반인 ROS2 프로젝트는 대부분 CTest로 테스트 스크립트를 구성하므로, colcon 빌드 후에는 colcon test를 실행하여 자동으로 모든 테스트를 진행한다.

전처리 단계:

* rosdep이나 apt를 통해 필요한 패키지를 모두 설치해야 빌드와 테스트가 문제없이 진행된다.
* 예:

  ```bash
  rosdep install --from-paths src --ignore-src -y
  ```

병렬 빌드 및 캐시:

* CI 시간을 단축하기 위해 colcon build 시 $-j$ 옵션 등을 활용하거나, 빌드 캐시를 도입할 수 있다.
* 다만, 캐시가 잘못 관리되면 간헐적 빌드 실패가 발생할 수 있으므로 주의해야 한다.

#### ROS2 Humble에서의 테스트 적용 예시

아래는 GitHub Actions를 사용하여 ROS2 Humble 패키지를 자동 빌드 및 테스트하는 간단한 워크플로 예시다.

```yaml
name: ROS2 Humble CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup ROS2 Humble
        uses: ros-tooling/setup-ros@v0.2
        with:
          ros-distro: humble

      - name: Install Dependencies
        run: |
          sudo apt-get update
          rosdep update
          rosdep install --from-paths src --ignore-src -y

      - name: Build
        run: colcon build --symlink-install

      - name: Test
        run: |
          colcon test
          colcon test-result --all
```

위와 같은 간단한 설정만으로도 CI에 대한 기초적인 로직이 완성된다. 실제 프로젝트 규모가 커지면 워크플로에 Lint, 코드 커버리지 측정, 문서 생성 등을 포함할 수 있다.

#### 병렬 빌드와 병렬 테스트

CI 환경에서 빌드와 테스트를 병렬로 수행하면 전체 실행 시간을 단축할 수 있다. 로컬 환경에서처럼 `-j` 옵션을 적용하거나, colcon을 이용할 때 빌드를 병렬로 처리하도록 설정할 수 있다.

colcon build의 병렬 옵션:

* 일반적으로 colcon build에 `--parallel-workers` 혹은 `-j` 옵션을 적용한다. CPU 코어 개수만큼 지정하면 빌드 시간을 크게 단축할 수 있다.
* 예:

  ```bash
  colcon build --symlink-install --parallel-workers 4
  ```

colcon test의 병렬 처리:

* 기본적으로 colcon test는 CTest가 호출되며, `-j` 옵션을 통해 동시에 여러 테스트를 수행할 수 있다.
* 단, ROS 노드를 사용하는 테스트는 서로 간섭이 생기지 않도록 주의해야 한다.

#### 다양한 플랫폼에서의 빌드와 테스트

ROS2는 여러 OS(Linux, Windows, macOS)에서 동작할 수 있으므로, CI에서 동시에 여러 플랫폼을 대상으로 빌드와 테스트가 가능하다. 예를 들어 GitHub Actions를 사용하면 다음과 같이 OS 매트릭스를 지정할 수 있다.

```yaml
name: ROS2 Humble Multi-Platform CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup ROS
        uses: ros-tooling/setup-ros@v0.2
        with:
          ros-distro: humble
      # 나머지 빌드/테스트 과정은 동일
```

이와 같이 매트릭스를 통해 여러 플랫폼을 동시에 테스트하면 특정 플랫폼에서만 발생하는 오류를 사전에 발견할 수 있다.

#### 아티팩트 저장

CI 실행 후, 빌드 결과물(바이너리, 로그, 테스트 리포트 등)을 \*\*아티팩트(Artifacts)\*\*로 저장할 수 있다. GitHub Actions에서는 이를 통해 테스트 로그나 문서 등이 보존된다.

```yaml
- name: Archive test results
  uses: actions/upload-artifact@v2
  with:
    name: test-results
    path: build/*/ Testing/*/Test.xml
```

이렇게 하면 CI가 끝난 후에도 테스트 결과를 내려받아 확인할 수 있으므로, 문제 발생 시 디버깅에 유용하다.

#### 캐시(Cache) 활용

의존성 설치나 빌드에 필요한 파일을 캐싱하면 CI 시간을 크게 단축할 수 있다.

* **apt/yarn/npm 캐시**: 의존성 설치 시간 단축
* **colcon 빌드 캐시**: 빌드 중간 결과를 재사용

GitHub Actions의 예시:

```yaml
- name: Cache colcon build
  uses: actions/cache@v2
  with:
    path: build
    key: ${{ runner.os }}-colcon-${{ hashFiles('**/*.cpp', '**/*.h', '**/*.cmake', '**/package.xml') }}
    restore-keys: |
      ${{ runner.os }}-colcon-
```

단, ROS2 Humble 프로젝트에서 캐시를 잘못 사용하면 잔여 아티팩트가 예상치 못한 에러를 일으킬 수 있으니 주의가 필요하다. 캐시가 제대로 적용되지 않는 상황을 대비해 정기적으로 클린 빌드도 수행하는 전략이 필요하다.

#### Nightly 빌드

낮에는 CI가 풀 리퀘스트 빌드와 테스트 위주로 수행되고, 밤에는 전체 프로젝트에 대해 종합적인 테스트(예: 정적 분석, 코드 커버리지 측정 등)를 수행하는 Nightly 빌드를 운영하는 경우가 많다. 이를 통해 부하가 많은 테스트를 분산 처리할 수 있으며, 평소에 발견되지 않은 잠재적 문제를 찾는 데 유리하다.

#### 코드 커버리지 측정

CI 과정에서 \*\*코드 커버리지(Code Coverage)\*\*를 측정하면, 테스트가 실제로 어느 정도 범위를 커버하고 있는지 정량적으로 파악할 수 있다. ROS2 프로젝트에서 코드 커버리지 측정은 주로 C++ 코드(또는 Python 코드)에 대한 테스트 적용 범위를 확인하기 위해 아래와 같은 흐름으로 진행된다.

커버리지 수집 도구 설정:

* Linux 환경에서는 일반적으로 `gcov` 및 `lcov`를 많이 활용한다.
* CMake에서 커버리지 옵션을 활성화하기 위해선 빌드 시 `$-fprofile-arcs -ftest-coverage$` 등을 적용해야 한다.

빌드 시 커버리지 플래그 추가:

* 예를 들어, CMakeLists.txt에 다음과 같은 처리를 추가할 수 있다.

```cmake
if(COVERAGE)
  add_compile_options(--coverage -O0)
  add_link_options(--coverage)
endif()
```

* colcon을 이용할 때는, 커맨드라인에서 다음과 같이 정의해주기도 한다:

```bash
colcon build --cmake-args -DCOVERAGE=ON
```

테스트 실행 후 커버리지 리포트 생성:

* 테스트를 수행하면, 빌드 디렉터리에 `.gcno`, `.gcda` 파일이 생성된다.
* `lcov` 로 이 파일들을 수집하여 `.info` 형식으로 변환할 수 있다.

```bash
lcov --capture --directory build --output-file coverage.info
```

* `genhtml` 을 사용하면 HTML 형태의 시각적 리포트를 얻을 수 있다.

```bash
genhtml coverage.info --output-directory coverage_report
```

CI에서 자동화:

* GitHub Actions 예시:

```yaml
- name: Run tests with coverage
  run: |
    colcon build --cmake-args -DCOVERAGE=ON
    colcon test
    colcon test-result --all
    lcov --capture --directory build --output-file coverage.info
    genhtml coverage.info --output-directory coverage_report
- name: Upload coverage artifact
  uses: actions/upload-artifact@v2
  with:
    name: coverage-report
    path: coverage_report
```

* 생성된 HTML 리포트를 아티팩트로 업로드해두면, 언제든지 다운로드하여 실제 테스트 커버리지를 확인할 수 있다.

#### 정적 분석(Static Analysis)과 스타일 검사(Lint)

자동화된 빌드와 테스트에 **정적 분석**과 **코드 스타일 검사**도 포함하면, 코드 품질을 한층 더 높일 수 있다.

* **ament\_lint**: ROS2에서 권장되는 C++ 코드 스타일, Python 코드 스타일, CMake 스타일 등을 검사하는 패키지이다.
* **cppcheck**, **clang-tidy**: C++ 정적 분석 도구로, 잠재적인 오류나 안전성 문제를 잡아준다.

**ament\_lint 예시**

ROS2 Humble 프로젝트에서 `package.xml` 파일에 ament\_lint를 의존성으로 추가해두고, 빌드 시 자동으로 린트 테스트를 수행할 수 있다.

```xml
<buildtool_depend>ament_lint_auto</buildtool_depend>
<buildtool_depend>ament_lint_common</buildtool_depend>
colcon build --packages-select my_package
colcon test --packages-select my_package
```

이때, `my_package` 내 `CMakeLists.txt`에서 `ament_lint_auto_find_test_dependencies()`를 호출하면 해당 패키지에 린트 규칙을 자동으로 적용한다.

#### 트러블슈팅: CI 과정에서 자주 발생하는 문제

자동화된 빌드와 테스트(CI)를 운영하다 보면, 아래와 같은 문제에 자주 봉착하게 된다. 이를 사전에 인지하고 대응 방안을 마련하면 CI 파이프라인을 안정적으로 유지할 수 있다.

* **의존성 버전 불일치**
  * 팀원 로컬 환경과 CI 서버 환경의 패키지 혹은 라이브러리 버전이 달라서 빌드 오류가 발생한다.
  * 대응 방안:
    * CI 빌드 환경(예: Docker 컨테이너, VM 이미지)을 동일하게 유지한다.
    * ROS2 Humble 공식 Docker 이미지를 사용하고, 필요 의존성을 자동 설치하도록 `rosdep`을 신경 써서 구성한다.
    * $rosdep$ 데이터베이스를 정기적으로 업데이트하여, 누락된 의존성으로 인해 빌드가 실패하는 경우를 최소화한다.
* **빌드 캐시 충돌**
  * 빌드 캐시를 잘못 사용하면, 이전 빌드의 산출물이 새 빌드에 영향을 미쳐 예측 불가능한 결과를 초래한다.
  * 대응 방안:
    * 정기적으로(예: 매주 1회) 캐시를 초기화하여 클린 빌드를 강제한다.
  * 유닛 테스트 실패나 예상치 못한 동작이 발생하면, 먼저 캐시를 비운 뒤 다시 빌드하여 문제 발생 원인을 캐시에서 배제한다.
* **테스트 간 리소스 충돌**
  * 여러 테스트가 동시에 실행될 때, 서로 공유 리소스(포트, 파일, 하드웨어 등)에 동시 접근하여 충돌이 일어날 수 있다.
  * 대응 방안:
    * 각 테스트를 **독립적**으로 설계한다. (예: 임시 파일이나 임시 포트를 사용)
    * ROS2 노드를 띄워서 통신 테스트를 할 경우에는 서로 다른 네임스페이스(namespace)를 활용한다.
    * 빌드 스크립트 또는 CTest 설정에서 테스트 실행 순서를 제어하거나, 병렬 테스트를 제한($-j 1$ 등)한다.
* **Ubuntu/Foxglove/Webots 등 다양한 시뮬레이터와의 호환성 이슈**
  * ROS2 환경에서 Gazebo(또는 Ignition), Webots 등 여러 시뮬레이터를 사용하는 경우, CI 환경에서 헤드리스(headless)로 구동 시 시뮬레이터 자체가 정상 작동하지 않는 경우가 발생한다.
  * 대응 방안:
    * X virtual framebuffer(Xvfb) 등 가상 디스플레이 환경을 설정한다.
    * 실제 GPU가 필요한 경우, GPU가 활성화된 Runner(또는 Docker, VM)를 마련한다.
    * GUI 렌더링이 필요한 경우, CI에서 VNC 같은 원격 접속 방식을 택하기도 한다.
* **실행 시간 초과**
  * 테스트 케이스가 너무 많거나 시뮬레이션 시간이 길어 CI가 타임아웃에 걸릴 수 있다.
  * 대응 방안:
    * 테스트 케이스를 핵심 기능과 확장 기능으로 분리한다.
      * 중요한 핵심 기능 테스트는 빠르게 수행하고, 나머지는 Nightly 빌드에서 수행
    * 시뮬레이터 테스트를 실제 물리 엔진까지 모두 돌리지 않고, 간단한 Mock 노드나 스텁(stub)을 이용해 단위 테스트만 우선 수행한다.

#### 모듈화된 CI 구성

프로젝트 규모가 확장되면 하나의 CI 파이프라인에 모든 단계를 넣는 대신, **다중 파이프라인** 혹은 **모듈화된 워크플로**가 필요해진다.

* **빌드 파이프라인**: 의존성 설치, 컴파일, 린트, 단위 테스트
* **통합 테스트 파이프라인**: 여러 ROS 패키지를 통합하여 기능 검증(시뮬레이터나 Mock을 통한 자동화 테스트)
* **릴리스 파이프라인**: 배포 전 최종 점검(버전 태깅, 바이너리 아티팩트 생성 등)

예를 들어, GitHub Actions에서는 워크플로 파일을 여러 개로 쪼개서 관리할 수 있다.

```bash
.github/workflows/
|-- build.yml
|-- integration_test.yml
|-- release.yml
```

각 워크플로 간에는 아티팩트 업로드/다운로드, 혹은 Docker 레지스트리 등을 통해 결과물을 연계한다.

#### 멀티 리포지토리(Multi-repo) 환경

ROS2 프로젝트가 커지면 여러 저장소(repo)로 분할되는 경우가 흔하다. 이때 각각의 저장소마다 CI를 설정하면, 의존 패키지를 포함하지 못해 테스트가 제한적일 수 있다.

* **서브모듈(Submodule) 활용**: Git submodule로 다른 저장소를 가져와 빌드할 수 있다.
* **rosinstall 파일 활용**: 여러 저장소를 한 번에 다운로드할 수 있도록 `.rosinstall` 파일에 명시한다.
* **통합 저장소(마스터 repo) 운영**: 모든 ROS2 패키지를 종합 관리하는 마스터 리포지토리를 하나 두고, 여러 서브 리포지토리의 변화를 주기적으로 병합한다.

이와 같이 설계하면 각 저장소마다 독립적으로 CI를 거치면서도, 최종적으로는 통합 빌드와 통합 테스트를 통해 전체적인 호환성을 확인할 수 있다.
