2024. 3. 20. 18:13ㆍ백엔드/자바
자바 - 상속(Inheritance)
상속은 지난 시간에 이미 객체 지향 프로그래밍의 네 가지 키워드 중 하나로 학습한 바가 있다. 여기선 본격적으로 상속을 구현하기 위한 자바 문법은 무엇이 있는지 알아보고자 한다.
다시금 자바 속 상속의 개념을 환기하자면 상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미한다.
클래스들 간 공유하는 속성과 기능들을 반복적으로 정의할 필요 없이 딱 한 번만 정의해 두고 간편하게 재사용할 수 있어 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있다.
아래는 상속의 장점을 간단하게 표현한 그림이다.

이처럼 자바에서 상속을 이용하면 상위 클래스의 특징을 하위 클래스에서 이어받아 코드의 중복 제거, 코드 재사용성 증대 효과를 누릴 수 있게 된다.
다만 주의할 점은 자바는 다중 상속을 지원하지 않는다. 다중 상속은 클래스가 동시에 하나 이상의 클래스를 상속받는 것을 뜻한다. C++, 파이썬 등 많은 언어들이 다중 상속을 지원하지만 자바는 상속의 모호성을 줄이기 위하여 다중 상속을 지원하지 않는다.
예를 들어 C라는 자식 클래스가 A, B라는 서로 다른 클래스(A, B 모두 hello() 메서드를 포함)를 동시에 상속(extends A, B)한다면 C는 A의 hello()를 받아야 할지 B의 hello()를 받아야 할지 알 수 없다. 파이썬처럼 다중 속성을 지원하는 언어는 이에 대한 우선순위 규칙을 정해두지만 자바는 그냥 다중 상속을 막아버렸다.
자식 클래스(child class)
자식 클래스(child class) 또는 하위 클래스(sub-class)란 부모 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 의미한다.
자바에서 자식 클래스는 다음과 같은 문법을 통해 선언한다.
class 자식클래스이름 extends 부모클래스이름 { ... }
다음 그림은 부모 클래스와 자식 클래스 간의 멤버의 포함 관계를 나타낸 그림이다.

