[java] 16. 인터페이스


인터페이스



인터페이스는 일종의 추상클래스입니다.

인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서

추상클래스와 달리 몸통을 갖춘 일반메서드 또는 멤버변수를 구성원으로 가질 수 없습니다.

오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않습니다.

인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그자체만으로

사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성됩니다.



인터페이스의 작성



인터페이스를 작성하는 것은 클래스를 작성하는 것과 같습니다.

다만 키워드로 class 대신 interface를 사용하는 것만 다릅니다.

그리고 interface에도 클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있습니다.


interface 인터페이스 이름{
	public static final 타입 상수이름=값;
	public abstract 반환형 메서드이름(매개변수목록);
}



일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있습니다.


1.모든 멤버변수는 public static final 이어야 하며, 이를생략할 수 있다.


2.모든 메서드는 public abstract 이어야하며, 이를 생략할 수 있다.
(단, static메서드와 디폴트메서드는 예외)



인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에

제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많습니다.

생략한 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해줍니다.


image



원래는 인터페이스의 모든 메서드는 추상메서드이어야 하는데,

JDK1.8부터 인터페이스에 static메서드와 디폴트 메서드의 추가를 허용하는 방향으로 변경되었습니다.



인터페이스의 상속



인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속,

즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능합니다.


image



클래스의 상속과 마찬가지로 자식 인터페이스(Fightable)은 부모 인터페이스(Movable, Attackable)에 정의된 멤버를

모두 상속받습니다.

그래서 Fightable 자체에는 정의된 멤버가 하나도 없지만 부모 인터페이스로부터 상속받은

두 개의 추상메서드, move(int x,int y)와 attack(Object u)을 멤버로 갖게됩니다.

인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며,

추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼,

인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 하는데,

그 방법은 키워드 implements를 사용하면 됩니다.


class 클래스이름 implements 인터페이스이름{
	//인터페이스에 정의된 추상메서드를 구현해야 한다.
}




