[Effective Java] 추상 클래스보다는 인터페이스를 우선하라

들어가며

자바가 제공하는 다중 구현 메커니즘으로 인터페이스와 추상 클래스가 있다. 자바 8 부터는 인터페이스도 디폴트 메소드를 제공할 수 있게 되어 두 메커니즘 모두 인스턴스 메소드를 구현 형태로 제공할 수 있다. 둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다.

인터페이스의 장점

인터페이스는 믹스인 정의에 안성맞춤이다. 믹스인이란 클래스가 구현할 수 있는 타입으로 믹스인을 구현한 클래스에 원래 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다. 예를 들어 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 자바는 다중상속이 안되기 때문에 추상 클래스로는 믹스인을 정의할 수 없다.

인터페이스는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에서는 계층을 엄격히 구분하기 어려운 개념도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
public interface Singer {
AudioClip sing(Song s);
}

public interface Songwriter {
Song compose(int chartPosition);
}

public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}

이와 같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어진다. 흔히 조합 폭발이라 부르는 현상이다.

래퍼클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다. 타입을 추상 클래스로 정의하면 그 타입에 기능을 추가하는 방법은 상속 뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도와 안정성이 떨어진다.

인터페이스의 활용

인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면 그 구현을 디폴트 메소드로 제공해 개발자의 일을 덜어주면 된다. 다만 equals와 hashCode같은 Object 메소드들을 디폴트 메소드로 제공하지 말자.

또한 인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적 멤버도 가질 수 없다. 단, JAVA 9 이상 부터는 static private는 예외다.

인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다. 인터페이스로는 타입을 정의하고 필요하면 디폴트 메소드 몇 개도 함께 제공한다. 그리고 골격 구현 클래스는 나머지 메소드들 까지 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface SimpleCalculator {
int plus(int num1, int num2);
int minus(int num1, int num2);
int division(int num1, int num2);
int simplePow(int x);
void doCalculate(int num1, int num2);

//divide & conquer pow를 default로 제공
default int pow(int x, int n) {
return n == 1 ? x : (n & 1) == 1 ? pow(x, n - 1) * n : simplePow(pow(x, n>>1));
}
}

public abstract class AbstractCalculator implements SimpleCalculator {
@Override
public int plus(int num1, int num2) {
return num1 + num2;
}

@Override
public int minus(int num1, int num2) {
return num1 - num2;
}

//multiply는 추상 메소드로 선언하고 simplePow에서 호출
public abstract int multiply(int num1, int num2);

@Override
public int division(int num1, int num2) {
return num1 / num2;
}

@Override
public int simplePow(int x) {
return multiply(x, x);
}
}

public class CalculatorImpl extends AbstractCalculator implements SimpleCalculator{
//doCalculate의 구현
@Override
public void doCalculate(int num1, int num2) {
System.out.println(num1 + " + " + num2 + " = " + plus(num1, num2));
System.out.println(num1 + " - " + num2 + " = " + minus(num1, num2));
System.out.println(num1 + " * " + num2 + " = " + multiply(num1, num2));
System.out.println(num1 + " / " + num2 + " = " + division(num1, num2));
System.out.println(num1 + " ^ " + num2 + " = " + pow(num1, num2));
}

//abstract multiply의 구현
@Override
public int multiply(int num1, int num2) {
return num1 * num2;
}
}
Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/08/Languages/Effective%20JAVA/item20/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.