Back-End/Spring

프로젝트 관리 심화: 챕터 1 (Docker)

Kr1 2026. 5. 4. 22:06

 

 

무엇을 배우나요?

  • Docker
    • Docker를 통해 애플리케이션을 컨테이너로 만들어서 일관되게 실행할 수 있습니다
    • Dockerfile을 작성하고, Docker 이미지를 만들고 배포할 수 있습니다.
  • Docker-compose
    • Docker-compose로 여러 개의 컨테이너를 쉽게 관리할 수 있습니다
    • 서비스 간의 연결 및 데이터 관리를 배우게 됩니다
  • CI/CD 파이프라인 구축
    • CI/CD의 기본 개념을 이해하고, 자동으로 빌드, 테스트, 배포하는 과정을 배울 수 있습니다.
    • GitLab을 사용하여 CI/CD 파이프라인을 설정할 수 있습니다.
  • AWS Elastic Container Service 사용
    • AWS ECS를 통해 컨테이너 애플리케이션을 배포하고 관리할 수 있습니다

 

🗨️ Docker

Docker 개요

Docker란?

  • Docker
    • Docker애플리케이션을 쉽게 만들고, 테스트하고, 배포할 수 있게 도와주는 소프트웨어 플랫폼입니다.
    • 애플리케이션을 컨테이너라는 가볍고 이식성 있는 패키지로 실행할 수 있습니다.
    • Docker 이미지애플리케이션을 실행하는 데 필요한 모든 것(코드, 런타임, 시스템 도구, 시스템 라이브러리 등)을 포함합니다.
  • 주요 특징
    • 컨테이너화: 애플리케이션과 필요한 모든 것을 하나의 패키지로 묶어 어디서든 실행할 수 있습니다.
    • 경량: Docker는 운영 체제의 커널을 공유하므로, 가상 머신보다 훨씬 가볍고 빠르게 실행됩니다.
    • 이식성: Docker 컨테이너는 어디서든 동일하게 실행됩니다. 예를 들어, 개발자의 컴퓨터에서 테스트 서버, 운영 서버까지 동일하게 동작합니다.
    • 확장성: Docker를 사용하면 여러 개의 컨테이너를 효율적으로 관리하고 쉽게 확장할 수 있습니다.
도커 이전에는 서버 환경마다 자바, 그레이들, 데이터베이스 등의 버전과 환경에 맞게 직접 설치, 설정해야 했으나, 도커 덕분에 복잡한 의존성 관리 및 설치 과정이 없어지고, 커맨드 몇 줄로 모든 것이 해결되어 관리와 배포가 매우 간편해졌습니다.
도커를 사용하면 서로 다른 환경이나 버전 문제가 사라지고, 의존성 관리 및 배포, 확장에 따른 실수도 크게 줄일 수 있습니다.

 

💡 주요 키워드

  • 키워드 정리
    • 이미지: 애플리케이션과 모든 실행에 필요한 파일을 포함한 읽기 전용 템플릿
    • 컨테이너: 이미지를 실행하여 동작하는 애플리케이션 인스턴스
    • Dockerfile: 이미지를 생성하기 위한 명령어가 담긴 스크립트 파일
    • Docker Hub: 이미지를 저장하고 공유하는 중앙 저장소
    • 볼륨: 컨테이너 데이터를 지속적으로 저장하는 메커니즘
    • 네트워크: 컨테이너 간의 통신을 관리하는 방식 
  • 이미지 (Image)
    • Docker 이미지는 애플리케이션과 그 실행에 필요한 모든 것을 포함하는 읽기 전용 템플릿입니다. 이미지에는 코드, 런타임, 라이브러리, 환경 변수, 구성 파일 등이 포함됩니다.
    • 이미지는 컨테이너를 생성하기 위한 청사진 역할을 합니다.
  • 컨테이너 (Container)
    • 컨테이너는 Docker 이미지를 실행한 상태입니다. 이미지 정적인 템플릿이라면, 컨테이너실제로 애플리케이션이 실행되는 동적인 환경입니다.
    • 컨테이너는 격리된 공간에서 애플리케이션을 실행하며, 필요한 모든 의존성을 포함합니다.
    • 하나의 시스템에서 여러 개의 컨테이너를 독립적으로 실행할 수 있습니다.
  • Dockerfile
    • Dockerfile은 Docker 이미지를 생성하기 위한 스크립입니다. 이 파일에는 이미지를 빌드하는 데 필요한 명령어들이 포함되어 있습니다.
    • Dockerfile을 사용하면 이미지 생성 과정을 자동화하고 일관되게 만들 수 있습니다.
  • Docker Hub
    • Docker Hub는 Docker 이미지를 공유하고 저장하는 중앙 저장소입니다. 사용자는 Docker Hub에서 다양한 공개 이미지를 다운로드하거나 자신만의 이미지를 업로드할 수 있습니다.
  • 볼륨 (Volume)
    • 볼륨은 컨테이너의 데이터를 지속적으로 저장할 수 있는 메커니즘입니다. 컨테이너가 삭제되더라도 볼륨에 저장된 데이터는 유지됩니다.
    • 볼륨을 사용하면 데이터를 컨테이너와 독립적으로 관리할 수 있습니다.
  • 네트워크 (Network)
    • Docker 네트워크는 컨테이너 간의 통신을 관리하는 방식입니다. Docker는 여러 가지 네트워크 드라이버를 제공하여 다양한 네트워크 설정을 지원합니다.
    • 기본적으로 모든 컨테이너는 브리지 네트워크를 통해 통신할 수 있습니다.

 

📎네트워크 종류

브리지 네트워크만 잘 알아두기!
  • Bridge Network (브리지 네트워크)
    • 기본적으로 Docker가 컨테이너를 실행할 때 사용하는 네트워크입니다.
    • 동일한 브리지 네트워크에 연결된 컨테이너들은 서로 통신할 수 있습니다.
    • 외부 네트워크와는 NAT (내부 네트워크의 여러 장치가 하나의 공용 IP 주소를 통해 외부 네트워크와 통신할 수 있도록 IP 주소를 변환하는 기술) 를 통해 통신합니다.
    • 일반적으로 단일 호스트에서 여러 컨테이너를 연결할 때 사용됩니다.
    • 명시하지 않으면 모두 브리지 네트워크에서 실행됩니다
