Java 상속 완전 정복 — extends로 코드를 물려받는 법
Java 상속(inheritance)의 동작 원리와 extends 키워드 사용법, protected 접근 제어자, super() 생성자 체인, IS-A 관계 설계 원칙까지 예제 중심으로 완전 정복한다
지난 글에서 getter와 setter로 캡슐화를 구현하는 방법을 살펴봤다. 이번에는 객체지향의 또 다른 핵심 기둥인 상속(Inheritance) 을 다룬다. 상속을 이해하면 중복 코드를 획기적으로 줄이고, 계층 구조를 통해 더 명확한 설계가 가능해진다.
상속이란 무엇인가
상속은 기존 클래스(부모 클래스, 슈퍼클래스)의 필드와 메서드를 새로운 클래스(자식 클래스, 서브클래스)가 물려받는 메커니즘이다. extends 키워드 하나로 수십 줄의 코드를 재사용할 수 있다.
// 부모 클래스
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void sound() {
System.out.println("...");
}
}
// 자식 클래스 — Animal을 상속
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 부모 생성자 호출 (반드시 첫 줄)
this.breed = breed;
}
@Override
public void sound() {
System.out.println("멍멍!");
}
public void fetch() {
System.out.println(name + "이(가) 공을 가져왔다!"); // 상속된 필드 사용
}
}
Dog extends Animal이라고 선언하는 순간, Dog는 Animal의 name, age 필드와 getName(), sound() 메서드를 자동으로 갖게 된다.
계층 구조 시각화
아래는 Animal을 부모로 두고 Dog와 Cat이 각각 상속하는 구조다. 두 자식 클래스는 공통 상태(name, age)는 부모에서, 고유한 상태와 행동은 스스로 추가한다.
IS-A 관계: 상속의 핵심 설계 기준
상속을 사용하기 전에 반드시 자문해야 할 질문이 있다. “자식은 부모인가?(IS-A)”
Dog is-a Animal→ 자연스럽다 ✓Engine is-a Car→ 어색하다 ✗ (엔진은 자동차가 아니라 자동차의 일부)
IS-A 관계가 성립하지 않는다면 상속 대신 컴포지션(has-a) 을 써야 한다. 상속을 무분별하게 사용하면 부모 클래스 변경이 모든 자식에게 영향을 미쳐 유지보수가 어려워진다.
protected 접근 제어자
부모 클래스의 private 필드는 자식 클래스에서도 직접 접근할 수 없다. 자식이 부모 필드를 직접 사용하길 원한다면 protected를 써야 한다.
public class Animal {
protected String name; // 같은 패키지 + 자식 클래스에서 접근 가능
private int age; // 자식 클래스에서도 직접 접근 불가
}
public class Dog extends Animal {
public void introduce() {
System.out.println(name); // OK — protected
// System.out.println(age); // 컴파일 에러 — private
System.out.println(getAge()); // OK — getter 사용
}
}
일반적으로는 private + getter를 유지하는 쪽이 캡슐화에 더 유리하다. protected는 상속 계층 내에서 공유할 이유가 명확할 때만 사용한다.
super() — 부모 생성자 호출
자식 클래스의 생성자에서는 첫 번째 줄에 반드시 super()를 호출해야 한다. 명시하지 않으면 컴파일러가 자동으로 super()(인수 없는 버전)를 삽입한다. 부모에 기본 생성자가 없다면 컴파일 에러가 발생한다.
public class Cat extends Animal {
private boolean indoor;
public Cat(String name, int age, boolean indoor) {
super(name, age); // 필수: 부모 Animal(String, int) 호출
this.indoor = indoor;
}
}
Cat c = new Cat("나비", 3, true);
System.out.println(c.getName()); // "나비" — 상속된 메서드
상속 코드 구조 전체 흐름
super()는 부모의 초기화 로직을 재사용하는 핵심이다. 오른쪽 코드에서 볼 수 있듯이 Dog 생성자는 name과 age 초기화를 부모에 위임하고, 자신은 breed만 초기화한다.
상속 체인과 Object 클래스
Java의 모든 클래스는 명시적 extends가 없더라도 java.lang.Object를 암묵적으로 상속한다. 따라서 상속 체인은 항상 Object에서 끝난다.
Object
└── Animal
├── Dog
└── Cat
Object가 제공하는 toString(), equals(), hashCode() 같은 메서드를 모든 Java 객체에서 호출할 수 있는 이유가 바로 이 때문이다.
단일 상속과 다중 인터페이스
Java는 단일 상속만 지원한다. 클래스는 하나의 부모 클래스만 extends할 수 있다. 다중 상속이 필요한 경우에는 인터페이스(implements)로 해결한다.
// 불가 — 컴파일 에러
public class Hybrid extends Dog, Cat { }
// 가능 — 인터페이스는 여러 개 구현 가능
public class RobotDog extends Dog implements Chargeable, Trackable { }
상속 사용 시 주의사항
상속은 강력하지만 남용하면 독이 된다.
- 깊은 상속 계층(4단계 이상) 은 추적이 어렵고 부모 변경 시 충격이 크다.
- 부모 클래스에 구체 로직을 너무 많이 두면 자식이 부모 구현에 강하게 결합된다.
- 설계 원칙상 IS-A가 명확하지 않으면 컴포지션을 선택한다.
final class로 선언된 클래스는 상속이 불가하다(String,Integer등).
지난 글: Java getter와 setter 완전 정복 — 올바른 설계와 안티패턴
다음 글: Java 메서드 오버라이딩 완전 정복 — @Override와 재정의 규칙
읽어주셔서 감사합니다. 😊