인터페이스는 일종의 추상 클래스다.
인터페이스는 추상 클래스처럼 추상 메서드를 갖지만 추상 클래스보다
추상화 정도가 높아서 추상 클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없다.
오직 추상 메서드와 상수만을 멤버로 가질 수 있으며,
그 외의 다른 어떠한 요소도 허용하지 않는다.
추상 클래스를 부분적으로만 완성된 '미완성 설계도'라고 한다면,
인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 '기본 설계도'라고 할 수 있다.
인터페이스도 추상 클래스처럼 완성되지 않은 불완전한 것이기 때문에
그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
인터페이스를 작성하는 것은 클래스 작성하는 것과 같다.
다만 키워드로 class 대신 interface를 한다.
그리고 interface에도 클래스와 같이 접근 제어자로
public 또는 default를 사용할 수 있다.
interface 인터페이스 이름 {
public static final 타입 상수이름 = 값;
public abstract 메서드 이름 (매개변수목록);
}
일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있다.
- 모든 멤버 변수는 public static final이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
단, static메서드와 디폴트 메서드는 예외다(JDK 1.8부터)
인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에
제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다.
생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.
원래는 인터페이스의 모든 메서드는 추상 메서드여야 하는데,
JDK1.8부터 인터페이스에 static 메서드와 디폴트 메서드(default method)의 추가를 허용하는 방향으로 변경됐다.
! 실무에서는 아직 JDK 1.8을 사용하지 않는 곳이 많기 때문에
JDK 1.8 이전의 규칙과 이후의 규칙을 모두 알고 있어야 한다.
인터페이스는 인터페이스로부터만 상속받을 수 있으며,
클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.
! 인터페이스는 클래스와 달리 Object클래스와 같은 최고 조상이 없다.
클래스의 상속과 마찬가지로 자손 인터페이스(Fightable)는 조상 인터페이스(Movable, Attackable)에 정의된
멤버를 모두 상속받는다.
그래서 Fightable 자체에는 정의된 멤버가 하나도 없지만
조상 인터페이스로부터 상속받은 두 개의 추상 메서드,
move(int x, int y)와 attack(Unit u)을 멤버로 갖게 된다.
인터페이스도 추상클래스처럼 그 자체로는 인터페이스를 생성할 수 없다.
추상 클래스가 상속을 통해 추상 메서드를 완성하는 것처럼,
인터페이스도 자신에 정의된 추상 메서드의 몸통을 만들어주는 클래스를 작성해야 하는데,
그 방법은 추상 클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다.
다만, 클래스는 확장한다는 의미의 키워드로 'extends'를 사용했지만
인터페이스는 구현한다는 의미의 키워드로 'implements'를 사용한다.
class 클래스 이름 implements 인터페이스 이름 {
// 인터페이스에 정의된 추상 메서드를 구현해야 한다
}
class Fighter implements Fightable {
public void move(int x, int y) { /* 내용 생략 */}
public void attack(Unit u) { /* 내용 생략 */
}
만일 구현해야 하는 인터페이스의 메서드 중 일부만 구현한다면,
abstract를 붙여서 추상 클래스로 선언해야 한다.
abstract class Fighter implements Fightable {
public void move(int x, int y) { /* 내용 생략 */ }
}
그리고 상속과 구현을 동시에 할 수도 있다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { /* 내용 생략 */ }
public void attack(Unit u) { /* 내용 생략 */ }
}
! 인터페이스의 이름에 주로 Fightable과 같이 '~을 할 수 있는'의 의미로 'able'로 끝내는 이름이 많은데,
이유는 어떤 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서다.
또한 그 인터페이스를 구현한 클래스는 '~를 할 수 있는' 능력을 갖추었다는의미이기도 하다.
이름이 '~able'로 끝나는 것은 인터페이스라고 추측할 수 있지만,
그렇다고 해서 모든 인터페이스가 반드시 '~able'로 끝나는 것은 아니니 주의하자.
두 조상으로부터 상속받는 멤버 중 멤버 변수의 이름이 같거나 메서드의 선언부가 일치하고
구현 내용이 다르다면 이 두 조상으로부터 상속받는 자손 클래스는 어느 조상의 것을 상속받게 되는 것인지 알 수 없다.
어느 한 쪽으로부터의 상속을 포기하던지, 이름이 충돌하지 않도록 조상 클래스를 변경해야 한다.
이렇듯 다중 상속은 장점도 있지만 단점이 더 크다고 판단하여 자바에서는 다중 상속을 허용하지 않는다.
다만 자바에서 많은 경우는 아니지만 인터페이스로 다중 상속을 구현할 수 있다.
! 그렇다고 해서 인터페이스가 다중 상속을 위한 것은 아니다.
인터페이스는 static 상수만 정의할 수 있으므로 조상 클래스의 멤버 변수와 충돌하는 경우는 거의 없고
충돌하더라도 클래스 이름을 붙여서 구분할 수 있다.
그리고 추상 메서드는 구현 내용이 전혀 없으므로 조상 클래스의 메서드와 선언부가 일치하는 경우에는
당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.
그러나, 이렇게 하면 상속받는 멤버의 충돌은 피하더라도 다중 상속의 장점을 잃게 된다.
두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고
다른 한 쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나,
어느 한 쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.
예를 들어서 TV클래스와 VCR 클래스가 있을 때, TVCR 클래스를 작성하기 위해
한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내부에 포함시켜서 내부적으로 인스턴스를 생성해서 사용한다.
public class TV {
protected boolean power;
protected int channel;
protected int volume;
public void power() { power = !power; }
public void channel Up() { channel++; }
public void channelDown() { channel--; }
public void volumeUp() { volume++; }
public void volumeDown() { volume--; }
}
public class VCR {
protected int counter; // VCR의 카운터
public void play() {
// Tape을 재생한다.
}
public void stop() {
// 재생을 멈춘다.
}
public void resetn() {
counter = 0;
}
public void getCounter() {
return counter;
}
public void setCounter(int c) {
counter = c;
}
}
VCR 클래스에 정의된 메서드와 일치하는 추상 메서드를 갖는 인터페이스를 작성한다.
public interface IVCR {
public void play();
public void stop();
public void reset();
public int getCounter();
public void setCounter(int c);
}
이제 IVCR 인터페이스를 구현하고 TV 클래스로부터 상속받는 TVCR클래스를 만든다.
VCR 클래스 타입의 참조 변수를 멤버 변수로 선언해서 IVCR 인터페이스의 추상 메서드를 구현하는데 사용한다.
public class TVCR extends TV implements IVCR {
VCR vcr = new VCR();
public void play() {
vcr.play(); // 코드를 작성하는 대신 VCR 인스턴스의 메서드를 호출한다.
}
public void stop() {
vcr.stop();
}
public void reset() {
vcr.reset();
}
public int getCounter() {
return vcr.getCounter();
}
public void setCounter(in c) {
vcr.setCounter(c);
}
}
IVCR 인터클래스를 구현하기 위해서 새로 메서드를 작성해야 하지만,
이처럼 VCR 클래스의 인스턴스를 사용하면 쉽게 다중상속을 구현할 수 있다.
또한 VCR 클래스의 내용이 변경되도 변경된 내용이 TVCR클래스에도 자동적으로 반영된다.
사실 인터페이스를 작성하지 않고도 VCR 클래스를 TVCR 클래스에 포함시키는 것만으로도 충분하지만,
인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있다.
다형성에 대해서 이야기할 때 자손 클래스의 인스턴스를 조상 타입의 참조변수로 참조하는 것이 가능하다고 했었다.
인터페이스 역시 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로
이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.
인터페이스 Fightable을 클래스 Fighter가 구현했을 때,
Fighter 인스턴스를 Fightable타입의 참조변수로 참조하는 것이 가능하다.
Fighter f = (Fightable)new Fighter();
또는
Fighter f = new Fighter();
! Fighter타입의 참조변수로는 인터페이스 Fightable에 정의된 멤버들만 호출 가능하다.
따라서 인터페이스는 다음과 같이 메서드의 매개변수의 타입으로 사용될 수 있다.
void attack(fightable f) {
//...
}
인터페이스 타입의 매개변수가 갖는 의미는
메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다.
그래서 attack메서드를 호출할 때는 매개변수로
Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨줘야 한다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { /* 내용 생략 */ }
public void attack(Fightable f) { /* 내용 생략 */ }
}
이처럼 Fightable인터페이스를 구현한 Fighter 클래스가 있을 때
attack 메서드의 매개변수로 Fighter인스턴스를 넘겨 줄 수 있다.
즉 attack(new Fighter())와 같이 할 수 있다는 것이다.
그리고 아래 코드처럼 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것도 가능하다.
Fightable method() {
...
Fighter f = new Fighter();
return f;
// 이 두문장을 한 문장으로 바꾸면
// return new Fighter();
}
★ 리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
위 코드에서는 method()의 리턴타입이 Fightable 인터페이스이기 때문에
메서드의 return문에서 Fightable 인터페이스를 구현한 Fighter 클래스의 인스턴스를 반환한다.
1. 개발 시간을 단축시킬 수 있다.
일단 인터페이스가 작성되면 이를 사용해서 프로그램을 작성하는 것이 가능하다.
메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.
그리고 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하게 되면,
인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않아도 양쪽에서 동시에 개발을 진행할 수 있다.
2. 표준화가 가능하다.
프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 뒤 개발자들에게 인터페이스를 구현해서
프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.
3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
서로 상속관계에 있지도 않고, 같은 조상 클래스를 갖고 있지 않은
서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.
4. 독립적인 프로그래밍이 가능하다.
인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에
실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.
클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면
한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그램이 가능하다.
예로 한 데이터베이스 회사가 제공하는 특정 데이터베이스를 사용하는데 필요한 클래스를 사용해서 프로그램을 작성했다면
이 프로그램은 다른 종류의 데이터베이스를 사용하기 위해서는 전체 프로그램 중 데이터베이스와 관련된 부분은 모두 변경해야 할 것이다.
그러나 데이터베이스 관련 인터페이스를 정의하고 이를 이용해서 프로그램을 작성하면,
데이터베이스의 종류가 변경되더라도 프로그램을 변경하지 않도록 할 수 있다.
단, 데이터베이스 회사에서 제공하는 클래스도 인터페이스를 구현하도록 요구해야 한다.
데이터베이스를 이용한 응용 프로그램을 작성하는 쪽에서는 인터페이스를 이용해서 프로그램을 작성하고
데이터베이스 회사에서는 인터페이스를 구현한 클래스를 작성해서 제공해야 한다.
실제로 자바에서는 다수의 데이터베이스와 관련된 다수의 인터페이스를 제공하고 있으며,
프로그래머는 이 인터페이스를 이용해서 프로그래밍하면 특정 데이터베이스에 종속되지 않는 프로그램을 작성할 수 있다.
지금까지 인터페이스의 특징과 구현 방법, 장점 등 일반적인 사항에 대해서 봤지만,
'인터페이스란 도대체 무엇인가'에 대한 의문은 아직도 있다.
인터페이스에 대해서 본질적인 측면에서 살펴보자.
인터페이스를 이해하기 위해 두 가지 사항을 염두에 두어야 한다.
- 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provide)기 있다.
- 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.)
class A {
public void methodA(B b) {
b.methodB();
}
}
class B {
public void methodB() {
System.out.println("methodB()");
}
}
class InterFaceTest {
public static void main(String agrs[]) {
A a = new A();
a.methodA(new B());
}
}
위 코드처럼 클래스 A와 B가 있을 때
클래스 A(User)는 클래스 B(Provide)의 인스턴스를 생성하고 메서드를 호출한다.
이 두 클래스는 서로 직접적인 관계에 있다. 이를 간단히 'A-B'라고 표현하자.
이 경우 클래스 A를 작성하려면 클래스 B가 이미 작성되어 있어야 한다.
그리고 클래스 B의 method()의 선언부가 변경되면 이를 사용하는 클래스 A도 변경되야 한다.
이처럼 직접적인 관계의 두 클래스는 한 쪽 (Provider)이 변경되면 다른 쪽(User)도 변경되야 한다는 단점이 있다.
하지만 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서
클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면
클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로
대체되도 클래스 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.
두 클래스간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해서
클래스 B(Provider)의 선언과 구현을 분리해야 한다.
먼저 아래와 같이 클래스 B에 정의된 메서드를 추상 메서드로 정의하는 인터페이스 I를 정의한다.
interface I {
public abstract void methodB();
}
그 다음 클래스 B가 인터페이스 I를 구현하도록 한다.
class B implements I {
public B implements I {
Systme.outprintln("methodB in B class");
}
}
이제 클래스 A는 클래스 B 대신 인터페이스 I를 사용해서 작성할 수 있다.
class A {
public void methodA(B b) {
b.methodB();
}
}
=>
class A {
public void methodA(I i) {
i.methodB();
}
}
! methodA가 호출될 때 인터페이스 I를 구현한 클래스의 인스턴스(클래스 B의 인스턴스)를 제공받아야 한다.
클래스 A를 작성하는데 있어서 클래스 B가 사용되지 않았다는 점에 주목하자.
이제 클래스 A와 클래스 B는 'A-B'의 직접적인 관계에서
'A-I-B'의 간접적인 관계로 바뀌었다.
결국 클래스 A는 여전히 클래스 B의 메서드를 호출하지만
클래스 A는 인터페이스 I하고만 직접적인 관계에 있기 때문에 클래스 B의 변경에 영향을 받지 않는다.
클래스 A는 인터페이스를 통해 실제로 사용되는 클래스의 이름을 몰라도 되고
심지어 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다.
클래스 A는 오직 집적적인 관계에 있는 인터페이스 I의 영향만 받는다.
인터페이스 I는 실제구현 내용(클래스 B)을 감싸고 있는 껍데기이며,
클래스 A는 껍데기 안에 어떤 알맹이(클래스)가 들어 있는지 몰라도 상관없다.
원래 인터페이스에 추상 메서드만 선언할 수 있는데,
JDK 1.8부터 디폴트 메서드와 static 메서드도 추가할 수 있게 됐다.
static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 예전부터 인터페이스에 추가하지 못할 이유가 없었다.
그러나 자바를 보다 쉽게 배울 수 있도록 규칙을 단순히 할 필요가 있기에
인터페이스의 모든 메서드는 추상 메서드이어야 한다는 규칙에 예외를 두지 않았다.
덕분에 인터페이스와 관련된 static메서드는 별도의 클래스에 따로 두어야 했다.
가장 대표적인 것으로 java.util.Collection 인터페이스가 있는데
이 인터페이스와 관련된 static 메서드드들이 인터페이스에는
추상 메서드만 선언할 수 있다는 원칙 때문에 별도의 클래스, Collections라는 클래스에 들어가게 됐다.
만약 인터페이스에 static 메서드를 추가할 수 있었다면 Collections클래스는 존재하지 않았을 것이다.
그리고 인터페이스의 static 메서드 역시 접근 제어자가 항상 public이며, 생략할 수 있다.
디폴트 메서드
조상 클래스에 새로운 메서드를 추가하는 것은 쉽지만, 인터페이스의 경우에는 쉽지 않다.
인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고,
이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.
인터페이스가 변경되지 않는 것이 제일 좋지만, 설계를 아무리 잘해도 언젠가는 변경하게 된다.
그때이 디폴트메서드라는 것을 사용한다.
디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로,
추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
디폴트 메서드는 앞에 키워드 default만 붙이며, 추상 메서드와 다르게
일반 메서드처럼 '몸통{}'이 있어야 한다.
디폴트 메서드 역시 접근 제어자가 'public'이며, 생략 가능하다.
interface My Interface {
void method();
void newMethod(); // 추상 메서드
}
위 코드처럼 newMethod()라는 추상 메서드를 추가하는 대신에
=>
interface MyInterface {
void method();
default void newMethod(){}
}
바로 위 코드처럼 디폴트 메서드를 추가하면, 기존의 MyInterface를 구현한 클래스를 변경하지 않아도 된다.
즉, 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것이다.
대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 생긴다.
이 충돌을 해결하는 규칙은 아래와 같다.
1. 여러 인터페이스의 디폴트 메서드 간의 충돌
인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
2. 디폴트 메서드와 조상 클래스의 메서드간의 충돌
조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
=> 이 규칙이 귀찮다면 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩하면 된다.