여기서 유의할 점은 이 그림은 부모 클래스와 자식 클래스 간의 멤버의 포함 관계를 나타낸다. 예를 들어 `a`라는 멤버가 부모 클래스에 존재한다면 자식 클래스는 항상 `a`라는 멤버를 가진다. 또한 자식 클래스는 `a` 뿐만 아니라 `b` 라는 멤버도 가질 수 있는데 이 `b` 는 부모 클래스에는 존재하지 않는다. 그래서 위와 같은 다이어그램으로 표현할 수 있는 것이다.
반면 곧 나올 개념적인 포함 관계는 이와는 반대로 표현된다.
자식 클래스에는 부모 클래스의 필드와 메서드만이 상속되며, 생성자와 초기화 블록은 상속되지 않는다. 또한, 부모 클래스의 접근 제어가 private나 default로 설정된 멤버는 자식 클래스에서 상속받지만 접근할 수는 없다.
class Parent {
private int a = 10; // private 필드
public int b = 20; // public 필드
}
class Child extends Parent {
public int c = 30; //public 필드
void display() {
// System.out.println(a); // 상속받은 private 필드 참조
System.out.println(b); // 상속받은 public 필드 참조
System.out.println(c); //자식 클래스에서 선언한 public 필드 참조
}
}
publilc class Inheritance1 {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
//실행 결과
//20
//30
위 예제에서 `System.out.print(b);` 는 자식 클래스의 메서드에서 부모 클래스에서 상속받은 public 필드를 참조하고 있다. 이처럼 자식 클래스에서 따로 선언하지 않은 필드라도 해당 이름의 필드를 부모 클래스에서 상속받았다면 문제가 없다.
하지만 주석처리된 `System.out.print(a);` 는 해당 필드의 부모 클래스가 private로 되어 있으므로, 오류를 발생시킨다. 또 한, 자식 클래스에서는 `System.out.print(c);`처럼 자신만의 필드나 메서드를 선언하여 사용할 수 있다.
IS-A 관계
IS-A 관계를 설명하기 위한 예시 코드이다.
class Animal {
String name;
void setName(String name) {
this.name = name;
}
}
class Dog extends Animal {
void sleep() {
System.out.println(this.name + " zzz");
}
}
public class Sample {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("poppy");
System.out.println(dog.name);
dog.sleep();
}
}
Dog 클래스가 Animal 클래스를 상속한다. 즉, Dog는 Animal의 하위 개념이라고 할 수 있다. 이런 경우 Dog는 Animal에 포함되기 때문에 '개(Dog)는 동물이다(Animal)'라고 표현할 수 있다. 자바에선 이러한 관계를 IS-A 관계라고 표현한다. 즉, 'Dog is a Animal(개는 동물이다)`과 같이 말할 수 있는 관계를 IS-A관계라고 하는 것이다. 이렇게 IS-A 관계(상속 관계)에 있을 때 자식 클래스의 객체는 부모 클래스의 자료형인 것처럼 사용할 수 있다. 그래서 다음과 같은 코딩이 가능하다.
Animal dog = new Dog(); // Dog is a Animal
여기서 한 가지 주의해야 할 점이 있다. Dog 객체를 Animal 자료형으로 사용할 경우에는 Dog 클래스에만 존재하는 sleep 메서드를 사용할 수 없다는 점이다. 이런 경우에는 Animal 클래스에 구현된 setName 메서드만 사용이 가능하다.
하지만 이 반대의 경우, 즉 부모 클래스로 만들어진 객체를 자식 클래스의 자료형으로는 사용할 수 없다.
//Dog dog = new Animal(); //컴파일 오류
이 부분을 개념적으로 살펴보면
Animal dog = new Dog(); //Dog is a Animal (o)
이 코드를 읽어보면 '개(Dog)로 만든 객체는 동물(Animal) 자료형이다'라고 해석할 수 있다.
하지만
Dog dog = new Animal(); // Animal is a Dog (x)
이를 개념적으로 읽어보면 '동물(Animal)로 만든 객체는 개(Dog) 자료형이다'로 해석할 수 있다. 하지만 이는 잘못된 명제이다. 왜냐하면 Animal로 만든 객체는 Dog 뿐만 아니라 Cat, Lion의 자료형도 가능하다. 따라서 개념적으로도 이 코드는 성립하지 않는다.
따라서 상속의 개념적인 포함관계는 다음처럼 그림처럼 그려진다.

