Back-End/Spring Advance & Boot

스프링 부트 - 부트와 내장 톰캣

Meluu_ 2024. 11. 19. 19:31

✔️ WAR 배포 방식의 단점


 

웹 애플리케이션을 개발하고 배포하려면 다음과 같은 과정을 거친다.

  1. 톰캣 같은 WAS 서버를 별도로 설치
  2. 애플리케이션 코드를 WAR로 빌드
  3. 빌드한 WAR 파일을 WAS에 배포

이러한 방식에는 단점이 존재

  • 톰캣 같은 WAS를 별도 설치
  • 개발 환경 설정이 복잡
    • 단순 자바면 main() 메서드만 실행
    • 웹 애플리케이션은 WAS 실행하고 WAR와 연동하기 위한 복잡한 설정이 들어감
  • 배포 과정이 복잡 (WAR 빌드 후 WAS에 배포)
  • 톰캣 버전 변경시 재설치

단순히 자바의 main() 메서드만 실행하면 웹 서버까지 같이 실행되도록하는 방법은 없을까?

톰캣 같은 웹서버를 라이브러리로 내장해버리는 것이다.  (내장 톰캣 기능)

 

 

✔️ 내장 톰캣


build.gradle 

plugins {
    id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    //스프링 MVC 추가
    implementation 'org.springframework:spring-webmvc:6.0.4'

    //내장 톰켓 추가
    implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
}

tasks.named('test') {
    useJUnitPlatform()
}

//일반 Jar 생성
task buildJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    with jar
}

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

 

EmbedTomcatSpringMain

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("", "/");

        //== 코드 추가 시작 webapp 폴더 문제 해결 ==

        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("", "dispatcher", dispatcher);
        context.addServletMappingDecoded("/", "dispatcher");
        tomcat.start();

    }
}

 

 

main() 메서드를 실행하면 다음과 같이 동작

  • 내장 톰캣 생성 후 8080 포트로 연결하도록 설정
  • 스프링 컨테이너를 만들고 필요한 빈을 등록
  • 스프링 MVC 디스패처 서블릿을 만들고 앞서만든 스프링 컨테이너에 연결
  • 디스패처 서블릿을 내장 톰캣에 등록
  • 내장 톰캣을 실행 

 

서블릿 컨테이너 초기화와 거의 같은 코드이며 ,

시작점이 개발자가 main() 메서드를 직접 실행하는가, 초기화 메서드를 통해서 실행하는가의 차이가 있을 뿐이다.

 

 

 

내장 톰캣을 애플리케이션에 포함했으므로 이번에는 빌드와 배포를 해보자

 

빌드

자바 main() 메서드를 실행하기 위해서는 jar 형식으로 빌드해야한다.

jar 안에는 META-INF / MANIFEST.MF 파일에 실행할 main() 메서드의 클래스를 지정해주어야한다. 

Manifest-Version: 1.0
Main-Class: hello.embed.EmbedTomcatSpringMain

 

 

Gradle의 도움을 받으면 쉽게 진행 가능

 

build.gradle

//일반 Jar 생성
task buildJar(type: Jar) {
    manifest {
        attributes 'Main-Class': 'hello.embed.EmbedTomcatSpringMain'
    }
    with jar
}

 

 

 

jar 빌드

window 기준 cmd 로 가서 해당 프로젝트 폴더로 이동

build/libs 로 이동 후 

gradlew clean buildJar

명령어 입력

 

그렇다면 jar 파일이 만들어졌을 것이다.

build/libs/xxx-0.0.1-SNAPSHOT.jar

 

이를 실행해보자

 

java -jar xxx-0.0.1-SNAPSHOT.jar

 

오류가 발생할 것이다. 이유는 압축을 풀어보면 알 수 있다.

 

압축해제

jar -xvf xxx-0.0.1-SNAPSHOT.jar

 

  • META-INF
    •  MANIFEST.MF
  • 패지키명
    • 내가 만든 클래스들

풀어보면 알겠지만 안에 외부 라이브러리가 하나도 없다. 그저 내가 만든 클래스 파일들만 들어있다. 

WAR는 내부에 라이브러리 역할을 하는 jar 파일을 포함하고 있다.

 

jar 파일은 jar 파일을 포함할 수 없다.

포함해도 인식이 안된다. 

 

