리모컨에 달린 버튼 목록 같은 것이다. 쓰는 사람은 버튼 이름만 알면 되고, 버튼을 눌렀을 때 실제로 무슨 일이 일어나는지는 그 리모컨에 연결된 기기가 정한다. 그래서 기기를 TV에서 조명으로 바꿔도 쓰는 쪽 코드는 한 글자도 고치지 않는다. 대신 기기는 목록에 적힌 버튼을 하나도 빠짐없이 만들어 두어야 하고, 목록에 없는 기능은 이 리모컨으로 부를 수 없다.
예시로 보기
main이 Television 클래스를 직접 쓰면, 켜는 대상을 조명으로 바꿀 때 main 안의 타입 이름을 전부 찾아 고쳐야 한다. 쓰는 쪽이 “켠다, 끈다”라는 버튼 이름만 알고 있으면 이 수고가 사라진다.
버튼 목록을 먼저 적는다. 몸통 { } 없이 이름만 있다.
interface RemoteControl {
void turnOn();
void turnOff();
}
기기 둘이 이 목록의 버튼을 각자 채운다.
class Television implements RemoteControl {
@Override
public void turnOn() { System.out.println("Turn on TV"); }
@Override
public void turnOff() { System.out.println("Turn off TV"); }
}
class Lights implements RemoteControl {
@Override
public void turnOn() { System.out.println("Turn on lights"); }
@Override
public void turnOff() { System.out.println("Turn off lights"); }
}
쓰는 쪽은 RemoteControl 타입 변수 하나만 둔다.
RemoteControl rc;
rc = new Television();
rc.turnOn();
rc = new Lights();
rc.turnOn();
Turn on TV
Turn on lights
| 줄 | rc에 들어 있는 객체 |
실제로 실행되는 메서드 | 출력 |
|---|---|---|---|
rc = new Television(); |
Television 객체 | — | — |
rc.turnOn(); |
Television 객체 | Television의 turnOn |
Turn on TV |
rc = new Lights(); |
Lights 객체 | — | — |
rc.turnOn(); |
Lights 객체 | Lights의 turnOn |
Turn on lights |
rc.turnOn();은 두 번 다 똑같이 썼는데 출력이 다르다. 변수에 무엇이 들어 있는지가 실행될 메서드를 정한다.
여기서 버튼 목록 RemoteControl을 인터페이스라고 부른다. 목록의 버튼을 채운 Television, Lights는 구현 클래스이고, 그 클래스로 만든 객체는 구현 객체다. 몸통 없이 이름만 적은 turnOn()은 추상 메서드다.
classDiagram
class RemoteControl {
<<interface>>
+int MAX_VOLUME
+int MIN_VOLUME
+turnOn()
+turnOff()
}
class Television {
+turnOn()
+turnOff()
}
class Lights {
+turnOn()
+turnOff()
}
class Main
Main ..> RemoteControl : 버튼 이름만 안다
RemoteControl <|.. Television : implements
RemoteControl <|.. Lights : implements
Main에서 나가는 화살표는 RemoteControl에서 멈춘다. Main은 Television도 Lights도 모른다.
비유가 틀어지는 곳이 하나 있다. 진짜 리모컨은 손에 잡히는 물건이지만 인터페이스는 객체로 만들 수 없다. 인터페이스는 버튼 목록이 적힌 종이에 가깝다.
정의
인터페이스는 두 객체를 이어 주는 연결 고리다. 객체 A는 인터페이스에 적힌 메서드만 부르고, 그 호출을 실제로 처리하는 일은 인터페이스 뒤에 있는 객체 B가 한다. B를 C로 바꿔도 A는 신경 쓸 것이 없다.
선언
class 자리에 interface를 쓴다. 접근 제한자는 클래스와 같다. public을 붙이면 어느 패키지에서나 쓰고, 아무것도 안 붙이면 같은 패키지 안에서만 쓴다.
interface RemoteControl { ... } // 같은 패키지 안에서만
public interface RemoteControl { ... } // 어디서나
중괄호 안에 둘 수 있는 것은 여섯 가지다.
| 넣을 수 있는 것 | 다루는 문서 |
|---|---|
| public 상수 필드 | 이 문서 |
| public 추상 메서드 | 이 문서 |
| public 디폴트 메서드, public 정적 메서드, private 메서드, private 정적 메서드 | 디폴트·정적·private 메서드 |
객체마다 값이 다른 보통 필드와 생성자는 목록에 없다. 인터페이스는 객체로 만들지 않으므로 객체마다 다른 값을 담을 곳도, 객체를 처음 채울 생성자도 필요 없다1.
상수 필드
인터페이스의 필드는 모두 바꿀 수 없는 값(상수)이다. public, static, final 세 키워드를 쓰지 않아도 컴파일할 때 자동으로 붙는다.
interface RemoteControl {
int MAX_VOLUME = 10; // public static final int MAX_VOLUME = 10; 과 같다
int MIN_VOLUME = 0;
}
System.out.println("Max:" + RemoteControl.MAX_VOLUME); // Max:10
static이 붙었으니 객체 없이인터페이스이름.상수이름으로 읽는다.final이 붙었으니 값을 바꾸는 줄은 컴파일 오류다.RemoteControl.MAX_VOLUME = 20;은cannot assign a value to static final variable MAX_VOLUME으로 막힌다2.- 선언하면서 바로 값을 넣어야 한다.
int MAX_VOLUME;만 쓰면= expected오류가 난다2. - 이름은 대문자로 쓰고, 단어가 여럿이면 밑줄로 잇는 것이 관례다.
추상 메서드
몸통 없이 선언만 있는 메서드다. public abstract를 쓰지 않아도 컴파일할 때 자동으로 붙는다.
interface RemoteControl {
void turnOn(); // public abstract void turnOn(); 과 같다
}
구현 클래스
클래스 이름 뒤에 implements 인터페이스이름을 붙인다. 이 클래스가 인터페이스의 추상 메서드를 모두 다시 정의(오버라이딩)했다는 뜻이다.
public class Television implements RemoteControl {
@Override
public void turnOn() {
System.out.println("Turn on TV");
}
}
지켜야 할 것은 두 가지다.
- 추상 메서드를 하나도 빠뜨리지 않는다. 선언(이름, 매개변수, 리턴 타입)도 똑같아야 한다.
turnOff()를 빠뜨리면Television is not abstract and does not override abstract method turnOff() in RemoteControl오류가 난다2. - 다시 정의할 때
public을 붙인다. 인터페이스 쪽이public이기 때문이다. 메서드 오버라이딩에서 접근 범위는 넓힐 수만 있고 좁힐 수 없다.public을 빼면attempting to assign weaker access privileges; was public오류가 난다2.
변수 선언과 객체 대입
인터페이스도 타입이다. 변수의 타입으로 쓸 수 있고, 참조 타입이라 null도 넣을 수 있다. 인터페이스로 객체를 쓰려면 구현 객체를 인터페이스 변수에 넣는다.
RemoteControl rc; // 선언만
RemoteControl rc = null; // 아직 가리키는 객체가 없다
RemoteControl rc = new Television(); // 구현 객체를 넣는다
rc.turnOn(); // 들어 있는 객체의 turnOn이 실행된다
null이 든 변수로 rc.turnOn()을 부르면 실행 중에 NullPointerException이 난다(참조 비교와 null)2.
인터페이스인 것과 아닌 것
자바 표준 라이브러리에서 고른 예다3.
| 이름 | 이유 | |
|---|---|---|
| 인터페이스다 | java.util.List |
“순서대로 담는다”는 메서드 목록만 정한다. 실제로 담는 일은 ArrayList, LinkedList가 한다 |
| 인터페이스다 | java.lang.Comparable |
“크기를 비교할 수 있다”는 메서드 compareTo 하나를 정한다 |
| 인터페이스다 | java.lang.Runnable |
“실행할 수 있다”는 메서드 run 하나를 정한다 |
| 아니다 | java.util.ArrayList |
클래스다. new ArrayList<>()로 객체를 만든다 |
| 아니다 | java.lang.String |
클래스다. 글자들을 담는 필드와 생성자가 있다 |
설계 이유
- 왜 전부 public인가. 인터페이스는 바깥에서 부르라고 내놓는 목록이다. 밖에서 못 부르는 버튼을 목록에 적을 이유가 없다. 안에서만 쓰는 도우미가 필요해지자 나중에 private 메서드가 따로 추가되었다1.
- 왜 필드는 상수뿐인가. 인터페이스는 객체로 만들지 않는다. 객체가 없으니 객체마다 다른 값을 담을 자리가 없다. 남는 것은 모두가 함께 읽는 고정된 값뿐이다.
- 왜 몸통을 비워 두는가. 몸통은 기기마다 다르다. 목록이 정하는 것은 “무엇을 부를 수 있는가”이고, “어떻게 하는가”는 구현 클래스의 몫이다.
스스로 설명해 보기
예시로 보기의 코드를 한 줄씩 본다.
1. RemoteControl rc = new Television();이 컴파일되는 이유
Television이 implements RemoteControl로 선언되어 있어서다. 컴파일러는 이 선언을 보고 Television 객체가 RemoteControl의 메서드를 모두 가졌다고 믿는다. implements가 없는 클래스의 객체를 넣으면 incompatible types 오류가 난다.
2. rc.turnOn();에서 컴파일러가 확인하는 것과 실행 때 일어나는 일
컴파일러는 변수의 타입인 RemoteControl에 turnOn() 선언이 있는지만 본다. 실행할 때는 rc에 들어 있는 객체의 클래스(Television)에서 turnOn을 찾아 실행한다.
3. Television의 turnOn 앞에 public이 꼭 있어야 하는 이유
인터페이스의 추상 메서드에는 public이 자동으로 붙는다. 다시 정의하는 메서드는 접근 범위를 좁힐 수 없다. public을 빼면 같은 패키지에서만 부를 수 있는 메서드가 되어 범위가 좁아진다.
이 구조의 핵심 아이디어는?
쓰는 쪽과 만드는 쪽 사이에 메서드 목록만 둔다. 쓰는 쪽은 목록만 보고 코드를 쓰고, 만드는 쪽은 목록만 채우면 된다. 두 쪽이 서로의 클래스 이름을 몰라도 된다.
이 방법을 쓸 수 있는 다른 상황은?
변수뿐 아니라 필드, 메서드의 매개변수, 배열의 원소 타입을 인터페이스로 두면 같은 효과가 난다. 타이어를 갈아 끼우는 자동차, 아무 탈것이나 모는 운전자가 그 예다(다형성).
활용
- 구현: 23_java-interface_impl.java
- 변수 타입을
List로 두고new ArrayList<>()를 넣는 코드가 이 구조다. 나중에LinkedList로 바꿔도 나머지 코드는 그대로다3. - 흔한 실수와 컴파일러가 하는 말2
| 실수 | 오류 메시지의 핵심 |
|---|---|
implements를 implement로 씀 |
'{' expected |
| 추상 메서드 하나를 빠뜨림 | is not abstract and does not override abstract method |
다시 정의하면서 public을 뺌 |
attempting to assign weaker access privileges; was public |
| 상수에 값을 다시 넣음 | cannot assign a value to static final variable |
new RemoteControl() |
RemoteControl is abstract; cannot be instantiated |
연결
- 선수: 메서드 오버라이딩(추상 메서드를 다시 정의한다), final과 상수, 접근 제한자
- 몸통이 있는 메서드를 인터페이스에 넣는 법: 디폴트·정적·private 메서드
- 인터페이스 여러 개를 한 클래스가 맡기: 다중 구현과 인터페이스 상속
- 비슷해 보이는 것: 추상 클래스, 가르는 법은 추상 클래스와 인터페이스 비교
- 같은 호출이 객체에 따라 다른 일을 하는 원리: 다형성. 원칙 수준의 소개는 객체 지향의 세 원칙
자주 하는 오해
RemoteControl rc라고 썼으니 RemoteControl 객체가 하나 생겼다”틀렸다. 인터페이스로는 객체를 만들 수 없다. new RemoteControl()은 RemoteControl is abstract; cannot be instantiated라는 컴파일 오류다. 그럴듯해 보이는 이유는 Television tv = new Television();처럼 변수 타입과 객체의 클래스가 늘 같았던 코드에 익숙해서다. 실제로 RemoteControl rc = new Television();에서 만들어진 것은 Television 객체 하나뿐이고, rc는 그 객체를 가리키는 변수다. 변수의 타입은 “이 변수로 무엇을 부를 수 있는가”만 정한다. 확인하는 법: rc.getClass().getName()을 찍으면 Television이 나온다.
확인 문제
C1 인터페이스의 필드와 추상 메서드에 컴파일러가 자동으로 붙이는 키워드를 각각 쓰라.
답: 필드에는 public static final, 추상 메서드에는 public abstract.
이유: 그래서 필드는 객체 없이 인터페이스이름.상수이름으로 읽고 값을 바꿀 수 없다. 추상 메서드는 몸통이 없고 어디서나 부를 수 있다. 흔한 오답은 필드에서 static을 빠뜨리는 것이다.
C2 구현 클래스에서 추상 메서드를 다시 정의할 때 public을 빼면 왜 컴파일 오류인가?
답: 인터페이스의 추상 메서드는 public이다. 다시 정의하는 메서드는 접근 범위를 원래보다 좁힐 수 없다. public을 빼면 같은 패키지 안에서만 부를 수 있게 되어 범위가 좁아진다.
이유: 좁혀도 된다면, 인터페이스 변수로는 어디서나 부를 수 있다고 약속한 메서드가 어떤 객체에서는 못 부르는 메서드가 된다. 약속이 깨진다.
C3 (가)~(마) 가운데 컴파일 오류가 나는 줄을 모두 고르고 이유를 쓰라.
interface RemoteControl {
int MAX_VOLUME = 10;
void turnOn();
}
class Television implements RemoteControl {
void turnOn() { } // (가)
}
class Main {
public static void main(String[] args) {
RemoteControl rc = new Television(); // (나)
RemoteControl.MAX_VOLUME = 20; // (다)
RemoteControl r2 = null; // (라)
RemoteControl r3 = new RemoteControl(); // (마)
}
}
답: (가), (다), (마).
이유: (가) public이 없어 접근 범위를 좁혔다. (다) 인터페이스의 필드는 final이라 값을 바꿀 수 없다. (마) 인터페이스는 new로 객체를 만들 수 없다. (나)는 구현 객체를 넣는 보통 쓰임이고, (라)는 참조 타입이라 null을 넣을 수 있다. 흔한 오답은 (라)를 오류로 고르는 것이다2.
C4 예시로 보기의 RemoteControl, Television, Lights와 상수 MAX_VOLUME = 10, MIN_VOLUME = 0이 있을 때 이 코드의 출력은?
RemoteControl[] rcs = { new Lights(), new Television(), new Lights() };
for (RemoteControl rc : rcs) rc.turnOn();
System.out.println(RemoteControl.MAX_VOLUME - RemoteControl.MIN_VOLUME);
답:
Turn on lights
Turn on TV
Turn on lights
10
이유: 배열의 원소 타입은 RemoteControl이지만 칸마다 든 객체가 다르다. rc.turnOn()은 그때그때 들어 있는 객체의 메서드를 실행한다. 상수는 객체 없이 인터페이스 이름으로 읽는다2.