19.4 Zenoh 애플리케이션을 위한 지속적 통합(CI) 자동화
아무리 뛰어난 천재 프로그래머라 할지라도, 인간은 반드시 실수를 저지른다. 오타 하나가 시스템 메모리를 조금씩 누수 시키는 Memory Leak를 유발하고, 이 작은 균열 하나가 전 세계에 퍼져 있는 로봇 군단의 동작을 멈추게 만들 수 있다.
이 참사를 막기 위해, 우리는 개발자가 코드를 메인 저장소(Git)로 합치기(Merge) 전에 인간의 코드를 대신 검사하고 테스트해 주는 수문장(Gatekeeper)을 세워야 한다. 이 수문장이 바로 지속적 통합 (Continuous Integration, CI) 파이프라인이다.
1. CI 파이프라인이 수호해야 하는 세 가지 도그마
단순히 “빌드가 성공했다“는 초록색 체크 마크(✓)에 안심해선 안 된다. 진정한 의미의 CI란 다음 세 가지의 도그마(Dogma)를 기계적으로 증명해 내는 과정이다.
- 문법적 무결성 (Syntactic Integrity): 들여쓰기 공간 크기가 조직 규칙에 부합하는가? 쓰이지도 않는 변수를 선언만 해두고 방치하지 않았는가? (
Linting & Static Analysis) - 논리적 무결성 (Logical Integrity):
1 + 1을 넣었을 때 정확히2가 반환되는가? 특정 센서 값이 마이너스(-)로 입력되었을 때 코드가 패닉(Panic)에 빠지지 않고 정해진 에러를 출력하는가? (Unit Testing) - 환경적 무결성 (Environmental Integrity): 내 노트북에서는 잘 돌아가는 코드가, 프로덕션 환경(우분투)에서도 의존성 깨짐 없이 잘 돌아가는가? Zenoh 라우터를 하나 띄우고 실제로 패킷을 던졌을 때 제대로 응답하는가? (
Integration Testing)
flowchart TD
Developer([개발자]) -->|1. PR 생성| GitHub(GitHub / GitLab)
subgraph "CI Pipeline (GitHub Actions)"
Direction TB
A[Git Checkout] --> B(Linter/Static Analyzer 실행)
B -->|실패 시 차단| Stop1[Merge 블록]
B -.->|성공| C(소스코드 컴파일 및 빌드)
C --> D(Unit Test 실행)
D -->|실패 시 차단| Stop2[Merge 블록]
D -.->|성공| E(Isolated Integration Test 환경 조성)
E --> F(TestContainers 기반 가상 Zenoh Router 구동)
F --> G(Integration Test 실행)
G -->|실패 시 보안/의존성 탈락| Stop3[Merge 블록]
G -.->|최종 통과| Success{Merge 권한 승인}
end
GitHub --> A
본 19.4 장에서는 GitHub Actions와 GitLab CI와 같은 강력한 도구들을 사용하여, 인간의 감정이 전혀 섞이지 않은 냉혹한 심판자(CI 봇)를 구축하고 가상 환경에서 Zenoh 기반의 코드를 극한으로 밀어붙이는 기술을 설계할 것이다.
2. GitHub Actions 및 GitLab CI 기반의 빌드 파이프라인 구성
코드를 저장하는 창고(Git Repository)는 단순한 파일 백업 장소가 아니다. 코드가 푸시(Push)되는 순간, 저장소 자체에 내장된 이벤트 후크(Webhook)가 폭발하며 가상의 워커 노드(Runner)들을 깨워 빌드를 시작하게 만드는 동적 엔진의 피벗 포인트다.
가장 대표적인 두 가지 CI 툴인 GitHub Actions와 GitLab CI를 활용하여, Zenoh 애플리케이션의 뼈대(바이너리)를 찍어내는 공조(Pipeline) 시스템의 명세 작성을 다룬다.
2.1 [Runbook] GitHub Actions 기반 완벽 주의 빌드 파이프라인
GitHub 저장소 최상단 .github/workflows/zenoh-ci.yml 파일 하나로 모든 것이 통제된다. 이 YAML 파일은 “언제 실행될 것인가”, “어떤 환경에서 실행될 것인가”, “무슨 명령을 수행할 것인가“를 정의한 법전이다.
2.1.1 지능적 트리거와 캐싱(Caching)을 통한 빌드 시간 압축
Rust와 C++의 컴파일 속도는 악명 높다. 매 빌드마다 기초 라이브러리(Dependencies)를 새로 다운로드하고 컴파일한다면 10분짜리 파이프라인이 1시간으로 늘어나는 재앙을 맞이한다. 이 지연을 타파하기 위해 강력한 캐싱(Cache) 메커니즘을 융합한다.
## .github/workflows/zenoh-ci.yml
name: Zenoh Core Application CI
## 메인 브랜치로 향하는 Pull Request(PR)가 열리거나 갱신될 때만 동작하라.
on:
pull_request:
branches: [ "main", "develop" ]
## 여러 빌드 스텝을 하나로 묶는 논리적 그룹 (Job)
jobs:
build_and_test_rust:
name: Build and Test Rust (Ubuntu-22.04)
runs-on: ubuntu-22.04 # 깨끗한 우분투 가상머신을 매번 새로 띄움
steps:
- name: 1. 코드 체크아웃 (소스코드 복사)
uses: actions/checkout@v4
- name: 2. Rust 툴체인 자동 세팅
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable # 안정화된 최신 Rust 버전을 세팅
- name: 3. Rust 종속성 캐싱 (최강의 무기)
# 이전에 다운받았던 라이브러리들(~/.cargo/registry)을 압축해서 로드해 온다.
# 이 한 줄로 빌드 시간이 15분에서 2분으로 단축된다.
uses: Swatinem/rust-cache@v2
- name: 4. 컴파일 검증 (바이너리 생성 금지, 문법 검사만 빠르게)
run: cargo check --all-targets --all-features
2.1.2 GitLab CI 구조의 선언적 간결성 통제
여러분의 기업이 보안을 이유로 퍼블릭 클라우드인 GitHub을 쓰지 못하고, 인트라넷 환경에 GitLab을 구축했다면 .gitlab-ci.yml 문법을 따라야 한다. GitLab은 stages 키워드를 통해 여러 파이프라인 파트를 횡단보도의 파란불처럼 앞 단계가 100% 끝나야만 뒤 단계를 진행시키는 엄격한 선형 제어(Linear Orchestration)를 선사한다.
## .gitlab-ci.yml
## 파이프라인을 3개의 거대한 스테이지로 나눈다.
stages:
- lint
- build
- test
## 환경 변수로 캐시 경로를 지정
variables:
CARGO_HOME: $CI_PROJECT_DIR/.cargo
## 전역 캐시 설정 (모든 Job 이 이 캐시를 물고 들어감)
cache:
paths:
- .cargo/
- target/
## 1번 스테이지 (Lint)
cargo_clippy:
stage: lint
image: rust:latest
script:
- rustup component add clippy
- cargo clippy -- -D warnings # 단 하나의 워닝(Warning)만 떠도 에러로 처리하여 즉사시킴
## 2번 스테이지 (Build - 1번이 통과해야만 실행됨)
cargo_build:
stage: build
image: rust:latest
script:
- cargo build --release
artifacts: # 빌드되어 나온 바이너리 산출물을 다음 스테이지로 넘김
paths:
- target/release/zenoh-client
위 사례들의 핵심은 “단순 스크립트 실행“이 아니다. CI의 성능(Performance)과 비용(Cost)을 아결정짓는 의존성 캐시 재활용 기술과, 완벽함을 담보하기 위한 타협 없는 린팅 장벽을 파이프라인 극초반에 박아 넣어 잘못된 코드가 시간을 낭비하지 못하게 Fail-Fast (빠른 실패) 사상을 구현한 것이다.
3. 언어별(Rust, C, TypeScript) 린팅(Linting) 및 정적 코드 분석
자동화 테스트를 돌리기 직전, 가장 비용이 적게 들면서도 코드의 퀄리티와 잠재적 버그를 90% 이상 차단해 주는 관문이 바로 정적 분석(Static Analysis)과 린팅(Linting)이다.
개발자 10명이 코드를 짜면 들여쓰기 공간(Space), 중괄호({})의 위치, 변수의 명명 규칙(Snake vs Camel case)이 10개로 나뉜다. 이는 코드의 가독성을 파괴하고 유지보수 비용을 기하급수적으로 치솟게 만든다.
또한 정적 분석기는 코드를 컴파일하기 전, 텍스트 형태의 소스 코드를 읽어 들여 “이 변수 포인터는 나중에 터질 확률이 높다”, “이루어질 수 없는 if 조건문이다“라는 식의 고급 추론(Heuristic)을 수행한다.
Zenoh가 포용하는 각 생태계의 대표적 린터를 파이프라인에 이식하라.
3.1 Rust 생태계의 심판자: clippy와 rustfmt
러스트 커뮤니티가 자랑하는 최고의 감시자 클리피(Clippy)는, 초보 개발자가 무심코 작성한 비효율적인 메모리 복사 코드(Clone)나 비동기(Async) 함수의 블로킹(Blocking) 실수를 정확히 타격한다.
- 파이프라인 통제 커맨드:
## 들여쓰기와 포맷팅이 하나라도 틀리면 즉시 파이프라인을 부순다.
cargo fmt --all -- --check
## 경고(Warning) 1개라도 발생 시 컴파일 에러(-D warnings)로 격상시켜버린다.
## "경고는 무시해도 괜찮아" 라는 나태함을 원천 봉쇄한다.
cargo clippy --all-targets --all-features -- -D warnings
3.2 C/C++ 생태계의 엄격한 교관: clang-format과 clang-tidy
임베디드 로봇 제어나 Zenoh-pico를 C로 개발할 때, 메모리 누수(Memory Leak)나 포인터 해제 오류(Segfault)는 로봇을 영원한 잠재우는 치명타다.
- 파이프라인 통제 커맨드:
## 구글(Google) 코딩 스타일 강제 적용
find src/ -iname *.h -o -iname *.c | xargs clang-format -i --style=Google
## 정적 메모리 파괴, 잠재적 포인터 에러 논리적 심층 분석
clang-tidy src/*.c -- -I/usr/local/include
clang-tidy는 단순히 포맷팅을 넘어서, 인간이 짠 if 논리가 맞는지, 배열이 초과하지 않았는지(Out of Bounds)에 대한 정밀 스캐닝을 수행한다.
3.3 TypeScript 생태계의 규율: ESLint와 Prettier
관제 대시보드나 백엔드 로직에 쓰이는 자바스크립트/타입스크립트는 언어 자체가 워낙 유연하여 개판 5분 전의 코드가 만들어지기 가장 쉽다. any 타입을 남발하거나 동등 연산자(==)를 허술하게 쓰지 못하도록 강력하게 조여야 한다.
- 파이프라인 통제 구성:
package.json안에 엄격한 스크립트를 지정해 놓고 CI가 구동하게 만든다.
// package.json의 scripts 영역
"scripts": {
// Prettier로 포맷검사를 하고, ESLint로 코드 품질을 타격함
// --max-warnings=0 옵션을 주어 자성(Warning)을 절대 허용치 않음.
"lint": "prettier --check . && eslint . --ext .ts --max-warnings=0"
}
3.4 멀티 언어 전역 통제 (SonarQube)
프로젝트 규모가 커져서 한 폴더 안에 C 펌웨어와 TS 모니터링 데몬이 혼재되어 있다면, 앞서 언급한 개별 도구를 넘어 엔터프라이즈 정적 분석 솔루션인 SonarQube 호스트의 힘을 빌려라. CI 파이프라인은 코드를 빌드할 때 SonarQube 로 암호화된 분석 스냅샷을 전송하며, 대시보드에서는 “코드 스멜(Code Smell) 등급”, “테스트 커버리지 부족” 등의 지표에 따라 CI의 최종 통과/탈락(Quality Gate) 판결을 내려준다.
인간의 눈으로 행하는 코드 리뷰(Code Review)는 아키텍처 토론과 협업 철학에 집중해야 한다.
띄어쓰기를 지적하거나 오타를 걸러내는 따위의 노가다는 기계(Linter)에게 완벽하게 일임하라.
4. 격리된 컨테이너 환경에서의 단위 테스트(Unit Test) 자동화
개발자 A가 만든 함수 로직이 개발자 B가 짠 컴퓨터 환경에서도 똑같이 답을 내놓을 것인가?
내 노트북(macOS)에서는 정상이던 시간 연산 코드가, 서버(Linux UTC) 구동 시 하루 전으로 인식되어 데이터를 엉뚱한 곳에 집어던지는 사태를 막아야 한다.
테스트 환경의 파편화를 근본적으로 방어하기 위해서는, CI/CD 시스템 위에서 직접 셸(Shell) 테스트 커맨드를 실행하는 야만적인 방법을 멈춰야 한다. 어떠한 테스트 로직이든 운영 서버와 100% 동일한 격리(Isolation) 상태의 일회용 도커 컨테이너(Ephemeral Docker Container) 내부에서 격발해야만 완벽한 신뢰를 얻을 수 있다.
4.1 단위 테스트(Unit Test)의 특성
단위 테스트는 네트워킹 라우터나 분산 데이터베이스 같은 “외부 시스템“의 개입 없이, 오직 메모리 위계에서 함수(Function) 하나의 인풋(Input)과 아웃풋(Output)의 정합성만을 재빠르게(보통 1초 이내) 측정하는 행위다.
4.1.1 [Runbook] K8s와 도커를 관통하는 순수 진공 테스트 망 구성
만약 당신의 코드가 zenoh-rust-client 모듈의 내부 데이터 가공 함수들을 테스트한다고 가정해 보자.
1. 테스트 전용 도커 컴포즈(docker-compose.test.yml) 선언
실제 빌드 프로세스와 테스트 구동 환경을 완전히 도커라이징(Dockerize) 한다.
version: '3.8'
services:
unittest-runner:
image: rust:1.75-bookworm
volumes:
- .:/workspace # 코드를 컨테이너 안으로 밀어넣음
working_dir: /workspace
# tests/ 디렉토리 안의 테스트만 분리해서 격발 (스레드 병렬 실행)
command: cargo test --lib -- --nocapture
2. CI 파이프라인에서 테스트용 컨테이너 기동 및 소멸 룰
GitHub Actions 스크립튼에서 위 컴포즈 파일을 up으로 실행한 뒤, 테스트가 성공하면 결과를 뱉고 이 가상의 공간 자체를 down으로 완전히 파괴해버린다.
- name: 5. 격리된 환경(Container)에서 Unit Test 격발
run: |
# 백그라운드가 아닌 포그라운드로 실행하여 종료 코드(Exit 0 or 1)를 수신함
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
# 끝난 컨테이너 흔적 완벽한 소각 청소
docker-compose -f docker-compose.test.yml down -v
4.2 코드 커버리지(Code Coverage) 기반의 암살
테스트 코드를 짜놓았지만 본능적으로 귀찮아진 개발자가 단 1개의 함수만 테스트해 두고 PR(Pull Request)을 올려버렸다면? 우리는 기계를 통해 강제적으로 게으름을 응징해야 한다. 코드를 얼마나 촘촘히 찔러 보았는지를 백분율(%)로 표기해 주는 “단위 테스트 커버리지” 도구를 파이프라인 말미에 장착하라.
- Rust 진영의 무기 (
tarpaulin)
## 코드 커버리지 측정 도구 로드 및 실행
cargo tarpaulin --ignore-tests --fail-under 80 --out Xml
위 커맨드의 --fail-under 80 이라는 무자비한 설정에 주목하라.
테스트가 아무리 성공(Pass)하더라도, 작성된 전체 소스 코드 중 80% 이상의 면적을 테스트하지 않았다면 가혹무참하게 CI 시스템 전체에 실패(Failed) 판정을 내리고 소스 병합(Merge) 권한을 막아버린다.
이러한 기계적이고 차가운 룰(Rule)만이 분산 네트워크라는 극한의 환경 속에서 코드의 신뢰성과 퀄리티 게이트(Quality Gate)를 유지시켜 주는 유일한 생명줄이다. 인간은 신뢰하되, 그의 코드는 증명될 때까지 절대로 신뢰하지 마라.