본문 바로가기
Lecture/김영한 - 스프링 부트 - 핵심 원리와 활용

스프링 부트 - 핵심 원리와 활용 - 스프링 부트와 내장 톰캣

by Soono991 2023. 3. 12.

💡이 포스팅은 김영한 님의 인프런 강의인 스프링 부트 - 핵심 원리와 활용 수강하고 학습한 내용을 정리한 포스팅입니다.

 

김영한 님의 강의를 수강하며 정리한 GitHub Repository입니다.

 

GitHub - kiekk/inflearn-kyh-spring-boot

Contribute to kiekk/inflearn-kyh-spring-boot development by creating an account on GitHub.

github.com

 

이번 챕터에서 정리할 내용은 다음과 같습니다.

  • window Tomcat 실행 이슈
  • jar vs fat-jar vs executable jar(실행가능한 jar)
  • META-INF, BOOT-INF, JarLauncher
  • ServletWebServerApplicationContextFactory.create()
  • refresh() 분석

 

window 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();
    }
}

 

Tomcat과 Connector, Context를 사용하여 직접 서블릿을 등록하는 예제인데, 영한님 영상(Mac)에서는 정상적으로 동작했지만, window에서 실행하니 아래와 같은 에러가 발생했습니다.

 

org.apache.catalina.LifecycleException: Failed to start component [org.apache.catalina.webresources.StandardRoot@3023df74]

Caused by: java.lang.IllegalArgumentException: The main resource set specified [...\tomcat\tomcat.8080\webapps] is not valid ...

 

window에서 내장 톰캣을 실행할 경우 간혹 에러가 발생한다고 하는데, 영한님께서 아래 링크를 커뮤니티에 올려주셔서 참고해 보면 좋을 것 같습니다.

 

 

Micronaut application fails to start when using "Tomcat" HTTP Server · Issue #138 · micronaut-projects/micronaut-servlet

Task List Steps to reproduce provided Stacktrace (if present) provided Example that reproduces the problem uploaded to Github Full description of the issue provided (see below) Steps to Reproduce C...

github.com

 

그래서 이 문제를 해결하기 위해서는 다음과 같이 코드를 수정하면 됩니다.

 

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("", "/");
        
        // 코드 추가 - 시작
        File docBaseFile = new File(context.getDocBase());
        if (!docBaseFile.isAbsolute()) {
            docBaseFile = new File(((org.apache.catalina.Host) context.getParent()).getAppBaseFile(), docBaseFile.getPath());
        }

        docBaseFile.mkdirs();
    	// 코드 추가 - 종료
        
        tomcat.addServlet("", "helloServlet", new HelloServlet());
        context.addServletMappingDecoded("/hello-servlet", "helloServlet");
        tomcat.start();
    }
}

 

혹시 몰라 이번에 다시 실행해 보니 에러가 발생하지 않는 것으로 보아 간헐적으로 파일 경로를 인식하지 못하는 문제인 것 같습니다.

 

 

jar vs fat-jar vs executable jar(실행가능한 jar)

jar

작성한 코드를 단순히 build 하게 되면 아래와 같이 jar 파일이 하나 생성됩니다.

이 jar 파일을 실행하게 되면 아래와 같은 에러가 발생하는데요

 

생성한 jar 파일을 풀어 그 안의 내용을 확인해 보겠습니다.

 

우리가 작성한 Class 들은 있지만, lib가 없습니다.

왜냐하면 lib들도 전부 jar로 되어 있는데, 기본적으로 jar 안에는 다시 jar를 포함할 수 없기 때문입니다.

 

 

FatJar (Uber Jar)

이를 해결하기 위해 사용하는 것이 바로 Fat Jar 또는 Uber Jar라고 불리는 방법입니다.

jar안에는 jar만 포함할 수 없을 뿐이지 Class는 얼마든지 포함할 수 있기 때문에 FatJar는 이 방법을 사용하여 위 문제를 해결합니다.

즉, lib들(jar)을 모두 압축을 푼 다음 Class파일을 jar 파일에 추가하는 것입니다.

 

FatJar로 빌드하기 위해서는 build.gradle에 다음과 같이 코드를 추가해야 합니다.

 

//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
}

FatJar로 빌드를 한 후 압축을 풀어보면 아래와 같이 무수히 많은 class들이 추가되는 것을 확인할 수 있습니다.

 

안의 내용을 확인해 보면 우리가 작성했던 hello 외에 사용되는 lib들이 모두 들어있는 것을 확인할 수 있습니다.

 

그리고 용량도 거의 10MB 정도로 매우 커진 것을 확인할 수 있습니다.

FatJar를 사용하면 lib들의 모든 Class들을 가지고 있어야 하기 때문에 불필요하게 용량이 커진다는 단점이 있습니다.

그리고 중복된 파일이 있을 경우 둘 중에 하나만 사용해야 하는 문제가 발생합니다.

 

 

Executable Jar(실행가능한 Jar)

