본문 바로가기

진리는어디에/VBA

[VBA] 객체(Object) 완벽 가이드

이 포스트는 Excel Macro Mastery 사이트의 'VBA Objects - The Ultimate Guide(by Paul Kelly)'의 내용을 다시 정리한 것입니다. 이번 포스트에서는 엑셀 VBA의 객체 대해 다룹니다.

Quick Guide

Task Examples
객체의 선언과 생성 Dim coll As New Collection
Dim o As New Class1
객체의 선언 Dim coll As Collection
Dim o As Class1
생성과 실행 Set coll = New Collection
Set o = New Class1
엑셀 객체에 할당 Dim wk As Workbook
Set wk = Workbooks("book1.xlsx")
CreateObject 를 이용해 생성 Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
기존 변수에 할당 Dim coll1 As New Collection
Dim coll2 As Collection
Set coll2 = coll1
함수에서 객체 리턴 Function GetCollection() As Collection

    Dim coll As New Collection
    Set GetCollection = coll

End Function
함수로 부터 객체 리턴 받기 Dim coll As Collection
Set coll = GetCollection

들어가며

만일 여러분이 VBA를 배우는데 진심이라면 VBA 객체를 이해하는 것은 매우 중요합니다. 사실 객체를 사용하는것은 어렵지 않으며 오히려 VBA 프로그래밍을 더 간결하고 쉽게 만들어 줍니다.

이 포스트에서는 VBA객체를 사용하는 훌륭한 방법들을 다룹니다. 콜렉션(Collection), 워크북(Workbook), 워크시트(Worksheet)와 같은 객체들이 복잡한 프로그램을 간단히 만들어 주고, 여러분의 작업시간과 노력을 줄여 주는지 살펴 볼것입니다.

VBA 객체(Obejct)란 무엇인가요?

객체가 무엇인지 이해하기 위해, 우리는 먼저 변수에 대해 이해해야 합니다. 아마 여러분은 String, Integer, Double, Date와 같은 VBA의 기본 타입을 알고 계실 것입니다. 우리는 이런한 데이터 타입들을 변수를 생성함으로써 사용 할 수 있습니다.

Dim Score As Long, Price As Double
Dim Firstname As String, Startdate As Date

Score = 45
Price = 24.55
Firstname = "John"
Startdate = #12/12/2016#

기본 VBA 변수들은 어플리케이션이 동작하는 동안 값을 저장한다는 딱한가지 목적밖에 없습니다. 우리는 이 변수를 이용하여 값을 저장하거나 저장된 값을 읽거나 할 수 있습니다.

Dim Marks As Long

' 변수에 값을 저장
Marks = 90
Marks = 34 + 44
Marks = Range("A1")

' 변수에 저장된 값을 읽기
Range("B2") = Marks
Debug.Print Marks

VBA에는 아이템들의 그룹을 저장하는데 사용하는 콜렉션(Collection)이라는 것이 있습니다. 다음 코드는 VBA에서 콜렉션을 사용하는 예입니다.

Sub UseCollection()
    
    Dim collFruit As New Collection
    
    ' Add item to the collection
    collFruit.Add "Apple"
    collFruit.Add "Pear"
    
    ' Get the number of items in the collection
    Dim lTotal As Long
    lTotal = collFruit.Count    
   
End Sub

콜렉션은 객체의 좋은 예입니다. 콜렉션은 단순 변수처럼 데이터를 저장하고 읽는것 이상의 역할을 가지고 있습니다. 위 예에서 처럼 Add 메소드를 이용해 아이템을 추가할 수도 있고 Count 프로퍼티를 이용해 현재 추가된 아이템의 개수를 리턴 받을 수도 있습니다.

VBA 객체의 정의
객체는 데이터와 프로시저(함수와 서브루틴)의 그룹이다. 프로시저는 객체 데이터에 일련의 작업을 하기위해 사용된다.

콜렉션(Collection) 객체에서 데이터는 내부에 저장되어 있는 아이템 그룹들을 의미한다. 프로시저는 Add, Remove, Count와 같이 객체 내의 데이터를 대상으로 동작한다.

왜 객체를 사용해야 하나요?

객체를 사용했을 때 주요 이점은 구현 세부 정보를 숨긴다는 것입니다. 위에서 살펴본 VBA 컬렉션을 생각해 봅시다. 콜렉션 객체는 우리가 알지 못하는 복잡한 일을 하고 있습니다. 예를 들어 신규 아이템이 추가되면 메모리를 할당하고, 아이템을 내부 데이터에 저장하고, 아이템 수를 업데이트하는 등의 작업을 수행해야 합니다.

우리는 이것이 어떻게 하고 있는지 알지 못하며 알 필요도 없습니다. 우리가 알아야 할 것은 Add 를 사용할 때 아이템이 추가되고 Remove 는 아이템을 제거하고 Count 는 아이템 수를 제공한다는 것입니다.

