← Posts

Database · Design · Modeling

정규화 - 한 테이블은 한 가지 사실만 말한다

정규형을 외우지 않고도 판단하는 법, 그리고 반정규화가 실제로 무엇을 사고파는 거래인지.

목차
  1. 한 줄이 두 가지를 말하고 있다
  2. 그래서 생기는 세 가지 사고
  3. 쪼갠다
  4. 정규형은 외우지 않아도 된다
  5. 반정규화 - 무엇을 사고 무엇을 파는가
  6. 진짜 비용은 조인이 아니다
  7. 중복처럼 보이지만 아닌 것
  8. 정리

회원이 이사해서 주소를 바꿨다. 그런데 주문 내역 화면에는 옛 주소가 그대로다. 어떤 데는 새 주소, 어떤 데는 옛 주소가 뜬다.

같은 사실이 여러 곳에 적혀 있기 때문이다. 한 곳만 고쳤고, 나머지는 그대로 남았다.

여기까지가 조회동시성 얘기였다면, 이번엔 애초에 어떻게 담을 것인가다.

한 줄이 두 가지를 말하고 있다

주문 테이블에 회원 정보를 같이 넣어뒀다고 하자.

주문번호회원ID회원이름회원주소상품수량
1001U7김철수서울 강남키보드1
1002U7김철수서울 강남마우스2
1003U3이영희부산 해운대모니터1

편해 보인다. 조인 없이 한 번에 다 나온다.

그런데 이 한 줄은 서로 다른 두 사실을 말하고 있다.

mixed-factsmixed-facts

U7은 김철수이고 서울 강남에 산다회원에 관한 사실이다. 1001번 주문은 키보드 1개주문에 관한 사실이다. 둘은 아무 상관이 없는데 한 줄에 묶여 있다.

그래서 생기는 세 가지 사고

갱신 이상. 김철수가 이사했다. 주소를 고치려면 U7이 등장하는 모든 줄을 찾아 고쳐야 한다. 주문이 100건이면 100줄이다. 하나라도 놓치면 맨 앞의 그 상황이 된다. 같은 회원인데 주소가 두 개가 된다.

삽입 이상. 새 회원이 가입만 하고 아직 아무것도 안 샀다. 이 사람을 어디에 넣나? 주문 테이블에 넣으려면 없는 주문을 지어내야 한다. 주문번호도, 상품도 비워둬야 한다.

삭제 이상. 이영희가 1003 주문을 취소했다. 그 줄을 지우면 이영희라는 사람 자체가 사라진다. 주문을 지웠을 뿐인데 회원이 없어졌다.

셋 다 같은 뿌리다. 한 곳에 두 사실이 있으니 하나를 건드리면 다른 게 딸려온다.

쪼갠다

사실별로 테이블을 나눈다.

diagramdiagram

이제 김철수의 주소는 딱 한 곳에 있다. 이사하면 한 줄만 고친다. 놓칠 다른 줄이 없다. 가입만 한 회원도 MEMBERS에 넣으면 되고, 주문을 지워도 회원은 남는다.

세 가지 사고가 동시에 사라졌다. 셋이 하나의 원인에서 나왔으니 당연하다.

참고

정규화의 목적을 “중복 제거”라고 외우면 반쯤 놓친다. 중복을 없애는 건 수단이고, 목적은 위 세 가지 사고를 막는 것이다. 그래서 중복을 봤을 때 물어야 할 건 “중복인가”가 아니라 “이게 사고를 부르나” 다. 사고를 알면서 사는 경우(반정규화)와 애초에 중복이 아닌 경우를 뒤에서 하나씩 본다.

정규형은 외우지 않아도 된다

1NF, 2NF, 3NF… 시험에는 나오지만 실무에서 이걸 외워서 판단하는 사람은 별로 없다.

정규형은 “사실이 어떻게 섞였나”의 분류일 뿐이다.

무엇이 섞였나
1NF 위반한 칸에 여러 값전화번호: "010-1111, 010-2222"
2NF 위반복합키의 일부에만 딸린 값키가 (주문번호, 상품)인데 상품명은 상품에만 딸림
3NF 위반키가 아닌 값에 딸린 값키는 주문번호인데 회원주소회원ID에 딸림

셋을 관통하는 질문은 하나다.

이 칸은 무엇에 딸려 있나?

