Database · NoSQL · Modeling

NoSQL - 조립을 언제 하느냐의 문제다

SQL을 안 쓴다는 뜻이 아니다. 사실을 쪼개 두고 읽을 때 붙일 것인가, 읽을 모양대로 미리 붙여 둘 것인가. 그 하나에서 이득도 대가도 전부 나온다.

·데이터베이스 공통 개념 13편
목차
  1. 화면 하나에 조인이 다섯 번 붙는다
  2. 부품 창고와 완성품 매장
  3. 읽을 모양대로 저장한다
  4. SQL을 안 쓴다는 뜻이 아니다
  5. 스키마가 없는 게 아니라 코드로 옮겨간다
  6. 대가 하나: 같은 사실이 여러 곳에 있다
  7. 대가 둘: 안 만들어둔 조회는 못 한다
  8. 대가 셋: 묶을 수 있는 범위가 좁다
  9. 경계는 생각보다 흐리다
  10. 그래서 실무에선 어떻게 쓰이나
  11. 정리

지금까지는 전부 “테이블에 담는다”를 깔고 한 이야기였다. 이번엔 그 전제를 뺀다.

화면 하나에 조인이 다섯 번 붙는다

상품 상세 화면을 그린다. 필요한 건 상품, 옵션, 재고, 리뷰 요약, 판매자 정보다. 다섯 테이블에 흩어져 있으니 조회 한 번에 조인이 다섯 번 붙는다.

잘못한 게 아니다. 정규화를 제대로 한 결과다. 판매자 상호는 판매자 테이블에만 있어야 하고, 그래서 상품을 읽을 때 붙여 와야 한다.

그런데 트래픽이 오르니 이 조회 하나가 제일 무겁다. 그리고 데이터가 커져서 샤딩을 하는 순간, 조인이 아예 안 된다.

여기서 질문 하나가 생긴다. 어차피 늘 같이 읽는데, 왜 매번 붙이나?

부품 창고와 완성품 매장

가구를 판다고 하자. 방식이 둘이다.

부품 창고. 나사는 나사끼리, 판자는 판자끼리, 종류별로 나눠 보관한다. 주문이 들어오면 그때 부품을 모아 조립해서 내보낸다. 창고가 깔끔하고, 나사 규격이 바뀌면 나사 칸 하나만 고치면 된다.

완성품 매장. 책상이면 책상, 의자면 의자를 조립된 채로 세워 둔다. 주문이 오면 그냥 내준다. 대신 자리를 많이 먹고, 같은 나사가 세워 둔 가구 전부에 이미 박혀 있다.

관계형 DB가 부품 창고다. 정규화가 바로 “같은 부품을 두 칸에 두지 마라”는 규칙이었다. 그리고 대부분의 NoSQL이 완성품 매장이다. 읽을 모양대로 미리 조립해서 통째로 저장한다.

지금부터 볼 이득과 대가는 전부 이 하나에서 나온다.

읽을 모양대로 저장한다

앞의 상품 상세 화면을 문서 하나로 담으면 이렇게 된다.

json
{
  "id": "P-1024",
  "name": "원목 책상 1200",
  "seller": { "id": "S-77", "name": "달구름가구" },
  "options": [
    { "size": "1200", "stock": 12 },
    { "size": "1400", "stock": 0 }
  ],
  "reviewSummary": { "count": 318, "average": 4.6 }
}

조회는 한 번이다. id로 문서를 꺼내면 화면에 필요한 게 전부 들어 있다.

쓸 때저장된 모양읽을 때
관계형 - 부품 창고
조각을 각자 넣는다상품 · 옵션 · 리뷰 · 판매자
붙인다DB가 조인해서 모은다

읽을 때마다 매번

문서형 - 완성품 매장
붙인다앱이 모아서 문서로 만든다
그대로 꺼낸다조회 한 번

쓸 때 한 번

두 판의 칸은 크기도 자리도 같다. 달라지는 건 붙이는 칸이 어느 쪽 끝에 있느냐뿐이고,
가운데의 조각 넷과 덩어리 하나는 그 결과다.

