지금까지 인터페이스의 특징과 구현하는 방법, 장점 등 인터페이스에 대한 일반적인 사항들에 대해서 모두 살펴보았다. 하지만 `인터페이스란 도대체 무엇인가?`라는 의문은 여전히 남아있다. 이번에는 본질적인 측면에 대해 살펴보자.
먼저 인터페이스를 이해하기 위해서는 다음의 두 가지 사항을 반드시 염두에 두고 있어야 한다.
-클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
-메서드를 사용(호출)하는 쪽(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)의 선언과 구현을 분리해야 한다.
먼저 다음과 같이 클래스 B에 정의된 메서드를 추상메서드로 정의하는 인터페이스 I를 정의한다.
interface I{
public abstract void methodB();
}
// 그 다음에는 클래스 b가 인터페이스 I를 구현하도록 한다.
class B implements I{
public void methodB() {
System.out.println("methodB in B class");
}
}
//이제 클래스 A는 클래스 B대신 인터페이스 I를 사용할 수 있다.
class A{
public void methodA(I i){
i.methodB();
}
}
클래스 a를 작성하는데 있어서 클래스 B가 사용되지 않았다는 점에 주목하자. 이제 클래스 A와 클래스 B는 `A-B`의 직접적인 관계에서 `A-I-B`의 간접적인 관계로 바뀐 것이다.
결국 클래스 A는 여전히 클래스 B의 메서드를 호출하지만, 클래스 A는 인터페이스 I하고만 직접적인 관계에 있기 떄문에 클래스 B의 변경에 영향을 받지 않는다. 클래스 A는 인터페이스를 통해 실제로 사용하는 클래스의 이름을 몰라도 되고 심지어는 실제로 구현된 클래스가 존재하지 않아도 문제되지 않는다. 클래스 A는 오직 직접적인 관계에 있는 인터페이스 I의 영향만 받는다.
인터페이스 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 InterfaceTest2 {
public static void main(String[] args) {
A a = new A();
a.autoPlay(new B()); //void autoPlay(I i) 호출
a.autoPlay(new C()); //void autoPlay(I i) 호출
}
}
클래스 A가 인터페이스 I를 사용해서 작성되긴 하였지만, 이처럼 매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.
클래스 Thread의 생성자인 Thread(Runnable target)이 이런 방식으로 되어있다.
이처럼 매개변수를 통해 동적으로 제공받을 수도 있지만 다음과 같이 제3의 클래스를 통해서 제공받을 수 있다 .JDBC의 DriverManager클래스가 이런 방식으로 되어 있다.
class InterfaceTest3 {
public static void main(String[] args) {
A3 a = new A3();
a.methodA();
}
}
class A3 {
void methodA() {
I3 i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString()); // i로 Object클래스의 메서드 호출가능
//
}
}
interface I3 {
public abstract void methodB();
}
class B3 implements I3 {
public void methodB() {
System.out.println("methodB in B class");
}
public String toString() {
return "class B";
}
}
class InstanceManager {
public static I3 getInstance() {
return new B3();
}
}
인스턴스르 직접 생성하지 않고, getInstance()라는 메서드를 통해 제공받는다. 이렇게 하면, 나중에 다른 클래스의 인스턴스로 변경되어도 A클래스의 변경없이 getInstance()만 변경하면 된다는 장점이 있다.
그리고 인터페이스 I 타입의 참조변수 i로도 Object클래스에 정의된 메서드들을 호출할 수 있다는 것도 알아두자, i에 toStirng()이 정의되어 있지 않지만, 모든 객체는 Object클래스에 정의된 메서드를 가지고 있을 것이기 떄문에 허용하는 것이다.
class A3 {
void methodA() {
I3 i = InstanceManager.getInstance();
i.methodB();
System.out.println(i.toString()); // i로 Object클래스의 메서드 호출가능
//
}
}
디폴트 메서드와 static메서드
원래는 인터페이스에 추상 메서드만 선언할 수 있는데, 1.8부터 디폴트 메서드와 static메서드도 추가할 수 있게 되었다. static메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 떄문에 예전부터 인터페이스에 추가하지 못할 이유가 없었다. 그러나 자바를 보다 쉽게 배울 수 있도록 규칙을 단순화 할 필요가 있어서 인터페이스의 모든 메서드는 추상메서드 이어야 한다는 규칙에 예외를 두지 않았다 .덕분에 인터페이스와 관련된 static메서드는 별도의 클래스에 따로 두어야했다.
가장대표적인 것으로 java.util.Collection 인터페이스가 있는데, 이 인터페이스와 관련된 static메서드들이 인터페이스에는 추상 메서드만 선언할 수 있다는 원칙 때문에 별도의 클래스, Collections라는 클래스에 들어가게 되었다. 만일 인터페이스에 static 메서드를 추가할 수 있었다면, Collections클래스는 존재하지 않았을 것이다. 그리고 인터페이스의 static메서드 역시 접근 제어자가 항상 public이며, 생략할 수 있다.
디폴트 메서드
조상클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 보통 큰 일이 아니다. 인터페이스에 메서드를 추가한다느 것은, 추사 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야하기 때문이다.
인터페이스가 변경되 않으면 제일 좋겠지만, 아무리 설계를 잘해도 언젠가 변경은 발생하기 마련이다. JDK의 설계자들은 고심 끝에 디폴트 메서드(default method)라는 것을 고안해 내었다. 디폴트 메서드는 추상메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 떄문에 디폴트 메서드가 새로 추가되어도 해당 인터이스를 구현한 클래스를 변경하지 않아도 된다.
즉, 조상 클래스에 새로운 메서드를 추가한 것과 동일해 지는 것이다. 대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다.
1. 여러 인터페이스의 디폴트 메서드 간의 충돌
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 해야 한다.
2. 디폴트 메서드와 조상클래스의 메서드 간의 충돌
- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.
위의 규칙을 외우기 귀찮으면, 그냥 필요한 쪽의 메서드와 같은 내용으로 오버라이딩 해 버리면 그만이다.
class DefaultMethodTest {
public static void main(String[] args) {
ChildDe c = new ChildDe();
c.method1();
c.method2();
MyInterface.staticMethod();
MyInterface2.staticMethod();
}
}
class ChildDe extends Parent implements MyInterface, MyInterface2 {
public void method1() {
System.out.println("method1() in ChildDe"); // �������̵�
}
}
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 ChildDe
method2() in MyInterface
staticMethod() in MyInterface
staticMethod() in MyInterface2
'자바' 카테고리의 다른 글
| 예외처리 1 (0) | 2023.01.13 |
|---|---|
| 내부 클래스 (0) | 2023.01.12 |
| 인터페이스 2 (0) | 2023.01.11 |
| 인터페이스 (0) | 2023.01.11 |
| 생성자(Constructor) (0) | 2022.10.04 |
