9osari.log
← All posts

[JAVA] 상속

April 21, 2025 · 9 min read · #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] 기본형과 참조형 예상질문