관리 메뉴

나구리의 개발공부기록

스프링 부트와 내장 톰캣, WAR 배포 방식의 단점, 내장 톰캣(설정/서블릿/스프링/빌드와 배포), 편리한 부트 클래스 만들기, 스프링 부트와 웹 서버(프로젝트 생성/실행 과정/ 빌드와 배포), 스프링 부트 실행 가능 JAR 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 부트 - 핵심 원리와 활용

스프링 부트와 내장 톰캣, WAR 배포 방식의 단점, 내장 톰캣(설정/서블릿/스프링/빌드와 배포), 편리한 부트 클래스 만들기, 스프링 부트와 웹 서버(프로젝트 생성/실행 과정/ 빌드와 배포), 스프링 부트 실행 가능 JAR

소소한나구리 2024. 11. 20. 16:07

출처 : 인프런 - 스프링 부트 - 핵심 원리와 활용(유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. WAR 배포 방식의 단점

(1) 웹 애플리케이션의 배포 과정

  • 1. 톰캣 같은 웹 애플리케이션 서버(WAS)를 별도로 설치
  • 2. 애플리케이션 코드를 WAR로 빌드
  • 3. 빌드한 WAR 파일을 WAS에 배포
  • 애플리케이션을 구동하고 싶으면 웹 애플리케이션 서버를 별도로 설치해야하는 구조이며 과거에는 이렇게 웹 애플리케이션 서버와 웹 애플리케이션 빌드 파일(WAR)이 분리되어 있는것이 당연한 구조였음

(2) 단점

  • 톰캣 같은 WAS를 별도로 설치해야하는 번거로움이 있음
  • 단순한 자바라면 별도의 설정을 고민하지 않고 main()메서드만 실행하면 되는데, 웹 애플리케이션은 WAS를 실행하고 WAR와 연동하기위한 별도의 개발환경 설정이 필요하여 복잡함
  • WAR를 만들고 또 WAS에 전달해야하는 배포 과정이 복잡함
  • 톰캣의 버전을 변경하려면 톰캣을 다시 설치해야 함

(3) 고민과 해결

  • 단순히 자바의 main() 메서드만 실행하면 웹 서버까지 같이 실행되도록 하는 방안을 누군가는 계속 고민해왔음
  • 이런 문제를 해결하기 위해 톰캣 같은 웹 서버를 하나의 라이브러리 처럼 포함해서 사용하도록 내장 톰캣(embed tomcat)기능을 제공함
  • 웹 애플리케이션 서버에 WAR 파일을 배포하여 WAS를 실행하는 방식에서 애플리케이션 JAR안에 다양한 라이브러리들과 WAS 라이브러리가 포함되는 방식으로 변경하여 main() 메서드를 실행하여 동작함

좌) 외장 톰캣과 WAR / 우) 애플리케이션 JAR안에 다양한 라이브러리와 WAS 라이브러리가 포함


2. 내장 톰캣

1) 설정

(1) 프로젝트 설정

  • 제공된 프로젝트 파일을 사용
  • 새로 생성해도 됨

(2) build.gradle 확인

  • 스프링 MVC와 내장 톰캣이 주입되어 있어 톰캣을 설치 하지 않고 main메서드로 톰캣을 실행할 수 있으며 라이브러리 내부의 코드를 보면 서블릿 관련 코드도 포함하고 있음을 알 수 있음
  • JAR 설정이 되어있는데 이부분은 나중에 다룸
plugins {
    id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    //스프링 MVC 추가
    implementation 'org.springframework:spring-webmvc:6.0.4'

    //내장 톰캣 추가
    implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
}

tasks.named('test') {
    useJUnitPlatform()
}

//일반 Jar 생성
task buildJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    with jar
}

//Fat Jar 생성
task buildFatJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    duplicatesStrategy = DuplicatesStrategy.WARN
    from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}

2) 서블릿

