본문 바로가기

Java

[Java] 인터페이스

추상 클래스

다형성의 활용에서 생길 수 있는 문제점들을 보완하기 위해서 추상 클래스와 추상 메서드를 사용해봅시다.

동물(Animal)과 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 추상 클래스라고 합니다.

추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스입니다. 

 

abstarct class AbstractAnimal {...}
  • 추상 클래스는 클래스를 선언할 때 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.
  • 추상 클래스는 기존 클래스와 완전히 같다. 다만 new AbstractAnimal( ) 와 같이 직접 인스턴스를 생성하지 못하는 제약이 추가된 것이다.

 

추상 메서드

부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있습니다.

이것을 추상 메서드라고 합니다. 추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드 입니다. 따라서 실체가

존재하지 않고, 메서드 바디가 없습니다.

public abstract void sound();

 

추상 메서드는 선언할 때 메서드 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 된다.

 

추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다. 

  • 그렇지 않으면 컴파일 오류가 발생한다. 
  • 추상 메서드는 메서드 바디가 없다. 따라서 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있다. 따라서 직접 생성 하지 못하도록 추상 클래스로 선언해야 한다.

추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다. 

  • 그렇지 않으면 컴파일 오류가 발생한다.
  • 추상 메서드는 자식 클래스가 반드시 오버라이딩 해야 하기 때문에 메서드 바디 부분이 없다. 바디 부분을 만들면 컴파일 오류가 발생한다.
  • 오버라이딩 하지 않으면 자식도 추상 클래스가 되어야 한다.

추상 메서드는 기존 메서드와 완전히 같습니다.

다만 메서드 바디가 없고, 자식 클래스가 해당 메서드를 반드시 오버라이딩 해야 한다는 제약이 추가된 것입니다.

 

추상 클래스와 추상 메서드를 사용한 예제를 보겠습니다.

public abstract class AbstractAnimal { // 추상 클래스를 선언

    public abstract void sound(); // 추상 메서드를 선언, 자식 메서드에서 무조건 오버라이딩 해야함

    public void move() {
        System.out.println("동물이 움직입니다.");
    }
}
  • AbstractAnimal 은 abstarct가 붙은 추상 클래스이다. 이 클래스는 직접 인스턴스를 생성할 수 없다.
  • sound( )은 abstract가 붙은 추상 메서드이다. 이 메서드는 자식이 반드시 오버라이딩 해야 한다.

이 클래스는 move( )라는 메서드를 가지고 있는데, 이 메서드는 추상 메서드가 아닙니다.

따라서 자식 클래스가 오바리이딩 하지 않아도 됩니다.

public class Dog extends AbstractAnimal{

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

public class AbstractMain {

    public static void main(String[] args) {
        //추상 클래스 생성 불가
        //AbstractAnimal abstractAnimal = new AbstractAnimal();

        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();
        
        AbstractAnimal[] animals = {dog, cat, caw};

        for (AbstractAnimal animal : animals) {
            soundAnimal(animal);
        }
    }
    private static void soundAnimal(AbstractAnimal abstractAnimal) {
        abstractAnimal.sound();
        abstractAnimal.move();
    }
}
  • 추상 클래스는 생성이 불가능하다.
  • AbstractAnimal이 추상이여서 인스턴스 생성이 불가능하다.
  • 추상 메서드는 반드시 오버라이딩 해야 한다. 만들지 않는다면 컴파일 오류가 발생한다.

이런 제약들을 제외하고 나머지는 모두 일반적인 클래스와 동일합니다. 

추상 클래스는 제약이 추가된 클래스일 뿐이라는 것 입니다.

메모리 구조, 실행 결과 모두 동일합니다.

 

정리

  • 추상 클래스는 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지해준다.
  • 추상 메서드는 새로운 동물의 자식 클래스를 만들때 실수로 오버라이딩 하지 않을 문제를 방지해준다.

 

 

순수 추상 클래스: 모든 메서드가 추상 메서드인 클래스

 

앞서 만든 예제에서 move( )도 추상 메서드로 만들어야 한다고 가정해봅시다.

이 경우 AbstractAnimal 클래스의 모든 메서드가 추상 메서드가 됩니다.

이런 클래스를 순수 추상 클래스라고 합니다.

 

move( )가 추상 메서드가 되었으니 자식들도 move( ) 를 오버라이딩 해야 합니다.

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}
public class Dog extends AbstractAnimal{
    @Override
    public void sound() {
        System.out.println("멍멍");
    }

    @Override
    public void move() {
        System.out.println("개 이동");
    }
}

