계층적인 도메인 구조에 대한 고민

appti·2024년 3월 26일
0

고민

목록 보기
1/3

서론

탭 관리 토이 프로젝트를 진행하며 도메인에 대한 설계를 진행하게 되었습니다.

현재 진행하고자 하는 도메인은 탭이며, 특징은 다음과 같습니다.

  • 페이지에서 탭을 계층적으로 볼 수 있어야 합니다.
  • 계층 구조 및 순서는 사용자가 자유롭게 변경할 수 있어야 합니다.
  • 탭에는 다양한 메타 데이터가 존재하며, 해당 탭의 공개 여부와 같은 사용자가 변경할 수 있는 메타 데이터 또한 존재합니다.
    • 이 때 부모 탭의 메타 데이터가 변경되는 경우 자식의 메타 데이터까지 변경되어야 합니다.

예를 들어, 알파벳이 각 탭을 의미한다고 가정했을 때 다음과 같은 구조가 가능합니다.

  • A
    • B
    • C
    • D
      • E
      • F
    • G
    • H
      • I
      • J
      • K
    • L

이 각각의 탭의 순서를 자유롭게 바꿀 수 있어야 했습니다.

여기서, 각 탭을 앞으로 탭 요소라고 칭하겠습니다.

구조

도메인을 구현하기 전에, 이를 어떤 식으로 구현할지 구조를 고민했습니다.

대표적인 계층 구조인 댓글-대댓글 형태는 구현해본 경험이 있기 때문에, 이를 기준으로 진행했습니다.

1. TabGroup(탭 그룹) - TabElement(탭 요소)

탭 요소가 부모-자식 간의 관계일 때, 이를 관리하기 위한 상위 도메인 탭 그룹을 도입한 방법입니다.

예시를 통해 표현하자면, 다음과 같습니다.

  • A (탭 그룹)
    • B (탭 요소)
    • C (탭 요소)
    • D (탭 그룹)
      • E (탭 요소)
      • F (탭 요소)
    • G (탭 요소)
    • H (탭 그룹)
      • I (탭 요소)
      • J (탭 요소)
      • K (탭 요소)
    • L (탭 요소)

탭 그룹은 탭 요소 뿐만 아니라 탭 그룹 자기 자신을 자식으로 가질 수 있습니다.
해당 요소가 탭 그룹인지 판단하기 위해서는 자식이 있는지 여부를 판단해야 합니다.

장단점

  • 장점
    • 객체 지향적으로, 계층적으로 탭 간의 관계를 표현할 수 있습니다.
    • 순서를 변경하는데 제약 조건이 없습니다.
  • 단점
    • 순서를 변경하는데 비용이 많이 소모됩니다.
      • A → H를 교체해야 하는 경우
        H의 자식들이 A에 그대로 유지되어야 하는 경우 A의 모든 자식을 H에 추가 → A의 자식 삭제 -> H의 모든 자식을 A에 추가 -> H의 기존 자식 삭제와 같이 비용 소모가 큰 방식이 필요합니다.
    • 각 요소를 승급/강등하는데 비용이 많이 소모됩니다.
      • 자식이 있다면 탭 그룹이 되며, 자식이 없다면 탭 요소가 되기 때문에 이를 처리하기 위해 많은 비용이 소모됩니다.

2. TabPage(탭 페이지) - TabGroup(탭 그룹) - TabElement(탭 요소)

1번에서 최상위 탭 요소(A)와 자식을 가지고 있는 탭 요소(H)간 순서를 변경했을 때 가장 큰 비용이 들어갑니다.
최상위 탭 요소는 자기 자신을 제외한 나머지 탭 요소를 모두 자식으로 갖고 있기 때문입니다.

이를 방지하기 위해, 최상위 도메인인 탭 페이지를 적용했습니다.
탭 페이지는 해당 탭을 식별하기 위한 기준이 되기 때문에 순서를 변경할 수 없습니다.

예시를 통해 표현하자면, 다음과 같습니다.

  • A (텝 페이지)
    • B (탭 요소)
    • C (탭 요소)
    • D (탭 그룹)
      • E (탭 요소)
      • F (탭 요소)
    • G (탭 요소)
    • H (탭 그룹)
      • I (탭 요소)
      • J (탭 요소)
      • K (탭 요소)
    • L (탭 요소)

