본문 바로가기

프로그래밍/Spring

Spring Boot 를 Docker 에 올려보자(feat. Spring Boot 2.3) (1)

Spring Boot를 이용해서 만든 App을 Docker 이미지로 만드는 작업에 대한 글을 쓰도록 하겠다. 사실 이 주제는 검색해보면 관련 글들이 많이 나와 있다. 다만 개인적으로 이번에 이 글을 쓰는 이유는 Spring Boot 2.3 부터 Spring Boot App을 Docker Image로 만드는 작업을 Maven 플러그인과 Gradle 플러그인을 이용해서 공식지원하게 되어 글을 쓰게 되었다. 일단 관련 내용은 여기를 클릭하면 볼 수 있다.

 

Docker Image는 Layer로 표현되는 계층구조로 되어 있다. 이것을 이용해서 같은 Layer 계층은 재활용해서 사용할 수 있는 구조로 되어 있다. Spring은 이를 활용하기 위해 Jar 파일을 만들때 App을 4개의 계층으로 분리해서 만드는 방법을 제공한다. 이 4개의 계층은 다음과 같다

 

dependencies Spring Boot App에서 사용되는 라이브러리중 Release 라이브러리들이 있는 계층
spring-boot-loader org.springframework.boot.loader 패키지 아래의 모든 패키지와 클래스들이 있는 계층
snapshot-dependencies Spring Boot App에서 사용되는 라이브러리중 snapshot 라이브러리들이 있는 계층
application Spring Boot App을 구성하는 class 및 resource들이 있는 계층

 

