[Java] Abstract class vs Interface

얄루얄루·2022년 11월 22일
0

Java

목록 보기
7/7

Abstract class

추상 클래스는 객체 생성을 할 수 없는 상속 전용 클래스이다.

클래스 내에 1개 이상의 추상 메소드가 존재할 시 추상 클래스로 분류가 된다.

객체 생성을 할 수 없다는 점을 제외하면 일반 클래스와 기능적으로 같다.

Interface

모든 메소드가 추상 메소드이다.

단, Java 8 이후로는 default 키워드를 사용하여 일반 메소드의 구현도 가능하다.
(당연히 구현된 클래스에선 이 일반 메소드의 사용이 된다)

공통점

둘 다 추상 메소드를 구현하여 다형성을 지원하기 위해 사용이 된다.

차이점

구성 측면에서는,

추상 클래스는 멤버 변수를 가질 수 있고, 일반 메소드도 멤버로 가질 수 있다.

인터페이스는 일반 메소드를 가질 수 없으며 멤버 변수는 가질 수 있지만 final로 선언되어야만 한다.

사용성 측면에서 보자면,

추상 클래스의 의미는 해당 클래스를 상속하여 포함된 기능을 이용 및 확장하는 것에 있다.

인터페이스의 의미는 해당 인터페이스를 구현한 클래스 간의 동일한 형태의 함수가 존재함 (즉, 해당 기능의 존재)를 보장하는 것에 있다.

필요성

두 가지를 다루다 보면, 이게 굳이 필요한가 싶을 때가 확실히 있긴 하다.

둘 중 하나만 있어도 딱히 문제 없을 것 같고.

예를 들어, 추상 클래스에서 추상 메소드 부분을 인터페이스로 뺀 후에 일반 클래스로 만들고 상속한 다음, 아까 뺀 인터페이스를 받아 구현해도 동일 결과가 나온다.

그렇기에 [추상 클래스]의 의미는 [공통된 값과 기능을 확실하게 가지는 분류]를 만들어 내는 것에 있다.

반면, [인터페이스]의 의미는 [구현된 클래스 모두에 공통된 기능이 있음을 보장]하는 것에 있다.

예시

예를 들면, 생물에는 동물과 식물이 있다.

여기서 공통된 값은 뭐가 있을까? 일단 수명이 있겠다. 탄생과 소멸의 기능도 있고, 번식의 기능도 있다.

그럼 생물을 추상 클래스로 만들면 어찌 될까?

public abstract class Creature {
    int age;
    int life;

    void create(){
        System.out.println("생성됨");
    }
    void destroy(){
        if(age < life){
            return;
        }
        System.out.println("소멸됨");
    }
    abstract void eat(); // 섭식
    abstract void breed(); // 번식
//        void breed(){
//        System.out.println("번식함");
//    }
}

대충 이런 느낌이 될 것이다.

eat, breed 메소드를 추상화 한 이유는 종족별로 섭식, 번식법이 다르니까 그렇게 해봤다.

주석된 부분처럼 해당 메소드들을 굳이 추상 메소드로 만들지 않을 수도 있다.

그러면 이 클래스를 추상 클래스로 만들 필요도 없어진다.

아니면 아까 말한 것처럼 추상 메소드 부분만 인터페이스로 빼내는 방법도 있다.

다만 이걸 일반 클래스로 만들었을 때, 제대로 써먹을 수 있느냐 하면 그건 또 아니다.

태어나고, 죽고, 먹고, 번식하고. 이것 밖에 못하는 객체를 대체 어디다 써먹는단 말인가?

그러니까 분류와 특정 기능의 보장을 위한 클래스다.

이걸 기본으로 해서 동물/식물 클래스를 만들어 보려는데, 그 전에 이걸 좀 보자.

대충 그려본 기능적인 분류다.

알고 보니 여기가 판타지 세계라 공격이 가능한 식물이 있다고 하자.

그리고 역시 공격이 불가능한 동물도 있다고 하자.

다만, 모든 동물은 이동 가능하고, 모든 식물은 이동 불가능하다.

그럼 4개의 클래스가 필요하다.

