JNI 적응기 (2) - so, CMake, ONNX

게으른 개발자·2024년 12월 21일
post-thumbnail

현재 회사에서 JNI와 더불어 회사의 주요 제품인 솔루션의 so 라이브러리에 추가 기능을 개발 요청이 들어왔다. 개발을 하다보니 이전에 알지 못했던 것들을 사용해야만 했는데 이것들이 무엇인지 이번에 정리해보고 왜 이것들을 이렇게 사용하는지 알아보고자 한다.

so(Shared Object) 란?

  • 리눅스 시스템에서 사용되는 공유 라이브러리(Shared library)로, 런타임에 로드되어 여러 프로그램에서 함께 사용될 수 있는 라이브러리이다.

특징

  • 동적 라이브러리
    • .so 파일은 컴파일 시점이 아니라 런타임에 로드된다. 이는 메모리 사용량 절감과 프로그램 크기 감소에 유리하다.
  • 공유
    • 여러 프로그램이 동일한 .so 파일을 사용할 수 있어 코드 재사용성을 높인다.
  • 의존성 확인 방법으론 ldd {so 파일명}.so 을 입력하면 확인이 가능하다. (아래 이미지 참고)

장점

  1. 업데이트 용이성
    • 라이브러리만 업데이트하면 모든 종속 프로그램이 즉시 반영
  2. 메모리 효율성
    • 메모리에 하나의 복사본만 로드하여 여러 프로그램에서 공유
  3. 재사용성
    • 여러 프로그램에서 같은 라이브러리를 사용할 수 있음

단점

  1. 의존성 문제
    • 프로그램 실행 시 필요한 .so 파일이 없으면 라이브러리 로딩 오류가 발생, 이를 DLL 지옥이라고도 부른다.
    • 진짜 지옥 그 자체이다....
  2. 버전 관리
    • 서로 다른 프로그램이 동일한 이름의 .so 파일을 필요로 하지만, 서로 다른 버전을 요구하면 충돌이 발생할 수 있다.

동적 라이브러리 vs 정적 라이브러리

  • 동적 라이브러리
    • 프로그램 실행 시점에 라이브러리가 메모리에 로드되어 동작
    • Dynamic Linker가 런타임에 필요한 라이브러리를 로드하고 심볼(함수, 변수 등)을 연결
  • 정적 라이브러리
    • 프로그램 컴파일 시점에 라이브러리 코드가 실행 파일에 포함
    • 라이브러리가 실행 파일의 일부가 되어 독립적으로 실행
  • 동적 라이브러리 vs 정적 라이브러리
특성동적 라이브러리정적 라이브러리
파일 확장자.so.a
링크 시점실행 시점(런타임)컴파일 시점
파일 크기 작음 
메모리 사용량 공유(효율적) 중복 포함(비효율적)
실행속도느림 (동적 로드 필요) 빠름
유지보수용이 (라이브러리만 교체) 어려움(재컴파일 필요)
의존성 문제 있음(.so 파일 필요) 없음
재사용성높음낮음

CMake란?

  • 크로스 플랫폼 빌드 시스템을 생성하는 오픈소스 도구이다. 다양한 운영체제와 컴파일러 환경에서 소프트웨어를 빌드, 테스트, 패키징할 수 있도록 빌드 설정을 관리한다.

특징

  1. 크로스 플랫폼 지원
    • Windows, macOS, Linux를 포함한 여러 플랫폼을 지원
    • Visual Studio 프로젝트 파일, Unix Makefile, Ninja 빌드 시스템 등 다양한 빌드 시스템을 생성할 수 있다.
  2. 컴파일러 독립적
    • GCC, Clang, MSVC 등 다양한 컴파일러를 지원한다.
  3. 간결한 설정
    • 프로젝트 빌드를 위한 규칙과 의존성을 간단한 설정 파일(CMakeLists.txt)에 기술합니다.
  4. 동적 라이브러리와 정적 라이브러리 모두 지원
    • .so, .dll, .dylib 같은 동적 라이브러리와 .a(Linux), .lib(Windows) 같은 정적 라이브러리를 빌드할 수 있다.
  5. 외부 라이브러리 관리
    • CMake는 외부 라이브러리를 검색하고 링크하는 기능을 제공(find_package, includedirectiries 등)
  6. 구성 및 빌드 분리
    • 소스 디렉토리와 빌드 디렉토리를 분리하여 소스 파일을 깔끔하게 유지 가능

