김영한의 실전 자바 - 기본편 강의 내용을 정리한 글이다.
Java 기본 — 상속
1. 상속이 필요한 이유
전기차(ElectricCar)와 가솔린차(GasCar)를 각각 별도의 클래스로 만들면 어떤 문제가 생기는지 먼저 살펴본다.
두 클래스를 보면 move() 메서드가 완전히 동일하다. 전기차든 가솔린차든 이동하는 방식은 같고, 에너지를 충전하는 방식만 다르다. 이런 구조에서 move()의 구현을 바꿔야 한다면 두 클래스를 모두 수정해야 한다. 차량 종류가 10가지라면 10곳을 모두 고쳐야 한다. 이것이 코드 중복의 문제다.
move()가 두 클래스에 중복 존재한다
2. 상속 관계
상속은 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다. 자바에서 상속을 사용하려면 extends 키워드를 사용한다. extends 대상은 하나만 선택할 수 있다. 자바는 단일 상속만 지원한다.
| 용어 | 다른 이름 | 설명 |
|---|---|---|
| 부모 클래스 | 슈퍼 클래스(super class) | 자신의 필드와 메서드를 자식에게 제공하는 클래스 |
| 자식 클래스 | 서브 클래스(sub class) | 부모 클래스로부터 필드와 메서드를 상속받는 클래스 |
앞의 예제에 상속을 적용하면 공통 기능인 move()를 부모 클래스 Car에 한 번만 작성하고, 자식 클래스들은 자신만의 고유 기능만 가지면 된다.
ElectricCar는 extends Car를 통해 Car를 상속받는다. 상속 덕분에 ElectricCar에서도 move()를 사용할 수 있다. 코드에 move()가 보이지 않지만 부모로부터 물려받았기 때문에 호출할 수 있다.
화살표 방향: 자식이 부모를 가리킨다 (UML 표기법)
중요한 점은 상속은 부모의 기능을 자식이 물려받는 것이므로 자식은 부모의 기능을 사용할 수 있지만, 반대로 부모 클래스는 자식 클래스에 접근할 수 없다. 부모 코드에는 자식에 대한 정보가 전혀 없다. 반면 자식 코드는 extends Car를 통해 부모를 알고 있다.
단일 상속
자바는 다중 상속을 지원하지 않는다. extends 대상은 하나만 선택할 수 있다. 다중 상속을 허용하면 이른바 다이아몬드 문제(Diamond Problem)가 발생하기 때문이다.
다이아몬드 문제: 두 부모 모두 move()를 가지면 어느 쪽을 실행해야 하는지 모호해진다
AirplaneCar가 Airplane과 Car를 동시에 상속받는다면, move()를 호출할 때 어느 부모의 move()를 실행해야 할지 애매해진다. 자바는 이 문제를 원천 차단하기 위해 클래스의 다중 상속을 허용하지 않는다. 대신 인터페이스의 다중 구현으로 이 문제를 우회할 수 있다.
3. 상속과 메모리 구조
상속에서 가장 중요한 개념 중 하나다. 상속 관계를 객체로 생성할 때 메모리 내부에서 무슨 일이 일어나는지 이해해야 한다.
new ElectricCar()를 호출하면 ElectricCar뿐만 아니라 상속 관계에 있는 Car까지 함께 포함해서 인스턴스를 생성한다. 참조값은 x001로 하나이지만, 실제 그 안에는 Car와 ElectricCar라는 두 가지 클래스 정보가 공존한다.
하나의 참조값 안에 부모와 자식 클래스 영역이 모두 존재한다
상속이라고 해서 단순하게 부모의 필드와 메서드만 물려받는 것이 아니다. 상속 관계를 사용하면 부모 클래스도 함께 포함해서 생성된다. 외부에서 보면 하나의 인스턴스를 생성하는 것 같지만, 내부에서는 부모와 자식이 모두 생성되고 공간도 구분된다.
메서드 탐색 순서
상속 관계에서 메서드를 호출할 때는 다음 규칙으로 탐색한다.
- 호출하는 변수의 타입(클래스)을 기준으로 해당 타입의 영역에서 먼저 메서드를 찾는다.
- 해당 타입에 메서드가 없으면 상위 부모 타입으로 올라가서 찾는다.
- 부모에서도 찾지 못하면 더 위의 부모로 올라가며 계속 탐색한다.
- 끝까지 찾아도 없으면 컴파일 오류가 발생한다.
상속 관계의 객체를 생성하면 내부에는 부모와 자식이 모두 생성된다. 메서드를 호출할 때는 호출자의 타입에서 먼저 탐색하고, 없으면 부모 타입으로 올라간다.
4. 상속과 기능 추가
상속의 가장 큰 장점은 기능을 추가하거나 새로운 자식 클래스를 만들 때 드러난다. 다음 두 가지 요구사항이 생겼다고 가정한다.
- 모든 차량에 문열기(
openDoor()) 기능을 추가해야 한다. - 새로운 수소차(
HydrogenCar)를 추가해야 한다. 수소차는fillHydrogen()으로 수소를 충전할 수 있다.
상속이 없었다면 ElectricCar, GasCar 각각에 openDoor()를 추가해야 한다. 하지만 상속 구조에서는 부모인 Car에만 한 번 추가하면 모든 자식 클래스가 자동으로 해당 기능을 갖게 된다.
Car에 openDoor()를 한 번 추가하면 모든 자식이 자동으로 갖게 된다. HydrogenCar도 손쉽게 확장 가능하다.
상속 덕분에 중복이 줄어들고, 새로운 차종을 추가할 때도 공통 기능을 일일이 복사할 필요가 없다. 이것이 상속의 핵심 가치다.
5. 메서드 오버라이딩
부모 타입의 기능을 자식에서 다르게 재정의하고 싶을 수 있다. 예를 들어, Car.move()는 "차를 이동합니다."를 출력한다. 전기차는 보통 더 빠르기 때문에 move()를 호출할 때 "전기차를 빠르게 이동합니다."로 출력을 바꾸고 싶다.
이렇게 부모에게서 상속받은 기능을 자식이 재정의하는 것을 메서드 오버라이딩(Method Overriding)이라 한다. 자바에서는 '재정의'라고도 부른다.
@Override는 애노테이션이다. 프로그램이 읽을 수 있는 특별한 주석 같은 것으로, 컴파일러에게 "이 메서드는 부모의 메서드를 오버라이딩한 것이다"라고 알려준다. 컴파일러는 이 애노테이션을 보고 오버라이딩 조건을 만족하는지 검사한다. 조건을 만족하지 않으면 컴파일 오류를 발생시켜 실수를 방지해준다. 필수는 아니지만 코드의 명확성을 위해 붙여주는 것이 좋다.
이제 electricCar.move()를 호출하면 Car의 move()가 아니라 ElectricCar의 move()가 실행된다. 메서드 탐색 순서에 따라 자신의 타입인 ElectricCar에서 먼저 찾기 때문이다.
오버로딩(Overloading)과 오버라이딩(Overriding) 비교
| 구분 | 오버로딩 (Overloading) | 오버라이딩 (Overriding) |
|---|---|---|
| 의미 | 같은 이름의 메서드를 매개변수를 달리해 여러 개 정의 | 부모의 메서드를 자식이 재정의 |
| 상속 필요 여부 | 불필요 (같은 클래스 내에서도 가능) | 필수 (상속 관계에서만 사용) |
| 메서드 시그니처 | 이름은 같고 매개변수 타입·수·순서가 달라야 함 | 이름, 매개변수, 반환 타입 모두 동일해야 함 |
| 번역어 | 과적 (과하게 물건을 담음) | 재정의 |
메서드 오버라이딩 조건
- 메서드 이름: 부모 메서드와 같아야 한다.
- 매개변수: 타입, 순서, 개수가 같아야 한다.
- 반환 타입: 같아야 한다. 단, 반환 타입이 하위 클래스 타입일 수 있다(공변 반환 타입).
- 접근 제어자: 상위 클래스의 메서드보다 더 제한적이어서는 안 된다.
protected로 선언된 메서드는public또는protected로 오버라이드할 수 있지만,private로는 불가능하다. static,final,private메서드: 오버라이딩 불가.static은 클래스 레벨에서 동작하고,final은 재정의를 금지하며,private는 하위 클래스에서 보이지 않는다.- 생성자: 오버라이딩 불가.
6. 상속과 접근 제어
상속 관계에서 접근 제어자가 어떻게 동작하는지 이해해야 한다. 접근 제어자를 복습하면 다음과 같다.
| 접근 제어자 | 같은 클래스 | 같은 패키지 | 상속 관계(다른 패키지) | 전체 외부 |
|---|---|---|---|---|
private |
O | X | X | X |
default (package-private) |
O | O | X | X |
protected |
O | O | O | X |
public |
O | O | O | O |
상속에서 핵심은 protected다. 다른 패키지에 있는 클래스라도 상속 관계라면 protected 멤버에 접근할 수 있다. 이를 활용해 외부에는 감추면서 자식 클래스에게만 노출하는 패턴을 만들 수 있다.
자식 클래스인 Child는 부모의 public과 protected 멤버에만 접근할 수 있다. default는 패키지가 다르면 상속 관계여도 접근이 불가하다. private은 자식이라도 절대 접근할 수 없다.
단, Parent.printParent()처럼 부모 클래스 안에 있는 메서드는 Parent 자신의 모든 필드와 메서드에 얼마든지 접근할 수 있다. 접근 제어자는 외부에서의 접근을 통제하는 것이지, 클래스 내부에서의 접근을 막는 것이 아니다.
자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같다. 따라서 접근 제어자의 영향을 받는다.
7. super - 부모 참조
부모와 자식의 필드명이 같거나 메서드가 오버라이딩되어 있으면, 자식에서 부모의 필드나 메서드를 직접 호출할 수 없다. 이때 super 키워드를 사용하면 부모를 참조할 수 있다. super는 이름 그대로 부모 클래스에 대한 참조다.
this는 본인 타입(Child)을, super는 본인 타입의 상위(Parent)를 참조한다
this는 자기 자신의 참조다. 생략할 수 있다.super는 부모 클래스에 대한 참조다. 필드명이나 메서드명이 겹칠 때 부모 것을 명시적으로 호출하기 위해 사용한다.
8. super - 생성자
상속 관계의 인스턴스를 생성하면 메모리 내부에는 자식과 부모 클래스가 각각 모두 만들어진다. Child를 만들면 부모인 Parent까지 함께 만들어진다. 따라서 각각의 생성자도 모두 호출되어야 한다.
상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.(규칙)
부모의 생성자를 호출할 때는super(...)를 사용한다.
생성자 호출 순서가 결과적으로 최상위 부모부터 이루어지는 것에 주목한다. 자식 생성자의 첫 줄에서 부모 생성자를 호출해야 하기 때문에, 초기화는 최상위 부모부터 하나씩 내려오는 순서로 진행된다.
호출 순서: ClassC → ClassB → ClassA / 실행 순서: ClassA → ClassB → ClassC
super() 생략 규칙
- 부모 클래스의 생성자가 기본 생성자(매개변수 없음)인 경우
super()를 생략할 수 있다. - 생략하면 자바가 자동으로
super()를 첫 줄에 삽입한다. - 부모에 기본 생성자가 없다면
super(...)를 명시적으로 작성해야 한다. this(...)를 사용하더라도 결국 어딘가에서 반드시 한 번은super(...)를 호출해야 한다.
9. final 키워드와 상속
상속과 관련하여 final 키워드가 어떤 의미를 가지는지 정리한다.
| 대상 | final의 의미 | 예시 |
|---|---|---|
| 클래스 | 상속 불가. 해당 클래스를 다른 클래스가 상속받을 수 없다. | public final class MyClass {...} |
| 메서드 | 오버라이딩 불가. 자식 클래스에서 이 메서드를 재정의할 수 없다. | public final void myMethod() {...} |
자바 표준 라이브러리에서 String 클래스가 대표적인 final 클래스다. 문자열의 핵심 동작을 외부에서 변경하지 못하도록 보호하기 위해 상속을 차단한다.
10. 상속 활용 예시 — 쇼핑몰 상품
상속을 실제로 활용하는 예시로 쇼핑몰 상품 구조를 살펴본다. Book, Album, Movie 세 가지 상품이 있고, 공통 속성(name, price)을 부모 클래스 Item으로 묶는다.
Book.print()는 super.print()로 부모의 print()를 먼저 호출한 뒤, 자신만의 추가 정보를 출력한다. 이처럼 오버라이딩과 super를 조합하면 부모의 기능을 재사용하면서 자식만의 기능을 덧붙일 수 있다.
정리
- 상속(
extends)은 부모 클래스의 필드와 메서드를 자식 클래스가 재사용할 수 있게 한다. 코드 중복을 줄이고 확장성을 높인다. - 단일 상속: 자바는 다이아몬드 문제를 피하기 위해 클래스의 다중 상속을 허용하지 않는다.
- 메모리 구조: 상속 관계의 객체를 생성하면 내부에 부모와 자식 클래스가 모두 생성된다. 메서드 탐색은 호출자의 타입에서 시작해 부모로 올라간다.
- 오버라이딩: 부모의 메서드를 자식이 재정의한다.
@Override애노테이션으로 컴파일러 검증을 받는 것이 좋다. super: 부모를 참조하는 키워드. 필드·메서드 접근(super.xxx)과 생성자 호출(super(...)) 두 가지 용도로 사용한다.- 생성자 규칙: 자식 생성자의 첫 줄에서 반드시 부모 생성자를 호출해야 한다. 기본 생성자의 경우 자바가 자동으로 삽입한다.
final: 클래스에 붙이면 상속 불가, 메서드에 붙이면 오버라이딩 불가.
'Study > Java' 카테고리의 다른 글
| [Java] 다형성 Part.1 (0) | 2026.04.09 |
|---|---|
| [Java] final (0) | 2026.04.07 |
| [Java] 자바의 메모리 구조 (0) | 2026.04.06 |
| [Java] 접근 제한자 (0) | 2026.04.06 |
| [Java] 패키지 (0) | 2026.04.06 |
