spring boot docker compose support ๋กœ์ปฌ์—์„œ ์ธํ”„๋ผ ์˜ฌ๋ ค์„œ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ (spring boot 3.1)

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉด์„œ ๋กœ์ปฌ์— docker๋กœ ์ธํ”„๋ผ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๊ณ  ๋กœ์ปฌ์—์„œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•ด ๋ณธ ์ ์ด ์žˆ์„ ๊ฒƒ์ด๋‹ค. ๋กœ์ปฌ์— ์ธํ”„๋ผ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด์„œ docker ์—”์ง„์„ ๊ตฌ๋™ํ•˜๊ณ  docker compose๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋กœ์ปฌ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ๋™ ์ „์— docker compose๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ปจํ…Œ์ด๋„ˆ๋กœ ์˜ฌ๋ฆฌ๋Š” ์ผ์€ ๋งค์šฐ ๋ฒˆ๊ฑฐ๋กญ๋‹ค.
ํ•˜์ง€๋งŒ spring boot docker compose support๋ฅผ ํ†ตํ•ด์„œ ์‰ฝ๊ฒŒ ๋กœ์ปฌ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ๋™ํ•  ์ˆ˜ ์žˆ๋‹ค. spring boot 3.1๋ถ€ํ„ฐ ์ง€์›๋˜๋Š” docker compose๋ฅผ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๋Š”์ง€ ๊ทธ ์‚ฌ์šฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด ๋ณด๊ณ ์ž ํ•œ๋‹ค.

spring boot docker compose๋Š” docker compose๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ yaml์— ๊ด€๋ จ ์„ค์ •์„ ์ •์˜๋งŒ ํ•˜๋ฉด ์ง€์ •๋œ docker compose ํŒŒ์ผ์„ ํ†ตํ•ด container๋ฅผ ์˜ฌ๋ฆฌ๊ณ  ConnectionDetails ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด์„œ ๋‚ด๋ถ€์ ์œผ๋กœ ์ž๋™ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด์„œ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ๋™์„ ์œ„ํ•ด์„œ ์ˆ˜๋™์œผ๋กœ docker container๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •์—์„œ ์™ธ๋ถ€ ์„œ๋น„์Šค ์—ฐ๊ฒฐ์— ๋Œ€ํ•œ ์„ค์ •์„ ๋”ฐ๋กœ ์ง€์ •ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

ConnectionDetails ์ถ”์ƒํ™”์— ๋Œ€ํ•œ ์„ค๋ช…์€
Spring Boot 3.1’s ConnectionDetails abstraction ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ธฐ ๋ฐ”๋ž€๋‹ค.

Spring Boot docker compose Dependency

spring boot docker compose๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ dependency๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

maven

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-docker-compose</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
XML

gradle

dependencies {
    developmentOnly("org.springframework.boot:spring-boot-docker-compose")
    testAndDevelopmentOnly("org.springframework.boot:spring-boot-docker-compose")
}
Groovy

testAndDevelopmentOnly(“org.springframework.boot:spring-boot-docker-compose”)
๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰์‹œ์—๋„ spring boot docker compose ๋ฅผ ์ด์šฉํ•˜์—ฌ docker container๋ฅผ ์‹คํ–‰ํ•˜๋„๋ก ํ•œ๋‹ค.
spring-boot-docker-compose ์ข…์†์„ฑ์ด ์ถ”๊ฐ€๋˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋™์ž‘์„ ํ•œ๋‹ค.

  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ compose.yaml ๋ฐ ๊ธฐํƒ€ ์ผ๋ฐ˜์ ์ธ ์ž‘์„ฑ ํŒŒ์ผ ์ด๋ฆ„์„ ๊ฒ€์ƒ‰ํ•œ๋‹ค.
  • ๊ฒ€์ƒ‰๋œ docker compose ์ž‘์„ฑ ํŒŒ์ผ์— ๋Œ€ํ•ด์„œ docker compose up ๋ช…๋ น์„ ์‹คํ–‰ํ•œ๋‹ค.
  • ์ง€์›๋˜๋Š” ๊ฐ ์ปจํ…Œ์ด๋„ˆ์— ๋Œ€ํ•œ ์„œ๋น„์Šค ์—ฐ๊ฒฐ Bean์„ ์ƒ์„ฑํ•œ๋‹ค. (ConnectionDetails ์ถ”์ƒํ™”)
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด shutdown ๋  ๋•Œ docker compose stop ๋ช…๋ น์„ ์‹คํ–‰ํ•œ๋‹ค.

