본문 바로가기

Java

인터페이스

 

훈수/저작권 관련 지적 환영합니다 - 댓글 또는 audgnssweet@naver.com

 

인터페이스


 

인터페이스
인터페이스는 서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면이다. 

자바에서 인터페이스는 두 모듈 간, 두 기능 간 경계면입니다.

말이 매우 추상적인데, 예시를 생각해보겠습니다.

 

선풍기를 생각해보겠습니다.

우리가 정지, 미풍, 약풍, 강풍 등의 버튼만 눌러주면 선풍기가 세기에 맞춰서 동작합니다.

우리는 정지, 미풍, 약풍, 강풍 등의 버튼에 대한 정보는 알고 있지만, 그 버튼을 누름으로써 선풍기 내부에서

어떤 작업들이 일어나 동작하게 되는지에 대해서는 관심이 없습니다.

단지 '어떤 버튼을 눌렀을 때 어떤 일이 일어나는구나' 만 알고 있으면 선풍기를 사용하는 데 있어 충분합니다.

 

우리와 선풍기 기능의 경계면, 이런 버튼들을 인터페이스라고 할 수 있습니다.


자바 초기에 사용하던 인터페이스보다 최신 버전에서 사용되는 인터페이스의 기능이 조금 더 늘어났지만,

인터페이스를 사용하는 목적을 정확히 익히기에는 초기버전의 기능만으로도 충분하므로,

1. 상수

2. 추상 메서드

자바 초기의 기능만을 다뤄보도록 하겠습니다.

 

public interface MyInterface {
    void doSomething();
    int myInt = 1;
}

인터페이스는 위와 같이 사용합니다.

참고로 접근 지정자는 클래스와 동일하게 public, default만 허용합니다.

 

첫 번째 줄은 인터페이스에 추상 메서드를 선언한 모습입니다.

두 번째 줄은 인터페이스에 상수를 선언한 모습입니다.

 

여기서 중요한 것은, 컴파일러가 컴파일시에 위 interface를 다음과 같이 만들어준다는 사실입니다.

 

public interface MyInterface {
    (public abstract) void doSomething();
    (public static final) int myInt = 1;
}

실제로 선언은 하지 않았지만 컴파일러가 default로 위와 같이 만들어줍니다.

즉 상수는 static 상수가 되고, 메서드는 추상 메서드가 됩니다.

이 점을 인지하시면 됩니다.

 

인터페이스는 추상 클래스와 동일하게 '독립적 인스턴스화' 될 수 없습니다.

방법이 있긴 한데, 그 자리에서 추상 메서드를 구현해야 합니다.

그러므로 concrete class에서 인터페이스를 구현하여 사용하는 방법이 일반적입니다. 아래 예시를 보겠습니다.

class MyImpl implements MyInterface {

    @Override
    public void doSomething() {
        System.out.println("do something");
    }
}

위의 인터페이스를 구현한 클래스입니다.

 

기존의 추상 클래스나 일반 상속과는 달리, implements를 사용한다는 점이 특징입니다.

사용 예시도 보겠습니다.

    public static void main(String[] args) {
        //상수 사용
        System.out.println(MyInterface.myInt);
        //메서드 사용
        MyInterface myInterface = new MyImpl();
        myInterface.doSomething();
    }

앞서 보았던 것처럼, 컴파일러에 의해서 상수는 static 상수가 되므로 클래스 이름으로 직접 사용합니다.

추상 메서드는 동적 바인딩에 의해서 실제 인스턴스에 맞는 메서드가 호출되게 됩니다.


인터페이스를 구현하는 클래스가 추상 클래스라면, 직접 인터페이스의 추상 메서드를 구현하지 않아도 되고,

해당 추상 클래스를 상속받는 클래스가 구현할 책임을 갖는다.

 

다시 말해서 위와 같은 구조가 가능하다는 것입니다.


자바에서는 클래스 상속을 단일 상속으로 제한하고 있습니다.

그러나 인터페이스는 단일 구현이 아니라, 다중 구현이 가능합니다.

 

interface MyInterface {
    void doSomething();
}

interface MyInterface2 {
    void doSomething2();
}

class MyImpl implements MyInterface, MyInterface2 {

    @Override
    public void doSomething() {
        System.out.println("do something");
    }

    @Override
    public void doSomething2() {
        System.out.println("do something2");
    }
}

위와 같은 구조로 다중 구현을 합니다.

여러 개를 구현하더라도, 추상 메서드만 전부 구현해주면 문제가 없습니다.

 

interface MyInterface {
    void doSomething();
}

interface MyInterface2 {
    void doSomething();
}

class MyImpl implements MyInterface, MyInterface2 {

