Java 상속 마스터하기: extends 키워드 작동 원리 (완전 가이드)

1. Introduction

Java는 기업 시스템부터 웹 애플리케이션, Android 개발에 이르기까지 다양한 분야에서 널리 사용되는 프로그래밍 언어입니다. 많은 기능 중에서도 객체 지향 프로그래밍을 배울 때 가장 필수적인 개념 중 하나가 “상속”입니다.
상속을 사용하면 새로운 클래스(서브클래스/자식 클래스)가 기존 클래스(슈퍼클래스/부모 클래스)의 기능을 물려받을 수 있습니다. 이를 통해 코드 중복을 줄이고 프로그램을 확장·유지보수하기가 쉬워집니다. Java에서는 상속을 extends 키워드로 구현합니다.
이 글에서는 Java에서 extends 키워드의 역할, 기본 사용법, 실용적인 적용 사례, 그리고 흔히 묻는 질문들을 명확히 설명합니다. 이 가이드는 Java 초보자뿐만 아니라 상속을 다시 복습하고 싶은 개발자에게도 유용합니다. 끝까지 읽으면 상속의 장단점과 중요한 설계 고려사항을 완전히 이해하게 될 것입니다.
그럼 “Java에서 상속이란 무엇인가?”를 자세히 살펴보겠습니다.

2. What Is Java Inheritance?

Java 상속은 한 클래스(슈퍼클래스/부모 클래스)가 자신의 특성 및 기능을 다른 클래스(서브클래스/자식 클래스)에게 전달하는 메커니즘입니다. 상속을 통해 부모 클래스에 정의된 필드(변수)와 메서드(함수)를 자식 클래스에서 재사용할 수 있습니다.
이 메커니즘은 코드를 보다 체계적으로 조직·관리하고, 공통 로직을 중앙화하며, 기능을 유연하게 확장·수정할 수 있게 해줍니다. 상속은 캡슐화와 다형성과 함께 객체 지향 프로그래밍(OOP)의 세 핵심 축 중 하나입니다.

About the “is-a” Relationship

상속의 일반적인 예는 “is-a 관계”입니다. 예를 들어, “Dog은 Animal이다.” 라는 의미는 Dog 클래스가 Animal 클래스를 상속한다는 뜻입니다. 개는 동물의 특성과 행동을 물려받으면서 자신만의 고유한 특징을 추가할 수 있습니다.

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("ワンワン");
    }
}

위 예시에서 Dog 클래스는 Animal 클래스를 상속합니다. Dog 인스턴스는 bark 메서드와 상속받은 eat 메서드 모두를 사용할 수 있습니다.

What Happens When You Use Inheritance?

  • 부모 클래스에 공통 로직과 데이터를 중앙화하여 각 서브클래스에서 동일한 코드를 반복해서 작성할 필요가 줄어듭니다.
  • 각 서브클래스는 고유한 동작을 추가하거나 부모 클래스의 메서드를 오버라이드할 수 있습니다.

상속을 사용하면 프로그램 구조를 정리하고 기능 추가·유지보수가 쉬워집니다. 하지만 상속이 항상 최선의 선택은 아니며, 설계 단계에서 진정한 “is-a 관계”가 존재하는지 신중히 평가하는 것이 중요합니다.

3. How the extends Keyword Works

Java에서 extends 키워드는 클래스 상속을 명시적으로 선언합니다. 자식 클래스가 부모 클래스의 기능을 물려받을 때 클래스 선언부에 extends ParentClassName 구문을 사용합니다. 이를 통해 자식 클래스는 부모 클래스의 모든 public 멤버(필드와 메서드)를 직접 사용할 수 있습니다.

Basic Syntax

class ParentClass {
    // Fields and methods of the parent class
}

class ChildClass extends ParentClass {
    // Fields and methods unique to the child class
}

예를 들어 앞서 소개한 AnimalDog 클래스를 사용하면 다음과 같습니다.

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("ワンワン");
    }
}

