[Java] 개념노트 36 추상 클래스와 인터페이스 비교하기

[Java 개념노트 36] 추상 클래스와 인터페이스 비교하기

안녕하세요. Java 개념노트 시리즈 서른여섯 번째 글입니다.

지난 글에서는 클래스가 반드시 구현해야 할 기능 약속을 정의하는 인터페이스에 대해 정리했습니다. 이번 글에서는 객체지향을 공부할 때 자주 헷갈리는 추상 클래스인터페이스를 비교해보겠습니다.

추상 클래스와 인터페이스는 둘 다 직접 객체를 만들 수 없고, 자식 클래스나 구현 클래스가 메서드를 완성해야 한다는 공통점이 있습니다. 하지만 두 개념은 사용하는 목적이 다릅니다.


1. 왜 헷갈릴까?

추상 클래스와 인터페이스는 처음 보면 비슷해 보입니다. 둘 다 미완성 메서드를 가질 수 있고, 자식 클래스가 구현해야 하는 구조를 만들 수 있기 때문입니다.

추상 클래스: 자식 클래스가 추상 메서드를 구현한다.
인터페이스: 구현 클래스가 인터페이스 메서드를 구현한다.

하지만 핵심 차이는 무엇을 표현하려는가입니다.

쉽게 말하면
추상 클래스는 공통 특징을 가진 부모 클래스에 가깝고, 인터페이스는 반드시 지켜야 할 기능 약속에 가깝습니다.

2. 추상 클래스 한 줄 정리

추상 클래스는 공통 필드와 공통 메서드를 제공하면서, 일부 메서드는 자식 클래스가 반드시 구현하도록 만드는 클래스입니다.

abstract class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void eat() {
        System.out.println(name + "이 먹습니다.");
    }

    abstract void sound();
}

위 코드에서 name 필드와 eat() 메서드는 공통 기능입니다. 반면 sound()는 동물마다 다르므로 추상 메서드로 두고 자식 클래스가 구현하게 만들었습니다.

3. 인터페이스 한 줄 정리

인터페이스는 클래스가 반드시 구현해야 할 기능 목록을 정의하는 약속입니다.

interface Payment {
    void pay(int amount);
}

위 코드에서 Payment 인터페이스는 결제 기능을 약속합니다. 카드 결제든 현금 결제든 이 인터페이스를 구현한다면 pay() 메서드를 반드시 작성해야 합니다.

4. 공통점 먼저 보기

추상 클래스와 인터페이스는 다음과 같은 공통점이 있습니다.

공통점 설명
직접 객체 생성 불가 추상 클래스와 인터페이스는 그 자체로 객체를 만들 수 없습니다.
자식 또는 구현 클래스 필요 미완성 메서드를 실제 클래스에서 구현해야 사용할 수 있습니다.
다형성 활용 가능 부모 타입 또는 인터페이스 타입으로 여러 객체를 다룰 수 있습니다.
오버라이딩과 연결 자식 클래스나 구현 클래스가 메서드를 다시 정의합니다.

5. 차이점 한눈에 보기

두 개념의 차이를 표로 정리하면 다음과 같습니다.

구분 추상 클래스 인터페이스
키워드 abstract class interface
사용 방식 extends implements
핵심 목적 공통 상태와 기능 제공 기능 구현 약속
필드 일반 필드 가능 기본적으로 상수 성격
생성자 가질 수 있음 가질 수 없음
일반 메서드 가능 default 메서드로 가능
다중 사용 클래스 상속은 하나만 가능 여러 인터페이스 구현 가능

6. 키워드 차이

추상 클래스는 abstract class로 만들고, 자식 클래스가 extends로 상속받습니다.

abstract class Animal {
    abstract void sound();
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}

인터페이스는 interface로 만들고, 클래스가 implements로 구현합니다.

interface Payment {
    void pay(int amount);
}

class CardPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원을 카드로 결제합니다.");
    }
}

7. 필드 차이

추상 클래스는 일반 클래스처럼 필드를 가질 수 있습니다. 객체마다 다른 값을 저장하는 필드도 만들 수 있습니다.

abstract class Member {
    String name;
    int point;

    Member(String name, int point) {
        this.name = name;
        this.point = point;
    }
}

반면 인터페이스의 필드는 기본적으로 public static final 성격을 가집니다. 즉, 상수처럼 사용됩니다.

interface AppConfig {
    int MAX_COUNT = 100;
}

위 코드는 다음과 비슷하게 볼 수 있습니다.

interface AppConfig {
    public static final int MAX_COUNT = 100;
}

기억하기
객체마다 다른 상태값이 필요하다면 추상 클래스가 더 자연스럽고, 고정된 상수만 필요하다면 인터페이스에도 둘 수 있습니다.

8. 생성자 차이

