태그 : oop

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

이 글은 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://c2.com/cgi/wiki?PrinciplesOfObjectOrientedDesign 이 문서와 wikipedia를 참고해서 작성했습니다.

이 문서를 참조해도 도움이 될듯 합니다.
http://labs.cs.utt.ro/labs/acs/html/lectures/5/lecture5.ppt

이 아티클들은 나중에 한번씩 꼭 읽어봐야 할 것들인 듯...
http://www.objectmentor.com/resources/publishedArticles.html

----------------------------------------------------------------------------------

클래스 디자인의 5원리

* SRP (The Single responsibility principle)
모든 객체는 하나의 책임만 지도록 하고 그 객체는 이 책임을 위해 최소한의 서비스만 갖는다. 흔히 여기서 말하는 '책임'을'변경하는 이유'로 바꿔서 표현하곤 한다. 즉 어떤 객체를 변경하는 이유는 한가지 뿐이여야 한다는 것이다.

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

* OCP (The Open Closed Principle)
정의는 '소프트웨어의 구성요소(객체, 모듈, 함수 등)은 확장에 대해서는 열려있지만 변경에 대해서는 닫혀있어야 한다' 이다. 이 말은 소스코드를 고치지 않고 객체의 행동을 바꿀 수 있다는 말로 이해할 수 있다.

소스코드 변경은 코드 리뷰와 단위 테스트를 다시해야 한다. 하지만 이미 검증된 객체를 그대로 놔두고 이를 확장하는 것은 그럴 필요가 없다.

이 아이디어는 두가지로 해석 할 수 있다. 먼저 기존 객체는 변경하지 말고 새로 추가된 기능을 기존 객체를 상속한 새객체에서 구현하는 방식이 있다. 이것을 Meyer's Open/Closed Principle라고 한다. 또 하나는Interface를 사용하는 것인데 정해진 인터페이스는 변경할 수 없고 그 구현은 변경이 가능하다는 것이다. 이것을Polymorphic Open/Closed Principle라고 한다.

개인적으로 Meyer의 것은 좀 이상적이고 상속이 시스템을 복잡하게 한다는 문제로 인터페이스를 많이 쓰는 요즘에는 별로 유용해 보이지 않는다.

http://en.wikipedia.org/wiki/Open_Closed_Principle
http://www.objectmentor.com/resources/articles/ocp.pdf

* LSP (The Liskov Substitution Principle)
어떤 함수 q()에 T타입의 객체 x를 전달해서 잘 작동한다면 T를 상속한 S타입의 객체 y를 전달해도 잘 작동해야 한다는 원리이다.

어찌보면 당연한데 실무를 하다보면 그렇지 않은 경우가 많다. 어떤 메소드 q() 안에서 전달되어온 객체가 T를 상속한 서브클래스 S인지 여부를 판단해서 분기를 처리를 하는 경우가 많이보는 잘못된 예이다.

Code:

public void q(T x)
{
    if(x instance of S)
        do something for S
    else
        do something for T
}


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

* ISP (The Interface Segregation Principle)
한 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스를 통한다.

A라는 클레스가 a라는 인터페이스를 구현했고 B와 C 클래스는 a 인터페이스를 통해서 A에 의존하고 하는데 B는 a에정의된메소드의 절반만 필요하고 나머지는 전혀 사용하지 않고 앞으로도 쓸 일이 없다. 반대로 C는 a의 메소드들 중에 B가사용하지 않는것들만 사용한다. 이럴 경우 a 인터페이스를 a1과 a2로 나누고 B는 a1을 C는 a2를 통해서 A에접근하도록하라는 얘기다.물론 A는 a1와 a2 모두를 implement 해야 하고...

이렇게 하면 혹시 나중에 인터페이스를 변경하게 되더라도 a1이나 a2 둘중 하나와 관련된 부분만 영향을 주므로 유지 보수에 유리하다.

http://www.objectmentor.com/resources/articles/isp.pdf

* DIP (The Dependency Inversion Principle)
A. 고수준 모듈은 저수준 모듈에 종속되어서는 안된다. 둘은 추상에 의존해야 한다.
B. 추상적인 것은 구체적인 것에 의존해서는 안된다. 구체적인 것이 추상적인것에 의존해야 한다.
기존에는 고수준 모듈이 저수준 모듈을 직접 사용해도 되는 것으로 생각했는데 이 원리는 한 모듈에 의존하지 말고 Interface나 추상 클래스 같은 것을 의존하도록 한다.

