Hey, Scripting Guy!이 관계를 저장할 수 있을까요?

Microsoft Scripting Guys

이 기사의 코드 다운로드: HeyScriptingGuy2007_09.exe (150KB)

관계는 매우 중요합니다. 그러면 Scripting Guys는 이러한 관계의 중요성을 어떻게 알고 있을까요? 저희는 TV를 많이 보는데 TV에서는 항상 관계가 중요하다고 이야기합니다.

드라마나 주말의 TV 영화에만 국한되는 것은 아닙니다. 예를 들어 사람들에게 대출을 권유하는 대부업체 광고가 있습니다. 이러한 업체는 대출해서는 안 되는 사람들에게도 기꺼이 대출을 해주기 위해 광고를 하는 것일까요? 아닙니다. 그러면 대출 고객에게 연간 900%에 이르는 이자를 부과하려는 것이기 때문일까요? 물론 아닙니다. 광고를 통해 대부업체가 고객과 개인적 관계를 형성하기 때문입니다. 광고에서 말하듯이 "관계가 중요"한 것이지요.

저희는 대부업체와의 아픈 경험을 갖고 있지만 부끄럽지는 않습니다. 아무튼, 여러분은 사람과 대부업체를 결속하는 사소하지만 생각해 보면 깊고 질긴 관계가 파탄에 이르지 않도록 냉철하게 생각해야 합니다.

이렇게 말하면 "대부업체에게 관계가 중요하다면 데이터베이스와 상호 작용하는 스크립트를 작성하는 사람에게도 관계가 매우 중요하겠다"고 생각하는 분도 계실겁니다. 데이터베이스와 상호 작용하는 스크립트를 작성하는 사람에게 관계가 매우 중요하다는 점은 맞습니다. 하지만 이러한 스크립트 작성자가 항상 이러한 관계의 중요성을 알고 있는 것은 아닙니다. 어쨌든 괜찮습니다. 이번 달 칼럼에서 바로잡을 수 있으니까요.

하드웨어 인벤토리를 관리하는 용도로 데이터베이스를 사용하는 시스템 관리자가 많이 있습니다. 이 경우 문제는 이러한 데이터베이스의 상당수가 적절하지 않은 방식으로 설정되어 있다는 점입니다. 예를 들어 모든 컴퓨터에 장착된 디스크 드라이브를 관리하는 데이터베이스가 필요하다고 가정해 보겠습니다. 데이터베이스 디자인에 대한 기본 지식이 없다면 그림 1과 같은 단일 테이블로 구성된 데이터베이스를 만들 가능성이 큽니다.

그림 1 단일 테이블 데이터베이스

그림 1** 단일 테이블 데이터베이스 **

그림에서 볼 수 있듯이 이 데이터베이스는 매우 단순합니다. 컴퓨터 이름 필드와 컴퓨터에 C: 드라이브 및 D: 드라이브가 있는지 여부를 나타내는 데 사용되는 Yes/No(예/아니요) 필드가 몇 개 있습니다. 이것이 전부입니다. 단순 명료합니다.

그렇다면 이 데이터베이스에는 어떤 문제가 있을까요? 사실, 거의 모든 것이 잘못되었습니다. 디스크 드라이브가 세 개 이상 있는 컴퓨터가 없다는 것이 확실하다면 이 디자인도 어느 정도는 제대로 작동합니다. 하지만 디스크 드라이브가 세 개인 컴퓨터가 있다면 어떻게 될까요? 이 경우에는 드라이브 E를 관리하는 필드를 더 추가해야 합니다. 그렇다면 디스크 드라이브가 11개인 컴퓨터가 있으면 어떻게 될까요? 또한 드라이브 각각의 속성을 관리해야 한다면 어떻게 될까요? 예를 들어 각 드라이브의 전체 크기를 관리하려고 한다고 가정해 보겠습니다. 이 경우 Drive_C_Size 필드, Drive_D_Size 필드, Drive_E_Size 필드가 필요합니다. 이것만 봐도 무엇이 문제인지 느낄 것입니다. 사용 가능한 디스크 공간, 디스크 커넥터 유형, 드라이브의 디스크 할당량 사용 여부 등을 관리하기는 거의 불가능해집니다.

