MongoDB
고친 사람 github-actions[bot]
MongoDB 는 데이터를 표의 행이 아니라 문서 한 덩어리로 저장하는 데이터베이스입니다. 사용자 한 명에 딸린 이름과 주소와 주문 목록을 한 문서에 함께 넣어 둡니다. 그래서 한 번 읽으면 필요한 것이 한꺼번에 나옵니다.
쉽고 빠른 이해
MongoDB 는 데이터를 필드 이름과 값을 짝지어 적은 문서로 넣고 꺼내는 데이터베이스입니다. 회원 한 명을 문서 하나에 담습니다. 그 문서 안에 주소와 관심사 목록까지 같이 넣습니다.
표에 맞춰 넣는 데이터베이스는 칸을 미리 정해야 합니다. 데이터 모양이 자주 바뀌면 그때마다 표를 고쳐야 합니다. 한 덩어리로 쓰는 데이터를 여러 표로 쪼갰다가 다시 붙이는 수고도 듭니다. MongoDB 는 덩어리를 덩어리째 저장해서 이 수고를 덜어 줍니다.
어떻게 도는가:
- 문서를 컬렉션이라는 묶음에 넣습니다. 문서마다 겹치지 않는 식별자가 붙습니다
- 찾을 때는 「이 필드가 이 값인 문서」처럼 조건을 문서 꼴로 넘깁니다
- 서버 여러 대에 같은 데이터를 복사해 둡니다. 데이터가 커지면 여러 서버에 나눠 담습니다
대가도 있습니다. 칸을 강제하지 않으니 잘못된 모양의 문서가 들어와도 막아 주지 않습니다. 여러 덩어리를 엮어 읽는 일은 표 기반 데이터베이스보다 번거롭습니다.
한 덩어리로 읽고 쓰는 데이터라면 MongoDB 가 잘 맞습니다. 여러 표를 가로지르는 관계 제약이 중심인 데이터라면 표 기반 데이터베이스를 고릅니다.
상세
이 절은 MongoDB 가 데이터를 어떤 모양으로 담는지부터 봅니다. 그다음 그 모양 때문에 무엇이 쉬워지고 무엇을 내려놓았는지를 따라갑니다. 끝에서는 서버 여러 대로 넓히는 두 방법, 복제와 샤딩을 봅니다.
MongoDB 는 NoSQL 데이터베이스로 묶입니다. NoSQL(Not only SQL)은 관계형 데이터베이스가 아닌 데이터베이스를 두루 부르는 이름입니다. 관계형 데이터베이스는 데이터를 표에 담고 SQL(Structured Query Language)로 다루는 데이터베이스입니다.
MongoDB 는 NoSQL 가운데서도 문서를 저장 단위로 삼는 문서 지향 데이터베이스입니다.
문서와 컬렉션
MongoDB 에서 데이터 한 건은 문서입니다. 문서는 필드 이름과 값의 짝을 모은 것입니다. 값으로는 숫자나 문자열만이 아니라 다른 문서와 배열도 들어갑니다.
회원 한 명을 문서로 적으면 이렇습니다.
{
"name": "민지",
"address": { "city": "서울", "zip": "04524" },
"tags": ["admin", "dev"]
}
주소는 문서 안의 문서입니다. 관심사는 배열입니다. 관계형 데이터베이스라면 주소와 관심사를 다른 표로 떼어 냈을 내용입니다. 여기서는 회원 문서 하나에 같이 들어 있습니다.
문서들을 모아 두는 묶음이 컬렉션입니다. 컬렉션들을 다시 묶은 것이 데이터베이스입니다. 관계형 데이터베이스와 맞대면 이렇게 짝지어집니다.
| 관계형 데이터베이스 | MongoDB |
|---|---|
| 테이블 | 컬렉션 |
| 행 | 문서 |
| 열 | 필드 |
| 외래 키로 이어진 다른 테이블 | 문서 안에 넣은 문서나 배열 |
마지막 줄이 둘이 갈리는 곳입니다. 앞의 세 줄은 이름만 다릅니다.
저장 형식 BSON
문서는 겉으로는 JSON(JavaScript Object Notation)처럼 보입니다. 디스크와 네트워크에서는 BSON(Binary JSON)이라는 이진 형식으로 오갑니다. JSON 을 기계가 빨리 읽고 쓰도록 바이트로 옮긴 형식입니다.
BSON 이 따로 있는 이유는 JSON 이 담지 못하는 값 때문입니다. JSON 에는 날짜 타입이 없습니다. 숫자도 정수와 실수를 가르지 않습니다. BSON 은 날짜 · 정수 · 실수 · 이진 데이터를 저마다 다른 타입으로 담습니다.
_id 필드
컬렉션의 문서마다 _id 필드가 있어야 합니다. 이 값은 컬렉션 안에서 겹치지 않습니다.
관계형 데이터베이스의 기본 키가 하는 일을 이 필드가 맡습니다.
문서를 넣을 때 _id 를 비워 두면 드라이버가 ObjectId 를 만들어 채웁니다. ObjectId 는 만든 시각을 앞부분에 담은 식별자입니다.
시각이 앞에 있으니 대체로 만든 순서대로 값이 커집니다.
시각 뒤에는 값을 만든 프로세스마다 다른 무작위 값과, 한 프로세스 안에서 하나씩 늘어나는 카운터가 붙습니다. 같은 순간에 여러 곳에서 만들어도 이 뒷부분이 달라 값이 겹치지 않습니다. 서버에 묻지 않고도 겹치지 않는 값을 만들 수 있는 까닭입니다.
컬렉션을 만들면 _id 에 유니크 인덱스가 저절로 생깁니다. 같은 _id 를 가진 문서를 또 넣으면 이 인덱스가 거절합니다.
스키마를 강제하지 않는 설계
관계형 데이터베이스는 테이블에 어떤 열이 있는지를 먼저 정해야 행을 넣을 수 있습니다. 그 약속이 스키마입니다. MongoDB 는 기본으로 이 약속을 요구하지 않습니다. 같은 컬렉션의 두 문서가 서로 다른 필드를 가져도 됩니다.
덕분에 데이터 모양이 바뀔 때 테이블을 고치는 작업 없이 새 필드를 넣기 시작할 수 있습니다. 회원마다 입력한 항목이 다르거나, 기능이 붙을 때마다 필드가 늘어나는 데이터에서 이 차이가 드러납니다.
대신 모양을 지키는 책임이 애플리케이션 코드로 넘어옵니다. 한쪽 코드가 name 으로 쓰고 다른 쪽 코드가 userName 으로 써도 데이터베이스는 둘 다 받아들입니다.
이 부담을 줄이려고 컬렉션에 검사 규칙을 걸 수도 있습니다. 규칙에 어긋난 문서를 거절하게 하는 스키마 검증입니다.
기본으로는 꺼져 있습니다. 켤지는 쓰는 쪽이 정합니다.
질의
MongoDB 는 SQL 을 쓰지 않습니다. 찾을 조건을 문서 꼴로 적어 넘깁니다. 아래는 대화형 셸에서 앞의 회원 문서를 넣고 찾는 모습입니다.
db.users.insertOne({
name: "민지",
tags: ["admin", "dev"]
})
db.users.find({ tags: "dev" }) // 민지 문서
조건 { tags: "dev" } 는 「tags 가 "dev" 인 문서」라는 뜻입니다. tags 는 배열인데도 민지의 문서가 나옵니다.
배열 필드에 값 하나를 조건으로 주면, 배열 안에 그 값이 든 문서를 찾기 때문입니다.
조건에 자주 쓰는 필드에는 인덱스를 겁니다. MongoDB 의 인덱스는 B-tree 로 만들어집니다. 인덱스가 없으면 컬렉션의 문서를 처음부터 끝까지 훑어야 합니다.
집계 파이프라인
합계나 그룹별 개수처럼 여러 문서를 모아 계산하는 일은 집계 파이프라인이 맡습니다. 문서들을 단계 여러 개에 차례로 흘려보내는 방식입니다. 단계마다 앞 단계가 넘긴 문서를 걸러 내거나 묶거나 정렬합니다.
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$userId",
cnt: { $sum: 1 } } }
]) // { _id: "u1", cnt: 3 }
첫 단계 $match 는 결제가 끝난 주문만 남깁니다. 둘째 단계 $group 은 남은 주문을 회원별로 묶고 몇 건인지 셉니다.
결과는 회원마다 문서 하나입니다. 코드 오른쪽 주석은 그중 한 회원의 결과입니다.
$group 안의 _id 는 무엇을 기준으로 묶을지 적는 칸입니다. 앞에서 본 문서 식별자 _id 와 이름만 같고 하는 일이 다릅니다.
결과 문서의 _id 에는 묶은 기준 값이 들어갑니다. 여기서는 회원 아이디 "u1" 입니다.
"$userId" 처럼 $ 로 시작하는 문자열은 글자 그대로가 아니라 각 문서의 userId 필드 값을 가리킵니다.
$sum: 1 은 묶인 문서마다 1을 더합니다. 문서마다 1씩 더하니 합계가 곧 문서 수입니다.
조인 대신 문서 안에 넣기
관계형 데이터베이스는 데이터를 여러 표로 나눠 두고 읽을 때 조인으로 붙입니다. MongoDB 는 반대로 함께 읽는 데이터를 처음부터 한 문서에 넣는 쪽을 기본으로 삼습니다. 이것을 임베딩이라고 부릅니다. 회원 문서 안에 주소를 넣은 것이 임베딩입니다.
모든 것을 넣을 수는 없습니다. 주문처럼 끝없이 쌓이는 데이터를 회원 문서 안에 넣으면 문서가 한없이 커집니다.
그럴 때는 주문을 다른 컬렉션에 두고 회원의 _id 만 적어 둡니다. 이것이 참조입니다.
참조로 나눈 데이터를 한 번에 읽고 싶으면 집계 파이프라인의 $lookup 단계로 붙일 수 있습니다.
다만 관계형 데이터베이스의 조인보다 손이 많이 갑니다. 외래 키가 없으니 어느 필드끼리 붙일지를 질의마다 적어야 합니다.
붙일 쪽 필드에 인덱스가 없으면 문서 하나를 붙일 때마다 상대 컬렉션을 처음부터 훑습니다.
이 때문에 MongoDB 에서 데이터를 설계할 때는 「무엇을 함께 읽나」를 먼저 묻고 문서의 경계를 정합니다.
내려놓은 것
MongoDB 는 문서 하나를 읽고 쓰는 일을 설계의 중심에 두었습니다. 그 대가로 내려놓은 것들이 있습니다.
| 내려놓은 것 | 결과 |
|---|---|
| 기본으로 강제하는 스키마 | 잘못된 모양의 문서를 데이터베이스가 막지 않습니다. 검사는 코드나 스키마 검증이 맡습니다 |
| 외래 키 제약 | 참조하는 문서가 지워져도 참조하는 쪽은 그대로 남습니다. 참조 무결성을 지키는 일은 애플리케이션 몫입니다 |
| 조인 중심의 질의 | 여러 컬렉션을 엮어 읽는 일이 번거롭습니다. 그래서 같은 데이터를 여러 문서에 겹쳐 넣는 비정규화를 자주 택합니다 |
문서 하나를 바꾸는 쓰기는 원자성을 가집니다. 문서의 필드 여럿을 고쳐도 전부 반영되거나 전혀 반영되지 않습니다.
여러 문서를 한꺼번에 묶는 트랜잭션도 지원합니다. 다만 설계의 기본 단위는 여전히 문서 하나입니다. 트랜잭션이 자주 필요하다면 문서의 경계를 잘못 그었다는 신호로 읽습니다.
복제 — 레플리카 셋
서버 한 대에만 데이터를 두면 그 서버가 죽을 때 서비스가 멈춥니다. MongoDB 는 같은 데이터를 여러 서버에 복사해 둡니다. 이렇게 같은 데이터를 나눠 가진 서버 묶음을 레플리카 셋이라고 부릅니다. 복제를 하는 단위입니다.
레플리카 셋에서 쓰기를 받는 서버는 하나뿐입니다. 그 서버가 프라이머리입니다. 프라이머리가 아닌 나머지 서버는 세컨더리입니다. 세컨더리는 프라이머리를 따라 적어 같은 데이터의 사본을 유지합니다.
프라이머리는 데이터를 바꿀 때마다 무엇을 바꿨는지 기록을 남깁니다. 이 변경 기록이 oplog(operations log, 연산 기록)입니다. 세컨더리는 이 기록을 받아 같은 순서로 적용합니다.
flowchart TD
A["애플리케이션"] -->|쓰기| P["프라이머리"]
P -->|oplog 를 복사| S1["세컨더리 1"]
P -->|oplog 를 복사| S2["세컨더리 2"]
프라이머리가 응답을 멈추면 남은 서버들이 투표로 세컨더리 하나를 새 프라이머리로 뽑습니다. 이것이 리더 선출입니다. 선출하는 동안 쓰기는 잠시 멈춥니다. 새 프라이머리가 서면 다시 받습니다.
쓰기가 몇 대에 복사된 뒤에 성공으로 칠지는 write concern 으로 고릅니다. 프라이머리에만 적힌 쓰기가 세컨더리로 복사되기 전에 프라이머리가 죽을 수 있습니다. 그러면 새로 뽑힌 프라이머리에는 그 쓰기가 없습니다.
과반의 서버에 복사된 뒤에 성공으로 치게 정하면 응답이 느려집니다. 대신 성공이라고 답한 쓰기는 프라이머리가 죽어도 새 프라이머리에 남습니다.
읽기는 세컨더리에서 하게 할 수도 있습니다. 대신 복사가 따라오기 전의 조금 낡은 값을 읽을 수 있습니다.
샤딩
데이터가 서버 한 대에 다 안 들어가면 여러 서버에 나눠 담습니다. 이것이 샤딩입니다. 나눠진 조각 하나를 담당하는 서버 묶음을 샤드라고 부릅니다. 샤드 하나하나가 다시 레플리카 셋입니다.
샤딩된 MongoDB 는 세 역할로 이루어집니다.
- mongos — 애플리케이션의 요청을 받아 맞는 샤드로 보내는 라우터
- 설정 서버 — 어느 데이터가 어느 샤드에 있는지 적은 배치표를 가진 서버
- 샤드 — 나눠진 데이터를 실제로 담는 서버 묶음
그림 맨 위의 애플리케이션은 이 세 역할에 들지 않습니다. MongoDB 를 쓰는 쪽입니다.
flowchart TD
A["애플리케이션"] --> R["mongos · 요청을 받아 샤드로 보낸다"]
R --> C["설정 서버 · 어느 데이터가 어느 샤드에 있나"]
R --> S1["샤드 1 · 레플리카 셋"]
R --> S2["샤드 2 · 레플리카 셋"]
애플리케이션은 샤드에 바로 붙지 않습니다. mongos 에 붙습니다. mongos 는 설정 서버에 적힌 배치표를 보고 요청을 맞는 샤드로 보냅니다. 애플리케이션 입장에서는 데이터베이스가 한 대처럼 보입니다.
문서를 어느 샤드에 둘지는 샤드 키가 정합니다. 샤드 키는 컬렉션마다 고르는 필드입니다. 문서들은 샤드 키 값에 따라 청크라는 덩어리로 묶입니다. 이 청크가 샤드마다 나뉘어 놓입니다.
질의 조건에 샤드 키가 있으면 mongos 는 그 값이 든 샤드 하나에만 묻습니다. 샤드 키가 없으면 모든 샤드에 묻고 결과를 모읍니다. 샤드 키를 무엇으로 고르느냐가 샤딩된 MongoDB 의 속도를 가르는 까닭입니다.
언제 고르나
데이터가 한 덩어리로 읽히고 쓰이는 경우에 문서 모델이 들어맞습니다. 상품마다 속성이 제각각인 카탈로그, 기기마다 필드가 다른 수집 데이터, 화면 하나에 필요한 것을 한 번에 꺼내는 프로필이 그런 경우입니다. 모양이 자주 바뀌는 초기 서비스에서도 스키마를 고치는 부담이 줄어듭니다.
반대로 여러 종류의 데이터가 서로를 촘촘히 가리키는 경우가 있습니다. 그 관계를 데이터베이스가 지켜 줘야 합니다. 회계 장부처럼 여러 표를 가로지르는 제약과 조인이 중심인 데이터입니다. 이런 데이터에서는 MongoDB 가 내려놓은 외래 키와 조인을 애플리케이션이 떠맡게 됩니다. 관계형 데이터베이스는 그 일을 기본 기능으로 합니다.
관련 항목
MongoDB 가 속하는 상위 분류
데이터베이스 · NoSQL · 문서 지향 데이터베이스 · 분산 데이터베이스
MongoDB 와 같은 일을 두고 겨루는 데이터베이스
PostgreSQL · MySQL · Couchbase · Amazon DocumentDB · Cassandra · DynamoDB · Redis
MongoDB 에 데이터를 담는 단위와 형식
문서 · 컬렉션 · BSON · JSON · ObjectId · 임베딩 · 참조
MongoDB 가 데이터를 지키는 규칙
기본 키 · 유니크 인덱스 · 스키마 검증 · 원자성 · 트랜잭션 · write concern · read concern
MongoDB 를 조회하는 명령과 기능
mongosh · 집계 파이프라인 · 인덱스 · B-tree · 복합 인덱스 · 해시 인덱스 · 조인
MongoDB 를 여러 서버로 넓히는 구성 요소
복제 · oplog · 리더 선출 · 샤딩 · 샤드 키 · 청크 · mongos · 설정 서버
MongoDB 가 비켜 선 관계형 설계 원칙
관계형 데이터베이스 · 스키마 · 정규화 · 비정규화 · 외래 키 · 참조 무결성 · SQL
MongoDB 에서 자주 나는 문제
다른 이름: mongodb · 몽고DB · 몽고디비