오브젝트

들어가며

각 책의 챕터를 공부하며 옮겨쓰기 방식의 블로깅을 하고있다. 게시된 글 들은 시간이 많다면 찬찬히 읽어보며 복기하는게 목적이다.

반면 여기 작성된 요약 정리는 실무 개발을 진행하면서 내가 꼭 기억해야 할 내용들을 한 눈에 보기 위해 작성한다.


바람직한 의존성 (8장 의존성 관리)

의존성변경과 관련이 깊다. 의존하는 대상(의존 객체)이 변경 될 경우 함께 변경 될 가능성이 바로 의존성이다.

객체간의 협력을 설계하는데 있어 의존성은 반드시 필요하다. 하지만 너무 구체적이고 과한 의존성은 오히려 해가된다.

이렇게 구체적이고 과한 의존성을 일컬어 높은 결합도라고도 표현한다.

그렇다면 바람직한 의존성이란 무엇일까?

바람직한 의존성이란 다양한 컨텍스트에서 재사용 가능한 의존성을 의미한다.

다시말하면 컨텍스트에 독립적인 의존성을 의미한다. 그리고 컨텍스트에 독립적인 의존성을 얻기 위해서는 고려해야 할 점들이 있다.

  1. 컴파일타임 의존성과 런타임 의존성을 다르게 설계한다.

컴파일타임 의존성클래스들의 의존성이고 런타임 의존성객체들간의 의존성이다. 두 의존성을 다르게 하기 위해서는 추상화에 의존해야 한다. 만일 구체적인 클래스에 의존하게 된다면 해당 클래스가 지니고 있는 구체적인 컨텍스트에 결합되게 되고 이는 결국 높은 결합도로 이어지게 된다.

생각해보면 간단하다. 의존하는 객체가 구체적인 클래스라면 클라이언트 객체는 구체적인 클래스의 인스턴스에게 메시지를 보낼 수밖에 없다. 해당 클래스의 인스턴스 외에는 협력이 어려워진다. 반면에 구체적인 클래스가 아니라 추상화(추상클래스, 인터페이스)에 의존하게 된다면 런타임에 주입된 객체에 따라 다양한 컨텍스트에서 재사용이 가능하다. 다시말해 구체적인 클래스가 아닌 다양한 클래스의 인스턴스와 협력이 가능하다.

  1. 퍼블릭 인터페이스(생성자, Setter)에 의존성을 명시적으로 표현한다.

구체 클래스가 아닌 추상화에 의존하는 것이 첫 걸음이라면 그 다음엔 해당 추상화를 런타임에 구체적인 객체로 의존성을 변경(동적 바인딩)시키기 위하여 퍼블릭 인터페이스에 의존성을 명시적으로 표현해야한다. 즉, 의존성 주입이 필요한 것이다. 만일 클래스 내부에서 의존 객체를 직접 new 연산자를 이용하여 바인딩 한다면 이는 구체 클래스에 의존하는 것과 다름이 없다.

  1. 생성과 사용의 책임을 분리한다.

생성과 사용의 책임을 분리하는 것은 의존성을 명시적으로 표현하는 것과 관련이 있다. 추상화에 의존한다면 의존 객체를 사용하는 클라이언트 객체는 의존 객체를 직접 생성해서는 안된다.


추상화 (9장 유연한 설계)

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다.

추상화란 ‘공통적’이고 핵심적인 부분만을 남기고 ‘세부적’이고 불필요한 부분을 생략함으로써 ‘복잡성을 극복’하는 기법이다.

따라서 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 공통적인 부분만 남게되고 문맥에 따라 변하는 세부적인 부분은 생략된다.

추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 공통적인 부분은 다양한 상황(다양한 컨텍스트)에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시말해서 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 대해 닫혀있다.

추상화를 통해 생략된 부분은 확장의 여지를 남긴다. 이것이 추상화개방-폐쇄 원칙을 가능하게 만드는 이유다.

