C++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]

C++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]

이 글의 핵심

C++ 로깅 라이브러리 (spdlog)에 대한 실전 가이드입니다. 빠른 로깅과 다중 싱크 [#27-3] 등을 예제와 함께 상세히 설명합니다.

들어가며: “로그가 느리고 관리가 어려워요”

실제 겪는 문제 시나리오

시나리오 1: 프로덕션 디스크 풀

웹 서버를 배포한 지 한 달, 갑자기 디스크 사용량이 100%에 도달해 서비스가 중단됐습니다. 원인은 로그 파일이었습니다. std::cout으로 출력한 로그를 >> app.log로 리다이렉트했는데, 로테이션이 없어 단일 파일이 수십 GB까지 커졌습니다. 날짜별·크기별 로테이션을 직접 구현하려니 코드가 복잡해지고, 기존 로그와의 호환성도 걱정됩니다.

시나리오 2: 에러 추적 불가

고객이 “로그인 실패”를 신고했지만, 로그에는 login failed만 찍혀 있었습니다. 어느 사용자, 어느 시점, 어떤 이유인지 알 수 없었습니다. cout만 쓰면 레벨 구분이 없어, 디버그용 출력과 에러 메시지가 섞이고, 프로덕션에서 불필요한 로그가 쏟아집니다. 에러 발생 시 즉시 flush도 안 되어, 크래시 직전 로그가 손실되기도 합니다.

시나리오 3: 로그 때문에 응답 지연

초당 1만 건 요청을 처리하는 API 서버에서, 각 요청마다 동기적으로 파일에 로그를 쓰고 있었습니다. 디스크 I/O가 블로킹되면서 평균 응답 시간이 50ms에서 200ms로 급증했습니다. 로그를 줄이자 디버깅이 어려워지고, 그대로 두자 성능이 나빠지는 딜레마에 빠졌습니다.

std::cout만 쓰면 이런 문제가 생깁니다:

  • 레벨 구분 없음: 디버그용 출력과 에러 메시지를 구분할 수 없어, 프로덕션에서 불필요한 로그가 쏟아짐
  • 파일 관리 어려움: 로그를 파일로 남기려면 직접 ofstream을 열고, 날짜별·크기별 로테이션을 직접 구현해야 함
  • 성능 이슈: 동기 I/O로 인해 로그 쓰기가 메인 로직을 블로킹함
  • 멀티스레드 불안전: 여러 스레드가 동시에 cout에 쓰면 출력이 뒤섞임

spdlog는 이런 문제를 해결합니다. 헤더 전용 또는 라이브러리로 사용 가능하고, 빠르며, 레벨(trace/debug/info/warn/error/critical)·다중 싱크(콘솔, 파일, 로테이션)·비동기 로깅을 쉽게 설정할 수 있습니다. 컴파일 타임에 레벨을 걸어 릴리즈에서 debug 로그 비용을 제거할 수 있고, 프로덕션 로깅에 적합합니다.

flowchart LR
  subgraph problem["문제 상황"]
    P1[cout만 사용]
    P2[레벨 없음]
    P3[파일 관리 수동]
    P4[동기 I/O 블로킹]
    P1 --> P2 --> P3 --> P4
  end
  subgraph solution["spdlog 해결"]
    S1[레벨별 필터링]
    S2[다중 싱크]
    S3[로테이션 자동]
    S4[비동기 로깅]
    S1 --> S2 --> S3 --> S4
  end
  problem --> solution

문제의 근본 원인

std::cout단일 스트림동기적으로 쓰는 도구일 뿐입니다. 로그 레벨, 파일 로테이션, 비동기 I/O 같은 개념이 설계에 포함되어 있지 않습니다. 반면 spdlog는 로거 → 싱크들 구조로, 한 번 로그를 남기면 콘솔·파일·원격 서버 등 여러 목적지로 동시에 전달할 수 있고, 싱크별로 레벨·패턴을 다르게 설정할 수 있습니다. 비동기 모드에서는 링 버퍼에만 넣고 즉시 반환하므로, I/O 대기로 인한 블로킹이 없습니다.

목표:

  • 기본 사용 (전역/로거 인스턴스)
  • 레벨 (trace, debug, info, warn, error, critical)
  • 콘솔·파일·로테이션 싱크
  • 커스텀 포맷터패턴
  • 비동기 로깅 구현
  • 자주 하는 실수성능 비교
  • 모범 사례프로덕션 패턴 (구조화 로깅, 로테이션)

이 글을 읽으면:

  • spdlog로 레벨별 로깅을 할 수 있습니다.
  • 파일·콘솔·로테이션 동시 출력을 설정할 수 있습니다.
  • 비동기 로깅으로 성능을 최적화할 수 있습니다.
  • 프로덕션에서 자주 발생하는 에러를 피할 수 있습니다.

요구 환경: spdlog는 헤더 전용 또는 라이브러리 링크. vcpkg(vcpkg install spdlog), Conan, FetchContent로 추가. C++11 이상. fmt 라이브러리 의존(spdlog에 포함된 버전 사용 시 별도 설치 불필요).

개념을 잡는 비유

시간·파일·로그·JSON은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.


목차

  1. 설치와 완전한 설정
  2. 로거와 레벨
  3. 다중 싱크 (콘솔/파일/로테이션)
  4. 커스텀 포맷터와 패턴
  5. 비동기 로깅 구현
  6. 자주 하는 실수 (싱크 생명주기, 스레드 안전성)
  7. 성능 비교 (동기 vs 비동기)
  8. 프로덕션 패턴 (구조화 로깅, 로테이션)
  9. 모범 사례 (Best Practices)

1. 설치와 완전한 설정

1.1 설치 방법

vcpkg (권장):

vcpkg install spdlog

CMake FetchContent:

include(FetchContent)
FetchContent_Declare(
  spdlog
  GIT_REPOSITORY https://github.com/gabime/spdlog.git
  GIT_TAG v1.12.1
)
FetchContent_MakeAvailable(spdlog)
target_link_libraries(your_target PRIVATE spdlog::spdlog)

Conan:

[requires]
spdlog/1.12.1

1.2 include

spdlog/spdlog.h 하나만 include 하면 전역 로거와 spdlog::info, spdlog::error 같은 편의 함수를 쓸 수 있습니다. 헤더 전용으로 쓸 수도 있고, 라이브러리로 링크하면 바이너리 크기와 컴파일 시간을 줄일 수 있습니다.

#include <spdlog/spdlog.h>

1.3 완전한 초기 설정 예제

프로그램 시작 시 한 번만 로거를 설정하는 패턴입니다. 레벨, 패턴, 싱크를 모두 구성합니다.

// main.cpp - 프로그램 진입점에서 한 번만 호출
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>

void setup_logging() {
    // 1. 콘솔 싱크 (색상 출력)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console_sink->set_level(spdlog::level::debug);

    // 2. 파일 싱크
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
    file_sink->set_level(spdlog::level::info);

    // 3. 멀티 싱크 로거 생성
    spdlog::logger logger("main", {console_sink, file_sink});
    logger.set_level(spdlog::level::debug);
    logger.set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");

    // 4. 전역 기본 로거로 등록
    spdlog::set_default_logger(std::make_shared<spdlog::logger>(logger));
    spdlog::flush_on(spdlog::level::err);  // error 이상 시 즉시 flush
}

1.4 간단 사용

spdlog::info(“Hello {}”, 42){} 자리에 42 가 들어간 문자열을 info 레벨로 출력합니다. set_level(debug) 로 레벨을 낮추면 debug 메시지도 나오고, 기본(info)에서는 debug 아래는 출력되지 않습니다. fmt 스타일 플레이스홀더 {} 를 사용해 가변 인자를 넘길 수 있습니다.

// g++ -std=c++17 -o spdlog_basic spdlog_basic.cpp -I<spdlog경로> -lspdlog && ./spdlog_basic
#include <spdlog/spdlog.h>

int main() {
    spdlog::info("Hello {}", 42);
    spdlog::error("Error code: {}", 0);
    spdlog::set_level(spdlog::level::debug);
    spdlog::debug("Debug message");
    return 0;
}

실행 결과: info/error/debug 로그가 포맷되어 출력됩니다 (예: [info] Hello 42).


2. 로거와 레벨

2.1 레벨 종류

  • trace: 가장 상세한 추적 (개발 시)
  • debug: 디버깅용
  • info: 일반 정보
  • warn: 경고
  • error: 에러
  • critical: 치명적 에러 (프로그램 종료 직전)

기본은 info. 그 아래 레벨(trace, debug)은 출력되지 않습니다.

spdlog::set_level(spdlog::level::debug);
spdlog::trace("trace");   // 레벨이 debug면 trace는 안 나옴
spdlog::debug("debug");   // 출력됨
spdlog::info("info");     // 출력됨

2.2 컴파일 타임 레벨 필터링

릴리즈 빌드에서 debug 로그의 문자열 포맷팅 비용을 완전히 제거하려면 SPDLOG_LEVEL 매크로를 사용합니다.

// CMakeLists.txt 또는 컴파일 옵션
// -DSPDLOG_LEVEL=1  (0=trace, 1=debug, 2=info, 3=warn, 4=error, 5=critical, 6=off)

또는 코드에서:

#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG
#include <spdlog/spdlog.h>

// SPDLOG_DEBUG는 SPDLOG_ACTIVE_LEVEL이 debug 이상일 때만 컴파일됨
SPDLOG_DEBUG("이 메시지는 릴리즈에서 완전히 제거됨");

2.3 로거 인스턴스

spdlog::get(“mylogger”) 로 이름으로 등록된 로거를 가져옵니다. 없으면 stdout_color_mt(“mylogger”) 로 콘솔 색상 출력용 로거를 만들어 등록하고, 이후 logger->info(…) 로 그 로거를 씁니다. _mt 접미사는 멀티스레드 안전 로거를 의미하며, 모듈별로 이름을 나눠 두면 로그 출처를 구분하기 쉽습니다.

auto logger = spdlog::get("mylogger");
if (!logger) {
    logger = spdlog::stdout_color_mt("mylogger");
}
logger->info("Using named logger");

2.4 모듈별 로거 분리 (완전한 예제)

네트워크·DB·비즈니스 로직 등 모듈별로 로거를 나누면 로그 필터링과 추적이 쉬워집니다.

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

// 모듈별 로거 생성 및 등록
void init_module_loggers() {
    auto net_logger = spdlog::stdout_color_mt("network");
    net_logger->set_pattern("[%H:%M:%S] [%n] %v");

    auto db_logger = spdlog::stdout_color_mt("database");
    db_logger->set_pattern("[%H:%M:%S] [%n] %v");

    auto biz_logger = spdlog::stdout_color_mt("business");
    biz_logger->set_pattern("[%H:%M:%S] [%n] %v");
}

int main() {
    init_module_loggers();

    spdlog::get("network")->info("Connection established");
    spdlog::get("database")->info("Query executed: SELECT *");
    spdlog::get("business")->info("Order processed");

    spdlog::shutdown();
    return 0;
}

3. 다중 싱크 (콘솔/파일/로테이션)

3.1 싱크 아키텍처

flowchart TB
  subgraph app["애플리케이션"]
    L[Logger]
  end
  subgraph sinks["싱크들"]
    C[Console Sink]
    F[File Sink]
    R[Rotating Sink]
  end
  L --> C
  L --> F
  L --> R
  C --> stdout["stdout"]
  F --> file["app.log"]
  R --> r1["app.1.log"]
  R --> r2["app.2.log"]

한 로거에 여러 싱크를 붙이면, 로그 메시지가 모든 싱크로 동시에 전달됩니다. 싱크별로 레벨을 다르게 설정할 수 있어, 예를 들어 콘솔에는 debug까지, 파일에는 info 이상만 남길 수 있습니다.

3.2 콘솔 싱크

#include <spdlog/sinks/stdout_color_sinks.h>

// _mt = 멀티스레드 안전, _st = 싱글스레드 전용
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console->set_level(spdlog::level::debug);

3.3 기본 파일 싱크

basic_logger_mt(“file”, “logs/app.log”) 는 “file” 이라는 이름의 로거를 만들고, 로그를 logs/app.log 파일에 씁니다. basic_file_sink 는 단순 파일 출력용입니다.

#include <spdlog/sinks/basic_file_sink.h>

auto logger = spdlog::basic_logger_mt("file", "logs/app.log");
logger->info("To file");

3.4 로테이션 파일 싱크

rotating_file_sink 는 파일 크기가 지정된 크기를 넘으면 새 파일로 넘깁니다. max_size 바이트, max_files 개수만큼 유지합니다.

#include <spdlog/sinks/rotating_file_sink.h>

// 5MB마다 로테이션, 최대 3개 파일 유지
auto rotating = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/app.log", 1024 * 1024 * 5, 3);

3.5 일별 로테이션 (daily_file_sink)

날짜가 바뀔 때마다 새 파일을 만듭니다.

#include <spdlog/sinks/daily_file_sink.h>

// 매일 자정에 새 파일, 오후 2시에 로테이션하려면 hour=14, minute=0
auto daily = std::make_shared<spdlog::sinks::daily_file_sink_mt>(
    "logs/daily", 0, 0);  // 0시 0분에 로테이션

3.6 콘솔 + 파일 + 로테이션 동시 사용

stdout_color_sink_mt, basic_file_sink_mt, rotating_file_sink_mt 를 각각 만들고, spdlog::logger 생성자에 싱크 목록을 넘깁니다. 이 로거에 info 를 호출하면 콘솔, 기본 파일, 로테이션 파일 모두에 동시에 출력됩니다.

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

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");
auto rotating = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/rotating.log", 1024 * 1024 * 5, 3);

spdlog::logger multi("multi", {console, file, rotating});
multi.set_level(spdlog::level::debug);
multi.info("콘솔, app.log, rotating.log 모두에 출력됨");

3.7 싱크별 레벨이 다른 완전한 예제

콘솔에는 개발용으로 debug까지, 파일에는 프로덕션용으로 info 이상만 남기는 설정입니다.

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

int main() {
    namespace fs = std::filesystem;
    fs::create_directories("logs");

    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");

    spdlog::logger logger("multi_level", {console, file});
    logger.set_level(spdlog::level::debug);  // 로거 최소 레벨

    logger.debug("콘솔에만 출력됨 (파일에는 info 이상만)");
    logger.info("콘솔과 파일 모두에 출력됨");
    logger.error("콘솔과 파일 모두에 출력됨");

    spdlog::shutdown();
    return 0;
}

4. 커스텀 포맷터와 패턴

4.1 포맷 문자열 (메시지 본문)

{} 플레이스홀더에 userId, timestamp 가 순서대로 들어갑니다. fmt 라이브러리와 같은 문법이라 {:d}, {:.2f} 등 형식 지정도 가능합니다.

int userId = 42;
std::string timestamp = "2026-03-12 10:00:00";
spdlog::info("User {} logged in at {}", userId, timestamp);
spdlog::info("Pi = {:.2f}", 3.14159);  // Pi = 3.14

4.2 패턴 (날짜/시간/레벨)

set_pattern 으로 로그 한 줄의 형식을 정합니다. 주요 패턴:

패턴설명
%Y-%m-%d날짜
%H:%M:%S시간
%e밀리초
%l레벨 (info, error 등)
%^ %$색상 시작/끝 (콘솔용)
%v메시지 본문
%s소스 파일명
%#줄 번호
%n로거 이름
// 상세한 디버깅용 패턴
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");

// 프로덕션용 간단 패턴
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%l] %v");

