• 카테고리

    질문 & 답변
  • 세부 분야

    프로그래밍 언어

  • 해결 여부

    해결됨

추상클래스 (abstract class)와 인터페이스(interface)의 최적의 쓰임?

23.03.20 07:09 작성 조회수 2.71k

0

안녕하세요 나도코딩 선생님...ㅎ

몇 주전에 나도코딩 자바편 강의를 완강하고 다시 2회차로 강의를 듣는 중입니다...ㅎ

추상 클래스 (abstract class)와 인터페이스(interface) 관련 강의를 들으면서 각각의 특징들 및 차이점들에 대해서 다시 조금씩 알아가고 있는데... (예를 들면, 추상클래스는 abstract 키워드를 가지고, abstract메소드를 가지고 있어서 객체를 생성할 수 없는 반면, 인터페이스(interface)는 보통 -able 키워드, 변수 X, 생성자 X, 오로지 메소드만 있다 등)

이 둘, 그러니까 추상 클래스 (abstract class)와 인터페이스(interface)는 '어느 때 (또는 어느 시점)'에 활용하는게 가장 적절한지 디테일하게 알 수 없을까요?

항상 좋은 강의와 답변 감사합니다...ㅎ

 

답변 1

답변을 작성해보세요.

0

※ 과거에 있었던 유사한 질문에 대한 답변글 이후에 새로운 내용을 추가하였습니다. 참고 부탁드려요 😉https://www.inflearn.com/questions/750857

안녕하세요?
추상클래스를 상속하거나 인터페이스를 구현하는 클래스는 반드시 각각에 선언된 추상메소드를 구현해야 합니다. 이렇게만 보면 둘은 비슷한데 왜 굳이 구분했는지 조금 의아할 수 있습니다.

예를 들어볼게요.
취업 준비생들끼리 팀을 이루어 면접 스터디를 진행한다고 가정하겠습니다. 그 중에 한명이 토론 면접에서 높은 점수를 받기 위한 팁을 알아왔다며 팀원들에게 아래와 같이 공유하였습니다.


토론 면접 팁 : 상대방의 발언이 끝나면 반드시 '잘 들었다'는 멘트를 먼저 하기

예1)
(필수) XXX 님의 말씀 잘 들었습니다.
(반론) 저는 생각이 조금 다른데요 ...

예2)
(필수) XXX 님의 말씀 잘 들었습니다.
(반론) 그 부분에 대해서는 저도 일부 동의하지만...


그러면 팀원들은 이 내용을 잘 기억해뒀다가 토론 면접에서 반드시 (필수) 멘트를 먼저 하고 나서 상대방에게 반론을 하게 될 것입니다. 여기서 (필수) 멘트에 해당하는 것은 모든 반론 앞에 공통적으로 들어가는 부분인데 이를 클래스로 만들면 아마 이렇게 추상 클래스로 만들 수 있을 겁니다.

public abstract class 토론면접 {
    public void 필수() {
        System.out.println("XXX 님의 말씀 잘 들었습니다.");
    }
    public abstract void 반론();
}

그리고 이 클래스를 상속하는 2개의 클래스는 다음과 같이 반론 메소드를 만들 수 있겠죠.

public class 토론면접_예1 extends 토론면접 {
    @Override
    public void 반론() {
        System.out.println("저는 생각이 조금 다른데요 ...");
    }
}

public class 토론면접_예2 extends 토론면접 {
    @Override
    public void 반론() {
        System.out.println("그 부분에 대해서는 저도 일부 동의하지만...");
    }
}

이 클래스들로부터 만들어진 객체를 이용할 때는 이런 순서로 각 메소드를 호출하게 될 것입니다.

토론면접객체.필수(); // 필수 멘트
토론면접객체.반론(); // 반론 멘트

그러면 필수 멘트는 기본으로 하고 반론 멘트를 이어서 하게 되겠죠.
이처럼 추상 클래스는 공통되는 부분을 모아서 추상 클래스에 정의하고, 그 외의 부분을 자식 클래스에서 확장하여 사용하는 개념으로 보시면 좋습니다. 단, 일반 상속과는 다르게 반드시 구현을 필요로 하는 추상 메소드가 존재할 수 있으므로 추상 클래스만으로는 객체를 생성할 수 없지요.

