Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

kangwooc/category-service

Repository files navigation

온라인 쇼핑몰 카테고리 서비스

멀티모듈 구조로 구성된 계층형 카테고리 관리 REST API 서비스입니다.

🚀 빠른 시작

필수 요구사항

  • JDK 21 이상
  • Gradle 8.14 이상

실행 방법

# 1. 프로젝트 클론
git clone https://github.com/kangwooc/musinsa-category.git
cd musinsa-category
# 2. 애플리케이션 실행
./gradlew :modules:api:bootRun
# Windows 환경
gradlew.bat :modules:api:bootRun

접속 정보

🏗️ 프로젝트 구조

category-service/
├── modules/
│ ├── api/ # REST API 컨트롤러 및 웹 계층
│ ├── application/ # 비즈니스 로직 및 서비스
│ ├── core/ # 공통 DTO, 설정, 예외처리
│ └── infra/ # 데이터베이스 및 인프라 계층
├── gradle/
├── build.gradle.kts
└── settings.gradle.kts

모듈별 역할

모듈 역할 주요 구성요소
api REST API 엔드포인트 제공 Controller, DTO, 전역 예외처리
application 비즈니스 로직 처리 Service, Mapper
core 공통 기능 및 설정 DTO, 예외 클래스, 설정
infra 데이터 영속성 관리 JPA Entity, Repository

🗄️ 데이터베이스 명세

테이블 구조

categories

Column Type Description
id BIGINT (PK) 카테고리 ID
name VARCHAR(100) 카테고리 명
created_at TIMESTAMP 생성일시
updated_at TIMESTAMP 수정일시

category_hierarchy (다대다 관계 테이블)

Column Type Description
parent_id BIGINT (FK) 부모 카테고리 ID
child_id BIGINT (FK) 자식 카테고리 ID

제약 조건

  • PRIMARY KEY (parent_id, child_id): 중복 관계 방지
  • CHECK (parent_id != child_id): 자기 참조 방지
  • CASCADE DELETE: 카테고리 삭제 시 관계도 함께 삭제

📡 API 명세

기본 정보

  • Base URL: http://localhost:8080/api/categories
  • Content-Type: application/json

엔드포인트

1. 카테고리 생성

POST /api/categories

새로운 카테고리를 생성합니다. 여러 부모 카테고리를 지정할 수 있습니다.

Request Headers
Content-Type: application/json
Request Body
{
 "name": "string", // 필수: 카테고리 이름 (최대 100자)
 "parentIds": [number] // 선택: 부모 카테고리 ID 배열 (최대 10개)
}
Request Body 검증 규칙
  • name: 필수값, 공백 불가, 최대 100자
  • parentIds: 선택값, 최대 10개, 중복 불가
Response (201 Created)
{
 "id": 15,
 "name": "무선 이어폰",
 "parentIds": [1, 3],
 "parentNames": ["전자제품", "액세서리"]
}
Error Cases
  • 400: 이름이 비어있거나 너무 긴 경우
  • 400: 부모가 10개를 초과하는 경우
  • 404: 지정한 부모 카테고리가 존재하지 않는 경우
cURL Example
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{
 "name": "무선 이어폰",
 "parentIds": [1, 3]
 }'

2. 카테고리 수정

PUT /api/categories/{id}

기존 카테고리의 정보를 수정합니다.

Path Parameters
  • id (number, 필수): 수정할 카테고리 ID
Request Body
{
 "name": "string", // 선택: 새로운 카테고리 이름
 "parentIds": [number] // 선택: 새로운 부모 카테고리 ID 배열
}
Request Body 특징
  • 모든 필드가 선택사항
  • parentIds가 빈 배열이면 모든 부모 관계 제거
  • parentIds가 null이면 부모 관계 변경하지 않음
Response (200 OK)
{
 "id": 15,
 "name": "업데이트된 이름",
 "parentIds": [2, 4],
 "parentNames": ["신발", "상의"]
}
Error Cases
  • 400: 자기 자신을 부모로 지정하는 경우
  • 400: 순환 참조가 발생하는 경우
  • 404: 카테고리 또는 부모 카테고리가 존재하지 않는 경우
cURL Example
curl -X PUT http://localhost:8080/api/categories/15 \
 -H "Content-Type: application/json" \
 -d '{
 "name": "블루투스 헤드셋",
 "parentIds": [1]
 }'

3. 카테고리 삭제

