blog

종속성 라이브러리 버전 충돌 기억하기: 서로 다른 Spdlog 간의 호환성 문제

이 문서에서는 두 개의 종속 라이브러리가 헤더 전용 타사 라이브러리인 spdlog에 의존하지만 버전이 다른 경우 심볼 충돌 문제를 해결하는 방법에 대해 설명합니다....

Oct 24, 2025 · 9 min. read
シェア

배경

spdlog는 강력하고 사용하기 쉬우며 확장 가능한 C++ 로깅 라이브러리로, 헤더 전용 방식으로 프로젝트에 도입하거나 미리 컴파일하여 정적 라이브러리 또는 동적 라이브러리로 변환할 수 있습니다. 여러 버전의 spdlog를 사용할 수 있습니다: , Project:XFT는 현재 XFTLogger 구현의 기초로 spdlog를 도입하고 있지만 상위 클라이언트 프로젝트에서도 spdlog를 로깅 라이브러리로 사용하는 경우.

이로 인해 XFT에서 사용하는 spdlog 버전이 고객 서비스 코드에서 사용하는 spdlog와 동일하지 않아 충돌 또는 비호환성 문제가 발생할 수 있는 상황이 발생할 수 있습니다.

이는 종속성이 있는 두 개의 라이브러리 A와 B가 동시에 다른 종속 라이브러리 C를 도입하고 종속 라이브러리 C의 버전이 다른 경우와 같습니다.

어떤 경우에 충돌 문제가 발생하는지, 버전 불일치로 인한 충돌을 피하려면 어떤 방식으로 spdlog를 도입해야 하는지 알아보기 위해 다양한 사례로 다음과 같은 실험을 수행했습니다.

실험적

spdlog비교 버전

Vivo의 FTServing 서비스는 spdlog-1.8.5를 사용하며, 실험적 테스트를 위해 1.9.2 및 1.12.0 버전이 선택되었습니다.

  • spdlog-1.8.5 대 1.9.2: 일부 인터페이스의 구현 코드가 약간 변경된 것을 제외하고는 인터페이스 등 주요 인터페이스가 동일합니다.
  • spdlog-1.8.5 대 1.12.0: 다음과 같은 몇 가지 일반적인 인터페이스가 변경되었습니다.
// spdlog-1.8.5 basic_file_sink라이브러리의 생성자
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
 explicit basic_file_sink(const filename_t &filename, bool truncate = false);
// ....... 
}
// spdlog-1.12.0 basic_file_sink라이브러리의 생성자
template<typename Mutex>
class basic_file_sink final : public base_sink<Mutex>
{
public:
 explicit basic_file_sink(const filename_t &filename, bool truncate = false, const file_event_handlers &event_handlers = {});
// ........
}
프로젝트 구조

  • 헤더 전용 접근 방식을 사용하여 spdlog-1.8.5에 도입된 xft_logger 폴더의 코드 파일은 다음과 같이 컴파일됩니다.
  • main.cpp는 종속성 라이브러리로 사용되는 spdlog-1.12.0 또는 1.9.2를 사용하여 실행 파일로 컴파일됩니다.

xft_logger/log.h 내용:

#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <iosfwd>
#include <string.h>
#include <vector>
#include <array>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/stdout_sinks.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include <toml.hpp>
class xftLogger {
public:
 // 생성자
 xftLogger();
 // 소멸자, spdlog는 로거의 수명 주기를 관리합니다..
 ~xftLogger() = default;
 static xftLogger& instance() {
 static xftLogger logger;
 return logger;
 }
 
 std::shared_ptr<spdlog::logger> const Get() const noexcept {
 return logger_;//.get();
 }
 // void no_destroy();
private:
 // spdlog 포인팅::logger라이브러리에 대한 스마트 포인터
 std::shared_ptr<spdlog::logger> logger_;
};
static xftLogger& logger = xftLogger::instance();
#define XFTLOG_FUNCTION static_cast<const char *>(__FUNCTION__)
#define XFTLOG_ERROR(...) logger.Get()->log(spdlog::source_loc{__FILE__, __LINE__, XFTLOG_FUNCTION}, spdlog::level::err, __VA_ARGS__)

xft_logger/log.cpp 내용:

#include "log.h"
xftLogger::xftLogger() {
 std::cout << "xftLogger::default Constructor" << std::endl;
 // spdlog::warn("XFT_LOG_CONF::{} is not set or file does not exist, Use default config instead.",conf_file_path);
 logger_ = std::make_shared<spdlog::logger>("TestXftLogger_01");
 auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 sink_1->set_level(spdlog::level::trace);
 sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%n%$] [%l] %v");
 logger_->sinks().push_back(sink_1);
 // basic_file_sink 
 auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/xft2.log");
 sink_2->set_level(spdlog::level::debug);
 sink_2->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%-6l] %v");
 logger_->sinks().push_back(sink_2);
 logger_->set_level(spdlog::level::trace);
 // logger_ = spdlog::basic_logger_mt("default_xft_logger", "logs/xft.log");
 std::cout << "logger_->name() is " << logger_->name() << std::endl;
}