public class AbstractMain {

    public static void main(String[] args) {

        AbstractAnimal[] animals = {new Dog(), new Cat(), new Caw()};

        for (AbstractAnimal animal : animals) {
            soundAnimal(animal);
        }
    }
    private static void soundAnimal(AbstractAnimal abstractAnimal) {
        abstractAnimal.sound();
        abstractAnimal.move();
    }
}

 

순수 추상 클래스

 

모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없습니다.

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}

 

이러한 순수 추상 클래스는 실행 로직을 전혀 가지고 있지 않고, 단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공합니다.

 

순수 추상 클래스는 다음과 같은 특징을 가집니다.

  • 인스턴스를 생성할 수 없다.
  • 상속시 자식은 모든 메서드를 오버라이딩 해야 한다.
  • 주로 다형성을 위해 사용된다.

상속하는 클래스는 모든 메서드를 구현해야 한다.

"상속시 자식은 모든 메서드를 오버라이딩 해야 한다" 라는 특징은 상속 받는 클래스 입장에서 보면 부모의 모든 메서드를 

구현해야 하는 것 입니다. 이런 특징을 잘 생각해보면 순수 추상 크래스는 마치 어떤 규격을 지켜서 구현해야 하는 것 처럼

느껴집니다. 

규격에 맞춰서 만들어야 인터페이스에 들어감

 

이것은 우리가 일반적으로 이야기하는 인터페이스와 같이 느껴집니다. 예를 들어 USB 인터페이스를 생각해봅시다.

USB 인터페이스는 분명한 규격이 있습니다. 이 규격에 맞추어 제품을 개발해야 연결이 가능합니다.

순수 추상 클래스가 USB 인터페이스 규격이라고 한다면 USB 인터페이스에 맞추어 마우스 키보드 같은 연결 장치를

구현할 수 있습니다.

 

이런 순수 추상 클래스의 개념은 프로그래밍에서 매우 자주 사용합니다.

자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공하고 있습니다.

 

인터페이스

 

순수 추상 클래스

public abstract class AbstractAnimal {
    public abstract void sound();
    public abstract void move();
}

 

인터페이스는 class가 아니라 interface 키워드를 사용하면 됩니다.

 

인터페이스

public interface InterfaceAnimal {
    public abstract void sound();
    public abstract void move();
}

 

인터페이스 - public abstract 키워드 생략 가능

public interface InterfaceAnimal {
    void sound();
    void move();
}

 

순수 추상 클래스는 다음과 같은 특징을 가집니다.

  • 인스턴스를 생성할 수 없다.
  • 상속 시 모든 메서드를 오버라이딩 해야 한다.
  • 주로 다형성을 위해 사용된다.

인터페이스는 순수 추상 클래스와 같습니다. 여기에 약간의 편의 기능이 추가됩니다.

  • 인터페이스의 메서드는 모두 public, abstract 이다.
  • 메서드에 public abstract를 생략할 수 있다. 생략이 권장된다.
  • 인터페이스는 다중 구현(다중 상속)을 지원한다.

 

여담으로 개발자들은 인터페이스를 보고 "내가 여기 있는걸 다 구현해야 겠네." 라고 생각한다고 합니다.

 

인터페이스와 멤버 변수

public interface AbstractAnimal {
    public static final int MY_PI = 3.14;
}

 

인터페이스는 빈 껍데기일 뿐인데 멤버 변수를 가지고 있다면 이상하지 않나요?

멤버 변수를 쓸 수 있긴하지만 상수 정도값만을 허용합니다.

 

인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주합니다. final은 변수의 값을 한번 설정하면 수정할 수 없습니다. 자바에서 static final을 사용해 정적이면서 고칠 수 없는 변수를 상수라 하고, 관례상 상수는 대문자에 언더스코어로 구분합니다.(스네이크 기법)

 

해당 키워드는 다음과 같이 생략할 수 있습니다. (생략 권장)

public interface AbstractAnimal {
    int MY_PI = 3.14;
}

 

 

 

클래스 상속 관계는 UML에서 실선을 사용하지만, 인터페이스 구현(상속) 관계는 UML에서 점선을 사용합니다.

또한, 인터페이스와 상속 관계는 구현 관계라고 표현합니다.

 

public interface InterfaceAnimal {
    void sound(); // public abstarct 생략
    void move();
}

 

인터페이스는 class 대신에 interface로 선언하면 됩니다.

sound( ), move( ) 메서드 앞에 public abstract 가 생략되어 있습니다.

따라서 상속 받는 곳에서 모든 메서드를 오버라이딩 해야 합니다.

