Java

6주차 - 상속

voider 2020. 12. 25. 00:42

상속(Inheritance)

상속. 상속에 대해 생각해보자. 먼 미래에 내가 누군가에게 무언가를 상속하게 된다고. 난 무엇을 상속해줄 수 있을까? 돈? 집? 거기까진 모르겠다. 아마 운이 좋다면 키보드 정도를 남기고 떠날 수도 있을 것 같다.

아무튼.

자바에서는 extends키워드를 이용하여 상속을 사용한다. extends라는 단어에서 알 수 있듯이 자바에서 상속은 곧 확장이다. 슈퍼 클래스가 가진 것들을 이용하여 더 큰 클래스를 만들 목적으로 상속을 사용한다.

상속은 클래스와 클래스 간에 이루어진다. 확장(extends)되어질 클래스를 슈퍼클래스, 상위클래스(또는 부모 클래스)라고 하고 확장하는 클래스를 서브클래스, 하위클래스(또는 자식 클래스)라고 한다. 현실세계는 이 사람 저 사람에게 상속 받을 수 있는 반면, 자바는 오직 하나의 클래스만 상속받을 수 있다.(단일상속) 두 번째 다른 점은 내가 원하는 것만 골라서 상속할 수 있고 어떤 것은 반드시 직접 재정의하도록 만들 수도 있다. 이를 테면 빵구난 양말을 그대로 상속하지 않고 반드시 꿰메서 신도록 강제할 수 있다는 뜻이다.(추상 클래스 상속) 세 번째는 슈퍼 클래스의 접근제어를 좁힐 수 없다. 슈퍼클래스의 public메소드를 상속받는 서브클래스에서 protected나 private으로 변경할 수 없다.

정리

  • 슈퍼클래스의 기능을 확장(extends)하여 서브클래스를 만든다.
  • 서브클래스는 오직 하나의 슈퍼클래스만 가질 수 있다. (단일 상속)
  • 서브클래스에서 슈퍼클래스 멤버의 접근제어자를 더 좁은 범위로 변경할 수 없다.
  • 추상(abstract)클래스에 추상 메서드를 선언해둠으로써 상속받는 서브 클래스에서 해당 메서드 구현을 강제할 수 있다.
  • 서브 클래스의 생성자에서 반드시 상위 클래스의 생성자를 호출하여 초기화해줘야 한다.
  • private생성자를 가진 클래스는 상속을 허용하지 않는다

 

마블의 어벤져스에 빗대어 상속을 말해볼 수 있을 것 같다. 어벤져스의 두 번째 시리즈인 에이지 오브 울트론은 어벤져스 1편을 상속 받아 만들어졌다. 당연한 얘기다. 1편과 2편의 내용이 전혀 다른 이야기를 '시리즈'라고 부를 순 없다. 어벤져스 시리즈를 클래스로 만들어보면 아래와 같다.

  1. 첫 번째 어벤져스

public class Avengers {
    Ironman ironman;
    Thor thor;
    CaptainAmerica captainAmerica;
    Hulk hulk;
    ...

    public Avengers() {
        this.ironman = new Ironman();
        ...
    }

    public void setSuit() {
        ironman.setSuit("오래 입은 수트");
    }
}
  1. 두 번째 시리즈 에이지 오브 울트론
public class Avengers2 extends Avengers {
    //새로운 주인공 추가
    Ultron ultron;

    public Avengers2() {
        super();
        ultron = new Ultron();
    }   

    @Override
    public void setSuit() {
        ironman.setSuit("새로운 수트");
    }
} 
  1. Avengers2에는 ironman이라는 변수가 없지만 Avengers를 상속받았기 때문에 Avengers가 가진 멤버에 접근할 수 있다. 이때 접근 제어자가 private인 멤버에는 접근할 수 없다.

super()

Avengers2 생성자에서 super()를 호출했다. 이건 상위 클래스(Avengers)의 생성자를 호출하는 메서드다. this()를 떠올려보면 쉽게 알 수 있다. this()가 생성자 내에서 다른 생성자를 호출할 때 사용한다면 super()는 서브 클래스의 생성자에서 상위클래스 생성자를 호출할 때 쓴다. 몇 가지 제약부터 말해보면 이렇다.

  1. 반드시 생성자 첫 줄에
  1. 선언하지 않으면 컴파일러가 자동으로 생성한다. 그렇다고는 하지만 상위 클래스가 기본 생성자를 가지고 있지 않다면 컴파일 에러가 발생한다.

