본문 바로가기

진리는어디에/VBA

[VBA] 클래스(class) 완벽 가이드

이 포스트는 Excel Macro Mastery 사이트의 'VBA Class Modules - The Ultimate Guide(by Paul Kelly)'의 내용을 다시 정리한 것입니다. 이번 포스트에서는 VBA의 클래스 모듈에 대해 다룹니다.

클래스 요약

용어 설명
클래스(Class) 사용자는 클래스를 기반으로 객체를 만들 수 있습니다
메소드(Method) 클래스 모듈의 공용 함수 또는 서브루틴
멤버 변수(Member Variable) 클래스 모듈에 선언된 변수
프로퍼티(Property) 변수 처럼 사용되는 특수 함수 또는 서브루틴
프로퍼티 타입(Property Type) Get, Set, Let
생성자(Event Initialize) 클래스 모듈 객체가 생성될 때 자동으로 실행되는 서브루틴
소멸자(Event Terminate) 클래스 모듈 객체가 삭제 되면 자동으로 실행되는 서브루틴
정적 선언 및 생성 Dim o As New Class1
동적 생성 및 생성 Dim o As Class1
Set o = New Class1
클래스 서브루틴 호출 o.WriteValues Total
클래스 함수 호출 Amount = o.Calculate()
클래스 프로퍼티 호출 o.Amount = 1
Total = o.Amount

들어가며

VBA 클래스 모듈을 사용하면 사용자가 정의한 자료구조의 객체를 만들 수 있습니다. VBA 객체에 익숙하지 않은 분이라면 먼저 [VBA] 객체(Object) 완벽 가이드를 보고 오는 것이 이해에 도움이 될 것입니다.

C#과 자바와 같은 언어에서는 클래스를 사용하여 사용자가 정의한 객체를 만듭니다. VBA의 클래스 모듈은 이러한 언어들의 클래스와 비슷한 역할을 가지고 있습니다. 다만 차이점이 있다면 다른 언어의 클래스에 비해 상속의 방식이 매우 제한적이라는 것입니다. VBA의 상속은 C#/자바의 인터페이스 상속과 유사한 방식으로 작동합니다.

VBA에서는 컬렉션, 워크북, 워크시트와 같은 기본으로 제공되는 내장 개체들이 있습니다. VBA 클래스의 목적은 우리가 필요한 개체를 직접 정의하여 사용할 수 있도록 하는것 입니다.

먼저 객체를 사용하는 이유를 살펴보는 것에서 부터 이 포스트를 시작해 보도록 하겠습니다.

객체를 사용하는 이유

객체를 사용하면 마치 블록을 이용해 건물을 만드는것 처럼 어플리케이션을 구축할 수 있습니다. 이 방식의 기본은 각 객체의 코드들은 서로 독립적이라는 것입니다. 흔히 클래스를 사용하여 어플리케이션을 구축하는 것을 레고 블록에 비유하곤 합니다. 서로 완전히 독립적인 블록들을 조합하여 건물, 차량, 우주 정거장 등을 만들 수 있습니다.

클래스를 사용하여 코드의 일부를 블록 처럼 사용하면 많은 이점을 얻을 수 있습니다.

  • 코드 구현 단위를 블록으로 제한함으로써 구현을 간단하게 만들 수 있습니다.
  • 어플리케이션의 개별 부분 테스트가 훨씬 쉽습니다.
  • 코드를 업데이트 하더라도 어플리케이션의 다른 부분에 끼치는 영향이 적습니다.
  • 어플리케이션에 새로운 개체를 추가하여 확장이 용이 합니다.

반면에 개체를 사용할 때의 단점도 있습니다.

  • 어플리케이션 빌드에 더 많은 시간이 걸립니다.
  • 객체가 무엇인지 정의하는 것이 모호할 때가 있습니다.
  • 클래스와 객체에 대한 개념을 이해 해야만 합니다.

클래스를 사용하여 프로그램을 만들면 디자인과 설계에 더 많은 시간을 소비해야 하므로 처음에는 개발 시간이 더 오래 걸리며 비효율적으로 느껴질 수 있습니다. 하지만 장기적으로 보면 코드 가독성과 확장성, 유지보수성을 높여주므로 코드를 쉽게 업데이트하고 재사용할 수 있습니다.

위의 말들이 뜬 구름 잡는 소리 처럼 들릴지라도 걱정할 필요 없습니다. 이 포스트의 나머지 부분에서 모든 것을 설명할 것입니다.

간단한 클래스 모듈 만들어 보기

우선 VBA에서 클래스 모듈을 만들고 코드에서 사용하느 아주 간단한 에제를 살펴 보도록 하겠습니다. 클래스 모듈을 생성하려면 프로젝트 창에서 마우스 우클릭으로 '삽입 > 클래스 모듈'을 선택 합니다(만일 엑셀 VBA 메뉴에 익숙하지 않으신 분이라면 [여기]에서 기본 메뉴 사용법을 살펴 보시길 권합니다).

