인터페이스(interface)는 계약(contract) 동작을 정의하는 데 사용할 수 있으며, 두 시스템 간의 상호작용을 위한 계약 역할도 합니다. 반면 추상 클래스(abstract class)는 주로 하위 클래스의 기본 동작(default behavior)을 정의하는 데 사용됩니다. 즉, 모든 자식 클래스가 동일한 기능을 수행하도록 강제하는 것입니다.
추상 클래스를 사용해야 하는 경우
- 상속 개념을 활용할 때: 추상 클래스는 파생 클래스에 공통 기반 구현을 제공하므로 상속 구조를 설계할 때 좋은 선택입니다.
- 비공개(non-public) 멤버를 선언하고 싶을 때: 인터페이스에서는 모든 메서드가 public이어야 하지만, 추상 클래스는 private, protected 등 다양한 접근 제어자를 사용할 수 있습니다.
- 미래에 새로운 메서드를 추가할 가능성이 있을 때: 인터페이스에 새 메서드를 추가하면 해당 인터페이스를 이미 구현한 모든 클래스가 새 메서드를 구현하도록 수정해야 합니다. 추상 클래스는 이런 문제가 없습니다.
- 컴포넌트의 여러 버전을 관리할 때: 추상 클래스는 버전 관리를 위한 간단하고 쉬운 방법을 제공합니다. 기반 클래스를 업데이트하면 상속받는 모든 클래스가 자동으로 변경 사항을 반영합니다. 반면 인터페이스는 한 번 만들면 변경하기 어렵고, 새 버전이 필요하면 완전히 새로운 인터페이스를 만들어야 하는 경우가 많습니다.
- 더 나은 하위 호환성(forward compatibility)이 필요할 때: 클라이언트가 이미 인터페이스를 사용 중이라면 변경할 수 없지만, 추상 클래스라면 기존 코드를 깨뜨리지 않고도 새로운 동작을 추가할 수 있습니다.
- 모든 구현체에 공통으로 구현된 기능을 제공하고 싶을 때: 추상 클래스는 클래스를 부분적으로 구현할 수 있게 해주지만, 전통적인 인터페이스에는 어떤 멤버의 구현도 포함되어 있지 않습니다.
예제
abstract class Car {
public void accelerate() {
System.out.println("Do something to accelerate");
}
public void applyBrakes() {
System.out.println("Do something to apply brakes");
}
public abstract void changeGears();
}
이제 Car를 상속하여 인스턴스화되기를 원하는 모든 클래스는 changeGears() 메서드를 반드시 구현해야 합니다.
class Alto extends Car {
public void changeGears() {
System.out.println("Implement changeGears() method for Alto Car");
}
}
class Santro extends Car {
public void changeGears() {
System.out.println("Implement changeGears() method for Santro Car");
}
}
인터페이스를 사용해야 하는 경우
- 기능이 서로 다른 다양한 객체에 유용할 때: 추상 클래스는 주로 밀접하게 관련된 객체에 사용해야 하며, 인터페이스는 관련 없는 클래스들에 공통 기능을 제공하는 데 가장 적합합니다.
- API가 당분간 변경되지 않을 것으로 예상될 때: 안정적인 API를 설계한다면 인터페이스가 좋은 선택입니다.
- 다중 상속과 유사한 효과가 필요할 때: Java는 클래스의 다중 상속을 지원하지 않지만, 여러 인터페이스를 구현할 수 있으므로 인터페이스로 이를 대체할 수 있습니다.
- 작고 간결한 기능 단위를 설계할 때: 작은 기능 조각에는 인터페이스를, 큰 기능 단위에는 추상 클래스를 사용하는 것이 일반적입니다.
예제
public interface Actor {
void perform();
}
public interface Producer {
void invest();
}
요즘 대부분의 배우들은 자신만의 영화를 제작할 만큼 충분히 부유합니다. 추상 클래스 대신 인터페이스를 사용한다면 하나의 클래스가 Actor와 Producer를 모두 구현할 수 있습니다. 또한 둘을 확장하는 새로운 ActorProducer 인터페이스를 정의할 수도 있습니다.
public interface ActorProducer extends Actor, Producer {
// some statements
}
참고: Java 8 이후의 변화
Java 8부터는 인터페이스에서도 default 메서드와 static 메서드를 통해 구현을 가질 수 있게 되었습니다. 따라서 "인터페이스에는 구현이 포함될 수 없다"는 내용은 Java 8 이전 기준이며, 최신 Java에서는 상황에 따라 달라질 수 있습니다. 그럼에도 불구하고 공통 상태(인스턴스 필드)를 유지해야 하거나 protected 멤버가 필요한 경우에는 여전히 추상 클래스가 더 적합한 선택입니다.