Dog extends Animal이라고 작성하면 Dog 클래스는 Animal 클래스를 상속받아 eat 메서드를 사용할 수 있게 됩니다.

Using Parent Class Members

상속을 통해 자식 클래스의 인스턴스는 접근 제한자가 허용하는 한 부모 클래스의 메서드와 필드에 접근할 수 있습니다.

Dog dog = new Dog();
dog.eat();   // Calls the parent class method
dog.bark();  // Calls the child class method

Important Notes

  • Java는 단일 상속만 허용합니다(한 클래스만 상속). extends 뒤에 여러 클래스를 지정할 수 없습니다.
  • 상속을 방지하고 싶다면 클래스에 final 수식자를 사용할 수 있습니다.

실무 개발 팁

extends를 올바르게 사용하면 공통 기능을 부모 클래스에 집중시켜 두고, 하위 클래스에서 동작을 확장하거나 맞춤화할 수 있습니다. 또한 기존 코드를 수정하지 않고 새로운 기능을 추가하고자 할 때 유용합니다.

4. 메서드 오버라이딩과 super 키워드

상속을 사용할 때, 부모 클래스에 정의된 메서드의 동작을 바꾸고 싶을 때가 있습니다. 이를 메서드 오버라이딩이라고 합니다. Java에서는 자식 클래스에 부모 클래스와 동일한 이름과 동일한 매개변수 목록을 가진 메서드를 정의함으로써 오버라이딩을 수행합니다.

메서드 오버라이딩

메서드를 오버라이드할 때는 보통 @Override 어노테이션을 추가합니다. 이는 컴파일러가 메서드 이름이나 시그니처를 실수로 잘못 작성했는지 감지하는 데 도움이 됩니다.

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("ドッグフードを食べる");
    }
}

위 예시에서 Dog 클래스는 eat 메서드를 오버라이드합니다. Dog 인스턴스에서 eat을 호출하면 출력은 “ドッグフードを食べる”가 됩니다.

Dog dog = new Dog();
dog.eat(); // Displays: ドッグフードを食べる

super 키워드 사용

오버라이드된 메서드 안에서 부모 클래스의 원래 메서드를 호출하고 싶다면 super 키워드를 사용합니다.

class Dog extends Animal {
    @Override
    void eat() {
        super.eat(); // Calls the parent class’s eat()
        System.out.println("ドッグフードも食べる");
    }
}

이 코드는 먼저 부모 클래스의 eat 메서드를 실행한 뒤, 하위 클래스의 동작을 추가합니다.

생성자와 super

부모 클래스에 매개변수가 있는 생성자가 존재한다면, 자식 클래스는 자신의 생성자 첫 줄에서 super(arguments)를 명시적으로 호출해야 합니다.

class Animal {
    Animal(String name) {
        System.out.println("Animal: " + name);
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
        System.out.println("Dog: " + name);
    }
}

요약

  • 오버라이딩은 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것을 의미합니다.
  • @Override 어노테이션 사용을 권장합니다.
  • 부모 클래스의 메서드 구현을 재사용하고 싶을 때 super를 사용합니다.
  • 부모 클래스의 생성자를 호출할 때도 super를 사용합니다.

5. 상속의 장점과 단점

Java에서 상속을 사용하면 프로그램 설계와 개발에 많은 이점을 제공하지만, 잘못 사용하면 심각한 문제를 초래할 수 있습니다. 아래에서는 장점과 단점을 자세히 설명합니다.

