티스토리 뷰
데이터 중심 애플리케이션 설계 1,2,3장
내용이 꽤 많아 개인적으로 인상적인 부분들 위주로 작성하였다.
1장 - 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션
기억에 남는 문구
단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시킬 수 없는 과도하고 광범위한 요구사항을 가지고 있다.
이를 해결하기 위해 작업은 단일 도구에서 효율적으로 수행할 수 있는 테스크로 나누고 다양한 도구들을 애플리케이션 코드를 이용해서 서로 연결한다.
예시)
비동기 테스크 - Message Queue
검색 요청 - Elastic Search
읽기 요청 - Cache
느낀점:
하나의 도구로 모든 상황을 해결하려고 하는 것이 아니라 각각의 용도에 맞는 도구를 선택하고 서로 연결해서 사용해야된다고 느꼈다.
나 또한 사이드 프로젝트에서 캐시를 활용한 경험이 있다. 별도로 Redis같은 외부 저장소를 사용한 건 아니고 로컬 메모리를 사용했다.
똑같은 자원을 어떻게 사용하는지에 따라 최적화를 할 수 있다는 것을 느꼈다.
신뢰성 - 무언가 잘못되더라도 시스템이 올바르게 동작해야 함
기억에 남는 문구
결함: 일부 컴포넌트가 잘못됨
장애: 시스템 전체가 멈춤
-> 서로 같지 않음
결함이 생겨도 장애로 이어지지 않게 하는 것이 핵심이다.
느낀점:
결국 운영을 하다보면 결함이 발생하기 마련이다. 그 결함을 미리 어떻게 대처하는지에 따라 큰 장애로 이어지는지 여부를 결정하는 것 같다.
예를 들어 벌크 헤드 패턴을 들 수 있겠다.
외부 io연결 작업에서 공용 스레드 풀을 사용하면 급작스러운 io작업에 모든 스레드가 소진되어 전체 시스템이 마비될 수도 있다.
사전에 독립적인 스레드풀을 만들어 놓았다면 이정도의 장애는 발생하지 않았을 것이다.
2장 - 데이터 모델과 질의 언어
데이터 모델 종류
- 관계형 모델
- 문서 모델
- 그래프 기반 데이터 모델
관계형 모델과 문서 모델
관계형의 경쟁자는 많았지만 결국은 관계형이 대세가 되었다.
2010년대에 NoSQL 등장하면서 관계형에 비해 더 나은 지역성을 가졌다.
여기서 지역성이란?
관련 정보를 한 곳에 모을 수 있는 건데
관계형은 각 흩어진 위치에 I/O 작업을 통해 합치는 반면
NoSQL은 JSON 표현으로 한 번에 볼 수 있음
여기서 I/O작업은 결국 시간이 더 걸리게 되는 요인이 된다.
ID필드와 텍스트 필드 비교
왜 텍스트 대신 ID를 쓰나?
텍스트 직접 저장 시 문제:
"San Francisco" → "SF" 로 이름 변경 시
→ 3개 행 전부 수정해야 함
→ 하나라도 빠뜨리면 불일치 발생
해결:

ID로 저장 시 테이블 값이 있는 1곳만 수정하면 된다. 그 이외에 참조된 곳은 의미없는 아이디만 참조하기에 신경쓰지 않아도 된다.
이것이 곧 DB 정규화의 핵심이다!
여기서 정규화란
중복을 제거하고 데이터를 한 곳에서만 관리하는 것 (다만 join 은 DB I/O를 유발하니 트레이드오프가 있다.)
선언형과 명령형의 차이점
선언형
- 목표를 달성하기 위한 방법이 아니라 알고자 하는 데이터의 패턴
- 결과가 충족해야 하는 조건과 데이터를 어떻게 변환(정렬, 그룹화, 집계)할지를 저장하기만 하면 됨
명령형
- 특정 순서, 특정 연산을 수행하게끔 컴퓨터에게 지시함
선언형 장점
- 자동 최적화: DB 엔진(옵티마이저)이 내부적으로 스스로 최적화함
- 병렬 실행: 결과의 패턴만 정의하였으니 과정은 병렬화 가능

명령형의 단점
- 코드 복잡성: 한 눈에 봐도 가독성이 좋지 않다. 순차적으로 depth들어가면서 이해를 하기에
- 상태 관리 어려움: selected 클래스 삭제되어도 파란색 배경은 자동 삭제되지 않고 삭제 코드를 또 작성해야된다.
- 유지보수 및 확장성: 새로운 api 사용하려면 코드를 처음부터 다시 작성해야 되는 번거로움

