인터페이스는 왜 필요한가?

다음은 어떤 동물원 사육사가 하는 일이다.

난 동물원의 사육사이다.
육식동물이 들어오면 난 먹이를 던져준다.
호랑이가 오면 사과를 던져준다.
사자가 오면 바나나를 던져준다.

이런 케이스를 코드로 담아보자. 다음과 같이 Animal, Tiger, Lion, Zookeeper 클래스를 작성하자.

Sample.java

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Tiger extends Animal {
}

class Lion extends Animal {
}

class ZooKeeper {
    void feed(Tiger tiger) {  // 호랑이가 오면 사과를 던져 준다.
        System.out.println("feed apple");
    }

    void feed(Lion lion) {  // 사자가 오면 바나나를 던져준다.
        System.out.println("feed banana");
    }
}

public class Sample {
    public static void main(String[] args) {
        ZooKeeper zooKeeper = new ZooKeeper();
        Tiger tiger = new Tiger();
        Lion lion = new Lion();
        zooKeeper.feed(tiger);  // feed apple 출력
        zooKeeper.feed(lion);  // feed banana 출력
    }
}

이전 챕터에서 보았던 Dog 클래스와 마찬가지로 Animal을 상속한 Tiger와 Lion이 등장했다. 그리고 사육사 클래스인 ZooKeeper 클래스를 위처럼 정의 하였다. ZooKeeper 클래스는 호랑이가 왔을 때, 사자가 왔을 때 각각 다른 feed 메소드가 호출된다.

 

프로그램을 실행하면 다음과 같은 결과가 출력될 것이다.

feed apple
feed banana

자, 이제 다음을 생각 해 보자.

동물원에 호랑이와 사자뿐이라면 ZooKeeper 클래스는 더이상 할일이 없겠지만 악어, 표범등이 계속 추가된다면 ZooKeeper는 육식동물이 추가될 때마다 매번 다음과 같은 feed 메소드를 추가해야 한다.

아래 추가한 메소드는 실제로 코드에 적용하지 말고 눈으로만 보자.

(... 생략 ...)

class ZooKeeper {
    void feed(Tiger tiger) {
        System.out.println("feed apple");
    }

    void feed(Lion lion) {
        System.out.println("feed banana");
    }

    public void feed(Crocodile crocodile) {
        System.out.println("feed strawberry");
    }

    public void feed(Leopard leopard) {
        System.out.println("feed orange");
    }
}

(... 생략 ...)

이렇게 육식동물이 추가 될 때마다 feed 메소드를 추가해야 한다면 사육사(ZooKeeper)가 얼마나 귀찮겠는가? 이런 어려움을 극복하기 위해서는 인터페이스의 도움이 필요하다.

 

인터페이스 작성하기

다음과 같이 코드 상단에 육식동물(Predator) 인터페이스를 추가하자.

interface Predator {
}

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

(... 생략 ...)

위 코드와 같이 인터페이스는 class가 아닌 interface 라는 키워드를 이용하여 작성한다.

※ 인터페이스는 클래스와 마찬가지로 Predator.java와 같은 단독 파일로 저장하는 것이 일반적인 방법이다. 여기서는 설명의 편의를 위해 Sample.java 파일의 최상단에 작성하였다.

그리고 Tiger, Lion 클래스는 작성한 인터페이스를 구현하도록(Implements) 수정하자.

(... 생략 ...)

class Tiger extends Animal implements Predator {
}

class Lion extends Animal implements Predator {    
}

(... 생략 ...)

인터페이스 구현은 위와같이 implements 라는 키워드를 사용한다.

이렇게 Tiger, Lion 클래스가 Predator 인터페이스를 구현하게 되면 ZooKeeper 클래스의 feed 메소드를 다음과 같이 변경 할 수 있다.

변경전

(... 생략 ...)

class ZooKeeper {
    void feed(Tiger tiger) {
        System.out.println("feed apple");
    }

    void feed(Lion lion) {
        System.out.println("feed banana");
    }
}

(... 생략 ...)

변경후

(... 생략 ...)

class ZooKeeper {
    void feed(Predator predator) {
        System.out.println("feed apple");
    }
}

(... 생략 ...)

feed 메소드의 입력으로 Tiger, Lion을 각각 필요로 했지만 이제 이것을 Predator라는 인터페이스로 대체할 수 있게 되었다. tiger, lion은 각각 Tiger, Lion의 객체이기도 하지만 Predator 인터페이스의 객체이기도 하기 때문에 위와같이 Predator를 자료형의 타입으로 사용할 수 있는 것이다. 상속에서 공부했던 IS-A 관계가 인터페이스에도 마찬가지로 적용된다. "Tiger is a Predator", "Lion is a Predator"가 성립된다.

  • tiger - Tiger 클래스의 객체, Predator 인터페이스의 객체
  • lion - Lion 클래스의 객체, Predator 인터페이스의 객체

