[8주차] 인터페이스

janjanee·2022년 8월 1일
0
post-thumbnail

2021.01.20 작성글 이전

8. 인터페이스

학습 목표 : 자바의 인터페이스에 대해 학습하세요.

인터페이스는 일종의 추상클래스이다.
추상클래스 처럼 추상메소드를 갖지만 추상클래스와 달리 일반 메소드 또는 멤버변수를 구성원으로 가질 수 없다.

추상클래스를 미완성 설계도라고 한다면,
인터페이스는 기본 설계도라고 할 수 있다.

8-1. 인터페이스 정의하는 방법

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

인터페이스는 class 대신 interface키워드를 사용한다.
클래스와 같이 접근제어자로 public 또는 default를 사용할 수 있다.

인터페이스는 다음과 같은 제약사항을 갖고있다.

  • 모든 멤버변수는 public static final이어야 하며, 생략가능

  • 모든 메소드는 public abstract 이어야 하며, 생략가능

    단, static 메소드와 디폴트 메소드는 예외(Java 8 부터)

8-2. 인터페이스 구현하는 방법

인터페이스도 추상클래스와 마찬가지로 그 자체로 인스턴스를 생성할 수 없다.

'implements' 키워드를 사용하여 인터페이스를 구현할 수 있다.

class 클래스명 implements 인터페이스명 {
    // 인터페이스에 정의된 추상메소드 구현
}

class Fighter implements Fightable {
    public void move(int x, int y) { ... }
    public void attack(Unit u) { ... }
}

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

abstract class Fighter implements Fightable {
    public void move(int x, int y) { ... }
}

또한 다음과 같이 상속과 구현을 동시에 한다.

class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { ... }
    public void attack(Unit u) { ... }
}

8-3. 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스 참조 가능, 인터페이스 타입으로의 형변환 가능

인터페이스 Fightable을 클래스 Fighter가 구현했다면, 아래와 같이 사용가능하다.

Fightable f = (Fightable)new Fighter();
or
Fightable f = new Fighter();

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

void attack(Fightable f) {
    ...
}

메소드의 리턴타입으로 인터페이스의 타입을 지정하는 것도 가능하다.

Fightable method() {
    ...
    return new Fighter();
}

리턴타입이 인터페이스라는 것은 메소드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는것을 의미한다.
즉, 위의 예제에서 method()의 리턴타입은 Fightable 인터페이스를 구현한 Fighter 클래스의 인스턴스를 반환한다.

아래의 ParserTest 예제에서 자세히 살펴보자.

interface Parseable {
    void parse(String fileName);
}

class XMLParser implements Parseable {

    @Override
    public void parse(String fileName) {
        System.out.println(fileName + "-XML parsing");
    }
}

class HTMLParser implements Parseable {

    @Override
    public void parse(String fileName) {
        System.out.println(fileName + "-HTML parsing");
    }
}

Parseable 인터페이스를 생성하고 parsing 목적인 추상메소드로 parse()를 정의한다.
XMLParser 클래스와 HTMLParser 클래스를 생성하고 이 둘은 Parseable 인터페이스를 구현한다.

class ParserManager {
    public static Parseable getParser(String type) {
        if(type.equals("XML")) {
            return new XMLParser();
        } else {
            return new HTMLParser();
        }
    }
}

ParserManager 클래스를 생성하고 이 클래스에는 getParser 메소드가 존재하는데 이는 Parseable 인터페이스가
리턴 타입이므로 type 값에 따라 Parseable 인터페이스를 구현한 XMLParser 또는 HTMLParser 인스턴스를 리턴한다.

public class ParserTest {
    public static void main(String[] args) {
        Parseable parser = ParserManager.getParser("XML");
        parser.parse("document.xml");
        parser = ParserManager.getParser("HTML");
        parser.parse("document2.html");
    }
}

처음 parse 참조변수에는 XMLParser 인스턴스의 주소값이 담기며, 그 다음 parse 참조변수에는 HTMLParser 인스턴스가 담긴다.

또 하나의 예제를 살펴보자.

Character라는 클래스가 있고 짱구, 도라에몽, 해리포터 클래스가 Character 클래스를 상속받았다.

도라에몽이랑 해리포터는 마법을 부릴 수 있기 때문에 마법과 관련된 새로운 메소드를 추가하려고 한다.

두 클래스에 모두 메소드를 적는것은 코드가 중복이 된다. Character 클래스에 메소드를 적자니, 짱구는 마법을 부릴 줄 모른다.

인터페이스를 이용하여 해결해보자.

public interface Magical {
    void teleport();
    void fly();
}

