당니의 개발자 스토리
Spring 프로젝트 배포 흐름 정리: Docker, Kubernetes 본문
Spring 프로젝트를 만들다 보면 처음에는 코드 작성에 집중하게 된다.
Controller를 만들고, Service를 만들고, Repository를 만들고, 테스트 코드도 작성한다.
그런데 어느 순간 이런 생각이 든다.
“내 컴퓨터에서 실행되는 건 알겠는데, 이걸 실제 서버에서는 어떻게 돌리지?”
여기에 대한 대답은
Docker, Kubernetes를 이해하는 것에 있다.
Docker는 실행 파일과 실행 환경을 함께 묶어준다.
Kubernetes는 그렇게 실행된 컨테이너들을 운영 환경에서 관리해준다.
즉, 도커와 쿠버네티스는 배포 흐름 안에서 순서대로 이어진다.
Spring 프로젝트를 서버에서 실행하려면 먼저 실행 가능한 결과물이 필요하다.
Maven은 이 결과물을 만들어주는 역할을 한다.
Maven은 코드를 컴파일하고, 테스트를 실행하고, 필요한 파일들을 하나로 묶어준다.
Spring Boot 프로젝트에서는 보통 jar 파일이 만들어진다.
mvn clean package
빌드가 끝나면 보통 이런 파일이 생긴다.
target/app.jar
이 jar 파일은 실행 가능한 자바 프로그램이다.
서버에 Java가 설치되어 있다면 아래처럼 실행할 수 있다.
java -jar app.jar
여기까지만 보면 Maven으로 jar 파일을 만들고 서버에서 실행하면 끝나는 것처럼 보인다.
하지만 실제 서버에서는 환경 차이가 문제가 될 수 있다.
내 컴퓨터에는 Java 17이 설치되어 있는데
서버에는 Java 11만 있을 수도 있다.
또는 환경 변수나 설정 파일이 다를 수도 있다.
그리고 필요한 라이브러리나 실행 조건이 서버마다 다를 수도 있다.
이런 복합적인 원인들로
그냥 단순하게 'jar 파일만 옮기면 끝'이라고 생각하면 문제가 생길 수 있다.
프로그램이 실행되는 환경까지 맞춰야 한다.
Docker는 왜 필요할까?
Docker는 프로그램을 실행 환경까지 함께 패키징 하기 위해 사용한다.
Spring Boot 애플리케이션을 실행하려면 Java가 필요하다.
Docker는 app.jar와 Java 실행 환경을 함께 묶어서 Docker Image로 만든다.
Docker Image는 실행 준비가 끝난 포장 상태라고 보면 된다.
물론 아직 실행된 것은 아니지만, 언제든 같은 환경에서 실행할 수 있도록 준비된 상태다.
프로젝트에서 자주 보이는 간단한 Dockerfile은 이런 식으로 작성할 수 있다.
FROM openjdk:17-jdk
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
이 파일은 Java 17 환경을 기반으로 하고,
Maven으로 만든 app.jar를 이미지 안에 복사한 뒤,
컨테이너가 실행될 때 java -jar app.jar 명령어를 실행하겠다는 뜻이다.
Docker Image는 docker build 명령어로 만들 수 있다.
docker build -t my-spring-app .
이렇게 하면 my-spring-app 이라는 이미지가 만들어진다.
이미지는 실행 전 상태다.
이 이미지를 실제로 실행하면 Container가 된다.
docker run my-spring-app
즉, Docker Image는 실행 준비가 끝난 포장이고,
Docker Container는 그 포장을 실제로 실행한 상태다.
하나의 같은 이미지로 컨테이너를 여러 개 만들 수도 있다.
이 말은 같은 Spring 애플리케이션을 여러 개 실행할 수 있다는 뜻이다.
사용자가 적을 때는 컨테이너 하나로도 충분할 수 있다.
하지만 사용자가 많아지면 하나의 컨테이너가 모든 요청을 처리하기 어려워진다.
그래서 같은 애플리케이션을 여러 개 띄우고 요청을 나눠서 처리한다.
서버는 쉽게 말하면 서비스용 컴퓨터다.
서버 한 대에서 컨테이너 여러 개가 실행될 수도 있고,
서버 여러 대에 컨테이너가 나뉘어 실행될 수도 있다.
그럼 여기서 이제,
Kubernetes는 대체 왜 필요할까?
컨테이너가 하나라면 Docker만으로도 관리할 수 있다.
직접 실행하고, 직접 중지하고, 문제가 생기면 다시 실행하면 된다.
하지만 컨테이너가 많아지면 관리가 어려워진다.
컨테이너가 죽었을 때 다시 실행해야 하고,
사용자가 많아지면 컨테이너 개수를 늘려야 하고,
새 버전을 배포할 때 기존 컨테이너를 교체해야 한다.
요청도 여러 컨테이너에 적절히 나눠줘야 한다.
이 과정을 사람이 직접 계속 관리하기는 어렵다.
Kubernetes는 이런 컨테이너 운영을 자동화하기 위해 사용한다.
Kubernetes는 컨테이너가 죽으면 다시 실행한다.
필요한 개수만큼 컨테이너를 유지한다.
배포할 때 새 버전으로 순서대로 교체할 수 있다.
여러 컨테이너로 요청을 나눠줄 수 있다.
그럼 Kubernetes에서 나오는 Pod란 무엇일까?
Kubernetes에서는 컨테이너를 직접 하나씩 관리하지 않고,
Pod라는 단위로 관리한다.
Pod는 Kubernetes에서 애플리케이션이 실행되는 기본 단위다.
보통 처음 이해할 때는 Pod 하나 안에 컨테이너 하나가 들어간다고 생각하면 된다.
Spring 애플리케이션 컨테이너가 하나 실행되면,
Kubernetes에서는 Pod 하나가 실행된다고 볼 수 있다.
Deployment는 무엇일까?
Pod 하나만 띄우는 것은 Docker로 컨테이너 하나 실행하는 것과 크게 다르지 않다.
운영 환경에서는 보통 같은 애플리케이션을 여러 개 유지해야 한다.
Deployment는 Pod의 개수를 관리하는 설정이다.
예를 들어 Spring 애플리케이션 Pod를 3개 유지하고 싶다면,
Deployment에 3개를 유지하라고 설정할 수 있다.
replicas: 3
그러면 Kubernetes는 Pod 3개가 유지되도록 관리한다.
만약 Pod 하나가 죽으면 Kubernetes가 다시 하나를 만든다.
그래서 다시 3개를 맞춘다.
즉, Deployment는 원하는 상태를 계속 유지하게 해주는 설정이다.
Service는 무엇일까?
Pod가 여러 개 있으면 사용자의 요청을 어디로 보낼지 정해야 한다.
사용자가 Pod 하나하나의 주소를 직접 알고 접근하는 것은 현실적이지 않다.
Service는 여러 Pod 앞에서 요청을 받아주는 역할을 한다.
사용자는 Service로 요청을 보내고,
Service는 뒤에 있는 Pod 중 하나로 요청을 전달한다.
이때 요청을 여러 Pod에 나눠주는 것을 로드 분산이라고 한다.
로드는 요청량이나 작업량을 의미한다.
로드 분산은 요청을 한 곳에 몰아넣지 않고 여러 곳에 나누는 것이다.
로드 밸런싱은 이 로드 분산을 실제로 수행하는 방식이다.
예를 들어 요청을 Pod 1, Pod 2, Pod 3에 나눠 보내는 것이다.
Spring 프로젝트 배포 흐름은 이렇게 연결된다.
Maven은 Spring 코드를 jar 파일로 만든다.
Docker는 jar 파일과 Java 실행 환경을 함께 묶어 이미지로 만든다.
Docker Image를 실행하면 Container가 된다.
Kubernetes는 Container를 Pod 단위로 실행하고 관리한다.
Deployment는 Pod 개수를 유지한다.
Service는 요청을 여러 Pod로 나눠준다.
처음에는 Maven, Docker, Kubernetes가 서로 다른 개념처럼 느껴진다.
하지만 실제 배포 흐름에서는 순서대로 연결된다.
Maven은 실행 파일을 만든다.
Docker는 실행 환경까지 함께 포장한다.
Kubernetes는 운영 환경에서 컨테이너들이 안정적으로 돌아가도록 관리한다.
이렇게 보면 Docker와 Kubernetes가 왜 필요한지 조금 더 자연스럽게 이해된다.
