[JAVA] Inheritance 2

Seongje kim, 20 December 2019

이번 포스트의 주제는 이전 포스트에 이어서 Java 상속과 관련된 추상 클래스와 인터페이스에 대한 내용입니다. 아래는 이전 포스트 링크입니다.
(Java) Inheritance 1

추상(Abstract) 클래스


추상 클래스란 abstract 라는 키워드와 함께 선언되며 추상 메서드를 포함하지 않거나 하나 이상을 가지는 클래스이다. 또한 추상 메서드를 제외한 멤버 변수 또는 일반 구현 메서드도 존재할 수 있다.

추상 메서드는 구현부가 없는 메서드를 말한다. 아래와 같이 어떤 추상 메서드를 포함한다면 추상 클래스로 선언되어야 한다.

public abstract class Vehicle {
	abstract void move();
}

public class Car extends Vehicle {

  	@Override
  	void move() {
    	System.out.println("Move Car!");
  	}
}

우선 설명하기에 앞서 ‘추상화하다’ 라는 의미를 되새겨보자. 어떤 개념이나 기능으로부터 핵심적인 내용 또는 일부를 간추려 내는것을 말한다. 반대로 생각하면 어떤 추상화된 개념이나 기능은 ‘구체화’ 를 통해 내용을 확장할 수 있다. 분명히 구체화된 내용들은 위에서 추상화된 개념으로부터 확장되었다는 공통점을 가진다.

다시 말해 추상화는 서로 관련된 내용들로부터 공통점을 찾아 간추려 내는것이 ‘목적’ 이다. 쉽게 예를 들어 우리는 자동차와 자전거를 ‘타는 것’ 이라는 공통점으로부터 ‘이동 수단’ 으로 추상화할 수 있다. 이 의미를 꼭 기억해두자.

다시 본론으로 돌아와 추상 클래스는 말그대로 추상적이다. 즉, 클래스가 완전한 구성을 가지지 않았기 때문에 구체화가 필요한 설계 도면과 같은 역할을 한다.

객체지향적인 관점으로 봤을 때 객체는 클래스로부터 생성되며 해당 클래스는 자신으로부터 생성될 객체의 특정 상태와 행동을 정의한다. 하지만 추상 클래스로부터 객체를 만든다면 해당 객체는 전혀 객체의 모습을 띄지 못한다. 너무 추상적이기 때문이다.

그래서 추상 클래스는 객체화가 불가능하며 상속을 통해서 자식 클래스에 의해 완성이 된다. 일반적으로 추상 클래스를 상속받는 자식 클래스에서는 부모 클래스의 모든 추상 메서드들을 재정의하여 구현한다.
(그렇지 않은 경우 자식 클래스 또한 abstract 키워드로 선언되어야 한다.)

결과적으로 어떤 추상 클래스를 상속받아 구현한 자식 클래스들은 부모 클래스를 중심으로 밀접한 관련성이 형성되며 구체화시킬 동작과 내용의 차이에 따라 분류된다.

위 클래스 계층 구조에서 빨간색 네모에 해당하는 클래스들이 추상 클래스이다. 물론 이들이 일반 클래스로 정의되어도 프로그램의 컴파일과 실행에는 아무런 문제가 되지 않는다. 밑에서 다시 언급하겠지만 중요한것은 추상화의 의미와 목적이다. 추상화의 본질적인 목적을 잊지 않았다면 이들이 왜 추상 클래스가 되어야 하는지 알 수 있을 것이다.

인터페이스 (Interface)


public interface Vehicle {
  	void move();
}

public class Car implements Vehicle {

  	@Override
  	public void move() {
    	System.out.println("Move Car!");
  	}
}

인터페이스란 interface 키워드로 선언되며 추상 클래스처럼 추상 메서드를 갖지만 추상화 강도가 강해서 그 외의 멤버를 허용하지 않는다. 인터페이스는 해당 인터페이스를 구현하는 모든 하위 클래스에 대해 특정 메서드 구현을 강제하는 역할을 한다. 즉, 같은 인터페이스를 구현하는 객체들의 같은 동작을 보장하기 위한 목적이다.

‘같은 동작을 보장한다’ 바로 이것이 핵심이다.
상속(extends)과 추상화의 의미보다 동작의 구현(implements)에 초점을 둔다.

Java는 클래스 간 다중 상속을 지원하지 않는다.

이전 포스트에서도 언급한 바와 같이 Java는 클래스 간 다중 상속을 지원하지 않는다. 다중 상속은 상속의 의미를 애매하게 만들기 때문이다.

그러나 인터페이스는 클래스와 다르게 다중 상속이 가능하다. 인터페이스는 추상 메서드 외에 다른 멤버를 허용하지 않기 때문에 다이아몬드 문제로 인한 충돌 발생 위험이 없다. 또한 동작의 구현이 목적이기 때문에 상속과 추상화의 의미를 강조할 필요가 없다. 때문에 인터페이스를 구현하는 하위 클래스는 다른 여러 개의 인터페이스들을 함께 구현할 수 있다.
(이는 밑에서 다시 설명하겠지만 다형성과 관련이 있다.)

이 사실들만 보면 추상 클래스와 비교하여 인터페이스의 존재 이유는 ‘다중 상속을 제공하기 위한 것이다’ 라는 오해를 살 수 있다.

Java 8 부터의 인터페이스

Java 8 부터 인터페이스는 상수로 선언된 변수를 사용 가능하며 메서드들은 static 또는 default 키워드와 함께 구현체를 가질 수 있다. (단, 메서드가 구현된 인터페이스는 클래스처럼 다중 상속을 받을 수 없게 된다.)

public interface Vehicle {
  	int A = 0;
  	void stop();

  	static void start() {
    	System.out.println("Start!");   // 재정의 불가
  	}

