spdlog로 C++ 로깅하기: 다중 싱크, 로테이션, 비동기 로깅과 싱크 수명 함정

들어가며: std::cout 로그가 만드는 문제

std::cout으로 찍은 로그를 셸 리다이렉트(>> app.log)로 파일에 남기는 방식은 처음에는 간단하지만, 서비스가 오래 돌수록 문제가 드러납니다. 로테이션이 없으니 파일 하나가 끝없이 커져 디스크를 채우고, 레벨 구분이 없으니 디버그 출력과 에러가 섞여 정작 필요한 줄을 찾기 어렵습니다. 에러가 났을 때 즉시 flush되지 않으면 크래시 직전의 로그가 버퍼에 남은 채 사라지고, 요청마다 동기적으로 파일에 쓰면 디스크 I/O가 느려지는 순간 응답 시간까지 같이 늘어납니다. 여러 스레드가 동시에 cout에 쓰면 줄이 뒤섞이기도 합니다.

spdlog는 이런 문제를 로거와 싱크 구조로 풉니다. 로거는 레벨(trace/debug/info/warn/error/critical)로 메시지를 거르고, 붙어 있는 여러 싱크(콘솔, 파일, 로테이션 파일 등)에 같은 메시지를 전달하며, 싱크마다 레벨과 출력 형식을 따로 정할 수 있습니다. 비동기 모드에서는 메시지를 큐에 넣고 바로 반환하므로, 파일 쓰기는 백그라운드 스레드가 맡습니다. 컴파일 타임 레벨을 지정하면 릴리스 빌드에서 debug 로그 호출 자체를 제거할 수도 있습니다.

flowchart LR
  subgraph problem[cout 로그]
    P1[레벨 없음]
    P2[파일 관리 수동]
    P3[동기 I/O 블로킹]
  end
  subgraph solution[spdlog]
    S1[레벨별 필터링]
    S2[다중 싱크·로테이션]
    S3[비동기 로깅]
  end
  problem --> solution

요구 환경: C++11 이상(최신 버전은 C++11/14/17/20 모두 지원). 헤더 전용 또는 컴파일된 라이브러리로 쓸 수 있고, vcpkg, Conan, CMake FetchContent로 추가할 수 있습니다. 포맷팅에 fmt 라이브러리를 쓰며 내장 버전이 함께 들어 있습니다.


설치와 기본 설정

vcpkg install spdlog
include(FetchContent)
FetchContent_Declare(
  spdlog
  GIT_REPOSITORY https://github.com/gabime/spdlog.git
  GIT_TAG v1.12.0
)
FetchContent_MakeAvailable(spdlog)
target_link_libraries(your_target PRIVATE spdlog::spdlog)
# conanfile.txt
[requires]
spdlog/1.12.0

spdlog/spdlog.h 하나만 include하면 기본 로거와 spdlog::info, spdlog::error 같은 편의 함수를 쓸 수 있습니다. 헤더 전용으로 쓰면 설정이 간단하지만, 여러 번역 단위에서 쓰는 큰 프로젝트에서는 컴파일된 라이브러리로 링크하는 편이 빌드 시간이 짧습니다.

시작 시 한 번 설정하기

// main.cpp — 프로그램 시작 시 한 번 호출
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>

void setup_logging() {
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console_sink->set_level(spdlog::level::debug);

    // basic_file_sink는 상위 디렉터리(logs/)가 없으면 만들어 줌
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
    file_sink->set_level(spdlog::level::info);

    auto logger = std::make_shared<spdlog::logger>(
        "main", spdlog::sinks_init_list{console_sink, file_sink});
    logger->set_level(spdlog::level::debug);
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");

    spdlog::set_default_logger(logger);
    spdlog::flush_on(spdlog::level::err);  // error 이상은 즉시 flush
}

basic_file_sink_mt의 두 번째 인자 true는 시작할 때 기존 파일을 비우라는 뜻입니다(기본값은 이어 쓰기). spdlog::flush_on은 기본 로거에 대해 “이 레벨 이상이면 바로 flush”를 설정하므로, 에러 직후 프로세스가 죽어도 그 줄은 파일에 남습니다.