이렇게 4가지 종류로 계층을 나눈 이유는 무엇일까? 공식적으로 언급된 내용은 없지만 개인적으로 계층을 이렇게 나눈  이유는 개발 및 운영하는 과정에서 변화의 정도가 심한 정도에 맞춰서 4개의 계층으로 나눈 것으로 보고 있다. 이 부분은 Docker Image의 Layer에 대한 이해를 하면 납득이 된다. 먼저 application 계층을 보자. application 계층은 우리가 만드는 java class와 resource로 구성이 되는데 이 계층은 가장 변화가 심한 계층이다. 개발이나 운영하는 과정에서 이 계층을 구성하는 java class와 resource는 새로운 것이 생성되거나 기존 것이 수정되거나 삭제되는 상황이 빈번하게 발생한다. 그래서 가장 변화가 심한 계층이다. 이 계층 다음으로 개발 및 운영하는 과정에서 변화가 빈번한 상황이 벌어지는 것이 snapshot-dependencies 계층이다. 예를 들어 개발이나 운영하는 과정에서 A라는 snapshot 라이브러리가 있다고 가정해보자. 이 라이브러리를 snapshot 버전으로 사용하는 이유는 자기가 라이브러리에서 원하는 기능이 정식 버전에서 지원되지 않아 해당 라이브러리를 개발하는 개발주체가 다음 버전에서 이 기능을 반영하기 위해 먼저 snapshot 으로 만든 상황일것이다. 또한 snapshot 라이브러리 특성상 계속 개선이 되다 보니 Spring Boot App이 사용하는 여러 라이브러리들 중에서 변경 빈도는 잦은 편이다. 그러나 이 라이브러리 변경 빈도가 application 계층에 있는 class와 resource들 만큼 라이브러리가 빈번하게 변하는 것은 아니다(우리가 개발하면서 B란 클래스를 하루에 10번도 넘게 변경할 상황이 벌어질 가능성이 있지만 snapshot 라이브러리가 그렇게까지 빈번하게 변경될 상황은 벌어지지 않는다) 그 다음으로 변경되는 빈도수가 많은게 Spring에서 제공되는 클래스 중에서 loader 패키지에 속한 클래스들의 변경 사항이 빈도수가 높으며 마지막으로 Spring Boot App에서 사용하는 라이브러리중 release 라이브러리들이 거의 변화가 없다고 보면 된다(release 라이브러리는 일단 동일한 버전에서는 변경이 없다. release 라이브러리가 변경이 되는 것은 버전을 올려서 새 버전으로 정식출시 하는 것이기 때문에 자주 발생하는 상황은 아니다. 다만 spring boot loader를 별도의 계층으로 뺀 것은 Spring Boot App을 실행하는 주체가 org.springframrwork.boot.loader 패키지의 클래스를 통해 실행을 시키다보니 이 부분만 별도 layer로 구성한거 아닐까 싶다. 썰이 좀 장황하게 길어졌는데 요약하자면 아래와 같이 변경빈도가 정해진다고 볼 수 있다.

 

App을 구성하는 class 및 resources > snapshot 라이브러리 > spring boot loader 관련 클래스 > release 라이브러리

 

그러면 이 변경빈도를 기준으로 왜 계층을 나누었나? Docker 이미지의 경우 변경된 내용이 없으면 해당 layer는 cache되어 재사용이 가능해지며 변경사항이 발생하면 새로이 layer를 만들어서 이미지를 구성하기 때문이다. 이 글에서는 Docker에 대한 자세한 설명은 하지 않을것이기에 관련 내용은 Docker Image Layer 관련 내용을 검색해서 알아보길 바란다. Docker 이미지 구성의 효율을 높이기 위해서 이러한 변경빈도에 따라 계층을 나눈뒤 이를 이용해서 Docker 이미지를 만들게 된다. 그러면 이렇게 layer 개념이 들어가게끔 할려면 Spring Boot App을 Build할 때 이러한 계층구조의 형태를 가지게끔 빌드를 해줘야 한다. 그러한 설정을 Maven의 pom.xml 에서는 다음과 같이 해주면 된다.

 

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <layers>
          <enabled>true</enabled>
        </layers>  
      </configuration>
    </plugin>
  </plugins>
</build>

 

원래 Spring Boot Project를 만들면 위의 내용에서 <configuration> 태그 부분을 빼고 만들어준다. 그러나 layer 적용을위해 <configuration> 태그를 추가한뒤 <layers> 태그를 추가해주면 끝난다(Gradle 에서 이를 적용하는 방법은 여기를 클릭하면 볼 수 있다)

 

그러면 이렇게 만든 jar 파일을 바로 Docker 이미지에 올려서 사용할까? 그렇게 만들면 layer로 사용한 의미가 없어진다. 이렇게 만든 jar 파일을 압축을 풀어서 올리게 된다. 다음의 명령으로 이렇게 layer가 적용된 Spring Boot jar 파일 압축을 풀 수 있다.

 

java -Djarmode=layertools -jar my-app.jar extract

 

extract 대신 list 주면 어떤 layer로 되어 있는지를 보여주며 extract를 주면 구성된 layer에 맞춰 my-app.jar를 압축을 풀게 된다. 아래의 그림은 mybatis 란 이름의 폴더에 위의 작업을 하고 난 뒤의 폴더 구조를 보여주고 있다. 

 

layer 형태를 지닌 jar 파일의 구조

 

mybatis 폴더 밑으로 위에서 언급했던 계층 이름인 application, dependencies, snapshot-dependencies, spring-boot-loader 이렇게 4개의 폴더가 존재하며 application 폴더 밑으로 BOOT-INF와 META-INF 폴더가 있고 BOOT-INF/classes 폴더 안에 프로젝트에서 작업한 class들과 resource들이 들어있는 것을 볼 수 있다. dependencies\BOOT-INF\lib 폴더에는 프로젝트에서 사용하는 release 버전의 라이브러리들이 들어가 있고 spring-boot-loader 폴더에는 위에서 설명했던 org.springframework.boot.loader 패키지의 하위 패키지 및 클래스들이 들어가 있다. 그리고 이렇게 풀려진 프로젝트를 다음과 같은 명령어로 실행시킬수 있게 된다.

 

java org.springframework.boot.loader.JarLauncher

 

압축이 풀려진 내용중에 spring-boot-loader 폴더 아래로 org.springframework.boot.loader 패키지의 하위 패키지 및 클래스들이 들어있기 때문에 JarLauncher 클래스를 이용해서 실행시킬수 있게 된다. 이렇게 Spring Boot Jar 파일을 작업한다고 이해했으니 이제 다음과 같은 Dockerfile 을 통해서 Docker 이미지를 만드는 작업을 진행하도록 한다

 

FROM openjdk:8-jre-alpine as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:8-jre-alpine
WORKDIR application
ENV server.port 8080
ENV spring.profiles.active default
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
CMD ["–spring.profiles.active=default"]

 

multi-stage 형태로 먼저 jar 파일 압축을 풀어서 이를 갖고 있는 임시용 이미지를 만든뒤 임시용 이미지에서 위에서 언급한 dependencies, spring-boot-loader, snapshot-dependencies, application 4개의 디렉토리를 복사해서 ENTRYPOINT 명령어로 JarLauncher 클래스를 실행하는 최종 이미지를 만들게 된다. (반드시 multi-stage 빌드 형태를 할 필요는 없다. Spring Reference 문서에 이렇게 나와 있어서 이 방법으로 해본것일뿐이지 핵심인 4개의 폴더를 COPY 명령을 통해 복사해서 이미지를 만들기만 하면 되는것이다) 아래와 같이 docker build 명령을 실행하면 docker 이미지를 만들수 있다

 

docker build --build-arg JAR_FILE=path/to/myapp.jar .

 

JAR_FILE 파라미터에 spring boot app jar 파일을 지정해주면 된다(예를 들어 Spring Boot로 만들어진 mybatis.jar 파일이 Dockerfile과 같은 경로에 있다면 path/to/myapp.jar 부분을 mybatis.jar 로 바꿔주면 된다) 이렇게 하면 docker image를 만들어 docker repository에 저장하게 된다. 여기서 주의할 점은 COPY 명령 순서이다. 즉 dependencies, spring-boot-loader, snapshot-dependencies, application 순으로 변경빈도가 낮은 디렉토리에서 높은 디렉토리 순으로 복사를 해야 한다. COPY 명령어 한줄한줄이 Docker 이미지에서 하나의 layer를 차지하게 되는데 만약 변경빈도가 높은 디렉토리 부터 낮은 디렉토리 순으로 복사하게 되면 변경빈도가 낮은 디렉토리에서는 변경된 내용이 없어서 layer를 새로 만들지 않아도 되지만 변경빈도가 높은 디렉토리에서 변경된 내용이 있기 때문에 여기서 layer를 새로 만들게 되므로 그 이후부터 새로이 강제적으로 layer가 만들어지게 된다. 이렇게 되면 기존 layer를 cache해서 만드는 것이 아니기 때문에 이미지의 크기가 올라가는 상황이 벌어지게 되므로 변경빈도가 낮은 디렉토리부터 높은 디렉토리 순으로 복사하는 것이 좋다.

 

이번 글에서는 Spring Boot 2.3 부터 자체적으로 지원하는 Docker image 만드는 방법에 대해 알아보았다. 다음 글에서는 이 방법 말고 Spring Boot 2.3이 지원해주는 또 다른 방법인 Build Pack을 이용해서 Docker Image를 만드는 방법과 이 방법을 비추하는 이유에 대해 설명하도록 하겠다