저자: 한동훈(
serene@unitel.co.kr)
여기서 하고자하는 이야기는 데이터베이스 프로그래밍과 관련된 것이며, 간단한 예제를 ASP로 보이겠지만 ASP에만 한정된 이야기는 아니다. 그러니 데이터를 사용하는 사람이라면 한번쯤 읽어보는 것도 나쁘지 않을 것 같다. 하지만 경험많은 분들이라면 벌써 무슨 이야기인지 잘 알고 있을 테니 다른 곳으로 넘어가는게 좋을 것 같다.(초보의 글이니… -_-)
엔티티(entity)라는 것은 쉽게 말하면 데이터베이스의 필드를 뜻한다. 그리고 엔티티는 하나의 특성만을 나타내야한다. 하지만 많은 분들이 이러한 것들을 무시하고 있고, 덕분에 다른 분들이 작성한 코드를 들여다보고 작업해야하는 경우에 두통을 일으키는 원인이 되곤한다.
예를 들어보자. 많은 분들이 회원 가입 프로그램을 작성할 때 회원의 관심 분야나 뉴스 레터를 받길 원하는 분야를 선택하도록 한다. 이 경우에 회원들은 여러 개의 항목을 선택할 수 있다.
a. 독서 b. 음악 c. 스포츠 d. 레저 e. 영화 f. 콘서트
등등의 관심분야가 있다면 어떤 사람은 한 가지 항목을, 어떤 사람은 두 가지 항목을 선택할 수 있을 것이다.
보통 이런 경우에 많은 프로그래머들은 Interest와 같은 필드를 만들어서 a를 선택하면 a만 넣고, a, c, d를 선택하면 "a,c,d"와 같이 넣도록 하는 형태를 가장 많이 사용하고 있다.
요즘에는 데이터베이스 설계와 ERD에 대한 관심이 높아서 누구나 이러한 것들을 갖고 있다. 그러나 대부분의 사람들은 그러한 ERD를 그리게 된 업무 규칙에 대한 문서를 작성하지 않는다. 결국에 ERD가 있다해도 데이터베이스에 실제로 어떤식으로 데이터가 들어가있는지 하나씩 확인해야하는 불편함이 있다. - 만약에 회원 관심 분야를 하나만 선택하도록 했다면 Interest라는 필드를 만들어서 값을 저장하는 것은 틀린 것이 아니다.
그러나 위와 같은 경우에 회원의 선택에 따라서 하나의 필드가 여러 개의 성격을 갖게 됩니다. 하나의 필드는 한 가지만 나타내야하는데 경우에 따라서 독서도 되고, 음악도 되고, 심한 경우에는 음악, 스포츠, 영화도 될 수 있다.
위 6가지에 대해서 모든 사용가능한 조합, 다시 말해 나타날 수 있는 성격이 6 * 5 * 4 * 3 * 2 * 1 = 720가지가 되어 버린다.
아마도 위와 같이 들어가는 데이터를 본 경험이 있거나 이렇게 프로그래밍한 경험이 있는 분들이 많을 것이다.
이러한 테이블의 문제점은 하나의 엔티티가 하나의 속성이 아닌 여러 개의 속성을 나타낸다는 것도 있지만, DBMS에서 회원들의 관심 분야를 알아내기위해 테이블을 풀스캔(full scan)해야하는 문제점이 있다. 또한 웹 상에는 6개의 체크 박스를 보여주고, 코드에서는 이것을 하나로 합쳐서 데이터베이스에 넣어야하는 불편함이 있다.
또한 a. 독서를 a. 게임과 같이 변경하고 g. 독서를 추가하는 경우에 데이터베이스의 데이터를 일관성있게 업데이트할 수 없다.(사실은 하면 할 수 있는데 두통이 생긴다. -_-)
또한 이러한 값에 대해서 항목들이 많아져서 a부터 z까지 모두 사용하고, aa를 추가로 사용하는 경우에 a만 선택한 사용자를 제대로 선택할 수 없다. 또한 여러 개의 항목을 선택한 사용자를 제외하고 순수하게 a만 선택한 사용자를 추출하는 경우에도 많은 어려움이 생긴다.
이와 같은 경우에 사용자 관심항목을 별도의 테이블로 독립시켜야한다. 즉, 사용자 - 교차엔티티 - 관심항목과 같이 3개의 테이블로 분리시켜야한다.
보통 이와 같은 형태로 다시 나타낼 수 있을 것이다. 이와 같이 분리하면 테이블을 JOIN해야하지만, 질의가 크게 복잡해지지도 않고, 위에 말한 모든 문제들을 간편하게 해결할 수 있다.(실은 예를 들기 위해 만든 것이고, UserName이 유일하다는 전제조건을 갖고 있다. ^^;)
위와 같은 경우에 1. 사용자 정보 + 관심항목, 2. 관심항목에 따른 사용자들을 보여주는 뷰를 별도로 작성해서 활용할 수도 있다.
이와 같이 하나의 엔티티가 하나의 성격만 갖도록 하면 프로그래머를 귀찮게 하는 문제들을 줄일 수 있고, 실제 데이터가 들어갔을 때 예상치 못한 버그가 생기는 것을 막을 수 있다.
트리 구조
트리 구조는 다중 엔티티의 조금 특별한 형태로 이해할 수 있다. 정확히 말하면 트리 구조는 그래프 구조의 특별한 형태지만, 그래프 구조 보다는 트리 구조를 일상에서 더 많이 사용하고 있다. 윈도우의 탐색기, SQL 서버의 Enterprise Manager와 같은 관리 콘솔, 웹 사이트에서의 메뉴 트리, 응용 프로그램에서의 메뉴 트리를 많이 사용하고 있다. 심지어 XML로 표현되는 데이터로 트리의 형태로 파악할 수 있고, 그렇게 처리하는 것이 의외로 간단하다.
여기서는 ASP와 DHTML을 사용해서 메뉴를 표시하기 위해 트리 구조를 사용할 것이고, 이 트리 구조의 데이터는 데이터베이스에서 가져올 것이다. 이 프로그램은 아주 간단하게 두 개의 테이블을 갖고 있으며, 이것만으로 충분하기 때문에 별도의 과정을 거치지 않는다. 일단 전체 예제와 결과 화면을 보고, 왜 여기서 이것을 소개하는지 살펴보자.
Topic 테이블
SubTopic 테이블
먼저 트리 구조를 처리하기 위한 모듈 modTreeView.asp를 작성한다. modTreeView.asp는 세 개의 프로시저를 갖고 있다. 하나는 InitTreeView()이며, 이것은 단순히 트리 구조에 사용할 자바 스크립트를 출력한다. TopicsInTree()는 루트 트리만 출력한다. SubTopicsInTree()hgs TopicsInTree()와 비슷한 로직으로 구성되며, 루트 트리 밑에 있는 자식 트리를 생성한다.
이제 소스를 살펴보도록 하자.
Sub InitTreeView()
%>
<%
End Sub
이 프로시저는 단순히 함수만을 출력하며, 메뉴를 동작시키는 부분이므로 페이지에서 한 번만 실행시켜야한다.
Sub TopicsInTree()
Dim dc " 데이터베이스 연결 부분
Dim sConn
sConn = "Provider=SQLOLEDB.1; uid=sa; pwd=passwd; Initial Catalog=database;Persist
Security Info=false;Data Source=localhost;"
Set dc = Server.CreateObject("ADODB.Connection")
dc.Open sConn
" 루트에 뿌려지는 트리를 위한 root 트리를 가져온다.
Dim rsMainTree
Dim MainTreeSQL
MainTreeSQL = "SELECT topicID, name, description, link, target from topic WHERE
topicID NOT IN( SELECT subtopicID FROM subtopic) ORDER BY menuOrder ASC"
Set rsMainTree = Server.CreateObject("ADODB.Recordset")
rsMainTree.CursorLocation = 3 "adUseClient
rsMainTree.CursorType = 3 "adOpenStatic
"데이터베이스에서 루트 트리 구조를 가져온다
rsMainTree.Open MainTreeSQL, dc
Response.Write("
")
If Not rsMainTree.EOF then
Do While Not rsMainTree.EOF
Response.Write("")
onderwerpID = rsMainTree("topicID")
onderwerp = rsMainTree("name")
link = rsMainTree("link")
target = rsMainTree("target")
description = rsMainTree("description")
" 모든 트리 구조는 DB에서 재귀적인 구조를 갖고 있으므로,
" 코드에서도 마찬가지로 재귀호출로 해결할 수 있다.
call SubTopicsInTree(dc, onderwerpID, onderwerp, 1, link, target, description)
rsMainTree.MoveNext
Response.Write(" |
")
Loop
End If
Response.Write("
")
If rsMainTree.State = 1 Then
rsMainTree.Close
End If
Set rsMainTree = Nothing
If dc.State = 1 Then
dc.Close
End If
Set dc = Nothing
End Sub
이 함수는 루트 트리만을 출력하는 역할을 한다. 질의를 눈여겨 보기 바란다.
SELECT topicID, name, description, link, target from topic
WHERE topicID NOT IN( SELECT subtopicID FROM subtopic) ORDER BY menuOrder ASC
NOT IN과 같이 되어 있고, subtopic 테이블을 사용한다. 다시 말해서 subtopic 테이블이 여기서는 교차 테이블의 역할을 한다. 이러한 특수한 형태의 테이블이 갖는 의미는 마지막에 설명하도록 하자. 다음으로 SubTopicsInTree()를 살펴보자.
Sub SubTopicsInTree(dc, topicID, topic, depth, link, target, description)
" Topic 테이블의 TopicID에 대해 모든 SubTopic 테이블에 직접 질의한다.
Set rsSubtopics = dc.Execute( _
"SELECT topicID, name, description, link, target FROM topic WHERE
topicID IN (SELECT subtopicID " + _
"FROM subtopic WHERE topicID = " + CStr(topicID) + " ) ORDER BY
menuOrder ASC")
Response.Write("
" & Chr(13))
If (depth > 1) Then
" 루트와 가지 사이에 10정도의 여백을 준다.
Response.Write(" | " & Chr(13))
End If
" 또 다른 가지가 있으면 재귀적으로 해결한다.
If Not rsSubtopics.EOF then
Response.Write(" " + topic + "")
Do While Not rsSubtopics.eof
subtopicID = rsSubtopics("topicID")
subtopic = rsSubtopics("name")
link = rsSubtopics("link")
target = rsSubtopics("target")
description = rsSubtopics("description")
call SubTopicsInTree(dc, subtopicID, subtopic, depth + 1, link,
target, description)
rsSubtopics.movenext
Loop
" We have a leaf. Simply print the leaf
Else
Cell = " "
Cell = Cell & ""
Cell = Cell & " "
Cell = Cell & topic & " "
Response.Write Cell
End if
Response.Write(" | |
" & Chr(13))
If rsSubTopics.State = 1 Then
rsSubTopics.Close
End If
Set rsSubTopics = Nothing
End Sub
이 함수의 역할은 루트의 밑에 있는 모든 가지와 잎(최종 노드)을 출력한다. 여기서 눈여겨 볼 것은 마찬가지로 질의다.
SELECT topicID, name, description, link, target
FROM topic
WHERE topicID
IN (SELECT subtopicID FROM subtopic WHERE topicID = @topicID )
ORDER BY menuOrder ASC
코드에서와 달리 잘 알아볼 수 있도록 약간 편집을 했고, 변수에 따라 바뀌는 부분은 @topicID로 표시했다.
볼 수 있는 것처럼 특정 topicID에 속한 값을 subtopic에서 가져오고, 이 값을 WHERE 절에 사용하여 루트가 아닌 가지만을 찾아낸다. 그리고 이 가지밑에 속한 또 다른 가지가 있는 경우에는 재귀호출을 사용해서 해결한다.
이와 같이 재귀호출을 사용하는 것은 많은 비용이 들지만 인트라넷과 같은 곳에서 유용하게 사용할 수 있고, 생각보다 빠르게 동작한다.(일반적으로 재귀호출은 일반 루프보다 빠르게 수행되지만 메모리를 많이 사용한다는 단점이 있다)
이제 트리 구조에 사용할 함수를 모두 만들었으니 간단히 SQL 스크립트로 Topic과 SubTopic 테이블을 생성하고, 몇 가지 데이터를 넣어보자.
CREATE TABLE [dbo].[TOPIC] (
[topicID] [int] NOT NULL ,
[name] [varchar] (50) COLLATE Korean_Wansung_CI_AS NOT NULL ,
[description] [text] COLLATE Korean_Wansung_CI_AS NULL ,
[link] [varchar] (255) COLLATE Korean_Wansung_CI_AS NULL ,
[target] [varchar] (50) COLLATE Korean_Wansung_CI_AS NULL ,
[menuOrder] [int] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[SUBTOPIC] (
[topicID] [int] NOT NULL ,
[subTopicID] [int] NOT NULL
) ON [PRIMARY]
GO
Topic 테이블에 들어가 있는 데이터에 대해서 설명하자. 먼저 TopicID를 입력한다. 자동생성 값으로 사용해도 된다. description은
태그의 title 속성에 사용한다. 이 속성은 링크에 마우스를 가져다대면 도구설명 상자에 나타낸다. link는 클릭할 때 이동할 링크를 지정하며 상대 경로, 절대 경로 모두 사용할 수 있다. target은 메뉴를 프레임에서 사용할 때 지정한다. 프레임을 사용하지 않기 때문에 여기서는 _self로 사용하였다. menuOrder는 메뉴가 표시되는 순서를 가리킨다. 이것은 트리 구조에 대해서 별개의 숫자로 매겨지기 때문에 직접 계산하는 것은 번거롭다. 나중에 이러한 메뉴 구조를 불러놓고, 트리 구조 자체를 수정할 수 있는 프로그램을 작성하여 자동으로 menuOrder를 갱신하도록 하면 될 것이다.
SubTopic 테이블에 들어간 데이터는 트리에서 루트와 가지 사이의 관계를 나타낸 것이다. Fruits는 Apple과 Banana를 자식 노드로 갖고 있으므로 Fruits의 topicID 1번이 Apple과 Banana의 TopicID 2, 3번의 부모임을 나타내는 것이다. 나머지는 모두 그러한 구조로 되어 있는 것이다.
트리구조를 살펴볼 TreeView.asp의 소스는 다음과 같다.
<%
call InitTreeView()
call TopicsInTree()
%>
결과 화면은 다음과 같다.
결과 화면이 그림처럼 이쁘게 나오지는 않을 것이다. 스타일 시트를 적용하여 그림과 같이 보다 보기 좋게 꾸미는 것은 여러분에게 맡긴다. 여기에 소개한 코드는 D.E.Keff의 코드에 기초한 것을 보다 확장한 것이다.
트리 구조 분석
실제로 이 트리 구조 테이블은 하나의 테이블에 모두 담아 놓을 수 있다고 생각할 수 있다. 그리고 실제로 그렇게 프로그래밍하는 사람도 있을 것이다.
루트 트리와 자식 트리간에는 link 정보외에는 동일한 구조를 사용한다.
실제로 이 트리 구조는 처음에 살펴본 것과 같이 Topic - R_TopicSubTopic - SubTopic의 관계가 된다. 그러나 트리 구조의 특수성과 웹 상의 메뉴를 나타내는 데 있어서 모든 메뉴를 나타낸다는 결과가 있으므로 Topic과 SubTopic을 하나의 테이블로 합친 것이다.
따라서 Topic - R_TopicSubTopic - Topic의 관계가 만들어진다. 또한 루트는 부모를 갖지 않는 최상위이므로 R_TopicSubTopic에 없는 레코드는 트리에서 루트라는 것을 알 수 있다.
이상으로 간단하게나마 다중 엔티티 프로그래밍에 대해서 살펴보았다. 실제로 하나의 엔티티에는 하나의 속성만 가져야한다.라는 주제는 데이터베이스 모델링의 제1정규화에 해당한다. 그리고 엔티티타입에 종속적이지 않은 속성은 따로 분리한다는 제2정규화에 해당한다. 실제로 제1정규화를 무시하고 다중 엔티티를 프로그래밍하게 되면 여러가지 예상치 못한 문제가 발생하게되고, 꽤나 번거로운 일들이 많이 생기게 된다. 따라서 JOIN을 두려워하지 말고 자주 쓰도록 한다. 자꾸 써서 익숙해지면 된다.
실제로 다중 엔티티 프로그래밍은 제1정규화에 대한 이야기였지만, 제1정규화라 하면 딱딱하고, 정규화에 대해서 학습한 사람들도 프로그래밍과 연결되지 않기 때문에 가장 흔하게 무시되고 있는 부분이라 생각한다. 조금은 감이 잡혔는가? 그렇다면 다행이라고 생각한다.
정규화는 제1정규화부터 시작한다. 제1정규화가 되지 않고 제2정규화나 제3정규화를 한다는 것은 있을 수 없는 일이다. 또한 제5정규화까지 있지만, 이들은 데이터베이스에 나타나는 여러가지 문제를 해결하기 위한 방법들이고, 데이터베이스가 제대로 설계되었는지 검증하는 방법이다. 또한 하나의 문제에 대해서 사람들마다 전혀 다른 데이터베이스를 설계할 수 있고, 모두 정규화를 만족시킬 수 있다. 데이터베이스 설계에는 정답이 없다.
그리고 제1정규화를 통해 문제를 해결하니 또 다른 문제가 나타나서, 그걸 해결하기 위해 제2정규화가 나타났으며, 제2정규화를 해결하니 또 다른 문제가 나타나서 제3정규화가 나타난 것이다. 이렇게 제5정규화까지 갔으나 해결하지 못한 설계에 대한 문제를 도메인 모델로서 한 큐에 해결하는 궁극의 솔루션이 나와 있다. 이러한 정규화와 데이터베이스 설계에 대해 궁금한 분들은 Reference를 참고하기 바란다.
요약
다중 엔티티를 풀어낼 수 있는 방법은 다양하고, 생각처럼 간단하지 않은 경우도 종종있으며, 정규화 이전에 1-2개의 테이블로 되던 것을 정규화이후에 7개 이상의 테이블을 JOIN하게되는 경우도 있다. 이 모든 것들이 불편하게 느껴질 수 있으나 실제로 이것은 데이터의 일관성을 유지할 수 있게 해주며, 알 수 없는 버그를 줄일 수 있는 가장 확실한 방법이다.
위에 설명한 것처럼 다중 엔티티는 제1정규화(또는 제1정규화 다음에 제2정규화)를 사용하여 해결할 수 있으며, SQL 코드에서는 JOIN등을 사용하여 해결할 수 있으며, 프로그래밍 코드에서는 컬렉션에 대한 반복 수행이나 재귀 호출등을 사용하여 해결할 수 있다.
마치며
여담이지만 실무에서는 별별 해괴한(?) 일들이 다 일어난다. JOIN 절대 안 쓰기, 한 줄에 SQL 질의 코딩하기, 하나의 테이블에 몽땅 집어넣기, 테이블 alias는 a, b, c 등으로 명명하기, 모든 질의를 소문자로 작성하기 등… 실로 별 해괴한 일들을 다 보게되고, 회사에서는 "짬"이 안된다는 이유로 따라가야하고, 의견을 내놓으면 "그럴 시간이 어딨어, 돌아가는 거나 만들어놔!"라는 얘기를 들을 때도 있다. "아, 현실과 이상은 이렇게 다르단 말인가!~"하며 좌절하지는 말자.
그래도 꾸준히 해 나가길 바라고, 스스로 할 수 밖에 없다. 누가 말했던가. 내일은 해가 뜬다고… 스스로 들인 시간과 노력은 언젠가 빛을 발하게 된다. ^^;
독자 Q&A
아주 빠르게 질문해 주신 분에게 감사드린다.
Q. R_UserUserInterest에서 UserName + UserInterestCode를 PK로 안 잡았네요? 보통 두 개 묶어서 PK하지 않나요?
A. 잡혀있습니다. 다만 그림에만 표시되어 있지 않습니다.(왜 안되어있는거지? -_-) 사람마다, 책마다 다르게 말하지만 저는 UserName + UserInterestCode와 같이 함께 묶어서 사용하는 것을 Compound Key라 하고, 하나의 필드만 키로 설정했을 때 Primary Key(기본키)라고 합니다. Compound Key 또는 Composite Key라고 하며 한글로는 조합키라고 많이 옮깁니다.
Q. Netscape에서도 저 메뉴를 쓸 수 있나요?
A. DHTML과 자바 스크립트를 지원하는 상위 수준의 브라우저(Up-Level Browser)에서는 모두 사용할 수 있고 제대로 동작합니다. Netscape, K 브라우저, 컨쿼러, IE 4.0 이상에서 모두 잘 동작하며, 맥 버전의 IE에서도 제대로 동작하는 것으로 알고 있습니다.
Q. 메뉴 구성 외에는 위와 같은 방법을 어디에 사용하나요?
A. 사내 인트라넷 구축시에 조직도 작성 프로그램을 만들 때 비슷한 알고리즘을 썼으며, 관리자가 편하게 윈도우용 클라이언트에서 메뉴를 수정할 수 있도록 해야했기 때문에 UI와 관련된 코드량이 상당히 많았습니다. 위의 예제는 단순히 뷰어랄까요. (그 당시에 서버는 Solaris를 썼습니다) 그외에 본문에 있는 것처럼 XML이나 임시로 수집한 시스템 정보를 트리 형태로 가공할 때도 많이 사용했습니다. 이때는 DBMS를 쓰지 않고, 단순히 시스템의 상태를 모아서 재가공하는 형태였으며 재귀호출의 경우에는 메모리를 많이 사용하기 때문에 데이터 크기를 적절히 제한할 필요가 있었습니다. 이런 경우에는 재귀대신에 루프 구조를 사용하세요. ^^;
레퍼런스
- 데이터베이스 설계와 구축
- 정규화와 데이터베이스 설계에 대해 설명하고 있으며, 도메인에 대해서도 자세히 설명하고 있다.
- C로 구현한 알고리즘, 3장
- C 언어를 사용하여 다양한 알고리즘을 설명한 책으로 알고리즘에 대한 좋은 책들 중에 하나다. 3장은 재귀호출에 대해서 다루고 있다. 위 예제에서 사용한 재귀호출에 대한 내용과 재귀호출을 루프로 변환하는 방법에 대해서 궁금하다면 참고하기 바란다.