위와 같이 새 클래스 모듈을 추가하면 'Class1'이라는 이름의 클래스 모듈이 추가 된것을 확인할 수 있습니다.

클래스 모듈의 이름을 clsCustomer로 변경해 보겠습니다. 그런 다음 클래스 모듈에 다음과 같이 변수를 추가합니다.

Public Name As String

이제 우리는 워크북에 속한 모든 모듈에서 이 클래스 모듈을 사용할 수 있습니다. 예를 들어

' 고객 클래스 모듈 생성
Dim oCustomer As  New clsCustomer

' 고객 이름 설정 
oCustomer.Name = "Kukuta"

' 고객 이름을 직접 실행 창에 출력(Ctrl + G) 
Debug.Print oCustomer.Name

클래스 모듈 vs 객체

VBA 클래스 모듈을 처음 사용하는 사름들은 종종 클래스와 객체를 혼동합니다. 클래스와 객체의 차이를 간단히 설명하기 위해 붕어빵 틀과 붕어빵의 예를 들어 보겠습니다.

클래스는 붕어빵 틀과 같습니다. 모양이 정해져 있고 어떤 밀가루 반죽을 넣던지 붕어빵을 구워내게 됩니다. 하지만 붕어빵 틀은 붕어빵이 아닙니다. 객체는 붕어빵 틀에서 구워져 나온 붕어빵과 같습니다. 동일한 틀에서는 동일한 모양의 붕어빵들이 구워져 나옵니다. 몇개든 상관 없습니다 계속 구워져 나옵니다. 앙꼬가 다를수도 있지만 결국 같은 모양의 붕어빵이 계속 구워져 나옵니다.

  • 클래스 모듈은 디자인으로 생각할 수 있습니다.
  • 객체는 디자인을 기본으로 생성된 아이템으로 생각할 수 있습니다.

VBA의 New키워드를 통해 객체를 생성할 수 있습니다.

' New를 이용해 객체 생성하기
Dim oItem As New Class1
Dim oCustomer1 As New clsCustomer
Dim coll As New Collection

NOTE : Wookbook과 Worksheet와 같은 경우는 이미 생성되어 있는 객체를 사용하므로 New를 이용하여 새로 생성하지 않습니다.

클래스 모듈 vs 일반 모듈

클래스 모듈에서 코드를 작성하는 것은 일반 모듈에서 작성하는 것과 거의 동일합니다. 사실 일반 모듈에서 사용하는 것과 동일한 코드를 사용할 수 있습니다. 하지만 이 코드가 사용되는 방식에서는 매우 다릅니다.

이번 섹션에서는 VBA 를 처음 접하시는 분들에게 종종 혼란을 일으키는 클래스와 일반 모듈의 주요 차이점에 대해서 살펴 보겠습니다. 

차이점 1 - 모듈 사용 방법

클래스 모듈에서 Sub/Function을 사용하기 위해서는 먼저 객체를 생성해야 합니다.

예를 들어 두 개의 똑같은 PrintCustomer Sub가 하나는 클래스 모듈에 있고 하나는 일반 모듈에 있다고 가정해 보겠습니다.

' 클래스 모듈 코드 - clsCustomer 
Public  Sub PrintCustomer()
    Debug.Print  "Sample Output" 
End  Sub
' 일반 모듈 코드 - clsCustomer 
Public  Sub PrintCustomer()
    Debug.Print  "Sample Output" 
End  Sub

우리는 위 두 코드가 정확히 똑같음을 알 수 있습니다. 하지만 클래스 모듈에서 PrintCustomer Sub를 사용하기 위해서는 해당 클래스 모듈 타입의 객체를 생성해야만 합니다.

Sub UseCustomer()
    Dim oCust As New clsCustomer ' 객체를 생성
    oCust.PrintCustomer
End Sub

하지만 일반 모듈의 PrintCustomer Sub를 사용하기 위해서는 단순히 호출을 하기만 하면 됩니다.

Sub UseCustomer()
    PrintCustomer
End Sub

차이점 2 - 객체 사본(Copy)의 수

일반 모듈에서 변수를 생성하면 단 하나의 복사본 만이 존재합니다. 하지만 클래스 모듈의 경우 생성하는 각 객체마다 각각의 변수 복사본이 존재합니다.

예를 들어 클래스 모듈과 일반 모듈에서 StudentName 변수를 생성한다고 상상해 보십시오.

' 일반 모듈
Public StudentName As String

StudentName = "John"

일반 모듈의 경우 어플리케이션에서 단 하나의 변수만이 존재 합니다. 그래서 어디에서 StudentName 변수를 수정 또는 참조 하더라도 항상 같은 값을 바라보게 됩니다.

반면에 클래스 모듈의 StudentName 변수는 각 클래스의 객체 마다 별도의 변수를 가지고 있습니다.