DELETE /api/categories/{id}

카테고리를 삭제합니다. 자식 카테고리가 있는 경우 삭제할 수 없습니다.

Path Parameters
  • id (number, 필수): 삭제할 카테고리 ID
Response (204 No Content)

응답 본문 없음

Error Cases
  • 400: 자식 카테고리가 존재하는 경우
  • 404: 카테고리가 존재하지 않는 경우
cURL Example
curl -X DELETE http://localhost:8080/api/categories/15

4. 카테고리 단건 조회

GET /api/categories/{id}

특정 카테고리의 상세 정보를 조회합니다.

Path Parameters
  • id (number, 필수): 조회할 카테고리 ID
Response (200 OK)
{
 "id": 15,
 "name": "무선 이어폰",
 "parentIds": [1, 3],
 "parentNames": ["전자제품", "액세서리"]
}
Error Cases
  • 404: 카테고리가 존재하지 않는 경우
cURL Example
curl -X GET http://localhost:8080/api/categories/15

5. 카테고리 트리 조회

GET /api/categories/{id}/tree

특정 카테고리를 루트로 하는 하위 트리 구조를 조회합니다.

Path Parameters
  • id (number, 필수): 루트로 사용할 카테고리 ID
Response (200 OK)
{
 "id": 1,
 "name": "전자제품",
 "parentIds": [],
 "children": [
 {
 "id": 2,
 "name": "스마트폰",
 "parentIds": [1],
 "children": [
 {
 "id": 3,
 "name": "아이폰",
 "parentIds": [2],
 "children": []
 },
 {
 "id": 4,
 "name": "갤럭시",
 "parentIds": [2],
 "children": []
 }
 ]
 }
 ]
}
특징
  • 순환 참조가 감지되면 "(circular reference)" 표시
  • 자식들은 이름순으로 정렬됨
Error Cases
  • 404: 카테고리가 존재하지 않는 경우
cURL Example
curl -X GET http://localhost:8080/api/categories/1/tree

6. 카테고리 경로 조회

GET /api/categories/{id}/paths

특정 카테고리까지의 모든 가능한 경로를 조회합니다.

Path Parameters
  • id (number, 필수): 경로를 조회할 카테고리 ID
Response (200 OK)
{
 "id": 10,
 "name": "무선 이어폰",
 "paths": [
 {
 "pathIds": [1, 10],
 "pathNames": ["전자제품", "무선 이어폰"],
 "depth": 1
 },
 {
 "pathIds": [3, 10],
 "pathNames": ["액세서리", "무선 이어폰"],
 "depth": 1
 },
 {
 "pathIds": [1, 2, 10],
 "pathNames": ["전자제품", "오디오", "무선 이어폰"],
 "depth": 2
 }
 ]
}
경로 설명
  • pathIds: 루트부터 현재 카테고리까지의 ID 경로
  • pathNames: 루트부터 현재 카테고리까지의 이름 경로
  • depth: 루트로부터의 깊이 (0: 루트, 1: 1단계 하위...)
Error Cases
  • 404: 카테고리가 존재하지 않는 경우
cURL Example
curl -X GET http://localhost:8080/api/categories/10/paths

7. 전체 카테고리 조회

GET /api/categories

모든 카테고리를 조회합니다. 트리 형태 또는 평면 목록으로 조회 가능합니다.

Query Parameters
  • tree (boolean, 선택, 기본값: false): 트리 형태로 반환할지 여부
Response (200 OK) - 평면 목록 (tree=false)
[
 {
 "id": 1,
 "name": "전자제품",
 "parentIds": [],
 "parentNames": []
 },
 {
 "id": 2,
 "name": "스마트폰",
 "parentIds": [1],
 "parentNames": ["전자제품"]
 }
]
Response (200 OK) - 트리 형태 (tree=true)
[
 {
 "id": 1,
 "name": "전자제품",
 "parentIds": [],
 "children": [
 {
 "id": 2,
 "name": "스마트폰",
 "parentIds": [1],
 "children": []
 }
 ]
 }
]
cURL Examples
# 평면 목록
curl -X GET http://localhost:8080/api/categories
# 트리 형태
curl -X GET "http://localhost:8080/api/categories?tree=true"

8. 루트 카테고리 조회

GET /api/categories/roots

부모가 없는 최상위 카테고리들을 조회합니다.

