콘텐츠로 이동

상속 (Inheritance)

왜 쓰는지

여러 클래스가 공통된 특성을 가질 때 이를 반복 작성하는 것은 비효율적입니다: - 코드 중복 제거: 공통 메서드를 한 곳에 작성 - 의미 표현: "개는 동물이다" (is-a 관계) 명확히 함 - 다형성 기반: 부모 타입으로 자식 객체를 다룸

핵심: 상속은 코드 재사용을 위한 수단이 아니라, 공통 개념과 is-a 관계를 표현하고 다형성을 구현하기 위한 메커니즘입니다.

어떻게 쓰는지

기본 상속

// 부모 클래스 (상위 타입)
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void sleep() {
        System.out.println(name + "은 잠을 잔다");
    }

    public void eat() {
        System.out.println(name + "은 먹이를 먹는다");
    }
}

// 자식 클래스 (하위 타입)
public class Dog extends Animal {
    public Dog(String name) {
        super(name);  // 부모 생성자 호출
    }

    @Override
    public void sleep() {
        System.out.println("개는 달콤하게 잔다");
    }

    public void bark() {
        System.out.println("멍멍!");
    }
}

// 사용
Animal dog = new Dog("뽀삐");  // 부모 타입으로 자식 객체 참조
dog.sleep();                   // "개는 달콤하게 잔다" (메서드 오버라이딩)
dog.eat();                     // "뽀삐은 먹이를 먹는다" (부모 메서드 사용)
// dog.bark();                 // ❌ 컴파일 에러 (부모 타입은 bark 모름)

메서드 오버라이딩 (Method Overriding)

public class Parent {
    public void display() {
        System.out.println("부모");
    }

    public final void finalMethod() {  // final이면 오버라이드 불가
        System.out.println("재정의 불가");
    }
}

public class Child extends Parent {
    @Override  // 부모 메서드를 재정의
    public void display() {
        System.out.println("자식");
    }

    // @Override
    // public void finalMethod() {}  // ❌ 컴파일 에러
}

super 키워드

public class Parent {
    protected String name = "부모";

    public void greet() {
        System.out.println("부모: " + name);
    }
}

public class Child extends Parent {
    public Child() {
        super();  // 부모 생성자 호출 (명시하지 않으면 자동 호출)
    }

    @Override
    public void greet() {
        super.greet();                    // 부모 메서드 호출
        System.out.println("자식: " + name);
    }
}

타입 검사와 캐스팅

Animal animal = new Dog("뽀삐");

// instanceof: 실제 객체 타입 확인
if (animal instanceof Dog) {
    Dog dog = (Dog) animal;  // 안전한 캐스팅
    dog.bark();
}

// 부정확한 캐스팅
Cat cat = (Cat) animal;  // ❌ ClassCastException 발생 (runtime error)

언제 쓰는지

상황 선택 이유
공통 특성 표현 ✅ 상속 is-a 관계 명확
다형성 구현 ✅ 상속 부모 타입으로 자식 다루기
프레임워크 확장 ✅ 상속 Spring의 @Service 등
has-a 관계 ❌ 컴포지션 "엔진은 자동차의 부분"
인터페이스로 충분 ❌ 인터페이스 계약만 필요한 경우

장점

장점 설명
코드 재사용 공통 메서드를 부모에 작성
계층적 구조 개념의 일반화-특수화 표현
다형성 부모 타입으로 자식 객체 다루기
유지보수 용이 공통 변경이 자동으로 전파
메서드 오버라이딩 자식이 메서드를 특화

단점

단점 설명
강한 결합 부모 변경이 자식에 전파
캡슐화 붕괴 부모의 protected 멤버에 의존
유연성 부족 런타임에 상속 관계 변경 불가
잘못된 is-a 관계 개념적으로 성립하지 않는 상속
다이아몬드 문제 다중 상속 시 모호성 발생

특징

1. 계층 구조 (Hierarchy)

// 계층: Object → Animal → Mammal → Dog
Object
└── Animal (동물)
    ├── Mammal (포유동물)
       ├── Dog ()
       ├── Cat (고양이)
       └── Human (인간)
    ├── Bird (조류)
    └── Fish (어류)

2. 메서드 해석 순서 (Method Lookup)

class Parent {
    public void method() { System.out.println("부모"); }
}

class Child extends Parent {
    @Override
    public void method() { System.out.println("자식"); }
}

Parent obj = new Child();
obj.method();  // "자식" (실제 객체 타입의 메서드 호출)

3. 생성자 호출 순서

class Parent {
    Parent() {
        System.out.println("부모 생성자");
    }
}

class Child extends Parent {
    Child() {
        super();  // 부모 생성자 먼저 호출 (명시하지 않으면 자동)
        System.out.println("자식 생성자");
    }
}

new Child();
// 출력:
// 부모 생성자
// 자식 생성자

4. 접근 제어자와 상속

public class Parent {
    public void publicMethod() {}      // 모든 곳에서 접근 가능
    protected void protectedMethod() {} // 자식에서 접근 가능
    private void privateMethod() {}    // 자식도 접근 불가
    void packageMethod() {}            // 같은 패키지만 가능
}

