TheArtandScienceofSmalltalk:Chapter 02

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 2 장 객체 입문

객체 입문

스몰토크는 객체 지향 프로그래밍 언어다. 객체 지향 프로그래밍은 객체의 디자인과 구현, 객체들의 상호작용을 명시하는 일로 구성된다. 이러한 일을 직접 행하느냐 아니면 VisualWorks와 같은 개발 환경을 통해서 행하느냐는 중요하지 않다. 당신이 하는 일은 객체를 생성하는 일이다. 그렇다면 객체는 과연 무엇인가? 바로 이번 장에서 이 질문에 대한 답을 제시하고자 한다. 그 다음은 프로그래밍을 작성하기 위해 객체를 어떻게 사용하는지를 살펴보겠지만, 우선 지금은 '객체 구조(object anatomy)'라고 부르는 것을 살펴보고자 한다.


객체 지향이라는 기술(description)은 많은 언어에 적용된다. 이는 그러한 언어들에서 대체적으로 유사한 개념의 집합을 아우른다. 이러한 개념에는 클래스와 인스턴스의 개념, 메시징, 캡슐화, 인스턴스화, 상속, 다형성과 같은 개념들이 포함된다. 이들은 일반적 개념들이지만 여기서는 스몰토크에 적용되는 구체적인 형태로 논할 것이다. 일부 다른 객체 지향 언어들은 이러한 개념을 갖고 있지 않을지도 모르며, 또 어떤 언어들은 추가적 개념을 갖기도 한다. 동일한 개념을 공유하는 언어들이라 하더라도 서로 다른 방식으로 구현한다. 다른 언어들을 통해 OOP로 노출된다면 이러한 점을 유의하라.


그러한 차이에도 불구하고 이전에 다른 객체 지향 언어로 프로그래밍을 한 경험이 있다면 이번 장에 실린 내용은 매우 친숙할 것이다. 반대로 OOP를 경험한 적이 없다면 본문에 제시된 개념들이 약간 생소함을 느낄 것이다. 절망하지 말길 바란다-책의 나머지 부분을 읽다보면 이해가 될 것이며, 스몰토크로 프로그래밍을 시작하면 객체가 어떻게 행동하는지 실제로 이해할 수 있을 것이다.


객체란 무엇인가?

객체는 코드와 데이터의 이산적(discrete)이고 완전한(self-contained) 결합이다. 아래 다이어그램은 객체가 무엇인지 상상하는 한 가지 방법을 표시한다. 스몰토크에서는 객체 내 코드가 메서드라 불리는 조각으로 나뉘며, 데이터는 다양한 타입으로 된 변수에 저장된다. 프로그램은 이러한 객체를 수천 개씩 포함할 수 있으며, 몇 바이트부터 몇 킬로바이트까지 크기가 다양하다. 객체는 실제 세계에서 (수표, 사람들 또는 주간 쇼핑), 혹은 컴퓨터 세계에서 (배열, 창 또는 이벤트 큐) 사물들을 표현할 수 있다.


아래 다이어그램에는 코드가 의도적으로 데이터를 둘러싸고 있는 것처럼 표시되어 있다. 그 이유는 각 객체 내 변수들은 그 특정 객체 내 메서드에 의해서만 접근이 가능하며, 다른 코드에 의해서는 접근이 불가하기 때문이다. 이는 OOP에서 캡슐화로 알려진 개념의 예제에 해당한다.


메서드는 다른 언어에서 서브루틴, 함수 또는 절차라고 부르는 것과 비슷하다. 메서드는 이름이 있는 완전한(self-contained) 코드 조각으로, 개별적으로 호출 가능하며, 실행이 완료되면 값을 리턴한다. 스몰토크 메서드는 관련 객체로 메시지를 전송함으로써 호출된다. 메시지는 메서드의 이름뿐만 아니라 필요한 파라미터가 무엇이든 포함할 것이다. 메시지를 전송하는 객체가 메시지 내에서 명명된 메서드를 포함하는 경우 그것이 메시지를 이해한다고 말하며, 메서드를 실행하여 결과를 리턴할 것이다. 다음 페이지에 실린 다이어그램에서 볼 수 있듯이 메시지를 전송하는 객체를 발신자(sender)라 부르고, 메시지를 실행하는 객체는 수신자(receiver)라 부른다.