# Docker에서 사용자 정의 브리지 네트워크 생성
# 같은 네트워크에 연결된 컨테이너끼리 이름(container name)으로 통신 가능
docker network create my-bridge-network

# nginx 이미지를 사용해서 container1 이라는 이름의 컨테이너를 백그라운드(-d)로 실행
# nginx는 가볍고 많이 사용하는 웹 서버 프로그램
# --network 옵션으로 위에서 만든 my-bridge-network에 연결
docker run -d --name container1 --network my-bridge-network nginx

# nginx 이미지를 사용해서 container2 컨테이너 실행
# container1과 같은 브리지 네트워크에 연결되므로 서로 통신 가능
# 예: container1 내부에서 ping container2 가능
docker run -d --name container2 --network my-bridge-network nginx
  • Host Network (호스트 네트워크)
    • 컨테이너가 호스트의 네트워크 스택을 직접 사용합니다.
    • 네트워크 격리가 없기 때문에 성능상 이점이 있지만, 보안 및 네트워크 충돌 위험이 있습니다.
    • 일반적으로 성능이 중요한 애플리케이션에 사용됩니다.
= 컨테이너가 호스트 컴퓨터의 네트워크를 그대로 같이 사용
= 빠르지만 서로 포트 충돌이나 보안 문제가 생길 수 있음
예) 내 PC의 8080 포트를 컨테이너도 그대로 사용
docker run -d --network host nginx
  • Overlay Network (오버레이 네트워크)
    • 여러 Docker 호스트에 걸쳐 있는 컨테이너를 연결할 때 사용됩니다.
    • Swarm 모드(Docker 컨테이너의 오케스트레이션과 클러스터링을 지원하여 여러 호스트에서 컨테이너를 관리하고 배포할 수 있는 기능)나 Kubernetes 같은 오케스트레이션 도구와 함께 사용됩니다.
    • 데이터 센터 또는 클라우드 환경에서 분산 시스템을 구축할 때 유용합니다.
= 서로 다른 서버(호스트)에 있는 Docker 컨테이너끼리 연결하는 네트워크
= 여러 대 서버를 하나처럼 연결할 때 사용
예) 서울 서버의 컨테이너 ↔ 부산 서버의 컨테이너 통신

 

Docker와 가상 머신의 비교

  • Docker의 장점과 단점
    1. 장점
      • 빠른 시작 시간과 낮은 오버헤드: Docker 컨테이너는 애플리케이션만 실행하고, 운영 체제의 핵심 부분은 공유하므로, 가상 머신보다 훨씬 빠르게 시작할 수 있습니다.
      • 높은 이식성과 확장성: Docker 컨테이너는 한 번 만들면 어디서든지 동일하게 실행됩니다. 예를 들어, 개발 환경, 테스트 환경, 실제 운영 환경 모두에서 같은 방식으로 동작합니다. 여러 컨테이너를 쉽게 추가하고 관리할 수 있어, 필요에 따라 애플리케이션을 확장하기 쉽습니다.
    2. 단점
      • 보안 격리가 가상 머신보다 약함: Docker 컨테이너는 동일한 운영 체제 커널을 공유하기 때문에, 가상 머신보다 보안 격리 수준이 낮습니다. 하나의 컨테이너에서 보안 문제가 발생하면, 같은 커널을 공유하는 다른 컨테이너에도 영향을 줄 가능성이 있습니다.
      • 운영 체제 종속성 존재: Docker는 리눅스 커널을 사용하여 작동하므로, 리눅스 운영 체제에서 가장 잘 동작합니다. 윈도우나 맥 같은 다른 운영 체제에서는 호환성 문제가 있을 수 있고, 리눅스 커널을 에뮬레이션하는 방식으로 작동해야 하기 때문에 성능이 저하될 수 있습니다.
  • 가상 머신(VM)의 장점과 단점
    1. 정의
      • 가상 머신(VM)하이퍼바이저를 통해 물리적 하드웨어 위에 가상화된 운영 체제를 실행하는 기술입니다. 하이퍼바이저여러 운영 체제를 동시에 실행할 수 있도록 물리적 하드웨어를 가상화하는 소프트웨어입니다.
    2. 장점
      • 격리된 환경 제공: 각 가상 머신은 완전히 독립된 운영 체제를 실행하므로, 하나의 가상 머신에서 문제가 발생해도 다른 가상 머신이나 호스트 시스템에 영향을 주지 않습니다.
      • 다양한 운영 체제 실행 가능: 가상 머신을 사용하면 한 물리적 서버에서 여러 종류의 운영 체제를 동시에 실행할 수 있습니다. 예를 들어, 같은 컴퓨터에서 윈도우, 리눅스, 맥OS를 동시에 실행할 수 있습니다. 이를 통해 개발자나 테스트 팀은 다양한 환경을 쉽게 구축하고 사용할 수 있습니다.
    3. 단점
      • 오버헤드가 크고, 느린 부팅 시간: 가상 머신은 전체 운영 체제를 실행해야 하기 때문에, 많은 메모리(RAM)와 CPU 자원을 소비합니다. 가상 머신을 부팅할 때 운영 체제를 처음부터 시작해야 해서 시간이 오래 걸립니다. 이는 실제 하드웨어에서 컴퓨터를 켜는 것과 비슷합니다.
      • 높은 리소스 소비: 가상 머신은 각기 독립된 운영 체제를 포함하므로, 여러 가상 머신을 실행하면 컴퓨터 자원을 많이 소모하게 됩니다. 예를 들어, 두 개의 가상 머신을 실행하면, 두 운영 체제를 모두 실행해야 하므로 메모리와 CPU 사용량이 크게 증가합니다. 이는 컴퓨터의 성능을 저하시킬 수 있습니다.

 

