From faa5fa183b78c2245586961176d9d66031b6b9d0 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 19:34:50 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20=EC=9A=B4=EC=98=81=20DB=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=EC=97=90=EC=84=9C=20PostgreSQL=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20exporter=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=9E=AC=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.db.example | 3 ++- .github/workflows/deploy-db.yml | 6 +++--- compose.db.prod.yaml | 35 +-------------------------------- infra/monitoring/README.md | 12 +++++++++-- 4 files changed, 16 insertions(+), 40 deletions(-) diff --git a/.env.db.example b/.env.db.example index 7f9bdfb7..7ab8251c 100644 --- a/.env.db.example +++ b/.env.db.example @@ -1,4 +1,5 @@ -# PostgreSQL +# PostgreSQL (AWS Lightsail Managed DB) +DB_HOST=change-this-lightsail-db-endpoint DB_NAME=uttae DB_USER=prod DB_PASSWORD=change-this-db-password diff --git a/.github/workflows/deploy-db.yml b/.github/workflows/deploy-db.yml index 5298df82..66d05dd8 100644 --- a/.github/workflows/deploy-db.yml +++ b/.github/workflows/deploy-db.yml @@ -60,8 +60,8 @@ jobs: - name: Deploy DB services and exporters run: | cd "$DEPLOY_PATH" - docker compose --env-file .env.db -f compose.db.prod.yaml pull postgres redis mongodb postgres-exporter redis-exporter mongodb-exporter node-exporter - docker compose --env-file .env.db -f compose.db.prod.yaml up -d --wait --wait-timeout "$DB_WAIT_TIMEOUT_SECONDS" postgres redis mongodb postgres-exporter redis-exporter mongodb-exporter node-exporter + docker compose --env-file .env.db -f compose.db.prod.yaml pull postgres-exporter redis mongodb redis-exporter mongodb-exporter node-exporter + docker compose --env-file .env.db -f compose.db.prod.yaml up -d --wait --wait-timeout "$DB_WAIT_TIMEOUT_SECONDS" postgres-exporter redis mongodb redis-exporter mongodb-exporter node-exporter docker compose --env-file .env.db -f compose.db.prod.yaml ps - name: Print DB logs on deploy failure @@ -69,7 +69,7 @@ jobs: run: | cd "$DEPLOY_PATH" docker compose --env-file .env.db -f compose.db.prod.yaml ps - docker compose --env-file .env.db -f compose.db.prod.yaml logs postgres redis mongodb postgres-exporter redis-exporter mongodb-exporter node-exporter --tail=200 + docker compose --env-file .env.db -f compose.db.prod.yaml logs postgres-exporter redis mongodb redis-exporter mongodb-exporter node-exporter --tail=200 - name: Prune unused Docker images if: always() diff --git a/compose.db.prod.yaml b/compose.db.prod.yaml index 308e5b61..791c6cbf 100644 --- a/compose.db.prod.yaml +++ b/compose.db.prod.yaml @@ -2,35 +2,6 @@ name: uttae-data services: - postgres: - image: 'postgis/postgis:17-3.5' - env_file: - - .env.db - environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] - interval: 10s - timeout: 5s - retries: 10 - ports: - - '${DB_PRIVATE_IP}:5432:5432' - volumes: - - postgres-data:/var/lib/postgresql/data - restart: unless-stopped - deploy: - resources: - limits: - cpus: '1.0' - memory: 2g - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" - redis: image: 'redis:8.6.2' command: ["redis-server", "--notify-keyspace-events", "Ex"] @@ -87,14 +58,11 @@ services: postgres-exporter: image: prometheuscommunity/postgres-exporter:v0.19.1 environment: - DATA_SOURCE_URI: postgres:5432/${DB_NAME}?sslmode=disable + DATA_SOURCE_URI: ${DB_HOST}:5432/${DB_NAME}?sslmode=require DATA_SOURCE_USER: ${DB_USER} DATA_SOURCE_PASS: ${DB_PASSWORD} ports: - '${DB_PRIVATE_IP}:${POSTGRES_EXPORTER_PORT}:9187' - depends_on: - postgres: - condition: service_healthy restart: unless-stopped logging: driver: "json-file" @@ -156,6 +124,5 @@ services: max-file: "3" volumes: - postgres-data: redis-data: mongodb-data: diff --git a/infra/monitoring/README.md b/infra/monitoring/README.md index 0fbf3a79..70e430db 100644 --- a/infra/monitoring/README.md +++ b/infra/monitoring/README.md @@ -67,7 +67,7 @@ docker compose --env-file .env.prod -f compose.app.prod.yaml up -d ## DB Server -DB 서버의 운영 compose에는 PostgreSQL, Redis, MongoDB와 함께 postgres-exporter, redis-exporter, mongodb-exporter, node-exporter를 포함한다. +DB 서버의 운영 compose에는 Redis, MongoDB와 함께 redis-exporter, mongodb-exporter, node-exporter를 포함한다. 1. DB 서버의 `.env.db`에 아래 값을 추가한다. @@ -84,7 +84,7 @@ MONGODB_EXPORTER_PORT=9216 docker compose --env-file .env.db -f compose.db.prod.yaml up -d ``` -`compose.db.prod.yaml`은 PostgreSQL `5432`, Redis `6379`, MongoDB `27017`, exporter 포트, node-exporter `9100`을 모두 `${DB_PRIVATE_IP}`에만 바인딩한다. 운영 방화벽에서도 해당 포트는 public inbound로 열지 않고, API 서버와 모니터링 서버의 private 접근만 허용한다. +`compose.db.prod.yaml`은 Redis `6379`, MongoDB `27017`, exporter 포트, node-exporter `9100`을 모두 `${DB_PRIVATE_IP}`에만 바인딩한다. 운영 방화벽에서도 해당 포트는 public inbound로 열지 않고, API 서버와 모니터링 서버의 private 접근만 허용한다. GitHub Actions로 배포할 때는 DB 서버에 `self-hosted`, `hbu-db` 라벨을 가진 runner를 설치하고 `production-db` environment에 `ENV_DB` secret을 등록한다. `ENV_DB`에는 DB 서버용 `.env.db` 파일 내용을 그대로 넣는다. `.github/workflows/deploy-db.yml`은 DB 서버의 `/opt/how-about-us-data`에 compose 파일과 env를 준비한 뒤 설정을 검증하고 DB 서비스와 exporter를 함께 배포한다. @@ -157,6 +157,14 @@ DB exporter target 렌더링 결과 예시: "component": "postgres", "exporter": "postgres-exporter" } + }, + { + "targets": ["10.0.0.13:9121"], + "labels": { + "host": "db-1", + "component": "redis", + "exporter": "redis-exporter" + } } ] ``` From 69433987390a52670902c4607b13e45319aca117 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 20:03:42 +0900 Subject: [PATCH 02/14] =?UTF-8?q?chore:=20=EC=9A=B4=EC=98=81=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.db.example | 4 + .github/workflows/deploy-compose.yml | 1 + .github/workflows/deploy-db.yml | 1 + compose.db.prod.yaml | 22 ++++- infra/monitoring/README.md | 5 +- src/main/resources/application-prod.yaml | 21 ++++- .../common/config/ProdDatabaseConfigTest.java | 92 +++++++++++++++++++ 7 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/howaboutus/backend/common/config/ProdDatabaseConfigTest.java diff --git a/.env.db.example b/.env.db.example index 7ab8251c..c151ac01 100644 --- a/.env.db.example +++ b/.env.db.example @@ -9,6 +9,10 @@ MONGO_USER=prod MONGO_PASSWORD=change-this-mongo-password MONGO_DB=uttae +# Redis +REDIS_PASSWORD=change-this-redis-password +REDIS_MAXMEMORY=384mb + # Private network bindings DB_PRIVATE_IP=10.0.0.13 diff --git a/.github/workflows/deploy-compose.yml b/.github/workflows/deploy-compose.yml index ecd0d687..2ed18cdc 100644 --- a/.github/workflows/deploy-compose.yml +++ b/.github/workflows/deploy-compose.yml @@ -108,6 +108,7 @@ jobs: grep '^APP_IMAGE=' "$DEPLOY_PATH/.env.prod" grep '^API_PRIVATE_IP=' "$DEPLOY_PATH/.env.prod" grep '^LOKI_PUSH_URL=' "$DEPLOY_PATH/.env.prod" + grep '^REDIS_PASSWORD=.' "$DEPLOY_PATH/.env.prod" docker compose --env-file "$DEPLOY_PATH/.env.prod" -f "$DEPLOY_PATH/compose.app.prod.yaml" config >/dev/null - name: Detect runtime config changes diff --git a/.github/workflows/deploy-db.yml b/.github/workflows/deploy-db.yml index 66d05dd8..2cc6a5f6 100644 --- a/.github/workflows/deploy-db.yml +++ b/.github/workflows/deploy-db.yml @@ -54,6 +54,7 @@ jobs: grep '^POSTGRES_EXPORTER_PORT=' .env.db grep '^REDIS_EXPORTER_PORT=' .env.db grep '^MONGODB_EXPORTER_PORT=' .env.db + grep '^REDIS_PASSWORD=.' .env.db docker compose --env-file .env.db -f compose.db.prod.yaml config >/dev/null diff --git a/compose.db.prod.yaml b/compose.db.prod.yaml index 791c6cbf..8f17d12c 100644 --- a/compose.db.prod.yaml +++ b/compose.db.prod.yaml @@ -4,9 +4,24 @@ name: uttae-data services: redis: image: 'redis:8.6.2' - command: ["redis-server", "--notify-keyspace-events", "Ex"] + env_file: + - .env.db + command: + - redis-server + - --notify-keyspace-events + - Ex + - --save + - "" + - --appendonly + - "no" + - --maxmemory + - ${REDIS_MAXMEMORY:-384mb} + - --maxmemory-policy + - noeviction + - --requirepass + - ${REDIS_PASSWORD} healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD-SHELL", "redis-cli --no-auth-warning -a \"$${REDIS_PASSWORD}\" ping"] interval: 10s timeout: 5s retries: 10 @@ -72,8 +87,11 @@ services: redis-exporter: image: oliver006/redis_exporter:v1.83.0 + env_file: + - .env.db environment: REDIS_ADDR: redis://redis:6379 + REDIS_PASSWORD: ${REDIS_PASSWORD} ports: - '${DB_PRIVATE_IP}:${REDIS_EXPORTER_PORT}:9121' depends_on: diff --git a/infra/monitoring/README.md b/infra/monitoring/README.md index 70e430db..b6a6fcf6 100644 --- a/infra/monitoring/README.md +++ b/infra/monitoring/README.md @@ -55,6 +55,7 @@ API 서버의 운영 compose에는 Alloy와 node-exporter가 필수 구성으로 ```bash API_PRIVATE_IP=10.0.0.11 LOKI_PUSH_URL=http://10.0.0.10:3100/loki/api/v1/push +REDIS_PASSWORD=change-this-redis-password ``` 2. API 서버에서 실행한다. @@ -76,6 +77,8 @@ DB_PRIVATE_IP=10.0.0.13 POSTGRES_EXPORTER_PORT=9187 REDIS_EXPORTER_PORT=9121 MONGODB_EXPORTER_PORT=9216 +REDIS_PASSWORD=change-this-redis-password +REDIS_MAXMEMORY=384mb ``` 2. DB 서버에서 실행한다. @@ -84,7 +87,7 @@ MONGODB_EXPORTER_PORT=9216 docker compose --env-file .env.db -f compose.db.prod.yaml up -d ``` -`compose.db.prod.yaml`은 Redis `6379`, MongoDB `27017`, exporter 포트, node-exporter `9100`을 모두 `${DB_PRIVATE_IP}`에만 바인딩한다. 운영 방화벽에서도 해당 포트는 public inbound로 열지 않고, API 서버와 모니터링 서버의 private 접근만 허용한다. +`compose.db.prod.yaml`은 Redis `6379`, MongoDB `27017`, exporter 포트, node-exporter `9100`을 모두 `${DB_PRIVATE_IP}`에만 바인딩한다. Redis는 `REDIS_PASSWORD`로 인증하고, 비영속 보조 상태에 맞춰 RDB/AOF를 끄며 `REDIS_MAXMEMORY`와 `noeviction` 정책으로 메모리 상한을 명시한다. 운영 방화벽에서도 해당 포트는 public inbound로 열지 않고, API 서버와 모니터링 서버의 private 접근만 허용한다. GitHub Actions로 배포할 때는 DB 서버에 `self-hosted`, `hbu-db` 라벨을 가진 runner를 설치하고 `production-db` environment에 `ENV_DB` secret을 등록한다. `ENV_DB`에는 DB 서버용 `.env.db` 파일 내용을 그대로 넣는다. `.github/workflows/deploy-db.yml`은 DB 서버의 `/opt/how-about-us-data`에 compose 파일과 env를 준비한 뒤 설정을 검증하고 DB 서비스와 exporter를 함께 배포한다. diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml index ae548111..4541c627 100644 --- a/src/main/resources/application-prod.yaml +++ b/src/main/resources/application-prod.yaml @@ -1,17 +1,28 @@ #file: noinspection SpellCheckingInspection spring: datasource: - url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME} + url: jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}?connectTimeout=3&socketTimeout=30&tcpKeepAlive=true&sslmode=require username: ${DB_USER} password: ${DB_PASSWORD} + hikari: + pool-name: UttaePostgresPool + maximum-pool-size: ${DB_POOL_MAX_SIZE:10} + minimum-idle: ${DB_POOL_MIN_IDLE:2} + connection-timeout: 3000 + validation-timeout: 1000 + idle-timeout: 600000 + max-lifetime: 1500000 + keepalive-time: 300000 data: redis: host: ${REDIS_HOST:${DB_HOST}} port: 6379 + database: ${REDIS_DATABASE:0} + password: ${REDIS_PASSWORD:} mongodb: - uri: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_HOST:${DB_HOST}}:27017/${MONGO_DB}?authSource=admin + uri: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@${MONGO_HOST:${DB_HOST}}:27017/${MONGO_DB}?authSource=admin&connectTimeoutMS=3000&serverSelectionTimeoutMS=3000&socketTimeoutMS=30000&maxPoolSize=${MONGO_MAX_POOL_SIZE:20}&minPoolSize=${MONGO_MIN_POOL_SIZE:2} docker: compose: @@ -52,3 +63,9 @@ cookie: management: server: port: 8081 + +app: + rate-limit: + redis: + database: ${spring.data.redis.database} + password: ${spring.data.redis.password:} diff --git a/src/test/java/com/howaboutus/backend/common/config/ProdDatabaseConfigTest.java b/src/test/java/com/howaboutus/backend/common/config/ProdDatabaseConfigTest.java new file mode 100644 index 00000000..a9d063da --- /dev/null +++ b/src/test/java/com/howaboutus/backend/common/config/ProdDatabaseConfigTest.java @@ -0,0 +1,92 @@ +package com.howaboutus.backend.common.config; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.core.io.ClassPathResource; + +class ProdDatabaseConfigTest { + + @Test + @DisplayName("prod 환경에서는 PostgreSQL 커넥션 풀과 JDBC 타임아웃을 명시한다") + void configuresPostgresPoolAndTimeoutsInProd() { + Map properties = loadProdProperties(); + + assertThat(properties) + .containsEntry( + "spring.datasource.url", + "jdbc:postgresql://${DB_HOST}:5432/${DB_NAME}" + + "?connectTimeout=3&socketTimeout=30&tcpKeepAlive=true&sslmode=require" + ) + .containsEntry("spring.datasource.hikari.pool-name", "UttaePostgresPool") + .containsEntry("spring.datasource.hikari.maximum-pool-size", "${DB_POOL_MAX_SIZE:10}") + .containsEntry("spring.datasource.hikari.minimum-idle", "${DB_POOL_MIN_IDLE:2}") + .containsEntry("spring.datasource.hikari.connection-timeout", "3000") + .containsEntry("spring.datasource.hikari.validation-timeout", "1000") + .containsEntry("spring.datasource.hikari.idle-timeout", "600000") + .containsEntry("spring.datasource.hikari.max-lifetime", "1500000") + .containsEntry("spring.datasource.hikari.keepalive-time", "300000"); + } + + @Test + @DisplayName("prod 환경에서는 MongoDB 드라이버 타임아웃과 풀 크기를 URI에 명시한다") + void configuresMongoTimeoutsInProd() { + Map properties = loadProdProperties(); + + assertThat(properties.get("spring.mongodb.uri")) + .contains("authSource=admin") + .contains("connectTimeoutMS=3000") + .contains("serverSelectionTimeoutMS=3000") + .contains("socketTimeoutMS=30000") + .contains("maxPoolSize=${MONGO_MAX_POOL_SIZE:20}") + .contains("minPoolSize=${MONGO_MIN_POOL_SIZE:2}"); + } + + @Test + @DisplayName("prod 환경에서는 Redis DB 번호를 앱과 rate limit 클라이언트가 공유한다") + void configuresRedisDatabaseInProd() { + Map properties = loadProdProperties(); + + assertThat(properties) + .containsEntry("spring.data.redis.database", "${REDIS_DATABASE:0}") + .containsEntry("app.rate-limit.redis.database", "${spring.data.redis.database}"); + } + + @Test + @DisplayName("prod Redis는 비영속 보조 상태에 맞춰 메모리 제한과 noeviction 정책을 명시한다") + void configuresRedisRuntimePolicyInProdCompose() throws IOException { + String compose = Files.readString(Path.of("compose.db.prod.yaml")); + + assertThat(compose) + .contains("--notify-keyspace-events") + .contains("Ex") + .contains("--save") + .contains("--appendonly") + .contains("\"no\"") + .contains("--maxmemory") + .contains("${REDIS_MAXMEMORY:-384mb}") + .contains("--maxmemory-policy") + .contains("noeviction"); + } + + private Map loadProdProperties() { + YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); + yaml.setResources(new ClassPathResource("application-prod.yaml")); + yaml.afterPropertiesSet(); + + Map properties = new HashMap<>(); + for (Map.Entry entry : Objects.requireNonNull(yaml.getObject()).entrySet()) { + properties.put(entry.getKey().toString(), entry.getValue().toString()); + } + return properties; + } +} From 48fb3d5937983cd65c478633697e2669fdb07df0 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 20:16:47 +0900 Subject: [PATCH 03/14] =?UTF-8?q?chore:=20Caddy=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=B3=B8=EB=AC=B8=20=ED=81=AC=EA=B8=B0=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- infra/caddy/Caddyfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/infra/caddy/Caddyfile b/infra/caddy/Caddyfile index 854aa3d2..a5f882d8 100644 --- a/infra/caddy/Caddyfile +++ b/infra/caddy/Caddyfile @@ -68,5 +68,9 @@ respond @429 `{"code":"RATE_LIMIT_EXCEEDED","message":"요청이 너무 많습니다. 잠시 후 다시 시도해 주세요"}` 429 } + request_body { + max_size 1MB + } + reverse_proxy app:8080 } From 438fb43f82392bbb1719b06b29567ba4d0c41119 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 20:27:29 +0900 Subject: [PATCH 04/14] =?UTF-8?q?docs:=20PR=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EC=A0=84=EB=9E=B5=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/review-code-against-docs/SKILL.md | 28 +++++++++++++++++-- CONTRIBUTING.md | 5 +++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.claude/skills/review-code-against-docs/SKILL.md b/.claude/skills/review-code-against-docs/SKILL.md index df69498c..46a93282 100644 --- a/.claude/skills/review-code-against-docs/SKILL.md +++ b/.claude/skills/review-code-against-docs/SKILL.md @@ -11,12 +11,36 @@ description: Use when the user asks to create, open, prepare, or publish a PR/pu ## Checklist -### Step 0. diff 수집 +### Step 0. 기준 브랜치 결정 및 diff 수집 + +먼저 `CONTRIBUTING.md`의 Branch Strategy를 확인하고 PR 기준 브랜치를 결정한다. + +기준 브랜치 우선순위: + +1. 사용자가 PR 대상 브랜치를 명시했으면 그 값을 사용한다. +2. 이미 열린 PR이 있으면 `gh pr view --json baseRefName`의 `baseRefName`을 사용한다. +3. 현재 브랜치가 `feature/*`이면 `dev`를 사용한다. +4. 현재 브랜치가 `hotfix/*`이면 `main`을 사용한다. +5. 현재 브랜치가 `dev`이면 `main`을 사용한다. +6. 현재 브랜치가 `main`이면 `dev`를 사용한다. 이 경우는 hotfix 반영 후 `main` → `dev` 백머지 PR이다. +7. 판단할 수 없으면 사용자에게 PR 대상 브랜치를 확인한다. + +브랜치 전략: + +| 현재 브랜치 | PR 대상 | +|------|------| +| `feature/*` | `dev` | +| `hotfix/*` | `main` | +| `dev` | `main` | +| `main` | `dev` 백머지 | ```bash -git diff main --name-only +git fetch origin --quiet +git diff --name-only origin/...HEAD ``` +보고서에는 기준 브랜치, 현재 브랜치, diff 범위를 먼저 적는다. + 변경된 `.java` 파일 목록을 추출한다. 변경 파일이 없으면 "변경 없음"으로 종료한다. 변경된 파일을 두 그룹으로 분류한다: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c591d4c..bf57f969 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,8 @@ feature/ → dev → main - `main`에 직접 push하지 않는다. - `feature/*` 브랜치는 `dev`에서 분기하고 `dev`로 머지한다. - 브랜치는 작업 단위로 짧게 유지한다. +- `feature/*` 브랜치의 PR 대상은 `dev`다. +- `dev` 브랜치의 릴리스 PR 대상은 `main`이다. ### Hotfix 흐름 @@ -28,8 +30,9 @@ hotfix/ → main → dev (백머지) ``` - `hotfix/*` 브랜치는 `main`에서 직접 분기한다. +- `hotfix/*` 브랜치의 PR 대상은 `main`이다. - 수정 완료 후 `main`에 머지한다. -- 머지 후 동일 변경사항을 반드시 `dev`에도 백머지하여 이후 릴리스에서 누락되지 않도록 한다. +- 머지 후 `main`에서 `dev`로 백머지 PR을 올려 동일 변경사항이 이후 릴리스에서 누락되지 않도록 한다. ## Commit Convention From 5d85459c0c8dd9cdfac16ed578a58c27785f157d Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 21:46:49 +0900 Subject: [PATCH 05/14] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=20PostgreSQL?= =?UTF-8?q?=20Docker=20=ED=97=AC=EC=8A=A4=EC=B2=B4=ED=81=AC=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pg_isready 명령어에 -h localhost -p 5432를 추가하여 TCP 5432 포트 바인딩 완료 여부까지 검증하도록 개선 - 설계 사양서(spec) 및 구현 계획서(plan) 추가 --- compose.db.dev.yaml | 2 +- ...026-06-08-postgresql-docker-healthcheck.md | 89 +++++++++++++++++++ ...08-postgresql-docker-healthcheck-design.md | 52 +++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/plans/2026-06-08-postgresql-docker-healthcheck.md create mode 100644 docs/superpowers/specs/2026-06-08-postgresql-docker-healthcheck-design.md diff --git a/compose.db.dev.yaml b/compose.db.dev.yaml index 6ec01249..400db9c7 100644 --- a/compose.db.dev.yaml +++ b/compose.db.dev.yaml @@ -12,7 +12,7 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U $${DB_USER} -d $${DB_NAME}"] interval: 10s timeout: 5s retries: 10 diff --git a/docs/superpowers/plans/2026-06-08-postgresql-docker-healthcheck.md b/docs/superpowers/plans/2026-06-08-postgresql-docker-healthcheck.md new file mode 100644 index 00000000..e02dbfa6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-postgresql-docker-healthcheck.md @@ -0,0 +1,89 @@ +# PostgreSQL Docker Healthcheck Timing Issue Resolution Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Resolve the PostgreSQL Docker healthcheck timing issue by verifying TCP availability instead of Unix socket availability, preventing early health status. + +**Architecture:** Update the `postgres` healthcheck script in `compose.db.dev.yaml` to include `-h localhost -p 5432`. This ensures the health check only passes when the container accepts TCP connections on port 5432, preventing dependent containers from starting too early. + +**Tech Stack:** Docker Compose, PostgreSQL 17 + +--- + +### Task 1: Update PostgreSQL Healthcheck configuration + +**Files:** +- Modify: `compose.db.dev.yaml` + +- [ ] **Step 1: Modify PostgreSQL healthcheck test command** + +Modify `compose.db.dev.yaml` to update the `test` property under `postgres.healthcheck`. + +```yaml + healthcheck: + test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U $${DB_USER} -d $${DB_NAME}"] + interval: 10s + timeout: 5s + retries: 10 +``` + +### Task 2: Verification + +**Files:** +- Config: `compose.db.dev.yaml` +- Config: `compose.app.dev.yaml` + +- [ ] **Step 1: Validate compose file syntax** + +Run docker-compose config check to verify there are no syntax errors. + +Run: +```bash +docker compose -f compose.db.dev.yaml config > /dev/null +``` +Expected: command exits with `0` status and no validation errors. + +- [ ] **Step 2: Restart the database container and check health status** + +Stop any running containers and start `postgres` service to verify its health status. + +Run: +```bash +docker compose -f compose.db.dev.yaml down +docker compose -f compose.db.dev.yaml up -d postgres +``` +Wait for 15-20 seconds for the healthcheck intervals to trigger. +Run: +```bash +docker compose -f compose.db.dev.yaml ps +``` +Expected: The status of the `postgres` container transitions to `healthy`. + +- [ ] **Step 3: Start the application and check connection** + +Start the application container which depends on the postgres container being healthy, and check if it starts successfully. + +Run: +```bash +docker compose -f compose.db.dev.yaml -f compose.app.dev.yaml up -d --build +``` +Wait for 15-20 seconds. +Verify if the `api-server` container is running and healthy: +```bash +docker compose -f compose.db.dev.yaml -f compose.app.dev.yaml ps +``` +Expected: `api-server` container status is `healthy` (or running) without connection failure errors in the logs. +Check logs for database connection: +```bash +docker compose -f compose.db.dev.yaml -f compose.app.dev.yaml logs api-server | grep -E "Connection|Database|Flyway" +``` +Expected: Flyway migration runs and database connection is established successfully. + +- [ ] **Step 4: Shut down containers** + +Clean up the containers. +Run: +```bash +docker compose -f compose.db.dev.yaml -f compose.app.dev.yaml down +``` +Expected: All containers stopped and removed. diff --git a/docs/superpowers/specs/2026-06-08-postgresql-docker-healthcheck-design.md b/docs/superpowers/specs/2026-06-08-postgresql-docker-healthcheck-design.md new file mode 100644 index 00000000..5952fd6c --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-postgresql-docker-healthcheck-design.md @@ -0,0 +1,52 @@ +# Design Spec: PostgreSQL Docker Healthcheck Timing Issue Resolution + +- **Date**: 2026-06-08 +- **Topic**: PostgreSQL Docker Healthcheck Timing Issue +- **Status**: Draft (Under Review) + +## 1. Background & Problem Statement + +In the local development environment, when bootstrapping the application using `docker compose`, the Spring Boot application (`api-server`) occasionally fails to connect to the PostgreSQL database during its startup phase. This results in connection failure errors, such as `java.io.EOFException` during the SSL handshake. + +### Root Cause +- **Early Health State**: The PostgreSQL container health check was configured as `pg_isready -U ${DB_USER} -d ${DB_NAME}`. +- **Unix Socket vs TCP**: Without specifying the host (`-h`) and port (`-p`), `pg_isready` defaults to checking connection availability via the local Unix domain socket. +- **Timing Mismatch**: The PostgreSQL database engine reports itself as healthy as soon as it accepts Unix socket connections. However, the external TCP port (`5432` mapped to host `5433`) is not yet fully initialized and ready to accept connections. +- **Premature Startup**: Since `api-server` depends on `postgres` being healthy (`condition: service_healthy`), it attempts to connect to `postgres:5432` via TCP immediately upon the container transitioning to the healthy state, resulting in connection failure. + +## 2. Proposed Changes + +We will modify the health check command in the local Docker Compose configuration to explicitly verify TCP connectivity on port 5432. + +### Target File +- [compose.db.dev.yaml](file:///home/minbros/projects/java/how-about-us-backend/compose.db.dev.yaml) + +### Modifications +Modify the `healthcheck.test` field under the `postgres` service: + +```diff + healthcheck: +- test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] ++ test: ["CMD-SHELL", "pg_isready -h localhost -p 5432 -U $${DB_USER} -d $${DB_NAME}"] + interval: 10s + timeout: 5s + retries: 10 +``` + +> [!NOTE] +> We use `$${DB_USER}` and `$${DB_NAME}` (double dollar sign) to escape the variables so they are evaluated inside the container shell, ensuring that the container's environment variables are correctly resolved. + +## 3. Impact & Risk Analysis + +- **Backward Compatibility**: No impact. This only changes the local development container configuration. +- **Resource Usage**: No impact. `pg_isready` is a lightweight utility. +- **Dependency**: The change ensures that any container depending on the `postgres` service (e.g., `api-server` in `compose.app.dev.yaml`) will only start when PostgreSQL is truly ready to receive TCP connections. + +## 5. Verification Plan + +1. **Verify Syntax**: Check YAML syntax using Docker Compose parser. +2. **Local Test Execution**: + - Run the DB dev compose: `docker compose -f compose.db.dev.yaml up -d` + - Monitor the health status: `docker compose -f compose.db.dev.yaml ps` and ensure the postgres container transitions to `healthy`. + - Run the app dev compose: `docker compose -f compose.db.dev.yaml -f compose.app.dev.yaml up -d --build` + - Check if `api-server` starts successfully without any database connection issues. From dc45317a0e036e680eb4ed4589aea87dbd7bdccb Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:05:00 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20MongoDB=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + docs/ai/erd.md | 4 +- scripts/mongo/backfill-message-sequence.js | 112 ------------------ .../common/config/MongoMigrationConfig.java | 33 ++++++ .../mongo/CreateMessageIndexesChangeUnit.java | 73 ++++++++++++ .../messages/document/ChatMessage.java | 1 + .../MongoMessageIndexMigrationTest.java | 52 ++++++++ 7 files changed, 163 insertions(+), 114 deletions(-) delete mode 100644 scripts/mongo/backfill-message-sequence.js create mode 100644 src/main/java/com/howaboutus/backend/common/config/MongoMigrationConfig.java create mode 100644 src/main/java/com/howaboutus/backend/common/migration/mongo/CreateMessageIndexesChangeUnit.java create mode 100644 src/test/java/com/howaboutus/backend/common/migration/MongoMessageIndexMigrationTest.java diff --git a/build.gradle b/build.gradle index 33504728..ac222a3c 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,8 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-flyway' runtimeOnly 'org.flywaydb:flyway-database-postgresql' + implementation 'io.mongock:mongock-standalone:5.5.1' + implementation 'io.mongock:mongodb-sync-v4-driver:5.5.1' implementation 'org.springframework.boot:spring-boot-starter-security' diff --git a/docs/ai/erd.md b/docs/ai/erd.md index 69a1eafe..97940b1d 100644 --- a/docs/ai/erd.md +++ b/docs/ai/erd.md @@ -101,9 +101,9 @@ CHECK `ck_room_members_left_role` (status='ACTIVE' OR role <> 'PENDING') | metadata | DOCUMENT | NOT NULL, DEFAULT `{}` | 장소 공유, AI 응답 등 메시지 타입별 확장 데이터 | | createdAt | DATE | NOT NULL | 생성일시 | -**인덱스:** `{ roomId: 1, sequence: 1 }` unique — 방별 sequence 중복 방지 및 `afterSequence` 조회용, `{ roomId: 1, sequence: -1, _id: -1 }` — 최근 메시지 조회용, `{ roomId: 1, createdAt: -1, _id: -1 }` — 기존 최근 조회/운영 호환용, `{ roomId: 1, _id: 1 }` — `afterId` 호환 조회용 +**인덱스:** `uidx_messages_room_sequence` `{ roomId: 1, sequence: 1 }` unique — 방별 sequence 중복 방지 및 `afterSequence` 조회용, `idx_messages_room_sequence_recent` `{ roomId: 1, sequence: -1, _id: -1 }` — 최근 메시지 조회용, `idx_messages_room_recent` `{ roomId: 1, createdAt: -1, _id: -1 }` — 기존 최근 조회/운영 호환용, `idx_messages_room_id` `{ roomId: 1, _id: 1 }` — `afterId` 호환 조회용 -> 운영 MongoDB 기존 메시지는 서버 배포 전 `scripts/mongo/backfill-message-sequence.js`로 room별 `_id` 오름차순 기준 `sequence: 1, 2, 3...`을 채운 뒤 missing/null 및 중복 검증을 통과해야 한다. `(roomId, sequence)` unique index는 backfill 검증 완료 후 스크립트에서 생성한다. +> 초기 빈 MongoDB 배포에서는 Mongock 마이그레이션이 `messages` 컬렉션과 위 인덱스를 생성한다. 운영 MongoDB 기존 메시지가 있는 환경은 서버 배포 전 room별 `_id` 오름차순 기준 `sequence: 1, 2, 3...` backfill과 missing/null 및 중복 검증을 별도 절차로 완료해야 한다. `(roomId, sequence)` unique index는 backfill 검증 완료 후 적용한다. > > 채팅 초기 진입/재접속에서 저장된 읽음 위치로 복귀할 때는 `room_members.last_read_message_id`를 `afterId`로 보낸다. 실시간 수신 중 브로드캐스트 누락이 감지되면 마지막 수신 message `sequence`를 `afterSequence`로 보내 복구한다. diff --git a/scripts/mongo/backfill-message-sequence.js b/scripts/mongo/backfill-message-sequence.js deleted file mode 100644 index a823a222..00000000 --- a/scripts/mongo/backfill-message-sequence.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Backfill room-local message sequence values. - * - * Usage: - * mongosh "$MONGO_URI" scripts/mongo/backfill-message-sequence.js - * - * Operational preconditions: - * - Back up production MongoDB first. - * - Stop message writes or enter maintenance mode while this script runs. - * - Deploy the server only after this script completes successfully. - */ - -const collection = db.messages; -const indexName = "uq_messages_room_sequence"; -const batchSize = 1000; - -function assertNoMissingSequence() { - const missingCount = collection.countDocuments({ - $or: [{ sequence: { $exists: false } }, { sequence: null }], - }); - if (missingCount !== 0) { - throw new Error(`messages still missing sequence: ${missingCount}`); - } -} - -function assertNoDuplicateSequence() { - const duplicates = collection - .aggregate([ - { - $group: { - _id: { roomId: "$roomId", sequence: "$sequence" }, - count: { $sum: 1 }, - }, - }, - { - $match: { - "_id.sequence": { $ne: null }, - count: { $gt: 1 }, - }, - }, - { $limit: 10 }, - ]) - .toArray(); - - if (duplicates.length > 0) { - throw new Error(`duplicate roomId+sequence values found: ${JSON.stringify(duplicates)}`); - } -} - -function flush(operations) { - if (operations.length === 0) { - return; - } - collection.bulkWrite(operations, { ordered: true }); - operations.length = 0; -} - -const roomIds = collection.distinct("roomId"); -print(`Backfilling message sequence for ${roomIds.length} rooms`); - -for (const roomId of roomIds) { - let expectedSequence = 1; - let existingCount = 0; - let backfilledCount = 0; - const operations = []; - const cursor = collection.find({ roomId }).sort({ _id: 1 }); - - while (cursor.hasNext()) { - const message = cursor.next(); - if (message.sequence !== undefined && message.sequence !== null) { - const existing = Number(message.sequence); - if (existing !== expectedSequence) { - throw new Error( - `existing sequence mismatch for roomId=${roomId}, _id=${message._id}, ` + - `existing=${existing}, expected=${expectedSequence}` - ); - } - existingCount += 1; - } else { - operations.push({ - updateOne: { - filter: { _id: message._id }, - update: { $set: { sequence: NumberLong(String(expectedSequence)) } }, - }, - }); - backfilledCount += 1; - } - - expectedSequence += 1; - if (operations.length >= batchSize) { - flush(operations); - } - } - - flush(operations); - print( - `roomId=${roomId}: total=${expectedSequence - 1}, ` + - `backfilled=${backfilledCount}, alreadyAssigned=${existingCount}` - ); -} - -assertNoMissingSequence(); -assertNoDuplicateSequence(); - -collection.createIndex( - { roomId: 1, sequence: 1 }, - { unique: true, name: indexName } -); - -assertNoMissingSequence(); -assertNoDuplicateSequence(); -print(`Backfill complete. Unique index ensured: ${indexName}`); diff --git a/src/main/java/com/howaboutus/backend/common/config/MongoMigrationConfig.java b/src/main/java/com/howaboutus/backend/common/config/MongoMigrationConfig.java new file mode 100644 index 00000000..e54a94f0 --- /dev/null +++ b/src/main/java/com/howaboutus/backend/common/config/MongoMigrationConfig.java @@ -0,0 +1,33 @@ +package com.howaboutus.backend.common.config; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoTemplate; + +import com.mongodb.client.MongoClient; + +import io.mongock.driver.mongodb.sync.v4.driver.MongoSync4Driver; +import io.mongock.runner.standalone.MongockStandalone; + +@Configuration +public class MongoMigrationConfig { + + private static final String MIGRATION_PACKAGE = "com.howaboutus.backend.common.migration.mongo"; + + @Bean + ApplicationRunner mongoMigrationRunner( + MongoClient mongoClient, + MongoDatabaseFactory mongoDatabaseFactory, + MongoTemplate mongoTemplate + ) { + return args -> MongockStandalone.builder() + .setDriver(MongoSync4Driver.withDefaultLock(mongoClient, mongoDatabaseFactory.getMongoDatabase().getName())) + .addMigrationScanPackage(MIGRATION_PACKAGE) + .addDependency(MongoTemplate.class, mongoTemplate) + .setTransactional(false) + .buildRunner() + .execute(); + } +} diff --git a/src/main/java/com/howaboutus/backend/common/migration/mongo/CreateMessageIndexesChangeUnit.java b/src/main/java/com/howaboutus/backend/common/migration/mongo/CreateMessageIndexesChangeUnit.java new file mode 100644 index 00000000..743d3bb5 --- /dev/null +++ b/src/main/java/com/howaboutus/backend/common/migration/mongo/CreateMessageIndexesChangeUnit.java @@ -0,0 +1,73 @@ +package com.howaboutus.backend.common.migration.mongo; + +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.index.IndexOperations; + +import com.howaboutus.backend.messages.document.ChatMessage; + +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@ChangeUnit(id = "create-message-indexes", order = "001", author = "minbros") +public class CreateMessageIndexesChangeUnit { + + private static final String MESSAGES_COLLECTION = "messages"; + private static final String ROOM_SEQUENCE_UNIQUE_INDEX = "uidx_messages_room_sequence"; + private static final String ROOM_SEQUENCE_RECENT_INDEX = "idx_messages_room_sequence_recent"; + private static final String ROOM_RECENT_INDEX = "idx_messages_room_recent"; + private static final String ROOM_ID_INDEX = "idx_messages_room_id"; + + private final MongoTemplate mongoTemplate; + + @Execution + public void createIndexes() { + mongoTemplate.indexOps(ChatMessage.class) + .createIndex(new Index() + .on("roomId", Direction.ASC) + .on("sequence", Direction.ASC) + .unique() + .named(ROOM_SEQUENCE_UNIQUE_INDEX)); + mongoTemplate.indexOps(ChatMessage.class) + .createIndex(new Index() + .on("roomId", Direction.ASC) + .on("sequence", Direction.DESC) + .on("_id", Direction.DESC) + .named(ROOM_SEQUENCE_RECENT_INDEX)); + mongoTemplate.indexOps(ChatMessage.class) + .createIndex(new Index() + .on("roomId", Direction.ASC) + .on("createdAt", Direction.DESC) + .on("_id", Direction.DESC) + .named(ROOM_RECENT_INDEX)); + mongoTemplate.indexOps(ChatMessage.class) + .createIndex(new Index() + .on("roomId", Direction.ASC) + .on("_id", Direction.ASC) + .named(ROOM_ID_INDEX)); + } + + @RollbackExecution + public void rollback() { + if (!mongoTemplate.collectionExists(MESSAGES_COLLECTION)) { + return; + } + IndexOperations indexOperations = mongoTemplate.indexOps(ChatMessage.class); + dropIndexIfExists(indexOperations, ROOM_SEQUENCE_UNIQUE_INDEX); + dropIndexIfExists(indexOperations, ROOM_SEQUENCE_RECENT_INDEX); + dropIndexIfExists(indexOperations, ROOM_RECENT_INDEX); + dropIndexIfExists(indexOperations, ROOM_ID_INDEX); + } + + private void dropIndexIfExists(IndexOperations indexOperations, String indexName) { + boolean exists = indexOperations.getIndexInfo().stream() + .anyMatch(indexInfo -> indexName.equals(indexInfo.getName())); + if (exists) { + indexOperations.dropIndex(indexName); + } + } +} diff --git a/src/main/java/com/howaboutus/backend/messages/document/ChatMessage.java b/src/main/java/com/howaboutus/backend/messages/document/ChatMessage.java index c1d4aacc..bd749ae2 100644 --- a/src/main/java/com/howaboutus/backend/messages/document/ChatMessage.java +++ b/src/main/java/com/howaboutus/backend/messages/document/ChatMessage.java @@ -16,6 +16,7 @@ @Getter @Document(collection = "messages") @CompoundIndexes({ + @CompoundIndex(name = "uidx_messages_room_sequence", def = "{'roomId': 1, 'sequence': 1}", unique = true), @CompoundIndex(name = "idx_messages_room_recent", def = "{'roomId': 1, 'createdAt': -1, '_id': -1}"), @CompoundIndex(name = "idx_messages_room_sequence_recent", def = "{'roomId': 1, 'sequence': -1, '_id': -1}"), @CompoundIndex(name = "idx_messages_room_id", def = "{'roomId': 1, '_id': 1}") diff --git a/src/test/java/com/howaboutus/backend/common/migration/MongoMessageIndexMigrationTest.java b/src/test/java/com/howaboutus/backend/common/migration/MongoMessageIndexMigrationTest.java new file mode 100644 index 00000000..3062b1b4 --- /dev/null +++ b/src/test/java/com/howaboutus/backend/common/migration/MongoMessageIndexMigrationTest.java @@ -0,0 +1,52 @@ +package com.howaboutus.backend.common.migration; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; + +import com.howaboutus.backend.support.BaseIntegrationTest; + +class MongoMessageIndexMigrationTest extends BaseIntegrationTest { + + @Autowired + private MongoTemplate mongoTemplate; + + @Test + @DisplayName("Mongock는 빈 MongoDB에 messages 컬렉션과 메시지 조회/순번 인덱스를 생성한다") + void mongockCreatesMessageIndexesOnEmptyMongo() { + assertThat(mongoTemplate.collectionExists("messages")).isTrue(); + + Map indexesByName = mongoTemplate.getCollection("messages") + .listIndexes() + .into(new ArrayList<>()) + .stream() + .collect(Collectors.toMap(index -> index.getString("name"), Function.identity())); + + assertIndex(indexesByName, "uidx_messages_room_sequence", + new Document("roomId", 1).append("sequence", 1), true); + assertIndex(indexesByName, "idx_messages_room_sequence_recent", + new Document("roomId", 1).append("sequence", -1).append("_id", -1), false); + assertIndex(indexesByName, "idx_messages_room_recent", + new Document("roomId", 1).append("createdAt", -1).append("_id", -1), false); + assertIndex(indexesByName, "idx_messages_room_id", + new Document("roomId", 1).append("_id", 1), false); + } + + private void assertIndex(Map indexesByName, String name, Document key, boolean unique) { + assertThat(indexesByName) + .containsKey(name); + assertThat(indexesByName.get(name).get("key", Document.class)) + .isEqualTo(key); + assertThat(indexesByName.get(name).getBoolean("unique", false)) + .isEqualTo(unique); + } +} From bacd2da7c20b360afeeca821825aaff1eb0fa66f Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:18:01 +0900 Subject: [PATCH 07/14] =?UTF-8?q?docs:=20MongoDB=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8ec82e81..bd6b4606 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,8 @@ src/main/resources/ - PostgreSQL 이미지는 `postgis/postgis:17-3.5`를 사용한다. - MongoDB 이미지는 `mongo:8`을 사용하며 채팅 메시지 저장소로 사용한다. +- PostgreSQL 스키마 변경은 Flyway SQL 마이그레이션으로 관리한다. +- MongoDB 컬렉션/인덱스 변경은 Mongock ChangeUnit으로 관리한다. 변경 클래스는 `common/migration/mongo/` 아래에 두고, Spring Data 자동 인덱스 생성에 의존하지 않는다. - 공간 데이터 엔티티에는 `hibernate-spatial` 타입을 사용한다. - dev 환경의 PostgreSQL 포트는 `5433`이다. - `spring.jpa.open-in-view=false`가 설정되어 있다. From 0517be4db2e0ddfe1d1a364fdaa95c68278ac67c Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:20:24 +0900 Subject: [PATCH 08/14] =?UTF-8?q?docs:=20PostGIS=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../specs/2026-06-08-remove-postgis-design.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-08-remove-postgis-design.md diff --git a/docs/superpowers/specs/2026-06-08-remove-postgis-design.md b/docs/superpowers/specs/2026-06-08-remove-postgis-design.md new file mode 100644 index 00000000..a08dc926 --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-remove-postgis-design.md @@ -0,0 +1,92 @@ +# Design Doc: Remove PostGIS and Spatial Dependencies + +- **Date:** 2026-06-08 +- **Status:** Approved +- **Author:** Antigravity + +--- + +## 1. Background & Context + +Currently, the project lists `PostgreSQL 17 + PostGIS 3.5` as part of its tech stack. However, the database schema (managed by Flyway migrations) and JPA Entities do not store spatial coordinate types (like `Point` or `Geometry`) nor do they query database-level spatial coordinates. Coordinates are stored either as floats in MongoDB messages or processed dynamically via Google Places API integrations. + +Therefore, PostGIS and spatial library dependencies represent unnecessary overhead and should be removed. + +--- + +## 2. Goals & Objectives + +- Remove spatial library dependencies (`hibernate-spatial`) from the project build configuration. +- Change the database Docker image from `postgis/postgis:17-3.5` to official `postgres:17` in the local development environment. +- Update Testcontainers database image in integration tests. +- Clean up all mentions of PostGIS and spatial coordinates in system documentation. + +--- + +## 3. Detailed Changes + +### A. Build Configurations +#### `build.gradle` +- Remove the dependency: + ```groovy + implementation 'org.hibernate.orm:hibernate-spatial' + ``` + +### B. Infrastructure Configs +#### `compose.db.dev.yaml` +- Update the PostgreSQL container image: + ```yaml + services: + postgres: + image: 'postgres:17' + ``` + +### C. Test Support Configs +#### `src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java` +- Update the Docker image for Testcontainers PostgreSQL: + ```java + DockerImageName.parse("postgres:17") + ``` + +### D. Documentation Updates +#### `AGENTS.md` +- Update the Database entry in **Tech Stack**: + ```markdown + - **Database**: PostgreSQL 17, MongoDB 8 + ``` +- Remove references to `hibernate-spatial` in **Tech Stack** -> **기타**: + - Remove `, hibernate-spatial` from the list. +- Update **Gotchas**: + - Change `postgis/postgis:17-3.5` to `postgres:17`. + - Remove `- 공간 데이터 엔티티에는 hibernate-spatial 타입을 사용한다.` rule. + +#### `README.md` +- Update Database entry in **Database** Section: + ```markdown + - **Database**: PostgreSQL 17, MongoDB 8 + ``` +- Update package map description for `places`: + ```markdown + ├── places/ ← 장소 (Google Places API 연동) + ``` + +#### `docs/ai/erd.md` +- Update the Tech Stack section: + ```markdown + - **기술 스택:** PostgreSQL, MongoDB (채팅 메시지), Redis (세션/토큰) + ``` + +--- + +## 4. Verification & Testing Plan + +1. **Checkstyle verification**: + ```bash + ./gradlew checkstyleMain checkstyleTest + ``` +2. **Build and Test execution**: + ```bash + ./gradlew clean build + ``` + - Ensure the compile phase passes without spatial library imports. + - Verify integration tests start successfully using `postgres:17` via Testcontainers and all test cases pass. From 41ade6ebcc535801015725bbef6d501d56522738 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:20:42 +0900 Subject: [PATCH 09/14] =?UTF-8?q?docs:=20PostGIS=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EA=B3=84=ED=9A=8D=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-06-08-remove-postgis.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-08-remove-postgis.md diff --git a/docs/superpowers/plans/2026-06-08-remove-postgis.md b/docs/superpowers/plans/2026-06-08-remove-postgis.md new file mode 100644 index 00000000..6a2f3abd --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-remove-postgis.md @@ -0,0 +1,151 @@ +# Remove PostGIS Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Clean up the project from unused PostGIS docker image, hibernate-spatial dependency, and all documentation references to them. + +**Architecture:** Remove `hibernate-spatial` from dependencies, switch PostgreSQL Docker images (in docker-compose dev config and Testcontainers integration test base class) to official `postgres:17`, and update docs to match the cleaned-up tech stack. + +**Tech Stack:** Gradle, Spring Boot, Docker Compose, PostgreSQL 17, Testcontainers + +--- + +### Task 1: Remove Build Dependency + +**Files:** +- Modify: `build.gradle` + +- [ ] **Step 1: Remove `hibernate-spatial` dependency** + + In [build.gradle](file:///home/minbros/projects/java/how-about-us-backend/build.gradle), find and remove the following line (around line 37): + ```groovy + implementation 'org.hibernate.orm:hibernate-spatial' + ``` + +- [ ] **Step 2: Run build dry-run and convention check** + + Run Gradle to ensure the build compiles successfully without the dependency. + Run: `./gradlew compileJava checkstyleMain` + Expected: BUILD SUCCESSFUL + +- [ ] **Step 3: Commit** + + Run: + ```bash + git add build.gradle + git commit -m "chore: build.gradle에서 hibernate-spatial 의존성 제거" + ``` + +--- + +### Task 2: Replace Docker Database Images + +**Files:** +- Modify: `compose.db.dev.yaml` +- Modify: `src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java` + +- [ ] **Step 1: Modify `compose.db.dev.yaml` PostgreSQL image** + + In [compose.db.dev.yaml](file:///home/minbros/projects/java/how-about-us-backend/compose.db.dev.yaml), modify the container image from `postgis/postgis:17-3.5` to `postgres:17` under the `postgres` service (around line 6): + ```yaml + postgres: + image: 'postgres:17' + ``` + +- [ ] **Step 2: Modify `BaseIntegrationTest.java` Testcontainers image** + + In [BaseIntegrationTest.java](file:///home/minbros/projects/java/how-about-us-backend/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java), modify the PostgreSQL Testcontainer Docker image (around line 23): + ```java + DockerImageName.parse("postgres:17") + ``` + +- [ ] **Step 3: Run target integration tests to verify database connectivity** + + Verify that the tests spin up and execute successfully using the standard `postgres:17` image. + Run: `./gradlew test --tests "*PlaceControllerTest"` + Expected: BUILD SUCCESSFUL + +- [ ] **Step 4: Commit** + + Run: + ```bash + git add compose.db.dev.yaml src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java + git commit -m "chore: 로컬 및 테스트용 PostgreSQL 이미지를 postgres:17로 교체" + ``` + +--- + +### Task 3: Clean up Documentation References + +**Files:** +- Modify: `AGENTS.md` +- Modify: `README.md` +- Modify: `docs/ai/erd.md` + +- [ ] **Step 1: Remove references in `AGENTS.md`** + + In [AGENTS.md](file:///home/minbros/projects/java/how-about-us-backend/AGENTS.md): + - Change line 21: + ```markdown + - **Database**: PostgreSQL 17, MongoDB 8 + ``` + - Modify line 26 under "기타" section: + Remove `, hibernate-spatial` from the list. + - Modify line 81: + ```markdown + - PostgreSQL 이미지는 `postgres:17`을 사용한다. + ``` + - Remove line 85: + Delete `- 공간 데이터 엔티티에는 hibernate-spatial 타입을 사용한다.` completely. + +- [ ] **Step 2: Remove references in `README.md`** + + In [README.md](file:///home/minbros/projects/java/how-about-us-backend/README.md): + - Change line 16: + ```markdown + - **Database**: PostgreSQL 17, MongoDB 8 + ``` + - Change line 32: + ```markdown + ├── places/ ← 장소 (Google Places API 연동) + ``` + +- [ ] **Step 3: Remove references in `docs/ai/erd.md`** + + In [docs/ai/erd.md](file:///home/minbros/projects/java/how-about-us-backend/docs/ai/erd.md): + - Change line 5: + ```markdown + - **기술 스택:** PostgreSQL, MongoDB (채팅 메시지), Redis (세션/토큰) + ``` + +- [ ] **Step 4: Verify markdown changes via Git diff** + + Ensure that all spatial and PostGIS references are removed. + Run: `git diff AGENTS.md README.md docs/ai/erd.md` + Expected: View output containing only expected modifications to database image names, spatial lists, and comments. + +- [ ] **Step 5: Commit** + + Run: + ```bash + git add AGENTS.md README.md docs/ai/erd.md + git commit -m "docs: 문서 내 PostGIS 및 Spatial 기술 스택 언급 제거" + ``` + +--- + +### Task 4: Full Build and Integration Verification + +**Files:** None + +- [ ] **Step 1: Re-launch local DB container with the new postgres:17 image** + + Ensure local docker container gets updated to the standard postgres image. + Run: `docker compose -f compose.db.dev.yaml down -v && docker compose --env-file .env.dev -f compose.db.dev.yaml up -d` + Expected: Services (postgres, redis, mongodb) start successfully. + +- [ ] **Step 2: Run full build and test execution** + + Run a complete clean build of the application. + Run: `./gradlew clean build checkstyleMain checkstyleTest` + Expected: BUILD SUCCESSFUL (All tests pass and checkstyle has 0 warnings) From d16d3f51d9face8bad10d8ea32931c9a30775ea6 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:21:08 +0900 Subject: [PATCH 10/14] =?UTF-8?q?chore:=20build.gradle=EC=97=90=EC=84=9C?= =?UTF-8?q?=20hibernate-spatial=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index ac222a3c..20740ff5 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.hibernate.orm:hibernate-spatial' runtimeOnly 'org.postgresql:postgresql' implementation 'org.springframework.boot:spring-boot-starter-flyway' runtimeOnly 'org.flywaydb:flyway-database-postgresql' From a0f00ea395fac8b72eb53184553cf3d755f32d2c Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:21:25 +0900 Subject: [PATCH 11/14] =?UTF-8?q?chore:=20=EB=A1=9C=EC=BB=AC=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20PostgreSQL=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=EB=A5=BC=20postgres:17=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.db.dev.yaml | 2 +- .../com/howaboutus/backend/support/BaseIntegrationTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/compose.db.dev.yaml b/compose.db.dev.yaml index 400db9c7..85a05dac 100644 --- a/compose.db.dev.yaml +++ b/compose.db.dev.yaml @@ -3,7 +3,7 @@ name: uttae services: postgres: - image: 'postgis/postgis:17-3.5' + image: 'postgres:17' platform: linux/amd64 env_file: - ./.env.dev diff --git a/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java b/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java index 67f2fcab..b5a96e0d 100644 --- a/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java +++ b/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java @@ -20,8 +20,7 @@ public abstract class BaseIntegrationTest { static { POSTGRES = new PostgreSQLContainer( - DockerImageName.parse("postgis/postgis:17-3.5") - .asCompatibleSubstituteFor("postgres")) + DockerImageName.parse("postgres:17")) .withDatabaseName("howaboutus_test") .withUsername("test") .withPassword("test"); From e486ed2f477b229d209f5069ba184b1f52acdcc0 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:21:43 +0900 Subject: [PATCH 12/14] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=EB=82=B4=20?= =?UTF-8?q?PostGIS=20=EB=B0=8F=20Spatial=20=EA=B8=B0=EC=88=A0=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EC=96=B8=EA=B8=89=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 7 +++---- README.md | 4 ++-- docs/ai/erd.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bd6b4606..76f59ac9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,12 +18,12 @@ ## Tech Stack - **Framework**: Spring Boot 4.0.5, Java 21 -- **Database**: PostgreSQL 17 + PostGIS 3.5, MongoDB 8 +- **Database**: PostgreSQL 17, MongoDB 8 - **Cache**: Redis 8 - **Auth**: Spring Security - **Realtime**: WebSocket + STOMP - **Build**: Gradle -- **기타**: Lombok, Spring Data JPA, Spring Data MongoDB, `hibernate-spatial` +- **기타**: Lombok, Spring Data JPA, Spring Data MongoDB ## Commands @@ -78,11 +78,10 @@ src/main/resources/ ## Gotchas -- PostgreSQL 이미지는 `postgis/postgis:17-3.5`를 사용한다. +- PostgreSQL 이미지는 `postgres:17`을 사용한다. - MongoDB 이미지는 `mongo:8`을 사용하며 채팅 메시지 저장소로 사용한다. - PostgreSQL 스키마 변경은 Flyway SQL 마이그레이션으로 관리한다. - MongoDB 컬렉션/인덱스 변경은 Mongock ChangeUnit으로 관리한다. 변경 클래스는 `common/migration/mongo/` 아래에 두고, Spring Data 자동 인덱스 생성에 의존하지 않는다. -- 공간 데이터 엔티티에는 `hibernate-spatial` 타입을 사용한다. - dev 환경의 PostgreSQL 포트는 `5433`이다. - `spring.jpa.open-in-view=false`가 설정되어 있다. - WebSocket + STOMP 사용 시 Spring Security 설정에서 WebSocket 엔드포인트를 별도 허용해야 한다. diff --git a/README.md b/README.md index eb8bada2..e0e6494e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - **Language**: Java 21 - **Framework**: Spring Boot 4.0.5 -- **Database**: PostgreSQL 17 + PostGIS 3.5, MongoDB 8 +- **Database**: PostgreSQL 17, MongoDB 8 - **Cache**: Redis 8 - **Auth**: Spring Security + JWT - **Realtime**: WebSocket + STOMP @@ -29,7 +29,7 @@ src/main/java/com/howaboutus/backend/ ├── auth/ ← 인증/인가 ├── bookmarks/ ← 북마크 ├── messages/ ← 채팅 메시지 (MongoDB) -├── places/ ← 장소 (PostGIS 공간 데이터) +├── places/ ← 장소 (Google Places API 연동) ├── realtime/ ← WebSocket 실시간 통신 ├── rooms/ ← 채팅방 ├── schedules/ ← 일정 diff --git a/docs/ai/erd.md b/docs/ai/erd.md index 97940b1d..e93f4a30 100644 --- a/docs/ai/erd.md +++ b/docs/ai/erd.md @@ -2,7 +2,7 @@ > **초안 문서입니다.** 구현 과정에서 컬럼 및 관계가 변경될 수 있습니다. -- **기술 스택:** PostgreSQL + PostGIS, MongoDB (채팅 메시지), Redis (세션/토큰) +- **기술 스택:** PostgreSQL, MongoDB (채팅 메시지), Redis (세션/토큰) - **ID 전략:** roomId → UUID (초대 URL 보안), MongoDB message `_id` → 읽음 위치 복귀/히스토리 cursor, PostgreSQL 엔티티 → BIGINT auto-increment --- From c40f8c2c004072be5e38945c5df4b848f27dbba3 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:23:43 +0900 Subject: [PATCH 13/14] =?UTF-8?q?chore:=20compose.db.dev.yaml=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20platform:=20?= =?UTF-8?q?linux/amd64=20=EC=84=A4=EC=A0=95=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- compose.db.dev.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/compose.db.dev.yaml b/compose.db.dev.yaml index 85a05dac..ae9f1425 100644 --- a/compose.db.dev.yaml +++ b/compose.db.dev.yaml @@ -4,7 +4,6 @@ name: uttae services: postgres: image: 'postgres:17' - platform: linux/amd64 env_file: - ./.env.dev environment: From 37dc2dc4072eac7fb3059256c8f42cb24acda777 Mon Sep 17 00:00:00 2001 From: minbros Date: Mon, 8 Jun 2026 22:30:53 +0900 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20PostgreSQL=20=EB=B0=8F=20MongoDB?= =?UTF-8?q?=20=EB=B2=84=EC=A0=84=EC=9D=84=2017.10=20=EB=B0=8F=208.0.21?= =?UTF-8?q?=EB=A1=9C=20=EC=83=81=EC=84=B8=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 6 +++--- README.md | 2 +- compose.db.dev.yaml | 4 ++-- compose.db.prod.yaml | 2 +- .../com/howaboutus/backend/support/BaseIntegrationTest.java | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 76f59ac9..2becf389 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ ## Tech Stack - **Framework**: Spring Boot 4.0.5, Java 21 -- **Database**: PostgreSQL 17, MongoDB 8 +- **Database**: PostgreSQL 17.10, MongoDB 8.0.21 - **Cache**: Redis 8 - **Auth**: Spring Security - **Realtime**: WebSocket + STOMP @@ -78,8 +78,8 @@ src/main/resources/ ## Gotchas -- PostgreSQL 이미지는 `postgres:17`을 사용한다. -- MongoDB 이미지는 `mongo:8`을 사용하며 채팅 메시지 저장소로 사용한다. +- PostgreSQL 이미지는 `postgres:17.10`을 사용한다. +- MongoDB 이미지는 `mongo:8.0.21`을 사용하며 채팅 메시지 저장소로 사용한다. - PostgreSQL 스키마 변경은 Flyway SQL 마이그레이션으로 관리한다. - MongoDB 컬렉션/인덱스 변경은 Mongock ChangeUnit으로 관리한다. 변경 클래스는 `common/migration/mongo/` 아래에 두고, Spring Data 자동 인덱스 생성에 의존하지 않는다. - dev 환경의 PostgreSQL 포트는 `5433`이다. diff --git a/README.md b/README.md index e0e6494e..01cb7008 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - **Language**: Java 21 - **Framework**: Spring Boot 4.0.5 -- **Database**: PostgreSQL 17, MongoDB 8 +- **Database**: PostgreSQL 17.10, MongoDB 8.0.21 - **Cache**: Redis 8 - **Auth**: Spring Security + JWT - **Realtime**: WebSocket + STOMP diff --git a/compose.db.dev.yaml b/compose.db.dev.yaml index ae9f1425..ebed58b3 100644 --- a/compose.db.dev.yaml +++ b/compose.db.dev.yaml @@ -3,7 +3,7 @@ name: uttae services: postgres: - image: 'postgres:17' + image: 'postgres:17.10' env_file: - ./.env.dev environment: @@ -34,7 +34,7 @@ services: - redis-data:/data mongodb: - image: 'mongo:8' + image: 'mongo:8.0.21' env_file: - ./.env.dev environment: diff --git a/compose.db.prod.yaml b/compose.db.prod.yaml index 8f17d12c..e57951ca 100644 --- a/compose.db.prod.yaml +++ b/compose.db.prod.yaml @@ -42,7 +42,7 @@ services: max-file: "3" mongodb: - image: 'mongo:8' + image: 'mongo:8.0.21' env_file: - .env.db environment: diff --git a/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java b/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java index b5a96e0d..a6a7969c 100644 --- a/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java +++ b/src/test/java/com/howaboutus/backend/support/BaseIntegrationTest.java @@ -20,7 +20,7 @@ public abstract class BaseIntegrationTest { static { POSTGRES = new PostgreSQLContainer( - DockerImageName.parse("postgres:17")) + DockerImageName.parse("postgres:17.10")) .withDatabaseName("howaboutus_test") .withUsername("test") .withPassword("test"); @@ -31,7 +31,7 @@ public abstract class BaseIntegrationTest { .withCommand("redis-server", "--notify-keyspace-events", "Ex") .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*\\n", 1)); - MONGO = new MongoDBContainer(DockerImageName.parse("mongo:8")); + MONGO = new MongoDBContainer(DockerImageName.parse("mongo:8.0.21")); POSTGRES.start(); REDIS.start();