자바가 다중 상속을 지원하지 않는 이유?

이 글은 LangDev의 토론을 뒤늦게 읽고는 너무 늦게 토론에 참여하기 뭐해 극적거리기 시작해서 미완성인 채로 남겨 두었다가, KSUG의 토론을 보고 마무리 지어야겠다는 생각이 들었고, 오늘에야 시간을 나서(내서?) 결론 내려 한다.

자바 개발자라면 누구나 알겠지만 자바는 클래스를 하나만 상속해서 확장할 수 있다.

class GundamMk2 extends Gundam {
...
}

클래스를 둘 이상 상속 받을려고 하면 컴파일이 안 된다.

class SpaceGundamV extends Gundam, Valkyrie {
...
}

자바를 첫 프로그래밍 언어로 배웠거나 자바로 객체지향 프로그래밍에 입문한 개발자라면 별다른 의문 없이 이런 제약을 받아들였겠지만 C++나 다른 다중 상속을 지원하는 언어를 먼저 익힌 사람이라면 궁금해하거나 이상하게 여길지 모르겠다.

위 그룹스의 글에서도 그렇고 다른 다중 상속 관련 토론을 보면 늘 다중 상속에는 "다이아몬드 문제"가 있다는 얘기가 나온다. 더 나아가 "다이아몬드 문제" 때문에 자바가 다중 상속을 지원하지 않는다고 주장하는 사람도 상당히 많다.

그럼 다이아몬드 문제가 뭘까? 한번 알아보자...니 귀찮다. -_-); 그래서 위키백과 링크 하나 달랑 던져 놓고 넘어가겠다.

http://en.wikipedia.org/wiki/Diamond_problem

위키백과의 글을 보면(이라고 쓰고 감상하면 이라고 읽는다. 영어따위!) 알겠지만, 설명이, 짧다(잉?).  별문제 아닌 것 같다. 더구나 밑에 다중 상속을 지원하는 여러 언어에서 이 문제를 어떻게 다루는지 설명하고 있(는 것 같)다. 결국, 다중 상속에는 다이아몬드 문제가 일어날 수 있지만 이 문제는 여러가지 방법으로 해결할 수 있는 사소한 문제일 뿐이(라고 추측할 수 있)다. 그러니 이 문제 때문에 자바가 다중 상속을 지원하지 않는다는 주장은 그리 설득력이 없어 보인다.

KSUG 메일링에서 몇몇 분은 상속 자체가 쉽게 문제로 발전할 수 있는 여지가 있으니 다중 상속은 더욱 취약하고, 그래서 애초에 다중 상속을 금지한 것 아니냐고 말한다.

혹시 상속보다 구성을 사용하자는 말을 들어봤는지 모르겠다. 객체지향 프로그래밍의 상징처럼 여겨지는 상속을 가능하면 쓰지 말고 객체 구성을 활용하자는 격언이다. 상속은 코드를 재사용하는 멋진 아이디어지만, (템플릿 메서드 패턴처럼) 상속해서 쓰도록 미리 고려된 객체가 아니라면 무척이나 불안전하고 위험한 부작용이 생길 수 있다. 캡슐화가 깨져 조상 객체와 피상속 객체 사이에 강한 결합이 생길 가능성이 있는 것이다. 그래서 이런 예상 밖의 부작용을 막으려고 구상 클래스의 public 메서드를 final로 만들어 오버라이드하지 못하게 막기도 한다. 아니면 최소한 상속했을 때 문제가 생기지 않도록 내부 정보를 문서화하라고도 한다.

상속이 생각보다 그리 유용하지 않고 위험하기까지 하니 다중 상속은 얼마나 안 좋겠냐는 생각은, 막연하지만, 어느 정도 타당성이 있어 보이고, 다중 상속을 제거했다는 자바 설계자의 선택이 현명해 보이기도 하다.