객체를 사용하면 애플리케이션을 분리된 블록으로 개발할수 있습니다. 이런 식으로 개발하면 애플리케이션의 특정 객체에서 변경이 발생했을 때, 다른 부분에 영향을 주지 않고 작업할 수 있습니다. 또한 애플리케이션에 항목을 더 쉽게 추가할 수 있습니다. 예를 들어 컬렉션을 모든 VBA 응용 프로그램에 추가할 수 있습니다. 기존 코드의 영향을 받지 않으며 기존 코드에 영향을 주지도 않습니다.

VBA 객체 구성 요소

VBA 객체는 아래 세가지 주요 항목을 가지고 있습니다.

  1. 프로퍼티(Property) - 객체의 속성이라고하며, 값을 설정하거나 검색하는데 사용됩니다.
  2. 메소드(Method) - 객체의 데이터에 대해 작업을 수행하는 함수 또는 서브루틴입니다.
  3. 이벤트(Event) - 객체에 특정 이벤트가 발생 했을 때 호출되는 함수 또는 서브루틴입니다.

비주얼 베이직 에디터에서 개체 브라우저(F2)또는 인텔리센스를 사용하면 각 객체들이 가진 구성을 살펴 볼수 있습니다. 예를 들어 아래 스크린샷은 Worksheet 객체의 멤버들을 보여주고 있습니다.

위 스크린샷에서 'Worksheet' 구성원들의 각 항목들 앞에는 아이콘들이 있는데 이 아이콘들의 의미는 다음과 같습니다.

간간하게 Worksheet의 처음 세 멤버만 살펴 보도록 하겠습니다.

  • 첫번째는 워크시트가 활성화 될때 호출 되는 Activate이벤트 입니다.
  • 두번째는 워크시트를 활성화하는데 사용할 수 있는 Activate 메소드 입니다.
  • 세번째는 어플리케이션(e.g Excel)을 참조 할 때 사용되는 Application 프로퍼티입니다.
' Prints "Microsoft Excel"
Debug.Print Sheet1.Application.Name

' Prints the worksheet name
Debug.Print Sheet1.Name

객체 프로퍼티

이제 객체의 구성 항목들에 대해 자세히 알아보는 시간을 가지도록 하겠습니다. 그중 먼저 객체 프로퍼티에 대해 다뤄보도록 하죠. 프로퍼티는 우리 말로 번역하면 '속성'이라고 합니다. 앞으로 '프로퍼티'와 '속성'이라는 단어가 나오면 같은 것을 가리킨다고 생각하시면 됩니다.

프로퍼티를 사용하면 객체에서 값을 읽거나 객체에 값을 쓸 수 있습니다. 변수를 읽고 쓰는 것과 같은 방식으로 프로퍼티를 읽고 씁니다.

' Set the name 
sheet1.Name = "Accounts"

' Get the name
sName = sheet1.Name

프로퍼티는 읽기 전용일 수 있습니다. 즉, 값을 읽을 수 있지만 값을 업데이트할 수는 없습니다. 예를 들어 VBA Range에서 Address는 읽기 전용 입니다.

' The address property of range
Debug.Print Sheet1.Range("A1").Address

프로퍼티에는 단순 변수뿐 아니라 객체를 설정하고 가져올 수 있습니다. 예를 들어 Worksheet에는 Range 객체를 반환하는 UsedRange속성이 있습니다.

Set rg = Sheet1.UsedRange

객체 메소드

메소드는 Sub 또는 Function입니다. 예를 들어 Add 는 Collection의 메소드입니다.

' Collection Add method
Coll.Add "Apple"

메소드는 개체 데이터와 관련된 어떠한 작업을 수행하는 데 사용됩니다. Collection의 경우 데이터는 우리가 저장하는 아이템 그룹입니다. Add, Remove 및 Count 메서드가 모두 이 데이터와 관련된 일부 작업을 수행하는 것을 볼 수 있습니다.

메소드 사용의 또 다른 예는 Workbook의 SaveAs 메소드 입니다.

Dim wk As Workbook
Set wk = Workbooks.Open "C:\Docs\Accounts.xlsx"
wk.SaveAs "C:\Docs\Accounts_Archived.xlsx"

객체 이벤트

Visual Basic은 이벤트 기반 언어입니다. 이것은 이벤트가 발생할 때 코드가 실행된다는 것입니다. 일반적인 이벤트는 버튼 클릭, 통합 문서 열기, 워크시트 활성화 등입니다.

아래 코드에서는 사용자가 Sheet1을 활성화할 때마다 메시지를 표시합니다. 이 코드는 Sheet1의 워크시트 모듈에 배치해야 합니다.

Private Sub Worksheet_Activate()
    MsgBox "시트1이 활성화되었습니다."
End Sub

지금까지 우리는 VBA 객체의 사용 이유와 구성에 대해서 배웠습니다. 이제 코드에서 객체를 사용하는 방법을 살펴보겠습니다.

VBA 객체 생성하기

VBA에서 코드는 객체를 사용하기 전에 객체를 "생성"해야 합니다. New 키워드 를 사용하여 객체를 생성합니다. 만일 객체를 생성하지 않은 상태에서 사용하려고하면 오류가 발생합니다. 아래 코드를 살펴 보십시오.

Dim coll As Collection 