변하는 부분은 생략하고 변하지 않는 부분은 고정하는 추상화 메커니즘이 개방-폐쇄 원칙의 기반이 된다.

추상화는 확장을 가능하게 하고 추상화에 대한 의존은 폐쇄를 가능하게 한다.

올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.

추상화가 수정에 대해 닫혀 있을 수 있는 이유는 변경되지 않을 부분은 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이다.

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 객체를 생성하는 책임객체를 사용하는 책임을 서로 분리해야 한다. 한 마디로 말해서 객체에 대한 생성과 사용을 분리해야 한다. 사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.

또한 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가할 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.

종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 단지 출발점이라는 사실을 명심해야 한다. 실제로 동작하는 애플리케이션은 데이터베이스 접근을 위한 객체와 같이 도메인 개념들을 초월하는 기계적인 개념들을 필요로 할 수 있다.

모든 책임을 도메인 객체에 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제에 봉착하게 될 가능성이 높아진다.

이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다.

의존성은 명시적으로 드러내야 한다. SERVICE LOCATOR 패턴과 같이 의존성을 암시적으로 구현 내부의 깊숙한 곳에 숨겨둔다면 의존성과 관련된 문제가 컴파일 타임이 아닌 런타임에 가서야 발견된다는 사실을 알 수 있다. 숨겨진 의존성이 이해하기 어렵고 디버깅하기 어려운 이유는 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미루기 때문이다.

의존성 역전 원칙과 관련하여 객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. MovieAmountDiscountPolicy 사이의 협력이 가지는 본질은 영화의 가격을 계산하는 것이다. 어떻게 할인 금액을 계산할 것인지는 협력의 본질이 아니다. 다시 말해서 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스(Movie) 이다.

따라서 상위 클래스하위 클래스 모두 추상화에 의존해야 한다.

가장 중요한 조언은 추상화에 의존하라는 것이다. 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다.


상속과 결합도 (10장 상속과 중복 코드)

중복 코드는 변경을 방해한다. 이것이 중복 코드를 제거해야 하는 가장 큰 이유다.

중복 코드가 가지는 가장 큰 문제는 코드를 수정하는 데 필요한 노력을 몇 배로 증가시킨다는 것이다.

상속을 이용하여 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다. 이것은 자식 클래스 작성자가 부모 클래스의 구현 방법에 대한 정확한 지식을 가져야 함을 의미한다.

따라서 상속은 결합도를 높인다. 특히나 이러한 결합은 자식 클래스에서 super 참조로 부모 클래스의 메서드를 직접 호출할 때 자주 보인다.

자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다. super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.

super 참조의 경우 자식 클래스가 부모 클래스의 [행동 구현]에 강하게 결합되는 양상을 보이는 것 같다. 즉, 부모 클래스의 super 참조 메서드가 어떤 행동을 하는지 (ex. 단건 통화 요금에 대한 기본 계산) 알아야 자식 클래스의 행동을 구현할 수 있기 때문에 [행동 구현에 강하게 결합]된다고 볼 수 있다.

취약한 기반 클래스 문제 (10장 상속과 중복 코드)

상속은 자식 클래스와 부모 클래스의 결합도를 높인다.

취약한 기반 클래스 문제는 캡슐화를 약화시키고 결합도를 높인다. 상속은 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시킨다. 이것이 상속이 위험한 이유인 동시에 상속을 피해야 하는 첫 번째 이유다.

객체를 사용하는 이유는 구현과 관련된 세부사항을 퍼블릭 인터페이스 뒤로 캡슐화 할 수 있기 때문이다. 캡슐화는 변경에 의한 파급효과를 제어할 수 있기 때문에 가치가 있다. 객체는 변경될지도 모르는 불안정한 요소를 캡슐화 함으로써 파급효과를 걱정하지 않고도 자유롭게 내부를 변경할 수 있다.

상속을 사용하면 부모 클래스의 퍼블릭 인터페이스가 아닌 구현을 변경할지라도 자식 클래스가 영향을 받기 쉬워진다.