public class Dog implements InterfaceAnimal{

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
    @Override
    public void move() {
        System.out.println("개 이동");
    }
}

 


public class InterfaceMain {

    public static void main(String[] args) {
        //인스턴스 생성 불가
        //InterfaceAnimal interface1 = new InterfaceAnimal(); 
            
        InterfaceAnimal[] interfaceAnimals = {new Dog(), new Cat(), new Caw()};

        for (InterfaceAnimal interfaceAnimal : interfaceAnimals) {
            soundAnimal(interfaceAnimal);
        }
    }
    private static void soundAnimal(InterfaceAnimal interfaceAnimal) {
        interfaceAnimal.sound();
        interfaceAnimal.move();
    }
}

 

앞에서 설명하던 순수 추상 클래스와 거의 유사합니다.  순수 추상 클래스가 인터페이스가 되었을 뿐입니다.

 

클래스, 추상 클래스, 인터페이스는 모두 같다.

  • 클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같다. 모두 자바에서는 .class로 다루어진다. 인터페이스를 작성할 떄도 .java에 인터페이스를 정의한다.
  • 인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 된다.
상속 VS 구현

부모 클래스의 기능을 자식 클래스가 상속 받을때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현합니다. 이렇게 서로 다르게 표현하는 이유는 상속은 이름
그대로 부모의 기능을 물려 받는 것이 목적인 반면 인터페이스는 모든 메서드가 추상 메서드입니다.
따라서 물려 받을수 있는 기능이 없고,오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 합니다. 따라서 구현한다고 표현합니다. 인터페이스는 메서드 이름만 있는 설계도 이고, 이 설계도가
실제로 어떻게 작동하는지 하위 클래스에서 모두 구현해야 합니다. 그래서 인터페이스의 경우 상속이 아니라
해당 인터페이스를 구현한다고 표현합니다.

 

인터페이스를 사용해야 하는 이유

모든 메서드가 추상 메서드인 경우 순수 추상 클래스를 만들어도 되고, 인터페이스를 만들어도 됩니다.

그런데 왜 인터페이스를 사용해야 하는 걸까요? 단순히 편리하다는 이유를 넘어서 다음과 같은 이유가 있습니다.

  • 제약: 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 약속(제약)을 주는 것이다. USB 인터페이스에 맞추어 키보드, 마우스를 개발해야 작동이 되듯이 인터페이스의 약속은 반드시 구현해야 한다. 그런데 순수 추상 클래스인 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워넣을 수도 있다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 더는 순수 추상 클래스가 아니게 될 수도 있다. 인터페이스는 모든 메서드가 추상 메서드이기 때문에, 이런 문제를 사전에 차단할 수 있는 효과를 볼 수 있다.
  • 다중 구현: 자바에서 클래스 상속은 부모를 하나만 지정할 수 있다. 반면에 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능하다.
참고
자바8 에 등장한 default 메서드를 사용하면 인터페이스 메서드를 구현할 수 있습니다. 하지만 이것은 예외적으로
아주 특별한 경우에만 사용해야 합니다. 자바9 에서 등장한 인터페이스의 private 메서드도 마찬가지 입니다. 

 

 

인터페이스 - 다중 구현

자바가 다중 상속을 지원하지 않는 이유

 

자바는 다중 상속을 지원하지 않습니다. 그래서 extend 대상은 하나만 선택할 수 있습니다.

이 말의 뜻은 부모가 또 부모를 가지는 것은 괜찮지만 부모는 하나만 선택할 수 있다는 뜻 입니다.

 

앞서 설명한 클래스는 다중 상속을 허용하지 않는데, 인터페이스의 다중 구현은 왜 허용했을까요??

인터페이스는 모두 추상 메서드로 이루어져 있기 때문입니다.

 

 

 

InterfaceA, InterfaceB 는 둘다 같은 methodCommon( ) 을 가지고 있습니다. 그리고 Child 는 두 인터페이스를 구현했습니다. 상속 관계의 경우 두 부모 중에 어떤 한 부모의 methodCommon( )을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생합니다. 하지만 인터페이스 자신은 구현을 가지지 않습니다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 합니다. 여기서 InterfaceA, InterfaceB는 같은 이름의 methodCommon( )을 제공하지만 이것의 기능을 Child가 구현해야 합니다. 그리고 어차피 오버라이딩에 의해 Child 에 있는 methodCommon( )이 호출됩니다.

결과적으로 두 부모 중에 어떤 한 부모의 methodCommon( )을 선택하는 것이 아니라 그냥 인터페이스를 구현한