Response (200 OK)
[
 {
 "id": 1,
 "name": "전자제품",
 "parentIds": [],
 "parentNames": []
 },
 {
 "id": 5,
 "name": "의류",
 "parentIds": [],
 "parentNames": []
 }
]
cURL Example
curl -X GET http://localhost:8080/api/categories/roots

9. 특정 부모의 자식 조회

GET /api/categories/parent/{parentId}/children

특정 카테고리의 직계 자식들을 조회합니다.

Path Parameters
  • parentId (number, 필수): 부모 카테고리 ID
Response (200 OK)
[
 {
 "id": 2,
 "name": "스마트폰",
 "parentIds": [1],
 "parentNames": ["전자제품"]
 },
 {
 "id": 3,
 "name": "노트북",
 "parentIds": [1],
 "parentNames": ["전자제품"]
 }
]
Error Cases
  • 404: 부모 카테고리가 존재하지 않는 경우
cURL Example
curl -X GET http://localhost:8080/api/categories/parent/1/children

10. 특정 깊이 카테고리 조회

GET /api/categories/depth/{depth}

특정 깊이에 있는 카테고리들을 조회합니다.

Path Parameters
  • depth (number, 필수): 깊이 (0: 루트, 1: 1단계 하위...)
Response (200 OK)
[
 {
 "id": 2,
 "name": "스마트폰",
 "parentIds": [1],
 "parentNames": ["전자제품"]
 },
 {
 "id": 6,
 "name": "상의",
 "parentIds": [5],
 "parentNames": ["의류"]
 }
]
Error Cases
  • 400: 깊이가 음수인 경우
cURL Example
curl -X GET http://localhost:8080/api/categories/depth/1

11. 계층 구조 검증

GET /api/categories/validate

전체 카테고리 계층 구조의 유효성을 검증합니다.

Response (200 OK) - 유효한 경우
{
 "isValid": true,
 "errors": []
}
Response (200 OK) - 오류가 있는 경우
{
 "isValid": false,
 "errors": [
 "Category 5 has itself as parent",
 "Category 7 has itself as child"
 ]
}
cURL Example
curl -X GET http://localhost:8080/api/categories/validate

HTTP 상태 코드

상태 코드 설명
200 OK 조회/수정 성공
201 Created 생성 성공
204 No Content 삭제 성공
400 Bad Request 잘못된 요청 (유효성 검사 실패 등)
404 Not Found 리소스를 찾을 수 없음

에러 응답 형식

{
 "errorCode": "NOT_FOUND",
 "message": "Category(999) not found"
}

🛠️ 기술 스택

Backend

  • Language: Kotlin 2.2.0
  • Framework: Spring Boot 3.5.5
  • Database: H2 (In-Memory)
  • ORM: Spring Data JPA + Hibernate
  • Build Tool: Gradle 8.14

Libraries

  • API Documentation: SpringDoc OpenAPI (Swagger)
  • Validation: Spring Boot Starter Validation
  • Logging: Kotlin Logging + Logstash Logback Encoder
  • Code Quality: Ktlint

🧪 테스트 실행

단위/통합 테스트

# 전체 테스트 실행
./gradlew test
# 특정 모듈 테스트
./gradlew :modules:api:test
./gradlew :modules:application:test
./gradlew :modules:infra:test
# 특정 테스트 클래스 실행
./gradlew :modules:api:test --tests CategoryControllerTest
./gradlew :modules:api:test --tests CategoryFullIntegrationTest
# 특정 테스트 메서드 실행
./gradlew :modules:api:test --tests "CategoryControllerTest.CreateCategoryTests.정상적인 카테고리 생성"
# 빌드 + 테스트 + 코드 품질 검사 (CI/CD에서 사용)
./gradlew clean build

API 테스트 CLI 명령어

애플리케이션이 실행 중일 때 (./gradlew :modules:api:bootRun) 다음 curl 명령어로 API를 테스트할 수 있습니다:

기본 CRUD 테스트

# 1. 루트 카테고리 생성
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "전자제품"}'
# 2. 자식 카테고리 생성 (부모 ID는 위에서 반환된 ID 사용)
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "스마트폰", "parentIds": [1]}'
# 3. 다중 부모 카테고리 생성
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "게이밍 스마트폰", "parentIds": [1, 2]}'
# 4. 카테고리 조회
curl -X GET http://localhost:8080/api/categories/1
# 5. 카테고리 수정 (이름만 변경)
curl -X PUT http://localhost:8080/api/categories/1 \
 -H "Content-Type: application/json" \
 -d '{"name": "소비자 전자제품"}'