상속의 장점

  1. 코드 재사용성 향상
    공통 로직과 데이터를 부모 클래스에 정의하면 각 하위 클래스에서 동일한 코드를 반복할 필요가 없어집니다. 이는 중복을 줄이고 유지보수성 및 가독성을 높입니다.
  2. 확장성 용이
    새로운 기능이 필요할 때 기존 코드를 수정하지 않고도 부모 클래스를 기반으로 새로운 하위 클래스를 만들 수 있습니다. 이는 변경 영향도를 최소화하고 버그 발생 가능성을 낮춥니다.
  3. 다형성 지원
    상속을 통해 “부모 클래스 타입의 변수로 자식 클래스 인스턴스를 참조”할 수 있습니다. 이는 공통 인터페이스와 다형적 동작을 활용한 유연한 설계를 가능하게 합니다.

상속의 단점

  1. 깊은 계층 구조는 설계를 복잡하게 만든다 상속 체인이 너무 깊어지면 행동이 어디서 정의되는지 이해하기 어려워져 유지보수가 힘들어집니다.
  2. 부모 클래스의 변경이 모든 하위 클래스에 영향을 미친다 부모 클래스의 동작을 수정하면 의도치 않게 모든 하위 클래스에 문제를 일으킬 수 있습니다. 부모 클래스는 신중한 설계와 업데이트가 필요합니다.
  3. 설계 유연성을 감소시킬 수 있다 상속을 과도하게 사용하면 클래스가 강하게 결합되어 향후 변경이 어려워집니다. 경우에 따라서는 컴포지션을 이용한 “has-a” 관계가 “is-a” 상속보다 더 유연합니다.

Summary

상속은 강력하지만 모든 것에 의존하면 장기적인 문제가 발생할 수 있습니다. 진정한 “is-a 관계”가 존재하는지 항상 확인하고, 적절할 때만 상속을 적용하십시오.

6. 상속과 인터페이스의 차이점

Java는 기능을 확장하고 조직화하기 위한 두 가지 중요한 메커니즘을 제공합니다: 클래스 상속(extends)과 인터페이스(implements). 두 방법 모두 코드 재사용과 유연한 설계를 지원하지만, 구조와 의도된 사용 방식은 크게 다릅니다. 아래에서 차이점을 설명하고 선택 방법을 안내합니다.

extendsimplements의 차이점

  • extends (상속)
  • 하나의 클래스만 상속받을 수 있습니다(단일 상속).
  • 부모 클래스의 필드와 완전히 구현된 메서드를 하위 클래스에서 직접 사용할 수 있습니다.
  • “is-a 관계”(예: 개는 동물이다)를 나타냅니다.
  • implements (인터페이스 구현)
  • 여러 인터페이스를 동시에 구현할 수 있습니다.
  • 인터페이스는 메서드 선언만 포함합니다(Java 8 이후에는 default 메서드가 존재합니다).
  • “can-do 관계”(예: 개는 짖을 수 있다, 개는 걸을 수 있다)를 나타냅니다.

인터페이스 사용 예시

interface Walkable {
    void walk();
}

interface Barkable {
    void bark();
}

class Dog implements Walkable, Barkable {
    public void walk() {
        System.out.println("歩く");
    }
    public void bark() {
        System.out.println("ワンワン");
    }
}

이 예시에서 Dog 클래스는 WalkableBarkable 두 인터페이스를 구현하여 다중 상속과 유사한 동작을 제공합니다.

인터페이스가 필요한 이유

Java는 클래스의 다중 상속을 금지합니다. 이는 부모 클래스가 동일한 메서드나 필드를 정의할 경우 충돌을 일으킬 수 있기 때문입니다. 인터페이스는 충돌하는 구현을 상속받지 않고도 클래스가 여러 “타입”을 채택하도록 함으로써 이 문제를 해결합니다.

올바른 사용 방법

  • 클래스 간에 명확한 “is-a 관계”가 존재할 때 extends를 사용합니다.
  • 여러 클래스에 공통된 행동 계약을 제공하고자 할 때 implements를 사용합니다.

예시:

  • “개는 동물이다” → Dog extends Animal
  • “개는 걸을 수 있고 짖을 수 있다” → Dog implements Walkable, Barkable