Ass image 02 01.png
객체는 서로 단단히 결합된 약간의 프로그램 코드와 약간의 데이터로 구성된다.


Ass image 02 02.png
객체의 메서드들은 그곳으로 메시지를 전송하는 다른 객체들에 의해 호출된다.


이러한 유형의 상호작용은 객체들이 서로에게 병렬로 메시지를 전송하는 병렬 처리 방식을 암시하는 것처럼 보일지도 모른다. 사실 메시지의 발신자는 메시지가 리턴될 때까지 막혀 있다. 메서드를 실행하기 위해 수신자가 추가로 메시지를 전송하는 것이 가능하며, 그 메시지들이 리턴될 때까지 스스로 막혀 있다. 이 점과 관련해 볼 때 메시지를 전송하는 것은 비객체 지향 언어에서 함수를 호출하는 것과 같다-병렬 처리의 암시는 없다 (별도이긴 하나 스몰토크는 고유의 가벼운 '스레드(threads)' 또는 프로세스를 지원한다).


객체 내의 변수들은 비객체 지향 언어에서의 변수와 유사하다. 그들에겐 이름이 있고 값을 포함한다. 나중에 논하겠지만, 많은 언어들과 달리 스몰토크 변수들은 타입을 갖고 있지 않다 (예: 정수나 문자열). 모든 변수는 어떤 타입의 객체든지 보유할 수 있다.


객체 정의하기와 생성하기

앞서 살펴보았듯 스몰토크와 같은 객체 지향 언어로 프로그래밍하는 과정은 객체를 디자인하고 그 상호작용을 명시하는 과정으로 구성된다. 이는 각 객체가 변수 내에 어떤 데이터를 보유할 것인지 결정하고 그 데이터에 의거해 행동하게 될 메서드를 작성하는 것을 의미한다. 하지만 객체들은 개별적으로 디자인되지 않는다. 프로그래머는 프로그램에서 모든 객체를 직접 명시하지 않아도 된다. 프로그램에는 너무나 많은 객체가 서로 닮아있기 때문에 개별적으로 직접 명시한다면 엄청난 낭비가 될 것이다. 프로그래머는 대신 스몰토크에서 클래스라고 알려진 특수 객체들을 명시한다. 이 클래스들은 형틀(template) 또는 청사진(blueprint)처럼 행동한다. 클래스는 프로그래머가 프로그램 내 다른 객체로 넣고자 하는 기능을 (메서드와 변수) 나타내는 일을 한다. 여기서 다른 객체란 스몰토크에서 인스턴스로 알려진다. 스몰토크에서 모든 객체는 클래스 아니면 인스턴스에 해당한다고 생각하면 된다.


스몰토크 클래스는 형틀처럼 행동하지만 또 다른 목적이 있다. 그것이 나타내는 인스턴스 객체를 만들어야 할 책임이 있다. 이러한 과정은 위의 다이어그램에서 보이는 바와 같이 인스턴스화라고 불린다. 따라서 스몰토크 클래스는 인스턴스에 대해 형틀의 역할을 하는 동시 그들이 나타내는 인스턴스를 제작하는 팩토리(factory)의 역할을 한다. 모든 인스턴스는 그것을 제작하여 형틀의 형태로 나타내는 특정 클래스의 인스턴스라고 말할 수 있다.

Ass image 02 03.png
클래스 객체는 인스턴스 객체에 대한 형틀처럼 행동하고, 인스턴스 객체를 제조하거나 인스턴스화할 책임이 있다.


