본문 바로가기
Java/Basic

인터페이스(interface)

by 최로이 2021. 4. 15.

1. 인터페이스(interface)

인터페이스는 일종의 추상 클래스다. 인터페이스는 추상클래스처럼 추상메소드를 지니고 있지만 추상클래스보다 추상화의 정도가 높아 추상클래스와는 달리 구현부를 갖춘 메소드나 멤버변수를 가질 수 없다.

오직 추상메소드와 상수만을 멤버로 가질 수 있다(JDK1.8에서부터 기본 default 메소드와 static 메소드도 지닐 수 있게 되었다) 이러한 인터페이스를 보면 '미완성'된 형태인 것을 알 수 있는데, 일반 클래스가 '설계도'라면 인터페이스는 '미완성 설계도'라고 할 수 있다. 때문에 인터페이스 자체만으로 사용되는 것이 아니라 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다. 쉽게 말해 인터페이스는 추상메소드의 모임.

 

1.1 인터페이스와 추상클래스의 차이

추상클래스: 추상 클래스는 말 그대로 클래스이기 때문에 추상 메소드를 제외한 나머지를 모두 구현할 수 있다.

인터페이스: 인터페이스는 상수, 추상메소드를 제외한 그 어떤 것도 가질 수 없으며, 구현된 것이 아무것도 없다.

 

1.2 인터페이스의 선언

interface 인터페이스명 {
  public static final 타입 상수명 = '값';
  public abstract 메소드명(매개변수 목록);
}

인터페이스를 사용할 땐 몇가지 제약사항이 있다.

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

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

③ JDK1.8에서부터 static 메소드와 default 메소드는 예외로 둔다.

④ 인터페이스의 모든 접근제어자는 무조건 public이다.

 

1.3 인터페이스의 상속

인터페이스는 인터페이스로부터만 상속 받을 수 있다. 일반적인 클래스와는 다르기 때문에 클래스들의 최고 조상인 Object 클래스를 최고조상으로 여기지 않으며, 다중상속이 가능하다.

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

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

//인터페이스 Fightable이 두 개의 인터페이스Movable, Attackable를 상속받음
interface Fightable extends Movable, Attackable {}

 

1.4 인터페이스의 구현

인터페이스도 추상클래스와 마찬가지로 인스턴스를 생성할 수 없다. 추상클래스가 상속을 통해 추상메소드를 완성하는 것처럼, 인터페이스도 자신에게 정의된 추상메소드의 구현부를 만들어주는 클래스를 작성해야 한다. 방법은 추상클래스가 자신을 상속 받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스는 확장한다는 의미의 'extends'를 사용하지만 인터페이스는 구현한다는 의미의 'implements'를 사용한다.

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

public class Fighter implements Fightable {
  public void move(int x, int y) { /*내용 생략*/}
  public void attack(Unit u) { /*내용 생략*/
}

만약 구현하는 인터페이스의 메소드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다. 즉, 인터페이스를 상속 받아 여러 메소드들 중에서 하나만 구현하더라도 클래스 앞에 abstract를 붙여야 한다는 의미다.

인터페이스명에는 주로 '~을(를) 할 수 있는'의 의미인 able로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메소드를 제공한다는 의미를 강조하기 위해서다. 또한 그 인터페이스를 구현한 클래스는 '~를 할 수 있는' 능력을 갖추었다는 의미이기도 하다. 이름이 able로 끝나는 것은 인터페이스라고 추측할 수 있지만 그렇다고 모든 인터페이스가 반드시 able로 끝나는 건 아니다.

 

예제)

package basic;

public 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 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클래스로부터 상속 받는다.

② Fighter클래스는 Fightable 인터페이스만을 구현했다

③ Unit클래스는 Object클래스의 자식이다.

④ Fightable인터페이스는 Movable, Attackable인터페이스의 자식이다.

⑤ 결론적으로 Fighter클래스는 이 모든 클래스와 인터페이스의 자식이 된다.

 

위 예제에서 주의깊게 봐야할 점은 구현한 인터페이스의 접근제어자를 public으로 지정했다는 것이다. 앞서 정리한 내용을 보면 원래 오버라이딩(메소드변경)을 할 때에는 부모 메소드보다 넓은 범위의 접근제어자를 지정해야 하는데 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으로 해야 하는 것이다. → 인터페이스를 상속하는 건 무조건 public!

 

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

인터페이스를 이용한 다형성의 특징은 세 가지 정도로 분류할 수 있다.

 

1) 인터페이스도 구현클래스의 부모다.

