Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

스프링 부트와 내장 톰캣 본문

Spring/Spring boot - 핵심 원리와 활용

스프링 부트와 내장 톰캣

studyHub 2025. 1. 25. 15:53

WAR 배포 방식은 WAS를 별도 설치해야하고, 빌드한 WAR 파일을 WAS에 배포해야 하는 번거로움이 있다. 스프링 부트는 이러한 과정없이 내장 톰캣을 제공해서 이러한 문제를 해결했다.

 

내장 톰캣을 실행해 배포 과정을 단순화 해보고,

나아가 내장 톰캣 실행, 스프링 컨테이너 생성, 디스패처 서블릿 등록의 모든 과정을 편리하게 처리해주는 스프링 부트를 직접 만들어보자.


내장 톰캣(embed tomcat) 기능

스프링 부트는 단순히 main()메서드만 실행하면 웹 서버까지 실행된다. 톰캣도 자바로 만들어져 있으니 톰캣을 마치 하나의 라이브러리로 포함해서 사용하는 것이다.

 

애플리케이션 JAR 안에 다양한 라이브러리들과 WAS 라이브러리가 포함되는 방식, main() 메서드를 실행해서 동작한다.
내장 톰캣 라이브러리 implementation 'org.apache.tomcat.embed:tomcat-embed-core'

 

main 메서드를 실행해서 톰캣을 띄우는 코드

public static void main(String[] args) throws LifecycleException {

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

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

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

    //서블릿 컨테이너에 디스패처 서블릿 등록
    Context context = tomcat.addContext("", "/");
    tomcat.addServlet("", "dispatcherServlet", dispatcherServlet);
    context.addServletMappingDecoded("/", "dispatcherServlet");

    tomcat.start();
}

 

 

빌드와 배포

main() 메서드를 실행하기 위해서는 JAR 형식으로 빌드해야 한다. 그리고 JAR 안에는 META-INF/MANIFEST.MF 파일에 실행할 main() 메서드의 클래스를 지정해주어야 한다. Gradle의 도움을 받으면 이 과정을 쉽게 진행할 수 있다.

 

그런데 JAR 파일은 내부에 라이브러리 역할을 하는 JAR 파일을 포함할 수 없다. 대안으로 Fat Jar 를 사용하면 된다.

JAR는 클래스를 포함할 수 있다. 라이브러리에 사용되는 JAR 를 풀면 나오는 class 들을 새로 만드는 JAR 에 포함하는 것이다. 이렇게 하면 수 많은 라이브러리에서 나오는 class 때문에 뚱뚱한(fat) JAR 가 탄생한다.

 

build.gradle

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

 

명령어

  • 빌드 : ./gradlew clean buildFatJar
  • 실행 : java -jar embed-0.0.1-SNAPSHOT.jar

이로써 JAR를 만들고 이것을 원하는 위치에서 실행만 하면 되는 단순한 배포 과정이 완성됐다.

 

여기서 문제는 Fat Jar에 있다.

  1. 모두 class 로 풀려있으니 어떤 라이브러리가 사용되고 있는지 추적하기 어렵다.
  2. 파일명 중복을 해결할 수 없다.
    예를 들어 META-INF/services/jakarta.servlet.ServletContainerInitializer 이 파일이 여러 라이브러리(jar)에 있을 수 있는데, 여러 라이브러리에서 해당 파일을 포함할 경우 파일명이 충돌해서 하나의 파일만 남아 정상 실행이 안된다.


편리한 부트 클래스 만들기

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

 

1. 지금까지 진행한 내장 톰캣 실행, 스프링 컨테이너 생성, 디스패처 서블릿 등록의 모든 과정을 묶어 run()메소드로 만들고, 인자로 스프링 설정 정보를 전달한다.

public class MySpringApplication {

    public static void run(Class configClass, String[] args){
        System.out.println("MySpringApplication.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 dispatcherServlet = new DispatcherServlet(appContext);

        //서블릿 컨테이너에 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");
        tomcat.addServlet("", "dispatcherServlet", dispatcherServlet);
        context.addServletMappingDecoded("/", "dispatcherServlet");

        try {
            tomcat.start();
        } catch (LifecycleException e) {
            throw new RuntimeException(e);
        }
    }
}

 

2. 설정 정보를 컴포넌트 스캔으로 전달하기 위해, @ComponentScan을 포함한 애노테이션을 만든다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented //문서화
@ComponentScan //컴포넌트 스캔 기능
public @interface MySpringBootApplication {
}

 

3. Main 메서드를 포함한 클래스에 위 애노테이션을 붙이고, run() 메소드를 호출한다.

(컴포넌트 스캔을 붙였으므로 최상단 패키지에 생성)

@MySpringBootApplication
public class MySpringBootMain {
    public static void main(String[] args) {
        MySpringApplication.run(MySpringBootMain.class, args);
    }
}

 

지금까지 만든 것을 라이브러리로 만들어서 배포한다면 그것이 바로 스프링 부트이다!

 


스프링 부트

스프링 부트 메인 클래스를 보면 직접 만든 스프링 부트와 똑같다.

@SpringBootApplication
public class BootApplication {

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

}

 

위 run 메소드를 뜯어보면 내장 톰캣을 생성하고, 스프링 컨테이너를 생성/연결하는 로직이 다 들어있다.

 

빌드와 배포

  • 빌드 : ./gradlew clean build
  • 실행 : java -jar boot-0.0.1-SNAPSHOT.jar

 

스프링 부트 jar 분석

jar를 풀어보면 구조가 아래와 같다.

Fat Jar가 아니라 처음보는 새로운 구조로 만들어져 있다. 심지어 jar 내부에 jar가 포함되어 있고, 인식까지 되었다.

 

스프링 부트 실행 가능 Jar

스프링 부트는 jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고 동시에 만든 jar 를 내부 jar를 포함해서 실행할 수 있게 했다. 이것을 실행 가능 Jar(Executable Jar)라 한다. jar 내부에 jar를 포함하기 때문에 어떤 라이브러리가 포함되어 있는지 쉽게 확인할 수 있고, jar 내 파일명 중복을 해결할 수 있다.

 

  • META-INF
    • MANIFEST.MF
  • org/springframework/boot/loader
    • JarLauncher.class : 스프링 부트 main() 실행 클래스
  • BOOT-INF
    • classes : 우리가 개발한 class 파일과 리소스 파일
    • lib : 외부 라이브러리 (jar 포함)
    • classpath.idx : 외부 라이브러리 경로
    • layers.idx : 스프링 부트 구조 경로

java -jar xxx.jar 를 실행하게 되면 우선 META-INF/MANIFEST.MF 파일을 찾는다. 그리고 여기에 있는 Main-Class 를 읽어서 main() 메서드를 실행하게 된다. 스프링 부트가 만든 MANIFEST.MF 는 조금 다르게 조작되어 있다.

Main-Class: org.springframework.boot.loader.JarLauncher //부트가 만든 main 클래스
Start-Class: hello.boot.BootApplication //우리의 main 클래스

 

JarLauncher 는 스프링 부트가 빌드시에 넣어준다. org/springframework/boot/loader/ JarLauncher 에 포함되어 있다. JarLauncher 를 통해서 BOOT-INF 에 있는 classes 와 lib 에 있는 jar 파일들을 읽어들인다.

이런 작업을 먼저 처리한 다음 StartClass: 에 지정된 main() 을 호출한다.

 

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

 

 

 


참고 자료 & 이미지 출처
스프링 부트 - 핵심 원리와 활용 (김영한 님)