본문 바로가기

[ BE ] 기술

[BE-기술] 자기참조를 활용한 계층형 카테고리 구현

안녕하세요 Not-Error 064 백엔드팀의 황윤준입니다. 

이번 채식이들 쇼핑몰 프로젝트에서 카테고리 자기 자신이 parent이자 child인 방식을 활용하여 하위 카테고리 조회 시 상위 카테고리가 노출되며, 상위 카테고리에서도 하위 카테고리 조회가 가능한 방식을 구현하였습니다.

✅  무한뎁스 카테고리 구현 방법을 활용하면

  • 하나의 테이블로 전체 카테고리 관리가 가능합니다.
  • 카테고리를 자유롭게 만들고 삭제할 수 있습니다.
  • 카테고리 계층구조를 표현해 자기 자신이 포함된 상하위 카테고리를 조회하기 편합니다.
  • 하나의 테이블에서 제작할 수 있기에 admin 페이지에서 운영자가 직접 카테고리를 추가 수정하는 api를 만들기에 적합합니다.

이번 카테고리 구현을 통해, 현시점에서 요구되는 개발요구사항을 넘어 카테고리의 목록과 구성을 자유롭게 확장하고 축소할 수 있는 확장성 있는 개발에 대해 많은 생각을 하게되었습니다

☑️ Table(Entity) 구현

▪️ Category(Enitity)

  1. 요구사항
    • 무한하게 계층을 표현할 수 있는 무한뎁스 카테고리를 구현합니다.
    • 카테고리를 depth에 따라 분류하고 조회할 수 있도록 구현합니다.
  2. 구현방법
    • 하나의 테이블에서 구현해야 하기 위해 self-join 사용합니다.
    • 자기참조를 이용해 parent와 child 관계를 이어줍니다.
    • Spring data JPA를 활용해 연관관계를 설정합니다.
@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long categoryId;

    @Column
    private String categoryName;

    @ManyToOne (fetch = FetchType.LAZY) // 1.
    @JoinColumn
    @Nullable
    //@JsonManagedReference
    private Category parent; // 2.

    /**
     * 자식 카테고리, 하위 카테고리를 List 형태로 호출
     */
    @OneToMany(mappedBy = "parent") // 3.
    //@JsonBackReference
    private List<Category> children = new ArrayList<>(); // 2.

    /**
     * 카테고리 깊이
     */
    @Column
    private Long depth;

😀 key points

@ManyToOne (fetch = FetchType.LAZY)

  1. JPA연관관계에서 ManyToOne, OneToOne은 fetchType.EAGER가 default이므로 LAZY 로 바꿔 줍니다. 지연 참조를 사용해줘야 n+1 문제를 방지할 수 있습니다.

parent, children

  1. 한 테이블 내에서 상하위 관계 표현을 위해 parent와 children을 설정해줍니다.
  2. children 칼럼에 mappedBy 속성을 활용해 parent를 연관관계의 owner로 설정해줍니다.

 

☑️ Repository Interface 구현

▪️ Category Repository

@Repositorpublic interface CategoryRepository extends JpaRepository<Category, Long> { //.1
    List<Category> findByDepth(Long depth);
}

😀 key points

JpaRepository

  1. JPA기능을 활용하기 위해 JPARepository를 상속해줍니다.

findByDepth

  1. spring data jpa는 메소드 이름으로 검색 조건을 만들어 줍니다. 여기서 parameter에 값만 제대로 넣어주면 직관적으로 조회를 해줍니다.
    • find + by + entity의 필드값
    • 여기서는 depth 레벨을 이용해 조회를 합니다.

 

☑️ Response DTO 구현

▪️ Category Response DTO

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class CategoryResponseDto { // 1.
    private Long categoryId;
    private String categoryName;
    private Long depth;
    private List<CategoryResponseDto> children;

/**
     *카테고리를 전달할 DTO를 정의한 정적 메서드
*/
public static CategoryResponseDto of(Category category){
        return new CategoryResponseDto(
                category.getCategoryId(),
                category.getCategoryName(),
                category.getDepth(),
                category.getChildren().stream().map(CategoryResponseDto::of).collect(Collectors.toList())
        );
    }
}

😀 key points

  1. Entity는 절대 컨트롤러에 그냥 전달해주지 않습니다. DTO 클래스를 만들어서 필요한 요소만 전달해줍니다. 여기서는 모두 전달해줍니다.

 

☑️ Service 구현

▪️ Category Service

@Service
@Transactional
@RequiredArgsConstructor
public class CategoryServiceImpl implements CategoryService{
    private final CategoryRepository categoryRepository; // 1.

    @Override
    public List<CategoryResponseDto> findCategoryList() {
        List<CategoryResponseDto> results = categoryRepository.findByDepth(1L).stream().map(CategoryResponseDto::of).collect(Collectors.toList()); // 2.
    return results;
    }
}

😀 key points

  1. CategoryRepository를 주입해줍니다.
  2. CategoryRepository에서 조회한 결과를 CategoryResponseDto로 변환하여 List로 받아 컨트롤러에 전달해줍니다. 여기서는 스트림을 이용해 List 형태로 반환하였습니다.

 

☑️ Controller 구현

▪️ Category Controller

@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/category")
public class CategoryController { 
    private final CategoryService categoryService; // 1.

/**
     *카테고리 전체조회
*/
@GetMapping
    public ResponseEntity getCategoryList() { // 2.
        List <CategoryResponseDto> response = categoryService.findCategoryList();
        return new ResponseEntity(
                new SingleCategoryResponse(response),
                HttpStatus.OK);
    }
}

😀 key points

  1. CategoryService를 주입해줍니다.
  2. CategoryService에서 받은 리스트를 ResponseEntity로 감싸 Response로 반환해주면 depth가 있는 카테고리 메뉴가 구현됩니다. 최종으로는 이 로직을 제품 콘트롤러에 통합해주면 됩니다.

최종 결과물

최종적으로 위와같은 계층형 구조로 카테고리를 전달할 수 있습니다.