Summary

  • 클래스는 하나의 부모 클래스만 상속받을 수 있지만, 여러 인터페이스를 구현할 수 있습니다.
  • 설계 의도에 따라 상속과 인터페이스를 선택하면 깔끔하고 유연하며 유지보수 가능한 코드를 만들 수 있습니다.

7. 상속 사용 시 모범 사례

Java에서 상속은 강력하지만 부적절하게 사용하면 프로그램이 경직되고 유지보수가 어려워집니다. 아래는 상속을 안전하고 효과적으로 사용하기 위한 모범 사례와 가이드라인입니다.

언제 상속을 사용하고 언제 피해야 하는가

  • 상속을 사용할 때:
  • 명확한 “is-a 관계”가 존재할 때(예: 개는 동물이다).
  • 부모 클래스의 기능을 재사용하고 확장하고자 할 때.
  • 중복 코드를 제거하고 공통 로직을 중앙화하고자 할 때.
  • 상속을 피해야 할 때:
  • 단순히 코드 재사용을 위해 사용하려는 경우(이는 부자연스러운 클래스 설계로 이어질 수 있습니다).
  • “has-a 관계”가 더 적절할 때 — 이 경우 컴포지션을 고려하십시오.

상속과 컴포지션 선택


  • 상속 (extends): is-a 관계
  • 예시: Dog extends Animal
  • 서브클래스가 슈퍼클래스의 유형을 실제로 나타낼 때 유용합니다.
  • 구성 (has-a 관계)
  • 예시: 자동차는 엔진을 가집니다
  • 내부적으로 다른 클래스의 인스턴스를 사용하여 기능을 추가합니다.
  • 더 유연하고 향후 변경에 적응하기 쉽습니다.

상속 오용 방지를 위한 설계 지침

  • 상속 계층을 너무 깊게 만들지 마세요 (3단계 이하로 유지).
  • 많은 서브클래스가 동일한 부모를 상속한다면, 부모의 책임이 적절한지 재평가하세요.
  • 부모 클래스의 변경이 모든 서브클래스에 영향을 미칠 위험을 항상 고려하세요.
  • 상속을 적용하기 전에 인터페이스와 구성과 같은 대안을 고려하세요.

final 수식자를 사용한 상속 제한

  • 클래스에 final을 추가하면 상속을 방지합니다.
  • 메서드에 final을 추가하면 서브클래스에서 오버라이드할 수 없습니다.
    final class Utility {
        // This class cannot be inherited
    }
    
    class Base {
        final void show() {
            System.out.println("オーバーライド禁止");
        }
    }
    

문서화 및 주석 강화

  • Javadoc이나 주석에 상속 관계와 클래스 설계 의도를 문서화하면 향후 유지보수가 훨씬 쉬워집니다.

요약

상속은 편리하지만 의도적으로 사용해야 합니다. 항상 “이 클래스가 정말 부모 클래스의 일종인가?” 라고 스스로 물어보세요. 확신이 서지 않으면 구성을 사용하거나 인터페이스를 대안으로 고려하세요.

8. 요약

지금까지 Java 상속과 extends 키워드를 기본부터 실용적인 사용까지 자세히 설명했습니다. 아래는 이 글에서 다룬 핵심 포인트를 정리한 내용입니다.

  • Java 상속은 서브클래스가 슈퍼클래스의 데이터와 기능을 이어받아 효율적이고 재사용 가능한 프로그램 설계를 가능하게 합니다.
  • extends 키워드는 부모와 자식 클래스 간의 관계(“is-a 관계”)를 명확히 합니다.
  • 메서드 오버라이딩과 super 키워드를 사용하면 상속된 동작을 확장하거나 맞춤화할 수 있습니다.
  • 상속은 코드 재사용, 확장성, 다형성 지원 등 많은 장점을 제공하지만, 깊거나 복잡한 계층 구조, 광범위한 영향의 변경 등 단점도 있습니다.
  • 상속, 인터페이스, 구성을 구분하는 이해는 올바른 설계 접근 방식을 선택하는 데 중요합니다.
  • 상속을 과도하게 사용하지 말고, 설계 의도와 근거를 명확히 하세요.

