본문 바로가기

개발 관련 지식/자바(Java)

[자바] 인터페이스(interface)

* 인터페이스(interface)

: 인터페이스는 일종의 추상 클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외의 다른 어떠한 요소도 허용하지 않는다.

추상클래스를 부분적으로만 완성된 '미완성 설계도'라고 한다면, 인터페이스는 구현된 것은 아무 것도 없고 밑그림만 그려져 있는 '기본 설계도'라 할 수 있다.

인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.

 

* 인터페이스의 작성

: 인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 키워드로 class 대신 interface를 사용한다는 것만 다르다. 그리고 interface에도 클래스와 같은 접근제어자로 public 또는 default를 사용할 수 있다.

 

<예문>

interface 인터페이스 이름{

public static final 타입 상수이름 = 값;

public abstract 메서드이름(매개변수 목록);

}

 

 

인터페이스의 멤버들은 다음과 같은 제약사항을 가지고 있다.

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

- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.

[참고] 인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다. 생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.

 

* 인터페이스의 상속

: 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.

 

 

[참고] 인터페이스는 클래스와 달리 Object 클래스와 같은 최고 조상은 없다.

 

<예문>

interface Movable{
 // 지정된 위치(x, y)로 이동하는 기능의 메서드
 void move(int x, int y);
}

interface Attackable{
 // 지정된 대상(u)을 공격하는 기능의 메서드
 void attack(Unit u);
}

interface Fightable extends Movable, Attackable{}

 

 

* 인터페이스의 구현

: 인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 하는데, 그 방법은 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스는 확장한다는 의미의 키워드 'extends'를 사용하지만 인터페이스는 구현한다는 의미의 키워드 'implements'를 사용할 뿐이다.

 

<예문>

class 클래스이름 implement 인터페이스이름{

 //인터페이스에 정의된 추상메서드를 구현해야 한다.

}


class Fighter implement Fightable{

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

public void attack(Unit u){ /* 내용 생략 */ }

}

 

 

[참고] 이 때 'Fighter 클래스는 Fightable 인터페이스를 구현한다.' 라고 한다.

 

 

만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, 추상클래스로 선언되어야 한다.

 

<예문>

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'로 끝나야 하는 것은 아니다.

 

<예문>

public class interfaceTest {
 public static void main(String[] args){
  Fighter f = new Fighter();
  
  if(f instanceof Unit1){
   System.out.println("f는 Unit1클래스의 자손입니다.");
  }
  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클래스의 자손입니다.");
  }
 }
}


interface Movable{
 // 지정된 위치(x, y)로 이동하는 기능의 메서드
 void move(int x, int y);
}


interface Attackable{
 // 지정된 대상(u)을 공격하는 기능의 메서드
 void attack(Unit u);
}


interface Fightable extends Movable, Attackable{} 

 

class Fighter extends Unit1 implements Fightable{

 @Override
 public void attack(Unit u) {
  // 내용 생략
 }

 @Override
 public void move(int x, int y) {
  // 내용 생략
 }
}

class Unit1{
 int currentHP; //유닛의 체력
 int x;   //유닛의 위치(x좌표)
 int y;   //유닛의 위치(y좌표)
}

 

 

 실제로 Fighter클래스는 Unit클래스로부터 상속받고 Fightable인터페이스만을 구현했지만, Unit클래스는 Object클래스의 자손이고, Fightable인터페이스는 Attackable과 Movable인터페이스의 자손이므로 Fighter클래스는 이 모든 클래스와 인터페이스의 자손이 되는 셈이다.

인터페이스는 상속 대신 구현이라는 용어를 사용하지만, 인터페이스로부터 상속받은 추상메서드를 구현하는 것이기 때문에 인터페이스도 조금은 다른 의미의 조상이라고 할 수 있다.

여기서 주의 깊게 봐두어야 할 것은 Movable인터페이스에 정의된 void move(int x, int y)를 Fighter클래스에서 구현할 때 접근 제어자를 public으로 했다는 것이다.

 

<예문>

interface Movable{

 void move(int x, int y);

}


class Fighter extends Unit implements Fightable {

public void move(int x, int y) { /* 실제 구현 내용 생략 */ }

public void attack(Unit u){ /* 실제 구현 내용 생략 */ }

}

 

 

오버라이딩 할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야한다. 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++에서는 다중상속을 허용하기 때문에 자바는 다중상속을 허용하지 않는다는 것이 단점으로 부각되는 것에 대한 대응으로 '자바도 인터페이스를 이용하면 다중상속이 가능하다.'라고 하는 것일 뿐 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다.

