C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때

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. 로깅 기초
  2. 로깅 레벨
  3. assert와 static_assert
  4. 로깅 라이브러리
  5. 실전 패턴
  6. 자주 발생하는 오류
  7. 성능 팁
  8. 프로덕션 패턴
  9. 모범 사례

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 비교

구분assertstatic_assertNDEBUG
검증 시점런타임컴파일 타임매크로 (빌드 시)
용도불변 조건·전제 조건 검증타입·플랫폼·상수 검증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.hsyslog_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타입 검증컴파일 타임

핵심 원칙:

  1. 중요한 지점에 로깅
  2. 레벨별로 구분
  3. assert로 불변 조건 검증
  4. 프로덕션에서 로그 레벨 조정
  5. 성능 크리티컬 경로는 조건부 로깅
  6. 사용자 입력은 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배 향상시키기