개요
요즘 Docker를 공부하고 있다. 가이드를 보면서 따라하다가 내 프로젝트를 직접 올려보기로 했다.
올려보려는 프로젝트는 사내에서 개발하려는 인사평가시스템을 개발하기 전에 집에서 먼저 개발해 봤던 프로젝트다.
회사에서 사용하는 기술에 맞춰 개발하기 위해 Mybatis, JSP를 사용해서 개발을 했다. 이 JSP 때문에 Docker에 올리는 작업이 골치 아팠다.
개발 환경
- Java 8
- Spring 2.7
- Mybatis
- JSP
- Gradle
- Jar -> War 변경
- IntelliJ
Docker에 올려보기
1. Dockerfile 작성
Dockerfile은 도커 이미지를 생성하기 위한 레시피같은 역할을 한다.
Dockerfile을 통해 이미지 생성부터 실행까지의 과정을 문서화하고 자동화할 수 있다.
Dockerfile은 프로젝트의 루트 디렉토리에서 생성했다.
이제 레시피를 작성해보자
# Java8을 사용하는 Docker 이미지를 만들기 위한 설정 파일
FROM openjdk:8-jdk-alpine
# 빌드한 jar파일을 컨테이너에 복사
COPY build/libs/*.jar app.jar
# 도커에게 컨테이너가 8080 포트를 외부에 노출할 것이라고 알려주는 명령어
EXPOSE 8080
# 컨테이너가 시작되면 실행할 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]
- FROM <이미지명>:<태그> : 도커의 베이스 이미지를 설정하는 명령어이다. 내 프로젝트는 Java8을 사용하기 때문에 Java8의 베이스 이미지를 가져왔다.
- COPY <복사할 파일 경로> <붙여 넣기 할 경로> : 지정한 파일을 지정한 위치로 복사해주는 명령어이다. 여기서는 빌드한 jar파일을 컨테이너로 옮기는 역할을 하고 있다.
- EXPOSE <포트> : 도커에게 컨테이너가 실행될 때 지정한 포트로 네트워크 수신할 것이라고 알려주는 명령어이다. docker run 명령어의 -p 옵션과 혼동하는 경우가 많은데(나도 그랬다), EXPOSE는 단순히 포트를 사용할 것이라고 알려주는 역할일 뿐이다. 호스트 포트와 연결하여 외부에 노출시키기 위해서는 docker run 명령어의 -p 옵션을 사용해야 한다.
- ENTRYPOINT [<실행할 파일>, <파라미터1>, <파라미터2>] : 컨테이너가 시작될 때 항상 실행하게 해주는 명령어. 비슷한 명령어로는 CMD가 있는데 CMD는 docker run 명령어로 덮어씌울 수 있지만 ENTRYPOINT는 무조건적으로 실행된다. 여기서는 컨테이너가 실행될 때 복사해 온 jar를 실행하도록 설정한 것이다. 자동으로 java -jar app.jar 명령어를 실행해주는 것이라고 보면 된다.
더 자세한 내용은 Docker 공식 문서를 확인하자. 아주 잘 정리되어 있다. (AI까지 있다ㅋㅋ)
2. JAR 생성
이제 컨테이너로 넘길 JAR파일을 생성해야 한다. 아래 명령어는 프로젝트의 루트 디렉토리에서 실행해야한다.
$ ./gradlew bootJar
생성된 JAR파일은 build/libs 디렉토리에서 확인 가능하다.
$ cd build/libs
$ ls
myapp-0.0.1-SNAPSHOT.jar
3. 이미지 생성
아래 명령어로 이미지를 생성한다. 뒤에 '.'을 놓치기 쉬운데 꼭 넣어줘야 한다.
docker build -t <이미지명> .
$ docker build -t myapp .
생성된 이미지는 아래 명령어를 통해 확인할 수 있다.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp latest 97fc9ec41404 1 days ago 143MB
4. 컨테이너 실행
이제 만들어놓은 레시피를 담은 이미지를 바탕으로 컨테이너를 실행한다.
$ docker run --rm -p 8080:8080 myapp
- run : Docker 컨테이너를 생성하고 실행하는 명령어
- --rm : 컨테이너가 종료되면 자동으로 컨테이너를 제거한다. 일회성 실행이기 때문에 해당 옵션을 넣었다.
- -p : 포트 매핑을 설정한다. 여기서는 호스트의 8080 포트를 컨테이너의 8080 포트에 연결한다.
이제 잘되는지 확인해보자
실패 - Whitelabel Error Page(404 Not Found)
Whitelabel Error Page(404 Not Found)가 나온다. JSP를 찾지 못하고 있다.
지금까지 IDE로 실행했을 때는 JSP를 잘 가져와서 보여줬었는데, JAR 파일로 실행을 하니 찾지 못하고 있는 것이다. 왜 그럴까?
원인
1. JAR와 WAR 패키징 차이
JAR (Java Archive)는 Java 애플리케이션이나 라이브러리를 패키징하는데 사용된다. WAR에 비해 내부 구조가 단순하며, 주로 컴파일된 클래스 파일과 리소스를 포함한다. JAR는 웹 리소스를 포함하지 않기 때문에 WEB-INF 디렉토리도 패키징하지 않는다.
WAR (Web Applcation Archive)는 Java 웹 애플리케이션을 패키징하는데 사용된다. WEB-INF 디렉토리를 반드시 포함하기 때문에 JSP, HTML, JavaScript, CSS 등의 웹 리소스도 함께 패키징된다.
JAR는 WEB-INF 디렉토리를 패키징하지 않기 때문에 당연히 WEB-INF 디렉토리에 포함되어 있는 JSP도 패키징되지 않았던 것이다.
# JAR 패키징 디렉토리 구조
my-application.jar
├── META-INF/
│ └── MANIFEST.MF
├── org/
│ └── myapp/
│ ├── Application.class
│ └── ...
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── templates/
└── ...
# WAR 패키징 디렉토리 구조
my-application.war
├── META-INF/
│ └── MANIFEST.MF
├── WEB-INF/
│ ├── classes/
│ │ └── org/
│ │ └── myapp/
│ │ ├── Application.class
│ │ └── ...
│ ├── lib/
│ │ └── dependency.jar
│ └── web.xml
├── static/
│ ├── css/
│ ├── js/
│ └── images/
└── WEB-INF/jsp/
└── ...
2. JSP를 지원하지 않는 Spring Boot
Spring Boot로 내장 톰캣이 포함되어 JAR를 패키징한다. 그래서 외부 WAS 없이도 애플리케이션을 실행할 수 있다. Dockerfile의 베이스 이미지에서 Java8만 받을 수 있었던 이유도 내장 톰캣이 있기 때문이다. 하지만 Spring Boot의 내장 톰캣은 JSP를 지원하지 않는다. Spring 공식 가이드 문서에서도 JSP를 사용하고 싶다면 WAR를 사용하라고 권장하고 있다.
해결
위와 같은 원인으로 인해 패키징 방식을 JAR에서 WAR로 변경하기로 했다. WAR로 변경하면서 수정해야 할 내용들이 있다.
build.gradle 수정
plugins {
id 'java'
id 'war' // 추가
id 'org.springframework.boot' version '2.7.15'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
Dockerfile 수정
이제는 내장 톰캣이 아닌 외장 톰캣 위에 War를 올려서 배포해야 하기 때문에 java8을 포함하고 있는 tomcat 베이스 이미지로 설정해주었다. 그리고 tomcat 베이스 이미지 환경에 맞춰 실행 흐름을 다시 작성해줘야 한다.
# Java 8을 사용하는 Docker 이미지를 만들기 위한 설정 파일
FROM tomcat:8.5.69-jdk8-openjdk
# 빌드한 war파일을 컨테이너에 복사
COPY build/libs/*.war /usr/local/tomcat/webapps/ROOT.war
# 도커에게 컨테이너가 8080 포트를 외부에 노출할 것이라고 알려주는 명령어
EXPOSE 8080
# 컨테이너가 시작되면서 톰캣을 실행할 명령어
CMD ["catalina.sh", "run"]
- FROM tomcat:8.5.69-jdk8-openjdk : Java8이 포함된 Tomcat 베이스 이미지를 설정해주었다. dockerhub의 tomcat 이미지에서 자신에게 필요한 설정을 가진 태그를 찾으면 된다.
- COPY build/libs/*.war /usr/local/tomcat/webapps/ROOT.war : 빌드한 war파일을 복사해주고 있다.
- /usr/local/tomcat/webapps : 기본적으로 $CATALINA_HOME/webapp 디렉토리가 Tomcat 배포 디렉토리이다. Tomcat은 이 디렉토리를 자동으로 스캔해서 WAR 파일을 배포한다. Tomcat 공식 이미지에서는 $CATALINA_HOME을 /usr/local/tomcat으로 설정해두었기 때문에 위 경로에 war파일을 복사해주었다. Dokcerhub Tomcat 이미지 페이지에 사용법이 나와있다.
- ROOT.war : ROOT는 임의로 작성한 파일명이 아니다. Tomcat은 기본적으로 ROOT.war만 스캔해간다. 다른 파일명을 사용하고 싶다면 별도 설정을 해주어야 한다.
- CMD ["catalina.sh", "run"] : 컨테이너가 시작되면서 톰캣을 실행해주는 명령어이다.
다시 Docker에 올려보자
$ ./gradlew clean build
$ docker build -t myapp .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp latest 0d1cfe3057bb 4 minutes ago 158MB
$ docker run --rm -p 8080:8080 myapp
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.15)
[2024-10-17 05:56:03.003] [ INFO] [localhost-startStop-1] [c.p.PesApplication] No active profile set, falling back to 1 default profile: "default"
[2024-10-17 05:56:04.004] [ INFO] [localhost-startStop-1] [c.p.PesApplication] Started PesApplication in 1.545 seconds (JVM running for 5.042)
성공!
마무리
Docker를 공부할겸 가볍게 해보려던게 이틀이나 소요됐다. 덕분에 docker 명령어에 조금 익숙해진 것 같고 Jar와 War의 차이, Tomcat에대해 학습할 수 있었다. Docker를 늦게 시작했다는 아쉬움이 있지만, 앞으로 더욱 능숙하게 사용할 수 있도록 노력해야겠다.
회사 생활과 병행하며 블로그를 작성하는 것이 쉽지 않지만, 이런 학습 경험을 기록하고 공유하는 것이 가치있는 날이 올거라 믿고 앞으로도 꾸준히 성장 과정을 남겨야겠다.
지금 도커에 배포해본 프로젝트는 JPA로 마이그레이션할 예정이다. Mybatis에서 JPA로 마이그레이션 하는 과정도 블로그로 남길 예정이다.
참조
'Docker' 카테고리의 다른 글
Docker - 도커 기본 명령어 (1) | 2024.10.16 |
---|