Skip to content

bung-dev/OData-Server-Test

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OData V4 Demo Server

Spring Boot 3 + Apache Olingo 5 기반의 OData V4 학습용 서버입니다. Product / Category / Supplier 3개 도메인으로 OData 표준 쿼리 옵션과 CRUD를 완전히 구현합니다.


목차


기술 스택

항목 버전 / 값
Java 21 (IntelliJ JBR)
Spring Boot 3.2.5
Apache Olingo 5.0.0 (Jakarta Servlet 호환)
Gradle 8.7
H2 Database in-memory (jdbc:h2:mem:odatadb)
Spring Data JPA Hibernate 6.4.4
Lombok 최신

프로젝트 구조

src/
├── main/java/com/example/odata/
│   ├── ODataApplication.java              # 진입점
│   ├── config/
│   │   └── ODataServletConfig.java        # Olingo 서블릿 /odata/* 등록
│   ├── edm/
│   │   └── DemoEdmProvider.java           # OData EDM 스키마 정의
│   ├── processor/
│   │   ├── DemoEntityCollectionProcessor.java  # 컬렉션 Read + 쿼리 옵션
│   │   ├── DemoEntityProcessor.java            # 단건 Read + CUD
│   │   └── DemoPrimitiveProcessor.java         # $count 지원
│   ├── service/
│   │   ├── ProductService.java            # 상품 비즈니스 로직
│   │   ├── CategoryService.java           # 카테고리 비즈니스 로직
│   │   └── SupplierService.java           # 공급자 비즈니스 로직
│   ├── mapper/
│   │   └── ODataEntityMapper.java         # JPA ↔ OData Entity 변환
│   ├── filter/
│   │   └── FilterExpressionVisitor.java   # $filter → JPA Specification
│   ├── exception/
│   │   └── ODataErrorHelper.java          # OData 표준 에러 응답 생성
│   ├── util/
│   │   ├── ODataPropertyExtractor.java    # Entity 프로퍼티 추출 유틸리티
│   │   ├── ExpandOptionResolver.java      # $expand 옵션 파싱 유틸리티
│   │   └── ODataStringUtils.java          # 문자열 변환 유틸리티 (toCamelCase)
│   ├── entity/
│   │   ├── Product.java
│   │   ├── Category.java
│   │   └── Supplier.java
│   ├── repository/
│   │   ├── ProductRepository.java
│   │   ├── CategoryRepository.java
│   │   └── SupplierRepository.java
│   └── data/
│       └── DataInitializer.java           # 시드 데이터 25개 자동 생성
└── test/java/com/example/odata/
    ├── service/                           # 서비스 단위 테스트
    ├── mapper/                            # 매퍼 단위 테스트
    ├── filter/                            # 필터 단위 테스트
    └── integration/                       # 전체 통합 테스트

도메인 모델

Category (1) ──── (N) Product (N) ──── (1) Supplier

Product

필드 타입 설명
Id Long PK (자동 생성)
Name String 상품명 (필수)
Description String 설명
Price Integer 가격 (원)
Rating Double 평점 (0.0 ~ 5.0)
CategoryId Long 카테고리 FK
SupplierId Long 공급자 FK
CreatedAt DateTimeOffset 생성 시각
UpdatedAt DateTimeOffset 수정 시각

Category

필드 타입 설명
Id Long PK
Name String 카테고리명 (필수)
Description String 설명

Supplier

필드 타입 설명
Id Long PK
Name String 공급자명 (필수)
Email String 이메일
Phone String 전화번호
Address String 주소

빠른 시작

사전 요구사항

  • IntelliJ IDEA 2025.x (JBR 21 포함)
  • 시스템 기본 Java가 1.8인 경우 반드시 JBR 사용

환경 변수 설정

export JAVA_HOME="/c/Program Files/JetBrains/IntelliJ IDEA 2025.3.4/jbr"

앱 실행

./gradlew bootRun --no-daemon

포트 8080이 사용 중이면:

./gradlew bootRun --no-daemon --args='--server.port=8081'

서버가 시작되면 시드 데이터(카테고리 3개, 공급자 3개, 상품 25개)가 자동으로 생성됩니다.

빌드만 (컴파일 확인)

./gradlew compileJava --no-daemon

API 사용 가이드

기본 URL: http://localhost:8080/odata

서비스 문서 & 메타데이터

# 서비스 문서 (EntitySet 목록)
GET http://localhost:8080/odata/

