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은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.
목차
- 설치와 완전한 설정
- 로거와 레벨
- 다중 싱크 (콘솔/파일/로테이션)
- 커스텀 포맷터와 패턴
- 비동기 로깅 구현
- 자주 하는 실수 (싱크 생명주기, 스레드 안전성)
- 성능 비교 (동기 vs 비동기)
- 프로덕션 패턴 (구조화 로깅, 로테이션)
- 모범 사례 (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 초기화 순서
- main 진입 직후 로깅 설정
- 디렉터리 생성 → 스레드 풀(비동기 시) → 싱크 생성 → 로거 생성 → 전역 등록
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 GitHub와 cppreference를 참고하세요. 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 실전