추상 클래스는 생성자를 가질 수 있습니다. 직접 객체를 만들 수는 없지만, 자식 객체를 만들 때 부모 부분을 초기화해야 하기 때문에 생성자가 호출될 수 있습니다.

abstract class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }
}

반면 인터페이스는 생성자를 가질 수 없습니다. 인터페이스는 객체의 상태를 초기화하는 클래스가 아니라 기능 약속을 정의하는 문법이기 때문입니다.

interface Payment {
    // Payment() { } // 생성자 작성 불가
}

9. 일반 메서드 차이

추상 클래스는 일반 메서드를 자연스럽게 가질 수 있습니다.

abstract class Animal {
    void eat() {
        System.out.println("먹습니다.");
    }

    abstract void sound();
}

인터페이스도 default 메서드를 사용하면 구현부가 있는 메서드를 가질 수 있습니다.

interface RemoteControl {
    void turnOn();

    default void checkBattery() {
        System.out.println("배터리를 확인합니다.");
    }
}

다만 기본 느낌은 다릅니다. 추상 클래스는 공통 기능을 부모가 제공하는 구조에 가깝고, 인터페이스는 기능 약속을 중심으로 생각하는 것이 좋습니다.

10. 다중 사용 차이

자바 클래스는 부모 클래스를 하나만 상속받을 수 있습니다. 따라서 추상 클래스도 하나만 상속받을 수 있습니다.

abstract class Animal {
}

abstract class Pet {
}

// class Dog extends Animal, Pet { } // 오류 발생

반면 인터페이스는 여러 개 구현할 수 있습니다.

interface Walkable {
    void walk();
}

interface Runnable {
    void run();
}

class Dog implements Walkable, Runnable {
    @Override
    public void walk() {
        System.out.println("걷습니다.");
    }

    @Override
    public void run() {
        System.out.println("달립니다.");
    }
}

Dog 클래스는 WalkableRunnable 두 인터페이스를 모두 구현했습니다.

핵심 포인트
클래스 상속은 하나만 가능하지만, 인터페이스 구현은 여러 개 가능합니다.

11. 추상 클래스가 어울리는 경우

추상 클래스는 자식 클래스들이 공통 상태와 공통 기능을 가져야 할 때 잘 어울립니다.

abstract class Payment {
    int amount;

    Payment(int amount) {
        this.amount = amount;
    }

    void printAmount() {
        System.out.println("결제 금액: " + amount);
    }

    abstract void pay();
}

위 코드에서 모든 결제 방식은 amount라는 공통 필드를 가지고, 결제 금액을 출력하는 printAmount() 메서드도 함께 사용할 수 있습니다. 결제 방식만 자식 클래스가 다르게 구현하면 됩니다.

12. 인터페이스가 어울리는 경우

인터페이스는 서로 다른 클래스들이 같은 기능을 반드시 가져야 할 때 잘 어울립니다.

interface Notifier {
    void send(String message);
}

이메일 알림, 문자 알림, 앱 푸시 알림은 서로 다른 방식으로 동작합니다. 하지만 모두 메시지를 보내는 기능이 필요하다면 Notifier 인터페이스로 약속할 수 있습니다.

class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("이메일 발송: " + message);
    }
}

class SmsNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("문자 발송: " + message);
    }
}

13. 같은 예제를 두 방식으로 비교하기

결제 기능을 추상 클래스와 인터페이스로 각각 표현해보겠습니다.

추상 클래스로 표현

abstract class Payment {
    int amount;

    Payment(int amount) {
        this.amount = amount;
    }

    void printAmount() {
        System.out.println("결제 금액: " + amount);
    }

    abstract void pay();
}

class CardPayment extends Payment {
    CardPayment(int amount) {
        super(amount);
    }

    @Override
    void pay() {
        printAmount();
        System.out.println("카드 결제");
    }
}

공통 필드인 amount와 공통 메서드인 printAmount()가 필요하기 때문에 추상 클래스가 자연스럽습니다.

인터페이스로 표현

interface Payment {
    void pay(int amount);
}

class CardPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println(amount + "원 카드 결제");
    }
}

상태값을 부모가 들고 있을 필요가 없고, 결제 기능만 약속하면 된다면 인터페이스가 간단합니다.

14. 다형성 관점에서 비교하기

추상 클래스와 인터페이스는 모두 다형성에 사용할 수 있습니다.

추상 클래스 타입으로 다형성 사용

Payment payment = new CardPayment(30000);

payment.pay();

인터페이스 타입으로 다형성 사용

Notifier notifier = new EmailNotifier();

notifier.send("안녕하세요.");

둘 다 부모 타입처럼 사용해서 실제 객체의 구현 메서드를 실행할 수 있습니다. 차이는 부모 역할을 하는 타입이 추상 클래스인지 인터페이스인지입니다.

15. 선택 기준 정리

처음에는 아래 기준으로 선택하면 이해하기 쉽습니다.

