이 글은 JAR과 WAR의 차이, 전통적인 웹 애플리케이션 배포 방식의 한계, 그리고 스프링 부트에서 JAR 방식이 어떻게 작동하는지까지 정리한 글입니다.
JAR
(Java Archive) 라고 하는 압축 파일을 만들 수 있다JAR
파일안에 main 메서드가 있어서 직접 실행하거나 OR 다른 곳에서 라이브러리로 사용하거나java -jar abc.jar
이런식으로 명령어를 통해 실행한다main()
메서드가 필요하고, MANIFEST.MF
파일에 실행할 메인 메서드가 있는 클래스를 지정해두어야 한다JAR
는 클래스와 관련 리소스를 압축한 단순한 파일이다.class
파일 몇개만 있으면 되지만 WAR 는 위 파일이 모두 포함되어야 하며 WAR 구조를 지켜야 한다WAR 구조
gradle
을 통해 빌드war
로 빌드함server-0.0.1-SNAPSHOT.war
파일의 압축을 풀면 아래 3가지 파일이 나옴index.html
: 정적 리소스META-INF
: 메인 메서드가 담긴 클래스에 대한 정보WEB-INF
: 자바 클래스와 라이브러리, 설정 정보가 들어간다WEB-INF
를 제외한 나머지 영역은 HTML, CSS 같은 정적 리소스가 사용되는 영역이다war
파일을 특정 톰캣 폴더에 옮기면 우리가 만든 자바 파일이 실행된다war
파일을 이런식으로 실행하여 배포함옛날 WAR 배포 방식의 단점
옛날에는 웹 애플리케이션을 개발하고 배포하려면 다음과 같은 과정을 거쳐야 한다
그렇지만 이 방식에는 단점이 있다
main()
을 실행하면 되지만한가지 제안
main()
메서드만 실행하면 웹 서버까지 같이 실행되도록 하면 되지 않을까 ?WAR
파일을 배포하는 방식, WAS 를 실행해서 동작한다 (옛날 방식)JAR
안에 다양한 라이브러리들과 WAS
라이브러리가 포함되는 방식main()
메서드를 실행해서 동작한다내장 톰캣 라이브러리
dependencies {
implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
}
내장 톰캣 설정
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);
// 스프링 컨테이너 생성
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();
}
}
이 덕분에 IDE 에 복잡한 톰캣 설정도 없어지고 main()
메서드만 실행하면 매우 편리하게 실행된다
물론 톰캣 서버를 설치하지 않아도 된다!
또한 스프링 부트에서 내장 톰캣 관련된 부분을 거의 자동화 및 제공하므로 내장 톰캣을 다룰일이 거의 없다
그럼 이제 빌드 및 배포에 대해 알아봐야 하는데 먼저 우리는 일반적인 자바 파일의 빌드에 대해 알아보자
main()
메서드안에서 코드를 작성하며 실행하기 위해서는 jar
형식으로 빌드해야 한다jar
안에는 META-INF/MANIFEST.MF
파일에 실행할 main()
메서드의 클래스를 지정해주어야 한다Manifest-Version: 1.0
Main-Class: hello.start.GongzaMainClass
그럼 이제 내장 톰캣을 통해 스프링 컨테이너를 구성한 프로젝트를 jar 로 빌드해보자
./gradlew clean buildJar
gradle 을 통해 jar 로 빌드해보자./build/lib
폴더 내부에 embed-0.0.1-SNAPSHOT.jar
파일이 생겼다java -jar embed-0.0.1-SNAPSHOT.jar
Error: Unable to initialize main class hello.embed.EmbedTomcatSpringMain
Caused by: java.lang.NoClassDefFoundError: org/springframework/web/context/WebApplicationContext
springframework
가 존재하지 않아서 발생하는 에러이다fat jar
또는 uber jar
라고 불리는 방법이 있다.class
파일이 나온다 이를 다시 우리 프로젝트와 합쳐서 새로운 JAR 파일을 만드는 방식.class
때문에 뚱뚱한(Fat) JAR 가 탄생한다 그래서 Fat Jar 라고 부른다빌드후 실행하면 정상동작한다
-rw-r--r-- 1 imyeong-gyu staff 10M Jan 19 18:48 embed-0.0.1-SNAPSHOT.jar
그러나 파일 크기가 10 메가 이다..
JAR 파일 압축을 풀면
...
├── SeparatorPathElement.class
├── SingleCharWildcardedPathElement.class
├── WildcardPathElement.class
├── WildcardTheRestPathElement.class
└── package-info.class
361 directories, 6017 files
jakarta
, org
등 라이브러리에 있는 자바파일도 포함되어 있으며 361개의 폴더와 6071 파일이 존재하게 된다 .class
파일로 풀려있으니 확인이 어렵다META-INF
파일이 두 라이브러리에 모두 포함된 경우./gradlew clean build
를 통해 빌드 후./build/lib/~~-SNAPSHOT.jar
파일을 java -jar
를 통해 실행하면 정상 동작한다SNAPSHOT.jar
파일을 보면 크기가 18M 이다-rw-r--r-- 1 imyeong-gyu staff 18M Jan 19 19:28 boot-0.0.1-SNAPSHOT.jar
BOOT-INF
/ META-INF
/ org
크게 3가지 파일이 존재한다plan.jar
파일은 단순히 현재 프로젝트에서 라이브러리 파일이 포함되지 않은 JAR 파일BOOT-INF
폴더 안에 lib
라는 라이브러리 파일이 존재하는데 이 파일은 JAR 파일이다이제 각각의 파일들을 설명해보자
MENIFEST.MF
파일만 존재하게 된다Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.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
org
파일 안에는 스프링 부트의 처음 main 클래스인 JarLauncher
파일이 존재하게 된다org/springframework/boot/loader
classes
파일과 lib
라는 파일이 존재한다classes
파일BootApplication.class
, HelloController
, HelloService
등lib
파일spring-webmvc-6.0.4.jar
, tomcat-embed-core-10.1.5.jar
등classpath.idx
: 외부 라이브러리 경로layers.idx
: 스프링 부트 구조 경로핵심은 JAR 를 푼 결과가 Fat JAR 가 아니라 처음보는 새로운 구조로 만들어져 있다
심지어 JAR 내부에 JAR 를 담아서 인식하는 것이 불가능한데, JAR 가 포함되어 있고 인식까지 되었다
이를 통해 문제를 해결하게 된다
.class
파일이 묶여있기 때문에 내부에 같은 경로의 파일이 존재하여도 둘다 인식이 가능하다참고로 실행 가능 JAR 는 자바 표준이 아닌 스프링 부트에서 새롭게 정의한 것이다
java -jar xxx.jar
를 통해 실행하게 되면 우선 META-INF/MANIFEST.MF
파일을 찾는다Main-Class
를 읽어서 main()
메서드를 실행하게 된다스프링 부트가 만든 MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.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
BootApplication
) 파일이 아닌 다른 파일이 메인 클래스로 등록되어 있다JarLauncher
파일이 Main-Class
파일로 지정되어 있음JarLauncher
가 실행된다JarLauncher
가 이런일을 처리해준다Start-Class:
에 지정된 우리가 만든 메인 클래스의 main()
을 호출한다JarLauncher
→ 우리가 만든 메인 클래스 main()
JVM
└─> JarLauncher (Main-Class)
├─> BOOT-INF/lib/*.jar 로드
├─> BOOT-INF/classes 로드
└─> Start-Class 의 main() 실행
BOOT-INF
폴더 안에 보면 classpath.idx
파일이 존재하는 걸 볼 수 있다lib
파일에 대한 클래스 파일 경로 정보 이다classpath.idx
파일에 담겨있다MANIFEST.MF
파일을 보면 Spring-Boot-Classpath-Index
의 값에 classpath
파일의 경로가 포함되어 있다MANIFEST.MF
파일을 읽어들이고 Spring-Boot-Classpath-Index
의 값을 읽어 외부 라이브러리 Jar 파일을 로더 할 수 있게 된다Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
Spring-Boot-Version: 3.0.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
스프링 부트는 복잡한 WAR 구조와 배포를 간소화하기 위해 실행 가능한 JAR 구조를 도입했고,
이로써 내장 톰캣, 라이브러리 포함, 독립 실행까지 모두 지원하는 환경을 만들었습니다.
참고: 실행 가능한 JAR 구조는 자바 표준은 아니며 스프링 부트가 자체 정의한 구조입니다.