Back
Featured image of post (도서) "NoSQL 철저 입문" - Key-Value 데이터베이스, 문서 데이터베이스

(도서) "NoSQL 철저 입문" - Key-Value 데이터베이스, 문서 데이터베이스

"NoSQL 철저 입문"이라는 책을 읽고 작성한 후기 시리즈의 두 번째 편 입니다. Key-Value DB와 문서 DB를 비교해봤습니다. 또한 Master-Slave 구조와 Partitioning에 대해서도 정리해봤습니다.

(도서) “NoSQL 철저 입문” - Key-Value DB, Document DB

시작하며

앞선 글에서는 “NoSQL 철저 입문”이라는 도서에 대한 총평과 NoSQL 데이터베이스들이 공통적으로 갖는 특징에 대해 알아봤다.

이번 글에서는 NoSQL들 중에서도 Key-Value DB와 Document DB이 특히나 서로 유사한 형태를 띄는 것 같아 묶어서 정리해보려한다.

추가적으로 각자의 고유한 특징은 아니더라도 공통적 특징일 수 있는 Master - Slave(read replica) 구조나 Partitioning, Sharding 등등에 대해서도 정리할 예정이다.

Key-Value Database

  • 키와 값 뿐인 가장 간단한 형태의 NoSQL 종류이다.
  • 아주 단순한 조회만이 지원된다. 키를 통해서만 조회가 가능하다.
    • 키를 알고 있다면 데이터를 바로 찾을 수 있지만 키가 없다면 풀서치를 해야할 수 있다.

Key-Value Database는 간단한 형태의 DB이기 때문에 테이블이나 그래프로 복잡한 관계를 나타내려는 경우에는 부적합하다. 단순 저장과 키를 바탕으로한 조회로 충분한 경우에 가볍게 사용하기 좋다.

아무래도 단순함은 빠른 속도와도 연관이 되기 때문에 속도가 빠른 편이고 대표적인 예시가 Redis이다. 단순한 동작에 In-Memory Database라는 특징으로 인해 Redis는 엄청난 속도를 자랑한다.

단, 메모리 같이 작은 크기의 저장소를 이용할 때는 저장소가 가득 차지 않도록 주의해야한다! 주로 이를 위해 주로 eviction policy라는 저장소가 가득 차기전에 데이터를 적절히 삭제하는 정책을 설정한다. 주로 인-메모리 저장소는 캐시로 사용되고, 캐시는 지역성을 근거로 하기 때문에 시간 지역성을 바탕으로한 LRU 관련 정책이 많이 사용된다.

Key-Value DB는 Key를 바탕으로 조회가 가능하기 때문에 Key에 조회 조건이 포함되어야 한다. 만약 어떤 유저의 최근 알림 내역 정보를 Key-Value DB 중 하나인 Redis에 저장한다고 해보자.

recent_notifications:{{username}} = [{{json string}}, {{json stirng}}]

예를 들어 유저가 umi0410이라면 
아래와 같이 Redis에 해당 유저의 최근 알림 내역 정보를 저장할 수 있을 것이다.

recent_notifications:umi0410 = [
    "{\"id\": 1, \"title\": \"1번 알림입니다.\"}",
    "{\"id\": 2, \"title\": \"2번 알림입니다.\"}"
]

위와 같이 데이터를 저장하면 읽을 때에도 특정 유저의 최근 알림 정보를 읽어올 수 있을텐데, 이때 앞서 언급했듯 Key만을 바탕으로 데이터를 조회하게 된다.

RDB의 경우 주로 의미 없는 Auto Increment ID를 PK이자 Clustered Index로 설정하기 때문에 Write 시에 Auto Increment ID를 바탕으로 디스크에 데이터가 연속된 위치에 저장되도록 하는 편이다. RDB는 PK가 아니라도 다양한 조건으로 값을 검색할 수 있기 때문에 PK는 성능을 고려해 의미 없는 Auto Increment ID를 넣는 것이다.