3장 - 저장소와 검색
LSM 트리가 무엇이고 어떻게 동작하는지 중점적으로 알아보았다.
1. 로그 구조화와 SS 테이블
로그 구조화: 모든 쓰기를 순차적으로 추가(append-only)하는 방식으로 데이터를 저장
[전통적 방식 - 제자리 수정 (in-place update)]
디스크: [A=1] [B=2] [C=3]
↓ B를 5로 수정
디스크: [A=1] [B=5] [C=3] ← 해당 위치를 직접 덮어씀
[로그 구조화 방식 - append-only]
디스크: [A=1] [B=2] [C=3] [B=5] ← 끝에 추가만 함
↑ 최신값
SS 테이블: 정렬된 키-값 쌍을 디스크에 저장하는 불변 파일 형식

인메모리 색인이 필요한 이유
색인이 없으면 특정 키를 찾을 때 O(N)으로 동작함
키와 디스크 위치를 모르기 때문에 몇 번째 바이트에 뭐가 있는지 계산이 불가하다.
해결
인메모리에 key와 offset(key의 바이트 단위)을 저장한다.
다만, 전체를 안하고 일부 키만 저장해도 된다.
메모리 색인 (Sparse)
apple → offset 0
dog → offset 200
monkey → offset 500
"cat" 검색
→ apple(0) < cat < dog(200)
→ offset 0부터 200까지만 스캔하면 됨
→ 전체 파일이 아닌 아주 작은 범위만 탐색
이렇게 전체가 아니라 그 사이만 스캔하면 되니까 효율적이게 된다.
그러면 메모리 비용 <-> 색인 범위간의 트레이드 오프인 것 같다.
유입되는 쓰기는 임의 순서로 발생한다. 데이터를 키로 정렬하려면 어떻게 해야될까?
실제 쓰기 요청이 들어오는 순서 (임의 순서)
1번째 쓰기: zebra = 100
2번째 쓰기: apple = 200
3번째 쓰기: mango = 300
4번째 쓰기: banana = 400
그런데 SSTable은 정렬이 필요하다.
문제
들어오는 쓰기: zebra → apple → mango → banana (뒤죽박죽)
↓
SSTable 저장: apple → banana → mango → zebra (정렬 필요)
디스크에 바로 쓰면서 동시에 정렬을 유지하는 것은 어렵다.
중간에 넣으려면 뒤의 데이터를 전부 밀어야 함...
해결
MemTable (메모리의 Red-Black Tree 활용)
: 임의 순서로 들어오는 쓰기를 메모리에서 정렬한 뒤, 정렬된 채로 디스크에 한 번에 쓴다.
그렇다면 문제는 없나?
아니다. 만약 DB가 고장나면 아직 디스크로 기록되지 않고 멤테이블에 있는 가장 최신 쓰기는 손실된다.
이 문제를 해결하기 위해서는 이전 절과 같이 매번 쓰기를 즉시 추가할 수 있는 분리된 로그를 디스크 상에 유지해야 한다.
2. LSM 트리
정렬된 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진을 LSM 저장소 엔진이라고 한다.
핵심 구성 요소
1. MemTable(메모리)
: 쓰기 요청을 가장 먼저 받는 곳
2. WAL(Write-Ahead Log)
: MemTable이 날아가도 복구를 위한 보험
3. SSTable(디스크)
: 키가 정렬된 불변 파일
쓰기 흐름
쓰기 요청
↓
① WAL에 append (디스크, 순서 무관)
↓
② MemTable에 삽입 (메모리, 자동 정렬)
↓ 가득 차면
③ SSTable로 Flush (디스크, 정렬된 상태)
↓ L0가 쌓이면
④ Compaction (병합·정렬·중복제거)
읽기 흐름
읽기 요청
↓
① MemTable 확인 (가장 최신)
↓ 없으면
② L0 SSTable 확인
↓ 없으면
③ L1 → L2 → ... 순서로 탐색
성능 최적화 - 1
기존 LSM 트리의 문제점
: DB에 존재하지 않는 키를 찾는 경우에 느릴 수 있다.
이유
: 존재하지 않는 걸 확인하기 위해 가장 오래된 세그먼트까지 거슬어 올라가야 하기에
해결책
블룸필터: 선택한 키가 존재하지 않는다는 것을 매우 빠르게 판별하는 자료구조
준비물: 비트 배열 + 해시 함수