※ 이와같이 객체가 한 개 이상의 자료형 타입을 갖게되는 특성을 다형성(폴리모피즘)이라고 하는데 이것에 대해서는 "다형성" 챕터에서 자세히 다루도록 한다.

이제 어떤 육식동물이 추가되더라도 ZooKeeper는 feed 메소드를 추가할 필요가 없다. 다만 육식동물이 추가 될 때마다 다음과 같이 Predator 인터페이스를 구현한 클래스를 작성하기만 하면 되는 것이다.

class Crocodile extends Animal implements Predator {
}

※ Crocodile 클래스는 실제 코드에 적용하지 말고 눈으로만 보자.

눈치가 빠르다면 이제 왜 인터페이스가 필요한지 감을 잡았을 것이다. 보통 중요 클래스를 작성하는 입장이라면 (여기서는 ZooKeeper가 중요한 클래스이다) 클래스의 구현체와 상관없이 인터페이스를 기준으로 중요 클래스를 작성해야만 한다. 구현체(Tiger, Lion, Crocodile,...)는 늘어날수 있지만 인터페이스(Predator)는 하나이기 때문이다.

인터페이스의 메소드

자, 그런데 위 ZooKeeper 클래스에 약간의 문제가 발생했다. 아래의 ZooKeeper클래스의 feed 메소드를 보면 호랑이가 오던지, 사자가 오던지 무조건 "feed apple" 이라는 문자열을 출력한다. 사자가 오면 "feed banana" 를 출력해야 하지 않겠는가!

(... 생략 ...)

class ZooKeeper {
    public void feed(Predator predator) {
        System.out.println("feed apple");  // 항상 feed apple 만을 출력한다.
    }
}

(... 생략 ...)

이번에도 인터페이스의 마법을 부려보자.

Predator 인터페이스에 다음과 같은 메소드를 추가 해 보자.

interface Predator {
    String getFood();
}

(... 생략 ...)

getFood 라는 메소드를 추가했다. 그런데 좀 이상하다. 메소드에 몸통이 없다?

인터페이스의 메소드는 메소드의 이름과 입출력에 대한 정의만 있고 그 내용은 없다. 그 이유는 인터페이스는 규칙이기 때문이다. 위에서 설정한 getFood라는 메소드는 인터페이스를 implements한 클래스들이 구현해야만 하는 것이다.

인터페이스에 위처럼 메소드를 추가하면 Tiger, Lion 등의 Predator 인터페이스를 구현한 클래스들에서 컴파일 오류가 발생할 것이다. 오류를 해결하려면 다음처럼 Tiger, Lion 클래스에 getFood 메소드를 구현해야 한다.

 

(... 생략 ...)

class Tiger extends Animal implements Predator {
    public String getFood() {
        return "apple";
    }
}

class Lion extends Animal implements Predator {
    public String getFood() {
        return "banana";
    }
}

(... 생략 ...)

※ 인터페이스의 메소드는 항상 public으로 구현해야 한다.

Tiger, Lion 클래스의 getFood 메소드에 육식동물의 먹이인 "apple", "banana"를 각각 리턴하도록 작성했다. 이렇게 getFood 메소드를 추가하면 컴파일 오류가 해결될 것이다.

이제 ZooKeeper 클래스도 다음과 같이 변경이 가능하다.

(... 생략 ...)

class ZooKeeper {
    void feed(Predator predator) {
        System.out.println("feed "+predator.getFood());
    }
}

(... 생략 ...)

feed 메소드가 "feed apple" 을 출력하던 것에서 "feed "+predator.getFood()를 출력하도록 변경되었다. predator.getFood()를 호출하면 Predator 인터페이스를 구현한 구현체(Tiger, Lion)의 getFood() 메소드가 호출된다.

그리고 프로그램을 실행해 보자. 원하던 데로 다음과 같은 결과값이 출력되는 것을 확인할 수 있을 것이다.

feed apple
feed banana

05-07 인터페이스 - 점프 투 자바 (wikidocs.net)

'JAVA > 참고 개인공부' 카테고리의 다른 글

JVM 개념 및 구조  (0) 2022.12.09
핵클 협력사 세션.  (0) 2022.10.04

+ Recent posts