반면 Key-Value Database는 Key를 바탕으로 밖에 데이터를 조회할 수가 없기 때문에 Key (RDB의 PK와 유사하게 ID의 역할을 한다고 볼 수 있음)에 의미가 담겨야한다.

  • 🔵 Key가 recent_notifications:umi0410 처럼 조회 목적에 부합하는 정보를 포함하는 경우 → umi0410 유저에 대한 데이터가 저장되는 key 값을 알 수 있기 때문에 직접 Key 값을 찍어서 데이터를 조회할 수 있음
  • ❌ Key가 recent_notifications:1 처럼 의미 없는 숫자값인 경우 → umi0410 유저에 대한 데이터를 찾으려면 Full scan을 해야할 수 있음.

Key-Value database는 언제 사용하면 좋을까?

책에 나온 Key-Value database 사용 예시를 다시 보니 별로 마음에 안 들어서 내 생각대로 적어보려한다. 기본적으로 Key-Value database는 Key만으로 조회 요구사항을 만족할 수 있어야한다.

Redis와 같은 In-memory 데이터베이스의 경우에는 낮은 레이턴시가 중요하다거나 수명이 짧은 데이터에 주로 사용한다. 많이 봐왔던 케이스는 캐시나 장바구니 정보, 세션 정보 등이 있는 것 같다.

유저가 처음 앱에 접속할 때 필요할 만한 알림 개수나 피드 정보등을 캐시해두면 좀 더 빨리 유저에게 정보를 전달할 수 있을 것이다.

장바구니나 세션 정보 같은 것들은 영구적으로 필요한 데이터가 아니면서 정합성이 엄청 중요하다거나 관계가 복잡한 데이터가 아니기 때문에 간단하게 Key-Value DB를 이용하면 좋을 것 같다.

“Redis 장바구니”나 “Redis shopping cart”로 구글링하면 자료가 꽤 나와서 나중에 시간 되면 좀 더 찾아보고싶다.

Document database

Document DB는 Key-Value와 꽤나 유사하지만 Value의 형태가 단순한 값이라기보단 JSON이나 YAML, XML 같은 Semi structured(반정형) 형태를 띄는 데이터베이스를 말한다.

문서는 Key-Value 쌍의 집합이라고 볼 수 있다. 따라서 데이터 형태가 단순하면 Key-Value DB를, 그보다 좀 더 복잡하면 Document DB를 사용하는 편이라고 한다. 여기에 추가적으로 클러스터가 훨씬 커지고, 한 Key에 관련된 정보가 방대해지면 Columnar DB를 도입하게 되는 것 같다.

Document DB는 관계형 데이터베이스와도 유사한 개념을 갖기도 한다.

  • 관계형 데이터베이스에서는 Row, 문서 데이터베이스에서는 문서가 존재
  • 관계형 데이터베이스에서는 Row의 집합인 테이블, Document DB에서는 문서의 집합인 컬렉션이 존재
  • 단, 관계형 데이터베이스는 같은 테이블 내의 Row들이 모두 동일한 형태를 갖도록 스키마를 강제한다.
  • 문서 데이터베이스는 같은 컬렉션 내의 문서들이 얼마든지 다른 형태를 가질 수 있다!

Document DB는 Unstructured data가 아닌 그나마 조금은 구조가 존재하는 Semi structured data를 저장하기 때문에 좀 더 다양한 질의가 가능하다. 예를 들어 Key가 아닌 속성을 바탕으로도 질의를 할 수 있고, 인덱스를 걸 수도 있다.

단 RDB와 마찬가지로 인덱스 설정을 너무 적게 하면 읽기 성능이 나빠질 수 있고, 인덱스 설정을 너무 많이 하면 쓰기 성능이 나빠질 수 있다.

BI(Business Intelligence)나 일반적인 웹 서비스는 Write 보다 Read가 훨씬 많다. 반면 로그성 데이터들은 Write가 Read보다 훨씬 많다.

만약 Read와 Write가 둘 다 많으면서도 짧은 Latency가 중요하다면 Write용으로는 단순 적재형 데이터베이스와 Read 용으로는 수많은 인덱스를 지원하기 용이한 데이터 웨어하우스 등을 이용하는 것이 좋다. 즉 어느 한 가지 DB만을 선택할 것이 아니라 각 DB를 각 필요한 상황에 적용할 수도 있다는 것이다. 주로 쓰기 작업에 최적화된 DB의 내용이 읽기 작업에 최적화된 DB로 전달되도록 구축한 뒤 쓰기 작업과 읽기 작업을 각각 다른 DB를 이용하는 형태가 많이 사용된다고 한다.

