인자를 가진 프로퍼티와 디폴트 프로퍼티(Default Property)

등록일시: 2002-09-11 09:12,  수정일시: 2018-04-07 23:20
조회수: 16,346
본문은 최초 작성 이후, 약 22년 이상 지난 문서입니다. 일부 내용은 최근의 현실과 맞지 않거나 동떨어져 있을 수 있으며 문서 내용에 오류가 존재할 수도 있습니다. 또한 본문을 작성하던 당시 필자의 의견과 현재의 의견에 많은 차이가 존재할 수도 있습니다. 이 점, 참고하시기 바랍니다.

지금부터 살펴볼 프로퍼티 프로시저(Property Procedure)의 고급 사용법은 VBScript의 클래스가 제공해주는 다양한 기능들 중에서도 그야말로 백미라고 말할 수 있는 강력한 기능이다. 필자는 개인적으로 이 기능을 인자를 가진 프로퍼티라고 부르고 있지만, 이 용어는 공식적인 용어는 아니다. 다만, 본문에서는 설명의 편의를 위해 계속 이 용어를 사용하도록 하겠다.

일반적으로 임의의 프로퍼티에 값을 설정하거나 반대로 프로퍼티에서 값을 읽어올 때는 보통 다음과 같은 형식의 코드를 사용하게 된다.

objTemp.x = 3
tempVar = objTemp.x

그러나, 프로퍼티가 인자를 가진 프로퍼티인 경우에는 다음과 같은 코드가 사용된다.

objTemp.x(4, 5, 6) = 3
tempVar = objTemp.x(4, 5, 6)

그야말로 인자를 가진 프로퍼티라는 그 명칭에 걸맞게 일반적인 경우와 달리 프로퍼티에 인자가 사용되고 있다. 이런 유형의 코드를 접해본 경험이 계시거나 직접 작성해본 경험이 계신 분들은 아마도 몇 분 없으실 것이라고 생각된다.

이해를 돕기 위해서 프로퍼티의 개념에 대해서 다시 한 번 정리해보도록 하겠다. 프로퍼티란 클래스의 내부에 숨겨진 멤버 변수를 클래스의 외부에 노출하는 프로퍼티 프로시저라고 불리우는 특수한 종류의 프로시저로, 개념상으로는 클래스의 멤버 변수와 클래스의 외부 코드 사이에 위치하여 양자 간의 데이터 이동에 간섭한다.

여기서 프로퍼티가 사실은 프로시저의 일종이라는 점에 주목하기 바란다. 그리고, 지금 논의하고 있는 기능의 명칭이 인자를 가진 프로퍼티라는 사실 또한 상기해보기 바란다. 프로시저와 인자, 이 양자 간의 관계란 그야말로 실과 바늘 같은 것으로서 따로따로 떼어서는 생각할 수 없는 밀접한 것들이다. 결국, 이 두 가지 사실로부터 다음과 같은 원론적인 개념을 이끌어낼 수 있다.

  1. 프로시저에는 갯수에 상관 없이 여러가지 자료형의 인자가 존재할 수 있다.
  2. 프로퍼티는 프로퍼티 프로시저라는 프로시저의 일종이다.
  3. 따라서, 프로퍼티에는 갯수에 상관 없이 여러가지 자료형의 인자가 존재할 수 있다.

가령, 방금 전에 살펴본 인자를 가진 프로퍼티의 예제 코드에서 tempVar = objTemp.x(4, 5, 6)라는 두 번째 라인은, 지금 우리가 논의하고 있는 주제가 프로퍼티라는 선입견을 버리고 코드 그 자체만 보면, objTemp라는 특정 클래스 인스턴스에서 제공되는 x라는 메서드를 세 개의 인자(4, 5, 6)를 넣고 호출하는 것과 전혀 다를 바가 없는 것이다. 결론적으로 프로퍼티에 인자가 사용되는 것은 전혀 이상한 일이 아니라는 것을 알 수 있다.