starter.spring.io๋ฅผ ํ†ตํ•ด์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ‘Docker Compose support’ ์ข…์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ง€์›๋˜๋Š” Docker Image์™€ ๊ด€๋ จ๋œ ์„œ๋น„์Šค driver๊ฐ€ ์ข…์†์„ฑ์œผ๋กœ ์ถ”๊ฐ€๋˜๋ฉด ์ž๋™์œผ๋กœ compose.yaml ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด ์ค€๋‹ค.

Service Connections

Container ์„œ๋น„์Šค์— ๋Œ€ํ•œ ์—ฐ๊ฒฐ์€ ํ˜„์žฌ ์ง€์›๋˜๋Š” Container์ธ ๊ฒฝ์šฐ ConnectionDetails ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด์„œ ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ ์„ค์ •์„ ํ•œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Container ๋‚ด๋ถ€์˜ ํฌํŠธ์™€ ํ˜ธ์ŠคํŠธ ํฌํŠธ๊ฐ€ ๋งตํ•‘๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ์จ ๋กœ์ปฌ์—์„œ ์—ฐ๊ฒฐ์„ ํ•  ๋•Œ๋Š” ํ˜ธ์ŠคํŠธ ํฌํŠธ๋ฅผ ํ†ตํ•ด์„œ ์ ‘์†์„ ํ•ด์•ผ ํ•˜๋Š”๋ฐ ์ด ํ˜ธ์ŠคํŠธ ํฌํŠธ๋Š” Container๊ฐ€ ์ƒˆ๋กœ ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ๋ณ€๊ฒฝ๋˜๋Š”๋ฐ spring boot์—์„œ ๋งตํ•‘๋œ ํ˜ธ์ŠคํŠธ ํฌํŠธ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ์ž๋™ ์„ค์ •ํ•œ๋‹ค. ์ง€์›๋˜์ง€ ์•Š๋Š” Container์˜ ๊ฒฝ์šฐ์—๋Š” ๋ณ„๋„๋กœ ์—ฐ๊ฒฐ ์„ค์ •์ด ํ•„์š”ํ•˜๊ณ  docker compose ์ •์˜์‹œ ํ˜ธ์ŠคํŠธ ํฌํŠธ๋ฅผ ๊ณ ์ • ํฌํŠธ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค. ํ˜„์žฌ ์ง€์›๋˜๋Š” container image๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Connection DetailsMatched on
CassandraConnectionDetailsContainers named “cassandra”
ElasticsearchConnectionDetailsContainers named “elasticsearch”
JdbcConnectionDetailsContainers named “gvenzl/oracle-xe”, “mariadb”,
“mssql/server”, “mysql”, or “postgres”
MongoConnectionDetailsContainers named “mongo”
R2dbcConnectionDetailsContainers named “gvenzl/oracle-xe”, “mariadb”,
“mssql/server”, “mysql”, or “postgres”
RabbitConnectionDetailsContainers named “rabbitmq”
RedisConnectionDetailsContainers named “redis”
ZipkinConnectionDetailsContainers named “openzipkin/zipkin”

์ฃผ์˜ํ•ด์•ผ ํ•  ๊ฒƒ์€ ์ด๋ฏธ์ง€ ์ด๋ฆ„์€ Matched on์— ๊ธฐ์žฌ๋œ ์ด๋ฆ„์ด์–ด์•ผ ์„œ๋น„์Šค๋กœ ์ž๋™ ์—ฐ๊ฒฐ์ด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฏธ์ง€์— ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ compose.yaml ํŒŒ์ผ์— label์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฅผ ํ•ด๊ฒฐ ํ•  ์ˆ˜ ์žˆ๋‹ค.

services:
  redis:
    image: 'mycompany/mycustomredis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.service-connection: redis
YAML

mycompany/mycustomredis ์ด๋ฏธ์ง€๋ฅผ spring boot์— redis ๋ผ๋Š” ๊ฒƒ์„ ์•Œ๋ ค์ค˜ RedisConnectionDetails๋ฅผ ํ†ตํ•ด ์ž๋™ ์—ฐ๊ฒฐ ๋œ๋‹ค. ์‹ค์ œ๋กœ spring-boot-docker-compose ๋””ํŽœ๋˜์‹œ์˜ org.springframework.boot.docker.compose.service.connection ํŒจํ‚ค์ง€์— ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์„œ๋น„์Šค๋“ค์ด ํ•˜์œ„ ํŒจํ‚ค์ง€์— ์ •์˜๋˜์–ด ์žˆ๋‹ค.