Docker는 언제 사용할까?

  • 일관된 개발 환경이 필요할 때
    • 개발, 테스트, 운영 환경이 다를 때 발생하는 문제를 피하고자 할 때 Docker를 사용하면 좋습니다. Docker를 사용하면 모든 환경에서 동일한 컨테이너를 실행할 수 있어, 환경 차이로 인한 문제를 줄일 수 있습니다
  • 애플리케이션을 빠르게 배포하고 싶을 때
    • Docker를 사용하면 애플리케이션을 빠르고 쉽게 배포할 수 있습니다. Docker 이미지를 빌드하고 이를 컨테이너로 실행하면, 필요한 모든 구성 요소가 포함되어 있어 별도의 설치 과정 없이 바로 실행할 수 있습니다.
  • 마이크로서비스 아키텍처를 도입할 때
    • Docker는 마이크로서비스 아키텍처와 잘 맞습니다. 각 서비스가 독립적으로 배포되고 실행될 수 있어, 여러 개의 컨테이너를 통해 다양한 서비스를 쉽게 관리할 수 있습니다
  • CI/CD 파이프라인을 구축할 때
    • Docker는 CI/CD 파이프라인에 적합합니다. 코드를 변경할 때마다 자동으로 빌드, 테스트, 배포할 수 있도록 설정할 수 있어, 개발 주기를 단축하고 배포의 신뢰성을 높일 수 있습니다.
  • 리소스 효율성을 높이고 싶을 때
    • Docker 컨테이너는 가상 머신보다 적은 리소스를 사용합니다. 운영 체제의 커널을 공유하므로, 더 많은 애플리케이션을 동일한 하드웨어에서 실행할 수 있습니다.
  • 애플리케이션 격리가 필요할 때:
    • 여러 애플리케이션을 독립적으로 실행하고자 할 때 Docker를 사용하면 각 컨테이너가 서로 격리되어 실행됩니다. 이를 통해 애플리케이션 간의 충돌을 방지하고 보안을 강화할 수 있습니다.
  • 쉽게 스케일링하고 싶을 때:
    • Docker를 사용하면 컨테이너 기반의 애플리케이션을 쉽게 확장할 수 있습니다. 필요한 만큼 컨테이너를 추가하여 수평 확장이 가능하며, 오케스트레이션 도구와 결합하여 자동 확장도 가능합니다.
  • 쿠버네티스(Kubernetes)와 함께 사용하고자 할 때
    • Docker는 쿠버네티스와 함께 사용하기에 매우 적합합니다. 쿠버네티스는 컨테이너 오케스트레이션 도구로, 다수의 Docker 컨테이너를 관리하고 자동 배포, 확장, 운영을 지원합니다. Docker 컨테이너를 쿠버네티스 클러스터에 배포하면, 애플리케이션의 가용성과 확장성을 높일 수 있습니다.

 

Docker 명령어

📎이미지 관련 명령어

  • 이미지 빌드 (Build)
    • 현재 디렉토리의 Dockerfile을 기반으로 myapp이라는 이름의 이미지를 생성합니다. -t 옵션을 사용하여 이미지의 이름과 태그를 입력할 수 있습니다.
docker build -t myapp:latest .
  • 이미지 가져오기
    • 도커 허브에서 해당 이미지를 가져옴
docker pull postgres
  • 이미지 목록 보기 (List Images)
    • 현재 로컬에 저장된 Docker 이미지를 목록으로 표시합니다.
docker images
  • 이미지 삭제
    • myapp:latest 이미지를 로컬 저장소에서 삭제합니다.
docker rmi myapp:latest

 

📎 Docker 컨테이너 관련 명령어

📌 컨테이너 아이디는 모두 작성할 필요없이 식별 가능한 자릿수까지만 입력하세요
🐳 도커 포트 매핑 (Port Mapping) 핵심 요약
  • 도커의 포트 매핑은 호스트(내 컴퓨터)와 컨테이너 내부를 연결하는 과정입니다.
    • 컨테이너 포트 (8080): 실제 서버(Spring Boot 등)가 실행되고 있는 독립적인 내부 위치
    • 호스트 포트 (18080): 외부(사용자)에서 해당 컨테이너로 접속하기 위해 열어두는 입구
docker run -p 호스트포트:컨테이너포트 예시: docker run -p 18080:8080
  • 외부에서 localhost:18080으로 요청을 보내면, 도커가 이를 컨테이너 내부의 8080 포트로 전달해 주는 원리입니다.

💡 꼭 알아야 할 '포트 중복' 규칙 

  • 컨테이너는 서로 완전히 격리된 독립 공간입니다. 따라서 내부(컨테이너) 포트는 여러 컨테이너가 똑같이 8080을 사용해도 전혀 문제가 없습니다. 
  • 하지만 호스트 포트는 하나의 컴퓨터에서 공유되는 입구이므로 절대 중복해서 사용할 수 없습니다.
    • ⭕ 정상 (호스트 포트 다름): 18080:8080 / 18081:8080
    • ❌ 불가 (호스트 포트 충돌): 18080:8080 / 18080:8080

 

  • 컨테이너 실행 (Run)
    • myapp:latest 이미지를 사용하여 컨테이너를 실행합니다. -d 옵션은 백그라운드에서 실행되도록 하고, -p 옵션은 호스트의 8080 포트를 컨테이너의 80 포트에 매핑합니다.
    • -d (detached mode): 컨테이너를 백그라운드에서 실행합니다. 이 옵션을 사용하면 터미널을 컨테이너에 붙잡히지 않고, 컨테이너가 백그라운드에서 계속 실행됩니다.
docker run -d -p 8080:80 myapp:latest

 

  • 컨테이너 내부 접속
    • -i (interactive): 컨테이너의 표준 입력(STDIN)을 열어둡니다. 이 옵션을 사용하면 컨테이너 내부에서 사용자 입력을 받을 수 있습니다.
    • -t (tty): 가상 터미널을 할당합니다. 이 옵션을 사용하면 컨테이너 내부에서 터미널을 사용할 수 있습니다.