(1) EmbedTomcatServletMain

  • embed 패키지를 생성하여 작성
  • 톰캣 설정: 내장 톰캣을 생성하고, 톰캣이 제공하는 커넥터를 사용해서 8080 포트에 연결
  • 서블릿 등록: 톰캣에 사용할 contextPath와 docBase를 지정, 이부분은 크게 중요하지 않으므로 예제처럼 적용
  • tomcat.addServlet()을 통해서 서블릿을 등록하고 context.addServletMappingDecoded() 통해서 등록한 서블릿의 경로를 매핑
  • 톰캣 시작: tomcat.start() 코드를 사용해서 톰캣을 시작
package hello.embed;

public class EmbedTomcatServletMain {
    public static void main(String[] args) throws LifecycleException {
        System.out.println("EmbedTomcatServletMain.main");

        // 톰캣 설정
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);

        // 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "helloServlet", new HelloServlet());
        context.addServletMappingDecoded("/hello-servlet", "helloServlet");
        tomcat.start();
    }
}

 

(2) 실행

  • 실행 후 매핑된 localhost:8080/hello-servlet에 접속해보면 서블릿이 정상 동작하여 응답값을 출력하는 것을 확인할 수 있음
  • 내장 톰캣을 이용한 덕분에 IDE에 별도의 복잡한 톰캣 설정과 설치없이 main() 메서드만 실행하면 톰캣까지 매우 편리하게 실행할 수 있음

** 참고

  • 내장 톰캣을 개발자가 직접 다룰일은 거의 없으며 스프링 부트에서 내장 톰캣 관련된 부분을 거의 자동화해서 제공하기 때문에 내장 톰캣을 깊이있게 학습하는 것은 권장하지 않음
  • 백엔드 개발자는 이외에도 공부해야할게 너무 많기 때문에 내장 톰캣이 어떤 방식으로 동작하는지 정도만 원리를 이해하는 정도면 충분함

3) 스프링 연동

(1) EmbedTomcatSpringMain

  • 스프링 컨테이너를 생성하고 내장 톰캣에 디스패처 서블릿을 등록하는 부분만 다르고 앞에서 했던 내용과 동일함
  • HelloController를 생성하여 빈으로 등록하는 설정이 되어있는 HelloConfig를 스프링 컨테이너로 등록
package hello.embed;

public class EmbedTomcatSpringMain {
    public static void main(String[] args) throws LifecycleException {
        System.out.println("EmbedTomcatSpringMain.main");

        // 톰캣 설정
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);

        // 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(HelloConfig.class);

        // 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcher = new DispatcherServlet(appContext);

        // 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "dispatcher", dispatcher);
        context.addServletMappingDecoded("/", "dispatcher");
        tomcat.start();
    }
}

 

(2) 동작 과정

  • main() 메서드를 실행하면 내장 톰캣을 생성해서 8080포트로 연결하도록 설정
  • 스프링 컨테이너를 만들고 필요한 빈을 등록
  • 스프링 MVC 디스패처 서블릿을 만들고 앞서 만든 스프링 컨테이너에 연결
  • 디스패처 서블릿을 내장 톰캣에 등록
  • 내장 톰캣을 실행

(3) 실행

  • main 메서드를 실행후 localhost:8080/hello-spring으로 들어가보면 HelloController가 정상 동작하여 응답값이 출력되는 것을 확인할 수 있음
  • 코드를 보면 서블릿 컨테이너 초기화와 거의 같은 코드이지만 시작점이 개발자가 main() 메서드를 직접 실행하는가, 서블릿 컨테이너가 제공하는 초기화 메서드를 통해서 실행하는가의 차이가 있을 뿐임

4) 빌드와 배포

(1) jar 형식으로 빌드

  • 자바의 main() 메서드를 실행하기 위해서는 jar 형식으로 빌드해야함
  • 그리고 jar안에는 META-INF/MANIFEST.MF 파일에 실행할 main()메서드의 클래스를 지정해주어야함
  • 해당 파일을 직접 만들면 빌드 과정에서 깨질 수도 있기 때문에 build.gradle에 아래와같이 설정하면 Gradle의 도움을 받아서 해당 과정을 쉽게 진행할 수 있음
