왜 필요한가 추상 클래스 인터페이스 다중 구현 default 메서드 비교·선택 📝 문제풀이
◀ 이전 📋 목차 CH 14 ▶
📐 CHAPTER 13 · 중급

설계도의 설계도, 추상 클래스와 인터페이스

이번 장의 한 문장은 이거예요 — "구현은 자식에게 맡기고, 있어야 할 기능만 강제한다." 완성된 부품이 아니라 "이런 기능은 반드시 있어야 해"라고 규칙만 정해 두는 설계도를 배웁니다. 천천히 따라오면 어렵지 않아요.

🎯 이 장을 끝내면
🤔
왜 필요한가
"미완성 설계도"가 왜 쓸모 있을까?
완성된 클래스만 있으면 될 것 같은데, 왜 일부러 비워 두는 걸까요?

지금까지 배운 클래스는 완성된 부품이었어요. new로 찍어내면 바로 쓸 수 있었죠. 그런데 팀으로 프로그램을 만들다 보면 이런 상황이 자주 생겨요 — "이 부품에는 이런 기능이 꼭 있어야 하는데, 구체적으로 어떻게 만들지는 담당자마다 다르다."

🗣️ 비유 — 이력서 양식 vs 다 쓴 이력서 회사가 이력서 양식(빈 칸이 있는 서식)을 나눠줘요. "이름·경력·자기소개는 반드시 채워라"라고 규칙만 정해 두죠. 실제 내용(구현)은 사람마다 달라요.
추상 클래스와 인터페이스가 바로 이 "양식"이에요. "이런 기능은 반드시 있어야 한다"는 규칙(빈 칸)만 정하고, 채우는 일(구현)은 그걸 상속·구현하는 자식에게 맡깁니다. 💡 핵심: 있어야 할 기능은 강제하되, 구현은 자유. 이게 이번 장 전체를 관통하는 아이디어예요.

이렇게 "규칙(있어야 할 기능)"과 "구현(실제 내용)"을 분리하면, 코드를 쓰는 쪽은 규칙만 믿고 프로그램을 짤 수 있어요. 예를 들어 "이 물체는 draw()를 가진다"는 것만 약속되면, 그게 원이든 사각형이든 상관없이 draw()를 호출하면 되죠. (이건 CH 12 다형성과 짝을 이루는 이야기예요.)

abstract 추상 메서드 interface implements extends default 다중 구현 is-a can-do
🧭
이번 장의 지도. ① 추상 클래스(설계도 절반) → ② 인터페이스(순수 규칙) → ③ 인터페이스 여러 개 구현 → ④ default로 규칙에 기본값 주기 → ⑤ 둘 중 뭘 고를까. 순서대로 읽으면 자연스럽게 이어집니다.
🧱
추상 클래스
본문 없는 메서드, 못 찍어내는 클래스
abstract 키워드 하나로 "미완성"임을 선언해요.
🔤
용어 — 추상(abstract)이란 "구체적인 내용은 비워 두고 뼈대·개념만 남긴다"는 뜻이에요. 반대말은 "구체적(concrete)". 완성된 그림이 아니라 밑그림·윤곽선만 그려 둔 상태라고 보면 돼요.

추상 메서드(abstract method)이름·매개변수·반환타입만 선언하고, 본문 { }이 없는 메서드예요. "이런 기능이 있어야 한다"는 선언만 하고, 실제로 어떻게 할지는 비워 둔 거죠. 세미콜론 ;으로 끝냅니다.

이런 추상 메서드를 하나라도 가진 클래스는 반드시 추상 클래스(abstract class)여야 해요. 클래스 앞에도 abstract를 붙입니다.

// abstract 붙은 추상 클래스 — "동물"이라는 설계도
abstract class Animal {
    String name;                 // ✅ 일반 필드 가질 수 있음

    Animal(String name) {        // ✅ 생성자도 가질 수 있음 (자식이 씀)
        this.name = name;
    }

    // 추상 메서드 — 본문이 없고 세미콜론으로 끝! "울음소리는 있어야 한다"만 강제
    abstract void sound();

    // ✅ 일반(구현된) 메서드도 함께 가질 수 있음
    void breathe() {
        System.out.println(name + "이(가) 숨을 쉰다.");
    }
}

추상 클래스의 가장 중요한 특징 두 가지예요.

