학습 자료

인터페이스와 다형성 — 구현을 바꿔 끼운다


Article

주문이 끝나면 메일을 보낸다.

class EmailNotifier {
    void send(String msg) { System.out.println("[메일] " + msg); }
}

class OrderService {
    private EmailNotifier notifier = new EmailNotifier();

    void order() {
        notifier.send("주문 완료");
    }
}

잘 돌아간다. 그런데 문자로도 보내달라는 요구가 들어왔다.

SmsNotifier 를 만들었다고 하자. 그럼 OrderService 를 고쳐야 한다. 필드 타입도, new 하는 부분도. 나중에 카카오톡이 추가되면 또 고친다.

메일을 보내는 일과 주문을 처리하는 일은 다른 일이다. 그런데 OrderServiceEmailNotifier 라는 이름을 알고 있는 한, 둘은 붙어 있다.

약속만 정한다

OrderService 에게 필요한 건 EmailNotifier 가 아니다. send(String) 을 할 줄 아는 무언가다.

그 "할 줄 아는 것"만 적어둔다.

interface Notifier {
    void send(String msg);
}

몸통이 없다. 세미콜론으로 끝난다. 무엇을 할 수 있는지만 적고, 어떻게 하는지는 안 적는다.

이제 실제로 할 줄 아는 클래스들이 이 약속을 지키겠다고 선언한다.

class EmailNotifier implements Notifier {
    public void send(String msg) { System.out.println("[메일] " + msg); }
}

class SmsNotifier implements Notifier {
    public void send(String msg) { System.out.println("[문자] " + msg); }
}

implements 는 "이 약속을 지킨다"는 뜻이다. 약속을 안 지키면 컴파일러가 잡는다.

class SmsNotifier implements Notifier { }
터미널
  • error: SmsNotifier is not abstract and does not override
  • abstract method send(String) in Notifier

인터페이스의 메서드는 public 이다. 구현할 때 public 을 빼먹으면 "접근 범위를 좁힐 수 없다"고 혼난다.

구현을 바꿔 끼운다

이제 OrderService 는 구체적인 이름을 모른다.

class OrderService {
    private final Notifier notifier;

    OrderService(Notifier notifier) {
        this.notifier = notifier;
    }

    void order() {
        notifier.send("주문 완료");
    }
}

필드 타입이 Notifier 다. new 도 하지 않는다. 밖에서 만들어서 넣어준다.

new OrderService(new EmailNotifier()).order();
new OrderService(new SmsNotifier()).order();
콘솔
  • [메일] 주문 완료
  • [문자] 주문 완료

OrderService 코드는 한 글자도 안 바뀌었다. 카카오톡을 추가해도 안 바뀐다.

14강에서 본 그 성질이 여기서 일한다. 컴파일러는 notifierNotifier 로만 알고, 실행할 때 JVM 이 상자 안의 진짜 객체를 보고 메서드를 고른다.

notifier.send("주문 완료") 가 실행될 때
  1. 컴파일러Notifier 에 send 가 있나? 있다. 통과
  2. JVM상자 안을 본다. EmailNotifier 다
  3. EmailNotifier.send이것을 실행한다

이걸 다형성이라고 부른다. 같은 이름의 호출이 여러 모습으로 동작한다.

인터페이스는 new 할 수 없다

당연하다. 몸통이 없다.

Notifier n = new Notifier();
터미널
  • error: Notifier is abstract; cannot be instantiated

타입으로만 쓴다. 상자의 이름표로는 쓰고, 내용물로는 못 쓴다.

Notifier n = new EmailNotifier();   // 이름표는 Notifier, 내용물은 EmailNotifier

16강에서 List<String> list = new ArrayList<>(); 라는 이상한 코드를 보게 된다. 왼쪽과 오른쪽이 다르다. 이제 그게 왜 그런지 안다. List 는 인터페이스다.

default 메서드 — 몸통이 있는 약속

인터페이스에 몸통을 넣을 수도 있다. default 를 붙인다.

interface Notifier {
    void send(String msg);