4.3 싱크별 다른 패턴

콘솔에는 색상과 상세 정보를, 파일에는 JSON 형태로 남기고 싶을 때:

// 콘솔: 색상 + 상세
console_sink->set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] [%s:%#] %v");

// 파일: JSON 스타일 (로그 수집 시스템 연동용)
file_sink->set_pattern("{\"time\":\"%Y-%m-%dT%H:%M:%S.%eZ\",\"level\":\"%l\",\"msg\":\"%v\"}");

4.4 패턴 조합 완전한 예제

개발/프로덕션 환경에 맞는 패턴을 적용한 예제입니다.

#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <filesystem>

int main() {
    std::filesystem::create_directories("logs");

    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    console->set_pattern("[%H:%M:%S.%e] [%^%l%$] [%n] %v");  // 개발용: 간결 + 색상

    auto file = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log", true);
    file->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%s:%#] %v");  // 파일: 상세 + 위치

    spdlog::logger logger("pattern_demo", {console, file});
    logger.set_level(spdlog::level::info);

    logger.info("User 42 logged in");
    logger.warn("High memory usage: {}%", 85);
    logger.error("Connection failed: timeout");

    spdlog::shutdown();
    return 0;
}

4.5 커스텀 포매터 클래스

패턴으로 부족할 때 custom_flag_formatter 를 사용해 완전히 커스텀한 포맷을 만들 수 있습니다.

#include <spdlog/pattern_formatter.h>

class my_custom_formatter : public spdlog::custom_flag_formatter {
public:
    void format(const spdlog::details::log_msg& msg,
               const std::tm& tm,
               spdlog::memory_buf_t& dest) override {
        // 예: 요청 ID를 로그에 추가
        dest.append("req_id:");
        dest.append(std::to_string(extract_request_id(msg)));
    }
};

// 등록: %i -> my_custom_formatter
auto formatter = std::make_unique<spdlog::pattern_formatter>();
formatter->add_flag<my_custom_formatter>('i');
logger->set_formatter(std::move(formatter));

5. 비동기 로깅 구현

5.1 동기 vs 비동기

sequenceDiagram
  participant App as 애플리케이션
  participant Buffer as 링 버퍼
  participant Worker as 백그라운드 스레드
  participant File as 파일

  Note over App,File: 동기 로깅
  App->>File: info() 호출 → 직접 쓰기 → 반환

  Note over App,File: 비동기 로깅
  App->>Buffer: info() 호출 → 버퍼에 넣고 즉시 반환
  Worker->>Buffer: 버퍼에서 꺼냄
  Worker->>File: 파일에 쓰기

동기 로깅은 info() 호출 시 실제 I/O가 끝날 때까지 블로킹됩니다. 비동기 로깅은 버퍼에만 넣고 바로 반환하므로, I/O가 메인 로직을 막지 않습니다.

5.2 비동기 로거 설정

init_thread_pool(8192, 1) 은 크기 8192인 로그 버퍼와 백그라운드 스레드 1개로 비동기 로깅 풀을 초기화합니다. create_async 로 만든 로거는 info 등을 호출해도 버퍼에만 넣고 바로 반환합니다.

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

int main() {
    std::filesystem::create_directories("logs");

    // 1. 스레드 풀 초기화 (버퍼 크기, 워커 스레드 수)
    spdlog::init_thread_pool(8192, 1);

    // 2. 비동기 로거 생성
    auto async_logger = spdlog::create_async<spdlog::sinks::rotating_file_sink_mt>(
        "async", "logs/async.log", 1024 * 1024 * 5, 3);

    async_logger->set_level(spdlog::level::debug);
    async_logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v");

    // 3. 사용
    for (int i = 0; i < 10000; ++i) {
        async_logger->info("Async message {}", i);  // 블로킹 없이 즉시 반환
    }

    // 4. 프로그램 종료 전 flush (남은 로그를 파일에 씀)
    spdlog::shutdown();
    return 0;
}

5.3 비동기 + 다중 싱크

비동기 로거에도 콘솔과 파일을 동시에 붙일 수 있습니다.

#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);

async_overflow_policy:

  • block: 버퍼 가득 차면 블로킹 (안전하지만 느릴 수 있음)
  • overrun_oldest: 버퍼 가득 차면 가장 오래된 로그 버림 (기본값, 성능 우선)

5.4 비동기 로깅 완전한 예제 (실행 가능)

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

int main() {
    std::filesystem::create_directories("logs");

    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_multi.log", 1024 * 1024, 3);

    auto logger = std::make_shared<spdlog::async_logger>(
        "async_demo", spdlog::sinks_init_list{console, file},
        spdlog::thread_pool(), spdlog::async_overflow_policy::overrun_oldest);
    logger->set_level(spdlog::level::info);
    logger->set_pattern("[%H:%M:%S.%e] [%l] %v");
    spdlog::register_logger(logger);

    // 멀티스레드에서 비동기 로깅
    std::vector<std::thread> threads;
    for (int t = 0; t < 4; ++t) {
        threads.emplace_back([logger, t]() {
            for (int i = 0; i < 100; ++i) {
                logger->info("Thread {} message {}", t, i);
            }
        });
    }
    for (auto& th : threads) th.join();

    spdlog::shutdown();
    return 0;
}

6. 자주 하는 실수 (싱크 생명주기, 스레드 안전성)

6.1 싱크 생명주기 문제

문제: 로거가 싱크를 참조하는데, 싱크가 먼저 소멸되면 크래시가 발생합니다.

// ❌ 잘못된 예: file_sink가 스코프를 벗어나면 소멸
{
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log");
    auto logger = std::make_shared<spdlog::logger>("bad", file_sink);
    spdlog::register_logger(logger);
}  // file_sink 소멸
spdlog::get("bad")->info("크래시 가능!");  // dangling reference

해결: 싱크와 로거를 shared_ptr 로 관리하고, 프로그램 종료 시 spdlog::shutdown() 을 호출해 로거가 싱크보다 먼저 정리되도록 합니다.

// ✅ 올바른 예: 싱크와 로거를 전역/지속적으로 유지
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log");
auto logger = std::make_shared<spdlog::logger>("good", file_sink);
spdlog::register_logger(logger);
// 프로그램 종료 시
spdlog::shutdown();

6.2 스레드 안전성: _mt vs _st

문제: 싱글스레드용 싱크(_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() 호출 → undefined behavior

해결: 멀티스레드 환경에서는 반드시 _mt 접미사가 붙은 싱크를 사용합니다.

// ✅ 안전
auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

6.3 init_thread_pool 중복 호출

문제: init_thread_pool 을 두 번 호출하면 기존 풀이 파괴되고, 이전 로거들이 dangling 상태가 됩니다.

// ❌ 잘못된 예
spdlog::init_thread_pool(8192, 1);
auto logger1 = spdlog::create_async<...>("async1", ...);
spdlog::init_thread_pool(4096, 2);  // logger1이 깨짐!

해결: init_thread_pool 은 프로그램 시작 시 한 번만 호출합니다.

6.4 로그 디렉터리 없음

문제: logs/app.log 경로를 사용하는데 logs 디렉터리가 없으면 파일 생성이 실패할 수 있습니다.

// ✅ 디렉터리 생성 후 로거 설정
#include <filesystem>
namespace fs = std::filesystem;
fs::create_directories("logs");
auto logger = spdlog::basic_logger_mt("file", "logs/app.log");

6.5 shutdown 없이 종료

문제: 비동기 로거 사용 시, shutdown() 을 호출하지 않고 프로그램이 종료되면 버퍼에 남아 있는 로그가 파일에 쓰이지 않고 손실됩니다.

// ✅ main 종료 전
int main() {
    // ... 로깅 ...
    spdlog::shutdown();  // 남은 로그 flush 후 정리
    return 0;
}

6.6 포맷 문자열과 인자 불일치

문제: 플레이스홀더 {} 개수와 인자 개수가 맞지 않으면 런타임 예외 또는 잘못된 출력이 발생합니다.

// ❌ 잘못된 예: 인자 부족
spdlog::info("User {} at {}", userId);  // fmt::format_error 또는 쓰레기값

// ❌ 잘못된 예: 인자 과다 (일부 버전에서는 무시됨)
spdlog::info("User {}", userId, timestamp);  // 예측 불가

해결: 플레이스홀더와 인자 개수를 항상 일치시킵니다.

// ✅ 올바른 예
spdlog::info("User {} at {}", userId, timestamp);

6.7 등록되지 않은 로거 사용

문제: spdlog::get(“unknown”) 은 등록되지 않은 이름이면 nullptr 를 반환합니다. null 체크 없이 사용하면 크래시합니다.

// ❌ 위험
auto logger = spdlog::get("nonexistent");
logger->info("크래시!");  // logger가 nullptr

해결: null 체크 후 사용하거나, spdlog::get 이 없으면 자동 생성하는 stdout_color_mt 를 사용합니다.

// ✅ 안전
auto logger = spdlog::get("mylogger");
if (logger) {
    logger->info("OK");
}

// 또는: 없으면 기본 로거 사용
auto logger = spdlog::get("mylogger");
if (!logger) logger = spdlog::default_logger();
logger->info("OK");

6.8 로거 설정 전 로깅 호출

문제: setup_logging() 을 호출하기 전에 spdlog::info() 를 호출하면, spdlog 기본 로거(콘솔)를 사용합니다. 의도한 파일·패턴이 적용되지 않을 수 있습니다.

// ❌ 순서 잘못됨
int main() {
    spdlog::info("이건 기본 로거에 출력됨");  // setup 전
    setup_logging();
    spdlog::info("이건 설정된 로거에 출력됨");
}

해결: main 진입 직후, 다른 초기화보다 먼저 로깅 설정을 호출합니다.

// ✅ 올바른 순서
int main() {
    setup_logging();  // 가장 먼저
    spdlog::info("설정된 로거 사용");
    // ...
}

7. 성능 비교 (동기 vs 비동기)

7.1 벤치마크 요약

모드100만 건 로그 (대략)메인 스레드 블로킹
동기 파일~2–5초매 로그마다
동기 콘솔~5–10초매 로그마다
비동기 파일~0.1–0.3초거의 없음
비동기 콘솔~0.2–0.5초거의 없음

실제 수치는 하드웨어, 디스크 속도, 로그 내용에 따라 달라집니다.

7.2 벤치마크 코드 예시

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

void benchmark_sync(int n) {
    auto logger = spdlog::basic_logger_mt("sync", "sync.log");
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < n; ++i) {
        logger->info("Sync message {}", i);
    }
    logger->flush();
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "Sync " << n << " msgs: " << ms << " ms\n";
}

void benchmark_async(int n) {
    spdlog::init_thread_pool(8192, 1);
    auto logger = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>("async", "async.log");
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < n; ++i) {
        logger->info("Async message {}", i);
    }
    spdlog::shutdown();
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "Async " << n << " msgs: " << ms << " ms\n";
}

