정규화(Normalization)

2023. 3. 3. 16:47SQL/SQL 전문가 가이드(2020 개정판)

데이터 모델링에서 정규화(Normalization)는 가장 기초적이지만 필수적으로 이뤄져야 하는 작업입니다. 성능을 위해 반정규화를 하기도 하지만, 그 이전에 정규화가 왜 필요한지를 반드시 알아야 합니다.

 

1. 제1 정규형: 모든 속성은 반드시 하나의 값을 가져야 한다

 

제1정규형은 하나의 속성에는 하나의 값을 가져야 합니다. 연락처 속성에 다중값(multivalued)이 들어가는 경우를 생각해 봅시다.

 

고객연락처 데이터

고객번호 고객명 연락처
10000 정우진 02-123-4567, 010-1234-5678
10001 한형식 010-5678-2345
10002 황영은 02-345-3456, 010-4567-7890

다음 표와 같이 데이터가 생성된다면 어떤 문제가 발생할 수 있는지 생각해 봅시다.

 

● 연락처 정보에서 집전화번호와 핸드폰 번호를 구별하기가 어렵습니다.

● A 고객은 집전화가 여러 대고, B 고객은 핸드폰이 여러 대라면 혼재된 속성에서 원하는 속성 값을 추출하기 어렵습니다.

●명확하지 않은 속성은 이메일처럼 다른 유형의 데이터를 포함할 수도 있어 본연의 의미가 퇴색될 수 있습니다.

 

이와 같이 데이터를 관리한다면 개발의 복잡성은 증가할 것이며, 연락처의 속성은 그 의미가 점차 퇴색될 것입니다.

고객연락처라는 엔터티를 추가하여 다중 값에 대한 문제점을 해결했습니다. 본 모델을 데이터로 표현하면 다음 표와 같습니다.

 

고객과 고객연락처 데이터

고객번호 고객명
10000 정우진
10001 한형식
10002 황영은

[고객]

고객번호 순번 연락처
10000 1 02-123-4567
10000 2 010-1234-5678
10001 1 010-5678-2345
10002 1 02-345-3456
10002 2 010-4567-7890

[고객연락처]

 

데이터를 보면 고객의 연락처가 많아져도 아무런 문제가 되지 않습니다. 집전화 번호 또는 핸드폰 번호를 구분하고 싶다면, 고객연락처 엔터티에 '연락처구분코드' 속성을 추가하면 됩니다. '연락처구분코드' 속성을 추가한다면, 이메일 등의 연락처 정보도 수용 가능합니다.

주문 엔터티로 주문이 발생했을 때의 정보를 관리합니다. 본 주문 모델을 보고, 우려되는 점을 생각해 봅시다.

● 상품을 3개 이상 주문할 수 없습니다.

● 상품 1, 상품 2 모두 빠르게 조회하고 싶다면 속성마다 인덱스를 추가해야 합니다.

 

모델에서 상품을 2개까지만 주문할 수 있으며, 3개 이상의 상품은 주문할 수 없습니다. 주문하고 싶을 때마다 속성을 매번 추가해야 하기에 요즘처럼 365일 24시간 서비스를 지속해야 하는 환경에서는 정해진 PM(Prevention Maintenance) 시간에 진행하게 됩니다. 또한, 상품명 1, 상품명 2를 빠르게 조회하기 위해서 상품번호 1, 상품번호 2 속성 모두에 인덱스를 추가해야 하는데, 인덱스를 추가한다는 것은 조회(SELECT) 속도는 빨라질 수 있으나, 입력·수정·삭제 속도는 느려진다는 것을 고려해야 합니다.

다음 그림과 같이 주문상세 엔터티를 추가하면 본 모델에서 상품을 몇 개를 주문하던 아무런 제약을 받지 않게 되고, 추가적인 인덱스도 필요 없습니다.

 

1. 제2 정규형: 엔터티의 일반속성은 주식별자 전체에 종속적이어야 한다.

 

주문상세 데이터

주문번호 상품번호 상품명
1100001 256 SQL 전문가 가이드
1100002 257 데이터아키텍처 전문가 가이드
1100003 256 SQL 전문가 가이드
1100004 256 SQL 전문가 가이드
1100005 258 데이터 분석 전문가 가이드

데이터를 확인해 보면 'SQL 전문가 가이드'라는 데이터가 반복되는 것을 볼 수 있습니다. 본 표에서 중복되는 데이터는 상품명 외 상품번호도 존재합니다. 그러나 상품번호는 고객이 상품을 주문함으로 발생하는 매핑 정보로서 의미를 가지고 있습니다.