다형성은 부모의 참조변수에 자식클래스의 인스턴스를 참조하는 것이 가능하다(다형성의 특징) 인터페이스 역시 이를 구현한 클래스의 부모(조상)라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로 형변환도 가능하다. 위 예제를 보면 Fightable을 클래스 Fighter가 구현했을 때, 다음과 같이 Fighter인스턴스를 Fightable타입의 참조변수로 참조하는 것이 가능하다.

Fightable f = (Fightable)new Fighter(); 또는 Fightable f = new Fighter();
→ 이때 Fightable타입의 참조변수로는 인터페이스 Fightable에 정의된 멤버들만 호출이 가능하다.

 

2) 인터페이스 타입의 매개변수는 인터페이스를 구현한 클래스의 객체만 가능하다.

인터페이스 타입의 매개변수가 갖는 의미는 메소드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로  제공해야 한다는 것이다. 그래서 attack메소드를 호출할 때는 매개변수로 Fightable인터페이스를 구현한 클래스의 인스턴스를 넘겨주어야 한다. 위 코드로 하자면 attack(new Fighter())과 같이 사용할 수 있다는 의미다.

 

3) 인터페이스를 메소드의 리턴타입으로 지정할 수 있으며, 타입이 일치하지 않으면 형변환을 한다. 즉 해당 인터페이스를 구현한 클래스를 반환하겠다는 의미가 된다.

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

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

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

위 문장의 구현부를 한 문장으로 바꾸면 다음과 같다.

return new Fighter();

 

1.6 인터페이스의 장점

1) 두 대상(객체)간의 연결, 대화, 소통을 돕는 중간역할을 한다.

만약 사람이 기계를 직접 다룬다면? 당연히 어렵다. 여러 이유가 있겠지만 가장 큰 이유는 사람과 기계와의 언어가 전혀 다르기 때문에. 그 과정을 수월하게 하기 위해 기계와 사람 간에 의사소통을 도와 쉽게 조작할 수 있도록 조정해주는 역할을 하는 매개체가 있다면 쉽게 사용할 수 있지 않을까? 그 역할을 하는 것이 인터페이스다. 예를 들어 사람과 컴퓨터가 윈도우를 통해 쉽게 명령을 전달할 수 있는 것처럼.

 

2) 개발시간을 단축할 수 있다.

인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 메소드를 호출하는 쪽에서는 메소드의 내용에 관계없이 선언부만 알면 되기 때문이다. 그리고 동시에 다른 한 쪽에서는 인터페이스를 구현하는 클래스를 작성하도록 하여, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.

 

3) 표준화가 가능하다 → ex) JDBC(인터페이스 집합)

프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 인터페이스를 구현하여 프로그램을 작성하도록 함으로써 보다 일관되고 정형화된 프로그램의 개발이 가능하다.

 

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

인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

 

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

서로 상속관계에 있지 않고, 같은 부모(조상)클래스를 두지 않은, 서로 아무런 관계도 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다. 

 

1.7 인터페이스의 이해

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

클래스 A와 클래스 B가 있다고 했을 때, 클래스 A(User)는 클래스B(Provider)의 인스턴스를 생성하고 메소드를 호출한다. 이 두 클래스는 서로 직접적인 관계에 있다. 이것을 간단하게 A-B라고 표현하자. 이 경우에는 클래스A를 작성하기 위해서는 클래스B가 이미 작성되어 있어야 한다. 그리고 클래스B의 메소드의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.

클래스A와 B의 관계

2) 메소드를 사용(호출)하는 쪽(User)에서는 사용하려는 메소드의(Provider)의 선언부만 알면 된다(내용은 몰라도 됨)

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

두 클래스간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 이용해 클래스의 선언과 구현을 분리해야 한다. 코드로 표현하면 다음과 같다.

//1.인터페이스 i
interface i {
	public abstract void methodB();
}

//2.인터페이스의 추상메소드를 구현한 클래스 B
class B implements I {
	public void methodB(){
    	system.out.println("methodB in B class");
    }
}

//3.클래스B로 직접 접근하는 방식
class A {
	public void methodA(B b) {
    	b.methodB();
    }
}

//4.인터페이스 i를 통해 접근하는 방식
class A {
	public void methodA(I i) {
    	i.methodB();
    }
}

 

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

위 코드중 세번째 방식

위 그림은 클래스A는 여전히 클래스B의 메소드를 호출하지만, 클래스A는 인터페이스 I하고만 직접적인 관계에 있기 때문에 클래스B의 변경에 영향을 받지 않는다.

인터페이스 I를 통한 접근방식

클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고, 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다. 클래스A는 오직 직접적인 관계에 있는 인터페이스 I의 영향만 받는다.

인터페이스 I는 실제 구현 내용(클래스B)을 감싸고 있는 껍데기이며, 클래스A는 껍데기 안에 어떤 알맹이(클래스)가 들어 있는지 몰라도 된다. 

댓글