늦은 프로그래밍 이야기

230104 TIL (계층형 카테고리) 본문

내일배움캠프/TIL, WIL

230104 TIL (계층형 카테고리)

한정규 2023. 1. 5. 01:05

팀프로젝트

계층형 카테고리 만들기

카테리고 기능을 만드는 작업을 하기로 했다. 강의에서는 일반 카테고리를 만들어서 진행을 하였지만, 다른 팀들이 계층형 카테고리를 구현한 것을 보고 계층형 카테고리를 만들고 싶었다.

처음에는 되면 좋고 안되면 그냥 일반 카테고리로 전향해서 만들어야겠다고 생각을 하며 만들기 시작했다.

엔티티에 categoryId, name, layer, parent를 만들어서 layer로 카테고리의 계층을 구분하고 parent에 부모 카테고리의 이름을 넣기로 했다.

public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long categoryId;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private int layer = 0;

    @Column
    private String parent = null;

최상위 부모 카테고리의 layer는 0으로 설정하고 parent는 없으므로 null 값을 주었다.

카테고리를 생성할 때 두개의 메소드로 나누어 최상위 부모 카테고리를 만드는 메소드와 자식 카테고리를 만드는 메소드를 구현하였다.

부모 카테고리를 만들 때는 RequestDto에 name 값만 받아와서 layer와 parent는 기본으로 부여한 값을 사용하였다.

public CategoryResponseDto createCategory(CategoryRequestDto requestDto) {
    Category category = new Category(requestDto);
    categoryRepository.save(category);
    return new CategoryResponseDto(category);
}

자식 카테고리를 만들 때는 부모 카테고리의 id값을 받아와서 부모 카테고리 객체를 불러온 다음 부모 카테고리의 layer에 +1 한 값을 자식 카테고리의 layer로 설정하여 생성하고, 부모 카테고리의 name을 받아와 parent에 넣어 주었다. 처음에는 parent를 넣어주지 않아서 카테고리들을 조회할 때 자신의 부모가 아닌 부모 카테고리에 붙어서 조회되는 결과가 나왔어서 부모 카테고리의 name을 넣어줬다.

public CategoryResponseDto createChildrenCategory(Long categoryId, CategoryRequestDto requestDto) {
    Category parentCategory = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    int layer = parentCategory.getLayer() + 1;
    Category category = new Category(requestDto, layer, parent);
    categoryRepository.save(category);
    return new CategoryResponseDto(category);
}

카테고리 조회에서 많은 어려움을 겪었는데, 처음에는 Repository에서 모든 카테고리를 불러와서 스트림으로 만들어주고 그 스트림 값과 전체 카테고리 리스트를 ResponseDto에 넘겨주었다.

publicList<CategoryListResponseDto> getCategory() {
List<Category> categoryList = categoryRepository.getAllByOrderByNameAsc();
List<CategoryListResponseDto> responseDtoList = categoryList.stream().map(category -> new CategoryListResponseDto(category, categoryList)).collect(Collectors.toList());
    return responseDtoList;
}

ResponseDto에서 받아온 카테고리 리스트를 for문으로 돌려 하나씩 뽑아서 현재 카테고리(부모)의 layer + 1 값과 같으면 ChildrenCategoryList에 넣어주었다.

public CategoryListResponseDto(Category category, List<Category> categoryList) {
    this.categoryId = category.getCategoryId();
    this.name = category.getName();
    this.layer = category.getLayer();

    List<CategoryListResponseDto> categoryListResponseDtoList = new ArrayList<>();
    for (Category c : categoryList) {
        if (category.getLayer() + 1 == c.getLayer()) {
            categoryListResponseDtoList.add(new CategoryListResponseDto(c, categoryList));
        }
    }
    this.childrenCategoryList = categoryListResponseDtoList;
}

자신의 부모 카테고리가 아닌 부모 카테고리에 자식 카테고리가 붙어 조회되는 문제

일단 부모 카테고리에 자식 카테고리의 리스트를 넣어주는데 성공하였지만, 기대한대로 작동하지 않았다.

1 → 2 → 3 4 → 5 → 6으로 부모에서 자식 카테고리를 만들어 주었는데

1 → 2 → 3, 6, 2 → 3, 6, 3 4 → 5 → 6, 5 → 6, 6 이런식으로 최상위 부모 외에도 최상위에 조회가 다시 되는 현상, 자신의 부모 카테고리가 아닌데도 layer가 하위 layer이면 뒤에 붙는 현상이 발생하였다.

