개발바닥곰발바닥
728x90

1. JPA를 사용한 카테고리 (하위메뉴) 구현

오픈마켓 프로젝트에서 상품들을 카테고리 별로 분류하기 위해 카테고리 기능을 구현하게 됐다.

카테고리는 depth가 있기 때문에, 하위 메뉴까지 가져올 수 있도록 구현해야 한다.

2. 테이블

계층형 구조를 위해 category 테이블에 자신의 PK를 부모로 삼는 parent 외래키를 넣어줬다.

3. Entity 도메인

<code />
@Entity @Getter @Builder @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; @Column(name = "name") private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent") private Category parent; @Column(name = "depth") private Long depth; @OneToMany(mappedBy = "parent") private List<Category> children = new ArrayList<>(); }

parent를 ManyToOne으로 관계 설정을 해주고, 하위 메뉴들을 가져오기 위해 List<Category> 형식을 가지고 있는 children을 OneToMany(mappedBy = “parent”)로 설정해준다.

4. CategoryResult

<code />
@Getter @AllArgsConstructor @NoArgsConstructor public class CategoryResult { private Long id; private String name; private Long depth; private List<CategoryResult> children; public static CategoryResult of(Category category) { return new CategoryResult( category.getId(), category.getName(), category.getDepth(), category.getChildren().stream().map(CategoryResult::of).collect(Collectors.toList()) ); } }

Entity로 가져온 결과를 컨트롤러에 그대로 전해주면 안되기 때문에, DTO 클래스를 만들어서 Category Entity를 DTO로 변환하기 위한 CategoryResult 클래스를 만들어준다.

5. CategoryRepository

<code />
@Repository @RequiredArgsConstructor public class CategoryRepository { private final EntityManager em; public List<Category> findAll() { return em.createQuery("select c from Category c where c.parent is NULL", Category.class).getResultList(); } }

현재 Spring Data JPA가 아니라 EntityManager를 사용하는 순수한 JPA를 사용하고 있기 때문에, Repository에서 findAll 메소드를 구현해준다.

여기서 where절에 parent가 NULL인 조건을 넣어주는 이유는 parent가 NULL이 아닌 것 까지 가져오면 이미 부모 카테고리가 children을 반환해주는데 하위 메뉴가 밑에서 또 나오기 때문이다.

6. ProductService

<code />
@Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; private final CategoryRepository categoryRepository; @Transactional(rollbackFor = Exception.class) public List<CategoryResult> getCategoryList() { List<CategoryResult> results = categoryRepository.findAll().stream().map(CategoryResult::of).collect(Collectors.toList()); return results; } }

ProductService에서는 CategoryRepository에서 findAll한 결과를 CategoryResult로 변환하여 List로 받아 컨트롤러에 전달해준다.

7. ProductController

<code />
@RestController @RequestMapping("/product") @RequiredArgsConstructor public class ProductController { private final ProductService productService; @GetMapping("/categorys") public ResponseEntity<?> getCategoryList() { return ResponseEntity.ok(productService.getCategoryList()); } }

ProductService에서 받은 리스트를 ResponseEntity로 감싸 Response로 반환해주면 depth가 있는 카테고리 메뉴 구현이 완성된다.

8. 결과

<code />
[ { "id": 1, "name": "도서", "depth": 1, "children": [ { "id": 2, "name": "전공서적", "depth": 2, "children": [ { "id": 5, "name": "컴퓨터시스템과", "depth": 3, "children": [] }, { "id": 6, "name": "컴퓨터정보과", "depth": 3, "children": [] }, { "id": 7, "name": "기계공학과", "depth": 3, "children": [] } { "id": 25, "name": "비서학과", "depth": 3, "children": [] }, { "id": 26, "name": "호텔경영학과", "depth": 3, "children": [] }, { "id": 27, "name": "산업디자인학과", "depth": 3, "children": [] }, ] }, { "id": 3, "name": "교양서적", "depth": 2, "children": [] }, { "id": 4, "name": "일반도서", "depth": 2, "children": [] } ] }, { "id": 31, "name": "의류", "depth": 1, "children": [] }, { "id": 32, "name": "전자제품", "depth": 1, "children": [] } ]

해당 API를 호출하면 위와 같은 Resposne를 받게 된다.

카테고리 별로 하위 메뉴까지 받아오는 것을 확인할 수 있다. 현재 Response에 parent_id가 빠져 있는데, DTO에 parent를 추가해주면 부모 메뉴도 쉽게 받아올 수 있다.

728x90
profile

개발바닥곰발바닥

@bestinu

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!