이전 글에서는 다형성과 추상 클래스, 캡슐화를 공부했다.
추상 클래스를 이용하면 공통 기능을 부모 클래스에 모아 코드의 중복을 줄일 수 있었고, 다형성을 이용하면 부모 타입 하나로 다양한 자식 객체를 사용할 수 있었다.
하지만 객체지향 프로그래밍에서는 기능만 정의하고 구현은 각 객체에게 맡기고 싶은 경우도 자주 발생한다.
예를 들어
처럼 "무엇을 할 수 있는가" 만 정의하고 싶은 경우가 있다.
이럴 때 사용하는 것이 인터페이스(Interface) 이다.
인터페이스는 객체가 가져야 하는 기능의 규격을 정의하며, Java에서 다형성을 가장 많이 활용하는 방법이기도 하다.
이번 글에서는 인터페이스의 개념부터 구현 클래스, 다형성, default 메서드, static 메서드, private 메서드, 그리고 추상 클래스와의 차이까지 정리해보려고 한다.
인터페이스는 객체의 사용 방법을 정의한 타입이다.
객체 내부가 어떻게 구현되어 있는지는 알 필요 없이,
어떤 기능을 사용할 수 있는지만 정의한다.
예를 들어 TV와 에어컨의 리모컨을 생각해보자.
사용자는
버튼만 알고 있으면 된다.
리모컨 내부 회로가 어떻게 동작하는지는 몰라도 사용할 수 있다.
인터페이스도 이와 동일하다.
개발자는 구현 방법을 몰라도 인터페이스만 알면 객체를 사용할 수 있다.
인터페이스를 사용하면
개발 코드와 실제 객체를 분리할 수 있다.
개발 코드
↓
Interface
↓
실제 객체
개발 코드는 인터페이스만 바라본다.
따라서 구현 객체가 변경되어도 개발 코드는 수정하지 않아도 된다.
예를 들어
Payment payment = new CardPayment();
이 코드를
Payment payment = new KakaoPayment();
으로 변경해도
사용하는 코드는 그대로 유지된다.
이것이 인터페이스가 제공하는 가장 큰 장점이다.
인터페이스는 클래스와 동일하게 .java 파일로 작성한다.
public interface RemoteControl {
}
클래스와 비슷하지만 중요한 차이가 있다.
인터페이스는 객체를 생성할 수 없다.
따라서 생성자를 가질 수 없다.
인터페이스에는 대표적으로 다음 요소를 선언할 수 있다.
인터페이스에는 일반 필드를 만들 수 없다.
오직 상수만 선언할 수 있다.
public interface RemoteControl {
int MAX_VOLUME = 10;
}
위 코드는 실제로 컴파일되면 다음과 동일하다.
public interface RemoteControl {
public static final int MAX_VOLUME = 10;
}
즉,
public
static
final
은 자동으로 추가된다.
상수 이름은 관례적으로 모두 대문자를 사용하며
단어 사이는 _를 사용한다.
인터페이스는 객체의 기능을 정의하는 것이 목적이다.
따라서 대부분 메서드는 구현부 없이 선언만 존재한다.
public interface RemoteControl {
void turnOn();
void turnOff();
}
실제로는
public abstract void turnOn();
과 같다.
public abstract는 생략 가능하다.
인터페이스는 객체를 만들 수 없기 때문에
반드시 구현 클래스가 필요하다.
public interface RemoteControl {
void turnOn();
}
구현 클래스는
implements
키워드를 사용한다.
public class Television implements RemoteControl {
@Override
public void turnOn() {
System.out.println("TV 전원 ON");
}
}
인터페이스의 모든 추상 메서드를 반드시 구현해야 한다.
객체는 인터페이스 타입으로 참조한다.
RemoteControl rc = new Television();
rc.turnOn();
실제 객체는 Television이지만
개발 코드는 RemoteControl만 알고 있다.
이처럼 인터페이스를 이용하면 구현 객체를 쉽게 교체할 수 있다.
인터페이스도 부모 클래스처럼 다형성을 구현할 수 있다.
RemoteControl rc;
rc = new Television();
rc = new Audio();
같은 변수지만
저장되는 객체에 따라 실행 결과가 달라진다.
인터페이스를 구현한 객체는
자동으로 인터페이스 타입으로 변환된다.
RemoteControl rc = new Television();
이는 다음과 같은 의미이다.
Television tv = new Television();
RemoteControl rc = tv;
별도의 형변환 없이 자동으로 변환된다.
인터페이스는 매개변수에서도 매우 많이 사용된다.
public class User {
public void use(RemoteControl rc) {
rc.turnOn();
}
}
호출할 때는
user.use(new Television());
user.use(new Audio());
처럼 다양한 구현 객체를 전달할 수 있다.
실제 실행되는 메서드는
각 객체가 구현한 메서드이다.
Java는 클래스의 다중 상속은 지원하지 않는다.
하지만
인터페이스는 여러 개를 동시에 구현할 수 있다.
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public class MultiFunctionPrinter
implements Printer, Scanner {
@Override
public void print() {
System.out.println("출력");
}
@Override
public void scan() {
System.out.println("스캔");
}
}
하나의 객체가 여러 인터페이스 타입으로 사용될 수 있다.
Printer printer = new MultiFunctionPrinter();
Scanner scanner = new MultiFunctionPrinter();
Java8 이전에는 인터페이스에는
추상 메서드만 존재했다.
하지만 Java8부터 여러 기능이 추가되었다.
인터페이스에서도 구현부를 가진 메서드를 작성할 수 있다.
public interface RemoteControl {
void turnOn();
default void setMute() {
System.out.println("무음 처리");
}
}
구현 클래스는
필요하면 그대로 사용하고
원한다면 오버라이딩할 수도 있다.
이미 배포된 인터페이스에 새로운 메서드를 추가하면
모든 구현 클래스가 컴파일 오류가 발생한다.
이를 해결하기 위해
default 메서드가 만들어졌다.
기존 구현 클래스는 수정하지 않아도 된다.
인터페이스도 static 메서드를 가질 수 있다.
public interface RemoteControl {
static void changeBattery() {
System.out.println("배터리 교체");
}
}
객체를 생성하지 않고 사용할 수 있다.
RemoteControl.changeBattery();
Java9부터는
default 메서드 내부에서 사용할 private 메서드도 만들 수 있다.
public interface RemoteControl {
default void turnOn() {
checkBattery();
}
private void checkBattery() {
System.out.println("배터리 확인");
}
}
외부에서는 호출할 수 없다.
인터페이스 내부의 코드 중복을 줄이기 위해 사용된다.
추상 클래스와 인터페이스는 매우 비슷해 보인다.
하지만 목적이 다르다.
추상 클래스는
공통 기능과 상태를 공유하기 위한 부모 클래스이다.
인터페이스는
객체가 반드시 가져야 하는 기능의 규격이다.
예를 들어
동물은 모두 이름과 나이가 있다.
이 경우
abstract class Animal
이 적절하다.
반면
날 수 있는 기능은
새도 가능하고
비행기도 가능하다.
공통 부모가 존재하지 않는다.
이 경우
interface Flyable
이 적절하다.
| 추상 클래스 | 인터페이스 |
|---|---|
| 상속은 extends | 구현은 implements |
| 객체 생성 불가 | 객체 생성 불가 |
| 필드 가능 | 상수만 가능 |
| 생성자 가능 | 생성자 없음 |
| 일반 메서드 가능 | default 이전에는 추상 메서드만 가능 |
| 단일 상속 | 다중 구현 가능 |
간단히 정리하면
"공통 기능은 추상 클래스"
"공통 규격은 인터페이스"
라고 이해하면 된다.
Java의 대표적인 API들도 대부분 인터페이스를 중심으로 설계되어 있다.
예를 들어
List<String> list = new ArrayList<>();
변수는 List 인터페이스 타입이다.
실제 객체는 ArrayList이다.
필요하면
list = new LinkedList<>();
로 변경해도 사용하는 코드는 거의 수정하지 않는다.
Spring Framework에서도
의존성 주입(DI)은 대부분 인터페이스를 기반으로 이루어진다.
즉,
인터페이스는 객체를 유연하게 교체할 수 있도록 만드는 핵심 기술이다.
이번 글에서는 인터페이스의 개념과 활용 방법을 정리했다.
implements를 사용한다.