7-7. 인터페이스

Hyun Jun·2022년 1월 22일
0

자바의 정석

목록 보기
32/52
post-thumbnail
post-custom-banner

인터페이스

개념

추상 클래스의 일종으로, 오직 상수추상 메서드만을 멤버로 가질 수 있음

추상 클래스 안에는 추상 메서드가 아닌 일반 메서드도 포함할 수 있으며, 상수가 아닌 모든 종류의 멤버 변수도 포함할 수 있음

추상 클래스와 마찬가지로 다른 클래스를 작성하는 데에 도움이 될 뼈대로서 쓰임.

 

인터페이스의 작성

추상 클래스와 작성법은 같지만, abstract class 대신 interface 키워드를 붙여줌

interface InterfaceName {
    public static final int CONSTANT_NAME = 7;
    public abstract void methodName(int param);
}

예시에서 보는 것과 같이 다음 규칙을 따름

  1. 모든 멤버 변수는 public static final이며, 해당 표현은 생략 가능함

  2. 모든 메서드는 public abstract이며, 해당 표현은 생략 가능함

    적용 예외: static 메서드와 default 메서드

    JDK 1.8부터는 interfacestatic 메서드와 default 메서드를 허용하기로 했기 때문에 2번 규칙은 Java 8 미만에서만 유효함

어차피 모든 멤버 변수와 메서드에 적용되는 사항이므로 생략이 가능한 것이며, 생략은 작성의 편의를 위한 것이므로 이 제어자들로 부여된 의미가 사라지는건 아님

일부러 생략하고 쓰더라도 컴파일 단계에서 컴파일러가 제어자들을 다시 붙여주기 때문

 

인터페이스의 상속

interface 끼리만 상속이 가능하고, class와 달리 다중 상속이 가능함.

자손 인터페이스는 조상 인터페이스의 멤버를 모두 상속 받음

interface Example1 {
    void method1();
}

interface Example2 {
    void method2();
}

interface Example3 extends Example1, Example2 {} // 비어있지만 추상 메서드 method1과 method2 를 물려받았음

 

인터페이스의 구현

인터페이스도 자신이 갖고 있는 추상 메서드를 정의해줄 클래스가 필요함

이 때 extends처럼 implements라는 키워드를 사용하여 인터페이스를 구현해줄 클래스를 정의할 수 있음

interface InterfaceName {
    void method1();
    void method2();
}

class ClassName1 implements InterfaceName {
    public void method1() {
        ... // 구현
    }
    public void method2() {
        ... // 구현
    }
}

abstract class ClassName2 implements InterfaceName {
    public void method1() {
        ... // 구현
    }
}

ClassName2에서처럼 인터페이스의 추상 메서드 중 일부만 구현하는 경우는 클래스 선언부에 abstract를 붙여줘야함.

 

주의할 점은 각 클래스에서 구현된 메서드 앞에 public 제어자가 붙었다는 것.

원래 인터페이스가 갖고 있던 메서드는 엄밀히는 public static 접근성을 갖고 있으므로, 상속하여 오버라이딩 할 때는 이것보다 넓은 범위로 해야함. 따라서 구현된 메서드의 제어자는 반드시 public으로 설정해야함

 

아래와 같이 상속과 구현을 동시에 하는 것도 가능.

class Child extends Parent implements InterfaceName {
    ... // 상속 & 구현
}

 

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

클래스의 상속에서 다중 상속의 문제점과, 그로 인해 Java에서 다중 상속을 지원하지 않는다는 사실에 대해 배웠음.

다중 상속이 된다면, 두 조상 클래스의 멤버변수의 이름이 같거나 메서드의 선언부가 일치하면 상속 받는 자손 입장에서는 어느 조상으로부터 받은 멤버인지 알 수가 없음

사실 Javaclass가 아닌 interface를 통해서는 다중 상속이 가능하게 해놨지만, 실제적으로 쓰는 일은 거의 없음.

 

기본적인 접근법

조상 클래스 1개와 인터페이스를 이용한 다중 상속

  • 인터페이스의 멤버변수인 static 상수 → 조상 클래스의 멤버 변수에 클래스 이름을 붙여서 구분 가능

  • 인터페이스의 abstract 메서드 → 추상 메서드는 구현부가 어차피 없으므로, 조상 클래스의 메서드를 상속받으면 됨

 

조금 더 실용적인 접근법

2개의 조상 클래스로부터 다중 상속