암기하기!
docker exec -it 컨테이너_아이디 /bin/bash

# docker       → 도커 명령어 실행
# exec         → 실행 중인 컨테이너 안에서 명령 실행
# -it          → 터미널 입력 가능(-i) + 터미널 화면 연결(-t)
# 컨테이너ID    → 접속할 대상 컨테이너
# /bin/bash    → 컨테이너 내부에서 bash 쉘 실행 (리눅스 터미널 열기)

 

  • 실행 중인 컨테이너 목록 보기 (List Running Containers)
    • 현재 실행 중인 컨테이너의 목록을 표시합니다.
docker ps

 

  • 모든 컨테이너 목록 보기 (List All Containers)
docker ps -a	# 중지된 컨테이너를 포함한 모든 컨테이너의 목록을 표시합니다.
docker ps -al	# 마지막으로 실행된 컨테이너를 가장 먼저 나열.

 

  • 컨테이너 중지 (Stop)
    • 지정된 container_id를 가진 컨테이너를 중지합니다.
docker stop container_id

 

  • 컨테이너 시작 (Start)
    • 중지된 컨테이너를 다시 시작합니다.
docker start container_id

 

  • 컨테이너 삭제 (Remove Container)
    • 지정된 container_id를 가진 컨테이너를 삭제합니다.
docker rm 컨테이너_아이디

 

 

📎 Docker 네트워크 및 볼륨 관련 명령어

  • 네트워크 생성 (Create Network)
    • mynetwork이라는 이름의 네트워크를 생성합니다.
docker network create mynetwork

 

  • 네트워크 목록 보기 (List Networks)
    • 현재 설정된 Docker 네트워크의 목록을 표시합니다.
docker network ls

 

  • 네트워크 삭제 (Remove Network)
    • mynetwork이라는 이름의 네트워크를 삭제합니다.
docker nework rm mynetwork

 

  • 볼륨 생성 (Create Volume)
    • myvolume이라는 이름의 볼륨을 생성합니다.
docker volume create myvolume

 

  • 볼륨 목록 보기 (List Volumes)
    • 현재 설정된 Docker 볼륨의 목록을 표시합니다.
docker volume ls

 

  • 볼륨 삭제 (Remove Volume)
    • myvolume이라는 이름의 볼륨을 삭제합니다.
docker volume rm myvolume

 

간단한 실습

📌 도커를 통해 PostgreSQL 컨테이너 2개를 실행해보겠습니다.

이미지 받기

docker pull postgres

# docker pull → Docker Hub에서 이미지 다운로드
# postgres     → PostgreSQL 공식 이미지 이름
# 이미지(Image) → 프로그램 + 실행환경을 묶어둔 실행 템플릿

컨테이너 실행하기

  • :z 옵션은 SELinux(Secure Enhanced Linux) 환경에서 사용되는 파일 시스템 옵션입니다.
  • 이 옵션은 Docker 컨테이너가 호스트 파일 시스템의 특정 디렉토리에 접근할 수 있도록 SELinux 컨텍스트를 설정합니다. SELinux는 Linux 시스템의 보안을 강화하기 위해 파일 및 프로세스에 대한 권한을 세밀하게 제어합니다.
  • :z 옵션은 해당 볼륨이 여러 컨테이너에서 공유될 수 있음을 나타내며, 이를 통해 컨테이너가 해당 디렉토리에 읽기 및 쓰기 권한을 갖도록 합니다.
docker run -d --name postgres-sample \
  -p 5433:5432 \
  -e POSTGRES_USER=admin1 \
  -e POSTGRES_PASSWORD=admin2 \
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  -v ${로컬_바인딩_폴더}:/var/lib/postgresql/data:z \
  postgres
# 새로운 컨테이너를 백그라운드(-d)로 실행하고, 컨테이너 이름 지정
docker run -d --name postgres-sample \
  
  # 호스트 5433 ↔ 컨테이너 내부 5432 포트 연결 (외부는 5433으로 접속)
  -p 5433:5432 \
  
  # 환경변수(-e): PostgreSQL 계정(username) 설정
  -e POSTGRES_USER=admin1 \
  
  # 환경변수(-e): PostgreSQL 비밀번호 설정
  -e POSTGRES_PASSWORD=admin2 \
  
  # 환경변수(-e): PostgreSQL 데이터가 저장되는 세부 위치 지정
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  
  # 볼륨 마운트(-v): 로컬-컨테이너 폴더 연결하여 데이터 유지. (:z는 SELinux 권한 허용)
  -v ${로컬_바인딩_폴더}:/var/lib/postgresql/data:z \
  
  # 실행할 PostgreSQL 공식 이미지 지정
  postgres

 

💾 볼륨 마운트(Volume Mount)와 :z 옵션의 이해
  • 볼륨 마운트 (-v)란? 도커 컨테이너는 종료되거나 삭제되면 그 안의 데이터도 모두 날아갑니다. 데이터베이스(DB)의 데이터가 날아가면 안 되기 때문에, 내 컴퓨터(호스트)의 특정 폴더와 컨테이너 내부의 폴더를 서로 연결(동기화)하는 작업입니다. 이렇게 하면 컨테이너를 지워도 데이터는 내 컴퓨터에 안전하게 보존됩니다.
  • SELinux와 :z 옵션이란? SELinux는 리눅스(특히 CentOS, RHEL 등)의 강력한 보안 시스템입니다. 이 시스템은 기본적으로 컨테이너가 내 컴퓨터의 폴더에 접근하는 것을 차단합니다. 마운트 경로 맨 끝에 :z를 붙여주면, 도커가 해당 폴더를 정상적으로 읽고 쓸 수 있도록 SELinux의 접근 권한을 알맞게 허용(레이블 조정)해 주는 역할을 합니다. 즉, 권한 거부(Permission Denied) 에러를 막기 위한 필수 옵션입니다.

 