장단점

  • 장점
    • 객체 지향적으로, 계층적으로 탭 간의 관계를 표현할 수 있습니다.
    • 1번에 존재하던 가장 비용이 비싼 순서 변경이 금지되었습니다.
  • 단점
    • 가장 비용이 비싼 순서 변경은 금지되었지만, 나머지 연산 또한 비용이 비쌉니다.
    • 순서 변경에 제약 조건이 생겼습니다.
    • 각 요소를 승급/강등하는데 비용이 많이 소모됩니다.

3. TabElement(탭 요소) 단일

이전 두 개의 방법 모두 탭 관련 도메인이 부모/자식을 갖게 되면서, 순서를 변경할 때 매우 비싼 비용이 발생했습니다.

다음과 같이 모든 요소를 탭 요소로 사용하며, 탭 요소 그 자체는 자신의 부모/자식에 대해서는 알 수 없도록 했고, depth와 order를 통해 자신이 표기될 위치만을 알 수 있도록 하는 방식입니다.

  • A (탭 요소)
    • B (탭 요소)
    • C (탭 요소)
    • D (탭 요소)
      • E (탭 요소)
      • F (탭 요소)
    • G (탭 요소)
    • H (탭 요소)
      • I (탭 요소)
      • J (탭 요소)
      • K (탭 요소)
    • L (탭 요소)

장단점

  • 장점
    • 순서 변경에 제약이 없습니다.
    • 순서 변경 시 비용이 비교적 적게 소모됩니다.
      • 특정 도메인으로 승격/강등 처리를 할 필요가 없습니다.
      • 순서 변경 시 자식을 변경할 필요가 없습니다.
  • 단점
    • 부모의 설정 메타데이터를 자식에게 적용하기 어렵습니다.
    • 다른 방식에 비해 비용이 저렴하지만, 여전히 데이터가 많은 경우 비용이 비쌉니다.

결론

부모-자식 관계를 계층적으로 관리하고 싶었지만, 비용적인 측면을 고려해서 3번을 선택했습니다.

개선 및 고려사항

3번의 단점은 다음과 같습니다.

  1. 부모의 설정 메타데이터를 자식에게 적용하기 어렵습니다.
  2. 다른 방식에 비해 비용이 저렴하지만, 여전히 데이터가 많은 경우 비용이 비쌉니다.

이를 개선해보고자 합니다.

메타 데이터를 관리할 위치

탭 요소 내부에 order, depth를 적용할지 아니면 외부에 관리할지 고민했습니다.

내부

public class TabElement {

    private Long id;
    private String title;
    private String url;
    private String description;
    private boolean isPublic;
    private int order;
    private int depth;
    ...
}

위와 같이 도메인에서 관리하게 된다면 다음과 같은 문제가 발생합니다.

  1. 도메인에 도메인과 상관 없는, 화면과 연관된 데이터가 존재합니다.

자기 자신을 계층적으로 관리하거나, 탭 그룹 & 탭 요소와 같이 부모-자식 관계라면 객체 지향적에 가깝게 관리할 수 있다고 생각합니다.

하지만 order, depth는 부모-자식 관계를 표현한 것이 아니라 단순히 탭 요소의 현재 위치, 즉 화면과 관련된 데이터가 되었습니다.

그렇기 때문에 화면에서 순서를 바꾸게 된다면, 탭 요소라는 도메인과 전혀 연관 없는 변경 사항으로 인해 데이터가 변경되게 됩니다.

  1. 데이터 변경 시 비용이 비쌉니다.

URL, 제목 등 도메인 내 중요한 데이터의 변경이 아니라 순서 변경이라는 화면과 관련된 데이터로 인해 변경이 됩니다.

또한, 순서가 변경되면서 어디까지 전파를 받았는지 알 수 없기 때문에 해당 탭 페이지에 해당하는 모든 요소를 탐색해야 합니다.
이 때, 관련된 탭과 관련된 데이터를 아예 삭제하는 경우 id에 영향을 주기 때문에 불가능하며 변경된 모든 값을 DB의 탭 요소 관련 테이블에서 찾아야 하기 때문에 다양한 이슈가 발생할 수 있습니다.

외부

public class TabElementMetadata {
    private boolean isPublic;
    private int order;
    private int depth;
}

위와 같이 외부로 분리한 경우 다음과 같은 문제가 발생합니다.

  1. DB 조회 시 인덱스 설정이 어렵습니다.