키 전체에 딸려 있지 않으면 나가야 한다. 위 표의 회원주소주문번호가 아니라 회원ID에 딸려 있다. 그러니 주문 테이블에 있을 자리가 아니다. 3NF니 이행종속이니 하는 말을 몰라도 이 질문 하나로 답이 나온다.

실무에서는 3NF면 대체로 충분하다. BCNF 이상이 필요한 상황은 드물고, 필요하면 그때 찾아봐도 늦지 않다.

반정규화 - 무엇을 사고 무엇을 파는가

일부러 되돌리는 것을 반정규화라고 한다. 대개 이유는 하나다. 조인이 싫어서.

그런데 이 거래의 정체를 정확히 봐야 한다.

반정규화는 성능을 사는 게 아니다. 이상현상 위험을 사서 조인을 파는 것이다. 앞서 본 세 가지 사고를 도로 떠안는 대신 조인을 없애는 교환이다.

그리고 여기서 앞 글들이 걸린다.

주의

반정규화 전에 실행 계획부터 보라. 조인 글에서 봤듯이 “조인이 느리다”의 상당수는 조인 탓이 아니었다. 통계가 낡아서 알고리즘 선택이 틀린 것이었다. 그걸 안 고치고 테이블을 합치면, 위험만 떠안고 원인은 그대로 남는다.

진짜 비용은 조인이 아니다

반정규화의 대가를 “데이터가 중복된다” 정도로 생각하면 가볍게 본 것이다. 디스크는 싸다.

진짜 비용은 책임이 넘어온다는 것이다.

정규화된 상태에서는 “김철수의 주소는 하나”라는 게 구조적으로 보장된다. 한 곳밖에 없으니 어긋날 수가 없다. 반정규화하면 “이 두 곳이 항상 같아야 한다”는 규칙이 새로 생기는데, DB는 그 규칙을 모른다.

ACID의 C에서 본 그대로다. 일관성은 DB에 맡겨두면 알아서 되는 게 아니라 내가 제약으로 표현한 만큼만 지켜진다. 그리고 “두 테이블의 이 값이 같아야 한다”는 제약은 표현하기가 어렵다. 결국 애플리케이션 코드가 지켜야 한다. 지금 짜는 코드뿐 아니라, 6개월 뒤 다른 사람이 짤 코드도.

중복처럼 보이지만 아닌 것

여기서 자주 헷갈리는 게 있다.

주문에 배송 주소를 저장한다고 하자. 회원 테이블에도 주소가 있으니 중복 같다. 반정규화 같다.

아니다. 다른 사실이다.

  • 회원의 주소 = 지금 이 사람이 사는 곳
  • 주문의 배송주소 = 그때 이 주문을 보낸 곳

회원이 이사해도 작년 주문의 배송지가 바뀌면 안 된다. 그건 이미 일어난 일이다. 만약 조인해서 현재 주소를 가져오면 과거가 왜곡된다.

값이 같아 보인다고 같은 사실이 아니다. “이 값이 언제 정해졌고, 무엇이 바뀌면 같이 바뀌어야 하나” 를 물으면 갈린다. 주문 시점에 못 박혀야 하는 값이면 그건 주문의 사실이고, 거기 있는 게 맞다.

가격도 같다. 상품 가격이 오른다고 작년 영수증 금액이 바뀌면 안 된다.

정리

  • 정규화의 목적은 중복 제거가 아니라 갱신·삽입·삭제 이상을 막는 것이다. 셋은 같은 뿌리에서 나온다.
  • 정규형을 외울 필요 없다. “이 칸은 무엇에 딸려 있나” 만 물으면 된다. 키 전체에 딸려 있지 않으면 나간다.
  • 반정규화는 이상현상 위험을 사서 조인을 파는 것이다. 그 전에 조인이 진짜 범인인지 실행 계획으로 확인한다.
  • 진짜 비용은 디스크가 아니라 DB가 보장하던 걸 사람이 지키게 되는 것이다.
  • 이력은 중복이 아니다. 그때 못 박혀야 하는 값이면 거기 있는 게 맞다.
참고

다음 글은 키다. 무엇을 기본키로 삼을지, 자연키와 대리키 중 무엇을 쓸지. 정규화가 “무엇을 어디에 둘까”였다면 키는 “그걸 무엇으로 식별할까”다.

관련 글