//예
class Fighter implements Fightable{
	public void move(int x, int y){/* 내용 생략 */}
	public void attack(Object 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(Object u){/* 내용 생략 */}
}




image



오버라이딩 할 때는 부모의 메서드보다 넓은 범위의 접근 제어자를 지정해야합니다.

Movable인터페이스에 void move(int x,int y)와 같이 정의되어 있지만,

사실 public abstract가 생략된 것이기 때문에 실제로는 public abstract void move(int x,int y)입니다.

그래서, 이를 구현하는 Fighter 클래스에서는 void move(int x, int y)의 접근제어자를 반드시 public으로 해야합니다.



인터페이스를 이용한 다중 상속



다중상속이 가지는 장점보다 단점이 더 크다고 판단한 자바에서는 다중상속을 허용하지 않는다고 배웠는데요

그러나 C++ 에서는 다중상속을 허용하기 때문에 자바는 다중상속을 허용하지 않는다는 것이

단점으로 부각되는 것에 대한 대응으로 자바도 인터페이스를 이용하여

다중상속을 구현할 수 있습니다.

실제론 다중상속을 구현하는 경우는 거의 없습니다.

만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면,

두 부모클래스 중에서 비중이 높은 쪽을 상속받고, 다른 한쪽은 클래스 내부에 멤버로 포함시키거나,

어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 합니다.

예를 들어, 다음과 같이 Tv클래스와 VCR 클래스가 있을때, TVCR 클래스를 작성하기 위해

두 클래스로부터 상속을 받을 수 있으면 좋겠지만 다중상속을 허용하지 않으므로,

한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내에 포함시켜서 내부적으로

인스턴스를 생성해서 사용합니다.


image



image



VCR클래스에 정의된 메서드와 일치하는 추상메서드를 갖는 인터페이스를 작성합니다.


image



이제 IVCR 인터페이스를 구현하고 Tv클래스로부터 상속받는 TVCR클래스를 작성합니다.


image



IVCR 인터페이스를 구현하기 위해서는 새로 메서드를 작성해야하는 부담이 있지만

이처럼 VCR클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있습니다.

사실 인터페이스를 새로 작성하지 않고도 VCR클래스를 TVCR클래스에 포함시키는 것만으로도 충분하지만,

인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있습니다.



인터페이스를 이용한 다형성



다형성에 대해 학습할 때 자식클래스의 인스턴스를

부모타입의 참조변수로 참조하는 것이 가능하다는 것을 배웠습니다.

인터페이스 역시 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로

이를 구현한 클래스의 인스턴스를 참조할 수 있으며,

다음과 같이 Fighter 인스턴스를 Fightable 타입의 참조변수로 참조하는 것이 가능합니다.


//클래스 Fighter는 인터페이스 Fightable을 구현
Fightable f=(Fightable)new Fighter;



또는

Fightable f=new Fighter();



따라서 인터페이스는 다음과 같이 메서드의 매개변수의 타입으로 사용 될 수 있습니다.


void attack(Fightable f){
	//...
}



인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의

인스턴스를 매개변수로 제공해야한다는 것입니다.

그리고 다음과 같이 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능합니다.


Fightable method(){
	...
	Fighter f=new Fighter();
	return f;
}



리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환하는 것을 의미합니다.

위의 코드에서는 method()의 리턴타입이 Fightable 인터페이스이기 때문에 메서드의 return 문에서

Fightable 인터페이스를 구현한 Fighter 클래스의 인스턴스를 반환합니다.

다음은 인터페이스를 이용한 예제입니다.


image



인터페이스의 장점



인터페이스를 사용하는 이유와 그 장점을 정리해 보면 다음과 같습니다.


1.개발시간을 단축시킬 수 있다.


일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다.

메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.




2.표준화가 가능하다.


프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게

인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다




3.서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.


서로 상속관계에 있지도 않고, 같은 부모클래스를 가지고 있지 않은 서로 아무런 관계도 없는 클래스들에게

하나의 인터페이스를 공통적으로 구현함으로써 관계를 맺어줄 수 있다.




4.독립적인 프로그래밍이 가능하다.


인터페이스를 이용하면 클래스 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성가능하다.

인터페이스 응용



인터페이스를 어떤 경우에 사용할 수 있는지 예를 보면서 설명해보겠습니다.

게임에 나오는 유닛을 클래스로 표현하고 이들의 관계를 상속계층도로 다음과 같이 표현해보았습니다.


image



게임의 나오는 모든 유닛들의 최고 조상은 Unit클래스이고 유닛의 종류는 지상유닛(GroundUnit)과 공중유닛(AirUnit)으로 나뉩니다.

그리고 지상유닛에는 Marine,SCV(건설인부),Tank가 있고,

공중유닛으로는 Dropship이 있습니다.

SCV에게 Tank와 Dropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair메서드를 정의한다면,

다음과 같을 것입니다.


class SCV{
	void repair(Tank t){
		//Tank를 수리한다.
	}
	void repair(Dropship t){
		//Dropship을 수리한다.
	}	
}



이런 식으로 수리가 가능한 유닛의 개수만큼 다른 버전의 오버로딩된 메서드를 정의해야만 합니다.

이것을 피하기 위해 매개변수의 타입을 이 들의 공통 조상으로 하면 좋겠지만 Dropship은 공통조상이 다르기 때문에

공통조상의 타입으로 메서드를 정의한다고해도

최소한 2개의 메서드가 필요할 것입니다.


void repair(GroundUnit gu){
	//매개변수로 넘겨진 지상유닛을 수리한다.
}

void repair(AirUnit au){
	//매개변수로 넘겨진 공중유닛을 수리한다.
}



그런데 GroundUnit 자식 중에는 Marine과 같이 기계화 유닛이 아닌 클래스도 포함될 수 있기 때문에

repair메서드의 매개변수 타입으로 GroundUnit은 부적합합니다.

이럴 때 인터페이스를 이용하면 기존의 상속체계를 유지하면서 이들 기계화 유닛에 공통점을 부여할 수 있습니다.

다음과 같이 Repairable이라는 인터페이스를 정의하고 수리가 가능한 기계화 유닛에게 이 인터페이스를 구현하도록 하면됩니다.


interface Repairable{} //인터페이스 정의

class SCV extends GroundUnit implements Repairable{
	//...
}

class Tank extends GroundUnit implements Repairable{
	//...
}

class Dropship extends AirUnit implements Repairable{
	//...
}



위의 3개의 클래스에는 같은 인터페이스를 구현했다는 공통점이 생겼습니다.

Repairable 인터페이스를 중심으로 상속계층도를 그려보면 다음과 같습니다.


image



그리고 repair 메서드의 매개변수의 타입을 Repairable로 선언하면,

이 메서드의 매개변수로 Repairable 인터페이스를 구현한 클래스의 인스턴스만 받아들여질 것입니다.


void repair(Repairable r){
	if(r instanceof Unit){
		Unit u=(Unit)r;
		//체력회복...
	}
}



위의 예제와 같이 인터페이스는 서로 관계없는 클래스들에게 관계를 맺어줄 수 있습니다.

한 가지 예를 더 들어보겠습니다.

게임에 나오는 건물들을 클래스로 표현하고 이들의 관계를 상속계층도로 표현하였습니다.


image



건물을 표현하는 클래스 Academy, Bunker, Barrck, Factory가 있고 이들의 부모인 Building 클래스가 있다고 합시다.

이 때 Barrack 클래스와 Factory 클래스에 다음과 같은 내용의, 건물을 이동시킬 수 있는,

새로운 메서드를 추가하고자 한다면 어떻게 해야할까요?


void liftOff() {/* 내용생략 */}

void move(int x,int y) {/* 내용생략 */}

void stop() {/* 내용생략 */}

void land() {/* 내용생략 */}



Barrack클래스와 Factory 클래스 모두 위의 코드를 적어주면 되긴 하지만,

코드가 중복된다는 단점이 있습니다.

그렇다고 해서 Building 클래스에 코드를 추가해주면,

Building 클래스의 다른 자식인 Academy와 Bunker 클래스도 추가된 코드를 상속받으므로 안 됩니다.

이런 경우에도 인터페이스를 이용해서 해결할 수가 있습니다.

우선 새로 추가하고자하는 메서드를 정의하는 인터페이스를 정의하고, 이를 구현하는 클래스를 작성합니다.


//인터페이스 정의

interface Liftable{
	void liftOff();
	