' 클래스 모듈
Public StudentName As String

Dim student1 As New clsStudent
Dim student2 As New clsStudent

student1.StudentName = "Bill"
student2.StudentName = "Ted"

클래스 모듈의 구성

이번 섹션에서는 클래스 모듈의 구성에 대해 살펴 보도록하겠습니다. 클래스 모듈은 아래 네 가지 구성 요소로 이루어져 있습니다.

  • 메소드(Method) - Function/Sub
  • 멤버 변수(Member Variable) - 클래스 객체에 속하는 변수
  • 프로퍼티(Properties) - 변수 처럼 동작하는 Function/Sub
  • 이벤트(Events) - 지정된 특정 이벤트에 의해 트리거 되는 Sub
' 멤버 변수
Private m_balance As Double

' 프로퍼티
Property Get Balance() As Double
    Balance = m_balance
End Property

Property Let Balance(value As Double)
    m_balance = value
End Property

' 클래스 생성시 호출되는 이벤트 Sub
Private Sub Class_Initialize()
    m_balance = 100
End Sub

' 메소드
Public Sub Withdraw(amount As Double)
    m_balance = m_balance - amount
End Sub

Public Sub Deposit(amount As Double)
    m_balance = m_balance + amount
End Sub

다음은 위 클래스 모듈을 사용하는 방법을 보여주고 있습니다.

Sub Demo_clsAccount()

    Dim oAccount As New clsAccount
    
    ' 잔고 출력
    Debug.Print "시작 잔고: " & oAccount.Balance

    ' 입금
    oAccount.Deposit 25

    ' 잔고 출력
    Debug.Print "입금 후 잔고: " & oAccount.Balance
    
    ' 출금
    oAccount.Withdraw 100

    ' 잔고 출력
    Debug.Print "출금 후 잔고: " & oAccount.Balance
    
End Sub

위 코드를 실행하면 아래와 같은 결과가 출력 됩니다.

시작 잔고: 100
입금 후 잔고: 125
출금 후 잔고: 25

이제 우리는 위 예를 바탕으로 보다 상세한 클래스 모듈의 구성에 대해 살펴 보도록 하겠습니다.

클래스 모듈 메서드(Method)

메소드는 클래스에 선언 되어 있는 Function/Sub와 같은 프로시져를 말합니다. 메소드 선언 앞에는 Public/Private와 같은 접속 한정자를 두어 접근권한을 제어할 수 있습니다.

다음 예를 통해 클래스 모듈의 메소드에 대해 살펴 보도록 하겠습니다. 아래 예제는 클래스 모듈의 이름을 clsExample로 지정했다고 가정합니다.

' 클래스 모듈 이름을 'clsExample'이라고 만들어 주세요

' Public 프로시져는 객체 외부에서 호출 가능합니다.
Public Sub PrintText(text As String)
    Debug.Print text
End Sub

Public Function Calculate(amount As Double) As Double
    Calculate = amount - GetDeduction
End Function

' Private 프로시져는 클래스 모듈 내부에서만 호출이 가능합니다.
Private Function GetDeduction() As Double
    GetDeduction = 2.78
End Function

위의 clsExample 클래스 모듈은 다음과 같이 사용할 수 있습니다.

Public Sub ClassMembers()
    
    Dim oSimple As New clsExample
    
    oSimple.PrintText "Hello"
     
    Dim total As Double
    total = oSimple.Calculate(22.44)
     
    Debug.Print total

End Sub

클래스 모듈 멤버 변수

이전 섹션에서 클래스 내부에 선언된 Function/Sub 프로시져를 메소드라고 부른다고 했습니다. 클래스 모듈에는 프로시져 뿐만 아니라 변수도 선언이 가능합니다. 이것을 '멤버 변수'라고 합니다.

멤버 변수는 VBA에서 사용하는 일반 변수들과 매우 비슷합니다. 차이점은 Dim대신 Public 또는 Private이라는 접근한정자를 사용한다는 것 뿐입니다.

Private Balance As Double
Public AccountID As String

NOTE : Dim과 Private은 정확히 동일한 작업을 수행하지만, Dim은 Sub/Function 내부에서 사용하고 Private은 Sub/Function 외부에서 사용하는 것이 규칙입니다.

Public 키워드는 클래스 모듈 외부에서 변수에 접근할 수 있음을 의미합니다. 예를 들어 :

Sub Demo_BankAccount()

    Dim oAccount As New clsBankAccount
    
    ' Valid - AccountID 는 public이기 때문에 외부에서 접근 가능
    oAccount.AccountID = "499789"
    
    ' ERROR - Balance 는 private이기 때문에 외부에서 접근 불가능.
    oAccount.Balance = 678.9
    
End Sub

위 예에서 Balance는 Private로 선언되어 있으므로 클래스 외부에서는 접근할 수 없습니다. Private으로 선언된 변수는 클래스 모듈 내에서만 사용 가능합니다.

