이 포스트는 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 키워드를 이용한 프로퍼티는 변수 처럼 값을 할당할 수 있음을 알 수 있습니다.
프로퍼티 요약
- 프로퍼티는 호출자에게 세부 구현 내용을 숨깁니다.
- 프로퍼티를 사용하면 프로시져를 변수와 동일한 방식으로 사용할 수 있습니다.
프로퍼티 타입
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 프로퍼티를 사용하는 방법을 보여 줍니다.
- TestLetSet Sub는 컬렉션을 만들고 값들을 추가합니다.
- 그런 다음 Set 프로퍼티를 사용하여 clsCurrency 클래스 개체에 추가합니다.
- 그런 다음 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 의 클래스 모듈에 대해 살펴 보는 시간을 가졌습니다. 클래스는 특정 포멧을 가지는 레코드를 코드로 표현하고 관리하기 쉽게 도와주며 위에서 언급된 방법 외에도 다양한 사용처가 있습니다. 우선은 간단한 클래스 모듈을 사용하여 엑셀 시트의 값을 읽고 저장하는것 부터 시작하여 클래스에 익숙해지고 좀 더 복잡한 클래스 사용방법에 대해 공부해 나가는 것이 좋습니다.
이상 오늘의 포스팅을 마칩니다.