상속은 Java 객체지향 프로그래밍의 핵심 개념 중 하나입니다. 규칙과 모범 사례를 이해하면 실제 개발에 효과적으로 적용할 수 있습니다.

9. 자주 묻는 질문 (FAQ)

Q1: 클래스가 Java에서 상속될 때 부모 클래스 생성자는 어떻게 되나요?
A1: 부모 클래스에 인수가 없는 (기본) 생성자가 있으면 자식 클래스 생성자에서 자동으로 호출됩니다. 부모 클래스에 매개변수가 있는 생성자만 존재한다면, 자식 클래스는 자신의 생성자 시작 부분에서 super(arguments) 를 사용해 명시적으로 호출해야 합니다.

Q2: Java에서 클래스의 다중 상속을 할 수 있나요?
A2: 아니요. Java는 클래스의 다중 상속을 지원하지 않습니다. 클래스는 extends 를 사용해 하나의 부모 클래스만 상속받을 수 있습니다. 다만, implements 를 사용해 여러 인터페이스를 구현할 수 있습니다.

Q3: 상속과 컴포지션의 차이점은 무엇인가요?
A3: 상속은 “is-a(관계)”를 나타내며, 자식 클래스가 부모 클래스의 기능과 데이터를 재사용합니다. 컴포지션은 “has-a(관계)”를 나타내며, 한 클래스가 다른 클래스의 인스턴스를 포함합니다. 컴포지션은 더 큰 유연성을 제공하고, 결합도를 낮추거나 향후 확장이 필요할 때 선호됩니다.

Q4: final 한정자는 상속과 오버라이딩을 제한하나요?
A4: 네. 클래스가 final 로 선언되면 더 이상 상속될 수 없습니다. 메서드가 final 로 선언되면 서브클래스에서 해당 메서드를 오버라이드할 수 없습니다. 이는 일관된 동작을 보장하거나 보안상의 이유로 유용합니다.

Q5: 부모 클래스와 자식 클래스가 같은 이름의 필드나 메서드를 정의하면 어떻게 되나요?
A5: 같은 이름의 필드가 두 클래스에 모두 정의된 경우, 자식 클래스의 필드가 부모 클래스의 필드를 가립니다(섀도잉). 메서드는 다르게 동작합니다: 시그니처가 동일하면 자식 클래스의 메서드가 부모 메서드를 오버라이드합니다. 필드는 오버라이드될 수 없으며, 오직 숨겨질 뿐입니다.

Q6: 상속 깊이가 너무 깊어지면 어떻게 되나요?
A6: 깊은 상속 구조는 코드를 이해하고 유지보수하기 어렵게 만듭니다. 로직이 어디에 정의되어 있는지 추적하기 힘들어지므로, 유지보수 가능한 설계를 위해 상속 깊이를 얕게 유지하고 역할을 명확히 구분하는 것이 좋습니다.

Q7: 오버라이딩과 오버로딩의 차이점은 무엇인가요?
A7: 오버라이딩은 부모 클래스의 메서드를 자식 클래스에서 재정의하는 것이고, 오버로딩은 같은 클래스 내에서 같은 이름이지만 매개변수 타입이나 개수가 다른 여러 메서드를 정의하는 것입니다.

Q8: 추상 클래스와 인터페이스는 어떻게 다르게 사용해야 하나요?
A8: 추상 클래스는 관련된 클래스들 간에 공통 구현이나 필드를 제공하고자 할 때 사용합니다. 인터페이스는 여러 클래스가 구현할 수 있는 행동 계약을 정의하고자 할 때 사용합니다. 공통 코드를 공유하려면 추상 클래스를, 여러 타입을 표현하거나 여러 “능력”이 필요할 때는 인터페이스를 사용하세요.