1. 서론
도커를 설치했다면, 도커 컨테이너를 만들 준비가 완료되었다는 뜻이다. 그렇다면 이제 이미지를 만들어 이를 컨테이너에 담아 우리가 원하는 어플리케이션으로 동작하도록 해야 한다. 당연히 이번의 경우, 컨테이너에 담길 어플리케이션은 스프링부트 어플리케이션이다. 스프링부트 어플리케이션을 이미지로 만들기 위해서는 스프링부트 프로젝트를 빌드해서 나온 산출물인 Executable JAR 파일이 필요하다. 따라서 본격적인 Docker 활용 전에, 웹 어플리케이션 구동과 스프링부트 프로젝트 빌드에 대해 살펴보자.
2. Java 웹 어플리케이션 구동 방식
Java 웹 어플리케이션 구동 방식에는 2가지가 있다.
- 외장 WAS : 톰캣과 같은 Web Application Server(이하 WAS) 내부에 WAR 파일을 배포하여 구동되는 방식이다. WAR 파일은 WAS가 실행되는 시점에 웹 어플리케이션 설정과 리소스를 로드한다. WAR 파일이 내포하고 있는 JSP나 Servlet과 같은 웹 어플리케이션 컴포넌트들이 WAS 컨테이너 위에서 실행되어야 하므로, WAR 파일로 웹 어플리케이션을 동작시키기 위해서는 이를 실행시킬 별도의 WAS가 필요하다.
- 내장 WAS : Executable JAR 안에 WAS가 내장되어 있는 형식이다. Executable JAR는 "실행 가능"한 JAR 파일로, 자바 표준에서 정의된 것은 아니며 스프링부트에서 새롭게 정의한 형식이다. 본래 자바 표준에서 JAR 안에 다른 JAR를 내포시키는 것은 불가능하지만, Executable JAR는 이를 가능하게 한다. 따라서 톰캣(WAS)이 내장되어 있으므로 별도의 외장 WAS가 존재하지 않아도, JAR 파일을 실행시킴으로써 웹 어플리케이션을 구동시킬 수 있다.
스프링부트 등장 이전 Java 웹 어플리케이션 구동을 위해서는 WAR 파일로 배포해야 했고, WAR 파일을 실행시키기 위해서는 톰캣과 같은 WAS가 별도로 필요했다. 이는 개발 환경 설정과 배포 과정의 복잡하게 만들었다. 스프링부트는 이를 해결하기 위해 Executable JAR와 내장 WAS Library를 도입해 빌드와 배포를 편리하게 만들었다. 따라서 스프링부트 프로젝트 빌드 시 JAR 파일이 생성되며, 이 Executable JAR 하나만으로 웹 어플리케이션을 배포할 수 있다.
build.gradle
...
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-web'
...
}
...
스프링부트 프로젝트의 build.gradle에 상기한 'org.springframework.boot:spring-boot-starter-web' 의존성을 추가하면, 해당 라이브러리 내부에 내장 톰캣이 포함되어 스프링부트 어플리케이션 실행 시 내장된 WAS가 구동되는 것이다.
3. Gradlew
먼저 빌드란 소스 코드와 프로젝트에 쓰인 리소스 등을 JVM이나 WAS가 인식할 수 있는 구조로 배포를 위해 패키징하는 과정 내지는 결과이다. 스프링부트 프로젝트를 빌드하게 되면, 상술한대로 하나의 JAR 파일로 배포할 수 있게 된다. 즉, 우리의 소스 코드와 자원을 하나의 정돈된 기계처럼 패키징해야 하는데, 이 패키징 작업을 담당하고 쉽게 만들어주는 것이 바로 빌드 도구이다.
Java 프로젝트 빌드 도구에는 주로 Maven, Gradle이 쓰이며, 필자의 프로젝트는 Gradle을 사용했으므로 이를 기준으로 설명하겠다. Gradle의 가장 큰 특징 중 하나는 Gradle을 따로 설치하지 않고도 Gradle Wrapper를 이용해 프로젝트를 빌드시킬 수 있다는 것이다. Gradle Wrapper는 Gradle이 설치되어 있지 않아도 Gradle task를 실행할 수 있도록 해주는 파일이다. 스프링부트 gradle 프로젝트를 생성했다면, 프로젝트 경로에 gradlew 파일이 존재할 것이다. 이는 Gradle Wrapper 실행 스크립트이며, 이를 통해 컴파일이나, 빌드를 할 수 있다.
- gradlew를 이용하면, 별도의 설치 없이도 바로 빌드할 수 있다. 버전을 신경 쓸 필요도 없이 프로젝트와 동일한 버전의 Gradle이 사용된다. 즉, 환경에 의존하지 않고 일관성 있는 빌드가 보장되므로 가장 권장되는 방식이다.
따라서 우리는 스프링부트 프로젝트 빌드를 위해 gradlew 스크립트 파일을 사용할 것이다.
4. 스프링부트 프로젝트 빌드
./gradlew <tasks>
터미널에서 gradlew가 존재하는 프로젝트 경로로 이동한 뒤, 위와 같은 형식으로 명령어를 입력하면 원하는 gradle task를 실행시킬 수 있다. 우리가 주로 사용할 task는 다음과 같은 2가지이다.
./gradlew build
- Library dependency check, Resource file packaging, Source code compile, Test code execution와 같은 과정을 자동으로 수행한 뒤, 어플리케이션 소스 코드를 실체화시켜 최종 산출물을 생성한다.
- 이 task가 완료되면 해당 프로젝트 경로에 build 폴더가 생성되며 디렉토리 구조는 다음과 같다.
build
|- - classes(프로젝트 자바 소스가 컴파일이 완료된 상태로 .class 파일이 존재한다.)
|- - generated
|- - libs(모든 컴파일이 완료되고 실행 준비가 되어 패키징된 JAR 파일이 존재한다.)
|- - reports(Test 결과를 요약한 html 보고서 포함, 빌드 실패 시 내부 경로의 tests/test/index.html에서 확인 가능.)
|- - resources
|- - test-results
|- - tmp
./gradlew clean
- build를 통해 생성된 gradle cache나 directory등 모든 산출물을 삭제하는 task이다.
- 이 task를 통해 build 환경을 초기화시켜, 새로운 build의 안정성을 향상시킨다. 이전 build에서 생성된 캐시나 디렉토리의 영향을 받지 않으므로 빌드 시간이 단축될 수도 있으며(캐시가 많을 경우), 이전 build에서 발생한 오류가 남아있지 않기 때문에 더 안정적인 빌드가 가능해진다.
따라서 같은 환경에서 계속해서 빌드를 시도한다면, 안정적인 빌드 수행을 위해 ./gradlew clean build 명령어를 사용하는 것이 유리하다. 해당 명령어를 사용하면 매 빌드 시도마다 clean task 후 build task가 수행되므로 빌드 안정성을 확보할 수 있게 된다.
이제 우리의 프로젝트 경로로 가서 ./gradlew clean build 명령어를 입력해보자.
PS C:\close\shds\urcarcher\urcarcher-be> ./gradlew clean build
Welcome to Gradle 8.8!
Here are the highlights of this release:
- Running Gradle on Java 22
- Configurable Gradle daemon JVM
- Improved IDE performance for large projects
For more details see https://docs.gradle.org/8.8/release-notes.html
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
> Task :compileJava
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: C:\close\shds\urcarcher\urcarcher-be\src\main\java\com\urcarcher\be\rani\TravelCourseController.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
BUILD SUCCESSFUL in 1m 6s
8 actionable tasks: 7 executed, 1 up-to-date
위처럼 build가 성공하면, 전술한대로 build 디렉토리가 생성되는데, 우리는 build/libs 경로에 존재하는 JAR 파일에 가장 주목해야 한다. 이 JAR 파일이 바로 우리가 배포에 사용할 Executable JAR이기 때문이다.
build
|- - libs
|- urcarcher-be-0.0.1-SNAPSHOT.jar
|- urcarcher-be-0.0.1-SNAPSHOT-plain.jar
우리가 주목해야하는 파일은 urcarcher-be-0.0.1-SNAPSHOT.jar이다. urcarcher-be-0.0.1-SNAPSHOT-plain.jar은 프로젝트에서 순수하게 우리가 개발한 코드만 JAR로 build된 것이다. 프로젝트의 모든 것이(외부 라이브러리 등) 담긴 JAR 파일은 urcarcher-be-0.0.1-SNAPSHOT.jar이다. 따라서 이 Executable JAR 파일을 java -jar build/libs/urcarcher-be-0.0.1-SNAPSHOT.jar 명령어를 통해 실행시킬 수 있다. 즉, WAS가 내장된 웹 어플리케이션이 구동되는 것이다.
5. Executable JAR 실행 원리
Executable JAR은 말 그대로 "실행" 가능하다고 언급했다. 그럼 어떤 방식으로 실행되어 웹 어플리케이션이 구동되는 것일까? 이를 알기 위해서는 Executable JAR의 내부 구조를 파악할 필요가 있다.
해당하는 Executable JAR의 압축을 풀어보면 내부 구조를 쉽게 파악할 수 있는데, 내부 구조는 다음과 같다.
urcarcher-be-0.0.1-SNAPSHOT.jar
|- - META-INF
|- MANIFEST.MF(JAR 실행 정보가 담겨 있음.)
|- - BOOT-INF
|- - classes(우리가 개발한 .class 파일 및 resource 파일)
|- - lib(프로젝트에 쓰인 외부 라이브러리들)
|- - org/springframework/boot/loader(JarLauncher 포함 돼 있음.)
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.urcarcher.be.UrcarcherBeApplication
Spring-Boot-Version: 3.3.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
Implementation-Title: urcarcher-be
Implementation-Version: 0.0.1-SNAPSHOT
이를 토대로 Executable JAR의 실행 원리를 파악해보면 다음과 같다.
- java -jar <jar file directory> 명령어가 실행되면 먼저 MANIFEST.MF를 찾는다.
- MANIFEST.MF의 Main-Class인 JarLauncher의 메인 메서드를 실행한다.
- 이에 따라 JarLauncher는 JAR 내부의 JAR들을 읽어들이는 기능과 구조에 맞게 클래스 정보를 읽어들인다.
- 이후 JarLauncher는 Start-Class에 지정된 class의 메인 메서드를 호출한다. 즉, 우리의 스프링부트 어플리케이션이 구동된다.
Main-Class를 제외한 나머지는 자바 표준이 아닌 스프링 부트가 임의로 사용하는 정보이며, IDE에서 프로젝트 구동 시에는 IDE의 도움을 받아 라이브러리를 인식하기 때문에 JarLauncher가 필요하지 않다.
References
- https://velog.io/@alsgus92/Android-Gradle-Task%EB%8A%94-%EB%8F%84%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%96%A4-%EC%97%AD%ED%95%A0%EC%9D%84-%EC%88%98%ED%96%89-%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C
- https://hanseom.tistory.com/331
- https://velog.io/@suhsein/Spring-WAR-JAR-Executable-JAR
- https://isaac-christian.tistory.com/entry/AWS-Spring-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-WAR-%ED%8C%8C%EC%9D%BC%EC%9D%84-Tomcat%EC%97%90-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0
- https://velog.io/@maldaliza/%EB%B9%8C%EB%93%9CBuild%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C
- https://velog.io/@alsgus92/Android-Gradle-Task%EB%8A%94-%EB%8F%84%EB%8C%80%EC%B2%B4-%EC%96%B4%EB%96%A4-%EC%97%AD%ED%95%A0%EC%9D%84-%EC%88%98%ED%96%89-%ED%95%98%EB%8A%94-%EA%B1%B8%EA%B9%8C
'Docker' 카테고리의 다른 글
[Docker] Spring boot App 배포 (4) - 서버에 배포하기 (0) | 2024.11.28 |
---|---|
[Docker] Spring boot App 배포 (2) - EC2 기본 + Docker 설치 (1) | 2024.11.23 |
[Docker] Spring boot App 배포 (1) - Docker 기본 (1) | 2024.11.19 |