티스토리 뷰

책도 읽고

오브젝트 (4) - 상속 주의사항

사용자 eungding 2021. 2. 19. 12:52
728x90
반응형

 


좋은 부분이 너무 많아서 책을 많이 옮겨왔는데 혹시 문제가 된다면 말씀해주세요-!

제가 조금 재구성한 부분이 있기때문에 꼭 책을 읽어보시는 것을 추천드립니다.


[ 요약 ]

 

상속 주의사항

 

1. 상속은 코드 재사용이 아니라 타입 계층을 구현하기 위해 쓴다.  (서브클래싱이 아니라 서브타이핑을 한다.)

2. is-a 관계가 언어적으로 맞다고 상속관계를 막 쓰면 안된다.  행동호환성을 고려해야한다. 

 

[1] 상속의 목적

 

상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이여야한다. 

 

상속은 코드를 쉽게 재사용할 수 있는 방법을 제공하지만 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 설계의 변경과 진화를 방해한다. 

반면 타입 계층을 목표로 상속을 사용하면 다형적으로 동작하는 객체들의 관계에 기반해 확장 가능하고 유연한 설계를 얻을 수 있게 된다. 

 

[ 구현 상속과 인터페이스 상속 ]

 

상속을 구현상속(implementaion inheritance)과 인터페이스 상속(interface inheritance)으로 분류할 수 있다.

 

1. 구현상속 subclassing

 

구현 상속을 서브클래싱(subclassing)이라고 부른다.

순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 경우를 가리킨다.

자식 클래스와 부모클래스이 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.

 

 

2. 인터페이스 상속 subtying

 

인터페이스 상속을 서브타이핑(subtying)이라고 부른다.

다형적인 협력을 위해 부모 클래스와 자식클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 경우를 가리킨다. 

자식과 부모클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다. 

 

 

상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용되야한다.

대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것을 오해다.

인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다. 

 

참고로 코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법이다.

(합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.)

 

 

[2] 리스코프 치환 원칙

 

1988년 바바라 리스코프는 올바른 상속 관계의 특징을 정의하기 위해 리스코프 치환 원칙을 (Liskov Substitution Principle, LSP)를 발표했다. 바바라 리스코프에 의하면 상속관계로 연결된 두 클래스가 서브타이핑 관계를 만족시키기 위해서는 다음의 조건을 만족시켜야한다. 

 

" T에 의해 정의된 모든 프로그램 P에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입이다."

 

 

서브클래싱은 리스코프 치환 원칙을 위반하고

서브타이핑은 리스코프 치환 원칙을 따른다. 

 

[3] is-a 관계와 행동호환성

 

is-a 관계로 표현된 문장을 볼 때마다 문장 앞에 "클라이언트 입장에서" 라는 말이 빠져있다고 생각하라.

클라이언트 관점에서 행동이 호환될 경우에만 is-a 관계로 표현 가능하고 자식 클래스가 부모 클래스 대신 사용될 수 있다. 

이름이 아니라 행동이 먼저다. 

 

일반적으로 클라이언트를 고려하지 않은 채 개념과 속성의 측면에서 상속 관계를 정할 경우

리스코프 치환원칙을 위반하는 서브클래싱에 이르게 될 확률이 높다.

 

[ 예제 1 ] 

 

리스코프 치환 원칙을 위반하는 고전적인 사례 중 하나로

"정사각형은 직사각형이다(Square is a Rectangle)" 가 있다. 

 

Rectangle을 상속받은 Square클래스를 추가했다고 해보자.

 

Rectangle과 협력하는 클라이언트는 직사각형의 너비와 높이가 다르다고 가정한다.

그래서 직사각형의 너비와 높이를 서로 다르게 설정하도록 프로그래밍할 것이다. 

public void resize(Rectangle rectangle, int width, int height) {
   rectangle.setWidth(width);
   rectangle.setHeight(height);
   assert rectangle.getWidth() == width && rectangle.getHeight() == height;
}

 

그러나 위 코드에서 resize 메서드의 인자로 Rectangle 대신 Square를 전달한다고 가정해보자.

Square의 setWidth 메서드와 setHeight 메서드는 항상 정사각형의 width와 height를 같게 설정한다.

위 코드에 따르면 Square의 width와 height는 항상 더 나중에 설정된 height의 값으로 설정된다.

따라서 다음과 같이 width와 height의 값을 다르게 설정할 경우 메서드 실행이 실패하고 말 것이다. 

 

Square square = new Square(10, 10, 10):
resize(square, 50, 100);

 

클라이언트 관점에서 Rectangle 대신 Square을 사용할 수 없기 때문에

Square는 Rectangle이 아니다. ( Square is a Rectangle 이라는 어휘적인 정의은 성립한다. 하지만 이름이 아니라 행동이 먼저다.)

 

그리고 두 클래스는 리스코프 치환 원칙을 위반하기 대문에 서브타이핑 관계가 아니라 서브 클래싱 관계다.

 

 

[ 예제 2 ]

 

"펭귄은 새다" 라는 어휘적인 정의은 성립한다.

하지만 펭귄은 새지만 날 수 없는 새이다.

 

어떤 애플리케이션에서 새에게 날 수 있다는 행동을 기대하면 펭귄은 새의 서브타입이 될 수 없다.

 

반면 어떤 애플리케이션에서 새에게 기대하는 행동이 단지 울음 소리를 낼 수 있다는 것이라면

새와 펭귄을 타입 계층으로 묶어도 된다. 

 

코드는 책에서 확인!

 

 

[ 결론 ]

 

따라서 슈퍼타입과 서브타입 관계에서 is-a 보다 행동호환성이 더 중요하다.

어떤 두 대상을 언어적으로 is-a라고 표현할 수 있더라도

일단은 상속을 사용할 예비 후보 정도로만 생각하라.

너무 성급하게 상속을 적용하라고 서두르지마라 

728x90
반응형
댓글
댓글쓰기 폼