염두에 두어야 할 것은 둘 이상의 항목이 있을 수 있는 엔터티(예를 들어 컴퓨터에는 둘 이상의 디스크 드라이브가 있을 수 있음)의 경우에는 "플랫 파일"(단일 테이블) 디자인이 그다지 적합하지 않다는 점입니다. 디스크 드라이브의 경우에는 그냥 두고, 한정된 수의 드라이브를 컴퓨터에 연결하려는 경우도 생각해 볼 수 있습니다. 하지만 데이터베이스를 쿼리하여 100GB 이상인 모든 디스크 드라이브의 목록을 가져와야 한다고 가정해 보겠습니다. 어떻게 해야 할까요? 먼저 Drive_C_Size 필드에서 100GB 드라이브를 찾은 다음 Drive_D_Size 필드, Drive_E_Size 필드 등에서도 같은 작업을 반복해야 합니다. 당연히 효율적이거나 효과적인 방법과는 거리가 멉니다. 그러면 대안은 무엇일까요? 플랫 파일 데이터베이스를 사용할 수 없다면 무엇을 사용할 수 있을까요?

이에 대한 대안이 바로 관계형 데이터베이스입니다. 관계형 데이터베이스는 한 컴퓨터에 여러 개의 디스크 드라이브가 있는 일대다 관계를 처리하기 위해 디자인되었습니다. 그렇다고 관계형 데이터베이스를 사용하기 위해 어떤 것을 새로 구입해야 한다는 것은 아닙니다. Microsoft® Access™, SQL Server™, Oracle 등의 데이터베이스가 있다면 이미 관계형 데이터베이스를 사용할 준비가 된 것입니다. 따로 구입할 것은 없으며 1) 데이터베이스의 두 테이블 사이의 관계를 설정하는 방법과 2) 이러한 관계를 활용할 수 있는 스크립트를 작성하는 방법만 알면 됩니다.

TV의 경우라면 관계를 이용하려고 하지도 말아야 하겠지만 이 경우는 다릅니다.

이 칼럼이 데이터베이스 디자인 전문가를 위한 것은 아니므로 데이터베이스 디자인에 지면을 할애하지는 않겠습니다. 대신 관계형 쿼리 작성에 도움이 되는 매우 간단한 관계형 데이터베이스 디자인을 소개하겠습니다. 이 데이터베이스에는 두 개의 테이블이 있습니다. 첫 번째 테이블의 이름은 Computers이고 ComputerName 및 SerialNumber 필드로 구성됩니다. ComputerName 필드에는 컴퓨터 이름을 저장하고 SerialNumber 필드에는 이름에서 알 수 있듯이 일련 번호를 저장합니다.

그러면 디스크 드라이브 정보는 어디에 넣을까요? 두 번째 테이블인 DiskDrives에 넣습니다. 이 테이블은 이름 자체로도 용도를 알 수 있는 SerialNumber, DriveLetter 및 DriveSize의 세 필드로 구성됩니다.

여기서 키 필드는 SerialNumber입니다. 이 필드가 낯익은가요? 그렇습니다. Computers 테이블에도 이 필드와 정확하게 동일한 필드가 있습니다. 이는 실수나 우연의 일치가 아니라 의도적인 것입니다. 이 두 테이블 간의 관계를 설정하려면 두 테이블 모두에 나타나는 하나의 필드가 있어야 합니다. 그래야 각 디스크 드라이브가 어떤 컴퓨터에 속하는지 알 수 있습니다.

