FireDrago
[Docker] Docker를 왜 써야하는데? 본문

우테코 Lv3 1차 데모데이 요구사항은 위와 같았다.
모든 팀원이 동일한 환경에서 개발할 수 있도록 처음부터 제대로 셋팅하세요 요구조건을 만족하기 위해Docker를 도입하기로 했다. 그런데 Docker가 왜 해결책인지, 어떤 방식으로 동일한 환경을 제공하는지
전혀 지식이 없었다. 그래서 도커에 관해 정리했다.
도커(Docker)란 ‘컨테이너 기반의 오픈소스 가상화 플랫폼’이다.
먼저 가상화부터 무엇인지 알아보자
가상화

가상화는 쉽게 이야기하면 하나의 물리적 컴퓨터에서 여러 개의 독립된 환경을 만들어내는 기술이다.
이 가상 환경은 마치 별개의 컴퓨터처럼 작동한다.
가상화 방식에는 여러 가지가 있는데, ‘하드웨어 가상화’는 OS부터 통째로 설치하는 방식이다.
반면 ‘OS 수준의 가상화’는 호스트의 OS를 공유하며 그 위에 격리된 공간만 만들기 때문에 훨씬 가볍고 빠르다.
도커는 바로 이 OS 수준의 가상화 기술을 사용하며, 이때 컨테이너라는 독립된 공간이 활용된다.
이 지점에서 한가지 의문이 생긴다.
호스트 OS가 각자 다른데 어떻게 호스트의 OS를 공유하는 거지?
mac과 window에서 Docker를 사용하기 위해서 Docker application을 반드시 설치해줘야 하는 이유다.DockerApplication이 마치 어댑터 처럼 호스트의 OS와 컨테이너를 이어주는 역할을 하는 것이다.DockerApplication은 내부에 Linux 기반의 가상 환경을 생성한다.
컨테이너

도커를 사용하면 컨테이너라는 격리된 공간에서 프로그램이 실행된다.컨테이너안에는 프로그램을 실행하기 위한 모든 파일이 포함되어 있다.
MySql 컨테이너가 있다면, 그 안에는 MySql을 실행하기위한 모든 파일이 들어있다.
컨테이너는 외부 환경과 격리되어 동작하기 때문에
팀원들이 각자 다른 환경을 가지고 있더라도 동일한 환경에서 MySql이 실행될 수 있다.
이미지
컨테이너를 이용해 동일한 환경 구축을 완료했다. 그럼 끝일까?
무거운 컨테이너를 직접 동료에게 전송하면 어떻게 될까?
컨테이너를 전송하는데에만 긴 시간이 필요할 것이다.
컨테이너를 만들기 위한 이미지가 필요한 이유다.
이미지는 컨테이너 실행에 필요한 파일과 설정값등을 포함하고 있다.
이미지는 컨테이너를 실행하기 위한 모든 정보를 가지고 있기 때문에 더 이상 의존성 파일을 컴파일하고 이것저것 설치할 필요가 없다.
새로운 서버가 추가되면 미리 만들어 놓은 이미지를 다운받고 컨테이너를 생성만 하면 된다.
한 서버에 여러개의 컨테이너를 실행할 수 있고, 수천대의 서버도 동일한 환경에서 실행 할 수 있다.
이미지 - 레이어

