오늘은 자바에서의 다중 상속에 대해 알아보겠습니다. 이전에 몇 개의 게시물에서 상속, 인터페이스 및 구성에 대해 자바로 작성한 내용을 살펴보았습니다. 이번 게시물에서는 자바의 다중 상속을 살펴보고, 구성과 상속을 비교해보겠습니다.
자바에서의 다중 상속
자바에서의 다중 상속은 여러 개의 슈퍼클래스를 가진 단일 클래스를 생성할 수 있는 기능입니다. C++와 같은 일부 인기있는 객체 지향 프로그래밍 언어와 달리, 자바는 클래스에서 다중 상속을 지원하지 않습니다. 자바는 클래스에서 다중 상속을 지원하지 않는 이유는 다이아몬드 문제가 발생할 수 있기 때문입니다. 이 문제를 해결하기 위해 복잡한 방법을 제공하는 대신, 다중 상속과 동일한 결과를 얻을 수 있는 더 좋은 방법이 있습니다.
Java에서 다이아몬드 문제
다이아몬드 문제를 쉽게 이해하기 위해, 자바에서 다중 상속이 지원된다고 가정해 봅시다. 그 경우에는 아래 이미지처럼 클래스 계층 구조가 있을 수 있습니다. SuperClass가 메서드를 선언한 추상 클래스이고 ClassA, ClassB는 구체적인 클래스입니다.
SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//ClassA 자체 메서드
public void methodA(){
}
}
ClassB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//ClassB 특정 메서드
public void methodB(){
}
}
이제 ClassC 구현이 아래와 같다고 가정하고 ClassA 및 ClassB를 모두 확장합니다. ClassC.java
package com.journaldev.inheritance;
// 다이아몬드 문제를 설명하기 위한 가정일 뿐입니다
//이 코드는 컴파일되지 않습니다
public class ClassC extends ClassA, ClassB{
public void test(){
//슈퍼 클래스 메서드 호출
doSomething();
}
}
알림: test()
메서드가 상위 클래스 doSomething()
메서드를 호출하고 있습니다. 이로 인해 컴파일러가 어떤 상위 클래스 메서드를 실행할지 모르는 모호함이 발생합니다. 마름모 모양의 클래스 다이어그램 때문에 이것은 자바에서 다이아몬드 문제로 불립니다. 자바의 다이아몬드 문제는 클래스에서 다중 상속을 지원하지 않는 주요 이유입니다. 클래스에서 다중 상속과 관련된 위의 문제는 적어도 하나의 공통 메서드를 가진 세 개의 클래스만 있는 경우에도 발생할 수 있습니다.
자바 인터페이스에서의 다중 상속
여러분은 클래스에서 다중 상속이 지원되지 않는다고 항상 언급하는 것을 알아챘을 것입니다. 그러나 인터페이스에서는 지원됩니다. 단일 인터페이스는 여러 인터페이스를 확장할 수 있습니다. 아래는 간단한 예입니다. InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
두 인터페이스가 동일한 메서드를 선언하고 있는 것을 주목하세요. 이제 이러한 두 인터페이스를 모두 확장하는 인터페이스를 가질 수 있습니다. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//InterfaceA와 InterfaceB에서 동일한 메서드가 선언됨
public void doSomething();
}
이것은 완벽합니다. 인터페이스는 메서드를 선언하기만 하며 실제 구현은 인터페이스를 구현하는 구체적인 클래스에서 이루어집니다. 따라서 Java 인터페이스에서 다중 상속의 어떠한 모호성도 없습니다. 그래서 Java 클래스는 다음과 같은 예제처럼 여러 인터페이스를 구현할 수 있습니다. InterfacesImpl.java
package com.journaldev.inheritance;
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
@Override
public void doSomething() {
System.out.println("doSomething implementation of concrete class");
}
public static void main(String[] args) {
InterfaceA objA = new InterfacesImpl();
InterfaceB objB = new InterfacesImpl();
InterfaceC objC = new InterfacesImpl();
// 아래의 모든 메서드 호출은 동일한 구체적인 구현으로 이어집니다.
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
주목하셨나요? 매번 슈퍼클래스 메서드를 재정의하거나 인터페이스 메서드를 구현할 때마다 @Override 주석을 사용하고 있다는 사실을. 재정의 주석은 세 개의 내장된 자바 주석 중 하나이며, 메서드를 재정의할 때 항상 재정의 주석을 사용해야 합니다.
구성을 활용한 구현
ClassA
함수 methodA()
와 ClassB
함수 methodB()
를 ClassC
에서 활용하려면 어떻게 해야 할까요? 해결책은 구성을 사용하는 것에 있습니다. 여기에는 두 클래스의 메서드를 활용하고 또한 하나의 객체에서 doSomething() 메서드를 사용하는 ClassC의 리팩토링된 버전이 있습니다. ClassC.java
package com.journaldev.inheritance;
public class ClassC{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
public void test(){
objA.doSomething();
}
public void methodA(){
objA.methodA();
}
public void methodB(){
objB.methodB();
}
}
구성 vs 상속
자바 프로그래밍의 가장 좋은 실천 방법 중 하나는 “상속보다는 구성을 선호하는” 것입니다. 우리는 이 접근 방식을 선호하는 몇 가지 측면을 살펴볼 것입니다.
-
우리가 다음과 같은 수퍼클래스와 서브클래스를 가지고 있다고 가정해보십시오:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
위의 코드는 컴파일되고 정상적으로 작동하지만, 만약 ClassC의 구현이 다음과 같이 변경된다면 어떻게 될까요:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
test()
메서드가 이미 서브클래스에 존재하지만 반환 유형이 다릅니다. 이제 ClassD는 컴파일되지 않을 것이며, 만약 어떤 IDE를 사용 중이라면 수퍼클래스나 서브클래스 중 하나의 반환 유형을 변경하라는 제안을 해줄 것입니다. 이제 우리가 제어할 수 없는 다수의 수퍼클래스를 가진 상황을 상상해보십시오. 우리는 컴파일 오류를 제거하기 위해 서브클래스 메서드 서명이나 이름을 변경할 수밖에 없을 것입니다. 또한, 서브클래스 메서드가 호출되는 모든 위치에서 변경을 해야 할 것이므로 상속은 우리의 코드를 취약하게 만듭니다. 상속을 사용하지 않고 합성을 사용하는 경우 위와 같은 문제가 발생하지 않으며, 이로 인해 합성이 상속보다 더 선호됩니다. -
상속의 또 다른 문제는 모든 수퍼클래스 메서드를 클라이언트에게 노출한다는 것입니다. 수퍼클래스가 제대로 설계되지 않았거나 보안 취약점이 있는 경우, 우리가 클래스를 구현할 때 완전한 주의를 기울이더라도 수퍼클래스의 부실한 구현으로 인해 영향을 받게 됩니다. 합성은 수퍼클래스 메서드에 대한 제어된 액세스를 제공하는 데 도움이 되지만, 상속은 수퍼클래스 메서드의 어떤 제어도 제공하지 않습니다. 이것도 합성이 상속보다 우위에 있는 주요한 이점 중 하나입니다.
-
구성의 또 다른 이점은 메소드 호출의 유연성을 제공한다는 것입니다. 위의
ClassC
구현은 최적이 아니며, 메소드 호출 시 컴파일 타임 바인딩을 제공하여 호출될 메소드를 결정합니다. 최소한의 변경으로 메소드 호출을 유연하고 동적으로 만들 수 있습니다.ClassC.java
package com.journaldev.inheritance; public class ClassC{ SuperClass obj = null; public ClassC(SuperClass o){ this.obj = o; } public void test(){ obj.doSomething(); } public static void main(String args[]){ ClassC obj1 = new ClassC(new ClassA()); ClassC obj2 = new ClassC(new ClassB()); obj1.test(); obj2.test(); } }
위 프로그램의 출력은 다음과 같습니다:
A의 doSomething 구현 B의 doSomething 구현
이러한 메소드 호출의 유연성은 상속에서 사용할 수 없으며, 구성을 선호하는 최선의 방법을 강조합니다.
-
구성에서 단위 테스트는 간단합니다. 우리는 상위 클래스에서 사용하는 모든 메소드를 알고 있으며, 테스트를 위해 가짜 객체를 만들 수 있습니다. 그러나 상속에서는 상위 클래스에 크게 의존하며, 상위 클래스의 모든 메소드를 사용할지 알 수 없으므로 상위 클래스의 모든 메소드를 테스트해야 합니다. 이는 추가 작업이며, 상속 때문에 불필요하게 수행해야 합니다.
자바에서 다중 상속과 구성에 대해 간략하게 살펴보았습니다. 이것으로 끝입니다.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java