[Java] 인터페이스

황세호·2021년 5월 16일
0

Java

목록 보기
4/6

인터페이스


인터페이스란?

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

추상클래스를 미완성 설계도라고 한다면, 인터페이스는 밑그림만 그려져 있는 기본 설계라고 할 수 있다.

인터페이스의 장점


  • 개발 시간을 단축시킬 수 있다.
  • 표준화가 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.

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

모든 유닛들의 최고 조상은 Unit 클래스이고, 유닛의 종류는 지상유닛과 공중유닛으로 나누어 진다.
이 때, SCV에게 Tank와 Dropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair 메서드를 정의한다면 다음과 같을 것이다.

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

이런 식으로 수리가 가능한 유닛의 개수만큼 다른 버전의 오버로딩된 메서드를 정의해야 할 것이다. 이것을 피하기 위해 매개변수의 타입을 이들의 공통 조상으로 하면 좋겠지만 Dropship은 공통조상이 다르기 때문에 해결되지 않는다.

또한 GroundUnit의 자손클래스 중에는 Marine과 같이 기계화 유닛이 아닌 클래스도 포함될 수 있기 때문에 부적합하다.
이렇게 상속관계에서 공통점이 안보일 때, 인터페이스를 이용하면 기존의 상속체계를 유지하면서 이들 기계회 유닛에 공통점을 부여할 수 있다.

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

interface Repairable() { }

class SCV extends GroundUnit implements Repairable { ... }

class Tank extends GroundUnit implements Repairable { ... }

class Dropship extends AirUnit implements Repairable { ... }

이렇게 Repairable 인터페이스를 구현하게되면,

  • Unit 별로 오버로딩된 메서드를 정의할 필요가 없다.
  • Repairable 타입의 매개변수를 선언한다.
  • 앞으로 repair가 필요한 새로운 클래스가 생성될 때마다 새로운 메서드를 정의할 필요없이 인터페이스를 구현하도록 하면 된다.
void repair (Repairable r) {
	...
}

인터페이스의 작성


인터페이스를 작성하는 것은 클래스를 작성하는 것과 같지만 키워드로 class 대신 interface를 사용하는 것만 다르다.

interface 인터페이스이름 {
	public static final {타입} {상수이름} =;
	public abstract {메서드이름}();
}

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

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. (단, static메서드와 디폴트 메서드는 예외(JDK1.8부터))

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

인터페이스의 상속


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

interface Movable {
	void move(int x, int y);
}

interface Attackable {
	void attack(Unit u);
}

interface Fightable extends Movable, Attackable { }

Fightable 자체에는 정의된 멤버가 하나도 없지만 조상 인터페이스로부터 상속받은 두 개의 추상메서드를 멤버로 갖게 된다.

인터페이스의 구현


class 클래스이름 implements 인터페이스이름 {
}

class Fighter implements Fightable {
	public void move(int x, int y) { }
	public void attack(Unit u) { }
}

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

abstract class Fighter implements Fightable {
	public void move(int x, int y) { }
}

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


인터페이스는 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 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;
	}
}
/* VCR에 대한 인터페이스 IVCR */
public interface IVCR {
	public void play();
	public void stop();
	public void reset();
	public int getCounter();
	public void setCounter(int c);
}

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

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);
	}
}

사실 인터페이스를 새로 작성하지 않고도 VCR클래스를 TVC클래스에 포함시키는 것만으로도 충분하지만, 인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점이 있다.

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


인터페이스는 이를 구현한 클래스의 조상이라고 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.

Fightable f = (Fightable)new Fighter();
Fightable f = new Fighter();

다음과 같이 메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것이 가능하다.

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

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

인터페이스의 이해


지금까지 인터페이스의 특징과 구현 방법, 장점 등에 대해 알아보았다. 하지만 ‘인터페이스란 도대체 무엇인가?’라는 의문은 여전히 남아있을 것이다. 이번에는 인터페이스에 대한 본질적인 측면에 대해 살펴보도록 하겠다.

먼저 인터페이스를 이해하기 위해서는 다음의 두 가지 사항을 반드시 염두에 두고 있어야 한다.

  • 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
  • 메서드를 사용하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다. (내용은 몰라도 된다.)
class A {
	public void methodA (B b) {
		b.methodB();
	}
}

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

위와 같이 A,B가 존재하면, 서로 직접적인 관계에 있다고 한다. (‘A-B’ 관계)
이 경우 클래스 A를 작성하기 위해서는 클래스 B가 이미 완성되어 있어야 하며, 클래스 B의 methodB()의 선언부가 변경되면 A 부분의 코드를 수정해야 하는 번거로움이 생긴다.

이 때, 인터페이스를 매개체로 해서 클래스 A가 인터페이스를 통해서 클래스 B의 메서드에 접근하도록 하면 클래스 B에 변경사항이 생겨도 클래스 A는 전혀 영향을 받지 않는다.

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();
	}
}

클래스 A는 여전히 클래스 B의 메서드를 호출하지만 ‘A-B’가 아닌 ‘A-I’의 관계를 가지므로 클래스 B의 변경에 영향을 받지 않게된다. 이로써, 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다.

class A{
	void autoPlay(I i) {
		i.play();
	}
}

interface I {
	public abstract void play();
}

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

class C implements I {
	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());
	}
 }
/* 실행결과 */
// play in B class
// play in C class

클래스 A가 인터페이스 I를 이용해 작성되긴 했지만, 이처럼 매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.
클래스 Thread의 생성자인 Thread(Runnable target)이 이런 방식이다.
(Runnable이 인터페이스)

이처럼 매개변수를 통해 동적으로 제공받는 방법 외에도, 다음과 같이 제 3의 클래스를 통해서 제공받을 수도 있다. JDBC의 DriverManager 클래스가 이런 방식으로 되어 있다.

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

class A{
	void methodA() {
		I i = InstanceManager.getInstance();
		i.methodB();
		System.out.println(i.toString()); // 모든 객체는 Object클래스에 정의된 메서드를 가지고 있기에 사용 가능
	}
}

interface I {
	public abstract void methodB();
}

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

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

인스턴스를 직접 생성받지 않고, getInstance()라는 메서드를 통해 제공받는다. 이렇게하면, 나중에 다른 클래스의 인스턴스로 변경되어도 A 클래스의 변경없이 getInstance()만 변경하면 된다는 장점이 생긴다.

profile
Developer

0개의 댓글