int main() {
    const int n = 100000;
    benchmark_sync(n);
    benchmark_async(n);
    return 0;
}

7.3 선택 가이드

  • 로그량이 적음 (< 1000건/초): 동기 로깅으로 충분
  • 로그량이 많음 (> 10000건/초): 비동기 로깅 권장
  • 실시간 응답 중요: 비동기 + 적절한 버퍼 크기
  • 디스크 I/O 병목: 비동기 + SSD 사용

8. 프로덕션 패턴 (구조화 로깅, 로테이션)

8.1 구조화 로깅 (JSON)

로그 수집 시스템(ELK, Loki, CloudWatch 등)과 연동할 때 JSON 형식이 유리합니다.

#include <spdlog/spdlog.h>

// 수동 JSON 포맷
spdlog::info(R"({"event":"user_login","user_id":{},"ip":"{}"})", userId, ip);

// 또는 custom formatter로 자동 JSON 래핑

8.2 로테이션 전략

전략사용 시점
rotating_file_sink로그량이 예측 가능하고, 파일 개수를 제한하고 싶을 때
daily_file_sink날짜별로 로그를 나누고 싶을 때
daily_rotating_file_sink날짜 + 크기 둘 다 적용 (spdlog 1.12+)
// 크기 10MB, 최대 5개 파일
auto rotating = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/app.log", 10 * 1024 * 1024, 5);