CMake 명령어

  • 이번 업무에 활용한 Cmake 명령어들 위주로 알아보자

1. CMake 버전 설정

cmake_minimum_required(VERSION 3.0)
  • 최소 CMake 버전 지정

2. 프로젝트 및 언어 설정

project(HelloProject {LANGUAGES C | CXX | NONE})
  • 프로젝트 이름과 언어를 설정
  • 언어는 선택이 가능하며, 필수 값은 아니다.
    • C: C, CXX: C++, NONE: 언어를 사용하지 않음

3. 소스 파일 포함

add_library(hello_library SHARED library.cpp)
  • 동적 라이브러리 생성
  • SHARED 대신에 STATIC 키워드를 사용하면 정적 라이브러리를 생성

4. 외부 라이브러리 사용

find_package(JNI REQUIRED)
  • CMake에서 프로그램 빌드를 위해 JNI 헤더와 라이브러리를 자동으로 찾는 명령어
find_library({VAR} 
	{NAMES <name1> [<name2> ...]}
	{PATHS <path1> [<path2> ...]}
	{NO_DEFAULT_PATH}
	{REQUIRED}
)
  • find_libraryCMake에서 라이브러리를 찾는 명령어이다.
  • VAR
    • 찾은 라이브러리의 경로를 저장할 변수 이름
  • NAMES
    • 찾으려는 라이브러리 이름을 나열
  • PATHS
    • 라이브러리를 검색할 사용자 지정 경로를 지정
  • NO_DEFAULT_PATH
    • CMake의 기본 검색 경로(시스템 디렉토리 등)를 무시하고 PATHS에 지정된 경로에서만 라이브러리를 검색하도록 설정
  • REQUIRED
    • VAR의 값이 필수록 입력이 되어야하는 여부. Required 설정이 되었을 경우 찾고자 하는 라이브러리를 못 찾으면 에러를 발생시킨다.
target_include_directories({target_name} 
	{visibility} 
	{directory_path}
)
  • target_name
    • 헤더 디렉토리를 추가할 CMake 타겟의 이름
  • visibility
    • 헤더 파일의 가시성을 설정
    • PUBLIC: 해당 타겟과 해당 타겟을 의존하는 모든 타겟에서 이 헤더 디렉토리를 사용할 수 있다.
    • PRIVATE: 해당 타겟에서만 헤더 디렉토리를 사용할 수 있다.
    • INTERFACE: 이 타겟을 의존하는 타겟에서만 헤더 디렉토리를 사용할 수 있다.
  • directory_path
    • 포함할 헤더 파일 디렉토리 경로
    • 여러 경로를 공백으로 구분해 나열할 수 있다.
target_link_libraries({target_name}
	{visibility}
    [LIBRARY_NAMES <library_name1> <library_name2> ...]
)
  • 특정 타겟에 의존 라이브러리를 연결하는 과정
  • target_name
    • 타겟 이름으로, 해당 라이브러리가 링크되는 대상
  • visibility
    • 링크의 가시성 범위를 설정
    • PUBLIC
      • 현재 타겟뿐만 아니라, 이를 사용하는 다른 의존 타겟도 나열된 라이브러리와 링크
      • PRIVATE: 현재 타겟에서만 사용하고, 의존 타겟에는 전달하지 않는다.
      • INTERFACE: 현재 타겟에서는 사용되지 않고, 의존 타겟에서만 사용

5.컴파일 옵션 설정

target_compile_definitions({target_name}
	{visibility}
	{macro_definitions}
)
  • 특정 타겟에 대해 컴파일러 매크로를 설정한다.
  • target_name
    • 해당 명령어가 적용될 빌드 타켓의 이름
  • visibility
    • 컴파일러 매크로 정의의 가시성 범위
    • PUBLIC
      • 이 매크로 정의는 타겟 자신과, 이 타겟을 사용하는 다른 의존 타겟에도 전달
    • PRIVATE
      • 타겟 자신에게만 매크로 정의를 전달, 의존 타겟에는 전달하지 않는다.
    • INTERFACE
      • 현재 타겟에 적용되지 않고, 의존 타겟에게만 전달
  • macro_definitions
    • 정의할 매크로 목록

