Skip to content

Latest commit

 

History

History
308 lines (224 loc) · 17.7 KB

local-setup.md

File metadata and controls

308 lines (224 loc) · 17.7 KB

로컬 개발 환경 설정

ReadMates를 로컬에서 실행하는 최소 절차입니다. 운영 배포는 배포 문서를 봅니다.

  • 완료 기준: backend health와 frontend dev server가 응답하고, 필요한 로그인 경로(dev-login 또는 Google OAuth)를 확인했습니다.
  • 운영 secret, private endpoint, 실제 멤버 데이터는 Git에 남기지 않습니다.
  • placeholder를 운영값으로 바꿔 커밋하거나, docker compose down -v처럼 데이터를 지우거나, 외부 provider 설정이 불확실하면 먼저 멈추고 확인합니다.

필수 도구

  • JDK 25 LTS
  • Node.js 24 (.node-version과 CI 기준)
  • pnpm@11.13.1 (루트 package.json의 packageManager, Corepack으로 활성화)
  • Docker Compose 또는 MySQL 8 compatible database

루트 pnpm workspace가 front, design/system, design/docs를 관리합니다. 백엔드는 Gradle wrapper와 Java 25 toolchain을 씁니다. 전역 Gradle 설치는 필요 없습니다.

의존성 설치

corepack pnpm install --frozen-lockfile

corepack이 PATH에 없으면 아래 fallback을 씁니다.

npx --yes corepack@0.35.0 pnpm install --frozen-lockfile

이 문서의 corepack pnpm ... 명령은 같은 방식으로 npx --yes corepack@0.35.0 pnpm ...로 바꿔 실행할 수 있습니다.

MySQL 준비

저장소의 compose.yml은 로컬 개발용 MySQL 8.4 컨테이너를 제공합니다.

docker compose up -d mysql

상태 확인:

docker compose ps

기본 접속 정보는 아래와 같습니다.

database: readmates
username: readmates
password: readmates
port: 3306

Root .env는 Git에서 무시됩니다. 기본값을 바꾸려면 .env.example의 Local Docker Compose MySQL section 중 필요한 변수만 .env에 복사해 수정합니다.

이미 별도 MySQL 8 compatible database를 사용한다면 같은 database를 만들고, backend 실행 시 SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD를 해당 값으로 바꿉니다.

이 database는 로컬 backend 실행과 E2E용입니다. Backend integrationTest는 Testcontainers가 MySQL을 직접 띄우므로 먼저 실행할 필요가 없습니다. E2E schema, CREATE DATABASE 권한, Flyway checksum mismatch는 test-guide.md를 봅니다.

Optional Redis

Redis는 선택 의존성이며 기본값은 비활성화입니다. 세션, 멤버십, 발행 기록, 노트 데이터의 최종 권위는 계속 MySQL입니다.

로컬 MySQL과 Redis를 함께 띄웁니다.

docker compose up -d mysql redis

Redis-backed 기능은 명시적으로 켭니다.

READMATES_REDIS_ENABLED=true \
READMATES_RATE_LIMIT_ENABLED=true \
READMATES_AUTH_SESSION_CACHE_ENABLED=true \
READMATES_PUBLIC_CACHE_ENABLED=true \
READMATES_NOTES_CACHE_ENABLED=true \
./server/gradlew -p server bootRun

기본 Redis URL은 redis://localhost:6379입니다. 다른 Redis를 쓰면 READMATES_REDIS_URL에 placeholder-safe 값을 넣고, 실제 secret이나 private endpoint는 Git에 남기지 않습니다.

Optional Kafka Notification Pipeline

Kafka 알림 relay/consumer를 로컬에서 확인할 때는 Compose의 kafka service를 MySQL과 함께 띄웁니다. 현재 Compose service는 Redpanda를 사용하지만 Spring에서는 Kafka bootstrap server로만 바라봅니다.

docker compose up -d mysql kafka

Redpanda healthcheck가 통과했는지는 docker compose ps kafka의 healthy 상태로 확인합니다.

로컬 bootstrap server는 localhost:9092입니다. Kafka integration test만 돌릴 때는 backend를 띄우거나 SMTP를 설정할 필요가 없습니다. Testcontainers가 Kafka를 띄우고 test configuration이 mail sender를 대체합니다.

