![[실전 DB 12] 비정규화, 셀 것을 미리 저장하기](https://img.thenullpage.com/posts/5704/5704_1_581b64.webp)
앞 편에서 댓글 수 같은 집계를 미리 저장해두는 비정규화를 잠깐 다뤘다. 이건 데이터베이스 최적화에서 강력하면서도 조심스러운 기법이다. 매번 세는 대신 저장된 값을 읽으니 빠르지만, 그 값이 실제와 어긋나지 않게 관리해야 한다. 이번 편은 비정규화의 원리와 판단 기준, 그리고 어긋남을 다스리는 법을 다룬다.
비정규화는 정석적인 데이터베이스 설계를 일부러 어기는 것이라, 언제 왜 하는지를 정확히 알아야 무기가 되고 그렇지 않으면 짐이 된다.
1. 정규화와 비정규화
데이터베이스 설계의 기본은 정규화다. 같은 정보를 중복 저장하지 않고 한 곳에만 두는 것이다. 댓글 수는 댓글을 세면 나오니, 굳이 따로 저장하지 않는 게 정규화의 원칙이다. 중복이 없으면 데이터가 서로 어긋날 일도 없고 관리가 깔끔하다.
비정규화는 이 원칙을 일부러 어기는 것이다. 세면 나오는 값을 굳이 따로 저장해 중복으로 둔다. 댓글 수를 글에 함께 저장해두는 것이다. 왜 깔끔한 정규화를 어기냐면, 성능 때문이다. 매번 세는 게 비싸니, 중복 저장을 감수하고 세기를 없애는 것이다.
그래서 비정규화는 성능과 깔끔함을 맞바꾸는 것이다. 데이터가 중복돼 관리가 복잡해지는 대신, 세는 비용을 없애 빨라진다. 이 교환이 이득일 때만 비정규화한다. 자주 읽고 세기가 비싼 값이면 이득이고, 드물게 읽거나 세기가 싸면 굳이 안 한다. 교환의 손익을 따지는 것이 시작이다.
비정규화를 처음 접하면 마치 규칙을 깨는 편법처럼 느껴진다. 하지만 정규화도 절대 법칙이 아니라 관리를 쉽게 하려는 원칙일 뿐이다. 그 원칙이 성능이라는 더 큰 목적과 충돌할 때, 근거를 갖고 어기는 것이 비정규화다. 규칙을 아는 사람이 이유를 갖고 어길 때에만 이건 편법이 아니라 설계가 된다.
비정규화가 낯설게 느껴지는 또 하나의 이유는, 배울 때 정규화를 미덕으로 배우기 때문이다. 중복은 나쁜 것, 한 곳에만 두는 게 옳은 것이라고 배운다. 그 배움이 몸에 배면 일부러 중복을 두는 게 잘못처럼 느껴진다. 하지만 그 미덕도 목적이 있는 것이고, 목적이 성능과 부딪히면 저울질의 대상이 된다는 걸 받아들여야 한다.
2. 언제 비정규화하나
비정규화를 하는 판단 기준은 읽기 빈도와 세기 비용이다. 그 집계 값을 자주 읽는데 세기가 비싸면, 비정규화로 큰 이득을 본다. 목록마다 보여주는 댓글 수나 추천 수가 대표적이다. 자주 보여지는데 매번 세면 앞 편의 반복 조회 문제가 된다. 이런 건 저장해두는 게 낫다.
반대로 드물게 읽는 값은 비정규화할 이유가 없다. 어쩌다 한 번 보는 통계라면, 그때 세면 된다. 저장해두고 갱신하는 부담을 계속 질 필요가 없다. 세기가 싼 값도 마찬가지다. 작은 범위를 세는 건 부담이 적으니 그때그때 세도 된다. 비정규화는 자주 읽고 세기가 비싼 값에만 선별적으로 한다.
내가 만든 사이트에서 비정규화한 건 목록에 늘 보이는 집계들이었다. 댓글 수, 추천 수처럼 모든 목록에 나오면서 매번 세면 비싼 값들이다. 이것들을 각 글에 저장해두니 목록 성능이 크게 좋아졌다. 반면 드물게 보는 값들은 그때 세도록 두었다. 어느 것을 저장하고 어느 것을 그때 셀지 선별하는 게 비정규화의 요령이다.
판단이 애매할 땐 다시 측정으로 돌아간다. 그 세기가 실제로 전체 읽기에서 큰 비중을 차지하는지 재보는 것이다. 비중이 크면 비정규화의 이득이 확실하고, 작으면 굳이 관리 부담을 질 필요가 없다. 짐작으로 여기저기 비정규화하지 말고, 측정으로 병목이 확인된 곳만 하는 게 안전하다.
비정규화한 값에는 성능 말고 다른 쓸모가 붙기도 한다. 저장된 댓글 수로 목록을 정렬하거나 거르는 것도 쉬워진다. 매번 세지 않고 저장된 숫자로 바로 정렬하니, 인기순 같은 화면도 가벼워진다. 세기를 없애려 저장한 값이 정렬과 필터에도 재활용되는 것이다. 한 번 잘 저장해두면 여러 곳에서 이득을 본다.
3. 저장된 값을 어떻게 갱신하나
비정규화의 핵심 과제는 저장된 값을 실제와 맞게 유지하는 것이다. 댓글 수를 저장해뒀으면, 댓글이 달리거나 지워질 때마다 그 값을 갱신해야 한다. 갱신을 놓치면 저장된 값이 실제와 어긋난다. 이 갱신을 확실히 하는 게 비정규화의 관건이다.
갱신 방법은 그 값을 바꾸는 작업에 저장 값 갱신을 함께 묶는 것이다. 댓글을 다는 흐름 안에 글의 댓글 수를 하나 늘리는 작업을 넣고, 댓글을 지우는 흐름에 하나 줄이는 작업을 넣는다. 댓글 변화와 저장 값 갱신을 한 흐름으로 묶어, 하나가 일어나면 다른 것도 반드시 함께 일어나게 하는 것이다.
이때 두 작업이 따로 놀지 않게 하는 게 중요하다. 댓글은 달렸는데 수 갱신이 실패하면 값이 어긋나기 시작한다. 그래서 관련된 작업을 하나의 원자적 단위로 묶어, 함께 성공하거나 함께 실패하게 한다. 절반만 반영되는 상태를 없애는 것이다. 이 원자성이 비정규화 값의 신뢰를 지킨다.
이건 앞선 시리즈에서 여러 번 나온 원리다. 관련된 작업을 흩어두지 말고 한 흐름으로 묶는 것이다. 글 삭제와 딸린 데이터 정리를 묶고, 글 삭제와 사이트맵 제거를 묶었듯, 댓글 변화와 댓글 수 갱신을 묶는다. 그래야 사람이 매번 기억하지 않아도 정합성이 저절로 유지된다.
갱신을 흐름에 묶을 때 흔한 실수가, 정상 경로에만 넣고 예외 경로를 빠뜨리는 것이다. 댓글을 다는 보통의 흐름엔 수 증가를 넣었는데, 글이 통째로 지워지며 댓글이 함께 사라지는 경로엔 안 넣는 식이다. 그러면 그 경로로 데이터가 바뀔 때마다 값이 어긋난다. 값을 바꾸는 모든 경로를 빠짐없이 챙겨야 갱신이 완성된다.
4. 어긋남에 대비하기
아무리 갱신을 잘 묶어도, 저장된 값이 어긋날 수 있다. 어떤 이유로 갱신이 빠지거나, 데이터를 직접 건드려 값이 안 맞게 될 수 있다. 그래서 비정규화한 값은 어긋남에 미리 대비하는 게 좋다. 주기적으로 실제 값과 대조해 맞추는 것이다.
이 대조를 정기 작업으로 할 수 있다. 저장된 댓글 수와 실제 댓글 수를 주기적으로 비교해, 어긋난 게 있으면 실제 값으로 바로잡는다. 앞서 다룬 정기 작업, 즉 정해진 시각에 도는 작업으로 이 정합성 점검을 돌리는 것이다. 평소엔 갱신으로 맞추고, 혹시 어긋난 건 정기 점검으로 잡는 이중 안전망이다.
이 대비가 비정규화를 안전하게 만든다. 비정규화의 가장 큰 위험이 값이 어긋나는 것인데, 갱신을 묶어 대부분 맞추고 정기 점검으로 나머지를 잡으면 그 위험이 관리된다. 비정규화의 성능 이득을 누리면서 정합성 위험은 통제하는 것이다. 안전망이 있으면 마음 놓고 비정규화를 쓸 수 있다.
내 경우 이 정기 점검에서 실제로 몇 건의 어긋남을 발견하고 바로잡은 적이 있다. 데이터를 직접 손대는 과정에서 갱신이 빠진 것들이었다. 점검이 없었다면 그 어긋남이 조용히 남아 잘못된 숫자를 계속 보여줬을 것이다. 안전망은 평소엔 아무 일도 안 하는 것 같지만, 이런 순간에 값을 한다.
정기 점검을 만들 때는 그 점검 자체가 무겁지 않게 한다. 전체를 매번 다 세어 대조하면, 어긋남을 잡으려다 오히려 큰 읽기가 든다. 그래서 한 번에 다 훑기보다 나눠서 조금씩 대조하거나, 최근 바뀐 것 위주로 점검하는 식으로 부담을 줄인다. 안전망도 비용이 있으니, 그 비용이 이득을 넘지 않게 조절하는 것이다.
5. 비정규화는 신중한 선택이다
비정규화는 강력하지만 신중해야 한다. 정규화의 깔끔함을 포기하고 관리 부담을 지는 것이라, 함부로 하면 어긋난 데이터가 쌓인다. 그래서 정말 필요한 값에만, 갱신과 점검을 확실히 갖춘 상태에서 한다. 성능 이득이 확실하고 관리를 감당할 수 있을 때만 하는 것이다.
기본은 정규화이고 비정규화는 예외라는 감각이 좋다. 웬만하면 정규화로 깔끔하게 두고, 성능 병목이 확실한 특정 집계에만 비정규화를 적용한다. 모든 걸 비정규화하려 들면 관리가 감당 안 된다. 꼭 필요한 곳에만 선별적으로 쓰는 게 비정규화를 짐이 아닌 무기로 만드는 길이다.
비정규화를 겪으며 얻은 감각은, 편리한 최적화일수록 뒤에 관리 부담이 따른다는 것이다. 값을 저장해두면 읽기는 편해지지만, 그 값을 실제와 맞추는 책임이 새로 생긴다. 세상에 공짜 최적화는 드물고, 대개는 한쪽의 편함을 다른 쪽의 부담과 맞바꾸는 것이다. 그 부담을 감당할 수 있을 때에만 그 편함을 취한다.
정리하면 비정규화는 세면 나오는 값을 미리 저장해 세기 비용을 없애는 기법으로, 자주 읽고 세기가 비싼 값에 선별적으로 하되 갱신을 흐름에 원자적으로 묶고 정기 점검으로 어긋남에 대비한다. 다음 편에서는 읽기를 더 근본적으로 줄이는 캐시 계층을 데이터베이스 관점에서 다룬다.