작업 일지 #4 카테고리 생성·수정 & 테스트 환경 분리

오늘 한 일 요약

 

  • 카테고리 생성 — <<POST /api/categories>> 구현, 상위 존재·이름 중복 검증 후 <<sortOrder>> 기본 0으로 저장.
  • 카테고리 수정 — <<PATCH /api/categories/{id}>> 구현, <<JsonNullable>> 로 부분 업데이트 지원·깊이 / 중복 예외 처리.
  • 테스트 환경 분리 — <<TestBase>> 베이스 + JUnit 확장으로 모든 테스트를 <<test>> 프로필로 강제.

 


카테고리 생성

개요

  • <<POST /api/categories>> 엔드포인트를 구현해 최상위·하위(1 단계) 카테고리를 등록할 수 있게 했다.
  • 입력 필드: <<name>>, <<parentId>> (nullable), <<sortOrder>> (nullable·기본값 0).

고민한 점

  • 전달된 <<parentId>>가 실제로 존재하는지 서비스 레이어에서 검증할 방법.
  • 카테고리 이름 중복을 어떤 범위(전역 vs 동일 부모)에서 제한할지 기준 설정.
  • 검증 실패 시 예외를 어디서 던지고 어떤 HTTP 상태로 변환할지.
  • <<sortOrder>>를 요청에 포함하지 않으면 어떤 값으로 저장할지(디폴트 0 확정).

해결 방법

  1. 상위 카테고리 존재 검증
    • <<parentId>>가 null이 아니면 <<findById>>로 조회하고, 없으면 <<NotFoundException>> 발생.
  2. 이름 중복 검증
    • “동일 부모 내에서만 유니크”로 정의.
    • 레포지터리 메서드 <<existsByNameAndParentId>>로 사전 체크 후 중복이면 <<DuplicateCategoryException>> 발생.
    • DB에도 <<UNIQUE (parent_id, name)>> 제약을 걸어 이중 방어.
  3. <<sortOrder>> 처리
    • 값이 없으면 서비스에서 0으로 설정해 저장(정렬 로직은 조회 단계에서 별도 구현 예정).
  4. 예외 처리
    • 서비스 레이어에서 발생한 예외를 글로벌 핸들러가 받아 <<400 Bad Request>>로 변환해 응답.

결과

  • 정상·예외(부모 미존재, 이름 중복) 시나리오를 포함한 단위 테스트 통과.
  • 기본 <<sortOrder>> 0이 일관되게 저장돼 이후 조회·정렬 로직 연계 준비 완료.

카테고리 수정

개요

  • <<PATCH /api/categories/{id}>> 엔드포인트를 구현해 카테고리의 <<name>>, <<parentId>>, <<sortOrder>> 중 필요한 항목만 갱신할 수 있게 했다.
  • 입력 DTO는 <<JsonNullable>> 기반으로 “값 있음 / 없음”을 명확히 구분한다.

고민한 점

  • 일부 필드만 변경할 때 <<null>> 과 “값 미전달”을 구분하지 못하면 의도치 않은 덮어쓰기가 발생한다.
  • <<JsonNullable>> 사용 시 모듈 미등록으로 <<HttpMessageConversionException>> 이 발생했다.
  • 상위 카테고리를 바꿀 때 “자기 자신을 부모로 지정”하거나 “깊이 2단계를 초과”하는 요청을 어떻게 막고 어떤 상태 코드로 응답할지.
  • 이름을 수정할 때 동일 부모 내 중복 검사를 다시 수행해야 한다.
  • 변경이 실제로 없으면 DB에 불필요한 UPDATE가 나가지 않게 해야 한다.

해결 방법

  1. DTO 설정
    • 모든 필드를 <<JsonNullable<T>>>로 선언.
    • <<ObjectMapper>>에 <<NullablesModule>>을 등록해 직‧역직렬화 오류를 해결.
  2. 부분 업데이트 로직
    • 서비스 레이어에서 <<if (request.name().isPresent())>> 패턴으로 필드별 적용.
    • 변경 내용이 전혀 없으면 204 No Content 응답.
  3. 상위 카테고리 검증
    • <<parentId>>가 있을 경우 <<findById>>로 존재 여부 확인 → 없으면 400 Bad Request 예외.
    • id == parentId 또는 깊이 초과(2단계 이상) 요청도 동일한 400 Bad Request 예외로 통일.
      • 깊이 초과 예외의 상태 코드를 잠시 고민했지만, “클라이언트가 잘못 보낸 요청”에 해당하므로 400으로 확정했다.
  4. 이름 중복 검증
    • 동일 부모 내에서만 유니크하도록 <<existsByNameAndParentId>>로 사전 체크.
    • 중복이 발견되면 409 Conflict 예외를 던져 상태-코드별 예외 체계에 맞춘다.
  5. 트랜잭션·감사
    • <<@Transactional>> 범위 내에서 변경 감지로 UPDATE.
    • JPA 감사 컬럼에 수정자·수정 시각 자동 기록.

결과

  • 정상 케이스와 예외 케이스(부모 미존재 400, 깊이 초과 400, 이름 중복 409)를 포함한 단위 테스트 14건 통과.
  • 모듈 미등록으로 발생하던 <<HttpMessageConversionException>> 재발 없음.

 


테스트 환경 분리

개요

모든 통합 테스트가 <<test>> 프로필로 동작하도록 <<TestBase>> 추상 클래스를 만들고, 여기서 공통 어노테이션을 선언했다.

  • <<@ActiveProfiles( "test" )>>
  • <<@SpringBootTest>>
  • <<@Transactional>>

고민한 점

  • 새 테스트 클래스를 만들 때 << TestBase >> 상속을 깜빡하면 여전히 개발 DB를 사용해 PK가 증가한다.
  • IDE·CI 어디서 실행해도 “프로필 누락”을 즉시 감지해 테스트를 중단시키고 싶다.

해결 방법

  1. << TestBase >> 추상 클래스
    • 모든 테스트 코드가 이 클래스를 상속하도록 가이드.
  2. JUnit 5 확장 <<RequireTestProfileExtension>> 작성
    • <<BeforeAllCallback>> 단계에서 스프링 컨텍스트의 활성 프로필을 조회.
    • <<test>> 가 아니면 <<Exception>> 을 던져 실행을 즉시 중단하고,
      “활성 프로필이 ‘test’가 아닙니다.” 라는 메시지를 출력.
  3. 서비스 로더 등록
    • <<src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension>> 파일에
      <<com.khmall.support.RequireTestProfileExtension>> 를 기입해 전역 확장으로 자동 적용.

결과

  • 모든 통합 테스트가 항상 <<test>> 프로필로 실행돼 개발 DB 오염이 사라졌다.
  • 상속을 빼먹은 테스트는 0.2 초 이내에 중단되며 명확한 안내 메시지를 출력해 휴먼 에러를 방지한다.
  • 추가 JVM 옵션이나 <<spring.profiles.active>> 설정 없이도 IDE, Gradle, CI에서 동일하게 동작한다.

 


하루 회고

배운 점

  • 입력 / 이름 중복 검증을 서비스 층에 둬 로직과 컨트롤러를 분리할 수 있었다.
  • <<JsonNullable>> 는 모듈 등록이 필수, 더티 체킹 덕분에 “변경 없음”은 DB 부하가 없다.
  • JUnit 확장으로 프로필 누락을 즉시 중단해 휴먼 에러를 줄였다.

다음 할 일

  1. 카테고리 조회(정렬·트리 응답)
  2. 카테고리 삭제(하위 노드 정책 결정)
  3. 상품 CRUD 구현