웹 애플리케이션을 개발하고 배포하려면 다음과 같은 과정을 거쳐야 한다.
단점
main()
메서드만 실행하면 된다.이런 문제를 해결하기 위해 톰캣을 라이브러리로 제공하는 내장 톰캣(embed tomcat) 기능을 제공한다.
springboot-embed라는 프로젝트를 생성했습니다.
build.gradle
//내장 톰캣 추가
implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
내장 톰캣은 쉽게 이야기해서 톰캣을 라이브러리로 포함하고 자바 코드로 직접 실행하는 것이다. 코드로 알아보자.
EmbedTomcatServletMain
package hello.embed;
import hello.servlet.HelloServlet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
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();
}
}
결과 확인
이번에는 내장 톰캣에 스프링까지 연동해보자.
EmbedTomcatSpringMain
package hello.embed;
import hello.spring.HelloConfig;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
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();
}
}
결과 확인
main()
메서드를 실행하기 위해서는 jar
형식으로 빌드해야 한다.jar
안에는 META-INF/MANIFEST.MF
파일에 실행할 main()
메서드의 클래스를 지정해주어야 한다.build.gradle
task buildJar(type: Jar) {
manifest {
attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
}
with jar
}
cmd창을 열고 해당 프로젝트로 이동하여 gradlew clean buildJar
명령어를 치면 build/libs/embed-0.0.1-SNAPSHOT.jar
이 만들어 진다.
java -jar embed-0.0.1-SNAPSHOT.jar
로 실행해보면 오류가 발생한다. 스프링 관련 클래스를 찾을 수 없다는 오류이다. JAR
압축을 풀어보면 스프링 라이브러리나 내장 톰캣 라이브러리가 전혀 보이지 않는다. 따라서 해당 오류가 발생한 것이다.대안으로 FatJar
라고 불리는 방법이 있다.
jar
를 풀면 class
들이 나온다. 이 class
를 뽑아서 새로 만드는 jar
에build.gradle
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
}
gradlew clean buildFatJar
로 빌드build/libs/embed-0.0.1-SNAPSHOT.jar
생성오류
포트번호가 겹치고 있다.
8080 포트 번호를 확인 후 포트를 죽여줬다.
또 다른 오류 발생
수동으로 /webapps폴더 생성
오류 해결 정상 작동
jar 압축 풀기
jar -xvf embed-0.0.1-SNAPSHOT.jar
를 통해서 압축을 풀었다.
Fat Jar의 장점
지금까지 진행한 내장 톰캣 실행, 스프링 컨테이너 생성, 디스패처 서블릿 등록의 모든 과정을 편리하게 처리해주는 나만의 부트 클래스를 만들어보자.
MySpringApplication
package hello.boot;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.List;
public class MySpringApplication {
public static void run(Class configClass, String[] args) {
System.out.println("MySpringBootApplication.run 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);
}
}
}
configClass
: 스프링 설정을 파라미터로 전달받는다.args
: main(args)
를 전달 받아서 사용한다. 참고로 예제에서는 단순히 해당 값을 출력한다.tomcat.start()
에서 발생하는 예외는 잡아서 런타임 예외로 변경했다.@MySpringBootApplication
package hello.boot;
import org.springframework.context.annotation.ComponentScan;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}
MySpringBootMain
package hello;
import hello.boot.MySpringApplication;
import hello.boot.MySpringBootApplication;
@MySpringBootApplication
public class MySpringBootMain {
public static void main(String[] args) {
System.out.println("MySpringBootMain.main");
MySpringApplication.run(MySpringBootMain.class, args);
}
}
hello
에 위치한 이유는 @MySpringBootApplication
에 컴포넌트 스캔이 추가되어 있는데, 컴포넌트 스캔의 기본 동작은 해당 애노테이션이 붙은 클래스의 현재 패키지 부터 그 하위 패키지를 컴포넌트 스캔의 대상으로 사용하기 때문이다 애노테이션이 붙은 hello.MySpringBootMain
클래스의 패키지 위치는 hello
이므로 그 하위의 hello.spring.HelloController
를 컴포넌트 스캔한다.MySpringApplication.run(설정 정보, args)
이렇게 한줄로 실행하면 된다.--> 모양이 어디서 본 것과 비슷하다 바로 스프링 부트이다.
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}
스프링 부트 기능 제공
springboot-start 프로젝트를 새로 생성하였습니다.
내장 톰캣 의존관계 확인
spring-boot-starter-web
를 사용하면 내부에서 내장 톰캣을 사용한다.
스프링 부트 내부에서 스프링 컨테이너를 생성하는 코드
class ServletWebServerApplicationContextFactory implements ApplicationContextFactory {
...
private ConfigurableApplicationContext createContext() {
if (!AotDetector.useGeneratedArtifacts()) {
return new AnnotationConfigServletWebServerApplicationContext();
}
return new ServletWebServerApplicationContext();
}
}
스프링 부트 내부에서 내장 톰캣을 생성하는 코드
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
...
Tomcat tomcat = new Tomcat();
...
Connector connector = new Connector(this.protocol);
...
return getTomcatWebServer(tomcat);
}
내장 톰캣이 포함된 스프링 부트를 직접 빌드해보자.
localhost:8080/hello-spring 요청시
잘 호출되는 것을 확인 할 수있다.
스프링 부트 jar 분석 -> 압축풀기
jar -xvf boot-0.0.1-SNAPSHOT.jar
META-INF
org/springframework/boot/loader
BOOT-INF
실행 가능 Jar
jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고 동시에 만든 jar를 내부 jar를 포함해서 실행할 수 있게 한다. 이것을 실행 가능 Jar(Executable Jar)라고 한다.
Jar 실행 정보
java -jar xxx.jar
를 실행하게 되면 우선 META-INF/MANIFEST.MF
파일을 찾는다. 그리고 여기에
있는 Main-Class
를 읽어서 main()
메서드를 실행하게 된다. 스프링 부트가 만든 MANIFEST.MF
를
확인해보자.
META-INF/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
Main-Class
main()
이 있는 hello.boot.BootApplication
이 아니라 JarLauncher
라는 전혀 다른 클래스를 실행하고 있다.JarLauncher
는 스프링 부트가 빌드시에 넣어준다. org/springframework/boot/loader/JarLauncher
에 실제로 포함되어 있다.jar
내부에 jar를 읽어들이는 기능이 필요하다. 또 특별한 구조에 맞게 클래스 정보도JarLauncher
가 이런 일을 처리해준다. 이런 작업을 먼저 처리한 다음 Start-Class
에 지정된 main()
을 호출한다.Start-Class
: 우리가 기대한 main()
이 있는 hello.boot.BootApplication
가 적혀있다.Spring-Boot-Version
: 스프링 부트 버전Spring-Boot-Classes
: 개발한 클래스 경로Spring-Boot-Lib
: 라이브러리 경로Spring-Boot-Classpath-Index
: 외부 라이브러리 모음Spring-Boot-Layers-Index
: 스프링 부트 구조 정보실행 과정 정리
1. java -jar xxx.jar
2. MANIFEST.MF
인식
3. JarLauncher.main()
실행
BOOT-INF/classes/
인식
BOOT-INF/lib/
인식
4. BootApplication.main()
실행