Floney

플로니 - 끝나지 않은 카테고리 삽질기(상속관계 @Builder)

딤섬뮨 2023. 4. 30. 21:46
728x90

아...지난 화에 이어 카테고리를 드디어 끝내나 했는데,

 

문제가 발생했다.

 

설계에 의문을 가질 때 아는 가장 쉬운 방법은 문제가 생기면 잘못된 설계라고 조언을 듣자마자 바로 문제가 생겼다 정답!

 

일단, 불행 중 다행으로 분리까진 괜찮은 도전 같다 하지만,

 

가계부 내역 생성을 할 때, OneToMany로 카테고리를 묶으려고 하니 문제가 생긴다.

 

만약 프론트에서 "선물"이라는 카테고리의 가게부 내역 선택해서 보냈다면,

현재 가계부의 고유한 카테고리는 BookCategory라는 Entity로

모든 가계부의 공통 카테고리는 Category로 정했기에

'선물'이라는 값이 들어오면 이 선물이 어떤 entity인지 BookCategory인지 Category인지 알기 위해 탐색을 두번 거쳐야한다.

 

(1) 기본 카테고리에서 탐색 => 없다면?

(2) 커스텀 카테고리에서 탐색의 형태로 이상한 형태가 이어지게된다

 

또한 가계부 내역 하나당, 카테고리를 여러개 고르는데(One To Many)

"지출","은행","선물" 이렇게 골랐을 경우 지출,은행은 기본 카테고리이고, 선물은 커스텀 카테고리라면?

 

하나의 자료구조에 묶을 수도 없고, 이게 어떤 카테고리인지 매번 유동적으로 서버에서 파악해서 매핑을 할 수 도 없다.

 

이를 절충하기 위한 조건은 기본 카테고리와 커스텀 카테고리는 같은 자료형을 써야한다는 것이다.

그래서 생각한게 둘을 상속관계로 만들어보자.

 

JPA에서는 RDBMS에서 상속을 비스무리 구현하기 위해 방법이 세가지 정도가 있다. 그 중 고민이 되었던 부분은 2가지였다.

(1) Single Table 전략

하나의 테이블안에 구분 컬럼(DTYPE)을 활용하여 다 넣는 전략

 

(2) 조인 전략 

각각 테이블을 만들어서 사용한다.

 

처음에 조인 전략으로 사용하긴 했는데, 우선 우리의 자식 테이블의 양이 많지 않을 것이고, 조인 전략을 사용하면 Insert문이 2번 날라가서 과한설계같았다.

 

그래서 택한 것은 1번이다.

 

 

 

이 모습을 구현하기 위해 다양한 삽질을 거쳤었다

 

(1) Builder문제

 

우리는 보통 dto에서 entity를 변환할 때, builder를 많이 사용한다.

 

하지만 상속관계를 거치면서 부모와 자식이 모두 @builder를 사용하게 되면, 부모의 Builder와 return 값이 호환되지 않는다는 에러가 나게 된다. 

The return type is incompatible with 부모클래스.builder()

 

이유는 자식 클래스의 Builder가 부모 클래스의 빌더를 상속하면서 같은 이름의 builder가 두개니 충돌이 일어나는 상황이다

따라서 @Builder(builderMethodName = "childBuilder)이런식으로 명시해줄 수 있다.

 

아니면, @SuperBuilder를 사용하면 된다. 이때 주의할 점은 @Builder과 같이 사용할 수는 없다.

처음에 @Builder도 사용하고 @SuperBuilder도 달았다가 에러를 보았다..

 

Lombok @Builder with Inheritance | Baeldung

 

 (2) 부모 클래스 추상화

 

처음에 부모 클래스인 Category 자체를 instance화 시킬려고 했는데 부모 클래스는 객체화 시키지 않는게 맞다고 한다. 부모 클래스가 가지는 역할은 오로지 공통 속성이나 동작을 정의하기 위함이지, 객체를 갖는건 아니다.

그래서, 객체 생성을 막기 위해 abstact클래스로 만들어주었다.

 

따라서 defaultCategory를 따로 만들어주었다

 

 

Category 부모 클래스

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Inheritance()
@SuperBuilder
public abstract class Category {

    @Id
    @GeneratedValue
    @Column(nullable = false)
    private Long id;

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Category parent;
    
    }

Default Category

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@SuperBuilder
@DiscriminatorValue("Default")
public class DefaultCategory extends Category {

    public static DefaultCategory rootParent() {
        return new DefaultCategory();
    }
}

BookCategory

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@SuperBuilder
public class BookCategory extends Category {

    @ManyToOne
    private Book book;

    public BookCategory(String name, Category parent, Book book) {
        super(name,parent);
        this.book = book;
    }
}

 

 

조회도 같은 부모를 상속한 덕분에, 하나의 자료형으로 합쳐서 조회할 수 있었다. 깔끔

Type으로 조회를 하면 되니 더 깔끔해졌다.

    @Override
    public List<Category> findAllCategory(String name, String bookKey) {
        Category targetRoot = jpaQueryFactory.selectFrom(category)
            .where(category.name.eq(name), category.instanceOf(DefaultCategory.class))
            .fetchOne();

        List<Category> children = jpaQueryFactory.selectFrom(category)
            .where(category.parent.eq(targetRoot), category.instanceOf(DefaultCategory.class))
            .fetch();

        children.addAll(jpaQueryFactory.selectFrom(bookCategory)
            .innerJoin(bookCategory.parent, category)
            .where(category.eq(targetRoot))
            .innerJoin(bookCategory.book, book)
            .where(book.bookKey.eq(bookKey))
            .fetch());
        return children;
    }

DTO로 변환하는 것도 한 메서드 내에서 해결할 수 있었다.

   public static List<CategoryResponse> to(List<Category> categories) {
        return categories.stream()
            .map(CategoryResponse::of)
            .collect(Collectors.toList());
    }
    public static CategoryResponse of(Category category) {
        return CategoryResponse.builder()
            .name(category.getName())
            .build();
    }

데이터베이스에는 다음과 같이 들어간다.

Default 카테고리의 부모- 자식은 dtype이 Default로 표시되고 가계부만의 카테고리는 book_id를 가진채로, BookCategory로 저장된다.

 

휴 이제 카테고리 설계를 그만 바꾸고 싶다아악.

그렇지만 이런저런 도전을 할 수 있어서 좋은 것 같기두..?

728x90