Docker + Spring Boot: 외부 JAR를 컴파일·빌드·런타임에 나눠 넣는 방법

DevBadger·2025년 12월 28일

Docker

목록 보기
3/3

외부 JAR를 Spring Boot에서 사용한다는 것은, 어느 시점의 classpath에 그 JAR를 올릴지 선택하는 문제입니다. 시점마다 목적과 설계 포인트가 달라지며, 특히 Docker 환경에서는 “언제, 어디에 복사해서 classpath에 태울지”를 명확히 하는 것이 중요합니다. 아래에서는 libs/external-logging-1.0.0.jar를 예시로 시점별로 정리해 보겠습니다.


1. 컴파일 시점에 넣는 경우

컴파일 시점에는 소스 코드가 외부 JAR 안의 클래스와 메서드를 import 할 수 있어야 합니다. Gradle에서는 일반 라이브러리처럼 implementation files("libs/external-logging-1.0.0.jar") 형태로 의존성을 추가합니다.

  • 특징

    • 컴파일 에러 없이 해당 JAR의 타입·메서드를 직접 사용합니다.

    • Spring Boot Gradle/Maven 플러그인을 사용할 경우, 보통 패키징 시점에 fat JAR(BOOT-INF/lib) 안에 함께 포함됩니다.

    • 버전이 자주 바뀌지 않는 공통 라이브러리에 적합합니다.

      fat jar는 “내 코드랑 필요한 라이브러리를 전부 한 덩어리 JAR 하나에 다 넣어 둔 실행용 최종 JAR 형태”라고 보시면 됩니다.

  • 예시 (build.gradle)

    dependencies {
        implementation files('libs/external-logging-1.0.0.jar')
    }
  • Docker 예시

    # 1단계: 빌드 (컴파일+패키징)
    FROM gradle:8-jdk AS builder
    WORKDIR /workspace
    COPY . .
    RUN gradle clean bootJar
    
    # 2단계: 런타임
    FROM eclipse-temurin:17-jre
    WORKDIR /app
    
    # 빌드 결과물만 복사 (외부 JAR는 fat JAR 안에 이미 포함)
    COPY --from=builder /workspace/build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar
    
    ENTRYPOINT ["java","-jar","/app/app.jar"]

이 패턴에서는 외부 JAR가 개발 환경과 빌드 컨테이너의 파일 시스템에 존재하고, 최종 app.jar 내부 BOOT-INF/lib에도 포함된다는 점이 핵심입니다.


2. 빌드(패키징) 시점에 넣는 경우

여기서 빌드 시점은 gradle build 또는 mvn package로 실행 가능한 하나의 JAR를 만드는 패키징 단계입니다. 이때 “어떤 JAR를 실행 JAR 내부에 넣을지” 혹은 “외부 폴더로 뺄지”를 결정합니다.

2-1. 실행 JAR 내부에 포함하는 경우

컴파일 의존성에 올려둔 외부 JAR는 Spring Boot 플러그인이 기본 설정이라면 자동으로 BOOT-INF/lib에 패키징합니다.

  • 장점
    • java -jar app.jar 한 줄로 실행이 가능합니다.
    • Docker 이미지에도 app.jar만 복사하면 됩니다.
  • 디렉터리 예시
    /opt/projects/my-app/
      build.gradle
      src/...
      libs/
        external-logging-1.0.0.jar
    /opt/projects/my-app-docker/
      Dockerfile
  • Dockerfile
    FROM eclipse-temurin:17-jre
    WORKDIR /app
    
    COPY build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar
    
    ENTRYPOINT ["java","-jar","/app/app.jar"]

2-2. 실행 JAR 밖에 두고 유지하는 경우

