hyeonga_code

Project_12_카테고리 기능 구현 본문

Project_HYEONGARL

Project_12_카테고리 기능 구현

hyeonga 2024. 6. 18. 05:59
반응형

 

시작. 

현재 작업한 카테고리 로직은 흔하게 사용되는 상위 카테고리 id를 가지고 있어 최상위 카테고리까지 불러오는 방식으로 작성했다. 

멘토님이 하위 카테고리나 상위 카테고리 정보를 가지고 있어야 하는 이유를 물으셨을 때, 카테고리 조회 시에 데이터베이스의 접근을 최소한으로 하기 위해 하위 카테고리 정보를 한 번에 가지고 오려했다고 말씀드렸고, 이에 이어진 접근은 사용자별로 카테고리를 하나의 데이터로 묶어 처리하는 방법은 어떨지에 대해 생각해보라고 하셨다.

 

 

 

 

기존 코드.

Category

더보기
package com.hyeongarl.entity;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name="categories")
@Builder
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long categoryId;

    @Column(name = "category_name", nullable = false)
    private String categoryName;

    @Column(name = "user_id")
    private Long userId;

    @Column(name = "parent_category_id")
    private Long parentCategoryId;

    @Column(name = "level")
    private Integer level;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonBackReference
    @JoinColumn(name = "parent_category_id", insertable = false, updatable = false)
    private Category parentCategory;

    @Builder.Default
    @JsonManagedReference
    @OneToMany(mappedBy = "parentCategory", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Category> subCategories = new ArrayList<>();
}

 

 

CategoryRequestDto

더보기
package com.hyeongarl.dto;

import com.hyeongarl.entity.Category;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class CategoryRequestDto {
    @NotNull(message = "Category 이름은 필수 입력값입니다.")
    private String categoryName;

    private Long parentCategoryId;

    public Category toEntity() {
        return Category.builder()
                .categoryName(categoryName)
                .parentCategoryId(parentCategoryId)
                .build();
    }
}

 


CategoryResponsetDto

더보기
package com.hyeongarl.dto;

import com.hyeongarl.entity.Category;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Map;
import java.util.stream.Collectors;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CategoryResponseDto {
    private Long categoryId;
    private String categoryName;
    private Map<String, CategoryResponseDto> subCategory;

    public static CategoryResponseDto fromEntity(Category category) {
        return CategoryResponseDto.builder()
                .categoryId(category.getCategoryId())
                .categoryName(category.getCategoryName())
                .subCategory(
                        category.getSubCategories() == null ? 
                        	null : category.getSubCategories()
                                .stream()
                                .collect(
                                	Collectors.toMap(
                                    		Category::getCategoryName, 
                                        	CategoryResponseDto::fromEntity)))
                .build();
    }
}

 

 


CategoryRepository

더보기
package com.hyeongarl.repository;

import com.hyeongarl.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
    boolean existsByCategoryName(String categoryName);

    List<Category> findByUserIdAndLevel(Long userId, int level);

    Category findByUserIdAndCategoryId(Long userId, Long categoryId);
}

 


CategoryService

더보기
package com.hyeongarl.service;

import com.hyeongarl.entity.Category;
import com.hyeongarl.error.CategoryAlreadyExistException;
import com.hyeongarl.error.CategoryInvalidException;
import com.hyeongarl.error.CategoryNotFoundException;
import com.hyeongarl.repository.CategoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

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

    public Category save(Category category) {
        category.setUserId(10L);

        if(category.getCategoryName() == null) {
            throw new CategoryInvalidException();
        }

        // 상위 카테고리가 없는 경우
        if(category.getParentCategoryId() == null){
            // 이름 중복 확인
            if(categoryRepository.existsByCategoryName(category.getCategoryName())) {
                throw new CategoryAlreadyExistException();
            }
            category.setLevel(1);
        } else {
            Category parentCategory = categoryRepository.findById(category.getParentCategoryId())
                            .orElseThrow(CategoryNotFoundException::new);
            category.setLevel(parentCategory.getLevel() + 1);
            category.setParentCategory(parentCategory);
            parentCategory.getSubCategories().add(category);
        }
        return categoryRepository.save(category);
    }

    public Map<String, Category> getCategories() {
        return categoryRepository.findByUserIdAndLevel(10L, 1)
                .stream()
                .collect(Collectors.toMap(Category::getCategoryName, Function.identity()));
    }

    public Category getCategory(Long categoryId) {
        Category result = categoryRepository.findByUserIdAndCategoryId(10L, categoryId);

        if(result == null) {
            throw new CategoryNotFoundException();
        }
        return result;
    }

    public Category updateCatgory(Long categoryId, Category categoryRequest) {
        Category category = categoryRepository.findByUserIdAndCategoryId(10L, categoryId);

        if(category == null) {
            throw new CategoryNotFoundException();
        }

        category.setCategoryName(
                categoryRequest.getCategoryName() == null ?
                        category.getCategoryName() : categoryRequest.getCategoryName());
        category.setParentCategoryId(
                categoryRequest.getParentCategoryId() == null ?
                        category.getParentCategoryId() : categoryRequest.getParentCategoryId());

        return categoryRepository.save(category);
    }

    public void deleteCategory(Long categoryId) {
        categoryRepository.deleteById(categoryId);
    }
}

 