Child에 있는 methodCommon( ) 이 사용되게 됩니다. 이런 이유로 인터페이스는 다이아몬드 문제가 발생하지 않습니다.

따라서 인터페이스의 경우 자바에서 다중 구현을 허용하고 있습니다.

 

public interface InterfaceA {
    void methodA();
    void methodCommon();
}
public interface InterfaceB {
    void methodB();
    void methodCommon();
}
public class Child implements InterfaceA, InterfaceB{

    @Override
    public void methodA() {
        System.out.println("Child.methodA");
    }
    @Override
    public void methodB() {
        System.out.println("Child.methodB");
    }
    @Override
    public void methodCommon() {
        System.out.println("Child.methodCommon");
    }
}
  • implements InterfaceA, InterfaceB 와 같이 다중 구현을 할 수 있다.
  • methodCommon( ) 의 경우 양쪽 인터페이스에 다 있지만 같은 메서드 이므로 구현은 하나만 하면 된다.
public class DiamondMain {

    public static void main(String[] args) {

        InterfaceA a = new Child();
        a.methodA();
        a.methodCommon();

        InterfaceB b = new Child();
        b.methodB();
        b.methodCommon();
    }
}

 

  1. a.methodCommon( ) 을 호출하면서 먼저 x001 Child 인스턴스를 찾는다.
  2. 변수 a가 InterfaceA 타입이므로 해당 타입에서 methodCommon( )을 찾는다.
  3. methodCommon( )은 하위타입인 Child 에서 오버라이딩 되어있다. 따라서 Child의 methodCommon( ) 이 호출된다.

  1. b.methidCommon( )을 호출하묜서 먼저 x002 Child 인스턴스를 찾는다.
  2. 변수 b가 InterfaceB 타입이므로 해당 타입에서 methodCommon( )을 찾는다.
  3. methodCommon( ) 은 하위 타입인 Child 에서 오버라이딩 되어 있다. 따라서 Child의 methodCommon( ) 이 호출된다.

 

클래스와 인터페이스 활용

그렇다면 이번엔 클래스 상속과 인터페이스 구현을 함께 사용해봅시다!

 

AbstractAnimal 은 추상 클래스 입니다.

  • sound( ): 동물의 소리를 내기 위한 sound( ) 추상 메서드를 제공한다.
  • move( ): 동물의 이동을 표현하기 위한 메서드 이다. 이 메서드는 추상 메서드가 아니다. 상속을 목적으로 사용된다.

Fly 는 인터페이스다. 나는 동물은 이 인터페이스를 구현할 수 있다.

  • Bird, Chicken은 날 수 있는 동물이다. fly( ) 메서드를 구현해야 한다.
public abstract class AbstractAnimal {
    
    public abstract void sound();
    public void move() {
        System.out.println("동물이 이동합니다.");
    }
}

 

public interface Fly {
    void fly();
}
public class Dog extends AbstractAnimal{

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
public class Bird extends AbstractAnimal implements Fly{

    @Override
    public void sound() {
        System.out.println("짹짹짹");
    }
    @Override
    public void fly() {
        System.out.println("새가 날고 있습니다.");
    }
}

 

Bird는 AbstractAniaml 클래스를 상속하고 Fly 인터페이스를 구현한다.

 

하나의 클래스 여러 인터페이스 예시

public class Bird extends AbstractAnimal implements Fly, Swim {...}

 

extend를 통한 상속은 하나만 할 수 있고 implements 를 통한 인터페이스는 다중 구현을 할 수 있기 때문에

둘이 함께 나온 경우 extend가 먼저 나와야 합니다.

public class Chicken extends AbstractAnimal implements Fly{

    @Override
    public void sound() {
        System.out.println("꼬끼오 꼬꼬!");
    }
    @Override
    public void fly() {
        System.out.println("닭이 날고 있습니다!");
    }
}
public class SoundFlyMain {

    public static void main(String[] args) {

        Dog dog = new Dog();
        Bird bird = new Bird();
        Chicken chicken = new Chicken();

        soundAnimal(dog);
        soundAnimal(bird);
        soundAnimal(chicken);
        FlyAnimal(bird);
        FlyAnimal(chicken);
    }
    private static void soundAnimal(AbstractAnimal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
    private static void FlyAnimal(Fly fly) { // Fly fly = new Bird(), new Chicken();
        System.out.println("날기 테스트 시작");
        fly.fly();
        System.out.println("날기 테스트 종료");
    }
}

 

이런 식으로 클래스의 상속과 인터페이스의 구현을 활용하여 코드를 만들 수도 있습니다.