6.조건부 설정

if(ENABLE_GPU)
	 message("ENABLE GPU")
else()
	message("DISABLE GPU")
endif()

ONNX란?

  • 서로 다른 딥러닝 프레임워크 간의 호환서응ㄹ 제공하는 오픈 포맷이다.
  • 기본적으로 Protobuf 형식으로 저장된다.

특징

1. 호환성

  • PyTorch, TensorFLow, Keras 등에서 학습한 모델을 ONNX 형식으로 변환해 다른 프레임워크나 런타임에서 사용 가능
  • 예를 들어, Python에서 PyTorch로 학습한 모델을 ONNX로 변환해 C++ 또는 Java 기반 시스템에서 사용할 수 있다.
    2. 중립적 포맷
  • 모델의 연산 그래프 및 가중치를 정의하는 중립적인 표현을 제공하여 프레임워크에 구애받지 않는다.
    3. 효율적인 실행
  • ONNX 모델을 최적화된 ONNX Runtime에서 실행되며, CPU, GPU, FPGA와 같은 다양한 하드웨어를 지원.
    4. 모델 최적화 도구
  • ONNX 모델을 최적화하여 더 빠른 추론 속도와 낮은 메모리 사용량을 제공

PMX란?

  • PyTorch 모델을 TorchServe에서 관리하고 배포하기 위해 사용하는 모델 포맷
  • TorchServe에서 모델의 메타데이터와 가중치를 패키징하여 효율적으로 배포 및 추론할 수 있도록 설계

PMX vs ONNX

특징PMXONNX
주요 사용 사례TorchServe 기반 모델 배포다양한 환경 및 프레임워크 간의 호환성
지원 포맷PyTorch 전용PyTorch, TensorFlow, Keras 등 다수
포맷 구조 PyTorch 모델 상태 및 메타데이터 포함 중립적인 연산 그래프 표현(Protobuf 기반)
추론 환경 TorchServe ONNX Runtime 및 기타 지원 프레임워크
사용성PyTorch에 최적화 프레임워크 독립적, 하드웨어 최적화 가능

정리

이번에 나는 왜 우리 회사는 솔루션을 이렇게 구성해서 사용할까? 에 대한 질문을 계속 생각하면서 정리를 해왔다. 물론 선임분들이나 동료분들께 질문을 드리면 쉽게 풀리겠지만 그전에 내 생각을 먼저 정리해서 말씀드리는게 맞을거 같아 아직 따로 여쭤보지는 않았다. 이번에 정리해보면서 느낀거는 아직 내가 Java를 잘 몰라서 그런건지는 몰라도 이미지를 처리를 하는 관점에서보면 Java보단 C++이 나은거 같아 JNI를 활용해 Spring Boot 서버에서 요청을 받고 해당 요청에 대한 이미지들을 C++에서 전처리하여 딥러닝 모델로 전달하여 결과를 받고 해당 결과에 대한 데이터들 정도만 Spring Boot에서 후처리 하는 방식으로 솔루션을 구성한게 아닐까 생각을 하고 있지만, 아직 이건 내가 Java를 잘 모르는 거일수도 있기 때문에 기회가 생긴다면 솔루션 so library 개발 담당자분에게 위 질문에 대한 답변을 듣고 Java에서도 구현이 가능한지 테스트를 해봐야겠다.

추가적으로 현재 회사에서 Java만 사용하여 모든 업무를 진행해왔는데 이번에 뜻하지 않은 기회가 생겨 Java 이외에 다른 영역에 살짝(?)걸칠 수 있게되었다. 처음에는 이게 도움이 될까 싶었지만 정리를 하면서 업무를 진행하다보니 궁금증이 폭발해서 회사 노트북을 집까지 가져와서 추가적인 공부를 했다. 업무를 마무리하는 예상 기간보다는 많이 늦어졌지만.... 결국에는 잘 마무리하게 되어 매우 보람찼다!!

항상 감사합니다 GPT님!!
profile
6년차 백엔드 엔지니어로 일하고 있습니다~!

0개의 댓글