본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
8장 : 상속과 다형성

상속과 super

상속은 기존 클래스의 코드를 가져오는 기능이면서 더 중요한 타입 관계를 만듭니다.

하위 클래스 객체는 상위 타입이 필요한 위치에 들어갈 수 있어야 합니다.

생성자는 상속되지 않으며 하위 생성 과정에서는 먼저 상위 부분이 초기화됩니다.

super는 상위 생성자나 가려진 멤버에 접근하는 통로입니다.

이 절에서는 재사용보다 대체 가능성이라는 기준으로 상속을 읽습니다.


extends가 하위 타입 관계를 만든다

Manager extends Employee라면 Manager는 Employee가 약속한 공개 동작을 모두 제공해야 합니다.

단지 필드를 공유하고 싶다는 이유만으로 관계를 만들지 않습니다.


상위 생성자가 객체의 기반 상태를 준비한다

하위 생성자의 첫 문장에서 super(...)를 호출하며 생략하면 접근 가능한 무인자 생성자를 호출합니다.

상위 타입이 요구하는 불변식을 먼저 만족시킵니다.

하위 객체를 만들 때도 상위 클래스가 맡은 필드는 상위 생성자가 먼저 준비합니다. 생성자 첫 문장의 super(...)를 생략할 수 있는 경우는 컴파일러가 인자 없는 super()를 넣을 수 있을 때뿐입니다.

InheritanceContract.java
public class InheritanceContract {
    static class Employee {
        private final String name;
        Employee(String name) {
            if (name.isBlank()) throw new IllegalArgumentException("name");
            this.name = name;
        }
        String label() { return name; }
    }

    static final class Manager extends Employee {
        private final int teamSize;
        Manager(String name, int teamSize) {
            super(name);
            if (teamSize < 0) throw new IllegalArgumentException("teamSize");
            this.teamSize = teamSize;
        }
        String teamLabel() { return super.label() + "(" + teamSize + ")"; }
    }

    public static void main(String[] args) {
        Employee employee = new Manager("Ada", 4);
        System.out.println(employee.label());
        System.out.println(((Manager) employee).teamLabel());
    }
}

상위 생성자와 하위 생성자에 로그를 넣으면 항상 상위 쪽이 먼저 출력됩니다. 하위 생성자에서 상위 필드를 전제로 작업할 수 있는 이유도 객체의 기반 부분이 먼저 초기화되기 때문입니다.

Ada
Ada(4)

Employee 참조는 Manager 객체를 받을 수 있지만 Manager 전용 메서드는 상위 계약에 포함되지 않습니다.

상위 생성자로 기반 상태 검증

하위 생성자의 super(...) 호출이 상위 클래스에 필요한 값을 넘깁니다. 상위 타입이 소유한 불변식은 상위 생성자 한곳에서 검사합니다.

하위 생성자가 추가 상태 준비

상위 생성자가 정상적으로 끝난 뒤 하위 필드를 초기화합니다. 이 순서 덕분에 하위 로직은 완성된 기반 상태를 전제로 할 수 있습니다.

상위 타입 위치에서 하위 객체 사용

완성된 하위 객체는 상위 타입 변수가 가리킬 수 있습니다. 이때 상위 타입의 공개 계약은 유지되어야 하며 하위 전용 기능은 정적 타입에 드러나지 않습니다.


private 상태는 상속돼도 직접 보이지 않는다

하위 클래스는 상위 객체 부분을 갖지만 private 필드에 직접 접근하지 못합니다.

상위가 하위 클래스에 열어 둔 메서드를 통해 협력합니다.

private 필드는 하위 객체 안의 상위 부분에 존재하지만 하위 클래스 이름으로 직접 접근할 수 없습니다. 상위 클래스가 접근 가능하게 제공한 label() 같은 동작을 통하면 내부 표현과 검증 규칙을 우회하지 않고 상태를 사용할 수 있습니다.

비교 1
class Parent {
    Parent(String id) { }
}
비교 2
class Child extends Parent {
    Child(String id) {
        super(id); // 상위 생성자는 하위 상태보다 먼저 준비됨
    }
}
비교 3
// class Broken extends Parent { Broken() { } }
// 컴파일 오류: Parent에 무인자 생성자가 없음

final로 확장 경계를 닫을 수 있다

변경되면 계약이 깨지는 클래스나 메서드는 final로 하위 재정의를 막습니다.

확장을 허용할 지점을 의도적으로 선택합니다.


하위 타입은 상위 타입의 약속을 지킨다

상속은 코드를 재사용하는 문법보다 상위 타입을 기대하는 모든 위치에 하위 객체를 넣을 수 있다는 계약입니다.

더 강한 선조건이나 약한 결과를 만들지 않습니다.

상위 타입을 받는 메서드에 하위 객체를 넣어도 상위 타입이 약속한 결과를 깨뜨리면 안 됩니다. 하위 구현에서 예외 조건을 더 엄격하게 만들거나 반환 의미를 바꿔 호출자가 타입별 분기를 해야 한다면 상속 관계를 다시 검토합니다.


상속은 구현 편의가 아니라 하위 객체를 상위 계약으로 사용해도 의미가 깨지지 않는 경우에만 선택합니다.