C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
이 글의 핵심
C++ 로깅·Assertion에 대해 정리한 개발 블로그 글입니다. 프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다. 로그는 "언제, 어디서, 어떤 값이었는지"를 남겨서 재현이 어려운 버그를 좁혀 주고,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: C++…
들어가며: “어디서 잘못됐는지 모르겠어요”
프로덕션에서 발생한 버그, 재현 불가
프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다.
로그는 “언제, 어디서, 어떤 값이었는지”를 남겨서 재현이 어려운 버그를 좁혀 주고, assert(어서션—“이 조건이 참이어야 한다”고 코드에 적어 두고, 거짓이면 프로그램을 중단해 버그를 드러내는 매크로)는 “이 조건이 깨지면 더 이상 진행하면 안 된다”는 불변 조건을 코드에 명시합니다. 실무에서는 로그 레벨(DEBUG/INFO/ERROR)을 환경별로 나누고, assert 실패 시 스택과 상태를 남기도록 설정해 두면 추적이 훨씬 수월해집니다.
문제의 코드:
void processOrder(Order* order) {
// 크래시 발생... 하지만 어디서?
order->calculate();
order->validate();
order->save();
}
로깅 추가 후:
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로 컴파일 타임 검증을 할 수 있습니다.
- 실전에서 버그를 빠르게 추적할 수 있습니다.
문제 시나리오: 로그·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. 로깅 기초
간단한 로깅
#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 정책을 적용한다.
매크로로 간편화
// 복사해 붙여넣은 뒤: 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. 로깅 레벨
레벨 정의
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";
}
};
사용 예제
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
로그 레벨 흐름도
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 (런타임 검증)
#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에서 해당 코드가 실행되지 않아 버그가 발생한다.
// NDEBUG 동작 원리 (cassert 내부)
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /* condition이 거짓이면 abort */
#endif
static_assert (컴파일 타임 검증)
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
#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)
// 함수 호출 전 반드시 만족해야 하는 조건
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)
// 함수 종료 시 반드시 만족해야 하는 조건
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)
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로 플랫폼 검증
// 64비트 환경에서만 컴파일 허용
static_assert(sizeof(void*) == 8, "This code requires 64-bit platform");
// 특정 타입 크기 검증
static_assert(sizeof(int) == 4, "int must be 4 bytes");
패턴 5: 타입 특성 검증
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 (추천)
#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 로그 레벨 전체 예제
#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;
}
설치
# 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를 사용할 수 있다.
#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 완전한 예제 (레벨, 포맷, 로테이션)
#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: 함수 진입/종료 로깅
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();
}
출력:
Entering main
Entering processData
Exiting processData
Exiting main
패턴 2: 조건부 로깅
#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: 성능 측정 로깅
#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: 에러 컨텍스트
#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: 구조화된 로깅
#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() 항상 호출
올바른 예:
// 람다/함수로 감싸서 지연 평가
#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에서 초기화가 수행되지 않음
올바른 예:
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 입력 시 프로세스 종료
}
올바른 예:
bool setAge(int age) {
if (age < 0 || age > 150) {
LOG_ERROR("Invalid age: {}", age);
return false;
}
// ...
return true;
}
오류 4: 로그 파일 권한/경로 오류
원인: 로그 디렉토리가 없거나 쓰기 권한이 없으면 로그 기록이 실패한다.
해결:
#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)를 여러 스레드에서 동시에 사용하면 데이터 레이스가 발생한다.
잘못된 예:
// ❌ 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"); }); // 데이터 레이스!
올바른 예:
// ✅ 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()가 호출되어 스택 언와인딩이 없고, 예외 핸들러가 실행되지 않는다. 리소스 정리(파일 닫기, 연결 해제 등)가 누락될 수 있다.
잘못된 예:
void processFile(const std::string& path) {
std::ifstream file(path);
assert(file.is_open() && "File open failed"); // ❌ 실패 시 file 닫기 안 됨
// ...
}
올바른 예:
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: 로그 레벨로 불필요한 연산 건너뛰기
// ❌ 나쁜 예: 항상 문자열 생성
logger.debug("User " + userId + " did " + action);
// ✅ 좋은 예: 레벨 체크 후에만 포맷
if (logger.should_log(LogLevel::DEBUG)) {
logger.debug("User {} did {}", userId, action);
}
spdlog는 내부적으로 레벨 체크를 하므로 spdlog::debug("{}", x)만 써도 된다.
팁 2: 구조화된 로깅으로 파싱 부담 감소
// ❌ 나쁜 예: 자유 형식 문자열 → 파싱 어려움
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 병목 완화
// 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 비활성화
#ifdef NDEBUG
#define LOG_DEBUG(...) ((void)0)
#else
#define LOG_DEBUG(...) logger.debug(__VA_ARGS__)
#endif
팁 5: 로그량 제한 (Rate Limiting)
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: 환경별 로그 설정
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
#include <csignal>
void setupCrashHandlers() {
auto flushAndExit = {
Logger::get().flush();
std::abort();
};
signal(SIGABRT, flushAndExit);
signal(SIGSEGV, flushAndExit);
signal(SIGFPE, flushAndExit);
}
패턴 3: 요청 ID 추적 (Request Tracing)
#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: 로그 로테이션 및 보관 정책
// 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: 민감 정보 마스킹
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));
}
프로덕션 체크리스트
- [ ] 로그 레벨을 환경 변수로 설정 가능하게
- [ ] 로그 파일 경로/권한 확인
- [ ] 로테이션 설정 (용량 또는 날짜)
- [ ] 크래시 시 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++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: 로깅·assert로 가정을 검증하고 디버깅 시간을 줄일 수 있습니다. 다음으로 CMake 고급(#17-1)를 읽어보면 좋습니다.
이전 글: [C++ 실전 가이드 #16-2] Sanitizers: 메모리 버그를 자동으로 찾는 도구
다음 글: [C++ 실전 가이드 #17-1] CMake 고급: 대규모 프로젝트 빌드 시스템 구축
관련 글
- C++ GDB/LLDB | cout 100개 찍어도 못 찾은 버그, 디버거로 5분 만에 해결
- C++ 디버깅 기초 완벽 가이드 | GDB·LLDB 브레이크포인트·워치포인트로 버그 5분 만에 찾기
- C++ Sanitizers | ASan·TSan으로 메모리 버그·data race 자동 탐지
- C++ 프로파일링 |
- C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기