그런데 애석하게도 다중 상속은 아주 유용하다. 단일 상속만 허락한다면 객체가 너무 경직된다. 아무리 객체의 단일 책임의 원칙을 강조한다고 해도 객체에 복합적인 특성을 부여해야 할 일이 많다. 객체 모델링을 할 때를 예로 들면, 말은 동물로 분류되면서 동시에 탈 것으로 분류될 수 있다. 스마트 폰은 정보기기면서 전화기다.

또, 나는 컴포넌트의 경계(boundary) 객체를 내부적으로 문맥(context) 객체로 쓰는 습관이 있는데, 이때도 다중 상속은 유용하다. 한 객체가 컴포넌트 외부와의 인터페이스 역할을 하면서 내부적으로는 문맥을 제공하니 한 객체가 바라보는 관점에 따라 두 가지 역할을 하는 샘이다. GUI의 콤보 박스는 입력창과 목록의 특성을 모두 가지고 있다고 할 수 있다.

아무리 객체가 현실을 그대로 모델링할 수 없다고 말하지만 어느 정도 다면성을 부여할 수 없다면 설계가 복잡해진다. 특히 정적 타이핑 언어에서는......

그래서 자바는 다중 상속을 지.원.한.다. 두둥!!!!

눈치 빠른 사람은 무슨 소린지 알아챘겠지만, 확실히 자바는 다중 상속을 지원하는 언어다. 다중 상속을 지원할 뿐 아니라 이런저런 문제를 일으키지 않도록 특별히 고려하기까지 하면서 말이다. 사실 자바를 비롯해 다중 상속을 지원하는 언어들은 다이아몬드 문제 같은 다중 상속의 문제를 회피하기 위해 나름의 장치를 마련해 놨다. 자바도 마찬가지로 자신만의 독특한 방식으로 다중 상속을 구현했다. 자바의 방식은 다이아몬드 문제뿐 아니라 다중 상속의 복잡성도 해결할 수 있다.

자바는 부모 객체가 특별한 조건에 부합될 때에만 다중 상속을 할 수 있도록 언어 차원에서 규제를 걸어 놨다. 더 정확히 말하자면, 자바에서 다중 상속을 하려면 부모 객체가 추상 객체여야 한다. 구상 객체는 다중 상속을 받지 못한다. 그것도 그냥 추상 객체면 되는 게 아니라 순수한 추상 객체여야 한다. 순수 추상 객체라는 말은 어떤 로직도 가지고 있지 말아야 한다는 의미로, 자바에서는 이런 추상 객체를 부르는 용어가 따로 있다. 바로 인터페이스다.

class ZetaGundam extends Gundam implements Fighter, Transformable  {
}

이쯤에서 내 말에 동의하는 사람도 있겠지만 버럭할 사람도 있으리라. 당신 말이 맞다. 자바는 다중 상속을 지원하지 않는다. 사실 상속도 지원하지 않는다. 무슨 말이냐고? 다음 코드가 자바에서 컴파일될까?

class RichChild inherits RichFather {
}

자바는 부자 아빠의 재산을 그 자녀가 물려받도록 허락할 생각이 없는 모양이다.

말장난을 한 김에 조금 더 해보겠다. 자바는 '상속'을 허락하지 않는 대신 '확장'할 수 있도록 하기로 했다. 우리는 습관처럼 extends라는 구문을 '상속'이라는 의미로 아무 생각 없이 쓰지만, 분명히 상속이 아니라 확장이다.

그게 그거라고?

아니다.

상속은 내가 물려받는 재산에 초점이 있다. 하지만 확장은 내가 해야 할 일의 성격에 초점이 있다.

졸리니 그냥 내가 말하려는 바를 말하겠다.

모 출판사의 편집자도 읽었다는 명저 Effective C++ (3판)의 32번과 38번 항목은 이렇다.

항목 32: public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자
항목 38: "has-a(...는 ...를 가짐)" 혹은 "is-implemented-in-terms-of(...는 ...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자

재산을 물려받는 '상속'이란 비유는 has-a가 될 가능성이 많다. '확장'이란 비유는 명백히 is-a를 뜻한다.