때문에 라이브러리 jar 파일을 모두 구해서 MANIFEST 파일에 해당 경로를 적어주거나,

라이브러리역할을 하는 jar 파일을 항상 함께 가져야한다. (비권장)

 

그나마 나은 대안은 FatJar 이다.

 

jar 안에 jar를 포함할 수 없지만 class는 얼마든지 포함 가능하다.

즉 라이브러리들을 전부 풀어서 class들로 뽑아내 새로운 jar에 포함하는 것이다.

수많은 라이브러리들의 class가 포함되기에 Fat(뚱뚱한) jar가 된다. 

 

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

 

 다시 빌드해보자

gradlew clean buildFatJar 

 

빌드시 용량이 큰 것을 볼 수있다. 

 

 

빌드한 jar 파일을 실행하면 정상적으로 실행되는 것을 확인할 수 있다.

java -jar xxx-0.0.1-SNAPSHOT.jar 

 

압축해제하면 수많은 라이브러리 클래스들이 포함되어있다.

 

Fat Jar 정리

Fat Jar 장점

jar에 필요한 라이브러리 내장 가능

내장 톰캣 라이브러리 jar 내부에 내장 가능

하나의 jar파일로 배포, 웹서버 설치 + 실행까지 모든 것을 단순화

 

 

WAR 단점과 해결

WAR FatJar
톰캣 같은 WAS 별도 설치 내장 톰캣 라이브러리로 jar 내부에 포함되어 해결
개발 환경 복잡 main() 메서드만 실행 
배포 과정 복잡  JAR 빌드 후 원하는 위치에서 실행
버전 업데이트시 톰캣 재설치 gradle 에서 내장 톰캣 라이브러리 버전만 변경하고 빌드 후 실행

 

 

Fat Jar 단점

어떤 라이브러리가 포함되어 있는지 확인 어려움

파일명 중복을 해결할 수 없음

  • 클래스나 리소스 명이 같은 경우 하나를 포기해야함
  • 서로 다른 라이브러리가 내부에 같은 이름의 클래스를 소유하고 있을때 Fat Jar를 만들면  둘 다 가지고 있는 파일 중에 하나의 파일만 선택되어 하나는 정상 동작하지 않는다. 

 

 

✔️ 편리한 부트 클래스 만들기


지금까지 진행한 과정을 편리하게 처리해주는 나만의 부트 클래스를 만들자.

 

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 dispatcher = new DispatcherServlet(appContext);

        // 디스패처 서블릿 등록
        Context context = tomcat.addContext("", "/");

        //== 코드 추가 시작 webapps 폴더 문제 해결 ==

        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("", "dispatcher", dispatcher);
        context.addServletMappingDecoded("/", "dispatcher");

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

    }
}

run()을 실행하면 바로 작동한다.

configClass : 스프링 설정을 파라미터로 전달받는다.

args : main(args) 를 전달 받아서 사용한다. 

 

 

@MySpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
public @interface MySpringBootApplication {
}

컴포넌트 스캔 기능이 추가된 단순한 애노테이션

시작할 때 이 애노테이션을 붙여서 사용

 

MySpringBootAppMain

@MySpringBootApplication
public class MySpringBootMain {

    public static void main(String[] args) {
        System.out.println("MySpringBootMain.main");
        MySpringApplication.run(MySpringBootMain.class, args);
    }
}

패키지 위치가 중요하다. @ComponentScan 의 기본 동작은 해당 애노테이션이 붙은 현재 패키지 + 그 하위 패키지

MySpringApplication.run(설정 정보, args); 이렇게 한줄 실행하면 된다.

내장 톰캣 실행, 스프링 컨테이너 생성, 디스패처 서블릿, 컴포넌트 스캔까지 모든 기능이 한번에 동작

 

 

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

 

 

 

 

✔️ 스프링 부트


스프링 부트는 지금까지의 고민한 문제들을 해결

  • 내장 톰캣을 사용해서 빌드와 배포를 편리하게 한다.
  • 빌드시 하나의 Jar를 사용하면서, Fat Jar 문제도 해결
  • 내장 톰캣 서버를 실행하기 위한 복잡한 과정을 모두 자동으로 처리

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.5'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

최신버전 스프링 부트로 보고싶어서 start.spring.io 에서 임의의 프로젝트를 만들고 build.gradle을 살펴봤다. 

