
참고
자바의 정석
인터페이스도 일종의 추상클래스이다. 추상클래스처럼 추상메서드를 갖지만, 다른 점은 일반메서드 또는 멤버변수를 가질 수 없다. 추상화정도가 추상클래스보다 높아 추상메서드와 상수만을 포함하고 있어야 한다. 일종의 기본 설계도라고 부른다.
그러면 인터페이스는 불완전한 것인데 어떻게 쓰일까? 그 자체로 바로 사용한다기보단, 클래스의 도움을 주는 조력자의 역할을 한다고 생각하면 좋을 것 같다.
인터페이스 작성 형식은 클래스와 동일하다. 다만 다른 점은 class라는 키워드 대신에 interface라는 키워드를 사용한다는 점이다.
interface 인터페이스이름 {
public static final 타입 상수 = 값;
public abstract 메서드이름(매개변수);
}
다만, 아래와 같은 제한 사항이 있기 때문에 이 제한사항을 잘 기억하고 사용하면 좋을 것이다.
- 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다.
단, static메서드와 디펄트 메서드는 예외 (JDK1.8부터)
일반적으로 제어자를 생략하는 경우가 많으며 생략된 제어자는 컴파일 시점에 컴파일러가 추가해준다.
원래는 인터페이스의 모든 메서드는 추상메서드여야 하지만, JDK1.8부터 인터페이스에 static 메서드와 디폴트 메서드를 추가를 허용하는 방향으로 변경되었다.
인터페이스는 인터페이스만을 상속 받을 수 있으며, 클래스와 달리 다중상속이 가능하다.
interface Movable {
void move(int x, int y);
}
interface Attackable {
void attack(Unit u);
}
interface Fightable extends Movable, Attackable {}
인터페이스는 클래스와 달리 Object 클래스와 같은 최상위 클래스가 없다.
인터페이스도 추상클래스처럼 그 자체로 인스턴스를 만들 수 없으며, 자신에 정의된 추상메서드를 재정의하는 클래스를 작성하고 인터페이스를 구현해줘야 한다. 즉, 추상 메서드를 재정의 할 클래스를 작성하고 클래스 이름 옆에 상속 extends 키워드 대신에 implements라는 키워드를 붙이고 인터페이스 이름을 작성해주면 된다.
class 클래스이름 implements 인터페이스이름 {
// 인터페이스에 정의된 추상메서드를 구현해야 한다.
}
class Fighter implements Fightable {
public void move(int x, int y) {}
public void attack(Unit u) {}
}
만약에 인터페이스의 추상메서드중 특정 메서드만 재정의를 하고 싶으면 추상 클래스처럼 abstract를 붙여서 재정의한 클래스에 표현해야한다.
또한 상속과 구현을 동시에 진행 할 수 있다. 이렇게 클래스를 점점 확장할 수 있다.
두개의 상위클래스를 다중상속 받는 클래스가 있다고 가정하자. 이 때 두 상위 클래스에 변수의 이름이 같거나 메서드의 선언부가 일치한 것들이 있고 구현내용이 서로 다르면 다중상속 입장에서 어느 것을 선택할지 애매모호하다.
그래서 다중상속은 여러 기능을 확장할 수 있다는 장점도 있지만 단점이 더 커서 자바에서는 다중상속을 허용하지 않는다. 하지만, C++에서는 허용이 됨으로 둘 다 공부하신 분들은 헷갈리 수 있을 것이다. 그리고 몇몇분들은 자바에서 다중상속은 인터페이스를 통해 다중상속을 하면 된다고 하시는 분들도 계신다. 물론 이론적으로는 가능하지만, 실제로 인터페이스로 다중상속을 구현하는 일은 거의 없다.
인터페이스의 다중상속이 가능했던 이유는 변수는 상수만 가능함으로 다른 멤버변수와 충돌할 일이 없고 충돌한다하더라도 인터페이스 이름으로 구분이 가능하다. 또한 추상메서드 선언부가 같다고 하더라도 구현내용이 없으므로 당연히 상위 인터페이스에 추상메서드를 그냥 사용하면 된다.
만일 2개의 클래스를 상속받아야 하는 상황이 생기면 비중 높은 클래스를 상속받고 부가적인 상위클래스를 HAS-A관계로 맺어서 사용하거나 인터페이스로 뽑아서 구현시키도록 하면 된다.
위와 같은 상황은 우리의 입장에서는 굳이 인터페이스를 하나 더 만들어서 불편하다는 생각이 있을 것이다. 하지만 인터페이스를 이용하면 다형성을 이용할 수 있다는 장점이 더 커서 이렇게 이용한다.
public class Tv {
protected boolean power;
protected int channel;
protected int volume;
public void power() {
power = !power;
}
public void channelUp() {
channel++;
}
public void channelDown() {
channel--;
}
public void volumeUp() {
volume++;
}
public void volumeDown() {
volume--;
}
}
public class VCR {
protected int counter;
public void play() {
// 재생로직
}
public void stop() {
// 정지 로직
}
public void reset() {
counter = 0;
}
public int getCounter() {
return counter;
}
public void setCounter(int c) {
counter = c;
}
}
public interface IVCR {
public void play();
public void stop();
public void reset();
public int getCounter();
public void setCounter(int c);
}
public class TVCR extends Tv implements IVCR {
VCR vcr = new VCR();
public void play() {
vcr.play();
}
public void stop() {
vcr.stop();
}
public void reset() {
vcr.reset();
}
public int getCounter() {
return vcr.getCounter();
}
public void setCounter(int c) {
vcr.setCounter(c);
}
}
다형성에 대해 배울때 하위 클래스의 인스턴스를 상위 클래스 타입으로 참조가 가능하다고 배웠다. 이처럼 인터페이스도 상위 클래스 개념에 속함으로 인터페이스 타입으로도 참조가 가능하며 형변환도 가능하다.
Fightable f = (Fightable) new Fighter();
Fightable f = new Fighter();
또한 인터페이스는 메서드의 매개변수의 타입으로 사용이 가능하다. 인터페이스 타입의 매개변수를 갖는다는 의미는 메서드 호출 시, 해장 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해줘야 한다.
class Fighter extends Unit implements Fightable {
public void move(int x, int y) {}
public void attack(Fightable f) {}
}
또한 메서드의 리턴타입으로 인터페이스의 타입을 지정할 수 있다. 리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
Fightable method() {
Fighter f = new Fighter();
return f;
}
이러한 특징은 분산환경 프로그래밍에서 큰 장점을 보게 된다. 클라이언트측 코드를 수정하지 않아도 서버측 코드를 수정함으로 새로운 프로그램을 사용이 가능하게 한다.
- 클래스를 사용하는 쪽 (User)과 클래스를 제공하는 쪽 (Provider)쪽이 있다.
- 메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.
(내용은 몰라도 된다.)
인터페이스를 매개체로 해서 사용하는 클래스쪽이 인터페이스를 통해 제공하는 클래스쪽을 접근하도록 하면, 제공하는 클래스쪽의 메서드의 선언부나 구현부가 변경이 되거나 클래스가 대체되어도 사용하는 클래스는 전혀 영향을 받지 않는다.
이렇게 클래스간의 직접적인 관계를 인터페이스를 통해 간접적인 관계로 바꿀 수 있다.
하지만 이런 관계는 항상 인터페이스를 구현한 클래스를 동적으로 제공해줘야 한다는 것이다. 실제로 이런 방법으로 구현된 클래스가 Thread 클래스의 생성자중 Runnable을 매개변수로 받는 생성자가 이런 방식으로 구현되어 있다.
하지만 이런 방법말고 제3의 클래스를 통해 제공받는게 더 효율적으로 느껴진다.
이 방법을 실제 사용하는 클래스가 JDBC의 DriverManager이다.
그리고 인터페이스 타입의 참조변수로도 Object 클래스에서 제공하는 메서드를 호출할 수 있다. 이 점을 이용하여 많은 연습을 해보자.
원래 인터페이스에는 상수와 추상 메서드만 선언이 가능했지만 JDK1.8부터 디폴트 메서드와 static 메서드가 추가가 되었다. 그리고 인터페이스의 static 메서드 역시 접근 제어자가 항상 public이며, 생략이 가능하다.
보통 인터페이스에 추상 메서드를 추가한다는 사실은 어마 무시한 사실이다. 이 인터페이스를 구현한 모든 클래스에 추가한 추상메서드를 추가해줘야 하며 각각 이 메서드를 단위 테스트도 돌려야 할 것이다. 이런 경우가 빈번하여 JDK1.8부터 인터페이스에 디폴트 메서드를 제공해주었다.
디폴트 메서드는 추상메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 다른 클래스를 변경하지 않아도 된다.
디폴트 메서드는 앞에 키워드로 default를 붙이며 추상메서드와 달리 일반 메서드 처럼 구현부가 있어야 한다. 또한 접근제어자는 public이며 생략이 가능하다.
디폴트 메서드 사용할 시, 주의해야할 점이 있다. 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다. 충돌할 경우 아래의 규칙을 생각하자.
- 여러 인터페이스의 디폴트 메서드 이름 충돌할 경우
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 한다.
- 디폴트 메서드와 상위 클래스의 메서드간 충돌
- 상위 클래스의 메서드가 상속되고 디폴트 메서드는 무시된다.