	void move(int x,int y);
	
	void stop();
	
	void land();
}




//인터페이스 Liftable을 구현한 클래스 정의

class LiftableImpl implements Liftable{
	public void liftOff() {/* 내용생략 */}
	
	public void move(int x,int y) {/* 내용생략 */}
	
	public void stop() {/* 내용생략 */}
	
	public void land() {/* 내용생략 */}
}



마지막으로 새로 작성된 인터페이스와 이를 구현한 클래스를 Barrack과 Factory 클래스에 적용하면 됩니다.

Barrack클래스가 Liftable 인터페이스를 구현하도록 하고, 인터페이스를 구현한 LiftableImple 클래스를

Barrack 클래스에 포함시켜서 내부적으로 호출해서 사용하도록 합니다.


class Barrack extends Building implements Liftable{
	LiftableImpl l=new LiftableImple();
	
	void liftOff() { l.liftOff(); }
	
	void move(int x, int y) { l.move(); }
	
	void liftOff { l.stop(); }
	
	void liftOff { l.land(); }
	
	...
}


class Factory extends Building implements Liftable{
	LiftableImpl l=new LiftableImple();
	
	void liftOff() { l.liftOff(); }
	
	void move(int x, int y) { l.move(); }
	
	void liftOff { l.stop(); }
	
	void liftOff { l.land(); }
	
	...
}




image



static 메서드



원래는 인터페이스에 추상메서드만 선언할 수 있는데,

JDK 8.1부터 디폴트 메서드와 static 메서드도 추가할 수 있게 되었습니다.

static 메스드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에

예전부터 인터페이스에 추가하지 못할 이유가 없었습니다.

그러나 자바를 보다 쉽게 배울 수 있도록 규칙을 단순히 할 필요가 있어서

인터페이스의 모든 메서드는 추상 메서드이어야 한다는 규칙에 예외를 두지 않았었습니다.

덕분에 인터페이스와 관련된 static 메서드는 별도의 클래스에 따로 두어야 했습니다.

가장 대표적인 것으로 java.util.Collection 인터페이스가 있습니다.

JDK 8.1 부터는 static 메서드 또한 인터페이스에 추가할 수 있으며,

접근제어자는 항상 public이며, 생략할 수 있습니다.



디폴트 메서드



부모클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만,

인터페이스의 경우에 메서드를 추가하는 것은 추상메서드를 추가한다는 것이고,

이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야하기 때문에

보통 큰 일이 아닙니다.

그래서 인터페이스가 변경되는 경우를 위하여 디폴트메서드라는 것이 생겨났습니다.

디폴트 메서드는 추상메서드의 기본적인 구현을 제공하는 메서드로,

추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도

해당 인터페이스를 구현한 클래스를 변경하지 않아도 됩니다.

디폴트 메서드 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드 처럼 몸통{}이 있어야합니다.


interface MyInterface{
	void method();
	default void newMethod(){}; //디폴트메서드
}



위와 같이 디폴트 메서드를 추가하면, 기존의 MyInterface를 구현한 클래스를 변경하지 않아도 됩니다.

즉, 부모클래스에 새로운 메서드를 추가한 것과 동일해지는 것입니다.

대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생합니다.

이 충돌을 해결하는 규칙은 다음과 같습니다.


1. 여러 인터페이스의 디폴트 메서드 간의 충돌

	-인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
	

2. 디폴트 메서드와 부모 클래스의 메서드 간의 충돌

	-부모 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.




image


image



출력결과 입니다.


image