Docker Image vs Container — 클래스와 인스턴스의 관계

최병현·2026년 6월 15일

spring boot

목록 보기
25/34

1. 개념 소개

Docker란 무엇인지 공부하고 나면, 항상 같이 따라오는 두 단어가 있다. 이미지(Image)컨테이너(Container). 처음엔 둘 다 비슷한 말처럼 들리는데, 사실 명확하게 다른 개념이다.

가장 직관적인 비유는 Java의 클래스와 인스턴스다.

  • 이미지 — 클래스(class). 어떻게 만들어질지 정의된 템플릿. 그 자체로는 실행되지 않음
  • 컨테이너 — 인스턴스(instance). 이미지를 바탕으로 만들어진 실행 가능한 실체
// Java로 비유하면
class MyApp { ... }          // 이미지: my-app:1.0

MyApp app1 = new MyApp();    // 컨테이너 1
MyApp app2 = new MyApp();    // 컨테이너 2
// 같은 클래스(이미지)로 여러 인스턴스(컨테이너)를 만들 수 있음
// 각 인스턴스는 독립적인 상태를 가짐

이미지는 읽기 전용이고 변하지 않는다. 컨테이너는 이미지를 기반으로 실행되며, 실행 중에 생기는 변경사항(파일, 상태)은 컨테이너 자신에게만 적용되고 이미지에는 영향을 주지 않는다.


2. 왜 필요한가

이미지와 컨테이너를 분리한 이유를 생각해보자.

만약 "실행할 때마다 환경을 새로 설치"해야 한다면? 매번 Java 설치, 의존성 다운로드, 설정 적용을 반복해야 한다. 느리고, 매번 똑같이 만들어진다는 보장도 없다.

이미지는 "한 번 만들어두고 여러 번 재사용"하기 위한 단위다. 빌드는 한 번만 하고, 그 결과물(이미지)을 가지고 컨테이너를 원하는 만큼 빠르게 띄울 수 있다.

이 분리 구조가 실무에서 가능하게 하는 것들이다.

  • 동일한 환경 재현 — 같은 이미지로 만든 컨테이너는 항상 동일하게 동작한다
  • 빠른 스케일링 — 트래픽이 늘면 같은 이미지로 컨테이너를 더 띄우면 된다
  • 버전 관리 — 이미지에 태그(v1.0, v1.1)를 붙여서 배포 버전을 명확히 관리
  • 롤백 용이성 — 문제가 생기면 이전 버전 이미지로 컨테이너를 다시 띄우면 된다
  • 격리 — 같은 이미지에서 만든 컨테이너끼리도 서로 영향을 주지 않는다

3. 전체 동작 흐름

Spring Boot 애플리케이션을 기준으로 이미지가 만들어지고 컨테이너가 실행되는 과정을 보자.

[이미지 빌드 과정]

Dockerfile
    |
    | docker build -t my-app:1.0 .
    ↓
각 명령어가 레이어로 쌓임
    Layer 1: FROM eclipse-temurin:21-jre
    Layer 2: WORKDIR /app
    Layer 3: COPY app.jar app.jar
    Layer 4: ENTRYPOINT ["java", "-jar", "app.jar"]
    ↓
[이미지 my-app:1.0 생성 완료]
    - 읽기 전용 레이어들의 집합
    - 로컬 이미지 저장소에 보관됨
    - docker images로 확인 가능


[컨테이너 실행 과정]

my-app:1.0 이미지
    |
    | docker run -d -p 8080:8080 --name app1 my-app:1.0
    ↓
① 이미지의 읽기 전용 레이어 위에
   쓰기 가능한 레이어(Container Layer) 추가
② 격리된 프로세스, 네트워크, 파일시스템 네임스페이스 할당
③ ENTRYPOINT에 정의된 명령 실행 (java -jar app.jar)
    ↓
[컨테이너 app1 실행 중]
    - 이미지 레이어(읽기 전용) + 컨테이너 레이어(쓰기 가능)
    - docker ps로 확인 가능