자신의 부모 카테고리가 아닌데도 붙는 현상은 parent 값과 부모 카테고리의 name 값을 비교하여 같으면 붙이는 if문을 추가하면 될 것 같았다.

ResponseDto에서 부모 카테고리의 name값과 자식 카테고리의 parent 값을 비교하는 조건을 and 조건으로 추가해주었다.

public CategoryListResponseDto(Category category,List<Category> categoryList) {
    this.categoryId = category.getCategoryId();
    this.name = category.getName();
    this.layer = category.getLayer();

    List<CategoryListResponseDto> categoryListResponseDtoList = new ArrayList<>();
    for (Category c : categoryList) {
        if (category.getLayer() + 1 == c.getLayer() && category.getName().equals(c.getParent())) {
            categoryListResponseDtoList.add(new CategoryListResponseDto(c, categoryList));
        }
    }
    this.childrenCategoryList = categoryListResponseDtoList;
}

최상위 부모 카테고리 외에도 최상위에 조회가 다시 되는 문제

조건을 추가한 후 부모 카테고리가 아닌데도 붙는 현상은 없어졌지만, 최상위 부모 외에도 최상위에 조회가 다시되는 현상은 계속 되었다.

많은 고민 끝에 최상위 부모 외에도 최상위에 조회가 다시 되는 현상은 최상위 부모 카테고리(즉, layer 값이 0인 카테고리)들을 따로 조회하여 스트림에 넣어주면 될 것 같았다. 즉, 시작점을 layer가 0인 카테고리들로 설정하는 것이다.

Repository에서 layer가 0인 카테고리들만 조회한 후 리스트에 넣어 스트림으로 만들어 준 후 부모 카테고리와 모든 카테고리 리스트를 ResponseDto에 넘겨주었다.

publicList<CategoryListResponseDto> getCategory() {
List<Category> categoryList = categoryRepository.getAllByOrderByNameAsc();
List<Category> parentCategoryList = categoryRepository.getCategoriesByLayer(0);
List<CategoryListResponseDto> responseDtoList = parentCategoryList.stream().map(category -> new CategoryListResponseDto(category, categoryList)).collect(Collectors.toList());
    return responseDtoList;
}

이제 원하는대로 부모 카테고리의 리스트 안에 해당 부모 카테고리의 parent 값을 가진 자식 카테고리만 조회가 되고, 최상위 부모 카테고리만 리스트의 맨 앞에 조회가 되었다.

해당 기능을 구현하면서 조회 기능을 ResponseDto의 생성자 부분에서 for문에 재귀를 적용하였는데, 처음 적용해 본 것 치고 작동을 아주 잘해서 만족스러운 경험이었다. 개인과제에서 대댓글 작성 부분에도 적용을 해보아서 확실하게 구현할 수 있게 만들어야 할 것이다.


카테고리 삭제가 원하는대로 작동하지 않는 문제

카테고리 삭제 기능을 구현하면서, 일반적인 삭제는 바로 구현을 했는데 부모 카테고리를 삭제하면 자식 카테고리가 삭제되지 않고 남아있다가 이전 부모 카테고리가 동일한 이름의 카테고리를 생성하면 삭제되지 않고 남아있던 자식 카테고리가 뒤에 붙어서 조회되는 문제가 발생했다.

public void deleteCategory(Long categoryId) {
    Category category = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    String parent = category.getName();
    categoryRepository.deleteById(categoryId);
    categoryRepository.deleteByParent(parent);
}

parent 변수에 부모 카테고리의 name을 담아 repository의 deleteByParent 메소드로 삭제한 부모 카테고리의 이름을 parent 값으로 가지고 있는 자식 카테고리를 삭제해 주는 코드를 작성하고 삭제를 진행해 보았다.

InvalidDataAccessApiUsageException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call 이라는 에러가 발생하며 삭제가 되지 않았다.

인터넷에 검색해보니 @Transactional 어노테이션을 붙여주면 해결이 된다고 한다. 두가지 이상의 삭제 메소드가 있는 경우는 Transactional 어노테이션을 사용해야 하는 것 같다.

@Transactional
public void deleteCategory(Long categoryId) {
    Category category = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    String parent = category.getName();
    categoryRepository.deleteById(categoryId);
    categoryRepository.deleteByParent(parent);
}