이러한 이유로 인터페이스가 다중상속을 위한 것으로 오해를 사곤 하는데, 앞으로 이 단원을 학습해 나가면서 인터페이스의 참다운 의미를 알게 될 것이다.

 

 

 인터페이스는 상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 극히 드물고 추상메서드는 구현내용이 전혀 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.

이렇게 하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다.

만일 두 개의 클래스로부터 상속을 받아야 할 상황이라면, 두 조상클래스 중에서 비종이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.

 

<예문>

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; //VCR의 카운터
 
public void play(){
 // Type을 재생한다.
 }
 
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);
 }
}

 

 

 

IVCR 인터페이스를 구현하기 위해서는 새로 메서드를 작성해야 하는 부담이 있지만 이처럼 VCR클래스의 인스턴스를 사용하면 손쉽게 다중상속을 구현할 수 있다.

또한 VCR클래스의 내용이 변경되어도 변경된 내용이 TVCR클래스에도 자동적으로 반영되는 효과도 얻을 수 있다.

 

 

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

: 다형성과 관련하여 자손클래스의 인스턴스를 자손타입의 참조변수로 참조하는 것이 가능하다.

인터페이스 역시 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다.

 

<예문>

Fightable f = (Fightable) new Fighter();

또는 

Fightable f = new Fighter();

 

 

 

 

[참고] Fightable 타입의 참조변수로는 인터페이스 Fightable 에 정의된 멤버들만 호출이 가능하다.

 

 

따라서 인터페이스는 아래와 같이 메서드의 매개변수의 타입으로 사용될 수 있다.

 

<예문>

void attack(Fightable f){

 // ...

}

 

 

 인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다.

그래서 attack메서드를 호출할 때는 매개변수로 Fightable 인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다.

 

<예문>

class Figther 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(){

// ...

return new Fighter();

}

 

 

 

 

 

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

 위의 코드에서는 method()의 리턴타입이 Fightable 인터페이스이기 때문에 메서드의 return문에서 Fightable인터페이스를 구현한 Fighter클래스의 인스턴스를 반환한다.

 

 

* 인터페이스의 장점

: 인터페이스를 사용하는 이유와 그 장점을 정리해보면,

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

2. 표준화가 가능하다.

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

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

 

<예문>

 한 데이터베이스 회사가 제공하는 특정 데이터베이스를 사용하는데 필요한 클래스를 사용해서 프로그램을 작성했다면 이 프로그램은 다른 종류의 데이터베이스를 사용하기 위해서는 전체 프로그램 중에서 데이터베이스 관련된 부분은 모두 변경해야 할 것이다.

 그러나 데이터베이스 관련 인터페이스를 정의하고 이를 이용해서 프로그램을 작성하면, 데이터베이스의 종류가 변경되더라도 프로그램을 변경하지 않도록 할 수 있다.

단, 데이터베이스 회사에서 제공하는 클래스도 인터페이스를 구현하도록 요구해야한다. 데이터베이스를 이용한 응용프로그램을 작성하는 쪽에서는 인터페이스를 이용해서 프로그램을 작성하고, 데이터베이스 회사에서는 인터페이스를 구현한 클래스를 작성해서 제공해야 한다.

 실제로 자바에는 다수의 데이터베이스 관련된 다수의 인터페이스를 제공하고 있으며, 프로그래머는 이 인터페이스를 이용해서 프로그래밍하면 특정 데이터베이스에 종속되지 않는 프로그램을 작성할 수 있다.

 

 

 게임 스타크래프트에 나오는 유닛을 클래스로 표현하고 이들 간의 관계를 살펴보면, 최고 조상은 Unit클래스이고 유닛의 종류는 지상유닛(GroundUnit)과 공중유닛(AirUnit)으로 나누어진다.

 그리고 지상유닛에는 Marine, SCV(건설인부), Tank가 있고, 공중유닛으로는 Dropship(수송선)이 있다. SVC에게 Tank와 Dropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair메서드를 정의한다면 다음과 같을 것이다.

 

<예문>

void repair(Tank t){

//Tank를 수리한다.

}


void repair(Dropship d){

//Dropship을 수리한다.

}

 

 

 위와 같은 방식으로 수리가 가능한 유닛의 개수만큼 다른 버전의 오버로딩된 메서드를 정의해야 할 것이다.

 이것을 피하기 위해 매개변수의 타입을 이 들의 공통 조상으로 하면 좋겠지만 Dropship은 공통조상이 다르기 때문에 공통조상의 타입으로 메서드를 정의한다고 해도 최소한 2개의 메서드가 필요할 것이다.

 그리고 GroundUnit의 자손 중에는 Marine과 같이 기계화 유닛이 아닌 클래스도 포함될 수 있기 때문에 repair메서드의 매개변수 타입으로 GroundUnit은 부적합하다.

 현재의 상속관계에서는 이들의 공통점은 없다. 이 때 인터페이스를 이용하면 기존의 상속체계를 유지하면서 이들 기계화 유닛의 공통점을 부여할 수 있다.

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

 

<예문>

interface Repairable{}


class SCV extends GroundUnit implement Repairable{

// ...

}


class Tank extends GroundUnit implement Repairable{

// ...

}


class Dropship extends AirUnit implement Repairable{

// ...

}

 

 

 

 위의 예문처럼 이제 이 3개의 클래스에는 같은 인터페이스를 구현했다는 공통점이 생겼다. 인터페이스 Repairable에 정의된 것은 아무것도 없고, 단지 인스턴스의 타입체크에만 사용될 뿐이다.

 그리고 repair메서드의 매개변수의 타입을 Repairable로 선언하면, 이 메서드의 매개변수로 Repairable 인터페이스를 구현한 클래스의 인스턴스만 받아들여질 것이다.

 

<예문>

void repair(Repairable r){

//매개변수로 넘겨받은 유닛을 수리한다.

}

 

 

 

 

 앞으로 새로운 클래스를 추가될 때, SVC의 repair메서드에 의해서 수리가 가능하도록 하려면 Repairable 인터페이스를 구현하도록 하면 될 것이다.

 

* 인터페이스의 이해

: 인터페이스를 이해하기 위한 두 가지 염두할 사항

1. 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.

2. 메서드를 사용(호출)하는 쪽(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 args[]){

A a = new A();

a.methodA(new B());

}

}

 

 

 위의 예제와 같이 클래스 A와 클래스 B가 있다고 하자. 클래스 A(User)는 클래스 B(Provider)의 인스턴스를 생성하고 메서드를 호출한다. 이 두 클래슨느 서로 직접적인 관계에 있다. 이 것을 간단히 'A-B'라고 표현하자.

 이 경우 클래스 A가 작성하기 위해서는 클래스 B가 이미 작성되어 있어야 한다. 그리고 클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.

이와 같이 직접적인 관계의 두 클래스는 한 쪽(Provider)이 변경되면 다른 한 쪽(User)도 변경되어야 한다는 단점이 있다.

 그러나 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 A나 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능의 다른 클래스로 대체 되어도 클래스 A는 전혀 영향을 받지 않도록 하는 것이 가능하다.

 두 클래스 간의 관계가 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해서 클래스 B(Provider)의 선언과 구현을 분리해야 한다.

 

<예문>

interface I{

public abstract void methodB();

}


class B implements I{

public void methodB(){

System.out.println("methodB in B class");

}

}


class A{

public void methodA(I i){

i.methodB();

}

}

 

 

[참고] methodA가 호출될 때 인터페이스 I를 구현한 클래스의 인스턴스(클래스 B의 인스턴스)를 제공받아야 한다.

 

 클래스 A를 작성하는데 있어서 클래스 B가 사용되지 않는다는 점에 주목하자. 이제 클래스 A와 클래스 B는 'A-B'의 직접적인 관계에서 'A-I-B'의 간접적인 관계로 바뀐 것이다.

 

<예문>

public class interfaceTest2 {
 public static void main(String[] args){
  A a = new A();
  a.methodA(new B());
  a.methodA(new C());
 }
}

interface I {
 public abstract void methodB();
}

class B implements I {
 public void methodB() {
  System.out.println("methodB in B class");
 }
}

class C implements I {
 public void methodB() {
  System.out.println("methodC in C class");
 }
}
class A {
 public void methodA(I i) {
  i.methodB();
 }
}

 

 

 

 

 

 

[참고] 클래스 A를 작성하는데 클래스 B가 관련되지 않았다는 사실에 주목하자.

 

 이처럼 매개변수를 통해 동적으로 제공받을 수도 있다는 사실을 확인하자~!

이와 같은 방식은 AWT 컴포넌트의 addActionListener(ActionListener I)와 클래스 Thread의 생성자인 Thread(Runnable target)가 이런 방식으로 되어 있다.

 

[참고] Runable과 ActionListener는 인터페이스이다.

 

 아래와 같이 제3의 클래스를 통해서 인스턴스를 제공받을 수도 있다. JDBC의 DriverManager클래스가 이런 방식으로 되어 있다.

 

<예문>

public class interfaceTest3 {
 public static void main(String[] args){
  A a = new A();
  a.methodA();
 }
}

class A {
 public void methodA() {
  I i = InstanceMangager.getInstance();
  // 제 3의 클래스의 메서드를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 얻어온다.
  i.methodB();
 }
}

interface I {
 public abstract void methodB();
}

class B implements I {
 public void methodB() {
  System.out.println("methodB in B class");
 }
}

class InstanceMangager {
 public static I getInstance(){
  return new B();
 }
}