    default void sendTwice(String msg) {
        send(msg);
        send(msg);
    }
}
Notifier n = new EmailNotifier();
n.sendTwice("공지");
콘솔
  • [메일] 공지
  • [메일] 공지

EmailNotifiersendTwice 를 만들지 않았는데 쓸 수 있다.

default 메서드는 이미 세상에 퍼진 인터페이스에 메서드를 추가해야 할 때 쓴다. 그냥 추가하면 기존 구현 클래스가 전부 컴파일 에러가 나기 때문이다. 19강에서 자바가 이 문법으로 무엇을 했는지 보게 된다.

추상 클래스 — 반쯤 만든 클래스

인터페이스와 비슷하지만 다른 것이 하나 더 있다.

abstract class Shape {
    abstract double area();

    void print() {
        System.out.println("넓이=" + area());
    }
}

class Circle extends Shape {
    double area() { return 3.14; }
}

abstract 메서드는 몸통이 없다. 자식이 반드시 채워야 한다. 반면 print() 는 몸통이 있고, area() 를 부른다. 아직 만들어지지도 않은 메서드를 부른다.

Circle 이 채워 넣으면 그때 동작한다.

추상 클래스도 new 할 수 없다. 구멍이 뚫려 있기 때문이다.

인터페이스 vs 추상 클래스

인터페이스

implements · 약속

  • 여러 개를 동시에 구현할 수 있다
  • 필드를 가질 수 없다 (상수만)
  • 생성자가 없다
  • "이것을 할 수 있다" — Notifier, Comparable

추상 클래스

extends · 반쯤 만든 것

  • 하나만 상속할 수 있다
  • 필드를 가질 수 있다
  • 생성자가 있다 (super(...))
  • "이것은 ~의 한 종류다" — Shape

핵심 차이는 개수다. 자바는 클래스를 하나만 상속할 수 있다.

class F extends D, E { }
터미널
  • error: '{' expected

문법 자체가 없다. 두 부모에 같은 이름의 메서드가 있으면 어느 것을 물려받을지 정할 수 없기 때문이다.

인터페이스는 몸통이 없으니 그런 다툼이 없다. 그래서 여러 개를 한꺼번에 구현할 수 있다.

class C implements A, B { }     // 된다

무엇을 고를까

실무에서는 인터페이스를 압도적으로 많이 쓴다. 추상 클래스는 자식들이 공통 필드와 공통 코드를 진짜로 나눠 가질 때만 꺼낸다.

정리하면

인터페이스는 약속이고, 다형성은 그 약속 뒤의 알맹이를 바꿔 끼울 수 있게 한다.

다시 짚어보기
  1. 01

    인터페이스는 할 수 있는 일만 적는다

    몸통이 없다. implements 로 약속을 지키겠다고 선언한다.

    interface Notifier {
        void send(String msg);
    }
  2. 02

    타입은 인터페이스로, 실물은 구현체로

    컴파일러는 이름표를 보고, JVM 은 내용물을 본다.

    Notifier n = new EmailNotifier();
  3. 03

    필요한 것을 스스로 만들지 말고 받는다

    생성자로 받으면 밖에서 바꿔 끼울 수 있다. 테스트도 쉬워진다.

    OrderService(Notifier notifier) {
        this.notifier = notifier;
    }
  4. 04

    인터페이스는 여럿, 클래스 상속은 하나

    자바에 다중 상속은 없다. 인터페이스는 몸통이 없어서 여럿이 가능하다.

  5. 05

    의심스러우면 인터페이스

    상속은 결합이 강하다. 재사용이 목적이라면 물려받지 말고 들고 있는다.

이제 값을 담을 그릇 이야기로 돌아간다. 9강의 배열은 길이가 고정이라 불편했다.

다음 강의에서 늘어나는 그릇을 만난다. 그 그릇들의 이름 앞에는 전부 인터페이스가 붙어 있고, List<String> 처럼 꺾쇠 괄호가 달려 있다. 괄호 안의 정체는 17강에서 밝힌다.

그리고 14강에서 미뤄둔 hashCode 이야기가 16강에서 완성된다.

인터페이스와 다형성 — 구현을 바꿔 끼운다 — 디코드랩(DCODELAB)