함수종속성(Functional Dependency)은 데이터들이 어떤 기준값에 의해 종속되는 현상을 지칭합니다. 이때 기준값을 결정자(Determinant)라 하고, 종속되는 값을 종속자(Dependent)라고 합니다. 상품명은 상품번호에 종속되어 있기에 종속자이며, 상품번호는 상품명을 결정하기에 결정자입니다. 다음 그림에서 주문상세 엔터티의 상품명은 식별자 전체가 아닌 일부에만 종속적인데, 이를 부분 종속(Partial Dependency)이라 합니다. '엔터티의 일반속성은 주식별자 전체에 종속되어야 한다'는 제2 정규형을 위배한 데이터의 문제점을 정리해 봅시다.

 

● 상품명이 변경되고 업무적으로 반영해주어야 한다면, 주문상세의 중복된 상품명을 모두 변경해야 합니다. 이때 많이 팔린 상품일수록 주문상세에서 변경해야 할 상품명의 부하도 크게 증가합니다.

● 주문상세의 상품명을 변경한다고 해도 특정 시점에는 아직 변경되지 않은 상품명이 존재하고, 이때 들어온 트랜젝션은 일관되지 않는 데이터를 조회하게 됩니다.

상품 엔터티를 추가하여 주문상세 엔터티의 부분 종속성을 제거할 수 있습니다. 상품명 속성을 상품 엔터티에서 관리하고, 상품번호를 매핑키로 활용하여, 상품명을 확인하는 구조로 데이터를 일원화해 관리함으로 위에서 제시한 문제점을 해결할 수 있습니다. '일반속성은 주식별자 전체에 종속해야 한다'는 제2 정규형을 만족하게 됩니다.

 

상품과 주문상세 데이터

상품번호 상품명
256 SQL 전문가 가이드
257 데이터아키텍처 전문가 가이드
258 데이터 분석 전문가 가이드

기존 주문상세 엔터티에서 상품엔터티를 분리하여 상품정보를 관리하도록 했습니다. 이렇게 데이터를 관리하면 주문상세 엔터티에서 상품번호만 들고 있고, 상품번호를 매핑키로 상품 엔터티에서 원하는 상품정보 데이터를 가져올 수 있으며, 이를 조인(Join)이라고 합니다. 또한 상품명이 변경되었다면 상품 엔터티에서 데이터를 일원화해 관리하고 있어 중복 데이터에 대한 문제점도 해결할 수 있습니다.

 

3. 제3 정규형: 엔터티의 일반속성 간에는 서로 종속적이지 않는다

고객번호는 주문번호에 종속적이고, 고객명은 고객번호에 종속적입니다. 이는 '고객명이 주문번호에 종속저'임을 의미하는데, 이를 이행적 종속(Transitive Dependency)이라 하고, 이행적 종속을 배제하는 것을 제3 정규형이라 합니다.

 

주문번호 → 고객번호 이고 고객번호 → 고객명 이면 주문번호 → 고객명 이다.

 

고객번호와 고객명 모두 주문번호에 종속하여 제2 정규형은 만족하였으나, 고객명이 식별자가 아닌 일반속성에 종속적인 제3 정규형 위배에 해당합니다.

 

● 만일 고객이 이름을 바꿔 고객명이 변경되었다면, 주문 엔터티에 고객명을 전부 갱신해야 합니다. 이는 주문과는 전혀 연관 없는 트랜젝션입니다.

● 데이터 중복으로 인해 발생하는 문제는 성능 부하 및 정합성 오류로 제2차 정규형과 동일합니다.

 

고객명 '정세준'에서 '정우진'으로 변경되었다면 주문 엔터티의 '정세준'이라는 고객명을 찾아 '정우진'으로 변경해줘야 합니다. 이때 '정세준' 고객이 주문한 내역이 많다면 성능 부하와 특정 시점에 발생하는 정합성 문제를 내재하고 있는 것입니다. 고객 엔터티를 분리하여 관리해야 합니다.

모델의 고객 엔터티를 보면 고객 속성 변경이 주문 엔터티에 영향을 주지 않는 구조입니다. 또한 데이터 중복에 대한 문제도 개선되었다고 볼 수 있으며, 주문 엔터티에 고객번호가 Null 허용인 것을 의아하게 생각한다면, 비회원도 주문이 가능한 구조라고 이해하면 됩니다. 정규화는 필수적이지만 무조건적이지는 않습니다. 상황에 따라서는 반정규화를 진행할 수도 있으며, 중요한 것은 기본적으로 정규화를 진행하고, 반정규화를 고려해야 합니다. 이로써 무분별한 반정규화를 방지하고, 무심코 놓칠 수 있는 부분도 챙길 수 있습니다.

 

4. 반정규화와 성능