이번에는 조금 다른 각도에서 생각해보도록 하자. 프로퍼티는 필연적으로 자신이 노출하는 클래스 멤버 변수의 자료형을 반영할 수 밖에 없다. 논리적인 관점에서 볼 때 Int 형 멤버 변수를 노출하는 프로퍼티는 Int 형 데이터를 받아들이거나 리턴해야만 한다. 또한, 문자열 멤버 변수를 노출하는 프로퍼티는 문자열 데이터를 받아들이거나 리턴해야만 한다. 물론, 억지로 프로퍼티와 그 프로퍼티가 노출하는 클래스 멤버 변수의 자료형을 다르게 프로그래밍 할 수도 있겠지만, 애초에 그럴 바에는 프로퍼티를 사용할 이유가 없는 것이다.

그렇다면, 예를 들어서 특정 프로퍼티가 노출하는 클래스 멤버 변수의 자료형이 크기가 1,024인 배열인 경우에는 어떻게 프로퍼티를 구현하는 것이 보다 효율적일까? 지금까지 해왔던 방식으로 프로퍼티를 작성한다면 그 배열 자체를 통채로 주고 받는 방법 밖에는 대안이 없다. 또는 원하는 인덱스에 존재하는 문자열 자료를 리턴받는 메서드를 새로 작성해야 한다.

그 중 첫 번째 방법은 그야말로 무식한 방법이라고 말할 수 있다. 크기가 1,024인 배열의 모든 인덱스에 각각 한글 10글자로 이뤄진 문자열 데이터가 존재한다고 생각해보면, 단순 계산으로도 배열 전체의 크기는 모두 1024 X 10 X 2 = 20,480 Byte가 된다. 단지 몇 글자로 이뤄진 문자열 하나를 설정하거나 얻기 위해서 매번 20,480 Byte의 데이터를 통채로 이동시키는 것은 말 그대로 무식한 짓이다.

사실 VBScript의 클래스에서도 ByVal 키워드와 더불어 ByRef 키워드가 지원되기는 한다. 익히 알고 있는 것처럼 인자를 선언할 때 ByRef 키워드를 함께 사용하면 런타임시에 데이터의 전체 값이 복사되는 것이 아니라 단지 데이터의 참조가 전달되므로 불필요한 오버로드를 줄일 수 있다. 그러나, 이 또한 근본적인 해결책은 아닌 셈이다. 당장 VBScript의 클래스에서야 ByRef 키워드를 사용하는 것으로 문제를 해결한다고 하지만, 이런 문제는 Microsoft Visual Basic 6.0을 사용해서 작성한 클래스, 즉 COM/COM+ 컴포넌트에서도 동일하게 발생하기 때문이다. 특히 서버간 원격 호출이 발생하거나 COM+ 응용 프로그램 설정과 IIS의 WAM(Web Application Manager) 등의 설정이 복잡하게 얽혀들어서 컴포넌트의 인스턴스가 서로게이트(Surrogate)인 DLLHOST.EXE의 프로세스 안에서 생성되는 상황에서는 아무리 DLL 형식의 컴포넌트라 할지라도 결국 Out-Of-Process 컴포넌트와 같이 프로세스간 마샬링이 일어나고, 아이러니하게도 ByRef 키워드보다는 ByVal 키워드가 더 권장되는 상황이 발생하게 된다.

따라서, 차라리 원하는 인덱스에 존재하는 문자열 자료를 리턴받는 메서드를 새로 작성하는 두 번째 방법이 더 효율적인 셈이다. 그런데, 여기서 한 가지 짚고 넘어가야 할 점이 있다. 메서드도 결국은 프로시저이고 프로퍼티도 역시 프로시저이다. 그렇다면 굳이 별도의 메서드를 새로 만들 필요 없이 프로퍼티 프로시저를 이용해서 현재의 문제를 해결할 수는 없는 것일까? 그 대답은 '물론, 가능하다.'고 그 결과물이 바로 인자를 가진 프로시저인 것이다.

