Backend

Spring Jpa SelfJoin 순환참조 방지하며 다른 엔티티와 맵핑하기

연_우리 2022. 11. 9. 01:43
반응형

목차



    보통 셀프조인은 카테고리처럼 1차, 2차, 3차.... 무한정으로 늘어날 수 있을 때 사용되는데,
    하나의 테이블로 모든 관계를 정의할 수 있어서 유용하게 쓰인다.

    셀프조인 형태로 구현한 카테고리 엔티티메뉴 엔티티를 맵핑하고 Json을 내려주는 과정에서
    어떻게 순환참조를 피할 수 있을까??


    예시데이터

    카테고리


    메뉴




    내가 원하는 응답값

    카테고리

    {
        "message": "카테고리 정보를 조회하였습니다.",
        "data": {
            "id": 2,
            "categoryCode": "soup_stews",
            "categoryName": "찜_탕_찌개",
            "parent": {
                "id": 1,
                "categoryCode": "koreanfood",
                "categoryName": "한식",
                "parent": null,
                "childList": null
            },
            "childList": [
                {
                    "id": 3,
                    "categoryCode": "stew",
                    "categoryName": "찌개",
                    "parent": null,
                    "childList": null
                },
                {
                    "id": 4,
                    "categoryCode": "steamed",
                    "categoryName": "찜",
                    "parent": null,
                    "childList": null
                }
            ]
        }
    }

    - 찌개에서도 parent를 타고 들어갈 수 있지만 depth를 지정하여 조회하고 싶다
    - parent와 child가 필요할때만 값을 조회하고 싶다




    메뉴
    카테고리별로 조회할 경우

    더보기
    {
        "message": "메뉴 정보 리스트를 조회하였습니다.",
        "data": {
            "content": [
                {
                    "createdDateTime": "2022-11-09 00:32:19",
                    "modifiedDateTime": "2022-11-09 00:32:19",
                    "id": 1,
                    "menuCode": 111,
                    "menuName": "김치찌개",
                    "description": "김치찌개입니다.",
                    "categoryCode": "stew",
                    "categoryId": 3,
                    "categoryName": "찌개"
                },
                {
                    "createdDateTime": "2022-11-09 00:32:41",
                    "modifiedDateTime": "2022-11-09 00:32:41",
                    "id": 2,
                    "menuCode": 112,
                    "menuName": "된장찌개",
                    "description": "된장찌개입니다.",
                    "categoryCode": "stew",
                    "categoryId": 3,
                    "categoryName": "찌개"
                }
            ],
            "currentPage": 1,
            "totalPages": 1,
            "pageSize": 10,
            "firstPage": true,
            "lastPage": true
        }
    }


    그냥 모든 메뉴 조회할 경우

    더보기
    {
        "message": "메뉴 정보 리스트를 조회하였습니다.",
        "data": {
            "content": [
                {
                    "createdDateTime": "2022-11-09 00:32:19",
                    "modifiedDateTime": "2022-11-09 00:32:19",
                    "id": 1,
                    "menuCode": 111,
                    "menuName": "김치찌개",
                    "description": "김치찌개입니다.",
                    "categoryCode": "stew",
                    "categoryId": 3,
                    "categoryName": "찌개"
                },
                {
                    "createdDateTime": "2022-11-09 00:32:41",
                    "modifiedDateTime": "2022-11-09 00:32:41",
                    "id": 2,
                    "menuCode": 112,
                    "menuName": "된장찌개",
                    "description": "된장찌개입니다.",
                    "categoryCode": "stew",
                    "categoryId": 3,
                    "categoryName": "찌개"
                },
                {
                    "createdDateTime": "2022-11-09 00:33:01",
                    "modifiedDateTime": "2022-11-09 00:33:01",
                    "id": 3,
                    "menuCode": 113,
                    "menuName": "잔치국수",
                    "description": "잔치국수입니다.",
                    "categoryCode": "noodle",
                    "categoryId": 5,
                    "categoryName": "국수"
                },
                {
                    "createdDateTime": "2022-11-09 00:33:10",
                    "modifiedDateTime": "2022-11-09 00:33:10",
                    "id": 4,
                    "menuCode": 114,
                    "menuName": "칼국수",
                    "description": "칼국수입니다.",
                    "categoryCode": "noodle",
                    "categoryId": 5,
                    "categoryName": "국수"
                },
                {
                    "createdDateTime": "2022-11-09 00:34:02",
                    "modifiedDateTime": "2022-11-09 00:34:02",
                    "id": 5,
                    "menuCode": 115,
                    "menuName": "아구찜",
                    "description": "아구찜입니다.",
                    "categoryCode": "steamed",
                    "categoryId": 4,
                    "categoryName": "찜"
                },
                {
                    "createdDateTime": "2022-11-09 00:34:12",
                    "modifiedDateTime": "2022-11-09 00:34:12",
                    "id": 6,
                    "menuCode": 116,
                    "menuName": "찜닭",
                    "description": "찜닭입니다.",
                    "categoryCode": "steamed",
                    "categoryId": 4,
                    "categoryName": "찜"
                }
            ],
            "currentPage": 1,
            "totalPages": 1,
            "pageSize": 10,
            "firstPage": true,
            "lastPage": true
        }
    }


    - 카테고리별로 조회할수도 있어야하지만 그냥 전체를 조회할 수도 있어야한다.
    - 그러기 위해선 객체마다 카테고리 정보가 있어야한다.




    내가 생각한 아이디어 첫번째 : 카테고리 순환참조 방지는 DTO로

    public class CategoryDto {
    
    	.....중략
        
        /**
         * 순환참조를 방지하기 위해 따로 정의한다
         */
        @Data
        private static class JsonRes {
            private Long id;
            private String categoryCode;
            private String categoryName;
            
            private JsonRes parent;            //JsonRes이다. 주의! 
            private List<JsonRes> childList;   //JsonRes이다. 주의!
            
            public JsonRes(Category category, boolean getParent, boolean getChild, Integer depth) {
                this.id = category.getId();
                this.categoryCode = category.getCategoryCode();
                this.categoryName = category.getCategoryName();
    
                if(depth > 0){               //depth가 0이 될때까지만 조회한다.
                
                    if(getParent){           //getParent가 true일때만 부모를 조회한다
                        Category parent = category.getParentCategory();
                        if(parent != null){  //부모를 조회할 때, 부모가 있을때만 JSON을 생성한다.
                            this.parent = new JsonRes(parent, getParent, false, depth-1);
                            //child는 아래에서 따로 구할거니까 false로 고정한다.
                        }
                    }
    
                    if(getChild){                //getChild가 true일때만 부모를 조회한다
                        List<Category> childList = category.getChildCategoryList();
                        if(childList != null){   //자식을 조회할 때, 자식이 있을때만 JSON을 생성한다.
                            this.childList = childList.stream()
                                    .map(child -> new JsonRes(child, getParent, true, depth-1))
                                    .collect(Collectors.toList());
                                    //자식 전부 구할거니까 true로 고정한다.
                        }
                    }
                }
    
            }
        }
    
    	.....중략
    
    }




    내가 생각한 아이디어 두번째 : 메뉴-카테고리 맵핑 시 카테고리 내용은 getter로

    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Table(name = "menu")
    @DynamicUpdate
    public class Menu extends BaseTimeEntity {
    
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "id", columnDefinition = "bigint comment '아이디'")
        private Long id;
        
        @Column(name = "menu_code", columnDefinition = "bigint comment '메뉴코드'")
        private Long menuCode;
        
        @Column(name = "menu_name", columnDefinition = "varchar(255) comment '메뉴명'")
        private String menuName;
        
        @Column(name = "description", columnDefinition = "text comment '메뉴설명'")
        private String description;
    
        @JsonIgnore
        @Getter(AccessLevel.PRIVATE)     //category 자체 getter 접근 막는다
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "category_id", columnDefinition = "bigint comment '카테고리 아이디'")
        private Category category;
    
        @Column(name = "category_code", columnDefinition = "varchar(255) comment '카테고리 코드'")
        private String categoryCode;
    
        @Transient  //DB에 저장하지 않고 사용하는 컬럼
        private Long categoryId;
    
        @Transient  //DB에 저장하지 않고 사용하는 컬럼
        private String categoryName;
    
    
        @Builder
        public Menu(Long id, Long menuCode, String menuName, String description, Category category) {
            this.id = id;
            this.menuCode = menuCode;
            this.menuName = menuName;
            this.description = description;
            this.category = category;
            this.categoryId = category.getId();
            this.categoryCode = category.getCategoryCode();  
            this.categoryName = category.getCategoryName();
        }
    
        public String getCategoryCode() {
            return this.category.getCategoryCode(); //getter 정의
        }
    
        public Long getCategoryId() {
            return this.category.getId();           //getter 정의
        }
    
        public String getCategoryName() {
            return this.category.getCategoryName();  //getter 정의
        }
    }





    전체 코드

    GitHub - lotu-us/category-menu-sample

    Contribute to lotu-us/category-menu-sample development by creating an account on GitHub.

    github.com





    테스트

    카테고리 조회 depth 확인


    카테고리 getChild, getParent 동작 확인




    메뉴 카테고리별 조회 / 일반조회








    이것저것 해보다가 이런 방법도 있겠구나~~ 해서 정리해봤다!

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기