coll.Add "Apple"

위 코드를 실행하면 3라인 Add에 도달하면 콜렉션 객체가 아직 생성 되지 않았기 때문에 다음과 같은 오류가 발생합니다.

VBA 객체를 만드는 세 단계가 있습니다.

  1. 변수 선언
  2. 새 객체 생성
  3. 객체에 변수를 할당

Dim과 New를 함께 사용하여 이 단계를 한 줄로 수행할 수 있습니다. 또는 한 줄에 변수를 선언한 다음 Set을 사용하여 다른 줄에 개체를 만들고 할당할 수 있습니다. 이 두 가지 기술을 모두 살펴보겠습니다.

Dim을 New와 함께 사용하기

Dim과 New를 함께 사용하면 한 줄에 모두 선언, 생성 및 할당합니다.

Dim coll As New Collection

하지만 위와 같은 코드를 사용하는 것은 많은 유연성을 제공하지 못합니다. 위 코드를 실행하면 항상 정확히 하나의 Collection 객체만을 생성합니다.

다음 섹션에서는 Set 을 살펴보겠습니다 . 이를 통해 조건에 ​​따라 각각의 새 개체에 대한 변수를 선언할 필요 없이 개체를 생성할 수 있습니다.

New와 함께 Set사용하기

한 줄에 개체 변수를 선언한 다음, Set을 사용하여 다른 줄에서 개체를 만들고 할당할 수 있습니다. 이것은 우리에게 많은 유연성을 제공합니다.

아래 코드에서는 Dim 을 사용하여 객체 변수를 선언합니다 . 그런 다음 Set 키워드와 New를 사용하여 생성하고 할당합니다 .

' Declare
Dim coll As Collection
' Create and Assign
Set coll = New Collection

개체 수가 다를 수 있는 경우 이러한 방식으로 Set을 사용합니다. Set을 사용하면 여러 객체를 생성할 수 있습니다. 즉, 필요에 따라 객체를 생성할 수 있습니다. Dim 및 New를 사용하여서는 이 작업을 수행할 수 없습니다. 또한 조건을 사용하여 객체를 생성해야 하는지 여부를 결정할 수 있습니다.

Dim coll As Collection

' Only create collection if cell has data
If Range("A1") <> "" Then
    Set coll = New Collection
End If

Dim과 Set의 미묘한 차이

Set과 함께 New 를 사용하는 것과 Dim과 함께 New 를 사용하는 것 사이에는 약간의 미묘한 차이가 있습니다. Dim과 함께 New를 사용할 때 VBA는 처음 사용할 때까지 개체를 생성하지 않습니다.

다음 코드에서 "Pear"를 추가하는 라인에 도달할 때까지 컬렉션이 생성되지 않습니다.

Dim coll As New Collection

' Collection is created on this line
coll.Add "Pear"

만일 여러분이 Add 라인에 중단점을 설정하고 coll 변수 값을 확인하면 '개체 변수 또는 With문의 변수가 설정되지 않았습니다.' 메시지가 표시됩니다.

Add 라인이 실행 되면 Collection이 생성되고 변수는 이제 하나의 항목이 있는 Collection을 표시합니다. 그 이유는 다음과 같습니다. Dim문 은 다른 VBA 코드와 다릅니다. VBA가 Sub나 Function에 도달하면 먼저 Dim 문을 살펴봅니다. 그리고 Dim 문의 아이템을 기반으로 메모리를 할당합니다. 이 시점에서 코드를 실행하지 않습니다(그래서 Dim 선언만 한 라인에는 브레이크 포인트도 걸리지 않습니다).

객체를 생성하려면 메모리를 할당하는 것 이상이 필요합니다. 실행 중인 코드가 포함될 수 있습니다. 따라서 VBA는 개체를 생성하기 전에 Sub의 코드가 실행될 때까지 기다려야 합니다.

Set을 New와 함께 사용하는 것은 Dim을 New와 함께 사용하는 것과는 다릅니다. Set 라인은 코드가 실행 중일 때 VBA에서 사용하므로 VBA는 Set 및 New를 사용하는 즉시 객체를 생성합니다.

Dim coll As Collection

' Collection is created on this line
Set coll = New Collection

coll.Add "Pear"

New를 사용할 때 염두에 두어야 할 또 다른 미묘함이 있습니다. 객체 변수를 Nothing으로 설정 한 다음 다시 사용하면 VBA가 자동으로 새 개체를 만듭니다.

Sub EmptyColl2()
 
    ' Create collection and add items
    Dim coll As New Collection
 
    ' add items here
    coll.Add "Apple"
 
    ' Empty collection
    Set coll = Nothing
 
    ' VBA automatically creates a new object
    coll.Add "Pear"
 
End Sub

만일 Set을 New와 함께 사용하여 컬렉션을 만든 경우 Add "Pear" 라인에서 오류가 발생합니다.

'New'가 필요하지 않은 경우

일부 객체에서는 New 키워드를 사용하지 않는 것을 보셨을 겁니다.

Dim sh As Worksheet
Set sh = ThisWorkbook.Worksheets( "Sheet1" )

