본문 바로가기
잡(job)기술

C++ 프로젝트가 나뉘어 있다면, CMake로 한 번에 묶어 빌드하자 - 그리고 Sanitizer는 꼭 붙이자

by 무니이구나 2026. 2. 26.

들어가며 – 돌아는 가는데, 왠지 찜찜하다

C++로 네트워크 프로그램을 만들다 보면 이상한 기분이 들 때가 있다. 컴파일은 문제없다. 실행도 된다. 그런데 가끔 이유 없이 뻗는다. 특히 RTSP처럼 소켓과 버퍼, 포인터를 계속 만지는 코드에서는 더 그렇다. 지금은 멀쩡해 보여도 어딘가에 지뢰가 묻혀 있을 것 같은 느낌. 그게 C++다.

파일이 분리된 구조라면, CMake로 통합 빌드를 구성하는 게 맞다. 여기에 Sanitizer까지 붙이면 훨씬 든든해진다. 그냥 취향 문제가 아니다. 유지보수와 안정성의 문제다.


파일이 나뉘었는데, 그냥 g++로 빌드하면 안 되나?

프로젝트 구조는 이렇게 되어 있다.

src/
  main.cpp
  net.cpp
  rtsp_session.cpp
include/
  net.hpp
  rtsp_session.hpp

 

처음에는 이렇게 빌드해도 된다.

g++ -std=c++17 main.cpp net.cpp rtsp_session.cpp -Iinclude

 

잘 돌아간다. 그래서 방심하게 된다. 그런데 파일이 하나 더 늘어나고, 빌드 옵션이 바뀌고, Debug와 Release를 나눠야 하고, Sanitizer를 조건부로 붙이고 싶어지면… 그때부터 꼬인다. 파일 하나 빠뜨려서 링커 에러가 나고, 옵션이 제각각이 되고, 어느 순간 빌드 커맨드가 점점 길어진다. 이쯤 되면 슬슬 생각하게 된다.

“이걸 계속 이렇게 해도 되나?”

 

그때 CMake를 쓰면 된다.


예제 CMakeLists.txt 전체 코드

아래는 cpp-rtsp-study 저장소에서 사용한 CMakeLists.txt 예시다.

