상속이란 extends 문법 super 오버라이딩 Object 클래스 단일 상속 📝 문제풀이
◀ 이전 📋 목차 CH 12 ▶
🧬 CHAPTER 11 · 중급

상속 — 물려받아서 다시 쓰기

앞 장에서 클래스로 객체를 만드는 법을 배웠어요. 이번엔 이미 만든 클래스를 물려받아(상속) 재사용하고 확장하는 법을 배웁니다. 같은 코드를 두 번 쓰지 않게 해 주는, 객체 지향의 핵심 무기예요.

🎯 이 장을 끝내면
🧬
개념
상속(Inheritance)이 뭔가요?
이미 만든 클래스를 "물려받아" 다시 쓰는 것. 한 문장으로 잡고 시작해요.

상속은 "기존 클래스가 가진 필드와 메서드를 그대로 물려받아, 재사용하고 확장하는 것"이에요. 새 클래스를 맨바닥부터 만드는 대신, 이미 잘 만들어 둔 클래스를 이어받아 필요한 부분만 더하는 거죠.

🌱 실생활 비유 — "물려받고, 내 것을 더한다"
🧬
유전 물려받기
부모의 눈·코를 물려받고 + 나만의 개성을 더해요.
🏪
가업 물려받기
부모 가게의 설비·단골을 물려받고 + 새 메뉴를 추가해요.
📜
레시피 물려받기
할머니 비법을 물려받고 + 내 비법 한 스푼을 더해요.
🤔 왜 상속을 쓸까 — 중복 제거 Dog(개)와 Cat(고양이) 클래스를 각각 만든다고 해봐요. 둘 다 이름(name)이 있고 먹기(eat)·자기(sleep)를 해요. 이 공통 코드를 양쪽에 똑같이 두 번 쓰면 낭비고, 나중에 고칠 때도 두 군데를 손봐야 해요.
그래서 공통 부분을 Animal(동물)이라는 클래스에 한 번만 만들고, Dog·Cat이 그걸 물려받게 하면 됩니다. 공통은 한 곳에서 관리하고, 각자의 특징만 따로 추가해요. 💡 상속의 핵심 이득: ① 코드 중복 제거 · ② 재사용 · ③ 확장 편의. "공통은 위에, 차이는 아래에" 라고 기억하세요.

상속 관계에는 용어가 있어요. 물려주는 쪽을 부모(상위 클래스, 슈퍼클래스, super class), 물려받는 쪽을 자식(하위 클래스, 서브클래스, sub class)이라고 불러요.

Animal
부모 · 상위 · 슈퍼클래스
name / eat() / sleep() 공통
▲ ▲
Dog
자식 · 하위 · 서브클래스
+ bark()
Cat
자식 · 하위 · 서브클래스
+ scratch()
🧭
"is-a" 관계. 상속은 보통 "자식 is a 부모"(자식은 일종의 부모다)가 성립할 때 써요. "개는 일종의 동물이다(Dog is an Animal)" — 자연스럽죠. 반대로 "자동차는 일종의 엔진이다"는 어색하니, 그럴 땐 상속 대신 포함(has-a)을 씁니다.
상속 extends 부모(슈퍼) 자식(서브) super 오버라이딩 @Override Object 단일 상속
🔗
문법
extends 로 물려받기
키워드 하나면 끝나요. class 자식 extends 부모.

상속은 extends(확장하다) 키워드로 표현해요. 자식 클래스 이름 뒤에 extends 부모클래스를 붙이면, 그 순간 자식은 부모의 필드·메서드를 모두 물려받습니다.

class 자식클래스 extends 부모클래스 {
    // 부모에게 물려받은 것 + 자식만의 새로운 것
}

아래 예제를 볼게요. Animal이 부모, DogCat이 자식이에요. 자식 안에는 eat()을 쓰지 않았는데도 부모에게서 물려받았기 때문에 그대로 호출할 수 있어요.

// 부모 클래스
class Animal {
    String name;

    void eat() {
        System.out.println(name + "가 먹이를 먹습니다.");
    }
    void sleep() {
        System.out.println(name + "가 잠을 잡니다.");
    }
}