클래스와 인스턴스 간 관계는 클래스 객체가 두 가지 타입의 코드와 데이터를 결합함을 의미한다. 클래스 객체 스스로 포함하는 코드와 데이터가 있고 (클래스 메서드와 클래스 변수로 알려짐), 클래스의 인스턴스가 포함하게 될 코드와 데이터에 대한 형틀이 있다 (인스턴스 메서드와 인스턴스 변수로 알려짐). 클래스 메서드와 변수는 인스턴스의 실제 제작과 관련된 기능을 구현한다. 인스턴스 메서드와 변수는 프로그래머가 이 클래스의 인스턴스를 디자인 한 바와 같이 행동한다.


클래스 객체는 클래스 메서드만 이해하고, 인스턴스 객체는 인스턴스 메서드만 이해한다. 이는 매우 중요한 차이점으로 때때로 혼돈을 야기하기도 한다. 프로그래밍을 시작하기 전에 마음속으로 분명히 이해해야 하는 차이이기도 하다. 자신의 객체들이 (클래스와 인스턴스) 당신이 전송하는 메시지를 이해할 것으로 기대한다면 클래스에 클래스 메시지를 전송하고 인스턴스에 인스턴스 메시지를 전송해야 한다!


특정 클래스의 인스턴스마다 그 클래스에서 정의된 인스턴스 변수의 고유 집합을 갖는다. 이러한 집합은 인스턴스들 간에 서로 공유되지 않는다. 따라서 클래스가 'size'라 불리는 인스턴스 변수를 정의할 경우 그 클래스의 모든 인스턴스는 'size라는 구분된 변수를 가질 것이다. 이 모든 'size' 변수들의 값은 서로 상이할 수 있으며, 아마도 상이할 것이다. 상황은 클래스 변수마다 다르다. 이는 클래스에서 정의되지만 그 클래스의 모든 인스턴스에서 눈에 보이며 공유된다.


개념적으로 우리는 클래스의 모든 인스턴스가 그 클래스에 정의된 인스턴스 메서드에 대한 고유의 동일한 복사본을 가질 것으로 생각할 수도 있으나 사실상 이는 비효율적이다. 따라서 스몰토크에서 인스턴스들은 그들의 클래스에서 정의된 메서드를 공유한다. 즉, 클래스 내 인스턴스 메서드의 정의를 변경할 경우, 그 클래스의 기존 인스턴스와 미래 인스턴스는 새로운 정의를 목격하고 사용할 것이다.


이상으로 이 모든 클래스와 인스턴스는 복잡할 수 있으나 아래와 같이 요약이 가능하다:

인스턴스 변수
모든 인스턴스에서 (공유되지 않고) 구분된 집합.
클래스 변수
클래스와 그 모든 인스턴스들이 공유하는 하나의 집합
인스턴스 메서드
클래스에서 정의되지만 인스턴스만 이해할 수 있다.
클래스 메서드
클래스에서 정의되며 클래스만 이해할 수 있다.


상속

많은 스몰토크 객체들은 서로 유사하기 때문에 프로그래머가 개별적으로 모든 객체를 디자인하지는 않는다는 사실을 이제 이해할 것이다. 대신 프로그래머는 인스턴스라 불리는 객체의 형틀인 클래스라는 객체를 생성한다. 프로그래머가 이제 클래스를 작성하여 클래스로 하여금 자신의 인스턴스를 생성하도록 요청하고 그에 따라 스몰토크 프로그램을 형성할 수 있을 것이라 생각할 것이다. 이론적으로는 사실이지만 실제로는 많은 인스턴스들이 서로 유사하듯 많은 클래스들도 서로 유사하다. 스몰토크 프로그래머는 상속이라 불리는 개념을 이용해 클래스들 간 이러한 유사점을 이용할 수 있다.


