[Java]개념노트 34 추상 클래스 이해하기

[Java 개념노트 34] 추상 클래스 이해하기

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

지난 글에서는 참조변수의 형변환, 업캐스팅, 다운캐스팅, instanceof에 대해 정리했습니다. 이번 글에서는 객체지향에서 공통 기능을 부모 클래스에 두면서, 일부 기능은 자식 클래스가 반드시 구현하도록 만드는 추상 클래스에 대해 알아보겠습니다.

추상 클래스는 상속, 오버라이딩, 다형성과 깊게 연결됩니다. 처음에는 조금 어렵게 느껴질 수 있지만, “미완성 설계도”라는 관점으로 보면 훨씬 이해하기 쉽습니다.


1. 추상 클래스란?

추상 클래스는 완성되지 않은 메서드를 포함할 수 있는 클래스입니다. 자바에서는 abstract 키워드를 사용해서 추상 클래스를 만듭니다.

추상 클래스는 직접 객체를 만들기 위한 클래스라기보다, 자식 클래스들이 공통으로 가져야 할 구조를 잡아주는 부모 클래스에 가깝습니다.

쉽게 말하면
추상 클래스는 공통 기능은 미리 만들어두고, 반드시 다르게 만들어야 하는 기능은 자식 클래스에게 맡기는 미완성 설계도입니다.

2. abstract 키워드

추상 클래스를 만들 때는 클래스 앞에 abstract를 붙입니다.

abstract class Animal {
}

위 코드는 Animal이라는 추상 클래스를 만든 것입니다. 추상 클래스는 일반 클래스처럼 필드와 메서드를 가질 수 있지만, 직접 객체 생성은 할 수 없습니다.

3. 추상 메서드란?

추상 메서드는 메서드 이름과 형태만 있고, 실행 내용이 없는 메서드입니다.

abstract void sound();

일반 메서드는 중괄호 { } 안에 실행할 코드를 작성합니다. 하지만 추상 메서드는 실행 내용이 없기 때문에 중괄호를 작성하지 않고 세미콜론 ;으로 끝납니다.

구분 예시 설명
일반 메서드 void eat() { } 실행 내용이 있습니다.
추상 메서드 abstract void sound(); 실행 내용이 없습니다.

4. 추상 메서드를 가진 클래스는 추상 클래스여야 한다

클래스 안에 추상 메서드가 하나라도 있으면 그 클래스는 반드시 추상 클래스가 되어야 합니다.

abstract class Animal {
    abstract void sound();
}

sound() 메서드는 추상 메서드입니다. 그래서 Animal 클래스 앞에도 abstract를 붙여야 합니다.

주의
추상 메서드가 있는 클래스는 반드시 abstract class로 선언해야 합니다.

5. 추상 클래스는 객체를 직접 만들 수 없다

추상 클래스는 미완성 설계도이기 때문에 직접 객체를 만들 수 없습니다.

abstract class Animal {
    abstract void sound();
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal(); // 오류 발생
    }
}

Animal은 추상 클래스이므로 new Animal()처럼 직접 객체를 생성할 수 없습니다. 추상 클래스를 사용하려면 자식 클래스가 상속받아 미완성 메서드를 구현해야 합니다.

6. 자식 클래스가 추상 메서드를 구현해야 한다

추상 클래스를 상속받은 자식 클래스는 부모의 추상 메서드를 반드시 구현해야 합니다. 이때 메서드 오버라이딩을 사용합니다.

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();

        dog.sound();
    }
}

abstract class Animal {
    abstract void sound();
}

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

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

멍멍

Dog 클래스는 Animal 추상 클래스를 상속받았습니다. 그리고 부모의 추상 메서드인 sound()를 오버라이딩해서 실제 동작을 작성했습니다.

7. 추상 클래스는 일반 메서드도 가질 수 있다

추상 클래스는 추상 메서드만 가질 수 있는 것이 아닙니다. 일반 필드와 일반 메서드도 가질 수 있습니다.

abstract class Animal {
    String name;

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

    abstract void sound();
}

위 코드에서 eat()은 일반 메서드입니다. 자식 클래스는 이 메서드를 그대로 물려받아 사용할 수 있습니다. 반면 sound()는 추상 메서드이므로 자식 클래스가 반드시 구현해야 합니다.

핵심 포인트
추상 클래스는 공통 기능은 일반 메서드로 제공하고, 자식마다 달라야 하는 기능은 추상 메서드로 강제할 수 있습니다.

8. 추상 클래스 전체 예제

이번에는 동물 예제로 추상 클래스를 사용해보겠습니다.

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("초코");
        Cat cat = new Cat("나비");

        dog.eat();
        dog.sound();

        cat.eat();
        cat.sound();
    }
}

