자기 참조 Entity란?
- 한 엔티티가 같은 엔티티의 다른 인스턴스를 참조하는 방식. 조직 구조나 카테고리 계층과 같은 계층적 데이터를 표현할 때 유용
- 하지만 자기 참조 구조는 잘못 관리될 경우 무한 순환 참조라는 문제 발생 가능 -> 데이터베이스 쿼리가 무한 루프에 빠짐
무한 순환 참조 예시
WITH RECURSIVE CategoryPath AS (
SELECT category_id, name, parent_category_id
FROM Categories
WHERE name = '필기구'
UNION ALL
SELECT c.category_id, c.name, c.parent_category_id
FROM Categories c
INNER JOIN CategoryPath cp ON cp.parent_category_id = c.category_id
)
SELECT * FROM CategoryPath;
- 위와 같이
필기구
카테고리에서 시작하여 부모 카테고리를 계속해서 추적해 나갈 경우, 필기구
의 부모인 도구
가 다시 필기구
를 부모를 참조하고 있기 떄문에 필기구
와 도구
가 무한히 반복하여 조회된다.
해결 방법
- 먼저 고객과의 prototype을 기반한 meeting 결과 시스템의 복잡성을 관리하기 위해 카테고리의 최대 깊이를 3으로 제한하였다.
- 또한 카테고리 생성/수정 시에 깊이 제한과, 순환 참조의 발생을 방지하는 로직을 추가하여 데이터의 정합성을 유지하였다.
예시: 카테고리 수정
@Transactional
public CommonRes patchCategory(String categoryCode, CategoryPatch patchInput) {
Category previousCategory = commonUtil.findCategoryByCode(categoryCode);
if (patchInput.getParentCategoryCode() != null) {
Category parentCategory = categoryRepository.findByCategoryCode(patchInput.getParentCategoryCode());
if (parentCategory == null) {
return new CommonRes(ResponseCode.NOT_FOUND.getCode(), "부모 카테고리를 찾을 수 없습니다.");
}
if (getCategoryDepth(parentCategory) >= 2) {
return new CommonRes(ResponseCode.BAD_REQUEST.getCode(), "카테고리 깊이가 3을 초과하였습니다.");
}
if (isCircularReference(parentCategory, categoryCode)) {
return new CommonRes(ResponseCode.BAD_REQUEST.getCode(), "서로를 부모 카테고리로 가질 수 없습니다.");
}
}
}
private int getCategoryDepth(Category category) {
int depth = 0;
Category current = category;
while (current.getParentCategory() != null) {
depth++;
current = current.getParentCategory();
if (depth == 2) {
break;
}
}
return depth;
}
private boolean isCircularReference(Category category, String targetCategoryCode) {
Category current = category;
while (current != null) {
if (current.getCategoryCode().equals(targetCategoryCode)) {
return true;
}
current = current.getParentCategory();
}
return false;
}
추가
- 애플리케이션 레벨에서 순환 참조를 방지할 수도 있지만, 데이터베이스 레벨에서도 제약 조건을 설정할 수 있다고 한다.
- 데이터베이스 트리거나 프로시저를 사용하여, 순환 참조나 깊이 제한 로직을 데이터베이스 레벨에서 직접 처리할 수 있다. 이 방법은 코드의 복잡성을 줄여줄 수 있지만, 변경사항을 추적하거나 업데이트하기 어렵고, 디버깅과 테스트가 어렵다는 단점이 존재.
PR 링크: Fix category self recursive error