본문 바로가기
✏️Java 공부/Java의 정석

[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 6 (인터페이스)

by 코코의 주인 2022. 7. 8.

인터페이스

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

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


인터페이스의 작성

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

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

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

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

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

예제

interface PlayingCard {
    public static final int SPADE = 4;
    final int DIAMOND = 3;	//public static final DIAMOND = 3;
    static int HEART = 2;	//public static final HEART = 2;
    int CLOVER = 1;			//public static final CLOVER = 1;
    
    public abstract String getCardNumber();
    String getCardKind();	//public abstract String getCardKind();
}

인터페이스의 상속

 인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속받는 것이 가능하다. 클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

 

예제

interface Mammal {	//포유류
    void fiding() {
        /*젖을 먹이는 메서드*/
    }
}

interface Bird {
	void eggLying() {
    	/*알을 낳는 메서드*/
    }
}

interface Platypus extends Mammal, Bird {
	/*알을 낳는 포유류 오리너구리*/
}

인터페이스의 구현

 인터페이스도 추상클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상 클래스가 상속을 통해 추상 메서드를 완성하는 것처럼 인터페이스도 자신에게 정의된 추상 메서드의 몸통을 만들어주는 클래스를 작성해야 한다. 그 방법은 추상 클래스가 자신을 상속받는 클래스를 정의하는 것과 다르지 않다. 다만 클래스와 달리 인터페이스는 구현한다는 의미의 키워드 'implements'를 사용한다.

class duckRacoon implements Platypus {
    public void piding() {
    	/*구현 생략*/
    }
    public void eggLying() {
    	/*구현 생략*/
    }
}

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


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

 자바에서는 다중 상속을 허용하고 있지 않다. 자바에서 다중상속을 허용하지 않는다는 것이 단점으로 부각되는 것에 대한 대응으로 '자바도 인터페이스를 이용하면 다중상속이 가능하다.'라고 하는 것일 뿐 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다. 때문에 인터페이스를 이용한 다중 상속에 대한 내용은 가볍게 이런 게 있다는 것만 알고 넘어가도 충분하다.


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

 다형성에 대해 학습할 때 자손 클래스의 인스턴스를 조상 타입의 참조 변수로 참조하는 것이 가능하다고 배웠다.

 인터페이스 역시 이를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형 변환도 가능하다.

예제

Platypus p = (Platypus)new duckRacoon;
또는
Platypus p = new duckRacoon;

 

인터페이스는 다음과 같이 메서드의 매개변수 타입으로도 사용될 수 있다.

void attck(Platypus p) {
    /*가라 오리너구리 몸통박치기*/
}

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

 

 그리고 메서드의 리턴 타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

Platypus method() {
    duckRacoon d = new duckRacoon();
    return d;
}

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


인터페이스의 장점

 인터페이스를 사용하는 이유와 그 장점을 정리해보면 다음과 같다.

 

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

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

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

 

2. 표준화가 가능하다.

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

 

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

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

 

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

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


인터페이스의 이해

 이제 대체 왜 인터페이스를 사용해야 하는지에 대해 알아보자.

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

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

예제

class User {
    public void methodU(Provider p) {
        p.methodP();
    }
}
class Provider {
    public void methodP() {
        System.out.println("Provider의 메서드");
    }
}

public class InterfaceTest {
    public static void main(String args[]) {
        User u = new User();
        u.methodU(new Provider());
    }
}

 클래스 User는 클래스 Provider의 인스턴스를 생성하고 메서드를 호출한다. 이 두 클래스를 서로 직접적인 관계에 있음을 볼 수 있다.

 이 경우 클래스 User를 작성하려면 클래스 Provider가 이미 작성되어 있어야 한다. 그리고 클래스 Provider의 methodP()의 선언부가 변경되면, 이를 사용하는 클래스 User의 내용도 변경되어야 한다.

 이와 같이 직접적인 관계의 두 클래스는 한 쪽(Provider)이 변경되면 다른 한 쪽(User)도 변경되어야 한다는 단점이 있다.

 

그러나 클래스 User가 클래스 Provider를 직접 호출하지 않고 인터페이스를 매개체로 해서 클래스 User가 인터페이스를 통해서 클래스 Provider의 메서드에 접근하도록 하면, 클래스 Provider에 변경사항이 생기거나 클래스 Provider가 같은 가능의 다른 클래스로 대체 되어도 클래스 User는 전혀 영향을 받지 않도록 하는 것이 가능하다.

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

 

먼저 클래스 Provider에 정의된 메서드를 추상메서드로 정의하는 인터페이스 I를 정의한다.

interface I {
    public abstract void methodP();
}

그 다음 클래스 Provider가 인터페이스 I를 구현하도록 한다.

class Provider implements I{
    public void methodP() {
        System.out.println("Provider 클래스의 메서드");
    }
}

이제 클래스 User는 클래스 Provider가 아닌 인터페이스 I를 사용해서 작성할 수 있다.

class User {
    public void methodU(I i) {
        i.methodP();
    }
}

 

 클래스 User를 작성하는데 있어서 클래스 Provider가 사용되지 않았다. 이제 클래스 User와 클래스 Provider는 'User-Provider'의 직접적인 관계에서 'User-I-Provider'의 간접적인 관계로 바뀐 것이다.

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

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


디폴트 메서드와 static 메서드

  인스턴스 메서드에는 추상 메서드만 선언할 수 있었지만 JDK 1.8부터는 디폴트 메서드와 static 메서드도 추가할 수 있게 되었다. static 메서드는 인스턴스와 관계가 없는 독립적인 메서드이기 때문에 인터페이스에 추가할 수 있게 한 것이다.

 

디폴트 메서드

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

 인터페이스가 변경되지 않으면 제일 좋겠지만 아무리 설계를 잘해도 언젠가 변경은 발생하기 마련이다. 때문에 디폴트 메서드(default method)라는 것이 고안되었다. 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

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

interface Myinterface {
    void method();
    default void newMethod();	//추가된 메서드를 디폴트 메서드로 선언
}

위와 같이 newMethod()라는 추상 메서드를 추가하는 대신 디폴트 메서드를 추가하면 기존의 Myinterface를 구현한 클래스를 변경하지 않아도 된다.

 대신, 새로 추가된 디폴트 메서드가 기존의 메서드와 이름이 중복되어 충돌하는 경우가 발생한다. 이 경우 아래의 규칙을 통해 해결할 수 있다.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
- 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩 해야 한다.

2. 디폴트 메서드와 조상 클래스 메서드 간의 충돌
- 조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

총평

 아 이제 객체지향 프로그래밍 파트에서 벗어나서 빨리 다른 부분을 하고싶다.

 애초에 이 책을 선택한 이유가 객체지향 프로그래밍 설명이 자세하게 되어있는 거 같아서 선택한 것이니 딱히 투덜댈 건 없는 거 같다...

 객체지향 프로그래밍이랑 자바에 대해 공부하고 싶은 분은 자바의 정석 추천한다.

댓글