⛔ 규칙 1 — new로 직접 객체를 만들 수 없다 추상 클래스는 미완성 설계도라서, 그 자체로는 물건을 찍어낼 수 없어요.
Animal a = new Animal("동물"); ❌ → 컴파일 에러! (sound()가 비어 있으니 실행할 수 없죠.) 그럼 어떻게 쓰냐고요? 상속받은 자식을 만들어서, 자식으로 객체를 생성해요. 부모 타입 변수에 담는 건 됩니다(다형성): Animal a = new Dog("바둑이");
✅ 규칙 2 — 자식은 추상 메서드를 "반드시" 구현해야 한다 추상 클래스를 extends로 상속받은 자식은, 부모의 모든 추상 메서드를 오버라이드(구현)해야 해요. 그래야 비로소 "완성된 클래스"가 되어 new로 찍어낼 수 있어요. 만약 하나라도 구현하지 않으면? 그 자식도 아직 미완성이므로 자식도 abstract로 선언해야 하고, 아니면 컴파일 에러가 납니다.
// Animal을 상속받은 자식 — 추상 메서드 sound()를 반드시 구현
class Dog extends Animal {
    Dog(String name) {
        super(name);             // 부모 생성자 호출
    }

    @Override
    void sound() {               // ✅ 추상 메서드를 구현(오버라이드)했으니 완성됨
        System.out.println(name + ": 멍멍!");
    }
}

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

    @Override
    void sound() {               // ✅ 고양이는 자기 방식대로 구현
        System.out.println(name + ": 야옹~");
    }
}

public class Main {
    public static void main(String[] args) {
        // Animal a = new Animal("x");   // ❌ 추상 클래스는 직접 new 불가!
        Animal a = new Dog("바둑이");    // ✅ 자식으로 생성, 부모 타입에 담음
        a.sound();                       // 바둑이: 멍멍!
        a.breathe();                     // 바둑이이(가) 숨을 쉰다.

        Animal b = new Cat("나비");
        b.sound();                       // 나비: 야옹~
    }
}
💡
정리. 추상 클래스 = 일반 필드·일반 메서드 + 추상 메서드(빈 칸)가 섞인 미완성 설계도. new 직접 불가, 자식이 빈 칸을 채워야 완성돼요.
📋
인터페이스
"규칙만 있는" 순수 설계도
추상 클래스가 "절반쯤 완성"이라면, 인터페이스는 "규칙 그 자체"예요.

인터페이스(interface)class 대신 interface 키워드로 만들어요. 전통적으로 인터페이스 안의 메서드는 전부 추상 메서드(본문 없음)이고, 필드는 상수(public static final)만 가질 수 있어요. 한마디로 "할 수 있어야 하는 기능 목록"이에요.

🔤
인터페이스(interface) — "이런 기능은 반드시 있어야 한다"는 규칙(약속)만 모아 둔 순수 설계도예요. 실제 내용은 비어 있고, 이를 implements한 클래스가 그 규칙을 채워 구현해요. 콘센트 규격처럼 "이 모양에 맞추면 꽂힌다"는 약속이라고 보면 돼요.
🔌
콘센트 "규격"
구멍 모양·전압만 약속해요. 실제 발전 방식은 상관없죠.
📋
인터페이스 = 규격서
"이 기능은 반드시 있어야 해"라는 약속(메서드 목록)만 정의해요.
🔦
플러그 = 구현 클래스
규격만 맞으면 선풍기든 노트북이든 꽂혀요(implements).
// interface 키워드로 선언 — "날 수 있다"는 능력의 규칙
interface Flyable {
    void fly();          // 자동으로 public abstract — 본문 없음
}

// 클래스는 implements 로 인터페이스를 구현한다
class Bird implements Flyable {
    @Override
    public void fly() {                  // ✅ 반드시 구현, public 이어야 함
        System.out.println("새가 날개짓으로 난다.");
    }
}

class Airplane implements Flyable {
    @Override
    public void fly() {
        System.out.println("비행기가 엔진으로 난다.");
    }
}
🔑 인터페이스의 규칙 (전통적 형태) 1. 구현 키워드는 implements — 상속의 extends와 헷갈리지 마세요! 인터페이스는 implements로 "구현"합니다.
2. 메서드는 추상 — 본문 없이 선언만. 구현 클래스가 반드시 채워야 해요. (자동으로 public abstract라, 구현할 때 public을 붙여야 합니다.)
3. 필드는 상수만 — 인터페이스에 필드를 쓰면 자동으로 public static final 상수가 돼요. 일반 인스턴스 변수는 못 가져요. 그래서 인터페이스는 "이 물건은 fly() 를 할 수 있다"처럼 능력(can-do)을 규정하는 데 잘 어울려요.
interface Config {
    int MAX = 100;       // 자동으로 public static final int MAX = 100;
    // int count;        // ❌ 상수라 값이 반드시 있어야 함 (일반 필드 불가)
}

