개발 프로젝트/[팀 프로젝트] 냉파마스터

냉파마스터 BE 작업 정리: 사전 재료 검색 API / 카테고리 목록 조회 API

namerong 2026. 6. 27. 02:00

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보다 테스트와 유지보수에 유리하다.