JPA 사용한 카테고리 구현 (infinite depth) - 02

Joshua_Kim·2021년 7월 25일
1
post-thumbnail
post-custom-banner

"민재씨 무한뎁스로 카테고리 로직 구현해봐요"

입사 후 공부하며 기술 스텍을 쌓던 내게 던져진 첫 실무 과제.

stack : Springboot 2.3.6 RELEASE, gradle, JPA, mysql ...


👉🏻 1편 JPA 사용한 카테 고리 구현 (링크 click)

저번시간에 이어 이번에는 Service 계층의 로직을 포스팅할 것이다.
그 전에 DTO 클래스를 구현하자 😊


#3. DTO 클래스 구현

@Getter
@Setter
@NoArgsConstructor
public class CategoryDTO {

    private Long categoryId;
    private String branch;
    private String code;
    private String name;
    private String parentCategoryName;
    private Integer level;
    private Map<String, CategoryDTO> children;

    public CategoryDTO (Category entity) {

        this.categoryId = entity.getId();
        this.branch = entity.getBranch();
        this.code = entity.getCode();
        this.name = entity.getName();
        this.level = entity.getLevel();
        if(entity.getParentCategory() == null) {
           
            this.parentCategoryName = "대분류";

        } else {
          
            this.parentCategoryName = entity.getParentCategory().getName();

        }

        this.children = entity.getSubCategory() == null ? null :
                entity.getSubCategory().stream().collect(Collectors.toMap(
                Category::getName(), CategoryDTO::new
                        
        ));
    }

    public Category toEntity () {
        return Category.builder()
                .branch(branch)
                .code(code)
                .level(level)
                .name(name)
                .build();
    }
}

key points

1. children을 map으로 변환

  • Entity 클래스에서는 subCategory를 list 타입으로 생성하였다.
  • 하지만 Service 로직에서 get 메소드를 통해 front에 json형식으로 데이터를 넘길때, Map 구조로 넘겨달라는 요구 사항이 있었기에 DTO에서 stream을 통해 변환시킬 예정이다. (후술예정)

2. entity.getParentCategory이 null 일 경우

  • parentCategory가 null 일 경우는 최초에 대분류를 생성했을 때의 경우다.
  • 이 경우 아래에서 포스팅할 Service 로직에서 ROOT를 생성하는데, entity에서 조회할때는 null값이 조회되어 그 끔찍한 NPE가 발생한다. 😤
  • 그렇기 때문에 if 조건을 달아서 dto를 통해 생성해준다.

3. List -> Map으로 변환 ! ✍🏻

  • DTO 클래스에서 가장 중요한 로직이다.
  • 요구 스펙에 따라 children category를 list가 아닌, map으로 반환해야한다.
  • 아래는 요구 스펙의 예시다
{
  "ROOT": {
        "categoryId": 1,
        "branch": "coupang",
        "code": "ROOT",
        "name": "ROOT",
        "parentCategoryName": "대분류",
        "level": 0,
        "children": {
            "1": {
                "categoryId": 2,
                "branch": "coupang",
                "code": "1",
                "name": "clothes",
                "parentCategoryName": "ROOT",
                "level": 1,
                "children": {}
            }
        }
    }
  }
  • entity.getSubCategory().stream().collect(Collectors.toMap(Category::getCode, CategoryDTO::new));
    • 우선은 List를 stream()을 통해 collect로 Collectors.toMap 을 사용해 map으로 바꾸겠다고 알려준다.
    • map은 key와 value가 있어야하는데, 요구 스펙의 key 타입은 String, value는 CategoryDTO 다.
    • key -> Category::getCode 는 사실 category -> category.getCode()로 풀어쓸수있다.
    • value -> 여기서 관건은 Entity인 Category를 DTO인 CategoryDTO로 변한하는 것.
      DTO 클래스에 생성자의 매개변수로 Entity를 이미 생성해놨으므로 이것을 이용하면된다.
      CategoryDTO::new는 사실 category -> new CategoryDTO (category) 로 풀어쓸 수 있다.

4. toEntity 메소드

  • builder를 사용하여 명시적으로 매개변수를 넣어줄 수 있게끔 만들어준다.