문서 DB에서 관계 나타내기

RDB는 관계를 주로 테이블 간의 참조를 통해 나타낸다. 관계의 종류에는 1 대 1, 1 대 다, 다 대 다가 있다.

  • 1 대 1과 1 대 N 관계
    • 1에서 완전히 상대 1이나 상대 N 을 완전히 서브 문서 형태로 정의를 할 수 있다.
  • M 대 N 관계
    • 한 쪽이 한 쪽을 완전히 포함할 수는 없어 결국엔 두 개의 컬렉션이 필요하다.
    • 자신과 관련된 상대방 정보는 각자가 저장하기 때문에 양쪽에 데이터가 최신화되고 참조 무결성을 보장하도록 애플리케이션 레벨에서 잘 처리해야한다.
    • 책에는 자신과 관련된 상대 문서들의 ID를 리스트로 관리해 참조하도록 나와있는데 이 경우에도 중첩 문서의 형태로 나타낼 수는 있을 것이라 생각한다.

간단히 Key-Value DB, Document DB에 대해 알아봤다. 이제는 NoSQL에서 노드의 역할은 어떤 것들이 있을지, 각 노드들은 어떤 데이터들을 어떤 로직에 의해 담당하게 되는지 정리해보려한다.

문서 DB는 언제 사용하면 좋을까?

  • 데이터 형태가 Key-Value에 저장할 정도로 간단하지는 않은 경우
  • Key 만으로 조회 요구사항을 충족할 수 없는 경우
  • ACID 트랜잭션까지는 필요하지 않은 경우
  • 데이터의 형태가 정형화되지 않고 조금씩은 달라서 Schema를 준수하기 힘든 경우
  • 관계형 데이터베이스로는 요청을 처리하는 한계가 있어 수평 Scale이 필요할 정도인 경우

요청이 쏟아지는 게 아니라면 사실 관계형 데이터베이스로 많은 상황들을 해결할 수 있고 그 경우 아직은 나는 관계형 데이터베이스를 사용하는 것을 선호한다. 아무래도 역사가 깊어서 레퍼런스도 많고 편리한 ORM 도구들도 많기 때문이다.

하지만 고정된 스키마를 준수하기 힘들거나 수평 scale이 필요한 경우에는 문서 DB를 사용하는 것이 더 적절하다. 예를 들면 게시판, 댓글, 채팅 메시지등이 될 수 있을 것 같다.

반면 주문이나 결제 내역, 재고 상황 같은 데이터들은 ACID 트랜잭션이 필요할 수 있어 관계형 데이터베이스를 사용하는 게 좋을 것 같다.

RDB에서 많이 사용되는 마스터-슬레이브(Read Replica) 구조

  • 쓰기 권한이 있는 Master node는 여전히 한 대, Master node를 복제해 Read 요청만 처리하는 Replica는 여러 대 둘 수 있는 구조
  • 여러 서버를 하나의 서버처럼 사용할 수 있는 형태인 클러스터 형태 치고는 그나마 단순하다.
  • 쓰기에 비해 읽기 요청이 월등히 많은 경우 사용할 수 있다!
  • 노드 간 통신이 많이 필요 없고 주로 복제를 위해 마스터 ↔ 슬레이브간의 통신만 하면 되는 경우가 많다.
    • Columnar DB의 경우 다수의 마스터가 존재할 수 있고, 이 경우 상태 체크를 위해 다수의 마스터 노드 간에 통신이 필요하고, 이를 위해 “가십 프로토콜” 같은 프로토콜이 존재하기도 한다.