main.cpp 내용:

#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <vector>
#include <array>
#include <set>
#include <algorithm>
#include "log.h"
#include "spdlog/spdlog.h"
#include "spdlog/details/file_helper.h"
int main() {
 spdlog::info("Welcome to spdlog!");
 auto logger00 = spdlog::logger("logger-00");
 // spdlog::file_event_handlers handlers;
 // handlers.before_open = [](spdlog::filename_t filename) { spdlog::info("Before opening {}", filename); };
 // handlers.after_open = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("After opening
", fstream); };
 // handlers.before_close = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("Before closing
", fstream); };
 // handlers.after_close = [](spdlog::filename_t filename) { spdlog::info("After closing {}", filename); };
 auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/basic-log.txt", true); //, handlers);
 sink_1->set_level(spdlog::level::info);
 sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
 logger00.sinks().push_back(sink_1);
 logger00.info("Hello, World!@@@@");
 XFTLOG_ERROR("XFTLOG_ERROR");
 return 0;
}

CMakelists.txt

cmake_minimum_required(VERSION 3.10)
set(CMAKE_C_COMPILER "/opt/compiler/gcc-8.2/bin/gcc")
set(CMAKE_CXX_COMPILER "/opt/compiler/gcc-8.2/bin/g++")
project(demoo)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE Debug)
set(XFTLoggerDir ${CMAKE_CURRENT_SOURCE_DIR}/xft_logger)
message(STATUS "===>> XFT Logger dir is ${XFTLoggerDir}")
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/toml11/)
set(xft_logger_src ${XFTLoggerDir}/log.cpp ${XFTLoggerDir}/log.h)
add_library(xftlogger SHARED ${xft_logger_src})
## target_compile_definitions(xftlogger PRIVATE SPDLOG_COMPILED_LIB)
###%%% 헤더 경로를 포함할 때 속성을 비공개 또는 공개로 설정하면 대상에서 도입한 헤더 검색 경로를 라이브러리 호출자에게 전달할지 여부가 표시됩니다.
target_include_directories(xftlogger PRIVATE
 ${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)
 
# target_link_libraries(xftlogger PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/build/libspdlog.a)
# target_link_libraries(xftlogger INTERFACE /docker_workspace/personal/3rd_party/spdlog-1.12.0/build/libspdlog.a)
find_package(Threads)
### 전 세계적으로 도입된 헤더 파일의 검색 경로
# include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)
# include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.9.2/include)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.12.0/include/)
set(DEMO_SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
add_executable(demoo ${DEMO_SRC_LIST})
target_include_directories(demoo PRIVATE ${XFTLoggerDir})
target_link_libraries (${PROJECT_NAME} PRIVATE pthread xftlogger)
실험 및 결과

xftlogger.so는 1.8.5-spdlog를 사용하여 항상 동일하게 유지되며, main.cpp는 다른 버전의 spdlog를 사용합니다;

1.8.5와 1.12.0 사이에 변경된 인터페이스는 다음과 같습니다.

2.3.1 실험 1: spdlog 위치 및 버전 호출의 영향
log.h1.9.2
log.h1.12.0
log.cpp1.9.2
log.cpp1.12.0
  • spdlog-1.12.0이 main.cpp에 도입된 경우, log.h 또는 log.cpp의 xftLogger 생성자가 다음과 같이 안정적으로 제대로 실행되지 않고 ::: 여러 번 다시 컴파일하고 실행하면 일부는 정상 실행이 완료되고 일부는 코어 덤프가 되는 현상이 발생합니다.

가능한 원인 분석 두 개의 서로 다른 버전의 spdlog 라이브러리가 동일한 인터페이스를 정의하지만 구현이 다른 경우 연결 중에 심볼 충돌이 발생할 수 있습니다. 컴파일러가 어떤 버전의 구현을 선택할지 불확실한 경우 가끔 코어 덤프가 발생합니다.

  • spdlog-1.9.2를 main.cpp에 도입했을 때 여러 번 다시 컴파일하고 실행했지만 안정적이고 정상적으로 실행될 수 있었습니다.
2.3.2 실험 2: CMake의 targer_xxx_yyyy에서 PUBLIC / PRIVATE 사용의 영향.
target_include_directories(xftlogger PRIVATE
 ${CMAKE_CURRENT_SOURCE_DIR}/3rd_party/spdlog-1.8.5/include/)

spdlog-1.12.0의 새로 수정된 인터페이스를 호출하도록 main.cpp를 수정합니다:

#include "log.h"
#include "spdlog/spdlog.h"
#include "spdlog/details/file_helper.h"
int main() {
 spdlog::info("Welcome to spdlog!");
 auto logger00 = spdlog::logger("logger-00");
 // 1.12.0이번 버전에 새로 추가된 기능, 1.8.5 .9.2없음.
 spdlog::file_event_handlers handlers;
 handlers.before_open = [](spdlog::filename_t filename) { spdlog::info("Before opening {}", filename); };
 handlers.after_open = [](spdlog::filename_t filename, std::FILE *fstream) { fputs("After opening
", fstream); };
 auto sink_1 = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 // 1.12.0-spdlog기본_file_sink_mt이 인터페이스는 다음과 같은 3가지 매개변수만 허용합니다.,1.8.5 .9.2단 2개의 매개변수
 auto sink_2 = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/basic-log.txt", true, handlers);
 
 sink_1->set_level(spdlog::level::info);
 sink_1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
 logger00.sinks().push_back(sink_1);
 logger00.info("Hello, World!@@@@");
 XFTLOG_ERROR("XFTLOG_ERROR");
 return 0;
}

다음 조합을 테스트해 보세요:

PUBLIC1.8.5 / 1.9.21.8.5 / 1.9.2
비공개1.12.02.3.1에서와 같이 이름이 같고 정의가 다른 인터페이스의 구현이 여러 개 있으며 링커 또는 실행 파일은 어느 것이 실행될지 알 수 없습니다.
공개1.8.5 / 1.9.2 /1.8.5 / 1.9.2
공개1.12.02.3.1에서와 같이 이름이 같고 정의가 다른 인터페이스의 구현이 여러 개 있으며 링커 또는 실행 파일은 어느 것이 실행될지 알 수 없습니다.

표의 네 번째 사례는 두 번째 사례와 비교하여 include_directories(spdlog-1.12.0)를 통해 전역적으로 spdlog-1.12.0의 헤더 경로를 포함하지만, xftlogger.so 컴파일 시 사용되는 대상 속성이 PUBLIC 및 PRIVATE라는 점이 유일한 차이점입니다. 유일한 차이점은 xftlogger.so를 컴파일할 때 사용되는 대상 속성이 PUBLIC 및 PRIVATE라는 점입니다.

이 두 가지 속성으로 생성된 flags.make 파일을 데모용 CMAKE와 비교한 결과, 유일한 차이점은 PUBLIC이 검색 경로에 xftlogger.so도 추가하지만 검색을 위해 spdlog-1.12.0 뒤에 넣기 때문에 실제로는 영향을 주지 않는다는 점입니다.

솔루션

헤더 전용 라이브러리는 모든 구현이 헤더 파일에 포함되어 있다는 특징이 있는데, 이는 헤더 파일이 포함될 때마다 관련 코드가 컴파일 유닛에 포함된다는 의미입니다.

가능한 해결책은 다음과 같습니다:

  • 버전 통합: XFT가 고객의 프레임워크 또는 서비스 코드와 동일한 버전의 spdlog 라이브러리를 사용하는지 확인합니다. 그러나 XFT는 상대적으로 낮은 수준의 산술 라이브러리이므로 상위 수준 클라이언트마다 매우 다른 spdlog를 사용할 수 있으므로 버전 통합을 달성하기가 어렵습니다.
  • 강제 인라인: spdlog 라이브러리의 대부분의 인터페이스 앞에는 매크로 SPDLOG_INLINE이 추가되며(GCC 컴파일러에서는 인라인으로 확장됨), 인라인에는 두 가지 효과가 있습니다:

1) 명령어 또는 코드 대체. 하지만 최신 C++의 경우 인라인은 컴파일러에 대한 제안일 뿐이며, 컴파일러는 자체적인 아이디어를 가지고 있습니다.

(2) 여러 번역 단위에서 동일한 이름과 동일한 매개 변수를 가진 인라인 함수를 정의할 수 있습니다. 키워드가 인라인인 함수 선언 또는 정의의 경우 링커는 연결 단계에서 여러 정의가 있는 인라인 함수를 하나만 취하므로 이름이 같고 매개 변수가 같은 인라인 함수는 구현이 다른 경우 정의되지 않은 동작을 유발합니다.

강제 인라이닝은 강제 인라이닝하려는 인터페이스 선언의 정의 앞에 컴파일 옵션 또는 GCC 컴파일러 수정자를 추가하여 명령어 또는 코드 치환을 강제함으로써 심볼 충돌을 방지합니다.

그러나 이 접근 방식은 소스 코드를 수정해야 하며 인라이닝에 적합하지 않은 심볼 충돌이 있는 일부 구현도 인라이닝될 수 있습니다.

  • 네임스페이스: spdlog 소스 코드에 네임스페이스를 중첩하거나 수정할 수 있지만 도입된 오픈 소스 타사 라이브러리의 소스 코드를 변경해야 하므로 종속성 관리에 도움이 되지 않습니다.
  • 핌플 규칙: "구현 포인터" 규칙은 라이브러리의 구현 세부 정보를 숨기는 데 사용되므로 헤더 파일 종속성과 잠재적인 바이너리 비호환성을 줄일 수 있습니다.
Read next

멀티스레드 작업 관리: CompletionService 애플리케이션에 대한 심층 연구

여러 비동기 작업을 처리하는 데 큰 강점을 보입니다. 완료된 작업을 저장하기 위해 내부 차단 대기열을 유지함으로써 완료된 작업의 결과를 쉽게 얻을 수 있어 완료된 순서대로 결과를 처리해야 할 때 특히 유용합니다. 뿐만 아니라

Oct 24, 2025 · 11 min read