// 자식 클래스 — Animal의 것을 물려받고, 자기 것을 추가
class Dog extends Animal {
    void bark() {                     // Dog만의 새로운 메서드
        System.out.println(name + ": 멍멍!");
    }
}

class Cat extends Animal {
    void scratch() {                  // Cat만의 새로운 메서드
        System.out.println(name + ": 스크래치!");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog();
        d.name = "바둑이";     // 물려받은 필드
        d.eat();               // 물려받은 메서드 → 바둑이가 먹이를 먹습니다.
        d.bark();              // 자기 메서드     → 바둑이: 멍멍!

        Cat c = new Cat();
        c.name = "나비";
        c.sleep();             // 물려받은 메서드 → 나비가 잠을 잡니다.
        c.scratch();           // 자기 메서드     → 나비: 스크래치!
    }
}
🧩 자식이 쓸 수 있는 것 = 부모 것 + 자기 것 Dog 객체 하나에는 물려받은 name·eat()·sleep()직접 만든 bark()모두 들어 있어요.
반대로 부모인 Animal은 자식이 추가한 bark()모릅니다. 물려주는 방향은 부모 → 자식(아래로) 한 방향이에요. 즉, 자식은 부모보다 더 많은 걸 가진 존재예요. "물려받은 것 + 새로 더한 것"이니까요.
💡
private은 못 봐요. 부모의 private 필드·메서드는 물려받긴 해도 자식이 직접 이름으로 접근할 수 없어요. 공통으로 열어줄 것은 보통 protectedpublic으로 두거나, getter를 통해 씁니다.
🔤
protected(접근 제한자) — private(이 클래스 안에서만)과 public(어디서나)의 중간이에요. 자식 클래스에서는 물려받은 멤버에 직접 접근할 수 있게 열어 주되, 상관없는 외부에는 감추고 싶을 때 써요.
⬆️
super
부모를 직접 부르는 super
super는 "부모 것"을 가리켜요. 필드·메서드·생성자 세 가지로 나눠 봐요.

super"부모(상위 클래스)"를 가리키는 키워드예요. 자식 안에서 부모의 필드나 메서드를 콕 집어 쓰고 싶을 때, 그리고 부모의 생성자를 호출할 때 사용해요.

🔑 super 의 두 가지 쓰임 ① 부모 멤버 접근super.필드, super.메서드(). 특히 자식이 부모 메서드를 재정의(오버라이딩)했을 때, 원래 부모 버전을 호출하려면 super.메서드()를 써요.
② 부모 생성자 호출super(값...). 자식 생성자 안에서 맨 첫 줄에 써서 부모 생성자를 먼저 실행시켜요.
class Animal {
    String name;

    Animal(String name) {              // 부모 생성자
        this.name = name;
        System.out.println("Animal 생성자 실행");
    }
    void info() {
        System.out.println("이름: " + name);
    }
}

class Dog extends Animal {
    String breed;                      // 품종 (Dog만의 필드)

    Dog(String name, String breed) {
        super(name);                   // ① 부모 생성자 먼저 호출 (반드시 첫 줄)
        this.breed = breed;
        System.out.println("Dog 생성자 실행");
    }
    void info() {
        super.info();                  // ② 부모의 info() 를 그대로 호출
        System.out.println("품종: " + breed);
    }
}

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog("바둑이", "진돗개");
        d.info();
    }
}
🖨️ 실행 결과 — 생성자 순서에 주목new Dog(...)를 실행하면 아래 순서로 출력돼요.
Animal 생성자 실행
Dog 생성자 실행
이름: 바둑이
품종: 진돗개 👉 자식 객체를 만들면 부모 생성자가 먼저 실행되고, 그다음 자식 생성자가 실행돼요. "집을 지을 때 기초(부모)부터 올리고 그 위에 내 방(자식)을 얹는다"고 생각하면 쉬워요.
⚠️
super(...)는 자식 생성자의 첫 줄이어야 해요. 만약 super(...)를 안 쓰면, 자바가 자동으로 부모의 기본 생성자 super()를 몰래 넣어줘요. 그래서 부모에 기본 생성자가 없고 매개변수 생성자만 있으면, 자식에서 super(값)직접 호출해 줘야 오류가 안 나요.
✍️
오버라이딩
메서드 재정의 (Overriding)
부모에게 물려받은 메서드를, 자식이 "자기 방식대로" 다시 정의하는 것.

