PROJECT/GDSC 프로젝트 트랙 : 물품 대여 서비스

[SpringBoot] 특정 카테고리 삭제 API 구현

yeonee911 2024. 8. 13. 01:22

TMI. 제일 좋아하는 꽃은 노란 장미

분홍색/빨간색보다 노란 장미가 제일 예뻐요🌟


1. 목표🎯

: 특정 카테고리를 삭제할 수 있는 기능 구현하기

 

2. 기능 명세서📜

Name Method URI Domain AuthZ
(특정) 카테고리 삭제 DELETE /category/{categoryId} category ADMIN

- AuthZ는 권한 검사. ADMIN(관리자) 계정만 이용할 수 있는 기능임을 명시.

 

 

3. 1차 코드 작성💻

3.1 CategoryController

// CategoryController
/**
	 * 특정 카테고리 삭제
	 */
	@PreAuthorize("hasRole('ADMIN')")	// ADMIN 검사
	@DeleteMapping("/{categoryId}")
	public ResponseEntity<Void>deleteCategory(@PathVariable Long categoryId) {
		categoryService.deleteCategory(categoryId);
		return ResponseEntity.ok().build();
	}

삭제할 카테고리는 이미 uri에서 표시 중(/{categoryId}).

따라서 @PathVariable을 사용하여 별도의 Response를 통해 카테고리ID를 받을 필요 없다.

 

@PathVariable

  • 경로 변수를 표시하기 위해 메서드에 매개변수에 사용된다.
  • 경로 변수는 중괄호 {id}로 둘러싸인 값을 나타낸다
  • url 경로에서 변수 값을 추출하여 매개변수에 할당한다.
  • 기본적으로 경로 변수는 반드시 값을 가져야 하며, 값이 없는 경우 404 오류가 발생한다.
  • 주로 상세 조회, 수정, 삭제와 같은 작업에서 리소스 식별자로 사용된다.

@PathVariable 이란? (velog.io)

 

@PathVariable 이란?

경로 변수를 표시하기 위해 메서드에 매개변수에 사용된다.경로 변수는 중괄호 {id}로 둘러싸인 값을 나타낸다url 경로에서 변수 값을 추출하여 매개변수에 할당한다.기본적으로 경로 변수는 반

velog.io

 

3.2 CategoryService

// CategoryService
/**
	 * 특정 카테고리 삭제
	 */
	@Transactional
	public void deleteCategory(Long categoryId) {
		Category category = categoryRepository.findById(categoryId)
			.orElseThrow(() -> new CustomException(CATEGORY_NOT_FOUND));
		categoryRepository.deleteById(categoryId);
	}

먼저 해당 카테고리가 존재하는지 확인한다. 존재하지 않는다면 CATEGORY_NOT_FOUND라는 CustomException 발생시키기.

기본적으로 제공하는 CRUD 기능을 이용하여 카테고리 삭제

 

3.3 CategoryRepository

// CategoryRepository

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {

	Optional<Category> findById(Long categoryId);
	void deleteById(Long categoryId);
}

JpaRepository를 상속받아야 한다!

 

 

4. 오류...🤯

 

4.1 오류가 두 개나..

