쇼핑몰 서비스를 개발하다 보면 카테고리 시스템은 필수적인 기능이다.
실제 이용하는 쇼핑몰들(예: 29CM, 네이버 쇼핑, 오늘의 집)을 살펴보면 대부분 계층형 카테고리로 구현되어있다는 것을 쉽게 볼 수 있다.
이번 부트캠프 최종 프로젝트에서 나는 카테고리 기능 구현을 맡아,
일반 카테고리 + MBTI 기반 카테고리를 구현했다.



📌 기능 개요
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 |