이미지는 여러 개의 읽기 전용 레이어(Read-only Layer) 로 구성된다.
Dockerfile이라는 설계도에 ubuntu를 바탕으로 nginx를 설치하고, 소스코드를 복사하는 내용을 적었다면,
각각의 명령어가 별개의 레이어로 쌓이게 된다.
만약 여기서 소스코드만 변경되면, ubuntu와 nginx 레이어는 그대로 재사용하고
마지막 소스코드 레이어만 새로 만들기 때문에 매우 효율적으로 이미지를 업데이트하고 관리할 수 있다.
이미지 - 읽기 전용
이미지는 한번 생성되면 불변이다.
즉 사용자가 임의로 수정할 수 없다.
동일한 환경을 공유하기 위해 내부 설정이 변경되어서는 안되기 때문이다.
만약 개별 컨테이너별로 관리되어야 하는 설정값이 있다면,
읽기/쓰기 레이어가 컨테이너 개별로 관리된다. 이미지 파일은 항상 읽기전용이다.
코드로 알아보기 - Dockerfile
그래서 코드는 어떻게 설정해야 할까? 프로젝트에 적용한 Dockerfile과 docker-compose.yml을 보면서 알아보자.
Dockerfile: 이미지 설계도
아래 코드는 우리 Spring Boot 애플리케이션을 이미지로 만드는 파일이다.
코드가 좀 길어 보이지만 '멀티 스테이지 빌드' 라는 아주 효율적인 방식을 사용하고 있다.
# 1단계: 빌드용 임시 컨테이너 (Builder)
FROM gradle:8.5-jdk21 AS builder
WORKDIR /app
COPY . .
RUN gradle build -x test --no-daemon
# 2단계: 실행용 최종 컨테이너
FROM eclipse-temurin:21-jdk-jammy
WORKDIR /app
# 빌더 컨테이너에서 빌드 결과물(.jar)만 복사
COPY --from=builder /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
1단계: 빌드용 임시 컨테이너FROM gradle... AS builder : 빌드만을 위한 임시 컨테이너를 만드는 과정이다.
여기엔 소스 코드를 .jar 파일로 빌드하기 위한 모든 것(Gradle, JDK)이 들어있다.
이 컨테이너의 역할은 gradle build를 실행해서 결과물을 만들어 낸다.
2단계: 실행용 최종 컨테이너FROM eclipse-temurin... : 실제 앱을 실행할 최종 컨테이너를 만든다.
여기엔 무거운 빌드 도구(Gradle)는 빼고, 딱 필요한 JDK 환경과 1단계에서 만든 파일만 복사한다 (COPY --from=builder).
덕분에 최종 이미지 크기가 엄청나게 가벼워 질 수 있게 된다.
모든 명령어는 레이어로 쌓인다.
FROM, COPY, RUN 같은 명령어 하나하나가 위에서 설명한 레이어로 쌓인다.
만약 내가 소스코드만 살짝 바꿨다면? Docker는 똑똑하게 이전 레이어(Gradle, JDK 설치 등)는 그대로 재사용하고,
변경된 소스코드를 복사하는 레이어부터 새로 빌드한다. 그래서 빌드 속도가 매우 빨라 질 수 있다.
코드로 알아보기 - docker-compose.yml
이제 우리 앱(app)과 데이터베이스(db)를 한 번에 띄워 줄 docker-compose.yml을 보자.
version: "3.8"
services:
# Spring Boot App 서비스
app:
container_name: moment-app-server
build: . # 현재 디렉토리의 Dockerfile을 사용해 이미지를 빌드해줘
ports:
- "80:8080" # 내 컴퓨터 80 포트와 컨테이너 8080 포트를 연결
env_file:
- ./.env # .env 파일에 있는 환경변수들을 사용해줘
depends_on: # 다른 서비스에 대한 의존성 설정
db:
condition: service_healthy # db 컨테이너가 'healthy' 상태가 되면 시작해줘
# MySQL DB 서비스
db:
image: "mysql:8" # 미리 만들어진 mysql:8 이미지를 가져와서 사용해줘
container_name: moment-mysql-db
ports:
- "3306:3306"
env_file:
- ./.env
volumes: # 컨테이너가 삭제돼도 데이터를 보존하기 위한 설정
- db-data:/var/lib/mysql
healthcheck: # 컨테이너가 정상인지 확인하는 방법
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
db-data:
app 서비스
build: .→ 현재 폴더의 Dockerfile을 사용해 이미지 빌드ports→ 외부 80 포트를 내부 8080 포트에 매핑env\_file→ 환경변수 로딩depends\_on→ db가 정상 상태가 되면 실행 시작
db 서비스
image: mysql:8→ 공식 MySQL 이미지 사용volumes→ 데이터 유지healthcheck→ 서비스 상태 체크
정리
결국 도커는 이미지 라는 실행 환경 설계도를 이용해,
어떤 컴퓨터에서든 동일하게 동작하는 컨테이너 라는 격리된 공간을 만들어주는 기술이다.
이를 통해 "제 컴퓨터에서는 잘 되는데요?"라는 문제를 원천적으로 해결하고,
모든 팀원이 동일한 환경에서 개발과 배포를 진행할 수 있게 해준다.