만약 맥에서 실행시 Permission denied 관련 에러가 난다면 다음의 내용을 참고하세요
  • Rancher desktop을 사용할 경우
    • Rancher desktop 윈도우에서 설정을 들어갑니다.
    • Virtual Machine > Volumes 로 들어가 Mount Type을 9p 로 변경합니다.
    • Security Model을 Mapped-xattr 로 변경합니다.
  • Docker desktop의 경우
    • Docker desktop 윈도우에서 설정에 들어갑니다.
    • General 탭에서 virtoFS을 oxsfs(Legacy)로 수정합니다.

 

🗨️ Docker Compose

Docker Compose 개요

Docker Compose란?

  • 정의:
    • Docker Compose다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구입니다.
    • docker-compose.yml 파일 하나로 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의할 수 있습니다.
      1. 여러 Docker 컨테이너를 한 번에 설정하고 실행하는 도구
      2. DB, 서버, Redis 같은 여러 컨테이너를 한 파일로 관리하고 한 번에 실행하는 도구\

Docker Compose 설치

  • Docker 20.10부터는 Docker Compose가 기본적으로 설치됩니다. 따라서 별도로 설치할 필요가 없습니다.
  • 이전 버전은 중간에 “-” 가 필요하지만 기본 설치 부터는 “ ”으로 대체 되었습니다.
docker-compose
docker compose

Docker Compose 파일 구조

  • docker-compose.yml:
    • Docker Compose 파일은 YAML 형식으로 작성되며, 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의합니다.
version: '3'
services:
  web:
    image: nginx
    ports:
      - "8080:80"
  app:
    build: .
    ports:
      - "8081:8080"
    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example
  1. version: Docker Compose 파일의 버전을 지정합니다.
  2. services: 애플리케이션의 각 서비스를 정의합니다.
  3. web, app, db: 각각의 서비스 이름입니다.
  4. image: 서비스를 실행할 Docker 이미지를 지정합니다.
  5. build: Dockerfile이 있는 디렉토리 경로를 지정하여 이미지를 빌드합니다.
  6. ports: 호스트와 컨테이너 간의 포트를 매핑합니다.
  7. depends_on: 다른 서비스가 먼저 실행되어야 하는 순서를 지정합니다.
  8. environment: 컨테이너의 환경 변수를 설정합니다.

 

Docker Compose 사용법

docker-compose.yml 파일 작성

  • 예제 docker-compose.yml 파일:
version: '3'
services:
  web:
    image: nginx
    ports:
      - "8080:80"
  app:
    build: .
    ports:
      - "8081:8080"
    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example

 

Docker Compose 명령어

  • docker compose up:
    • docker-compose.yml 파일에 정의된 서비스를 빌드하고 시작합니다. 만약 이미 빌드된 이미지가 있다면 이를 사용합니다.
    • 백그라운드에서 실행하려면 d 옵션을 추가합니다.
docker compose up -d
docker compose -f /path/to/your/project/docker-compose.yml up
  • docker compose down:
    • 실행 중인 모든 서비스를 중지하고 컨테이너, 네트워크, 볼륨 등을 정리합니다.
docker compose down
  • docker compose build:
    • docker-compose.yml 파일에 정의된 서비스를 빌드합니다.
docker compose build
  • docker compose ps:
    • 현재 실행 중인 서비스를 확인합니다.
docker compose ps
  • docker compose logs:
    • 실행 중인 서비스의 로그를 확인합니다.
docker compose logs

 

이러한 방식으로 Docker Compose를 사용하면 여러 컨테이너로 구성된 애플리케이션을 쉽게 관리하고 배포할 수 있습니다. docker-compose.yml 파일 하나로 모든 서비스의 설정을 관리할 수 있어 일관된 환경을 제공할 수 있습니다.
참고: Docker Compose는 Docker 버전 20.10부터 기본적으로 포함되어 있으므로 별도의 설치가 필요하지 않습니다 (Docker) (Docker Documentation).

 

실습 예제

  • 1) docker-compose.yml 파일 작성
version: '3'
services:
  web:
    image: nginx
    ports:
      - "8080:80"
  app:
    build: .
    ports:
      - "8081:8080"
    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example
  • 2) Dockerfile 작성
    • app 서비스의 이미지를 빌드하기 위한 Dockerfile을 작성합니다.
# 1. 빈 컨테이너에 'Java 17'이 미리 설치된 리눅스 환경(템플릿)을 가져옵니다.
# 우리가 직접 리눅스를 깔고 Java를 설치할 필요 없이, 세팅이 끝난 버전을 사용하는 것입니다.
FROM eclipse-temurin:17-jre

# 2. 내 컴퓨터에서 빌드된 완성본(target/myapp.jar)을 컨테이너 내부의 '/app' 폴더로 복사합니다.
COPY target/myapp.jar /app/myapp.jar

# 3. 지금부터 터미널 작업 위치를 컨테이너 안의 '/app' 폴더로 이동합니다. (리눅스의 'cd /app'과 같음)
WORKDIR /app

# 4. 컨테이너가 켜질 때 자동으로 실행할 명령어를 지정합니다. 
# 터미널에 'java -jar myapp.jar'를 입력하여 Spring Boot 서버를 켜는 것과 똑같은 동작입니다.
ENTRYPOINT ["java", "-jar", "myapp.jar"]
FROM openjdk:11-jre-slim 
강의에서 사용하는 openjdk:11은 deprecated 되었습니다.
아래와 같이 FROM eclipse-temurin:17-jre로  진행해주세요~!

(주의)
- Java 17 이상에서 실행 가능
- Spring Boot 3 이상에서 사용
  • 3) Docker Compose로 서비스 시작
    • docker-compose.yml 파일이 있는 디렉토리에서 다음 명령어를 실행하여 서비스를 시작합니다.
docker compose up -d

 

🗨️ 애플리케이션 생성

 

