개발 공부/상속 : 서브클래싱, 서브타이핑

상속 : 서브클래싱과 서브타이핑

수달하나 2023. 10. 6. 01:30

객체지향을 조금이라도 자세히 공부했다면 상속은 객체지향의 가장 중요한 특징중 하나인 캡슐화를 방해하는 요소라는 것을 알 수 있을 것이다. 그럼에도 불구하고 상속또한 객체지향의 대표적인 특징으로써 존재하고 있다. 상속을 이용하여 코드를 재사용함으로써 중복코드를 없애는 방향으로 개발을 하고 있지만 상속의 본질적 목표는 중복코드를 제거하는 것이 아니다.


서브클래싱 과 서브타이핑

상속을 사용하는 목적을 크게 두 가지로 구분해서 확인 할 수 있다. 첫 번째는 위에서 설명한 것 처럼 중복코드를 줄임으로써 코드의 재사용 성을 높이기 위함이다. 이것이 서브클래싱의 개념이다. 하지만 첫 번째보다 더 중요한 두 번째 목적은 타입 계층을 구현하여 서브클래스가 수퍼클래스를 대체하기 위함이다. 이것이 서브타이핑의 개념이다. 

부모클래스로부터 상속을 받는 자식클래스가 좀더 특정화된 부분을 나타낸다고 생각한다면 부모클래스를 대체한다는 것이 말이 안된다고 생각 할 수 있지만 객체지향적 관점에서 바라본 상속은 클라이언트를 기준으로 하기 때문에 정적 코드와는 다른 지향점을 가지고 있다.

 

객체의 퍼블릭 인터페이스가 객체의 타입을 결정하기 때문에 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다. 상속의 올바른 용도는 이러한 타입 계층을 구현하는 것이다. 상속을 통하여 is-a 관계를 모델링 하며 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가 에 대한 확신이 있어야지 올바른 상속 관계라고 얘기 할 수 있다. 

이것은 곧 추상화를 위한 확장성을 포함하고 있다는 얘기와 일맥상통하다. 


새는 날 수 있으며 펭귄은 새이다.

  • 펭귄은 새다.
  • 새는 날 수 있다.

위 두 가지 사실을 통해서 Bird 클래스는 fly 메서드를 가지고 있고 Penguin 클래스는 Bird 클래스를 상속받아서 구현 할 수 있다.

따라서 펭귄은 날 수 있게 된다 ?

 

펭귄 은 날지 못한다. 따라서 위와같은 상속의 사용 방법은 옳지 못한 방법이다. 상속은 어휘적인 정의가 아닌 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 잘 보여주는 예시이다. 행동의 호환 여부를 판단하는 것은 클라이언트의 관점이다. 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있지만 클라이언트가 두 타입을 동일하게 행동하지 않을 것이라고 생각한다면 두 타입을 타입 계층으로 묶어서는 안된다.

 

잘못된 설계라는 것을 확인한 개발자들은 위와 같은 오류를 제거하기 위해 상속 계층을 유지하면서 해결 할 수 있는 방법을 찾기 시작한다.

 

1 . 오버라이딩 메서드의 빈 내부 구현

첫 번째 방법은 오버라이드한 메서드의 내구 구현을 비워두는 방법이다. 하지만 위와 같은 방법은 모든 새는 날 수 있다 라는 클라이언트의 기대를 만족시킬 수 없기 때문에 올바른 설계라고 할 수 없다.

 

2. 오버라이딩 메서드의 예외 던지기

두 번째 방법은 새로운 메서드를 생성하여 메서드의 내부 구현에 예외를 던지는 것이다. 하지만 위와 같이 비워두는 것과 마찬가지로 예외를 던지는 방법은 Bird 클래스가 예외를 던질 것이라고 기대하지 않기 때문에 클라이언트 관점에서 Bird 와 Penguin의 행동이 호환되지 않는다.

 

3. 전달인자의 타입에 따라 메세지를 전송

Bird 의 타입이 Penguin이 아닐 경우에만 메서드를 전송하도록 하는 이 방법은 만약 날 수 없는 또 다른 새가 상속 계층에 추가된다면 새로운 타입을 체크하는 코드를 또 추가해야 하기 때문에 구체적인 클래스에 대한 결합도를 높이므로 좋은 해결 방법이 아니다.

 

상속의 주 목적인 서브타이핑을 실현 할 수 없는 상황에서 위와같은 임시방편의 해결방법은 추후에 버그와 유지보수에 있어서 큰 어려움을 주기 때문에 위와 같은 문제가 발생한다면 코드를 통해 해결방법을 찾는것보다 상속의 구조 차제를 변경하는 것이 옳다.

 

날 수 없는 새와 날 수 있는 새를 구분 짓고 상속 계층을 분리한다면 서로 다른 요구사항을 가진 클라이언트를 만족시킬 수 있다.

 

혹은 Penguin 을 Bird 와 다른 구현체로 인정하고 각각의 구현체가 상속받을 수 있는 인터페이스를 정의하는 것또한 새로운 해결 방법이 될 수 있다.

위와 같은 방식으로 인터페이스를 통해 각 객체가 가질수 있는 특징들을 더 넓고 다양하게 상속받아 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙인 인터페이스 분리 원칙을 실현 할 수도 있다.


자연어에 현혹되지 말고 요구사항 속 클라이언트의 기대 행동에 집중하라. 리스코프 치환 원칙

새와 펭귄의 연관관계와 마찬가지로 클래스의 이름 사이에 연관성이 있다는 사실은 아무런 의미가 없다. 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안되기 때문이다. 

 

상속을 사용하는 이유의 핵심은 코드의 재 사용성이 아닌 타입 계층을 구현하는 것이고 서브타입이 슈퍼타입을 대체할 수 있어야 하며 클라이언트가 차이점을 인식하지 못한 채 슈퍼타입의 인터페이스를 이용해서 서프타입과 협력할 수 있는 관계를 정의하는 것이다.

클라이언트 입장에서 서브타입은 슈퍼타입의 한 종류 일뿐으로 인식할 수 있어야 한다. 이것을 리스코프 치환 원칙이라 한다.