객체지향의 기반은 캡슐화를 통한 변경의 통제다. 상속은 코드 재사용을 위해 캡슐화의 장점을 희석시키고 구현에 대한 결합도를 높임으로써 객체지향이 가진 강력함을 반감시킨다.

퍼블릭 인터페이스에 대한 고려 없이 단순히 코드 재사용을 위해 상속을 이용하는 것은 위험하다. 객체지향의 핵심은 객체들의 협력이다. 단순히 코드를 재사용하기 위해 불필요한 오퍼레이션이 인터페이스에 포함되도록 해서는 안된다.

상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨뜨릴 수 있다. (Stack과 Properties를 기억하라.)

메서드 오버라이딩 오작용 문제 (10장 상속과 중복 코드)

자식 클래스가 부모 클래스의 메서드를 오버라이딩 할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다. 즉, 자식 클래스가 부모 클래스의 오버라이딩 하려는 메서드의 구현에 대해서 상세하게 알아야 한다(다른 메서드가 호출하는지 등등).

예를 들면 부모 클래스의 특정 메서드를 오버라이딩 했는데 부모 클래스의 특정 오퍼레이션이 해당 메서드를 호출(메시지 전송) 하는 경우, 자식 클래스 개발자는 오버라이딩 시 해당 부분까지 파악하지 못한다면 (부모 클래스의 내부 구현을 자세하게 알지 못한다면) 기대하는 것과 다르게 작동할 수 있다. (InstrumentedHashSet 사용 문제를 기억)

메서드 오버라이딩으로 인한 파급 효과를 분명하게 문서화 해야한다. 달리말해 오버라이딩 가능한 메서드들의 자체 사용(self-use), 즉, 오버라이딩 하려는 메서드들이 같은 클래스의 다른 메서드를 호출하거나 호출받는지에 대해 반드시 문서화 해야한다. 더 일반적으로 말하면 오버라이딩 하려는 메서드를 호출할 수 있는 어떤 상황에 대해서도 문서화 해야한다는 것이다.

상속은 코드 재사용을 위해 캡슐화를 희생한다. 완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른 방법을 사용해야 한다.

부모 클래스와 자식 클래스 동시 수정 문제 (10장 상속과 중복 코드)

자식 클래스가 부모 클래스의 메서드를 오버라이딩하거나 불필요한 인터페이스를 상속받지 않았음에도 부모 클래스를 수정할 때 자식 클래스를 함께 수정해야 할 수도 있다. 상속을 사용하면 자식 클래스가 부모 클래스의 구현에 강하게 결합되기 때문에 이 문제를 피하기는 어렵다.

결합도란 다른 대상에 대해 알고 있는 지식의 양이다. 상속은 기본적으로 부모 클래스의 구현을 재사용한다는 기본 전제를 따르기 때문에 자식 클래스가 부모 클래스의 내부에 대해 속속들이 알도록 강요한다. 따라서 코드 재사용을 위한 상속은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황 역시 빈번하게 발생할 수 밖에 없다.

서브 클래스는 올바른 기능을 위해 슈퍼 클래스의 세부적인 구현에 의존한다. 슈퍼 클래스의 구현은 릴리스를 거치면서 변경될 수 있고, 그에 따라 서브클래스의 코드를 변경하지 않더라도 깨질 수 있다. 결과적으로 슈퍼클래스의 작성자가 확장될 목적으로 특별히 그 클래스를 설계하지 않았다면 서브클래스는 슈퍼클래스와 보조를 맞춰서 진화해야 한다.(함께 수정되어야 한다.)

클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수 밖에 없다.

취약한 기반 클래스 문제를 완전히 없앨 수는 없지만 어느 정도 까지 위험을 완화시키는 것은 가능하다. 문제 해결의 열쇠는 바로 추상화다.

추상화에 의존하자 (10장 상속과 중복 코드)

