이 글은 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.jarError: 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 구조는 자바 표준은 아니며 스프링 부트가 자체 정의한 구조입니다.