thenullpage.com

알림을 보내는 기능을 만든다고 하자. 메일로도 보내고 문자로도 보내고 앱 푸시로도 보낸다. 셋 다 보낸다는 동작 하나로 묶이는데 안에서 하는 일은 완전히 다르고 물려줄 공통 코드도 없다. 억지로 부모 클래스를 만들면 알맹이 없는 클래스만 늘어난다. 자바는 구현은 빼고 할 수 있는 일의 목록만 정해 두는 수단으로 인터페이스를 준비해 뒀다.


인터페이스는 지켜야 할 약속이다. class 대신 interface로 선언하고, 메서드는 이름과 인자와 반환 타입만 적은 뒤 세미콜론으로 끊는다. 몸통이 없어 그 자체로는 아무 일도 못 한다.


public interface Notifier {
void send(String message);
}


implements는 그 약속을 지키겠다는 선언이다. 인터페이스를 구현한 클래스는 비어 있는 메서드를 전부 채워야 한다.


public class MailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[메일] " + message);
}
}

// SmsNotifier도 같은 모양으로 만들어 [문자]를 찍는다고 하자


하나라도 빠뜨리면 does not override abstract method send라는 에러가 난다. 인터페이스 메서드는 적지 않아도 public이라 구현하는 쪽에서 public을 빼도 막힌다.


인터페이스 타입 하나로 여러 구현을 다룬다. 변수 타입을 인터페이스로 두면 안에 무엇이 들었든 send를 부를 수 있다.


List<Notifier> list = new ArrayList<>();
list.add(new MailNotifier());
list.add(new SmsNotifier());

for (Notifier n : list) {
n.send("서버 점검 안내");
}
// 출력:
// [메일] 서버 점검 안내
// [문자] 서버 점검 안내


알림 수단이 다섯 개 더 늘어도 이 반복문은 손댈 데가 없다. 인터페이스를 쓰는 실질적인 이유가 여기다.


클래스 상속은 하나뿐이지만 인터페이스는 여러 개 붙는다. 자바는 extends를 두 개 적을 수 없고, implements 뒤에는 쉼표로 얼마든지 나열할 수 있다. 둘을 같이 쓰면 extends가 먼저다.


public interface Loggable {
void log();
}

public class SlackNotifier implements Notifier, Loggable {
@Override
public void send(String message) {
System.out.println("[슬랙] " + message);
}

@Override
public void log() {
System.out.println("기록 남김");
}
}


default는 몸통이 있는 인터페이스 메서드다. 인터페이스에 메서드를 하나 추가하면 기존 구현 클래스가 전부 컴파일 에러가 나는데, default를 붙이면 기본 동작을 같이 줘서 안 깨뜨린다.


// Notifier 안에 추가
default void sendAll(List<String> messages) {
for (String m : messages) {
send(m);
}
}

new MailNotifier().sendAll(List.of("점검 시작", "점검 완료"));
// 출력:
// [메일] 점검 시작
// [메일] 점검 완료


MailNotifier에는 sendAll이 없는데 부르니까 돌아간다. 마음에 안 들면 구현 클래스에서 오버라이딩하면 된다.


static 메서드는 인터페이스 이름으로만 부른다. 객체를 만들어 주는 코드를 인터페이스 옆에 붙여 둘 때 쓴다.


// Notifier 안에 추가
static Notifier of(String type) {
return type.equals("mail") ? new MailNotifier() : new SmsNotifier();
}

Notifier n = Notifier.of("mail");
n.send("가입 환영"); // [메일] 가입 환영


default와 달리 구현 클래스는 이걸 물려받지 않아서 MailNotifier.of는 컴파일 에러다. 인터페이스에 int MAX = 200;처럼 값을 적으면 public static final이 자동으로 붙어 상수가 된다.


추상 클래스는 절반만 완성된 클래스다. class 앞에 abstract를 붙이면 new로 만들 수 없고, 몸통 없는 메서드에 abstract를 붙여 자식에게 숙제로 넘긴다. 인터페이스와 달리 필드와 생성자를 가질 수 있다.


public abstract class BaseNotifier {
protected final String sender;

protected BaseNotifier(String sender) {
this.sender = sender;
}

public abstract void send(String message);

public void report() {
System.out.println("발신자는 " + sender);
}
}

public class DeskNotifier extends BaseNotifier {
public DeskNotifier(String sender) {
super(sender);
}

@Override
public void send(String message) {
System.out.println(sender + " 보냄: " + message);
}
}

BaseNotifier b = new DeskNotifier("system");
b.send("점검 안내"); // system 보냄: 점검 안내
b.report(); // 발신자는 system

new BaseNotifier("system");
// 컴파일 에러: BaseNotifier is abstract; cannot be instantiated


sender 값과 report라는 완성된 동작은 부모가 들고 있고 매번 달라지는 send만 자식이 채웠다. 부모는 미완성이라 직접 만들 수 없다.


갈림길은 상태와 개수다. 공통으로 들고 다닐 필드와 생성자가 있고 코드도 겹치면 추상 클래스다. 공유할 몸통은 없고 이런 일을 할 수 있다는 자격만 정하는 거라면 인터페이스다. 자격을 여러 개 붙여야 한다면 클래스는 하나만 상속되므로 인터페이스뿐이다. 자바 표준 라이브러리도 그렇다. List는 인터페이스고 ArrayList와 LinkedList가 그 구현이다.


List<String> names = new ArrayList<>();
// 오른쪽만 new LinkedList<>()로 바꿔도 아래 코드는 그대로


이번 편에서 손에 쥐어야 할 감각은 세 가지다. 인터페이스는 몸통 없는 약속이고 구현 클래스가 그 자리를 전부 채워야 한다는 것, default와 static 중 구현 클래스가 물려받는 쪽은 default뿐이라는 것, 공유할 필드와 코드가 있으면 추상 클래스라는 것이다. Notifier를 직접 쳐 놓고 구현 클래스를 하나씩 늘려 보면 반복문이 왜 그대로인지 보인다.