왜 상위 클래스의 생성자를 호출해서 초기화 시켜줘야 할까? 만약 어디선가 Avengers2 생성한다고 가정해보자.

   Avengers2 avengers = new Avengers2();

어떤 일이 벌어질까? 먼저 Avengers2의 생성자가 호출되면서 초기화 코드가 실행될 것이다. 맨 첫 줄에 컴파일러가 자동으로 super()를 추가해준다고 했으니 상위 클래스인 Avengers가 Avengers2보다 먼저 초기화된 다음 Avengers2를 초기화 한다. 중요한 것은 반드시 상위 클래스가 먼저 초기화된다는 것이다. 이유는 간단하다. Avengers2가 Avengers의 멤버들을 자유롭게 사용하기 위해선 Avengers가 먼저 JVM메모리에 올라와야 하기 때문이다. 따라서 반드시 super()를 이용해서 상위 클래스를 먼저 초기화해야 한다. 만약 상위 클래스가 기본 생성자를 가지고 있지 않다면 꼭 super()를 직접 호출해서 필요한 데이터를 인자로 넣어주어야 한다.

Override

Avengers2를 보면 상위 클래스에 있는 setSuit()를 오버라이딩했다. 오버라이딩이란 상위클래스에 있는 메서드를 현재 클래스에 맞게 재정의하는 것이다. 이렇게 상위 클래스에 있는 메서드를 있는 그대로 사용할 필요는 없고 필요에 따라 알맞게 재정의하여 사용할 수 있다.

상속의 (치명적인) 단점

4개의 어벤져스 시리즈가 있다. 각각의 시리즈는 바로 전 시리즈를 상속해서 만들어진다. 그렇게 만들어진 마지막 시리즈는 엔드 게임이다.

public class Avengers4EndGame extends Avengers3InfinityWar {
    //...
} 

엔드게임 클래스는 어벤져스 1, 2, 3 시리즈를 모두 상속 받고 있는 것이나 다름 없다. 매번 똑같은 주인공을 선언해야 하는 중복은 줄어들겠지만 그만큼이나 치명적인 단점이 있다. 현실세계에서는 그럴 일이 없겠지만 프로그래밍 세계에서는 자주 일어나는 어벤져스1의 세부 사항을 변경해야 해달라는 요구사항이 들어온다면 무척 난감해진다. 1시리즈의 변경이 일어나면 2, 3, 4도 영향을 받을 수밖에 없는 구조다. 그것이 어떤 영향일지는 상속 계층이 늘어나면 늘어날수록 예상하기 어려워진다. 이건 상속이 클래스와 클래스 사이를 강하게 결합하고 있기 때문이다.

이 문제는 추상클래스로 어느 정도는 해결할 수 있다. 어벤져스1이나 어벤져스2 같은 시리즈를 상속받지 말고, 어벤져스 시리즈라면 응당 가져야 할 것들을 모아놓은 추상클래스를 만든다.

public abstract class AvengersAbstract {
    Ironman ironman;
    Thor thor;
    CaptainAmerica captainAmerica;
    Hulk hulk;

    //상속 받는 클래스에서 멤버를 초기화하도록 '강제'
    public abstract void initMember();
}

public class Avengers extends AvengersAbstract {
    @Override
    public void initMember() {
        ironman = new Ironman();
        thor = null; //"어벤져스1에서 토르를 빼주세요 요구사항"
        ...
    }
}

public class Avengers3 extends AvengersAbstract {
     Thanos thanos;
    @Override
    public void initMember() {
        thanos = new Thanos();
    }
}

AvengersAbstract클래스에 멤버를 초기화하는 추상메서드를 만들었다. 서브 클래스는 반드시 이 추상메서드를 구현해서 사용해야 한다. 따라서 자신의 필요에 따라 다르게 멤버를 초기화 할 수 있다. 이 방식의 장점은 Avengers1의 변화가 다른 시리즈에 영향을 미치지 않는다는 것이다. 따라서 어벤져스1에서는 토르를 빼달라는 변경 요구사항에도 유연하게 대응할 수 있다. 어벤져스1에서 토르를 null로 설정한다고 해서 어벤져스2나 3에는 아무런 영향을 끼치지 않는다.

하지만 이것 또한 임시 방편일 뿐이다. 지금은 단순화 해서 이 정도지만 계층은 얼마든지 늘어날 수 있다. 계층이 늘어나면 늘어날수록 결합도가 높아지기 때문에 상속의 단점은 명확해진다. 따라서 결합을 낮추는 인터페이스를 사용하라고 권장되고는 한다.