티스토리 뷰

우리가 프로그래밍을 할때 추상 클래스(Abstract Class)와 인터페이스(Interface)의 차이를 명확히 모르는 경우가 많습니다. 둘다 '하나 이상의 추상 메서드를 가지고있어야 한다' 라는 것을 알고있지만

"그래서 둘의 차이점은 뭐고 언제 추상클래스를 사용하고 인터페이스를 사용해야하는건데?" 라는 궁금증이 있으실겁니다.

 

이번 글에서는 추상 클래스와 인터페이스가 무엇인지 간단하게 살펴보고

이 둘의 차이점을 예시를 통해 완벽하게 이해할 수 있으실 겁니다.

 

그리고 실제로 면접 질문에도 자주 나오는 내용이라고 합니다?

(저는 취준생이라 잘 모릅니다..ㅎㅎ)


추상 클래스(Abstract Class)

 

추상 클래스는 클래스(class) 앞에 'abstract' 키워드를 사용하여 정의하며, 하나 이상의 추상 메서드를 가지고 있거나 abstract로 정의가 된 클래스를 말합니다. 

추상 메서드를 선언하여 상속을 통해서 하위 클래스에서 반드시 구현하도록 강제하는 클래스입니다.

 

추상 클래스는 추상 메서드를 포함하고 있다는 점을 제외하면 일반 클래스와 모든 점이 동일합니다.

(추상 메서드뿐 아니라 생성자, 필드(멤버 변수), 일반 메서드가 포함 가능하다.)

 

아래는 추상 클래스의 특징을 간단하게 설명해놓았습니다.

  • 추상 클래스는 인스턴스, 즉 객체를 만들수 없는 클래스입니다. 즉, 추상 클래스는 new 키워드로 객체 생성이 불가능 합니다.
  • 추상 클래스는 일반 클래스 상속과 동일하게 상속 키워드로 extends를 사용합니다.
  • 추상 메서드는 (추상 클래스를 상속 받는) 하위 클래스에서 추상 메서드의 구현을 강제해야 합니다. (Override)
  • 추상 메서드를 포함하는 클래스는 반드시 추상 클래스여야 합니다.
  • 다중 상속이 불가능합니다.

추상 클래스(Abstract class) 의 자세한 내용은 제 다른 글에 잘 정리되어 있으니 아래 링크를 통하여 확인바랍니다.

 

 

[JAVA] 추상 클래스(Abstract) 완벽 이해하기

추상 클래스란? 추상 클래스는 추상 메서드를 하나라도 가지고 있는 클래스를 만한다. 추상 메서드는 "메서드가 완성되지 않은, 껍데기만 있는 메서드" 이다. 쉽게 이해하기 위해서 예를 들자면

developshrimp.com

 


인터페이스(Interface)

 

인터페이스는 'interface' 키워드를 사용하여 정의하며, 오직 추상 메서드와 상수(static final)만을 가지고 있는 것을 인터페이스라고 합니다.

추상 클래스와 마찬가지로 인터페이스 또한 인터페이스의 선언되어있는 추상 메서드를 구현(implements) 하는 클래스에서 반드시 구현하도록 강제하고 있습니다.

 

다음은 인터페이스의 특징을 간단하게 정리해놓은것입니다.

  • 인터페이스의 모든 멤버 변수는 public static final 이어야 하며, 이를 생략할 수 있습니다. (기본값)
  • 인터페이스의 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있습니다. (기본값)
  • Java 8 부터는 static, default method를 사용할 수 있습니다.
  • 인터페이스는 상속 키워드로 implements 를 사용합니다
  • 다중 상속이 가능합니다.
 

[JAVA] 인터페이스(Interface) 핵심 이해하기

인터페이스(Interface)란 인터페이스는 프로그램 내 다양한 기능을 하는 클래스들에게 기본이 되는 틀(구조)를 제공하는 역할을 한다. 이전의 포스팅을 보았다면 알겠지만, 추상 클래스와 비슷한

developshrimp.com

 


추상 클래스와 인터페이스 비교

 

위에서 설명을 보고 둘의 차이가 정확히 와닿지 않을 수 있습니다.

추상 클래스와 인터페이스 둘 다 하나 이상의 추상 메서드를 두고...