abstract class Animal {
    String name;

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

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

    abstract void sound();
}

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

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

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

    @Override
    void sound() {
        System.out.println("야옹");
    }
}

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

초코이 먹습니다.
멍멍
나비이 먹습니다.
야옹

eat()은 모든 동물이 공통으로 사용하는 기능이므로 추상 클래스에 일반 메서드로 작성했습니다. sound()는 동물마다 다르기 때문에 추상 메서드로 만들고 자식 클래스에서 각각 구현했습니다.

9. 추상 클래스에도 생성자가 있다

추상 클래스는 직접 객체를 만들 수 없지만 생성자를 가질 수 있습니다.

자식 객체를 만들 때 부모 클래스 부분도 함께 초기화되어야 하므로, 추상 클래스의 생성자도 호출될 수 있습니다.

abstract class Animal {
    String name;

    Animal(String name) {
        this.name = name;
        System.out.println("Animal 생성자 실행");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
        System.out.println("Dog 생성자 실행");
    }
}

사용 예시는 다음과 같습니다.

Dog dog = new Dog("초코");

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

Animal 생성자 실행
Dog 생성자 실행

추상 클래스인 Animal의 객체를 직접 만든 것은 아니지만, Dog 객체를 만들 때 부모 부분 초기화를 위해 Animal 생성자가 먼저 실행됩니다.

10. 추상 클래스와 다형성

추상 클래스도 부모 타입으로 사용할 수 있습니다. 즉, 추상 클래스 타입 변수로 자식 객체를 참조할 수 있습니다.

Animal animal1 = new Dog("초코");
Animal animal2 = new Cat("나비");

animal1.sound();
animal2.sound();

Animal은 추상 클래스이지만 타입으로는 사용할 수 있습니다. 실제 객체는 DogCat입니다.

오버라이딩된 메서드는 실제 객체 기준으로 실행됩니다.

멍멍
야옹

11. 추상 클래스 배열 활용하기

다형성을 활용하면 추상 클래스 타입 배열에 여러 자식 객체를 담을 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Animal[] animals = {
            new Dog("초코"),
            new Cat("나비"),
            new Bird("파랑")
        };

        for (int i = 0; i < animals.length; i++) {
            animals[i].eat();
            animals[i].sound();
        }
    }
}

abstract class Animal {
    String name;

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

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

    abstract void sound();
}

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

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

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

    @Override
    void sound() {
        System.out.println("야옹");
    }
}

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

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

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

초코이 먹습니다.
멍멍
나비이 먹습니다.
야옹
파랑이 먹습니다.
짹짹

배열 타입은 Animal[] 하나이지만, 실제로는 Dog, Cat, Bird 객체를 함께 담았습니다. 이것이 추상 클래스와 다형성을 함께 사용하는 대표적인 형태입니다.

12. 추상 메서드를 구현하지 않으면?

추상 클래스를 상속받은 자식 클래스가 추상 메서드를 구현하지 않으면 오류가 발생합니다.

abstract class Animal {
    abstract void sound();
}

class Dog extends Animal {
    // sound()를 구현하지 않음
}

위 코드는 오류가 발생합니다. DogAnimal의 추상 메서드 sound()를 구현해야 하기 때문입니다.

만약 자식 클래스도 추상 메서드를 구현하지 않으려면, 자식 클래스도 추상 클래스로 선언해야 합니다.

abstract class Animal {
    abstract void sound();
}

abstract class Dog extends Animal {
    // Dog도 추상 클래스라면 sound()를 아직 구현하지 않을 수 있음
}

13. 추상 클래스는 언제 사용할까?

추상 클래스는 공통 기능과 강제 구현 기능이 함께 필요할 때 사용하기 좋습니다.

상황 추상 클래스가 어울리는 이유
자식 클래스들이 공통 필드를 가져야 할 때 부모 추상 클래스에 필드를 둘 수 있습니다.
공통 메서드를 미리 제공하고 싶을 때 일반 메서드를 작성해서 자식 클래스가 그대로 사용할 수 있습니다.
자식마다 반드시 다르게 구현해야 하는 기능이 있을 때 추상 메서드로 구현을 강제할 수 있습니다.
상속 구조에서 기본 틀을 잡고 싶을 때 미완성 부모 클래스로 공통 구조를 제공할 수 있습니다.

14. 추상 클래스와 일반 클래스 차이

일반 클래스와 추상 클래스의 차이를 정리하면 다음과 같습니다.

구분 일반 클래스 추상 클래스
객체 생성 가능 직접 생성 불가
추상 메서드 포함 불가능 가능
일반 메서드 포함 가능 가능
필드 포함 가능 가능
주 목적 완성된 객체 생성 공통 구조 제공, 자식 구현 강제