결론
비트 중 하나라도 0
-> 확실히 없음
모두 1
-> 있을 수도 있음
-> 실제로 SSTable 탐색 필요
성능 최적화 - 2
Compaction 전략: 두 가지로 나뉜다.
목적
시간이 지나면 옛날 값들이 쌓이게 된다.(새 값으로 추가되거나, 삭제됐지만 아직 파일에 존재하는 경우)
1. 크기계층 컴팩션
: 작은 SSTable -> 큰 SSTable에 병합
처음
[1MB] [1MB] [1MB] [1MB] ← 작은 것 4개 쌓임
↓ 병합
[4MB] ← 하나로 합침
또 쌓임
[4MB] [4MB] [4MB] [4MB] ← 중간 것 4개 쌓임
↓ 병합
[16MB] ← 또 하나로 합침
계속 반복
[16MB] [16MB] [16MB] [16MB]
↓
[64MB]
장점:
쓰기 속도가 빠르다.
-> 그냥 새 파일 만들고 나중에 합치면 된다.
단점:
같은 키가 여러 파일에 존재 가능하다.
-> 읽을 때 여러 파일 확인 필요
병합 직전에 디스크 공간 일시적으로 2배 필요하다는 단점이 있다.
불변이기에 동일한 데이터를 변경하고 싶을 때 수정할 수 없고 써야 된다.
이는 SSTable A, B로 나뉠 수 있다.

2. 레벨 컴팩션
: 키 범위를 더 작은 SSTable로 나누고 오래된 데이터는 개별 레벨로 이동
L0 (키 범위 겹쳐도 됨, Flush된 파일들)
[a,z,m,b...] [c,k,g,p...]
↓ L0가 일정 개수 차면
L1으로 내려보내면서 키 범위 재정렬
[a-g] [h-n] [o-t] [u-z]
→ 겹치는 파일만 골라서 부분 병합
↓ L1이 차면
L2로 내려보냄 (L1보다 10배 큰 용량)
[a-c][d-f][g-i]...(더 잘게 쪼개짐)
장점:
읽기 빠름 (분리된 구역만 확인하면 되니까)
단점:
쓰기 느림 (Compaction할 때마다 같은 데이터를 반복해서 새로 써야 되기 때문에)
LSM 트리의 장단점
장점
1. 쓰기 처리량이 높다.
B 트리:
데이터 수정 시 해당 페이지를
디스크에서 찾아서 → 덮어씀
→ 랜덤 쓰기 발생 (느림)
LSM 트리:
항상 순차적으로 SSTable 파일을 씀, 즉 여러 페이지를 덮어 쓰는 것이 아님
→ 순차 쓰기 (빠름)
순차 쓰기가 왜 빠를까?
HDD의 매커니즘과 관련있다.

SSD은 물리적 헤드가 없어서 랜덤/순차 차이가 분명하진 않다.
그래도 낮은 쓰기 증폭과 파편화 감소는 SSD의 경우 훨씬 유리하다.

2. 쓰기 증폭이 더 낮다.
B 트리:
데이터 쓰기 전 로그 한 번
+ 트리에 실제 쓰기 한 번
+ 페이지 분리 시 또 쓰기
→ 최소 두 번 이상 기록
LSM 트리:
Compaction이 있지만
순차적으로 한번에 씀
→ 상대적으로 쓰기 증폭 낮음
LSM 트리에서 쓰기 증폭이 있다고 했는데 왜 B트리보다는 낮을까?


3. 압축률이 좋다.
B 트리:
페이지 단위로 저장
→ 페이지가 꽉 안 차면
남는 공간 낭비 (파편화)
LSM 트리:
SSTable을 꽉꽉 채워서 씀
+ Compaction 시 불필요한 데이터 제거
→ 디스크 공간 효율적
→ B 트리보다 더 적은 파일 생성
단점
1. Compaction이 읽기/쓰기 성능에 영향
Compaction 진행 중:
디스크 자원을 Compaction이 점유
↓
새로운 읽기/쓰기 요청이
디스크 쓰려고 대기해야 함
↓
응답 시간이 길어짐
반면 Btree는
데이터 요청 -> 해당 페이지 찾아서 -> 바로 덮어씀 -> 끝
이 과정에서 백그라운드 작업이 없고 항상 일정한 속도임
2. Compaction이 유입 쓰기 속도를 못 따라갈 수 있다.
쓰기가 매우 빠르게 들어오면:
Compaction이 처리하는 속도 < 쓰기 유입 속도
↓
SSTable 파일이 계속 쌓임
↓
디스크 공간 부족 위험
읽기 시 확인할 파일 증가 → 읽기도 느려짐
→ 명시적 모니터링 필요!
3. 같은 키의 다중 복사본 존재
B 트리:
각 키가 색인의 한 곳에만 정확히 존재
→ 트랜잭션 처리 쉬움
LSM 트리:
Compaction 전까지
여러 SSTable에 같은 키 존재 가능
→ 트랜잭션 처리 복잡
→ 강력한 트랜잭션이 필요하면
B 트리가 훨씬 유리

