TheArtandScienceofSmalltalk:Chapter 08
- 제 8 장 의존성 메커니즘
의존성 메커니즘
스몰토크 시스템은 근본적으로 프로그래머로서 본인이 빌드한 본인만의 애플리케이션 상에 있는 애플리케이션 프레임워크이다. 보통 재사용 가능성이 높은 다수의 클래스를 제공하고 (인스턴스화와 상속을 통해), 앞에 여러 장에 걸쳐 그 중 몇 가지 클래스를 살펴보았다.
이번 장에서는 단일 클래스 또는 클래스의 집합을 시스템에 걸쳐 실행되는 프레임워크의 일부에서 살펴볼 것이다. 몇몇 클래스에 제한되지 않고 모든 클래스에서 사용할 수 있다. 자신의 코드에서도 사용이 가능하나 시스템 코드에서 많이 사용된다. 더 중요한 것은 스몰토크 '모델-뷰-컨트롤러'(MVC) 아키텍처의 기반(basis)으로서 다음 장에서 살펴보겠다.
이번 장의 주제는 바로 의존성 메커니즘이다. 이 메커니즘이 처음엔 객체들 간 이루어지는 특이하고 내재적인 통신 방법으로 보일지 모른다. 하지만 본장과 다음 장을 읽고 나면 스몰토크가 왜 그러한 메커니즘을 도입했는지, 자신의 코드에서 어떻게 사용할 수 있는지를 이해할 것이다.
의존성의 개념
앞의 여러 장에서 우리는 객체들 간 존재할 수 있는 많은 유형의 관계에 대해 논했다. 클래스와 그 서브클래스 간 관계인 상속을 살펴보았다. 그리고 인스턴스와 그 클래스 간 관계인 인스턴스화도 검토했다. 한 객체의 인스턴스 변수들이 다른 객체를 어떻게 '가리키는지' 혹은 '포함하는지'도 설명했다. 스몰토크 객체들 간 네 번째로 중요한 관계 유형이자 여기서 집중하게 될 관계를 의존성이라 부른다.
의존성은 프로그래머가 어떤 객체든 두 개를 연결하는 데 사용할 수 있는 관계다. 그러한 이유를 간략히 알아보자. 위의 다이어그램이 설명하듯, 각 객체는 프로그래머가 원하는 만큼 의존성 관계에 가담할 수 있다. 두 객체가 의존성에 의해 연관된 경우 우리는 한 객체가 의존적이다 또는 나머지 객체의 종속자(dependent)라고 말한다.
아래 다이어그램에서 객체 B는 객체 A에 의존한다 (또는 객체 B는 객체 A의 종속자다). 반대로 객체 A는 객체 B를 그 종속자들 중 하나로 가진다. 관계는 비대칭적이다. 즉, 단순히 B가 A에 의존한다고 해서 A가 B의 종속자임을 의미하는 것은 아니다. 종속자일 수는 있다-단지 구분된 관계일 것이다.
이 모든 용어는 매우 혼란스럽게 들릴 수도 있다. 고맙게도 단어들은 일상 영어와 완전히 일관된 방식으로 사용된다. 혼란스러울 경우 잠시 주의를 기울이고 생각하면 특정 관계를 해결하는 데 도움이 될 것이다.
그렇다면 왜 우리는 객체들이 서로 의존적이길 원하는가? 그 이유는 바로 변화와 관련될 것이다. 종종 한 객체 내 인스턴스 변수의 값이 변화하면 다른 객체에 큰 영향을 미칠 것이다. 의존성 메커니즘은 객체들에게 그들의 인스턴스 변수 값에 일어난 변화를 그들의 종속자로 전달할 수 있는 수단을 제공한다. 종속자는 관계 당사자(interested party)로 생각하면 되는데, 이 관계 당사자는 자신이 의존하는 객체가 그 인스턴스 변수의 값을 변경할 때마다 알고 싶어 한다.
의존성 메커니즘이 실제로 어떻게 작용하는지 살펴보고 사용 방법을 살펴보기 전에 한 마디 경고를 하겠다. 의존성 메커니즘은 때때로 매우 혼란스러울 수 있고, 심지어 숙련된 스몰토커에게도 마찬가지다. 종종 객체가 다른 객체에서 일어난 변경사항을 마법처럼 알아가듯 보이기도 한다. 절대 그렇지 않다. 여러 형태의 마법과 마찬가지로 무슨 일이 일어나는지 정확히 알지 못해서 그렇게 보일 뿐이다.
의존성은 클래스 라이브러리 내 일부 클래스에 의해 사용되는데, 특히 우리가 살펴볼 MVC 클래스에 의해 사용된다. 이러한 클래스를 재사용할 경우 당신은 내재적으로 의존성을 사용할 것이며, 당신이 관여할 필요 없이 올바르게 모두 작동할 것이다. 마치 마법처럼. VisualWorks를 이용해 자신의 사용자 인터페이스를 구성하도록 할 때도 마찬가지다.
하지만 MVC 클래스를 확장하거나 서브클래싱할 경우 (가령 새로운 유형의 사용자 인터페이스 위젯을 생성하기 위해) 혹은 자신의 클래스에 어떤 이유로 의존성을 사용하길 원할 경우, 당신은 의존성 관계를 설정하고 메커니즘이 올바로 작동하도록 올바른 메서드를 호출해야 할 것이다. 실제로 당신이 마법사가 되어야 한다. 다음 절은 이를 어떻게 행하는지 알려준다.
의존성이 작용하는 방식
의존성 메커니즘은 Object 클래스에서 정의된 메서드 집합에 의해 구현된다. 즉, 메서드 내 모든 객체는 의존성 관계에 관여할 수 있음을 의미한다. 일부 메서드는 효율성 또는 다른 이유로 인해 계층구조의 하층에서 동일한 일을 다른 방식으로 실행하도록 구현된다. 의존성이 어떻게 작용하는지 보기 위해 클래스를 살펴보는 경우 이러한 점 때문에 혼동되지 않도록 한다. 메커니즘은 내재적으론 다르게 작용할지 모르지만 당신은 정확히 동일한 방식으로 사용해야 한다.
모든 객체는 종속자에 해당하는 다른 객체들의 컬렉션을 가진다. 이러한 컬렉션을 찾기 위해서는 객체로 dependents 메시지를 전송한다. 때때로 종속자들은 위에서 상속된 인스턴스 변수에 보관된다. 이러한 경우 인스펙터를 통해 볼 수 있다. 그렇지 않은 경우 (사실상 Object로부터 상속된 기본 메커니즘이 사용 중일 때마다), 종속자는 다른 곳에 (클래스 변수에) 저장되어 인스펙터에서 직접 볼 수 없을 것이다. 하지만 dependents 메시지는 당신에게 항상 종속자를 제공하고, 없을 경우 nil을 제공할 것이다. 객체의 종속자를 보려면 인스펙터에서 self dependents를 평가하면 된다.
객체가 다른 객체에 의존하는 종속자가 되도록 만들기 위해서는 addDependent:를 사용하라. 아래 표현식은 myObject를 yourObject의 종속자로 만들 것이다:
yourObject addDependent: myObject.
이 표현식을 자세히 살펴보자. 객체는 종속자의 컬렉션을 보유한다. 즉, 객체는 그것에 의존하는 객체의 리스트를 보유한다. 객체는 자신이 의존하는 객체 리스트를 보유하지 않는다. 객체는 자신이 의존하는 다른 객체들에 대해서는 알지 못한다. 자신에게 의존하는 객체들에 대해서만 알고 있을 뿐이다. 혼란스럽게 들릴 수도 있지만 의존성 관계가 작용하는 부분을 기억하는 것이 중요함을 알게 될 것이다.
객체가 더 이상 다른 객체의 종속자가 되지 않도록 하려면 removeDependent: 를 사용한다. 아래 표현식은 myObject 를 더 이상 yourObject 의 종속자가 되지 않도록 만들 것이다:
yourObject removeDependent: myObject.
위에서 설명한 Object 의 dependents access 프로토콜 내 세 개의 메서드뿐만 아니라 좀 더 복잡하지만 덜 유용한 메서드도 발견할 것이다. 의존성을 준비하는 방법을 알았으니 어떻게 작용시킬 것인지 살펴보자.
의존성의 핵심은 하나의 객체가 (종속자) 다른 객체가 변경되는 시기를 알 수 있도록 만드는 데 있다. 이러한 기능을 수행하기 위한 두 개의 메서드 집합이 있다. 우선 '변경된(changed)' 메서드라 부르는 집합이 있고, '업데이트(update)' 메서드라 부르는 집합이 있다. 다음 페이지에 소개한 다이어그램은 이러한 메서드들 간 관계를 보여준다.
의존성 메커니즘에서 가장 중요한 부분은 다음과 같다. 객체에게 '변경된' 메시지가 전송될 때마다 객체는 자동으로 그 모든 종속자에게 '업데이트' 메시지를 전송할 것이다. 물론 스몰토크의 모든 것과 마찬가지로 이러한 자동적 행위는 숨겨지지 않는다. Object의 변화하는 프로토콜에서 볼 수 있도록 되어 있다. 하지만 최상의 실용성을 위해서는 그것을 마법으로 간주하라. 객체가 '변경된' 메시지를 수신하면 그 모든 종속자들은 마법처럼 '업데이트' 메시지를 수신할 것이다.
하지만 여기서 전혀 마법과 어울리지 않는 것은 '변경된' 메시지의 전송이다. 객체를 변경하고 그것의 종속자들이 변화에 대해 알기를 원할 경우, '변경된' 메시지가 변경된 객체로 전송되어야 한다. 때로는 당신이 상속한 코드에서 이러한 일이 발생하기도 하고, 때로는 당신 스스로 실행해야 한다. 어떤 방법이 되었든, 변경된 내용을 전달하고 싶다면 '변경된' 메시지가 확실히 전달되어야 한다. 마찬가지로 객체들이 의존하는 객체가 변경 시 의존 객체가 어떤 일을 행하길 원한다면 당신은 '업데이트' 메시지를 구현해야 한다.
Object에는 '업데이트' 메시지의 기본 구현부가 있다. 이러한 메서드들은 기본적으로 어떤 일도 수행하지 않으며, '업데이트' 메시지를 구현하지 않는 종속자를 가진 객체에게 당신이 '변경된' 메시지를 전송할 때 오류가 발생하지 않도록 확보하기 위해 존재한다. 기본 정의를 상속받아 안전하게 어떤 일도 수행하지 않는다.
따라서 의존성이 작용하기 만들기 위해선 당신이 '변경된' 메시지를 전송하고 결과가 되는 '업데이트' 메시지를 포착하기 위해 '업데이트' 메서드를 구현해야 한다. 다시 말하지만 이는 혼란스러울 수 있다. 어떤 타입의 메시지를 하나의 객체로 전송하고, 다른 객체는 다른 메시지를 수신하게 된다. 이러한 두 가지 타입의 메시지를 좀 더 자세히 살펴보자.
'변경된' 메시지
'변경된' 메시지에는 세 가지가 있는데, 각각 매개변수를 각각 0개, 1개, 2개씩 취한다. 이러한 메시지들은 객체에게 변경할 것을 부탁하는 것이 아니라 그것이 변경되었음을 말해주는 동시 그것의 종속자들에게 알리도록 전달해야 함을 기억하라. 세 가지 메시지 중 어떤 것을 선택하는지는 변경내용에 관해 얼마만큼의 정보를 전달하길 원하는지에 따라 좌우된다. 예를 들어:
anObj changed: an&Bpect with: aParm. (two parameters)
anObj changed: anAspect. (one parameter)
anObj changed. (no parameters)
위의 메시지들은 Object의 changing 프로토콜에서 구현된다 (좀 더 복잡한 메시지들도 구현된다. 여기선 다루지 않지만 원한다면 살펴봐도 좋다). 이 메시지들은 당신이 재구현하거나 오버라이드하지 않아도 된다. 그저 전송하고 이미 정의된 행위에 의존하면 된다.
이러한 메서드들 중 가장 강력한 것은 두 개의 매개변수를 취하는 changed:with: 이다. 첫 번째 매개변수는 전형적으로 the aspect라고 알려진다. 이는 Object의 어떤 부분이나 측면(aspect)이 변경되었는지 명시하도록 해준다. (항상은 아니지만) 종종 이것은 변경된 인스턴스 변수의 이름이 될 것이다. 두 번째 매개변수는 객체 측면의 측면이 어떻게 변경되었는지 전달하도록 해준다. 때로는 (항상은 아니지만) 이는 인스턴스 변수의 새 값이 될 것이다.
나머지 두 개의 메서드는 (changed: 와 changed) 편리한 메서드들이다. 이는 aParameter, 또는 anAspect와 aParameter에 대해 nil을 이용해 changed:with: 를 전송하는 것과 정확히 동일하다. Object에서 그들의 정의부를 살펴보면 이것이 사실임을 발견할 것이다. 해당 메서드들을 사용하면 코드의 가독성이 약간 더 높아질 수 있다.
'업데이트' 메시지
'변경된' 메시지와 마찬가지로 '업데이트' 메시지도 매개변수를 하나, 두 개, 세 개를 취한다. 이 메시지들은 절대 직접 전송하지 않을 것임을 기억한다. 하지만 자신의 객체에 의존하는 다른 객체들에게 '변경된' 메시지를 전송할 경우, 자신의 객체가 이 메시지들을 수신할 것이다. 정확히 어떤 메시지가 수신되는지는 당신이 구현한 대상에 따라 좌우된다. 이상하게 보일진 모르겠지만 이는 이러한 메서드들의 상속된 버전이 사실 서로 포함하기 때문에 발생한다. 즉, 오버라이드하지 않는 이상 각 메서드는 다음으로 가장 단순한 메서드를 호출할 것임을 의미한다. 세 가지 업데이트 메서드는 다음과 같다:
dependent update: anAspect with: aParm from: anObj.
dependent update: anAspect with: aParameter.
dependent update: anAspect.
첫 번째 메시지가 가장 강력하다. 해당 메시지를 구현했다면, 당신의 객체는 그것이 의존하는 다른 객체가 '변경된' 메시지 중 어떤 것이든 수신할 때마다 해당 메시지를 수신할 것이다. anAspect와 aParameter의 값은 '변경된' 메시지에서 사용되고, 혹은 더 단순한 '변경된' 메시지들 중 하나가 사용될 경우는 nil이 될 것이다. anObject의 값은 '변경된' 메시지가 전송되는 객체로서, 이것이 포함됨으로 인해 '업데이트' 메시지가 발생한 곳을 알 수 있다.
update:with:from: 메서드를 구현하지 않았다면 당신의 클래스는 Object로부터 기본 구현부를 상속받을 것이다. 당신의 클래스로 구현한 경우 이는 단순히 update:with:를 호출한다. 여기서 당신은 동일한 매개변수로 접근하게 되지만, 어디서부터 업데이트가 되는지는 (anObject) 볼 수 없을 것이다.
update:with: 메서드를 구현하지 않았다면 (Object로부터 상속된) 기본 구현부는 update:를 호출할 것이다. 여기서는 anAspect로의 접근성만 얻는다. update: 메서드를 구현하지 않을 경우, 기본 구현부는 리턴하는 것 외에는 어떤 일도 하지 않는다. update 메서드가 없다는 사실을 주목한다. 매개변수 없이 호출될 것이란 희망으로 구현을 시도하지 말길 바란다. 원하는대로 되지 않을 것이다!
update:with와 update: 는 update:with:from: 의 간편한 버전임을 볼 수 있을 것이다. 무의미해보일지도 모르지만 일반적인 스몰토크 스타일은 당신이 필요로 하는 수만큼의 매개변수만 취하는 버전을 사용한다. 그러면 코드가 약간은 더 읽기 수월해진다.
다음 페이지에 실린 다이어그램은 우리가 논한 내용을 요약하며, 어떻게 더 단순한 '변경된' 메서드가 더 복잡한 메서드를 호출하는 반면 좀 더 복잡한 '업데이트' 메서드는 더 단순한 메서드를 호출하는지를 보여준다. 궁극적으로는 객체가 이러한 '업데이트' 메시지들 중에서 최소한 하나를 이해하지 않는 한 어떤 일도 발생하지 않을 것이다.
의존성은 어떻게 사용되는가
방금까지 살펴본 '변경된' 메시지와 '업데이트' 메시지는 온갖 방식으로 사용 가능하다. 하지만 가장 흔히 사용되는 방법은 의존적 객체가 다른 객체 내 단일 인스턴스의 값이 변경되었음을 알도록 하는 방법이다.
메서드를 이용해 다른 객체 내 인스턴스 변수로 접근하는 것에 관해 이야기를 한 바가 있다. 이제 'set' 메서드의 내부인 경우 (예: size:) 객체는 인스턴스 변수의 값을 설정한 후 '변경된' 메시지를 자신에게(itself) 전송하고, 객체의 모든 종속자들은 변경내용에 관해 알게 될 것이다. 이를 실행하는 매우 흔한 메서드 정의를 소개하겠다:
size: aHumber
size := aNumber.
self changed: #size.
이제 size 변수의 값을 설정하고 나면 객체는 자신에게 #size 기호를 매개변수로 하는 changed: 메시지를 전송함을 주목한다. 이 기호는 '업데이트' 메시지를 통해 종속자에게 전달되고, 어떤 변수가 변경되었는지 표시하는 데 사용된다. 인스턴스 변수명을 기호로 사용하는 것은 순수한 규칙임을 주목한다. 전달된 매개변수는 어떤 객체든 될 수 있다 (기호뿐만이 아니라). 이는 매우 강력한 기능으로, 스몰토크에서 프로그래밍을 하면서 매우 유용하다고 생각하게 될 것이다.
아래의 '변경된' 메시지 다음에 의존성 메시지에 의해 호출될지 모르는 다른 클래스 내 '업데이트' 메서드의 메서드 정의를 예로 들어보겠다:
update: anAspect
anAspect = #colour ifTrue: [self redraw].
anAspect = #size ifTrue: [self resize].
이러한 'update' 메서드는 객체가 의존하는 객체들에 대한 두 가지 유형의 변경내용을 처리하도록 고안되었다. 변경된 것이 색상일 경우 하나를 실행하고, 크기가 변경된 경우 다른 일을 실행한다. 어떤 유형의 변경내용이든 무시될 것이다.
이런 간단한 예는 의존성이 어떻게 작용하는지를 보여준다. aspect와 the.parameter를 창의적으로 이용하면 훨씬 더 복잡하면서 강력하게 의존성을 이용하는 방법이 떠오를 것이다. 의존성에 대한 자신의 이해를 확인하기 위해 작은 테스트 클래스를 빌드해보는 것도 좋은 생각이다. 워크스페이스를 이용해 자신의 클래스에 대한 인스턴스를 만들고, 서로 의존하도록 만들어서 '변경된' 메시지를 전송하여 자신의 '업데이트' 메서드가 호출되는 결과를 살펴보라.
클래스 라이브러리는 의존성을 사용할 수 있는 방법을 보여주는 예를 찾아보기에 좋은 장소다. '변경된' 메시지의 전송자를 살펴보고, '업데이트' 메시지의 구현자를 살펴보는 것부터 시작하라. 이를 실행할 경우 다음 장의 주제에 해당하는 다수의 클래스를 발견할 것이다.
요약-왜 의존성을 갖는가?
의존성 메커니즘은 하나의 객체가 다른 객체에 일어난 변경사항에 대해 알 수 있도록 마련하는 범용적이자 재사용 가능한 방식이다. 변경내용에 관심이 있는 객체들은 변경내용에 관심을 둔 객체들에게 의존함으로써 관심을 표한다. 해당 객체가 변경되면 '변경된' 메시지가 전송되고 (또는 스스로에게 전송하고) '업데이트' 메시지가 모든 관련된 객체로 (종속자에게) 전송되어 그 결과 원하는 것을 실행하도록 허용한다.
그렇다면 '변경하는 객체가 직접 그 변경내용에 관해 직접 알려주는 메시지를 다른 객체에게 전송할 수는 없을까?'라고 생각하게 된다. 이에 대한 답으로, 특정 클래스를 작성할 당시 다른 객체들이 자신의 객체에 관심을 가질 것임을 알 수는 있지만 구체적으로 어떤 객체들인지 혹은 얼마나 많은 객체들이 존재할 것인지는 알 수 있는 방도가 없다. 즉, 당신은 알려진 객체들에게 '나의 내용이 변경되었다'라고 명시적인 메시지를 전송할 수가 없다는 의미다. 대신 할 수 있는 일은 당신이 변경했음을 알리고 (스스로 '변경된' 메시지를 전송하여), 런타임에서 내용이 변경되면 ('업데이트 메시지'를 전송함으로써) 변경내용을 알고 싶다고 말한 객체들에게 (당신의 종속자들) 의존성 메커니즘이 알릴 수 있도록 한다.
이런 방식을 통해 의존성 메커니즘은 객체들 간 전송되는 하드코딩 메시지들보다 훨씬 더 동적일 수 있는 것이다. 또한 그것에 관심을 둔 객체들 사이에 순진성을 허용하기도 한다. 듣기엔 별로일지 모르지만 사실상 캡슐화 또는 OOP가 장려하는 기능의 분할에 도움이 된다. 그리고 의존성을 이용해 관련 당사자에게 자신이 변경되었음을 알리는 클래스의 재사용 가능성을 상당히 향상시키기도 한다.
이러한 런타임 관심 등록과 강력한 캡슐화, 이 두 가지는 아마 표준 스몰토크 시스템에서 의존성 메커니즘 사용자들 중 가장 많은 이들이 사용하는 기능일 것이다. 스몰토크 그래픽 사용자 인터페이스는 '변경' 및 '업데이트' 메시지를 명시적으로 자주 사용하게 만드는 아키텍처를 기반으로 한다. 이러한 아키텍처를 모델-뷰-컨트롤러 혹은 MVC라 부르는데, 이는 다음 장에서 살펴보겠다.