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를 추가해주면 부모 메뉴도 쉽게 받아올 수 있다.
'JAVA > Spring' 카테고리의 다른 글
[Spring] DTO 클래스 깔끔하게 관리하기 (0) | 2022.05.07 |
---|---|
[Spring] Spring Data Redis로 JWT RefreshToken 관리하기 (0) | 2022.04.12 |
[Spring Boot] 스프링 부트 외부 접속 안되는 문제 (0) | 2022.04.07 |
[Spring] JPA Column default 값 적용 안되는 문제 (0) | 2022.04.04 |
[Spring] IoC와 DI의 이해 (0) | 2022.04.01 |