상세 컨텐츠

본문 제목

6. 협업의 시작 인터페이스 (Java_Interface)

How To Java/Java Tutorial

by 카페코더 2020. 3. 22. 17:50

본문

반응형

취업 준비를 하며 복습한다는 마음으로, 이것을 보는 누군가에게 
도움이 되었으면 하는 마음으로 Java-Tutoral을 작성해 봅니다. 
입문서와 순서가 잘못되었을 수 있고, 제가 아는 정보가 틀렸을 수 있습니다.
이 글에 대한 잘못된 정보나, 오탈자 등 수정해야 할 항목 혹은 
추가해야 할 항목은 댓글로 알려주시면 감사하겠습니다.

이 자료가 올라가는 저장소 : https://github.com/hwk0911/Java-tutorial

 

hwk0911/Java-tutorial

Java tutorial. Contribute to hwk0911/Java-tutorial development by creating an account on GitHub.

github.com

드디어 올 것이 왔다. 인터페이스.

필자는 Oracle Java8을 통해 Java programming을 공부했다.
시간이 오래 걸린 챕터가 몇 있는데, 바로 여기 인터페이스가 가장 오래 걸렸다.

이해하긴 어렵지만, 그렇다고 뛰어넘기에는 너무나 중요한 인터페이스.
이번 포스팅에서 다뤄보려 한다.

뭐... 이것도 인터페이스긴 하니까...

바로 전 강의 5. 상속 _ 물려받아 사용하자에서 간단하게 추상 클래스와, 인터페이스 에대 다뤘다.

이번 포스팅에서는 인터페이스에 대해 조금 더 다뤄보려 한다.

1. 인터페이스

우선 인터페이스의 정의부터 알아보자.

Java의 인터페이스는 객체의 사용방법을 정의한다.
이것을 세세히 분석해보면, 객체는 클래스로부터 나오고, 객체는 클래스의 메서드를 통해 작동한다.
즉 객체가 사용할 메서드들을 정의한 일종의 청사진의 개념으로 알아두면 좋다.

어떤 참고서에 보면, 인터페이스는 Java8부터 지원하는 람다식 때문에 중요성이 높아졌다 한다.
람다식은 함수적 인터페이스의 구현 객체를 생성하기 때문이라 한다.

하지만 필자가 생각하는 인터페이스의 가장 큰 역할은 객체지향 개발 5원칙 중 하나인,
OCP(Open-Close-Principle, 개방 폐쇄의 원칙)을 수행하는 도구라는 것이다.

OCP에 대해 간단하게 설명하면, 개방 폐쇄의 원칙으로,
수정에는 닫혀있고, 확장에는 열려있는 형태의 객체지향 프로그래밍을 말한다.

위에 인터페이스는 객체가 사용할 메서드들을 정의한 청사진이라 서술했다.
인터페이스를 상속받는 클래스는 인터페이스에서 제시한 함수를 모두 구현해야 한다.
따라서 수정에는 막혀있다 볼 수 있다.

하지만 인터페이스를 상속받는 클래스는 인터페이스의 메서드만을 구현해야 하는 것이 아닌,
구현된 메서드들을 다룰 메서드를 추가로 구현할 수 있다. 즉 확장이 가능한 것이다.

List 인터페이스를 통해 ArrayList, Vector 등 많은 클래스들이 나오는 것 같은 개념이다.

2. 인터페이스 사용하기

백문이 불여일견, 우선 직접 사용해보고 뭔지 감을 잡아보도록 하자.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

위와 같이 아주 간단한 인터페이스를 선언해보자.

FishBread라는 인터페이스를 선언하고, base, jam, name의 메서드를 명시해주자.
이러면 인터페이스의 선언이 끝이 난다. 시작이 반이니 벌써 반이나 했다.

그렇다면 이 인터페이스를 어떻게 활용하는지 알아보자.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

class ChouCreamFishBread implements FishBread{

}

인터페이를 선언했고, 그것을 구현하는 ChouCreamFishbread 클래스를 선언해주자.

전에 Interface는 final, abstract와 함께 3대 규약이라 서술했었다.

따라서 우리는 Interface에 명시한 모든 메서드를 ChouCreamFishBread에 구현해야 한다.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

class ChouCreamFishBread implements FishBread{
    String base;
    String jam;
    String name;

    ChouCreamFishBread() {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "chouCream";
    }

