Item 20 - 추상 클래스보다는 인터페이스를 우선하라
인터페이스의 장점
- 자바 8부터 인터페이스도 디폴트 메서드를 제공할 수 있다.
- 기존 클래스에 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
- BUT, 추상 클래스는 그렇지 않다.
- 새로 추가된 추상 클래스의 모든 자손이 상속 구조를 가지면서 혼란을 준다.
- 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다. (선택적인 기능 추가)
- 원래의 ‘주된 타입’ 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
- BUT, 추상 클래스는 그렇지 않다. - 두 부모를 둘 수 없기 때문이다.
- 인터페이스는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
- Singer, Songwriter를 모두 구현해도 문제되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
public interface Singer {
// ...
}
public interface Songwriter {
// ...
}
public interface SingerSongWriter extends Singer, Songwriter {
// ...
}
- 래퍼 클래스와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.
- BUT, 추상 클래스는 기능 추가 방법이 상속밖에 없어서 활용도가 떨어진다.
디폴트 메서드
- 인터페이스의 메서드 중 구현이 명백한 것은 인터페이스의 디폴트 메서드로 제공할 수 있다.
- 하지만 Object 클래스의 메서드를 디폴트 메서드로 제공하면 안된다. 왜..🤔 ?
- Object 클래스의 메서드는 모든 클래스가 암묵적으로 상속받는 기본 메서드다.
- 이미 모든 객체에 대해 구현되어 있다는 의미.
- Object 클래스의 메서드를 디폴트 메서드로 제공하면, 인터페이스가 클래스를 이겨버리는 상황이다.
- 디폴트 메서드 핵심 목적은 “인터페이스의 진화”.
- Object 클래스의 메서드는 모든 클래스가 암묵적으로 상속받는 기본 메서드다.
인터페이스와 추상 골격 구현 클래스(skeletal implementation)
- Template Method 패턴
- 인터페이스와 추상 클래스의 장점을 모두 취할 수 있다.
- 인터페이스로는 타입을 쉽게 정의하고, 필요하다면 디폴트 메서드 몇 개도 함께 제공한다.
- 골격 구현 클래스는 나머지 메서드들까지 구현한다.
예시 코드
1
2
3
4
5
6
7
// Appliance 인터페이스
public interface Appliance {
void turnOn();
void turnOff();
void use();
void doRoutine();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// WashingMachine 클래스
public class WashingMachine implements Appliance {
@Override
public void turnOn() {
System.out.println("Powering on the appliance");
}
@Override
public void turnOff() {
System.out.println("Powering off the appliance");
}
@Override
public void use() {
System.out.println("Washing clothes...");
}
@Override
public void doRoutine() {
greet();
eat();
sleep();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Microwave 클래스
public class Microwave implements Appliance {
@Override
public void turnOn() {
System.out.println("Powering on the appliance");
}
@Override
public void turnOff() {
System.out.println("Powering off the appliance");
}
@Override
public void use() {
System.out.println("Heating food...");
}
@Override
public void doRoutine() {
greet();
eat();
sleep();
}
}
WashingMachine
과Microwave
클래스는Appliance
인터페이스를 구현하고 있다.- 두 클래스는
use()
를 제외한 모든 메서드 동작이 같다.- 추상 골격 클래스로 중복 코드 제거 가능!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 추상골격 구현 클래스 (보통 Abstract~ 네이밍을 사용)
public abstract class AbstractAppliance implements Appliance {
// 같은 동작을 하는 코드들만 재정의한다.
@Override
public void turnOn() {
System.out.println("Powering on the appliance");
}
@Override
public void turnOff() {
System.out.println("Powering off the appliance");
}
@Override
public void doRoutine() {
turnOn();
use();
turnOff();
}
}
1
2
3
4
5
6
7
// WashingMachine 클래스
public class WashingMachine extends AbstractAppliance {
@Override
public void use() {
System.out.println("Washing clothes...");
}
}
1
2
3
4
5
6
7
// Microwave 클래스
public class Microwave extends AbstractAppliance {
@Override
public void use() {
System.out.println("Heating food...");
}
}
단순히 골격 구현을 확장하는 것만으로 인터페이스를 구현하는데 필요한 일이 완료된다.
시뮬레이트한 다중 상속(simulated multiple inheritance)
- 다중 상속의 많은 장점을 제공하는 동시에 단점은 피하게 해준다.
- 어떤 클래스가 상속 받아야 하는 클래스가 이미 있어서 추상 골격 구현 클래스를 상속 받지 못한다면 시뮬레이트한 다중 상속을 사용할 수 있다.
예시코드
- WashingMachine이 Electronic을 상속받아야 해서 추상 골격 구현 클래스를 상속받지 못할 경우
- 시뮬레이트한 다중상속 이용하자!
1
2
3
4
5
6
// 기본적인 Electronic 기기 클래스
public class Electronic {
public void checkVoltage() {
System.out.println("Checking voltage...");
}
}
1
2
3
4
5
6
7
// 골격 구현 클래스를 확장한 클래스
public class InnerAbstractAppliance extends AbstractAppliance {
@Override
public void use() {
System.out.println("Using a general appliance...");
}
}
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
// HomeAppliance 클래스
public class HomeAppliance extends Electronic implements Appliance {
// 확장된 골격 구현을 private 필드로 정의
private InnerAbstractAppliance innerAbstractAppliance = new InnerAbstractAppliance();
@Override
public void turnOn() {
// 각 메서드 호출을 내부 클래스의 인스턴스에 전달
innerAbstractAppliance.turnOn();
}
@Override
public void turnOff() {
innerAbstractAppliance.turnOff();
}
@Override
public void use() {
innerAbstractAppliance.use();
}
@Override
public void doRoutine() {
checkVoltage(); // 전압 체크 기능을 추가
innerAbstractAppliance.doRoutine();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 구체적인 Appliance 구현 클래스
public class WashingMachine extends HomeAppliance {
@Override
public void use() {
System.out.println("Washing clothes...");
}
}
public class Microwave extends HomeAppliance {
@Override
public void use() {
System.out.println("Heating food...");
}
}
골격 구현 작성 단계
- 인터페이스 내에 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정한다.
- 해당 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것 이다.
- 기반 메서드들을 사용하여 직접 구현할 수 있는 메서드들은 디폴트 메서드로 제공한다.
- 단 Object 메서드는 제외한다.
- 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 있다면, 해당 인터페이스를 구현하는 골격 구현 클래스에서 작성하자.
단순 구현(simple implementation)
- 상속을 위해 인터페이스를 구현했지만 추상 클래스가 아닌 것.
💡 핵심 정리
- 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
- 복잡한 인터페이스라면 골격 구현을 함께 제공하는 방법을 고려하자.
- 골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 해당 인터페이스를 구현한 클래스에서 활용하도록 하는 것이 좋다.
This post is licensed under CC BY 4.0 by the author.