// 매일 자정 로테이션
auto daily = std::make_shared<spdlog::sinks::daily_file_sink_mt>(
    "logs/daily", 0, 0);

8.3 프로덕션 설정 체크리스트

  • 로그 디렉터리 존재 확인 및 생성
  • 릴리즈 빌드에서 SPDLOG_LEVEL 로 debug/trace 제거
  • flush_on(spdlog::level::err) 로 에러 시 즉시 flush
  • 비동기 사용 시 shutdown() 호출
  • 로테이션 크기/개수 설정 (디스크 용량 고려)
  • 모듈별 로거 이름 분리 (추적 용이)
  • 민감 정보(비밀번호, 토큰) 로그 출력 금지

8.4 완전한 프로덕션 설정 예제

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

void setup_production_logging() {
    namespace fs = std::filesystem;
    fs::create_directories("logs");

    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 file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/app.log", 10 * 1024 * 1024, 5);
    file->set_level(spdlog::level::info);
    file->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] [%s:%#] %v");

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

    spdlog::set_default_logger(logger);
}

8.5 에러 전용 로그 파일 분리

에러·크리티컬 로그만 별도 파일에 남기면 모니터링과 알림 연동이 쉽습니다.

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

void setup_error_separate_logging() {
    std::filesystem::create_directories("logs");

    auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto all_file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/all.log", 10 * 1024 * 1024, 5);
    auto err_file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/error.log", 5 * 1024 * 1024, 3);

    all_file->set_level(spdlog::level::info);
    err_file->set_level(spdlog::level::err);  // error 이상만

    spdlog::logger logger("prod", {console, all_file, err_file});
    logger.set_level(spdlog::level::info);
    spdlog::set_default_logger(std::make_shared<spdlog::logger>(logger));
}