컴퓨터 이름 등이 아니라 일련 번호를 사용하여 두 테이블을 연결한 이유는 무엇일까요? 그럴 만한 이유가 있습니다. 컴퓨터 이름은 자주 바뀔 수 있으며 실제로도 그렇기 때문입니다. 하지만 일련 번호는 일반적으로 변경되지 않습니다.

이제 이 두 테이블을 검색하여 각 컴퓨터에 속한 디스크 드라이브의 목록을 가져올 수 있는 스크립트인 그림 2를 살펴보겠습니다. 그림 3에서는 이 스크립트가 반환하는 데이터의 종류를 보여 줍니다.

Figure 3 검색 결과

atl-ws-01
C:

atl-ws-01
D:

atl-ws-01
E:

atl-ws-02
C:

atl-ws-02
D:

Figure 2 테이블 검색

Const adOpenStatic = 3
Const adLockOptimistic = 3

Set objConnection = CreateObject("ADODB.Connection")
Set objRecordSet = CreateObject("ADODB.Recordset")

objConnection.Open _
 "Provider = Microsoft.Jet.OLEDB.4.0; " & _
 "Data Source = c:\scripts\test.mdb" 

objRecordSet.Open "SELECT Computers.*, DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber " & _
 "ORDER BY Computers.ComputerName, DiskDrives.Drive", objConnection, adOpenStatic, _
  adLockOptimistic

objRecordSet.MoveFirst

Do Until objRecordset.EOF
 Wscript.Echo objRecordset.Fields.Item("ComputerName")
 Wscript.Echo objRecordset.Fields.Item("Drive")
 Wscript.Echo
 objRecordset.MoveNext
Loop

objRecordSet.Close
objConnection.Close

이번 달 칼럼의 목적은 데이터베이스 연결 및 사용에 대한 기본 사항을 설명하려는 것이 아닙니다. 데이터베이스 스크립팅이 처음인 경우에는 Scripting Guys 웹캐스트 "시스템 관리자를 위한 데이터베이스 스크립팅"(go.microsoft.com/fwlink/?LinkId=22089)을 참조하십시오. 여기에서는 Access 데이터베이스(C:\Scripts\Test.mdb)에 액세스하여 Computers 및 DiskDrives라는 이름의 관계형 테이블로 작업할 것입니다. 스크립트에서 이 부분을 충분히 살펴볼 것입니다.

또한 Access 2007을 사용하여 작업할 때는 약간의 변경을 해야 한다는 점을 알아야 합니다. 다음과 같이 공급자를 Microsoft.Jet.OLEDB.4.0에서 Microsoft.ACE.OLEDB.12.0으로 변경하는 것이 중요합니다.

objConnection.Open _
 "Provider = Microsoft.ACE.OLEDB.12.0; " & _
 "Data Source = c:\scripts\test.accdb" 

이게 다입니다. 여기서 좀 더 자세히 살펴볼 것은 다음과 같이 두 테이블에서 데이터를 검색하는 SQL 쿼리입니다.

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic 

복잡해 보입니까? 어느 정도는 그럴 수 있습니다. 그러면 더 쉽게 이해할 수 있도록 이 쿼리를 나눌 수 있는지 보겠습니다.

쿼리의 첫 번째 부분은 매우 쉽습니다. 두 테이블, 즉 Computers와 DiskDrives의 모든 필드를 선택해야 합니다. 따라서 이 부분의 SQL은 다음과 같습니다.

SELECT Computers.*, DiskDrives.* 

여기서 별표(*)는 당연히 "모든 것"을 의미하는 와일드카드 문자일 뿐입니다.

SQL 쿼리를 작성해 본 적이 있거나 SQL 쿼리 언어의 하위 집합을 사용하는 WMI(Windows® Management Instrumentation) 쿼리를 작성해 본 적이 있으면 루틴을 이해할 것입니다. 선택할 항목을 지정한 후에는 이들 항목을 선택할 출처를 지정합니다. 따라서 쿼리의 다음 부분은 다음과 같습니다.

FROM Computers INNER JOIN DiskDrives ON Computers.SerialNumber = DiskDrives.SerialNumber