//일반 Jar 생성
task buildJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    with jar
}

 

(2) 빌드 및 실행

  • mac: ./gradlew clean buildJar
  • 윈도우: gradlew clean buildJar
  • 터미널에서 프로젝트의 위치로 이동하여 해당 명령어를 실행하면 build/libs에 embed-0.0.1-SNAPSHOT.jar가 생성됨
  • java -jar embed-0.0.1-SNAPSHOT.jar로 해당 jar파일을 실행하면 스프링 관련 클래스를 찾을 수 없다는 오류가 발생됨

오류

(3) jar 분석

  • build/libs로 이동하여 jar -xvf embed-0.0.1-SNAPSHOT로 압축을 해제
  • JAR를 푼 결과를 보면 스프링 라이브러리나 내장 톰캣 라이브러리가없기 때문에 jar를 실행했을 때 해당 오류가 발생한 것임
  • 과거의 WAR의 압축을 해제한 구조를 보면 lib에 라이브러리역할을 하는 jar파일을 포함하고 있었음

embed-0.0.1-SNAPSHOT.jar 압축 해제

(4) jar 파일은 jar 파일을 포함할 수 없음

  • JAR파일을 내부에 라이브러리 역할을 하는 JAR파일을 포함할 수 없고 포함한다고 해도 인식이 되지 않는 한계가 있음
  • WAR는 웹 애플리케이션 서버(WAS)위에서만 실행할 수 있기 때문에 WAR를 사용할 수도 없음
  • 대안 중 하나로 라이브러리 JAR 파일을 모두 구해서 MANIFEST 파일에 해당 경로를 적어두면 인식이 되지만 매우 번거롭고 JAR파일안에 JAR파일을 포함할 수 없기 때문에 라이브러리 역할을 하는 JAR파일도 항상 함께 가지고 다니고 있어야하기 때문에 권장하지 않으므로 이 방법은 설명하지 않음

5) JAR 배포 문제 대안

(1) FatJar

  • fat jar 또는 uber jar라고 불리는 방법
  • JAR안에는 JAR를 포함할 수 없지만 클래스는 얼마든지 포함할 수 있음
  • build.gradle에 Fat Jar 관련 설정을 적용
//Fat Jar 생성
task buildFatJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    duplicatesStrategy = DuplicatesStrategy.WARN
    from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
    with jar
}

 

(2) 빌드 및 실행

  • ./gradlew clean buildFatJar로 실행, 빌드시 Encountered duplicate path 경고가 나올 수 있는데 해당 부분은 무시
  • build/libs로 들어가서 생성된 embed-0.0.1-SNAPSHOT.jar의 용량을 확인해보면 10M이상의 큰 사이즈의 jar파일이 생성되어있음
  • jar 파일을 실행해보면 톰캣이 정상적으로 동작하는 것을 확인할 수 있음
  • 해당 jar파일을 풀어보면 직접 만든 클래스를 포함하여 수 많은 라이브러리에서 제공하는 클래스들이 포함되어있는 것을 확인할 수 있음

(3) Fat Jar 정리

  • Fat Jar 덕분에 하나의 jar 파일에 필요한 라이브러리들을 내장할 수 있게 되었음
  • 내장 톰캣 라이브러리를 jar 내부에 내장할 수 있게 되었음
  • 덕분에 하나의 jar파일로 배포부터, 웹 서버 설치 + 실행까지 모든 것을 단순화 할 수 있음