Private Balance As Double

Public Sub SetBalance()
    Balance = 100
    Debug.Print Balance
End Sub

일반적으로 멤버 변수를 Public으로 선언하여 객체 외부의 코드에서 클래스의 값을 직접 변경할 수 있게하는 것은 좋지 않은 방법이라 말합니다. 클래스 객체 외부에서 클래스의 작동 방식을 방해 할 수 있기 때문입니다.

그래서 VBA에서는 사용자가 멤버 변수에 직접 접근하는 것을 막기위해 프로퍼티(Properties)라는 것을 제공합니다.

클래스 모듈 프로퍼티(속성, Properties)

  • Get - 클래스로 부터 객체 또는 값을 반환합니다.
  • Let - 클래스 내부에 있는 값을 설정 합니다.
  • Set - 클래스의 객체를 설정 합니다.

VBA 프로퍼티의 형식

일반적인 프로퍼티의 형식은 아래와 같습니다.

Public Property Get () As Type
End Property

Public Property Let (varname As Type )
End Property

Public Property Set (varname As Type )
End Property

우리는 앞에서 프로퍼티가 단순한 Sub 프로시져의 일종이란느 것을 앞에서 보았습니다. 프로퍼티의 목적은 호출자가 값을 리턴 받거나 설정할 수 있도록하는 것입니다.

프로퍼티를 사용하는 이유

우리는 왜 변수를 Public으로 선언하여 직접 접근하여 읽고 쓰면 안될까요?

몇가지 예를 들어 설명하겠습니다. 아래 처럼 국가들의 목록을 관리하는 클래스가 있다고 상상해 보싶시오. 우리는 국가의 목록을 배열에 저장 할 수 있을 겁니다.

Public arrCountries As Variant

' 클래스가 초기화 될때 배열의 크기 설정
Private Sub Class_Initialize()
    ReDim arrCountries(1 To 1000)
End Sub

사용자가 리스트에 있는 국가의 수를 얻기 위해서는 다음과 같이할 수 있을 겁니다.

Dim oCountry As New clsCountry

' 국가의 갯수 얻기
NumCountries = UBound(oCountry.arrCountries) - LBound(oCountry.arrCountries) + 1

하지만 위 코드에는 두 가지 큰 문제점이 있습니다.

  • 국가 수를 얻으려면 목록이 저장되는 방법(예:Array)를 알아야 합니다.
  • 만일 우리가 저장소를 배열에서 Collection으로 변경하면 배열을 참조하고 있던 코드들을 모두 변경해야 합니다.

그래서 위 문제를 해결하기 위해 국가의 갯수를 리턴하는 함수를 추가해 보겠습니다.

Private arrCountries() As String

Public Function Count() As Long
    Count = UBound(arrCountries) + 1
End Function

그럼 이제 다음과 같이 사용할 수 있습니다.

Dim oCountries As New clsCountries

Debug.Print "Number of countries is " & oCountries.Count

이 코드는 위에 언급된 두 가지 문제를 해결 했습니다. 배열을 컬렉션으로 변경하더라도 참조하는 부분에서는 코드에 변경없이 동일하게 사용할 수 있습니다. 또한 호출자는 국가 목록이 저장되는 방식에 대해 몰라도 상관 없습니다. 호출자가 알아야할 것은 단지 Count 함수를 호출하면 국가 수가 반환 된다는것 뿐입니다. 내부에서 무슨 일이 있건 그것은 사용자가 알아야 할 일이 아닙니다.

우리는 Sub 또는 Function을 사용하여 멤버 변수에 대한 접근을 제한하여 앞에 언급된 문제를 해결하는 방법을 살펴 보았습니다. 이제는 '프로퍼티'를 사용하면 보다 우아하게 문제를 해결하는 방법에 대해 살펴 보도록 하겠습니다.

Function/Sub 대신 프로퍼티 사용

우리는 Count Function을 만드는 대신에 Count 프로퍼티를 만들 수 있습니다. 방식은 아래 예에서 보시다 싶이 둘다 서로 비슷합니다.

' 함수 버전 국가 갯수 얻기
Public Function Count() As Long
    Count = UBound(m_countries) - LBound(m_countries) + 1
End Function

' 프로퍼티 버전 국가 갯수 얻기
Property Get Count() As Long
    Count = UBound(m_countries) - LBound(m_countries) + 1
End Property

위 코드는 전체적으로 비슷해 보이긴하지만 프로퍼티를 사용 할 때는 Let, Get과 같은 키워드를 통해 접근을 제어하고 있습니다.

Private m_totalCost As Double

Property Get totalCost() As Long
     totalCost = m_totalCost
End Property

Property Let totalCost(value As Long)
     m_totalCost = value
End Property

Let을 사용하면 프로퍼티를 변수 처럼 사용할 수 있습니다.

oAccount.TotalCost = 6