# 6. 카테고리 수정 (부모 관계만 변경)
curl -X PUT http://localhost:8080/api/categories/3 \
 -H "Content-Type: application/json" \
 -d '{"parentIds": [1]}'
# 7. 카테고리 삭제
curl -X DELETE http://localhost:8080/api/categories/3

조회 API 테스트

# 전체 카테고리 조회 (평면 리스트)
curl -X GET http://localhost:8080/api/categories
# 전체 카테고리 조회 (트리 구조)
curl -X GET "http://localhost:8080/api/categories?tree=true"
# 루트 카테고리들만 조회
curl -X GET http://localhost:8080/api/categories/roots
# 특정 카테고리의 트리 조회
curl -X GET http://localhost:8080/api/categories/1/tree
# 특정 카테고리의 모든 경로 조회
curl -X GET http://localhost:8080/api/categories/2/paths
# 특정 부모의 자식들 조회
curl -X GET http://localhost:8080/api/categories/parent/1/children
# 특정 깊이의 카테고리들 조회
curl -X GET http://localhost:8080/api/categories/depth/1
# 계층 구조 유효성 검증
curl -X GET http://localhost:8080/api/categories/validate

복잡한 시나리오 테스트

# 전자상거래 카테고리 구조 예시 생성
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "패션"}'
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "스포츠"}'
# 교차 카테고리 생성 (패션 + 스포츠)
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "스포츠웨어", "parentIds": [2, 3]}'

에러 케이스 테스트

# 순환 참조 시도 (에러 발생해야 함)
curl -X PUT http://localhost:8080/api/categories/1 \
 -H "Content-Type: application/json" \
 -d '{"parentIds": [2]}' \
 -v # 상세 HTTP 응답 확인
# 존재하지 않는 부모로 생성 시도 (에러 발생해야 함)
curl -X POST http://localhost:8080/api/categories \
 -H "Content-Type: application/json" \
 -d '{"name": "잘못된 카테고리", "parentIds": [999]}' \
 -v
# 자식이 있는 카테고리 삭제 시도 (에러 발생해야 함)
curl -X DELETE http://localhost:8080/api/categories/1 -v

API 응답 예시

성공 응답 (카테고리 생성)

{
 "id": 1,
 "name": "전자제품",
 "parentIds": [],
 "parentNames": []
}

에러 응답 (순환 참조)

{
 "errorCode": "BAD_REQUEST",
 "message": "Cannot update category: would create circular reference"
}

🏗️ 빌드 및 배포

# 프로젝트 빌드
./gradlew build
# JAR 파일 생성
./gradlew :modules:api:bootJar
# 생성된 JAR 실행
java -jar modules/api/build/libs/api-0.0.1-SNAPSHOT.jar

💡 주요 특징

아키텍처

  • 멀티모듈 구조: 관심사 분리 및 모듈간 의존성 관리
  • 계층형 아키텍처: API → Application → Core → Infra
  • 도메인 주도 설계: 비즈니스 로직과 인프라 분리

데이터 관리

  • 트리 구조: 무제한 depth 카테고리 계층 지원
  • Lazy Loading: 성능 최적화를 위한 지연 로딩
  • EntityGraph: N+1 문제 해결을 위한 페치 조인

개발 편의성

  • Swagger UI: 인터랙티브 API 문서
  • H2 Console: 개발 중 데이터베이스 확인
  • Hot Reload: 개발 중 자동 재시작

📝 개발 가이드

새로운 엔드포인트 추가

  1. core 모듈에 DTO 정의
  2. application 모듈에 서비스 로직 구현
  3. api 모듈에 컨트롤러 추가

데이터베이스 스키마 변경

  1. infra 모듈의 Entity 수정
  2. application.ymlddl-auto 설정 확인
  3. 테스트 데이터 업데이트

🔍 모니터링

로깅

  • 형식: JSON 구조화 로깅 (Logstash 호환)
  • 레벨: INFO (기본), DEBUG (개발 시)
  • 추적: TraceId 기반 요청 추적

개발 도구

  • Code Style: Ktlint를 통한 코드 스타일 검사
  • API 테스트: Swagger UI를 통한 실시간 테스트

About

카테고리 서비스

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /