자바를 비롯한 객체지향 언어에 대해서 공부한 독자들이나 적어도 개념을 이해하신
분들은 상속이라는 단어가 주는 뉘앙스를 잘 아시리라 생각합니다.
어떤이는 객체지향의 꽃은 상속이고, 상속을 많이하여 설계할수록 효율성이 높고 좋은
설계로 발전할 가능성이 많다고 합니다. 자바에서 그만큼 상속의 비중을 높이 평가하는
것이고, 상속이 가지는 특성 ,즉 기존에 만들어진 객체를 충분히 잘 활용 한다는 것을
강조하고 싶었나 봅니다. 필자는 설계를 할 때 상속을 많이 사용하지 않습니다.
조금 지나친 표현이긴 합니다만 그렇게 해야할 필요를 잘 못 느끼기 때문입니다.
하지만 상속을 하지 않으면 객체 지향적인 설계를 포기하는 것이 아닌가요?
필자의 답은 절대 그렇지 않습니다. 콤포지션을 예로 들 때 언급했지만 자바에서의
상속의 의미는 필요한 객체를 처음부터 만들기 보다는 기존에 존재하는 객체를 효율적으로
이용하고자 할 때 하나의 방법으로 선택할 수 있는 것일 뿐입니다.
그 이상 , 그 이하도 아니라고 생각합니다.
상속을 맹목적으로 이용하는 것보다는 상속의 사례를 이해하고, 가장 좋은 방법으로
객체를 잘 활용하는 방법, 즉 비용을 줄이고 효율을 높이는 설계를 하면 그것으로 족합니다.
보통버거와 큰 버거와 무척 큰 버거
Burger를 만들어 보겠습니다.
package burger; // burger 패키지
public class Burger {
protected int price; // 버거의 가격
protected String description; // 버거의 개요
public Burger(int price, String description) {
this.price = price;
this.description = description;
System.out.println("Burger를 만들었습니다.");
}
public Burger(int price) {
this(price,"버거");
}
public Burger(String description) {
this(1300,description); // 기본가격은 1300원
}
public Burger() {
this(1300,"버거"); // 기본개요는 "버거"
}
public int getPrice() {
return price;
}
public String getDescription() {
return description;
}
}
예제 3 - 6 Burger.java
Burger는 가격과 개요를 갖고 있고, 기본적으로 1300원에 팔고자 했으며, “버거”라는
개요를 갖고 있습니다.
설명을 쉽게 하기 위해서 버거가 만들어질 때 클래스의 이름인 "Burger"를 알수 있도록
“Burger를 만들었습니다.”라는 메시지가 출력합니다.
Burger 클래스의 멤버변수인 가격과 개요는 각각 protected 지정자가 붙어있습니다.
객체의 멤버변수는 어떤 관계에 있는 제 3의 객체에서 Burger를 만들었는지에 관계없이
직접 읽고,쓰지 못하도록 private
나름대로 특별한 이유가 있어서 protected라고 했습니다.
즉, Burger설계자는 Burger클래스를 상속해서 새로운 클래스를 설계하고자 할 때 ,
상속할 새로운 클래스에서 Burger클래스를 좀더 쉽게 이용할 수 있도록 배려를 한 것
입니다. 여기에 대해선 곧 풀어 보겠습니다.
상속의 키워드 extends
Burger가 아주 잘 팔려서 회사에서 Burger를 좀 개량한 BigBurger가 출시 됐습니다.
원래 많이 팔이는 제품은 업그레이드해서 좀더 높은 가격에 팔고 있으니까요.
버거 회사에도 크게 다르지 않습니다.
새로 나온 BigBurger를 자바로 만들어야 겠는데, 가만히 보니 Burger와 크게 다르지
않습니다. BigBurger도 가격과 개요라는 개념을 그대로 같고 있기 때문입니다.
다만 눈에 보기에 크기가 좀더 클 뿐이니까요.
그래서 BigBurger는 Burger를 상속해서 사용하기도 했습니다. 상속이 가지는 가장 기본적인
생각은 기존에 만들어져 있는 객체의 기능에다 추가로 무엇을 더하고자 할 때 좋습니다.
BigBurger 클래스가 Burger 클래스를 상속한다는 표시는 예제 3 - 7 클래스 선언에서 볼 수
있드시 extends 입니다.
package burger;
public class BigBurger extends Burger { // Burger를 상속했습니다.
public BigBurger(int price) {
super(price); // Oops
System.out.println("BigBurger를 만들었습니다.");
}
public BigBurger(String description) {
super(description); // Oops
System.out.println("BigBurger를 만들었습니다.");
}
public BigBurger() {
super(1800,"큰 버거");
System.out.println("BigBurger를 만들었습니다.");
}
public BigBurger(int price,String description) {
super(price,description);
System.out.println("BigBurger를 만들었습니다.");
}
}
예제 3 - 7 BigBurger.java
BigBurger는 특별한 이유가 없다면 기본가격을 1800원으로 하고, 개요는 “큰 버거”로
하기로 했습니다.
BigBurger클래스는 상속한 클래스답게 (기존 객체를 활용할 목적인 클래스답게) 자신에게도
꼭 필요한 정보인 가격과 개요를 저장할 멤버변수를 만들지 않았고,가격과 개요을 알려주는
멤버메소드도 만들지 않았습니다.
만약 BigBurger클래스가 상속하지 않은 일반적인 클래스 경우라면 BigBurger 클래스에서
멤버변수 price, description를 읽고 쓸수 없습니다. 너무나 당연한 것입니다.
멤버변수가 없는데 어떻게 읽고,쓸 수 있겠습니까?
하지만 BigBurger클래스는 Burger클래스를 상속했습니다. 이렇게 BigBurger가 Burger를
상속한 상태에서는 BigBurger는 내부적으로 Burger를 갖고 있다고 생각해도 무방합니다.
Burger가 가지고 있는 멤버 변수 price,description와 Burger가 정의해 놓은 멤버 메소드인
getPrice(),getDescription()등을 모두 얻어 쓸수 있게 되는 것입니다.
BigBurger클래스에서 멤버 변수,멤버 메소드를 선언한 것은 아무것도 없지만 마치
BigBurger클래스에서 선언한 것처럼 읽고,쓸고 호출할 수 있습니다.
다만 price,description,getPrice(),getDescription()등을 BigBurger 스스로 만든 것이
아니고 Burger 에서 만든 것을 상속받았기 때문에 마음대로 읽고,쓰고, 호출하는데
지장이 조금 있을 뿐입니다.
BigBurger의 입장에서 정리해보면 BigBurger객체는 멤버변수 price, description을 갖고
있고, getPrice(), getDescription()을 가지고 있지만 상속한 Burger 클래스로부터 얻어
쓰는 처지 이기 때문에 읽고,쓰고,호출하는데 제약을 받게 된다는 뜻입니다.
BigBurger는 Burger에서 private으로 지정된 멤버변수는 존재하기는 하지만 읽을수도 없고
쓸 수도 없습니다.
Burger에서 protected로 지정된 멤버변수는 얼마든지 읽고 쓸수 있습니다. 바로 Burger
클래스와 BigBurger클래스는 상속의 관계에 있으므로 아주 특별한 관계에 있는 셈이고
protected는 특별한 관계에 있는 클래스에서는 읽고,쓰게 해주니까요.
이번에는 다행히 Burger클래스의 설계자가 멤버변수인 price,description에 protected
는 마음껏 price, description을 읽고,쓸수 있습니다.
getPrice()와 getDescription()은 public
마음껏 호출 할 수 있음은 당연하겠습니다.
BigBurger가 Burger를 상속함으로써 Burger가 가지는 상태 및 행위를 BigBurger도 가질 수
있고, 행위를 할 수 있게 되었습니다.
super의 의미
BigBurger의 생성자에 보면 super라는 키워드가 보입니다. 이 super라는 키워드의 의미는
BigBurger가 상속하는 대상 객체를 의미합니다. 즉, BigBurger가 내부적으로 갖고 있다고
생각하는 그 객체를 의미하는 것이라고 생각하면 무난합니다.
this는 존재하는 객체 자신에 대한 레퍼런스라고 한다면 , super는 상속한 객체에 대한
레퍼런스 정도로 이해하면 됩니다.
회사에서 Burger,BigBurger가 아주 호응이 좋아서 BigBurger 후속으로 DoubleBigBurger를
만들었다고 합니다.
package burger;
public class DoubleBigBurger extends BigBurger {
public DoubleBigBurger(int price) {
super(price,"두배 큰 Burger");
System.out.println("DoubleBigBurger를 만들었습니다.");
}
public DoubleBigBurger(String description) {
super(2400,description);
System.out.println("DoubleBigBurger를 만들었습니다.");
}
public DoubleBigBurger() {
super(2400,"두배 큰 Burger");
System.out.println("DoubleBigBurger를 만들었습니다.");
}
public DoubleBigBurger(int price,String description) {
super(price,description);
System.out.println("DoubleBigBurger를 만들었습니다.");
}
}
예제 3 - 8 DoubleBigBurger.java
DoubleBigBurger는 BigBurger를 상속했으며 기본적으로 2400원에 팔리고
“두배 큰 Burger”라는 개요을 갖고 있습니다.
상속한 객체의 생성
상속은 콤포지션과는 달리 조금 복잡한 면이 있습니다.
Burger, BigBurger, DoubleBigBurger를 만들어보고 객체의 메소드를 호출해보도록
하겠습니다.
package burger;
public class BurgerInheritance {
public static void main(String[] args) {
Burger b = new Burger();
System.out.println("Burger " + b.getDescription());
System.out.println("-----------------");
BigBurger bb1 = new BigBurger(1800);
System.out.println("BigBurger1 " + bb1.getDescription());
System.out.println("-----------------");
BigBurger bb2 = new BigBurger();
System.out.println("BigBurger2 " + bb2.getDescription());
System.out.println("-----------------");
DoubleBigBurger dbb = new DoubleBigBurger();
System.out.println("DoubleBigBurger " + dbb.getDescription());
System.out.println("-----------------");
}
}
예제 3 - 9 BurgerInheriance.java
예제 3 - 9 BurgerInheritance 프로그램은 기본 가격의 Burger를 하나 만들고 개요을
알아보고 있고, 그 다음은 1800원 하는 BigBurger를 하나 만들고 개요를 알아보고 있으며,
그 다음은 기본 가격의 BigBurger를 만든후 개요를 알아보고, 마지막으로 DoubleBigBurger
를 만들고 개요를 알아보고 있습니다. 그 결과가 어떻게 나올까요?예상해 보세요.
그림 3 - 2 [ compile run (Burger.java BigBurger.java DoubleBigBurger.java
BurgerInheritance.java ) ]
결과가 예상대로 나오신 건가요?
아니죠? 조금 어려운데요. 하나씩 살펴보겠습니다.
BurgerInheritance 클래스를 실행시켰을 때 맨 먼저 하는 것은 Burger를 하나 만들고
Burger의 개요을 출력하는 것입니다.
Burger를 만들면 생성자에서 “Burger를 만들었습니다”라는 메시지를 출력할 테고,
getDescription()을 호출하면 “버거”라는 개요가 연이어 출력됩니다. 예상대로 입니다.
하지만 1800원짜리 BigBurger를 만든후 개요를 출력하는 경우에 조금 이상하게도
“Burger를 만들었습니다” 라는 메시지가 먼저 출력되고, “BigBurger를 만들었습니다”가
출력됩니다. 그리고 매우 이상하게도 개요정보가 "큰 버거"가 아니라 “버거”라고
출력되었습니다. 도무지 이해가 안되는 부분인데, 반드시 풀어서 이해해야 합니다.
먼저 풀어볼 부분은
첫째 “Burger를 만들었습니다”가 출력된 이유이고, 둘째 개요정보가 “큰 Burger”가
아니라 왜 그냥 “Burger”인 것인가 하는 점입니다.
첫째는 이런 이유에서입니다. BigBurger가 Burger를 상속한 관계라면 만들어지는
BigBurger는 내부적으로 Burger를 갖고 있는 셈이 됩니다.
BigBurger가 만들어졌다면 Burger가 이미 만들어져 있다는 얘기입니다.
결국 BigBurger보다 Burger가 먼저 만들어지고, 그후에 BigBurger가 완성되는 것이지요. BigBurger가 Burger를 내부적으로 갖고 있는다는 뜻은 Burger를 만들었다는 뜻인데 자바는
객체를 만들때는 반드시 해당 객체의 생성자를 호출한다는 것입니다.
BigBurger는 내부적으로 Burger를 갖기위해서 Burger의 생성자을 호출해 주어야 합니다.
에제 3 - 7의 BigBurger의 생성자에 보면 super(price)라는 부분이 있습니다.
public BigBurger(int price) {
super(price);
System.out.println("BigBurger를 만들었습니다.");
}
super는 자바에서 특별한 의미로 지정해 놓은 키워드로 this 키워드와 비교하면 이해가
쉽습니다. this 키워드가 객체자신을 의미하는 것처럼 super키워드는 상속되는 객체를 의미
합니다. 여기서는 BigBurger가 Burger를 상속하고 있으므로 super는 Burger를 의미하고, this는 BigBurger를 의미합니다. 즉, BigBurger의 생성자에서 Burger의 생성자를 호출하고
있는 셈입니다. 그 생성자는 int형 인자를 가진 Burger의 생성자를 호출한 셈이됩니다.
BigBurger의 생성자안에서 Burger의 생성자를 호출했으므로, “Burger를 만들었습니다”가
출력되고, Burger 객체가 만들어 진다음에야 “BigBurger를 만들었습니다”가 뒤이어
출력 되는 셈입니다.
둘째, 그렇다면 왜 BigBurger를 만들었는데 개요가 “큰 버거”가 아니라 그냥 “버거” 일까요? 특별한 이유요? 그런거 없습니다.
BigBurger를 클래스를 설계한 프로그래머의 실수입니다.
BigBurger의 생성자를 살펴보면 Burger의 int형 생성자를 호출합니다.
그런데 Burger의 int형 생성자가 하는 일이라고는 Burger클래스의 멤버변수 price에는
인자 price의 값을 할당하고, 개요변수에는 “버거”라고 할당하는 것입니다.
당연히 멤버변수 price에는 인자로 넘어온 price의 값이 저장되고, 멤버변수 description에는
인자로 넘어온 “버거”라는 값이 저장되어 있을 것 입니다.
여기까지가 BigBurger의 생성자를 호출한 상태입니다.
이후에 BigBurger 레퍼런스를 이용해서 getDescription()을 호출했습니다.
이 때 BigBurger가 하는일은 BigBurger의 멤버변수 description(실제로는 Burger에서 선언)
을 돌려주는 것인데 현재 값은 당연히 “버거”라고 되어 있겠지요. 이러 이유입니다.
BigBurger의 설계자는 이렇게 되는 것을 원하지는 않았을 것입니다.
이를 바로 잡고자 한다면 super(price) 대신에, super(price,”큰 버거”)라고 하는 것이
옳습니다. 이렇게 해야지만 price와 description을 원하는대로 초기화 할 수 있으니까요.
반변에 두번재 BigBurger(bb2)는 멤버변수를 적절하게 초기화를 했기 때문에 제대로 된
개요정보인 “큰 Burger”라는 개요을 얻을 수 있었습니다.
Double BigBurger를 살펴보면 Double BigBurger는 BigBurger를 상속했습니다.
그렇다면 Double BigBurger를 만든다고 했을 때 BigBurger를 먼저 만들어야 하고, BigBurger에 대한 생성자를 호출해 주어야 합니다. 또 BigBurger를 만들어야 한다면,
Burger를 먼저 만들어야하고, Burger에 대한 생성자가 호출 되어야 합니다.
DoubleBigBurger 생성자에서는 모두 BigBurger의 int형과 String형 생성자를 호출하고 있고,해당되는 BigBurger 생성자에서도 마찬가지로 적절한 Burger의 생성자를 잘 호출하고 있습니다.DoubleBigBurger는 내부적으로 BigBurger를 갖고 있는 셈이고, 그 BigBurger는
내부적으로 Burger를 갖고 있는 셈이 됩니다.
이처럼 상속을 하게 되면 실제 프로그램하는 코딩의 양이 줄어들게 되고, 객체간의
관계가 상당히 체계적으로 정립되는 특성이 있습니다.
새로운 객체를 만들고자 할 때 콤포지션을 이용하는 경우 기존에 만들어 놓은 객체를
사용해야 하므로 새로운 객체를 하나 만들기 위해서 먼저 기존의 객체를 만들어야 합니다.
이로 인해서 전체적인 객체의 수가 증가합니다.
하지만 상속도 DoubleBigBurger의 예에서처럼 상속에서도 실제 만들어내는 객체의 수는
마찬가지로 증가합니다. 상속을 해서 설계를 한다고 해서 만들어지는 객체의 양이 줄어드는
것은 아니며, 상당한 수의 상속을 거친 최하위 레벨의 객체를 하나 만들기 위해서는 최상위
객체에서부터 최하의 객체에 이르기 까지의 모든 객체가 최상위 객체서부터 순차적으로 생성되고 있다는 것을 알아야 겠습니다.
'자바 Basic > 클래스의 재사용' 카테고리의 다른 글
자바공부-1. 콤포지션 (Composition) (2) | 2007.11.30 |
---|