9osari.log
← 목록으로

[JAVA] 상속

2025년 4월 21일 · #java#김영한-java-기본

상속이란?

상속을 왜 쓰는지?

전기차와 가솔린차 클래스가 있다고 해보자. 각각 move() 라는 공통 기능이 있음에도 따로 구현되어 있다면, 중복을 줄이고 구조를 더 깔끔하게 만들 방법이 필요함. 바로 이럴 때 상속이 등장한다.

public class ElectricCar {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
    public void charge() {
        System.out.println("충전합니다.");
    }
}

public class GasCar {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
    public void fillUp() {
        System.out.println("기름을 주유합니다.");
    }
}
public class CarMain {
  public static void main(String[] args) {
    ElectricCar electricCar = new ElectricCar();
    electricCar.move();
    electricCar.charge();
    GasCar gasCar = new GasCar();
    gasCar.move();
    gasCar.fillUp();
  }
}

차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.

move() ← 코드 중복이 확인된다.

이런경우 상속을 사용하는 것이 좋다. 부모 클래스 Car 을 만들어 자식 클래스에서 상속 받으면 된다.

상속 적용

//부모 클래스
public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }
}
//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class ElectricCar extends Car {
    public void charge() {
        System.out.println("충전합니다.");
    }
}

//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class GasCar extends Car {
    public void fillUp() {
        System.out.println("기름을 주유합니다.");
    }
}

전기차와 가솔린차가 Car 를 상속 받은 덕분에 electricCar.move() , gasCar.move() 를 사용할 수 있다.

ElectricCar electricCar = new ElectricCar();
electricCar.move();   // 부모의 move() 호출
electricCar.charge();

GasCar gasCar = new GasCar();
gasCar.move();        // 부모의 move() 호출
gasCar.fillUp();

차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.

자바는 다중 상속을 허용하지 않음

다중 상속을 허용하면 move() 같은 메서드가 어떤 부모의 것을 써야 할지 모호해짐 → 다이아몬드 문제

그래서 자바는 하나의 클래스만 상속 가능. 물론 부모가 또 다른 부모를 하나 가지는 것은 괜찮다.

대신 인터페이스는 여러 개 구현 가능해서, 다중 상속의 일부 장점을 인터페이스로 보완한다.

상속과 메모리 구조

ElectricCar electricCar = new ElectricCar();

image.png

![image.png](/assets/img/inheritance/image 1.png)

자식 → 부모 순서대로 메서드 탐색

상속 시 객체 구조 : 부모 + 자식 하나의 인스턴스에 같이 생성

메서드 탐색 순서 : 자식 → 부모 → 조부모..

참조 변수 타입 : 메서드 탐색 기준이 됨 ElectricCar electricCar

호출 불가능한 경우 : 컴파일 에러

상속 관계 객체는 하나의 인스턴스 안에 부모와 자식 클래스 구조를 함께 포함하며,

메서드 호출 시 참조 변수의 타입과 클래스 계층에 따라 기능을 탐색 한다.

상속과 기능 추가

public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }

    // 공통 기능 추가
    public void openDoor() {
        System.out.println("문을 엽니다.");
    }
}

부모 클래스에 기능을 추가하면, 모든 자식 클래스가 자동으로 상속받는다!

새로운 자식 클래스 추가

public class HydrogenCar extends Car {
    public void fillHydrogen() {
        System.out.println("수소를 충전합니다.");
    }
}
ElectricCar electricCar = new ElectricCar();
electricCar.move();         // 부모 기능
electricCar.charge();       // 자식 고유 기능
electricCar.openDoor();     // 부모에 추가된 새 기능

HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();

차를 이동합니다.
충전합니다.
문을 엽니다.
차를 이동합니다.
수소를 충전합니다.
문을 엽니다.

상속을 사용하면 공통 기능을 한 번에 관리할 수 있고, 새로운 클래스를 간편하게 추가할 수 있어 유지보수와 확장(extend)매우 유리하다.

상속과 메서드 오버라이딩(Overriding)

부모에게 상속받은 메서드를 자식 클래스에서 새로운 방식으로 재정의하는 것

@Override
public void move() {
    System.out.println("전기차를 빠르게 이동합니다.");
}

부모클래스

public class Car {
    public void move() {
        System.out.println("차를 이동합니다.");
    }

    public void openDoor() {
        System.out.println("문을 엽니다.");
    }
}

자식클래스

public class ElectricCar extends Car {
    @Override
    public void move() {
        System.out.println("전기차를 빠르게 이동합니다.");
    }

    public void charge() {
        System.out.println("충전합니다.");
    }
}

메인

ElectricCar electricCar = new ElectricCar();
electricCar.move(); // 오버라이딩된 메서드 호출

전기차를 빠르게 이동합니다.

자식이 같은 이름의 메서드를 가지고 있으면 그 메서드가 우선 실행

@Override 애노테이션

@Override
public void move() {
    // ...
}

오버라이딩 조건

부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분하다.

오버라이딩은 자식 클래스가 부모 클래스의 기능을 자기 방식으로 재정의하는 것.

다형성 구현의 핵심이며, @Override 를 반드시 붙이자!

상속과 접근제어자

상속 관계에서 자식 클래스는 부모 클래스의 필드와 메서드에 접근 제어자에 따라 접근할 수 있다.

접근 제어자는 정보 은닉과 캡슐화의 핵심!

접근 제어자의미같은 클래스같은 패키지자식 클래스(다른 패키지)외부
private완전 차단
(default)패키지 전용
protected패키지 + 상속 허용
public전부 허용