문제를 해결하는 가장 일반적인 방법은 자식 클래스가 부모 클래스의 구현이 아닌 추상화에 의존하도록 만드는 것이다. 정확하게 말하면 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정해야 한다.

개인적으로 코드 중복 제거를 위해 상속을 도입할 때 따르는 두 가지 원칙이 있다.

  • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라. 메서드 추출을 통해 두 메서드를 동일한 형태로 보이도록 만들 수 있다.

  • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것 보다 자식 클래스의 추상적인 메서드를 부모 클래스로 올리는 것이 재사용과 응집도 측면에서 더 뛰어난 결과를 얻을 수 있다.

추상화에 의존하기 위해 가장 먼저 해야할 일은 중복 코드 안에서 차이점을 별도의 메서드로 추출하는 것이다. 이것은 흔히 말하는 변하는 것으로 부터 변하지 않는 것을 분리하라 또는 변하는 부분을 찾고 이를 캡슐화 하라 라는 조언을 메서드 수준에서 적용한 것이다.

부모 클래스, 자식 클래스 모두 추상화에 의존하게 되면 각 클래스들은 서로 다른 변경의 이유를 갖게된다. 부모 클래스는 공통 코드의 구현이 바뀔 경우에만 변경의 원인을 갖게되고, 자식 클래스들은 추상화의 구현(추상 메서드의 구현)이 바뀔 경우에만 변경의 원인을 갖게된다. 따라서 각 클래스들이 단일 책임의 원칙을 준수하기 때문에 응집도가 높아진다.

이로써 부모 클래스는 자신의 내부에 구현된 추상 메서드를 호출하기 때문에 추상화에 의존한다. 또한 의존성 역전 원칙도 준수하는데, 상위 수준을 구현하는 부모 클래스가 세부 수준을 구현하는 자식 클래스에 의존하는게 아니라 자식 클래스들이 추상화인 부모 클래스에 의존하기 때문이다.

지금까지 살펴본 모든 장점들은 클래스들이 추상화에 의존하기 때문에 얻어지는 장점이다. 상속 계층이 코드를 진화시키는 데 걸림돌이 된다면 추상화를 찾아내고 상속 계층 안의 클래스들이 그 추상화에 의존하도록 코드를 리팩터링 하라. 차이점을 메서드로 추출하고 공통적인 부분은 부모 클래스로 이동하라.

클래스는 메서드 뿐 아니라 인스턴스 변수도 함께 포함한다. 따라서 클래스 사이의 상속은 자식 클래스가 부모 클래스가 구현한 행동 뿐만 아니라 인스턴스 변수에 대해서도 결합되게 만든다.

하지만 인스턴스 초기화 로직을 변경하는 것이 두 클래스에 동일한 로직 코드를 중복시키는 것 보다는 현명한 선택이다.


합성과 유연한 설계(11장)

코드 재사용 측면에서 합성상속보다 더 좋은 방법이다.

상속은 부모 클래스와 자식 클래스 사이의 관계가 컴파일 타임에 고정되지만, 합성은 두 객체 사이의 의존성이 런타임에 해결된다. 바로 이 점이 상속보다 합성이 보다 유연하고 변경하기 쉬운 설계를 가능하게 한다.

합성과 상속은 재사용의 대상이 다르다. 상속은 부모 클래스 안에 구현된 코드 자체를 재사용하지만 합성은 포함된 객체의 퍼블릭 인터페이스를 재사용한다. 따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경할 수 있다.

코드 재사용을 위해 상속을 사용하게 되면 클래스 폭발 문제 또는 조합 폭발 문제에 부딪힐 수 있다. 언급한 바와 같이 상속의 경우 부모 클래스와 자식 클래스가 컴파일타임에 고정되기 때문에 런타임에 기능들의 조합을 바꿀 수 없다. 즉, 여러 기능의 조합을 하나의 클래스(자식 클래스)안에 밀어넣는 방식이기 때문에 새로운 기능이 추가되고 조합이 다양해 질 때 마다 조합의 수 만큼 자식 클래스를 새로 생성해야 한다.