상속은 프로그래머가 사실상 '이 새 클래스는 다음과 같은 방식을 제외하고는 기존의 클래스와 같다,'고 말할 수 있도록 허용한다. 새 클래스를 서브클래스라 부르고 기존 클래스는 슈퍼클래스라 부른다. 두 클래스가 이러한 방식으로 연관되면 서브클래스는 슈퍼클래스로부터 상속되었다고 말한다. 스몰토크에서는 클래스가 그로부터 상속된 다수의 서브클래스를 가질 수 있다. 하지만 각 서브클래스는 하나의 슈퍼클래스로부터 직접 상속만 가능하다. 모든 클래스는 서브클래스인 동시에 슈퍼클래스가 될 수 있다. 이는 상속 계층구조(inheritance hierarchy)라 불리는 클래스 가계도(family tree)를 발생시킨다. 아래 다이어그램에서 클래스 C는 클래스 A로부터 상속되고 (A의 서브클래스이고), 클래스 D, E, F에 의해 상속된다 (클래스 D, E, F의 슈퍼클래스이다).


특정 클래스의 인스턴스들은 그들의 클래스에서 정의된 모든 메서드를 이해하고, 그들의 클래스의 슈퍼클래스에서 정의된 모든 메서드를 이해한다. 따라서 클래스 D의 인스턴스는 D, C, A를 비롯해 트리의 최상위에 있는 클래스에서 정의된 메서드를 모두 이해한다. 좀 더 혼동되도록 추가하자면, 인스턴스가 이해하는 메서드가 사실상 인스턴스의 클래스에서 (슈퍼클래스가 아닌) 정의될 경우 클래스는 메서드를 구현한다고 말할 수 있다.


상속에 따르면 클래스의 인스턴스들은 그들의 클래스에서 정의된 모든 인스턴스 변수를 포함할 것이며, 그들의 클래스의 슈퍼클래스에서 정의된 모든 인스턴스 변수들도 포함할 것이다. 따라서 클래스 G의 인스턴스는 G, B, A를 비롯해 트리의 최상위에 있는 클래스에서 정의된 인스턴스 변수를 가질 수 있다.


일반적으로는 여느 클래스의 인스턴스든지 만들 수 있음을 주목한다. 다시 말해, 인스턴스를 가질 수 있는 클래스는 비단 계층구조의 최하위에 (트리의 leave) 위치한 클래스만이 아니다. 하지만 프로그래머는 때때로 인스턴스를 갖지 않도록 클래스를 디자인하기도 한다. 그렇다고 해서 쓸모없는 클래스라는 의미는 아니다. 이는 인스턴스를 갖게 될 다른 클래스들에 의해 상속될 기능을 모으기 위해서이다. 인스턴스를 갖지 않은 클래스들을 종종 추상 클래스라고 부른다. 반면 인스턴스를 갖도록 디자인된 클래스는 구체적 클래스라고 불린다.


Ass image 02 04.png
클래스 상속 계층구조의 단편 모습으로, 모든 클래스가 다른 하나의 클래스로부터 정확히 어떻게 상속되는지를 보여준다.


오버라이딩과 다형성

스몰토크에서 상속은 첨가물(additive)로 생각할 수 있겠다. 각 클래스는 그것의 슈퍼클래스의 (그리고 그것의 슈퍼클래스의 슈퍼클래스 등등 )기능을 모두 상속하고 자신만의 고유 기능도 (메서드와 변수) 어느 정도 추가한다. 하지만 클래스가 이미 상속 중인 기능을 추가하려고 하면 어떤 일이 발생할까? 답은 클래스가 메서드를 추가하느냐 아니면 변수를 추가하느냐에 따라 달라진다.


클래스가 그것이 상속하는 변수와 동일한 이름으로 된 새 변수를 정의하려고 하면 단순히 오류가 발생한다. 스몰토크에서는 적어도 상속 계층구조에서 더 낮은 계층의 변수를 재정의할 이유가 없다.


하지만 클래스가 그것이 상속하는 메서드와 동일한 이름으로 된 새 메서드를 정의할 경우, 새 메서드는 그 클래스와 서브클래스의 인스턴스에서 상속된 메서드를 대체한다. 상속된 메서드는 계층구조에서 낮은 계층에 있는 정의에 의해 오버라이드되었다고 말한다. 원본 메서드는 사라지지 않는다. 여전히 그것이 정의된 클래스의 인스턴스와, 오버라이드되지 않은 서브클래스의 인스턴스에 적용된다.