public class Main {
    public static void main(String[] args) {
        Flyable f = new Bird();          // ✅ 인터페이스 타입에 구현체를 담음(다형성)
        f.fly();                         // 새가 날개짓으로 난다.

        f = new Airplane();
        f.fly();                         // 비행기가 엔진으로 난다.

        System.out.println(Config.MAX);  // 100  (상수는 이렇게 접근)
    }
}
🆚
추상 클래스 vs 인터페이스 첫인상. 추상 클래스는 일반 필드·구현된 메서드도 가질 수 있는 "절반 완성 설계도", 인터페이스는 상수와 (전통적으로) 추상 메서드만 있는 "순수 규칙집"이에요. 자세한 비교는 sec-5에서 표로 정리합니다.
🧩
다중 구현
부모는 하나, 능력은 여러 개
자바가 "다중 상속"을 인터페이스로 대신하는 방법이에요.

자바에서 클래스는 부모를 딱 하나만 상속할 수 있어요(단일 상속). class A extends B extends C처럼 부모를 여러 개 두는 건 불가능해요. 부모가 여럿이면 "같은 이름의 메서드가 충돌"하는 등 복잡한 문제가 생기기 때문이에요.

하지만 인터페이스는 여러 개를 동시에 implements 할 수 있어요. 인터페이스는 "규칙 목록"일 뿐 구현 충돌이 (전통적으로) 없으니까요. 그래서 인터페이스가 자바의 다중 상속을 대체합니다.

👨‍👦
extends B
부모 클래스 하나 (단일 상속)
+
📋
implements C, D
인터페이스 여러 개 (능력 추가)
🦸
class A
부모 특성 + 여러 능력을 다 가짐
// 규칙(능력) 인터페이스 두 개
interface Swimmable { void swim(); }
interface Flyable   { void fly();  }

class Animal { String name; }        // 평범한 부모 클래스

// extends 는 하나, implements 는 여러 개! (extends 를 먼저 씀)
class Duck extends Animal implements Swimmable, Flyable {
    @Override public void swim() { System.out.println("오리가 헤엄친다."); }
    @Override public void fly()  { System.out.println("오리가 난다.");   }
}

public class Main {
    public static void main(String[] args) {
        Duck d = new Duck();
        d.swim();                    // 오리가 헤엄친다.
        d.fly();                     // 오리가 난다.

        Swimmable s = d;             // ✅ 능력 타입으로도 볼 수 있음
        s.swim();
    }
}
📝 문법 포인트 — extends와 implements 순서 한 클래스에서 상속과 구현을 같이 할 때는 반드시 extends를 먼저, implements를 나중에 씁니다.
class Duck extends Animal implements Swimmable, Flyable { ... }
인터페이스는 쉼표 ,여러 개를 나열할 수 있어요. 상속(extends)은 클래스니까 딱 하나만! 💡 헷갈리면: "클래스는 extends 하나, 인터페이스는 implements 여러 개." 이 한 줄만 기억하세요.
🧵
왜 이게 편할까? 오리는 "동물"이면서 동시에 "헤엄칠 수 있고 날 수도 있는" 존재예요. 상속 하나로는 이 여러 성격을 다 담기 어렵지만, 능력을 인터페이스로 쪼개서 필요한 만큼 붙이면 유연하게 표현할 수 있어요.
🛠️
default 메서드
인터페이스에 "기본 구현"을 넣다 (Java 8+)
규칙집인 인터페이스가, 예외적으로 몸통을 가질 수 있게 됐어요.

원래 인터페이스의 메서드는 전부 추상(본문 없음)이었어요. 그런데 Java 8부터 인터페이스 안에 default 키워드를 붙여 기본 구현(본문 있는 메서드)을 제공할 수 있게 됐어요.

interface Greetable {
    void hello();                        // 여전히 추상 — 구현 강제

    // Java 8+ : default 메서드는 본문을 가진다 (구현체가 없어도 그냥 씀)
    default void bye() {
        System.out.println("안녕히 가세요! (기본 인사)");
    }

    // static 메서드도 가능 — 인터페이스 이름으로 직접 호출
    static void info() {
        System.out.println("Greetable 인터페이스입니다.");
    }
}