🔗 Feign Client & EnableFeignClients 핵심 정리
  • Feign다른 서버의 API를 호출할 때 HTTP 요청 코드를 직접 작성하지 않고 메서드처럼 호출하기 위한 방식이다.
  • 사용할 때는 먼저 인터페이스를 만들고, 여기에 FeignClient 애노테이션을 붙여 어떤 서버를 호출할지 지정한다.
  • 인터페이스 안의 메서드에는 호출할 API 경로를 매핑해주면, 해당 메서드가 실제 HTTP 요청과 연결된다.
  • 이렇게 정의한 인터페이스는 그대로는 동작하지 않기 때문에, 애플리케이션 시작 클래스에 EnableFeignClients를 붙여 Feign을 활성화해야 한다.
  • EnableFeignClientsFeignClient가 붙은 인터페이스를 찾아서 Spring이 사용할 수 있는 객체로 만들어주는 역할을 한다.
  • 이후에는 해당 인터페이스를 주입받아 일반 메서드처럼 호출하면 내부적으로 HTTP 통신이 실행된다.
  • 정리하면 FeignClient는 “어떤 API를 호출할지 정의”, EnableFeignClients는 “그걸 실제로 동작하게 만드는 설정”이다.

 

🌊 마이크로서비스 간 통신 (Feign) 전체 흐름 요약
  • 상황: Service A가 Service B의 데이터를 가져와서 사용자에게 보여줘야 하는 상황
    1. 사용자 요청 진입: 외부(브라우저 등)에서 Service A의 컨트롤러(@GetMapping("/hi"))로 요청이 들어옵니다.
    2. 메서드 호출 (Feign 작동): Service A는 직접 복잡한 통신 코드를 짜는 대신, 주입받은 BServiceClient 인터페이스의 getHello() 메서드를 그냥 툭 호출합니다.
    3. 스프링의 자동 통신 (마법의 순간): 스프링 프레임워크가 Feign 인터페이스에 적힌 설정(@GetMapping("/hello")와 타겟 URL)을 보고, 알아서 HTTP GET 요청을 만들어 Service B로 발사합니다.
    4. 타겟 서버 응답: Service B의 컨트롤러가 해당 요청(/hello)을 받아 처리한 후 "hello"라는 결과값을 Service A로 돌려줍니다.
    5. 최종 결과 반환: Service A는 받아온 "hello" 데이터를 가공하여 최종적으로 사용자에게 응답을 내려줍니다.
  • 보내는 쪽(Service A의 Feign 인터페이스)과 받는 쪽(Service B의 컨트롤러)의 HTTP 메서드와 URL 경로(예: @GetMapping("/hello"))는 무조건 똑같아야 합니다. 하나라도 다르면 404 에러가 발생하며 통신이 끊어집니다.
    1. 보내는 쪽 (Service A의 FeignClient):
      • @GetMapping("/hello")
        👉 "나 타겟 서버의 /hello라는 주소로 GET 요청을 쏠게!"
    2. 받는 쪽 (Service B의 Controller):
      • @GetMapping("/hello")
        👉 "나 /hello라는 주소로 들어오는 GET 요청을 받아서 기다릴게!"

 

service-a (요청을 보내는 서버 - Port: 18080)

AApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

// [핵심] 프로젝트 내의 @FeignClient 어노테이션을 찾아 통신 구현체를 생성하도록 활성화합니다.
@EnableFeignClients
@SpringBootApplication
public class AApplication {

	public static void main(String[] args) {
		SpringApplication.run(AApplication.class, args);
	}

}
AController.java
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AController {

    // [핵심] 스프링이 자동 생성해 준 FeignClient 구현체를 의존성 주입(DI) 받습니다.
    private final BServiceClient bServiceClient;

    @GetMapping("/hi")
    public String hello() {
        // [핵심] RestTemplate 같은 복잡한 HTTP 세팅 없이, 일반 메서드 호출하듯 타겟 서버의 API를 깔끔하게 실행합니다.
        String hello = bServiceClient.getHello();
        return "sevice-a: hi ###### service-b: " + hello;
    }
}
  • 사용자 요청 진입: 외부(브라우저 등)에서 Service A의 컨트롤러(@GetMapping("/hi"))로 요청이 들어옵니다.
  • 메서드 호출 (Feign 작동): Service A는 직접 복잡한 통신 코드를 짜는 대신, 주입받은 BServiceClient 인터페이스의 getHello() 메서드를 그냥 툭 호출합니다.
  • 최종 결과 반환: Service A는 받아온 "hello" 데이터를 가공하여 최종적으로 사용자에게 응답을 내려줍니다.
BServiceClient.java
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

// [핵심] 타겟 서버(Service B)와 통신하기 위한 명세서(인터페이스)입니다.
// url 속성은 application.properties에 설정된 주소를 동적으로 읽어옵니다.
@FeignClient(name = "service-b", url = "${service.b.url}")
public interface BServiceClient {

    // [핵심] 구현 코드 없이 선언만 해두면, 타겟 서버의 'GET /hello' API를 호출하는 HTTP 통신 코드를 스프링이 자동 생성합니다.
    @GetMapping("/hello")
     public String getHello();
}
  • 스프링의 자동 통신 (마법의 순간): 스프링 프레임워크가 Feign 인터페이스에 적힌 설정(@GetMapping("/hello")와 타겟 URL)을 보고, 알아서 HTTP GET 요청을 만들어 Service B로 발사합니다.
application.properties
spring.application.name=service-a

server.port=18080

# [핵심] FeignClient 인터페이스가 HTTP 요청을 보낼 타겟 서버(Service B)의 실제 주소입니다.
service.b.url=http://localhost:18081

 

service-b (요청을 받는 타겟 서버 - Port: 18081)

BController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BController {
	
    // [핵심] Service A의 FeignClient(@GetMapping("/hello"))가 실제로 호출하게 되는 최종 목적지 API입니다.
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}
  • 타겟 서버 응답: Service B의 컨트롤러가 해당 요청(/hello)을 받아 처리한 후 "hello"라는 결과값을 Service A로 돌려줍니다.
application.properties
spring.application.name=service-b

