자바/자바

(자바) 상속 정리 *인프런_널널한 개발자

불광동 물주먹 2025. 9. 8. 22:33

 

자바 상속에서 꼭 짚고 넘어가야 할 핵심

1. 파생 클래스 생성 시 부모 클래스도 함께 생성된다

  • 자식 클래스를 new 하면, 부모 클래스의 생성자가 먼저 호출되고, 그 다음 자식 클래스 생성자가 실행됩니다.
  • 이 과정은 콜 스택(call stack)에 생성자 호출이 쌓였다가 차례대로 실행되는 것과 비슷합니다.

📌 예시

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

class Child extends Parent {
    Child() {
        System.out.println("Child 생성자");
    }
}

public class Main {
    public static void main(String[] args) {
        new Child();
    }
}

 

출력:

Parent 생성자
Child 생성자

 

 


2. 상속은 코드 흐름을 2차원적으로 만든다

  • 단순히 “현재 클래스 안의 코드 흐름”만 존재하는 게 아니라,
  • 부모 → 자식 관계로 흐름이 확장되며, 현재 작성자와 미래 작성자가 “대화”할 수 있게 됩니다.
  • 이때 중요한 것은 메서드 오버라이딩을 통해, 부모가 정의한 틀을 자식이 적절히 확장할 수 있다는 점입니다.

➡️ 즉, 상속은 단순히 재사용을 위한 게 아니라, “현재와 미래가 소통하는 구조”라는 점을 이해해야 합니다.


3. 다중 상속은 불가능, 인터페이스는 다중 구현 가능

  • 자바 클래스는 단일 상속만 지원합니다. (부모는 하나만 가질 수 있음)
  • 하지만 인터페이스는 여러 개 구현 가능 → “행위 계약”을 다중으로 가져갈 수 있음.

📌 예시

 
interface Flyable { void fly(); }
interface Swimmable { void swim(); }

class Duck implements Flyable, Swimmable {
    public void fly() { System.out.println("날다"); }
    public void swim() { System.out.println("헤엄치다"); }
}

4. 파생 클래스에서 부모 클래스의 필드를 다시 정의하지 말 것

  • 자식 클래스에서 부모 클래스의 필드와 동일한 이름으로 변수를 다시 정의하는 것은 혼란만 불러일으키는 행위입니다.
  • 유지보수성도 떨어지고, 코드 읽는 사람에게 혼동을 줍니다.

➡️ 널널한 개발자님은 이걸 “바보짓”이라고까지 표현합니다.
즉, 상속은 확장을 위한 것이지, 부모의 속성을 덮어씌우는 장치가 아님을 명심해야 합니다.


5. “현재 vs 미래”의 관점에서 상속을 바라보기

  • 부모 클래스는 현재 시점에서 작성하는 코드
  • 자식 클래스는 미래 시점에 확정되는 코드

➡️ 따라서 부모 클래스를 작성할 때는, 미래의 확장을 고려해야 합니다.
예를 들어, 내가 6개월 뒤 다시 코드를 봤을 때, 혹은 다른 개발자가 이 클래스를 이어받았을 때도 의도를 쉽게 파악할 수 있어야 합니다.


6. 상속의 깊이는 얕게 가져가라

  • 상속이 상속을 타고 이어지는 구조(증조클래스, 고조클래스…)는 바람직하지 않습니다.
  • 그렇게 되면 “위로 몇 단계를 타고 올라가야 이 클래스의 동작을 알 수 있는지” 복잡해지고, 유지보수가 어려워집니다.

➡️ 실무에서는 상속의 깊이를 얕게 가져가고, 합성(Composition) 을 활용하는 것이 더 좋은 경우가 많습니다.

 

 * 상속으로만 해결하려는 경우 (안 좋은 예)

 
class Engine {
    void start() { System.out.println("엔진 가동"); }
}

class Car extends Engine { // Car가 Engine을 상속
    void drive() { System.out.println("운전 시작"); }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.start(); // Engine 기능 직접 사용
        car.drive();
    }
}

문제점:

  • Car 가 Engine의 하위 타입처럼 보이지만 사실 논리적으로 “Car는 Engine이다”라는 관계는 이상합니다.
  • 나중에 Car 말고 Truck, Bus 등 여러 차종이 생기면 상속 구조가 불필요하게 깊어지고 꼬입니다.

2. 합성을 활용하는 경우 (더 나은 설계)

 
class Engine {
    void start() { System.out.println("엔진 가동"); }
}

class Car {
    private Engine engine = new Engine(); // 합성: Car 안에 Engine 포함

    void drive() {
        engine.start(); // Engine 기능 사용
        System.out.println("운전 시작");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.drive();
    }
}

 

장점:

  • Car는 Engine을 “가지고 있다(has-a)” 관계 → 현실 모델링에도 적합
  • 나중에 Car에 다른 종류의 Engine(예: 전기 엔진, 하이브리드 엔진)을 넣을 수도 있음
  • 상속 깊이가 늘어나지 않고, 조합만 바꿔서 확장 가능

3. 실무에서 합성이 선호되는 이유

  • 상속: “is-a” 관계 → 강한 결합, 부모에 의존
  • 합성: “has-a” 관계 → 약한 결합, 유연한 확장

➡️ 합성은 부품 교체처럼 객체 조합으로 기능을 확장할 수 있어서, 서비스 코드가 커질수록 훨씬 관리하기 쉽습니다.

 


 핵심 요약

  1. 자식 생성자는 부모 생성자를 거쳐 실행된다 (콜 스택 이해 필요).
  2. 상속은 코드 흐름을 2차원적으로 만들어 “현재와 미래의 대화”가 가능해진다.
  3. 클래스는 단일 상속만 가능, 인터페이스는 다중 구현 가능하다.
  4. 부모 필드를 자식에서 다시 정의하는 것은 절대 피해야 한다.
  5. 부모 클래스는 현재, 자식 클래스는 미래 → 미래 확장을 고려한 코드 작성이 필수.
  6. 상속의 깊이는 얕게, 불필요한 다단계 상속은 지양하라.