[같은 이미지로 컨테이너 추가 생성]

    | docker run -d -p 8081:8080 --name app2 my-app:1.0
    ↓
[컨테이너 app2 실행 중]
    - app1과 동일한 이미지 레이어 공유
    - 별도의 컨테이너 레이어 (app1과 독립적)
    - app1, app2는 서로 영향 없음

핵심은 이미지 레이어는 공유되고, 컨테이너 레이어만 각자 따로 갖는다는 것이다. 그래서 같은 이미지로 여러 컨테이너를 띄워도 디스크 공간이 크게 늘지 않는다.


4. 핵심 구성 요소

4-1. 이미지 레이어 (Image Layer)

이미지는 여러 개의 읽기 전용(read-only) 레이어가 쌓인 구조다. Dockerfile의 각 명령(FROM, COPY, RUN 등)이 하나의 레이어가 된다. 레이어는 캐싱되고 여러 이미지 간에 공유될 수 있다.

예를 들어 eclipse-temurin:21-jre 베이스 이미지를 사용하는 여러 프로젝트의 이미지들은 이 베이스 레이어를 공유한다. 디스크에 중복 저장되지 않는다.

4-2. 컨테이너 레이어 (Container Layer)

컨테이너가 실행될 때 이미지 레이어 위에 추가되는 쓰기 가능한 레이어다. 컨테이너 안에서 파일을 생성하거나 수정하면 이 레이어에 기록된다. 컨테이너가 삭제되면 이 레이어도 함께 삭제된다 — 즉, 컨테이너 안의 변경사항은 영구적이지 않다.

4-3. 이미지 태그 (Tag)

이미지 이름 뒤에 붙는 버전 식별자다. my-app:1.0, my-app:latest처럼 사용한다. latest는 "가장 최근 버전"이라는 의미가 아니라, 태그를 명시하지 않았을 때 사용되는 기본 태그명일 뿐이다. 운영 환경에서는 latest 대신 명시적인 버전 태그를 사용하는 것이 안전하다.

4-4. 이미지 ID와 컨테이너 ID

이미지와 컨테이너는 각각 고유한 ID(해시값)를 가진다. 같은 이미지에서 만든 컨테이너라도 컨테이너 ID는 모두 다르다. docker ps -a로 정지된 컨테이너까지 포함해서 확인할 수 있다.


5. Spring Boot에서 어떻게 연결되는가

Spring Boot 프로젝트에서 이미지와 컨테이너의 관계가 실제로 어떻게 적용되는지 보자.

[개발 단계]
- 코드 수정 → docker build로 새 이미지 생성 (my-app:1.1)
- 기존 이미지(my-app:1.0)는 그대로 남아있음

[배포 단계]
- 운영 서버에서 my-app:1.1 이미지로 새 컨테이너 실행
- 기존 my-app:1.0 컨테이너는 중지 후 제거 (또는 무중단 배포 시 점진적 교체)

[스케일링]
- 트래픽 증가 → my-app:1.1 이미지로 컨테이너 3개 추가 실행
- 모두 같은 이미지를 기반으로 하므로 동일하게 동작

[롤백]
- my-app:1.1에 버그 발견 → my-app:1.0 이미지로 컨테이너 재실행
- 빠르게 이전 버전으로 복구

버전 관리의 단위는 이미지이고, 실행 단위는 컨테이너다. 배포, 스케일링, 롤백 모두 "어떤 이미지로 컨테이너를 띄울 것인가"의 문제로 단순화된다.


6. 간단한 예제 코드

6-1. 이미지 생성과 확인

# 이미지 빌드
docker build -t my-app:1.0 .

# 로컬에 저장된 이미지 목록 확인
docker images

# 출력 예시
# REPOSITORY   TAG    IMAGE ID       SIZE
# my-app       1.0    a1b2c3d4e5f6   220MB

6-2. 같은 이미지로 여러 컨테이너 실행

