C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
이 글의 핵심
프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다. 로그는 언제, 어디서, 어떤 값이었는지를 남겨서 재현이 어려운 버그를 좁혀 주고,… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
💡 초보자를 위한 한 줄: 로그에는 상관 ID·요청 ID·스레드 ID처럼 “나중에 grep할 키”를 남기고,
assert는 내부 불변 조건에만 쓰는 편이 안전합니다(사용자 입력은 명시적 검증).static_assert는 컴파일 타임에 틀린 가정을 막습니다. 16-2 Sanitizers 다음이 읽기 순서에 맞습니다.
들어가며: “어디서 잘못됐는지 모르겠어요”
프로덕션에서 발생한 버그, 재현 불가
프로덕션(실제 서비스가 돌아가는 운영 환경)에서 간헐적으로 크래시가 발생했습니다. 하지만 로그가 없어서 원인을 찾을 수 없었습니다.
로그는 “언제, 어디서, 어떤 값이었는지”를 남겨서 재현이 어려운 버그를 좁혀 주고, 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로 컴파일 타임 검증을 할 수 있습니다.
- 실전에서 버그를 빠르게 추적할 수 있습니다.
실무 적용 경험: 이 글은 대규모 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. 로깅 기초
간단한 로깅
#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가 아닌 명시적 검증으로 처리
초보자를 위한 체크리스트
- 프로덕션에서
DEBUG로그가 과도하게 켜져 있지 않은가? -
assert에 부수 효과(함수 호출로 상태 변경)를 넣지 않았는가? - 크래시 시 플러시·크래시 핸들러로 마지막 로그가 디스크에 남는가?
💡 초보자 팁: 본문 2. 로깅 레벨·3. assert와 static_assert·6. 자주 발생하는 오류를 함께 보세요.
자주 묻는 질문 (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배 향상시키기
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.