나는 AWS를 주로 사용해온터라 RDS를 다룰 때에는 Read replica 얘기가 빠짐 없이 등장해서 친숙했다. RDB는 원래 수평적 Scale이 힘든 편이다. 엄격히도 일관성을 중요시하다보니 분산 환경을 지원하지 않는 경우가 많은 것 같고(확실하진 않음 ㅎㅎ) 지원한다해도 노드 수가 증가함에 따라 성능도 선형적으로 증가하기는 힘들다. 그래도 RDB에서의 수평적 Scale에 대한 니즈도 많이 존재하다보니 요즘은 AWS에서도 Aurora에 Multi master 기능을 붙이거나 RDS에도 최대 15개 갈이의 Read Replica를 지원하기도 한다.

하지만 아무래도 RDB의 엄격한 일관성이 갖는 한계를 완전히 떨쳐낼 수는 없기 때문일지 손가락 발가락 합쳐서 셀 수 있는 개수 정도 까지만 수평 Scale이 가능한 것 같다.

그 개수를 NoSQL 중 하나인 Redis와 비교해보자. Redis의 경우는 1000개의 마스터 노드까지도 선형적으로 성능이 증가한다고 한다. 선형적인 증가를 조건으로 했을 때 최대 1000개의 마스터인 것이지 약 16,000여개의 마스터까지도 늘릴 수는 있긴하다. 게다가 각 마스터 노드에 대한 슬레이브까지 구성할 수 있으니 얼마나 확장성에서 차이가 나는지 어느 정도 실감할 수 있다.

파티셔닝 (Partitioning)

파티셔닝이란 무언가를 분리하는 작업을 말하는데 데이터베이스에서는 주로 데이터를 분리해서 저장하는 경우를 말한다.

  • 수직 파티셔닝 - 한 테이블을 여러 가상의 테이블로 분리해서 저장하는 것
  • 수평 파티셔닝 - partition key를 바탕으로 분산하여 그에 해당하는 값을 저장하는 것

파티셔닝 예시
파티셔닝 예시

RDB에서는 주로 수직 파티셔닝만을 지원하고, NoSQL에서는 굳이 수직 파티셔닝을 이용하기보단 수평 파티셔닝을 이용하는 경우가 많다. (왜 NoSQL에서는 수평 파티셔닝을 잘 사용하지 않는지는 잘 모르겠다.)

RDB에서는 수직 파티셔닝을 통해 자주 함께 접근하는 컬럼들은은 SSD 파티션에 저장하고, 잘 접근하지 않는 컬럼들은 HDD 파티션에 저장하는 것 같다.

NoSQL에서는 주로 Partition key로 데이터가 저장될 노드가 결정되고 그 노드에 데이터를 저장한다.

샤딩 (Sharding)

그중에서도 수평 파티셔닝은 샤딩이라는 또 다른 이름을 갖고 있다.

AWS ElasticCache의 Shard. 출처: https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Shards.html
AWS ElasticCache의 Shard. 출처: https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Shards.html

파티션 키(샤드 키)를 통해 그 키에 대한 데이터를 담당하는 노드 그룹을 샤드라고 하고, 한 샤드는 Primary 노드만 존재할 수도 있고 Read replica들도 존재할 수도 있다.

그럼 파티션 키라는 놈을 바탕으로 데이터가 어디에 저장될 지를 구체적으로 어떻게 결정하는 걸까?

  • Range Partitioning - 파티션 키의 구간에 따라 어떤 노드가 담당할 지를 미리 선언해놓고, 이를 참조하여 샤드를 정한다.
  • Hash Partitioning - 파티션 키를 인자로 해싱 함수를 수행해 얻어지는 결과값을 샤드로 정한다.

주로 많이 사용되는 접근법은 Range 기반과 Hash 기반 두 가지이다.

Range Partitioning

Range partitioning은 아래와 같이 미리 파티션 키의 구간에 따라 어떤 노드가 담당할지를 미리 선언해야한다. 샤드가 4개 존재한다고 하겠다.

  • id: 1 ~ id: 1000은 1번 샤드가 담당
  • id: 1001 ~ id: 2000은 2번 샤드가 담당
  • id: 2001 ~ id: 3000은 3번 샤드가 담당
  • id: 3001 ~ id: 4000은 4번 샤드가 담당

아주 직관적이고 간단하다는 장점이 있다.