# 컨테이너 1 - 호스트 8080 포트
docker run -d -p 8080:8080 --name app1 my-app:1.0

# 컨테이너 2 - 호스트 8081 포트 (같은 이미지!)
docker run -d -p 8081:8080 --name app2 my-app:1.0

# 컨테이너 3 - 호스트 8082 포트
docker run -d -p 8082:8080 --name app3 my-app:1.0

# 실행 중인 컨테이너 확인
docker ps

# 출력 예시
# CONTAINER ID   IMAGE          PORTS                    NAMES
# 1a2b3c4d       my-app:1.0     0.0.0.0:8080->8080/tcp   app1
# 5e6f7g8h       my-app:1.0     0.0.0.0:8081->8080/tcp   app2
# 9i0j1k2l       my-app:1.0     0.0.0.0:8082->8080/tcp   app3

세 개의 컨테이너는 동일한 이미지(my-app:1.0)에서 만들어졌지만, 각자 독립적인 프로세스로 실행되고 있다. 서로의 메모리, 파일시스템에 영향을 주지 않는다.

6-3. 컨테이너 안에서 파일 변경 — 이미지에 영향 없음

# 컨테이너 안으로 들어가기
docker exec -it app1 /bin/sh

# 컨테이너 안에서 파일 생성
echo "test" > /app/test.txt
exit

# 같은 이미지로 새 컨테이너 실행
docker run -d --name app4 my-app:1.0

# app4 안에는 test.txt가 없음 (이미지는 변경되지 않았음)
docker exec app4 ls /app

6-4. 컨테이너 변경사항을 이미지로 만들기 (참고용, 권장하지 않음)

# 컨테이너의 현재 상태를 새 이미지로 커밋
docker commit app1 my-app:1.0-modified

# 주의: 이 방식은 Dockerfile 기반 빌드보다 추적이 어려워서
# 실무에서는 권장하지 않음. Dockerfile을 수정하고 다시 build하는 게 정석

6-5. 정리 명령어

# 컨테이너 중지
docker stop app1 app2 app3

# 컨테이너 삭제 (중지된 컨테이너만 가능)
docker rm app1 app2 app3

# 이미지 삭제 (해당 이미지로 만든 컨테이너가 없어야 가능)
docker rmi my-app:1.0

# 사용하지 않는 리소스 한꺼번에 정리
docker system prune

7. 자주 헷갈리는 부분

컨테이너를 삭제해도 이미지는 남는다

docker rm으로 컨테이너를 지워도 이미지는 영향받지 않는다. 이미지는 별도로 docker rmi로 삭제해야 하며, 해당 이미지를 사용 중인 컨테이너(중지 상태 포함)가 있으면 삭제되지 않는다.

컨테이너 안의 데이터는 컨테이너가 삭제되면 사라진다

컨테이너 레이어는 컨테이너의 생명주기와 함께한다. 로그 파일, 업로드된 파일, DB 데이터 같은 것들을 컨테이너 안에만 저장하면 컨테이너를 삭제하는 순간 전부 사라진다. 영구 보관이 필요한 데이터는 볼륨(Volume)을 사용해서 호스트 파일시스템이나 별도 저장소에 보관해야 한다.

같은 이미지 = 항상 같은 컨테이너가 아니다

같은 이미지로 만든 컨테이너라도, 실행 중에 각자의 상태(메모리, 파일, 환경변수)가 달라질 수 있다. 환경변수를 다르게 주거나(-e), 파일을 다르게 마운트하면 같은 이미지에서 시작했어도 서로 다르게 동작하는 컨테이너가 된다.

# 같은 이미지, 다른 환경변수 → 다르게 동작
docker run -d -e SPRING_PROFILES_ACTIVE=dev my-app:1.0
docker run -d -e SPRING_PROFILES_ACTIVE=prod my-app:1.0

이미지 이름과 레지스트리 경로

