프날 오토핫키 강좌  v2

⚠ 이 강좌는 오토핫키 v2를 다룹니다

지금 보시는 강좌는 과거 오랜 시간동안 알려진 오토핫키(v1.1)의 차세대 버전인 오토핫키 v2를 다루고 있습니다.
만약 구버전인 '오토핫키 v1.1'의 강좌를 찾으신다면 프날 오토핫키 강좌(https://pnal.kr)를 봐주시면 되지만, 새로 오토핫키를 배우신다면 v2 버전을 배우시는 것을 강력히 추천드립니다.

67. 배열과 맵, 객체 이모저모


낯선 개념을 익혔으니 조금 쉬어가 보겠습니다. 이 강의 내용을 반드시 익힐 필요는 없지만, 중요하지 않다고는 할 수 없습니다. 다만 프로그래밍 경험이 적은 여러분은 이해하기 어려운 내용이 일부 섞여있습니다. 어디까지나 조금 깊은 내용을 다루는 만큼, 완벽히 이해할 필요가 없이 '일단은' 휙휙 읽어도 됩니다. 키보드에서도 잠시 손을 떼보자고요.

물론, 쉬운 부분은 키보드로 타이핑 해보면서 배워도 됩니다.

배열의 배열

오토핫키에선 배열 안의 요소로 배열을 넣을 수 있습니다. 그 뿐만 아니라 모든 객체를 넣을 수 있습니다. 맵도 넣을 수 있고, 어떤 인스턴스 또한 배열 안에 들어갈 수 있습니다. 비슷한 원리로, 맵 안에도 또다른 배열/맵/모든 객체가 들어갈 수 있습니다.

즉, 아래와 같이 outerArray 배열의 요소로 innerArray 배열을 할당할 수 있습니다.

...outerArray[1] := innerArray
outerArray의 첫 번째 요소로 innerArray를 통째로 할당해준 모습

이 경우 아래와 같이 첨자 연산자를 연달아 써서 innerArray의 요소에 접근할 수 있습니다.

...MsgBox(outerArray[1][n])
innerArray의 n번째 요소에 접근

outerArray의 1번 요소의 n번째 요소에 접근하기 위해 outerArray[1][n]이라고 적어주었습니다. outerArray의 1번 요소는 결국 innerArray이기 때문에, 이는 innerArray[n]에 접근한 것과 같습니다. 예시에서는 이미 존재하는 innerArray를 할당했지만, 실제로는 배열 리터럴 [element1, element2, ...]이나 배열 생성자 Array(element1, element2, ...)을 이용하여 새 배열을 할당할 수도 있습니다.

일반적인 언어에선 '다차원 배열'이라고 부르는 기능을 오토핫키에선 이러한 '배열의 배열'로 구현할 수 있습니다.

Tip: 다차원 배열? 배열의 배열?

이하의 내용은 C, C++등의 언어로 코딩 경험이 있어서 기존 배열과의 괴리에서 오는 의문점을 해소하기 위해 쓴 글이니, 오토핫키로 프로그래밍을 접한 분들은 읽지 않고 넘어가셔도 좋습니다.

오토핫키의 배열은 객체의 확장이기 때문에, 사실 엄밀히 말하면 이 내용이 고전적 언어에서의 다차원 배열은 아닙니다. 배열은 원래 자료가 메모리에 선형적으로 저장됩니다. 그래서 고전적인 다차원 배열(특히 2차원의 경우)은 첫번째 요소의 메모리 주소로 다음 행의 요소까지 접근할 수 있습니다. 그러나 오토핫키를 포함한 현대의 언어는 거의 그렇지 않습니다.

이러한 현대적인 방식을 굳이 고전적인 다차원 배열 구현과 구분해서 말하려면 "배열의 배열", 영어로는 Array of array, Nested array 등으로 말합니다.

C++은 고전적인 다차원 배열을 지원하며, Java는 오토핫키처럼 '배열의 배열' 방식의 다차원 배열을 지원합니다. 여러 언어의 다차원 배열 구현 방식을 비교하려면 위키백과의 Comparison of programming languages (array) 문서의 Array system cross-reference list 문단을 살펴보는 것도 좋습니다. 표의 Multidimensional 열을 참고하시면 됩니다.

객체 리터럴

우리가 배열을 표현할 때 [element1, element2, ...]와 같이 각 요소를 대괄호 안에 적어준 것을 배열 리터럴이라고 했습니다. '리터럴'은 '있는 그대로'라는 뜻인데, 이는 배열 리터럴이 그 자체로 배열이라서 붙은 이름입니다. 즉, [element1, element2, ...] 그 자체로 완전한 배열입니다. 다만 변수에 담지 않았을 뿐이지요.

그에 비해 맵 리터럴은 없다고 했습니다. 맵을 '있는 그대로' 표현해주는 방법은 없습니다. 모든 맵은 변수에 담겨야만 표현이 가능합니다. 참고로, 배열 생성자와 맵 생성자(Array(), Map())는 그 자체로 배열이나 맵이 아닙니다. 새 배열과 맵을 만들어주는 구문일 뿐입니다.

그런데, 객체 리터럴은 있습니다. 객체를 있는 그대로 표현하는 방법인데요, 이를 통해 복잡한 클래스를 만들지 않고도 간단한 객체를 즉석에서 쓸 수 있습니다. 객체 리터럴은 중괄호 {}를 이용합니다.

{Field1: Value1, Field2: Value2, ...}
두 개의 필드가 있는 새 객체 리터럴

위 예시의 객체 리터럴은 Field1, Field2 필드를 가지고 있는 객체를 의미합니다. 각 필드엔 Value1, Value2라는 값이 들어있습니다. 필드명은 그대로 적어주시면 되지만, 만약 값이 문자열이면 따옴표 표시를 해주어야 합니다.

어라.. 이거 키-값 쌍 아닌가요?

네, 정확하게 보셨습니다. 필드명을 맵의 '키', 필드의 값을 맵의 '값'이라고 생각해보세요. obj 맵의 Field1 키는 곧 Value1일 것입니다. 실제로 객체 리터럴로 간단하게 키-값 쌍을 이용할 수 있습니다. 그래서 맵 리터럴은 필요 없습니다.

대신, 객체 리터럴을 통해 만든 키-값 쌍은 실제론 맵이 아니라 '필드'와 '필드의 값'으로 구성된 객체이므로, 멤버 접근 연산자 .를 이용해서 각 필드에 접근해야겠죠? 따라서 맵처럼 사용하지 말고 아래와 같이 사용하시면 되겠습니다.

1obj := {X: 120, Y: 300}
2MsgBox(obj.X)
객체 리터럴을 통해 간단한 키-값 쌍 구현

필드와 속성의 차이

61강에서 멤버 변수 = 필드 = 속성이라고 했습니다. 그런데 왜 '필드'와 '속성'이라는 두 개의 용어가 사용될까요?

사실 그 둘은 다릅니다. 우선 필드는 객체가 가지고 있는 변수를 통칭하며, 멤버 변수의 의미와 가장 유사할 것입니다. 한편 객체 지향 프로그래밍의 가장 중요한 개념 중 하나는 정보 은닉 입니다. 쉽게 말하자면 객체 내부가 어떻게 구현되어 있는지 감추라는 뜻입니다. 그런 면에서 필드에 직접 접근하여 값을 사용하는 것은 적절하지 않은 구현입니다.

그래서 똑똑하신 분들이 'Get'과 'Set'이라는 개념을 만드는데, 필드의 값을 가져오거나(Get) 새로 할당(Set)하기 위한 메서드를 만들어서 해당 메서드를 이용하기 시작합니다. 이 메서드를 각각 Getter와 Setter라고 합니다. Getter 메서드 안엔 '필드의 값을 가져오는' 구문을 자유롭게 짜넣을 수 있고, 값을 가져올 때 수행할 동작을 같이 작성해줄 수 있습니다. Setter 메서드 안엔 '필드에 값을 할당하는' 구문을 쓰죠. 역시, 값을 그냥 할당하지 않고 여러 동작을 수행하는 메서드로 만들 수 있습니다.

너무 어려우니까 됐고, 아무튼 가장 느슨한 정의로서 이러한 Getter와 Setter의 집합을 '속성'이라고 합니다. 즉, 원칙대로라면 '속성'은 외부에서 보이지만 '필드'는 보이지 않아야합니다. 개발자는 속성을 통해 필드의 값을 가져오거나 수정합니다. 필드에 직접 접근하지 않습니다. Java가 이 구현을 사용하는 대표적인 언어입니다.

C# 언어에서는 더욱 발전하여 단순한 Getter와 Setter 메서드를 만드는 방법이 아닌, 한 필드의 Getter와 Setter를 묶는 별도의 속성 기능이 있습니다. 그래서 Java 진영과 C# 진영이 생각하는 '속성'은 미묘하게 다를 수 있습니다.

아무튼 오토핫키도 필드와는 다른 별도의 '속성' 개념이 있습니다. 그러나 프로그래밍 입문을 고려한 기초 강좌인 이곳에서는 속성을 만들어보지 않을 것이며, 사실 구현도 다른 언어와 다르기 때문에 강좌하지 않습니다. 되려 혼란만 가중시킬 수 있습니다. 직접 속성을 만드는 방법은 규모가 큰 프로그램을 작성할 때 다시 자세히 찾아보시길 바랍니다.

강좌에서는 지금까지 그래왔듯 객체 내 변수에 직접 접근하고, 그 변수를 필드라고 부르겠습니다. 그러나, 오토핫키에서 기본 제공하는 객체(Array, Map, 추후 배울 Gui 등)의 멤버 변수는 원칙대로 속성 형태를 사용하므로, 필드와는 구분하여 속성이라고 적겠습니다. 다만 헷갈리신다면 지금까지와 같이 그냥 '멤버 변수'나 '필드'와 같다고 여기셔도 현재 단계에서는 무방합니다.

요약: 이 강좌에서의 필드와 속성의 구분

우리는 필드만 만들고 필드를 그대로 사용할 것이지만, 오토핫키에서 기본 제공하는 객체의 것은 속성이라고 적겠습니다. 헷갈린다면, 글에 적힌 '필드'와 '속성'을 유의어 내지 동의어로 생각해도 현재 단계에서는 무방합니다.

예를 들어서 오토핫키에서 제공하는 Array 객체의 Length는 '속성'이라고 적을 것이고, 우리가 직접 만든 다른 객체의 멤버 변수는 '필드'라고 적겠습니다.

생성자

사실 앞서 조금씩 말했던 용어인데, 생성자라는 말을 했습니다. 생성자는 객체가 생성될 때 자동으로 호출되는 메서드입니다. 우리 클래스로부터 새 인스턴스를 만들 때 아래와 같이 적어주었죠?

...instance := ClassName()
instance 인스턴스 생성

ClassName 클래스에는 (우리가 적어주지 않더라도) 클래스 이름과 같은 ClassName() 메서드가 있습니다. 이 메서드는 객체를 만들 때 자동으로 실행되며, 그 안에서 객체에서 사용할 여러 값을 초기화하거나 준비 작업을 할 수 있습니다. 물론 매개변수도 받을 수 있습니다. 그럴 경우 ClassName() 메서드를 ClassName 클래스 안에 새로 정의해주어야 합니다.

다만 프날은 객체 지향 프로그래밍 강좌가 아니며, 프로그래밍을 처음 배우는 여러분을 대상으로 하고 있기 때문에 역시 배우지 않겠습니다. 강좌에 나온 말은 '배열 생성자'와 '맵 생성자' 뿐이므로, 여러분은 아직 이렇게만 이해해주세요.

1. '배열 생성자'는 Array() 구문을, '맵 생성자'는 Map() 구문을 말하는구나!
2. '생성자'는 무언가를 생성하는 구문이구나!


깊은 복사와 얕은 복사

배열, 맵을 포함하여 모든 객체는 그 자체가 변수에 담긴 것이 아닌, 간접적으로 해당 변수에 담겨있습니다. ('참조'를 떠올려보시면 간접적으로 변수에 값이 담길 수도 있다는 점을 이해하실 수 있습니다.)

그렇기 때문에, 아래와 같이 객체를 다른 변수에 대입하면, 우리는 이를 복제라고 인식하기 쉽지만...

...arrayA := arrayB
객체 복제? 아닙니다!

실제로 엄밀히 말하면 arrayB에 배열 그 자체가 담긴 것이 아닌, 실제 배열의 참조가 담겨있기 때문에, arrayA와 arrayB는 똑같은 배열을 가리킵니다. 따라서 둘은 복제된 관계가 아닌, 한 배열을 공유하는 관계입니다. 그렇기 때문에 위의 경우에서 arrayB의 값을 바꾸면 arrayA의 값도 바뀝니다. 이렇게 복사된 관계를 얕은 복사라고 합니다.

반면, 객체의 주소는 놔두고 값만 같은, 완전히 다른 객체로 복사할 수도 있는데, 이를 깊은 복사라고 합니다. 깊은 복사는 아래와 같이 Clone() 메서드로 수행합니다.

...arrayA := arrayB.Clone()
arrayB와 값만 같고 주소가 다른 arrayA

따라서, 객체를 다른 변수에 대입할 땐 깊은 복사와 얕은 복사의 차이를 명확히 알고 원하는 방식을 선택해야합니다.

어째 알아들을 수 없는 내용만 쏟아진 느낌이라도, 그냥 그렇구나 하고 넘어가주세요. 딱 하나, '객체 리터럴' 만은 앞으로 강좌에 한두번씩 나올 수 있으니 이해해주시면 되겠습니다.

다음 강은 배열과 맵과 관련된 프로그래밍 문제입니다.

질문하러 가기