Spring Boot 프로젝트를 jar로 빌드하게 되면 이번에도 역시 용량이 큰 jar 파일이 생성된 것을 확인할 수 있습니다.

그래서 혹시 이것도 FatJar인가? 싶지만 압축을 해제하여 내용을 확인해보면 확연히 다른 것을 볼 수 있습니다.

 

기존에 존재하던 META-INF 외에 BOOT-INF, org 폴더가 생성되어 있습니다.

 

 

BOOT-INF

먼저 BOOT-INF 폴더를 확인해보면 아래와 같이 classes 폴더와 lib 폴더로 구성되어 있으며, classes에는 저희가 작성한 Class들이 있고 lib 폴더에는 jar 파일들이 들어있습니다.

분명 jar 안에는 jar를 포함할 수 없다고 했는데, Spring Boot가 바로 jar 안에 jar를 포함할 수 있도록 해줍니다.

그리고 이것을 실행가능한 jar(Executable Jar)라고 부릅니다.

이렇게 jar를 포함하게 되면 FatJar에서의 문제였던 중복된 파일 문제를 해결할 수 있습니다.

 

org (JarLauncher)

그리고 org 폴더가 별도로 생성되어 있고 내부를 확인해보면 Launcher나 Runner 같은 Class들이 들어있습니다.

왜 이들은 jar로 포함되어 있지 않고 별도의 Class들로 되어 있을까요?

 

이 부분을 이해하기 위해서는 META-INF/MANIFEST.mf 파일을 봐야 합니다.

 

 

MANIFEST.mf에 작성되어 있는 Main-Class를 보면 우리가 작성한 클래스가 아닙니다.

우리가 작성한 Main Class는 Start-Class에 작성되어 있습니다.

이는 Spring Boot에서 직접 하드코딩으로 작성해 놓은 것이며 jar를 실행하게 되면 우리가 작성한 클래스의 main 메서드를 실행하는 것이 아닌 JarLauncher의 main 메서드를 실행합니다.

 

 

그리고 JarLauncher는 MANIFEST.mf에 작성한 Start-Class를 가져와 실행하는 것으로 우리가 작성한 클래스를 실행하도록 하고 있습니다.

 

왜 굳이 우리가 작성한 클래스를 직접 실행하지 않고 JarLauncher에서 실행하도록 했을까?

이름에서도 유추할 수 있듯이 JarLauncher는 jar를 실행하는 녀석이라고 생각할 수 있습니다.

실행가능한 Jar는 자바 표준은 아니고 Spring Boot에서 지원하는 구조이기 때문에 jar 내부에서 다시 jar를 읽어올 수 있는 기능이 필요한데,

이 기능을 JarLauncher가 담당합니다.

먼저 JarLauncher가 jar를 읽는 작업을 마친 후 Start-Class에 지정된 main 메서드를 실행하는 것입니다.

 

MANIFEST.mf

이 파일에는 jar 파일의 메타데이터 정보가 들어있다고 볼 수 있는데, 아래 파일에서 Spring-Boot-로 시작하는 것은 Spring-Boot에서 직접 넣어준 옵션이라고 보면 됩니다.

build.gradle에서 war 파일에 옵션을 설정할 수 있는데, 아래와 같이 설정할 수 있습니다.

//일반 Jar 생성
task buildJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain' // 시작할 Main-Class
        attributes 'Build-Jdk-Spec': '17' // JDK 스펙
        attributes 'Created-By': 'shyoon991' // 생성자
    }
    with jar
}

이 외에도 더 많은 옵션이 있는데 이 부분은 아래 링크를 참고하시면 좋을 것 같습니다.

https://www.baeldung.com/java-jar-manifest

https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest

 

executable jar와 관련해서는 아래 공식 문서도 참고해보시면 좋을 것 같습니다.

 

The Executable Jar Format

PropertiesLauncher has a few special features that can be enabled with external properties (System properties, environment variables, manifest entries, or loader.properties). The following table describes these properties: Key Purpose loader.path Comma-sep

docs.spring.io

 

ServletWebServerApplicationContextFactory.create()

ServletWebServerApplicationContextFactory 객체는 ApplicatioContext 객체를 생성하는 Factory 클래스이며, SpringBootApplication.run() 메서드 실행 도중에 실행됩니다.

 

강의에서는 직접 DispatcherServlet을 구성하기 위해 Context도 직접 생성했는데 위와 같이 Spring Boot에서는 이 부분을 내부적으로 수행해 준다. 정도로만 알고 넘어가겠습니다.

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);
        }
    }
}

 

refresh() 분석

개인적으로 생각하기에 Spring Boot에서 가장 핵심적인 부분은 refresh() 메서드인 것 같습니다. 이 부분은 토비 님 강의를 학습할 때도 작성했던 내용인데 빠른 시일 내에 refresh() 메서드를 분석한 후 정리해 보도록 하겠습니다.

댓글