java 버전을 지정, test관련 빼고 다 똑같다. 

 

라이브러리 의존관계를 따라가보면 내장 톰캣(tomcat-embed-core)이 포함되어있다.

 

 

 

 

 스프링 부트와 웹서버 - 실행 과정

@SpringBootApplication
public class BootApplication {

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

 

스프링 부트를 실행할 때는 자바 main() 메서드에서 SpringApplication.run() 을 호출해주면 된다 .

메인 설정 정보를 넘겨주는데, 보통 @SpringBootApplication 애노테이션이 있는 현재 클래스를 지정해주면 된다. 

 

 

해당  코드 한줄에서 핵심은 2가지

1. 스프링 컨테이너 생성

2. WAS(내장 톰캣) 생성

 

 

 

스프링 부트 - 빌드와 배포

빌드

gradlew clean build

 

실행하면 정상적으로 실행된다. 

jar를 압축해제하면 신기한 관경을 볼 수 있다.

 

바로 lib 폴더에 jar 형태로 라이브러리들이 들어있다.

분명히 위에서 jar는 jar를 포함할 수 없다고했는데 이게 무슨일인지 알아보자

 

 

스프링 부트 실행 가능 Jar

스프링 부트는 Fat Jar의 문제를 해결하기 위해

jar 내부에 jar를 포함할 수 있는 특별한 구조의 jar를 만들고

동시에 만든 jar를 내부 jar를 포함해서 실행 가능하게 함

 

이로써 어떤 라이브러리가 포함되어있는지 쉽게 확인이 가능해졌으며

파일명 중복 문제도 해결된다. (jar 파일을 압축해제해서 파일명 중복 문제가 생겼던 것이므로)

 

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

 

Jar 실행 정보

java -jar xxx.jar 를 실행하게 되면 우선 MAINFEST.MF 파일을 찾고, 이 안에 Main-Class를 읽어서 main() 메서드를 실행한다.

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.boot.BootApplication
#...

Main-Class 에 BootApplication이 아니라 JarLauncher 라는 클래스가 되어있다. 

 

이는 스프링 부트가 빌드시에 넣어준 것이며,

jar내부에 jar를 읽어들이는 기능, 특별한 구조에 맞게 클래스 정보를 읽는 일을 JarLauncher가 처리한다.

그 다음 Start-Class 에 지정된 main()을 호출한다. 

 

 

스프링 부트 로더

org/springframework/boot/loader 하위에 있는 클래스들

JarLauncher을 포함한 스프링 부트가 제공하는 실행가능 Jar를 실제 구동시키는 클래스들이 포함

 

 

실행 과정

  1. java -jar xxx.jar
  2. MANIFEST.MF 인식
  3. JarLauncher.main() 실행
  4. BOOT-INF/classes/ 인식
  5. BOOT-INF/lib/ 인식
  6. BootApplication.main() 실행

 

 

참고

당연히 IDE에서 직접 실행할때는 JarLacunher가 필요없다.

 

 

 

 

✔️ 스프링 부트  스타터와 라이브러리 관리


하나의 포스트로 만들기에는 양이적어 여기에다 작성한다. 

 

라이브러리 자동 관리

각 라이브러리 버전 호환 문제때문에 골치아프다. 

스프링 부트는 버전을 직접 관리해준다.

plugins {
	id 'io.spring.dependency-management' version '1.1.6'
}

현재 부트 버전에 가장 적절한 외부 라이브러리 버전을 자동으로 관리해준다. 

 

따라서 라이브러리 버전을 명시할 필요가 사라졌다.

 

 

스프링 부트 스타터

스프링 부트 스타터 라이브러리는 하나로 웹 프로젝트에 필요한 대중적인 라이브러리를 모두 포함해준다.

dependencies {
 //3. 스프링 부트 스타터
 implementation 'org.springframework.boot:spring-boot-starter-web'
}

 

자세한 건 아래를 참조하자.

스프링 부트 스타터 목록

 

Build Systems :: Spring Boot

Each release of Spring Boot provides a curated list of dependencies that it supports. In practice, you do not need to provide a version for any of these dependencies in your build configuration, as Spring Boot manages that for you. When you upgrade Spring

docs.spring.io

 

 

🔖 학습내용 출처


스프링 부트 - 핵심 원리와 활용 / 김영한