Magical 인터페이스를 정의하고 추상메소드 teleport와 fly를 추가한다.

public class MagicalImpl implements Magical {
    @Override
    public void teleport() {
        System.out.println("순간이동!");
    }

    @Override
    public void fly() {
        System.out.println("날아라!");
    }
}

Magical 인터페이스를 구현하는 MagicalImpl 클래스를 생성한다.

마지막으로 도라에몽과 해리포터 클래스가 Magical 인터페이스를 구현하도록 하고, 인터페이스를 구현한
MagicalImpl 클래스를 도라에몽, 해리포터 클래스에 포함시켜서 내부적으로 호출해서 사용한다.

public class Doraemon extends Character implements Magical{
    MagicalImpl m = new MagicalImpl();

    @Override
    public void teleport() {
        m.teleport();
    }

    @Override
    public void fly() {
        m.fly();
    }

    ...
}

8-4. 인터페이스 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와 달리 다중 상속이 가능하다.

interface Movable {
    void move(int x, int y);
}

interface Attackable {
    void attack(Unit u);
}

interface Fightable extends Movable, Attackable { }

Fightable 인터페이스는 Movable, Attackable 두 개의 인터페이스를 상속 받았기 때문에
본인 자신에겐 정의된 멤버가 없지만 두 인터페이스의 멤버를 상속 받아 move와 attack을 갖게된다.

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

자바에서 한 클래스는 여러 클래스를 상속 받을 수 없다. 즉, 다중 상속이 불가능한데
이런 경우 보통은, 한 쪽만 선택하여 상속받고 나머지 한 쪽은 클래스 내부에 포함시켜 내부적 인스턴스를 생성해서
사용하도록 한다.

위의 경우를 인터페이스를 이용하여 다형적인 특징과 함께 구현하는 방법을 살펴보자.

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

    public void power() { power = !power; }
    public void channelUp() { channel++; }
    public void channelDown() { channel--; }
    public void volumeUp() { volume++; }
    public void volumeDown() { volume--; }
}
public class VCR {
    protected int counter;

    public void play() { System.out.println("play"); }
    public void stop () { System.out.println("stop"); }
    public void reset() { counter = 0; }
    public int getCounter() { return counter; }
    public void setCounter (int c) { counter = c; }
}

TV와 VCR 두 개의 클래스가 있고, TVCR 클래스를 새로 정의하여 앞의 두 클래스를 상속받고 싶다.

두 클래스 중 TV를 상속받고, VCR 클래스는 내부 인스턴스로 사용할 것이다.
이때, 인터페이스 IVCR을 만들어 VCR 클래스에 정의된 메소드와 일치하는 추상메소드를 작성한다.

public interface IVCR {
    void play();
    void stop();
    void reset();
    int getCounter();
    void setCounter(int c);
}

TVCR 클래스에 TV 클래스를 상속받고, IVCR 인터페이스를 추상메소드를 구현한다.
IVCR의 추상메소드 구현부에 VCR의 인스턴스 메소드를 호출한다.

public class TVCR extends Tv implements IVCR{
    VCR vcr = new VCR();

    @Override
    public void play() {
        vcr.play();
    }

    @Override
    public void stop() {
        vcr.stop();
    }

    @Override
    public void reset() {
        vcr.reset();
    }

    @Override
    public int getCounter() {
        return vcr.getCounter();
    }

    @Override
    public void setCounter(int c) {
        vcr.setCounter(c);
    }

    public static void main(String[] args) {
        TVCR tvcr = new TVCR();
        tvcr.play();
    }
}

8-5. 강한결합과 느슨한결합

객체간의 강한결합과 느슨한결합이 존재하는데 이 개념을 통해 인터페이스의 본질적인 측면을 알아보자.

강한결합

Class A {
    public void methodA(B b) {
        b.methodB();
    }
}

Class B {
    public void methodB() {
        System.out.println("methodB()");
    }
}

A와 B 클래스가 있다. 위의 경우 A와 B가 강한 의존성을 갖고 있기 때문에 강한결합이라고 볼 수 있다.
위의 예제에서 강한 의존성의 조건은 무엇일까?

  • A를 작성하기 위해서 B가 반드시 존재해야한다.
  • B의 methodB()의 선언부가 변경되면, 이를 사용하는 A쪽의 코드도 변경되어야 한다.

느슨한결합

위의 강한결합을 인터페이스를 이용하여 느슨한결합으로 만들 수 있다.
A와 B 두 클래스의 관계를 간접적으로 변경하는 것이다.

interface I {
    void methodB();
}

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

