1. 소개
Java는 기업 시스템부터 웹 애플리케이션, Android 개발에 이르기까지 다양한 분야에서 널리 사용되는 프로그래밍 언어입니다. 많은 기능 중에서도 “상속(inheritance)”은 객체 지향 프로그래밍을 배울 때 가장 필수적인 개념 중 하나입니다.
상속을 사용하면 새로운 클래스(서브클래스/자식 클래스)가 기존 클래스(슈퍼클래스/부모 클래스)의 기능을 이어받을 수 있습니다. 이를 통해 코드 중복을 줄이고 프로그램을 보다 쉽게 확장·유지보수할 수 있습니다. Java에서는 extends 키워드를 사용해 상속을 구현합니다.
이 글에서는 Java에서 extends 키워드의 역할, 기본 사용법, 실용적인 적용 사례, 그리고 흔히 묻는 질문들을 명확히 설명합니다. 이 가이드는 Java 초보자뿐만 아니라 상속을 다시 복습하고 싶은 개발자에게도 유용합니다. 끝까지 읽으면 상속의 장단점과 중요한 설계 고려사항을 완전히 이해하게 될 것입니다.
그럼 “Java에서 상속이란 무엇인가?”에 대해 자세히 살펴보겠습니다.
2. Java 상속이란?
Java 상속은 한 클래스(슈퍼클래스/부모 클래스)가 자신의 특성 및 기능을 다른 클래스(서브클래스/자식 클래스)에게 전달하는 메커니즘입니다. 상속을 통해 부모 클래스에 정의된 필드(변수)와 메서드(함수)를 자식 클래스에서 재사용할 수 있습니다.
이 메커니즘은 코드를 보다 체계적으로 조직·관리하고, 공통 프로세스를 중앙화하며, 기능을 유연하게 확장하거나 수정할 수 있게 해줍니다. 상속은 캡슐화와 다형성과 함께 객체 지향 프로그래밍(OOP)의 세 가지 핵심 기둥 중 하나입니다.
“is-a” 관계에 대하여
상속의 일반적인 예는 “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 메서드 모두를 사용할 수 있습니다.
상속을 사용하면 어떤 일이 발생할까?
- 부모 클래스에 공통 로직과 데이터를 중앙화하여 각 서브클래스에서 동일한 코드를 반복 작성할 필요가 줄어듭니다.
- 각 서브클래스는 고유한 동작을 추가하거나 부모 클래스의 메서드를 오버라이드할 수 있습니다.
상속을 활용하면 프로그램 구조를 정리하고 기능 추가·유지보수를 쉽게 할 수 있습니다. 하지만 상속이 항상 최선의 선택은 아니며, 설계 단계에서 실제로 “is-a 관계”가 존재하는지를 신중히 평가하는 것이 중요합니다.
3. extends 키워드 작동 방식
Java에서 extends 키워드는 클래스 상속을 명시적으로 선언합니다. 자식 클래스가 부모 클래스의 기능을 상속받을 때 클래스 선언부에 extends ParentClassName 구문을 사용합니다. 이를 통해 자식 클래스는 부모 클래스의 모든 public 멤버(필드와 메서드)를 직접 사용할 수 있게 됩니다.
기본 문법
class ParentClass {
// Fields and methods of the parent class
}
class ChildClass extends ParentClass {
// Fields and methods unique to the child class
}
예를 들어 앞서 소개한 Animal과 Dog 클래스를 사용하면 다음과 같습니다:
class Animal {
void eat() {
System.out.println("食べる");
}
}
class Dog extends Animal {
void bark() {
System.out.println("ワンワン");
}
}
Dog extends Animal이라고 작성함으로써 Dog 클래스는 Animal 클래스를 상속받아 eat 메서드를 사용할 수 있게 됩니다.
부모 클래스 멤버 사용하기
상속을 통해 자식 클래스의 인스턴스는 부모 클래스의 메서드와 필드에 접근할 수 있습니다(접근 제한자가 허용하는 경우에 한해):
Dog dog = new Dog();
dog.eat(); // Calls the parent class method
dog.bark(); // Calls the child class method
Important Notes
- Java는 단일 상속만 허용합니다(한 클래스만 상속 가능).
extends뒤에 여러 클래스를 지정할 수 없습니다. - 상속을 방지하고 싶다면 클래스에
final수식자를 사용할 수 있습니다.
Practical Development Tips
extends를 적절히 사용하면 공통 기능을 부모 클래스에 집중시켜 관리하고, 하위 클래스에서 동작을 확장하거나 맞춤화할 수 있습니다. 기존 코드를 수정하지 않고 새로운 기능을 추가하고자 할 때도 유용합니다.
4. Method Overriding and the super Keyword
상속을 사용할 때, 부모 클래스에 정의된 메서드의 동작을 변경하고 싶을 때가 있습니다. 이를 “메서드 오버라이딩”이라고 합니다. Java에서는 자식 클래스에 부모 클래스와 동일한 이름과 동일한 매개변수 목록을 가진 메서드를 정의함으로써 오버라이딩을 수행합니다.
Method Overriding
메서드를 오버라이드할 때는 보통 @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: ドッグフードを食べる
Using the super Keyword
오버라이드된 메서드 내부에서 부모 클래스의 원래 메서드를 호출하고 싶다면 super 키워드를 사용합니다.
class Dog extends Animal {
@Override
void eat() {
super.eat(); // Calls the parent class’s eat()
System.out.println("ドッグフードも食べる");
}
}
이는 먼저 부모 클래스의 eat 메서드를 실행하고, 그 뒤에 하위 클래스의 동작을 추가합니다.
Constructors and 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);
}
}
Summary
- 오버라이딩은 부모 클래스의 메서드를 자식 클래스에서 다시 정의하는 것을 의미합니다.
@Override어노테이션을 사용하는 것이 권장됩니다.- 부모 클래스의 메서드 구현을 재사용하고 싶을 때
super를 사용합니다. super는 부모 생성자를 호출할 때도 사용됩니다.
5. Advantages and Disadvantages of Inheritance
Java에서 상속을 사용하면 프로그램 설계와 개발에 많은 이점을 제공하지만, 잘못 사용할 경우 심각한 문제를 초래할 수 있습니다. 아래에서는 장점과 단점을 자세히 설명합니다.
Advantages of Inheritance
- 코드 재사용성 향상 공유 로직과 데이터를 부모 클래스에 정의하면 각 하위 클래스에서 동일한 코드를 반복할 필요가 없습니다. 이는 중복을 줄이고 유지보수성과 가독성을 높입니다.
- 확장 용이 새로운 기능이 필요할 때 기존 코드를 수정하지 않고도 부모 클래스를 기반으로 새로운 하위 클래스를 만들 수 있습니다. 이는 변경 영향도를 최소화하고 버그 발생 가능성을 낮춥니다.
- 다형성 지원 상속을 통해 “부모 클래스 타입의 변수로 하위 클래스 인스턴스를 참조”할 수 있습니다. 이는 공통 인터페이스와 다형적 동작을 활용한 유연한 설계를 가능하게 합니다.
Disadvantages of Inheritance
깊은 계층 구조는 설계를 복잡하게 만든다
상속 체인이 너무 깊어지면 동작이 어디에서 정의되는지 파악하기 어려워져 유지보수가 힘들어집니다.부모 클래스의 변경이 모든 하위 클래스에 영향을 미친다
부모 클래스의 동작을 수정하면 의도치 않게 모든 하위 클래스에 문제를 일으킬 수 있습니다. 부모 클래스는 신중하게 설계하고 업데이트해야 합니다.설계 유연성을 감소시킬 수 있다
상속을 과도하게 사용하면 클래스가 긴밀히 결합되어 향후 변경이 어려워집니다. 경우에 따라서는 “has-a” 관계를 이용한 컴포지션이 “is-a” 상속보다 더 유연합니다.
Summary
상속은 강력하지만 모든 상황에 의존하면 장기적인 문제가 발생할 수 있습니다. 진정한 “is-a 관계”가 존재하는지 항상 확인하고, 적절할 때만 상속을 적용하세요.
6. 상속과 인터페이스의 차이점
Java는 기능을 확장하고 조직화하기 위한 두 가지 중요한 메커니즘을 제공합니다: 클래스 상속(extends)과 인터페이스(implements). 두 메커니즘 모두 코드 재사용과 유연한 설계를 지원하지만, 구조와 의도된 사용 방식은 크게 다릅니다. 아래에서 차이점을 설명하고 선택 방법을 안내합니다.
extends와 implements의 차이점
- extends (상속)
- 하나의 클래스만 상속받을 수 있습니다(단일 상속).
- 부모 클래스의 필드와 완전히 구현된 메서드를 하위 클래스에서 직접 사용할 수 있습니다.
“is-a 관계”를 나타냅니다(예: Dog는 Animal이다).
implements (인터페이스 구현)
- 여러 인터페이스를 동시에 구현할 수 있습니다.
- 인터페이스는 메서드 선언만 포함합니다(Java 8 이후에는 default 메서드도 존재).
- “can-do 관계”를 나타냅니다(예: Dog는 짖을 수 있다, Dog는 걸을 수 있다).
인터페이스 사용 예시
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 클래스는 Walkable과 Barkable 두 인터페이스를 구현하여 다중 상속과 유사한 동작을 제공합니다.