Dim wk As Workbook
Set wk = Workbooks.Open( "C:\Docs\Accounts.xlsx" )

통합 문서가 열리거나 생성되면 VBA는 자동으로 통합 문서에 대한 VBA 개체를 생성합니다. 또한 해당 통합 문서의 각 워크시트에 대한 워크시트 객체를 만듭니다.

반대로 통합 문서를 닫으면 VBA가 연결된 VBA 개체를 자동으로 삭제합니다. 이것은 좋은 소식입니다. VBA는 우리를 위해 모든 일을 대신 하고 있습니다. 따라서 Workbooks.Open 을 사용 하면 VBA가 파일을 열고 통합 문서에 대한 통합 문서 개체를 만듭니다.

기억해야 할 중요한 포인트는 각 Workbook에 대해서는 각각 하나의 객체만 있다는 것입니다. 이것은 Workbook을 참조하기 위해 다른 변수를 사용하는 경우에도 모두 동일한 객체를 참조한다는 뜻입니다.

Dim wk1 As Workbook
Set wk1 = Workbooks.Open("C:\Docs\Accounts.xlsx")

Dim wk2 As Workbook
Set wk2 = Workbooks("Accounts.xlsx")

Dim wk3 As Workbook
Set wk3 = wk2

CreateObject 사용하기

Excel VBA에 포함되지 않은 몇 가지 매우 유용한 라이브러리가 있습니다. 이 라이브러리는 Dictionary, 데이터베이스 rorcp, Outlook VBA 객체, Word VBA 객체 등이 포함됩니다.

이 라이브러리는 COM 인터페이스를 사용하여 작성되었습니다. COM의 장점은 여러분의 프로젝트에서 이러한 라이브러리를 쉽게 사용할 수 있다는 것입니다.

만일 우리가 라이브러리에 대한 참조를 추가하면, 다른 일반적인 객체들과 동일한 방법으로 라이브러리의 객체를 생성할 수 있습니다.

' 도구 > 참조 선택
' 사용가능한 참조에서 "Microsoft Scripting Runtime"
Dim dict As New Scripting.Dictionary

만일 라이브러리에 대한 참조를 사용하지 않는 경우 CreateObject를 이용하여 런타임에 객체를 생성 할 수 있습니다.

Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")

VAB 객체 할당

Let 키워드는 기본 변수나 프로퍼티에 값을 할당 할 때 사용합니다.

Dim sText As String, lValue As Long

Let sText = "Hello World"
Let lValue = 7

' 둘다 같은 작업을 수행합니다
sheet1.Name = "Data"
Let sheet1.Name = "Data"

Let키워드는 선택사항이기 때문에 대부분 코드에서 직접적으로 사용하지 않습니다. 하지만 Let 키워드가 무엇을 위해 필요한지 아는 것은 중요합니다.

sText = "Hello World"
lValue = 7

하지만 객체 변수에 객체를 할당할 때는 Let대신 Set 키워드를 사용합니다. "객체 변수"란 string, long, double등 과 같이 기본 변수가 아닌 모든 변수를 의미 합니다.

' wk 는 객체 변수
Dim wk As Worksheet
Set wk = ThisWorkbook.Worksheets(1)

' coll1 는 객체 변수
Dim coll1 As New Collection
coll1.Add "Apple"

' coll2 는 객체 변수
Dim coll2 As Collection
Set coll2 = coll1

Let 키워드와 달리 Set 키워드 사용은 필수 입니다. 객체 변수를 할당 할 때 Set 키워드 사용을 잊어 버리면 아래 오류가 발생합니다.

coll2 = coll1

얼핏 보기에 Let과 Set은 같은 일을하는것 처럼 보이지만 실제로는 아래와 같은 차이가 있습니다.

  • Let : 값을 저장합니다.
  • Set : 객체의 주소를 저장합니다.

'값'과 '주소'에 대해 보다 더 자세한 사항은 다음 섹션에 설명 됩니다.

VBA 객체와 메모리

New와 Set이 하는 일을 이해하려면 변수가 메모리에서 어떻게 표현되는지를 먼저 이해해야 합니다. 변수를 선언하면 VBA는 메모리에 변수에 대한 공간을 만듭니다. 그 공간은 '메모리 위치한 엑셀의 셀' 정도로 생각하면 됩니다.

Dim X As long, Y As Long

우리가 값을 변수에 할당하게 되면 VBA는 새로운 값을 해당 공간(메모리에 있는 셀 같은 곳)에 저장합니다.

X = 25
Y = 12

우리는 앞에서 아래와 같은 코드를 봤습니다.

Dim coll As New Collection

이 위 코드는 메모리에 객체를 생성합니다. 하지만 객체를 변수에 저장하지 않습니다. 이게 무슨 말인가 싶습니다. 객체를 만드는데 변수에 저장하지 않으면 도데체 어디에 저장한다는 말입니까.

VBA는 메모리 어딘가에 객체를 만들고 변수에는 객체가 있는 곳의 주소를 저장합니다. 만일 프로그래밍에 익숙하신 분이라면 이것이 포인터라는 것을 이미 알고 계실 겁니다. 만일 프로그래밍에 익숙하지 않다고하더라도 당황하실 필요 없습니다.