반정규화는 정규화를 반대로 하는 것으로 역정규화라고 합니다. 정규화는 데이터의 중복을 최소화했다면, 반정규화는 성능을 위해 데이터 중복을 허용하는 것입니다. 반정규화가 조회성능을 향상할 수 있을지 모르겠으나, 그로 인한 입력·수정·삭제 성능은 저하될 수 있습니다.

 

가. 반정규화를 적용한 모델에서 성능이 향상될 수 있는 경우

본 모델에서 생소한 속성만 설명하면, 주문 엔터티에서 주문상태코드는 주문 상태에 대한 코드값으로 '주문·취소·반품·교환' 등의 정보를 관리하고, 결제일시 속성은 실제 결제를 진행한 일시정보를 관리합니다. 결제 엔터티에서 결제수단구분코드 속성은 '카드결제·계좌이체·핸드폰결제' 등을 관리하는 코드값입니다. 결제수단번호 속성은 결제수단구분코드에서 사용한 실제 '카드번호·계좌번호·핸드폰번호' 등을 관리하는 속성값입니다.

모델은 결제 엔터티에 고객번호 속성을 반정규화했으며, 수정된 SQL은 다음과 같습니다.

SELECT A.결제수단번호
  FROM (SELECT B. 결제수단번호
  			FROM 주문 A, 결제 B
            WHERE A.주문번호 = B.주문번호
               AND A.고객번호 = 1234
               AND B.결제수단구분코드 = '신용카드'
             ORDER BY B.결제일시 DESC
           ) A
  WHERE ROWNUM = 1;

본 SQL문은 고객번호가 1234인 고객의 주문정보를 결제 테이블과 조인으로 가져온 후, 신용카드 결제 접보를 결제일시로 내림차순 정렬해 최근 1거읜 결제수단번호를 가져오는 SQL입니다. 1234 고객의 주문 내역이 많을수록 성능이 나빠지는 문제가 존재합니다. 최종결과는 1건을 가져오지만, 주문내역이 많을수록 해당 주문테이블과 결제테이블의 조인 건수가 증가하게 되며, 조인된 결제정보를 모두 읽고, 내림차순 정렬하여 최근 1건의 데이터를 가져옵니다. 즉, 주문내역이 많을수록 조인에 대한 부하가 증가하여 성능이 나빠지는 구조입니다.

모델은 결제 엔터티에 고객번호 속성을 반정규화했습니다. 수정된 SQL은 다음과 같습니다.

SELECT A.결제수단번호
  FROM (SELECT A.결제수단번호
  			FROM 결제 A
            WHERE A.고객번호 = 1234
              AND A.결제수단구분코드 = '신용카드'
            ORDER BY A.결제일시 DESC
          ) A
  WHERE ROWNUM = 1;

결제 테이블에 '고객번호+결제수단구분코드+결제일시'로 인덱스를 생성하고, 'Index Range Scan Descending'으로 최종 1건의 데이터만 읽어 결제수단번호를 가져와 최적의 SQL로 성능 부하를 극적으로 개선할 수 있습니다. 

 

나. 반정규화를 적용한 모델에서 성능이 저하될 수 있는 경우

주문정보는 주문 엔터티에서 가져올 수 있고, 송장번호는 배송 엔터티에서 가져올 수 있습니다. 즉 주문과 배송 엔터티를 함께 조인해야 합니다. 그러나 성능을 위해 주문 엔터티에 송장번호를 반정규화했습니다.

주문 엔터티에 송장번호를 반정규화하면, 배송 엔터티와 조인을 하지 않아도 됩니다. 조인을 제거했기에 더 빠른 성능을 확보할 수 있습니다. 주문 모델의 송장번호는 주문 시점에는 NULL 데이터가 들어가며, 배송준비가 완료되어야 송장번호를 갱신(UPDATE)할 수 있게 됩니다. 반정규화를 하기 전에는 없었던 갱신(UPDATE) 로직이 새로 추가되었습니다. 그러나 이점이 미미한 경우엔 불필요한 로직으로 인해 발생되는 과금을 유의하여 반정규화가 꼭 필요할 때에 적용해야 합니다. 반정규화는 데이터 불일치로 인한 정합성 문제뿐 아니라, 불필요한 트랜젝션으로 인한 성능 문제를 만들어내기 때문입니다.

'SQL > SQL 전문가 가이드(2020 개정판)' 카테고리의 다른 글

모델이 표현하는 트랜잭션의 이해  (0) 2023.03.05
관계(Relationship)와 조인(Join)의 이해  (2) 2023.03.04
식별자(Identifier)  (0) 2023.03.02
관계(Relationship)  (0) 2023.03.01
속성(Attribute)  (0) 2023.02.28