DB에서 탭 요소를 조회할 때, DB 자체에서 정렬한다면 탭 요소 외부에 있는 별도의 테이블의 조건으로 정렬해야 하므로 인덱스 설정이 어렵습니다.

결론

탭 요소 내부에 order, depth를 두는 것보다는 외부에 order, depth를 두는 것이 낫다고 판단했습니다.

특히 순서는 자주 변경될 수 있기 때문에, 화면과 관련된 메타 데이터로 인해 탭 요소 관련 테이블이 변경되는 것보다는 외부 테이블이 변경되는 것이 낫다고 판단했습니다.

개선 사항

순서가 변경되는 것보다는 탭 페이지를 조회하는 이벤트가 훨씬 많이 발생할 것이기 때문에 조회 방식을 개선하는 것이 필수적입니다.

이를 대비해, 조인으로 조회하거나 DB에 두 번 접근해 탭 요소와 탭 요소 메타데이터를 조회해 애플리케이션 레벨에서 조회하는 방법을 고려했습니다.

조인과 단일 조회 쿼리 두 번 중 어느 부분이 더 효율적일지는 직접 측정해봐야 알겠지만, 애플리케이션 레벨에서 깔끔한 방법은 Comparable 인터페이스를 구현한 별도의 DTO를 통해 조인으로 조회하는 것이라고 생각됩니다.

이후 유틸리티 클래스를 통해 간단하게 정렬이 가능하기 때문입니다.


탭 요소에 메타 데이터를 관리하면서 그 값을 DB에서 변경하는 것보다는, 외부에서 관리하는 메타 데이터로 관리하는 것이 더 비용이 저렴합니다.

다만, 그래도 여전히 탭 페이지의 데이터가 많이질수록 부담이 커지게 됩니다.

만약 조인으로 조회하는 쿼리가 효율적이라면 bulk update를 통해 해결할 수 있을 것으로 보이며, 그렇지 않다면 MongoDB와 같은 Documentation 기반의 NoSQL을 통해 메타 데이터를 하나하나 일일이 변경하지 않고, 통째로 변경할 수 있을 것으로 보입니다.

다만 RDB에 두 번 조회하는 것과, RDB 한 번 + NoSQL 한 번 조회하는 것 중 어느 것이 더 효율적일지는 성능 테스트가 필요해 보입니다.

부모의 메타 데이터 전파

설정 메타 데이터의 경우 지금은 공개 여부 뿐입니다.
그러므로 이번 예시에서는 공개 여부를 설정하는 식으로 진행하겠습니다.

  • A
    • B
    • C
    • D
      • E
      • F
    • G
    • H
      • I
      • J
      • K
    • L

이렇게 있을 때, H에 대해 설정한다면 하위 I, J, K가 모두 적용되어야 합니다.

현재 탭 요소에서는 자식이나 부모를 알 수 없기 때문에, depth와 order의 조합으로 찾아야 합니다.

백엔드에서 이러한 변경 사항을 모두 비교하며 찾아서 변경하는 것은 매우 비효율적일 것입니다.

어처피 클라이언트에서 순서 및 메타 데이터를 모두 변경할 것이기 때문에 부모의 메타 데이터를 백엔드에서 전파하는 것이 아닌, 프론트엔드에서 전파하면 될 것이라고 판단했습니다.

프론트엔드의 경우 순서를 변경한 뒤 그 상태에서 depth에 따라 부모 메타 데이터를 자식에게 전파한 결과를 백엔드로 전송한다면 백엔드에서 추가적인 처리가 없을 것이라고 판단했습니다.

최종 결론

최종 결론은 다음과 같습니다.

  • 탭 요소는 TabElement 단일로 관리
  • TabElement에는 별도로 부모-자식 관계를 확인할 수 있는 메타 데이터를 저장하지 않음
    • 메타 데이터는 외부에서 관리
    • 외부에서 관리함으로 인해 발생하는 성능 이슈는 다음과 같은 방법으로 개선 시도
      • 조회 시 성능
        • 조인
        • RDB 두 번 접근
        • RDB 한 번 + NoSQL 한 번
      • 변경 시 성능
        • bulk update
        • NoSQL에서 별도의 업데이트 없이 삭제 후 추가
  • 탭 순서 및 공개 여부와 같은 메타 데이터는 프론트에서 모두 처리한 뒤 백엔드로 전달
profile
안녕하세요

0개의 댓글