cmake_minimum_required(VERSION 3.16)
project(rtsp_tcp_h264_dump LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 코어 라이브러리
add_library(rtsp_core
  src/net.cpp
  src/rtsp_session.cpp
)

target_include_directories(rtsp_core PUBLIC
  ${CMAKE_CURRENT_SOURCE_DIR}/include
)

target_compile_options(rtsp_core PRIVATE
  -Wall -Wextra -Wpedantic
)

# Sanitizer 설정 (Debug에서만 적용)
add_library(rtsp_sanitizers INTERFACE)

target_compile_options(rtsp_sanitizers INTERFACE
  $<$<CONFIG:Debug>:-fsanitize=address,undefined>
  $<$<CONFIG:Debug>:-fno-omit-frame-pointer>
)

target_link_options(rtsp_sanitizers INTERFACE
  $<$<CONFIG:Debug>:-fsanitize=address,undefined>
)

# 실행 파일
add_executable(rtsp_dump src/main.cpp)

target_link_libraries(rtsp_dump PRIVATE
  rtsp_core
  rtsp_sanitizers
)
 

CMake는 빌드 도구라기보다 ‘설계서’에 가깝다

CMake는 컴파일러가 아니다. 직접 코드를 빌드하는 프로그램이 아니다. 프로젝트를 어떻게 구성할지 설명해두면, 그에 맞는 빌드 시스템을 생성해준다. 쉽게 말해 빌드 절차를 문서화해두는 셈이다.

예제에서는 코어 로직을 라이브러리로 분리했다.

add_library(rtsp_core
  src/net.cpp
  src/rtsp_session.cpp
)

 

굳이 이렇게 나누는 이유가 있다. main.cpp는 말 그대로 진입점이다.

실제 로직은 따로 두는 편이 훨씬 낫다. 테스트를 붙이기도 쉽고, 구조도 또렷해진다. 프로젝트가 커질수록 이런 분리가 차이를 만든다. 작은 프로젝트일수록 오히려 더 습관을 들여야 한다고 생각한다.


Sanitizer를 붙이는 이유 – 메모리는 배신한다

C++에서 메모리는 굉장히 자유롭다. 그 자유가 문제다.

예를 들어 이런 코드가 있다.

int* p = new int(10);
delete p;
std::cout << *p;

 

컴파일은 된다. 경고도 안 나온다. 하지만 이미 해제된 메모리를 다시 읽고 있다. 이건 분명히 잘못이다. 그런데 런타임까지 가기 전에는 알 수 없다. 심지어 실행해도 당장 안 터질 수도 있다. 이걸 잡아주는 것이 AddressSanitizer다.

컴파일 옵션에 다음을 추가한다.

-fsanitize=address

 

그러면 컴파일러가 코드에 감시 장치를 심어둔다. 실행 중에 메모리를 잘못 건드리면 즉시 잡아낸다. 어디에서, 어떤 호출 경로를 통해 들어왔는지도 함께 보여준다.

배열 범위를 넘는 접근도 마찬가지다.

int arr[5];
arr[10] = 3;

이런 코드도 컴파일은 된다. 하지만 AddressSanitizer를 켜두면 실행하자마자 바로 경고를 뿜는다.

이걸 한 번 경험하면, Debug 빌드에서 Sanitizer를 끄는 게 오히려 불안해진다.


Undefined Behavior(UB) – 더 교묘한 문제

메모리 오류는 비교적 직관적이다. UB는 조금 더 까다롭다. Undefined Behavior, 말 그대로 “정의되지 않은 동작”이다. C++ 표준에서 결과를 보장하지 않는 영역이다.

예를 들어:

int x = INT_MAX;
x += 1;

 

정수 오버플로우다. 많은 사람이 그냥 음수로 바뀐다고 생각한다. 하지만 C++ 표준에서는 이 동작이 정의되지 않았다. 어떤 컴파일러에서는 음수가 될 수 있다. 어떤 경우에는 최적화 과정에서 코드가 이상하게 변형될 수도 있다. 이게 무서운 이유다. “어디서는 되는데 어디서는 안 되는 코드”가 만들어진다. 이걸 감지하는 게 UBSanitizer다.

-fsanitize=undefined

 

이 옵션을 켜면, 정수 오버플로우나 잘못된 시프트, 0으로 나누기 같은 UB를 실행 중에 탐지한다. 눈에 보이지 않던 위험이 드러난다.


Debug에서만 켜는 이유

Sanitizer는 검사 코드를 많이 삽입한다. 실행 속도가 느려진다. 그래서 보통 Debug 빌드에서만 활성화한다.

CMake에서는 이렇게 설정할 수 있다.

$<$<CONFIG:Debug>:-fsanitize=address,undefined>

 

Debug일 때만 옵션이 적용된다. Release에서는 자동으로 빠진다. 이런 조건부 설정이 가능하다는 점도 CMake의 장점이다.


-fno-omit-frame-pointer는 왜 필요한가

Sanitizer가 에러를 출력할 때 콜 스택을 보여준다. 어떤 함수에서 어떤 함수로 흘러왔는지. 그 정보가 정확하려면 프레임 포인터를 유지해야 한다.

-fno-omit-frame-pointer

 

이 옵션이 그 역할을 한다. 디버깅할 때 스택 추적이 깔끔하게 나오는 이유다. 작은 옵션이지만 실제로는 꽤 중요하다.


네트워크 코드라면 더더욱 필요하다

RTSP 같은 코드는 recv()로 받은 버퍼를 직접 다루고, 헤더를 파싱하고, 세션 객체를 관리한다. 포인터와 메모리 해제가 얽힌다.

문제는 이런 버그가 “항상” 재현되지 않는다는 점이다. 테스트 환경에서는 조용하다가, 운영 환경에서만 터진다. 이런 상황을 몇 번 겪고 나면 생각이 달라진다.

Sanitizer는 적어도 개발 단계에서 그 위험을 드러내준다. 모든 문제를 잡아주는 건 아니지만, 메모리 관련 실수 상당수를 조기에 발견할 수 있다. 그 차이는 생각보다 크다.


정리해보면

파일이 분리된 C++ 프로젝트라면 CMake로 구조를 정리해두는 게 맞다. 빌드가 단순해지고, 옵션 관리가 쉬워진다.

  • AddressSanitizer는 메모리 오류를 잡는다.
  • UBSanitizer는 정의되지 않은 동작을 드러낸다.

Debug 빌드에 이 둘을 붙여두는 습관은, 나중에 큰 비용을 막아준다. 코드는 “동작한다”에서 끝나지 않는다. “안전하게 동작한다”까지 가야 한다.

 

이 글에서 사용하는 예제 코드는 아래 저장소에 있다.

👉 https://github.com/moony211/cpp-rtsp-study