여기서 짚을 게 있다. 조인이 사라진 게 아니라 자리를 옮긴 것이다. 부품을 붙이는 일은 여전히 누군가 한다. 관계형은 읽을 때마다 DB가 붙이고, 문서형은 쓸 때 한 번 애플리케이션이 붙여서 넣어 둔다.

읽기가 쓰기보다 훨씬 많으면 이 거래는 남는 장사다. 붙이는 일을 한 번만 하고 여러 번 우려먹는 셈이니까.

SQL을 안 쓴다는 뜻이 아니다

이름 때문에 오해가 많은데, NoSQL은 “SQL을 쓰지 않는다”가 아니라 “SQL만은 아니다”(Not Only SQL)에 가깝다. 실제로 많은 제품이 SQL을 닮은 질의어를 갖고 있다.

가르는 건 질의어가 아니라 데이터를 어떤 모양으로 담느냐다. 크게 네 갈래로 나뉜다.

문서
키-값
컬럼 패밀리
그래프

네 갈래는 질의어가 아니라 담기는 구조가 다르다. 키-값의 값이 빗금인 건 DB가 그 안을 안 들여다본다는 뜻이고,
이 글은 문서형을 기준으로 이어간다.

갈래담는 모양잘하는 일
문서중첩된 JSON 덩어리 하나한 화면에 필요한 걸 통째로
키-값열쇠 하나에 값 하나아주 단순하고 아주 빠른 조회
컬럼 패밀리행마다 컬럼 구성이 달라도 된다넓고 성긴 데이터, 대량 쓰기
그래프점과 선. 관계 자체가 데이터몇 다리 건너 이어지는 탐색

앞으로 볼 이야기는 문서형을 기준으로 한다. 넷 중 관계형과 가장 자주 비교되고, 완성품 매장이라는 성질이 가장 뚜렷하기 때문이다.

참고

그래프형만은 결이 다르다. 나머지 셋이 “관계를 덜 쓰는 대신 규모를 얻는” 쪽이라면, 그래프형은 관계를 더 잘 다루려고 만들어졌다. “친구의 친구의 친구”처럼 조인을 여러 번 겹쳐야 하는 질문에서 관계형보다 낫다. NoSQL이 한 덩어리가 아니라는 걸 보여주는 예다.

스키마가 없는 게 아니라 코드로 옮겨간다

문서형은 흔히 “스키마가 없다”고 소개된다. 문서마다 필드 구성이 달라도 DB가 안 막는 건 사실이다. 그래서 컬럼을 추가하려고 테이블을 잠글 일이 없다.

문제는 그다음이다. DB가 안 막으면 아무도 안 막는 게 아니라, 코드가 막아야 한다.

json
{ "stock": 12 }
{ "stock": "12" }
{ "stockCount": 12 }

셋 다 들어간다. 에러도 안 난다. 반년 뒤에 셋이 섞인 컬렉션을 만나면, 그 필드를 읽는 코드마다 세 경우를 다 방어해야 한다.

그래서 정확히 말하면 스키마가 없어진 게 아니라 DB 밖으로 밀려난 것이다. 예전에는 ALTER TABLE이 막아주던 걸 이제는 애플리케이션 코드와 리뷰가 막는다. 유연함을 산 대가로 누가 지키느냐가 바뀐다.

대가 하나: 같은 사실이 여러 곳에 있다

판매자 S-77이 상호를 바꿨다. 관계형이면 판매자 행 하나를 고치면 끝이다. 문서형에서는 그 이름이 상품 문서 1만 개에 이미 박혀 있다.

관계형 - 사실은 한 군데에만
판매자
달구름가구
상품 - 이름이 아니라 번호를 들고 있다
S-77S-77S-77 S-77S-77S-77
고칠 곳 1
문서형 - 문서마다 박혀 있다
상품 문서 - 이름이 통째로 들어 있다
달구름가구달구름가구달구름가구 달구름가구달구름가구달구름가구
고칠 곳 1만