9. 모범 사례 (Best Practices)

9.1 초기화 순서

  1. main 진입 직후 로깅 설정
  2. 디렉터리 생성 → 스레드 풀(비동기 시) → 싱크 생성 → 로거 생성 → 전역 등록
int main(int argc, char* argv[]) {
    setup_logging();  // 첫 번째
    // 그 다음 다른 초기화
    return run_app();
}

9.2 레벨 사용 가이드

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

9.3 로그 메시지 작성

// ❌ 나쁜 예: 맥락 부족
logger->error("Failed");

// ✅ 좋은 예: 누가, 무엇을, 왜
logger->error("User {} login failed: invalid password (attempt {})", userId, attemptCount);

9.4 비용이 큰 연산은 레벨 체크

로그 레벨이 꺼져 있어도 인자 평가는 먼저 일어납니다. 비용이 큰 연산은 람다나 매크로로 감싸세요.

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

// ✅ 좋은 예: 레벨 체크 후 호출
if (logger->should_log(spdlog::level::debug)) {
    logger->debug("State: {}", expensive_dump());
}

9.5 싱크·로거 생명주기

  • 싱크와 로거는 shared_ptr 로 관리
  • shutdown()main 종료 직전, 또는 atexit에 등록
  • 동적 로거 생성/삭제는 피하고, 시작 시 한 번만 구성

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 로깅·Assertion | 프로덕션 간헐적 크래시, 로그 없이 재현 불가일 때
  • C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]
  • C++ 오픈소스 기여: 유명 라이브러리 분석부터 첫 Pull Request까지 [#45-1]

이 글에서 다루는 키워드 (관련 검색어)

C++ spdlog, 로깅 라이브러리, 로그, 비동기 로깅, 로그 로테이션, 구조화 로깅 등으로 검색하시면 이 글이 도움이 됩니다.


정리

항목내용
레벨set_level, trace/debug/info/warn/error/critical
싱크stdout, file, rotating, daily, 여러 개 동시
포맷set_pattern, {} 플레이스홀더, 커스텀 포매터
비동기init_thread_pool, create_async, shutdown 필수
실수싱크 생명주기, _mt vs _st, shutdown 누락, 포맷 불일치
성능비동기가 동기 대비 수십 배 빠를 수 있음
프로덕션로테이션, JSON, flush_on, SPDLOG_LEVEL, 에러 파일 분리

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. spdlog로 로그 레벨 설정, 콘솔/파일 출력, 포맷 문자열, 멀티스레드 안전 사용, 비동기 로깅, 로테이션을 설정하는 방법을 다룹니다. 실무에서는 위 본문의 예제와 프로덕션 체크리스트를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. spdlog GitHubcppreference를 참고하세요. fmt 라이브러리 문서도 포맷 문자열 문법 이해에 도움이 됩니다.

한 줄 요약: spdlog로 빠른 로깅과 다중 싱크·비동기·로테이션을 설정할 수 있습니다. 다음으로 소켓 기초(#28-1)를 읽어보면 좋습니다.

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

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


관련 글

  • C++ Boost 라이브러리 | Asio·Filesystem·Regex·설치부터 프로덕션까지 완벽 가이드
  • C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]
  • C++26 리플렉션 기초 | ^^ 연산자·std::meta::info로 타입 정보 조회하기
  • C++ 컴파일 타임 프로그래밍 기법 | 런타임 오버헤드 제거와 constexpr·consteval 실전