http://en.wikipedia.org/wiki/Dependency_inversion_principle
http://www.objectmentor.com/resources/articles/dip.pdf


패키지 결속성

다음 세가지 원칙은 어떤 클래스들을 한 패키지로 묶을 것이냐에 대한 논의이다. 한마디로 응집력이 높은 것들을 한 패키지로 묶어야 하는 것인데 어느정도로 응집력이 높아야 하는지에 대한 가이드 라인 정도로 이해하면 된다.

여기에 있는 세가지 룰은 실무에서 적용하기 힘들 정도로 엄격한데 내 생각에는 패키지라는 용어를 두가지로 이해하고 상황에 따라 융통성있게 사용하면 될 것 같다.
일단 패키지를 컴포넌트로 이해 할 수 있다. 이 경우 세가지 룰은 잘 적용이 될 수 있다.

또 하나는 tar나 zip 같은 배포 단위의 의미이다. 자바에서는 jar가 되는데 jar는 컴포넌트 배포 모델이므로 한jar에는 한 컴포넌트만 있는 것이 원칙이겠지만 대부분 여러 컴포넌트를 한 jar에 묶어서 배포하곤 한다. 이런 경우 세가지원칙은 지켜지기 힘들다.

REP (The Reuse Release Equivalence Principle)
The granule of reuse is the granule of release. 재사용의 단위는 릴리즈의 단위이다.한마디로 말해서 재사용을 패키지나 컴포넌트 단위로 하지 특정 컴포넌트에서 필요한 몇가지 class만 뽑아서 가지고 간다거나 하지못하게 하라는 말이다. 패키지 내부의 상황은 외부에서 들여다 볼 수 없는 Black box 같은 것이어야 한다.

http://www.objectmentor.com/publications/granularity.pdf

CCP (The Common Closure Principle)
패키지 안의 클래스들은 같은 종류의 변경에 대해 동시에 종결되어야 한다. 패키지에 영향을 주는 변경이 있다면 패키지 안의 모든 클래스에 영향을 주어야 한다.

어떤 한가지 이유로 클래스들을 변경해야 할 경우 이 클래스들은 한 패키지 안에 있어야 한다. CCP 정의에 따르면 한가지변경이 모든 클래스에 다 영향을 주어야 한다고 하는데 그정도로 서로 강하게 결속된 것 들만 한 패키지로 모으라는 뜻 같다.

CRP (The Common Reuse Principle)
패키지 안의 클래스들은 다 함깨 재사용되어야 한다. 이 원리는 REP에 따르는 결과라고 볼 수 있다. 만약 한 클라이언트가 패키지안의 한 클래스에 의존하고 있다면 패키지 안의 전체 클래스에 의존하고 있는 것이다.

http://www.objectmentor.com/publications/granularity.pdf

패키지 연관성
다음의 세가지 원칙은 패키지와 패키지사이의 연관성에 대한 문제이다. 그런데 여기서는 패키지를 컴포넌트로 이해하고 보는게 좋을 듯 싶다. 위의 세가지에와 다르게 다음 세가지는 엄격하게 지키는 것이 좋다. 특히 ADP는 필수이다.

ADP (The Acyclic Dependencies Principle)
패키지 간의 의존 구조는 순환 의존이 없어야 한다.

http://www.objectmentor.com/publications/granularity.pdf

SDP (The Stable Dependencies Principle)
패키지 간의 의존성은 패키지 안정성의 방향으로 되어야 한다. 한 패키지는 그것 보다 안정적인 패키지들에만 의존해야 한다.

http://www.objectmentor.com/resources/articles/stability.pdf

SAP (The Stable Abstractions Principle)
가장 안정적인 패키지는 가장 추상화되었을 것이다. 불안정한 패키지들은 구현(concrete) 일 것이다. 한 패키지의 추상화된 정도는 안정성에 비래할 것이다.

http://www.objectmentor.com/resources/articles/stability.pdf

by 박성철 | 2007/10/24 11:47 | 프로그래밍 이야기 | 트랙백(1) | 덧글(0)

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