인터페이스
인터페이스란?
인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.
추상클래스를 부분적으로만 완성된 미완설 설계도 라고 한다면, 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 기본 설계도 라 할 수 있다
인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
interface 인터페이스 이름 {
public static final 타입 상수이름 = 값;
public abstract 메서드이름(매개변수 목록);
}
일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있다.
- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
단, static메서드는 디폴트 메서드는 예외(JDK1.8부터)
인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 떄문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다, 생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.
public interface PlayingCard {
public static final int SPADE = 4;
final int DIAMOND =3;
static int HEART = 2;
int CLOVER = 1;
public abstract String getCardNumber();
String getCardKind();
}
원래는 인터페이스의 모든 메서드는 추상메서드이어야 하는데, JDK1.8부터 인터페이스에 static 메서드와 디폴트 메서드(default method)의 추가를 허용하는 방향으로 변경되었다.
인터페이스의 상속
인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.
public interface Movable{
void move(int x, int y);
}
public interface Attackable {
void attack(Unit u);
}
public interface Fightable extends Movable, Attackable {}
클래스의 상속과 마찬가지로 자손 인터페이스(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 implemnts Fightable{
public void move (int x, int y) {/*내용 생략*/}
}
그리고 다음과 같이 상속과 구현을 동시에 할 수 도 있다.
class Fighter extends Unit implemnts Fightable{
public void move (int x, int y) {/*내용 생략*/}
public void attack (Unit u) {/*내용 생략*/}
}
인터페이스의 이름에는 주로 Fightable 과 같이 ~을 할 수 있는의 이미인 able로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서 이다.
또한 그 인터페이스를 구현한 클래스는 ~를 할 수 있는 능력을 갖추었다는 의미이기도 하다. 이름이 able 로 끝나는 것은 인터페이스라고 추측할 수 있지만 모든 인터페이스의 이름이 able로 끝나는 것은 아니다.
class FighterTest {
public static void main(String[] args) {
Fighter f = new Fighter();
if (f instanceof Unit) {
System.out.println("f는 Unit클래스의 자손입니다.");
}
if (f instanceof Fightable) {
System.out.println("f는 Fightable인터페이스를 구현했습니다.");
}
if (f instanceof Movable) {
System.out.println("f는 Movable인터페이스를 구현했습니다.");
}
if (f instanceof Attackable) {
System.out.println("f는 Attackable인터페이스를 구현했습니다.");
}
if (f instanceof Object) {
System.out.println("f는 Object클래스의 자손입니다.");
}
}
}
class Fighter extends Unit implements Fightable {
public void move(int x, int y) { /* 내용 생략 */ }
public void attack(Unit u) { /* 내용 생략 */ }
}
class Unit {
int currentHP; // 유닛의 체력
int x; // 유닛의 위치(x좌표)
int y; // 유닛의 위치(y좌표)
}
interface Fightable extends Movable, Attackable { }
interface Movable { void move(int x, int y); }
interface Attackable { void attack(Unit u); }
f는 Unit클래스의 자손입니다.
f는 Fightable인터페이스를 구현했습니다.
f는 Movable인터페이스를 구현했습니다.
f는 Attackable인터페이스를 구현했습니다.
f는 Object클래스의 자손입니다.
실제로 Fighter 클래스는 Unit 클래스로부터 상속받고 Fightable 인터페이스만을 구현했지만, Unit클래스는 Object클래스의 자손이고, Fightable 인터페이스는 Attackable과 Movable인터페이스의 자손이므로 Fighter클래스는 이 모든 클래스와 인터페이스의 자손이 되는 셈이다. 인터페이스는 상속 대신 구현이라는 용어를 사용하지만, 인터페이스로 부터 상속받은 추상메서드를 구현하는 것이기 떄문에, 인터페이스도 조금은 다른 의미의 조상이라고 할 수 있다 .여기서 주의 깊게 봐두어야 할 것은 Movable 인터페이스에 정의된 void move(int x, iny )를 FIgher클래스에서 구현할 때 접근 제어자를 public으로 했다는 것이다.
오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다는 것을 기억할 것이다. Movable 인터페이스에 void move(int x, int y)와 같이 정의되어 있지만, 사실 public abstract가 생략된 것이기 때문에 실제로 public abstract void move(int x, int y)이다. 그래서, 이를 구현하는 Fihter클래스에서는 void move(int x, int y)의 접근 제어자를 반드시 public 으로 해야 하는 것이다.
인터페이스를 이용한 다중상속
두 조상으로부터 상속받은 멤버 중에서 멤버변수의 이름이 같거나 메서드의 선언부가 일치하고 구현 내용이 다르다면 이두 조상으로부터 상속받는 조상클래스는 어느 조상의 것을 상속받게 되는 것인지 알 수 없다. 어느 한 쪽으로부터의 상속을 포기하던가, 이름이 충돌하지 않도록 조상클래스를 변경하는 수 밖에 없다.
그래서 다중상속은장점도 있지만 단점이 더 크다고 판단 하였기 떄문에 자바에서는 다중상속을 허용하지 않는다. 그러나 또 다른 객체지향 언어인 C++ 에서는 다중상속을 허용하기 때문에 자바는 다중상속을 허용하지 않는다는 것이 단점으로 부각되는 것에 대한 대응으로 자바도 인터페이스를 이용하면 다중상속이 가능하다. 라고 하는 것일 뿐 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다. 이러한 이유로 인터페이스가 다중상속을 위한 것으로 오해를 사곤 하는데, 앞으로 이 단원을 학습해 나가면서 인터페이스의 참다운 의미를 알 게 될 것이다. 인터페이스를 이용한 다중상속에 대한 내용은 가볍에 맛만 보고 넘거나느 정도면 충분 할 것 같다.
인터페이스는 static 상수만 정의할 수 있으므로 조상크래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능핟. 그리고 추상메서드는 구현내용이 전혀 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우네느 당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다. 그러나 이렇게 하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다. 만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다. 예를 들어, 다음과 같이 Tv클래스와 VCR클래스가 있을 때, TVCR클래스를 작성하기 위해 두 클래스로부터 상속을 받을 수만 있으면 좋겠지만 다중상속을 허용하지 않으므로, 한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내에 포함시켜서 내부적으로 인스턴스를 생성해서 사용하도록 한다.
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 volumnUp() {
volume++;
}
public void volumnDown() {
volume--;
}
}
public class VCR {
protected int counter; // vcr의 카운터
public void play() {
// Tape을 재생한다.
}
public void stop() {
// 재생을 멈춘다.
}
public void reset() {
counter = 0;
}
public int 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인터페이스의 추상메서드를 구현하는데 사용한다.
이때 vcr 클래스 타입의 참조변수를 멤버변수로 선언하여 IVCR 인터페이스의 추상메서드를 구현하는데 사용한다.
IVCR인터페이스를 구현하기 위해서는 새로 메서드를 작성해야하는 부담이 있지만 이처럼 VCR클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있다. 또한 VCR 클래스의 내용이 변경되어도 변경된 내용이 TVCR클래스에도 자동적으로 반영되는 효과도 얻을 수 있다.
사실 인터페이스를 새로 작성하지 않고도 VCR클래스를 TVCR클래스에 포함시키는 것만으로도 충분하지만, 인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있다.
다형성에 대해 학습할 때 자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능하다는 것을 배웠다. 인터페이스 역시 이를 구현할 클래스의 조상이라 할 수 있으므로 해당 인터페이스의 타입의 참조변수로 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입의 형변환도 가능하다. 인터페이스 Fightable을 클래스 Fighter가 구현했을 대 , 다음과 같이 Fighter인스턴스를 Fighteable 타입의 참조변수로 참조하는 것이 가능하다.
Fightable f = (Fightable) new Fighter();
Fightable f= new Fighter();
따라서 인터페이스는 다음과 같이 메서드의 매개변수의 타입으로 사용될 수 있다.
void attack(Fightable f){
}
인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현할 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다 .그래서 attack 메서드를 호출할 때는 매개변수로 Fightable 인터ㅍ페이스를 구현할 클래스의 인스턴스를 넘겨주어야 한다.
class Fighter extends Unit implement Fightable {
public void move(int x, int y){};
public void attack(Fightable f){};
}
위와 같이 Fightable 인터페이스를 구현 한 Fighter 클래스가 있을 때, attack 메서드의 매개변수로 Fighter인스턴스를 넘겨 줄 수 있다 .즉, attack(new Fighter())와 같이 할 수 있다는 것이다 .그리고 다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
리턴타입이 인터페이스 라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
위의 코드에서는 method() 의 리턴타입이 Fightable 인터페이스이기 때문에 메서드의 return 문에서 Fightable인터페이스를 구현한 Fighter클래스의 인스턴스를 반환한다.
interface Parseable {
public abstract void parse(String fileName);
}
class XMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName + "-XML parsing completed.");
}
}
class HTMLParser implements Parseable {
public void parse(String fileName) {
System.out.println(fileName + "-HTML parsing completed.");
}
}
class ParserManager {
// 리턴타입이 Parseable 인터페이스이다.
public static Parseable getParser(String type) {
if (type.equals("XML")) {
return new XMLParser();
} else {
HTMLParser p = new HTMLParser();
return p;
}
}
}
class ParserTest {
public static void main(String[] args) {
Parseable parseable = ParserManager.getParser("XML");
parseable.parse("반갑습니다. XML::");
Parseable parseable2 = ParserManager.getParser("HTML");
parseable2.parse("반갑습니다. HTML::");
}
}
Parseable인터페이스는 구문분석(parsing)을 수행하는 기능을 구현할 목적으로 추상메서드 parse(String fileName)을 정의했다. 그리고 XMLParser클래스와 HTMLParser클래스는 Parserble인터페이스를 구현하였다. ParserManager클래스의 getParser메서드는 매개변수로 넘겨받은 type의 값에 따라 XMLParser인스턴스 또는 HTMLParser인스턴스를 반환한다.
getParser메서드의 수행결과로 참조변수 parser는 XMLParser인스턴스의 주소값을 갖게 된다. 마치 Parseable parser = new XMLParser(); 이 수행된 것과 같다.
참조변수 parser를 통해 parse()를 호출하면, parser가 참조하고 잇는 XMLParser인스턴스의 parse메서드가 호출된다. 만일 나중에 새로운 종류의 XML 구문분석기 NewXMLParser 클래스가 나와도 ParserTest클래스는 변경할 필요 없이 ParserManager클래스의 getParser메서드 에서 return NEW XMLParser(); 대신 return new NewXMLParser(); 로 변경하기만 하면 된다.
이러한 장점은 특히 분산환경 프로그래밍에서 그 위력을 발휘한다. 사용자 컴퓨터에 설치된 프로그램을 변경하지 않고 서버측의 변경만으로도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능하다.