다음은 이런 개념들을 바탕으로 해서 실제로 인자를 가진 프로퍼티를 구현한 간단한 예제 코드다. 이 코드에서는 0에서 9까지 모두 10개의 인덱스를 갖고 있는 clsEgoCube 클래스의 배열형 멤버 변수 m_Array에 대한 프로퍼티를 구현하고 있다. 재미있게도 인자를 가진 프로퍼티의 사용법과 C# 의 인덱서(Indexer)의 사용법이 매우 유사하다는 점을 염두에 두고서 코드를 살펴보기 바란다.

<%

  Dim oCls
  Dim strClassName
  Dim i
  
  
  '** clsEgoCube 클래스의 인스턴스를 생성한다.
  Set oCls = New clsEgoCube
  
  '** 인자를 가진 프로퍼티에 값을 설정한다.
  For i = 0 To 9
    oCls.ArrayProp(i) = "인덱스 " & i & " 번 입니다."
  Next
  
  '** 인자를 가진 프로퍼티에서 값을 읽어온다.
  For i = 0 To 9
    Response.Write "<font size=-1>" & oCls.ArrayProp(i) & "</font><br>"
  Next
  
  Set oCls = Nothing
  
  
  '** clsEgoCube 클래스 정의
  Class clsEgoCube
    
    '** 크기가 10 인 배열
    Private m_Array(9)
    
    Public Property Let ArrayProp(intIndex, strArg)
      m_Array(intIndex) = strArg
    End Property
    
    Public Property Get ArrayProp(intIndex)
      ArrayProp = m_Array(intIndex)
    End Property
    
  End Class
  
%>

이 코드를 주의 깊게 살펴보면 그 원리의 대부분을 쉽게 파악할 수 있을 것이다. 다만 주의해야 할 점은 각각의 프로퍼티 프로시저에 사용된 인자의 갯수와 순서, 그리고 실제로 그 프로퍼티가 호출될 때의 사용법이다. Property Let 프로시저나 Property Set 프로시저에서 가장 마지막 인자가 프로퍼티에 설정될 실제값을 담고 있다는 점에 주의하기 바란다.

다음 코드는 위의 코드를 일부 수정한 것으로 이차원 배열형 멤버 변수 m_Array를 인자를 가진 프로퍼티로 구현한 것이다. 이처럼 인자를 가진 프로퍼티는 그 본질이 프로시저라는 사실로부터 기인해서 전달되는 인자의 갯수에 전혀 구애를 받지 않는다.

<%

  Dim oCls
  Dim strClassName
  Dim i, j
  
  
  '** clsEgoCube 클래스의 인스턴스를 생성한다.
  Set oCls = New clsEgoCube
  
  '** 인자를 가진 프로퍼티에 값을 설정한다.
  For i = 0 To 9
    For j = 0 To 4
      oCls.ArrayProp(i, j) = "인덱스 " & i & ", " & j & " 번 입니다."
    Next
  Next
  
  '** 인자를 가진 프로퍼티에서 값을 읽어온다.
  For i = 0 To 9
    For j = 0 To 4
      Response.Write "<font size=-1>" & oCls.ArrayProp(i, j) & "</font><br>"
    Next
  Next
  
  Set oCls = Nothing
  
  
  '** clsEgoCube 클래스 정의
  Class clsEgoCube
    
    '** 크기가 10, 5 인 2 차원 배열
    Private m_Array(9, 4)
    
    Public Property Let ArrayProp(inti, intj, strArg)
      m_Array(inti, intj) = strArg
    End Property
    
    Public Property Get ArrayProp(inti, intj)
      ArrayProp = m_Array(inti, intj)
    End Property
    
  End Class
  
%>

또한, 인자의 데이터 형에도 제한이 없다는 장점이 있다. 가령 이번에는 프로퍼티로 구현할 클래스 내부 변수의 데이터 형이 배열이 아닌 Dictionary 형이라고 생각해 보자. 이 때는 배열의 경우와는 달리 인덱스가 아닌 문자열 형식의 키가 필요하다. 다음은 그런 경우를 구현한 코드다.

