[Spring boot & JPA] 계층형 카테고리 구현하기

2025. 10. 14. 23:01·project

쇼핑몰 서비스를 개발하다 보면 카테고리 시스템은 필수적인 기능이다.

실제 이용하는 쇼핑몰들(예: 29CM, 네이버 쇼핑, 오늘의 집)을 살펴보면 대부분 계층형 카테고리로 구현되어있다는 것을 쉽게 볼 수 있다.

이번 부트캠프 최종 프로젝트에서 나는 카테고리 기능 구현을 맡아, 

일반 카테고리 + MBTI 기반 카테고리를 구현했다.

 

29cm, 네이버 쇼핑, 오늘의 집의 카테고리

📌 기능 개요

1. 일반 카테고리 

  • 3단계 계층 구조(최상위 → 중위 → 하위)
  • 마우스 오버 시 하위 카테고리 동적 표시
  • 계층별 상품 필터링

2. MBTI 카테고리

  • MBTI 유형별 상품 분류(E, I, N, S, T, F, J, P)
  • 각 MBTI별 세부 카테고리 제공

📌 데이터베이스 설계

카테고리 테이블은 자기 참조 구조로 설계했다.

즉, 각 카테고리가 자신의 상위 카테고리를 참조하도록 구성하여 계층형 구조를 표현했다.

카테고리 테이블 일부

CategoryEntity 구조

@Entity
@Table(name="PRODUCT_CATEGORY_TB")
public class CategoryEntity {
    @Id
    private Long categoryNo;           // 카테고리 번호
    
    @Column(name="CATEGORY_NM")
    private String categoryName;        // 카테고리명
    
    @Column(name="MBTI_NM") 
    private String mbtiName;           // MBTI 타입 (E, I, N, S, T, F, J, P)
    
    @Column(name="DEPTH")
    private int depth;                 // 계층 깊이 (1, 2, 3)
    
    // 계층 관계 매핑
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PARENT_CATEGORY_NO")
    private CategoryEntity parent;     // 부모 카테고리
    
    @OneToMany(mappedBy = "parent")
    private List<CategoryEntity> children = new ArrayList<>(); // 자식 카테고리들
}

 

  • depth를 명시적으로 저장하여, 계층 구조 접근 속도를 높였고
  • 부모-자식 관계 매핑으로 전체 계층 구조를 한 번에 구성할 수 있게 했다.

 

📌 백엔드 구현

백엔드 흐름도

1. GlobalController - 전역 데이터 주입

// 모든 컨트롤러 요청에서 categories를 모델에 담음
	// 모든 페이지에서 카테고리 데이터 보여주기 위함
	@ModelAttribute("categories")
	public List<CategoryDto> addCategories() {
		return categoryService.getCategoryMenu();
	}

	// MBTI 카테고리 데이터를 모든 페이지에 추가
	@ModelAttribute("mbtiGroups")
	public Set<String> addMbtiGroups() {
		return categoryService.getCategoriesGroupedByMbti().keySet();
	}

	// MBTI 카테고리 맵을 모든 페이지에 추가
	@ModelAttribute("categoryMap")
	public Map<String, List<CategoryDto>> addCategoryMap() {
		return categoryService.getCategoriesGroupedByMbti();
	}

 

2. CategoryService - 계층 구조 구성

public List<CategoryDto> getCategoryMenu() {
    // 1. 모든 카테고리 조회
    List<CategoryEntity> allCategories = categoryRepository.findAll();
    
    // 2. Map으로 변환하여 O(1) 조회 성능 확보
    Map<Long, CategoryDto> categoryMap = allCategories.stream()
        .map(CategoryDto::new)
        .collect(Collectors.toMap(CategoryDto::getCategoryNo, dto -> dto));
    
    // 3. 계층 구조 구성
    List<CategoryDto> topLevelCategories = new ArrayList<>();
    allCategories.forEach(category -> {
        CategoryDto dto = categoryMap.get(category.getCategoryNo());
        if (category.getParent() == null) {
            // 최상위 카테고리
            topLevelCategories.add(dto);
        } else {
            // 자식 카테고리를 부모에 연결
            Long parentId = category.getParent().getCategoryNo();
            categoryMap.get(parentId).getChildren().add(dto);
        }
    });
    
    return topLevelCategories;
}
  • Map을 활용해 O(1) 조회 속도로 계층 구조를 빠르게 생성
  • 한 번의 DB 조회로 전체 계층 구조를 로드하여 효율성 확보