CategoryController

더보기
package com.hyeongarl.controller;

import com.hyeongarl.dto.CategoryRequestDto;
import com.hyeongarl.dto.CategoryResponseDto;
import com.hyeongarl.entity.Category;
import com.hyeongarl.service.CategoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequiredArgsConstructor
@RequestMapping("/category")
public class CategoryController {
    private final CategoryService categoryService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public CategoryResponseDto saveCategory(@RequestBody CategoryRequestDto categoryRequest) {
        return CategoryResponseDto.fromEntity(categoryService.save(categoryRequest.toEntity()));
    }

    @GetMapping
    public Map<String, CategoryResponseDto> getCategories() {
        return categoryService.getCategories().entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> CategoryResponseDto.fromEntity((Category) entry.getValue())
                ));
    }

    @GetMapping("/{categoryId}")
    public CategoryResponseDto getCategory(@PathVariable Long categoryId) {
        return CategoryResponseDto.fromEntity(categoryService.getCategory(categoryId));
    }

    @PutMapping("/{categoryId}")
    public CategoryResponseDto updateCategory(@PathVariable Long categoryId,
                                              @RequestBody CategoryRequestDto categoryRequest) {
        return CategoryResponseDto.fromEntity(
        	categoryService.updateCatgory(categoryId, categoryRequest.toEntity()));
    }

    @DeleteMapping("/{categoryId}")
    public void deleteCategory(@PathVariable Long categoryId) {
        categoryService.deleteCategory(categoryId);
    }

}

 

 

+ 적용할 수 있는 방법

1. MySQL의 JSON 컬럼

장점

관계형 데이터베이스의 강점을 활용할 수 있다.

데이터 일관성을 보장할 수 있다.

SQL 쿼리로 직접 JSON 데이터를 다룰 수 있다.

MySQL 의 기존 인프라를 활용할 수 있다.

 

단점

JSON 데이터를 저장하고 조회할 때 성능 저하가 발생할 수 있다.

복잡한 쿼리의 경우 성능이 떨어질 수 있다.

JSON 데이터에 대한 인덱싱이 제한적이다.

 

 

 

2. MongoDB

장점

JSON 형태의 문서를 자연스럽게 저장할 수 있다.

데이터 구조가 유연하며 스키마를 강제하지 않는다.

트리 구조 데이터를 효율적으로 관리할 수 있다.

NoSQL 특성상 수평적인 확장이 용이하다.

 

단점

기존 MySQL과 별도로 MongoDB 인프라를 구축해야 한다.

데이터 일관성을 보장하는 것이 어렵거나 추가적인 작업이 필요할 수 있다.

새로운 기술 스택에 대한 공부를 해야 한다.

 


3. 캐시에 저장하고 캐시에서 조회 (Redis)

장점

매우 빠른 조회 성능을 제공한다.

데이터베이스 부하를 줄일 수 있다.

Redis 같은 인메모리 데이터베이스는 TTL 설정으로 데이터 유효성을 관리할 수 있다.

 

단점

데이터가 변경될 때 캐시를 갱신하는 로직이 필요하다.

캐시 용량에 대한 관리가 필요하다.

캐시 데이터와 실제 데이터베이스 간의 동기화 문제가 발생할 수 있다.

 

 

 

반응형