스프링부트 jar 구성요소 & 동작과정

송어·2024년 3월 6일
2

The spring-boot-loader modules lets Spring Boot support executable jar and war files. If you use the Maven plugin or the Gradle plugin, executable jars are automatically generated, and you generally do not need to know the details of how they work.

Spring boot에서 Maven이나 Gradle 플러그인을 사용해 프로젝트를 빌드하면 자동으로 jar파일이 생성되고, java -jar xxx.jar명령어를 사용하면 스프링부트 프로젝트가 실행된다.
공식문서에는 일반적으로 작동 방식에 대한 세부 정보를 알 필요가 없어도 사용할 수 있도록 편리한 기능을 자체적으로 지원하고 있다.

오늘은 빌드된 스프링부트 프로젝트의 jar 파일 에 대해 알아보자

jar의 구성 요소

이전에 다루었던 여러 프로젝트 중 하나를 빌드해 jar 파일을 생성 후 Dcompiler를 사용해 해당 파일의 내부를 살펴보았다.

xxx.jar
ㄴBOOT-INF
ㄴMETA-INF
ㄴorg

일반적인 스프링부트 jar 파일의 내부 구조이다.

BOOT-INF

BOOT-INF 폴더의 내부 구조이다. BOOT-INF 폴더는 애플리케이션의 자바 코드를 컴파일 한 .class파일과 여러 리소스 파일을 가지고 있다. jar 파일이 실행되면 해당 폴더에 있는 파일들을 읽도록 지정되어 있다.

clesses

clesses 폴더는 직접 작성한 자바 코드를 컴파일 한 바이트 코드(.cless)를 가지고 있는 영역이다.

libs

libs 폴더에는 코드 실행에 필요한 외부 라이브러리들이 jar형태로 존재한다. 스프링부트는 중첩된 Jar 파일(Jar파일 안의 Jar파일을 말함)을 로드하는 데 사용되는 클래스인 org.springframework.boot.loader.jar.NestedJarFile를 사용해 외부 라이브러리에 해당하는 jar 파일을 읽어온다.

classpath.idx

- "BOOT-INF/lib/mysql-connector-j-8.3.0.jar" // 제일 먼저 클래스로더에 로드됨
- "BOOT-INF/lib/hibernate-core-6.4.1.Final.jar"
- "BOOT-INF/lib/spring-data-jpa-3.2.2.jar"
- "BOOT-INF/lib/spring-aspects-6.1.3.jar"
- "BOOT-INF/lib/jmustache-1.15.jar"
- "BOOT-INF/lib/spring-security-config-6.2.1.jar"
- "BOOT-INF/lib/spring-security-web-6.2.1.jar"
- "BOOT-INF/lib/spring-webmvc-6.1.3.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-3.2.2.jar"
- "BOOT-INF/lib/spring-boot-3.2.2.jar"
- "BOOT-INF/lib/spring-security-core-6.2.1.jar"
- "BOOT-INF/lib/spring-context-6.1.3.jar"
- "BOOT-INF/lib/spring-aop-6.1.3.jar"
- "BOOT-INF/lib/spring-web-6.1.3.jar"
- "BOOT-INF/lib/aspectjweaver-1.9.21.jar"
- "BOOT-INF/lib/HikariCP-5.0.1.jar"

classpath.idx파일은 lib에 있는 외부 라이브러리 및 의존성의 경로를 명시하고 있다.
해당 경로의 정렬 순서는 해당 Jar 파일이 클래스로더에 의해 로드될 때의 순서를 나타낸다고 한다.

layers.idx

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

layers.idx 파일은 도커에서 jar 이미지를 들 때 쓰는 정보이다. 이 파일은 OCI (Open Container Initiative) 이미지의 레이어를 설명하는 파일이다.

OCI(Open Container Initiative)는 컨테이너 관련 표준을 개발하는 단체로서 OCI 이미지란 컨테이너 개발 관련 표준 스을 준수했다는 의미로 생각하자

org.springframework.boot.loader

해당 패키지는 스프링부트 애플리케이션을 실행하기 위한 로더와 관련된 클래스를 포함하고 있다. 해당 패키지 내에 있는 클래드들이 Jar 파일을 처리하고 애플리케이션을 실행시키는 역할을 한다. 해당 패키지 하위에 애플리케이션에 대한 정보를 읽어와 실행시키는 Launcher 클래스 등이 있다.

META-INF

META-INF
ㄴservice
ㄴMANIFEST.MF

META-INF는 java 애플리케이션에서 사용되는 메타 데이터와 설정 정보를 저장하는 폴더이다.
jar이든 war이든 META-INF 폴더를 가지고 있으며 애플리케이션의 구현과 실행에 관한 정보를 담고 있는 MANIFEST.MF를 가지고 있는 폴더인 만큼 매우 중요하다.

MANIFEST.MF

Manifest-Version: 1.0
  
Main-Class: org.springframework.boot.loader.launch.JarLauncher
  
Start-Class: com.core.testspringsecurity.TestSpringSecurityApplication // 실제로 시작하려는 클래스
  
Spring-Boot-Version: 3.2.2
  
Spring-Boot-Classes: BOOT-INF/classes/
  
Spring-Boot-Lib: BOOT-INF/lib/
  
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
  
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
  
Build-Jdk-Spec: 17
  
Implementation-Title: TestSpringSecurity
  
Implementation-Version: 0.0.1-SNAPSHOT

MANIFEST.MF파일은 해당 애플리케이션의 구현과 실행에 필요한 메타 데이터를 가지고 있다.

해당 파일의 내용 중 특히 중요한 정보인 Main-Class는 애플리케이션이 실행될 때 시작되는 진입점으로 사용되는 클래스를 지정한다.

