인터페이스
인터페이스는 일종의 추상클래스다. 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.
오직 추상메서드와 상수만을 멤버로 가질 수 있으며, 그 외는 없다.
추상 클래스를 부분적으로 완성된 미완성 설계도라고 한다면, 인터페이스는 구현된 것이 아무 것도 없고 밑그림만 그린 기본 설계도라 할 수 있다.
인터페이스도 추상클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기 보다는 다른 클래스를 작성하는데 도움 줄 목적으로 작성된다.
인터페이스 작성
인터페이스 작성은 클래스 작성과 같다. 다만 키워드로 class 대신 interface를 사용하는 것만 다르다. interface에도 클래스와 같이 접근 제어자로 public 또는 default를 사용할 수 있다.
interface 인터페이스이름
{
public static final 타입 상수이름 = 값;
public abstract 메서드이름(매개변수목록);
}
클래스 멤버와 달리 인터페이스 멤버들은 다음 제약사항이 있다.
1. 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
2. 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다.
- 단 static 메서드와 default 메서드는 예외 (JDK1.8부터)
interface PlayingCard
{
public static final int SPADE = 4;
final int DIAMOND = 3; // public static int
static int HEART = 2; // public static int
int CLOVER = 1; // public static int
public abstract String getCardNumber();
String getCardKind(); // public abstract String getCardKind();
}
원래 인터페이스의 모든 메서드는 추상메서드여야 하는데, JDK1.8부터 인터페이스에 static 메서드와 defuault 메서드의 추가를 허용했다. 실무에서는 아직 적용이 안된 곳이 많아 둘 다 알고 있어야 한다.
인터페이스 상속
인터페이스는 인터페이스로부터만 상속 받을 수 있으며 클래스와는 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능하다.
interface Movable
{
void move(int x, int y); // 지정된 위치(x, y)로 이동하는 메서드
}
interface Attackable
{
void attack(Unit u); // 저정된 대상(u)를 공격하는 메서드
}
interface Fightable extends Movable, Attackable
{
...
}
Fightable은 parent 클래스로부터 상속 받은 두 개의 추상 메서드, move(int x, int y)와 attack(Unit u)를 멤버로 갖는다.
인터페이스 구현
인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 implements를 사용해 구현한다.
추상클래스 extends
인터페이스 implements
class 클래스이름 implements 인터페이스이름
{
// 인터페이스에 정의된 추상메서드 구현
}
class Fighter implements Fightable
{
public void move(int x, int y)
{
...
}
public void attack(Unit u)
{
...
}
}
Fighter 클래스는 Fightable 인터페이스를 구현한다.
만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, 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)
{
...
}
}
인터페이스 이름은 주로 '~을 할 수 있는'의 'able'로 끝나는 경우가 많은데, 어떠한 기능을 하는데 필요한 메서드를 제공한다는 의미다.
f는 Unit클래스의 자손..
f는 Fightable인터페이스 구현했음..
f는 Movable인터페이스 구현했음...
f는 Attackable인터페이스 구현했음...
f는 Object의 클래스 자손...
interface Fightable extends Movable, Attackable //**
{
...
}
interface Movable
{
void move(int x, int y);
}
interface Attackable
{
void attack(Unit u);
}
class Unit
{
int currentHP;
int x;
int y;
}
class Fighter extends Unit implements Fightable //**
{
public void move(int x, int y)
{
...
}
public void attack(Unit u)
{
...
}
}
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의 클래스 자손...");
}
}
}
예제에서 중요한 것은 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으로 해야 한다.
인터페이스 다중 상속
두 parent로부터 상속 받는 멤버 중에서 멤버 변수의 이름이나 메서드의 선언부가 일치하고 구현 내용이 다르다면 이 두 parent로부터 상속 받는 child 클래스는 어느 parent 것을 상속 받게 되는 건지 알 수 없다. 상속을 포기하던가, parent 클래스 이름을 변경하는 수 밖에 없다.
인터페이스를 이용하면 다중 상속이 가능하나 구현하는 경우는 거의 없다.
두 개의 클래스로부터 상속을 받야야 할 상황이라면, 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고 다른 한 쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한 쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현한다.
예를 들어, 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 volumeUp()
{
volume++;
}
public void volumeDown()
{
volume--;
}
}
public class VCR
{
protected int counter;
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 인터페이스의 추상메서드를 구현하는데 사용한다.
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(int c)
{
vcr.setCounter(c);
}
}
이처럼 VCR 클래스의 인스턴스를 사용하면 간단하게 다중 상속을 구현할 수 있다.
인터페이스 장점
- 개발 시간 단축
일단 인터페이스가 작성되면, 이를 사용해서 프로그램 작성이 가능하다. 메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.
- 표준화 가능
프로젝트에 사용되는 기본 틀을 인터페이스로 작성해 정형화된 프로그램 개발이 가능하다.
- 서로 관계 없는 클래스들에게 관계를 맺어주기 가능
하나의 인터페이스를 공통적으로 구현해 관계 없는 클래스 관계 맺기 가능하다.
- 독립적인 프로그래밍 가능
예제 1.
게임에 나오는 모든 유닛들의 최고 parent는 Unit 클래스이고 Unit 종류는 지상 유닛, 공중 유닛으로 나뉜다.
지상 유닛에는 Marine, SCV(건설인부), Tank가 있고,
공중 유닛은 Dropship(수송선)이 있다.
SCV에게 Tank와 Dropship의 기계화 유닛을 수리할 수 있는 기능 제공 위해 repair 메서드를 정의한다면
void repair(Tank t)
{
// Tank 수리
}
void repair(Dropship d)
{
// Dropship 수리
}
이런 식으로 수리가 가능한 유닛 수만큼 다른 버전의 오버로딩된 메서드를 정의해야 할 것이다.
현재 상속 관계에서는 이들의 공통점은 없다. 이 때 인터페이스를 이용하면 기존의 상속 체계를 유지하면서 이 들의 기계화 유닛에 공통점을 부여할 수 있다.
다음과 같이 Repairable 인터페이스를 정의하고 수리가 가능한 기계화 유닛에 이 인터페이스를 구현한다.
interface Repairable {}
class SCV extends GroundUnit implements Repairable
{
...
}
class Tank extends GroundUnit implements Repairable
{
...
}
class Dropship extends AirUnit implements Repairable
{
...
}
이제 이 3개의 클래스에는 같은 인터페이스를 구현했다는 공통점이 생겼다. 인터페이스 Repairable에 정의된 것은 아무것도 없고, 단지 인스턴스의 타입 체크에만 사용된다.
그리고 repair 메서드의 매개변수의 타입을 Repairable로 선언하면, 이 메서드의 매개변수로 Repairable 인터페이스를 구현한 클래스의 인스턴스만 받아들여진다.
void repair(Repairable r)
{
// 매개변수로 넘겨받은 유닛 수리한다
}
앞으로 새로운 클래스가 추가될 때, SCV의 repair 메서드에 의해서 수리가 가능하도록 하려면 Repairable 인터페이스를 구현하도록 하면 된다.
예제 2.
건물을 표현하는 클래스 Academy, Bunker, Barrack, Factory가 있고 이들의 parent인 Building 클래스가 있다.
이 때 Barrack 클래스와 Factory 클래스에 다음과 같은 내용의, 건물을 이동 시킬 수 있는 새로운 메서드를 추가한다면 어떻게 할까?
void lifeOff() { ... }
void move(int x, int y) { ... }
void stop() { ... }
void land() { ... }
Barrack 클래스와 Factory 클래스 모두 위의 코드를 적어주면 되긴 하지만, 코드가 중복되는 단점이 있다.
그렇다고 parent 클래스인 Building 클래스에 코드를 추가하면 Building 클래스의 다른 child인 Academy와 Bunker 클래스도 추가된 코드를 상속 받으므로 안된다.
이런 경우에도 인터페이스로 해결할 수 있다. 새로 추가하고자 하는 메서드를 정의하는 인터페이스를 정의하고 이를 구현하는 클래스 작성한다.
interface Liftable
{
void liftOff(); // 건물 들어올림 // public abstract 생략
void move(int x, int y); // 건물 이동
void stop(); // 건물 정지
void land(); // 건물 착륙
}
class LiftableImpl implements Liftable
{
public void liftOff() {...}
public void move(int x, int y) {...}
public void stop() {...}
public void land() {...}
}
작성된 인터페이스와 이를 구현한 클래스를 Barrack과 Factory 클래스에 적용한다.
Barrack 클래스가 Liftable 인터페이스를 구현하도록 하고, 인터페이스를 구현한 Liftable Impl 클래스를 Barrack 클래스에 포함시켜서 내부적으로 호출해서 사용한다.
이렇게 같은 내용의 코드를 각각 작성하지 않고 LiftableImpl 클래스 한 곳에서 관리할 수 있다.
class Barrack extends Building implements Liftable
{
LiftableImpl l = new LiftableImpl();
public void liftOff() { l.liftOff(); }
public void move(int x, int y) { l.move(x, y); }
public void stop() { l.stop(); }
public void land() { l.land(); }
...
}
class Factory extends Building implements Liftable
{
LiftableImpl l = new LiftableImpl();
public void liftOff() { l.liftOff(); }
public void move(int x, int y) { l.move(x, y); }
public void stop() { l.stop(); }
public void land() { l.land(); }
...
}
인터페이스 이해
- 클래스 User와 클래스 Provider가 있다.
- 메서드를 호출하는 User는 사용하려는 메서드의 선언부만 알면 된다. (내용은 몰라도 된다.)
methodB()
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(User)는 클래스 B(Provider)의인스턴스를 생성하고 메서드를 호출한다. 이 두 클래스는 직접적인 관계에 있다.
클래스 A를 작성하려면 클래스 B가 이미 작성되어 있어야 한다.
클래스 B의 methodB()의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.
이와 같이 직접적인 관계의 두 클래스는 provider가 변경되면 user도 변경되어야 하는 단점이 있다.
그러나 클래스 A가 클래스 B를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해 클래스 B의 메서드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 해도 클래스 A는 영향을 받지 않는다.
먼저 클래스 B에 정의된 메서드를 추상메서드로 정의하는 인터페이스 ONE을 정의한다.
interface ONE
{
public abstract void methodB();
}
그리고 클래스 B가 인터페이스 ONE을 구현하도록 한다.
class B implements ONE
{
public void methodB()
{
System.out.println("methodB in classB");
}
}
이제 클래스 A는 클래스 B 대신 인터페이스 ONE을 사용해 작성할 수 있다. A-B의 직접적인 관계에서 A-ONE-B의 간접적인 관계로 바뀌었다.
class A
{
public void methodA(ONE i)
{
i.methodB();
}
}
전체 코드
play in B class
play in C class
interface ONE
{
public abstract void play();
}
class A
{
void autoPlay(ONE i)
{
i.play();
}
}
class B implements ONE
{
public void play()
{
System.out.println("play in B class");
}
}
class C implements ONE
{
public void play()
{
System.out.println("play in C class");
}
}
class InterfaceTest
{
public static void main(String[] args) {
A a = new A();
a.autoPlay(new B());
a.autoPlay(new C());
}
}
method in B class
class B
interface One
{
public abstract void methodB();
}
class InstanceManager
{
public static One getInstance()
{
return new B();
}
}
class A
{
void methodA()
{
One i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString()); // i로 Object 클래스의 메서드 호출 가능
}
}
class B implements One
{
public void methodB()
{
System.out.println("method in B class");
}
public String toString()
{
return "class B";
}
}
class InterfaceTest
{
public static void main(String[] args) {
A a = new A();
a.methodA();
}
}
인스턴스를 직접 생성하지 않고, getInstance() 메서드 통해 제공 받는다. 이렇게 하면 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경 없이 getInstance()만 변경하면 되는 장점이 있다.
class InstanceManager
{
public static One getInstance()
{
return new B(); // 다른 인스턴스로 변경하려면 여기만 변경하면 됨
}
}