공격 가능, 이동 가능한 동물
공격 불가능, 이동 가능한 동물
공격 가능, 이동 불가능한 식물
공격 불가능, 이동 불가능한 식물

이렇게 4가지의 클래스를 만들어 각각 관리해도 기능적인 문제는 전혀 없다.

다만, 호환적인 부분에서 곤란해진다.

예를 들면, 생물1이 공격을 한다.

그러면 공격 기능을 실행해야 할 텐데,

종족마다 공격력이 다르므로 공격자의 종족을 알아야 한다.

이 때 공격자를 어느 클래스로 받아야 하는가? 동물? 식물?

당연하지만 동물 밑에는 인간, 엘프, 드워프, 곰, 호랑이 등의 여러 클래스들이 존재할 예정이며, 식물 쪽도 마찬가지다.

이걸 전부

if(공격자 instanceof 인간) {
	인간.공격(10);
} else if(공격자 instanceof 호랑이){
	호랑이.공격(50);
}

이런 식으로 비교해서 실행해야 할까? 생노가다가 따로 없다.

게다가 어떤 종족은 공격이 불가능하다는 걸 생각해 보면, 휴먼 에러의 발생 가능성도 높아진다.

그러니까 일단은 분류 별로 완벽하게 동일한 기능을 가지고 있는 이동에 대해서만 처리하자.

public abstract class Animal extends Creature {
    int numOfLegs;
    public void move(){
        System.out.println(numOfLegs + "발로 이동함");
    }
}
public abstract class Plant extends Creature {
	// 이동에 대해서만 말하고 있어서 비어있지만, 광합성 등이 들어갈 수 있다.
}

동물이 가지는 공통 기능, 식물이 가지는 공통 기능의 작성이 끝났다.

이제 그 하위 분류이며 실제로 쓰일 객체를 작성하고, 공격 부분도 해결해야 한다.

어떤 클래스에는 있는데, 어떤 클래스에는 없고, 적어도 해당 기능을 가지는 클래스들 사이에서는 동일한 기능을 보장해야 하는 것이 필요할 때 바로 인터페이스를 꺼낸다.

public interface Attackable {
    void attack();
    default void attack(int damage) {
        System.out.println(damage + "의 세기로 공격함");
    }
}
public class Human extends Animal implements Attackable {
    int power = 10;

    public Human() {
        numOfLegs = 2;
    }

    @Override
    public void attack() {
        Attackable.super.attack(power);
    }

    @Override
    void eat() {
        System.out.println("잡식을 함");
    }

    @Override
    void breed() {
        System.out.println("번식을 함");
    }
}

사실 위 예제는 구현이 조금 어색하긴 하다.

억지로 default 메소드를 쓴 것에 가깝다.

아무튼 이를 이용해 테스트 코드를 작성해보자

public class Main {
    static void printFunctionality(Creature creature){
        creature.create();
        if(creature instanceof Animal){
            Animal animal = (Animal) creature;
            animal.move();
        }
        if(creature instanceof Attackable){
            ((Attackable)creature).attack();
        }
    }
    public static void main(String[] args) {
        printFunctionality(new Human());
    }
}
//결과
생성됨
2발로 이동함
10의 세기로 공격함

조금 억지스럽고 더러운 코드이긴 한데, 메소드로 빼놓으면 그럭저럭 봐 줄만 하다.

여기서 살펴보고자 한 건,

생성된 객체를 추상 클래스를 이용해 업캐스팅하여 받았다는 것. 이 때문에 동식물을 가리지 않고 처리가 가능하다.

이후는 다운캐스팅을 통해 이동 기능이 있는 동물 클래스로 분류된다면 이동 기능 실행을,

공격 기능이 있는 클래스라면 공격 기능을 실행하게 했다.

printFunctionality()의 인자로 나중에 new Tiger()나 new Elf() 따위가 들어가도 아무 문제 없이 호환이 되는 구조다.

이런 식의 구조를 만드는 것은

나중에 어느 한 쪽에 수정을 가할 때, 그 부분에 의존하는 즉, 영향을 받는 모든 부분을 일일이 다 수정해야만 하는 참사를 막는 방지책이 될 수 있다.

profile
시간아 늘어라 하루 48시간으로!

0개의 댓글

관련 채용 정보