생물의 분류체계와 같이 하위 클래스(자식 클래스)로 내려갈수록 포함하는 개체는 적어지고 기능은 구체화된다.
Object 클래스
잠시 클래스 중 가장 최상위에 있는 클래스인 Object 클래스에 대해 알아보자. 자바에서 만드는 모든 클래스는 Object 클래스를 상속받는다. 사실 우리가 만든 Animal 클래스는 다음 코드와 기능적으로 완전히 동일하다. 하지만 굳이 다음 코드처럼 Object 클래스를 상속하지 않아도 자바에서 만들어지는 모든 클래스는 Object 클래스를 자동으로 상속받게끔 되어 있다.
class Animal extends Object {
String name;
void setName(String name) {
this.name = name;
}
}
따라서 자바에서 만드는 모든 객체는 Object 자료형으로 사용할 수 있다. 따라서 다음과 같은 코딩도 가능하다.
Object animal = new Animal(); // Animal is a Object
Object dog = new Dog(); // Dog is a Object
상속 클래스의 생성자
각 클래스마다 생성자는 반드시 하나 이상 존재한다.
그렇다면 상속된 클래스 간의 생성자 호출은 어느 순서대로 이루어질까?
상속 관계에서 자식 클래스를 인스턴스화하려고 하면 부모 클래스의 객체가 먼저 인스턴스화되고, 자식 클래스 객체가 인스턴스화한다.
class Art {
public Art() {
System.out.println("Art 인스턴스 생성");
}
}
class Drawing extends Art {
public Drawing() {
System.out.println("Drawing 인스턴스 생성");
}
}
class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon 인스턴스 생성");
}
}
public class VoyageLogApplication {
public static void main(String[] args) {
Cartoon cartoon = new Cartoon();
}
}
//실행 결과
//Art 인스턴스 생성
//Drawing 인스턴스 생성
//Cartoon 인스턴스 생성
super 예약어
상속 클래스의 생성자를 다룰 때 항상 등장하는 개념이 `super` 예약어이다. `super` 예약어는 부모 클래스로부터 상속받은 필드나 메서드를 자식 클래스에서 참조하는 데 사용하는 참조 변수이다.
생성자파트에서 다뤘듯이 this 예약어를 이용하면 인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에 `this` 예약어를 사용하여 구분할 수 있었다. 이와 마찬가지로 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 `super` 예약어를 사용하여 구별할 수 있다.
class Parent {
int a = 10; // 부모 인스턴스 변수
}
class Child extends Parent {
int a = 20; // 자식 인스턴스 변수
void display() {
System.out.println(a); //자식 인스턴스 변수 a 출력
System.out.println(this.a); //자식 인스턴스 변수 a 출력
System.out.println(super.a); //부모 인스턴스 변수 a 출력
}
}
public class Inheritance2 {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
//실행 결과
//20
//20
//10
위의 코드처럼 같은 변수명이라도 `this` 예약어를 쓰는지 `super` 예약어를 쓰는지에 따라 호출하는 인스턴스 변수가 달라지게 된다.
super() 메서드
생성자 파트에서 본 `this()` 메서드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, `super()` 메서드는 부모 클래스의 생성자를 호출할 때 사용된다.
자식 클래스의 인스턴스를 생성하면, 해당 인스턴스에는 자식 클래스의 고유 멤버뿐만 아니라 부모 클래스의 모든 멤버도 포함되어 있다. 이는 무언가가 부모 클래스의 생성자까지 호출해 주기 때문이라고 볼 수 있다. `super()` 메서드는 개발자가 직접 적어주지 않아도 자바 컴파일러가 자식 클래스의 생성자 첫 줄에 자동으로 `super()` 명령문을 추가하여, 부모 클래스의 멤버를 초기화할 수 있도록 해준다.
하지만 자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않아야만, 자동으로 기본 생성자를 추가해 준다. 만약 다음 예제처럼 부모 클래스에 매개 변수를 가지는 생성자를 하나라도 선언했다면, 부모 클래스에는 기본 생성자가 자동으로 추가되지 않을 것이다.
class Parent {
int a;
Parent(int n) { a = n; }
}
그런데 만약 Parent 클래스를 상속받은 자식 클래스에서 super() 메서드를 사용하여 부모 클래스의 기본 생성자를 호출하게 되면, 오류가 발생한다.
class Parent {
int a;
Parent(int n) { a = n; }
}
class Chile extends Parent {
int b;
Child() {
//super(); //오류 발생
b = 20;
}
}
왜냐하면 부모 클래스인 Parent 클래스에는 기본 생성자가 추가되어 있지 않기 때문이다. 따라서 매개변수를 가진 생성자를 선언해야 할 경우에는 되도록 다음과 같이 기본 생성자까지 명시적으로 선언하는 것이 좋다.
class Parent {
int a;
Parent() { a = 10; }
Parent(int n) { a = n; }
}
class Chile extends Parent {
int b;
Child() {
super(); //Parent() 호출
b = 20;
}
}
다른 방법으로는 자식 클래스 생성자에서 부모 클래스 생성자의 메서드 시그니처에 맞는 `super(n)`으로 부모 클래스의 생성자를 직접 호출할 수 있다.
class Parent {
int a;
Parent(int n) { a = n; } //생성자 직접 지정 -> 디폴트 생성자 x
}
class Chile extends Parent {
int b;
Child() {
super(n); // 자식 생성자의 입력값 n을 받아 부모 생성자 Parent(int n) 를 호출
b = 20;
}
}
정리하면 부모 클래스와 자식 클래스가 서로 상속 관계를 맺고 `new` 키워드를 통해 자식 클래스를 초기화할 때, 반드시 자식 클래스 생성자 내에서 부모 클래스 생성자를 호출하는 메서드 `super()` 가 첫 번째 행에서 실행된다.
만일 직접 생성자를 정의하거나 생성자 오버로딩이 되어 있을 경우 메서드 시그니처에 맞는 생성자를 `super(name)` 메서드로 직접 정의해서 호출해야 한다. (생략된 `super()` 는 디폴트 생성자만 호출하기 때문이다)
`this()`는 잘 사용되지 않는 편이지만 `super()`는 빈번하게 사용된다.
메서드 오버라이딩(overriding)
이전에 메서드를 다루며 메서드 오버로딩(method overloading)을 공부했다. 이번에 배울 메서드 오버라이딩(method overriding)은 처음 접할 시 오버로딩과 개념이 많이 혼동될 수 있어 차이점을 분명히 아는 것이 중요하다.
메서드 오버로딩과 오버라이딩 모두 객체지향 프로그래밍의 다형성을 구현하기 위한 수단이다.
우선 오버라이딩에 대해 좀 더 자세히 알아보자.
메서드 오버라이딩이란 상속받은 부모 클래스의 메서드를 재정의하여 사용하는 것을 의미한다.
다음 코드를 통해 오버라이딩을 이해하자.
class Parent {
void display() {
System.out.println("부모클래스 display 메서드");
}
}
class Child extends Parent {
void display() {
System.out.println("자식클래스 display 메서드");
}
}
public class Inheritance3 {
public static void main(String[] args) {
Parent parent = new Parent();
parent.display();
Child child = new Child();
child.display();
Child parent_child = new Child();
parent_child.display();
}
}
//실행결과
//부모클래스 display 메서드
//자식클래스 display 메서드
//자식클래스 display 메서드
위 코드에선 부모 클래스에서 이미 정의해 놓은 `display()` 메서드를 자식 클래스에서 재정의해서 쓰고 있다.
자바에서 오버라이딩을 쓸 땐 몇 가지 주의할 점이 있다.
- 1. 오버라이딩이란 메서드의 동작만을 재정의하는 것이므로, 메서드의 선언부는 기존 메서드와 완전히 같아야 한다.
- 2. 부모 클래스의 메서드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다.
- 3. 부모 클래스의 메서드보다 더 큰 범위의 예외를 선언할 수 없다.
위와 같은 조건을 지키지 않으면 오버라이딩을 할 수 없다.
이제 종종 오버라이딩과 혼동되는 오버로딩과의 차이점을 알아보자.
- 오버로딩(Overloading): 서로 다른 시그니처를 갖는 여러 메서드를 하나의 이름으로 정의
- 오버라이딩(Overriding): 상속 관계에 있는 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 같은 시그니쳐를 갖는 메서드로 다시 정의
| 구분 | 오버로딩 | 오버라이딩 |
| 메서드 이름 | 동일 | 동일 |
| 매개변수(타입 혹은 개수) | 다름 | 동일 |
| 리턴 타입 | 상관없음 | 동일 |
오버로딩은 같은 이름을 가진 함수에 다양한 파라미터를 쌓는 느낌(Loading)이라면, 오버라이딩은 기존에 있던 함수에 올라타는 (Riding) 느낌이라고 생각하면 이해가 쉽다.

오버로딩과 오버라이딩을 화살에 빗대 표현하기도 하지만 개인적으론 문자의 의미대로 해석하는 것이 좀 더 와닿는다.
아래의 코드를 보며 오버로딩 와 오버라이딩을 확실히 이해하자.
class Parent {
void display() {
System.out.println("부모 클래스의 display() 메소드입니다.");
}
}
class Child extends Parent {
int count;
// 오버라이딩(overriding)된 display 메서드
void display() {
count++;
System.out.println("자식 클래스의 display() 메소드입니다.");
}
// 오버로딩(overloading)된 display() 메소드
void display(String str) {
System.out.println(str);
}
void display(int c) {
this.count += c;
System.out.println(count);
}
void display(boolean b) {
if(b == true) {
super.display();
}
}
}
public class Main {
public static void main(String[] args) {
Child ch = new Child();
// 오버라이딩(overriding) 된 자식 메서드 출력
ch.display();
// 오버로딩(overloading) 된 자식 메서드 출력
ch.display("Hello World");
ch.display(900);
ch.display(true);
}
}
//실행결과
//자식 클래스의 display() 메소드입니다.
//Hello World
//901
//부모 클래스의 display() 메소드입니다. // 오버라이딩 되었던 부모 메서드 출력
다시 한번 강조하길 오버라이딩을 할 땐 접근제어자 지정에 대해 조심해야 한다. 부모 메서드에 정의된 접근제어자의 범위보다 낮은 범위의 제어자는 지정할 수 없다. 예를 들어 부모 메서드에 `protected void display()`라고 정의되어 있다면 같은 protected 혹은 더 개방적인 public으로 설정해야만 오버라이딩이 된다. 만약 private나 default 제어자로 오버라이딩하려고 하면 컴파일 에러가 생긴다.
접근 제어자
객체 지향 프로그래밍에선 캡슐화를 쓴다고 배운 적이 있다. 캡슐화는 정보 은닉의 목적을 달성하기 위하여 쓰는데, 정보 은닉이란 내부의 동작을 감추고 외부에는 필요한 부분만 노출시키는 것이다. 우리가 자판기를 이용할 때도 "동전을 넣고 버튼을 누르면 음식이 나온다" 정도만 알면 되지 굳이 자판기의 동작 원리를 알 필요는 없다.
객체 지향 프로그래밍에서도 마찬가지다. 협업자 혹은 사용자가 어떻게 동작하는 지를 굳이 알 필요는 없다. 또한 만약 외부 사용자가 내부 동작 원리를 알게 되면 실수 혹은 악의적인 행위에 의해 코드가 망가지는 경우가 생길 수도 있다.(정보 보호 차원) 이런 불상사를 예방하기 위해 자바에선 접근 제어자를 사용해 외부 사용자의 개입을 막는다.

자바에서 접근 제어자(access modifier)는 네 가지가 존재한다. 접근 제어자는 클래스, 필드, 메서드 선언부 앞에 붙어 대상의 접근 범위를 지정해 준다.
- 1. private: 같은 클래스 내에서만 접근 가능
- 2. (default): 같은 패키지 내에서만 접근 가능
- 3. protected: 같은 패키지 내에서, 다른 패키지여도 자손 클래스라면 접근 가능
- 4. public: 접근 제한이 없음

public 제어자
- 프로그램 어디에서나 접근이 가능하다.

public class Everywhere {
public String var = "누구든지 허용"; // public 필드
public String getVar() { //public 메서드
return this.var;
}
}
default 제어자
- 따로 접근 제어자를 명시하지 않을 경우 기본값이다.
- 같은 패키지에 속한다면 어디서든지 접근 가능하다.
- But, 다른 패키지에 있으면 접근이 불가능하다.

package test;
public class SamePackage {
String sameVar = "같은 패키지는 허용"; //default 필드
}
package test;
public class SameClass {
String var = "다른 패키지는 접근 불가"; // default 필드
public static void main(String[] args) {
SamePackage samepackage = new SamePackage();
System.out.println(samepackage.sameVar); // 같은 패키지는 허용
}
}
protected 제어자
- default와 public의 중간에 위치한 제어자이다.
- 기본적으로 다른 패키지에서는 default와 같이 접근이 불가능하지만, 상속된 클래스(자식 클래스)는 접근이 가능하다.

package house; // 패키지가 동일하지 않다.
public class HouseKim {
protected String lastname = "kim"; // lastname은 누군가 HouseKim을 상속하면 접근이 가능하다
}
package truck; // 패키지가 동일하지 않다.
import house.HouseKim; // 다른 패키제 있는 클래스를 사용하기 위해 불러옴
public class TruckPark extends HouseKim {
public static void main(String[] args) {
TruckPark park = new TruckPark();
System.out.println(park.lastname); // kim(HouseKim의 자식클래스는 접근 가능)
}
}
private 제어자
- private 가 선언된 클래스 멤버는 외부에 공개되지 않으며, 외부에서는 직접 접근할 수 없다.
- 따라서 private 멤버는 클래스 내부의 세부적인 동작을 구현하는 데 사용된다.
- 대부분 자바 프로그래밍에서 권장되는 제어 기법은 클래스 멤버 변수들은 private를 지정해서 접근을 제한하고, 메서드를 public으로 지정해, 오로지 메서드를 통해서만 해당 객체의 멤버를 수정할 수 있도록 한다.
- 대표적으로 Getter/Setter 를 구현하는데 자주 사용된다.

public class Car {
private int speed; // 오로지 Car 클래스 내부에서만 접근 가능
private boolean stop;
/* 따라서 메서드를 통해 클래스의 private 멤버의 값을 변경하도록 설정 */
//Getter 메서드
public int getSpeed() {
return this.speed;
}
//Setter 메서드
public void setSpeed(int speed) {
//만약 옳지 않은 입력값을 넣을 경우 메서드 내에서 한번 거를 수 있음
if(speed < 0) {
this.speed = 0;
} else {
this.speed = speed;
}
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.setSpeed(-50); //잘못된 속도 변경
System.out.println("현재 속도 : " car.getSpeed()); //속도는 음수가 될 수 없음
car.setSpeed(60); // 올바른 속도 변경
System.out.println("현재 속도 : " car.getSpeed()); //60 출력
}
}
지금까지 객체 지향 프로그래밍의 상속 기능과 관련된 자바 문법을 공부했다. 다만 객체 지향 프로그래밍의 네 가지 키워드는 서로 엮여있는 부분이 있어서 오직 상속만 공부했다기보단 상속을 중점적으로 공부하는 과정에서 추상화, 다형성(오버로딩, 오버라이딩), 캡슐화(접근제어자)가 등장했다. 다음 시간에는 다형성을 중점적으로 알아볼 것이다. 대체로 인터페이스와 추상 클래스에 대한 내용을 다룰 예정이다.
참고
☕ 자바 객체 지향(OOP) 클래스 문법 - 한방 가이드
클래스(class) 구성 자바에서 클래스(class)란 객체를 정의하는 틀 또는 설계도와 같은 의미로 사용된다. 자바에서는 이러한 설계도를 가지고, 여러 객체를 생성하여 사용하는 식으로 프로그래밍을
inpa.tistory.com
코딩교육 티씨피스쿨
4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등
tcpschool.com
05-05 상속
[TOC] ## 상속 자바에는 자식 클래스가 부모 클래스의 기능을 그대로 물려받을 수 있는 상속(inheritance) 기능이 있다. 예제를 통해서 자바의 상속에 대해서 자세…
wikidocs.net
'백엔드 > 자바' 카테고리의 다른 글
| 자바 - 인터페이스(Interface) (0) | 2024.03.27 |
|---|---|
| 자바 - 추상 클래스(abstract class) (0) | 2024.03.26 |
| 자바 - 클래스(인스턴스, 필드, 메서드, 생성자) (0) | 2024.03.19 |
| 자바 - 객체 지향 프로그래밍의 4가지 특징(추상화, 상속, 다형성, 캡슐화) (0) | 2024.03.15 |
| 자바 - 래퍼 클래스(Wrapper Class) (0) | 2024.03.14 |