하지만 단점도 존재한다. 예상하지 못한 파티션 키(4000 < id)인 id: 4001인 데이터가 등장하게 되면 어떻게 될까? 올바르게 데이터를 저장하지 못할 수 있다.

또한 1번, 2번 샤드는 비교적 오래된 유저에 대한 데이터이고 3, 4번 샤드는 최근 유입 유저에 대한 데이터라 각 샤드들에 대한 요청의 수가 상이하다면 어떻게 될까? 올바르게 요청이 분산되지 못하고 특정 샤드들에게만 집중될 수 있다는 문제가 있다.

또한 결국은 어딘가에 이 Range 할당 정보를 저장하고 그것을 조회해야한다. 공통으로 필요한 데이터가 어느 한 곳에 저장되어야한다는 부분에서 요청이 증가하고 샤드도 확장되는 상황에서 이런 Range 할당 정보를 제공하는 측이 병목이 될 수도 있다.

Hash Partitioning

Hash partitioning은 Range partitioning과 달리 어떤 샤드가 어떤 파티션 키를 담당하는지에 대한 매핑 테이블이 존재하지 않는다. 요청자측에서 Hash 함수를 수행한 결과값으로 자신이 요청해야하는 노드를 유추할 수 있다.

예를 들어 Range partitioning과 같이 샤드가 4개 존재한다고 해보자

  • id % 4 == 1 이면 1번 샤드가 담당
  • id % 4 == 2 이면 2번 샤드가 담당
  • id % 4 == 3 이면 3번 샤드가 담당
  • id % 4 == 0 이면 4번 샤드가 담당

클라이언트에 필요한 정보는 샤드 개수 뿐이다. id=4001인 데이터가 등장해도 4001 % 4는 1이므로 1번 샤드가 담당하면 된다.

지금 예시에서는 Partition key가 id로 numeric한 값을 갖기 때문에 hash 함수를 거치지 않고 모듈러 연산만으로도 원활히 동작하는 것 같지만 실제로는 non-numeric한 값들에 대해서도 지원하고 골고루 데이터가 분산될 수 있도록 하기 위해 해싱 함수들을 사용한다.

단 Hashing partitioning에도 단점이 존재하는데 바로 수평 Scale시에 각 샤드가 담당하는 데이터가 상이해질 수 있다. 예를 들면 노드가 2대 → 4대로 증가하는 경우를 예로 들어보겠다.

  • 기존 (노드 2대)
    • id = 1 → 1번 노드
    • id = 2 → 2번 노드
    • id = 3 → 1번 노드
    • id = 4 → 2번 노드
  • 수평 Scale 후 (노드 4대)
    • id = 1 → 1번 노드
    • id = 2 → 2번 노드
    • id = 31번 노드 3번 노드
    • id = 42번 노드 4번 노드

위와 같이 데이터를 담당하는 샤드들이 많이 변경되어 rebalance가 필요하다는 어려움이 존재한다. 게다가 이 어려움은 Scale out 때 뿐만 아니라 Scale in 시에도 존재하기도 한다.

이런 rebalance 과정을 좀 더 최적화하기 위해 consistent hashing이라는 개념이 존재하고 이에 대한 구현체로 virtual node들을 두는 ketama consistent hashing도 존재한다. 이에 대해서는 책에 깊게 등장하진 않고 구글링으로 많은 자료를 찾아볼 수 있기에 이번 글에서는 생략하도록 하겠다.

마치며

Key-Value 데이터베이스와 Document 데이터베이스의 특징에 대해 적어봤다.

책 내용을 잊어버리기 전에 한 번 정리해보면서 복습하려했었는데 살짝 까먹어가던 내용이나 대충 읽고 넘어간 부분에 대해서도 점검해볼 수 있었던 것 같다.

( 책 내용 뿐만 아니라 나중에 복습할 용도로 제가 기존에 알고 있던 내용이나 주관적인 의견들도 조금씩 적어놨습니다. 잘못된 내용이라 생각되는 부분이 있으시면 알려주시면 감사하겠습니다! )

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Hugo로 만듦
JimmyStack 테마 사용 중