두 번째 차이점은 Let 및 Get을 사용하면  Get 또는 Let 프로퍼티를 참조할 때 동일한 이름을 사용할 수 있다는 것입니다. 결과로 우리는 프로퍼티를 변수와 동일한 방법으로 사용할 수 있습니다. 이것이 Function/Sub 대신에 프로퍼티를 사용하는 이유입니다.

프로퍼티(속성 : Properties)
변수 처럼 편리하게 사용할 수 있지만, 실제로는 Function/Sub
oAccount.TotalCost = 6
value = oAccount.TotalCost

또한 이전의 예제에서는 SetTotalCost와 같은 함수로 인자를 넘겨 주어야 했던 반면 Let 키워드를 이용한 프로퍼티는 변수 처럼 값을 할당할 수 있음을 알 수 있습니다.

프로퍼티 요약

  1. 프로퍼티는 호출자에게 세부 구현 내용을 숨깁니다.
  2. 프로퍼티를 사용하면 프로시져를 변수와 동일한 방식으로 사용할 수 있습니다.

프로퍼티 타입

VBA  프로퍼티에는 세 가지 타입이 있습니다. 우리는 앞에서 이미 Get과 Let을 보았습니다. 지금 부터 볼 것은 Set 입니다.

Set은 Let과 유사하게 값을 설정하는데 사용되지만 그 대상은 변수가 아닌 객체 입니다. 원래 Viusal Basic에서 Let 키워드는 변수를 할당하는데 사용되었습니다. 사실 지금도 생략 가능하기에 대부분의 경우 사용하지 않아서 그렇지 변수에 값을 할당 할 때 여전히 Let 키워드를 사용할 수 있습니다.

' 아래 두 줄은 같음
a = 7
Let a = 7

VBA에서 변수에 값을 할당할 때 일반 값을 할당한다면 Let, 객체를 할당한다면 Set을 사용합니다.

' Using Let
Dim a As Long
Let a = 7

' Using Set
Dim coll1 As Collection, coll2 As Collection
Set coll1 = New Collection
Set coll2 = coll1
  • Let은 기본 변수 타입에 값을 할당하는데 사용 됩니다.
  • Set은 객체 변수에 값을 할당하는데 사용 됩니다.

다음 에에서는 문자열 변수에 대해 Get 및 Let 프로퍼티를 name이라는 이름을 사용하여 선언합니다.

Private m_name As String

' Get/Let Properties
Property Get name() As String
    name = m_name
End Property

Property Let name(name As String)
    m_name = name
End Property

그럼 다음과 같이 name 프로퍼티를 사용할 수 있습니다.

Sub TestLetSet()

    Dim name As String
    Dim oPerson As New clsPerson
    
    ' Let Property
    oPerson.name = "Bill"
    
    ' Get Property
    name = oPerson.name

End Sub

다음 예에서 객체 변수에 대해 Get과 Set 프로퍼티를 사용하는 것을 살펴 보겠습니다.

Private m_Prices As Collection

' Get/Set Properties
Property Get Prices() As Collection
    Set Prices = m_Prices
End Property

Property Set Prices(newPrices As Collection)
    Set m_Prices = newPrices
End Property

위의 예에서는 프로퍼티가 감싸고 있는 변수가 Collection 객체이기에 Set을 사용하고 있습니다. 아래 예제는 클래스와 함께 Let/Set 프로퍼티를 사용하는 방법을 보여 줍니다.

  1. TestLetSet Sub는 컬렉션을 만들고 값들을 추가합니다.
  2. 그런 다음 Set 프로퍼티를 사용하여 clsCurrency 클래스 개체에 추가합니다.
  3. 그런 다음 Get 프로퍼티를 사용하여 클래스 개체에서 읽습니다.
Sub TestLetSet()
    
    ' 컬렉션 객체를 생성하고 가격 추가
    Dim Prices As New Collection
    Prices.Add 21.23
    Prices.Add 22.12
    Prices.Add 20.12
        
    Dim oCurrency As New clsCurrency
    
    ' clsCurrency의 Set 프로퍼티 사용 컬렉션을 클래스에 추가
    Set oCurrency.Prices = Prices

    Dim PricesCopy As Collection
    ' clsCurrency의 Get 프로퍼티 사용
    ' 클래스 객체의 컬렉션을 읽음
    Set PricesCopy = oCurrency.Prices
    
    ' 직접 출력창에 결과 출력
    PrintCollection Prices, "Prices"
    PrintCollection PricesCopy, "Copy"
    
End Sub

' 직접 출력창에 컬렉션 내용 출력
Sub PrintCollection(c As Collection, name As String)

    Debug.Print vbNewLine & "Printing " & name & ":"

    Dim item As Variant
    For Each item In c
        Debug.Print item
    Next item

End Sub

클래스 모듈 이벤트

