Java 기초 문법

Java ) 인터페이스(interface) 총 정리

Albosa2lol 2023. 6. 1. 19:08

서론

Spring 공부하던 중 interface부분에 대해 이해가 부족한 것 같아 자세하게 한번 정리해 보려고 합니다.

목차
1. 인터페이스란?
2. 인터페이스의 작성 방법
3. 인터페이스의 상속
4. 인터페이스의 구현
5. 인터페이스를 이용한 다형성
6. 인터페이스의 장점

 

1. 인터페이스란?

인터페이스는 일종의 추상 클래스입니다. 인터페이스는 추상 클래스처럼 추상 메서드를 갖지만 추상 클래스보다 추상화 정도가 높아서 추상 클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없습니다. 오직 "추상 메서드"와 "상수"만을 멤버로 가질 수 있습니다.

 추상 클래스를 부분적으로만 완성된 '미완성 설계도'라고 한다면, 인터페이스는 구현된 것은 아무것도 없고 밑그림만 그려져 있는 '기본 설계도'라 할 수 있습니다.

 인터페이스도 추상 클래스처럼 완성되지 않은 불완전한 것이기 때문에 그 자체만으로 사용되기보다는 다른 클래스를 작성하는데 도움을 줄 목적으로 작성됩니다.

 

2. 인터페이스의 작성 방법

인터페이스를 작성하는 것은 클래스를 작성하는 것과 같습니다. 다만 키워드로 class 대신 interface를 사용한다는 것만 다릅니다. 그리고 interface에도 클래스와 같이 접근제어자 public 또는 default를 사용할 수 있습니다.

interface 인터페이스이름{
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수목록);
}

 

일반적인 클래스의 멤버들과 달리 인터페이스의 멤버들은 다음과 같은 제약사항이 있습니다.

- 모든 멤버 변수는 public static final 이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.(단, static메서드와 default 메서드는 JDK1.8부터 예외)

 

interface PlayingCard {
    public static final int SPADE = 4;
    final int DIAMOND = 3;   // public static final int DIAMOND = 3;
    static int HEART = 2;    // public static final int HEART = 2;
    int CLOVER = 1;          // public static final int CLOVER = 1;

    public abstract String getCardNumber();
    String getCardKing();   // public abstract String getCardKind();
}

생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 위와 같이 추가해 줍니다.

 

3. 인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속을 받는 것이 가능합니다.(참고: 인터페이스는 클래스와 달리 Object클래스와 같은 최고 조상이 없습니다.)

interface Movable {
  /** 지정된 위치(x, y)로 이동하는 기능의 메서드 */
  void move(int x, int y);
}

interface Attackable {
 /** 지정된 대상(u)을 공격하는 기능의 메서드 */
  void attack(Unit u);
}

interface Fightable extends Movable, Attackable {}

클래스의 상속과 마찬가지로 자손 인터페이스(Fightable) 조상 인터페이스(Movable, Attackable)에 정의된 멤버를 모두 상속받습니다.

 그래서 Fightable자체에는 정의된 멤버가 하나도 없지만 조상 인터페이스로부터 상속받은 두 개의 추상 메서드, move(int x, int y) attack(Unit u)을 멤버로 갖게 됩니다.

 

4. 인터페이스의 구현

인터페이스도 추상 클래스처럼 그 자체로는 인스턴스를 생성할 수 없으며, 추상 클래스가 상속을 통해 추상 메서드를 완성하는 것처럼, 인터페이스도 자신에게 정의된 추상 메서드의 몸통을 만들어주는 클래스를 작성해야 합니다. 

class ClassName implements InterfaceName {
  // 인터페이스에 정의된 추상메서드 구현
}

interface InterfaceName{
}

인터페이스는 구현한다는 의미의 키워드 'implements'를 사용하여 위와 같이 구현할 수 있습니다.

 

abstract class Fighter implements Fightable {
    public void move(int x, int y) { /* 내용 생략 */ }
}

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

 

class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { /* 내용 생략 */ }
    public void attack(Unit u) { /* 내용 생략 */ }
}

그리고 위와 같이 상속 구현 동시에 할 수도 있습니다.

한번 아래의 전체 코드를 한번 예시로 들어 정리해보겠습니다.

class FighterTest {
    public static void main(String[] args) {
        Fighter f = new Fighter();

        if (f instanceof Unit) {
            System.out.println("f는 Unit클래스의 자손입니다.");
        }
        if (f instanceof Fightable) {
            System.out.println("f는 Fightable인터페이스를 구현했습니다.");
        }
        if (f instanceof Movable) {
            System.out.println("f는 Movable인터페이스를 구현했습니다.");
        }
        if (f instanceof Attackable) {
            System.out.println("f는 Attackable인터페이스를 구현했습니다.");
        }
        if (f instanceof Object) {
            System.out.println("f는 Object클래스의 자손입니다.");
        }
    }
}

