diff --git a/README.md b/README.md index 47dfb559..ce8d02ac 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,209 @@ # Beach Complex -공공데이터를 바탕으로 전국 해수욕장 정보를 조회하고, 상태 정보, 즐겨찾기, 예약, 알림 기능까지 함께 제공하는 해수욕장 서비스 프로젝트입니다. -이 레포는 Spring Boot 백엔드 애플리케이션의 원본이며, 프론트엔드와 문서는 별도 레포에서 관리합니다. +**부산 해수욕장 7곳의 시설·혼잡도를 조회하고, 즐겨찾기와 방문 예약을 관리하는 서비스입니다.** -## 프로젝트 개요 +[![CI](https://github.com/Beach-complex/Beach_complex/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Beach-complex/Beach_complex/actions/workflows/ci.yml) +[![JaCoCo line coverage 89.8% — 2026-10-01 snapshot](./assets/badges/coverage.svg)](#품질과-배포) -- 프로젝트 성격: 팀 프로젝트 -- 목적: 해수욕장 관련 공공데이터와 사용자 기능을 한 서비스 안에서 제공 -- 주요 기술: Spring Boot, PostgreSQL(PostGIS), Flyway, Docker Compose -- 현재 구현 중심 흐름: 인증, 해수욕장 조회, 상태 정보 조회, 즐겨찾기, 예약, 알림 +Spring Boot 백엔드와 배포·모니터링 설정을 담은 저장소입니다. -## 현재 상태 +[프론트엔드](https://github.com/Beach-complex/beach_complex_client) · [주요 구현](#주요-구현) · [문제 해결](#문제-해결) · [로컬 실행](#로컬-실행) · [문서](#문서) -- 백엔드 API와 배포/테스트 설정이 구성된 저장소입니다. -- 인증, 해수욕장 조회, 즐겨찾기, 예약, 알림 관련 API가 존재합니다. -- DB 마이그레이션은 Flyway로 관리합니다. -- React 프론트엔드는 [Beach-complex/frontend](https://github.com/Beach-complex/frontend), 협업/운영 문서는 [Beach-complex/docs](https://github.com/Beach-complex/docs)에서 관리합니다. +## 기술 스택 -## 주요 기능 +![Java 21](https://img.shields.io/badge/Java-21-437291) +![Spring Boot 3.3](https://img.shields.io/badge/Spring_Boot-3.3-6DB33F) +![PostgreSQL 16 · PostGIS 3.4](https://img.shields.io/badge/PostgreSQL_16-PostGIS_3.4-4169E1) +![Caffeine](https://img.shields.io/badge/Cache-Caffeine-8A684B) +![OpenTelemetry](https://img.shields.io/badge/Tracing-OpenTelemetry-425CC7) +![Grafana](https://img.shields.io/badge/Observability-Grafana-F46800) +![Docker Compose](https://img.shields.io/badge/Docker-Compose-2496ED) +![Terraform](https://img.shields.io/badge/IaC-Terraform-844FBA) -### 사용자 기능 -- 회원가입, 로그인, JWT 기반 인증 -- 사용자별 즐겨찾기 등록, 삭제, 토글, 조회 +| 영역 | 기술 | +| --- | --- | +| 프론트엔드 | React (별도 저장소) | +| 백엔드 | Java 21, Gradle, Spring Boot 3.3, Spring Security, JWT, Spring Data JPA / Hibernate | +| 데이터베이스·캐시 | PostgreSQL 16 / PostGIS 3.4, Flyway, Caffeine, Redis 7 (로컬 개발용) | +| 외부 연동 | Firebase Cloud Messaging(FCM), AWS Lambda Function URL / SigV4, Spring Mail / Gmail SMTP | +| 메트릭·로그·Trace 계측 | Spring Boot Actuator, Micrometer, OpenTelemetry, Logback | +| 모니터링 | Prometheus, node_exporter, Grafana Alloy, Loki, Tempo, Grafana | +| 인프라 | AWS EC2, EBS, Security Group, IAM role, Docker / Docker Compose, Terraform, cloud-init | +| CI/CD | GitHub Actions, GitHub Container Registry(GHCR), SSH 배포 | +| 품질 | JUnit 5, Testcontainers, JaCoCo, Checkstyle, Spotless, Gitleaks | -### 해수욕장 정보 -- 해수욕장 목록 조회 -- 해수욕장 상세 조회 -- 해수욕장 시설 정보 조회 -- 해수욕장 상태 정보 조회 +## 시스템 구성 -### 예약 및 알림 -- 예약 생성, 조회, 취소 -- 알림 설정 및 FCM 토큰 등록 -- 알림 테스트 API 제공 +[![Beach Complex 시스템 구성도 — 클라이언트, 앱 EC2, 관측 EC2, 외부 연동, CI/CD](./assets/images/system-architecture.png)](./assets/images/system-architecture.png) -## 기술 스택 +앱 서버는 Spring Boot와 PostgreSQL/PostGIS를 Docker Compose로 실행합니다. React 클라이언트는 REST API를 호출하고, 백엔드는 혼잡도 조회에 AWS Lambda, 이메일 인증에 Gmail SMTP, 푸시 알림에 FCM을 사용합니다. -### Backend -- Java 21 -- Spring Boot 3.3 -- Spring Security -- Spring Validation -- Spring Data JPA -- Spring Data Redis -- Spring Retry -- Spring Mail -- JWT -- Swagger UI - -### Database / Infra -- PostgreSQL -- PostGIS -- Flyway -- Redis 설정 -- Caffeine Cache -- Docker Compose - -### Quality -- JUnit 5 -- Testcontainers -- JaCoCo -- Checkstyle -- Spotless -- GitHub Actions +모니터링은 별도 EC2에 Prometheus·Tempo·Loki·Grafana를 두는 구성입니다. 앱의 Trace는 Tempo로, Alloy가 수집한 Docker 로그는 Loki로 보냅니다. Grafana에서 메트릭, 로그, Trace를 조회합니다. -## 프로젝트 구조 +## 주요 구현 -```text -Beach_complex/ -├── src/main/java/com/beachcheck/ -│ ├── auth/ # 인증, 토큰, 이메일 인증 -│ ├── beach/ # 해수욕장, 시설, 상태 정보 -│ ├── external/ # 외부 API 연동 -│ ├── global/ # 공통 설정, 예외, 보안, 유틸 -│ ├── notification/ # 알림 -│ ├── outbox/ # Outbox 이벤트 발행/전송 -│ ├── reservation/ # 예약 -│ └── user/ # 사용자, 즐겨찾기 -├── src/test/java/com/beachcheck/ -│ ├── auth/ -│ ├── beach/ -│ ├── external/ -│ ├── global/ -│ ├── notification/ -│ ├── outbox/ -│ ├── reservation/ -│ ├── support/ # 테스트 공통 설정/fixture -│ └── user/ -├── src/main/resources/ -│ ├── application.yml -│ ├── db/migration/ -│ └── firebase-service-account.json.example -├── docs/ # 문서 레포 진입점 -├── .github/workflows/ # CI 및 자동화 -├── docker-compose.yml -└── build.gradle +### 반경 검색과 조회 캐시 + +위도·경도와 반경을 입력하면 가까운 해수욕장부터 반환합니다. PostGIS의 `ST_DWithin`으로 검색 범위를 제한하고 `ST_Distance`로 정렬합니다. 반경은 API에서 km로 받아 DB 조회 시 m로 변환합니다. + +시설·상태 조회에는 Caffeine 캐시를 적용했습니다. 기존 메모리 맵에 만료 시간과 크기 제한이 없어 교체했으며, 별도 서버가 필요 없는 로컬 캐시를 선택했습니다. TTL은 10분, 캐시별 최대 항목은 1,000개입니다. + +[공간 쿼리](./src/main/java/com/beachcheck/beach/repository/BeachRepository.java) · [조회 API 통합 테스트](./src/test/java/com/beachcheck/beach/integration/BeachControllerIntegrationTest.java) · [Caffeine 선택 배경과 대안](https://github.com/Beach-complex/beach_complex_docs/blob/main/adr/001-cache-migration-caffeine.md) + +### Outbox 기반 푸시 알림 + +알림과 전송할 이벤트를 하나의 DB 트랜잭션으로 저장합니다. API는 저장 직후 응답하고, 워커가 이벤트를 읽어 FCM으로 푸시를 보냅니다. 전송 결과와 재시도할 작업을 DB에 남겨 별도 메시지 브로커 없이 처리합니다. + +테스트 푸시 API의 처리 흐름은 다음과 같습니다. + +```mermaid +sequenceDiagram + participant client + participant api + participant database + participant worker + participant fcm + client->>api: POST /api/notifications/test + api->>database: 알림과 Outbox, producer traceparent 저장 + database-->>api: 단일 트랜잭션 커밋 + api-->>client: 202 Accepted + worker->>database: 처리 대상 Outbox 조회 후 조회 트랜잭션 종료 + database-->>worker: 이벤트 목록 + worker->>database: 이벤트별 새 트랜잭션에서 알림 상태 확인 + worker->>fcm: 푸시 전송 + fcm-->>worker: 전송 결과 + worker->>database: 성공 또는 재시도 또는 영구 실패 상태 저장 ``` -## API 영역 +워커는 전송을 시도할 때마다 새 Trace를 만들고, 이벤트에 저장된 `traceparent`를 Span Link로 연결합니다. 이를 통해 재시도한 작업에서도 최초 요청을 찾을 수 있습니다. FCM 전송 후 DB 커밋에 실패하면 같은 알림이 다시 전송될 수 있습니다. -현재 코드 기준으로 아래 컨트롤러가 구성되어 있습니다. +[Outbox 설계와 대안](https://github.com/Beach-complex/beach_complex_docs/blob/main/adr/ADR-008-outbox-rdb-task-queue.md) · [전송·재시도 테스트](./src/test/java/com/beachcheck/outbox/service/OutboxEventDispatcherTest.java) · [Trace 연결 테스트](./src/test/java/com/beachcheck/outbox/tracing/OutboxTracingIntegrationTest.java) -- `AuthController` -- `BeachController` -- `BeachConditionController` -- `BeachFacilityController` -- `ReservationController` -- `UserFavoriteController` -- `NotificationController` +### 로그와 Trace 조회 -## 테스트와 협업 +Grafana에서 로그의 `traceId`를 누르면 해당 요청의 Trace를 볼 수 있습니다. Trace에서도 같은 요청의 로그를 조회할 수 있도록 Loki와 Tempo 데이터소스를 연결했습니다. -- 단위 테스트와 통합 테스트를 함께 운영합니다. -- Testcontainers를 사용해 데이터베이스 연동 테스트를 검증합니다. -- GitHub Actions에서 빌드, 테스트, 커버리지 리포트 업로드를 수행합니다. -- 협업 규칙, 기술 결정 기록, 트러블슈팅 문서는 `Beach-complex/docs` 레포에서 관리합니다. -- `.github/ISSUE_TEMPLATE`, `.github/PULL_REQUEST_TEMPLATE`, `.github/pull_request_template.md`로 이슈와 PR 양식을 관리합니다. +로그 수집은 Alloy가 맡습니다. 앱이 표준 출력으로 남긴 로그를 Docker 파일에서 읽어 Loki로 보내므로, 앱에 로그 전송 코드를 넣거나 Docker socket을 열 필요가 없습니다. 로그 파일의 회전 한도를 넘기거나 저장 볼륨에 장애가 생기면 로그가 유실될 수 있습니다. -## 실행 방법 +JWT·FCM 토큰·SQL 바인딩 값은 Trace에 기록하지 않습니다. `traceId`는 로그 본문에만 남기고 Loki 인덱스 라벨에서는 제외했습니다. -### 요구 사항 -- JDK 21 -- Node.js 20+ -- Docker +[Loki 선택 배경](https://github.com/Beach-complex/beach_complex_docs/blob/main/adr/ADR-014-logging-storage-backend.md) · [Alloy 선택 배경](https://github.com/Beach-complex/beach_complex_docs/blob/main/adr/ADR-015-log-collector.md) · [Trace 수집 계약](./docs/TRACING_CONTRACT.md) -### 1. 저장소 클론 +## 문제 해결 -```bash -git clone https://github.com/Beach-complex/Beach_complex.git -cd Beach_complex -``` +### Outbox 트랜잭션 간 잠금 충돌 -### 2. 인프라 실행 +폴링 트랜잭션이 잡은 행 잠금 때문에 전송 트랜잭션의 갱신이 멈추는 문제가 있었습니다. 폴링 루프에 `NOT_SUPPORTED`를 적용하고, 조회 트랜잭션이 끝난 뒤 이벤트별 전송 트랜잭션을 시작하도록 바꿨습니다. 조회 메서드의 read-only 설정과 쓰기 잠금이 충돌하는 문제도 함께 수정했습니다. -```bash -docker-compose up -d postgres redis -``` +통합 테스트에서는 준비 데이터를 먼저 커밋한 뒤 전송과 상태 변경을 확인합니다. 이미 전송한 알림을 건너뛰는 경우도 검사합니다. + +[재현·원인·수정 기록](https://github.com/Beach-complex/beach_complex_docs/blob/main/backend/troubleshooting/outbox-integration-requires-new-lock-readOnly-deadlock.md) · [통합 테스트](./src/test/java/com/beachcheck/outbox/integration/OutboxPublisherIntegrationTest.java) + +### Docker 로그 분할·회전 처리 + +긴 JSON 로그가 여러 Docker 레코드로 나뉘고, Alloy가 멈춘 동안 로그 파일이 회전하면서 일부 기록이 누락되는 문제가 있었습니다. + +Alloy가 컨테이너와 출력 스트림별로 로그 조각을 합치도록 수정했습니다. 파일 지문과 읽기 위치도 저장해 재시작 후 이어 읽습니다. CI에서는 출력이 섞인 40KB JSON, 수집 중지 중 회전한 로그 110건, 파일 경계를 넘은 90KB JSON이 원문대로 복구되는지 확인합니다. + +[CI 검증 결과](./assets/reports/log-pipeline-20261001.json) · [검증 스크립트](./deploy/observability/scripts/verify-log-pipeline.py) · [운영 제약](./deploy/observability/README.md#운영-제약) -### 3. 백엔드 실행 +그 밖의 기록: [Docker Engine과 Testcontainers의 API 버전 불일치 해결](https://github.com/Beach-complex/beach_complex_docs/blob/main/backend/troubleshooting/troubleshooting-testcontainers-docker-api-version-400.md) + +## 품질과 배포 + +`main`·`develop` 대상 PR과 push에서 [CI](./.github/workflows/ci.yml)를 실행합니다. + +| 검증 대상 | 검사 내용 | +| --- | --- | +| 백엔드 | UTF-8, 컴파일·Checkstyle·Spotless, 단위·통합 테스트, 빌드, JaCoCo | +| 관측 환경 | Terraform·cloud-init·Compose 설정, 합성 로그·Trace 전송, 회전·재시작·Loki 장애 복구 | + +DB 쿼리와 API 테스트는 Testcontainers의 PostgreSQL/PostGIS에서 실행합니다. Trace 테스트에서는 [SQL 바인딩 값이 수집되지 않는지](./src/test/java/com/beachcheck/global/tracing/JdbcTracingIntegrationTest.java), [외부 호출까지 Trace가 이어지는지](./src/test/java/com/beachcheck/external/congestion/CongestionClientTracingIntegrationTest.java) 검사합니다. + +커버리지 배지는 2026-10-01 커밋 `af6dceda` 기준이며 자동 갱신되지 않습니다. 라인 커버리지는 89.8% (1,617 / 1,801)입니다. [CI 결과](https://github.com/Beach-complex/Beach_complex/actions/runs/36867196523) · [JaCoCo 보고서](./assets/reports/jacoco-20261001.xml) + +관측 스택은 [Prometheus·Tempo·Grafana의 EC2 실행과 연동](https://github.com/Beach-complex/Beach_complex/pull/238)을 확인했습니다. [Loki·Alloy 로그 파이프라인](https://github.com/Beach-complex/Beach_complex/pull/264)은 로컬 Docker에서 합성 로그와 Trace로 테스트했습니다. + +- **비밀정보 검사:** 설치한 로컬 [Git 훅](./.githooks/pre-commit)이 [Gitleaks](./scripts/secret-scan.sh)로 스테이징된 추가 줄을 검사합니다. +- **배포:** `main`의 백엔드 관련 파일이 바뀌면 이미지를 빌드해 GHCR에 올리고, SSH로 EC2의 Compose 서비스를 갱신합니다. [CD 수동 실행](./.github/workflows/backend-cd.yml)도 지원합니다. + +## 로컬 실행 + +JDK 21, Docker Compose, Git, OpenSSL이 필요합니다. 아래 Bash 명령은 이메일·푸시 발송을 끈 상태로 API를 실행합니다. ```bash -./gradlew bootRun -``` +git clone https://github.com/Beach-complex/Beach_complex.git +cd Beach_complex +docker compose up -d --wait postgres redis -Windows PowerShell에서는 아래 명령을 사용합니다. +export SPRING_PROFILES_ACTIVE=dev +export APP_FIREBASE_ENABLED=false +export APP_OUTBOX_POLLING_ENABLED=false +export APP_MAIL_ENABLED=false +export MANAGEMENT_HEALTH_MAIL_ENABLED=false +export CONGESTION_SIGV4_ENABLED=false +export JWT_SECRET=$(openssl rand -hex 32) -```powershell -.\gradlew.bat bootRun +./gradlew bootRun ``` -### 4. 프론트엔드 실행 +DB 마이그레이션과 초기 데이터 입력은 앱 시작 시 Flyway가 처리합니다. 외부 혼잡도 API를 연결하지 않으면 30분마다 실행되는 갱신 작업에서 연결 실패 로그가 남을 수 있습니다. + +[Swagger UI](http://localhost:8080/swagger-ui.html) · [Health](http://localhost:8081/actuator/health) ```bash -git clone https://github.com/Beach-complex/frontend.git -cd frontend -npm install -npm run dev +# 해운대 좌표 기준 반경 5km 검색 +curl 'http://localhost:8080/api/beaches?lat=35.1587&lon=129.1599&radiusKm=5' + +# Docker 실행 상태에서 테스트·빌드·커버리지 확인 +./gradlew test assemble jacocoTestReport ``` -### 5. 접속 주소 +PowerShell 실행 방법, JWT 키 변경 시 주의사항, Redis 실행 이유와 외부 서비스 설정은 [로컬 개발 가이드](./docs/LOCAL_DEVELOPMENT.md)를 참고하세요. + +## 프로젝트 구조 -- Frontend: `http://localhost:5173` -- Backend API: `http://localhost:8080` -- Swagger UI: `http://localhost:8080/swagger-ui.html` +```text +Beach_complex/ +├── src/main/java/com/beachcheck/ +│ ├── auth/ # JWT, 이메일 인증·발송 +│ ├── beach/ # 검색, 시설·상태, 혼잡도 갱신 스케줄러 +│ ├── external/ # 혼잡도 API, AWS SigV4 +│ ├── global/ # 보안, 캐시, 예외, 로그·Trace 설정 +│ ├── notification/ # 알림 생성·설정 +│ ├── outbox/ # 이벤트 저장·폴링·FCM 전송 +│ ├── reservation/ # 방문 예약 +│ └── user/ # 사용자, 즐겨찾기 +├── src/main/resources/ +│ ├── db/migration/ # Flyway 마이그레이션 +│ └── application*.yml +├── src/test/ # 단위·통합 테스트, 공통 fixture +├── deploy/ +│ └── observability/ # Terraform, Compose, 관측 파이프라인 검증 +├── docs/ # 로컬 개발 가이드, Trace 공통 계약 +├── assets/ # README 구성도·CI 검증 기록 +├── .github/workflows/ # CI, 컨테이너 빌드·배포 +└── docker-compose.yml # 로컬 PostgreSQL/PostGIS, Redis +``` ## 문서 -- 프로젝트 문서 홈: [Beach-complex/docs](https://github.com/Beach-complex/docs) -- 로컬 문서 진입점: [`docs/README.md`](./docs/README.md) -- SSOT 문서: [docs/ssot.md](https://github.com/Beach-complex/docs/blob/main/ssot.md) -- ADR: [docs/adr](https://github.com/Beach-complex/docs/tree/main/adr) -- 트러블슈팅 문서: [docs/troubleshooting/docs-README.md](https://github.com/Beach-complex/docs/blob/main/troubleshooting/docs-README.md) -- 프론트엔드 레포: [Beach-complex/frontend](https://github.com/Beach-complex/frontend) -- 프론트엔드 가이드: [docs/guides/backend-developer-frontend-guide.md](https://github.com/Beach-complex/docs/blob/main/guides/backend-developer-frontend-guide.md) +이 저장소에는 공개용 개발 가이드와 일부 기술 문서를 두고 있습니다. 상세 설계와 내부 협업·운영 기록은 비공개 문서 저장소에서 관리합니다. 본문에 연결된 ADR과 문제 해결 기록도 해당 저장소의 접근 권한이 필요합니다. + +- [로컬 개발 가이드](./docs/LOCAL_DEVELOPMENT.md) +- [문서 저장소 · 비공개](https://github.com/Beach-complex/beach_complex_docs) · [ADR 목록 · 비공개](https://github.com/Beach-complex/beach_complex_docs/tree/main/adr) +- [문제 해결 기록 · 비공개](https://github.com/Beach-complex/beach_complex_docs/tree/main/backend/troubleshooting) +- [관측 환경 가이드](./deploy/observability/README.md) · [Trace 공통 계약](./docs/TRACING_CONTRACT.md) +- [프론트엔드](https://github.com/Beach-complex/beach_complex_client) -## 팀 +## 팀 & 역할 | 역할 | 이름 | GitHub | | --- | --- | --- | | Backend / Infra | 박재홍 | [@PHJ2000](https://github.com/PHJ2000) | -| Backend | 박건우 | [@GunwooPar](https://github.com/GunwooPar) | +| Backend | 박건우 | [@geonusp](https://github.com/geonusp) | | Frontend / PM | 정도경 | [@DoGyeong888](https://github.com/DoGyeong888) | ## 라이선스 -이 프로젝트는 [MIT](./LICENSE) 라이선스를 따릅니다. +[MIT License](./LICENSE) diff --git a/assets/badges/coverage.svg b/assets/badges/coverage.svg new file mode 100644 index 00000000..30b3e43c --- /dev/null +++ b/assets/badges/coverage.svg @@ -0,0 +1 @@ +JaCoCo line coverage 89.8%, af6dceda, 2026-10-01JaCoCo line89.8% diff --git a/assets/images/system-architecture.png b/assets/images/system-architecture.png new file mode 100644 index 00000000..5e219052 Binary files /dev/null and b/assets/images/system-architecture.png differ diff --git a/assets/reports/jacoco-20261001.xml b/assets/reports/jacoco-20261001.xml new file mode 100644 index 00000000..89a304bd --- /dev/null +++ b/assets/reports/jacoco-20261001.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/reports/log-pipeline-20261001.json b/assets/reports/log-pipeline-20261001.json new file mode 100644 index 00000000..f09f6a74 --- /dev/null +++ b/assets/reports/log-pipeline-20261001.json @@ -0,0 +1,25 @@ +{ + "checked_at_utc": "2026-10-01T13:16:28.322906+00:00", + "scope": "isolated local Docker; synthetic app logs and OTLP trace; not EC2 deployment or browser UI", + "checks": [ + "Local Loki, Tempo, Grafana and Docker log emitters ready", + "Real Docker JSON is preserved; unrelated logs excluded; only four low-cardinality labels", + "Grafana datasource health and provisioned log-to-trace / trace-to-log targets resolve", + "Alloy persisted file fingerprints/offsets and resumed logs written during its stop", + "40 KB JSON preserved during live rotation with interleaved stdout/stderr from two containers", + "All 110 retained rotated records and a 90 KB JSON spanning files recovered after Alloy restart", + "Alloy discovers a recreated backend without a hardcoded container ID", + "Alloy retries delivery across a short Loki outage", + "Loki data survives container recreation; compactor retention active at 72h" + ], + "images": { + "loki": "grafana/loki:3.7.8", + "tempo": "grafana/tempo:3.0.2", + "grafana": "grafana/grafana:13.1.3", + "alloy": "grafana/alloy:v1.19.2", + "emitter": "python:3.12-slim", + "emitter2": "python:3.12-slim", + "ignored": "python:3.12-slim" + }, + "production_deployed": false +} diff --git a/docs/LOCAL_DEVELOPMENT.md b/docs/LOCAL_DEVELOPMENT.md new file mode 100644 index 00000000..f90d4fcc --- /dev/null +++ b/docs/LOCAL_DEVELOPMENT.md @@ -0,0 +1,121 @@ +# 로컬 개발 가이드 + +[README로 돌아가기](../README.md) + +## 1. 준비 + +- JDK 21 +- Docker Engine 또는 Docker Desktop과 Docker Compose +- Git + +백엔드는 저장소의 Gradle Wrapper로 실행합니다. Node.js는 프론트엔드를 함께 개발할 때 필요합니다. + +```bash +git clone https://github.com/Beach-complex/Beach_complex.git +cd Beach_complex +docker compose up -d --wait postgres redis +``` + +기본 DB 접속 정보는 `localhost:15432`, 데이터베이스 `beach_complex`, 사용자·비밀번호 `beach`입니다. Redis 포트는 `6379`입니다. API 캐시는 Caffeine을 사용하지만 기본 Actuator Health에는 Redis 점검이 포함되어 있어, Redis를 실행하지 않으면 Health가 `503`을 반환합니다. + +## 2. 외부 인증정보 없이 백엔드 실행 + +기본 설정은 Firebase와 메일 발송을 활성화합니다. 로컬에서 API를 먼저 확인하려면 아래처럼 비활성화해 실행하세요. Bash 예시는 JWT 키 생성에 OpenSSL을 사용합니다. + +```bash +export SPRING_PROFILES_ACTIVE=dev +export APP_FIREBASE_ENABLED=false +export APP_OUTBOX_POLLING_ENABLED=false +export APP_MAIL_ENABLED=false +export MANAGEMENT_HEALTH_MAIL_ENABLED=false +export CONGESTION_SIGV4_ENABLED=false +export JWT_SECRET=$(openssl rand -hex 32) + +./gradlew bootRun +``` + +### Windows PowerShell + +```powershell +$env:SPRING_PROFILES_ACTIVE = "dev" +$env:APP_FIREBASE_ENABLED = "false" +$env:APP_OUTBOX_POLLING_ENABLED = "false" +$env:APP_MAIL_ENABLED = "false" +$env:MANAGEMENT_HEALTH_MAIL_ENABLED = "false" +$env:CONGESTION_SIGV4_ENABLED = "false" +# 로컬 실행용 임시 키 생성 +$env:JWT_SECRET = [guid]::NewGuid().ToString("N") + [guid]::NewGuid().ToString("N") + +.\gradlew.bat bootRun +``` + +앱을 시작하면 Flyway가 마이그레이션과 초기 데이터를 적용합니다. 위 설정에서는 이메일과 푸시를 발송하지 않습니다. + +새 JWT 키로 앱을 다시 시작하면 이전에 발급한 액세스 토큰은 사용할 수 없습니다. + +혼잡도는 30분마다 갱신하며, 외부 API를 연결하지 않으면 갱신 시 연결 실패 로그가 남을 수 있습니다. `CONGESTION_SIGV4_ENABLED=false`는 요청 서명만 끄므로 스케줄러는 계속 실행됩니다. + +## 3. 동작 확인 + +| 용도 | 기본 주소 | +| --- | --- | +| Backend API | | +| Swagger UI | | +| OpenAPI JSON | | +| Health | | + +```bash +# 해수욕장 목록 +curl http://localhost:8080/api/beaches + +# 해운대 좌표 기준 반경 5km 검색 +curl 'http://localhost:8080/api/beaches?lat=35.1587&lon=129.1599&radiusKm=5' +``` + +프론트엔드를 연결할 때 현재 Spring Security의 CORS 허용 출처는 `http://localhost:3000`과 `http://localhost:8080`입니다. 다른 포트를 사용한다면 [SecurityConfig](../src/main/java/com/beachcheck/global/security/SecurityConfig.java)의 설정을 맞춰야 합니다. + +## 4. 외부 서비스 연결 + +| 연동 | 설정 | +| --- | --- | +| PostgreSQL | `SPRING_DATASOURCE_URL`, `SPRING_DATASOURCE_USERNAME`, `SPRING_DATASOURCE_PASSWORD` | +| 이메일 | `APP_MAIL_ENABLED=true`, `SPRING_MAIL_USERNAME`, `SPRING_MAIL_PASSWORD`; 메일 상태 점검 시 `MANAGEMENT_HEALTH_MAIL_ENABLED=true` | +| 이메일 인증 링크 | `APP_EMAIL_VERIFICATION_BASE_URL` — 기본값 `http://localhost:8080/api/auth/verify-email` | +| Firebase FCM | `APP_FIREBASE_ENABLED=true`, `APP_OUTBOX_POLLING_ENABLED=true`, 서비스 계정 설정 | +| Firebase 서비스 계정 | `APP_FIREBASE_CREDENTIALS_JSON_BASE64` 또는 `APP_FIREBASE_CREDENTIALS_PATH` | +| 혼잡도 API | `CONGESTION_BASE_URL`; AWS 연동 시 `CONGESTION_SIGV4_ENABLED=true`, `AWS_REGION`, 실행 환경의 AWS 자격증명 또는 `AWS_PROFILE` | +| Trace 전송 | `MANAGEMENT_OTLP_TRACING_ENDPOINT` — 예: `http://localhost:4318/v1/traces` | +| Trace 샘플링 | `MANAGEMENT_TRACING_SAMPLING_PROBABILITY` — 기본값 `0.1` | + +Trace endpoint를 지정하지 않으면 수집기 없이 실행할 수 있습니다. 외부 연동 값은 실행 환경에 주입하며, 로컬 예제의 JWT 키는 배포 환경에서 별도 비밀값으로 교체합니다. 전체 기본값은 [application.yml](../src/main/resources/application.yml), 배포 변수 예시는 [backend-ec2.env.example](../deploy/backend-ec2.env.example)에 있습니다. + +## 5. 테스트와 Git 훅 + +통합 테스트에는 실행 중인 Docker 데몬이 필요합니다. + +```bash +./gradlew compileJava checkstyleMain spotlessCheck +./gradlew test assemble jacocoTestReport + +# 선택: 로컬 Git 훅 설치 +./gradlew installGitHooks +``` + +테스트 결과는 `build/reports/tests/test/index.html`, 커버리지는 `build/reports/jacoco/test/html/index.html`에 생성됩니다. 관측 테스트 도구와 실행 조건은 [검증 가이드](../deploy/observability/README.md#로컬-검증)를 참고하세요. + +## 6. API 참고 + +실행 후 [Swagger UI](http://localhost:8080/swagger-ui.html)에서 요청·응답 스키마를 확인할 수 있습니다. 인증이 필요한 API에는 `Authorization: Bearer ` 헤더를 보냅니다. 반경 검색은 `lat`, `lon`, `radiusKm`을 모두 전달해야 합니다. + +| 영역 | 대표 경로 | 인증 | +| --- | --- | --- | +| 회원가입·로그인·토큰 갱신 | `POST /api/auth/signup`, `POST /api/auth/login`, `POST /api/auth/refresh` | 불필요 | +| 내 정보·로그아웃 | `GET /api/auth/me`, `POST /api/auth/logout` | JWT | +| 이메일 인증 | `GET /api/auth/verify-email`, `POST /api/auth/verify-email/confirm` | 인증 링크의 토큰 | +| 해수욕장 검색 | `GET /api/beaches` | 불필요 | +| 시설 조회 | `GET /api/beaches/{beachId}/facilities` | 불필요 | +| 최근 상태 조회 | `GET /api/beaches/{beachId}/conditions/recent` | 불필요 | +| 즐겨찾기 | `GET /api/favorites`, `POST /api/favorites/{beachId}` | JWT | +| 예약 생성·목록 | `POST /api/beaches/{beachId}/reservations`, `GET /api/beaches/reservations` | JWT | +| 알림 설정 저장 | `POST /api/notifications/fcm-token`, `PUT /api/notifications/settings` | JWT | +| 테스트 푸시 발송 | `POST /api/notifications/test` | JWT |