HOWTO · Java
Java에서 두 개의 클래스 확장
Java 클래스가 하나의 클래스만 확장할 수 있는 이유와 인터페이스와 컴포지션이 여러 타입의 동작을 어떻게 결합하는지 알아보세요.
이 페이지의 내용
Java 클래스는 두 클래스를 상속할 수 없습니다. 클래스에는 직접 상위 클래스가 하나만 있으므로 class Child extends First, Second와 같은 선언은 유효한 Java 문법이 아닙니다. 여러 타입의 동작을 결합하려면 클래스 하나를 상속하면서 필요한 만큼 인터페이스를 구현하거나, 다른 객체를 보유하고 컴포지션을 통해 작업을 위임하면 됩니다.
Java 클래스가 두 클래스를 상속할 수 없는 이유
Java의 클래스 상속은 단일 상속입니다. 각 클래스는 extends로 상위 클래스를 하나까지만 지정할 수 있습니다. 따라서 객체에는 하나의 클래스 상속 계층만 생기며, 두 부모 클래스가 같은 멤버를 정의할 때 어떤 구현이나 상태를 적용할지 모호해지는 일을 피할 수 있습니다. 상위 클래스를 두 개 나열한 클래스 선언은 컴파일러가 거부합니다. 이는 프로그램 실행 중 발생하는 문제가 아니라 컴파일 시점의 제약입니다.
다음은 의도적으로 작성한 잘못된 Java 문법이며 컴파일되지 않습니다: class Child extends First, Second {}. 새 클래스가 실제로 그 타입의 특수한 형태일 때는 상위 클래스를 하나 사용하세요. 다음 섹션에서는 인터페이스 계약으로 추가 기능을 표현하는 방법을 살펴봅니다.
상위 클래스 하나를 상속하고 여러 인터페이스 구현하기
클래스는 클래스 하나를 상속하면서 여러 인터페이스를 구현할 수 있습니다. 상위 클래스는 공통 상태나 구현을 제공할 수 있고, 각 인터페이스는 클래스가 제공하겠다고 약속하는 기능을 선언합니다. Java 키워드를 구분해야 합니다. 클래스는 상위 클래스에 extends를 사용하고 인터페이스에는 implements를 사용합니다. 인터페이스 자체는 여러 인터페이스를 상속할 수 있지만, 그렇다고 클래스가 여러 상위 클래스를 갖게 되는 것은 아닙니다.
다음의 완전한 예제에서는 인터페이스 계약 두 개를 정의하고 상위 클래스 하나와 함께 모두 구현합니다:
public class CatalogItem extends Record implements Printable, Identifiable {
CatalogItem(String name) {
super(name);
}
@Override
public void printLabel() {
System.out.println(name());
}
@Override
public String id() {
return "item-" + name().toLowerCase();
}
public static void main(String[] args) {
CatalogItem item = new CatalogItem("Book");
item.printLabel();
System.out.println(item.id());
}
}
interface Printable {
void printLabel();
}
interface Identifiable {
String id();
}
class Record {
private final String name;
Record(String name) {
this.name = name;
}
String name() {
return name;
}
}
Book
item-book
CatalogItem은 Record에서 name() 구현을 상속하고 두 인터페이스가 요구하는 메서드를 제공합니다. 서로 관계없는 클래스들이 같은 타입이나 기능 계약을 공유해야 할 때 인터페이스를 사용할 수 있습니다. 인터페이스는 두 번째 상위 클래스처럼 상태나 클래스 구현을 자동으로 제공하지 않습니다.
컴포지션으로 클래스들의 동작 결합하기
컴포지션은 포함 관계(has-a 관계)를 통해 객체를 결합합니다. 클래스가 협력 객체에 대한 참조를 저장하고 그 객체의 메서드를 호출하는 방식입니다. 각 동작을 독립적으로 활용할 수 있거나 협력 객체를 교체할 수 있어야 할 때 유용합니다. 포함하는 클래스는 외부에 공개할 작업을 정하고, 호출자가 필요한 작업만 위임할 수 있습니다.
다음 예제에서는 SmartDevice가 PowerController와 MessageSender를 포함하고 각 작업을 이 객체들에 위임합니다:
public class SmartDevice {
private final PowerController power = new PowerController();
private final MessageSender sender = new MessageSender();
void start() {
power.powerOn();
}
void sendMessage(String message) {
sender.send(message);
}
public static void main(String[] args) {
SmartDevice device = new SmartDevice();
device.start();
device.sendMessage("Ready");
}
}
class PowerController {
void powerOn() {
System.out.println("Power on");
}
}
class MessageSender {
void send(String message) {
System.out.println("Sending: " + message);
}
}
Power on
Sending: Ready
SmartDevice는 PowerController나 MessageSender가 되는 것이 아니라 두 객체를 사용합니다. 규모가 큰 프로그램에서는 생성자를 통해 협력 객체를 전달하거나 인터페이스로 표현할 수 있습니다. 그러면 구현을 교체하기 쉽고 포함하는 클래스를 독립적으로 테스트하기도 편해집니다.
적절한 재사용 방식 선택하기
새 타입이 상위 클래스 하나의 특수한 형태이고 그 구현을 상속해야 한다면 클래스 상속을 선택하세요. 다른 클래스들과 공유하는 기능을 제공하겠다고 타입이 약속해야 한다면 인터페이스를 추가하세요. 동작을 여러 구성 요소로 조립하거나 위임하거나 각각 독립적으로 변경해야 한다면 컴포지션을 사용하세요.
각 클래스의 책임을 명확하게 유지하고 호출자에게 필요한 작업만 공개하세요. 범위가 넓은 인터페이스보다 작은 인터페이스 계약을 우선하고, 공통 협력 객체에 속하는 동작을 중복 구현하지 마세요. 이러한 선택은 요구 사항이 바뀌어도 관계를 이해하고 테스트하고 조정하기 쉽게 해줍니다. 요약하면 Java 클래스는 두 클래스를 상속할 수 없습니다. 일반적인 재사용 요구는 상위 클래스 하나, 여러 인터페이스 계약, 그리고 컴포지션으로 결합한 협력 객체로 해결할 수 있습니다.