■ 추상 클래스와 인터페이스의 차이점
- 추상 클래스 : 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야한다.
- 인터페이스 : 인터페이스에서 정의한 메서드를 모두 정의한 클래스라면 다른 어떤 클래스를 상속했든 상관없이 같은 타입으로 취급.
즉, 인터페이스는 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다. 인터페이스가 요구하는 메서드를 구현하고, implements만 추가하면 된다.
추상 클래스의 경우 2개의 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층 구조상 두 클래스의 공통 조상이어야한다. 새로 추가된 추상 클래스의 모든 자손이 적절하지 않은 상황에서도 이를 상속하게 된다.
■ 믹스인 정의
인터페이스는 믹스인 정의에 맞춤이다. 믹스인이란 클래스가 주기능 외에도 추가적인 기능을 혼합한 것이다. 믹스인을 활용한 예시로 Comparable이 있다. Comparable을 구현할 경우 클래스의 인스턴스끼리의 순서를 정할 수 있게된다. 이처럼 대상 타입의 주된 기능에 선택적 기능을 '혼합'한다고해서 믹스인이라고한다.
■ 계층 구조가 없는 타입 프레임워크
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 가수와 작곡가라는 인터페이스가 있고 이를 확장하는 SingerSongwriter 인터페이스를 만들 수 있다.
public interface Singer {
void sing();
}
public interface SongWriter {
void compose();
}
public interface SingerSongwriter extends Singer, SongWriter {
void actSensitive();
}
public class People implements SingerSongwriter {
@Override
public void actSensitive() {
}
@Override
public void sing() {
}
@Override
public void compose() {
}
}
위의 코드를 클래스로 같은 구조를 만들려면 속성이 n개라면 지원해야 할 조합의 수는 2^n개가 된다. 결국 고도 비만의 계층구조가 만들어진다.
public abstract class Singer {
abstract void sing();
}
public abstract class SongWriter {
abstract void compose();
}
public abstract class SingerSongWriter {
abstract void actSensitive();
abstract void sing();
abstract void compose();
}
■ 추상 골격 구현 클래스
Java8이 등장하고 부터는 인터페이스에 디폴트 메소드의 기능을 제공해주면서 중복되는 메서드 구현의 시간을 줄여주었다. 하지만 디폴트 메서드의 경우 여러 단점이 존재하기 때문에 추상 골격 구현 클래스를 제공함으로써 인터페이스와 추상 클래스의 장점을 모두 가져갈 수 있다.
- 본인이 만든 인터페이스가 아니면 디폴트 메서드를 추가할 수 없다.
- 인터페이스는 인스턴스 필드를 가질 수 없고 public이 아닌 정적 메서드를 가질 수 없다.
- Object 메소드인 equals와 hashcode를 디폴트 메소드로 제공 안함
인터페이스로는 타입을 정의하고 메소드 구현이 필요한 부분은 추상 골격 구현 클래스에서 구현하는 것 이다. 아래 소스는 attack() 메서드를 제외하고 모두 중복되는 것을 볼 수 있다. 중복된 부분을 추상 골격 구현 클래스를 이용하여 정의하는 것 이다.
//추상 골격 구현 클래스 사용 하지 않는 버전.
public interface Character {
public void move();
public void seat();
public void attack();
}
public class Thief implements Character{
@Override
public void move() {
System.out.println("걷다");
}
@Override
public void seat() {
System.out.println("앉다");
}
@Override
public void attack() {
System.out.println("표창을 던진다");
}
}
public class Wizard implements Character{
@Override
public void move() {
System.out.println("걷다");
}
@Override
public void seat() {
System.out.println("앉다");
}
@Override
public void attack() {
System.out.println("마법봉을 휘두르다");
}
}
public static void main(String[] args) {
Thief thief = new Thief();
Wizard wizard = new Wizard();
thief.process();
wizard.process();
}
아래 코드는 추상 골격 구현 클래스를 이용하여 정의한 코드이다.
//추상 골격 구현 클래스 사용하는 버전
public abstract class AbstractCharacter implements Character{
@Override
public void move() {
System.out.println("걷다");
}
@Override
public void seat() {
System.out.println("앉다");
}
@Override
public void process() {
move();
seat();
attack();
}
}
public class Thief extends AbstractCharacter implements Character{
@Override
public void attack() {
System.out.println("표창을 던진다");
}
}
public class Wizard extends AbstractCharacter implements Character{
@Override
public void attack() {
System.out.println("마법봉을 휘두르다");
}
}
■ 정리
- 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해보자.
- 골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다. 가능한 한이라고 말한 것은 인터페이스에 걸여 있는 구현상의 제약 때문에 추상 클래스로 골격 구현을 제공하는 경우가 흔해서이다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템22 인터페이스는 타입을 정의하는 용도로만 사용하라 (2) | 2021.03.01 |
---|---|
[Effective Java] 아이템21 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2021.03.01 |
[Effective Java] 아이템19 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2021.02.23 |
[Effective Java] 아이템18 상속보다는 컴포지션을 사용하라 (0) | 2021.02.21 |
[Effective Java] 아이템17 변경 가능성을 최소화하라 (0) | 2021.02.17 |