org.springframework.boot.loader.launch.JarLauncher 클래스를 진입점으로 지정하고 있는데 Jar 파일로 패키징 된 스프링부트 애플리케이션은 내부적으로 필요한 모든 의존성을 포함하고 있어 외부에서 별도의 웹서버 없이 단독으로 실행이 가능하다.

이러한 특성으로 인해 JAR 파일이 실행될 때 특정 클래스가 시작점으로 사용되는데 JarLauncher클래스가 애플리케이션을 실행하는데 필요한 초기화 작업을 처리하고, 실제로 실행되는 클래스인 Start-Class: com.core.testspringsecurity.TestSpringSecurityApplication 정보를 읽어와 main 메서드를 실행시킨다.

Jar의 실행 과정을 더 자세하게 다루어 보자

Jar 실행 과정

public class JarLauncher extends ExecutableArchiveLauncher {  
    public JarLauncher() throws Exception {  
    }  
  
    protected JarLauncher(Archive archive) throws Exception {  
        super(archive);  
    }  
  
    protected boolean isIncludedOnClassPath(Archive.Entry entry) {  
        return isLibraryFileOrClassesDirectory(entry);  // BOOT-INF/classes/와 BOOT-INF/lib/를 클래스 경로에 포함시킴
    }  
  
    protected String getEntryPathPrefix() {  
        return "BOOT-INF/";  // 경로 접두사 반환 -> Spring Boot의 실행 가능한 JAR 구조를 나타냄
    }  
  
    static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) { // 주어진 항목이 clesses 디렉토리인지 lib 디렉토리인지 판단 
        String name = entry.name();  
        return entry.isDirectory() ? name.equals("BOOT-INF/classes/") : name.startsWith("BOOT-INF/lib/");  
    }  
  
    public static void main(String[] args) throws Exception {  
        (new JarLauncher()).launch(args); // 애플리케이션 호출
    }  
}

먼저 Java -jar 명령어를 실행하게 되면 MANIFEST.MF파일의 main-class 정보를 내부적으로 읽어 JarLauncher클래스를 실행시킨다.

JarLauncher클래스에서 Jar파일의 소스 코드 정보와 의존성 등을 포함하는 경로를 판단하고 등록해 launch 메서드로 애플리케이션을 호출한다.

public abstract class ExecutableArchiveLauncher extends Launcher {  
    private static final String START_CLASS_ATTRIBUTE = "Start-Class";  
    protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";  
    protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx";  
    private final Archive archive;  
    private final ClassPathIndexFile classPathIndex;  
 
    protected ExecutableArchiveLauncher(Archive archive) throws Exception {  
        this.archive = archive;  
        this.classPathIndex = this.getClassPathIndex(this.archive);  
    }  
  
    ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {  
        if (!archive.isExploded()) {  
            return null;  
        } else {  
            String location = this.getClassPathIndexFileLocation(archive);  
            return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location);  
        }  
    }  
  
    private String getClassPathIndexFileLocation(Archive archive) throws IOException {  
        Manifest manifest = archive.getManifest();  
        Attributes attributes = manifest != null ? manifest.getMainAttributes() : null;  
        String location = attributes != null ? attributes.getValue("Spring-Boot-Classpath-Index") : null;  
        return location != null ? location : this.getEntryPathPrefix() + "classpath.idx";  
    }
    
protected String getMainClass() throws Exception { // mainclass 정보 반환 
    Manifest manifest = this.archive.getManifest();  
    String mainClass = manifest != null ? manifest.getMainAttributes().getValue("Start-Class") : null;  
    if (mainClass == null) {  
        throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);  
    } else {  
        return mainClass;  
    }  
    
    /// .... 이하 생략
    
}

JarLauncher의 상위 클래스인 ExecutableArchiveLauncher이다. 아카이브를 직접 생성하거나 하위 객체로부터 아카이브 객체(Spring boot 애플리케이션을 포함하는 Jar파일을 말함)를 받아 정의한다. 주어진 아카이브에서 classpath.idx 파일을 로드하는 로직도 담고 있고, 실제로 사용되는 메인 클래스 정보를 반환하는 로직도 가지고 있다.

protected void launch(String[] args) throws Exception {  
    if (!this.isExploded()) {  
        Handlers.register();  
    }  
  
    try {  
        ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls()); // 클래스로더 생성
        String jarMode = System.getProperty("jarmode"); 
        String mainClassName = this.hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : this.getMainClass();  // start-class 정보를 읽어옴
        this.launch(classLoader, mainClassName, args);  
    } catch (UncheckedIOException var5) {  
        throw var5.getCause();  
    }  
}

protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {  
    Thread.currentThread().setContextClassLoader(classLoader); // 스레드에 클래스로더를 지정 - 해당 클래스 로더의 리소스 및 클래스 사용
    Class<?> mainClass = Class.forName(mainClassName, false, classLoader); // 지정한 이름의 클래스 로드 - main class 명을 로드함
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // main 메서드 찾아옴
    mainMethod.setAccessible(true);  
    mainMethod.invoke((Object)null, args); // main 메서드 호출
}

최상위 클래스인 Launcher의 launch메서드이다. 클래스 로더를 생성하고 MANIFEST.MF파일에서 start-class 정보를 읽어 main class를 실행시킨다.


https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#packaging-executable.configuring.layered-archives

https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html

https://camel-it.tistory.com/28

2개의 댓글

comment-user-thumbnail
2024년 3월 6일

👍

답글 달기
comment-user-thumbnail
2025년 1월 12일

잘 보고 갑니다.

답글 달기