./server/gradlew -p server integrationTest --tests 'com.readmates.notification.kafka.*'

로컬 backend에서 relay/consumer와 실제 이메일 발송까지 켤 때는 Kafka와 Spring mail property를 함께 넣습니다. READMATES_NOTIFICATIONS_ENABLED=true는 SmtpMailDeliveryAdapter를 선택하므로 SPRING_MAIL_HOST 같은 mail 설정이 필요합니다.

READMATES_NOTIFICATIONS_ENABLED=true \
READMATES_KAFKA_ENABLED=true \
READMATES_KAFKA_BOOTSTRAP_SERVERS=localhost:9092 \
READMATES_KAFKA_NOTIFICATION_EVENTS_TOPIC=readmates.notification.events.v1 \
READMATES_KAFKA_NOTIFICATION_DLQ_TOPIC=readmates.notification.events.dlq.v1 \
READMATES_KAFKA_NOTIFICATION_CONSUMER_GROUP=readmates-notification-dispatcher \
READMATES_KAFKA_NOTIFICATION_RELAY_BATCH_SIZE=50 \
READMATES_KAFKA_NOTIFICATION_MAX_PUBLISH_ATTEMPTS=5 \
READMATES_NOTIFICATION_RETRY_DELAY_MINUTES=5,15,60,240 \
READMATES_NOTIFICATION_MAX_DELIVERY_ATTEMPTS=5 \
SPRING_MAIL_HOST='<smtp-host>' \
SPRING_MAIL_PORT='<smtp-port>' \
SPRING_MAIL_USERNAME='<smtp-username>' \
SPRING_MAIL_PASSWORD='<smtp-password>' \
READMATES_NOTIFICATION_SENDER_EMAIL='no-reply@example.com' \
READMATES_NOTIFICATION_SENDER_NAME='ReadMates' \
./server/gradlew -p server bootRun

SMTP provider가 auth 또는 TLS를 요구하면 SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=true, SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=true 같은 Spring mail property도 로컬 환경에만 추가합니다. 예시에는 실제 credential이나 private endpoint를 남기지 않습니다.

Optional Prod-parity Compose Stack

OCI 운영과 같은 Docker Compose 런타임(Caddy + Spring + MySQL + Redis + Redpanda)에서 로컬 검증이 필요하면 deploy/local/compose.override.yml을 함께 띄웁니다. .env는 위 MySQL section의 READMATES_LOCAL_MYSQL_* 값을 사용합니다 (READMATES_LOCAL_MYSQL_ROOT_PASSWORD는 필수).

cp .env.example .env  # READMATES_LOCAL_MYSQL_ROOT_PASSWORD 등 편집
docker compose -f deploy/oci/compose.yml -f deploy/local/compose.override.yml --env-file .env up

override는 readmates-api를 dev profile로 띄우고 8080/3306/6379 포트를 host에 엽니다. 운영 VM에서는 쓰지 않습니다.

Backend 실행

로컬 개발은 dev profile을 사용합니다. 이 profile은 sample seed data와 dev-login fixture를 켭니다.

SPRING_PROFILES_ACTIVE=dev \
SPRING_DATASOURCE_URL='jdbc:mysql://localhost:3306/<local-db-name>?serverTimezone=UTC' \
SPRING_DATASOURCE_USERNAME='<local-db-user>' \
SPRING_DATASOURCE_PASSWORD='<local-db-password>' \
READMATES_APP_BASE_URL=http://localhost:5173 \
READMATES_AUTH_BASE_URL=http://localhost:5173 \
READMATES_AUTH_RETURN_STATE_SECRET='<local-return-state-signing-secret>' \
READMATES_ALLOWED_ORIGINS=http://localhost:5173 \
READMATES_BFF_SECRET='<local-bff-secret>' \
READMATES_IP_HASH_BASE_SECRET='<local-ip-hash-base-secret>' \
READMATES_AUTH_SESSION_COOKIE_SECURE=false \
./server/gradlew -p server bootRun

주요 backend 환경 변수는 다음과 같습니다.

