[Toy Project] 자기 참조 Entity: 무한 순환 참조 이슈 해결

최지나·2024년 5월 3일
2

자기 참조 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

profile
의견 나누는 것을 좋아합니다 ლ(・ヮ・ლ)

0개의 댓글