개발바닥곰발바닥
728x90

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

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

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

테이블

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

Entity 도메인

@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”)로 설정해준다.

CategoryResult

@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 클래스를 만들어준다.

CategoryRepository

@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을 반환해주는데 하위 메뉴가 밑에서 또 나오기 때문이다.

ProductService

@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로 받아 컨트롤러에 전달해준다.

ProductController

@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가 있는 카테고리 메뉴 구현이 완성된다.

결과

[
    {
        "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

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