spring boot docker compose support - Service Connections

๋˜ํ•œ DockerComposeConnectionDetailsFactory์— DockerComposeConnectionDetails inner class๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”๋ฐ ์ด๋ฅผ ์ƒ์†ํ•˜๋Š” class๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค. ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์ด ์œ„ ํ‘œ์— ์ง€์ •๋œ container image์™€ ์ผ๋งฅ ์ƒํ†ตํ•œ๋‹ค.

spring boot docker compose support - ์ง€์›๋˜๋Š” ์ธํ”„๋ผ

๊ฐ DockerComposeConnectionDetails ์ž์‹ ํด๋ž˜์Šค๋“ค์€ ๊ฐ ์„œ๋น„์Šค์— ํ•ด๋‹นํ•˜๋Š” ConnectionDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค.

Docker Compose Lifecycle

๊ธฐ๋ณธ์ ์œผ๋กœ spring boot๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘๋  ๋•Œ docker compose up์„ ํ˜ธ์ถœํ•˜๊ณ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ค‘์ง€๋  ๋•Œ docker compose stop์„ ํ˜ธ์ถœํ•œ๋‹ค. ๋‹ค๋ฅธ lifecycle ๊ด€๋ฆฌ๋ฅผ ํ•˜๊ณ ์ž ํ•  ๋•Œ๋Š” spring.docker.compose.lifecycle-management ์†์„ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ’์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • none : docker compose๋ฅผ ์‹œ์ž‘ํ•˜๊ฑฐ๋‚˜ ์ค‘์ง€ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • start-only : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘๋  ๋•Œ๋งŒ docker compose๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  • start-and-stop : ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘๋  ๋•Œ docker compose๋ฅผ ์‹œ์ž‘ํ•˜๊ณ  ์ค‘์ง€๋  ๋•Œ docker compose๋ฅผ ์ค‘์ง€ํ•œ๋‹ค. spring.docker.compose.start.command ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ docker compose up ํ˜น์€ docker compose start๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    spring.docker.compose.stop.command ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ docker compose down ํ˜น์€ docker compose stop์„ ์„ค์ •ํ•œ๋‹ค.
spring:
  docker:
    compose:
      lifecycle-management: start-and-stop
      start:
        command: up
      stop:
        command: down
        timeout: 1m
YAML

Spring Boot Docker Compose for Test

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰์‹œ์‹คํ–‰ ์‹œ docker compose๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š์ง€๋งŒ ์•„๋ž˜ ์„ค์ •์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰ ์‹œ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

spring.docker.compose.skip.in-tests=false
YAML

์ƒ˜ํ”Œ ์ฝ”๋“œ

docker compose๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ƒ˜ํ”Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด์ž. ์ƒ˜ํ”Œ ์ฝ”๋“œ๋Š”

  • kafka, zookeeper, mariadb ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ƒ์„ฑํ•˜๋Š” compose.yaml ํŒŒ์ผ์„ ์ •์˜ํ•œ๋‹ค.
  • Member ์ƒ์„ฑ ์š”์ฒญ API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด Kafka์˜ test-topic ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค.
  • Kafka์˜ test-topic ์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ Member ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.

๋ฐ์ดํ„ฐ๋ฅผ kafka๋ฅผ ํ†ตํ•ด์„œ ์†ก์ˆ˜์‹  ํ•œ ๋’ค์— DB์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ์ด์ƒํ•˜๊ธด ํ•˜์ง€๋งŒ ์–ด๋””๊นŒ์ง€๋‚˜ docker compose ์‚ฌ์šฉ์— ๋Œ€ํ•œ ์ƒ˜ํ”Œ์ด๋ฏ€๋กœ docker compose ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ์ค‘์ ์„ ๋‘๊ธธ ๋ฐ”๋ž€๋‹ค.

dependency

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <!-- spring boot docker compose๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ•„์š”ํ•œ dependency -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-docker-compose</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
XML

