목차
지금까지는 컬럼이 정해진 세계였다. 문서형 저장소에는 그 제약이 없다. 그러면 이 시리즈의 문제들도 같이 없어지나.
ALTER가 없는 세계
문서형 저장소에서 필드를 하나 더하는 방법은 그냥 넣는 것이다.
db.users.insertOne({ id: 1, nickname: "kim", displayName: "김" })기존 문서에는 displayName이 없고 새 문서에는 있다. DB는 아무 말도 안 한다. 선언할 것도, 잠글 것도, 기다릴 것도 없다. 스키마가 없어진 게 아니라 코드로 밀려난 것이라는 이야기는 이미 했으니, 여기서는 그래서 구조를 바꿀 때 무슨 일이 벌어지나만 본다.
이것만 보면 앞에서 본 고생이 통째로 사라진 것 같다. 절반은 맞다.
없어진 건 잠금이지 일이 아니다
사라진 것부터 정확히 세어 보자.
| 앞에서 본 문제 | 문서형에서 |
|---|---|
| DDL이 통로를 막는다 | 없다. 선언 자체가 없다 |
| 테이블을 통째로 다시 쓴다 | 없다. 문서마다 따로 있다 |
| 기존 문서가 새 필드를 안 갖고 있다 | 그대로 남는다 |
| 옛 모양과 새 모양이 섞인다 | 오히려 커진다 |
윗줄 둘이 없어진 건 진짜 이득이다. 5천만 건짜리 테이블에 컬럼을 더하려고 새벽까지 기다리던 일이 사라진다.
그런데 아랫줄 둘은 그대로다. 기존 5천만 개 문서에는 여전히 displayName이 없다. 관계형에서는 NULL이라는 이름으로 있었고 여기서는 아예 필드가 없다는 차이뿐이다. 읽는 쪽이 그 상태를 감당해야 하는 건 똑같다.
가게로 치면 공사 허가를 안 받아도 되는 것이다. 통로를 막을 일이 없어졌다. 그런데 물건을 새 진열대로 옮기는 일은 누가 대신 해주지 않는다.
세 갈래로 갈린다
기존 문서를 어떻게 할지가 갈린다. 실무에서 쓰이는 답은 셋이다.
- 읽을 때 맞춰준다. 문서를 꺼낼 때 코드가 “
displayName이 없으면nickname을 쓴다”고 처리한다. 저장소는 안 건드린다. - 배경에서 고친다. 따로 도는 작업이 옛 문서를 찾아 새 모양으로 바꿔 쓴다.
- 안 한다. 옛 문서는 옛 모양대로 두고 새것만 새 모양으로 쌓는다.
세 번째가 문서형에서 가장 흔한 선택인데, 선택했다는 자각 없이 그렇게 되는 경우가 대부분이다. 아무것도 안 하면 저절로 세 번째가 되기 때문이다.
읽을 때 맞추면 코드에 지층이 쌓인다
첫 번째 방법은 당장 편하다. 저장소를 안 건드리니 위험이 없고, 배포 한 번으로 끝난다.
문제는 그게 영구적이라는 것이다. 옛 모양 문서가 하나라도 남아 있는 한 그 처리 코드는 못 지운다. 그리고 구조 변경은 한 번으로 끝나지 않는다.
const name =
doc.displayName ?? // 3차 개편 이후
doc.profile?.name ?? // 2차 개편 때
doc.nickname; // 최초3년 치 개편이 읽기 코드 한 곳에 지층으로 쌓인다. 새로 온 사람은 이 세 줄이 왜 있는지 모르고, 하나를 지워도 되는지 판단할 방법이 없다. 옛 모양 문서가 남아 있는지 확인하려면 결국 전수 조사를 해야 하는데, 그럴 거면 처음부터 고치는 게 나았다.
관계형 쪽에서 봤던 “지우기를 미루면 방치가 된다”가 여기서는 스키마가 아니라 코드에 나타난다.
배경에서 고치면 결국 백필이다
두 번째 방법은 이름만 다를 뿐 백필과 같은 일이다. 그리고 챙겨야 할 것도 같다.
- 끊어서 돌린다
- 진행 위치를 적어 둔다
- 도는 동안 새로 들어오는 문서는 이미 새 모양이어야 한다
- 다 됐는지 세어 본다
바뀌는 건 하나다. 읽을 때 맞춰주는 코드를 같이 둬야 한다. 백필이 도는 중에는 옛 모양과 새 모양이 섞여 있으니까. 그리고 백필이 끝나면 그 코드를 지운다 - 지우는 게 이 방법의 목적이다.
첫 번째와 두 번째의 차이는 여기다. 읽을 때 맞추기만 하면 그 코드가 영원히 남고, 백필까지 하면 끝이 있다.
버전 필드를 두면 끝이 보인다
옛 모양이 남았는지 확인하는 문제는 문서마다 자기 모양을 적어 두면 풀린다.
{ "_v": 3, "id": 1, "displayName": "김" }이 숫자 하나가 두 가지를 준다. 읽는 코드는 _v로 갈라 처리하면 되니 필드를 하나씩 더듬어 볼 필요가 없고, _v가 3보다 작은 문서가 몇 개인지 세면 마이그레이션이 끝났는지 바로 알 수 있다.
관계형에서 기록부가 하던 일을 문서 하나하나가 나눠 갖는 셈이다. 다만 위치가 다르다 - 관계형은 DB 전체가 하나의 상태를 갖지만, 문서형은 문서마다 상태가 다를 수 있다. 그래서 “우리 DB는 3번까지 됐다”가 아니라 “3번까지 온 문서가 99.98%다”가 된다.
버전 필드는 처음부터 넣어야 값이 있다. 이미 세 번 개편한 뒤에 넣으면 옛 문서들에는 _v가 없어서, 그것들을 가려내는 문제가 또 남는다. 첫 설계 때 넣는 숫자 하나가 나중의 전수 조사를 아낀다.
관계형 안의 JSON 칸도 같은 값을 치른다
요즘은 관계형 DB에도 JSON 컬럼이 있다. 컬럼 하나 안에 자유로운 구조를 넣으니 “선언 없이 필드를 더할 수 있다”는 이점을 그대로 가져온다.
그러면 값도 그대로 따라온다. 그 컬럼 안의 구조를 바꾸는 순간 이 글의 문제가 전부 나타난다. DB는 그 안을 안 지켜주므로 옛 모양이 섞이고, 읽는 코드에 지층이 쌓이고, 고치려면 백필을 해야 한다.
그래서 판단 기준이 생긴다. 자주 바뀌고 조회 조건에 안 들어가는 것을 JSON 칸에 넣는다. 반대로 모든 행이 반드시 갖고 조회 조건에 들어가는 것을 JSON에 넣으면, 컬럼으로 뺐으면 DB가 공짜로 해줬을 검사를 코드가 떠안는다.
여기서 가게 비유를 닫는다
이 시리즈를 가게 개조로 끌고 왔다. 마지막으로 안 맞는 자리를 정리한다.
- 가게는 밤에 닫을 수 있다. 가장 큰 균열이고 첫 편에서 밝힌 것이다. 점검 시간을 낼 수 있으면 여기서 본 것 대부분은 필요 없다.
- 가게 공사는 눈에 보인다. DDL이 통로를 막은 것도, 벽을 헌 것도 안 보인다. 그래서 스스로 재고 관측해야 한다.
- 가게는 물건을 옮기면 옛 자리가 빈다. 데이터는 복사라서 두 벌이 공존하고, 공존하는 동안 어긋날 수 있다.
그래도 비유가 끝까지 버틴 것이 하나 있다. 손님이 들어오는 채로 고쳐야 한다는 것, 그리고 그 제약이 순서를 정한다는 것이다. 문을 닫을 수 있으면 순서는 아무래도 좋다.
실무에서: 어느 쪽이든 물어볼 것은 같다
관계형이든 문서형이든, 구조를 바꾸기로 했을 때 물어볼 것은 셋으로 같다.
| 관계형 | 문서형 | |
|---|---|---|
| 거는 동안 남을 막나 | DDL이 얼마나 잡나 | 안 막는다 |
| 기존 것을 어떻게 채우나 | 백필 | 백필 · 읽을 때 맞추기 |
| 끝났는지 어떻게 아나 | 남은 행 세기 | 버전 필드로 세기 |
문서형은 첫 줄이 공짜다. 그게 진짜 이점이고, 그래서 큰 테이블을 자주 바꾸는 곳에서 값이 크다.
대신 둘째와 셋째 줄이 저절로 되지는 않는다. DB가 안 막아주는 만큼 “다 됐는지”를 물어볼 곳도 없어져서, 세는 장치를 스스로 만들어 둬야 한다. 유연함은 검사를 없앤 게 아니라 검사할 사람을 바꾼 것이다.
정리
- 문서형에서 사라지는 건 잠금이지 일이 아니다. DDL이 통로를 막는 문제는 진짜로 없어진다.
- 기존 문서를 채우는 일은 그대로 남고, 옛 모양과 새 모양이 섞이는 문제는 오히려 커진다.
- 읽을 때 맞춰주기만 하면 그 코드가 영원히 남는다. 끝을 보려면 결국 백필이다.
- 버전 필드 하나가 “다 됐는지”를 셀 수 있게 만든다. 처음부터 넣어야 값이 있다.
- 관계형의 JSON 칸도 같은 값을 치른다. 자유로운 구조를 산 대가는 저장소가 아니라 코드가 낸다.