(4) WAR 단점 해결

  • 톰캣 같은 WAS가 라이브러리로 JAR 내부에 포함되어있어 별도로 설치해야하는 번거로움이 해결됨
  • IDE의 복잡한 WAS 설정이 필요하지 않고 단순히 main() 메서드만 실행하면 되기 때문에 복잡한 개발환경 설정이 없어짐
  • JAR를 만들고 원하는 위치에서 실행만하면 되기 때문에 배포과정이 단순해짐
  • WAS의 버전관리를 위해 재설치하는 번거로움이 없이 gradle에서 내장 톰캣 라이브러리 버전만 변경하고 빌드 후 실행하면 버전관리가 됨

(5) Fat Jar의 단점

  • 완벽해 보이는 Fat Jar도 여전히 단점을 포함하고 있음
  • 모두 class로 풀려있다보니 어떤 라이브러리로 사용되어있는지 추적하기어려워 어떤 라이브러리가 포함되어있는지 확인이 어려움
  • 파일명 중복을 해결할 수 없어 클래스나 리소스 명이 같은 경우 하나를 포기해야하는데 이것은 심각한 문제를 발생함
  • 서블릿 컨테이너 초기화에서 학습한 부분을 떠올려보면 META-INF/services/jakarta.servlet.ServletContainerInitializer 파일이 여러 라이브러리에(JAR)에 있을 수 있음
  • 만약 A, B 라이브러리가 해당 파일을 사용해서 서블릿 컨테이너 초기화를 시도한다고 가정하면 Fat Jar를 만들면 파일명이 같으므로 둘 다 가지고 있는 파일 중에 하나일 파일만 선택되어 나머지 하나는 포함되지 않았으므로 정상 동작하지 않음
  • 아까 Fat Jar로 빌드를 했을때 엄청많이 출력되었던 경고 문구가 build.gradle에 Fat Jar 설정 시 중복이 발생되면 해당 경고를 띄우도록 설정했기 때문에 출력되었던 경고 문구임

3. 편리한 부트 클래스 만들기

1) 나만의 부트 클래스

  • 지금까지 진행한 내장 톰캣 실행, 스프링 컨테이너 생성, 디스패처 서블릿 등록의 모든 과정을 편리하게 처리해주는 나만의 부트 클래스를 생성
  • 부트는 이름 그대로 시작을 편하게 처리해주는 것을 뜻함

(1) MySpringApplication

  • boot 패키지 생성후 작성
  • EmbedTomcatSpringMain 클래스에 작성해두었던 코드 내용을 복사한 후, 스프링 컨테이너로 등록되는 클래스가 매개변수로 전달되는 클래스가 되도록 코드를 변경
  • String[] args: main(args)를 전달 받아서 사용하며, 해당 예제에서는 단순히 해당 값을 출력함
  • tomcat.start()에서 발생하는 예외를 던지지않고 try-catch로 잡아서 런타임 예외로 변경
package hello.boot;

public class MySpringApplication {
    public static void run(Class configClass, String[] args) {
        System.out.println("MySpringApplication.main args=" + List.of(args));

        // 톰캣 설정
        Tomcat tomcat = new Tomcat();
        Connector connector = new Connector();
        connector.setPort(8080);
        tomcat.setConnector(connector);

        // 스프링 컨테이너 생성
        AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
        appContext.register(configClass);

        // 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
        DispatcherServlet dispatcher = new DispatcherServlet(appContext);

        // 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "dispatcher", dispatcher);
        context.addServletMappingDecoded("/", "dispatcher");
        try {
            tomcat.start();
        } catch (LifecycleException e) {
            throw new RuntimeException(e);
        }
    }
}

 

(2) @MySpringBootApplication

  • 컴포넌트 스캔 기능이 추가된 단순한 애노테이션
  • 시작할 때 이 애노테이션을 붙여서 사용하면됨
  • 다른 여러가지의 기능을 추가로 붙여도 됨
package hello.boot;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}

 

(3) HelloConfig - 수정

  • 해당 클래스에 붙어있는 @Configuration을 주석처리
  • 컴포넌트 스캔을 사용할 예정이여서 HelloController에 붙어있는 @RestController에는 내부에 @Component가 있어서 자동으로 컴포넌트 스캔 대상이 됨