starter.spring.io๋ฅผ ํ†ตํ•ด์„œ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ spring-boot-docker-compose ๋””ํŽœ๋˜์‹œ์™€ ํ˜„์žฌ ์ง€์›๋˜๋Š” container ์ข…๋ฅ˜์ธ mariadb์— ๋Œ€ํ•œ mariadb-java-client ๋””ํŽœ๋˜์‹œ๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด compose.yaml ํŒŒ์ผ์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.

services:
  mariadb:
    image: 'mariadb:latest'
    environment:
      - 'MARIADB_DATABASE=mydatabase'
      - 'MARIADB_PASSWORD=secret'
      - 'MARIADB_ROOT_PASSWORD=verysecret'
      - 'MARIADB_USER=myuser'
    ports:
      - '3306'
YAML

์—ฌ๊ธฐ์„œ ์ถ”๊ฐ€๋กœ kafka๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— kafka container ์ƒ์„ฑ์„ ์œ„ํ•œ ์ •์˜๋„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

services:
  mariadb:
    image: 'mariadb:latest'
    environment:
      - 'MARIADB_DATABASE=mydatabase'
      - 'MARIADB_PASSWORD=secret'
      - 'MARIADB_ROOT_PASSWORD=verysecret'
      - 'MARIADB_USER=myuser'
    ports:
      - '3306'
  zookeeper:
    image: docker.io/bitnami/zookeeper:3.8
    ports:
      - "2181:2181"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    image: docker.io/bitnami/kafka:3.4
    ports:
      - "9092:9092"
      - "9094:9094"
    environment:
      - ALLOW_PLAINTEXT_LISTENER=yes
      - KAFKA_ENABLE_KRAFT=no
      - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - KAFKA_CFG_LISTENERS=INTERNAL://kafka:9094,EXTERNAL://kafka:9092
      - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9094,EXTERNAL://127.0.0.1:9092
      - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL
    depends_on:
      - zookeeper
YAML

application.yml

spring:
  profiles:
    active: local
  kafka:
    producer:
      bootstrap-servers: localhost:9092
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      client-id: kafka-test-producer
    consumer:
      bootstrap-servers: localhost:9092
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      group-id: test-group-id
      auto-offset-reset: earliest
      client-id: kafka-test-consumer
      properties:
        spring.json.trusted.packages: com.example.spring.dockercompose.dto
    listener:
      concurrency: 3
      ack-mode: record
---
# ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ๋™ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ docker compose๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.
# docker compose mariadb container๊ฐ€ ์ง€์› ๋˜๋ฏ€๋กœ ๋ณ„๋„์˜ ์—ฐ๊ฒฐ์„ค์ • (datasource)์€ ํ•„์š”์—†๋‹ค.
# ์ด์œ ๋Š” ConnectionDetails ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด์„œ spring boot์—์„œ ์ž๋™์œผ๋กœ ์—ฐ๊ฒฐ์„ ํ•ด์ค€๋‹ค.
spring:
  config:
    activate:
      on-profile: local

  jpa:
    hibernate:
      ddl-auto: create

  # spring boot docker compose enable ์„ค์ •
  docker:
    compose:
      enabled: true
      lifecycle-management: start_and_stop
      stop:
        command: down
        timeout: 1m
---

# production ํ™˜๊ฒฝ์—์„œ๋Š” docker compose disable
# ์„ค์ •์œผ๋กœ docker compose๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.
# mariadb datasource ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•  ๊ฒƒ์ด๋‹ค.
spring:
  config:
    activate:
      on-profile: production
  docker:
    compose:
      enabled: false
YAML

local profile์ธ ๊ฒฝ์šฐ์—๋งŒ docker compose๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ•˜์˜€๋‹ค.
production profile์˜ ๊ฒฝ์šฐ์—๋Š” docker compose ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค. kafka๋Š” ํ˜„์žฌ spring boot docker compose์—์„œ ์ง€์›๋˜์ง€ ์•Š๋Š” Container์ด๊ธฐ ๋•Œ๋ฌธ์— ConnectionDetails ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•œ ์ž๋™ ์—ฐ๊ฒฐ์ด ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •์„ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

Class ์ฝ”๋“œ

@Configuration
@Slf4j
@EnableKafka
public class TopicConfig {
	public static final String TOPIC_NAME = "test-topic";

	@Bean
	public NewTopic testTopic() {
		return new NewTopic(TOPIC_NAME, 3, CreateTopicsRequest.NO_REPLICATION_FACTOR);
	}
}
Java