#4-1. Service - saveCategory 구현

드디어, service 로직 포스팅을 한다. 😃
우선 saveCategory 메소드를 살펴보자.
리팩토링 과정도 쓸까 하다가 쓰면서 주섬주섬 생각나는 것들을 포스팅해보겠다.

@Service
@RequiredArgsConstructor
@Transactional
public class CategoryService {
  private final CategoryRepository categoryRepository;

  public Long saveCategory (CategoryDTO categoryDTO) {

      Category category = categoryDTO.toEntity();
//대분류 등록
      if (categoryDTO.getParentCategoryName() == null) {

          //JPA 사용하여 DB에서 branch와 name의 중복값을 검사. (대분류에서만 가능)
          if (categoryRepository.existsByBranchAndName(categoryDTO.getBranch(), categoryDTO.getName())) {
              throw new RuntimeException("branch와 name이 같을 수 없습니다. ");
          }
//orElse로 refactor
          Category rootCategory = categoryRepository.findByBranchAndName(categoryDTO.getBranch(),"ROOT")
                  .orElseGet( () ->
                          Category.builder()
                          .name("ROOT")
                          .code("ROOT")
                          .branch(categoryDTO.getBranch())
                          .level(0)
                          .build()
                  );
          if (!categoryRepository.existsByBranchAndName(categoryDTO.getBranch(), "ROOT")) {
              categoryRepository.save(rootCategory);
          }
          category.setParentCategory(rootCategory);
          category.setLevel(1);
 //중, 소분류 등록
      } else {
          String parentCategoryName = categoryDTO.getParentCategoryName();
          Category parentCategory = categoryRepository.findByBranchAndName(categoryDTO.getBranch(), parentCategoryName)
                  .orElseThrow(() -> new IllegalArgumentException("부모 카테고리 없음 예외"));
			category.setLevel(parentCategory.getLevel() + 1);
          category.setParentCategory(parentCategory);
          parentCategory.getSubCategory().add(category);
      }

      //category.setLive(true);
      return categoryRepository.save(category).getId();
  }

key points

1. 대분류, 소분류일 경우를 구분

  • '대분류' 카테고리일 경우에는 두가지를 생각해야한다.
  • 해당 branch의 최초 대분류 카테고리일 경우 ROOT를 생성해야한다.
    • ROOT를 생성하는 이유는, 고객이 조회할때 branch의 이름으로만 조회하면 하위 카테고리가 모두 조회되게끔 만들 것이기 때문이다. (아마 이렇게 말해도 이해가 안될 것이다. 뒤에 나올 get 메소드와, api controller 계층에서 더 자세히 설명할 예정이다.)
  • 최초 대분류 카테고리가 아닐 경우, ROOT를 생성하지 않고, 그 ROOT의 자식으로 들어가야한다.
  • 소분류일 경우에는 level을 부모보다 1 더하여 저장해야한다.

2. orElseGet, orElseThrow 활용

  • 대분류 등록시, ROOT를 찾지 못할 경우 ROOT를 builder를 활용하여 생성하도록 로직을 짰다.
  • 이 경우 optional 타입으로 반환하는 repository의 메소드를 orElseGet을 사용하여 ROOT가 없을 경우에 생성하도록 refactor 하였다.

3. existsByBranchAndName 을 활용

  • 데이터의 비교는 DB단에서 최대한 거른 다음에 가져오는 것이 빠르다.

    for (Category temp : categories()) {
           if (temp.getBranch().equals(category.getBranch()) && temp.getName().equals(category.getName())){
               throw new RuntimeException("branch와 name이 같은 대분류는 있을 수 없습니다.");
                  }
    }
    	```
  • 원래는 위와 같은 로직을 짜려고 했는데, 이렇게 할경우 데이터를 왕창 다가져와서 비교하므로 느리다.

  • 최대한 DB에서 처리할 수 있는 것은 처리해서 뱉는 것이 좋다. 그렇기 때문에 CategoryRepository 인터페이스에서 boolean타입의 메소드를 생성한 것이다. (1편 참조 링크 click)


    다음 편에서는 Service 계층을 완성해보겠다.
profile
인문학 하는 개발자 💻
post-custom-banner

0개의 댓글