본문 바로가기

JavaScript

[Java] 상속 (inheritance)에 대해서

1. 상속(inheritance)의 개념과 장점


상속(inheritance)이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해 코드를 공통적으로 관리하기 때문에 중복을 줄이고 프로그램의 생산성과 유지보수에 크게 기여할 수 있다. 

 

class Parent { }

class Child extends Parent {
	// ...
}

상속은 위 코드와 같이 표현한다. 서로 다른 클래스가 있고 한 클래스가 extends로 다른 한 클래스를 상속받을 때 두 클래스는 상속 관계에 있다고 한다. 상속을 받는 클래스는 자손 클래스 또는 자식 클래스라고 하며, 상속하는 클래스는 조상 클래스 또는 부모 클래스라고 한다. 

 

위 코드에서 부모 클래스는 Parent, 자식 클래스는 Child가 된다. 

  • 조상 클래스/부모 클래스: Parent
  • 자손 클래스/자식 클래스: Child

Child 클래스는 Parent 클래스를 상속받았다. 자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에 Child 클래스는 Parent 클래스의 멤버들을 포함하게 된다. 그렇기 때문에 Parent 클래스에 멤버를 추가하면 Child 클래스에도 추가된다. 즉, 자손 클래스는 조상 클래스보다 항상 같거나 많은 멤버를 갖게 된다. 상속에 사용되는 키워드가 'extends'인 것도 클래스를 확장(extend)한다는 의미에서 만들어진 것이다.

 

반대로 Parent 클래스는 Child 클래스에 멤버를 추가해도 영향을 받지 않는다. 상속을 받는 쪽만 클래스가 확장되기 때문이다. 

 

정리하자면,

  • 자손 클래스는 조상 클래스의 멤버를 상속받는다.
  • 자속 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
  • 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
  • 상속은 'extends' 키워드로 표현한다.

(상속의 예시)

public class Tv {
	boolean power;
	int channel;
	
	void power() { power = !power; }
	void channelUp() { ++channel; }
	void channelDown() { --channel; }
}
public class CaptionTv extends Tv { // CaptionTv는 TV를 상속 받는다.
	boolean caption;
	
	void displayCaption(String text) {
		if (caption) {
			System.out.println(text);
		}
	}
}
public class CaptionTvTest {
	public static void main(String[] args) {
		CaptionTv ctv = new CaptionTv();
		
		ctv.channel = 10; // Tv의 멤버 사용
		ctv.channelUp(); // Tv의 멤버 사용
		System.out.println(ctv.channel); // 11
		ctv.displayCaption("Hello, World!");
		ctv.caption = true;
		ctv.displayCaption("Hello, World!"); // Hello, World!
	}
}

위 코드는 전원 on/off 기능과 채널 up/down 기능을 가진 Tv 클래스로부터 상속을 받은 CaptionTv 클래스를 작성한 코드이다. CaptionTv 클래스에는 자막 기능을 추가하였다. main에서는 CaptionTv의 인스턴스를 생성하고 채널 기능과 자막 기능을 모두 사용하였다. Caption Tv 클래스의 인스턴스를 생성하면 Tv 클래스의 멤버도 생성되기 때문에 따로 Tv 클래스의 인스턴스를 생성하지 않고도 Tv클래스의 멤버들을 사용할 수 있다. 

 

=> 자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐져 생성된다. 

 

2. 포함관계(has-a) 


앞에서 설명한 방법과는 다르게 클래스를 재사용하는 방법으로 클래스 간에 '포함(Composite)'관계를 맺어 주는 방법이 있다. 클래스 간에 포함관계를 맺어 주는 것은 한 클래스의 멤버변수로 다른 클래스 타임의 참조변수를 선언하는 것을 뜻한다. 

 

public class Point {
	int x;
	int y;
}
public class Circle {
	Point p = new Point(); // ①
	int r; // ②
}