#include <spdlog/spdlog.h>
int main() {
    spdlog::info("Hello {}", 42);          // [info] Hello 42
    spdlog::error("Error code: {}", 0);
    spdlog::set_level(spdlog::level::debug);
    spdlog::debug("Debug message");        // 레벨을 낮춘 뒤라 출력됨
}

메시지의 {}는 fmt 문법의 자리표시자로, {:.2f}처럼 형식 지정도 할 수 있습니다.


로거와 레벨

레벨은 낮은 쪽부터 trace, debug, info, warn, error, critical이며 기본은 info입니다. 로거의 레벨보다 낮은 메시지는 버려집니다.

spdlog::set_level(spdlog::level::debug);
spdlog::trace("trace");   // 출력 안 됨
spdlog::debug("debug");   // 출력됨
spdlog::info("info");     // 출력됨

컴파일 타임 레벨 필터링

set_level은 실행 중 필터라서 꺼진 로그도 호출 코드와 인자 평가는 남습니다. 릴리스 빌드에서 debug 로그 호출 자체를 없애려면 SPDLOG_ACTIVE_LEVEL을 헤더 include 전에 정의하고 SPDLOG_ 매크로로 로깅합니다.

// CMake에서는 target_compile_definitions(app PRIVATE SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO)
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO
#include <spdlog/spdlog.h>

SPDLOG_DEBUG("빌드에서 제거됨 (ACTIVE_LEVEL보다 낮음)");
SPDLOG_INFO("남아 있음 (실행 시 레벨도 info 이하여야 출력)");
spdlog::debug("이 호출은 매크로가 아니라서 제거되지 않음");

SPDLOG_ 매크로는 소스 파일과 줄 번호도 함께 넘기므로, 패턴의 %s(파일명)와 %#(줄 번호)는 이 매크로로 남긴 로그에서만 채워집니다. spdlog::info()나 logger->info()로 남긴 로그에서는 이 자리가 비어 있습니다.

이름 있는 로거

#include <spdlog/sinks/stdout_color_sinks.h>

auto logger = spdlog::get("mylogger");   // 등록된 로거가 없으면 nullptr
if (!logger) {
    logger = spdlog::stdout_color_mt("mylogger");  // 생성과 동시에 등록
}
logger->info("Using named logger");

stdout_color_mt 같은 팩토리 함수는 로거를 만들면서 전역 레지스트리에 등록하므로, 같은 이름으로 두 번 만들면 “logger with name … already exists” 예외가 납니다. 모듈별로 이름을 나눠 두면 패턴의 %n으로 로그 출처를 구분할 수 있습니다.

void init_module_loggers() {
    for (auto name : {"network", "database", "business"}) {
        auto lg = spdlog::stdout_color_mt(name);
        lg->set_pattern("[%H:%M:%S] [%n] %v");
    }
}

int main() {
    init_module_loggers();
    spdlog::get("network")->info("Connection established");
    spdlog::get("database")->info("Query executed");
    spdlog::shutdown();
}

다중 싱크: 콘솔, 파일, 로테이션

flowchart TB
  L[Logger] --> C[Console Sink]
  L --> F[File Sink]
  L --> R[Rotating Sink]
  C --> stdout[stdout]
  F --> file[app.log]
  R --> r0[rotating.log]
  R --> r1[rotating.1.log]
  R --> r2[rotating.2.log]

한 로거에 여러 싱크를 붙이면 메시지가 모든 싱크로 전달되고, 싱크마다 레벨을 따로 둘 수 있습니다. 로거 레벨이 먼저 적용되고, 통과한 메시지에 다시 싱크 레벨이 적용됩니다.

#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/daily_file_sink.h>

// _mt = 내부 뮤텍스로 멀티스레드 안전, _st = 단일 스레드 전용
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

// 단순 파일
auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log");