자신을 구현하고 있는 하위 클래스에서 추상 메서드를 강제화하고..

추상 클래스는 일반 클래스들 처럼 일반 변수들과 일반 메서드가 들어갈 수 있다는 건 알지만 그래서 뭐 어떻게 쓰라는건지..

이해가 되지 않으실 수 있습니다.

 

가장 중요한 포인트는 추상 클래스와 인터페이스는 존재 목적이 다르다는 것입니다.

추상 클래스는 그 추상 클래스를 상속받아서 기능을 이용하고, 확장시키는 데 있습니다.

반면에 인터페이스는 함수의 껍데기만 있는데, 그 이유는 그 함수의 구현을 강제하기 위해서 입니다.

구현을 강제함으로써 구현 객체의 같은 동작을 보장할 수 있습니다.

 

둘이 비슷해보이지만 명확하게 다른 존재 이유가 있습니다.

그 이유는 바로 JAVA는 다중 상속을 지원하지 않기 때문인데요. 

다중 상속은 아래 코드와 같이 여러 개의 슈퍼클래스를 두는 것을 말합니다.

 

스마트폰을 예시로 들어보겠습니다.

우리의 스마트폰에는 카메라(Camera) 기능과 GPS 기능이 있습니다.

(아래 코드는 실행되지 않는 코드입니다. 문법 오류 발생합니다.)

abstract class Camera {
    abstract void takePhoto();
    abstract void recordVideo();
    void off();
}

abstract class GPS {
    abstract void findLocation();
    void off();
}

class Smartphone extends Camera, GPS {
    @Override
    public void takePhoto() {...}

    @Override
    public void recordVideo() {...}

    @Override
    public void findLocation() {...}
}

 

만약 위 코드에서 Smartphone 에 off() 메서드를 호출한다면, Camera에 있는 off() 와 GPS의 off() 중 어떤 메서드를 실행해야 하는지 헷갈리게 될 것입니다. 이것이 바로 다중 상속의 모호성인데요.

이 다중 상속의 모호성 때문에 자바는 과감하게 다중 상속을 못하도록 해버렸습니다.

 

그렇기 때문에 아래 코드와 같이 인터페이스를 통해 off() 메서드를 강제화하여 다중 상속의 모호함을 제거하고 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 동일한 기능으로 구현할 수 있게 해줍니다.

 

그리고 두 인터페이스를 분리한 것은 객체 지향 설계의 SRP(단일 책임 원칙) 과 ISP(인터페이스 분리 원칙)을 준수하는 좋은 예가 됩니다.

 


추상 클래스 사용 예 - 중복 멤버 통합

 

위에서 설명 했던대로 추상 클래스는 공통적인 기능을 구현할 수 있는 메서드와 함께 추상 메서드를 포함할 수 있습니다.

이 구조는 기본적인 행동 방식을 제공하면서도, 특정 행동을 하위 클래스가 제공하도록 강제합니다.

 

우선 아래의 Cat 과 Dog의 예제 코드를 확인해보겠습니다.

class Cat {
    int x, y;

    void move(int x, int y) {
        System.out.println("이동합니다. x: " + x + " y: " + y);
    }
    
    void eat() {
        System.out.println("고양이가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("고양이가 잠을 잡니다.");
    }
}

class Dog {	
    int x, y;

    void move(int x, int y) {
        System.out.println("이동합니다. x: " + x + " y: " + y);
    }

    void eat() {
        System.out.println("강아지가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("강아지가 잠을 잡니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat();
        Dog dog = new Dog();
        
        cat.move(1, 2);
        cat.eat();
        cat.sleep();
        
        dog.move(5, 5);
        dog.eat();
        dog.sleep();
    }
}

 

불편함이 느껴지시나요? 정말 간단한 예제를 예로 들었기 때문에 와닿지 않으실 수 있습니다.

위 코드에서 Cat 과 Dog 클래스 안에는 int x, y; 코드와 move() 메서드가 중복되어 있는 것을 확인할 수 있습니다.

둘이 정말 똑같은 코드임에도 불구하고 각각의 클래스에 작성해주어야 하는 것이죠.

 

만약 위 코드에서 동물이 100마리 추가되어야 한다고 가정해봅시다.

각 동물마다 eat() , sleep() 메서드를 구현해주고 똑같은 코드(move 와 int x,y)를 100번 추가하셔야합니다.

만약에 동물들의 move() 를 변경하고 싶다면? 100번 똑같이 수정해주시면 되는거죠.

얼마나 비효율적인지 실감이 되시나요?

 

다음 아래 코드는 추상 클래스를 사용한 예제 코드입니다.

abstract class Animal {
    int x, y;