단순히 기능의 확장 측면에서만 보았을 때에는 크게 문제가 되지 않을 수 있다. 문제는 상속으로 인해 중복 코드합성에 비해 더 늘어날 수 있다는 점이다. 또한 부모 클래스의 구현에 의존하기 때문에 부모 클래스의 구현이 조금이라도 변경되는 경우 자식 클래스에 대해 파급 효과가 미치지 않는지 하나하나 확인해야 한다.

합성은 컴파일 타임 관계를 런타임 관계로 변경함으로써 클래스 폭발 문제를 해결한다.

합성을 사용하면 구현이 아닌 퍼블릭 인터페이스에 대해서만 의존할 수 있기 때문에 런타임에 객체의 관계를 변경할 수 있다. 즉, 의존성 주입 시 어떤 객체를 주입하느냐에 따라 다양한 조합이 가능하다.

합성의 진가는 새로운 클래스를 추가하거나 수정할 때 더욱 드러난다. 상속은 새로운 기능을 추가하기 위해 상속 계층에 불필요할 정도로 많은 클래스를 추가해야만 했다. 합성을 기반으로 한 설계는 새로운 기능을 위해서 단 하나의 클래스만 추가하면 해결되는 경우가 많다.

따라서 객체 합성이 클래스 상속보다 더 좋은 방법이다.

상속은 부모 클래스의 세부적인 구현에 자식 클래스를 강하게 결합시키기 때문에 코드의 진화를 방해한다.

코드를 재사용하면서도 건전한 결합도를 유지하기 위해 더 좋은 방법은 합성을 사용하는 것이다. 상속이 구현을 재사용하는데 비해 합성은 객체의 인터페이스를 재사용 한다는 점을 반드시 유념하자.

그렇다면 상속은 사용하면 안되는 것일까? 상속을 사용해야 하는 경우는 언제일까? 이 의문에 답하기 위해서는 먼저 상속을 “구현 상속”과 “인터페이스 상속” 두가지로 나눠야 한다. 그리고 이번에 언급했던 상속의 모든 단점들은 “구현 상속”에 국한된다.


상속은 서브타입 계층 구축을 위해서 사용한다(12장 다형성)

상속의 진정한 목적은 코드 재사용이 아니라 다형성을 위한 서브타입 계층을 구축하는 것이다. 상속이 클래스들을 계층으로 쌓아 올린 후에 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다.

다시한번 강조하지만 상속의 목적은 코드 재사용이 아니다. 상속은 프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위한 것이다. 타입 계층에 대한 고민 없이 코드를 재사용하기 위해 상속을 사용하면 이해하기 어렵고 유지보수하기 버거운 코드가 만들어질 확률이 높다.

상속의 메커니즘을 이해하는 데 필요한 몇 가지 개념은 아래와 같다.

  • 업캐스팅
  • 동적 메서드 탐색
  • 동적 바인딩
  • self 참조
  • super 참조

데이터 관점, 행동 관점의 상속 (12장 다형성)

데이터 관점의 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함 한다고 볼 수 있다.

행동 관점의 상속은 부모 클래스의 퍼블릭 메서드를 자식 클래스의 퍼블릭 인터페이스에 포함 한다.

부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐진다고 표현했지만 실제로 클래스의 코드를 합치거나 복사하는 작업이 수행되는 것은 아니다. 그렇다면 어떻게 부모 클래스에서 구현한 메서드를 자식 클래스의 인스턴스에서도 수행 할 수 있는것일까? 그 이유는 “런타임”에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문이다.

업캐스팅과 동적 바인딩

코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 업캐스팅동적 바인딩이라는 메커니즘이 작동하기 때문이다. 업캐스팅은 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다. 동적 메서드 탐색은 부모 클래스의 타입에 대해 메시지를 전송하더라도 실행 시에는 실제 클래스를 기반으로 실행될 메서드가 선택되게 해준다.