3. MBTI 카테고리 그룹화

public Map<String, List<CategoryDto>> getCategoriesGroupedByMbti() {
    List<String> mbtiTypes = List.of("E","I","N","S","T","F","J","P");
    Map<String, List<CategoryDto>> result = new LinkedHashMap<>();
    
    for(String mbti : mbtiTypes) {
        List<CategoryDto> categories = categoryRepository.findByMbtiName(mbti)
            .stream()
            .map(CategoryDto::new)
            .toList();
        result.put(mbti, categories);
    }
    return result;
}
  • LinkedHashMap 사용으로 MBTI 타입 순서 고정
  • 조회 시 Map의 O(1) 성능을 활용해 빠른 응답 가능

📌 프론트엔드 구현

html 구조

일반 카테고리 모달

<div id="show_categories">
    <div class="category_menu">
        <!-- 메인 카테고리 -->
        <ul id="main_category">
            <li th:each="topCategory : ${categories}" 
                th:data-category-no="${topCategory.categoryNo}">
                <a th:href="@{/category/{no}(no=${topCategory.categoryNo})}"
                   th:text="${topCategory.categoryName}"></a>
            </li>
        </ul>
        
        <!-- 서브 카테고리 -->
        <div id="sub_category_wrapper">
            <ul th:each="topCategory : ${categories}" 
                th:if="${!topCategory.children.isEmpty()}"
                th:data-parent-no="${topCategory.categoryNo}" 
                class="sub_category_list">
                <li th:each="subCategory : ${topCategory.children}">
                    <a th:href="@{/category/{no}(no=${subCategory.categoryNo})}"
                       th:text="${subCategory.categoryName}"></a>
                </li>
            </ul>
        </div>
    </div>
</div>
  • 마우스 오버 시 하위 카테고리 표시
  • 카테고리 클릭 시 해당 카테고리 상품 페이지로 이동

MBTI 카테고리 모달

최종 프로젝트로 구현한 계층형 카테고리

<div id="show_mbti_categories">
    <div class="mbti_category_menu">
        <!-- MBTI 메인 카테고리 -->
        <ul id="main_mbti_category">
            <li th:each="mbti : ${mbtiGroups}" th:data-mbti="${mbti}">
                <a th:href="@{/mbti/{type}(type=${mbti})}" th:text="${mbti}"></a>
            </li>
        </ul>
        
        <!-- MBTI 서브 카테고리 -->
        <div id="sub_mbti_category_wrapper">
            <ul th:each="mbti : ${mbtiGroups}" 
                th:data-parent-mbti="${mbti}" 
                class="sub_mbti_category_list">
                <li th:each="category : ${categoryMap[__${mbti}__]}">
                    <a th:href="@{/mbti/category/{no}(no=${category.categoryNo})}"
                       th:text="${category.categoryName}"></a>
                </li>
            </ul>
        </div>
    </div>
</div>
  •  MBTI 유형 클릭 시 해당 MBTI 그룹의 세부 카테고리 표시
  • 카테고리 클릭 시 필터링된 상품 목록 페이지로 이동

📌 동작 플로우

1. 페이지 로드 시

  • GlobalController가 모든 페이지에 카테고리 데이터 자동 주입
  • Thymeleaf가 서버 데이터를 HTML에 렌더링
  • JavaScript가 DOM 요소에 이벤트 리스너 등록

2. 일반 카테고리 클릭 시

  • 사용자가 "카테고리" 버튼 클릭
  • 모달 표시 및 첫 번째 카테고리 활성화
  • 마우스 오버 시 해당 서브 카테고리 표시
  • 카테고리 링크 클릭 시 해당 페이지로 이동

