일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- YouTube Data API
- monorepo
- fine-grained
- 추상 클래스
- typeorm
- 파일조회
- API 설계
- 셀렉트어드민
- Mock
- 티스토리챌린지
- NestJS
- 책임부과
- Connection pool
- SROOM
- nestjs decorator
- API 개발
- jest
- SW마에스트로
- nestjs libraries
- mailerservice
- java
- 멀티테넌시
- nestjs library
- coarse-grained
- 어드민 페이지
- 오블완
- 권한검증
- 오브젝트
- 자바
- guard
- Today
- Total
독산구너
트리구조 구현을 위한 MongoDB 사용전략 본문
글의 목적
인턴 과정 중 투입된 프로젝트에서 '업무'와 '업무가 포함되는 그룹' 정보를 다루게 되었습니다. task와 group으로 칭하겠습니다.
group은 하위 group은 가질 수 있고, 하나의 task는 하나의 group에만 포함되는 구조입니다. 이러한 트리구조를 가장 잘 다룰 수 있는 데이터 저장방식을 고민하였고, 그 과정을 기록하고자 합니다
요구사항
- 그룹-태스크 트리구조 저장
- 그룹 내 다수의 그룹이 생성될 수 있으며, 없을수도 있다.
- 그룹 내 다수의 태스크가 생성될 수 있으며, 없을수도 있다.
- 태스크 하위에는 그룹 또는 태스크 생성이 불가하다
- 드래그 앤 드롭으로 그룹간 그룹이동, 그룹간 태스크 이동
- 그룹 삭제, 태스크 삭제
Group A
│
├── Task 1
├── Task 2
└── Group B
├── Task 3
├── Group C
│ ├── Task 4
│ └── Group D
│ ├── Task 5
│ └── Group E
│ └── Task 6
└── Group F
├── Task 7
└── Group G
├── Task 8
└── Task 9
Group H
│
├── Task 10
└── Group I
├── Task 11
├── Group J
│ └── Task 12
└── Group K
└── Task 13
왜 MongoDB를 쓰나요?
현재 Group-Task 는 트리구조이고, RDBMS를 사용할 경우 이러한 트리구조를 저장하기에 적합하지 않다는 결론을 내렸습니다. 이유는 다음과 같습니다.
- parentGroupId를 통해 재귀적으로 트리구조를 만들면 하나의 구조를 불러오기 위해 다수의 쿼리를 순서대로 수행해야 한다.
- parentGroupId를 통해 재귀적으로 트리구조를 만들 시 두 그룹의 parent가 서로를 가르킬 경우 무한루프에 빠지게 될 가능성이 있다.
- 하위노드를 찾기위해 Full Range Scan이 필요하다 (인덱스를 생성하게 되면 parent가 자주 수정될 경우 성능 저하로 이어질 수 있다.)
- 순서를 보장하기 위해 추가적인 작업(Order by)이 필요하다.
MongoDB Document를 사용해서 얻는 이점은 다음과 같습니다.
- 트리구조를 그대로 저장해 항상 루트노드에서부터 탐색하므로 시간복잡도가 낮다(O(1))
- 같은 Document에서 순서를 보장한다.
MongoDB를 사용할 때의 단점은 다음과 같습니다.
- 루트가 아닌 경우 특정 group을 Random Access할 수 없습니다. (트리를 타고 내려가며 찾아야 합니다.)
- RDBMS 가 존재하는데 트리구조 구현만을 위해 NoSQL을 도입하게 됩니다. group depth가 낮을수록 오버스펙이고, 리소스 낭비입니다.
사용 전략
1. Embedded (중첩)
: 트리구조를 그대로 중첩된 문서로 저장하는 방식
// 스키마
const GroupSchema = new mongoose.Schema({
uuid: String,
project_id: String,
name: String,
upper_group_id: String,
sequence: Number,
tasks: [
{
uuid: String,
name: String,
project_id: String,
content: String,
upper_group_id: String,
completed: Boolean,
schedule_use: Boolean,
start_date: Number,
end_date: Number,
time_setup: Boolean,
sequence: Number
}
],
groups: Array
});
장점
- 직관적이고 중첩된 데이터를 한꺼번에 가져올 수 있어서 읽기 성능에 좋음
- 그룹 삭제 단일쿼리로 처리가능
단점
- 트리 깊이가 깊어질수록, 태스크 탐색의 시간복잡도 증가 (모든 노드를 탐색)
- 수정 시 문서 전체를 업데이트 해야함
- 내용이 많은 Task를 매번 덮어쓰는 것에 대한 리소스 낭비
2. Parent Reference (부모참조)
: 부모 그룹 참조방식으로 구현. 기존 RDBMS를 사용하는 것으로 가정했을 때 생각했던 방식
// 스키마
const GroupSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
project_id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
upper_group_id: {
type: String,
default: null
},
sequence: {
type: Number,
required: true
}
});
const TaskSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
name: {
type: String,
required: true
},
project_id: {
type: String,
required: true
},
content: {
type: String,
required: true
},
upper_group_id: {
type: String,
required: true
},
completed: {
type: Boolean,
required: true,
default: false
},
schedule_use: {
type: Boolean,
required: true,
default: false
},
start_date: {
type: Number,
default: null
},
end_date: {
type: Number,
default: null
},
time_setup: {
type: Boolean,
default: false
},
sequence: {
type: Number,
required: true
}
});
장점
- 유연성: 업데이트 시 트리구조 전체를 업데이트 할 필요가 없고, parent_group만 수정하여 구조변경 가능
- 트리구조를 그대로 저장하지 않기 때문에, 문서 크기에 대한 제한이 없다.
단점
- 조회시 while문을 사용해 여러번 쿼리를 날려야 한다.
- 부모-자식 관계의 일관성을 지키기 위해 추가적인 로직이 필요하다.
- NoSQL의 장점을 살릴 수 없다.
3. Materialized Path (경로 직접저장)
: 각 그룹 또는 태스크가 자신의 상위 경로 전체를 문자열로 저장하는 방식
// 스키마
const GroupSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
project_id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
path: {
type: String,
required: true
},
sequence: {
type: Number,
required: true
}
});
const TaskSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
name: {
type: String,
required: true
},
project_id: {
type: String,
required: true
},
content: {
type: String,
required: true
},
path: {
type: String,
required: true
},
completed: {
type: Boolean,
required: true,
default: false
},
schedule_use: {
type: Boolean,
required: true,
default: false
},
start_date: {
type: Number,
default: null
},
end_date: {
type: Number,
default: null
},
time_setup: {
type: Boolean,
default: false
},
sequence: {
type: Number,
required: true
}
});
장점
- 경로 기반으로 빠르게 탐색 가능
- 쿼리가 간단
단점
- 그룹 이동시 하위 그룹의 경로 모두 수정해야
4. Hybrid (중첩 + 참조)
: 내용이 많아 자주 수정시 리소스가 많이 소모되는 Task는 그룹참조 방식으로따로 저장하며, 내용이 적지만 트리구조인 Group은 한번의 쿼리로 불러올 수 있도록 중첩 방식으로 저장
const GroupSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
project_id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
sequence: {
type: Number,
required: true
},
groups: [this], // 그룹 자체를 포함하여 재귀적으로 스키마 정의
});
const TaskSchema = new mongoose.Schema({
uuid: {
type: String,
required: true,
unique: true
},
name: {
type: String,
required: true
},
project_id: {
type: String,
required: true
},
content: {
type: String,
required: true
},
completed: {
type: Boolean,
required: true,
default: false
},
schedule_use: {
type: Boolean,
required: true,
default: false
},
start_date: {
type: Number,
default: null
},
end_date: {
type: Number,
default: null
},
time_setup: {
type: Boolean,
default: false
},
sequence: {
type: Number,
required: true
}
});
장점
- 쿼리 한번으로 트리구조의 Group 탐색 가능
- Task를 따로 저장해서 그룹이동시 그룹 구조만 덮어써서 수정가능, 내용이 많은 Task는 수정x
- Task 탐색에 시간복잡도 낮춤 (인덱스 사용)
- 수정, 삭제가 빠르다 (그룹 collection에 영향x)
단점
- Collection이 2개로 나뉘어져서 관리가 어려워질 수 있다.
전략 탐색을 통해
프로젝트 특성을 고려해 데이터 저장 방식을 고려해봤습니다. NoSQL을 써보기로 한 이상 mongoDB의 이점을 살리기 위해 4번째 방법인 Hybrid로 설계해보려 합니다.
앞서 언급한 MongoDB 도입 단점에서 보았듯이, group의 트리구조를 저장하기위해 새로운 noSQL을 도입한건 오버스펙일 수 있습니다.
하지만 서비스 성능과 프로젝트 특성에 맞는 DB를 고려해보고 전략을 구성해봤다는 점에서 많이 배울 수 있었습니다.
중첩 + 참조 방식을 통해, group, task의 내용(이름, 내용, 일정정보 등)은 RDBMS에 저장하고(id로 임의접근 가능), 트리구조만을 저장하는 document를 생성하게 됩니다.
이 구조는 group이 다른 group으로 이동하게 되면 document 전체를 덮어써야 하는 단점이 있지만, 조회성능이 빠르기 때문에 적용했습니다.
업무관리 플랫폼의 특성상 group간 이동보다는 조회 및 내용 수정이 잦을 것이라고 생각했기 때문입니다. 그렇기에 트리구조와 내용은 빠른 조회(O(1))로 가능하도록 구성해봤습니다.