동적 바인딩

객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다. foo.bar()라는 코드를 읽는 것만으로는 실행되는 bar가 어떤 클래스의 메서드인지 판단하기 어렵다. foo가 가리키는 객체가 실제로 어떤 클래스의 인스턴스인지를 알아야 하고 bar 메서드가 해당 클래스의 상속 계층의 어디에 위치하는지를 알아야 한다. 이처럼 실행될 메서드를 런타임에 결정하는 방식을 동적 바인딩이라고 부른다.

동적 메서드 탐색(self 참조)과 다형성

객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.

  • 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사한다. 존재하면 메서드를 실행하고 탐색을 종료한다.

  • 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때 까지 상속 계층을 따라 올라가며 계속된다.

  • 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단한다.

메시지 탐색과 관련하여 self 참조에 대한 이해가 중요하다.

객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 “객체”를 가리키도록 한다.

동적 메서드 탐색은 self가 가리키는 객체의 클래스에서부터 시작되어 상속 계층의 역방향으로 진행되며, 메서드 탐새이 종료되는 순간 self 참조는 자동으로 소멸된다.

시스템은 class 포인터, parent 포인터, self 참조를 조합하여 동적으로 메서드를 탐색한다.

자동 메시지 위임과 동적 문맥

  • 자동 메시지 위임 : 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받으면 상속 계층을 따라 부모 클래스에게 처리를 위임한다.

  • 동적 문맥 : 메서드를 탐색하기 위해 동적인 문맥을 사용한다. 메서드를 수신했을 떄 실제로 어떤 메서드를 실행할지 결정하는 것은 컴파일 타임이 아니라 런타임에 이루어지며 메서드 탐색을 위한 경로(문맥)은 self 참조를 이용해서 결정한다.

따라서 메서드 탐색을 위한 문맥(흐름)은 동적으로 이루어지며 여기서 가장 중요한 역할을 하는 것이 self 참조이다.

중요한 것은 메시지를 수신한 “객체의 타입”이 무엇이냐에 따라 메서드 탐색을 위한 문맥(경로)가 동적으로 바뀐다는 것이다. 그리고 이 동적인 문맥(경로)를 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 “self 참조”이다.

동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다.

self 전송

self 전송은 현재의 클래스 내의 메서드를 호출하는 것이 아니라 현재의 객체에게 메시지를 전송하는 것이다. 현재 객체란 바로 self 참조가 가리키는 객체이다.

self 전송을 이해하기 위해서는 self 참조가 가리키는 바로 그 객체에서부터 메시지 탐색을 다시 시작한다는 사실을 기억해야 한다.

self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킨다.

“업캐스팅”과 “동적 바인딩”이라는 언어적인 특성과 실행 시점에 적절한 메서드를 선택하는 “동적 메서드 탐색”을 혼합해서 동일한 코드를 이용해 서로 다른 메서드를 실행하는 것이 가능해진다. 객체 지향 프로그래밍 언어는 이와 같은 메커니즘의 도움을 받아 “동일한 메시지에 대하여 서로 다른 메서드를 실행” 할 수 있는 “다형성”을 구현하는 것이다.

포워딩과 위임

객체가 다른 객체에게 요청을 처리할 때 인자로 self를 전송한다면 위임이라고 부른다. 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.

반면 포워딩의 경우 인자로 self를 전달하지 않는다. 이것은 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요가 없고 단순히 코드를 재사용 하고 싶은 경우라고 할 수 있다.

“위임”은 객체 사이의 동적인 연결관계를 이용해 상속을 구현하는 방법이다.


객체지향 패러다임 관점에서의 타입

객체의 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.

객체지향에서는 객체가 수신할 수 있는 메시지를 기준으로 타입을 분류하기 때문에 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.

객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.

객체를 바라볼 떄는 항상 “객체가 외부에 제공하는 행동에 초점”을 맞춰야 한다. 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이라는 사실을 명심하자.

참고 및 출처

  • 오브젝트