오버라이딩(재정의)"부모에게 물려받은 메서드를, 자식이 같은 이름·같은 모양으로 새로 정의하는 것"이에요. 물려받은 대로 쓰는 게 아니라, 자식에 맞게 내용을 바꿔치기 하는 거죠.

class Animal {
    void sound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override                          // 재정의라고 표시하는 애너테이션
    void sound() {                     // 이름·매개변수·반환형이 부모와 똑같음
        System.out.println("멍멍!");   // 내용만 자식 방식으로 교체
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        new Animal().sound();   // 동물이 소리를 냅니다.
        new Dog().sound();      // 멍멍!   ← 재정의된 버전이 실행
        new Cat().sound();      // 야옹!
    }
}
🏷️ @Override 애너테이션 @Override"나 지금 부모 메서드를 재정의하는 거야"라고 컴파일러에게 알리는 표시예요. 필수는 아니지만 강력 추천합니다.
이걸 붙이면, 만약 실수로 메서드 이름이나 매개변수를 잘못 써서 재정의가 아니라 그냥 새 메서드가 돼버렸을 때, 컴파일러가 오류로 잡아줘요. 오타로 인한 버그를 막아주는 안전장치예요.
🔤
애너테이션(annotation) — 코드에 @로 붙이는 표시(메모)예요. 프로그램 동작을 직접 바꾸진 않지만, 컴파일러나 도구에게 "이건 이런 코드야"라고 알려 줘요. @Override는 "이건 부모 걸 재정의한 메서드야"라는 표시라서, 실수를 잡아 주는 거예요.
📐
오버라이딩 규칙. 재정의하려면 부모 메서드와 메서드 이름·매개변수 목록·반환형(시그니처)이 같아야 해요. 하나라도 다르면 재정의가 아니라 다른 메서드가 됩니다. 또한 접근 범위를 부모보다 더 좁게 만들 수는 없어요(예: publicprivate으로 못 낮춤).

여기서 자주 헷갈리는 게 오버로딩(Overloading)과의 차이예요. 이름이 비슷해서 섞이기 쉬운데, 완전히 다른 개념이에요. 표로 확실히 갈라 둡시다.

구분오버라이딩 (Overriding · 재정의)오버로딩 (Overloading · 중복정의)
부모 메서드를 자식이 다시 정의같은 이름 메서드를 여러 개 정의
관계부모–자식 상속 관계에서 발생한 클래스 안에서 발생 (상속 무관)
메서드 이름같아야 함같아야 함
매개변수똑같아야달라야 함 (개수·타입)
반환형같아야 함상관없음 (구분 기준 아님)
한 줄 요약"내용을 바꿔치기""이름은 같고 매개변수만 다른 형제들"
🔁 번외 — 오버로딩 예시로 차이를 눈으로 확인
오버로딩은 한 클래스 안에서 이름이 같고 매개변수만 다른 메서드를 여러 개 두는 거예요. 상속과는 아무 관계가 없어요.
class Calculator {
    int add(int a, int b)         { return a + b; }        // ①
    int add(int a, int b, int c)  { return a + b + c; }    // ② 매개변수 개수 다름
    double add(double a, double b){ return a + b; }        // ③ 매개변수 타입 다름
}
add는 이름은 같지만 매개변수가 다르므로 각각 별개의 메서드예요(오버로딩). 반면 오버라이딩은 부모의 메서드 하나를 자식이 똑같은 모양으로 갈아끼우는 것이라 매개변수가 같아야 하죠. "오버로 = 매개변수 다름, 오버라이 = 매개변수 같음"으로 외워두면 편해요.
👑
최상위 부모
모든 클래스의 조상, Object
아무것도 안 물려받은 것 같아도, 사실 모든 클래스는 Object의 자손이에요.

