인터페이스와 다형성 — 구현을 바꿔 끼운다
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 하는 부분도. 나중에 카카오톡이 추가되면 또 고친다.
메일을 보내는 일과 주문을 처리하는 일은 다른 일이다. 그런데 OrderService 가 EmailNotifier 라는 이름을 알고 있는 한, 둘은 붙어 있다.
약속만 정한다
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강에서 본 그 성질이 여기서 일한다. 컴파일러는 notifier 를 Notifier 로만 알고, 실행할 때 JVM 이 상자 안의 진짜 객체를 보고 메서드를 고른다.
- 컴파일러Notifier 에 send 가 있나? 있다. 통과
- JVM상자 안을 본다. EmailNotifier 다
- 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("공지");
- [메일] 공지
- [메일] 공지
EmailNotifier 는 sendTwice 를 만들지 않았는데 쓸 수 있다.
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 할 수 없다. 구멍이 뚫려 있기 때문이다.
인터페이스
implements · 약속
- 여러 개를 동시에 구현할 수 있다
- 필드를 가질 수 없다 (상수만)
- 생성자가 없다
- "이것을 할 수 있다" —
Notifier,Comparable
추상 클래스
extends · 반쯤 만든 것
- 하나만 상속할 수 있다
- 필드를 가질 수 있다
- 생성자가 있다 (
super(...)) - "이것은 ~의 한 종류다" —
Shape
핵심 차이는 개수다. 자바는 클래스를 하나만 상속할 수 있다.
class F extends D, E { }
- error: '{' expected
문법 자체가 없다. 두 부모에 같은 이름의 메서드가 있으면 어느 것을 물려받을지 정할 수 없기 때문이다.
인터페이스는 몸통이 없으니 그런 다툼이 없다. 그래서 여러 개를 한꺼번에 구현할 수 있다.
class C implements A, B { } // 된다
무엇을 고를까
실무에서는 인터페이스를 압도적으로 많이 쓴다. 추상 클래스는 자식들이 공통 필드와 공통 코드를 진짜로 나눠 가질 때만 꺼낸다.
정리하면
인터페이스는 약속이고, 다형성은 그 약속 뒤의 알맹이를 바꿔 끼울 수 있게 한다.
- 01
인터페이스는 할 수 있는 일만 적는다
몸통이 없다. implements 로 약속을 지키겠다고 선언한다.
interface Notifier { void send(String msg); } - 02
타입은 인터페이스로, 실물은 구현체로
컴파일러는 이름표를 보고, JVM 은 내용물을 본다.
Notifier n = new EmailNotifier(); - 03
필요한 것을 스스로 만들지 말고 받는다
생성자로 받으면 밖에서 바꿔 끼울 수 있다. 테스트도 쉬워진다.
OrderService(Notifier notifier) { this.notifier = notifier; } - 04
인터페이스는 여럿, 클래스 상속은 하나
자바에 다중 상속은 없다. 인터페이스는 몸통이 없어서 여럿이 가능하다.
- 05
의심스러우면 인터페이스
상속은 결합이 강하다. 재사용이 목적이라면 물려받지 말고 들고 있는다.
이제 값을 담을 그릇 이야기로 돌아간다. 9강의 배열은 길이가 고정이라 불편했다.
다음 강의에서 늘어나는 그릇을 만난다. 그 그릇들의 이름 앞에는 전부 인터페이스가 붙어 있고, List<String> 처럼 꺾쇠 괄호가 달려 있다. 괄호 안의 정체는 17강에서 밝힌다.
그리고 14강에서 미뤄둔 hashCode 이야기가 16강에서 완성된다.