<%

  Dim oCls
  Dim strClassName
  
  
  '** clsEgoCube 클래스의 인스턴스를 생성한다.
  Set oCls = New clsEgoCube
  
  '** 인자를 가진 프로퍼티에 값을 설정한다.
  oCls.DicProp("TestKey") = "Scripting.Dictionary 테스트입니다."
  
  '** 인자를 가진 프로퍼티에서 값을 읽어온다.
  Response.Write "<font size=-1>" & oCls.DicProp("TestKey") & "</font><br>"
  
  Set oCls = Nothing
  
  
  '** clsEgoCube 클래스 정의
  Class clsEgoCube
    
    Private m_ObjDic
    
    Public Property Let DicProp(strKey, strArg)
      If m_ObjDic.Exists(strKey) Then
        m_ObjDic(strKey) = strArg
      Else
        m_ObjDic.Add strKey, strArg
      End If
    End Property
    
    Public Property Get DicProp(strKey)
      If m_ObjDic.Exists(strKey) Then
        DicProp = m_ObjDic(strKey)
      Else
        DicProp = ""
      End If
    End Property
    
    '** Initialize 이벤트와 Terminate 이벤트
    Private Sub Class_Initialize
      Set m_ObjDic = Server.CreateObject("Scripting.Dictionary")
    End Sub
    
    Private Sub Class_Terminate
      Set m_ObjDic = Nothing
    End Sub
    
  End Class
  
%>

뿐만 아니라 만약 필요하다면 전혀 다른 데이터 형의 인자들을 갯수에 상관 없이 섞어서 사용할 수도 있다. 이처럼 인자를 가진 프로퍼티는 적절히 사용하기만 한다면 C#의 인덱서 부럽지 않은 기능을 구현할 수 있는 것은 물론이고 그 용도의 다양성 또한 무궁무진하다고 말할 수 있다.

디폴트 프로퍼티(Default Property)

지금부터 설명하려고 하는 디폴트 프로퍼티(Default Property)는 처음 접하는 개념이라고 생각하시는 분들이 계실 수도 있겠지만, ADO 객체를 사용해 본 경험이 조금이라도 있는 분들이라면 자신도 모르게 디폴트 프로퍼티의 혜택을 받아온 셈이다. 특히나 ASP 프로그래밍을 하면서 Recordset 객체를 단 한 번도 접해보지 못할 확률은 현실적으로 거의 없다고 생각되므로 이 글을 읽으시는 여러분의 대부분은 이미 디폴트 프로퍼티의 혜택을 받으신 분들이라고 말할 수 있다.

가령 Recordset 객체를 사용해서 데이터베이스에서 MyColumn이라는 컬럼의 값을 가져온다고 가정해보면, 아마도 다음과 같은 코드를 사용하는 것이 일반적일 것이다. 여기에서 objRec는 Recordset 객체다.

strMyColumnValue = objRec("MyColumn")

이 코드는 이미 디폴트 프로퍼티 기능이 반영된 결과이다. 만약, 디폴트 프로퍼티를 사용하지 않으려고 한다면 다음과 같이 코드를 수정해야 한다.

strMyColumnValue = objRec.Fields("MyColumn").Value

사실은 이것이 정상적인 코드다. 처음과 같은 코드가 가능한 것은 Recordset 객체의 프로퍼티 중에서 Fields 컬렉션 프로퍼티가 Recordset 객체의 디폴트 프로퍼티고, Fields 컬렉션의 아이템에 해당하는 Field 객체의 프로퍼티 중에서 Value 프로퍼티가 디폴트 프로퍼티여서 코드에서 생략하는 것이 가능하기 때문이다. 실제로 각각의 디폴트 프로퍼티들을 코드에서 생략해보면 처음의 코드가 된다는 것을 알 수 있을 것이다.

별다른 설명없이 대뜸 코드부터 예로 들어 설명하긴 했지만 아마도 이 방법이 디폴트 프로퍼티에 대해서 가장 쉽게 접근하는 방법이 아닐까 생각한다. 그렇다면 이제부터는 디폴트 프로퍼티에 대해서 좀 더 자세히 알아보기로 하자.