좌표상의 한 점을 다루는 Point 클래스를 재사용해서 Cricle 클래스를 작성하였다. Circle 클래스는 좌표를 표현하는 ①Point 클래스의 멤버변수(x, y)②원의 반지름(r)을 표현하는 Circle 클래스만의 멤버변수로 이루어져 있다. 

 

3. 클래스 간의 관계 결정하기(상속관계 or 포함관계)


  • 상속관계: ~은 ~이다 (is-a)
  • 포함관계: ~은 ~을 가지고 있다. (has-a)

클래스를 상속관계로 작성할 것인지 포함관계로 작성할 것인지 어떻게 결정할까? 그럴 때는 '~은 ~이다(is-a)'와 '~은 ~을 가지고 있다(has-a)'를 넣어서 문장을 만들어보면 클래스 간의 관계가 보다 명확해진다.

  • 원(Circle) 점(Point)이다. - Circle is a Point.
  • 원(Circle) 점(Point)을 가지고 있다. - Circle has a Point.

두 문장을 만들었을 때 두 번째 문장이 더 자연스러우므로 Circle 클래스와 Point 클래스 간에는 포함관계를 맺어주는 것이 더 좋다. 그러나 매번 이렇게 관계가 딱 떨어지는 것은 아니므로 기본적인 원칙에 대한 감을 잡는 정도로만 사용하자.

 

4. super


super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는 데 사용되는 참조변수이다. 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 멤버와 이름이 같을 때는 super를 붙여서 구별할 수 있다. 

조상 클래스로부터 상속받은 멤버도 자신의 멤버이므로 super대신 this를 사용할 수도 있다. 그래도 조상 클래스의 멤버와 자손 클래스의 멤버가 중복되어 서로 구별해야 하는 경우는 super를 사용하는 게 좋다. 

* 조상의 멤버와 자신의 멤버를 구별하는 데 사용된다는 점을 제외하고는 super와 this는 근본적으로 같다. 

 

public class Parent {
	int x = 10;
}
public class Child extends Parent {
	int x=20;
	
	void method() {
		System.out.println("x=" + x); // 자손 클래스 x=10
		System.out.println("this.x=" + this.x); // 자손 클래스 x=10
		System.out.println("super.x=" + super.x); // 조상 클래스 x=20
	}
}
public class SuperTest {

	public static void main(String[] args) {
		Child c = new Child();
		c.method();
	}
}
/* 출력 결과:
x=20
this.x=20
super.x=10 */

위 예제는 this와 super가 서로 다른 값을 참조하는 경우를 보여준다. Parent 클래스와 Child 클래스 모두 x라는 이름의 멤버변수가 존재하는데, 이때 this와 super로 이름이 같은 두 변수를 구분할 수 있다. this.x는 자손 클래스에 선언된 멤버변수 x를 뜻하며, super.x는 조상 클래스로부터 상속받은 멤버변수 x를 뜻한다.

 

super는 변수뿐만 아니라 메서드를 호출할 때도 쓸 수 있다. 특히 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우 super를 사용한다.

 

public class Point {
	int x;
	int y;
	
	String getLocation() {
		return "x: " + x  + ", y: " + y;
	}
}
public class Point3D extends Point {
	int z;
	String getLocation() { // 오버라이딩
		return super.getLocation() + ", z: " + z; // 조상의 메서드 호출
	}
}

조상 클래스의 메서드의 내용에 추가적으로 작업을 덧붙이는 경우라면 위 코드처럼 super를 사용해서 조상 클래스의 메서드를 포함시키는 것이 좋다. 

 

또한 super()는 조상 클래스의 생성자로 사용된다. 자손 클래스의 인스턴스를 생성할 때 조상 클래스 멤버의 초기화 작업이 수행되어야 하는데 이때 super()이 사용된다. 만약 상속을 받은 클래스의 인스턴스를 생성했는데 super()으로 조상 클래스를 생성(초기화) 하지 않으면 컴파일러는 자동적으로 'super();'를 생성자의 첫 줄에 삽입한다.