docker pull nginx처럼 짧게 쓰는 이미지 이름은 사실 docker.io/library/nginx:latest의 축약형이다. 구조는 레지스트리주소/네임스페이스/이미지명:태그다.

docker pull myregistry.com/myteam/my-app:1.0
            └────────┬────────┘ └──┬──┘ └──┬──┘ └┬┘
               레지스트리       네임스페이스 이미지명 태그

8. 실무에서 중요한 포인트

이미지 태그는 명확하게 — latest 사용 주의

latest 태그는 "최신"이라는 보장이 없다. 누군가 my-app:latest로 푸시하면 그게 곧 latest가 된다. 운영 배포에서는 커밋 해시, 버전 번호, 빌드 번호 같은 명확한 태그를 사용해서 "어떤 코드 상태의 이미지가 배포되어 있는지" 항상 추적 가능하게 해야 한다.

docker build -t myregistry/my-app:v1.2.3 .
docker build -t myregistry/my-app:$(git rev-parse --short HEAD) .

이미지 크기 관리

이미지가 크면 빌드, 푸시, 풀 시간이 모두 늘어난다. 멀티 스테이지 빌드, 경량 베이스 이미지(jre vs jdk), .dockerignore 활용으로 최종 이미지 크기를 최소화하는 것이 배포 속도에 직결된다.

컨테이너는 "일회용"으로 다루기 (Immutable Infrastructure)

컨테이너에 직접 들어가서(docker exec) 파일을 수정하는 방식은 변경 이력이 남지 않고, 컨테이너를 재생성하면 사라지는 임시 수정이 된다. 변경이 필요하면 Dockerfile을 수정하고 새 이미지를 빌드해서 컨테이너를 교체하는 방식이 재현 가능하고 추적 가능한 운영 방식이다.

이미지 스캔으로 보안 취약점 확인

베이스 이미지나 의존성에 알려진 보안 취약점이 있을 수 있다. docker scan이나 CI 파이프라인에 통합된 이미지 스캔 도구로 이미지 빌드 단계에서 취약점을 점검하는 것이 좋다.


9. 정리

  • 이미지는 변하지 않는 읽기 전용 템플릿, 컨테이너는 이미지를 실행한 살아있는 인스턴스다
  • 이미지는 여러 개의 읽기 전용 레이어로 구성되고, 여러 이미지/컨테이너 간에 공유될 수 있다
  • 컨테이너는 이미지 레이어 위에 쓰기 가능한 컨테이너 레이어를 추가해서 동작한다
  • 컨테이너 안에서의 변경사항은 이미지에 영향을 주지 않고, 컨테이너가 삭제되면 함께 사라진다
  • 같은 이미지로 여러 컨테이너를 독립적으로 실행할 수 있다 — 스케일링과 일관성의 기반이 된다
  • 영구 데이터는 볼륨으로 분리해야 하고, 변경은 Dockerfile 수정 → 재빌드 → 컨테이너 교체 방식이 정석이다
  • 운영 환경에서는 latest 대신 명확한 버전 태그로 이미지를 관리해야 추적과 롤백이 쉽다

10. 느낀 점

이미지와 컨테이너를 그냥 "같은 거 다르게 부르는 말"이라고 생각했는데, 직접 같은 이미지로 컨테이너 세 개를 띄워보고 하나의 컨테이너 안에서 파일을 만들어도 다른 컨테이너에는 영향이 없는 걸 확인하고 나서야 "아, 이게 클래스랑 인스턴스 관계구나"가 명확하게 이해됐다.

컨테이너 안에서의 변경은 휘발성이라는 걸 이해하는 게 가장 중요한 포인트인 것 같다. 컨테이너에 들어가서 직접 수정하고 싶은 유혹이 들 때마다, "이건 컨테이너가 재생성되면 사라진다"는 걸 떠올리면 왜 Dockerfile을 수정해서 다시 빌드해야 하는지, 왜 볼륨이 필요한지가 자연스럽게 연결된다.

profile
Develop

0개의 댓글