#title 보이스-코드 정규화와 XML
[[TableOfContents]]
아마도 데이터베이스에서의 정규화는 C언어의 포인터(*)와 같은 레벨일 것이다. 많은 사람들이 C언어를 공부하다가 포인터에서 좌절을 느끼고, 데이터베이스에서는 정규화에서 좌절을 느낄 것이다. 정규화라는 말 자체도 어려울뿐더러 정규화를 설명하는 말도 매우 어려운 것이 사실이다. 또한 실무에 적용하기도 만만치 않다. 만약 정규화에 자신이 없다면 보이스-코드 정규화[* BCNF:Boyce-Codd Normal Form]라도 제대로 알고 있어야 한다. 왜냐하면 대부분의 경우 BCNF만 해도 95점을 딸 수 있고, 개념자체도 매우 쉽기 때문이다. 차근히 살펴보자.
==== 함수적 종속과 결정자 ====
y = f(x)
f(x) = x^2^
y = 4 라면, x = 2 or -2
함수의 내용을 알고 있다고 가정하고, y가 단일값이라면, y가 종속자이고 x는 결정자이다. 왜냐하면 함수 f(x)의 x값은 2와 -2로 y값인 4는 x값을 결정하지 못하기 때문이다. 즉, y=4인 경우 x의 값은 2 또는 -2 로 답할 수 있다. 이는 x의 값이 2인지 -2인지 결정할 수 없음을 말한다. 만약 x=3이라면 y값은 9가된다. 즉, y값을 x값이 결정했다. 함수적 종속을 표현[* 일반적으로 화살표(->)로 표시]하면
x -> y
과 같이 표현할 수 있고, "y는 x에 함수적으로 종속된다. 결정자는 x이고, 종속자는 y이다." 라고 이야기 하면 된다. y의 값이 4가 된 것은 x=2,-2와 함수 f()의 내용이 결정을 했다는 것만 알면 BCNF는 95% 완료다.
현실로 돌아오자. 예를 들어, 어느 게임포탈에서 주민번호와 이름을 개인 고객으로부터 입력받는다고 가정하자. 대상은 대한민국 국적을 가진 사람이다. 이름과 주민번호 사이의 관계에서 '이름'은 같은 이름이 존재할 수 있으므로 '이름'을 가지고는 주민번호를 찾아 낼 수(식별 할 수) 없다. 하지만 주민번호로는 이름을 알 수 있다. 여기서 주민번호는 결정자이며, 이름은 주민번호에 함수적으로 종속된다.
==== BCNF란? ====
BCNF는 "모든 결정자[* 위의 예에서 4]가 후보키이면 BCNF이다"가 정의의 끝이다. 후보키는 주키가 될 수 있는 후보가 되는 키이다. 후보키인지 아닌지를 따져보려면 직접 데이터를 보거나 업무 규칙을 파악하여 유일성[* Unique]만 보장되는 컬럼이나 컬럼의 조합이면 된다. 이것이 BNCF의 전부다. 단, 기본 컬럼[* 수정일자, 일련번호와 같은 것을 제외한 순수한 컬럼을 말한다.]이 3개 이상인 경우만 BCNF에 적용된다.
==== BCNF 를 쉽게 도출하는 방법 ====
현실적으로 f(x)가 어떻게 생겨먹었는지 찾아내는 것은 쉬운 일이 아니지만 다음과 같은 순서로 접근하면 쉽게 BCNF를 도출할 수 있다.
1. 적절하게 컬럼들을 범주화 한다. (엔터티의 개념이 있다면 이미 BCNF는 끝이다.)
2. 중복을 없앤다.(반복그룹이 나타나는 것을 찾아 없앤다) 단, 중복 제거 과정 중 데이터의 손실이 발생해서는 안 된다.
직접 예를 들어서 BCNF를 도출해 보도록 하자. 다음과 같은 테이블[* 원래는 릴레이션이라고 해야 하지만 이해를 돕기 위해 테이블이라고 하겠다] 있다.
||학번||학생명||학년||소속학과||개설과목명||학점||강사명||개설학과||
||100 ||김대중||3 ||전산 ||DB ||3 ||이순신||전산 ||
||200 ||노무현||3 ||전기 ||전자기학 ||3 ||강감찬||전기 ||
||200 ||노무현||3 ||전기 ||회로이론 ||2 ||강감찬||전기 ||
||300 ||박정희||2 ||전산 ||알고리즘 ||3 ||조자룡||전산 ||
||400 ||이승만||3 ||전산 ||알고리즘 ||3 ||조자룡||전산 ||
||400 ||이승만||3 ||전산 ||DB ||3 ||이순신||전산 ||
'''(1) 범주화'''
* 학생(학번, 학생명, 학년, 소속학과)
* 개설과목(개설과목명, 학점, 강사명, 개설학과)
'''(2) 후보키 수집'''
* 학생의 후보키: (학번)
* 개설과목의 후보키: (개설과목명, 강사명, 개설학과)?, (개설과목명, 강사명)?, (개설과목명, 개설학과)?, (강사명, 개설학과)?
대부분 개설과목에서 헤깔리기 시작한다. 위에 필자가 써 놓은 것처럼 어떤 것이 후보키인지 명확하게 드러나지 않는다. 이런 경우에는 각각의 경우에 대해서 중복되는 값이 발생하는지를 살펴보면 된다. 개설과목이 학점을 결정하는 것은 확실하므로 결과에만 반영하도록 하겠다.
||개설과목명||강사명||개설학과||
||DB ||이순신||전산 ||
||전자기학 ||강감찬||전기 ||
||회로이론 ||강감찬||전기 ||
||알고리즘 ||조자룡||전산 ||
||알고리즘 ||조자룡||전산 || <-- 이 Row는 빼도 데이터의 손실이 없다.||
||DB ||이순신||전산 || <-- 이 Row는 빼도 데이터의 손실이 없다.||
일단 개설과목을 유일하게 만들어보면 강사명과 개설학과에 반복그룹이 존재하는 것을 확인 할 수 있다. 만약 강사명을 유일하게 만들면 '강감찬' 강사가 가르치는 과목인 '회로이론' 또는 '전자기학'중 하나가 없어지게 된다. 개설과목 자체가 없어지는 것으로 데이터의 손실[* 정규화하면 쪼개는 것을 생각하게 되는데, 무조건 쪼개는 것이 아니라 "무손실 분해"가 기본이다.]이 일어나게된다. 개설학과을 유일하게 만들어도 마찬가지가 된다. 그러나 개설과목을 유일하게 만들어도 강사명과 개설학과에 대한 정보는 사라지지 않는다는 것을 알 수 있다.
||개설과목명||강사명||개설학과||
||DB ||이순신||전산 ||
||전자기학 ||강감찬||전기 ||
||회로이론 ||강감찬||전기 ||
||알고리즘 ||조자룡||전산 ||
이제 다했다. 일단 개설과목명은 유일해졌으니 나머지 강사명과 개설학과에서 유일하게 만들어서 강사명 및 개설학과의 데이터가 손실되지 않게 하면 된다. 처음으로 돌아가서 다음과 같이 하면 된다.
||강사명||개설학과||
||이순신||전산 ||
||강감찬||전기 ||
||강감찬||전기 ||<-- 이 Row는 빼도 데이터의 손실이 없다.||
||조자룡||전산 ||
||조자룡||전산 ||<-- 이 Row는 빼도 데이터의 손실이 없다.||
||이순신||전산 ||<-- 이 Row는 빼도 데이터의 손실이 없다.||
이제 아래와 같은 테이블이 만들어졌다. 강사명을 유일하게 만들면 개설학과에 대한 데이터는 사라지지 않는다. 하지만 개설학과를 유일하게 만들면 강사에 대한 데이터가 사라지게 된다. 바꾸어 말하면 '강감찬' 강사가 없어지면 개설학과에 대한 데이터인 '전기'도 사라짐을 의미한다. 즉, 개설학과에 대한 데이터를 살리고 죽이는 것[* 결정자]이 강사명이 되는 것이다.
||강사명||개설학과||
||이순신||전산 ||
||강감찬||전기 ||
||조자룡||전산 ||
그러므로 위에서 보았던 (개설과목명, 강사명, 개설학과)에서 개설학과는 빠져도 강사명을 통해서 개설학과를 알 수 있게 된다. 결론적으로 아래와 같이 분해되어 BCNF가 된다.
||학번||학생명||학년||소속학과||
||100 ||김대중||3 ||전산 ||
||200 ||노무현||3 ||전기 ||
||300 ||박정희||2 ||전산 ||
||400 ||이승만||3 ||전산 ||
||개설과목명||학점||강사명||
||DB ||3 ||이순신||
||전자기학 ||3 ||강감찬||
||회로이론 ||2 ||강감찬||
||알고리즘 ||3 ||조자룡||
||강사명||개설학과||
||이순신||전산 ||
||강감찬||전기 ||
||조자룡||전산 ||
==== BCNF 결론 ====
정규화란 궁극적으로 데이터베이스의 '데이터 중복의 최소화' 원칙에 따른다. 데이터의 중복을 없앰으로써 불필요한 처리비용을 없애고, 여러 가지 이상 현상들을 제거하여 데이터의 불일치나 무결성을 굳건히 지키는 최고의 튜닝도구다. 앞서서 골치아픈 이론을 조금 설명하였지만 결론적으로 보면 범주화를 잘하고, 중복을 어떤 방법이건 간에 없애면 된다. 몇 차 정규화냐가 중요한 것이 아니라 데이터의 중복과 이상현상을 제거하는 것이 중요한 것이다.
==== 보너스[* 외부키의 개념이 있어야 알아 먹는 글이므로 모르면 그냥 넘어가길 바란다.] ====
사실 예제는 조금 어거지가 있다. 학과만해도 소속학과, 개설학과가 나온다. 이것은 실제로 외부키(관계의 논리적인 표현)나 마찬가지이다. 또한 개설과목도 외부키이고, 어떤 테이블에서는 강사명도 외부키이자 주키의 일부가 된다. 모델링에서 나오는 개체, 관계, 속성의 개념만 명확하다면 정규화는 식은 죽 먹기다. 실제 데이터 모델링을 해 본다면 다음 그림과 같은 모양이 될 것이다. (속성은 대충 필요한 것만 넣었다.)
attachment:bcnf_erd.jpg
==== 정규화와 XML ====
XML이 뜨기 시작한지 꽤 오래되었다. 필자는 XML이란 단어를 1999년에 처음 접했으며, 지금까지 가끔씩 사용해 왔었다. XML은 반구조적이다. 반만 구조적이라는 뜻이다. 예를 들어 다음과 같은 테이블 구조를 보자.
'''테이블구조1'''
||작업코드||사용장비1||사용장비2||사용장비3||사용장비4||
||1||굴삭기||지게차||레미콘||삽||
||.[[BR]].||.[[BR]].||.[[BR]].||.[[BR]].||.[[BR]].||
이런 구조의 장점은 여러 로우를 접근하지 않아도 데이터의 처리가 가능한 것 밖에 없다. 아마도 특정 어플리케이션에 종속적으로 설계되었을 것이다. 이러한 구조는 명확하게 1차 정규화 위배는 아니지만 실무적으로 봤을 때는 정신 건강에 해로운 상태다. 단순히 '사용장비'라는 다중값을 옆으로 펼쳐 놓았을 뿐이다. 그러므로 교과서에 나오는 완전 비정규화 테이블과 같은 구조와 똑같다고 할 수 있다.
'''테이블구조2'''
||작업코드||사용장비1[[BR]]사용장비2[[BR]]사용장비3[[BR]]사용장비4||
그래서 필자의 도메인에서는 적어도 테이블구조1은 1차 정규화 위배이다. 어쨌든 문제는 하나의 속성에 여러 값이 존재하는 다중값 속성이 문제이다. '테이블구조1'과 같이 설계를 했을 경우 특정 상황에서 성능에는 좋을지 모르겠으나 대부분 변화에 취약하다. 또한 데이터베이스는 여러 관점을 모두 수용하여 안장점을 찾아야 하기 때문에 또한 좋지 않다. 만약 사용장비5이 들어오면 어쩔 것인가? 업무가 변화했다고 테이블의 구조가 변하는 것 자체가 문제인 것이다. 왜냐하면 테이블에 종속적인 어플리케션들이 모두 변경될 가능성이 있기 때문이다. 데이터베이스 관점에서 보면 단순히 컬럼 1개를 추가하는 일이지만 시스템 전체를 놓고 봤을 때는 서비스를 중단해야 할 수도 있고 여러 사람 괴롭힐 수 있기 때문이다. 이는 유지보수 비용의 증가다.
그렇다고 무조건 '테이블구조1'이 나쁘다는 것은 아니다. 앞서 말했듯이 상황에 따라서 최적화된 솔루션일 수도 있다. 어쨌든 트래이드 오프가 존재하게 된다. 그래서 이러한 경우 반구조적인 방법[* 중립적인 방법]을 생각해 볼 수 있다. 예상했다시피 XML을 이용하는 것이다. 그러면 변화와 성능에 모두 만족할 수 있을 것이다. 예를 들면 다음과 같은 경우다.
{{{
use tempdb
go
create table test(작업코드 int, 사용장비 xml);
insert test values(1, '<장비>굴삭기장비><장비>지게차장비><장비>레미콘장비><장비>삽장비>');
select
작업코드
, 사용장비.value('/장비[1]', 'varchar(10)') 사용장비1
, 사용장비.value('/장비[2]', 'varchar(10)') 사용장비2
, 사용장비.value('/장비[3]', 'varchar(10)') 사용장비3
, 사용장비.value('/장비[4]', 'varchar(10)') 사용장비4
from test;
/*
작업코드 사용장비1 사용장비2 사용장비3 사용장비4
----------- ---------- ---------- ---------- ----------
1 굴삭기 지게차 레미콘 삽
(1개 행 적용됨)
*/
}}}
좋지 아니한가?
==== XML 예제 ====
{{{
declare @x xml
set @x='
1
2
3
4
5
6
'
SELECT @x.query('
for $step in /ManuInstructions/Location[1]/Step
return $step
')
--/ManuInstructions/Location[1]에서 "[1]"가 없음을 주목
declare @x xml
set @x='
1
2
3
4
5
6
'
SELECT @x.query('
for $step in /ManuInstructions/Location/Step
return $step
')
}}}