(4) MySpringBootAppMain

  • hello 패키지에 생성, 패키지의 위치가 중요함
  • @MySpringBootApplication에 컴포넌트 스캔이 추가되어있는데, 컴포넌트 스캔의 기본 동작은 해당 애노테이션이 붙은 클래스의 현재 패키지부터 그 하위 패키지를 컴포넌트 스캔의 대상으로 사용하기 때문임
  • 해당클래스의 위치가 hello 패키지에있으므로 hello와 그 하위의 패키지에 있는 @Component가 붙은 클래스를 모두 컴포넌트 스캔하므로 HelloController가 컴포넌트 스캔의 대상이 됨
  • MySpringBootApplication 애노테이션과 MySpringApplication.run() 메서드만 기억하면 MySpringApplication.run(설정 정보, args) 한줄로 내장 톰캣 실행, 스프링 컨네이너 생성, 디스패처 서블릿, 컴포넌트 스캔까지 모든 기능이 한번에 편리하게 동작하게 됨
package hello;

@MySpringBootApplication
public class MySpringBootAppMain {
    public static void main(String[] args) {
        System.out.println("MySpringBootAppMain.main");
        MySpringApplication.run(MySpringBootAppMain.class, args);
    }
}

 

(5) 스프링 부트

  • 스프링 부트로 프로젝트를 생성해보았으면 이미 눈치를 챘을 것
  • 지금까지 만든 것을 라이브러리로 만들어서 배포한다면 그것이 바로 스프링 부트
  • 생성된 프로젝트의 최상단에 생성되는 클래스를 열어서 내부 구조를 살펴보면 지금까지 직접 작성한 형태의 구조로 무수히 많은 코드들이 작성되어있음을 확인할 수 있음

4. 스프링 부트와 웹 서버(프로젝트 생성/실행 과정/ 빌드와 배포)

1) 프로젝트 생성

(1) 스프링 부트로 여러가지 문제 해결

  • 스프링 부트는 지금까지 고민한 문제를 깔끔하게 해결해줌
  • 내장 톰캣을 사용하여 빌드와 배포를 편리하게 해줌
  • 빌드시 하나의 Jar를 사용하면서 동시에 Fat Jar문제도 해결함
  • 지금까지 진행한 내장 톰캣 서버를 실행하기 위한 복잡한 과정을 모두 자동으로 처리함

(2) 프로젝트 생성

  • 제공된 프로젝트를 복사하여 사용하였으나, 스프링 부트 스타터 사이트로 스프링 프로젝트를 생성해도 됨
  • build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.2'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

 

(3) HelloController

  • controller 패키지를 생성하여 동작 확인을 위한 컨트롤러를 생성
package hello.boot.controller;

@RestController
public class HelloController {

    @GetMapping("/hello-spring")
    public String hello() {
        System.out.println("HelloController.hello");
        return "Hello Spring!!";
    }
}

 

(4) 애플리케이션 실행 / 내장 톰캣과 라이브러리 버전

  • 컨트롤러 작성 후 BootApplication을 실행하여 매핑된 url로 접속해보면 정상적으로 컨트롤러가 동작하는 것을 확인할 수 있음
  • 라이브러리의 의존관계를 따라가보면 내장 톰캣(tomcat-embed-core)이 포함된 것을 확인할 수 있음
  • 라이브러리의 버전도 현재 사용하고있는 부트의 버전과 가장 적절한 외부 라이브러리의 버전을 자동으로 선택해주기때문에 의존관계 입력 시 버전정보를 입력해주지 않아도 됨

2) 실행 과정