자바의 모든 클래스는 Object 클래스맨 꼭대기 부모로 둡니다. extends를 안 써도, 자바가 자동으로 extends Object를 붙여줘요. 즉 세상의 모든 자바 클래스는 Object의 자손이에요.

// 이렇게 쓰면
class Person { }

// 자바 내부적으로는 이것과 같음
class Person extends Object { }

그 덕분에 우리가 만든 모든 객체는 Object에게 물려받은 메서드를 기본으로 가지고 있어요. 대표적인 두 가지를 봐요.

Object 메서드하는 일보통 어떻게?
toString()객체를 문자열로 표현. System.out.println(객체) 할 때 자동 호출돼요.사람이 읽기 좋게 오버라이딩해서 사용
equals()두 객체가 같은지 비교내용 비교하려면 오버라이딩
hashCode()객체의 해시값 반환 (컬렉션에서 사용)equals와 함께 재정의
getClass()객체의 실제 클래스 정보 반환그대로 사용
class Person {
    String name;
    Person(String name) { this.name = name; }

    @Override
    public String toString() {                 // Object의 toString() 재정의
        return "Person{name=" + name + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person("홍길동");
        System.out.println(p);   // Person{name=홍길동}  ← 재정의한 toString() 자동 호출
    }
}
💡
왜 중요할까. toString()·equals()모든 객체에 이미 있는 이유가 바로 상속이에요. 우리는 그걸 재정의(오버라이딩)해서 우리 클래스에 맞게 바꿔 쓸 뿐이죠. "상속이 실제로 이렇게 쓰인다"는 걸 보여주는 대표 사례예요.
1️⃣
단일 상속
자바는 부모가 하나뿐
자바 클래스는 여러 부모를 동시에 상속받을 수 없어요. 이유와 대안을 봐요.

자바 클래스는 부모를 딱 하나만 가질 수 있어요. 이걸 단일 상속(Single Inheritance)이라고 해요. 즉 extends 뒤에는 클래스 하나만 올 수 있고, 여러 클래스를 동시에 상속받는 다중 상속은 불가능합니다.

class A { }
class B { }

// ✅ 가능 — 부모 하나
class C extends A { }

// ❌ 불가능 — 자바는 클래스 다중 상속을 지원하지 않음 (컴파일 오류)
// class D extends A, B { }
🤔 왜 다중 상속을 막았을까 — "다이아몬드 문제" 만약 AB이름이 같은 메서드 run()을 각각 가졌다고 해봐요. D가 둘 다 상속받으면, D.run()A의 것인지 B의 것인지 모호해져요.
이런 충돌(다이아몬드 문제)을 애초에 없애려고, 자바는 클래스 다중 상속을 아예 금지했어요. "부모는 하나" 규칙으로 상속 구조를 단순하고 명확하게 유지하는 거죠.
🧩
그럼 여러 특징을 물려주고 싶으면? 자바는 인터페이스(interface)로 이 한계를 보완해요. 클래스는 부모를 하나만 extends 하지만, 인터페이스는 여러 개implements 할 수 있어요. 인터페이스는 다음 챕터들(CH 13)에서 자세히 배웁니다. 지금은 "클래스 다중 상속은 안 되지만, 인터페이스로 여러 개를 구현할 수 있다"만 기억하세요.
🌳 번외 — 상속은 여러 "단계"로 이어질 수 있어요
"부모 하나" 규칙은 직속 부모가 하나라는 뜻이지, 조상이 하나라는 뜻은 아니에요. 상속은 여러 단계로 층층이 이어질 수 있어요.
class Animal { }
class Pet extends Animal { }        // Pet 의 부모는 Animal
class Dog extends Pet { }           // Dog 의 부모는 Pet, 조상은 Animal → Object
DogPet을 물려받고, PetAnimal을 물려받아요. 그러니 DogPet·Animal·Object의 것을 모두 물려받죠. 이렇게 사슬처럼 이어지는 걸 상속 계층(계층 구조)이라고 해요. 그래도 각 클래스의 직속 부모는 여전히 하나랍니다.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.
CH 12 🎭 다형성 — 하나의 타입, 여러 모습. 상속 위에서 진짜 힘을 발휘하는 개념