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

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

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

인터페이스도 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다.

 

 

인터페이스의 작성

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

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

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

- 모든 멤버변수는 bublic static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract이어야 하며, 이를 생략할 수 있다.
단, static메서드와 default메서드는 예외

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

 

 

 

인터페이스의 상속

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

클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

 

 

인터페이스의 구현

인터페이스도 추상 클래스처럼 그 자체로는 인스턴스를 생성할 수 어 ㅄ으며, 추상 클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야하는데, 그 방법은 추상클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다.

다만 클래스는 확장한다는 의미의 키워드 extends를 사용하지만 인터페이스는 구현한다는 의미의 키워드 implements를 사용할 뿐이다.

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

또한 인터페이스는 상속과 구현을 동시에 할 수 있다(다중 상속이 가능하므로)

 

인터페이스의 이름에는 주로 able로 끝나는 것들이 많은데, 그 이유는 어떠한 기능 또는 행위를 하는데 필요한 메서드를 제공한다는 의미를 강조하기 위해서이다. 또한 그 인터페이스를 구현한 클래스는 ~를 할 수 있는 능력을 갖추었다는 의미이기도 하다.

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

오버 라이딩할 때는 조상의 메서드보다 넓은 범위의 접근 제어자를 지정해야 한다. 이는 인터페이스도 마찬가지로 적용된다.

 

 

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

자바는 일반적으로 다중 상속을 허용하지 않는다. 인터페이스가 이와 다르게 다중상속이 가능해 다중상속을 위한 것으로 오해를 사게 된다.

인터페이스는 static상수만 정의할 수 있으므로 조상 클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 추상메서드는 구현내용이 없으므로 조상클래스의 메서드와 선언부가 일치하는 경우에는 조상 클래스 쪽의 메서드를 상속받으면 되므로 문제 되지 않는다.

그러나, 이렇게 하면 상속받는 멤버의 충돌을 피할 수 있지만, 다중 상속의 장점을 잃게 된다. 만약 두 클래스로부터 상속을 받아야 할 상황이라면, 두 조상 클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 표함 시키는 방법으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.

 

 

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

자손 클래스의 인스턴스를 조상 타입의 참조 변수로 참조하는것이 가능하다. 인터페이스 역시 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형 변환도 가능하다. 인터페이스는 매서드의 매개변수의 타입으로 사용될 수 있다.

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

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

 

 

인터페이스의 장점

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

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

인터페이스가 작성되면 메서드를 호출하는 쪽에서 메서드의 내용과 관계없이 선언 부만 알면 프로그램을 작성하는 것이 가능하다.

그리고 동시에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하게 하면, 인터페이스를 구현하는 클래스가 작성될 때까지 기다리지 않고도 양쪽에서 동시에 개발을 진행할 수 있다.

 

2. 표준화가 가능하다.

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

 

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

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

 

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

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

 

 

 

인터페이스의 이해

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

- 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
- 메서드를 사용(호출)하는 쪽(User)에서는 사용하려는 메서드(Provider)의 선언부만 알면 된다.

 

public class InstanceofTest {
	public static void main(String[] args) {
		FireEngine fe = new FireEngine();
		
		if(fe instanceof FireEngine) {
			System.out.println("This is a FireEngine instance.");
		}
		
		if(fe instanceof Car) {
			System.out.println("This is a Car instance.");
		}
		if(fe instanceof Object) {
			System.out.println("This is an Object instance.");
		}
		System.out.println(fe.getClass().getName());
	}

} // class
class Car{}
class FireEngine extends Car{}

이 경우 클래스 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");
	}
}

이제 클래스 A는 클래스 B 대신 인터페이스 I를 사용해서 작성할 수 있다.

class A {
	public void methodA(B b) {
		b.method();
	}
}
class A {
	public void methodA(I i)
		i.method();
	}
}

클래스 A를 작성하는 데 있어서 클래스 B가 사용되지 않았다는 점에 주목해서 보면 클래스 A와 클래스 B는 A-B의 직접적인 관계에서 A-I-B의 간접적인 관계로 바뀌었다.

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

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

 

 

 

디폴트 메서드와 static메서드

static메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 인터페이스에 추가가 가능하지만 자바의 규칙을 단순화하기 위해 인터페이스의 모든 메서드는 추상 메서드여야 한다는 규칙에 예외를 두지 않았다. 덕분에 인터페이스와 관련된 static메서드는 별도의 클래스에 따로 두어야 했다.

 

디폴트 메서드 

조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 큰 일이다. 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 구현한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.

이에 default method라는 것을 고안해서 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

default 메서드는 앞에 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public이며, 생략 가능하다.

인터페이스 메서드에 디폴트 메서드를 추가하면, 조상 클래스에 새로운 메서드를 추가한 것과 동일해진다. 그러나 새로 추가된 디폴트 메서드가 기존 메서드와 이름이 중복되어 충돌하는 겨우가 발생하는데, 이 충돌을 해결하는 규칙은 다음과 같다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
2. 디폴트 메서드와 조상 클래스 메서드 간의 충돌
- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다(사용하지 않도록 주의하자)

 

public class DefaultMethodTest {
	public static void main(String[] args) {
		Child c = new Child();
		c.method1();
		c.method2();
		MyInterface.staticMethod();
		MyInterface2.staticMethod();
	}
}

class Child extends Parent implements MyInterface, MyInterface2 {
	public void method1() {
		System.out.println("method1() in Child"); // 오버라이딩
	}
}

class Parent {
	public void method2() {
		System.out.println("method2() in Parent");
	}
}

interface MyInterface {
	default void method1() {
		System.out.println("method1() in MyInterface");
	}

	default void method2() {
		System.out.println("method2() in MyInterface");
	}

	static void staticMethod() {
		System.out.println("staticMethod() in MyInterface");
	}
}

interface MyInterface2 {
	default void method1() {
		System.out.println("method1() in MyInterface2");
	}

	static void staticMethod() {
		System.out.println("staticMethod() in MyInterface2");
	}
}
method1() in Child
method2() in Parent
staticMethod() in MyInterface
staticMethod() in MyInterface2

 

+ Recent posts