Post

[JAVA] 상속 알아보기

상속

김영한의 실전 자바 기본편을 보며 정리해보는 상속

thumbnail

상속이란 주로 부모 자식 간의 관계로 많이 비유된다.

이목구비의 생김새나 어떤 행동 양식이 비슷하게 보이는 것은 우리가 부모님에게 같은 유전자를 물려받았기 때문이다. 이렇게 부모로부터 무언가를 물려받는 것을 상속이라고 한다.

우리는 이렇게 물려받은 유전자를 베이스로 커가면서 겪는 경험과 환경적 요소를 통해 다른 사람들과는 다른 ‘나’를 만들어간다.

이미 가지고 있는 부분에서 더 많은 것을 쌓아 올리는 것이다.

이런 개념으로 자바에서는 상속을 확장이라는 의미인 extends 키워드를 사용하고 있다.

상속은 객체지향에서 중요한 역할을 한다. 이를 이해하기 위해 상속은 왜 사용해야 하는지, 어떻게 사용하는 것인지 더 알아보자.

👶 예시로 알아보는 상속

이번엔 관점을 바꿔보자. 본인이 만약 인간을 여러 명 만들어야 한다면 어떻게 하겠는가?

1
2
3
4
5
6
7
1. 인간이라는 종은 머리, 팔, 몸통, 다리 등으로 이루어져 있다.
2. 새끼를 낳아 번식한다.
3. 눈, 코, 입이 얼굴에 달려 있다.
4. 음식을 먹을 수 있다.
5. 발과 다리를 사용해 움직일 수 있다.
...
100. 이 인간은 눈동자 색이 검다.

이렇게 많은 특징을 가지고 있는 인간 한 명을 정성들여 만들었다.

이제 눈동자 색이 초록색인 인간 하나를 더 만들려 하는데, 문제가 있다.

1
2
3
4
5
6
7
1. 인간이라는 종은 머리, 팔, 몸통, 다리 등으로 이루어져 있다.
2. 새끼를 낳아 번식한다.
3. 눈, 코, 입이 얼굴에 달려 있다.
4. 음식을 먹을 수 있다.
5. 발과 다리를 사용해 움직일 수 있다.
...
100. 이 인간은 눈동자 색이 초록색이다.

눈동자 색을 제외하고 모든 특징이 이전에 만든 인간과 같은데, 다시 처음부터 만들어야 한다는 것이다.

이건 정말 단순노동이 아닐 수 없다.

중복되는 1번부터 99번째 특징은 모두 갖되, 내가 바꾸고 싶은 부분만 마음대로 바꿀 순 없을까?

이를 위해 상속이라는 개념이 만들어졌다.

상속을 활용하기 위해 먼저 중복되는 99개의 특징와 행동을 묶은 Person이라는 클래스를 만든다. 이런 클래스를 부모 클래스 혹은 슈퍼 클래스라고 부른다.

이 클래스를 상속 받은 객체는 Person에 있는 99개의 특징을 모두 가지고 있다. 자식 클래스, 서브 클래스로 불린다.

이제 인간이라면 가져야 할 99개의 특징을 모두 가지고 있지만 각기 다른 생김새를 가지고 있는 인간들을 손쉽게 만들 수 있다.

이처럼 상속은 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다.

참고로 상속은 추상화, 캡슐화, 다형성과 같은 특징으로도 귀결된다.

정리

  • 상속은 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해준다.
  • 중복된 코드를 제거한다.
  • 다형성을 구현하게 해준다.

🔥 상속과 메모리 구조

직접 코드를 짜보기 전에 먼저 상속 관계를 객체로 생성할 때 메모리 구조를 이해해야 한다.

Parent를 상속받는 Child 인스턴스를 만들어보고 구조를 살펴보자.

1
Child child1 = new Child();

alt text

new Child()를 호출하면 Child 뿐만 아니라 상속 관계에 있는 Parent까지 포함해서 인스턴스가 생성된다. 참조값은 x001 하나지만 실제로 그 안에는 Parent, Child 두 가지 클래스 정보가 공존하게 된다.

즉, 상속은 단순히 부모의 필드와 메서드만 물려 받는게 아니라 부모 클래스도 함께 포함되어 생성되는 것이다. 외부에서 볼 때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 분리된다.

child1.run()를 호출하면 먼저 x001에 찾아간다. child의 타입이 Child이므로 Child 안에 run() 메서드가 존재하는지 확인하고 실행한다.

child1.move()와 같이 Child안에 존재하지 않는 메서드를 호출한다면 부모인 Parent에 해당 메서드가 있는지 확인하고 실행한다. Parent에도 존재하지 않다면 계속해서 상위 타입에 존재하는지 찾는다. 어디에도 존재하지 않으면 컴파일 오류가 발생하게 된다.

상속과 메모리 구조 정리

  • 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성된다.
  • 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
  • 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.

💎 상속 관계와 다이아몬드 문제

자바는 extends 대상을 하나만 선택할 수 있도록 제한하고 있다.