3. MBTI 카테고리 클릭 시 

  • 사용자가 "MBTI 카테고리" 버튼 클릭
  • MBTI 타입별로 그룹화된 카테고리 표시
  • MBTI 타입 선택 시 해당 MBTI 페이지로 이동
  • 하위 카테고리 선택 시 MBTI에 속한 해당 카테고리 페이지로 이동

📌기술적 도전

1. 복잡한 계층 구조 처리

문제 : 3단계 계층 구조에서 현재 위치에 따른 사이드바 구성

해결 : CategoryInfo Dto를 통한 계층별 정보 제공

@Getter
@AllArgsConstructor
public class CategoryInfo {
    private final CategoryDto topCategory;
    private final List<CategoryDto> midCategories;
    private final List<CategoryDto> subCategories;
    private final CategoryDto clickedCategory;
}

2. 상품과 도서 통합 표시

문제 : MBTI 페이지에서 상품과 도서를 함께 표시

해결 : ItemDto 통합 DTO 패턴 적용

@Getter
public class ItemDto {
	private final String itemType;
	private final Long itemNo;
	private final String itemName;
	private final Long itemPrice;
	private final String imageUrl;
	private final String businessName;
	
	@Setter
    private boolean isWished = false;
	
	//상품
	public ItemDto( ProductListDto product) {
		this.itemType = "PRODUCT";
		this.itemNo = product.getId();
		this.itemName = product.getName();
		this.itemPrice = (long) product.getPrice();
		this.imageUrl = product.getMainImageUrl();
		this.businessName = product.getBusinessNm();
		this.isWished = product.isWished();
	}
	//도서
	public ItemDto( BookListDto book) {
		this.itemType = "BOOK";
		this.itemNo = book.getId();
		this.itemName = book.getTitle();
		this.itemPrice = (long) book.getPrice();
		this.imageUrl = book.getImgUrl();
		this.businessName = book.getBusinessNm();
		this.isWished = book.isWished();
	}
	
	// mbti 카테고리용 통합
	public ItemDto(SearchResultDto result) {
	    this.itemType = result.getItemType();
	    this.itemNo = result.getItemNo();
	    this.itemName = result.getItemName();
	    this.itemPrice = result.getItemPrice();
	    this.imageUrl = result.getImageUrl();
	    this.businessName = result.getBusinessName();
	    this.isWished = result.isWished();
	}
}

📌정리

처음에는 단순해 보였지만, 계층 구조를 어떻게 설계하고 관리할지, 데이터를 효율적으로 다루는 방법,

UI와 자연스럽게 연결하는 과정까지 고민할 게 많았다.

특히 전역 컨트롤러로 공통 데이터를 관리하는 방식이나,

MBTI 분류를 UI에 녹여내는 과정에서 설계 단계의 중요성을 많이 느꼈다.

기능 하나를 만들더라도 처음 구조를 잘 잡는 게 얼마나 중요한지 실감할 수 있었다.

 

무엇보다도 내가 직접 설계한 구조가 실제 화면에 반영되는 것을 보면서 큰 보람을 느꼈다.

'project' 카테고리의 다른 글

현대이지웰 풀스택 5회차 최종 프로젝트 회고 - MTYPE  (0) 2025.10.21
Intersection Observer API로 무한 스크롤 구현하기  (0) 2025.09.16
'project' 카테고리의 다른 글
  • 현대이지웰 풀스택 5회차 최종 프로젝트 회고 - MTYPE
  • Intersection Observer API로 무한 스크롤 구현하기
eun_log
eun_log
  • eun_log
    개발은
    eun_log
  • 전체
    오늘
    어제
    • 분류 전체보기 (75)
      • 코테 (17)
      • CS (6)
        • 자료구조, 알고리즘 (3)
        • 네트워크 (0)
        • 데이터베이스 (2)
        • 운영체제 (0)
      • frontend (32)
        • JavaScript (29)
        • html&css (1)
        • project_study (2)
      • backend (15)
        • Java (15)
      • project (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
eun_log
[Spring boot & JPA] 계층형 카테고리 구현하기
상단으로

티스토리툴바