# Service A와 포트가 겹치지 않도록 18081 포트에서 실행합니다.
server.port=18081

 

RUN

 

🗨️ Docker 사용

application.properties 수정

📌 포트를 모두 아래와 같이 8080 으로 변경합니다.
  • Why?! 도커의 외부 포트를 조절하여 애플리케이션을 실행할 것이기 때문입니다.
  • → 호스트에서는 서로 다른 포트를 매핑하여 여러 서비스를 동시에 실행하기 위함입니다.
  • service-a > application.properties
spring.application.name=service-a

server.port=8080

service.b.url=http://service-b:8080
  • service-b > application.properties
spring.application.name=service-b

server.port=8080

 

Dockerfile 생성

  • service-a > Dockerfile
# 1. 빈 컨테이너에 'Java 17'이 미리 설치된 리눅스 환경(템플릿)을 가져옵니다.
# 우리가 직접 리눅스를 깔고 Java를 설치할 필요 없이, 세팅이 끝난 버전을 사용하는 것입니다.
FROM eclipse-temurin:17-jre

# 2. 컨테이너 내부에 '/tmp'라는 임시 데이터 저장소(볼륨)를 만듭니다.
# Spring Boot가 실행될 때 내부적으로 톰캣(Tomcat) 서버를 쓰는데, 이 톰캣이 임시 파일을 저장할 공간이 필요하기 때문입니다.
VOLUME /tmp

