본문으로 건너뛰기
Previous
Next
C++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]

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은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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++ 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++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 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 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.