    void move(int x, int y) {
        System.out.println("이동합니다. x: " + x + " y: " + y);
    }

    abstract void eat();
    abstract void sleep();
}

class Cat extends Animal {
    void eat() {
        System.out.println("고양이가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("고양이가 잠을 잡니다.");
    }
}

class Dog extends Animal {
    void eat() {
        System.out.println("강아지가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("강아지가 잠을 잡니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        
        cat.move(1, 2);
        cat.eat();
        cat.sleep();
        
        dog.move(5, 5);
        dog.eat();
        dog.sleep();
    }
}

 

위 코드에서는 추상 클래스 Animal을 구현하고 그 하위 클래스로 Cat 과 Dog을 두었습니다.

이렇게 되면 공통적인(중복) 코드인 int x, y; 와 move() 는 추상 클래스인 Animal 에 구현해 두고 나머지 각 하위 클래스에서 특징에 맞게 구현해야하는 eat(), sleep() 만 구현해주면 되는 것이죠.

 

만약 100개의 동물이 추가가 된다면, 각 특징에 맞는 메서드인 eat(), sleep()만 작성해주면 됩니다.

100개의 공통메서드를 구현할 필요가 없게 되는것이죠!

물론 수정을 할때도 Animal 클래스의 move()만 수정한다면 한 번의 수정으로 100개의 동물 클래스의 move 메서드가 수정됩니다.

 

그렇다면, '이 예제에서 인터페이스는 못 쓰는건가요?' 라는 궁금증이 생기신 분들도 있으실 겁니다.

아래 코드는 인터페이스를 사용하여 작성한 예제입니다.

interface Animal {
    void move(int x, int y);
    void eat();
    void sleep();
}

class Cat implements Animal {
	int x, y;

    void move(int x, int y) {
        System.out.println("이동합니다. x: " + x + " y: " + y);
    }

    void eat() {
        System.out.println("고양이가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("고양이가 잠을 잡니다.");
    }
}

class Dog implements Animal {
	int x, y;

    void move(int x, int y) {
        System.out.println("이동합니다. x: " + x + " y: " + y);
    }
    
    void eat() {
        System.out.println("강아지가 밥을 먹습니다.");
    }

    void sleep() {
        System.out.println("강아지가 잠을 잡니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal cat = new Cat();
        Animal dog = new Dog();
        
        cat.move(1, 2);
        cat.eat();
        cat.sleep();
        
        dog.move(5, 5);
        dog.eat();
        dog.sleep();
    }
}

 

여기까지 공부하신 분들은 딱 알아차리시겠지만 인터페이스(Interface)에는 상수 혹은 추상 메서드만 존재할 수 있습니다.

추상 클래스처럼 멤버 변수와 일반 메서드를 사용할 수 없죠.

 

그렇기 때문에 공통적으로 들어가야하는 멤버 변수인 int x, y; 구문과 공통적으로 사용해야하는 move() 일반 메서드를 interface에서 사용할 수 없습니다.

 

위 코드를 보시면 추상 클래스를 사용하지 않은 코드보다 더 많은 양의 코드가 작성되었죠.

실용성은 1도 없는 상태로 말이에요

추상 클래스를 사용하지 않은 것처럼 100개의 동물 클래스가 추가된다면 100개의 공통 로직을 작성해주어야 합니다.

 

그렇기 때문에 이와 같이 공통적인(중복) 기능을 구현하면서, 일부메서드는 하위 클래스에서 구현하도록 강제해야 할 때 인터페이스가 아닌 추상 클래스를 사용해야 한다고 할 수 있습니다.


인터페이스 사용 예 - 자유로운 타입 상속

 

추상 클래스와 인터페이스의 차이에서 인터페이스가 사용되는 예제를 보여드렸는데요.

좀 더 깊고 확실하게 이해하기 위해 다른 예제를 준비했습니다.

소제목인 자유로운 타입 상속을 보고는 무슨뜻인지 잘 이해가 가질 않으실겁니다.

 

우선 결제방식을 예제로 아래 코드를 확인해보겠습니다.

interface Payment {
    void processPayment(int price);
}

class CreditCard implements Payment {
    public void processPayment(int price) {
        System.out.println("신용카드로 결제 : 가격 = " + amount);
    }
}

class KakaoPay implements Payment {
    public void processPayment(int price) {
        System.out.println("카카오페이로 결제 : 가격 = " + amount);
    }
}

public class Main {
    