kafka์— ‘test-topic’์ด๋ฆ„์˜ ํ† ํ”ฝ์„ ์ƒ์„ฑํ•œ๋‹ค.

@Entity
@Table(name = "Member")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Member{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, unique = true)
    private String userName;

    @Column(name = "age", nullable = false, length = 4)
    private Integer age;

    public static Member of(Long id) {
        return new Member(id);
    }

    private Member(Long id) {
        this.id = id;
    }

    @Builder
    protected Member(Long id, String userName, Integer age) {
        this.id = id;
        this.userName = userName;
        this.age = age;
    }
}
Java

Member ํ…Œ์ด๋ธ” ์ •์˜ entity ํด๋ž˜์Šค๋‹ค.

@Builder
public record MemberDto(@JsonProperty("id") Long id,
						@JsonProperty("userName") String userName,
						@JsonProperty("age") Integer age) {

	public Member entity() {
		return Member.builder()
			.userName(userName)
			.age(age)
			.build();
	}
}
Java

Member entity ๋ณ€ํ™˜์„ ์œ„ํ•œ DTO ํด๋ž˜์Šค๋‹ค.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
Java

Member entity ์กฐํšŒ ๋ฐ ์ €์žฅ์„ ์œ„ํ•œ repository ํด๋ž˜์Šค๋‹ค.

@Slf4j
@Service
@RequiredArgsConstructor
public class ConsumerService {
	private final MemberRepository memberRepository;

	@KafkaListener(
		topics = TopicConfig.TOPIC_NAME,
		clientIdPrefix = "topic1-listener",
		groupId = "${spring.kafka.consumer.group-id}"
	)
	public void listen(MemberDto memberDto, ConsumerRecordMetadata metadata) {
		log.info("received message: {}", memberDto);
		log.info("received topic: {}", metadata.topic());
		log.info("received partition: {}", metadata.partition());
		log.info("received offset: {}", metadata.offset());
		Member saved = memberRepository.save(memberDto.entity());
		log.info("saved member {}", saved);
	}
}
Java

kafka ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ ์„ ์œ„ํ•œ consumer ํด๋ž˜์Šค๋‹ค.

@Service
@Slf4j
@RequiredArgsConstructor
public class ProducerService {
	private final KafkaTemplate<String, Object> kafkaTemplate;

	public void produce(final MemberDto memberDto) {
		String key = UUID.randomUUID().toString();
		kafkaTemplate.send(TopicConfig.TOPIC_NAME, key, memberDto)
			.whenComplete((result, throwable) -> {
				if (throwable != null) {
					log.error("fail to send message, {}", throwable.getMessage());
				} else {
					RecordMetadata metadata = result.getRecordMetadata();
					log.info("send message: {}", memberDto);
					log.info("send topic: {}", metadata.topic());
					log.info("send partition: {}", metadata.partition());
					log.info("send offset: {}", metadata.offset());
				}
			});
	}
}
Java

kafka์˜ ‘test-topic’์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•˜๊ธฐ ์œ„ํ•œ producer ํด๋ž˜์Šค๋‹ค.

@RestController
@RequiredArgsConstructor
public class MemberController {
	private final MemberRepository memberRepository;
	private final ProducerService producerService;

	@GetMapping("/member/{id}")
	public ResponseEntity<Member> getMember(@PathVariable Long id) {
		Optional<Member> optionalMember = memberRepository.findById(id);
		Member member = optionalMember.orElseThrow(NoSuchElementException::new);
		return ResponseEntity.ok(member);
	}

	@PostMapping("/member")
	public void postMember(@RequestBody MemberDto memberDto) {
		producerService.produce(memberDto);
	}
}
Java

REST API controller ํด๋ž˜์Šค๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

spring boot ์—์„œ docker compose๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์ „์— ์•„๋ž˜ ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.

  • docker ์—”์ง„์ด ๊ตฌ๋™๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • docker compose ํ˜น์€ docker-compose CLI ๋ช…๋ น์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.