사실 일반적인 FROM 문보다 조금 복잡하지만 그럴 만한 이유가 있습니다. 일반적인 SQL 쿼리에서는 하나의 테이블에서만 데이터를 선택하지만, 이 경우에는 한 번에 두 테이블에서 데이터를 선택합니다.

구문을 좀 더 자세히 살펴보고 작동 방식을 알아보겠습니다. 여기서 볼 수 있듯이 Computers 테이블과 DiskDrives 테이블에서 데이터를 선택하도록 스크립트를 작성합니다. 하지만 "and"라는 단어가 아니라 INNER JOIN이라는 구문을 사용한다는 점을 주목하십시오. 이 구문은 관계의 유형을 정의하며 두 테이블의 정보를 하나의 레코드 집합으로 조인하는 방법을 설명합니다. 이외에도 다른 유형의 조인도 있는데 잠시 후 이에 대해 살펴보겠습니다.

두 테이블 사이에 존재하는 관계의 유형을 지정하는 것은 사용자의 몫입니다. 테이블이 서로 관련되는 정확한 방식을 지정하는 것도 사용자의 몫입니다. 다음 코드에서 이 작업을 합니다.

ON Computers.SerialNumber = DiskDrives.SerialNumber

이 코드에서는 테이블을 조인하는 방법을 정의합니다. 레코드는 Computers 테이블의 SerialNumber 필드가 DiskDrives 테이블의 SerialNumber 필드와 일치할 때마다 함께 그룹화됩니다. ComputerName과 같은 다른 필드를 필드 조인에 사용하면 어떻게 될까요? 이 경우 코드는 다음과 같습니다.

ON Computers.ComputerName = DiskDrives.ComputerName

자세히 본다면, 여기서 두 필드는 이름이 같을 필요는 없으며 같은 데이터만 포함되어 있으면 됩니다. 하지만 같은 이름을 사용하면 관계 필드를 더 쉽게 구분할 수 있습니다. 한 가지만 염두에 두면 됩니다. 이름이 같은 두 개의 필드가 있기 때문에 이들 필드 중 하나를 참조할 때 항상 TableName.FieldName 구문을 사용해야 합니다. 즉, Wscript.Echo obj- Recordset.Fields.Item("Computers.SerialNumber")와 같은 코드를 사용해야 합니다.

이것만 주의하면 됩니다. 쿼리의 나머지 부분은 데이터를 컴퓨터 이름별로 정렬하고 다시 드라이브별로 정렬할 뿐입니다.

ORDER BY ComputerName, Drive

그다지 어렵지 않죠? 여기서 잠시 조인에 대해 이야기하겠습니다. 쿼리에 왜 내부 조인을 사용했을까요? 간단합니다. 내부 조인은 각 테이블에 일치하는 값이 있는 레코드만 반환합니다. 121989라는 일련 번호가 있다고 가정하겠습니다. 이 일련 번호가 Computers와 DiskDrives 테이블 모두에 있는 경우에는 해당 레코드가 반환됩니다. 물론 일련 번호는 SerialNumber 필드 외에는 다른 어디에도 표시되어서는 안 됩니다. 이해가 되셨나요?

이제 일련 번호가 121989라는 컴퓨터가 있는데 이 일련 번호를 가진 디스크 드라이브는 없다고 가정해 보겠습니다. 이 경우에는 일련 번호가 121989인 컴퓨터에 대해 데이터가 반환되지 않습니다. 내부 조인은 조인된 각 테이블에서 값이 서로 일치하는 레코드가 있는 경우에만 정보를 반환하기 때문입니다.

즉, 내부 조인 쿼리는 하드 디스크가 있는 모든 컴퓨터를 반환합니다. 하드 디스크가 없는 컴퓨터 또는 컴퓨터에 설치되지 않은 하드 디스크는 반환하지 않습니다. 이러한 결과가 필요할 때도 있습니다. 하지만 하드 디스크가 없는 컴퓨터 또는 컴퓨터에 설치되지 않은 하드 디스크의 목록이 필요한 경우에는 어떻게 해야 할까요?