인터페이스가 필요한 이유
Java는 클래스의 다중 상속을 금지합니다. 이는 부모 클래스가 동일한 메서드나 필드를 정의할 경우 충돌이 발생할 수 있기 때문입니다. 인터페이스는 충돌하는 구현을 상속받지 않고도 클래스가 여러 “타입”을 채택하도록 함으로써 이 문제를 해결합니다.
올바른 사용 방법
- 클래스 간에 명확한 “is-a 관계”가 존재할 때
extends를 사용합니다. - 여러 클래스에 공통된 행동 계약을 제공하고 싶을 때
implements를 사용합니다.
예시
- “Dog는 Animal이다” →
Dog extends Animal - “Dog는 걸을 수 있고 짖을 수 있다” →
Dog implements Walkable, Barkable
Summary
- 클래스는 하나의 부모 클래스만 상속받을 수 있지만, 여러 인터페이스를 구현할 수 있습니다.
- 설계 의도에 따라 상속과 인터페이스를 선택하면 코드가 깔끔하고 유연하며 유지보수가 쉬워집니다.
7. 상속 사용 시 모범 사례
Java에서 상속은 강력하지만, 부적절하게 사용하면 프로그램이 경직되고 유지보수가 어려워집니다. 아래는 상속을 안전하고 효과적으로 사용하기 위한 모범 사례와 가이드라인입니다.
언제 상속을 사용하고 언제 피해야 할까
- 상속을 사용할 때:
- 명확한 “is-a 관계”가 존재할 경우(예: Dog는 Animal이다).
- 부모 클래스의 기능을 재사용하고 확장하고 싶을 때.
중복 코드를 제거하고 공통 로직을 중앙화하고 싶을 때.
상속을 피해야 할 때:
- 단순히 코드 재사용을 위해 사용한다면(이는 종종 부자연스러운 클래스 설계로 이어짐).
- “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: 두 클래스에서 같은 이름의 필드가 정의되면, 자식 클래스의 필드가 부모 클래스의 필드를 숨깁니다 (shadowing). 메서드는 다르게 동작합니다: 시그니처가 일치하면 자식 메서드가 부모 메서드를 오버라이드합니다. 필드는 오버라이드할 수 없고—숨길 수만 있습니다.
Q6: 상속 깊이가 너무 커지면 어떻게 되나요?
A6: 깊은 상속 계층은 코드를 이해하고 유지보수하기 어렵게 만듭니다. 로직이 어디에 정의되었는지 추적하기 어렵습니다. 유지보수 가능한 설계를 위해 상속 깊이를 얕게 유지하고 역할을 명확히 분리하세요.
Q7: 오버라이딩과 오버로딩의 차이는 무엇인가요?
A7: 오버라이딩은 자식 클래스에서 부모 클래스의 메서드를 재정의합니다. 오버로딩은 같은 클래스에서 같은 이름의 메서드를 여러 개 정의하되 매개변수 타입이나 개수를 다르게 합니다.
Q8: 추상 클래스와 인터페이스를 어떻게 다르게 사용해야 하나요?
A8: 추상 클래스는 관련 클래스들 사이에 공유 구현이나 필드를 제공하고 싶을 때 사용합니다. 인터페이스는 여러 클래스들이 구현할 수 있는 동작 계약을 정의할 때 사용합니다. 공유 코드에는 추상 클래스를 사용하고, 여러 타입을 나타내거나 여러 “능력”이 필요할 때는 인터페이스를 사용하세요.