class Person implements Greetable {
    @Override
    public void hello() {                // 추상 메서드는 반드시 구현
        System.out.println("안녕하세요!");
    }
    // bye() 는 구현 안 해도 됨 → 인터페이스의 default 구현을 그대로 사용
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello();                       // 안녕하세요!
        p.bye();                         // 안녕히 가세요! (기본 인사)  ← default 사용
        Greetable.info();                // static 은 인터페이스 이름으로 호출
    }
}
🤷 왜 default 메서드가 생겼을까? 상상해 보세요. 이미 수백 개의 클래스가 구현하고 있는 인터페이스에 메서드 하나를 추상으로 새로 추가하면 어떻게 될까요? → 그 인터페이스를 구현한 모든 클래스가 갑자기 컴파일 에러가 나요(새 메서드를 구현 안 했으니까).
그래서 Java 8은 "기존 구현을 깨지 않고 인터페이스에 새 기능을 추가"할 수 있도록 default 메서드를 만들었어요. 기본 구현이 있으니, 구현 클래스들이 손대지 않아도 그대로 돌아가죠. 💡 대표 사례: Java 8이 컬렉션에 forEach 같은 기능을 default로 추가할 수 있었던 것도 이 덕분이에요.
💡
한 줄 정리. default = 인터페이스가 주는 "기본 구현(있어도 되고 오버라이드해도 됨)". 도입 버전은 Java 8. static 메서드는 인터페이스 이름으로 직접 호출하는 유틸 성격이에요.
⚖️
비교 · 선택
추상 클래스 vs 인터페이스, 뭘 고를까?
헷갈리는 둘을 표로 정리하고, 고르는 기준을 알려줄게요.
구분추상 클래스 (abstract class)인터페이스 (interface)
키워드abstract classinterface
사용 키워드extends (상속)implements (구현)
개수 제한하나만 상속 (단일 상속)여러 개 구현 가능 (다중)
일반 필드✅ 가질 수 있음❌ 상수(public static final)만
구현된 메서드✅ 일반 메서드 가능default/static만 (Java 8+)
생성자✅ 가짐 (자식이 super로 씀)❌ 없음
의미(관점)is-a — "~는 ~의 한 종류다"can-do — "~할 수 있다"
🎯 고르는 기준 — 딱 두 갈래로 추상 클래스를 고른다 → 자식들이 공통 필드·공통 코드를 많이 공유하고, 서로 "~의 한 종류(is-a)"인 강한 혈연 관계일 때. 예: Dog·Cat은 모두 Animal의 한 종류이고 name 필드·breathe()를 공유.
인터페이스를 고른다 → 혈연과 무관하게 "~할 수 있다(can-do)"는 능력만 규정하고, 여러 능력을 조합하고 싶을 때. 예: Bird·Airplane은 종류가 전혀 다르지만 둘 다 fly() 할 수 있음. 💡 애매하면 인터페이스부터 고려하세요. 다중 구현이 되어 더 유연하고, default 메서드로 기본 구현도 줄 수 있어서 현대 자바에서 선호되는 편이에요.
🔗
둘은 경쟁이 아니라 협력. 실제 코드에서는 "추상 클래스로 공통 뼈대를 잡고, 인터페이스로 능력을 얹는" 식으로 함께 쓰는 경우가 아주 많아요(class Duck extends Animal implements Swimmable, Flyable처럼요).
☕ 번외 — 인터페이스 하나의 추상 메서드만 있으면? (함수형 인터페이스 맛보기)
추상 메서드가 딱 하나인 인터페이스를 함수형 인터페이스(functional interface)라고 불러요. 이런 인터페이스는 나중에 배울 람다식과 짝을 이룹니다.
@FunctionalInterface          // 추상 메서드가 하나임을 표시(선택)
interface Calc {
    int apply(int a, int b);  // 추상 메서드 하나
}

// 지금은 이렇게 구현하지만...
Calc add = new Calc() {
    public int apply(int a, int b) { return a + b; }
};
// 나중엔 람다로 이렇게: Calc add = (a, b) -> a + b;  (고급 챕터에서!)
지금은 "추상 메서드 하나짜리 인터페이스는 특별하게 쓰인다" 정도만 기억해 두면 충분해요. default·static 메서드는 몇 개 있어도 함수형 인터페이스 자격에 영향 없어요(추상 메서드 개수만 셈).
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.
CH 14 ⚠️ 예외 처리 — try·catch·finally로 오류에 대비하고, 프로그램이 뻗지 않게 만들기