본문 바로가기

SearchDeveloper/SpringBoot

스프링부트 jar 구성 및 실행 원리

maven이나 gradle 같은 빌드 툴로 스프링부트 프로젝트를 빌드하면 하나의 산출물인 jar 파일이 생성된다. 이 jar의 구성은 어떻게 되어있는지, java -jar app.jar 를 하면 application이 어떻게 실행되는지 알아보겠다.

 

1. jar 의 구성 요소

먼저 아주 아주 간단한 프로젝트를 생성한다.

그리고 gradle의 bootJar task 를 실행하면 build/libs 폴더에 jar 가 하나 생성된다.

이 jar 는 “스프링부트” 가 “우리가 개발한 소스 코드”를 실행하는데 필요한 모든 것을 가지고 있다.

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

root 폴더 기준으로 하나씩 알아보자

① BOOT-INF : “우리가 개발한 소스 코드” 영역

BOOT-INF
 ㄴ classes
 ㄴ lib
 ㄴ classpath.idx
 ㄴ layers.idx

1) classes

  • jvm 이 읽을 수 있도록 컴파일된 클래스파일이 존재한다.

BOOT-INF/classes

2) lib

  • 코드 실행에 필요한 디펜던시 라이브러리들이 존재한다.

BOOT-INF/lib

3) classpath.idx

  • 스프링부트가 lib 에 있는 외부 라이브러리들을 찾을 수 있도록 클래스패스를 명시한다.

BOOT-INF/classpath.idx

 

4) layers.idx

  • 도커에서 jar 이미지를 만들 때 쓰는 정보이다. 도커는 하나의 이미지를 만들 때 이미지를 차곡차곡 쌓아 최종 이미지를 만드는 레이어 구조인데 이때 필요한 레이어 정보이다. 도커를 쓰지 않는다면 무시해도 될 듯하다.

BOOT-INF/layers.idx

② org/springframework/boot/loader : “스프링부트” 영역

  • mvnrepository.com 에도 있는 org.springframework.boot:spring-boot-loader 라이브러리이다.
  • jar 실행 시 spring-boot-loader 가 우리 소스 코드를 호출한다.
  • spring-boot-loader 가 jar 안에 있으니 내 프로젝트 어딘가에 디펜던시로 숨어있겠지하고 눈 씻고 10번 넘게 찾아봤는데도 없다. 그 이유는, gradle이 빌드할 때 디폴트로 넣어주기 때문이다. (https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#packaging-executable.configuring.layered-archives)
  • 기본 java 에도 jar 를 실행해주는 클래스가 있을텐데 굳이 spring-boot-loader 를 통해 실행하는 이유는 무엇인가?
    • 자바는 jar 안의 jar (=nested jar) 를 로드하는 방식은 제공하고 있지 않다. 그래서 스프링부트는 spring-boot-loader 라는 자신만의 방식으로 nested jar 까지 로드하는 방법을 제공한다.

org.springframework.boot:spring-boot-loader

③ META-INF : “스프링부트”와 “우리가 개발한 소스 코드”를 연결해주는 영역

META-INF
 ㄴ MANIFEST.MF

1) MANIFEST.MF

  • jar 가 실행되면 가장 먼저 Main-ClassJarLauncher 가 호출된다.
  • JarLauncher 에서 우리가 개발한 Start-Class 를 호출하기 위해 필요한 경로 설정 파일이다.

MANIFEST.MF

 

2. jar 실행 원리

1) java -jar jarAnalysis.jar 커맨드를 실행한다.

2) MANIFEST.MFMain-ClassJarLauncher 를 호출한다.

3) JarLauncherMANIFEST.MFStart-ClassJarAnalysisApplication 를 호출한다.

⇒ 핵심은 우리가 개발한 main 메소드를 스프링 프레임워크가 대신 호출해준다는 것이다.

참고로 이것이 프레임워크와 라이브러리의 차이이다. 라이브러리는 우리가 호출한다. 즉, 코드를 호출하는 통제권은 우리에게 있다. 이에 비해 프레임워크는 우리가 짠 코드를 대신 호출해준다. 즉, 흐름 통제권은 프레임워크가 가지고 있다.

이제 실제로 스프링부트 코드 까보면서 스프링부트가 어떻게 우리 코드를 호출해주는지 눈으로 직접 보자.

 

JarLauncher 를 호출하는 법? 이 클래스에는 main 메소드가 존재한다!

JarLauncher.java

org.springframework.boot.loader.JarLauncher

jar 시작 시 가장 먼저 실행되는 JarLauncher 의 메인 메소드이다.

JarLauncherExecutableArchiveLauncherLauncher 의 상속 관계를 가지고 있다.

main(String[] args) 에서는 최상의 부모인 Launcher 클래스의 launch(args) 메소드를 호출하고 있다. 이 안에서는 어떤 일이 일어날까?

 

개발자의 main 메소드 호출을 위한 준비물 만들기

Launcher.java

org.springframework.boot.loader.Launcher

launch(args) 에서는 main 메소드 클래스명클래스로더를 생성한다. 메인 클래스와 클래스로더를 가져오는 기능에 대한 구현은 자식클래스인 ExecutableArchiveLauncher 에서 해주고 있다.

ExecutableArchiveLauncher.java

org.springframework.boot.loader.ExecutableArchiveLauncher

ExecutableArchiveLauncher.java

org.springframework.boot.loader.ExecutableArchiveLauncher

이제 오버로드된 launch(args, launchClass, classLoader) 메소드에 들어가보자

 

개발자의 main 메소드 호출 거의 다왔다

Launcher.java

org.springframework.boot.loader.Launcher

Launcher.java

org.springframework.boot.loader.Launcher

쓰레드에 클래스로더 지정해주고, MainMethodRunner 클래스의 run() 메소드를 호출한다. 이름이 직관적이라 거의 다 온것같다. run() 구현체로 빨리 이동해보자

 

드디어 main 메소드 호출 구현부 도착!: reflection 으로 호출

MainMethodRunner.java

java.lang.reflect.Method.MainMethodRunner

런타임에 리플렉션을 사용해 메인클래스에서 “main” 이라는 이름의 메소드를 찾아 호출한다.

 

스프링부트 jar 구성 및 실행 원리

 

글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.

 

레퍼런스

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

https://wordbe.tistory.com/entry/Spring-Boot-2-Executable-JAR-스프링-부트-실행

https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/

https://java.ihoney.pe.kr/523