추상 클래스와 인터페이스 적재적소에 사용하기
자바가 제공하는 다중 구현 매커니즘에는 추상 클래스와 인터페이스가 있다.
아주 친숙한 개념이지만 막상 다중 구현을 할 때 어떤 매커니즘을 사용해야 할지 의문이 생길 때가 많다.
Java8부터는 인터페이스가 디폴트 메서드를 제공하게 되면서 추상 클래스와 인터페이스 간의 차이가 더 줄어들었다. 이 말은, 추상 클래스가 할 수 있는 일은 인터페이스도 할 수 있게 되었다는 뜻이다.
그렇다면 대체 언제 추상 클래스를 사용하고 언제 인터페이스를 사용해야 하는 것일까?
추상 클래스
추상 클래스의 핵심 목적
추상 클래스는 자신을 상속받아 구현하는 모든 클래스가 자신의 하위 타입이 되는 것에 목적을 가지고 있다.
즉, 하위 클래스가 자신의 기능을 상속받아 그 기능을 확장을 하는 것에 목적을 가지고 있다.
따라서 추상 클래스는 기능에 초점을 맞추기보다는 개념의 구조적 표현에 초점이 맞춰져 있다고 볼 수 있다.
다음 예시는 Figure라는 추상 클래스를 상속받는 하위 클래스가 어떻게 그 기능을 확장하고 있는지 보여준다.
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
final double length;
final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}
Figure를 상속하고 있는 Cirecle과 Rectangle은 도형이라는 구조를 표현하면서 이를 구하는 기능을 자신에 맞게 재정의하고 있다.
추상 클래스 사용 기준
자바의 단일 상속 특성상 추상 클래스의 하위 클래스는 다른 클래스를 확장할 수 없고 반드시 추상 클래스의 하위 클래스가 되어야 하므로 클래스 계층 구조의 is-a
관계의 근거가 명확해야 한다.
즉, 하위 클래스는 확실히 추상 클래스인가에 대한 질문에 확실히 ‘그렇다'라고 답할 수 있어야 한다.
- 사각형은 확실히 도형인가?
- 스포츠카는 확실히 자동차인가?
- 고양이는 확실히 동물인가?
위와 같은 질문에 ‘그렇다'라고 답할 수 있어야 추상 클래스를 선택하는 것에 근거가 된다는 것이다.
따라서 추상 클래스를 선택할 때에는 클래스 계층 구조가 명확하고 어떤 개념을 구조적으로 잘 표현할 수 있을 때 사용하는 것이 바람직하다.
인터페이스
인터페이스의 핵심 목적
인터페이스는 자신을 구현하는 모든 클래스가 같은 기능을 구현하도록 강제하는 것에 목적이 있다.
따라서 어떤 기능의 일관된 구현을 강제하는 것에 목적을 가지고 있다고 볼 수 있다.
자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 말해주는 타입 정의 용도로 사용하며, 추상 클래스와는 다르게 기능에 초점을 맞춘다.
인터페이스는 다중 상속을 지원하므로 새로운 타입을 정의하는데 제약이 사라지고 구조적 표현이 관심사가 아니기 때문에 전혀 다른 클래스도 같은 인터페이스를 가질 수 있다.
public interface Moveable {
boolean move();
}
public Car implements Moveable {
@Override
boolean move() {
// ....
}
}
public Human implements Moveable {
@Override
boolean move() {
// ....
}
}
앞서 말했듯, 인터페이스는 어떤 일을 할 수 있는지에 초점이 맞춰있기 때문에 일반적으로 명사나 ~able, ~ible로 끝나는 형용사의 이름이 잘 어울린다.
인터페이스 사용 기준
인터페이스는 일반적으로 계층 구조로 표현하기 힘든 개념이나 해당 클래스가 어떤 기능을 할 수 있는지 표현할 때 많이 사용한다. 추상 클래스가 할 수 있는 대부분의 일은 인터페이스도 할 수 있기 때문에 일반적으로 추상 클래스보다 인터페이스를 우선 고려하는 편이다.
계층 구조로 표현하기 힘든 개념
가수와 작곡가라는 개념을 구현한다고 생각해보자.
어떤 사람은 가수 겸 작곡가로 활동하고 있고 또 어떤 사람은 가수로, 어떤 사람은 작곡가로 활동할 수 있다.
우리는 인터페이스를 통해 이러한 조합들을 손쉽게 만들어낼 수 있다.
public interface Singer {
void sing(Song s);
}
public interface SongWriter {
void compose(int chartPosition);
}
위와 같은 인터페이스를 만들어 놓고 자신에게 해당하는 인터페이스만 구현하면 된다.
public SomeSinger implements Singer {
@Override
public void sing(Song s) {
// ....
}
}
public SomeSongWriter implements SongWriter {
@Override
public void compose(int chartPosition) {
// ....
}
}
public SomeSingerSongWriter implements Singer, SongWriter {
@Override
public void sing(String s) {
// ....
}
@Override
public void compose(int chartPosition) {
// ....
}
}
혹은 앞서 만들어 놓은 인터페이를 활용해 또 다른 인터페이스를 만들어낼 수도 있다.
public interface SingerSongWriter extends Singer, SongWriter{
void strum();
void actSensitive();
}
해당 클래스가 어떤 일들을 할 수 있는지 표현
// print와 clone 기능을 가지고 있다는 것을 표현한다.
public SomeClass implements Printable, Cloneable {
@Override
public void print() {
// ....
}
}
인터페이스는 위와 같은 사용법 외에도 기존 클래스에 어떤 기능을 아주 쉽게 끼워 넣을 수 있고 마커 인터페이스나 함수형 인터페이스로도 활용할 수 있다는 장점을 가지고 있다.
하지만 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버도 가질 수 없다는 단점도 존재하므로 이를 잘 고려해 사용해야 한다.
추상 골격 구현 클래스를 함께 제공하면 추상 클래스와 인터페이스의 장점을 모두 취할 수 있다.
핵심 정리
- 추상 클래스는 개념의 구조적 표현과 하위 타입이 필요할 때 고려하자.
- 추상 클래스를 선택할 때는 구조적 개념의 is-a 관계가 명확한지 따져보자.
- 하위 클래스는 확실히 추상 클래스인가에 대한 질문에 확실히 ‘그렇다'라고 답할 수 있어야 한다.
- ex) 사각형은 확실히 도형인가?
- 구조적 표현이나 관계가 불명확한 경우 인터페이스를 사용하자.
- 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 말해주는 타입 정의 용도라면 인터페이스를 사용하자.
- 추상 클래스의 대부분의 기능을 인터페이스도 할 수 있으므로 인터페이스를 우선 고려하자.
- 추상 클래스와 인터페이스를 적절히 사용해 추상 골격 구현 클래스를 제공하면 이 둘의 장점을 모두 취할 수 있다.