2개 중 비중이 높은 쪽을 선택하여 extends로 상속을 받고

  1. 나머지 한쪽은 클래스 내부에 멤버로 포함시키거나, (= 내부에서 인스턴스를 생성하여 사용)

  2. 필요한 부분만 뽑아서 인터페이스로 만든 다음 implements로 구현시킴

 

예시)

Tv라는 클래스와 Computer라는 클래스를 모두 상속 받는 새로운 클래스 SmartTv를 만드려고 함

// Tv.java

public class Tv {
    protected boolean power;
    protected int channel;
    protected int volume;

    public void togglePower() {
        ...
    }
    public void channelUp() {
        ...
    }
    public void channelDown() {
        ...
    }
    public void volumeUp() {
        ...
    }
    public void volumeDown() {
        ...
    }
}
// Computer.java

public class Computer {
    public void runApp() {
        ...
    }
    public void quitApp() {
        ...
    }
    public void toggleInternet() {
        ...
    }
    public void toggleBluetooth() {
        ...
    }
}

클래스 2개를 상속받는 것은 불가능하므로, 비중이 높다고 판단한 Tv만 상속받고 Computer는 인터페이스로 만들어 구현하기로 함

// IComputer.java

public interface IComputer {
    public void runApp();
    public void quitApp();
    public void toggleInternet();
    public void toggleBluetooth();
}
// SmartTv.java

public class SmartTv extends Tv implements IComputer {
    // Tv 클래스의 멤버는 모두 상속받았으므로, 인터페이스의 추상메서드들만 구현해주면 됨

    Computer computer = new Computer(); // 클래스 내부에 멤버로서 인스턴스를 만들어 놓음

    public void runApp() {
        computer.runApp(); // 새로 작성할 필요 없이, 참조변수를 통해 인스턴스 메서드를 호출해오면 됨.
    }
    public void quitApp() {
        computer.quitApp();
    }
    public void toggleInternet() {
        computer.toggleInternet();
    }
    public void toggleBluetooth() {
        computer.toggleBluetooth();
    }
}

이렇게 되면 결과적으로 Tv 클래스와 Computer 클래스 둘 모두로부터 상속받는 것과 마찬가지가 됨.

굳이 Computer를 인터페이스로 만들지 않고 인스턴스만 내부로 가져와서 활용해도 되지만, 인터페이스를 이용하면 다형적 특성을 이용할 수 있다는 장점 존재.

 

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

클래스의 다형성에서는 조상 클래스 타입의 참조변수로 자손 클래스 인스턴스를 참조할 수 있다고 배웠음.

인터페이스도 자신을 구현한 클래스의 조상격이므로 조상인 인터페이스의 타입을 가진 참조변수로 자손인 클래스의 인스턴스를 참조할 수 있음.

interface IParent {
    ...
}

class Child implements IParent {
    ...
}
IParent p = (IParent)new Child();
// 또는
IParent p = new Child();

여기서 당연히 참조변수 p는 인터페이스 IParent에 정의된 멤버만 호출이 가능하고, 그 외에 Child가 가진 멤버들은 호출이 불가함.

 

인터페이스 타입을 매개변수에서도 활용할 수 있음

이 때 매개변수에는 👉 해당 인터페이스를 구현한 클래스의 인스턴스 👈를 제공해야함

class Child extends Parent implements IParent {
    public void method(IParent p) {
        ...
    }
    ...
}
Child c = new Child();
c.method(new Child()); // IParent를 구현한 클래스는 Child 이므로 Child의 인스턴스를 매개변수로 넘겼음

 

인터페이스 타입을 리턴 타입으로 지정하는 것도 가능함

이 때 리턴값은 👉 해당 인터페이스를 구현한 클래스의 인스턴스 👈이어야 함.

IParent method() {
    ...
    IParent p = new Child();
    return p;
    // 좀 더 간략하게 return new Child(); 라고 해도 됨
}

리턴 타입이 IParent 인터페이스 타입이므로, 반환되는 값은 그 인터페이스를 구현한 Child 클래스의 인스턴스임

 