local profile์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด container๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Network spring-docker-compose_default  Creating
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Network spring-docker-compose_default  Created
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Creating
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Creating
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Created
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Created
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Creating
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Created
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Starting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Starting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Started
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Starting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Started
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Started
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Waiting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Waiting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Waiting
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-mariadb-1  Healthy
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-zookeeper-1  Healthy
[utReader-stderr] o.s.boot.docker.compose.core.DockerCli   : Container spring-docker-compose-kafka-1  Healthy
Java

ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ container ์ƒํƒœ๋ฅผ ํ™•์ธํ•ด ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด container๊ฐ€ ์ƒ์„ฑ๋จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
spring boot 3.1 docker compose support - ์ƒ˜ํ”Œ ์ฝ”๋“œ - undefined - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

mariadb container๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— MariaDbJdbcDockerComposeConnectionDetails ํด๋ž˜์Šค๋ฅผ ๋””๋ฒ„๊น…ํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ
๊ฐ™์ด host port์™€ container port ๋งตํ•‘์ด ์ž๋™์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

spring boot docker compose support - ์ƒ˜ํ”Œ ์ฝ”๋“œ ๋””๋ฒ„๊น…
spring boot docker compose support - ํฌํŠธ ๋งคํ•‘ ํ™•์ธ
spring boot docker compose support - spring boot environment ํ™•์ธ

mariadb container์˜ port ๋งตํ•‘์„ ๋ณด๋ฉด 0.0.0.0:50730 -> 3306/tcp๋กœ ํ˜ธ์ŠคํŠธํฌํŠธ 50730๊ณผ ์ปจํ…Œ์ด๋„ˆํฌํŠธ 3306์ด ๋งตํ•‘๋œ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ ๋””๋ฒ„๊น…์„ ํ†ตํ•ด์„œ portMappings๋ฅผ ์‚ดํŽด๋ณด๋ฉด 50730 port์™€ 3306 port๊ฐ€ ๋งตํ•‘๋˜์–ด ์—ฐ๊ฒฐ ์ •๋ณด๊ฐ€ ์„ค์ •๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. spring boot์—์„œ๋Š” docker compose๋ฅผ ํ†ตํ•ด์„œ ์ƒ์„ฑ๋œ container์— ๋Œ€ํ•œ inspect ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •์ด ๋˜๋Š” ๊ฒŒ ์•„๋‹Œ๊ฐ€ ์‹ถ๋‹ค.

postman์„ ํ†ตํ•ด์„œ Member ์ƒ์„ฑ API๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
spring boot 3.1 docker compose support - ์ƒ˜ํ”Œ ์ฝ”๋“œ - undefined - ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

๋กœ๊ทธ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ์„ฑ๋œ๋‹ค.

send message: MemberDto[id=null, userName=test-member, age=100]
send topic: test-topic
send partition: 2
send offset: 0
received message: MemberDto[id=null, userName=test-member, age=100]
received topic: test-topic
received partition: 2
received offset: 0
saved member Member(id=1, userName=test-member, age=100)
Java

kafka container๋ฅผ ํ†ตํ•ด์„œ test-topic์œผ๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์†ก์ˆ˜์‹ ๋˜๊ณ  DB์— member๊ฐ€ ์ €์žฅ๋œ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
postman์„ ํ†ตํ•ด์„œ Member ์ •๋ณด๋ฅผ ์š”์ฒญํ•œ๋‹ค.

spring boot docker compose support - postman ํ˜ธ์ถœ ํ…Œ์ŠคํŠธ


์ „์ฒด ์ฝ”๋“œ๋Š”ย gitlab spring-docker-compose ์—์„œ ํ™•์ธํ•ด ๋ณด๊ธฐ ๋ฐ”๋ž€๋‹ค. ์ƒ˜ํ”Œ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ spring boot docker compose๋ฅผ ํ†ตํ•ด์„œ ์ •๋ง ์†์‰ฝ๊ฒŒ ๋กœ์ปฌ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•„์ง ConnectionDetails ์ถ”์ƒํ™”๊ฐ€ ์ง€์›๋˜๋Š” container๊ฐ€ ๋งŽ์ง€๋Š” ์•Š์ง€๋งŒ ์•ž์œผ๋กœ ์ง€์›๋˜๋Š” container๊ฐ€ ๋Š˜์–ด๋‚  ๊ฒƒ์ด๋ผ ๊ธฐ๋Œ€ํ•œ๋‹ค.


์ฐธ๊ณ ๋งํฌ
https://spring.io/blog/2023/06/21/docker-compose-support-in-spring-boot-3-1/</a >
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.docker-compose</a >
https://spring.io/blog/2023/06/19/spring-boot-31-connectiondetails-abstraction/</a >