// 5MB를 넘으면 rotating.log → rotating.1.log → ... 로 밀어내고 최대 3개 보관
auto rotating = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/rotating.log", 1024 * 1024 * 5, 3);

// 매일 0시 0분에 새 파일: logs/daily_2026-10-02.log 형태
auto daily = std::make_shared<spdlog::sinks::daily_file_sink_mt>("logs/daily.log", 0, 0);

싱크별 레벨과 패턴

#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>

int main() {
    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console->set_level(spdlog::level::debug);   // 콘솔: debug까지
    console->set_pattern("[%^%l%$] %v");

    auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/app.log", 1024 * 1024, 3);
    file->set_level(spdlog::level::info);       // 파일: info 이상만
    file->set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");

    auto logger = std::make_shared<spdlog::logger>(
        "multi_level", spdlog::sinks_init_list{console, file});
    logger->set_level(spdlog::level::debug);    // 로거 레벨이 가장 먼저 적용됨

    logger->debug("콘솔에만 출력됨");
    logger->info("콘솔과 파일 모두에 출력됨");
    spdlog::shutdown();
}

logger->set_pattern()은 그 로거에 붙은 모든 싱크의 패턴을 한꺼번에 바꾸므로, 싱크별로 다른 패턴을 쓰려면 싱크에 직접 set_pattern을 호출하고 로거의 패턴은 건드리지 않습니다.


패턴과 커스텀 포매터

플래그설명
%Y-%m-%d날짜
%H:%M:%S시간
%e밀리초
%l레벨 이름
%^ %$색상 범위 시작/끝 (색상 싱크에서만 의미)
%v메시지 본문
%n로거 이름
%t스레드 ID
%s %#소스 파일명, 줄 번호 (SPDLOG_ 매크로로 남긴 로그만)
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v");

로그 수집 시스템용으로 패턴만으로 JSON 비슷한 줄을 만들 수도 있습니다.

file_sink->set_pattern(R"({"time":"%Y-%m-%dT%H:%M:%S.%e","level":"%l","msg":"%v"})");

다만 %v의 메시지 안에 따옴표나 줄바꿈이 있으면 이스케이프되지 않아 JSON이 깨집니다. 메시지에 사용자 입력이 섞인다면 JSON 라이브러리로 직렬화한 문자열을 남기거나 JSON 전용 포매터를 쓰는 편이 안전합니다.

패턴 플래그로 부족하면 custom_flag_formatter로 플래그를 직접 정의합니다. 순수 가상 함수인 format과 clone을 모두 구현해야 합니다.

#include <spdlog/pattern_formatter.h>

thread_local std::string g_request_id = "-";  // 요청 처리 시작 시 설정

class request_id_flag : public spdlog::custom_flag_formatter {
public:
    void format(const spdlog::details::log_msg&, const std::tm&,
                spdlog::memory_buf_t& dest) override {
        dest.append(g_request_id.data(), g_request_id.data() + g_request_id.size());
    }
    std::unique_ptr<custom_flag_formatter> clone() const override {
        return spdlog::details::make_unique<request_id_flag>();
    }
};

auto formatter = std::make_unique<spdlog::pattern_formatter>();
formatter->add_flag<request_id_flag>('*').set_pattern("[%l] [req:%*] %v");
spdlog::set_formatter(std::move(formatter));

비동기 로거에서는 포맷팅이 백그라운드 스레드에서 일어나므로, 위처럼 호출 스레드의 thread_local 값을 읽는 플래그는 엉뚱한 값을 찍습니다. 비동기 로거라면 요청 ID를 메시지 인자로 넘기거나 spdlog의 MDC 기능(spdlog/mdc.h, 최근 버전)을 검토합니다.


async_logger와 스레드 풀로 비동기 로깅

sequenceDiagram
  participant App as 애플리케이션
  participant Q as 큐
  participant Worker as 백그라운드 스레드
  participant File as 파일
  Note over App,File: 동기 로깅
  App->>File: info() 호출 시 직접 포맷·쓰기 후 반환
  Note over App,File: 비동기 로깅
  App->>Q: info() 호출 시 큐에 넣고 반환
  Worker->>Q: 큐에서 꺼냄
  Worker->>File: 포맷·쓰기