이런 경우에는 외부 조인을 사용할 수 있습니다. 일단, 외부 조인에는 왼쪽 우선 및 오른쪽 우선의 두 가지 유형만 있다고 간주하겠습니다. 예제 데이터베이스에는 Computers(마스터 테이블이므로 "왼쪽" 테이블로 간주됨)와 DiskDrives(마스터 테이블이 아니기 때문에 "오른쪽" 테이블로 간주됨)의 두 테이블이 있습니다. 반환되는 레코드 집합에 디스크 드라이브가 설치되지 않은 컴퓨터를 포함한 모든 컴퓨터가 포함되도록 하려는 경우를 예로 들겠습니다. 이 경우에는 LEFT OUTER JOIN 구문을 사용하여 다음과 같은 쿼리를 만듭니다.

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "LEFT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

왼쪽 우선 외부 조인은 오른쪽 테이블에 해당 레코드가 없더라도 왼쪽 테이블에 있는 모든 레코드를 반환합니다. 따라서 모든 컴퓨터가 반환되기는 하지만 오른쪽 테이블(디스크 드라이브)에서는 컴퓨터와 연관된 레코드만 반환됩니다.

또는 컴퓨터에 설치되지 않은 디스크 드라이브를 포함하여 모든 디스크 드라이브의 목록이 필요할 수 있습니다. DiskDrives 테이블은 관계에서 오른쪽 테이블이기 때문에 다음과 같이 RIGHT OUTER JOIN 구문을 사용합니다.

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "RIGHT OUTER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "ORDER BY ComputerName, Drive", _
 objConnection, adOpenStatic, adLockOptimistic

DiskDrives가 관계에서 오른쪽 테이블이라면 Computers는 왼쪽 테이블이라기보다는 관계에 있어 남편 테이블이라고 해야 할 것입니다. Scripting Guys 중에는 남편은 관계에 있어 결코 오른쪽이 아니라는 것을 뼈저리게 경험한 사람도 있습니다.

오른쪽 우선 외부 조인을 사용하면 오른쪽 테이블의 모든 레코드(모든 디스크 드라이브)가 반환되지만 디스크 드라이브와 연관된 컴퓨터(왼쪽 테이블의 레코드)만 반환됩니다.

얼핏 보면 이상한 것 같지만 잠시만 테스트해 보면 작동 방식을 이해할 수 있을 것입니다. 테스트할 데이터베이스가 필요한 경우를 위해 microsoft.com/technet/scriptcenter/resources/tnmag/archive.mspx에 샘플을 게시해 두었습니다.

한 가지 중요한 것은, 외부 조인을 사용할 때 최소한 스크립트의 첫 번째 줄에는 On Error Resume Next 문을 지정해야 한다는 점입니다. 이유가 무엇일까요? 왼쪽 우선 외부 조인을 수행한다고 가정해 보겠습니다. 이 경우 하드 디스크가 설치되지 않은 컴퓨터가 반환될 가능성이 있습니다. 이 사실은 드라이브 문자를 표시하는 다음 코드 줄에 도달하기 전까지는 문제가 되지 않습니다.

Wscript.Echo objRecordset.Fields.Item("Drive")

이 특정 컴퓨터에는 Drive 필드가 없기 때문에 스크립트는 중지됩니다.

C:\Scripts\database.vbs(22, 9) Microsoft VBScript runtime error: Type mismatch

하지만 On Error Resume Next 문을 구현하면 스크립트는 컴퓨터에 디스크 드라이브가 없다는 사실을 무시하고 다음으로 넘어갈 수 있습니다. 또는 다음과 같은 코드를 사용하여 Drive 필드의 값을 검사한 다음 적절한 조치를 취할 수도 있습니다.