class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { /* 내용 생략 */ }

    public void attack(Unit u) { /* 내용 생략 */ }
}

class Unit {
    int currentHP;    // 유닛의 체력
    int x;            // 유닛의 위치(x좌표)
    int y;            // 유닛의 위치(y좌표)
}

interface Fightable extends Movable, Attackable {
}

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

interface Attackable {
    void attack(Unit u);
}

위 코드의 출력 결과

위 코드의 클래스와 인터페이스간의 관계

실제로 Fighter클래스 Unit클래스로부터 상속받고 Fightable인터페이스만을 구현했지만, Unit클래스 Object클래스의 자손이고, Fightable인터페이스 Attackable Movable인터페이스의 자손이므로 Fighter클래스는 이 모든 클래스 인터페이스의 자손이 되는 셈이 됩니다.

 

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

인터페이스 인터페이스를 구현한 클래스의 조상이라 할 수 있으므로 해당 인터페이스 타입의 참조 변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형 변환도 가능합니다. 

Fightable f = new Fighter();

예를 들어 위와 같이 인터페이스 Fightable 클래스 Fighter 구현했을 때, 다음과 같이 Fighter인스턴스 Fightable타입 참조 변수로 참조하는 것이 가능합니다.

 

void attack(Fightable f){
    // 내용 생략
}

따라서 인터페이스는 위와 같이 메서드의 매개변수의 타입으로도 사용될 수 있습니다.

인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것입니다. 그래서 attack메서드를 호출할 때는 매개변수 Fightable인터페이스 구현한 클래스의 인스턴스를 넘겨주어야 합니다.

 

class Fighter extends Unit implements Fightable {
    public void move(int x, int y) { /* 내용 생략 */}
    public void attack(Fightable f) { /* 내용 생략 */}
}

한번 위의 코드를 살펴보겠습니다.

Fightable인터페이스를 구현한 Fighter클래스가 있을 때, attack메서드 매개변수로 Fighter인스턴스를 넘겨줄 수 있습니다. 즉, attack(new Fighter())와 같이 할 수 있다는 것입니다. 

 

Fightable method() {
  //...
  Figther fighter = new Fighter();
  return fighter;
}

그리고 위와 같이 메서드 리턴 타입으로 인터페이스의 타입을 지정하는 것 역시 가능합니다.

리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미합니다.

위의 코드에서는 method()의 리턴 타입이 Fightable인터페이스이기 때문에 메서드의 return문에서 Fightable인터페이스를 구현한 Fighter클래스 인스턴스를 반환합니다.

아래의 다형성 예제 코드를 한번 살펴보겠습니다.

interface Parseable {
    // 구문 분석작업을 수행한다.
    public abstract void parse(String fileName);
}

class ParserManager {
    // 리턴타입이 Parseable인터페이스이다.
    public static Parseable getParser(String type) {
        if (type.equals("XML")) {
            return new XMLParser();
        } else {
            Parseable p = new HTMLParser();
            return p;
            // return new HTMLParser();
        }
    }
}

class XMLParser implements Parseable {
    public void parse(String fileName) {
        /* 구문 분석작업을 수행하는 코드를 적는다. */
        System.out.println(fileName + "- XML parsing completed.");
    }
}

class HTMLParser implements Parseable {
    public void parse(String fileName) {
        /* 구문 분석작업을 수행하는 코드를 적는다. */
        System.out.println(fileName + "-HTML parsing completed.");
    }
}

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");
    }
}

츨력 결과

만일 나중에 새로운 종류의 NewXMLParser클래스가 나와도 ParserTest클래스는 변경할 필요 없이, ParserManager클래스의 getParser메서드에서 'return newXMLParser(); 대신 'return new NewXMLParser();'로 변경하기만 하면 됩니다.

이러한 장점은 특히 분산 환경 프로그래밍에서 그 위력을 발휘하는데, 사용자 컴퓨터에 설치된 프로그램을 변경하지 않고 서버 측의 변경만으로도 사용자가 새로 개정된 프로그램을 사용하는 것이 가능합니다.

 

6. 인터페이스의 장점

그렇다면 왜 인터페이스를 사용할까요? 그 장점은 다음과 같습니다.

  • 개발 시간을 단축시킬 수 있다.
  • 표준화가 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.

인터페이스 장점 예제 코드

class RepairableTest {
    public static void main(String[] args) {
        Tank tank = new Tank();
        Dropship dropship = new Dropship();

        Marine marine = new Marine();
        SCV scv = new SCV();

        scv.repair(tank);    // SCV가 Tank를 수리하도록 한다.
        scv.repair(dropship);
//	scv.repair(marine);  // 주석을 풀면 컴파일 에러 발생
    }
}

interface Repairable {
}

class GroundUnit extends Unit {
    GroundUnit(int hp) {
        super(hp);
    }
}

class AirUnit extends Unit {
    AirUnit(int hp) {
        super(hp);
    }
}