    @Override
    public void name() {
        this.name = "슈크림 붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

이렇게 모두 구현했고, 정보 확인을 위한 Obejct클래스의 toString()을 오버라이드 하여 재정의 한다.

이렇게만 해도 인터페이스의 활용은 충분히 되었다 생각한다.
그렇다면 슈크림 붕어빵의 객체를 생성하여 toString() 메서드를 통해 결과를 확인해보자.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

class ChouCreamFishBread implements FishBread{
    String base;
    String jam;
    String name;

    ChouCreamFishBread() {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "chouCream";
    }

    @Override
    public void name() {
        this.name = "슈크림 붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

class Main {
    public static void main(String[] args) {
        ChouCreamFishBread chouCreamFishBread = new ChouCreamFishBread();
        System.out.println(chouCreamFishBread.toString());
    }
}

/*
결과 : 

이름 : 슈크림 붕어빵
베이스 : 마가린 + 밀가루
내용물 : chouCream

Process finished with exit code 0
 */

슈크림 붕어빵의 객체를 생성하고, 객체의 정보를 출력했다.

결과는 아주 확실하게 잘 나왔다. 이렇게 인터페이스를 사용하는 기초를 알아봤다.

3. 인터페이스의 다형성

갑작스럽게 다형성이 나와 당황했을 것이다. 아니라면 당신은 이미 Java 고인 물.

다형성은 말 그대로 여러 형태를 갖는단 얘기다.

간단하게 설명해, 인터페이스의 객체를 생성하여, 인터페이스를 구현한 클래스로 사용할 수 있다.

이해를 돕기 위해 다음 코드를 살펴보자.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

class ChouCreamFishBread implements FishBread{
    String base;
    String jam;
    String name;

    ChouCreamFishBread() {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "chouCream";
    }

    @Override
    public void name() {
        this.name = "슈크림 붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

class BasicFishBread implements FishBread{
    String base;
    String jam;
    String name;

    BasicFishBread () {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "redBean";
    }

    @Override
    public void name() {
        this.name = "팥붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

FishBread 인터페이스를 구현한, ChouCreamFishBread와, BasicFishBread를 선언한다.

이제 FishBread의 객체를 선언하고, ChouCreamFishBread와 BasicFishBread를 통해 초기화하면 된다.

interface FishBread {
    void base ();
    void jam ();
    void name ();
}

class ChouCreamFishBread implements FishBread{
    String base;
    String jam;
    String name;

    ChouCreamFishBread() {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "chouCream";
    }

    @Override
    public void name() {
        this.name = "슈크림 붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

class BasicFishBread implements FishBread{
    String base;
    String jam;
    String name;

    BasicFishBread () {
        this.base();
        this.jam();
        this.name();
    }

    @Override
    public void base() {
        this.base = "마가린 + 밀가루";
    }

    @Override
    public void jam() {
        this.jam = "redBean";
    }

    @Override
    public void name() {
        this.name = "팥붕어빵";
    }

    @Override
    public String toString() {
        return "이름 : " + this.name + "\n"
                + "베이스 : " + this.base + "\n"
                + "내용물 : " + this.jam;
    }
}

class Main {
    public static void main(String[] args) {
        FishBread fishBread = new ChouCreamFishBread();
        System.out.println(fishBread.toString());

        fishBread = new BasicFishBread();
        System.out.println(fishBread.toString());
    }
}

/*
결과 :

이름 : 슈크림 붕어빵
베이스 : 마가린 + 밀가루
내용물 : chouCream
이름 : 팥붕어빵
베이스 : 마가린 + 밀가루
내용물 : redBean

Process finished with exit code 0
 */

위의 완성된 코드를 살펴보면, 인터페이스 FishBread의 객체인 fishBread를 선언하여 사용한 것을
알 수 있다. fishBread를 ChouCreamFishBread 클래스와, BasicFishBread 클래스로 다형성을 통해
사용한 것을 알 수 있다.

결과는 단순히 객체를 선언해서 사용한 것과 같다.

4. 인터페이스의 상속

클래스의 상속에서 클래스는 단 하나의 부모를 갖는다 서술했다.
하지만, 인터페이스는 복수 상속이 가능하다. 

다음 코드를 살펴보자.

interface engine {
    void setMaxSpeed();
}

interface handle {
    void setSteering();
}

interface car extends engine, handle {
    void setModelName ();
}

class avante implements car {
    @Override
    public void setMaxSpeed() {

    }

    @Override
    public void setSteering() {

    }

    @Override
    public void setModelName() {

    }
}

engine, handle 인터페이스를 선언하고, 이 둘을 상속받는 car 인터페이스를 선언했다.

아반떼 클래스를 선언하여, car 인터페이스의 구현 클래스로 선언했다.

이때, car가 상속받는 engine, handle 인터페이스도 모두 구현해야 한다.

구현한 코드는 다음과 같다.

interface Engine {
    void setMaxSpeed();
}

interface Handle {
    void setSteering();
}

interface Car extends Engine, Handle {
    void setModelName ();
}

class Avante implements Car {
    Integer maxSpeed;
    Integer steering;
    String modelName;

    Avante() {
        setMaxSpeed();
        setModelName();
        setSteering();
    }

    @Override
    public void setMaxSpeed() {
        this.maxSpeed = 200;
    }

    @Override
    public void setSteering() {
        this.steering = 360;
    }

    @Override
    public void setModelName() {
        this.modelName = "아반떼";
    }

    @Override
    public String toString() {
        return "모델명 : " + this.modelName + "\n"
                + "최대 속도 : " + this.maxSpeed + "\n"
                + "최대 조향각도 : " + this.steering + "\n";
    }
}

class Main {
    public static void main(String[] args) {
        Car car = new Avante();
        System.out.println(car.toString());

    }
}

/*
결과 :

모델명 : 아반떼
최대 속도 : 200
최대 조향각도 : 360

Process finished with exit code 0
 */

위와 같이 인터페이스를 상속받는 인터페이스를 선언하고, 그것을 구현하는 클래스를 사용하여
다형성을 사용하여 결과를 출력할 수 있다.

5. 디폴트 메서드와 인터페이스 확장

디폴트 메서드는 default키워드로 선언된 메서드를 말한다.
Interface에 명시된, 추상 메서드의 경우는 body, 즉 구현부를 가질 수 없다.

하지만, default로 명시된 추상 메서드의 경우 구현부를 가질 수 있게 된다.
반대로 구현부가 없이는 선언할 수 없다.

interface Calculator {
    Integer getSum (int i, int j);
    Integer getMul (int i, int j);
    default Integer getSub (int i, int j) {
        return i-j;
    }
}

class EngineeringCalculator implements Calculator{
    @Override
    public Integer getSum(int i, int j) {
        return i+j;
    }

    @Override
    public Integer getMul(int i, int j) {
        return i*j;
    }
}

class Main {
    public static void main(String[] args) {
        EngineeringCalculator engineeringCalculator = new EngineeringCalculator();

        System.out.println(engineeringCalculator.getMul(1,2));
        System.out.println(engineeringCalculator.getSum(1,2));
        System.out.println(engineeringCalculator.getSub(1,2));
    }
}

/*
결과 :

2
3
-1

Process finished with exit code 0
 */

Calculator를 구현한 EngineeringCalculator클래스에서 getSum, getMul 메서드만을 구현했다.

후에 메인을 통해 getMul, getSum, getSub를 사용하여 결과를 출력했다.
인터페이스에 구현한 getSub 역시 잘 작동하는 것을 볼 수 있다.

그렇다고 getSub를 저 상태로만 사용하는 것이 아닌, 오버라이드 하여 재정의하여 사용이 가능하다.

그렇다면 이제 인터페이스의 확장에 대해 알아보자.

인터페이스는 OCP를 위한 최적의 도구라 위에 서술했다.
수정에는 닫혀있고, 확장에는 열려있는 구조여야 한다.

단순 계산기만으로 해결할 수 없는 문제를 공학용 계산기에서 추가적인 기능을 구현하여 사용하듯이,
기능을 확장할 수 있어야 한다.

대부분의 단순 계산기에 존재하지 않는, A의 N제곱을 구하는 메서드를 확장해보자.

interface Calculator {
    Integer getSum (int i, int j);
    Integer getMul (int i, int j);
    default Integer getSub (int i, int j) {
        return i-j;
    }
}

class EngineeringCalculator implements Calculator{
    @Override
    public Integer getSum(int i, int j) {
        return i+j;
    }

    @Override
    public Integer getMul(int i, int j) {
        return i*j;
    }

    public Integer getPow (double i, double j) {
        Double retAnswer = Math.pow(i, j);
        return retAnswer.intValue();
    }
}

class Main {
    public static void main(String[] args) {
        EngineeringCalculator engineeringCalculator = new EngineeringCalculator();

        System.out.println(engineeringCalculator.getMul(1,2));
        System.out.println(engineeringCalculator.getSum(1,2));
        System.out.println(engineeringCalculator.getSub(1,2));
        System.out.println(engineeringCalculator.getPow(10,5));
    }
}

/*
결과 :

2
3
-1
100000

Process finished with exit code 0
 */

이런 식으로 인터페이스의 기능만을 사용하기보다, 각 클래스에 대한 공통된 사항을
인터페이스로 명시하고, 각 클래스에서 해당 메서드를 구현하는 방식의 코딩을 할 수 있다.

 

인터페이스 포스팅을 이것으로 마치겠다.

반응형

관련글 더보기

GitHub 댓글

댓글 영역