(1) 스프링 부트의 실행 과정

  • 스프링 부트를 실행할 때는 자바 main()메서드에서 SpringApplication.run()을 호출해주면 됨
  • 여기에 메인 설정 정보를 넘겨주는데 보통 @SpringBootApplication 애노테이션이 있는 현재 클래스를 지정해주면 됨
  • 참고로 현재 클래스에는 @SpringBootApplication애노테이션이 있는데 해당 애노테이션안에는 컴포넌트 스캔을 포함한 여러가지 기능이 설정되어있으며 기본 설정은 현재 패키지와 그 하위 패키지를 모두 컴포넌트 스캔함
  • 이 단순해 보이는 코드 한줄 안에서는 수많은 일들이 일어나는데, 핵심은 스프링 컨테이너를 생성하고, WAS(내장 톰캣)을 생성하는 것 2가지임
@SpringBootApplication
public class BootApplication {

	public static void main(String[] args) {
		SpringApplication.run(BootApplication.class, args);
	}
}

 

(2) 스프링 부트 내부에서 스프링 컨테이너와 내장 톰캣을 생성하는 코드

  • ServletWebServerApplicationContextFactory: 스프링 컨테이너 생성하는 코드
  • TomcatServletWebServerFactory: 내장 톰캣을 생성하는 코드
  • 내부 구조에는 엄청나게 많은 코드가 있지만 그 중 일부를 살펴보면 지금까지 앞서 진행했던 동일한 방식으로 스프링 컨테이너를 만들고 내장 톰캣을 생성하고 그 둘을 연결하는 과정을 진행하고 있음을 알 수 있음

** 참고

  • 스프링 부트는 너무 큰 라이브러리이기 때문에 스프링 부트를 이해하기 위해 모든 코드를 하나하나 파보는 것은 권장하지 않음
  • 스프링 부트가 어떤 식으로 동작하는지 개념을 이해하고 꼭 필요한 부분의 코드만 확인하고 넘어갈 것

3) 빌드와 배포

(1) 빌드 및 실행

  • 해당 프로젝트의 경로로 이동하여 ./gradlew clean build로 빌드를 실행
  • 이후 build/libs로 이동해보면 boot-0.0.1-SNAPSHOT.jar라는 파일이 생성되어있음
  • java -jar jar파일명으로 실행해보면 스프링 부트실행 로그와 함께 스프링부트 애플리케이션이 실행되고 내장 톰캣이 8080포트로 실행된 것을 확인할 수 있음

(2) 스프링 부트 jar 분석

  • jar -xvf boot-0.0.1-SNAPSHOT.jar 로 해당 jar파일을 압축을 풀어서 구조를 확인해보면 Fat Jar가 아닌 새로운 구조로 만들어져 있음
  • 심지어 jar 내부에 jar를 담아서 인식하는 것이 불가능한데 jar가 포함되어있고 인식까지 되어있음
  • BOOT-INF/classes : 우리가 개발한 class 파일과 리소스 파일
  • BOOT-INF/lib : 외부 라이브러리(jar파일이 들어있음)
  • org/springframework/boot/loader/JarLauncher.class: 스프링 부트 main() 실행 클래스

** 참고

  • 빌드 결과를 보면 boot-0.0.1-SNAPSHOT-plain.jar 파일도 보이는데 이것은 우리가 개발한 코드만 순수한 jar로 빌드한 것이므로 무시해도 됨

 

5. 스프링 부트 실행 가능 JAR

1) 스프링 부트의 특별한 구조의 JAR

(1) 실행 가능 JAR

  • 위에서 알아본 Fat Jar의 여러가지의 단점과 문제를 해결하기 위해 jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고 동시에 만든 jar를 내부 jar를 포함해서 실행할 수 있게 하였음
  • 이것을 실행 가능 Jar(Executable Jar)라고 하며 이것을 사용하여 문제들을 깔끔하게 해결할 수 있음
  • jar 내부에 jar를 포함하기 때문에 어떤 라이브러리가 포함되어있는지 쉽게 확인할 수 있어 어떤 라이브러리가 포함되어있는지 확인하기 어려운 문제를 해결함
  • 내부에 같은 경로의 파일이 있어도 둘 다 인식할 수 있으므로 파일명 중복 문제가 해결되었음
  • 실행 가능 Jar는 자바 표준이 아니고 스프링 부트에서 새롭게 정의한 것