class B implements I {
    public void methodB() {
        System.out.println("methodB in B class");
    }
}

그 다음 B 클래스에서 I를 구현한다.

class A {
    public void methodA(I i) {
        i.methodB();
    }
}

이제 A 클래스에서 B 대신 인터페이스 I를 사용해서 작성할 수 있다.

만약, 아래와 같은 새로운 C 클래스가 생성됐을때,

class C implements I {
    public void methodB() {
        System.out.println("methodB in C class");
    }
}

A 클래스에는 아무런 변경사항 없이 methodB()를 사용할 수 있다.

class InterfaceTest {
    public static void main(String[] args) {
        A a = new A();
        a.methodA(new B());
        a.methodA(new C());
    }
}

인터페이스를 통해 느슨한결합을 만들어 코드 재사용성과 유연성을 높인다.

8-6. 인터페이스의 기본 메소드 (Default Method), 자바 8

Java 8 이상부터 지원하는 메소드

어떤 인터페이스가 존재하고 그 인터페이스를 구현하는 많은 클래스들이 있다고 가정하자.
1년 뒤 해당 인터페이스에 새로운 기능이 추가되어야 한다는 사실을 깨달았다.

만약, 새로운 메소드를 추가한다면? 인터페이스는 모든 메소드가 추상메소드이므로
인터페이스를 구현한 모든 클래스에서 해당 메소드를 모두 구현해주어야 한다.

구현 인터페이스가 100개 였다면? 100개의 클래스에 새로 추가될 추상메소드를 구현해야 할까..?

이러한 문제점을 해결하기 위해 default method를 사용할 수 있다.

interface I {
    void method();
    default newMethod() {
        // 구현내용
    }
}
  • 메소드 앞에 default 키워드를 붙인다.
  • 추상 메소드와 달리 일반 메소드처럼 몸통 {}이 있어야 한다.
  • 접근 제어자가 public이며, 생략 가능하다.

default Method를 추가하여 100개의 클래스에 구현하고 다니는 일을 하지 않아도 된다.

default method는 메소드는 기존의 메소드들과 이름이 충돌 됐을 경우 몇 가지 규칙이 있다.

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

    인터페이스를 구현한 클래스에서 디폴트 메소드를 오버라이딩

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

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

다음 예제를 살펴보자.

public interface Test1 {
    default void same() {
        System.out.println("Test1");
    };
}

public interface Test2 {
    default void same() {
        System.out.println("Test2");
    };
}

두 개의 인터페이스가 존재하며, 같은 default same() 메소드를 갖고있다.

public class Test implements Test1, Test2 {

    @Override
    public void same() {
        Test1.super.same();
        Test2.super.same();
    }

    public static void main(String[] args) {
        Test t = new Test();
        t.same();
    }
}

두 개의 인터페이스를 구현하면 컴파일 에러가 발생하는데, 이때 구현할 클래스에서 오버라이딩을 하면된다.
위와 같이 super 키워드를 사용하여, 호출도 가능하다.

8-7. 인터페이스의 static 메소드, 자바 8

Java 8 이상부터 지원하는 메소드

static 메소드는 인스턴스와 관계없는 독립적인 메소드이다.
Java 8 이전버전은 인터페이스에 추상메소드만 두어야 한다는 조건이 규칙이 있었기 때문에 static도 허용하지 않았다.

static 리턴타입 메소드명(매개변수) {...}
public interface I {
    static void printHello(){
        System.out.println("Hello~");
    }
}

default, static 키워드가 지원되면서 추상클래스는 의미없어진게 아닐까?

-> 아니다!
대부분은 추상클래스로 작성된 것들을 인터페이스로 바꿀 수 있지만,

인터페이스에서는 상수외 상태값을 갖는 멤버 변수를 선언하지 못한다.

따라서, 추상클래스는 여전히 효용 가치가 있다.

8-8. 인터페이스의 private 메소드, 자바 9

Java9 에 private methodprivate static method 추가

특정 기능을 처리하여 외부에 노출하고 싶지 않은 메소드들도 public으로 강제 노출되었기 때문에
9버전 부터 private 메소드를 지원하게 됐다.

private 메소드도 default 메소드와 마찬가지로 구현부를 가져야 한다.

public interface PrivateTest {
    default void printHello() {
        print("Hello");
    }

    default void printHi() {
        print("Hi");
    }

    private void print(String s) {
        System.out.println(s);
    };
}

References

  • 남궁성, 『자바의 정석』, 도우출판(2016)
  • 출처가 없는 이미지는 직접 그림
profile
얍얍 개발 펀치

0개의 댓글