Spring Boot 기반의 이커머스 플랫폼으로, 단순 CRUD 구현에서 시작해 고가용성 백엔드 시스템으로 진화하는 과정을 담은 프로젝트입니다.
"비즈니스 로직의 이해와 데이터 접근 기초 확립"
- 핵심 기술: Spring Boot 3.x, MyBatis, MySQL
- 학습 성과: SQL Mapper를 통한 DB 핸들링, 기초 CRUD API 설계 학습
- 기록: 기본적인 도메인 기능(회원, 상품, 주문, 환불)을 완성했습니다.
"객체 지향적 설계와 고도화된 기술 도입"
-
핵심 기술: Spring Data JPA, QueryDSL, Redis, Docker
-
전환 배경: MyBatis의 유지보수 생산성 한계를 극복하기 위해 JPA를 도입하고, 확장 가능한 아키텍처로 재설계했습니다.
실무 수준의 이커머스 플랫폼을 구축하는 프로젝트입니다. 회원 관리, 상품 관리, 주문 처리 등 온라인 쇼핑몰의 핵심 기능을 구현하며, 추후 Redis, Kafka, ElasticSearch 등을 활용한 고급 기능을 추가할 예정입니다.
- ✅ RESTful API 설계 및 구현
- ✅ 체계적인 예외 처리 및 Validation
- 🔜 Spring Security + JWT 인증/인가
- 🔜 Redis 기반 장바구니 및 캐싱
- 🔜 Kafka 이벤트 기반 아키텍처
- 🔜 토스페이먼츠 결제 연동
- Language: Java 21
- Framework: Spring Boot 3.5.6
- Persistence: MyBatis 3.x (SQL Mapper)
- Validation: Jakarta Bean Validation
- Build Tool: Gradle
- Development: MySQL 8.0
- Test: H2 (In-Memory)
- Cache: Redis
- Message Queue: Apache Kafka
- Payment: 토스페이먼츠 API
- Container: Docker, Docker Compose
- 회원가입 (이메일 중복 체크)
- 로그인 (이메일/비밀번호 검증)
- 회원 조회 (ID, 이메일)
- 전체 회원 목록 조회
- 상품 등록
- 상품 목록 조회 (페이징, 정렬)
- 상품 상세 조회
- 상품 수정
- 상품 삭제
- 재고 관리 (자동 증감)
- 카테고리 생성 (계층 구조 지원)
- 카테고리 상세 조회
- 전체 카테고리 조회 (페이징)
- 최상위 카테고리 조회
- 부모별 자식 카테고리 조회
- 카테고리 수정
- 카테고리 삭제 (자식 존재 시 제한)
- 주문 생성 (재고 자동 차감)
- 주문 목록 조회 (페이징, 정렬)
- 주문 상세 조회
- 주문 취소 (재고 자동 복구)
- 환불 요청 (주문 검증)
- 환불 목록 조회 (페이징, 전체/사용자별/주문별)
- 환불 상세 조회
- 환불 승인 (재고 복구)
- 환불 거절 (사유 입력)
- 체계적인 예외 처리 (CustomException, ErrorCode)
- 입력값 검증 (Bean Validation)
- 빌더 패턴 적용 (모든 엔티티)
- 통합 테스트 (Repository, Service, Controller)
Base URL:
http://localhost:8080Method Endpoint Description Request Body Response Error POST /api/users/signup회원가입 email,password(min 8),name201 Created 400 (중복 이메일) POST /api/users/login로그인 email,password200 OK 401 (인증 실패) GET /api/users/{id}ID로 회원 조회 - 200 OK 404 (회원 없음) GET /api/users/email/{email}이메일로 회원 조회 - 200 OK 404 (회원 없음) GET /api/users전체 회원 조회 - 200 OK - Request/Response 예시
회원가입 (POST /api/users/signup)
// Request { "email": "user@example.com", "password": "password123", "name": "홍길동" } // Response (201 Created) { "id": 1, "email": "user@example.com", "name": "홍길동" }
로그인 (POST /api/users/login)
// Request { "email": "user@example.com", "password": "password123" } // Response (200 OK) { "id": 1, "email": "user@example.com", "name": "홍길동" }
| Method | Endpoint | Description | Request Body | Response | Error |
|---|---|---|---|---|---|
| POST | /api/products |
상품 등록 | name(1-100), description(max 500), price(0-1억), stock(≥0), categoryId(필수) |
201 Created | 400 (Validation), 404 (카테고리 없음) |
| GET | /api/products |
전체 상품 목록 | - | 200 OK | - |
| GET | /api/products/{id} |
상품 상세 조회 | - | 200 OK | 404 (상품 없음) |
| PUT | /api/products/{id} |
상품 수정 | name, description, price, stock, categoryId |
200 OK | 404 (상품/카테고리 없음) |
| DELETE | /api/products/{id} |
상품 삭제 | - | 204 No Content | 404 (상품 없음) |
Request/Response 예시
상품 등록 (POST /api/products)
// Request
{
"name": "무선 이어폰",
"description": "고음질 블루투스 이어폰",
"price": 89000,
"stock": 100,
"categoryId": 1
}
// Response (201 Created)
{
"id": 1,
"name": "무선 이어폰",
"description": "고음질 블루투스 이어폰",
"price": 89000,
"stock": 100,
"status": "AVAILABLE",
"categoryId": 1,
"categoryName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}전체 상품 목록 조회 (GET /api/products)
// Response (200 OK)
[
{
"id": 1,
"name": "무선 이어폰",
"description": "고음질 블루투스 이어폰",
"price": 89000,
"stock": 100,
"status": "AVAILABLE",
"categoryId": 1,
"categoryName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}
]상품 상세 조회 (GET /api/products/{id})
// Response (200 OK)
{
"id": 1,
"name": "무선 이어폰",
"description": "고음질 블루투스 이어폰",
"price": 89000,
"stock": 100,
"status": "AVAILABLE",
"categoryId": 1,
"categoryName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}| Method | Endpoint | Description | Request Body | Response | Error |
|---|---|---|---|---|---|
| POST | /api/categories |
카테고리 생성 | name(필수), description, parentId(optional) |
201 Created | 400 (중복 이름) |
| GET | /api/categories/{id} |
카테고리 상세 조회 | - | 200 OK | 404 (카테고리 없음) |
| GET | /api/categories |
전체 카테고리 조회 (페이징) | Query: page, size, sort |
200 OK | - |
| GET | /api/categories/parent/{parentId} |
부모별 자식 카테고리 조회 | - | 200 OK | - |
| PUT | /api/categories/{id} |
카테고리 수정 | name, description, parentId |
200 OK | 404 (카테고리 없음) |
| DELETE | /api/categories/{id} |
카테고리 삭제 | - | 204 No Content | 400 (자식 있음), 404 (카테고리 없음) |
Request/Response 예시
카테고리 생성 (POST /api/categories)
// Request - 최상위 카테고리
{
"name": "전자제품",
"description": "전자제품 카테고리"
}
// Request - 하위 카테고리
{
"name": "노트북",
"description": "노트북 카테고리",
"parentId": 1
}
// Response (201 Created)
{
"id": 2,
"name": "노트북",
"description": "노트북 카테고리",
"parentId": 1,
"parentName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}전체 카테고리 조회 (GET /api/categories?page=0&size=10)
// Response (200 OK)
{
"content": [
{
"id": 1,
"name": "전자제품",
"description": "전자제품 카테고리",
"parentId": null,
"parentName": null,
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
},
{
"id": 2,
"name": "노트북",
"description": "노트북 카테고리",
"parentId": 1,
"parentName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"sorted": true,
"unsorted": false
}
},
"totalElements": 2,
"totalPages": 1,
"last": true,
"first": true
}최상위 카테고리 조회 (GET /api/categories/root)
// Response (200 OK)
[
{
"id": 1,
"name": "전자제품",
"description": "전자제품 카테고리",
"parentId": null,
"parentName": null,
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
},
{
"id": 3,
"name": "의류",
"description": "의류 카테고리",
"parentId": null,
"parentName": null,
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}
]부모별 자식 카테고리 조회 (GET /api/categories/parent/1)
// Response (200 OK)
[
{
"id": 2,
"name": "노트북",
"description": "노트북 카테고리",
"parentId": 1,
"parentName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
},
{
"id": 4,
"name": "스마트폰",
"description": "스마트폰 카테고리",
"parentId": 1,
"parentName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}
]카테고리 수정 (PUT /api/categories/2)
// Request
{
"name": "노트북(수정)",
"description": "수정된 설명",
"parentId": 1
}
// Response (200 OK)
{
"id": 2,
"name": "노트북(수정)",
"description": "수정된 설명",
"parentId": 1,
"parentName": "전자제품",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:01:00"
}| Method | Endpoint | Description | Request Body | Response | Error |
|---|---|---|---|---|---|
| POST | /api/orders |
주문 생성 (재고 차감) | userId, productId, count(1-1000) |
201 Created | 400 (재고 부족), 404 (회원/상품 없음) |
| GET | /api/orders |
주문 목록 (페이징) | Query: page, size, sort |
200 OK | - |
| GET | /api/orders/{id} |
주문 상세 조회 | - | 200 OK | 404 (주문 없음) |
| POST | /api/orders/{id}/cancel |
주문 취소 (재고 복구) | - | 204 No Content | 404 (주문 없음), 400 (이미 취소) |
Request/Response 예시
주문 생성 (POST /api/orders)
// Request
{
"userId": 1,
"productId": 1,
"count": 2
}
// Response (201 Created)
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"totalAmount": 178000,
"status": "PENDING",
"orderItems": [
{
"productId": 1,
"productName": "무선 이어폰",
"price": 89000,
"quantity": 2,
"subtotal": 178000
}
],
"createdAt": "2025-11-10T12:00:00"
}주문 목록 조회 (GET /api/orders?page=0&size=10)
// Response (200 OK)
{
"content": [
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"totalAmount": 178000,
"status": "PENDING",
"orderItems": [
{
"productId": 1,
"productName": "무선 이어폰",
"price": 89000,
"quantity": 2,
"subtotal": 178000
}
],
"createdAt": "2025-11-10T12:00:00"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10,
"sort": {
"sorted": true,
"unsorted": false
}
},
"totalElements": 1,
"totalPages": 1,
"last": true,
"first": true
}| Method | Endpoint | Description | Request Body | Response | Error |
|---|---|---|---|---|---|
| POST | /api/refunds |
환불 요청 | userId, orderId, reason |
201 Created | 400 (이미 환불됨), 404 (회원/주문 없음) |
| GET | /api/refunds/{id} |
환불 상세 조회 | - | 200 OK | 404 (환불 없음) |
| GET | /api/refunds |
전체 환불 목록 (페이징) | Query: page, size, sort |
200 OK | - |
| GET | /api/refunds/user/{userId} |
특정 사용자 환불 내역 | - | 200 OK | - |
| GET | /api/refunds/order/{orderId} |
특정 주문 환불 내역 | - | 200 OK | - |
| PUT | /api/refunds/{id}/approve |
환불 승인 (관리자) | - | 200 OK | 404 (환불 없음), 400 (이미 처리됨) |
| PUT | /api/refunds/{id}/reject |
환불 거절 (관리자) | rejectReason (optional) |
200 OK | 404 (환불 없음), 400 (이미 처리됨) |
Request/Response 예시
환불 요청 (POST /api/refunds)
// Request
{
"userId": 1,
"orderId": 1,
"reason": "상품 불량"
}
// Response (201 Created)
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"orderId": 1,
"reason": "상품 불량",
"status": "PENDING",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}환불 상세 조회 (GET /api/refunds/{id})
// Response (200 OK)
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"orderId": 1,
"reason": "상품 불량",
"status": "PENDING",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}전체 환불 목록 조회 (GET /api/refunds?page=0&size=10)
// Response (200 OK)
{
"content": [
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"orderId": 1,
"reason": "상품 불량",
"status": "PENDING",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:00:00"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 10
},
"totalElements": 1,
"totalPages": 1
}환불 승인 (PUT /api/refunds/{id}/approve)
// Response (200 OK)
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"orderId": 1,
"reason": "상품 불량",
"status": "APPROVED",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:05:00"
}환불 거절 (PUT /api/refunds/{id}/reject)
// Request (Optional)
"반품 기간 초과"
// Response (200 OK)
{
"id": 1,
"userId": 1,
"userName": "홍길동",
"orderId": 1,
"reason": "상품 불량",
"status": "REJECTED",
"createdAt": "2025-11-10T12:00:00",
"updatedAt": "2025-11-10T12:05:00"
}