중요한것은 포인터 같은 용어가 아니라 메모리 어딘가에 객체가 생성되어 있고, 변수에는 그 곳을 가리키는 주소를 저장하고 있다는 것뿐입니다.

사실 VBA는 이것을 매우 우아하게 처리하고 있기 때문에 우리는 객체를 가리키는 변수가 주소를 저장하는지 객체를 저장하는지 상관 없이 모두 똑같이 보일 수 있습니다. 하지만 이 차이를 이해하면 Set 실제로 하는 일에 대해 훨씬 수월해집니다.

Set의 작동 방식

아래 코드를 살펴 보도록하겠습니다.

Dim coll1 As New Collection
Dim coll2 As Collection

Set coll2 = coll1

여기에는 컬렉션이 하나만 생성되었습니다. 따라서 coll1과 coll2는 동일한 컬렉션 객체를 참조합니다. 이 코드에서 coll1은 새로 생성된 컬렉션의 주소를 가집니다.

Set을 사용할 때 coll1에서 coll2로 객체의 주소를 복사합니다. 따라서 이제 둘다 메모리에서 동일한 컬렉션을 가리키고 있습니다.

앞에서 우리는 Workbook 변수에 대해 살펴 보았습니다(그리고 Workbook을 참조하기 위해 몇 개의 변수를 사용하더라도 모두 동일한 객체를 가리킨다고 배웠습니다). 이 코드를 다시 살펴 보도록 하겠습니다.

Dim wk1 As Workbook
Set wk1 = Workbooks.Open("C:\Docs\Accounts.xlsx")

Dim wk2 As Workbook
Set wk2 = Workbooks("Accounts.xlsx")

Dim wk3 As Workbook
Set wk3 = Workbooks(2)

Accounts.xlsx 워크북(한글로 통합 문서)를 열면 VBA는 워크북에 대한 객체를 만듭니다. 위 코드에서 워크북 변수를 할당하면 VBA는 워크북 객체의 '주소'를 변수에 할당합니다. 위 코드에서 세 개의 변수는 모두 동일한 워크북의 객체를 참조 합니다.

만일 아래와 같은 코드를 사용한다면..

wk1.SaveAs "C:\Temp\NewName.xlsx"

VBA는 wk1의 주소를 사용하여 어떠한 워크북이 사용될지 결정합니다. 이 작업을 우아하게(우리가 몰라도 되게끔) 수행하므로 워크북 변수를 사용할 때 객체를 직접 저장하는 것처럼 보입니다.

이 섹션에서 배운 내용을 요약하면 아래와 같습니다.

  • Let : 값을 기본 변수에 씁니다.
  • Set : 주소를 변수에 씁니다.

객체와 프로시저

VBA에서는 Function과 Sub를 프로시저라고 합니다. 우리가 객체를 프로시저에게 인자로써 넘겨줄 때 객체 자체가 아닌 객체를 가리키는 주소를 넘기게 됩니다. Function(Sub는 리턴이 없음)에서 객체를 리턴할 때 역시 객체의 주소만 리턴 됩니다.

아래 코드를 살펴 보도록 하겠습니다. 아래 코드는 하나의 컬렉션 객체만 존재하며, 함수와 주고 받을 때는 주소만 왔다 갔다합니다.

Sub TestProc()
    
    ' Create collection
    Dim coll1 As New Collection
    coll1.Add "Apple"
    coll1.Add "Orange"

    Dim coll2 As Collection
    ' UseCollection passes address back to coll2
    Set coll2 = UseCollection(coll1)

End Sub

' Address of collection passed to function
Function UseCollection(coll As Collection) As Collection
    Set UseCollection = coll
End Function

ByRef와 ByVal 사용하기

프로시저에 객체를 가리키는 변수가 아닌 일반 변수를 전달 할 때 ByRef 또는 ByVal을을 사용하여 전달할 수 있습니다.

ByRef는 변수의 주소를 전달한다는 의미입니다. 프로시저에 변수가 변경되면 원본도 변경 됩니다. ByVal은 변수의 복사본을 생성하여 전달한다는 의미 입니다. 프로시저에서 변수가 변경 된다고 하더라도 원본은 변경되지 않습니다.

' 값으로 전달
Sub PassByVal(ByVal val As Long)

' 참조로 전달
Sub PassByRef(ByRef val As Long)
Sub PassByRef(val As Long)

대부분의 경우 ByVal을 사용하는 것은 프로시저에서 변수가 실수로 변경되는 것을 방지하기 위해 사용하면 좋습니다. 하지만 컬렉션 객체를 프로시저에 전달할 때는 항상 컬렉션 객체의 주소를 전달합니다.

이것이 의미하는 바는 우리가 프로시저에서 컬렉션 객체를 변경하면 이는 프로시저 외부에서도 변경된다는 의미입니다. 예를 들어 아래 코드에는 컬렉션을 변경하는 두 프로시저가 있습니다. 하나는 ByRef를 사용하고 다른 하나는 ByVal을 사용합니다. 두 경우 모두 TestProcs() 프로시저로 돌아갔을 때 컬렉션이 변경된 것을 확인할 수 있습니다.