15. 추상 클래스와 인터페이스 미리 보기

추상 클래스를 배우면 자연스럽게 인터페이스와 비교하게 됩니다. 인터페이스는 다음 글에서 자세히 다룰 예정입니다.

이번 단계에서는 간단히 이렇게만 구분하면 됩니다.

구분 추상 클래스 인터페이스
느낌 상속용 미완성 부모 클래스 기능 명세서
관계 is-a 관계에 가까움 can-do 기능 약속에 가까움
다중 구현 클래스 상속은 하나만 가능 여러 인터페이스 구현 가능

인터페이스까지 배우면 객체지향 설계가 훨씬 선명해집니다.

16. 템플릿 메서드 맛보기

추상 클래스는 전체 흐름은 부모가 정하고, 세부 동작은 자식 클래스가 구현하도록 만들 때도 사용할 수 있습니다.

abstract class Report {
    void createReport() {
        printTitle();
        printContent();
        printFooter();
    }

    void printTitle() {
        System.out.println("리포트 제목");
    }

    abstract void printContent();

    void printFooter() {
        System.out.println("리포트 끝");
    }
}

class SalesReport extends Report {
    @Override
    void printContent() {
        System.out.println("매출 내용을 출력합니다.");
    }
}

createReport()의 전체 흐름은 부모 클래스가 가지고 있습니다. 하지만 실제 내용 출력인 printContent()는 자식 클래스가 구현합니다.

이런 방식은 나중에 객체지향 설계를 공부할 때 다시 만나게 됩니다.

17. 추상 클래스 사용 시 자주 하는 실수

추상 클래스를 처음 배울 때는 아래와 같은 실수를 자주 합니다.

실수 문제점 해결 방법
추상 클래스를 직접 생성 new Animal() 오류 자식 클래스로 객체 생성
추상 메서드에 중괄호 작성 추상 메서드는 구현부가 없음 abstract void sound(); 형태로 작성
추상 메서드가 있는데 클래스에 abstract 누락 컴파일 오류 발생 클래스 앞에 abstract 작성
자식 클래스가 추상 메서드 구현 누락 자식 클래스가 완성되지 않음 오버라이딩하거나 자식도 추상 클래스로 선언
추상 클래스와 인터페이스를 완전히 같은 것으로 생각 설계 목적을 혼동 추상 클래스는 상속용 부모 클래스에 가깝다고 이해

18. 직접 연습해보기

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

public class Main {
    public static void main(String[] args) {
        Payment cardPayment = new CardPayment(30000);
        Payment cashPayment = new CashPayment(15000);

        cardPayment.pay();
        cashPayment.pay();
    }
}

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("카드로 결제합니다.");
    }
}

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

    @Override
    void pay() {
        printAmount();
        System.out.println("현금으로 결제합니다.");
    }
}

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

결제 금액: 30000
카드로 결제합니다.
결제 금액: 15000
현금으로 결제합니다.

Payment 추상 클래스는 공통 필드인 amount와 공통 메서드인 printAmount()를 제공합니다. 결제 방식인 pay()는 결제 종류마다 다르기 때문에 추상 메서드로 만들고 자식 클래스에서 구현했습니다.

19. 이번 글 정리

이번 글에서는 자식 클래스에게 일부 구현을 맡기는 추상 클래스에 대해 정리했습니다. 핵심 내용은 다음과 같습니다.

  • 추상 클래스는 abstract class로 선언한다.
  • 추상 메서드는 구현부가 없는 메서드이다.
  • 추상 메서드는 abstract 반환타입 메서드명(); 형태로 작성한다.
  • 추상 메서드를 가진 클래스는 반드시 추상 클래스여야 한다.
  • 추상 클래스는 직접 객체를 생성할 수 없다.
  • 추상 클래스를 상속받은 자식 클래스는 추상 메서드를 구현해야 한다.
  • 추상 클래스는 일반 필드와 일반 메서드도 가질 수 있다.
  • 추상 클래스에도 생성자가 있을 수 있다.
  • 추상 클래스는 다형성과 함께 사용할 수 있다.
  • 공통 기능은 부모가 제공하고, 자식마다 다른 기능은 추상 메서드로 강제할 수 있다.

한 줄 요약
추상 클래스는 공통 기능은 제공하면서, 자식 클래스가 반드시 구현해야 할 기능을 추상 메서드로 남겨두는 미완성 부모 클래스입니다.

다음 글 예고
다음 글에서는 [Java 개념노트 35] 인터페이스 이해하기라는 주제로 클래스가 반드시 지켜야 할 기능 약속을 정의하는 방법을 정리해보겠습니다.

GWDEVELBlog Java 개념노트 시리즈