실행 JAR에는 기본 의존성만 담고, 일부 JAR는 파일 시스템 상의 별도 디렉터리에 두고 loader.path 등으로 런타임 classpath에 추가하는 패턴입니다.

  • 특징
    • 컴파일 시점에는 외부 JAR를 의존성으로 포함해서 타입을 직접 import합니다.
    • 패키징 단계에서는 해당 JAR를 BOOT-INF/lib에서 제외하고, 별도 디렉터리(/app/ext-libs)에 복사합니다.
    • 최종 실행 시점에 이 디렉터리를 loader.path로 지정해 classpath에 올립니다.
  • 결과 구조 예시
    /app/app.jar
    /app/ext-libs/external-logging-1.0.0.jar
  • Dockerfile 예시
    FROM eclipse-temurin:17-jre
    WORKDIR /app
    
    # 1) 앱 JAR 복사
    COPY build/libs/my-app-0.0.1-SNAPSHOT.jar app.jar
    
    # 2) 외부 JAR를 이미지 안의 별도 디렉터리에 복사
    COPY libs/external-logging-1.0.0.jar ext-libs/external-logging-1.0.0.jar
    
    # 3) 외부 JAR 디렉터리를 loader.path 로 추가
    ENTRYPOINT ["java",
        "-Dloader.path=ext-libs/",
        "-Dloader.main=com.example.MyApplication",
        "-cp","app.jar",
        "org.springframework.boot.loader.PropertiesLauncher"]

이 방식의 핵심은 “외부 JAR 교체를 앱 재빌드 없이 하고 싶다”는 요구를 만족시키는 것입니다.


3. 런타임 시점에 넣는 경우

런타임 시점은 이미 만들어진 Spring Boot 실행 JAR를 띄울 때, 프로세스 실행 명령에서 외부 JAR를 classpath에 추가하는 단계를 의미합니다.

  • 대표 패턴
    • Spring Boot PropertiesLauncher + loader.path 사용.
    • 혹은 애플리케이션 코드에서 URLClassLoader 등을 이용해 특정 디렉터리의 JAR를 동적으로 로딩하는 플러그인 구조.
  • 특징
    • 외부 JAR를 교체한 뒤 애플리케이션을 재기동하거나, 잘 설계하면 플러그인 형태로 동적 로딩도 가능합니다.
    • 컴파일 타임에 타입을 모르는 경우가 많기 때문에, 인터페이스/리플렉션 기반 설계가 필요합니다.
    • 설계가 복잡해지면 classloader 문제와 유지보수 난이도가 크게 올라갑니다.
  • 디렉터리 예시
    /app/app.jar
    /app/ext-libs/external-logging-1.0.0.jar
  • 실행 명령 예시 (PropertiesLauncher 사용)
    java \
      -cp app.jar \
      -Dloader.path=ext-libs/ \
      -Dloader.main=com.example.MyApplication \
      org.springframework.boot.loader.PropertiesLauncher

또는 단순히 JVM classpath를 이용해 외부 JAR를 포함하는 형태도 가능합니다.

java -classpath "app.jar:ext-libs/*" com.example.MyApplication

4. 시점별 비교 정리

시점classpath 기준 위치 예시코드에서 import 가능한가?app.jar 안에 포함 여부주 사용 목적
컴파일 시점Gradle/Maven 의존성(implementation, api)가능 (O)보통 포함되도록 설정되는 경우가 많음일반 라이브러리 의존, 강한 타입 의존
빌드 시점BOOT-INF/lib에 넣을지 vs 외부 디렉터리로 뺄지새로 결정 X (컴파일 때 이미 결정)플러그인 설정·Docker COPY로 포함/미포함 제어fat JAR 구성, app.jar + ext-libs 구조
런타임 시점loader.path(/ext-libs 등), 커스텀 ClassLoader직접 import는 보통 X보통 실행 JAR 밖에 두고 필요 시 로딩플러그인, 고객별 모듈, 교체 가능한 확장

import 가능 여부는 오직 “컴파일 시점에 그 JAR가 의존성으로 classpath에 올라가 있었는가”에 의해 결정됩니다.


5. 어떤 패턴을 선택할지 기준

Spring Boot 프로젝트에 외부 JAR를 “넣어야 하는” 상황에서는 다음 기준으로 선택하는 것이 좋습니다.

  • 코드가 그 JAR에 강하게 의존하고, 자주 바뀌지 않는 경우
    • 컴파일·빌드 시점에 일반 의존성으로 포함하고, fat JAR에 함께 패키징하는 것이 관리에 유리합니다.
  • 고객/환경마다 다른 구현을 꽂아야 하거나, 자주 교체되는 경우
    • 빌드 결과에는 최소한의 훅만 두고, 런타임에 loader.path 또는 플러그인 구조로 외부 JAR를 올리는 패턴이 더 적합합니다.

Docker에서는 “이미지 빌드 시점(복사 위치)”과 “컨테이너 실행 시점(java 옵션)”을 구분해 생각하면, 각 시점별 설계가 훨씬 명확해집니다.

profile
개발 오소리

0개의 댓글