상속이란?
상속은 객체지향 4대 특성 중 하나의 특성인데, 상속이란 기존의 클래스(상위 클래스)를 '재사용 및 확장'한 새로운 클래스(하위 클래스)를 작성하는 것이다.
여기서 '재사용'이라는 표현을 사용했는데, 이는 상위 클래스의 멤버(변수, 메서드)를 하위 클래스가 모두 상속받아 (재)사용할 수 있기 때문이다. 이때 상위 클래스의 생성자나 초기화 블럭은 상속되지 않는다. 만일 여러 클래스들이 있을 때 '공통되는 멤버'가 있다면 상속 관계를 적용할 수 있는지 생각해볼 법하다.
또한 '확장'이라는 표현을 사용했는데, 이는 하위 클래스가 상위 클래스로부터 상속받은 멤버에 더해서 추가적인 멤버를 작성할 수 있기 때문이다. (메서드를 오버라이딩함으로써 재정의할수도 있다.) 이때 새롭게 추가되는 멤버는 상위 클래스와 독립적인 것들로 하위 클래스는 상위 클래스를 포함한다고 볼 수 있다.
하위 클래스 is a kind of 상위 클래스
이처럼 상위 클래스를 상속(확장)한 하위 클래스 간에는 일종의 is a kind of 관계가 있다고 할 수 있다. 예를 들어, 삼각형, 사각형, 다각형 등의 클래스들이 있다고 해보자. 이들의 공통점은 모두 '도형의 한 종류'라는 점이다.
즉, 코드를 작성할 때 도형이라는 상위 클래스를 정한 후 공통되는 멤버 로직을 작성하고 이를 상속받는 삼각형, 사각형 등의 클래스들은 해당 멤버들을 모두 상속받는다면, 중복되는 코드를 작성할 필요도 없고, 추후에 변경하기에도 용이해진다. 아울러 하위 클래스들은 자신만의 상태나 기능을 확장할 수도 있다.
super와 super()
이러한 상속 관계에서는 super 키워드라는 게 존재한다. this와 마찬가지로 super은 참조변수로서 상위 클래스의 인스턴스를 가리킨다. 이 super은 참조 변수이므로 상위 클래스의 멤버에도 접근할 수도 있다. 다만, super.super 같은 형태로 상위의 상위 클래스의 인스턴스에는 접근이 불가능하다.
또한 this()와 마찬가지로 super()은 생성자로서 상위 클래스의 생성자를 호출한다. 이때 하위 클래스의 인스턴스를 생성 시 상위 클래스의 멤버를 상속받아야하므로 상위 클래스의 생성자를 호출(상위 클래스의 멤버를 초기화)해야할 필요가 있기에, 하위 클래스의 생성자 첫 줄에는 super()이 나와야 한다. 하지만 별도로 기입하지 않을 경우에도 컴파일러가 이를 추가해준다.
public abstract class Figure {
final double area;
public Figure(double area) {
this.area = area;
}
// ...
}
class Triangle extends Figure {
public Triangle(int[][] c){
super(calculateData(c));
}
// ...
}
이처럼 하위 클래스에 의해 상위 클래스의 생성자가 호출되면서 해당 상위 클래스는 동일한 방법으로 자신의 상위 클래스의 생성자를 호출하며, 최상위 클래스인 Object를 만날 때까지 상위 클래스에 대한 생성자 호출이 반복된다.
참고로 생성자 첫 줄에 오버로딩된 다른 생성자를 this()로 호출하는 경우(이것도 일종의 규칙이므로)에는 첫 줄에 super()이 나와야 하는 규칙이 무마된다. 또한 this()와 super()는 하나의 생성자에서 같이 사용될 수 없다.
메서드 오버라이딩과 다이나믹 메서드 디스패치
메서드 오버라이딩
앞서 삼각형, 사각형 등의 클래스들은 상위 클래스인 도형 클래스의 멤버를 상속받는다고 했다. 이때 특정 메서드에 대해 하위 클래스 자기 자신만의 특색을 갖춘 메서드로 '재정의'할 수도 있는데, 이를 메서드 오버라이딩(Overring)이라고 한다. 참고로 상위 클래스의 특정 메서드를 오버라이딩 시 재정의하는 메서드의 접근 제어자는 상위 클래스의 메서드 보다 같거나 크게 해줄 수 있으며, 반환타입을 하위 클래스의 타입으로 변경하는 것이 가능하다.
public class Figure {
// ...
public void display() {
System.out.println("나는 도형!!");
}
}
class Triangle extends Figure {
// ...
@Override
public void display() {
System.out.println("나는 삼각형!!");
}
}
다이나믹 메서드 디스패치
자바에서는 상위 클래스 타입의 참조 변수로 하위 클래스의 인스턴스를 참조(업 캐스팅)할 수 있도록 함으로써 객체지향 4대 특성 중 하나인 다형성을 제공한다. (참고로 하위 클래스 타입의 참조 변수로 상위 클래스 인스턴스를 참조할 수는 없다.)
예를 들어, 상위 클래스인 Figure를 상속받은 하위 클래스들(삼각형, 사각형 등)이 여러 개 있다고 가정해보자. 이를 다음과 같이 선언할 수 있다.
int len = coordinates.length;
Figure figure;
if (len == 1) figure = new Coordinate(); // 업 캐스팅, 형변환 생략 가능
else if (len == 2) figure = new Line();
else if (len == 3) figure = new Triangle();
else if (len == 4) figure = new Rectangle();
else figure = new Polygon();
일반적으로는 참조변수의 타입과 인스턴스의 타입이 같지만, 위 코드를 보면 상위 클래스 타입의 참조 변수로 여러 개의 하위 클래스의 인스턴스를 참조하고 있는 것을 확인할 수 있다. 이때 유의해야할 점으로는 하위 클래스의 인스턴스가 생성되었지만 해당 참조 변수(figure)로 사용할 수 있는 멤버는 참조 변수의 타입인 상위 클래스를 따른다는 것이다.
즉, 상위 클래스 타입의 참조 변수(figure)로는 상위 클래스의 멤버 변수에만 접근할 수 있는 것이다. 다만, 하위 클래스가 상위 클래스의 메서드를 오버라이딩한 경우에는 상위 클래스의 메서드가 아닌 하위 클래스의 메서드가 호출되게 된다. 물론, 하위 클래스가 새롭게 정의한 메서드는 상위 클래스 타입의 참조 변수로는 아에 접근도 불가능하다.
여기서 만일 모든 하위 클래스들이 상위 클래스 특정 메서드를 오버라이딩했다면 어떤 효과를 낼 수 있을까? 바로 하나의 참조변수로 여러 형태의 메서드를 호출할 수 있는 '사용편의성'을 얻게 되는 것이다.
public class Figure {
// ...
public void display() {
System.out.println("나는 도형!!");
}
}
class Triangle extends Figure {
// ...
@Override
public void display() {
System.out.println("나는 삼각형!!");
}
}
class Rectangle extends Figure {
// ...
@Override
public void display() {
System.out.println("나는 직사각형!!");
}
}
Triangle 클래스와 Rectangle 클래스는 모두 Figure 클래스를 상속받고 display 메서드를 오버라이딩하고 있다. 이 경우 Figure 타입의 참조변수에 Triangle 인스턴스가 할당되냐 Rectangle 인스턴스가 할당되냐에 따라 똑같은 display 메서드를 호출해도 다른 결과가 나타나게 된다.
즉, 상위 클래스인 Figure이 완충 장치 역할을 함으로써 display 메서드를 호출하는 입장에서는 특정 구현체에 의존하지 않아 상황에 따라 유연한 실행 결과를 얻을 수 있고 구현체가 바뀌더라도 인터페이스를 변경하지 않아도 된다. 아울러, 개발자 입장에서는 새로운 구현체를 개발함에 있어 기존 구현체를 새로운 구현체로 갈아끼우기만하면 되므로 '확장성'을 얻을 수 있다.
Figure figure1 = new Triangle();
Figure figure2 = new Rectangle();
figure1.display(); // 나는 삼각형!!
figure2.display(); // 나는 직사각형!!
이때 어떤 인스턴스가 생성되고 할당될지는 실행 단계(Runtime)에서만이 알 수 있다. 즉, 사용자 입력 등에 따라 동적으로 결정되는 것이다. 이처럼 실행 단계에서 여러가지 형태의 메서드 중 하나를 동적으로 결정하는 매커니즘을 다이나믹 메서드 디스패치(Dynamic Method Dispatch)라고 한다.
추상 클래스
추상 클래스란?
일반 클래스와 추상 클래스의 차이점은 추상 클래스는 추상 메서드를 포함하고 있다는 것이다. 추상 메서드는 구현부 없이 선언부만 작성된 메서드인데, 추상 클래스를 상속하는 하위 클래스에 의해 구현된다. 이때 추상 클래스를 상속하는 하위 클래스는 추상 메서드를 오버라이딩하도록 강제된다. 이러한 추상 클래스는 인스턴스를 생성할 수 없다(new 사용 불가)는 특징이 있다.
추상 클래스의 활용
앞서 Figure 클래스를 상속받은 Triangle 클래스와 Rectangle 클래스에서 Figure 클래스의 display 메서드를 오버라이딩하여 다형성을 나타냈었다. 이때 Triangle, Rectangle 외에 다른 도형들이 기능적으로 추가될 수 있으며, 해당 도형들에 대해서도 마찬가지로 자기 특색에 맞는 display 메서드를 구현해야할 수도 있다.
이때 Figure 클래스를 display라는 추상 메서드를 포함한 추상 클래스로 선언한다면, Figure 클래스를 상속받는 각종 도형 클래스들이 display 메서드를 구현하도록 강제할 수 있는 효과를 얻을 수 있다.
public abstract class Figure {
// ...
public abstract void display();
}
public class Triangle extends Figure {
@Override
public void display() {
System.out.printf("나는 삼각형!!");
}
}
public class Rectangle extends Figure {
@Override
public void display() {
System.out.printf("나는 직사각형!!");
}
}
아울러 앞선 예시에서도 Figure 클래스가 Figure 클래스 자체로 쓰이지 않는 이상 Figure 인스턴스를 생성할 필요도 없었고 display 메서드를 구현할 필요는 없었다. 애초에 삼각형이나 직사각형 등 도형의 한 종류(구현체)로서만 사용되는 애플리케이션이라면 더욱이 추상 클래스로 사용함이 적합하다.
더욱이 특정 추상 메서드를 구현한 여러 하위 클래스들이 있다면 해당 추상 메서드를 호출하는 입장에서는 사용자 입력 내지 설정 등에 따라 유연하게(동적으로) 특색에 맞는 결과를 얻어낼 수 있게 된다.
참고자료
- 도우출판 "자바의 정석"
- 위키북스 "스프링 입문을 위한 자바 객체 지향의 원리와 이해"
'Technology > Java' 카테고리의 다른 글
Java의 인터페이스에 대한 고찰 (4) | 2022.09.08 |
---|---|
Java의 패키지에 대해 알아보자! (feat. 클래스패스, import) (0) | 2022.09.01 |
Java final 키워드의 여러가지 쓰임새 (0) | 2022.08.26 |
Java의 Object 클래스가 제공하는 기능 분석 (0) | 2022.08.26 |
Java의 클래스와 객체 이해하기 (0) | 2022.08.16 |