부모 클래스

public class Parent {
    public int publicValue;
    protected int protectedValue;
    int defaultValue;
    private int privateValue;

    public void publicMethod() { ... }
    protected void protectedMethod() { ... }
    void defaultMethod() { ... }
    private void privateMethod() { ... }

    public void printParent() {
        // 모든 필드/메서드 접근 가능
    }
}

자식 클래스

public class Child extends Parent {
    public void call() {
        publicValue = 1;       // 가능
        protectedValue = 1;    // 가능 (상속 관계이므로)
        // defaultValue = 1;   // ❌ 다른 패키지 접근 불가
        // privateValue = 1;   // ❌ 접근 불가

        publicMethod();        // 가능
        protectedMethod();     // 가능
        // defaultMethod();    // ❌
        // privateMethod();    // ❌

        printParent();         // public 메서드는 호출 가능
    }
}

Parent.publicMethod
Parent.protectedMethod
==Parent 메서드 안==
publicValue = 1
protectedValue = 1
defaultValue = 0
privateValue = 0
Parent.defaultMethod
Parent.privateMethod
필드/메서드자식 클래스에서 접근이유
public✅ 가능외부 포함 모두 접근 가능
protected✅ 가능상속 관계이므로 가능
default (패키지 전용)❌ 불가다른 패키지라서 불가
private❌ 불가클래스 내부에서만 사용 가능

자식 클래스는 부모 클래스의 public, protected 만 접근 가능하며, 부모 클래스 내부에서는 자신의 모든 필드와 메서드에 접근 할 수 있다.

접근제어자 UML 표기법

접근제어자와 메모리 구조

![image.png](/assets/img/inheritance/image 2.png)

상속 관계에서 객체가 생성되면, 부모 클래스와 자식 클래스가 하나의 인스턴스 안에 함께 존재 하지만 메모리 내부에서는 구분 되어 있다.

자식에서 부모의 기능을 호출하는 건 부모 입장에서는 외부에서 접근하는 것과 같음

즉, 아무리 내부에 있어도

객체 내부에 부모와 자식이 함께 있어도, 접근 제어자 규칙은 여전히 적용된다. 자식이 부모 기능을 호출하는 것은 결국 외부에서 접근하는 것과 같기 때문!

super

부모의 필드/메서드 참조

자식 클래스에서 부모의 필드나 메서드와 이름이 같을 때, 부모 쪽 기능을 호출하고 싶을 때 사용

부모 클래스

public class Parent {
    public String value = "parent";
    public void hello() {
        System.out.println("Parent.hello");
    }
}

자식 클래스

public class Child extends Parent {
    public String value = "child";

    @Override
    public void hello() {
        System.out.println("Child.hello");
    }

    public void call() {
        System.out.println("this value = " + this.value);   // child
        System.out.println("super value = " + super.value); // parent
        this.hello();   // Child.hello
        super.hello();  // Parent.hello
    }
}

메인 클래스

public class Super1Main {
	 public static void main(String[] args) {
			 Child child = new Child();
			 child.call();
	 }
}

-- 실행결과 --
this value = child
super value = parent
Child.hello
Parent.hello

![image.png](/assets/img/inheritance/image 3.png)

super 는 부모 클래스의 필드나 메서드를 참고할 때 사용!

이름이 겹칠 경우, 자식이 우선이므로 super 로 명시적으로 부모 접근 필요!

부모 생성자 호출

자식 객체를 생성하면, 부모 객체도 함께 생성됨 따라서 자식 생성자에서 반드시 부모 생성자도 호출해야 함

생성자 규칙

public class ClassA {
    public ClassA() {
        System.out.println("ClassA 생성자"); //5
    }
}
public class ClassB extends ClassA {
    public ClassB(int a, int b) { //3
        super(); // 부모의 기본 생성자 호출 4
        System.out.println("ClassB 생성자 a=" + a + " b=" + b); //6
    }
public class ClassC extends ClassB {
    public ClassC() {
        super(10, 20); // 부모 생성자 직접 선택 2
        System.out.println("ClassC 생성자"); //7
    }
}
public class Super2Main {
	 public static void main(String[] args) {
			 ClassC classC = new ClassC(); //1
	 }
}

-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자

상속 관계에서 객체가 생성될 때, 생성자는 부모부터 실행되고 자식으로 내려온다. 이 순서를 통해 부모의 데이터가 먼저 초기화된 후 , 자식의 데이터가 초기화된다.

this(…) 와 super(…)의 관계

public class ClassB extends ClassA {
    public ClassB(int a) {
        this(a, 0); // 자기 생성자 호출
        System.out.println("ClassB 생성자 a=" + a);
    }

    public ClassB(int a, int b) {
        super(); // 부모 생성자 호출
        System.out.println("ClassB 생성자 a=" + a + " b=" + b);
    }
}
public class Super2Main {
	 public static void main(String[] args) {
			ClassB classB = new ClassB(100);
	 }
}

-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=100 b=0
ClassB 생성자 a=100

정리

상속은 자바 객체지향의 핵심 기초 개념 중 하나이며, 앞으로 배울 다형성, 추상 클래스, 인터페이스 개념의 기반이 된다.


> 출처: [김영한의 실전 자바 - 기본편](https://www.inflearn.com/course/김영한의-실전-자바-기본편/dashboard)
···
← PREV [JAVA Questions] 클래스와 객체 예상질문 NEXT → [JAVA Questions] 기본형과 참조형 예상질문