첫 번째 오류

Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement [Referential integrity constraint violation: "FK2N9W8D0DP4BSFRA9DCG0046L4: PUBLIC.ITEM FOREIGN KEY(CATEGORY_ID) REFERENCES PUBLIC.CATEGORY(CATEGORY_ID) (CAST(4 AS BIGINT))"; SQL statement:

 

두 번째 오류

Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FK2N9W8D0DP4BSFRA9DCG0046L4: PUBLIC.ITEM FOREIGN KEY(CATEGORY_ID) REFERENCES PUBLIC.CATEGORY(CATEGORY_ID) (CAST(4 AS BIGINT))"; SQL statement:

 

4.2 원인 살펴보기

먼저 첫 번째 오류부터 알아보자. 챗지피티 짱!ㅎㅎ

이 에러 메시지는 Hibernate에서 발생한 ConstraintViolationException으로, 데이터베이스의 참조 무결성 제약 조건을 위반했음을 나타냅니다. 구체적으로는 외래 키 제약 조건이 위반되었음을 의미합니다.

메시지의 주요 부분을 살펴보면:
- "Referential integrity constraint violation": 참조 무결성 제약 조건 위반.
- "FK2N9W8D0DP4BSFRA9DCG0046L4": 위반된 외래 키 제약 조건의 이름.
- "PUBLIC.ITEM FOREIGN KEY(CATEGORY_ID) REFERENCES PUBLIC.CATEGORY(CATEGORY_ID)": ITEM 테이블의 CATEGORY_ID 외래 키가 CATEGORY 테이블의 CATEGORY_ID를 참조하고 있다는 의미.
- "(CAST(4 AS BIGINT))": 외래 키 제약 조건에 의해 참조하려는 CATEGORY_ID 값이 4인 상황.

이 에러는 ITEM 테이블에 삽입하거나 업데이트하려는 데이터가 CATEGORY 테이블에 존재하지 않는 CATEGORY_ID를 참조하고 있을 때 발생합니다. 즉, ITEM 테이블의 CATEGORY_ID가 CATEGORY 테이블에 존재하지 않는 값을 참조하고 있기 때문에, 데이터베이스가 무결성을 유지할 수 없어 에러가 발생한 것입니다.

해결 방법은 다음과 같습니다:

1. 참조 무결성 확인: ITEM 테이블에 삽입하려는 데이터의 CATEGORY_ID가 실제로 CATEGORY 테이블에 존재하는지 확인합니다.
2. 데이터 검증: CATEGORY_ID가 올바른지, CATEGORY 테이블에 맞는 데이터가 있는지 검토합니다.
3. 데이터 정합성: CATEGORY 테이블에 필요한 데이터가 누락되지 않았는지 확인하고, 필요시 데이터를 추가하거나 수정합니다.

이렇게 하면 외래 키 제약 조건 위반 문제를 해결할 수 있을 것입니다.

그런데 여기서 의문. 나는 category를 삭제하려는데 왜 자꾸 item얘기가 나오는 거지?🤔

item과 관련된 코드는 하나도 건드리지 않았는데??!

카테고리를 삭제할 때 item 얘기가 나오는 이유는 ITEM 테이블이 CATEGORY 테이블을 참조하고 있기 때문입니다. 즉, ITEM 테이블의 CATEGORY_ID 컬럼이 CATEGORY 테이블의 CATEGORY_ID를 외래 키로 참조하고 있어서 카테고리 삭제 시 ITEM 테이블의 참조 무결성을 고려해야 합니다.

쉽게 얘기하자면, category와 item은 서로 연결되어 있다. 그런데 이 상황에서 category를 삭제해버리니까 item이 길 잃고 남겨졌다는 문제이다!!

 

정말 생각지도 못한 오류였다.... 카테고리를 삭제할 때, 카테고리'만' 삭제해서는 안된다니.

당연히 카테고리를 삭제하면 그 안에 속하는 아이템까지 자동적으로 삭제해주는 줄 알았다.

 

4.3 해결책 구상하기

1. 카테고리 삭제하기 전에, item부터 모두 삭제한다.
2. 그 후 category를 삭제 : 연결된 item이 없으므로 안전하게 삭제 가능!
3. 수정사항 db에 반영해주기

 

 

5. 해결!!🥳

기존에 총 4개의 카테고리 존재 && 2번째 카테고리 삭제하기
다시 전체 카테고리를 조회 시, categoryId 2번이 삭제된 것을 확인할 수 있다!!

 

 

6. 최종 코드🍀

CategoryController는 변경사항 없음.

6.1 CategoryService

// CategoryService
/**
	 * 특정 카테고리 삭제
	 */
	@Transactional
	public void deleteCategory(Long categoryId) {
		Category category = categoryRepository.findById(categoryId)
			.orElseThrow(() -> new CustomException(CATEGORY_NOT_FOUND));
		// 카테고리와 관련된 아이템을 먼저 삭제
		itemRepository.deleteByCategoryId(categoryId);

		// 그 후에 카테고리 삭제
		categoryRepository.deleteById(categoryId);
	}

카테고리에 속하는 아이템을 삭제하는 코드! 딱 한 줄만 추가되었다!!

 

6.2 ItemRepository

// ItemRepository
public interface ItemRepository extends JpaRepository<Item, Long> {

	@Modifying
	@Query("DELETE FROM Item i WHERE i.category.id = :categoryId")
	void deleteByCategoryId(@Param("categoryId") Long categoryId);
}

 

 


 

특정 카테고리를 삭제하는 api는 비교적 빠르게 작성했다!! 

 

오류가 처음에 두 개가 떴었는데, 첫 번째 오류를 해결하다보니 두 번째 오류도 자연스럽게 해결되었다.

Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FK2N9W8D0DP4BSFRA9DCG0046L4: PUBLIC.ITEM FOREIGN KEY(CATEGORY_ID) REFERENCES PUBLIC.CATEGORY(CATEGORY_ID) (CAST(4 AS BIGINT))"; SQL statement:

그래도 무슨 오류였는지 알아나 보자.

JdbcSQLIntegrityConstraintViolationException는 데이터베이스에서 외래 키 제약 조건을 위반할 때 발생하는 오류입니다. 메시지에서 Referential integrity constraint violation이라고 명시된 것은 외래 키 제약 조건이 충족되지 않았음을 의미합니다. 특히, FK2N9W8D0DP4BSFRA9DCG0046L4는 데이터베이스에서 정의된 외래 키 제약 조건의 이름입니다.

문제 분석
이 오류는 ITEM 테이블의 CATEGORY_ID 열이 CATEGORY 테이블의 CATEGORY_ID 열과 연결되어 있을 때 발생합니다. CATEGORY_ID를 참조하는 ITEM 레코드가 존재할 때, CATEGORY 레코드를 삭제하려고 하면 외래 키 제약 조건에 의해 삭제가 차단됩니다.

첫 번째 오류와 결국 같은 오류였다!