동기 로거는 info() 호출 안에서 포맷팅과 싱크 쓰기를 끝내고 반환합니다. 비동기 로거는 메시지를 큐에 넣고 바로 반환하며, 스레드 풀의 워커가 나중에 포맷팅과 쓰기를 합니다.

#include <spdlog/async.h>
#include <spdlog/sinks/rotating_file_sink.h>

int main() {
    // 큐 크기 8192개, 워커 스레드 1개 (프로그램에서 한 번만)
    spdlog::init_thread_pool(8192, 1);

    auto async_logger = spdlog::create_async<spdlog::sinks::rotating_file_sink_mt>(
        "async", "logs/async.log", 1024 * 1024 * 5, 3);
    async_logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

    for (int i = 0; i < 10000; ++i) {
        async_logger->info("Async message {}", i);  // 큐에 넣고 반환
    }

    spdlog::shutdown();  // 큐에 남은 로그를 모두 쓰고 정리
}

여러 싱크를 붙인 비동기 로거는 생성자로 직접 만듭니다.

#include <spdlog/async.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>

spdlog::init_thread_pool(8192, 1);
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/async.log", 1024 * 1024 * 5, 3);
auto async_multi = std::make_shared<spdlog::async_logger>(
    "async_multi", spdlog::sinks_init_list{console, file},
    spdlog::thread_pool(), spdlog::async_overflow_policy::overrun_oldest);
spdlog::register_logger(async_multi);

큐가 가득 찼을 때의 정책은 세 가지입니다. block(기본값)은 호출 스레드가 자리가 날 때까지 기다려 로그를 잃지 않는 대신 지연이 생깁니다. overrun_oldest는 가장 오래된 메시지를 덮어써 호출 스레드를 막지 않는 대신 로그를 잃을 수 있고, create_async_nb가 이 정책을 쓰는 단축 함수입니다. 최근 버전에는 새 메시지를 버리는 discard_new도 있습니다. 위 예제처럼 overrun_oldest를 고르는 것은 “로그 유실보다 요청 지연이 더 나쁘다”고 명시적으로 선택한 경우입니다.


자주 하는 실수

종료 시점과 수명

로거는 싱크를 shared_ptr로 들고 있으므로, 싱크를 만든 지역 변수가 스코프를 벗어나도 싱크는 살아 있습니다. 실제로 문제가 되는 것은 종료 시점입니다. spdlog::shutdown() 이후나 전역 객체의 소멸자에서 로거를 쓰면 이미 정리된 레지스트리나 스레드 풀에 접근하게 됩니다. 비동기 로거는 스레드 풀을 약한 참조로만 들고 있어서, 풀이 사라진 뒤에 로그를 남기면 “thread pool doesn’t exist anymore” 같은 에러가 발생합니다. 로깅 설정은 main 진입 직후에, shutdown()은 다른 정리가 끝난 main의 마지막에 두고, 전역 객체의 소멸자에서는 로깅하지 않는 것이 안전합니다.

_st 싱크를 여러 스레드에서 사용

// ❌ stdout_color_sink_st는 락이 없음
auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_st>();
auto logger = std::make_shared<spdlog::logger>("mt", sink);
// 여러 스레드에서 logger->info() → 데이터 레이스

// ✅ 멀티스레드에서는 _mt
auto safe_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

init_thread_pool을 두 번 호출

init_thread_pool을 다시 호출하면 전역 스레드 풀이 새 풀로 교체되고, 다른 소유자가 없는 이전 풀은 파괴됩니다. 이전 풀을 쓰던 비동기 로거는 더 이상 로그를 남기지 못하고 에러를 냅니다. 스레드 풀은 프로그램 시작 시 한 번만 초기화합니다.

shutdown 없이 종료