is-a는 부모 객체 A를 자식 객체 B가 상속했을 때 "B는 A다"라고 할 수 있음을 뜻하는 말이다. 팩토리 객체에 Pool 기능을 추가해 성능을 높이는 경우를 예로 들 수 있겠다. has-a로 변질되는 때는 마치 인간의 몸에 외계 생물의 DNA가 들어와 유전자를 변형시켜 인간이 아닌 뭔가로 바꿔버리는 경우다. 예전에 본 한 레가시 코드가 기억나는데, DB 질의 결과로 반환되는 객체가 RecordSet 형태였다. 소스를 열어보니 HashMap을 확장, 아니, 상속해서 만들었지만 어느 곳에서도 이 객체를 Map으로 다루는 곳은 없었다.

상속의 이런 측면을 메서드 수준에서 보면 객체지향 원리 중 하나인 리스코프 치환 원칙을 따른다고 할 수 있고 조금 거시적인 관점에서 보면 변경에 대해 닫혀있고 확장에 대해서는 열린, 개방-폐쇄 원칙 준수한다고 할 수 있다. 상속 받은, 아니, 확장한 자식 객체가 여전히 외부에서 봤을 때 부모 객체와 같게 보인다면 결국 좋은 객체지향 원칙을 준수하게 되는 것이다.

내 결론은 이렇다. 우리가 '상속'이란 말을 is-a로 사용한다면, 자바는 분명히 다중 상속을 지원하는 언어다. 반대로, has-a로 변질될 위험성이 있는 의미로 사용한다면, 자바는 다중 상속을 지원하지 않는 언어다. C++에서는 이 부분을 개발자가 실천해야할 규범으로 열어 놓았다면 자바는 언어차원에서 조금 더 강제할 뿐이다. 자바의 다중 상속 지원 방식이 최선인지는 모르겠다. 하지만 분명히 좋은 방법 중 하나라고 생각한다.

최근에 스칼라라는 언어를 공부하면서 트레잇(trait)을 알게 됐는데 이 또한 특수한 형태의 추상 객체로서 나름이 방식으로 다중 상속을 지원한다. 스칼라 진영 측에서는 인터페이스의 장점을 살리면서 단점을 보완한, 개선된 방식이라고 말하지만 나는 아직 확신이 안 선다. 뭐 똑똑한 사람들이 더 낫다고 하니 그런가보다 싶을 뿐이다. 사실 언어가 제공하는 장치를 너무 비판하는 것도 별로 지혜롭다고 할 수 없다. Effective C++에 반복해서 나오는 말처럼 "심사숙고"해서 잘 쓰면 된다.

by 박성철 | 2011/07/15 02:44 | 프로그래밍 이야기 | 트랙백 | 덧글(15)

트랙백 주소 : http://gyumee.egloos.com/tb/3200829
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by 박꿈 at 2011/07/15 05:00
좋은글 잘 보고 갑니다.
Commented by 꼼셩꼼셩 at 2011/07/15 08:25
유용한 글이네요. 상속이 아닌 확장의 개념으로 보면 확실히 이해하기 쉬워지네요.
Commented by 도오 at 2011/07/15 10:12
좋은 글 잘 보고 갑니다
감사합니다 ^^
Commented by 아르 at 2011/07/15 12:24
예제 클래스명에 심호함이 담겨있군요.. 잘읽었습니다 ㅋㅋ
Commented by SiroTan。◕‿‿◕。 at 2011/07/15 13:19
어떻게 이해하느냐에.. 차이 하지만 분명 익숙해진걸 못하면 손이 거부하더군요.
좋은글 잘 봤습니다.
Commented by 긁적 at 2011/07/15 13:31
이거 좋은 글이네요. 잘 읽고 갑니다.
Commented by arload at 2011/07/15 15:06
안녕하세요 :)
저도 패턴에 아주 관심이 많은 지라. 글의 맥락과는 다른 애기지만, 아래 쓰셨던 내용은
"혹시 상속보다 구성을 사용하자는 말을 들어봤는지 모르겠다. 객체지향 프로그래밍의 상징처럼 여거지는 상속을 가능하면 쓰지 말고 객체 구성을 활용하자는 격언이다. " 다른분에게 약간 오해의 여지가 있을수도 있어서요 ^^;;