이번에는 다른 예를 들어볼까요? 우리 주변에서 카페는 굉장히 많이 찾을 수 있습니다. 카페에 가면 먼저 음료를 주문하고 자리에 착석 또는 근처에서 대기하다가 음료가 나오면 받아서 자리로 이동 후 한동안 시간을 가지고 나서 음료를 반납하고 퇴장합니다. 고객 입장에서는 이렇지만 카페 직원 입장은 순서가 조금 다를 수 있는데, 간단히 아래와 같다고 해보겠습니다.


1) 음료 주문
2) 음료 제작
3) 주문 고객 호출


1) 먼저 고객으로부터 음료를 주문받습니다. 그런데 카페마다 주문받는 방법은 다를 수 있겠죠. 직원분이 직접 주문을 받을 수도 있고 키오스크를 통해서 주문받도록 할 수도 있고 모바일 앱을 통해 주문받을 수도 있을 겁니다. 어찌됐건 음료를 주문받는다는 건 동일하죠.
2) 그 다음에는 음료를 제작합니다. 음료 제작 방법도 다양한데요. 이미 다 되어 있는 음료를 컵에 따라주기만 할 수도 있고 기계를 이용해서 즉석으로 음료를 만들 수도 있고 드립 커피 등등 주문 음료마다, 가게마다 방식이 조금씩 다를겁니다. 어찌됐건 음료를 제작한다는 건 동일하죠.
3) 음료가 준비되면 주문 고객을 호출합니다. 주문 고객 호출 방법도 다양한데요. 커다란 TV 에 주문번호를 보여줄 수도 있고 진동벨을 울릴 수도 있고 모바일 앱으로 안내할 수도 있고 직접 직원분이 큰 목소리로 고객을 호출할 수도 있을 겁니다. 어찌됐건 주문 고객을 호출한다는 건 동일하죠.

이처럼 음료 주문, 제작, 고객 호출의 방법은 카페마다 서로 다를 수 있지만 어찌됐건 음료를 주문받고, 주문받은 음료를 제작하고, 고객을 호출한다는 것은 같습니다. 즉, 카페마다 공통적으로 뭔가를 하는 것 대신 각자의 방식대로 같은 목적을 달성하는거죠. 이 상황을 인터페이스로 만들면 이렇게 될 수 있겠네요.

public interface 카페 {
    void order(); // 음료 주문
    void make(); // 음료 제작
    void call(); // 주문 고객 호출
}

그러면 이 인터페이스를 구현하는 클래스는 저마다의 방식으로 동작을 정의할 겁니다.

public class 카페_예1 implements 카페 {
    @Override
    public void order() {
        System.out.println("키오스크로 주문받습니다.");
    }

    @Override
    public void make() {
        System.out.println("기계로 음료를 제작합니다.");
    }

    @Override
    public void call() {
        System.out.println("진동벨로 안내합니다.");
    }
}

public class 카페_예2 implements 카페 {
    @Override
    public void order() {
        System.out.println("직원분이 직접 주문받습니다.");
    }

    @Override
    public void make() {
        System.out.println("직원분이 직접 음료를 제작합니다.");
    }

    @Override
    public void call() {
        System.out.println("직원분이 직접 호출합니다.");
    }
}

서로의 구체적인 동작은 다르지만 적어도 '카페' 라는 인터페이스를 구현하면 모두 동일한 목적을 가지는 order(), make(), call() 이라는 동작이 준비가 되겠지요. 이처럼 인터페이스는 이를 구현하는 모든 클래스들이 저마다의 방식으로 같은 동작을 약속합니다. 그리고 추상클래스와는 다르게 인터페이스는 다중상속이 가능하죠.