(2) 실행 가능 Jar 내부 구조

  • boot-0.0.1-SNAPSHOT.jar
  • META-INF
    - MANIFEST.MF
  • org/springframework/boot/loader
    - JarLauncher.class : 스프링 부트 main() 실행 클래스
  • BOOT-INF
    - classes : 우리가 개발한 class 파일과 리소스 파일
    - hello/boot/BootApplication.class
    - hello/boot/controller/HelloController.class
    - ...
  • lib : 외부 라이브러리
    - spring-webmvc-6.0.4.jar
    - tomcat-embed-core-10.1.5.jar
    - ...
  • classpath.idx : 외부 라이브러리 모음
  • layers.idx : 스프링 부트 구조 정보

(3-1) Jar 실행 정보

  • java -jar xxx.jar를 실행하게 되면 우선 META-INF/MANIFEST.MF 파일을 찾아 여기에 있는 Main-Class를 읽어서 main()메서드를 실행하게 됨

MANIFEST.MF 파일

(3-2) Main-Class

  • 우리가 기대한 main()이 있는 BootApplication이 아니라 JarLauncher라는 전혀 다른 클래스를 실행하고 있음
  • JarLauncher는 스프링 부트가 빌드시에 넣어주며 org/springframework/boot/loader의 경로에 실제로 포함되어있음
  • 스프링 부트는 jar 내부에 jar를 읽어들이는 기능과 특별한 구조에 맞게 클래스 정보도 읽어들여야하는데 바로 JarLauncher가 이런 일들을 처리해줌
  • JarLauncher가 먼저 이런 작업들을 처리한다음 Start-Class에 지정된 main()을 호출함

(3-3) Start-Class

  • 우리가 기대한 main()가 있는 실제 애플리케이션

(3-4) 기타

  • 스프링 부트가 내부에서 사용하는 정보들
  • Spring-Boot-Version : 스프링 부트 버전
  • Spring-Boot-Classes : 개발한 클래스 경로
  • Spring-Boot-Lib : 라이브러리 경로
  • Spring-Boot-Classpath-Index : 외부 라이브러리 모음
  • Spring-Boot-Layers-Index : 스프링 부트 구조 정보

** 참고

  • Main-Class를 제외한 나머지는 자바 표준이 아니며 스프링 부트가 임의로 사용하는 정보임

(4) 스프링 부트 로더

  • org/springframework/boot/loader 하위에 있는 클래스들
  • JarLauncher를 포함한 스프링 부트가 제공하는 실행 가능 Jar를 실제로 구동시키는 클래스들이 포함되어 있음
  • 스프링 부트는 빌드시에 이 클래스들을 포함해서 만들어줌

(5) BOOT-INF

  • classes: 우리가 개발한 class파일과 리소스 파일
  • lib: 외부 라이브러리
  • classpath.idx: 외부 라이브러리 모음
  • layers.idx: 스프링 부트 구조 정보
  • WEB-INF라는 내부 폴더에 사용자 클래스와 라이브러리를 포함하고 있는 WAR의 구조를 본따서 만들었으며 이름도 BOOT-INF임
  • JarLauncher를 통해서 여기에 있는 classes와 lib에 있는 jar파일들을 읽어들임

(6) 실행 과정 정리

  1. java -jar xxx.jar
  2. MANIFEST.MF
  3. JarLauncher.main() 실행, BOOT-INF의 classes와 lib의 파일들을 인식
  4. BootApplication.main() 실행

** 참고

  • 실행 가능 Jar가 아니라 IDE에서 직접 실행할 때는 BootApplication.main()을 바로 실행하여 IDE가 필요한 라이브러리를 모두 인식할 수 있게 도와주기 때문에 JarLauncher가 필요하지 않음