상황 추천 이유
공통 필드가 필요하다 추상 클래스 필드와 생성자를 통해 공통 상태를 관리할 수 있습니다.
공통 메서드 구현을 제공하고 싶다 추상 클래스 일반 메서드를 자연스럽게 제공할 수 있습니다.
기능 약속만 필요하다 인터페이스 구현 클래스가 반드시 메서드를 작성하도록 만들 수 있습니다.
여러 기능 약속을 동시에 적용해야 한다 인터페이스 여러 인터페이스를 구현할 수 있습니다.
상속 관계가 자연스럽다 추상 클래스 고려 자식이 부모의 한 종류라면 상속 구조가 자연스러울 수 있습니다.

16. 함께 사용할 수도 있다

추상 클래스와 인터페이스는 둘 중 하나만 선택해야 하는 것이 아닙니다. 필요하면 함께 사용할 수 있습니다.

interface Printable {
    void print();
}

abstract class Document {
    String title;

    Document(String title) {
        this.title = title;
    }

    void printTitle() {
        System.out.println("문서 제목: " + title);
    }
}

class Report extends Document implements Printable {
    Report(String title) {
        super(title);
    }

    @Override
    public void print() {
        printTitle();
        System.out.println("리포트를 출력합니다.");
    }
}

Document 추상 클래스는 문서의 공통 상태와 기능을 제공합니다. Printable 인터페이스는 출력 기능을 반드시 구현하도록 약속합니다. Report 클래스는 둘을 함께 사용합니다.

17. 자주 하는 오해

추상 클래스와 인터페이스를 비교할 때 아래와 같은 오해를 자주 합니다.

오해 정리
인터페이스는 메서드 구현을 절대 못 가진다. default 메서드와 static 메서드는 구현부를 가질 수 있습니다.
추상 클래스는 무조건 추상 메서드를 가져야 한다. 추상 메서드가 없어도 abstract class로 선언할 수 있습니다.
인터페이스가 항상 추상 클래스보다 좋다. 목적이 다릅니다. 공통 상태가 필요하면 추상 클래스가 더 자연스러울 수 있습니다.
추상 클래스와 인터페이스는 객체지향에서 같은 역할이다. 둘 다 다형성에 쓰이지만, 추상 클래스는 부모 클래스, 인터페이스는 기능 약속에 가깝습니다.

18. 직접 연습해보기

아래 코드를 직접 작성하고 실행해보세요.

public class Main {
    public static void main(String[] args) {
        Report report = new Report("월간 매출 리포트");
        Notice notice = new Notice("점검 안내");

        report.print();
        notice.print();
    }
}

interface Printable {
    void print();
}

abstract class Document {
    String title;

    Document(String title) {
        this.title = title;
    }

    void printTitle() {
        System.out.println("제목: " + title);
    }
}

class Report extends Document implements Printable {
    Report(String title) {
        super(title);
    }

    @Override
    public void print() {
        printTitle();
        System.out.println("리포트 내용을 출력합니다.");
    }
}

class Notice extends Document implements Printable {
    Notice(String title) {
        super(title);
    }

    @Override
    public void print() {
        printTitle();
        System.out.println("공지 내용을 출력합니다.");
    }
}

실행 결과는 다음과 같습니다.

제목: 월간 매출 리포트
리포트 내용을 출력합니다.
제목: 점검 안내
공지 내용을 출력합니다.

19. 이번 글 정리

이번 글에서는 추상 클래스인터페이스의 공통점과 차이점을 비교했습니다. 핵심 내용은 다음과 같습니다.

  • 추상 클래스와 인터페이스는 직접 객체를 만들 수 없다.
  • 둘 다 자식 클래스나 구현 클래스가 메서드를 완성해야 사용할 수 있다.
  • 추상 클래스는 공통 상태와 공통 기능을 제공하는 부모 클래스에 가깝다.
  • 인터페이스는 클래스가 반드시 구현해야 할 기능 약속에 가깝다.
  • 추상 클래스는 extends로 상속받는다.
  • 인터페이스는 implements로 구현한다.
  • 추상 클래스는 필드와 생성자를 가질 수 있다.
  • 인터페이스의 필드는 기본적으로 상수 성격을 가진다.
  • 클래스 상속은 하나만 가능하지만, 인터페이스는 여러 개 구현할 수 있다.
  • 공통 상태가 필요하면 추상 클래스, 기능 약속이 필요하면 인터페이스를 먼저 생각하면 좋다.

한 줄 요약
추상 클래스는 공통 상태와 기능을 가진 미완성 부모 클래스이고, 인터페이스는 반드시 구현해야 할 기능을 정해두는 약속입니다.

다음 글 예고
다음 글에서는 [Java 개념노트 37] Object 클래스 이해하기라는 주제로 모든 클래스의 최상위 부모인 Object 클래스와 대표 메서드를 정리해보겠습니다.

GWDEVELBlog Java 개념노트 시리즈