target-tracking-service에 Dockerfile을 새로 만들면서 나온 개념들. "왜 이렇게 써야 하는가"에 초점.

1. 이미지(Image)와 컴테이너(Container)는 다른 것이다

이미지는 클래스, 컴테이너는 인스턴스에 가깝다. 같은 이미지로 컴테이너를 몇 개든 띄울 수 있고, 컴테이너를 지워도 이미지는 남아있다.

2. 왜 멀티스테이지 빌드인가

# ---- Build stage ----
FROM eclipse-temurin:21-jdk-alpine AS build
...
RUN ./gradlew bootJar --no-daemon -x test

# ---- Run stage ----
FROM eclipse-temurin:21-jre-alpine
COPY --from=build /app/build/libs/*.jar app.jar

JDK와 JRE는 용도가 다르다. JDK는 javac 같은 컴파일러까지 포함해서 무겁고, JRE는 이미 컴파일된 .class/.jar를 실행만 하는 최소 구성이다.

빌드하려면 JDK가 필요하지만 실행할 때는 컴파일러가 전혀 필요 없다. 단일 스테이지로 하면 최종 이미지 안에 컴파일러·Gradle 캐시·소스코드까지 전부 남아 이미지가 훨씬 커진다. 멀티스테이지는 build라는 임시 스테이지에서 무거운 빌드 작업을 다 끝낸 다음, COPY --from=build로 결과물(jar) 따만 가벼운 실행용 이미지로 복사해온다. 이미지 크기도 줄고 공격 표면도 줄어든다.

3. 레이어 캐싱과 COPY 순서

COPY gradlew build.gradle settings.gradle ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon || true

COPY src ./src
RUN ./gradlew bootJar --no-daemon -x test

Docker 이미지는 각 명령마다 레이어를 하나씩 쌓는다. 이전 레이어와 입력이 또같으면 다시 실행하지 않고 캐시를 재사용한다. 의존성 설치(build.gradle 기반)와 소스코드 복사(src)를 의도적으로 분리했다 — 소스만 고쳤을 때 의존성 다운로드 레이어는 캐시를 그대로 쓰고 src부터만 다시 실행된다 — 재빌드가 훨씬 빨라진다.

4. .dockerignore가 하는 일

docker build .를 실행하면 Docker는 현재 디렉토리 전체를 "빌드 컨테스트"로 데몬에 전송한다. .dockerignore는 .gitignore와 같은 문법으로 이 전송 대상에서 빼야 할 것을 지정한다(.git, build 산출물 등). 실수로 .env 같은 민감 파일이 이미지 레이어에 들어가는 것도 막아준다.

5. docker-compose는 뭐을 해주는가 — 컴테이너 간 네트워킹

docker-compose.yml로 여러 컴테이너를 같이 띄우면, Compose가 자동으로 전용 네트워크를 만들고 그 안에서 서비스 이름 = 호스트 이름으로 서로를 찾을 수 있게 해준다.

environment:
  SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/defense_db

컴테이너 안에서 다른 컴테이너로 갈 때는 서비스 이름, 호스트에서 컴테이너로 갈 때는 localhost + 매핑된 포트 — 이 둘을 헷갈리면 안 된다.

6. Kafka advertised.listeners가 까다로운 이유

클라이언트가 Kafka 브로커에 접속하는 과정은 2단계다: (1) bootstrap-servers로 최초 접속해서 브로커 목록/주소 메타데이터를 받고, (2) 그 메타데이터에 적힌 주소(advertised listener)로 실제 프로듀스/컨슈머 요청을 보낸다. 그런데 "어디서 접속하느냐"에 따라 같은 브로커라도 올바른 주소가 다르다.