If IsNull(objRecordset.Fields.Item("Drive")) _Then
    Wscript.Echo "No disk drives installed."
Else
    Wscript.Echo _
      objRecordset.Fields.Item("Drive")
End If

이 코드를 사용하여 실제 드라이브 문자가 아니라 Null 값이 반환되는지 확인합니다. Null 값이 반환되면 "No disk drives installed"라는 메시지를 표시합니다. Null 값이 반환되지 않으면 Drive 필드의 값을 표시합니다. 출력 결과는 그림 4와 같습니다.

Figure 4 적절한 결과 표시

atl-ws-01
C:

atl-ws-01
D:

atl-ws-02
C:

atl-ws-02
D:

alt-ws-03
No disk drives installed.

장황한 설명 대신 몇 가지 쿼리를 더 살펴보겠습니다. 디스크 드라이브가 50GB보다 큰 경우 컴퓨터 및 해당 컴퓨터에 설치된 디스크 드라이브의 목록을 반환하는 내부 조인 쿼리를 예로 들겠습니다. 드라이브 크기가 GB 단위로 저장된다고 가정하겠습니다.

objRecordSet.Open "SELECT Computers.*, " & _
 "DiskDrives.* FROM Computers " & _
 "INNER JOIN DiskDrives ON " & _
 "Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "WHERE DriveSize > 50 " & _
 "ORDER BY Computers.ComputerName, " & _
 "DiskDrives.Drive", objConnection, _
 adOpenStatic, adLockOptimistic

여기서 확실히 알 수 있듯이 원래의 쿼리에 표준 WHERE 절만 추가되었습니다.

WHERE DriveSize > 50

컴퓨터의 드라이브 E에 대한 정보만 필요한 경우에는 어떻게 해야 할까요? 문제 없습니다. 다음과 같이 적절한 Where 절만 추가하면 됩니다.

WHERE Drive = 'E:'

다음은 디스크 드라이브가 설치되지 않은 컴퓨터의 컬렉션을 반환하는 조금 더 복잡한 쿼리입니다.

objRecordSet.Open _
 "SELECT Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "FROM Computers LEFT JOIN DiskDrives " & _
 "ON Computers.SerialNumber = " & _
 "DiskDrives.SerialNumber " & _
 "GROUP BY Computers.ComputerName, " & _
 "DiskDrives.Drive " & _
 "HAVING (DiskDrives.Drive) Is Null " & _
 "ORDER BY Computers.ComputerName", _
 objConnection, adOpenStatic, adLockOptimistic

이 쿼리는 조금 더 복잡하며 지면 관계상 작동 방식은 설명하지 않겠습니다. 하지만 가장 중요한 점인 의도대로 작동한다는 것은 확실합니다.

다음으로 중요한 것은 '관계'입니다. 이것은 여러 번 언급했지요. 그렇다고 해서 관계가 항상 올바르다는 의미는 아닙니다. 영화 "애니 홀(Annie Hall)"에서 마지막에 Woody Allen이 말한 것처럼 말이지요. "관계... 이건 마치 상어와 같아요. 계속 앞으로 나아가지 않으면 죽거든요. 지금 우리 손에 놓인 건 죽은 상어라고 생각해요." Woody Allen이 내부 조인과 외부 조인에 대해서 알았더라면 좋았을 텐데요. 이 쿼리 방법을 사용하면 언제나 성공적인 관계를 유지할 수 있을 것입니다.

Microsoft Scripting Guys는 Microsoft에서 고용되어 일하고 있는 Microsoft의 직원들입니다. 이들은 좋아하는 야구 경기와 기타 여러 활동을 하는 시간을 제외하고는 항상 TechNet 스크립트 센터를 운영합니다. 자세한 내용은 www.scriptingguys.com에서 확인하십시오.

© 2008 Microsoft Corporation 및 CMP Media, LLC. All rights reserved. 이 문서의 전부 또는 일부를 무단으로 복제하는 행위는 금지됩니다..