μμ± κ°κ² μΆμ²μ μ§μνλ λ°°λ¬ μλΉμ€ νλ«νΌμ λλ€. μ κ΅ μ§μμ λμμΌλ‘ νλ©°, νμ¬λ κ΄νλ¬Έ λ΄ μ§μμμλ§ μλΉμ€λ₯Ό μ§μν©λλ€. Spring Boot λ°±μλ νλ‘μ νΈμ λλ€.
- νμ μν λΆλ΄
- μλΉμ€ κ΅¬μ± λ° μ€ν λ°©λ²
- νλ‘μ νΈ λͺ©μ
- ERD
- κΈ°μ μ€ν
- μν€ν μ²
- κΈ°μ μ μμ¬κ²°μ
- API μλν¬μΈνΈ
- ν μ€νΈ
- κΈ°μ μ€ν μ μ λ°°κ²½
| νμ | λ΄λΉ λλ©μΈ |
|---|---|
| μ νμ | κ°κ², κ°κ² μΉ΄ν κ³ λ¦¬, μ§λ API λμ |
| μ΄νλΉ | λ‘κ·ΈμΈ λ° JWT μΈμ¦, νμ, νμ μ§μ, κ΄λ¦¬μ |
| μ μ¬λΉ | μ£Όλ¬Έ, μ₯λ°κ΅¬λ, μλ² κ΅¬μΆ λ° λ°°ν¬ |
| νμ§ν | λ©λ΄, νκ·Έ, κ°κ² μ΄μμκ°, μ΄μ νμ© μ§μ |
| νλ³λ | κ²°μ , 리뷰, μμ± μΆμ² AI |
νΌμ³λ³΄κΈ°
docker --version # Docker 24+ κΆμ₯
docker compose version # Docker Compose v2+cp .env.example .env
# .env νμΌμ μ΄μ΄ νμν κ°μ μ€μ νμΈμ.env μ£Όμ νλͺ©:
# PostgreSQL
POSTGRES_DB=baedalsodae
POSTGRES_USER=baedal_user
POSTGRES_PASSWORD=your_password
# Redis
SPRING_DATA_REDIS_PASSWORD=your_redis_password
# JWT
JWT_SECRET=your_jwt_secret_key_at_least_32_chars
JWT_EXPIRATION=3600000
JWT_REFRESH_EXPIRATION=604800000# μ 체 μλΉμ€ λΉλ ν μ€ν
docker compose up --build
# λ°±κ·ΈλΌμ΄λ μ€ν
docker compose up -d
# λ‘κ·Έ νμΈ
docker compose logs -f backend
docker compose logs -f db
docker compose logs -f redis| μλΉμ€ | URL |
|---|---|
| λ°±μλ API | http://localhost:8080/api/v1 |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
# μ€μ§
docker compose down
# μ€μ§ + λ³Όλ₯¨ μμ (DB μ΄κΈ°ν)
docker compose down -v- Java 17+
- PostgreSQL 16
- Redis 7
# 1. νκ²½ λ³μ μ€μ
cp .env.example .env
# 2. Dockerλ‘ PostgreSQL Β· Redis μ€ν
docker-compose up -d
# 3. μ ν리μΌμ΄μ
μ€ν (IntelliJ νκ²½ λ³μ μ€μ ν μ€ν λλ μλ λͺ
λ Ήμ΄)
export POSTGRES_DB=baedalsodae
export POSTGRES_DB_URL=jdbc:postgresql://localhost:5432/baedalsodae
export POSTGRES_USER=baedal_user
export POSTGRES_PASSWORD=your_password
export SPRING_DATA_REDIS_HOST=localhost
export JWT_SECRET=your_jwt_secret
./gradlew bootRun| κ΅¬λΆ | κΈ°μ |
|---|---|
| Backend | Spring Boot 3.5 Β· Java 17 Β· Spring Data JPA Β· Spring Security |
| Database | PostgreSQL 16 |
| Cache / Session | Redis 7 |
| Query | QueryDSL 5.0 (컀μ κΈ°λ° νμ΄μ§λ€μ΄μ ) |
| AI | Spring AI (OpenAI Β· pgvector) |
| Infrastructure | Docker Β· Docker Compose |
| Logging | Logback Β· logstash-logback-encoder (JSON) |
| API λ¬Έμ | Spring REST Docs |
| Testing | JUnit 5 Β· Mockito |
| Build Tool | Gradle |
BaedalSodaeλ μμ± μ£Όλ¬Έμ ν΅μ¬ κΈ°λ₯μΌλ‘ νλ λ°°λ¬ μλΉμ€ νλ«νΌμ λλ€.
- μμ± μ£Όλ¬Έ: Spring AI(OpenAI)λ₯Ό νμ©ν΄ μ¬μ©μμ μμ± μ λ ₯μ λΆμνκ³ μ ν©ν λ©λ΄λ₯Ό μΆμ²
- μλΉμ€ μ§μ: μ κ΅ μ/λ Β· μ/κ΅°/ꡬ κΈ°λ°μΌλ‘ μ€κ³λμμΌλ, νμ¬λ κ΄νλ¬Έ λ΄ μ§μμμλ§ μλΉμ€ μ΄μ μ€
AWS VPC λ΄ Public/Private Subnet λΆλ¦¬ ꡬμ±. HAProxy(80 port)κ° νΈλν½μ 2κ°μ Spring Boot μΈμ€ν΄μ€(8080)λ‘ λΆμ°νλ©°, PostgreSQL RDSμ ElastiCache Redisλ Private Subnetμ 격리λ©λλ€.
νΌμ³λ³΄κΈ°
baedalsodae/
βββ src/
β βββ main/
β β βββ java/com/project/baedalsodae/
β β β βββ allowedRegion/ # νμ© μ§μ κ΄λ¦¬
β β β βββ auth/ # μΈμ¦/μΈκ° (JWT)
β β β βββ cart/ # μ₯λ°κ΅¬λ
β β β βββ event/ # λλ©μΈ μ΄λ²€νΈ λ°ν/ꡬλ
β β β βββ global/ # κ³΅ν΅ μλ΅, μμΈ, μ νΈ
β β β βββ menu/ # λ©λ΄ μΉ΄ν
κ³ λ¦¬ Β· λ©λ΄ μμ΄ν
β β β βββ order/ # μ£Όλ¬Έ
β β β βββ payment/ # κ²°μ
β β β βββ recommendation/ # AI μΆμ² (Spring AI)
β β β βββ review/ # 리뷰
β β β βββ store/ # κ°κ² Β· κ°κ² μΉ΄ν
κ³ λ¦¬ Β· μ΄μμκ°
β β β βββ tag/ # νκ·Έ
β β β βββ user/ # μ¬μ©μ Β· μ£Όμ
β β βββ resources/
β β βββ application.yml
β β βββ application-local.yml
β β βββ application-dev.yml
β β βββ application-prod.yml
β β βββ logback-spring.xml
β βββ test/
β β βββ java/com/project/baedalsodae/
β β βββ {domain}/
β β β βββ controller/ # @WebMvcTest + Spring REST Docs
β β β βββ service/ # Mockito λ¨μ ν
μ€νΈ
β β β βββ fixture/ # ν
μ€νΈ ν½μ€μ²
β β βββ event/ # EventTest Β· EventPollerTest
β β βββ payment/ # PaymentListenerTest Β· PaymentEventPublisherTest
β β βββ cart/ # CartQueryDslSmokeTest
β β βββ global/config/ # TestSecurityConfig
β βββ docs/asciidoc/ # REST Docs adoc μμ€
β βββ allowed-region.adoc
β βββ auth.adoc
β βββ admin-store.adoc
β βββ admin-user.adoc
β βββ menu-category.adoc
β βββ menu-item.adoc
β βββ store.adoc
β βββ store-hours.adoc
β βββ store-category.adoc
β βββ user.adoc
β βββ user-address.adoc
βββ docs/
β βββ API.md # μ 체 API μλν¬μΈνΈ λͺ©λ‘
βββ docker-compose.yml
βββ Dockerfile
βββ build.gradle
erDiagram
p_user {
UUID id PK
string username UK
string email UK
string nickname UK
string phone
string password
string name
string role
UUID user_main_address_id
}
p_user_address {
UUID id PK
UUID user_id FK
string sido_code
string sido_name
string sigungu_code
string sigungu_name
string dong_code
string dong_name
string road_address
string detail_address
string description
}
p_store_category {
UUID id PK
string name
string description
}
p_store {
UUID id PK
UUID user_id
UUID store_category_id FK
string name
string business_number UK
string phone
string sido_code
string sigungu_code
string road_address
string detail_address
string description
double avg_rating
int review_count
string store_status
}
p_store_hours {
UUID id PK
UUID store_id FK
string day_of_week
time open_time
time close_time
time break_start
time break_end
boolean is_open
}
p_menu_category {
UUID id PK
UUID store_id FK
string name
int order_no
}
p_menu_item {
UUID id PK
UUID menu_category_id FK
string name
decimal price
string description
boolean is_popular
int order_no
string menu_status
}
p_tag {
UUID id PK
string name UK
}
p_tag_mapping {
UUID id PK
UUID tag_id FK
UUID menu_item_id FK
int order_no
}
p_cart {
UUID id PK
UUID user_id UK
UUID store_id FK
}
p_cart_item {
UUID id PK
UUID cart_id FK
UUID menu_item_id FK
int quantity
}
p_order {
UUID id PK
string order_no UK
UUID user_id
string user_nickname_snapshot
string user_phone_snapshot
UUID store_id
string store_name_snapshot
UUID address_id
string delivery_address_snapshot
string status
string store_request_note
string delivery_request_note
decimal total_amount
decimal delivery_fee
decimal discount_amount
decimal final_amount
}
p_order_item {
UUID id PK
UUID order_id FK
UUID menu_item_id
string name_snapshot
decimal price_snapshot
int quantity
}
p_order_status_history {
UUID id PK
UUID order_id
string from_status
string to_status
string actor_type
UUID actor_id
string reason
}
p_payment {
UUID id PK
UUID order_id
UUID user_id
decimal amount
string payment_method
string status
datetime paid_at
string pg_transaction_id
}
p_review {
UUID id PK
UUID order_id
UUID user_id
int rating
string content
boolean is_hidden
}
p_allowed_region {
UUID id PK
string sido_code
string sido_name
string sigungu_code UK
string sigungu_name
boolean is_active
}
p_user ||--o{ p_user_address : "has"
p_user ||--o| p_cart : "has"
p_store_category ||--o{ p_store : "categorizes"
p_store ||--o{ p_store_hours : "has"
p_store ||--o{ p_menu_category : "has"
p_store ||--o{ p_cart : "in"
p_menu_category ||--o{ p_menu_item : "has"
p_menu_item ||--o{ p_tag_mapping : "has"
p_menu_item ||--o{ p_cart_item : "in"
p_tag ||--o{ p_tag_mapping : "mapped"
p_cart ||--o{ p_cart_item : "has"
p_order ||--o{ p_order_item : "has"
p_order ||--o{ p_order_status_history : "tracks"
p_order ||--o| p_payment : "has"
p_order ||--o| p_review : "has"
μμΈ λ΄μ©: docs/decisions.md
| μ£Όμ | μμ½ |
|---|---|
| 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ | offset λ°©μμ λ°μ΄ν° λλ½ λ¬Έμ λ₯Ό ν΄κ²°νκΈ° μν΄ cursor λ°©μ λμ . QueryDSLλ‘ λ€μ€ μ λ ¬ μ λ΅ κ΅¬ν |
| RESTful API μ€κ³ | μμ΄μ΄νλ μ κΈ°λ° ν μΌλΌμΈ ν URI λͺ λͺ , HTTP λ©μλ, μν μ½λ 컨벀μ ν μ 체 ν©μ |
| μμ΄μ΄νλ μ κΈ°λ° ν μΌλΌμΈ | κ°λ° μ κΈ°λ₯ λ²μΒ·νλ©΄ νλ¦μ μκ°ννμ¬ μꡬμ¬ν μ€ν΄ λ°©μ§ λ° API μ€κ³ κΈ°μ€ κ³΅μ |
| 컀μ κΈ°λ° μ λ ¬ 보μ₯ | λ³΅ν© μ λ ¬ κΈ°μ€μμ λμΌ κ° λ°μ μ id νμ΄λΈλ μ΄νΉ + crossBoundary OR 쑰건μΌλ‘ νμ΄μ§ κ²½κ³ μ νμ± λ³΄μ₯ |
| refreshToken + Redis λΈλ리μ€νΈ | 무μνμ± μ μ§λ₯Ό μν΄ accessToken/refreshToken λΆλ¦¬, λ‘κ·Έμμλ ν ν°μ Redis TTLλ‘ λΈλ리μ€νΈ κ΄λ¦¬ |
| μμλ°μ€ ν¨ν΄ + μ΄λ²€νΈ ν΄λ§ | νΈλμμ
μμ μ±μ μν΄ @TransactionalEventListener λμ μμλ°μ€ ν¨ν΄ + μ€μΌμ€λ¬ ν΄λ§ μ ν |
| λΉκ΄μ λ½ λμ | λ©λ΄ μμ λ³κ²½ μ λ€μ€ row λμ μμ β λκ΄μ λ½ μ¬μλ 볡μ‘λ λ¬Έμ λ‘ λΉκ΄μ λ½ μ ν |
| WireMock κΈ°λ° κ°μ PGμ¬ κ΅¬ν | μ€μ PGμ¬ κ³μ½ μμ΄ WireMockμ Docker Composeλ‘ λμ κ²°μ μΉμΈΒ·μ·¨μ μ νλ¦μ μ€μ μ λμΌνκ² κ²μ¦ |
| pgvector + STT κΈ°λ° λ©λ΄ μΆμ² | μμ± μ λ ₯(STT) β ν μ€νΈ μλ² λ© β pgvector μ μ¬λ κ²μμΌλ‘ μμ°μ΄ κΈ°λ° λ©λ΄ μΆμ² ꡬν |
κΈ°λ³Έ κ²½λ‘:
/api/v1Β· μ 체 λͺ©λ‘: docs/API.md Β· REST Docs: GitHub Pages
| κ·Έλ£Ή | μ£Όμ URL ν¨ν΄ | μ€λͺ |
|---|---|---|
| Auth | /auth/** |
νμκ°μ , λ‘κ·ΈμΈ, ν ν° |
| Users | /users/me |
νλ‘ν μ‘°ν/μμ |
| User Address | /user-addresses/** |
λ°°λ¬ μ£Όμ κ΄λ¦¬ |
| Store Categories | /store-categories/** |
κ°κ² μΉ΄ν κ³ λ¦¬ κ΄λ¦¬ |
| Stores | /stores/** |
κ°κ² μ‘°ν/κ΄λ¦¬ |
| Store Hours | /stores/{id}/hours |
μ΄μμκ° κ΄λ¦¬ |
| Menu | /menu-categories/**, /menu-items/** |
λ©λ΄ κ΄λ¦¬ |
| Cart | /carts/** |
μ₯λ°κ΅¬λ |
| Orders | /orders/** |
μ£Όλ¬Έ μμ±/μν κ΄λ¦¬ |
| Reviews | /reviews/** |
리뷰 μμ±/μ‘°ν |
| Payments | /payments/** |
κ²°μ μ‘°ν |
| Allowed Regions | /allowed-regions/** |
νμ© μ§μ κ΄λ¦¬ |
| Tags | /tags |
νκ·Έ μ‘°ν |
| Recommendations | /recommendations/** |
AI μμ± μΆμ² |
| Admin | /admins/** |
κ΄λ¦¬μ μ μ© |
# μ 체 ν
μ€νΈ μ€ν
./gradlew test
# νΉμ ν΄λμ€λ§ μ€ν
./gradlew test --tests "com.project.baedalsodae.order.service.OrderServiceTest"
# ν
μ€νΈ 리ν¬νΈ νμΈ
open build/reports/tests/test/index.htmlνΌμ³λ³΄κΈ°
http/ β IntelliJ HTTP Client μλ ν
μ€νΈ
βββ cart.http
βββ menu.http
βββ order.http
βββ store.http
βββ store-hours.http
βββ (κΈ°ν λλ©μΈλ³ .http νμΌ)
src/test/
βββ java/com/project/baedalsodae/
βββ allowedRegion/
β βββ controller/ AllowedRegionControllerTest β REST Docs
β βββ service/ AllowedRegionServiceImplTest
β βββ fixture/ AllowedRegionFixture
βββ auth/
β βββ controller/ AuthControllerTest β REST Docs
β βββ service/ AuthServiceTest
β βββ fixture/ AuthFixture
βββ cart/
β βββ controller/ CartControllerTest β REST Docs
β βββ repository/ CartQueryDslSmokeTest β QueryDSL ν΅ν©
β βββ service/ CartServiceTest
βββ event/
β βββ entity/ EventTest
β βββ poller/ EventPollerTest
βββ menu/
β βββ controller/ MenuCategoryControllerTest β REST Docs
β β MenuItemControllerTest β REST Docs
β βββ service/ MenuCategoryServiceImplTest
β β MenuItemServiceImplTest
β βββ fixture/ MenuCategoryMockFixture Β· MenuItemMockFixture Β· ...
βββ order/
β βββ controller/ OrderControllerTest β REST Docs
β β AdminOrderControllerTest β REST Docs
β βββ service/ OrderServiceTest
βββ payment/
β βββ listener/ PaymentListenerTest
β βββ publisher/ PaymentEventPublisherTest
βββ review/
β βββ controller/ ReviewControllerTest β REST Docs
β βββ fixture/ ReviewTestConstants
βββ store/
β βββ controller/ StoreControllerTest β REST Docs
β β AdminStoreControllerTest β REST Docs
β β StoreCategoryControllerTest β REST Docs
β β StoreHourControllerTest β REST Docs
β βββ service/ StoreCommandServiceTest Β· StoreQueryServiceTest
β β StoreCategoryServiceTest Β· StoreHoursServiceImplTest
β βββ dto/ CreateStoreCategoryRequestTest
β βββ fixture/ StoreHoursMockFixture Β· StoreHoursRequestFixture Β· ...
βββ tag/
β βββ service/ TagServiceImplTest Β· TagMappingServiceImplTest
βββ user/
β βββ controller/ UserControllerTest β REST Docs
β β AdminUserControllerTest β REST Docs
β β UserAddressControllerTest β REST Docs
β βββ service/ UserServiceTest Β· UserAddressServiceTest
β βββ fixture/ UserFixture
βββ global/config/ TestSecurityConfig
| λ μ΄μ΄ | μ λ΅ | λꡬ |
|---|---|---|
| Service | Mockitoλ‘ Repository λͺ©νΉ, λΉμ¦λμ€ λ‘μ§ κ²μ¦ | JUnit 5 + Mockito |
| Controller | @WebMvcTest + MockMvcλ‘ HTTP μ¬λΌμ΄μ€ ν
μ€νΈ + API λ¬Έμ μλ μμ± |
JUnit 5 + MockMvc + Spring REST Docs |
| μλ E2E | IntelliJ HTTP Clientλ‘ μ€μ μλ² λμ API νλ¦ κ²μ¦ | .http νμΌ |
Postman κ°μ λ³λ ν΄ μμ΄ IDE μμμ λ°λ‘ APIλ₯Ό νΈμΆνκ³ , .http νμΌμ GitμΌλ‘ ν μ μ²΄κ° κ³΅μ ν μ μμ΄ λμ
νμ΅λλ€.
- IDE ν΅ν©: λ³λ ν΄ μ€μΉ μμ΄ IntelliJμμ λ°λ‘ μ€ν
- νμκ΄λ¦¬:
.httpνμΌμ Gitμ 컀λ°νμ¬ νμ λͺ¨λ λμΌν μμ² κ³΅μ - μ°μ νΈμΆ: JavaScriptλ‘ μ΄μ μλ΅κ°μ λ€μ μμ²μ νμ© β λ‘κ·ΈμΈ β μ£Όλ¬Έ μμ± κ°μ 2~3λ¨κ³ νλ¦μ ν λ²μ μ€ν κ°λ₯
- νκ²½ λΆλ¦¬:
http-client.env.jsonμΌλ‘ dev/prodλ³ URLΒ·ν ν° λ± νκ²½λ³μ λΆλ¦¬ κ΄λ¦¬
- μμ°μ±: Spring Boot Auto-configurationμΌλ‘ λΉ λ₯Έ κ°λ°
- μμ μ±: λκ·λͺ¨ μλΉμ€μμ κ²μ¦λ νλ μμν¬
- μνκ³: Spring Security, Spring Data JPA λ± νλΆν λΌμ΄λΈλ¬λ¦¬
- Java 17: Record, Sealed Class λ± μ΅μ λ¬Έλ² νμ©, LTS λ²μ μΌλ‘ μμ μ± ν보
- μ λ’°μ±: ACID μμ μ§μ, 볡μ‘ν νΈλμμ (μ£Όλ¬Έ μ²λ¦¬) μμ νκ² μ²λ¦¬
- JSON μ§μ: λ°μ ν λ°μ΄ν°λ₯Ό JSONBλ‘ μ μ₯ κ°λ₯
- μ±λ₯: κ³ κΈ μΈλ±μ± (λΆλΆ μΈλ±μ€, λ³΅ν© μΈλ±μ€)μΌλ‘ λμ©λ 리뷰 쿼리 μ΅μ ν
- μΈμ κ΄λ¦¬: JWT Refresh Token μ€μ μ μ₯ λ° λΈλ리μ€νΈ κ΄λ¦¬
- μΊμ±: μμ£Ό μ‘°νλλ κ°κ² λͺ©λ‘, λ©λ΄ μ 보 μΊμ±μΌλ‘ DB λΆν κ°μ
- TTL μ§μ: ν ν° λ§λ£λ₯Ό μλμΌλ‘ μ²λ¦¬
- νμ μμ : μ»΄νμΌ νμμ 쿼리 μ€λ₯ κ°μ§
- λμ 쿼리: 컀μ κΈ°λ° νμ΄μ§λ€μ΄μ , λ€μ€ μ λ ¬ μ λ΅μ λ©μλ λ¨μλ‘ λΆλ¦¬ν΄μ κ΄λ¦¬
- AI μΆμ²: OpenAI μλ² λ© κΈ°λ° λ©λ΄ μ μ¬λ κ²μ
- λ²‘ν° μ μ₯: pgvectorλ‘ PostgreSQLμ μλ² λ© λ²‘ν° μ μ₯ λ° κ²μ
- νκ²½ μΌκ΄μ±: κ°λ°/μ€ν μ΄μ§/νλ‘λμ νκ²½ λμΌνκ² μ μ§
- 격리μ±: κ° μλΉμ€(λ°±μλ, DB, Redis)κ° λ 립λ 컨ν μ΄λμμ μ€ν
- κ°νΈν μ€ν:
docker compose upν μ€λ‘ μ 체 μ€ν κΈ°λ
- JUnit 5:
@ExtendWith,@DisplayName, νλΌλ―Έν°ν ν μ€νΈ λ± κ°λ ₯ν ν μ€νΈ ννλ ₯ - Mockito: μΈλΆ μμ‘΄μ±μ λͺ©νΉνμ¬ μμ λΉμ¦λμ€ λ‘μ§λ§ λ¨μ ν μ€νΈ
- Spring REST Docs: ν μ€νΈ μ½λλ‘ API λ¬Έμλ₯Ό μλ μμ±, μ½λμ λ¬Έμμ μΌμΉ 보μ₯
μ΄ νλ‘μ νΈλ μΌκ΄λ Java μ½λ μ€νμΌμ μ μ§νκΈ° μν΄ Spotless(Google Java Format) λ₯Ό μ¬μ©ν©λλ€.
main, dev λΈλμΉλ‘ PRμ μ¬λ¦΄ λ GitHub Actions CIλ₯Ό ν΅ν΄ μλμΌλ‘ μ½λ ν¬λ§·μ κ²μ¬νλ©°, ν¬λ§·μ΄ μ΄κΈλ μ½λμ λ¨Έμ§λ₯Ό μ°¨λ¨ν©λλ€.
# ν¬λ§· μλ μ λ ¬ μ μ© (μ»€λ° μ νμ)
./gradlew spotlessApply
# ν¬λ§· μ€μ μ¬λΆ κ²μ¬ (CI κ²μ¦ λͺ
λ Ήμ΄)
./gradlew spotlessCheck.envνμΌμ μ λ Gitμ 컀λ°νμ§ λ§μΈμ (.gitignoreμ ν¬ν¨λ¨)- JWT Secret Keyλ μ΅μ 32μ μ΄μμΌλ‘ μ€μ νμΈμ
- νλ‘λμ
λ°°ν¬ μ
SPRING_PROFILES_ACTIVE=prodμ€μ νμ
This project is for educational purposes.