여러 개의 클래스를 상속받으면 다이아몬드 문제가 발생하기 때문이다.

예를 들어 다음과 같은 구조로 클래스가 설계되었다고 해보자.

alt text

MotherFatherPerson 클래스를 상속받고 있고, 각각 work() 메서드와 eyesColor 변수를 재정의해서 사용하고 있다.

우리는 이제 Child 인스턴스를 만들어 다음과 같은 코드를 실행해볼 것이다.

1
2
Child child = new Child();
child.work();

현재 Child 인스턴스에는 work() 메서드가 존재하지 않으므로 상위 클래스에서 work() 메서드를 찾아볼 것이다.

그런데 문제가 있다. 상속받은 Mother 클래스와 Father 클래스에 모두 work() 클래스가 있다. 이 중 어떤 클래스의 work()를 사용할 것인지 child는 알 수 없다. 따라서 컴파일 에러가 발생한다. 이와 같은 문제를 다이아몬드 문제라고 한다.

이런 다이아몬드 문제를 막고자 자바에서는 클래스당 하나의 상속만을 허용한다.

자바 코드로 알아보는 상속

이번에는 직접 자바에서 동물을 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Animal {
    public void move() {
        System.out.println("동물이 이동합니다.");
    }

    public void sleep() {
        System.out.println("동물이 잠을 잡니다.");
    }

    public void eat() {
        System.out.println("동물이 밥을 먹습니다.");
    }
}

public class Rabbit extends Animal { }

move(), sleep()이라는 기능을 가지고 있는 Animal 클래스를 만들고 Animal 클래스를 상속하는 Rabbit 클래스를 만들었다.

1
2
3
4
5
6
7
8
public class AnimalExtendsMain {
    public static void main(String[] args) {
        Rabbit rabbit = new Rabbit();
        rabbit.move();
        rabbit.sleep();
        rabbit.eat();
    }
}
1
2
3
동물이 이동합니다.
동물이 잠을 잡니다.
동물이 밥을 먹습니다.

Rabbit 클래스는 아무 기능도 가지고 있지 않지만 부모 클래스의 move()sleep()을 가져다 사용할 수 있다.

이제 Rabbit 클래스를 좀 더 토끼답게 수정해보자.

1
2
3
4
5
6
7
8
9
10
public class Rabbit extends Animal {
    public void jump() {
        System.out.println("토끼가 점프합니다.");
    }

    @Override
    public void eat() {
        System.out.println("토끼가 풀을 먹습니다.");
    }
}

토끼는 다른 동물과 다르게 높게 점프할 수 있고, 주식으로 풀을 먹는다.

eat() 메서드와 같이 부모 타입의 기능을 자식 타입에서 재정의 하는 것을 메서드 오버라이딩(Method Overriding) 이라고 한다.

❕ 참고: @Override상위 클래스의 기능을 재정의 하는 것을 나타내는 애노테이션이다. 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 되었는지 확인한다. 오버라이딩 조건을 만족하지 않으면 컴파일 에러를 발생시킨다. 필수는 아니지만, 코드의 명확성을 위해 붙여주는 것이 권장된다.

1
2
3
4
5
6
7
8
public class AnimalExtendsMain {
    public static void main(String[] args) {
        Rabbit rabbit = new Rabbit();
        rabbit.jump();
        rabbit.eat();
        rabbit.move();
    }
}
1
2
3
토끼가 점프합니다.
토끼가 풀을 먹습니다.
동물이 이동합니다.

이제 rabbit.eat()을 실행하면 부모 클래스의 eat()이 아닌 Rabbit 클래스에서 재정의한 eat()이 실행되는 것을 알 수 있다.

호출한 rabbit의 타입이 Rabbit이기 때문에 인스턴스 내부의 Rabbit 타입을 먼저 살펴보기 때문이다. 이미 실행할 메서드를 찾았기 때문에 부모 타입을 찾아가지 않는다.

물론, 여전히 부모 클래스가 가지고 있는 기능도 이용할 수 있다.

super

super - 부모 참조

부모 클래스에 있는 같은 이름의 필드나 메서드를 사용하고 싶다면 super 키워드를 사용한다.

부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때 super 키워드를 사용하면 부모를 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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); // this 생략 가능
        System.out.println("super value = " + super.value);
        this.hello(); // this 생략 가능
        super.hello();
    }
}

public class SuperMain {
    public static void main(String[] args) {
        Child child = new Child();
        child.call();
    }
}
1
2
3
4
this value = child
super value = parent
Child.hello
Parent.hello

super - 생성자

상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.

상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다.

Child를 만듬녀 부모인 Parent까지 함께 만들어지는 것이다.

따라서 각각의 생성자도 모두 호출되어야 한다.

단, 부모 클래스의 생성자가 기본 생성자인 경우에는 super()를 생략할 수 있다.

참고자료

🔗 김영한의 실전 자바 기본편

This post is licensed under CC BY 4.0 by the author.