    public static void main(String[] args) {
        Payment creditCard = new CreditCard();
        Payment kakaoPay = new KakaoPay();
        
        creditCard.processPayment(8000);
        kakaoPay.processPayment(8000);
    }
}

 

위 코드같은 경우, Payment 인터페이스는 다양한 결제 방식(신용카드, 카카오페이 등) 에 대해 공통 인터페이스를 제공합니다. 각 결제 방식 클래스는 Payment 인터페이스를 구현하고 있죠.

 

여기서 "어라? 추상 클래스를 사용해도 문제가 없을거 같은데? 라고 생각하실텐데요.

여기서 추상 클래스를 사용하신다면 신용카드와 카카오페이의 "결제" 라는 공통의 개념을 공유하지만 완전히 다른 객체 유형이라는 점에서 문제가 발생할 수 있습니다.

 

좀 더 자세히 설명해보자면, (설명이 부족하고 틀린부분이 있을 수 있습니다. 댓글 부탁드립니다)

신용카드(CreditCard) 와 카카오페이(KakaoPay) 라는 두 클래스는 기능적으로 결제를 처리하는 공통점이 있지만 신용카드(CreditCard) 클래스는 금융 카드 관련 클래스 일 수 있고, 카카오페이(KakaoPay)는 온라인 결제 서비스 관련 클래스일 수 있습니다.

 

다른 예로, 제가 좋아하는 파스타를 먹고나서 돈을 지불해야합니다.

신용카드와 현금중 고민을 하는 데, 신용카드와 현금의 공통점은 어쨌든 파스타의 가격을 지불하는 것이죠.

하지만 둘은 논리적으로? 같은 개념이 아니라는 뜻이죠. 현금이 신용카드와 같이 금융 카드 관련 클래스는 아니니까 말이죠

 

부족한 것 같아 마지막으로 다른 예를 하나만 더 들어보겠습니다. 

아까의 Animal 추상 클래스를 보시면 우리는 동물(Animal) 추상 클래스에 Cat 과 Dog 클래스가 하위 클래스로 존재하고 있습니다. 고양이(Cat) 와 강아지(Dog) 는 동물(Animal) 과 논리적으로 연관이 있습니다.

 

여기에 극단적으로 Animal 클래스에 사과(Apple) 클래스가 하위 타입으로 구현되었다고 생각해봅시다.

코드상으로는 에러를 발생시키지는 않지만, 그리고 극단적으로 고양이와 사과는 '사람이 아니다' 라는 공통점이 있지만 뭔가 이상하죠? 논리적으로 맞지 않은 클래스라는 것입니다. 

 

정리하면, 추상 클래스의 상속은 "is-a" 관계를 의미하고 인터페이스는 "can-do" 관계를 의미합니다.

영어 뜻 그대로 Cat is a Animal, Dog is a Animal 즉 ,Cat은 Animal의 한 종류라는 의미가 됩니다.

 

반대로 인터페이스는 "can-do" 처럼 수행가능 하다는 뜻으로 '신용카드(CreditCard), 카카오페이(KakaoPay)는 결제(Payment)가 가능하다' 라는 의미가 되는 것이죠. 즉, 인터페이스는 "is-a" 관계를 갖지않습니다.

 

마지막으로 추상 클래스와 인터페이스를 간단하게 정리한 표로 마무리 하겠습니다.

 


 

출처(참고)

https://brunch.co.kr/@kd4/6

https://hahahoho5915.tistory.com/70

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EC%B6%94%EC%83%81_%ED%81%B4%EB%9E%98%EC%8A%A4_%EC%A0%95%EB%A6%AC

+ 오랜만에 만난 챗GPT