# EDM 메타데이터 (스키마 전체)
GET http://localhost:8080/odata/$metadata

데이터 조회 (Read)

컬렉션 조회

# 상품 목록 (기본 페이지: 20건, nextLink 포함)
GET http://localhost:8080/odata/Products

# 카테고리 목록
GET http://localhost:8080/odata/Categories

# 공급자 목록
GET http://localhost:8080/odata/Suppliers

단건 조회

# ID로 조회
GET http://localhost:8080/odata/Products(1)
GET http://localhost:8080/odata/Categories(2)
GET http://localhost:8080/odata/Suppliers(3)

건수 조회

GET http://localhost:8080/odata/Products/$count
# 응답: 25

쿼리 옵션

$filter — 필터링

# 가격이 100,000원 초과
GET http://localhost:8080/odata/Products?$filter=Price gt 100000

# 평점 4.0 이상이고 가격 500,000원 초과
GET http://localhost:8080/odata/Products?$filter=Price gt 500000 and Rating ge 4.0

# 이름에 '갤럭시' 포함
GET http://localhost:8080/odata/Products?$filter=contains(Name,'갤럭시')

# 이름이 'LG'로 시작
GET http://localhost:8080/odata/Products?$filter=startswith(Name,'LG')

# 가격이 50만 미만 또는 평점 4.5 초과
GET http://localhost:8080/odata/Products?$filter=Price lt 500000 or Rating gt 4.5

지원 연산자: eq, ne, gt, lt, ge, le, and, or, not 지원 함수: contains, startswith, endswith

$orderby — 정렬

# 가격 내림차순
GET http://localhost:8080/odata/Products?$orderby=Price desc

# 이름 오름차순
GET http://localhost:8080/odata/Products?$orderby=Name asc

# 복합 정렬
GET http://localhost:8080/odata/Products?$orderby=Rating desc,Price asc

$select — 필드 선택

# Name, Price 필드만 반환
GET http://localhost:8080/odata/Products?$select=Name,Price

# 단건에도 적용 가능
GET http://localhost:8080/odata/Products(1)?$select=Name,Description,Rating

주의: 프로퍼티 이름은 EDM 정의 기준 대소문자 구분 (Name, Price — 소문자 불가)

$expand — 연관 엔티티 포함

# 카테고리 정보 포함
GET http://localhost:8080/odata/Products(1)?$expand=Category

# 공급자 정보 포함
GET http://localhost:8080/odata/Products(1)?$expand=Supplier

# 카테고리 + 공급자 모두 포함
GET http://localhost:8080/odata/Products(1)?$expand=Category,Supplier

# 전체 expand
GET http://localhost:8080/odata/Products(1)?$expand=*

$top / $skip — 페이징

# 처음 5건
GET http://localhost:8080/odata/Products?$top=5

# 11번째부터 5건
GET http://localhost:8080/odata/Products?$top=5&$skip=10

서버 드리븐 페이징: 기본 페이지 크기 20건, 최대 100건. 다음 페이지가 있으면 응답에 @odata.nextLink가 포함됩니다.

$count — 건수 포함

# 응답 body에 @odata.count 포함
GET http://localhost:8080/odata/Products?$count=true

복합 쿼리 예시

# 가격 50만 초과 상품을 이름순 정렬, 상위 3건, Name/Price만 반환
GET http://localhost:8080/odata/Products?$filter=Price gt 500000&$orderby=Name&$top=3&$select=Name,Price

# 갤럭시 포함 상품, 공급자 정보 포함, 평점 내림차순
GET http://localhost:8080/odata/Products?$filter=contains(Name,'갤럭시')&$expand=Supplier&$orderby=Rating desc

데이터 생성 (Create)

상품 생성

curl -X POST http://localhost:8080/odata/Products \
  -H "Content-Type: application/json" \
  -d '{
    "Name": "신상품",
    "Description": "새로 출시된 상품",
    "Price": 150000,
    "Rating": 4.2,
    "CategoryId": 1,
    "SupplierId": 1
  }'

응답: 201 Created + Location: Products(26) 헤더

카테고리 생성

curl -X POST http://localhost:8080/odata/Categories \
  -H "Content-Type: application/json" \
  -d '{"Name": "스포츠", "Description": "운동용품 카테고리"}'

공급자 생성

curl -X POST http://localhost:8080/odata/Suppliers \
  -H "Content-Type: application/json" \
  -d '{
    "Name": "현대전자",
    "Email": "contact@hyundai.com",
    "Phone": "02-9999-0000",
    "Address": "서울시 서초구"
  }'