@Transactional 어노테이션을 붙여주니 원하는대로 부모 카테고리를 삭제하면 삭제한 부모 카테고리의 자식 카테고리들도 같이 삭제되는 기능이 완성되었다. @Transactional 어노테이션이 일련의 메소드를 한 묶음으로 만들어주는 기능을 한다는 것은 알고 있지만, 아직 자세한 기능은 알지 못해서 @Transactional에 대해 조금 더 공부해야 할 것 같다.


카테고리 수정이 되지 않는 문제

강의에서 예제 코드를 작성할 때나 개인 과제를 진행하였을 때 해당 엔티티의 메소드를 사용하여 값을 변경해주면 DB에도 쿼리가 날라가 값이 바뀌었는데, 이번에는 값이 수정이 되지 않았다.

public void updateCategory(Long categoryId, CategoryRequestDto requestDto) {
    Category category = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    Optional<Category> found = categoryRepository.findByName(requestDto.getName());
    if (found.isPresent()) throw new IllegalArgumentException("중복된 카테고리명이 존재합니다.");

    category.updateCategoryName(requestDto);
}

뭐가 잘못 되어서 안되는지 생각하다가 @Transaction이 해당 엔티티의 값이 변경되는 것을 감지하고 DB로 변경된 값을 update 쿼리를 통해 반영해 준다는 말이 생각이 나서 @Transactional 어노테이션을 붙여 넣어주었더니 수정이 안되는 문제가 해결되었다.

@Transactional
public void updateCategory(Long categoryId, CategoryRequestDto requestDto) {
    Category category = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    Optional<Category> found = categoryRepository.findByName(requestDto.getName());
    if (found.isPresent()) throw new IllegalArgumentException("중복된 카테고리명이 존재합니다.");

    category.updateCategoryName(requestDto);
}

다시 한번 머릿속에 흩어져 있는 @Transactional 어노테이션에 관한 내용과 아직 알지 못하는 관련 내용을 공부하여 정리해야 할 것 같다는 생각이 들었다.


카테고리명 수정 시 자식 카테고리가 부모 카테고리를 잃는 문제

자식 카테고리가 부모 카테고리의 name을 parent 값으로 가지고 포인터 역할을 하기 때문에 부모 카테고리의 name을 수정하니 수정한 부모 카테고리의 자식 카테고리들이 부모를 잃고 조회가 되지 않다가 같은 이름의 부모 카테고리를 생성하니 다시 붙어 나오는 문제가 발생하였다.

해당 부모 카테고리의 이름을 수정했을 때 자식 카테고리들의 거취를 고민해보았다. 해당 자식 카테고리들의 layer를 1씩 낮춰주어서 상위로 끌어 올리려는 생각을 했지만, 이름만 수정한 것이기 때문에 해당 자식 카테고리들도 수정된 부모 카테고리를 따라가는 것이 맞다고 생각이 들어 자식 카테고리의 parent 값을 수정한 부모 카테고리의 name으로 다시 넣어주는 방식으로 선택을 하였다.

@Transactional
public void updateCategory(Long categoryId, CategoryRequestDto requestDto) {
    Category category = categoryRepository.findByCategoryId(categoryId).orElseThrow(
            () -> new IllegalArgumentException("존재하지 않는 카테고리입니다.")
    );
    Optional<Category> found = categoryRepository.findByName(requestDto.getName());
    if (found.isPresent()) throw new IllegalArgumentException("중복된 카테고리명이 존재합니다.");

    String parent = category.getName();
    List<Category> childrenCategoryList = categoryRepository.getCategoriesByParent(parent);
    for (Category childrenCategory : childrenCategoryList) {
        childrenCategory.updateChildrenCategoryParent(requestDto.getName());
    }

    category.updateCategoryName(requestDto);
}

categoryId로 불러온 category의 name을 사용해 자식 카테고리의 parent 값을 통해 자식 카테고리들을 조회하고 for문을 통해 하나씩 parent의 값을 입력한 name 값으로 변경해준 후 부모 카테고리의 이름을 변경하는 코드로 수정 하였다.

제대로 작동하는 것을 확인했지만, Service에서 for문을 통해서 로직을 처리하는 것이 객체지향 관점에서 좋지 않아 보여 category 엔티티에 메소드를 추가하여 리팩토링 하는 것을 고려해 봐야겠다.


'내일배움캠프 > TIL, WIL' 카테고리의 다른 글

230106 TIL (KPT 회고)  (0) 2023.01.06
230105 TIL  (0) 2023.01.05
230103 TIL  (0) 2023.01.04
230102 TIL  (0) 2023.01.02
9주차 WIL  (0) 2023.01.01
Comments