그럼 두번째 원칙을 다음과 같이 명확히 말하면..

Favor Object Composition (with Interface Inheritance) over Implementation Inheritance.

Favor Object Composition (withSubtyping) over Subclassing.

자세한 내용은 아래를 참조해 주세요!!
http://arload.wordpress.com/2009/02/18/misconception_of_gof_dp/
구현 상속 보다는 (인터페이스 상속을 기반으로한 ) 객체 조합을 선호하라는 말로 해석되어 질수 있습니다.

다중 상속은 여러가지 이유가 있을수도 있지만, 항상 모든게 Trade-Off이지만, ISP와 SRP 사이의 갈등을 해결할수도 있지만 반대로, 문제를 만들수도 있어서 조심히 써야 될거 같습니다. :) is-a와 has-a의 비유는 정말 나이스 한거 같습니다!!

나중에 저도 이 표현좀 써먹도록 하겠습니다 .:) 좋은글 정말 잘 읽었습니다.!!

Commented by Raspy at 2011/07/16 00:45
말장난이라곤 하셨지만, 따지고보면 실제론 이게 당연한 거겠죠 ㅋㅋ
문득 영미권 프로그래머들은 어떤식으로 받아들이는지 궁금하네요
Commented by 스쿨쥐 at 2011/07/17 22:25
좋은 글 잘 읽었습니다. ^^ is-a와 has-a에 대해서 새로운 눈을 뜨게되었네요. 감사합니다. ^^
저도 간혹 좀 써먹겠습니다. ^^
Commented by 디키썬 at 2011/07/29 16:49
좋은 글 감사합니다. 인터페이스는 특이한(보통 책에는 특별한이라고 표현하지만) 클래스이므로 자바가 다중 상속을 지원하는 맞습니다.

IS-A와 HAS-A도 좋은 말씀.
extends와 inherit 도 재미있었습니다. ^^
Commented by 홍민희 at 2012/02/23 08:38
글 잘 읽었습니다. 논의에 도움이 될까 해서 부가적인 의견을 써봅니다.

Java는 1995년에 나왔습니다. 다중 상속의 다이아몬드 문제를 해결하기 위한 C3 선형화 알고리즘은 1996 OOPSLA 컨퍼런스에서 Dylan 언어 디자인 팀에 의해 소개되었습니다. 그러므로 Java가 첫 버전 때 다중 상속을 구현하고 싶었어도 그 당시로서는 다이아몬드 문제를 안고 갈 수밖에 없었다고 봅니다. 그런 제약사항이 Java의 다중 상속을 제공하지 않는 결정을 하는 이유 중 하나가 될 수 있었을 것입니다. (그것만이 이유라고 얘기하는 것은 아닙니다.)

또한 Java의 interface는 다중 상속이 아닙니다. 적어도 Ruby의 mixin이나 언급하신 Scala 등에서의 trait 정도가 되어야 다중 상속이 필요한 상황을 커버할 수 있습니다. 예를 들어 제가 블로그에 썼던 글에 ‘다중 상속이 없어서 생기는 제약’에 대해 언급한 적이 있습니다. http://blog.dahlia.kr/post/2968635983

