1. 작업 범위
이번 작업에서는 냉장고 재료 기능의 기반이 되는 두 가지 API를 구현했다.
| 이슈 | 기능 | API |
| #14 | 사전 재료 검색 | GET /api/v1/products/search?keyword={keyword} |
| #15 | 카테고리 목록 조회 | GET /api/v1/categories |
두 API는 냉장고 재료 등록/수정, 장보기, 레시피 추천, 재료 필터링 등 여러 기능에서 공통으로 사용될 수 있는 기반 API다.
2. 사전 재료 검색 API
목적
사용자가 재료명을 입력하면 DB에 등록된 사전 재료 중 활성 상태인 재료만 검색해 반환한다.
주요 구현 요소
| 구분 | 내용 |
| Entity | Product |
| Repository | ProductRepository |
| Service | ProductService |
| Controller | ProductController |
| Response DTO | ProductSearchResponse |
| Exception | ProductNotFoundException |
API
GET /api/v1/products/search?keyword=두
핵심 Repository 메서드
List<Product> findByIsActiveTrueAndNameContaining(String keyword);
boolean existsByProductIdAndIsActiveTrue(Long productId);
핵심 Service 흐름
검색어 입력
→ 활성 상태 Product 부분 검색
→ ProductSearchResponse로 변환
→ List 반환
DTO 변환 방식
public static ProductSearchResponse from(Product product) {
return new ProductSearchResponse(
product.getProductId(),
product.getProductCategoryId(),
product.getName(),
product.getDefaultExpiryDays()
);
}
from()은 Entity를 Response DTO로 변환하는 정적 팩토리 메서드다.
Controller에서 Entity를 직접 반환하지 않고, 응답에 필요한 값만 DTO로 변환하기 위해 사용했다.
stream / map / toList 흐름
return productRepository.findByIsActiveTrueAndNameContaining(keyword).stream()
.map(ProductSearchResponse::from)
.toList();
List<Product>
→ stream()
→ Product 하나씩 ProductSearchResponse로 변환
→ List<ProductSearchResponse>
예외 처리 흐름
public void validateExists(Long productId) {
if (!productRepository.existsByProductIdAndIsActiveTrue(productId)) {
throw new ProductNotFoundException(productId);
}
}
이 메서드는 냉장고 재료 등록/수정, 장보기 등록 등에서 존재하지 않거나 비활성화된 재료 ID가 저장되는 것을 막기 위한 검증 로직이다.
3. 카테고리 목록 조회 API
목적
냉장고 재료 등록/수정 화면에서 사용할 카테고리 목록을 제공한다.
프론트에서는 드롭다운이나 필터 옵션으로 사용할 수 있다.
주요 구현 요소
| 구분 | 내용 |
| Entity | ProductCategory |
| Repository | ProductCategoryRepository |
| Service | ProductCategoryService |
| Controller | ProductCategoryController |
| Response DTO | ProductCategoryResponse |
API
GET /api/v1/categories
DB 기준
CREATE TABLE product_categories (
product_category_id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
);
Entity 매핑
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_category_id")
private Long productCategoryId;
@Column(name = "name", nullable = false, unique = true)
private String name;
핵심 Service 흐름
public List<ProductCategoryResponse> findCategories() {
return productCategoryRepository.findAll().stream()
.map(ProductCategoryResponse::from)
.toList();
}
product_categories 전체 조회
→ ProductCategoryResponse로 변환
→ List 반환
응답 형태
[
{
"productCategoryId": 1,
"name": "채소"
}
]
4. 인증 및 권한 확인
두 API는 SecurityConfig 기준 인증이 필요한 API로 설정되어 있다.
/api/v1/products/search
/api/v1/categories
토큰 없이 호출한 경우
401 Unauthorized
응답 예시:
{
"success": false,
"message": "인증이 필요합니다.",
"data": null
}
토큰 포함 호출한 경우
Postman Authorization 설정:
Type: Bearer Token
Token: accessToken
정상 호출 시:
200 OK
확인한 내용
| 항목 | 결과 |
| 인증 없이 호출 | 401 응답 |
| Bearer Token 포함 호출 | 200 응답 |
| 권한 정책 | 인증 사용자 접근 가능 |
5. 테스트 정리
테스트는 강의에서 익숙하게 사용한 @SpringBootTest 방식으로 작성했다.
사전 재료 검색 테스트
검증 항목:
부분 검색 시 결과 반환
검색 결과 없으면 빈 리스트 반환
존재하지 않는 productId 검증 시 ProductNotFoundException 발생
예외 테스트 흐름:
Throwable thrown = catchThrowable(() -> productService.validateExists(productId));
assertThat(thrown).isInstanceOf(ProductNotFoundException.class);
given: 존재하지 않는 productId 준비
when: validateExists 실행
then: ProductNotFoundException 발생 확인
카테고리 목록 조회 테스트
검증 항목:
카테고리 목록 조회 시 ProductCategoryResponse 리스트 반환
카테고리 ID가 null이 아님
카테고리 이름이 비어 있지 않음
6. DoD 기준 정리
#14 사전 재료 검색 API
| 완료 | 조건상태 |
| 검색 API 200 응답 확인 | 완료 |
| 활성 상태 재료만 조회 | 완료 |
| 부분 검색 동작 | 완료 |
| 검색 결과 없음 처리 | 완료 |
| 존재하지 않는 재료 ID 검증 | 완료 |
| 권한 없는 접근 예외 처리 | 완료 |
#15 카테고리 목록 조회 API
| 완료 | 조건상태 |
| 카테고리 API 200 응답 확인 | 완료 |
| 카테고리 ID와 이름 반환 | 완료 |
| 권한 정책 확인 | 완료 |
| 데이터 없는 경우 빈 배열 처리 | findAll() 구조상 빈 리스트 반환 |
| 냉장고 등록/수정 카테고리 검증 연결 | 냉장고 API 구현 시 연결 예정 |
7. 구현하면서 정리한 개념
record
응답 DTO처럼 값을 담아 반환하는 객체에 사용할 수 있다.
public record ProductCategoryResponse(
Long productCategoryId,
String name
) {
}
자동으로 제공되는 것:
생성자
필드 접근 메서드
equals
hashCode
toString
record의 getter는 getName()이 아니라 다음 형태다.
response.name()
response.productCategoryId()
Entity와 DTO 분리
Entity는 DB 테이블과 매핑되는 객체다.
DTO는 API 요청/응답에 사용하는 객체다.
Controller에서 Entity를 바로 반환하지 않고 DTO로 변환한 이유:
응답 필드 제어
DB 구조 노출 방지
화면/API 명세에 맞는 응답 제공
생성자 주입
Controller와 Service에서는 생성자 주입을 사용했다.
private final ProductCategoryService productCategoryService;
public ProductCategoryController(ProductCategoryService productCategoryService) {
this.productCategoryService = productCategoryService;
}
필드 주입 방식인 @Autowired보다 테스트와 유지보수에 유리하다.
'개발 프로젝트 > [팀 프로젝트] 냉파마스터' 카테고리의 다른 글
| 냉장고 유통기한 자동 반영 보완 (0) | 2026.07.02 |
|---|---|
| 장보기 연동과 사전 재료 관리자 기능 정리 (0) | 2026.06.30 |
| 장보기 도메인 API 구현 정리 (0) | 2026.06.29 |
| 냉장고 유통기한 조회와 프론트 연동, 그리고 검색 성능 개선 정리 (0) | 2026.06.28 |
| 냉장고 재료 CRUD 및 사용 처리 API 구현 (0) | 2026.06.28 |