이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › DB › 확장과 분산
NoSQL과 문서 모델
주문을 테이블 셋에 나눌까, 문서 하나에 담을까
같은 데이터(주문과 주문 항목)를 두 가지 방식으로 저장해 봅니다. 관계형은 상품, 주문, 주문 항목을 테이블 셋에 나눠 두고 읽을 때 조인합니다. 문서 모델은 MongoDB 같은 문서 DB가 쓰는 방식으로, 주문 하나를 문서 하나로 보고 그 안에 항목 배열과 상품 이름까지 함께 담습니다.
일부만 실행으로 확인 · MySQL 8.4.11의 JSON 컬럼으로 문서 모델을 재현해 스무 가지 조합을 실행으로 대조. 전용 문서 DB는 실행하지 못함
이 실습에 나오는 개념
- NoSQL
- 관계형 모델을 쓰지 않는 데이터베이스를 통틀어 부르는 말이다. 키-값 저장소(Redis), 문서 저장소(MongoDB), 와이드 컬럼 저장소(Cassandra), 그래프 데이터베이스 등이 있다. 정해진 스키마 없이 유연하게 저장하고 여러 서버로 확장하기 쉽게 설계된 것이 많지만, 조인과 복잡한 트랜잭션은 관계형 데이터베이스보다 제한적이다.
- 문서 모델(문서 데이터베이스)
- 데이터를 행과 컬럼이 아니라 문서라는 단위로 저장하는 방식이다. 문서는 보통 JSON과 비슷한 모양으로, 값 안에 배열이나 또 다른 객체를 품을 수 있다. 그래서 주문과 그 주문의 항목처럼 함께 읽는 데이터를 문서 하나에 담아 두고 조인 없이 한 번에 읽는다. MongoDB가 대표적인 문서 데이터베이스다. 여러 문서에 같은 값을 복사해 넣었다면 그 값이 바뀔 때 복사본을 모두 고쳐야 하므로, 무엇을 문서 안에 넣고 무엇을 따로 둘지가 설계의 핵심이 된다.
- 고정 스키마와 유연한 스키마
- 스키마는 데이터가 어떤 항목들로 이루어지고 각 항목이 어떤 자료형인지를 정한 구조다. 관계형 데이터베이스는 테이블을 만들 때 컬럼을 먼저 정하고 그 구조에 맞는 행만 받아들이며, 구조를 바꾸려면 ALTER TABLE을 실행해야 한다. 문서 데이터베이스는 대체로 문서마다 항목이 달라도 저장할 수 있는데, 이것을 유연한 스키마라고 한다. 구조를 바꾸기 쉬운 대신 잘못된 이름이나 빠진 항목도 그대로 저장되므로, 모양을 지키는 일을 애플리케이션 코드나 따로 켜는 검사 기능이 맡아야 한다.
- 정규화와 반정규화
- 정규화는 같은 데이터가 여러 곳에 중복 저장되지 않도록 테이블을 나누는 설계 과정이다. 중복이 있으면 한 곳만 수정되어 데이터가 서로 어긋나는 문제가 생긴다. 반정규화는 조회 성능을 위해 의도적으로 중복을 허용하거나 테이블을 합치는 것으로, 조인이 줄어드는 대신 중복된 데이터를 함께 갱신해야 하는 부담이 생긴다.
- JOIN (INNER, OUTER)
- 두 테이블의 행을 조건에 따라 결합하는 연산이다. INNER JOIN은 양쪽에서 조건이 맞는 행만 결과에 포함한다. LEFT OUTER JOIN은 왼쪽 테이블의 모든 행을 포함하고 오른쪽에 짝이 없으면 NULL로 채운다. 짝이 없는 데이터도 결과에 남겨야 하는지에 따라 선택한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 문서 모델에서 상품 이름을 바꾸려면 그 상품이 든 문서를 모두 고쳐야 합니다.
- 관계형에서 이름을 바꾸면 — 주문이 몇 건이든 product 테이블의 한 행만 고칩니다.
- 문서로 주문 하나 읽기 — 조인 없이 문서 하나로 끝납니다.
- 관계형으로 주문 하나 읽기 — 테이블 셋을 조인해야 주문 하나가 완성됩니다.
- 문서에 새 항목 넣기 — 구조를 바꾸지 않고 바로 저장됩니다. 대신 문서마다 모양이 달라집니다.
- 관계형에 없는 컬럼을 넣으면 — 오류 1054로 거절됩니다. 먼저 테이블 구조를 바꿔야 합니다.
- 문서에서 필드 이름을 틀리면 — 오류 없이 저장되고, 수량 합계가 조용히 틀려집니다.
처음 설정으로 실행하면
- 문서 모델로 저장한 상태입니다. order_doc 테이블에 문서 3개가 있고, 문서 하나가 주문 한 건입니다. 문서 안에 주문한 사람과 항목 배열(items)이 통째로 들어 있으며, 항목마다 상품 이름이 복사되어 있습니다.
- UPDATE order_doc SET doc = JSON_SET(…) WHERE …: items 배열에 '키보드'가 든 문서를 모두 찾아, 문서마다 그 자리의 이름을 바꿉니다. 고친 문서는 3개입니다. 상품 이름이 주문 문서마다 복사되어 있기 때문에, 그 상품이 든 문서 수만큼 고쳐야 합니다.
- 이번에는 3개를 모두 고쳤으므로 모든 주문이 새 이름으로 보입니다. 하지만 일부만 고치고 멈추면 같은 상품이 문서마다 다른 이름으로 남습니다. 이 값들이 서로 같아야 한다는 것을 DB는 알지 못합니다.
실행 결과
- 문서에 복사해 둔 값은 바뀔 때마다 복사본을 모두 찾아 고쳐야 합니다. 읽기를 쉽게 만든 대가입니다. 그래서 문서 모델에서는 잘 바뀌지 않는 값만 복사해 넣거나, 상품 번호만 넣어 두고 이름은 따로 조회합니다.
- 다만 주문 당시의 상품 이름과 가격을 기록으로 남겨야 한다면, 복사해 둔 값이 나중에 바뀌지 않는 것이 오히려 맞는 동작입니다. 복사가 늘 나쁜 것은 아닙니다.
- NoSQL은 관계형이 아닌 DB를 통틀어 부르는 말이고, 문서 모델은 그 가운데 하나입니다. 어느 쪽이 더 좋다기보다, 무엇을 자주 함께 읽고 무엇이 자주 바뀌는지에 따라 고릅니다.
코드
-- 문서 모델을 MySQL의 JSON 컬럼으로 재현한 것. 행 하나가 문서 하나다
CREATE TABLE order_doc (id INT PRIMARY KEY, doc JSON NOT NULL);
-- 문서의 예 (주문 1번)
-- {"member": "kim",
-- "items": [{"product_id": 1, "name": "키보드", "qty": 1},
-- {"product_id": 2, "name": "마우스", "qty": 2}]}
-- 문서마다 items 배열에서 '키보드'가 있는 자리를 찾아 그 자리의 name을 바꾼다
UPDATE order_doc
SET doc = JSON_SET(doc, JSON_UNQUOTE(JSON_SEARCH(doc, 'one', '키보드', NULL, '$.items[*].name')), '기계식 키보드')
WHERE JSON_SEARCH(doc, 'one', '키보드', NULL, '$.items[*].name') IS NOT NULL;
이 모형의 전제
문서 모델은 MySQL의 JSON 컬럼으로 재현했습니다. 스무 가지 조합(저장 방식 둘, 하는 일 넷, 주문 수 2건, 3건, 5건)을 이 PC의 MySQL 8.4.11에서 화면과 같은 문장으로 실행해, 오류 코드, EXPLAIN에 나오는 테이블 수와 결과 행 수, UPDATE가 고친 행(문서) 수, gift가 있는 문서 수, 수량 합계가 화면과 일치하는 것을 확인했습니다. 설명에 쓴 다음 사실도 실행으로 확인했습니다. 조인 없이 읽으면 테이블마다 한 번씩 쿼리 세 번이 필요합니다. ALTER TABLE로 컬럼을 추가한 뒤에는 같은 INSERT가 들어가고 기존 행에도 그 칸이 생깁니다. 문서 하나만 고치면 같은 상품의 이름이 두 가지로 남습니다. JSON_SCHEMA_VALID 제약을 건 뒤에는 필드 이름을 틀린 문서가 오류 3819로 거절됩니다. 전용 문서 DB(MongoDB 등)는 이 PC에 없어 실행하지 못했습니다. 그래서 여기서 보인 것은 "주문을 문서 하나로 담았을 때의 읽기와 쓰기의 모양"이며, 문서 DB 제품의 동작 그대로는 아닙니다. 쿼리 문법, 배열 안의 값을 고치는 방법, 스키마 검사 기능, 여러 문서에 걸친 트랜잭션과 조인 지원은 제품과 버전에 따라 다릅니다. 어느 방식이 더 빠른지는 데이터의 양과 인덱스, 제품에 따라 달라지므로 다루지 않았고, 화면의 결과는 속도가 아니라 읽거나 고치는 곳의 수입니다. 키-값 저장소, 와이드 컬럼 저장소, 그래프 DB 같은 다른 종류의 NoSQL은 다루지 않았습니다.
앞 실습: Redis의 한 줄 처리