diff --git a/CLAUDE.md b/CLAUDE.md index 52510b8..bbe2a23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,9 @@ Korean Travel Guide Backend - A Spring Boot Kotlin application providing OAuth a # Run specific test ./gradlew test --tests "WeatherServiceTest" + +# Compile Kotlin only (faster than full build) +./gradlew compileKotlin ``` ### Code Quality @@ -54,6 +57,24 @@ open http://localhost:8080/h2-console curl http://localhost:8080/actuator/health ``` +### Local RabbitMQ Setup +```bash +# Start RabbitMQ with docker-compose (for WebSocket testing) +docker network create common +docker-compose up -d + +# Check RabbitMQ status +docker logs rabbitmq-1 + +# Access RabbitMQ Management UI +open http://localhost:15672 +# Username: admin +# Password: (from .env PASSWORD_1 or default: qwpokd153098) + +# Stop RabbitMQ +docker-compose down +``` + ### Redis (Optional - for caching) ```bash # Start Redis with Docker @@ -73,7 +94,7 @@ The codebase follows Domain-Driven Design with clear separation: ``` com/back/koreaTravelGuide/ ├── common/ # Shared infrastructure -│ ├── config/ # App, Security, Redis, AI configs +│ ├── config/ # App, Security, Redis, AI, DevConfig │ ├── security/ # OAuth2, JWT, filters │ ├── exception/ # Global exception handler │ └── ApiResponse.kt # Standard API response wrapper @@ -86,7 +107,8 @@ com/back/koreaTravelGuide/ │ │ └── tour/ # Tourism API integration │ ├── userChat/ # WebSocket chat between Guest-Guide │ │ ├── chatroom/ # Chat room management -│ │ └── chatmessage/ # Message persistence +│ │ ├── chatmessage/ # Message persistence & publishing +│ │ └── stomp/ # WebSocket config (Simple/Rabbit) │ └── rate/ # Rating system for AI sessions & guides ``` @@ -95,15 +117,43 @@ com/back/koreaTravelGuide/ 1. **Each domain is self-contained** with its own entity, repository, service, controller, and DTOs 2. **Common utilities live in `common/`** - never duplicate config or security logic 3. **AI Chat uses Spring AI** with function calling for weather/tour tools -4. **User Chat uses WebSocket** (STOMP) for real-time messaging -5. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically +4. **User Chat uses WebSocket** with profile-based configuration: + - **Dev**: SimpleBroker (in-memory, single server) + - **Prod**: RabbitMQ STOMP Relay (scalable, multi-server) +5. **Port-Adapter pattern** for message publishing (`ChatMessagePublisher` interface with `SimpleChatMessagePublisher` and `RabbitChatMessagePublisher` implementations) +6. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically ### Critical Configuration Files - **build.gradle.kts**: Contains BuildConfig plugin that generates constants from YAML files (area-codes.yml, prompts.yml, etc.) -- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers +- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers, RabbitMQ for local testing +- **application-prod.yml**: Production config with PostgreSQL, Redis required, RabbitMQ with connection stability settings - **SecurityConfig.kt**: Currently allows all requests for dev (MUST restrict for production) - **AiConfig.kt**: Spring AI ChatClient with OpenRouter (uses OPENROUTER_API_KEY env var) +- **DevConfig.kt**: Auto-generates 2 dummy GUIDE users on startup (dev profile only) + +### Profile-Based Configuration Strategy + +The application uses Spring profiles (`@Profile` annotation) to switch implementations: + +**Development Profile (`dev`):** +- H2 in-memory database +- `SimpleChatMessagePublisher` - uses Spring's SimpleBroker (no RabbitMQ needed) +- `UserChatSimpleWebSocketConfig` - basic WebSocket with in-memory broker +- Redis optional (session.store-type: none) +- Dummy guide data auto-generation + +**Production Profile (`prod`):** +- PostgreSQL database +- `RabbitChatMessagePublisher` - publishes to RabbitMQ +- `UserChatRabbitWebSocketConfig` - STOMP Broker Relay to RabbitMQ +- Redis required (session.store-type: redis) +- Connection stability settings (timeouts, heartbeats) + +When adding new features that differ between dev/prod, follow this pattern: +1. Create an interface in the domain layer +2. Create separate implementations with `@Profile("dev")` and `@Profile("prod")` +3. Inject via the interface, Spring will wire the correct implementation ## Working with Spring AI @@ -121,6 +171,44 @@ fun getTourSpots(area: String): TourResponse **Important**: System prompts are managed in `src/main/resources/prompts.yml` and compiled into BuildConfig at build time. +## WebSocket & Real-Time Messaging + +### Architecture +User-to-user chat uses WebSocket with STOMP protocol. The implementation switches based on profile: + +**Development**: Uses Spring's SimpleBroker (in-memory) +- Suitable for single-server development +- No external dependencies +- Messages stored in memory only + +**Production**: Uses RabbitMQ STOMP Relay +- Scales across multiple server instances +- Messages persist in RabbitMQ +- Handles reconnection and failover + +### Message Flow +1. Client connects to WebSocket endpoint: `/ws/userchat` +2. Client sends message to: `/pub/chat/send` +3. Server processes and publishes to: `/topic/chat/{roomId}` +4. `ChatMessagePublisher` interface abstracts the publishing mechanism +5. Messages are persisted to database via `ChatMessageService` + +### RabbitMQ Configuration +Located in `application.yml` and `application-prod.yml`: +```yaml +spring: + rabbitmq: + host: ${RABBITMQ_HOST} + port: ${RABBITMQ_PORT} + username: ${RABBITMQ_USERNAME} + password: ${RABBITMQ_PASSWORD} + stomp-port: ${RABBITMQ_STOMP_PORT} # Default: 61613 +``` + +RabbitMQ requires two plugins enabled: +- `rabbitmq_management` - Management UI (port 15672) +- `rabbitmq_stomp` - STOMP protocol support (port 61613) + ## Testing Strategy Tests are in `src/test/kotlin` mirroring the main structure: @@ -152,7 +240,7 @@ Always run `./gradlew ktlintCheck` before committing - it's enforced by git hook - **Development**: H2 in-memory (jdbc:h2:mem:testdb), resets on restart - **Production**: PostgreSQL (configured in application-prod.yml) -- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart) +- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart), `update` in prod Main entities: - `User` - OAuth users with roles (GUEST/GUIDE/ADMIN) @@ -166,7 +254,7 @@ Required `.env` file (copy from .env.example): ```bash # AI (Required) OPENROUTER_API_KEY=sk-or-v1-... -OPENROUTER_MODEL=anthropic/claude-3.5-sonnet +OPENROUTER_MODEL=z-ai/glm-4.5-air:free # OAuth (Required for auth) GOOGLE_CLIENT_ID=... @@ -183,7 +271,14 @@ TOUR_API_KEY=... # JWT (Required for production) CUSTOM__JWT__SECRET_KEY=... -# Redis (Optional - caching) +# RabbitMQ (Required for prod WebSocket) +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=admin +RABBITMQ_PASSWORD=qwpokd153098 +RABBITMQ_STOMP_PORT=61613 + +# Redis (Optional in dev, required in prod) REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= @@ -217,6 +312,33 @@ Common exceptions mapped to HTTP status: 5. **Commit**: `{type}(scope): summary` (e.g., `feat(be): Add weather caching`) 6. **PR title**: `{type}(scope): summary (#{issue})` (e.g., `feat(be): Add weather caching (#42)`) +## Infrastructure & Deployment + +### Terraform Infrastructure (`infra/main.tf`) +EC2 instance setup with: +1. Docker & docker-compose installation +2. Container network (`common`) +3. Nginx Proxy Manager (ports 80, 443, 81) +4. Redis (port 6379) +5. PostgreSQL 16 (port 5432) +6. RabbitMQ with management & STOMP plugins (ports 5672, 15672, 61613) + +**Order matters**: Docker → docker-compose → network → containers + +### CI/CD Pipeline (`.github/workflows/deploy.yml`) +Blue-Green deployment strategy: +1. Build on GitHub Actions runner +2. Transfer JAR to EC2 +3. Deploy to blue/green container based on availability +4. Health check before switching traffic +5. Environment variables injected via `docker run -e` + +### Docker Compose Files +- `docker-compose.yml` (root) - Local development RabbitMQ +- `infra/rabbitmq-docker-compose.yml` - EC2 production RabbitMQ template + +Both require `common` network to be pre-created: `docker network create common` + ## Common Issues & Solutions ### Build fails with "BuildConfig not found" @@ -228,13 +350,20 @@ Common exceptions mapped to HTTP status: - Start Redis: `docker run -d -p 6379:6379 --name redis redis:alpine` - Check: `docker logs redis` +### RabbitMQ connection issues +- In dev: RabbitMQ is optional (SimpleBroker used instead) +- In prod: RabbitMQ is required for WebSocket +- Start local RabbitMQ: `docker-compose up -d` +- Check logs: `docker logs rabbitmq-1` +- Verify plugins: `rabbitmq_management` and `rabbitmq_stomp` must be enabled + ### ktlint failures - Auto-fix: `./gradlew ktlintFormat` - Pre-commit hook enforces this - setup via `./setup-git-hooks.sh` ### Spring AI errors - Verify `OPENROUTER_API_KEY` in .env -- Check model name matches OpenRouter API (currently: anthropic/claude-3.5-sonnet) +- Check model name matches OpenRouter API (currently: z-ai/glm-4.5-air:free) - Logs show AI requests: `logging.level.org.springframework.ai: DEBUG` ### OAuth login fails @@ -242,6 +371,12 @@ Common exceptions mapped to HTTP status: - Check redirect URIs match OAuth provider settings - Dev: `http://localhost:8080/login/oauth2/code/{provider}` +### WebSocket connection fails +- Check profile: dev uses SimpleBroker, prod uses RabbitMQ +- In prod: Ensure RabbitMQ is running and accessible +- Verify STOMP port (61613) is open and reachable +- Check `UserChatStompAuthChannelInterceptor` for authentication issues + ## Important Notes - **Never commit .env** - it's gitignored, contains secrets @@ -250,3 +385,8 @@ Common exceptions mapped to HTTP status: - **Global config in common/** - don't duplicate security/config in domains - **BuildConfig is generated** - don't edit manually, modify YAML sources - **Redis is optional in dev** - required for production caching/sessions +- **RabbitMQ is optional in dev** - required for production WebSocket scaling +- **Profile-based beans** - use `@Profile` to switch implementations between dev/prod +- **Dummy data in dev** - 2 guide users auto-generated on startup (DevConfig.kt) +- **Docker network required** - `common` network must exist before running docker-compose +- **Terraform order matters** - Docker installation before docker-compose, containers after both diff --git a/docker-compose.yml b/docker-compose.yml index 3722ff1..94e58a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,20 +4,26 @@ services: rabbit: image: rabbitmq:3-management # 버전 명시 권장 (예: 3.13-management) container_name: rabbitmq-1 + networks: + - common # 이미 존재하는 공용 네트워크 사용 ports: - "5672:5672" # AMQP (Spring ↔ RabbitMQ) - "61613:61613" # STOMP (Relay가 여기에 붙음) - "15672:15672" # Management UI (로컬에서만) environment: + TZ: Asia/Seoul # 타임존 설정 RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: admin - volumes: # 영속화가 필요할 때만 유지 - - ./dockerProjects/rabbitmq-1/volumes/etc/rabbitmq:/etc/rabbitmq - - ./dockerProjects/rabbitmq-1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq - - ./dockerProjects/rabbitmq-1/volumes/var/log/rabbitmq:/var/log/rabbitmq + RABBITMQ_DEFAULT_PASS: "$PASSWORD_1" + volumes: + - ./volumes/etc/rabbitmq:/etc/rabbitmq + - ./volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - ./volumes/var/log/rabbitmq:/var/log/rabbitmq command: > sh -c " rabbitmq-plugins enable rabbitmq_management && rabbitmq-plugins enable rabbitmq_stomp && rabbitmq-server " +networks: + common: + external: true # 이미 만들어진 네트워크를 사용 \ No newline at end of file diff --git a/infra/main.tf b/infra/main.tf index ba9c8be..51ac310 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -210,6 +210,13 @@ yum install docker -y systemctl enable docker systemctl start docker +# docker-compose 설치 +echo "docker-compose 설치 중..." +curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose +ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose +docker-compose --version + # 도커 네트워크 생성 docker network create common @@ -263,33 +270,48 @@ CREATE DATABASE \"${var.app_1_db_name}\" OWNER team11; GRANT ALL PRIVILEGES ON DATABASE \"${var.app_1_db_name}\" TO team11; " -# rabbitmq 설치 -docker run -d \ - --name rabbitmq_1 \ - --restart unless-stopped \ - --network common \ - -p 5672:5672 \ - -p 61613:61613 \ - -p 15672:15672 \ - -e RABBITMQ_DEFAULT_USER=admin \ - -e RABBITMQ_DEFAULT_PASS=${var.password_1} \ - -e TZ=Asia/Seoul \ - -v /dockerProjects/rabbitmq_1/volumes/data:/var/lib/rabbitmq \ - rabbitmq:3-management - -# RabbitMQ가 준비될 때까지 대기 -echo "RabbitMQ가 기동될 때까지 대기 중..." -until docker exec rabbitmq_1 rabbitmqctl status &> /dev/null; do - echo "RabbitMQ가 아직 준비되지 않음. 5초 후 재시도..." - sleep 5 -done -echo "RabbitMQ가 준비됨. STOMP 플러그인 활성화 중..." - -# RabbitMQ STOMP 플러그인 활성화 -docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_stomp -docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_management - -echo "RabbitMQ 설치 및 설정 완료!" +# RabbitMQ docker-compose.yml 생성 +mkdir -p /dockerProjects/rabbitmq_1 +cat > /dockerProjects/rabbitmq_1/docker-compose.yml <<'RABBITMQ_COMPOSE' +version: "3.8" + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq_1 + restart: unless-stopped + networks: + - common + ports: + - "5672:5672" + - "61613:61613" + - "15672:15672" + environment: + TZ: Asia/Seoul + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: \${PASSWORD_1} + volumes: + - /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/log/rabbitmq:/var/log/rabbitmq + command: > + sh -c " + rabbitmq-plugins enable rabbitmq_management && + rabbitmq-plugins enable rabbitmq_stomp && + rabbitmq-server + " + +networks: + common: + external: true +RABBITMQ_COMPOSE + +# RabbitMQ 시작 +echo "RabbitMQ 시작 중..." +cd /dockerProjects/rabbitmq_1 +docker-compose up -d + +echo "RabbitMQ docker-compose 설치 완료!" echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin diff --git a/infra/rabbitmq-docker-compose.yml b/infra/rabbitmq-docker-compose.yml new file mode 100644 index 0000000..c682e85 --- /dev/null +++ b/infra/rabbitmq-docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq_1 + restart: unless-stopped + networks: + - common + ports: + - "5672:5672" + - "61613:61613" + - "15672:15672" + environment: + TZ: Asia/Seoul + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: ${PASSWORD_1} + volumes: + - /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/log/rabbitmq:/var/log/rabbitmq + command: > + sh -c " + rabbitmq-plugins enable rabbitmq_management && + rabbitmq-plugins enable rabbitmq_stomp && + rabbitmq-server + " + +networks: + common: + external: true diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4786d61..661b05d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,6 +11,7 @@ spring: exclude: - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration # Redis 없어도 실행 가능하도록 변경 - org.springframework.boot.autoconfigure.session.SessionAutoConfiguration # Redis 없어도 실행 가능하도록 변경 + - org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration # RabbitMQ 없어도 실행 가능하도록 변경 (dev용) config: import: - "optional:file:.env[.properties]"