좀 더 일반적으로는 두 개의 다른 클래스가 동일한 이름으로 된 두 개의 메서드를 정의할 수 있다. 이러한 기능은 상속된 메서드 정의를 오버라이드할 수 있다는 기능과 함께 객체 지향 프로그래밍의 강력한 특징에 해당한다. 이는 동일한 이름으로 된 서로 다른 종류의 메서드 정의가 존재할 수 있음을 의미한다. 메시지가 특정 메서드를 명명하는 객체로 전송될 때 실행되는 실제 메서드는 객체의 클래스에 따라 좌우된다. 이러한 기능을 다형성이라 부른다. 이것이 그토록 강력한 기능이 되는 이유는, 여러 다른 클래스들이 동일한 일을 고유의 방식으로 정의할 수 있도록 해주기 때문이다.


그렇다면 메시지가 객체로 전송될 때 시스템은 어떻게 실행하기에 올바른 메서드를 찾을까? 답은 메시지를 수신하는 객체가 인스턴스인 클래스를 먼저 살펴본다는 것이다. 그곳에 올바른 이름의 메서드가 있다면 그 메서드가 실행된다. 메서드가 없다면 시스템은 슈퍼클래스를 검색한다. 만일 그곳에서 올바른 메서드를 찾지 못한다면 슈퍼클래스의 슈퍼클래스를 살펴보고, 계층구조의 최상위 클래스까지 검색한다. 시스템이 트리의 최상위 클래스까지 도달했는데도 올바른 이름의 메서드를 찾지 못한다면 오류가 발생한다.


이러한 상향식 검색 과정은 실행되는 메서드가 항상 메시지를 수신하는 객체의 클래스와 가장 가까이에서 정의된 메서드임을 의미한다. 생각해보면 서브클래스가 어떻게 그들의 슈퍼클래스의 메서드를 오버라이드할 수 있는지 알 수 있을 것이다. 이번 장은 객체 지향 프로그래밍에서 중요한 개념들을 모두 소개한다. OOP에서 객체=코드+데이터임을 살펴보았다. 객체 내 데이터는 그 객체에 private하다-이 개념을 캡슐화라 부른다. 코드는 메서드들로 나뉘고, 이 메서드들은 메시지를 전송하여 호출한다. 클래스는 형틀과 같이 행동하고, 인스턴스를 만드는 팩토리로 생각할 수 있다. 이러한 과정을 인스턴스화라고 부른다. 클래스는 상속을 이용해 다른 클래스들과 기능을 공유하기도 한다. 일부 클래스는 추상적이고 (전혀 인스턴스를 갖지 않음) 일부는 구체적이다. 마지막으로 오버라이딩과 다형성은 하나 이상의 메서드 정의가 존재하도록 허용한다.


이러한 개념을 비교하고 대조하는 것은 유용하다. 특히 클래스와 인스턴스 간 차이를 기억하는 것이 중요하다. 상속과 인스턴스화의 차이를 아는 것도 유용하겠다. (상속은 클래스들 간 관계를 말하는 반면, 인스턴스화는 인스턴스와 그것의 클래스의 관계를 나타낸다.) 인스턴스가 그 클래스로부터 그 기능을 상속받는다는 말이 물론 매력적으로 들릴 수는 있지만 사실은 아니다. 인스턴스는 그 클래스로부터 기능을 받는데, 단순히 그것이 그 클래스의 인스턴스라는 이유 때문이다.


이번 장의 시작 부분에서 제시하였듯 이 모든 개념들은 처음엔 약간 생소하게 보일지도 모르겠다. 완벽히 이해하지 못했다 하더라도 걱정말길 바란다. 실용적인 경험을 얻으면서 점점 더 분명해질 것이다. 여기서 이해했다 하더라도 '나만의 객체를 어떻게 디자인하며 스몰토크에서 그 객체들을 어떻게 구현할 것인가?'라는 질문은 여전히 남아 있을지도 모른다. 이 질문은 책의 나머지 부분에서 강조하고자 한다.


Notes