Post

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 클래스의 메서드를 디폴트 메서드로 제공하면, 인터페이스가 클래스를 이겨버리는 상황이다.
    • 디폴트 메서드 핵심 목적은 “인터페이스의 진화”.

인터페이스와 추상 골격 구현 클래스(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();
    }
}
  • WashingMachineMicrowave 클래스는 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...");
    }
}

골격 구현 작성 단계

  1. 인터페이스 내에 다른 메서드들의 구현에 사용되는 기반 메서드들을 선정한다.
    • 해당 기반 메서드들은 골격 구현에서는 추상 메서드가 될 것 이다.
  2. 기반 메서드들을 사용하여 직접 구현할 수 있는 메서드들은 디폴트 메서드로 제공한다.
    • 단 Object 메서드는 제외한다.
  3. 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 있다면, 해당 인터페이스를 구현하는 골격 구현 클래스에서 작성하자.

단순 구현(simple implementation)

  • 상속을 위해 인터페이스를 구현했지만 추상 클래스가 아닌 것.

💡 핵심 정리

  • 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
  • 복잡한 인터페이스라면 골격 구현을 함께 제공하는 방법을 고려하자.
  • 골격 구현은 가능한 한 인터페이스의 디폴트 메서드로 제공하여 해당 인터페이스를 구현한 클래스에서 활용하도록 하는 것이 좋다.
This post is licensed under CC BY 4.0 by the author.