상호 하나를 바꿀 때 칠해진 칸을 전부 고쳐야 한다. 한쪽은 한 칸이고 다른 쪽은 전부다 -
여기 그린 여섯은 실제로는 문서 1만 개다.

정규화 편에서 본 갱신 이상현상이 여기서 그대로 돌아온다. 그때는 반정규화를 “필요하면 골라서 하는 것”으로 다뤘는데, 문서형에서는 그게 기본값이다. 그러니 대가도 기본값으로 따라온다.

실무에서 쓰는 손은 대체로 셋이다.

  • 안 바뀌는 것만 박아 둔다. 상호처럼 바뀔 수 있는 건 ID만 두고 필요할 때 따로 읽는다.
  • 바뀌면 뒤에서 고친다. 전부 즉시 맞추는 대신, 배치나 이벤트로 시차를 두고 따라잡는다.
  • 바뀐 시점을 기록으로 남긴다. 주문서에 박힌 가격처럼, 애초에 그때의 값이 맞는 경우도 많다. 이건 중복이 아니라 이력이다.
주의

셋째를 첫째·둘째와 헷갈리면 안 된다. 주문 당시 가격을 주문 문서에 박아 두는 건 중복이 아니라 그 시점의 사실을 남기는 것이다. 상품 가격이 나중에 올랐다고 옛 주문서의 금액까지 따라 오르면 오히려 틀린다.

대가 둘: 안 만들어둔 조회는 못 한다

관계형의 진짜 힘은 조인 자체가 아니라 나중에 아무 질문이나 할 수 있다는 것이다. 테이블만 제대로 쪼개 두면, 처음에 상상도 안 한 조회를 나중에 SQL 한 줄로 낼 수 있다.

문서형에서는 그게 잘 안 된다. 읽을 모양대로 세워 둔 것이라, 세워 둔 모양이 아닌 질문이 오면 답할 준비가 안 되어 있다.

“이 판매자의 상품을 리뷰 평점 순으로” 같은 질문이 새로 생겼다고 하자. 관계형이면 조회 하나를 새로 짜면 되지만, 문서형이면 그 모양의 문서를 또 세워 두거나, 인덱스를 새로 만들거나, 전부 훑어야 한다.

그래서 순서가 뒤집힌다.

무엇을 먼저 정하나
관계형데이터의 구조를 정하고, 질문은 나중에 SQL로
문서형할 질문을 먼저 정하고, 그 답 모양대로 저장

이건 샤드 키가 되돌리기 가장 어려운 결정이었던 것과 같은 계열이다. 미리 정해야 하고, 나중에 바꾸려면 데이터를 통째로 다시 써야 한다.

대가 셋: 묶을 수 있는 범위가 좁다

트랜잭션은 “전부 되거나 전부 안 되거나”를 보장하는 묶음이었다. 문서형에서 그 묶음의 기본 단위는 대체로 문서 하나다.

문서 하나 안에서는 안전하다. 위 상품 문서에서 옵션 재고 둘을 같이 바꾸는 건 한 덩어리로 처리된다. 관계형이었으면 여러 행을 걸치는 일인데 여기서는 한 문서 안이다.

문제는 문서를 넘어갈 때다. 상품 문서와 주문 문서를 같이 바꿔야 하면 그건 두 덩어리이고, 중간에 실패하면 한쪽만 바뀐 채로 남는다. 요즘 제품들이 여러 문서 트랜잭션을 지원하긴 하지만 관계형만큼 싸지 않고, 샤드를 넘어가면 더 비싸진다.

그래서 문서형 모델링의 실질적인 규칙이 하나 나온다. 한 문서에 담을 범위 = 한 번에 같이 바뀌어야 하는 범위. 화면 단위가 아니라 변경 단위로 자르는 것이다.