> Java에 mixin이나 다중 상속이 없고 인터페이스만 제공되기 때문에, 굳이 구현할 필요 없는 것들을 구현해야 하고 boilerplate code가 등장하기 시작한다. 예를 들어 java.util.List 인터페이스의 toArray() 메서드는 iterator()와 size()를 구현했으면 알아서 채워질 수 있는 메서드이다.
Commented by 박성철 at 2012/07/08 22:04
@홍민희
요즘 블로그질을 거의 안 하다보니 댓글 다신 것도 늦게 봤고 본 다음에도 답글 다는 데 시간이 걸렸네요.
C3 선형화는 예전에 파이썬 공부하면서 접하긴 했는데 그게 OOPSLA에서 Dylan 언어 발표하면서 소개된 건 줄은 몰랐어요. 그런데 제가 알기로 C3 선형화도 모든 다중 상속 상황을 다 해결해주는 건 아니(라기 보다는 제약이 있)고, C3 선형화가 다중 상속을 선택한 언어에서 선택할 수 있는 훌륭한 직관적인 다이아몬드 문제를 해결할 해법이지만, 다중상속의 복잡성이 가시는 건 아니라고 봐요. 제 말은, (저처럼) 똑똑하지 못한 대부분의 사람들이 엉터리 설계를 할 가능성은 여전히 남는다는 거죠.
인터페이스가 보일러판때기 코드를 양산하는 건 맞지만, 제가 글에 쓴 HAS-A가 아닌 IS-A 관점에서 보면, 분명히 다중상속을 지원하는 게 맞습니다. 말씀하신 상속은 HAS-A의 다중상속이고요. 물론 보통, 상속이라고 하면 HAS-A 관점의 상속이란 사실은 저도 알지만, 그래서 말장난을 해 본 격이긴 해도, Go 언어처럼 아예 상속을 지원하지 않는 언어도 있으니 그냥 철학이 다르다고 보시면 좋을 것 같아요. 보일러판때기 코드를 작성해야 하는 귀찮은 길을 걷는 편이 복잡한 상황을 머리에 달고 살아야 하는 쪽보다 편하다는 선택 말이죠.
(비록 C3 선형화로 다이아몬드 문제가 해결되어) 컴파일러는 다중 상속으로 인한 다이아몬드 문제가 어느정도 해결되었을지 모르지만 사람의 아둔한 머리는 여전히 복잡한 상속관계를 머리에 그리고 있어야 하는 문제가 있어요. 상속으로 인한 강한 결합 문제도 다중 상속에서는 더 커질 수 있고요.
참고로 새로 나올 자바 8에서는 인터페이스의 메서드에 기본 구현을 지정할 수 있는 기능이 생겨서 링크거신 글의 toArray() 같은 부분은 해결이 될 수 있지 않나 싶네요. (되려나?)
Commented by 뭉근 at 2014/01/09 16:44
좋은 글 잘 보고 갑니다. ^^
Commented by at 2016/10/23 19:30
중국 무술의 우아함을 보는 느낌의 글이군요.
Commented by 폭8 at 2019/05/23 21:32
잘봤습니다. 자바는 인터페이스를 통해 다중상속이 가능하도록 하고 있죠. 근데 이 인터페이스란 놈이 추상 함수만 담을 수 있어서 생기는 문제도 하나 있죠. 클래스마다 추상 함수를 일일히 구현해야 한다는점. 때문에 같은 기능이여도 클래스마다 따로따로 구현해줘야 하고 수정할 땐 일일히 다 바꿔야 하죠. 이는 객체 지향의 목적인 코드의 재사용성을 무의미하게 만드는 행위인 것 같아요. 스칼라가 트레이트라는 새로운 유형을 지원하는 것도 이 때문이겠죠. 트레이트는 한마디로 일반적인 함수도 정의할 수 있는 인터페이스인데 이런 문제를 해결하려는 방책이겠죠.

그리고 다중 상속이 기피되는 이유가 하나 더 생각났는데요, 일반적인 상속관계에선 부모는 무조건 메모리 상단에 위치하니 접근하는 위치도 항상 같지만 다중상속에서 두번째 부모부터는 부모 자체만 쓸때랑 메모리에 올라오는 위치가 달라지는 것도 다중 상속이 기피되는 이유일 것 같습니다. 예를 들어 클래스 A를 상속하는 클래스 B는 AB로 나타나서 A를 그대로 써도 지장 없는데 클래스 A, B를 상속하는 클래스 C는 메모리에 ABC 순서로 올라오고 이러면 B의 위치가 밀려나는 문제점이 있죠.

:         :

:

비공개 덧글

◀ 이전 페이지다음 페이지 ▶