클래스 모듈에는 두 개의 특수한 이벤트 프로시져가 있습니다.

  • Initialize - 클래스의 새 객체가 생성될 때 마다 호출 됩니다.
  • Terminate - 클래스의 객체가 삭제될 때 마다 호출 됩니다.

C++/C#, 자바와 같은 언어에 대한 경험이 있으시다면 이것을 생성자와 소멸자라라고 부른다는 사실을 이미 알고 계실지도 모르겠습니다.

이번 섹션에서는 Initialize와 Terminate를 구현한 간단한 clsSimple 클래스를 만들어 보면서 VBA 클래스 객체의 생성자와 소멸자의 호출에 대해 살펴 보도록하겠습니다.

Private Sub Class_Initialize()
    Debug.Print "Class is being initialized"
End Sub

Private Sub Class_Terminate()
    Debug.Print "Class is being terminated"
End Sub

Public Sub PrintHello()
    Debug.Print "Hello"
End Sub

Class_Initialize

다음 예제에서 우리는 Dim과 New를 동시에 사용해 객체를 생성해 볼 것입니다. 이 경우 oSimple 객체는 최초 참조가 발생할 때 까지 객체가 생성 되지 않습니다.

Sub ClassEventsInit()

    Dim oSimple As New clsSimple
    
    Debug.Print "Before Class Initialize"
    
    ' Initialize occurs here
    oSimple.PrintHello

End Sub
OUTPUT 
Before Class Initialize
Class is being initialized
Hello
Class is being terminated

Set과 New를 이용해 객체를 생성할 때는 위와 다른 결과가 출력 됩니다. Set이 호출 될 때 객체가 생성되는 것을 확인 할 수 있습니다.

Public Sub ClassEventsInit()

    Dim oSimple As clsSimple
    
    Set oSimple = New clsSimple
    
    Debug.Print "Before Class Initialize"
    ' Initialize occurs here
    oSimple.PrintHello

End Sub
Class is being initialized
Before Class Initialize
Hello
Class is being terminated

NOTE : Dim과 Set의 차이에 대해 보다 자세한 사항은 [여기]에서 확인 가능합니다.

생성자 매개변수

대부부분 언어에서 생성자에 매개 변수를 전달하여 객체를 초기화 할 수 있지만 VBA에서는 그렇게 할 수 없습니다. 하지만 클래스 팩토리(Class Factory)를 사용하여 이 문제를 해결할 수 있습니다.

먼저 클래스 모듈에는 객체를 초기화 하는 메소드를 추가하고, 일반 모듈에는 객체를 생성하는 함수를 하나 만들어 보겠습니다.

' 클래스 모듈 코드 - clsSimple
Public Sub Init(Price As Double) 

End Sub 

' ========================================

' 일반 모듈 코드
Public Sub Test()

    ' CreateSimpleObject 함수를 이용해 객체 생성
    Dim oSimple As clsSimple
    Set oSimple = CreateSimpleObject(199.99)

End Sub

Public Function CreateSimpleObject(Price As Double) As clsSimple

    Dim oSimple As New clsSimple
    oSimple.Init Price

    Set CreateSimpleObject = oSimple

End Function

위 예제 처럼 객체를 생성할 때 직접 생성하는 것이 아닌 CreateSimpleObject를 이용해 대리 생성을하고 내부에서 클래스 초기화 메소드를 호출하게 한다면 마치 인자를 가진 생성자를 사용하는 것과 같은 효과를 얻을 수 있습니다.

Class_Terminate

객체의 종료 이벤트는 클래스 객체가 삭제 될 때 호출 됩니다. VBA에서 객체가 삭제되는 시점은 아무곳에서도 해당 객체를 참조하지 않을 때 입니다. 아래와 같이 Nothing을 대입하여 객체를 삭제하게 할 수 있습니다.

Sub ClassEventsTerm()

    Dim oSimple As clsSimple
    Set oSimple = New clsSimple
    
    ' Terminate occurs here
    Set oSimple = Nothing
   
End Sub

만일 Nothing을 셋팅하지 않는다면 객체는 범위를 벗어 날때 자동으로 삭제 됩니다. 범위를 벗어 난다는 것은 객체가 생성된 프로시져가 종료 되는 것을 의미합니다.

클래스 모듈 예제 1

이번 섹션에서는 일반적으로 자주 사용되는 클래스 모듈 형식을 살펴 보며 클래스 모듈에 익숙해지는 시간을 가져 볼 것입니다. 아래와 같은 데이터 테이블이 있다고 상상해 보십시오.

학생의 이름을 기준으로 다양한 보고서를 만들고 싶습니다. 이를 위해 2차원 배열을 사용하거나 컬렉션 배열을 사용할 수 있습니다.

For i = 2 To rg.Rows.Count
    
    ' 각 학생 정보를 위해 새로운 컬렉션 생성
    Set rowColl = New Collect
    
    ' 학생 이름
    rowColl.Add rg.Cells(i, 1).Value
    
    ' 학과 
    rowColl.Add rg.Cells(i, 2).Value
    
    ' 기타 등등 추가...생략...
    
    ' 컬렉션을 메인 컬렉션에 추가
    coll.Add rowColl
    