여기서 창고 비유가 안 맞기 시작한다. 지금까지의 이야기는 창고가 한 채일 때의 계산이었다. 그런데 NoSQL 제품들이 실제로 태어난 계기는 대개 창고를 여러 채로 늘려야 했던 쪽이다. 완성품을 통째로 세워 두면 가구 하나가 창고 한 채 안에서 끝나서 창고를 나누기 쉽다. 부품 창고는 그게 안 된다. 조립하려면 여러 채를 뒤져야 하니까. 비유는 여기까지고, 그 앞은 샤딩 편이 다룬 이야기다.

경계는 생각보다 흐리다

“SQL이냐 NoSQL이냐”로 물으면 답이 잘 안 나오는데, 이유는 양쪽이 서로를 닮아왔기 때문이다.

  • 관계형 DB에 JSON 컬럼이 들어왔다. 그 안의 값으로 인덱스도 걸고 조회도 한다. 한 테이블 안에서 정형과 비정형을 섞을 수 있다.
  • 문서형 DB에 스키마 검증과 트랜잭션이 들어왔다. 필드 타입을 강제할 수도 있고, 여러 문서를 묶을 수도 있다.

그래서 제품 이름으로 고르는 질문은 점점 나쁜 질문이 된다. 남는 건 이 글이 처음부터 물어온 것 하나다. 이 데이터를 읽을 때 붙일 것인가, 쓸 때 붙일 것인가. 그 답에 따라 관계형 안에서 JSON 컬럼을 쓸 수도 있고, 문서형으로 갈 수도 있다.

그래서 실무에선 어떻게 쓰이나

한쪽으로 다 옮기지 않는다. 흔한 구성은 관계형이 원장을 쥐고, 자주 읽히는 화면용 데이터만 문서형에 사본으로 두는 것이다. 정확성이 걸린 곳은 조인과 트랜잭션이 있는 쪽에 남기고, 읽기 부하만 덜어낸다.

문서 크기가 자라는 방향을 본다. 배열을 품는 설계는 편한데, 그 배열이 끝없이 자라면 언젠가 문서 하나가 감당 못 할 크기가 된다. 상품에 옵션 열 개는 괜찮지만 상품에 리뷰 전부는 안 된다. 리뷰는 개수가 정해져 있지 않다. 그래서 위 예제도 리뷰 본문이 아니라 요약만 담았다.

질의를 먼저 적고 모델을 그다음에 짠다. 관계형에서 하던 순서와 반대라 처음에는 어색하다. “이 화면이 무엇을 묻는가”를 목록으로 적고, 그 목록에 답할 수 있는 문서 모양을 만든다.

조인이 그리워지면 대개 모델링이 틀린 것이다. 문서형에서 조인 비슷한 걸 흉내 내기 시작했다면, 잘라야 할 자리에서 안 자른 것이거나 애초에 관계형이 맞는 데이터일 수 있다. 도구를 탓하기 전에 자른 자리를 다시 본다.

정리

가르는 축질의어가 아니라 담는 모양. 읽을 때 붙이나, 쓸 때 붙이나
이름의 뜻SQL을 안 쓴다가 아니라 “SQL만은 아니다”
갈래문서 · 키-값 · 컬럼 패밀리 · 그래프. 그래프는 결이 다르다
스키마없어진 게 아니라 DB 밖으로 나갔다. 코드가 지킨다
중복반정규화가 선택이 아니라 기본값. 그래서 갱신 이상현상도 기본값
조회미리 세워 둔 모양만 싸다. 새 질문은 새 모양을 요구한다
트랜잭션기본 단위가 문서 하나. 그래서 문서 = 같이 바뀌는 범위
고르는 법제품이 아니라 접근을 고른다. 관계형 안의 JSON 컬럼도 답이 될 수 있다

읽을 모양대로 미리 붙여 두는 이 발상은, 사실 한 층 더 아래에서도 반복된다. 값을 제자리에 고쳐 쓸 것인가 그냥 덧붙여 둘 것인가 하는 문제다.

다음 글은 그 바닥을 본다. B-tree와 LSM-tree, 그리고 읽기와 쓰기 중 무엇을 파는가.