변수 용도
SPRING_PROFILES_ACTIVE dev profile을 켜면 dev seed migration과 dev-login fixture가 활성화됩니다.
SPRING_DATASOURCE_URL MySQL JDBC URL입니다.
SPRING_DATASOURCE_USERNAME MySQL 사용자입니다.
SPRING_DATASOURCE_PASSWORD MySQL 비밀번호입니다.
READMATES_APP_BASE_URL 로그인 성공 후 돌아갈 frontend origin입니다.
READMATES_AUTH_BASE_URL Google OAuth callback redirect_uri에 쓰는 auth origin입니다. 로컬에서는 보통 READMATES_APP_BASE_URL과 같은 값을 둡니다.
READMATES_AUTH_RETURN_STATE_SECRET OAuth returnTo target 서명 secret입니다. 로컬에서도 placeholder-safe 값을 명시하면 production return-state 경계를 더 가깝게 확인할 수 있습니다.
READMATES_ALLOWED_ORIGINS mutating API 요청의 Origin 또는 Referer 허용 origin입니다. 쉼표로 여러 값을 줄 수 있습니다.
READMATES_BFF_SECRET BFF가 Spring API로 전달하는 공유 secret입니다. 로컬에서도 frontend proxy와 같은 값을 쓰면 production boundary를 비슷하게 테스트할 수 있습니다.
READMATES_BFF_SECRET_REQUIRED 기본 true, application-dev.yml은 false입니다. 그래도 READMATES_BFF_SECRET을 설정하면 /api/**는 X-Readmates-Bff-Secret을 요구합니다.
READMATES_IP_HASH_BASE_SECRET rate limit/client IP hash의 base secret입니다. 빈 값은 dev에서도 startup 실패이므로 placeholder 값을 넣습니다. 특수한 경우에만 readmates.security.ip-hash.allow-empty-secret=true를 씁니다.
READMATES_AUTH_SESSION_COOKIE_SECURE 로컬 HTTP에서 readmates_session cookie를 테스트하려면 false로 둡니다.
READMATES_FLYWAY_LOCATIONS Flyway migration 위치입니다. 기본은 classpath:db/mysql/migration이고, dev profile은 classpath:db/mysql/dev를 추가합니다.
READMATES_NOTIFICATIONS_ENABLED 알림 relay/consumer bean을 켜는 상위 flag입니다. 기본은 false입니다.
READMATES_KAFKA_ENABLED Kafka notification relay/consumer를 켭니다. 기본은 false입니다.
READMATES_KAFKA_BOOTSTRAP_SERVERS Kafka bootstrap server 목록입니다. 로컬 Compose 기본값은 localhost:9092입니다.
READMATES_KAFKA_NOTIFICATION_EVENTS_TOPIC notification event topic입니다. 기본은 readmates.notification.events.v1입니다.
READMATES_KAFKA_NOTIFICATION_DLQ_TOPIC consumer error handler가 사용하는 DLQ topic입니다. 기본은 readmates.notification.events.dlq.v1입니다.
READMATES_KAFKA_NOTIFICATION_CONSUMER_GROUP notification consumer group입니다. 기본은 readmates-notification-dispatcher입니다.
READMATES_NOTIFICATION_RETRY_DELAY_MINUTES email delivery dispatch/worker retry delay 목록입니다. 기본은 5,15,60,240분입니다.
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_ID Google OAuth를 로컬에서 직접 시험할 때 필요한 client id입니다. dev-login만 쓰면 필요하지 않습니다.
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_CLIENT_SECRET Google OAuth를 로컬에서 직접 시험할 때 필요한 client secret입니다. dev-login만 쓰면 필요하지 않습니다.
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GOOGLE_SCOPE Google OAuth scope입니다. 기본 운영 예시는 openid,email,profile입니다.

macOS Keychain으로 로컬 Google OAuth 실행

실제 Google 로그인을 로컬에서 확인할 때는 운영 OAuth client를 재사용하지 않고 별도 Web client를 만듭니다. Google Cloud의 승인된 redirect URI에는 다음 callback만 추가합니다.

http://localhost:5174/login/oauth2/code/google
http://localhost:5173/login/oauth2/code/google

Client ID와 client secret은 .env, 프런트 VITE_*, shell history에 기록하지 않습니다. macOS에서는 아래 명령이 각 값을 대화형으로 요청해 Keychain에 저장합니다. -w를 마지막 인자로 유지해야 값이 명령행 인자에 들어가지 않습니다.

security add-generic-password -U -a "$USER" -s readmates.local.google-oauth.client-id -w
security add-generic-password -U -a "$USER" -s readmates.local.google-oauth.client-secret -w

로컬 Google OAuth one-command stack

아래 한 줄 실행기로 backend + frontend를 함께 띄우고 포트 충돌, health readiness, signal 정리를 처리합니다.

./scripts/run-local-google-oauth-stack.sh

권장 실행 예시:

READMATES_LOCAL_GOOGLE_OAUTH_FRONTEND_PORT=5174 \
READMATES_LOCAL_GOOGLE_OAUTH_BACKEND_PORT=28080 \
READMATES_LOCAL_GOOGLE_OAUTH_MANAGEMENT_PORT=28081 \
READMATES_LOCAL_GOOGLE_OAUTH_STARTUP_TIMEOUT_SECONDS=180 \
READMATES_LOCAL_GOOGLE_OAUTH_OPEN_BROWSER=false \
./scripts/run-local-google-oauth-stack.sh

다음 검증 명령은 실제 redirect 계약만 점검합니다.

./scripts/verify-local-google-oauth-stack.sh

수동 검증(브라우저):

  1. stack 실행 완료 후 출력된 Login URL로 이동해 Google 화면 진입을 확인합니다.
  2. 등록된 테스트 계정으로 로그인 후 ReadMates로 복귀하는지 확인합니다.
  3. chooseAccount=true 시작 URL로 동일 계정 재선택 흐름을 확인합니다.
  4. Ctrl+C 종료 후 기존 서비스와 기존 포트가 유지되는지 확인합니다.

수동 확인은 5174 callback을 Google Cloud에 등록한 경우에만 합니다. 다른 포트는 자동 redirect 검증만 합니다.

Troubleshooting

이슈 대응
Keychain 누락/포맷 오류 security find-generic-password -a "$USER" -s readmates.local.google-oauth.client-id -w로 존재 여부를 점검하고, 필요하면 대화형 security add-generic-password ... -w로 재등록합니다.
invalid_client client ID를 교체하지 말고, Google Cloud 승인 callback과 현재 localhost callback을 동일하게 등록했는지 확인합니다.
redirect_uri_mismatch 등록한 callback(http://localhost:5174/login/oauth2/code/google 또는 5173)과 정확히 일치하는지 확인합니다.
포트 충돌 lsof -nP -iTCP:5174 -sTCP:LISTEN로 listener만 조회해 현재 충돌 포트를 파악하고 오버라이드 포트를 설정해 다시 실행합니다. 기존 서비스는 종료하지 않습니다.
readiness timeout READMATES_LOCAL_GOOGLE_OAUTH_STARTUP_TIMEOUT_SECONDS를 조정하고 management health + /login 응답을 확인해 선행 실패 원인을 좁힙니다.

로그/출력은 client ID, client secret, OAuth code/state, 쿠키를 노출하지 않습니다.

기존 방식(고급 사용)

backend와 frontend를 따로 띄워야 할 때만 씁니다.

./scripts/run-local-google-oauth.sh
VITE_ENABLE_GOOGLE_LOGIN=true corepack pnpm --dir front dev

VITE_ENABLE_GOOGLE_LOGIN은 표시/숨김 switch일 뿐 credential이 프론트엔드로 전달되지 않습니다. 기존 dev-login은 VITE_ENABLE_DEV_LOGIN로 별도 제어합니다.

운영 migration은 server/src/main/resources/db/mysql/migration에만 추가합니다. server/src/main/resources/db/migration은 사용하지 않으며, 새 파일을 그 위치에 만들면 운영 Flyway가 읽지 않습니다.

Server 로그는 JSON이므로 ./server/gradlew -p server bootRun 2>&1 | jq '.'로 볼 수 있습니다.

Frontend 실행

Vite 개발 서버는 front/vite.config.ts의 proxy를 사용합니다. 로컬에서는 Cloudflare Pages Functions 대신 Vite proxy가 /api/bff/**, /oauth2/authorization/**, /login/oauth2/code/**를 Spring backend로 전달합니다.

READMATES_API_BASE_URL=http://localhost:8080 \
READMATES_BFF_SECRET='<local-bff-secret>' \
VITE_ENABLE_DEV_LOGIN=true \
corepack pnpm --dir front dev

브라우저에서 http://localhost:5173을 엽니다.

주요 frontend 환경 변수는 다음과 같습니다.

변수 용도
READMATES_API_BASE_URL Vite dev proxy와 Cloudflare Pages Functions가 바라볼 Spring API origin입니다. 문서 예시는 로컬 http://localhost:8080 또는 placeholder https://api.example.com만 사용합니다.
READMATES_BFF_SECRET Vite dev proxy 또는 Pages Functions가 Spring으로 보낼 X-Readmates-Bff-Secret 값입니다. 브라우저 bundle에 들어가는 VITE_ 변수로 만들지 않습니다.
VITE_ENABLE_DEV_LOGIN 로컬 dev-login 버튼 표시를 명시적으로 켭니다. import.meta.env.DEV에서도 표시되지만, 로컬 의도를 분명히 하기 위해 설정할 수 있습니다.
VITE_ENABLE_GOOGLE_LOGIN 실제 Google credential을 backend에 안전하게 주입한 로컬 환경에서만 Google action을 표시하는 비민감 switch입니다. credential 자체를 넣지 않습니다.
NEXT_PUBLIC_ENABLE_DEV_LOGIN 이전 로컬 env 파일 호환을 위한 legacy 변수입니다. 새 설정에는 VITE_ENABLE_DEV_LOGIN을 사용합니다.

Dev-login 흐름

Dev-login은 로컬 개발과 E2E fixture를 위한 흐름입니다. production auth가 아닙니다.

  • Backend endpoint는 POST /api/dev/login입니다.
  • Controller는 @Profile("!prod & !production")와 readmates.dev.login-enabled=true 조건에서만 등록됩니다.
  • application-dev.yml은 readmates.dev.login-enabled=true를 설정합니다.
  • Frontend login 화면은 production build에서는 dev-login 버튼을 숨깁니다.
  • Fixture 계정은 host@example.com, member1@example.com 같은 example.com 주소를 사용합니다.
  • Dev seed는 기존 account alias인 users.short_name과 현재 표시 이름 저장소인 memberships.short_name을 모두 채웁니다. 화면 표시와 프로필 수정 API는 displayName 필드를 주고받고 membership 단위 memberships.short_name을 갱신하므로, /app/me와 /app/host/members에서 표시 이름 변경 흐름을 로컬로 확인할 수 있습니다.
  • Dev seed와 E2E fixture에는 OPEN 세션, CLOSED/PUBLISHED 기록, 새 DRAFT 예정 세션 흐름이 있습니다. 호스트는 /app/host에서 예정 세션의 게스트 접근(HOST_ONLY ↔ GUEST_READABLE)을 바꾸고 DRAFT를 OPEN으로 시작한 뒤, /app/host/sessions/:sessionId/edit에서 닫기와 기록 발행을 확인할 수 있습니다.

운영 로그인은 Google OAuth 흐름입니다. 브라우저는 /oauth2/authorization/google로 시작하고, callback은 /login/oauth2/code/google로 돌아오며, Spring은 성공 시 readmates_session cookie를 발급합니다. 로컬 route guard, loader, API 401 흐름도 같은 origin의 안전한 relative returnTo만 /login?returnTo=...와 OAuth start에 전달하므로, 복귀 경로를 확인할 때 absolute URL이나 login/reset/invite/OAuth path를 fixture로 쓰지 않습니다.

종료와 초기화

MySQL 컨테이너를 멈추려면 아래 명령을 사용합니다.

docker compose down

로컬 database volume까지 삭제하려면 아래 명령을 사용합니다. 기존 seed와 개발 중 데이터가 사라집니다.

docker compose down -v