지금 우리가 논의하고 있는 디폴트 프로퍼티의 그 근본 개념은 VBScript로부터 비롯된 것은 아니다. 지금까지 마이크로소프트의 플랫폼을 전제로 알아본 객체에 관한 대부분의 개념들이 의례 그렇듯이 이 개념도 COM으로부터 비롯된 것이다. Visual C++에서는 IDL에서 디폴트 프로퍼티로 설정하고 싶은 프로퍼티의 ID의 값을 0으로 설정함으로서 디폴트 프로퍼티를 선언한다. 그리고, Visual Basic에서는 추가 기능 메뉴의 클래스 작성기 유틸리티도구 메뉴의 프로시저 특성 메뉴를 사용해서 디폴트 프로퍼티를 설정한다. 그리고, 마지막으로 VBScript에서는 Default 키워드를 사용하여 디폴트 프로퍼티를 설정한다.

엄격하게 말하자면 사실 '디폴트 프로퍼티'라는 용어에는 어느 정도 어폐가 있다. 왜냐하면 디폴트 속성은 프로퍼티뿐만 아니라 프로시저에도 적용될 수 있기 때문이다. 이를 역으로 생각해보면 디폴트 속성은 근본적으로 프로시저에 적용되는 개념이라는 결론을 내릴 수 있다. 지금까지 누누히 강조해 왔지만 프로퍼티 역시도 프로시저의 일종인 프로퍼티 프로시저라는 점을 다시 한 번 기억하기 바란다.

일단 어떤 프로시저가 Default로 선언되면 그 프로시저는 명시적인 호출 없이도 사용 가능하다. 예를 들어서, 다음과 같이 간단하게 선언된 클래스가 있다고 생각해보자.

Class clsEgoCube

  '** Default 키워드를 사용하여 선언된 프로시저
  Public Default Function GetSiteURL()
      GetSiteURL = "http://www.egocube.pe.kr/"
  End Function
    
End Class

이 클래스의 인스턴스에서 GetSiteURL() 메서드를 호출하는 코드는 일반적으로 다음과 같을 것이다.

  Dim oCls
  
  Set oCls = New clsEgoCube
  Response.Write "<font size=-1>" & oCls.GetSiteURL() & "</font><br>"
  Set oCls = Nothing

그러나, GetSiteURL() 메서드는 Default 키워드를 사용하여 선언되었으므로 다음과 같은 코드 역시도 성립된다.

  Dim oCls
  
  Set oCls = New clsEgoCube
  Response.Write "<font size=-1>" & oCls & "</font><br>"
  Set oCls = Nothing

이번에는 디폴트 프로퍼티를 설정해보도록 하자. 프로퍼티 프로시저의 선언에 Default 키워드를 사용해서 디폴트 프로퍼티를 설정할 때는 다음과 같이 클래스를 작성하면 된다. 이 코드에서는 Name이라는 이름의 문자열 형식 디폴트 프로퍼티를 선언하고 있다. 이 때 한 가지 주의해야 할 점은, 디폴트 프로퍼티를 설정할 때에는 오직 Property Get 프로시저의 선언에만 Default 키워드를 사용할 수 있다는 점이다.

  Class clsEgoCube

    Private m_Name
    
    Public Property Let Name(strArg)
      m_Name = strArg
    End Property
    
    '** Property Get 프로시저의 선언에서만 Default 키워드를 사용할 수 있다.
    Public Default Property Get Name()
      Name = m_Name
    End Property
    
  End Class

마찮가지로 이 클래스의 인스턴스로부터 Name 프로퍼티의 값을 가져오는 코드는 다음과 같은 일반적인 코드로도 가능하고...

  Dim oCls
  
  Set oCls = New clsEgoCube
  oCls.Name = "clsEgoCube"
  Response.Write "<font size=-1>" & oCls.Name & "</font><br>"
  Set oCls = Nothing

