분산 시스템 & 아키텍처 학습 프로젝트 : 데이터 모델, 저장소와 질의
데이터 중심 애플리케이션 설계 책을 시작하다
AI의 발전으로 개발자가 가져야 하는 태도에 대해서 많은 의견이 있다. 이 기사에서는 바이브 코딩으로 대표되는 AI의 사용을 비판적인 시선으로 보고 있지만, 어쨌든 나는 경력을 시작하는 주니어의 입장에서 변화와 전통을 유연하게 받아들이고 내가 배울 수 있는 것은 다 배우려고 한다. 그래서 시작하는 스터디다.
이 책을 관통하는 가장 중요한 질문이 첫 번째 장에 나와 있다.
“어떤 시스템을 만들어야 하는가?”
신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션
그렇다면 이런 애플리케이션은 어떤 애플리케이션일까?
오늘날 많은 애플리케이션은 계산 중심과는 다르게 데이터 중심적이다. 이러한 애플리케이션의 경우 CPU 성능은 애플리케이션을 제한하는 요소가 아니며, 더 큰 문제는 보통 데이터의 양, 데이터의 복잡도, 데이터의 변화 속도다.
따라서 애플리케이션 개발자는 더 이상 애플리케이션에만 관여하는 게 아니다. 데이터 저장과 처리를 위한 도구는 최근에 만들어지고 다양한 사용 사례에 최적화되고 있으며, 애플리케이션의 요구사항 역시 단일 도구로는 처리할 수 없을 정도로 광범위해지고 있다. 따라서 개발자는 데이터 시스템 설계자가 되기도 해야 한다.
그렇다면 데이터 시스템이나 서비스를 설계할 때 마주치는 문제들은 어떻게 해결해야 할까? 예를 들면 다음과 같다.
- 내부적으로 문제가 있어도 데이터를 정확하고 완전하게 유지하려면?
- 시스템의 일부 성능이 저하되더라도 클라이언트에 일관되게 좋은 성능을 어떻게 제공할 수 있을까?
- 부하 증가를 다루기 위해 규모를 확장하는 방법은?
- 서비스를 위해 좋은 API는 어떤 모습일까?
어떤 애플리케이션에 대해서도 정확히 맞는 깔끔한 해결책은 없다. 그렇지만 소프트웨어 시스템에서 중요하게 여기는 다음 세 가지 원칙을 준수하는 방향으로 나아가면 우리 애플리케이션에 맞는 해결책을 찾을 수 있다.
1. 신뢰성(Reliability)
결함으로 인해 장애가 발생하지 않도록 내결함성 구조를 설계하는 것이 가장 좋다.
2. 확장성(Scalability)
시스템이 현재 안정적으로 동작하는 것이 미래에도 안정적으로 동작하는 것을 보장해주지 않는다. 따라서 다음과 같은 요소를 먼저 고려한다.
- 부하: 시스템 설계에 따라 어떤 부하 매개변수를 추적할지는 달라진다. 이 책에서는 트위터의 발전 과정에 따라 주목한 부하 매개변수를 설명했는데, 유명인의 계정이 등장하면서 하이브리드 방식을 차용한 것이 흥미로웠다.
- 성능: 부하를 기술하면, 이 부하가 증가할 때 어떤 일이 일어나는지에 따라서 처리량(throughput)이나 응답 시간(response time)과 같은 성능 매개변수를 생각해볼 수 있다. 실무에서 사용되는 지연 시간에 대해서 예를 들어줬다. 꼬리 지연 시간에 따라서 가장 소중한 고객의 경험을 올리는 것, SLA(서비스 수준 목표)를 정의하는 것 등이 흥미로웠다.
이런 부하에 대응하는 접근 방식으로 scale up, down, out, in이 있다.
3. 유지보수성(Maintainability)
소프트웨어 비용의 대부분은 유지보수에 들어간다.
유지보수의 내용은 버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 새 사용 사례를 위한 변경, 기술 부채 상환, 새로운 기능 추가 등이 있다.
유지보수성이라는 추상적인 개념의 핵심 내용은 이렇다.
1) 운영팀이 시스템을 원활하게 운영할 수 있도록 해야 한다.
좋은 소프트웨어라도 나쁘게 운영할 경우 작동을 신뢰할 수 없다.
2) 새로운 엔지니어가 시스템을 쉽게 이해할 수 있도록 만들어라.
복잡도의 수렁에 빠진 커다란 진흙덩어리가 되지 않도록 추상화와 패턴 사용을 적극 활용할 것.
3) 변화를 쉽게 만들어라.
2장: 데이터 모델과 질의 언어
이런 기술 서적의 장점은 모두 알고 있을 것이라고 기대하는 지식 선에서부터 설명하기 시작한다는 것이고, 이것은 단점과 동일하다.
데이터 모델이란 무엇일까?
처음에 읽기 시작했을 때는 너무 당연하게도 지금 만지고 있는 백엔드 API에서 사용하는 데이터 모델만이 떠올랐다. 읽다 보니 그것이 전부가 아니라는 것을 알게 된다. 애플리케이션 계층과 데이터베이스 계층에서 관계형 모델에만 치중되어 있다 보니 당연히 모델은 관계형 모델만을 생각했는데, 풀고자 하는 문제에 따라서 다른 모델을 사용해야 하고, 역사적으로 많은 논의가 있어서 여기까지 왔다는 것을 망각하고 있었다.
이런 관점에서 보자면 데이터 모델은 단순히 데이터를 구조화하고 표현하는 방식이다.
NoSQL의 탄생
관계형 모델의 우위를 뒤집으려는 가장 최신의 시도로, 어떤 특정 기술을 참고한 것이 아니라 다음 문제를 풀기 위해서 새로운 모델을 제안하는 방식이다.
- 매우 높은 쓰기 처리량을 어떻게 달성할 것인가?
- 관계형 모델에서 지원하지 않는 특수 질의 동작을 표현하는 방식
- 표현력이 풍부한 데이터 모델에 대한 바람
그렇지만 저자는 결과적으로 관계형 데이터베이스가 비관계형 데이터베이스와 함께 쓰이는 방향이 정착될 것이라고 본다. 실제로 그렇게 되고 있기도 하다. 이런 개념을 “다중 저장소 지속성(Polyglot Persistence)”이라고 한다.
데이터 모델 챕터에서 역사적으로 사람들이 풀려고 했던 문제들
1) 객체 관계형 불일치(Impedance Mismatch)
데이터를 관계형 테이블에 저장하기 위해서 애플리케이션 코드와 데이터베이스 모델 객체 사이에 전환 계층이 필요한데, 이를 가리키는 개념이다. ORM을 사용해도 이 모델 간의 차이를 완벽히 숨길 수 없다.
2) 다대일과 다대다 관계
사용자 인터페이스에 자신의 부서를 입력하라는 칸을 주는 방법을 예로 들어보자.
- 자유 입력 칸을 준다: 모든 사람이 평문으로 입력하고 이걸 그대로 저장한다. 코드 작성이 쉽다. 그렇지만 모두 마음대로 입력한다면 이걸 갱신하거나 관련 항목을 조회하는 경우 사용하기 어렵다.
- 표준 목록을 준다: 만들기 귀찮지만 갱신하거나 조회하는 데 사용하기 쉽다.
중복된 데이터를 정규화하려면 다대일 관계가 필요한데, 다대일 관계는 문서 모델에 적합하지 않다.
관계형 데이터베이스는 조인에 힘을 줬고, 문서 데이터베이스는 조인이 아니라 지역성(Locality)에 힘을 줬다. 즉 문서 데이터베이스는 조인 기능이 약하다는 뜻이다. 그래서 문서 데이터베이스에서 조인을 수행하려면 지원하는 질의가 아닌 경우 다중 질의를 만들어서 애플리케이션에서 조인을 흉내 내야 한다.
문서 데이터베이스란?
문서 데이터베이스란 NoSQL 데이터베이스의 한 종류로, 테이블이 아니라 문서 단위로 저장하고 이 문서는 주로 JSON, XML, BSON의 형식을 가진다. 그래서 데이터 간의 관계를 조인이 아니라 임베딩, 참조 방식으로 처리한다.
예를 들면 이런 식이다.
{
"postId": "P123",
"title": "문서 데이터베이스란?",
"author": "김개발",
"views": 1500,
"tags": ["NoSQL", "Database", "JSON"],
"comments": [
{
"user": "이독자",
"comment": "설명이 좋네요!"
},
{
"user": "박질문",
"comment": "RDBMS와 차이점은 뭔가요?"
}
]
}
MongoDB, DynamoDB, Firebase 등이 이에 해당한다.
문서 데이터베이스 vs 과거의 계층 모델
책에서는 이 문서 데이터베이스가 결국 과거의 계층 모델을 구현한 내용이 아니냐는 의문에 대해서 이야기한다.
계층 모델은 모든 데이터를 레코드 내에 중첩된 레코드 트리로 표현하는데, 다대다 관계 표현이 어려운 문제에 부딪혀 네트워크 모델로 진화했다. 네트워크 모델이란 레코드 간 연결을 경로로 표현하는 방식을 사용하고, 따라서 이 연결은 외래 키라기보다는 포인터와 더 비슷하다. 이렇게 경로로 표현하는 방식에는 다대다 관계를 표현하는 데 유리한 지점이 있었으나, 노드를 찾아가기 위해서는 모든 경로를 따라가야 하는 단점이 있었다.
네트워크 모델 vs 문서 데이터베이스의 핵심 차이점
네트워크 모델 (1960s-1970s):
- 목적: 계층형 모델(1:N)의 한계를 넘어 M:N (다대다) 관계를 표현하기 위해 고안되었다.
- 구조: 데이터 레코드들이 포인터(Pointer)를 통해 복잡한 그래프(네트워크) 형태로 직접 연결된다.
- 특징: 구조가 매우 복잡하고 경직되어 있으며, 데이터 정의나 수정이 어렵다.
문서 데이터베이스 (1990s-2000s 이후):
- 목적: 관계형 모델(RDBMS)의 엄격한 스키마와 확장성의 한계를 극복하고, 유연한 데이터 구조와 대규모 분산 처리를 위해 고안되었다. (NoSQL의 한 종류)
- 구조: 데이터를 JSON이나 BSON 같은 유연한 ‘문서(Document)’ 단위로 저장한다.
- 특징: 스키마가 유연하고(Schema-less), 개별 문서가 독립적인 구조를 가질 수 있다.
가장 큰 차이는 데이터를 연결하는 방식이다.
- 네트워크 모델: 데이터 간의 관계가 미리 정의된 물리적인 포인터(연결선)로 고정된다. 데이터에 접근하려면 이 복잡한 연결망을 직접 따라가야 한다.
- 문서 데이터베이스: M:N 관계를 표현하는 방식이 다르다.
- 포함(Embedding): 관련 데이터를 하나의 문서 안에 ‘포함’시킨다. (예: 블로그 글 문서 안에 댓글 배열을 포함). 이 방식은 오히려 계층형 모델과 유사한 면이 있다.
- 참조(Referencing): 문서 안에 다른 문서의 ID(고유 식별자)를 저장하여 ‘참조’한다. 이는 관계형 모델의 외래 키와 비슷하지만, 데이터베이스가 아닌 애플리케이션 수준에서 관계를 해석(Join)하는 경우가 많다.
요약하자면, 네트워크 모델은 데이터 레코드 간의 복잡한 M:N 연결망 자체에 중점을 둔 초기 모델이며, 문서 데이터베이스는 유연하고 독립적인 ‘문서’를 기본 단위로 하는 현대적인 NoSQL 모델이다.
그래서 어떤 데이터베이스를 선택해야 할까?
모든 부분을 아직 이해하진 못했지만, 데이터 지역성이 더 중요한 경우에는 문서 데이터베이스를, 조인을 통한 다대다 관계의 쉬운 질의를 원한다면 관계형 데이터베이스를 선택하는 게 좋다.
질의 언어
1) 선언형 질의 언어
목표를 달성하기 위한 방법이 아니라 알고자 하는 데이터의 패턴을 작성해서 결과가 충족해야 하는 조건과 변환 조건을 제시한다. 이런 언어를 사용하는 데이터베이스는 질의 최적화기를 가지고 있어서 이를 어떻게 달성해야 하는지를 사용자가 알 필요는 없다. 이 경우에는 엔진의 상세 구현이 숨겨져 있어 세부 내용을 알지 않더라도 성능 최적화가 가능하다.
2) 명령형 질의 언어
특정 순서로 특정 연산을 수행하게끔 지시한다.
3) 맵리듀스형 질의 언어
(책에서 소개됨)
그래프형 데이터 모델과 질의 언어
속성 그래프 모델 / 트리플 저장소 모델 / 그래프용 선언형 질의 언어 / 명령형 그래프 질의 언어 / 그래프 처리 프레임워크
(이 부분은 생략)
3장: 저장소와 검색
저장소의 가장 중요한 역할 두 가지는 1) 저장하기와 2) 조회하기다.
애플리케이션 개발자는 사용 가능한 여러 저장소 엔진 중에, 워크로드(작업 부하) 유형에 좋은 성능을 낼 수 있는 저장소를 선택하는 능력을 길러야 한다.
로그 구조 계열 저장소 엔진
가장 기본적으로 생각해 볼 수 있는 데이터베이스를 생각해보면, 키-값의 쌍으로 구성된 데이터베이스를 생각할 수 있다.
#!/bin/bash
db_set() {
echo "$1,$2" >> database
}
db_set key value를 호출하면 데이터베이스에 키와 값을 저장한다. 이 구조에서는 삭제나 갱신은 불가능하고 추가만 가능하다. 너무 간단해 보이는 구조지만 실제로 로그 파일을 이렇게 만든다.
로그(Log)는 append-only 파일이다. 조금 더 일반적인 의미로 연속된 추가 전용 레코드를 말한다.
그런데 이런 구조의 데이터베이스는 2번 기능인 조회하기를 할 때 무조건 풀 스캔을 해야 하는 문제가 있다. 그래서 색인(Index)을 사용한다.
색인은 질의 성능에만 영향을 주고 데이터베이스의 내용에는 영향을 미치지 않는다. 그렇지만 추가로 내용을 작성하게 하므로 반드시 쓰기 성능을 하락시킨다. 그래서 질의 성능을 높이지 않는 색인은 성능에 좋지 않은 영향을 준다.
해시 색인
가장 간단하게는 해시 색인을 달 수 있다.
해시 색인은 로그 구조화 파일이 키-값 쌍으로 구성되어 있고, 키가 어떤 오프셋에 들어있는지를 나타내는 해시맵을 가지고 있다. 이 해시맵에서 키를 찾고 키가 들어있는 오프셋에서 값을 찾는 방식이다.
그런데 이렇게 해시 색인을 가지고 있으면 결국에는 디스크 공간이 부족해지는 한계에 부딪히게 된다. 그래서 세그먼트로 로그를 나누는 방식을 사용한다.
세그먼트는 변경이 안 되는 테이블을 말한다.
컴팩션(Compaction)은 동일한 키를 가지고 있는 값의 경우 최신 값만 남기는 과정을 말하는데, 세그먼트를 병합해서 성능을 올리는 과정을 수행할 때 이런 과정으로 수행된다.
[Seg 1] [Seg 2] [Seg 3] [Active Segment]
(닫힘) (닫힘) (닫힘) (쓰기 중)
↓ ↓
(병합 대상)
새로운 쓰기는 이전 세그먼트에 저장되지 않고, 병합 및 컴팩션 과정은 닫힌 세그먼트끼리만 이뤄진다.
이런 구조에서도 역시 한계가 있다. 해시 테이블은 메모리에 저장해야 하므로 키가 너무 많으면 문제가 되고, 범위 질의에 적합하지 않다.
그래서 SSTable(Sorted String Table)과 LSM 트리가 등장한다.
페이지 지향 계열 저장소 엔진
B-Tree
가장 널리 사용되는 색인 구조로, LSM 트리와 같이 키로 정렬된 키-값 쌍을 유지한다.
B-Tree와 LSM Tree의 비교
(이후 내용 계속…)