[2026] C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
이 글의 핵심
프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다. 로그는 언제, 어디서, 어떤 값이었는지를 남겨서 재현이 어려운 버그를 좁혀 주고,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며: “어디서 잘못됐는지 모르겠어요”
프로덕션에서 발생한 버그, 재현 불가
프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다.
로그는 “언제, 어디서, 어떤 값이었는지”를 남겨서 재현이 어려운 버그를 좁혀 주고, assert(어서션—“이 조건이 참이어야 한다”고 코드에 적어 두고, 거짓이면 프로그램을 중단해 버그를 드러내는 매크로)는 “이 조건이 깨지면 더 이상 진행하면 안 된다”는 불변 조건을 코드에 명시합니다. 실무에서는 로그 레벨(DEBUG/INFO/ERROR)을 환경별로 나누고, assert 실패 시 스택과 상태를 남기도록 설정해 두면 추적이 훨씬 수월해집니다.
문제의 코드:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void processOrder(Order* order) {
// 크래시 발생....하지만 어디서?
order->calculate();
order->validate();
order->save();
}
로깅 추가 후: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
void processOrder(Order* order) {
LOG_INFO("Processing order: " << order->getId());
order->calculate();
LOG_DEBUG("Calculation done");
order->validate();
LOG_DEBUG("Validation done");
order->save();
LOG_INFO("Order saved");
}
주의사항: 민감한 개인정보·토큰은 마스킹하고, DEBUG 로그 폭주는 I/O 병목이 될 수 있어 레벨을 런타임에 조절하세요. 로그 출력:
[INFO] Processing order: 12345
[DEBUG] Calculation done
[ERROR] Validation failed: invalid price
원인 발견: validate()에서 실패
이 글을 읽으면:
- 효과적인 로깅 전략을 수립할 수 있습니다.
- assert로 버그를 조기에 발견할 수 있습니다.
- static_assert로 컴파일 타임 검증을 할 수 있습니다.
- 실전에서 버그를 빠르게 추적할 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
문제 시나리오: 로그·Assertion이 필요한 상황
시나리오 1: 고객 환경에서만 재현되는 크래시
상황: 개발 PC에서는 정상 동작하지만, 특정 고객 서버에서만 주기적으로 크래시가 발생한다. 원인: 메모리 레이아웃, CPU 코어 수, 타이밍 차이로 인한 레이스 컨디션, 또는 특정 입력 데이터 조합. 해결: 크래시 직전 상태를 로그로 남기고, assert로 불변 조건을 검증해 개발 단계에서 버그를 잡는다.
시나리오 2: “어제는 됐는데 오늘은 안 돼요”
상황: 코드 변경 없이 갑자기 동작이 달라졌다. 배포 환경, 외부 서비스, 데이터 변경 등이 원인일 수 있다. 해결: 주요 분기점마다 INFO 레벨 로그를 남겨 “어디까지 실행됐는지” 추적한다.
시나리오 3: 성능 저하 원인 불명
상황: CPU 사용률이 갑자기 올라갔지만, 어떤 함수가 원인인지 모른다. 해결: 함수 진입/종료 시점 로깅, 또는 성능 측정용 로깅으로 병목 구간을 찾는다.
시나리오 4: API 전제 조건 위반
상황: “이 함수는 size > 0일 때만 호출해야 한다” 같은 전제가 문서에만 있고, 호출자가 위반하면 undefined behavior가 발생한다. 해결: assert로 전제 조건을 코드에 명시하고, 위반 시 즉시 중단시켜 버그를 드러낸다.
시나리오 5: 분산 시스템에서 요청 추적 불가
상황: 마이크로서비스 A → B → C로 요청이 전파되는데, C에서 에러가 발생해도 “어느 사용자 요청에서 발생했는지” 알 수 없다. 원인: 요청 ID(request ID)를 로그에 포함하지 않아, 여러 요청이 섞여 들어올 때 추적이 불가능하다. 해결: 요청 진입 시점에 고유 ID를 생성하고, 모든 로그에 해당 ID를 포함시켜 분산 추적(distributed tracing)이 가능하게 한다.
시나리오 6: 메모리 누수·해제 후 사용(Use-After-Free)
상황: 프로그램이 몇 시간 후 갑자기 크래시한다. Sanitizer 없이 실행 중이라 원인을 특정하기 어렵다. 원인: 특정 코드 경로에서만 발생하는 메모리 오류. 로그가 없으면 “어느 객체가, 언제 해제됐는지” 파악이 어렵다. 해결: 객체 생성/해제 시점에 로그를 남기고, assert로 포인터 유효성을 검증해 개발 단계에서 버그를 잡는다.
목차
1. 로깅 기초
간단한 로깅
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <fstream>
#include <chrono>
#include <iomanip>
class Logger {
std::ofstream file;
public:
Logger(const std::string& filename) {
file.open(filename, std::ios::app);
}
template <typename T>
void log(const T& message) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
file << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
<< " " << message << "\n";
file.flush(); // 버퍼 즉시 반영 (크래시 시에도 로그 보존)
}
};
int main() {
Logger logger("app.log");
logger.log("Application started");
logger.log("Processing data...");
}
주의점: flush()를 호출하지 않으면 버퍼에 쌓인 로그가 크래시 시 손실될 수 있다. 프로덕션에서는 로그량과 성능을 고려해 주기적 flush 정책을 적용한다.
매크로로 간편화
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o log_macro log_macro.cpp && ./log_macro
#include <iostream>
#define LOG(msg) \
std::cout << "[" << __FILE__ << ":" << __LINE__ << "] " << msg << "\n"
int main() {
LOG("Starting application");
int x = 10;
LOG("x = " << x);
return 0;
}
실행 결과:
[main.cpp:10] Starting application
[main.cpp:12] x = 10
2. 로깅 레벨
레벨 정의
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
enum class LogLevel {
DEBUG, // 상세 정보 (개발용)
INFO, // 일반 정보
WARNING, // 경고
ERROR, // 에러
FATAL // 치명적 에러
};
class Logger {
LogLevel minLevel = LogLevel::INFO;
public:
void setLevel(LogLevel level) {
minLevel = level;
}
void log(LogLevel level, const std::string& message) {
if (level < minLevel) return;
std::cout << "[" << levelToString(level) << "] " << message << "\n";
}
private:
std::string levelToString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
}
return "UNKNOWN";
}
};
사용 예제
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Logger logger;
logger.setLevel(LogLevel::INFO);
logger.log(LogLevel::DEBUG, "This won't be printed");
logger.log(LogLevel::INFO, "Application started");
logger.log(LogLevel::ERROR, "Failed to open file");
출력:
[INFO] Application started
[ERROR] Failed to open file
로그 레벨 흐름도
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TD
subgraph 환경별[환경별 로그 레벨]
DEV[개발: DEBUG]
STG[스테이징: INFO]
PRD[프로덕션: WARNING 또는 ERROR]
end
subgraph 메시지[메시지 레벨]
D[DEBUG]
I[INFO]
W[WARNING]
E[ERROR]
F[FATAL]
end
DEV --> D
DEV --> I
DEV --> W
DEV --> E
DEV --> F
STG --> I
STG --> W
STG --> E
STG --> F
PRD --> W
PRD --> E
PRD --> F
3. assert와 static_assert
assert (런타임 검증)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <cassert>
void processArray(int* arr, int size) {
assert(arr != nullptr); // null 체크
assert(size > 0); // 크기 체크
for (int i = 0; i < size; ++i) {
arr[i] *= 2;
}
}
int main() {
int arr[10];
processArray(arr, 10); // ✅ OK
// processArray(nullptr, 10); // ❌ Assertion failed!
}
assert 비활성화:
# Release 빌드에서 assert 제거
g++ -DNDEBUG main.cpp -o myapp
assert vs static_assert vs NDEBUG 비교
| 구분 | assert | static_assert | NDEBUG |
|---|---|---|---|
| 검증 시점 | 런타임 | 컴파일 타임 | 매크로 (빌드 시) |
| 용도 | 불변 조건·전제 조건 검증 | 타입·플랫폼·상수 검증 | assert 비활성화 |
| 실패 시 | 프로그램 중단(abort) | 컴파일 에러 | - |
| 프로덕션 | NDEBUG 시 제거됨 | 항상 포함 | Release 빌드에 정의 |
| 부수 효과 | 넣으면 안 됨 | 없음 | assert 전체 제거 |
| NDEBUG 상세: |
NDEBUG가 정의되면assert(expr)는((void)0)으로 치환되어 완전히 제거된다.- CMake Release 빌드, Visual Studio Release 구성은 기본적으로
NDEBUG를 정의한다. - 주의: assert 내부에 할당·초기화 등 부수 효과를 넣으면 Release에서 해당 코드가 실행되지 않아 버그가 발생한다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// NDEBUG 동작 원리 (cassert 내부)
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /* condition이 거짓이면 abort */
#endif
static_assert (컴파일 타임 검증)
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
template <typename T>
class Buffer {
static_assert(std::is_trivially_copyable_v<T>,
"T must be trivially copyable");
T data[100];
};
int main() {
Buffer<int> buf1; // ✅ OK
// Buffer<std::string> buf2; // ❌ 컴파일 에러
}
커스텀 assert
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#define ASSERT(condition, message) \
do { \
if (!(condition)) { \
std::cerr << "Assertion failed: " << message << "\n" \
<< "File: " << __FILE__ << "\n" \
<< "Line: " << __LINE__ << "\n"; \
std::abort(); \
} \
} while (0)
int main() {
int x = 10;
ASSERT(x > 0, "x must be positive");
ASSERT(x < 100, "x must be less than 100");
}
Assertion 패턴 모음
패턴 1: 전제 조건 검증 (Precondition)
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 함수 호출 전 반드시 만족해야 하는 조건
void resizeBuffer(std::vector<int>& buf, size_t newSize) {
assert(newSize > 0 && "newSize must be positive");
assert(newSize <= MAX_BUFFER_SIZE && "Buffer size limit exceeded");
buf.resize(newSize);
}
패턴 2: 사후 조건 검증 (Postcondition)
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 함수 종료 시 반드시 만족해야 하는 조건
int safeDivide(int a, int b) {
assert(b != 0 && "Division by zero");
int result = a / b;
assert(result * b == a && "Integer division overflow?");
return result;
}
패턴 3: 불가능한 분기 (Unreachable)
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
enum class State { Idle, Running, Done };
void handleState(State s) {
switch (s) {
case State::Idle: /* ....*/ break;
case State::Running: /* ....*/ break;
case State::Done: /* ....*/ break;
default:
assert(false && "Unhandled state");
}
}
패턴 4: static_assert로 플랫폼 검증
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 64비트 환경에서만 컴파일 허용
static_assert(sizeof(void*) == 8, "This code requires 64-bit platform");
// 특정 타입 크기 검증
static_assert(sizeof(int) == 4, "int must be 4 bytes");
패턴 5: 타입 특성 검증
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
template <typename T>
void fastCopy(T* dest, const T* src, size_t count) {
static_assert(std::is_trivially_copyable_v<T>,
"T must be trivially copyable for fast copy");
std::memcpy(dest, src, count * sizeof(T));
}
4. 로깅 라이브러리
spdlog (추천)
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include "spdlog/spdlog.h"
int main() {
spdlog::info("Application started");
spdlog::debug("Debug message");
spdlog::error("Error occurred");
// 파일 로깅
auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/app.log");
file_logger->info("Logged to file");
}
spdlog 로그 레벨 전체 예제
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#include "spdlog/spdlog.h"
int main() {
spdlog::set_level(spdlog::level::debug);
spdlog::trace("추적"); spdlog::debug("디버그"); spdlog::info("정보");
spdlog::warn("경고"); spdlog::error("에러"); spdlog::critical("치명적");
spdlog::info("User {} from {}", 12345, "192.168.1.1");
return 0;
}
설치
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# vcpkg
vcpkg install spdlog
# CMake
find_package(spdlog REQUIRED)
target_link_libraries(myapp PRIVATE spdlog::spdlog)
spdlog 커스텀 싱크 (Custom Sink)
로그를 파일·콘솔 외에 Syslog, 네트워크 등으로 보내려면 spdlog::sinks::base_sink를 상속해 커스텀 싱크를 구현한다. Linux에서는 spdlog/sinks/syslog_sink.h의 syslog_sink_mt를 사용할 수 있다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include "spdlog/sinks/base_sink.h"
#include <mutex>
#include <vector>
template<typename Mutex>
class MemoryBufferSink : public spdlog::sinks::base_sink<Mutex> {
std::vector<std::string> buffer_;
protected:
void sink_it_(const spdlog::details::log_msg& msg) override {
spdlog::memory_buf_t formatted;
spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);
buffer_.push_back(std::string(formatted.data(), formatted.size()));
}
void flush_() override {}
};
spdlog 완전한 예제 (레벨, 포맷, 로테이션)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
void setupProductionLogger() {
// 콘솔: 색상 출력
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::info);
// 파일: 5MB마다 로테이션, 최대 3개 파일 유지
auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 1024 * 1024 * 5, 3);
file_sink->set_level(spdlog::level::debug);
std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
auto logger = std::make_shared<spdlog::logger>("multi_sink", sinks.begin(), sinks.end());
// 포맷: [2026-03-10 14:30:00.123] [info] [main:42] 메시지
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%s:%#] %v");
logger->set_level(spdlog::level::debug);
spdlog::set_default_logger(logger);
}
int main() {
setupProductionLogger();
spdlog::info("Application started");
spdlog::debug("Detailed debug info: x={}", 42);
spdlog::error("Error: file not found - {}", "config.json");
return 0;
}
5. 실전 패턴
패턴 1: 함수 진입/종료 로깅
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class FunctionLogger {
std::string funcName;
public:
FunctionLogger(const char* name) : funcName(name) {
std::cout << "Entering " << funcName << "\n";
}
~FunctionLogger() {
std::cout << "Exiting " << funcName << "\n";
}
};
#define LOG_FUNCTION() FunctionLogger __func_logger(__FUNCTION__)
void processData() {
LOG_FUNCTION();
// 작업...
}
int main() {
LOG_FUNCTION();
processData();
}
출력: 다음은 간단한 text 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Entering main
Entering processData
Exiting processData
Exiting main
패턴 2: 조건부 로깅
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#ifdef DEBUG
#define LOG_DEBUG(msg) std::cout << "[DEBUG] " << msg << "\n"
#else
#define LOG_DEBUG(msg)
#endif
int main() {
LOG_DEBUG("This only prints in debug build");
}
패턴 3: 성능 측정 로깅
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <chrono>
#include <iostream>
#include <thread>
class PerfLogger {
std::string name;
std::chrono::high_resolution_clock::time_point start;
public:
PerfLogger(const char* n) : name(n) {
start = std::chrono::high_resolution_clock::now();
}
~PerfLogger() {
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << name << " took " << duration.count() << " ms\n";
}
};
#define LOG_PERF(name) PerfLogger __perf_logger(name)
void slowFunction() {
LOG_PERF("slowFunction");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
패턴 4: 에러 컨텍스트
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <string>
#include <vector>
class ErrorContext {
std::vector<std::string> context;
public:
void push(const std::string& msg) {
context.push_back(msg);
}
void pop() {
if (!context.empty()) {
context.pop_back();
}
}
void printContext() {
std::cout << "Error context:\n";
for (const auto& msg : context) {
std::cout << " " << msg << "\n";
}
}
};
ErrorContext errorCtx;
void processOrder(int orderId) {
errorCtx.push("Processing order " + std::to_string(orderId));
// 에러 발생
if (orderId < 0) {
errorCtx.printContext();
throw std::runtime_error("Invalid order ID");
}
errorCtx.pop();
}
패턴 5: 구조화된 로깅
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <iostream>
#include <map>
#include <string>
class StructuredLogger {
public:
void log(const std::string& event,
const std::map<std::string, std::string>& fields) {
std::cout << "event=" << event;
for (const auto& [key, value] : fields) {
std::cout << " " << key << "=" << value;
}
std::cout << "\n";
}
};
int main() {
StructuredLogger logger;
logger.log("user_login", {
{"user_id", "12345"},
{"ip", "192.168.1.1"},
{"timestamp", "2026-03-17"}
});
}
출력:
event=user_login user_id=12345 ip=192.168.1.1 timestamp=2026-03-17
6. 자주 발생하는 오류
오류 1: 로그 문자열 연결 시 성능 저하
원인: LOG_DEBUG("value=" << value) 형태에서 DEBUG가 비활성화돼도 operator<<가 실행되어 문자열 연산이 발생한다.
잘못된 예:
#define LOG_DEBUG(msg) \
if (false) std::cout << msg // ❌ msg 평가는 항상 수행됨
LOG_DEBUG("expensive: " << computeExpensiveString()); // computeExpensiveString() 항상 호출
올바른 예: 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 람다/함수로 감싸서 지연 평가
#define LOG_DEBUG_EXPR(expr) \
do { if (LOG_LEVEL <= DEBUG) { expr; } } while (0)
LOG_DEBUG_EXPR(std::cout << "expensive: " << computeExpensiveString());
또는 spdlog의 spdlog::debug("{}", computeExpensiveString())처럼 포맷 문자열을 사용하면, 레벨 체크 후에만 인자가 평가된다.
오류 2: assert에 부수 효과(side effect) 넣기
원인: NDEBUG 정의 시 assert 전체가 제거되므로, assert 안의 코드가 실행되지 않는다.
잘못된 예:
assert(ptr = getNext()); // ❌ Release에서 ptr 할당이 사라짐!
assert(initConnection()); // ❌ Release에서 초기화가 수행되지 않음
올바른 예: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
ptr = getNext();
assert(ptr != nullptr);
bool ok = initConnection();
assert(ok && "Connection init failed");
오류 3: 사용자 입력 검증에 assert 사용
원인: assert는 프로그래머 실수를 위한 것이지, 잘못된 사용자 입력을 처리하는 용도가 아니다. 잘못된 예:
void setAge(int age) {
assert(age >= 0 && age <= 150); // ❌ 사용자가 -1 입력 시 프로세스 종료
}
올바른 예: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
bool setAge(int age) {
if (age < 0 || age > 150) {
LOG_ERROR("Invalid age: {}", age);
return false;
}
// ...
return true;
}
오류 4: 로그 파일 권한/경로 오류
원인: 로그 디렉토리가 없거나 쓰기 권한이 없으면 로그 기록이 실패한다. 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <filesystem>
void ensureLogDirectory(const std::string& path) {
std::filesystem::path p(path);
if (!std::filesystem::exists(p)) {
std::filesystem::create_directories(p);
}
}
// 사용
ensureLogDirectory("logs");
auto logger = spdlog::basic_logger_mt("app", "logs/app.log");
오류 5: static_assert 메시지 누락
원인: 컴파일 에러 시 원인 파악이 어렵다. 잘못된 예:
static_assert(sizeof(int) == 8); // ❌ "condition is false"만 보임
올바른 예:
static_assert(sizeof(int) == 8, "This code assumes 64-bit int");
오류 6: 멀티스레드 환경에서 로거 공유
원인: 단일 스레드용 로거(basic_logger_st)를 여러 스레드에서 동시에 사용하면 데이터 레이스가 발생한다.
잘못된 예:
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ st = single-threaded
auto logger = spdlog::basic_logger_st("app", "app.log");
std::thread t1([&]{ logger->info("from t1"); });
std::thread t2([&]{ logger->info("from t2"); }); // 데이터 레이스!
올바른 예: 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ mt = multi-threaded
auto logger = spdlog::basic_logger_mt("app", "app.log");
std::thread t1([&]{ logger->info("from t1"); });
std::thread t2([&]{ logger->info("from t2"); });
오류 7: assert로 예외 처리 대체
원인: assert 실패 시 std::abort()가 호출되어 스택 언와인딩이 없고, 예외 핸들러가 실행되지 않는다. 리소스 정리(파일 닫기, 연결 해제 등)가 누락될 수 있다.
잘못된 예:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void processFile(const std::string& path) {
std::ifstream file(path);
assert(file.is_open() && "File open failed"); // ❌ 실패 시 file 닫기 안 됨
// ...
}
올바른 예: 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
void processFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
spdlog::error("Failed to open file: {}", path);
throw std::runtime_error("File open failed");
}
// RAII로 file 자동 정리
}
오류 8: 로그 포맷과 인자 개수 불일치
원인: spdlog::info("User {} logged in", id, name)처럼 {} 개수와 인자 개수가 맞지 않으면 런타임 에러가 발생한다. 해결: {} 개수와 인자 개수를 일치시킨다.
7. 성능 팁
팁 1: 로그 레벨로 불필요한 연산 건너뛰기
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 나쁜 예: 항상 문자열 생성
logger.debug("User " + userId + " did " + action);
// ✅ 좋은 예: 레벨 체크 후에만 포맷
if (logger.should_log(LogLevel::DEBUG)) {
logger.debug("User {} did {}", userId, action);
}
spdlog는 내부적으로 레벨 체크를 하므로 spdlog::debug("{}", x)만 써도 된다.
팁 2: 구조화된 로깅으로 파싱 부담 감소
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 나쁜 예: 자유 형식 문자열 → 파싱 어려움
logger.info("User 12345 logged in from 192.168.1.1 at 2026-03-10");
// ✅ 좋은 예: JSON/키=값 형식 → 로그 수집기에서 바로 파싱
logger.info(R"({"event":"login","user_id":12345,"ip":"192.168.1.1"})");
팁 3: 비동기 로깅으로 I/O 병목 완화
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// spdlog 비동기 로거: 로그를 버퍼에 넣고 별도 스레드에서 파일에 기록
#include "spdlog/async.h"
#include "spdlog/sinks/basic_file_sink.h"
auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>(
"async_logger", "logs/async.log");
팁 4: 프로덕션에서 DEBUG 비활성화
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
#ifdef NDEBUG
#define LOG_DEBUG(...) ((void)0)
#else
#define LOG_DEBUG(...) logger.debug(__VA_ARGS__)
#endif
팁 5: 로그량 제한 (Rate Limiting)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class RateLimitedLogger {
std::unordered_map<std::string, std::chrono::steady_clock::time_point> lastLog;
std::chrono::seconds minInterval{1};
public:
void log(const std::string& key, const std::string& msg) {
auto now = std::chrono::steady_clock::now();
auto it = lastLog.find(key);
if (it != lastLog.end() && (now - it->second) < minInterval) {
return; // 스킵
}
lastLog[key] = now;
std::cout << msg << "\n";
}
};
8. 프로덕션 패턴
패턴 1: 환경별 로그 설정
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
LogLevel getLogLevelFromEnv() {
const char* env = std::getenv("LOG_LEVEL");
if (!env) return LogLevel::WARNING; // 기본: 프로덕션은 WARNING
if (strcmp(env, "DEBUG") == 0) return LogLevel::DEBUG;
if (strcmp(env, "INFO") == 0) return LogLevel::INFO;
if (strcmp(env, "WARNING") == 0) return LogLevel::WARNING;
if (strcmp(env, "ERROR") == 0) return LogLevel::ERROR;
return LogLevel::WARNING;
}
int main() {
Logger::get().setLevel(getLogLevelFromEnv());
// ...
}
패턴 2: 크래시 시 로그 flush
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <csignal>
void setupCrashHandlers() {
auto flushAndExit = {
Logger::get().flush();
std::abort();
};
signal(SIGABRT, flushAndExit);
signal(SIGSEGV, flushAndExit);
signal(SIGFPE, flushAndExit);
}
패턴 3: 요청 ID 추적 (Request Tracing)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <string>
#include <thread>
thread_local std::string g_requestId;
void setRequestId(const std::string& id) {
g_requestId = id;
}
void logWithRequestId(LogLevel level, const std::string& msg) {
std::string full = "[" + g_requestId + "] " + msg;
Logger::get().log(level, full);
}
// HTTP 요청 처리 시
void handleRequest(const Request& req) {
setRequestId(req.getId());
logWithRequestId(LogLevel::INFO, "Request started");
// ...
}
패턴 4: 로그 로테이션 및 보관 정책
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// spdlog: 크기 기반 로테이션
auto logger = spdlog::rotating_logger_mt("app", "logs/app.log", 1024*1024*100, 5);
// 100MB마다 로테이션, 최대 5개 파일 (app.log, app.1.log, ...)
// 날짜 기반 로테이션 (spdlog::daily_logger_mt)
auto daily = spdlog::daily_logger_mt("daily", "logs/daily", 0, 0);
// 매일 자정 새 파일
패턴 5: 민감 정보 마스킹
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::string maskSensitive(const std::string& input) {
if (input.size() <= 4) return "****";
return input.substr(0, 2) + "****" + input.substr(input.size() - 2);
}
void logUserInfo(const User& u) {
LOG_INFO("User login: id={}, email={}", u.id, maskSensitive(u.email));
}
프로덕션 체크리스트
아래 코드는 markdown를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
- [ ] 로그 레벨을 환경 변수로 설정 가능하게
- [ ] 로그 파일 경로/권한 확인
- [ ] 로테이션 설정 (용량 또는 날짜)
- [ ] 크래시 시 flush 핸들러 등록
- [ ] 민감 정보(비밀번호, 토큰) 로그 제외
- [ ] 비동기 로깅으로 I/O 병목 최소화
- [ ] assert는 NDEBUG로 프로덕션에서 제거
- [ ] 요청 ID로 분산 추적 가능하게
9. 모범 사례 (Best Practices)
로깅·Assertion 모범 사례
로깅: 적절한 레벨(DEBUG/INFO/ERROR), 구조화(JSON·키=값), 컨텍스트(누가·언제·무엇을), 민감 정보 제외, 루프 내 과도한 로깅 지양.
Assertion: 전제·사후 조건 검증, && "설명" 메시지 포함, 부수 효과 금지, 사용자 입력은 assert 대신 if+return false.
환경별 레벨: 개발(DEBUG) → 스테이징(INFO) → 프로덕션(WARN/ERROR).
// ❌ 모호함 → ✅ 구체적
spdlog::info("Error"); // spdlog::error("DB failed: {}", err.what());
spdlog::debug("Done"); // spdlog::info("Order {} saved", orderId);
실무 체크리스트 요약
- 개발: DEBUG 레벨, assert 활성화, 함수 진입/종료 로깅
- 스테이징: INFO 레벨, 로테이션 설정, 요청 ID 추적
- 프로덕션: WARNING/ERROR 레벨, NDEBUG, 비동기 로깅, 민감 정보 마스킹
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ Sanitizers | ASan·TSan으로 메모리 버그·data race 자동 탐지
- C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
이 글에서 다루는 키워드 (관련 검색어)
C++ 로깅, assert, 디버그 매크로, 조건부 컴파일, 에러 로그, spdlog, static_assert, 프로덕션 로깅 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 도구 | 용도 | 시점 |
|---|---|---|
| 로깅 | 실행 흐름 추적 | 런타임 |
| assert | 불변 조건 검증 | 런타임 (디버그) |
| static_assert | 타입 검증 | 컴파일 타임 |
| 핵심 원칙: |
- 중요한 지점에 로깅
- 레벨별로 구분
- assert로 불변 조건 검증
- 프로덕션에서 로그 레벨 조정
- 성능 크리티컬 경로는 조건부 로깅
- 사용자 입력은 assert가 아닌 명시적 검증으로 처리
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 효과적인 로깅 전략, assert와 static_assert 활용, 그리고 실전에서 버그를 빠르게 추적하고 예방하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.