End If

상상할 수 있듯이 위 코드는 컬럼이 추가 될때 마다 수정되고 매우 지저분해 집니다.

다행히도 우리에게는 앞에서 배운 클래스 모듈이 있습니다. 배열이나 컬렉션을 이용해 인덱스로 복잡하게 접근하는것 보다 각 레코드에 의미 있는 이름을 부여하고 코드로 관리할 수 있습니다. 

' clsStudent class module
Private m_sName As String       ' 이름
Private m_sDepartment As String ' 학과
Private m_lScore As Long        ' 점수
Private m_lNumber As Long       ' 학번

' Properties
Public Property Get Name() As String
    Name = m_sName
End Property
Public Property Let Name(ByVal sName As String)
    m_sName = sName
End Property

' 기타 등등..

레코드를 추가 할 때마다 다음과 같이 간단하게 처리할 수 있습니다. 아래와 같은 방식은 코드를 훨씬 더 읽기 쉽게하며 각 항목이 무엇에 사용되는지 명확하게 합니다.

Dim oStudent As clsStudent

' 새 학생정보 생성
Set oStudent = New clsStudent

' 상세 정보 추가
oStudent.Name = rg.Cells(i, 1)
oStudent.Department = rg.Cells(i, 2)
oStudent.Score = rg.Cells(i, 3)
oStudent.Number = rg.Cells(i, 4)

' 학생 정보 추가
coll.Add oStudent

VBA의 Cell과 Range에 접근하는 방법은 [VBA] Range와 Cell 완벽 가이드를 참고 부탁 드립니다.

이제 이 데이터를 사용하여 보고서를 만들고 파일에 쓰는 작업들을 쉽게할 수 있습니다.

Sub PrintStudent(coll As Collection)
    
    Dim oStudent As clsStudent

    For Each oStudent In coll
        Debug.Print oStudent.Name, oAlbum.Department ... 생략 ....
    Next
    
End Sub

클래스 모듈 예제 2

이번 섹션에서는 클래스 객체를 사용할 때 몇 가지 멋진 트릭을 살펴 보겠습니다. 아래와 같은 제품 목록이 있다고 상상해 보십시오.

제품마다 필요한 필드가 다르기 때문에 각 제품 타입에 따라 다른 클래스 모듈을 사용하도록 하겠습니다. 책을 위한 클래스 모듈과 영화를 위한 별도의 클래스 모듈 타입을 추가하도록 하겠습니다. 먼저 클래스 모듈을 생성합니다.

  • 책 클래스 모듈
' 클래스 모듈 - clsBook 
' 멤버 변수 
Private m_Title As  String 
Private m_Year As  Long

' 속성 
Property Get ItemType() As  String 
    ItemType = "Book" 
End  Property

Property  Get Title() As String
    Title = m_Title
End  Property

Property  Get Year() As  Long
    Year = m_Year
End Property

' 메서드 
Public  Sub Init(rg As Range)
    m_Title = rg.Cells(1, 2)      ' Gone Girl
    m_Year = CLng(rg.Cells(1, 4)) ' 2012
End Sub

Public Sub PrintToImmediate()
    Debug.Print ItemType, m_Title, m_Year
End  Sub
  • 영화 클래스 모듈
' CLASS MODULE - clsFilm
' 멤버 변수
Private m_Title As String
Private m_Year As Long

' 속성
Property Get ItemType() As String
    ItemType = "Film"
End Property

Property Get Title() As String
    Title = m_Title
End Property

Property Get Year() As Long
    Year = m_Year
End Property

' 메서드
Sub Init(rg As Range)
    m_Title = rg.Cells(1, 2)      'The Force Awakens
    m_Year = CLng(rg.Cells(1, 5)) '2015
End Sub

Public Sub PrintToImmediate()
    Debug.Print ItemType, m_Title, m_Year
End Sub

위 두 코드 샘플에서 유일한 차이점은 Init Sub에서 인자로 넘어온 Range의 어떤 셀을 읽느냐 입니다.

각 레코드를 읽을 때 우리는 그것이 책인지 영화인지 판단해야 합니다. 그리고 적절한 클래스의 객체를 생성합니다. 예를 들어 각 타입에 따른 변수를 생성한다고 가정해 보겠습니다.

' 각 타입 마다 하나의 변수가 필요함
Dim oBook As clsBook
Dim oFilm As clsFilm

' 책 타입
Set oBook = New clsBook

' 영화 타입
Set oFilm = New clsFilm

만일 책과 영화 외에도 다른 타입의 레코드들이 있다면 위 코드는 매우 지저분해질 것입니다. 여기서 다행인것은 우리는 하나의 변수로 위 문제를 해결할 수 있다는 것입니다.