	default void move() {
    	System.out.println("Move!");    // 재정의 가능
  	}	
}

default 또는 static 으로 선언되지 않은 모든 메서드들은 암묵적으로 abstract 이기 때문에 접근 제어자 생략이 가능하다. 인터페이스에서 모든 변수는 기본적으로 public static final 이며, 모든 메서드는 public abstract 이다. 이들 또한 생략이 가능하다.

이러한 변화 덕분에 상위 계층에서 구현체를 가지려고 할 경우 추상화 또는 상속의 목적에 관련이 없고, 인터페이스를 사용함으로써 얻는 장점이 더 많음에도 불구하고 어쩔 수 없이 추상 클래스를 사용했어야 했던 불편에서 벗어날 수 있게 되었다. 추상 클래스의 장점을 인터페이스에게도 제공한 것이다.

추상 클래스 vs 인터페이스


추상 클래스와 인터페이스 모두 추상 메서드를 가지며 자신의 객체를 생성할 수 없고, 이들을 구현하는 하위 클래스에게 구현을 강제한다는 공통점을 가진다.

그렇다면 왜 Java는 추상 클래스와 인터페이스를 구분하였을까?
단지 다중 상속의 문제점을 해결하기 위함일까?

결론은 아니다.
이 둘을 언제, 어떻게 구분하여 사용해야 하는지가 핵심이다.

앞서 강조했듯이 추상 클래스와 인터페이스의 목적을 보면 존재 이유가 다르다. 추상 클래스는 ‘추상화’ 또는 ‘기능의 확장’ 이 주된 목적이고, 인터페이스는 ‘객체의 같은 동작 보장’ 또는 ‘구현’ 이 주된 목적이다.

추상 클래스와 인터페이스의 적절한 사용

  • 추상 클래스
  1. 여러 개의 가까운 클래스들(is-a 관계가 형성될) 사이에 동일한 코드를 공유해서 사용하고 싶을 때.
  2. 추상 클래스를 상속한 클래스들이 많은 공통 메소드들과 필드와 public 보다 다양한 접근 제어자에 의해 사용하고 싶을 때.
  3. non-staticnon-final 필드를 선언하고 싶을때. 즉, 각 객체들의 상태를 접근하고 수정할 수 있는 메서드 선언이 가능하다.
  • 인터페이스
  1. 크게 관련없는 클래스들이(is-a 정도는 아닌 has-a 정도인) 인터페이스를 구현해야 할 필요가 있을 때.
  2. 특정 데이터 타입의 행위를 특별하게 정의하길 원하며 누가 그것의 행위를 구현하는지에 대한 관심은 없을 때.
  3. 다중 구현상속의 이점을 누려야 할 때.

정리

의미상으로 추상 클래스를 만들어야 하는 시점은 비슷한 클래스끼리 공통적인 추상성을 뽑아서 상위 클래스와의 계층 구조를 만들고 상속의 강점을 이용하고 싶을 때 사용한다. 하지만 추상화 시킬 클래스들이 관련이 전혀 없다면 인터페이스를 사용하는 것이 좋다. 클래스 또는 객체 간의 의존성과 결합성을 낮추는데도 이득을 볼 수 있기 때문이다.

그러므로 만일 클래스 간 밀접한 관련이 있더라도 우선 인터페이스 사용을 고려하는 것이 좋다. 그러나 다중 상속에 제약이 없고, 상속을 사용했을 때 유지 보수 및 코드 재사용에서 많은 이득을 볼 수 있다면 추상 클래스를 활용한다.

인터페이스와 다형성


이전 포스트에서 다룬 다형성에서는 인터페이스 또한 중요한 수단이 된다. 인터페이스는 다중 상속이 가능하기 때문에 이를 구현하는 하위 클래스는 다른 여러 개의 인터페이스들을 함께 구현할 수 있다. 또한 특정한 인터페이스를 구현하고 있는 클래스가 있을 때 이 클래스의 데이터 타입으로 인터페이스를 지정 할 수 있다.

아래 코드를 보자.

interface I1 {
    public String A();
}

interface I2 {
    public String B();
}

class C implements I1, I2{

    @Override
    public String A(){
        return "A";
    }

    @Override
    public String B(){
        return "B";
    }
}

public class Main {

    public static void main(String[] args) {
        C obj_C = new C();
        I1 obj_I1 = new C();
        I2 obj_I2 = new C();

        obj_C.A();    // 정상
        obj_C.B();    // 정상

        obj_I1.A();   // 정상
        obj_I1.B();   // 오류

        obj_I2.A();   // 오류
        obj_I2.B();   // 정상
    }
}

obj_I1.b() 에서 오류가 발생하는 이유는 obj_I1 의 데이터 타입이 인터페이스 I1 이기 때문이다. 인터페이스 I1 은 메서드 A() 만을 정의하고 있고, I1 을 데이터 타입으로 하는 객체는 마치 메서드 A() 만을 가지고 있는 것처럼 동작한다.

이것은 인터페이스의 매우 중요한 특징 중의 하나를 보여준다.

C 클래스 객체인 obj_I1 의 데이터 타입을 I1 으로 한다는 것은 C 클래스 객체를 외부에서 제어할 수 있는 조작 장치를 I1 의 멤버로 제한한다는 의미가 된다. 결과적으로 C 클래스가 인터페이스 I1 과 I2 를 다중 상속함으로써 하나의 클래스가 다양한 형태를 띠게 되는 것이다. 이는 일반적인 클래스의 상속 관계에서 이루어지는 다형성과는 다른 양상의 다형성을 보여준다.


이것으로 이번 포스트에서는 이전 포스트와 함께 Java의 상속과 추상 클래스 및 인터페이스를 알아보았습니다. 감사합니다.