클래스를 이야기 할 때, 부모클래스와 자식클래스라는 이야기를 많이 한다.
이를 빌어 쉽게 이야기하면 자식클래스는 부모클래스를 상속하여 구현한다. 라고 말한다.
좀더 프로그래머의 관점에서 보도록하자.
1. 상속은 부모 클래스로 기본적인 기능을 구현한다. 그리고 자식 클래스로 상속받고 좀 더 특화된 기능을 구현한다. 자식 클래스는 부모 클래스를 특수화한다고 생각 할 수 있다.
2. 부모클래스를 기본적인 기능으로 구현할 때 공통된 것을 추출하여 구현할 수 있겠다. 즉 자식클래스는 부모클래스의 일종이 아니다. 같은 것이 아니란 이야기이다.
3. 공통된 부분을 부모클래스가 가져가고 추후에 각 자식클래스는 추가되거나 수정되는 기능을 구현하면 효율이 좋다.
이렇게 상속을 이용하면 높은 자유를 제공한다. 하지만 상속을 많이 사용하면 코드가 복잡해지고 이를 정상적으로 동작시키도록 제어도 추가해야 할 수 있다.
상속과 관련하여 리스코프의 치환 원칙이라는 것을 주의해야 할 사항으로 자주 언급된다.
리스코프의 치환 원칙은 CLU라는 언어의 설계자인 Liskob가 1987년에 제창한 하였다.
T형의 객체 x에 관해 어떤 속성 q(x)가 항상 참이라고 한다.
S가 T의 파생형이면, S형의 객체 y의 속성 q(y)가 항상 참이어야 한다.
이는 어떤 클래스 T의 객체인 x의 속성이 참이라면, 그럴 상속받은 자식 클래스 S의 해당 객체도 항상 참이어야 한다 라는 말이다.
즉 현실세계에서는 모르겠지만, 객체지향에서의 상속은 부모에게 구현되어 있는 속성들에 대해서는 자식들의 것들도 모두 만족해야 한다는 이야기이다.
결국 이런 상속관계와 형의 부자관계간 정합성을 유지하는게 중요하다.
이를 위해 상속은 is-a관계를 성립해야 한다. 라고 표현한다. 그리고 클래스 상속과 형 구조의 정합성을 유지하기에는 굉장히 어렵다.
자식으로 진화할 수록 할 수있는 기능이 다양해질 수 있다.
예를 살펴보자.
Windows 환경에서 창(window)이라는 부모 클래스 A가 있다.
이를 상속받는 자식 클래스가 두개있다. 하나는 메뉴바가 있는 클래스 B이고 하나는 스크롤바가있는 클래스 C이다.
이 상태에서 메뉴바도 있고 스크롤바도 있는 클래스 D를 만들고 싶다.
만약 D를 구현하기 위해서 A를 상속받은 후 B, C 클래스의 내용을 구현한다고 해보자. 이는 불필요할 수 있다. 왜냐면 D가 B,C 를 상속받을 수 있기 때문이다. 이를 다중상속이라고 한다. 다중상속을 하면 별도의 코드를 다시 만들 필요가 없게된다.
Python 표준 라이브러리에서 사용되고있는 다중상속의 예를 보자.
class ForkingUDPServer(ForkingMixIn, UDPServer): Pass
class ForkingTCPServer(ForkingMixIn, TCPServer): Pass
class ThreadUDPServer(ThreadingMixIn, UDPServer): Pass
class ThreadTCPServer(ThreadingMixIn, TCPServer): Pass
이 라이브러리들은 통신에 TCP, UDP를 이용할지와, 병렬처리방법으로 fork를 이용할지 thread를 이용할지에 관한 내용이다.
이는 각 부분의 속성을 하나씩 속성받아 구현된 함수이다.
이렇게하면 기존의 코드를 유용하게 그대로 사용할 수 있다.
하지만 다중 상속에는 주의할 점이 있다. A 클래스와 B 클래스모두 x라는 이름을 갖는 변수가 있다고 생각해보자. 이 둘을 상속받는 클래스 C에서 x를 참고할때 과연 어느 부모에게서 참조를 해야하는 것일까?
Java에서는 이런 문제를 고려하여 다중상속을 금지하기로 했다. 하지만 다중상속이라는 편리함도 버리게 된다. 즉 코딩 시 재사용을 목적으로 한 상속을 피하려는 의지가 강하다. 예를들어 Abstract Window Toolkit(AWT)라는 그래픽 툴킷에서는 다시 상속한 각종 메소드를 오버라이드하여 사용한다라는 규칙을 만들기도 하였지만 이클립스 등에서 사용되는 Standard Wiget Toolkit(SWT)은 계승하면 안된다라는 규칙이 있다.
이를 대신하여 발달된 것이 delegation 이라는 개념이다.
사용하고 싶은 코드를 가지고있는 클래스 객체를 만들고, 필요에 따라서 해다 클래스에 처리를 맡기는 것이다. 이렇게하면 상속을 사용함으로써 형이나 이름 공간까지 함께 계승하는 것이 문제의 원인이기 때문에 단순히 객체만 보유하면 위의 문제를 막을 수 있다.
자바의 코드를 예를 들어보자.
pubilc class TestDelegate {
public static void main(String[] args) {
new UseInheritance().useHello(); // -> hello!
new UseDelegate().useHello(); // -> hello!
}
}
class Hello{
public void hello(){
System.out.println("hello!");
}
}
class UseIngeritance extends Hello{
public void useHello(){
hello();
}
}
class UseDelegate{
public void useHello(){
Hello h = new Hello();
public void useHello(){
h.hello()
}
}
}
위의 코드는 상속을 사용한 코드 재사용과, delegation을 사용한 코드 재사용을 나타내고 있다.
실제로 상속보다는 delegation을 이용한 방법이 훨씬 낫다. delegation의 참조도 소스에 하드코딩하는 것이 아닌 설정 파일을 사용해서 실행시에 주입하는 것이 편하다. 이런 발상으로 의존성 주입(dependency injection)이 탄생했다.
하지만 java에서도 다중상속을 금지하고 있긴하지만 interface라는 개념으로 다중상속을 구현한다.
인터페이스는 코드를 가지지 않는 클래스이며 반드시 자식 클래스에서 이를 구현해야한다. 이를 추상클래스의 의미이다.
단도직입적으로 말하자면 상속을 코드 재사용을 위해서 하고싶다. 다른 방법은 없을까? 라고 생각할 수 있겠다.
이는 interface를 사용해 has-a관계를 가지며 구현할 수 있겠다.
java 에서는 상속(abstract)를 extends 키워드로, interface는 implement 키워드로 구현한다.
.
참고. 코딩을 지탱하는 기술
'Development > Etc.' 카테고리의 다른 글
type(형) (0) | 2016.02.06 |
---|---|
언어별로 문자열 처리 (0) | 2016.02.06 |
전역스코프 동적스코프 정적스코프 (0) | 2016.02.06 |
언어별 클래스의 의미 (0) | 2016.02.06 |
이벤트기반과 메세지기반 아키텍처의 차이 (0) | 2016.02.06 |