정리해보면,
추상 클래스는 객체로 생성될 수 없는 클래스로 자식 클래스에서 확장될 수 있도록 만들어진 클래스입니다. 추상 클래스에는 추상 메소드와 일반 메소드를 모두 포함시킬 수 있으며 인스턴스 변수도 가질 수 있지요. 보통 기본적인 구현은 추상 클래스에서 제공하고 하위 클래스에서는 고유의 동작을 확장하기 위해 사용합니다.
인터페이스는 스펙이 정의된 메소드들의 집합입니다. 인터페이스를 구현하는 클래스는 반드시 이 메소드들의 동작을 정의해야 하며, 해당 클래스는 동일한 사용 방법과 동작을 보장할 수 있습니다. 이를 통해 다형성이 가능해집니다. 인터페이스는 보통 클래스 간 상속 관계가 없는 다른 클래스가 동일한 메소드를 구현해야 할 때 사용되는데, 때로는 단순히 클래스의 타입을 구분짓기 위해 사용되기도 합니다.

동물을 예로 들어볼까요?
모든 동물은 먹기도 하고 자기도 합니다. 잠은 모두 똑같이 자는데 동물마다 먹는 음식은 서로 다를 거예요. 그러면 먼저 기본적인 동물의 동작은 이렇게 추상 클래스를 이용하여 정의할 수 있을 겁니다.

public abstract class Animal {
    public abstract void eat();
    public void sleep() {
        // 일반 메소드 동작 정의
    }
}

그런 다음에 이 Animal 클래스를 확장하여 개, 고양이, 물고기 등으로 확장할 수 있겠네요. 그러면 각 동물들이 먹는 기능을 이렇게 따로 정의할 수 있을 겁니다.

public class Dog extends Animal {
    public void eat() {
        System.out.println("소시지");
    }
}

public class Cat extends Animal {
    public void eat() {
        System.out.println("참치캔");
    }
}

public class Fish extends Animal {
    public void eat() {
        System.out.println("사료");
    }
}

기본적인 구현은 Animal 에서, 고유의 동작은 Dog, Cat, Fish 에서 정의되었습니다.

이번에는 같은 동물인데 인터페이스를 활용해볼게요. 간단히 소리를 내는 하나의 추상 메소드만 정의하겠습니다.

public interface Audible {
    void makeSound();
}

그런 다음에 각 동물에 대해 소리를 내는 기능을 추가한 뒤 makeSound() 메소드를 정의하겠습니다.

public class Dog extends Animal implements Audible {
    ...

    public void makeSound() {
        System.out.println("멍");
    }
}

public class Cat extends Animal implements Audible {
    ...

    public void makeSound() {
        System.out.println("냐옹");
    }
}

그러면 강아지와 개는 소리를 내는 기능이 더해졌고 각 클래스 내에서 어떤 소리를 내는지 정의하였습니다. 그런데 물고기는 어떨까요? 물고기는 소리를 따로 내지 않습니다. 그러면 Audible 이라는 인터페이스를 구현하는 건 어울리지 않겠네요. 별도의 인터페이스를 따로 하나 만들어보겠습니다. 알아보기 쉽게 이름을 짓고 메소드는 따로 추가하지 않을게요.

public interface SilentAnimal {

}

그리고 Fish 클래스에 이를 구현하도록 해볼게요.

public class Fish extends Animal implements SilentAnimal{
    public void eat() {
        System.out.println("사료");
    }
}

사실 굳이 Fish 클래스에 이렇게 인터페이스를 정의하지 않아도 되기는 하지만 인터페이스를 정의함으로써 소리를 내는 동물과 구분을 지을 수 있습니다. 그리고 main() 메소드에서는 이런 식으로 활용이 가능해지죠.

Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Fish();

for (Animal animal : animals) {
    if (animal instanceof Audible) {
        ((Audible) animal).makeSound(); // 개, 고양이
    } else if (animal instanceof SilentAnimal){
        System.out.println("소리를 내지 않는 동물"); // 물고기
    }
}

실행 결과는 이렇습니다.

멍
냐옹
소리를 내지 않는 동물

설명을 길게 적었지만 사실 추상 클래스와 인터페이스를 마치 공식처럼 '이럴 땐 추상 클래스, 저럴 땐 인터페이스' 라고 콕 찝어서 말하기는 다소 어려운 부분이 있는 것 같습니다. 지금으로서는 다양한 프로젝트 경험을 쌓아가면서 어떤 게 적합한지 감을 익혀나가는 게 가장 좋은 방법이라고 생각됩니다 😊
감사합니다.

kmr345님의 프로필

kmr345

2023.03.21

default method를 사용하는건 어떤가요?