# 3. 도커 이미지를 구울(Build) 때 사용할 변수(ARG)를 선언합니다.
# 'JAR_FILE'이라는 변수에 Gradle이 만들어낸 결과물(jar 파일)의 경로를 담아둡니다.
ARG JAR_FILE=build/libs/*.jar

# 4. 내 컴퓨터의 ${JAR_FILE} 경로에 있는 파일을 컨테이너 내부의 'app.jar'라는 깔끔한 이름으로 복사합니다.
# 버전이 붙어 복잡한 원본 파일명(예: myapp-1.0.1.jar)을 다루기 쉽게 'app.jar' 하나로 통일해 버리는 과정입니다.
COPY ${JAR_FILE} app.jar

# 5. 컨테이너가 켜질 때 자동으로 실행할 명령어를 지정합니다. 
# 터미널에 'java -jar /app.jar'를 입력하여 Spring Boot 서버를 켜는 것과 똑같은 동작입니다.
ENTRYPOINT ["java","-jar","/app.jar"]

 

  • jarSpring Boot 프로젝트를 하나의 실행 파일로 묶어놓은 것이다. 즉, 코드 + 라이브러리 + 설정이 전부 들어있는 완성된 프로그램 파일이다. 그래서 jar만 있으면 다른 PC에서도 바로 실행 가능하다.
  1. Java 실행 환경 준비 (FROM)
  2. 내가 만든 프로그램(jar) 넣기 (COPY)
  3. 그 프로그램 실행 (ENTRYPOINT)
💾 도커 데이터 관리 핵심 비교: VOLUME vs -v
  • VOLUME (Dockerfile 내부 작성 / 익명 볼륨)
    • 핵심 목적: 성능 최적화 및 임시 데이터 처리
    • 특징: 컨테이너 내부의 파일 시스템은 구조상 읽고 쓰는 속도가 느립니다. 따라서 지워져도 무방한 임시 파일(예: Spring Boot의 Tomcat 작업 파일)을 다룰 때, 호스트 디스크의 성능을 빌려 빠르게 읽고 쓰기 위해 생성하는 '일회성 임시 작업장'입니다.
    • 수명: 컨테이너가 삭제되면 그 역할이 끝나며, 데이터도 함께 폐기되는 것이 원칙입니다.

 

  • -v 옵션 (실행 명령어 / 바인드 마운트)
    • 핵심 목적: 데이터 영구 보존 및 안전 보장
    • 특징: 호스트 컴퓨터(또는 AWS 서버)의 명확한 특정 경로와 컨테이너를 강력하게 연결합니다. 데이터베이스의 고객 정보나 서버 로그처럼, 어떤 상황에서도 절대 유실되면 안 되는 '핵심 데이터'를 안전하게 격리하여 저장하는 용도입니다.
    • 수명: 컨테이너가 종료되거나 완전히 삭제되어도, 연결된 호스트의 파일은 영구적으로 안전하게 유지됩니다.

 

  • service-b > Dockerfile
FROM eclipse-temurin:17-jre

VOLUME /tmp

ARG JAR_FILE=build/libs/*.jar

COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java","-jar","/app.jar"]

 

도커 네트워크 생성

  • 도커끼리 컨테이너 이름으로 호출하기 위해선 기본 브리지 네트워크가 아닌 사용자 정의 네트워크에서 진행해야 합니다.
docker network create my-network

 

Service-b 실행하기

📌 service-b의 프로젝트 루트 폴더에서 진행합니다 (Dockerfile의 위치)

 

  • 프로젝트 빌드
# (빌드: 개발한 코드를 실행 가능한 파일로 만드는 과정 / Gradle: 이를 도와주는 자동화 도구)

# ./gradlew : 프로젝트에 내장된 Gradle(Wrapper)을 실행 (따로 설치할 필요 없이 바로 사용)
# clean     : 이전 빌드 과정에서 생긴 찌꺼기(build 폴더)를 깨끗하게 삭제 (초기화)
# bootJar   : Spring Boot 서버를 띄울 수 있는 최종 실행 파일(.jar)을 생성

./gradlew clean bootJar
  • 이미지 생성
    • 이미지 이름을 img-service-b 로 만듭니다. (마지막의 . 은 도커파일의 위치)
# 현재 경로의 Dockerfile을 읽어 'img-service-b'라는 이름의 실행용 이미지를 구워냅니다.
# -t (tag) : 생성될 도커 이미지의 이름(태그)을 지정
# . (dot)  : Dockerfile이 위치한 경로 지정 (현재 디렉토리를 의미, 필수 입력)

docker build -t img-service-b .
  • 컨테이너 생성
    • 컨테이너의 이름을 service-b, 외부 포트는 18081 로 지정합니다. 내부 포트가 8080 임을 확인합니다.
# 생성된 이미지로 'service-b' 컨테이너를 실행하며, 네트워크와 외부 접속 포트를 설정합니다.

docker run -d --name service-b \
 --network my-network \
 -p 18081:8080 \
 img-service-b
 
# -d                   : 터미널을 멈추지 않고 백그라운드에서 실행 (Detached)
# --name service-b     : 컨테이너의 식별 이름을 지정
# --network my-network : 컨테이너 간 통신을 위해 지정된 도커 가상 네트워크에 연결
# -p 18081:8080        : 포트 매핑 (외부 호스트 포트 18081 ↔ 내부 컨테이너 포트 8080)
# img-service-b        : 실행할 베이스 이미지의 이름
  • 컨테이너 확인
    • 컨테이너 리스트 명령어로 컨테이너가 실행되어있는지 확인합니다. (확인하는 습관을 만들기!)
docker ps                                                   
CONTAINER ID   IMAGE                                 COMMAND                  CREATED         STATUS                PORTS                                                                                                                                  NAMES
ffdea753159b   img-service-b                         "java -jar /app.jar …"   6 seconds ago   Up 2 seconds          0.0.0.0:18081->8080/tcp, :::18081->8080/tcp

 

Service-a 실행하기

📌 service-a의 프로젝트 루트 폴더에서 진행합니다 (Dockerfile의 위치)
  • 프로젝트 빌드
./gradlew clean bootJar
  • 이미지 생성
    • 이미지 이름을 img-service-a 로 만듭니다. (마지막의 . 은 도커파일의 위치)
 docker build -t img-service-a .
  • 컨테이너 생성
    • 컨테이너의 이름을 service-a, 외부 포트는 18080 로 지정합니다. 내부 포트가 8080 임을 확인합니다.
    • service-b를 호출하는 url을 확인합니다. 컨테이너의 이름 및 내부 포트로 호출하는 것을 볼 수 있습니다.
docker run -d --name service-a \
 --network my-network \
 -p 18080:8080 \
 -e SERVICE_B_URL=http://service-b:8080 \
img-service-a
  • 컨테이너 확인
    • 컨테이너 리스트 명령어로 컨테이너가 실행되어있는지 확인합니다. (확인하는 습관을 만들기!)
(base) ➜  com.service.a docker ps                                                   
CONTAINER ID   IMAGE                                 COMMAND                  CREATED          STATUS                PORTS                                                                                                                                  NAMES
9fea304cd49e   img-service-a                         "java -jar /app.jar"     5 seconds ago    Up 2 seconds          0.0.0.0:18080->8080/tcp, :::18080->8080/tcp

 

🗨️ Docker Compose 사용

📌 도커를 사용하면 컨테이너 두개를 띄우는것도 매우 헷갈립니다. 컴포즈를 사용하면 이러한 상황을 피할 수 있습니다.
  • 기존 도커 컨테이너를 모두 삭제합니다. (이미지가 아닙니다)
docker rm -f 서비스_A_컨테이너_아이디
docker rm -f 서비스_B_컨테이너_아이디

Docker compose 파일 생성

  • service-a 프로젝트와 service-b 프로젝트의 상위 폴더에서 docker-compose.yml 파일을 생성하여 아래와 같이 작성합니다.
version: '3.8'

services:
  service-a:
    image: img-service-a
    ports:
      - "18080:8080"
    environment:
      - SERVICE_B_URL=http://service-b:8080
    depends_on:
      - service-b

  service-b:
    image: img-service-b
    ports:
      - "18081:8080"

networks:
  default:
    driver: bridge
version: '3.8'               # Docker Compose 파일의 문법 버전 지정

services:                    # 한 번에 실행할 컨테이너(서비스) 목록 정의
  service-a:                 # 도커 내부 네트워크에서 '도메인 이름'처럼 사용됨
    image: img-service-a     # 컨테이너를 생성할 대상 이미지
    ports:
      - "18080:8080"         # 포트 매핑 (외부 호스트 18080 ↔ 내부 컨테이너 8080)
    environment:
      # [핵심] 도커 내부망 통신이므로, B의 호스트 포트(18081)가 아닌 '컨테이너 이름'과 '내부 포트(8080)'를 사용!
      - SERVICE_B_URL=http://service-b:8080 
    depends_on:
      - service-b            # 실행 순서 제어: service-b를 먼저 켠 후 service-a를 실행

  service-b:
    image: img-service-b     
    ports:
      - "18081:8080"         # 외부에서 B에 직접 접근할 땐 18081 포트 사용

networks:                    # 컨테이너들이 서로 통신할 가상 네트워크 설정
  default:
    driver: bridge           # 기본 격리망 구축 (이 파일 내의 서비스들끼리만 안전하게 통신)
컨테이너의 내부 IP는 매번 바뀌기 때문에, 도커 컴포즈에 적힌 서비스 이름(service-b) 자체를 변하지 않는 도메인 주소(naver.com처럼)로 사용하여 서버 간 통신을 안전하게 보장하는 것이다
docker compose up -d

 

  • 네트워크는 어떻게 된 걸까?
    1. 네트워크 생성 확인
      • Docker Compose는 자동으로 네트워크를 생성하며, 이 네트워크는 docker-compose.yml 파일이 있는 디렉토리의 이름을 기반으로 합니다. 네트워크의 이름은 다음과 같은 형식으로 생성됩니다
      • “directoryname_default”
    2. docker network ls 를 사용하여 네트워크가 생성되었는지 확인해봅니다.

 

'Back-End > Spring' 카테고리의 다른 글

대규모 스트림 처리  (0) 2026.05.14
프로젝트 관리 심화: 챕터 2 (CI/CD)  (0) 2026.05.14
MSA (Microservice Architecture)  (0) 2026.04.14
Spring 숙련: 챕터 2  (0) 2026.04.08
Spring 숙련: 챕터 1  (0) 2026.04.07