또는, 다음과 같이 명시적인 호출을 생략할 수도 있다.

  Dim oCls
  
  Set oCls = New clsEgoCube
  oCls.Name = "clsEgoCube"
  Response.Write "<font size=-1>" & oCls & "</font><br>"
  Set oCls = Nothing

이처럼 Default 키워드를 사용할 때 주의해야 할 점이 두 가지 있다. 먼저, Default 키워드는 하나의 클래스에서 오직 한 번만 사용 가능하다는 것이다. 두 번째는 Default 키워드는 반드시 Public 접근 제한문과 함께 사용해야만 한다는 점이다. 만약, Default 키워드를 Private 접근 제한문과 함께 사용하면 ''Default' 지정 시는 또한 'Public'도 지정해야 합니다.'라는 조금 어색한 메세지와 함께 오류가 발생하게 된다. 이는 매우 당연한 결과로서 클래스의 외부에 노출되지도 않는 프로시저를 디폴트로 설정할 필요는 없는 것이다.

그러나 이처럼 쉽고 편리해 보이기만 하는 디폴트 프로퍼티도 주의해서 사용하지 않으면 경우에 따라서는 양날의 검이 될 수도 있다. 다음의 코드에서는 방금 위에서 작성한 clsEgoCube 클래스의 인스턴스를 생성하고 디폴트 프로퍼티인 Name 프로퍼티에 접근하고 있다.

  Dim oCls
  
  Set oCls = New clsEgoCube
  oCls.Name = "clsEgoCube"
  Response.Write "<font size=-1>" & TypeName(oCls.Name) & "</font><br>"
  Response.Write "<font size=-1>" & TypeName(oCls) & "</font><br>"
  Set oCls = Nothing

당연한 얘기지만 이 코드에서 첫 번째 Response.Write 문과 두 번째 Response.Write 문의 출력값은 다르다. 이 두 구문은 전혀 다른 의미를 갖고 있는 것이다. 첫 번째 Response.Write 문은 Name 프로퍼티의 데이터 형인 'String'을 출력한다. 그러나, 두 번째 Response.Write 문은 clsEgoCube 클래스의 인스턴스인 oCls의 데이터 형인 'clsEgoCube'를 출력한다. 디폴트 프로퍼티라는 개념에만 집착해서 이러한 미묘한 차이점을 생각하지 않고 프로그래밍을 하다보면 큰 실수를 저지를 수도 있는 것이다.

거꾸로 디폴트 프로퍼티에 값을 설정할 때에도 동일한 이유로 주의를 기울여야 한다. 위의 코드를 약간 수정해 보도록 하겠다.

  Dim oCls
  
  Set oCls = New clsEgoCube
  oCls.Name = "clsEgoCube"
  oCls = "clsEgoCube"
  Set oCls = Nothing

이 경우에도 전혀 의도하지 않은 결과가 발생하게 된다. oCls.Name = "clsEgoCube" 구문의 경우는 그 의도 자체도 명확하고 오류가 발생할 이유가 전혀 없다. 그러나, oCls = "clsEgoCube" 구문은 경우가 다르다. 자칫 clsEgoCube 클래스의 디폴트 프로퍼티인 Name 프로퍼티에 'clsEgoCube'라는 문자열을 대입하는 것으로 받아들이기 쉽다. 그러나, 이 구문을 곧이곧대로 해석하자면 clsEgoCube 클래스의 인스턴스 레퍼런스 변수인 oCls가 String 형 변수로 바뀌고 'clsEgoCube'라는 문자열이 대입되는 것이다.

이런 디폴트 프로퍼티와 관련된 오류를 피하기 위한 요령은 이렇다. 디폴티 프로퍼티를 선언하는 것은 문법적인 오류만 조심하면 된다. 반면, 실제로 디폴트 프로퍼티를 사용할 때는 반드시 디폴트 프로퍼티로부터 그 설정된 값을 얻어올 때만으로 한정해야 한다. 디폴트 프로퍼티에 값을 설정하거나, 디폴트 프로퍼티에 설정된 값이 아닌 프로퍼티 자체에 관한 처리를 할 때는 반드시 프로퍼티명을 명시해야한다.