VBA에서는 변수를 Variant 타입으로 선언할 수 있습니다. Variant 타입의 변수는 변수가 사용될 때 변수의 타입을 결정할 수 있습니다.

이것은 객체를 다룰 때 매우 유용하며 우리는 하나의 변수로 위의 문제 - 타입이 늘어날 수록 변수도 늘어나는 -  를 해결할 수 있습니다.

' 앞의 예제와는 달리 하나의 변수만 선언 합니다.
Dim oItem As Variant

' 책 타입의 객에 입니다.
Set oItem = New clsBook

' 같은 변수지만 이번엔 영화 타입의 객체 입니다.
Set oItem = New clsFilm

 

그리고 Variant 타입을 사용하면 각 클래스 모듈에 동일한 이름과 인자를 가진 프로시져가 있는 경우 같은 변수를 사용하여 동일한 방식으로 호출할 수 있습니다.

예를 들어 clsBook 클래스는 InitBook이라는 프로시져가 있고, clsFilm에는 InitFilm이라는 프로시져가 있다고 상상해 보십시오. 이런 경우 우리는 아래와 같은 코드로 타입별로 프로시져를 구분하여 호출해야만할 것입니다.

If Type = "Book" Then
    oItem.InitBook
ElseIf Type = "Film" Then
    oItem.InitFilm

하지만 앞에서 우리가 만들었던 예제 처럼 두 클래스 모두 동일한 Init이라는 프로시져를 가지고 있다면 If/ElseIf 구문 없이 다음과 같이 작성 가능합니다.

' 아래 코드는 oItem의 타입과 상관 없이 Init 프로시져를 호출 합니다
oItem.Init

이제 적절한 객체를 생성하는 함수를 만들수 있습니다. 객체 지향 프로그래밍에는 클래스 팩토리(class factory)라는 방식을 이용하여 주어진 타입에 따라 적절한 클래스 객체를 생성할 수 있습니다.

우리는 앞의 '생성자 매개변수'  섹션에서 Initialize 이벤트 프로시져가 매개변수를 사용하지 않는다는 것을 보았습니다. 그리고 이 문제를 해결하기 위해 클래스 팩토리라는 것을 만들어 사용했습니다.

우리는 클래스 팩토리를 응용하여 타입 별로 적절한 클래스 객체를 생성하는 클래스 팩토리 프로시져를 만들어 보도록하겠습니다. 타입은 엑셀 시트의 맨 첫번째 열의 값('Book', 'Film')을 기준으로 하도록 하겠습니다.

Function ClassFactory(rg As Range) As Variant

    ' 첫번째 열의 제품 타입 얻기
    Dim sType As String
    sType = rg.Cells(1, 1)

    ' 타입에 따라 객체 생성하기
    Dim oItem As Variant
    Select Case sType
    
        Case "Book":
            Set oItem = New clsBook
        Case "Film":
            Set oItem = New clsFilm
        Case Else
            MsgBox "Invalid type"
    
    End Select
    
    ' 타입에 따라 컬럼의 값 파싱하여 객체 초기화 하기
    oItem.Init rg
    
    ' 상품 객체 리턴하기
    Set ClassFactory = oItem
        
End Function

다음은 ClassFactory를 사용하는 코드 입니다.

Sub ReadProducts()
    
    ' 컬렉션 객체 생성
    Dim coll As New Collection
    Dim product As Variant
    
    Dim rg As Range

    ' 워크시트로 부터 제품 정보 읽기
    Dim i As Long
    For i = 1 To 2
        Set rg = Sheet1.Range("A" & i & ":E" & i)
        
        // 타입에 따른 객체 생성
        Set product = ClassFactory(rg)
        coll.Add product
    Next

    ' 각 제품의 상세 정보 출력하기
    PrintCollection coll

End Sub

우리는  variant 객체를 이용하여 내용을 출력할 수도 있습니다. 두 객체에 동일한 이름과 매개변수(eg. PrintToImmediate)를 가지는 Sub 프로시져가 있다면 Variant 타입을 이용하여 호출할 수 있습니다.

Public Sub PrintCollection(ByRef coll As Collection)
    
    Dim v As Variant
    For Each v In coll
        ' 제품 정보 출력
        v.PrintToImmediate
    Next
    
End Sub

마치며

이번 포스트에서는 VBA 의 클래스 모듈에 대해 살펴 보는 시간을 가졌습니다. 클래스는 특정 포멧을 가지는 레코드를 코드로 표현하고 관리하기 쉽게 도와주며 위에서 언급된 방법 외에도 다양한 사용처가 있습니다. 우선은 간단한 클래스 모듈을 사용하여 엑셀 시트의 값을 읽고 저장하는것 부터 시작하여 클래스에 익숙해지고 좀 더 복잡한 클래스 사용방법에 대해 공부해 나가는 것이 좋습니다.

이상 오늘의 포스팅을 마칩니다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!