Sub TestProcs()
    Dim c As New Collection
    c.Add "Apple"
    
    PassByVal c
    ' Prints Pear
    Debug.Print c(1)
    
    PassByRef c
    ' Prints Plum
    Debug.Print c(1)
   
End Sub

' Pass by value
Sub PassByVal(ByVal coll As Collection)
    ' Remove current fruit and add Pear
    coll.Remove (1)
    coll.Add "Pear"
End Sub

' Pass by reference
Sub PassByRef(ByRef coll As Collection)
    ' Remove current fruit and add Plum
    coll.Remove (1)
    coll.Add "Plum"
End Sub

이제 두 번째 예제를 살펴 보도록 하겠습니다. 여기서 우리는 새로운 컬렉션을 가리키도록 객체 변수를 셋팅하고 있습니다. 이 예에서 ByVal 및 ByRef는 다른 결과를 돌려줍니다.

PassByVal Sub에서는 원본 객체 변수의 복사본이 생성 됩니다. 새 컬렉션을 가리키는 것은 이 복사본입니다. 따라서 원래 객체 변수는 아무런 영향을 받지 않습니다.

PassByRef Sub에서는 동일한 객체 변수를 사용하고 있으므로 New Collection을 가리킬 때 원래 객체 변수는 이제 새 컬렉션을 가리킵니다.

Sub TestProcs()

    Dim c As New Collection
    c.Add "Apple"
    
    PassByVal c
    ' Prints Apple as c pointing to same collection
    Debug.Print c(1)
    
    PassByRef c
    ' Prints Plum as c pointing to new Collecton
    Debug.Print c(1)

End Sub

' Pass by value
Sub PassByVal(ByVal coll As Collection)
    Set coll = New Collection
    coll.Add "Orange"
End Sub

' Pass by reference
Sub PassByRef(ByRef coll As Collection)
    Set coll = New Collection
    coll.Add "Plum"
End Sub

VBA가 포인터를 사용하는 이유

우리는 지금까지 VBA에서 객체를 변수에 저장 할 때, 객체 자체를 저장하는 것이 아닌 메모리 어딘가에 위치하고 있는 객체의 주소를 저장한다는 것을 배웠습니다. 여러분은 왜 객체는 일반 변수 처럼 값을 저장하는 것이 아닌 주소를 저장하는지(우리는 앞에서 이것을 포인터라고 부른다고 배웠습니다) 궁금할 것입니다. 간략하게 요약하면 그 이유는 객체의 값을 저장하는것 보다 주소를 저장하는 것이 훨씬 더 효율적이기 때문입니다.

50000개의 항목을 가지고 있는 컬렉션이 있다고 가정해 보겠습니다. 어플리케이션이 실행 중일 때 이 컬렉션의 여러 복사본을 만든다면 어떻게 될까요? 매번 복사 연산마다 엄청난 복사 비용과 복사된 객체들을 모두 유지해야하므로 필요 메모리 또한 만만치 않을 것입니다. 하지만 객체의 주소를 주고 받는다면 위에서 언급된 많은 비용을 줄일 수 있습니다.

이런 이유로 VBA에서는 객체에 대해 값을 직접 저장하는 것이 아닌 주소를 저장하고 또한 주고 받습니다.

간단한 메모리 실험

지금까지 살펴본 내용들을 설명하기 위해 예제 코드를 살펴 보도록 하겠습니다. 아래 코드는

  • 변수의 메모리 주소를 제공하는 VarPtr
  • 객체의 메모리 주소를 제공하는 ObjPtr

를 사용합니다.

메모리 주소는 단순한 long integer 타입이며, 그 값 자체는 그리 중요하지 않습니다. 여기서 흥미롭게 살펴 볼 것은 주소를 비교할때 입니다.

Sub Memory()

    Dim coll1 As New Collection
    Dim coll2 As Collection
    
    Set coll2 = coll1
    
    ' 변수 Coll1 및 Coll2의 주소 가져오기 
    ' 64비트의 경우 Dim addrColl1 As LongPtr, addrColl2 As LongPtr 
    Dim addrColl1 As Long, addrColl2 As Long    
    addrColl1 = VarPtr(coll1)
    addrColl2 = VarPtr(coll2)
    
    Debug.Print "coll1 변수의 주소는 " & addrColl1
    Debug.Print "coll2 변수의 주소는 " & addrColl2
    
    ' 컬렉션의 주소를 가져옵니다. 
    ' 64비트의 경우 Dim addrCollection1 As LongPtr, addrCollection2 As LongPtr
    Dim addrCollection1 As Long, addrCollection2 As Long
    addrCollection1 = ObjPtr(coll1)
    addrCollection2 = ObjPtr(coll2)
    
    Debug.Print "coll1 변수가 가리키는 컬렉션 객체의 주소는 " & addrCollection1
    Debug.Print "coll2 변수가 가리키는 컬렉션 객체의 주소는 " & addrCollection2