class Unit {
    int hitPoint;
    final int MAX_HP;

    Unit(int hp) {
        MAX_HP = hp;
    }
    //...
}

class Tank extends GroundUnit implements Repairable {
    Tank() {
        super(150);        // Tank의 HP는 150이다.
        hitPoint = MAX_HP;
    }

    public String toString() {
        return "Tank";
    }
    //...
}

class Dropship extends AirUnit implements Repairable {
    Dropship() {
        super(125);        // Dropship의 HP는 125이다.
        hitPoint = MAX_HP;
    }

    public String toString() {
        return "Dropship";
    }
    //...
}

class Marine extends GroundUnit {
    Marine() {
        super(40);
        hitPoint = MAX_HP;
    }
    //...
}

class SCV extends GroundUnit implements Repairable {
    SCV() {
        super(60);
        hitPoint = MAX_HP;
    }

    void repair(Repairable r) {
        if (r instanceof Unit) {
            Unit u = (Unit) r;
            while (u.hitPoint != u.MAX_HP) {
                /* Unit의 HP를 증가시킨다. */
                u.hitPoint++;
            }
            System.out.println(u.toString() + "의 수리가 끝났습니다.");
        }
    }
    //...
}

출력결과

위 코드는 스타크래프트 게임 유닛을 대상으로 클래스로 구현한 것입니다. 

상속 계층도

모든 유닛들의 최고 조상은 Unit클래스이고 유닛의 종류는 지상 유닛(GroundUnit) 공중 유닛(AirUnit)으로 나누어집니다. 그리고 지상 유닛에는 Marine, SCV, Tank가 있고, 공중 유닛으로는 Dropship이 있습니다. SCV에게 Tank, Dropship과 같은 기계화 유닛을 수리할 수 있는 기능을 제공하기 위해 repair()를 정의한다면 다음과 같을 것입니다.

void repair(Tank tank) {
  // Tank를 수리한다.
}

void repair(Dropship dropship) {
  // Dropship을 수리한다.
}

이런 식으로 수리가 가능한 유닛의 개수만큼 다른 버전의 오버로딩된 메서드를 정의해야 할 것입니다. 이것을 피하기 위해 매개변수의 타입을 이 들의 공통 조상으로 하면 좋겠지만 Dropship은 공통조상이 다르기 때문에 공통조상의 타입으로 메서드를 정의한다고 해도 아래와 같이 최소한 2개의 메서드가 필요할 것입니다.

void repair(GroundUnit groundUnit) {
  // 지상유닛 수리
}

void repair(AirUnit airUnit) {
  // 공중유닛 수리
}

그리고 GroundUnit의 자손 중에는 Marine과 같이 기계화 유닛이 아닌 클래스도 포함될 수 있기 때문에 repair메서드 매개변수 타입으로 GroundUnit은 부적합합니다. 

 현재의 상속관계에서는 이들의 공통점이 없습니다. 때문에 인터페이스를 이용하여 기존의 상속체계를 유지하면서 이들 기계화 유닛에 공통점을 부여할 수 있습니다.

아래와 같이 Repairable이라는 인터페이스를 정의하고 수리가 가능한 기계화 유닛에게 이 인터페이스 구현하도록 하면 됩니다.

interface Repairable {
}

// 지상유닛 상속, 수리인터페이스 구현
class SCV extends GroundUnit implements Repairable {
    // ...
}

// 지상유닛 상속, 수리인터페이스 구현
class Tank extends GroundUnit implements Repairable {
    // ...
}

// 공중유닛 상속, 수리인터페이스 구현
class Dropship extends AirUnit implements Repairable {
    // ...
}

이제 이 3개의 클래스에는 같은 인터페이스를 구현했다는 공통점이 생겼습니다. 인터페이스 Repairable에 정의된 것은 아무것도 없고, 단지 인스턴스의 타입 체크에만 사용될 뿐입니다. Repairable인터페이스를 중심으로 상속계층도를 그려보면 다음과 같습니다.

상속 계층도

repair메서드 매개변수 r Repairable타입이기 때문에 인터페이스 Repairable에 정의된 멤버만 사용할 수 있습니다. 그러나 Repairable에는 정의된 멤버가 없으므로 이 타입의 참조 변수로는 할 수 있는 일은 아무것도 없습니다.

 그래서 instanceof연산자로 타입을 체크한 뒤 캐스팅하여 Unit클래스에 정의된 hitPoint와 MAX_HP를 사용할 수 있도록 하였습니다. 그다음엔 유닛의 현재 체력(hitPoint)이 유닛이 가질 수 있는 최고 체력(MAX_HP)이 될 때까지 체력을 증가시키는 작업을 수행합니다.

 Marine은 Repairable 인터페이스를 구현하지 않았으므로 SCV클래스의 repair메서드의 매개변수로 Marine을 사용하면 컴파일 시에 에러가 발생하게 됩니다.