public class Child extends Parent {
    @Override
    protected void protectedMethod() {  // 접근성 확대 가능 (protected → public)
        super.protectedMethod();
    }

    // private 메서드는 override 불가 (상속 대상이 아님)
}

5. 다중 상속과 인터페이스

// Java는 클래스 다중 상속 불가
// class Dog extends Animal, Pet {}  // ❌ 불가능

// 인터페이스는 다중 구현 가능
interface Pet {
    void play();
}

interface Hunter {
    void hunt();
}

public class Dog extends Animal implements Pet, Hunter {
    @Override
    public void play() { System.out.println("놀기"); }

    @Override
    public void hunt() { System.out.println("사냥"); }
}

주의할 점

❌ 잘못된 is-a 관계

// 나쁜 설계: has-a를 is-a로 표현
public class Car extends Engine {}  // "자동차는 엔진이다?" (틀림)

// 올바른 설계: 컴포지션 사용
public class Car {
    private Engine engine;  // "자동차는 엔진을 가진다"
}

✅ 올바른 방식: - is-a 관계만 상속 사용 - has-a 관계는 컴포지션 사용

❌ 부모 변경의 파급 효과

public class Parent {
    protected int value = 10;
}

public class Child extends Parent {
    public void display() {
        System.out.println(value);  // protected 필드에 의존
    }
}

// 나중에 Parent를 변경하면...
public class Parent {
    private int value = 10;          // protected → private로 변경
    // protected int newValue = 20;  // 새로운 필드 추가
}

// Child는 깨짐! → "부모의 내부 구현에 강하게 의존"

✅ 올바른 방식: - 부모의 public 메서드만 사용 - protected 필드는 최소화

⚠️ 다이아몬드 구조 (Java 8+)

// Java 8 이후 Interface의 default 메서드로 재발생
interface A {
    default void method() { System.out.println("A"); }
}

interface B extends A {
    default void method() { System.out.println("B"); }
}

interface C extends A {
    default void method() { System.out.println("C"); }
}

// ❌ D는 method를 어디서 상속받을지 모호함
class D implements B, C {
    // 명시적으로 재정의 필요
    @Override
    public void method() {
        B.super.method();  // B의 method 호출
    }
}

⚠️ 부모 메서드의 의도 무시

class Parent {
    public void process() {
        validate();   // 부모가 validate 호출을 기대
        execute();
    }

    protected void validate() { ... }
    protected void execute() { ... }
}

class Child extends Parent {
    @Override
    protected void validate() {
        // 부모의 validate 호출 없이 다른 검증 수행
        // → 부모의 process() 논리가 깨짐
    }
}

베스트 프랙티스

권장 방식 이유
is-a 관계를 먼저 확인 "자식은 부모다"라는 문장이 자연스럽지 않으면 상속 후보가 아님
상속보다 컴포지션 우선 검토 has-a 관계는 필드로 포함하는 편이 결합도가 낮음
부모 클래스의 protected 범위 최소화 자식이 부모 내부 구현에 강하게 묶이는 것을 줄임
오버라이딩에는 항상 @Override 사용 시그니처 오타를 컴파일 타임에 잡을 수 있음
확장 의도가 없으면 final 고려 잘못된 상속과 의도치 않은 오버라이딩을 막음
부모 생성자에서 오버라이딩 가능한 메서드 호출 금지 자식 초기화 전 메서드가 실행되어 예측하기 어려운 버그가 생길 수 있음

상속은 코드 중복 제거만 보고 선택하면 위험합니다.

공통 코드가 있다는 이유만으로 부모 클래스를 만들면 부모 변경이 여러 자식에게 전파됩니다. 먼저 개념적으로 같은 종류인지 확인하고, 단순 재사용 목적이면 컴포지션이나 유틸리티 메서드를 검토합니다.

실무에서는?

위치 상속 사용 기준
프레임워크 확장 지점 Spring, Servlet, 테스트 프레임워크처럼 상속 기반 확장 모델이 정해진 경우
템플릿 메서드 패턴 알고리즘 흐름은 부모가 고정하고 일부 단계만 자식이 바꾸는 경우
예외 계층 BusinessException, OrderException처럼 의미 있는 예외 분류가 필요한 경우
도메인 모델 정말 안정적인 is-a 관계일 때만 얕은 계층으로 사용
서비스 로직 재사용 상속보다 별도 컴포넌트를 주입받는 방식이 대체로 안전
JPA Entity 상속 매핑 전략과 쿼리 비용이 생기므로 단순 코드 재사용 목적으로는 피함

정리

항목 설명
목적 공통 개념 표현, 다형성 구현
문법 class Child extends Parent
메서드 해석 실제 객체 타입의 메서드 호출
접근 제어 protected로 자식에 노출
주의 강한 결합, is-a 관계 확인 필요
대안 has-a 관계는 컴포지션 사용

관련 파일: - OOP 개요 — 객체지향 4원칙 흐름 - 다형성 — 상속의 활용 - 추상화 — 인터페이스와 다중 구현 - 캡슐화 — 접근 제어자 이해