End Sub
NOTE : 64비트 버전의 엑셀을 사용하는 경우 Long 대신 LongPtr을 사용하십시오.

코드를 실행하면 아래와 같은 결과를 얻을 수 있습니다(설명을 위해 두번 실행했습니다)

coll1 변수의 주소는 1625541725984
coll2 변수의 주소는 1625541725976
coll1 변수가 가리키는 컬렉션 객체의 주소는 1625539430608
coll2 변수가 가리키는 컬렉션 객체의 주소는 1625539430608

coll1 변수의 주소는 1625541725984
coll2 변수의 주소는 1625541725976
coll1 변수가 가리키는 컬렉션 객체의 주소는 1625539431136
coll2 변수가 가리키는 컬렉션 객체의 주소는 1625539431136

위 결과를 살펴 보면

  • 실행 할 때 마다 주소가 달라집니다.
  • coll1 컬렉션 객체와 coll2 컬렉션 객체의 주소는 항상 같습니다.
  • coll1 변수와 coll2 변수의 주소는 항상 같습니다.

이것은 동일한 컬렉션의 주소를 가진 두개의 다른 변수가 있음을 보여줍니다. 위 결과에서 두 번의 실행 결과 변수의 주소가 동일하게 출력되고 있지만 그렇다고해서 이 주소가 매 실행 마다 동일함을 보장하는 것은 아닙니다.

메모리 정리

변수에 New를 여러번 사용해 객체를 여러개 할당하면 어떻게 될까요? 아래 코드에서 coll 변수에 Set 및 New를 두번 적용해보도록 하겠습니다.

Dim coll As Collection

Set coll = New Collection
coll.Add "Apple"

' 새 컬렉션을 만들고 coll을 가리키도록 셋팅
Set coll = New Collection

위 예제의 3라인, 7라인에서 각각 두개의 새 컬렉션 객체를 만들었습니다. 두번째 컬렉션 객체를 만들 때 coll 변수가 새로운 컬렉션 객체를 참조하도록 만들었습니다. 이제 어떠한 변수도 첫번째 만들어진 컬렉션 객체를 참조하지 않으며, 해당 객체에는 접근할 수 있는 방법이 없습니다.

메모리 할당과 해제를 직접 관리하는 C++과 같은 언어는 위와 같은 상황에선 메모리 누수가 발생합니다. 그러나 VBA에서는 더 이상 사용되지 않는 객체의 메모리는 자동으로 정리됩니다. 이를 '가비지 컬렉션(garbage collection)'이라고 합니다.

정리하면 객체를 참조하는 변수가 없으면 VBA는 메모리에서 객체를 자동으로 삭제합니다. 위의 코드에서 3라인에서 생성된 "Apple"을 가지고 있는 컬렉션 객체는 7라인에서 coll이 새로운 객체를 가리키게 되면 삭제 됩니다.

메모리 정리 예

메모리가 정리되는 과정을 직접 보고 싶다면 다음에 나오는 대로 따라해 보세요.

clsCustomer라는 클래스 모듈을 만들고 아래 코드를 추가하십시오.

Public Firstname As String

Private Sub Class_Terminate()
    MsgBox "Customer " & Firstname & " is being deleted."
End Sub
NOTE : 클래스 모듈을 만들기 위해서는 '프로젝트 창에서 마우스 우클릭 > 삽입 > 클래스 모듈'을 선택한 후, 클래스 모듈의 속성창에서 클래스의 이름을 변경 해주면 됩니다.

 

Class_Terminate 는 객체가 삭제될 때 호출 되는 이벤트 프로시져입니다. 이 이벤트에 메시지 상자를 출력하는 코드를 배치하면 객체의 삭제가 정확히 언제 일어 났는지 알수 있습니다.

디버그 모드(F5)에서 F8을 사용하여 아래 코드를 단계적으로 실행 시켜 봅시다. 8라인에서 oCust 변수에 새로운 객체를 할당하면 Jack이 삭제 되었다는 메시지가 표시 됩니다. 그리고 프로시져가 종료하면 Jill이 삭제되었다는 메시지가 표시 될겁니다.

Sub TestCleanUp()
    
    Dim oCust As New clsCustomer
    oCust.Firstname = "Jack"
    
    ' Jack will be deleted after this line
    Set oCust = New clsCustomer
    oCust.Firstname = "Jill"
    
End Sub

VBA는 객체가 스코프를 벗어나면 자동으로 객체를 삭제합니다. 측, Sub나 Function에서 선언하면 Sub/Function이 종료 될때 해당 스코프가 종료되므로 이것을 스코프에서 벗어났다고 이야기 합니다.

객체에 Nothing 설정하기

가끔 VBA 코드를 보면 Nothing이라고 셋팅 되는 변수들을 보실수 있습니다.

Set coll = Nothing

 

 Nothing으로 할당 된 변수는 아무런 값을 가지고 있지 않다는 것을 의미합니다. 이는 명시적으로 변수에 아무런 값이 할당 되어있지 않음을 표시하거나, 가리키고 있는 객체를 즉시 삭제해야 하는 경우 사용합니다(하지만 앞에서 살펴 보았듯이 스코프를 벗어나는 순간 자동으로 삭제 되므로 대부분의 경우 이럴 필요는 없습니다).