비동기 로거를 쓰면서 spdlog::shutdown()을 호출하지 않고 프로세스가 끝나면, 큐에 남은 로그가 쓰이지 않을 수 있습니다. 특히 exit()이나 std::quick_exit(), 시그널로 종료하는 경로에서는 정상적인 정리가 일어나지 않으므로, 종료 직전 spdlog::shutdown()을 호출하거나 중요한 레벨은 flush_on으로 즉시 내보냅니다.

포맷 문자열과 인자 불일치

spdlog::info("User {} at {}", userId);             // ❌ 인자 부족
spdlog::info("User {}", userId, timestamp);         // 인자 과다: 남는 인자는 무시됨

인자가 부족하면 C++20에서 최신 spdlog/fmt는 형식 문자열을 컴파일 시점에 검사해 컴파일 에러를 내고, 그 외 환경에서는 실행 시 fmt::format_error가 발생해 spdlog가 에러 메시지를 대신 출력합니다. 인자가 남는 경우는 에러 없이 무시되므로 리뷰로 잡아야 합니다.

등록되지 않은 로거

auto logger = spdlog::get("nonexistent");
logger->info("크래시!");  // ❌ nullptr 역참조

auto lg = spdlog::get("mylogger");
if (!lg) lg = spdlog::default_logger();  // ✅ 없으면 기본 로거
lg->info("OK");

설정 전 로깅

setup_logging()보다 먼저 spdlog::info()를 호출하면 spdlog가 만들어 둔 기본 콘솔 로거로 출력되어, 의도한 파일이나 패턴이 적용되지 않습니다. 정적 초기화 단계(전역 객체의 생성자)에서 로깅하는 코드도 같은 문제를 겪으므로, 로깅 설정을 main의 첫 줄에 둡니다.


동기와 비동기 로깅의 처리량 측정

비동기 로깅이 빨라지는 부분은 “호출 스레드가 반환하기까지의 시간”입니다. 포맷팅과 디스크 쓰기 비용 자체는 백그라운드 스레드로 옮겨질 뿐 사라지지 않으므로, 로그량이 디스크 처리 속도를 계속 넘으면 큐가 가득 차고 정책에 따라 호출 스레드가 막히거나 로그가 버려집니다. 실제 차이는 하드웨어, 싱크 종류, 메시지 길이에 따라 크게 달라지므로 직접 측정해 보는 것이 가장 정확합니다.

#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <chrono>
#include <iostream>

template <typename F>
long long measure_ms(F&& f) {
    auto start = std::chrono::steady_clock::now();
    f();
    auto end = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}

int main() {
    const int n = 100000;

    auto sync_logger = spdlog::basic_logger_mt("sync", "sync.log", true);
    auto sync_ms = measure_ms([&] {
        for (int i = 0; i < n; ++i) sync_logger->info("Sync message {}", i);
        sync_logger->flush();
    });

    spdlog::init_thread_pool(8192, 1);
    auto async_logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
        "async", "async.log", true);
    auto enqueue_ms = measure_ms([&] {
        for (int i = 0; i < n; ++i) async_logger->info("Async message {}", i);
    });
    auto total_ms = enqueue_ms + measure_ms([] { spdlog::shutdown(); });

    std::cout << "sync: " << sync_ms << " ms\n"
              << "async enqueue: " << enqueue_ms << " ms, until flushed: " << total_ms << " ms\n";
}

호출 스레드가 체감하는 시간(enqueue)과 모든 로그가 실제로 기록되기까지의 시간(until flushed)을 따로 보는 것이 핵심입니다. 로그량이 적고 지연에 민감하지 않은 프로그램은 동기 로거로도 충분하고, 요청 처리 경로에서 많은 로그를 남기며 지연이 중요한 서버라면 비동기 로거와 넉넉한 큐 크기, 그리고 큐가 찼을 때의 정책을 함께 정합니다.


로그를 쓰는 습관

레벨사용 예
trace루프 내부, 매우 상세한 상태
debug개발용 변수 값, 흐름 추적
info정상 동작, 요청 처리 완료
warn예상 가능한 비정상, 재시도
error처리 실패, 사용자 영향
critical프로그램을 계속할 수 없는 상황