인터페이스의 장점

  1. 개발 시간 단축

    인터페이스를 작성하고 나면,

    • 선언부만 가지고서 메서드 호출이 가능하고, 이를 이용해 바로 프로그램 작성이 가능함 (구현이 될 때까지 기다리지 않아도 됨)

    • 이 와중에 다른 한쪽에서는 인터페이스를 구현하는 클래스를 작성하여 구현부를 만들어내면 됨

  2. 표준화

    프로젝트의 기본 틀이 담긴 인터페이스를 작성하면, 여러 개발자가 이 틀을 토대로 프로그램을 작성하게 되므로 일관되고 정형화된 프로그래밍이 가능해짐

  3. 관계 없던 클래스들의 관계 형성

    서로 상속관계도 아니고, 공통 조상도 없는 클래스끼리도 하나의 인터페이스를 공통적으로 구현하게 함으로써 관계를 맺어줄 수 있음

  4. 독립적인 프로그래밍

    클래스의 선언과 구현을 분리시킬 수 있어 구현에 독립적인 프로그래밍이 가능해짐. 클래스와 클래스 간의 직접 관계 대신 클래스와 인터페이스를 통한 간접 관계를 이용하면 한 클래스의 변경으로 다른 클래스가 영향을 받는 일이 없어짐

 

인터페이스의 이해

클래스 간의 관계가 직접적이면 변화에 취약해질 수 밖에 없음.

인터페이스는 클래스 간의 관계에서 중간다리 역할을 함으로써 한 클래스에서 다른 클래스의 변화에 대한 의존성을 떨어뜨림.

핵심은 인터페이스를 통한 클래스의 선언과 구현 분리

class ClassA {
    void autoPlay(ClassB b) {
        b.play();
    }
}

class ClassB {
    void play() {
        System.out.println("Playing from class B");
    }
}

class Test {
    public static void main(String[] args) {
        ClassA a = new ClassA();
        a.autoPlay(new B());
    }
}

ClassA의 메서드 autoPlayClassB의 인스턴스를 매개변수로 받고 있음.

만약 같은 기능을 하는 다른 클래스로 ClassB를 대체하고자 하거나, play 메서드의 선언부가 바꾸려면 그 때마다 ClassAautoPlay 메서드도 수정을 해줘야함.

이렇게 다른쪽의 변화에 의존적인 구조를 인터페이스를 이용해서 개선할 수 있음

class ClassA {
    void autoPlay(Playable p) {
        p.play();
    }
}

interface Playable {
    public abstract void play();
}

class ClassB implements Playable {
    public void play() {
        System.out.println("Playing from class B");
    }
}

class ClassC implements Playable {
    public void play() {
        System.out.println("Playing from class C");
    }
}

class Test {
    public static void main(String[] args) {
        ClassA a = new ClassA();
        a.autoPlay(new ClassB());
        a.autoPlay(new ClassC()); // ClassB 타입 대신 ClassC를 넣어도 아무 문제 없음. ClassA는 인터페이스 Playable과만 직접 관계에 있기 때문
    }
}

기존의 ClassAClassB 구조가

ClassAPlayableClassB 구조로 바뀌면서 양 클래스간 의존성이 떨어지게 되었음

 

디폴트 메서드와 static 메서드

JDK 1.8부터는 인터페이스 안에 default 메서드와 static 메서드를 포함할 수 있게 되었음

기존에도 포함시키지 못할 이유는 없었으나, Java 학습의 편의성을 위해 규칙에 예외를 만들지 않으려 했다고 함.

그래서 JDK 1.8 이전에 만들어진 java.util.Collection 인터페이스는 static 메서드들을 별도의 클래스인 Collections에 보관하고 있음

 

디폴트 메서드

인터페이스를 구현하는 자손 클래스는 모든 추상 메서드를 구현할 의무가 있기 때문에, 인터페이스에 새 메서드가 추가되면 모든 자손 클래스에 추가사항을 반영해야함.

이런 불편함을 막기 위해, 추상 메서드의 기본 구현을 제공하는 메서드인 default 메서드가 고안됨

  • 자손 클래스에서 구현할 의무 없음

  • 자체 구현부 존재

  • public 접근 제어자 사용 (생략 가능)

 

interface Example {
    void method();
    void newMethod();
}

위 예시처럼 새 메서드인 newMethod를 추상 메서드로 쓰는 대신에

interface Example {
    void method();
    default void newMethod() {
        ... // 구현부 존재
    }
}

default 메서드로 작성할 수 있음. 이 경우, Example 인터페이스가 구현된 클래스들은 변경할 필요 없음

 

단, 새로 추가된 default 메서드와 기존 메서드의 이름이 겹쳐 충돌할 수 있음

  1. 여러 인터페이스의 디폴트 메서드간 충돌

    인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩하여 해결 가능

  2. 디폴트 메서드와 조상 클래스의 메서드 간의 충돌

    조상 클래스의 메서드가 상속 되고, 디폴트 메서드는 무시됨.

profile
Back-end Engineer 👨‍💻
post-custom-banner

0개의 댓글