데이터 수정 (Update / Patch)

PATCH — 부분 수정 (권장)

변경할 필드만 전송. 나머지 필드는 기존 값 유지.

# 가격만 변경
curl -X PATCH http://localhost:8080/odata/Products(1) \
  -H "Content-Type: application/json" \
  -d '{"Price": 990000}'

# 여러 필드 변경
curl -X PATCH http://localhost:8080/odata/Products(1) \
  -H "Content-Type: application/json" \
  -d '{"Price": 1100000, "Rating": 4.9}'

# 카테고리 변경
curl -X PATCH http://localhost:8080/odata/Products(1) \
  -H "Content-Type: application/json" \
  -d '{"CategoryId": 2}'

응답: 200 OK + 수정된 엔티티 전체

PUT — 전체 교체

전송하지 않은 필드는 null로 설정됩니다.

curl -X PUT http://localhost:8080/odata/Products(1) \
  -H "Content-Type: application/json" \
  -d '{
    "Name": "갤럭시 S25",
    "Description": "최신 플래그십",
    "Price": 1350000,
    "Rating": 5.0,
    "CategoryId": 1,
    "SupplierId": 1
  }'

데이터 삭제 (Delete)

# 상품 삭제
curl -X DELETE http://localhost:8080/odata/Products(26)

# 카테고리 삭제
curl -X DELETE http://localhost:8080/odata/Categories(4)

# 공급자 삭제
curl -X DELETE http://localhost:8080/odata/Suppliers(4)

응답: 204 No Content


에러 응답

모든 에러는 OData 표준 형식으로 반환됩니다:

{
  "error": {
    "code": "404",
    "message": "Product not found: 99999"
  }
}
HTTP 상태 발생 조건
400 Bad Request Name 누락, 잘못된 $filter 구문
403 Forbidden 접근 권한 없음
404 Not Found 존재하지 않는 ID, 존재하지 않는 CategoryId/SupplierId
405 Method Not Allowed 컬렉션에 PUT/DELETE 요청
409 Conflict 리소스 충돌
500 Internal Server Error 예상치 못한 서버 오류
501 Not Implemented 미구현 기능 요청

H2 콘솔

앱 실행 중 브라우저에서 접속:

URL:      http://localhost:8080/h2-console
JDBC URL: jdbc:h2:mem:odatadb
Username: sa
Password: (없음)

테스트 실행

export JAVA_HOME="/c/Program Files/JetBrains/IntelliJ IDEA 2025.3.4/jbr"

# 전체 테스트 실행
./gradlew test --no-daemon

# 특정 테스트만 실행
./gradlew test --no-daemon --tests "com.example.odata.integration.ODataIntegrationTest"
./gradlew test --no-daemon --tests "com.example.odata.service.ProductServiceTest"

테스트 구성 (총 63개)

테스트 클래스 테스트 수 내용
ProductServiceTest 11 CUD + 검증 단위 테스트
CategoryServiceTest 7 CUD 단위 테스트
SupplierServiceTest 7 CUD 단위 테스트
ODataErrorHelperTest 4 에러 응답 생성 검증
ODataEntityMapperTest 6 JPA ↔ OData 매핑 검증
FilterExpressionVisitorTest 7 $filter → JPA Predicate 검증
ODataIntegrationTest 21 전체 E2E 통합 테스트

참고: Olingo는 별도 서블릿으로 등록되므로 MockMvc 사용 불가. 통합 테스트는 @SpringBootTest(webEnvironment = RANDOM_PORT) + TestRestTemplate 사용.


서비스 베이스 URL: http://localhost:8080/odata


리팩토링 이력

항목 내용
util/ODataStringUtils 추출 DemoEntityCollectionProcessor, FilterExpressionVisitor의 중복 toCamelCase() 통합
util/ExpandOptionResolver 추출 DemoEntityCollectionProcessor, DemoEntityProcessor의 중복 resolveExpandNames() 통합
util/ODataPropertyExtractor 추출 DemoEntityProcessor의 4개 프로퍼티 추출 헬퍼 통합
ODataErrorHelper 확장 conflict(), forbidden(), notImplemented() 메서드 추가
ProductService 레이지 로딩 개선 암묵적 .getName() 트리거 → Hibernate.initialize() 명시적 초기화

About

OData 서버 구현 테스트

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages