JustDoEat

[문제해결]카테고리입니다, 셀프조인도 같이 드시면 맛있습니다 본문

Yajoba/Backend

[문제해결]카테고리입니다, 셀프조인도 같이 드시면 맛있습니다

kingmusung 2024. 10. 13. 00:31

개요

 

상품에는 카테고리가 있어야 한다, 중고 XX, 당근 XX을 보면 상품을 등록할 때 카테고리를 등록하잖아요?

 

고민중독이다.. 생각해 보니 카테고리에는 대, 중, 소 분류가 있다.

 

기존 테이블대로 가지고 간다면. 카테고리 테이블 안에 데이터는? 어떻게 넣어줄 거지?라는 생각이 강하게 들었다.

 

" 문자열이니까 "전자기기/노트북/맥북", "식품/육류/소고기" 이런 식으로 다 넣는 건가? "

 

물론 단일 카테고리로 간다면 괜찮지만, 우리는 대, 중, 소로 가기로 했기에 뭔가 이상하다는 생각을 했다.

 

모든 경우의 수를 안에다가 다 넣을 수 없지 않은가..

 


고민을 했던 부분

"프런트엔드에서 상품을 저장할 때 카테고리를 텍스트로 넘겨줌 그리고 디비에 저장"

근데 이건 말이 안 되는 거 같았다, 프런트 엔드에서 카테고리 목록을 제공하고 사용자가 이걸 써서 요청을 보내고, 이 카테고리를 저장한다는 건... 데이터의 중복, 일관성이 당연히 없을 거 같아서 생각을 하자마자, 난 바보인걸 깨달았다.

 

당연히 백엔드에서 카테고리 목록을 만들고, 이걸 프런트엔드에서 쓰게 해야, 추후 데이터의 변동이 있더라도 일관성, 무결성을 유지할 수 있다는 것이다!-

 

"대분류, 중분류, 소분류 3개의 테이블을 만들자"

나의 가녀린 머리로 생각한 거 치고는 훌룡했다, 하지만 카테고리에 대한 정보를 보내주려면 3개의 테이블에서 데이터를 꺼내 와야 하는데 분명히 좋은 방법이 있을 거라고 생각을 하고, 반려를 시켰다.

 

이걸 그대로 실행 한다면 추후에 뇌가 가녀리다는 소리를 들을 거 같았다.


해결

"이럴 때 쓰라고 셀프조인을 배웠잖아!"

셀프조인을 이론으로만 배운 나는 이걸 어느 상황에서 쓰면 좋을지 몰랐었는데 이참에 내 머릿속에 데이터베이스가 생겼다.

 

 Category eat = Category.builder().name("식품").parent(null).build(); //대분류
        categoryRepository.save(eat);

        Category electric = Category.builder().name("전자기기").parent(null).build();
        categoryRepository.save(electric);

        Category food = Category.builder().name("고기").parent(eat).build(); //중분류
        categoryRepository.save(food);
        Category vegetable = Category.builder().name("채소").parent(eat).build();
        categoryRepository.save(vegetable);

        Category beef = Category.builder().name("소고기").parent(food).build(); //소분류
        categoryRepository.save(beef);

 

일단, 대분류, 중분류, 소분류를 나누어서 생각을 해보자.

 

대분류에 "식품", "전자기기"가 있고

중분류에 "고기", "채소"가 있다면, 이 친구의 대분류 즉 부모님은 식품일 것이다.

마찬가지로 소분류의 "소고기"는 중분류의 "고기"가 부모님일 것이다.

 

이걸 서비스랑 연관 지어 생각을 하자면,

 

1. 클라이언트가 카테고리를 선택할 때 먼저 대분류를 누를 것이다, 대분류에 대한 목록만 응답으로 주면 된다. 

    @Query("SELECT c FROM Category c WHERE c.parent IS NULL")
    List<CategoryProjection> findAllNoParent();
    // 대분류만 꺼냄, 대분류는 부모님이 안계심

 

 

보다시피 대분류는 아쉽게도 부모님이 안 계신다.

 

쿼리를 날릴 때 parent_id = null 인 값만 보내준다면 그건 대분류일 것이다.

 

2. 클라이언트가 대분류에서 식품을 클릭한다면, 식품의 PK값을 FK로 가지고 있는 데이터를 응답으로 주면 중분류가 나올 것이다.

    @Query("SELECT c.id AS id, c.name AS name FROM Category c WHERE c.parent.id = :parentId")
    List<CategoryProjection> findBelowCategory(@Param("parentId") Integer parentId);

 

3. 클라이언트가 이제 중분류에서 고기를 선택한다면, 고기의 PK값을 FK로 가지고 있는 데이터를 응답으로 주면 마찬가지로 소분류가 나올 것이다.

 

+ 아이디어는 얻었으니 엔티티 수정은 아래와 같이 했다.

 

일단 테이블 구조를 바꿔야 한다. 카테고리에는 본인 자신을 의례키로 가질 필드를 만들어준다.

public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(nullable = false, unique = true)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY) // 셀프조인을 위해 추가된거임
    @JoinColumn(name = "parent_id")
    private Category parent;

}

 

상품에는 대분류 중분류 소분류를 가지고 있을 필드를 만들어준다.

public class product{

전 부 생 략

@ManyToOne(fetch = FetchType.LAZY)  // 대분류와 다대일 관계 설정
    @JoinColumn(name = "b_category_id", nullable = false) // 외래 키 이름 수정
    private Category bCategory;

    @ManyToOne(fetch = FetchType.LAZY)  // 중분류와 다대일 관계 설정
    @JoinColumn(name = "m_category_id", nullable = false) // 외래 키 이름 수정
    private Category mCategory;

    @ManyToOne(fetch = FetchType.LAZY)  // 소분류와 다대일 관계 설정
    @JoinColumn(name = "s_category_id", nullable = false) // 외래 키 이름 수정
    private Category sCategory;
    }

 


이번에도 이론과 적용은 다른 범주라고 한번 더 느꼈다, 물론 나의 공부량이 적은 것도 문제일수도 있겠다, 이론을 실전에 적용을 하려면 많은 경험이 필요한 거 같다. 솔직히 셀프조인에 대해서 배웠지만, 바로 떠오르지 못했다. 이런 이벤트가 생길수록 이론이 실전무기로 변하는 과정이 되는 거 같다.