메모리 요약

이 섹션에서 배운 내용을 정리해보도록 하겠습니다.

  • New 키워드를 사용하면 메모리에 새 객체가 생성 됩니다.
  • 객체 변수에는 객체의 메모리 주소가 저장 됩니다.
  • Set을 사용하면 객체 변수의 주소가 변경 됩니다.
  • 객체가 더 이상 참조되지 않으면 VBA에서 자동으로 삭제합니다. 이를 가비지 컬렉션이라고 합니다.
  • 대부분의 경우 객체를 Nothing으로 설정할 필요는 없습니다.

Set이 유용한 이유

Set이 유용하게 쓰이는 두가지 예를 살펴 보도록 하겠습니다.

먼저 clsCustomer라는 매우 간단한 클래스 모듈을 만들고 아래 코드를 추가합니다.

Public Firstname As String
Public Surname As String

Set 예제 1

첫 번째 시나리오는 워크시트의 고객 목록을 읽고 있습니다. 고객의 수는 10명에서 1000명까지 다양합니다. 분명히 1000개의 객체를 일일이 선언하는 것은 보통일이 아닙니다. 이것은 코드의 낭비일 뿐만 아니라 최대 1000명의 고객만을 처리할 수 있다는 의미이기도 합니다.

' 절대..절대 이런식으로 코드를 작성하지 마시오!!
Dim oCustomer1 As New clsCustomer
Dim oCustomer2 As New clsCustomer
' .
' .
' .
Dim oCustomer1000 As New clsCustomer

우리가 가장 먼저 할 일은 데이터가 있는 행의 수를 얻는 것입니다. 그런 다음 각 행에 대한 고객 객체를 만들고 데이터로 채웁니다. 그런 다음 이 고객 객체를 컬렉션에 추가합니다.

Sub ReadCustomerData()

    ' 우리는 항상 하나의 컬렉션을 가질 것입니다 
    Dim coll As New Collection
    
    ' 고객 수는 시트를 읽을 때마다 다를 수 있습니다. 
    Dim lLastRow As Long
    lLastRow = Sheet1.Range("A" & Sheet1.Rows.Count).End(xlUp).Row
    
    Dim oCustomer As clsCustomer
    Dim i As Long
    ' 전체 고객 목록 읽기 
    For i = 1 To lLastRow
    
        ' ' 각 행에 대해 새 clsCustomer를 만듭니다.
        Set oCustomer = New clsCustomer
        
        ' 데이터 추가
        oCustomer.Firstname = Sheet1.Range("A" & i)
        oCustomer.Surname = Sheet1.Range("B" & i)
        
        '  컬렉션에 clsCustomer 개체 추가
        coll.Add oCustomer
        
    Next i

End Sub

Set을 사용할 때 마다 oCustomer에게 최신 객체를 "가리키도록" 할당합니다.그런 다음 컬렉션에 고객을 추가합니다. 여기서 일어나는 일은 VBA가 겍체 변수의 복사본(객체의 복사본이 아님)을 만들어 컬렉션에 배치한다는 것입니다.

우리는 객체 변수를 하나만 선언했고 Set을 이용해 재활용할 수 있습니다. 코드가 간단해진것은 물론 고객의 숫자와 상관 없이 처리 가능해졌습니다.

Set 예제 2

Set을 사용하는 두 번째 예를 살펴 보도록 하겠습니다. 고정된 수의 고객이 있지만 이름이 'B'로 시작하는 고객만 읽으려고 한다고 상상해 보십시오. 유효한 객체를 찾을때만 clsCustomer 클래스의 객체를 생성합니다.

Sub ReadCustomerB()

    ' 우리는 항상 하나의 컬렉션을 가질 것입니다 
    Dim coll As New Collection
   
    Dim oCustomer As clsCustomer, sFirstname As String
    Dim i As Long
    
    ' 전에 고객 목록 읽기 
    For i = 1 To 100
    
        sFirstname = Sheet1.Range("A" & i)
        
        ' 이름이 B로 시작하는 경우에만 고객 생성
        If Left(sFirstname, 1) = "B" Then
            ' 새 clsCustomer 생성 
            Set oCustomer = New clsCustomer
            
            ' 데이터 추가
            oCustomer.Firstname = sFirstname
            oCustomer.Surname = Sheet1.Range("B" & i)
            
            ' 컬렉션에 추가
            coll.Add oCustomer
        End If
        
    Next i

End Sub

B로 시작하는 고객의 수는 중요하지 않습니다. 이 코드는 각 고객에 대해 정확히 하나의 객체를 생성합니다.

마치며

이것으로 VBA 객체에 대한 포스팅을 마칩니다. 다음 포스트에서는 이번 포스트에서 잠깐 다루었던 Class Module을 사용하여 VBA에서 자신만의 커스텀 객체를 만드는 방법을 살펴 보도록 하겠습니다.

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

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