메시지에는 누가, 무엇을, 왜를 담습니다. logger->error("Failed")보다 logger->error("User {} login failed: invalid password (attempt {})", userId, attempt)가 장애 분석에 훨씬 쓸모 있습니다. 비밀번호, 토큰, 개인정보는 남기지 않습니다.

로그 레벨이 꺼져 있어도 인자는 먼저 평가됩니다. 비용이 큰 인자는 레벨을 먼저 확인합니다.

// ❌ debug가 꺼져 있어도 expensive_dump()가 호출됨
logger->debug("State: {}", expensive_dump());

// ✅ 레벨 확인 후 호출
if (logger->should_log(spdlog::level::debug)) {
    logger->debug("State: {}", expensive_dump());
}

구조화 로깅과 운영 설정

로그 수집 시스템(ELK, Loki, CloudWatch 등)과 연동할 때는 JSON처럼 기계가 읽을 수 있는 형식이 유리합니다. 메시지 형식 문자열 안에서 JSON의 중괄호를 쓰려면 fmt 문법에 맞게 {{와 }}로 이스케이프해야 합니다.

// fmt 형식 문자열에서 리터럴 중괄호는 {{ }}
spdlog::info(R"({{"event":"user_login","user_id":{},"ip":"{}"}})", userId, ip);

이스케이프하지 않으면 {"event"...가 자리표시자로 해석되어 형식 에러가 납니다. 값에 따옴표가 들어갈 수 있다면 직접 조립하지 말고 JSON 라이브러리로 직렬화한 문자열을 남깁니다.

싱크사용 시점
rotating_file_sink크기 기준으로 나누고 파일 수를 제한할 때
daily_file_sink날짜별로 나눌 때 (보관 파일 수 지정 가능)
hourly_file_sink시간 단위로 나눌 때
#include <spdlog/spdlog.h>
#include <spdlog/async.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>

void setup_production_logging() {
    spdlog::init_thread_pool(8192, 1);

    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console->set_level(spdlog::level::info);
    console->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v");

    auto all_file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/all.log", 10 * 1024 * 1024, 5);
    all_file->set_level(spdlog::level::info);
    all_file->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%t] %v");

    // error 이상만 따로 모아 모니터링·알림에 연결
    auto err_file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/error.log", 5 * 1024 * 1024, 3);
    err_file->set_level(spdlog::level::err);

    auto logger = std::make_shared<spdlog::async_logger>(
        "prod", spdlog::sinks_init_list{console, all_file, err_file},
        spdlog::thread_pool(), spdlog::async_overflow_policy::block);
    logger->set_level(spdlog::level::info);
    logger->flush_on(spdlog::level::err);
    spdlog::set_default_logger(logger);
}

배포 전에는 로테이션 크기와 보관 개수가 디스크 용량에 맞는지, 릴리스 빌드에 SPDLOG_ACTIVE_LEVEL이 의도대로 설정되었는지, 비동기 로거라면 종료 경로에서 shutdown()이 호출되는지 확인합니다.


같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. SPDLOG_ACTIVE_LEVEL과 set_level은 어떻게 다른가요?

A. set_level은 런타임 필터라서 레벨이 낮은 로그를 버리긴 하지만 호출 코드 자체는 바이너리에 남아 있습니다. SPDLOG_ACTIVE_LEVEL은 컴파일 타임 설정으로, 지정한 레벨보다 낮은 SPDLOG_DEBUG·SPDLOG_TRACE 같은 매크로 호출을 아예 제거합니다. 이 효과는 매크로를 사용할 때만 적용되고 logger->debug()를 직접 호출한 코드에는 적용되지 않으며, spdlog 헤더를 include하기 전에 정의해야 합니다.

다음으로 소켓 기초(#28-1)를 읽어 보면 좋습니다.

이전 글: C++ 실전 가이드 #27-2: JSON (nlohmann)

다음 글: [C++ 실전 가이드 #28-1] 소켓 프로그래밍 기초: TCP/UDP로 연결과 송수신