    @Override
    public void doSomething() {
        System.out.println("do something");
    }
}

자바에서 클래스 다중 상속을 금지하는 이유 중 하나가 여러 개 상속받은 클래스들의 메서드 이름이 겹친다면

어떤 메서드를 호출해야 하는지 모호한 경우가 있기 때문입니다.

 

인터페이스에서도 마찬가지로 이런 경우가 생길 수는 있지만, 인터페이스에서는 구현하는 쪽이 인터페이스가 아니라

상속받은 쪽이기 때문에 동일한 이름의 함수라면, 하나로 통일해서 정의하고

해당 함수를 호출할 때마다 해당 함수가 호출되면 문제가 없기 때문에 이를 허용하고 있습니다.

 

위의 소스코드에서는

    public static void main(String[] args) {
        MyInterface myInterface = new MyImpl();
        myInterface.doSomething();

        MyInterface2 myInterface2 = new MyImpl();
        myInterface.doSomething();
    }

이렇게 실행한다 해도, MyImpl 클래스에서 직접 구현하고 있는 doSomething() 메서드가 동일하게 2번 호출됩니다.


인터페이스도 클래스와 마찬가지로 인터페이스끼리는 상속 구조가 가능합니다.

 

interface MyInterface {
    void doSomething();
}

interface MyInterface2 extends MyInterface{
    void doSomething2();
}

class MyImpl implements MyInterface2 {

    @Override
    public void doSomething() {
        System.out.println("do something");
    }

    @Override
    public void doSomething2() {
        System.out.println("do something2");
    }
}

클래스에서와 마찬가지로, 인터페이스도 인터페이스끼리 상속 구조일 때는 extends 키워드를 사용합니다.


그렇다면 인터페이스는 언제 사용하는 것이 좋을까요?

 

1.

인터페이스는 기능의 명세만을 제공하여, 그 기능을 서로 다르게 구현하는 클래스들을 만들 수 있고

이를 통해서 다형성을 실현할 수 있는 도구입니다.

그러므로, 기능의 추상화를 통한 다형성이 필요할 때 사용합니다.

 

2.

A, B 두 모듈이 개발되어야 한다고 생각해보겠습니다.

A에서는 B모듈의 기능을 사용합니다. 그렇다면 일반적으로 B 모듈이 전부 개발되기 전에는

A모듈을 개발할 수 없을 것입니다. A모듈에서 B모듈의 기능을 사용하고 있기 때문입니다.

 

그러나 B모듈의 기능을 interface로 추상화하고, A에서는 interface만을 사용하고

B는 interface를 구현하게 된다면

개발도 각각 이뤄질 뿐 아니라

A, B가 모두 개발되었을 때 합치면 정상적으로 작동합니다.

 

3.

성질이 다른 여러 클래스들을 하나로 묶을 때 사용합니다.

그림으로 예시를 보겠습니다.

위의 그림에서 A, B, C는 각각 성질이 다른 클래스들을 모아놓고 있습니다.

 

예를 들어 A100, B100, C100 만이 가능한 기능 (ex print() 메서드)를 삽입하고 싶습니다.

이미 성질이 비슷한 클래스들끼리 모여 상속 구조를 이루고 있는 상태에서

중간에 특정 클래스들만 동작하는 기능을 넣는 것은 쉽지 않습니다.

넣을 수 있다 해도, 해당 메서드를 실행해야 한다면 특정 클래스를 찾아서 실행해야 하는 상황이 올 수 있습니다.

이럴 때 interface Printable을 만들어 이 기능이 필요한 클래스들에서 구현한다면

위 그림처럼 속성을 아우르는 기능 추가 및 동작이 가능합니다.

그래서 interface는 ~able이라는 이름을 가지는 것들을 많이 사용한다고 합니다.

 

여기까지 인터페이스를 사용하는 경우에 대해서 알아보았습니다.


프로그램 첫 설계부터 완벽하게 인터페이스를 추출하고 시작한다면 정말 좋겠지만,

인터페이스를 처음 사용해보시는 분들이라면

먼저 인터페이스 없이 구현을 하고

구현된 클래스들에서 public 한 것들, 공통인 것들을 interface로 분리하더라도 문제가 없습니다.

(code refactoring은 필수이기 때문에)

이렇게 감각을 익혀나가시면 될 것 같습니다.

'Java' 카테고리의 다른 글

Join 종류  (0) 2021.05.12
추상 클래스 & 인터페이스  (0) 2021.05.11
추상 메서드와 추상 클래스  (0) 2021.05.11
(상속) 메서드 오버라이딩과 다형성  (0) 2021.05.11
(상속) 메서드 오버라이딩과 동적 바인딩  (0) 2021.05.07