19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

클래스의 확장이 필요하지 않다면 상속을 금지하라 상속을 허용한다면, 상속을 고려한 설계를 하고 문서를 남겨라

📌 1. 발표 전 알아야 할 개념

상속이 왜 위험한가?

  • 캡슐화가 깨질 가능성이 있다

  • 유지보수가 어렵다

  • 예기치 않은 오류가 발생할 수도 있다다


📕 2. 상속을 고려한 설계와 문서화해라

🔗 전체 예제 코드 : [Item19] 테스트 코드

Car - SportsCar - CarMain 흐름도를 이미지로 나타내면 아래 그림과 같다.

2-1. 상속용 클래스는 재정의할 수 있는 메서드들을 문서로 남겨야 한다

  • 문서에 담겨야 하는 내용들은 다음과 같다.

    1. 재정의 가능성이 있는 메서드

    2. 어떤 순서로 호출하는지

    3. 각각의 호출 결과가 어떤 영향을 미치는지

  • javadocs(/**이나 ///로 작성)의 @implSpec을 활용한다

    /**
     * @implSpec
     * drive () method accelerates by calling {@link #accelrate ()}
     * The subclass can implement different acceleration methods by redefining Accelrate ().
     */
    public final void drive() {
        System.out.println("driving");
        accelrate();
        System.out.println("speed is " + speed);
    }

2-2. Hook을 활용하여 유연한 설계를 하자

  • Hook이란? 재정의 가능한 메서드

  • protected를 이용하여 하위 클래스가 동작을 변경할 수 있도록 한다

  • 부모 클래스의 중요 내용은 변경하지 않고, 하위 클래스가 확장할 수 있음

    // Car 클래스스
    /**
     * 가속 (protected로 구현된 hoock 메서드)
     * 하위 클래스에서 가속 방식 변경 가능
     */
    protected void accelrate() {
        speed+=30;
        System.out.println("accelrate");
    }

    // SportsCar 클래스
    // Car를 상속
    public class SportsCar extends Car{
    @Override
    // accelrate 메서드 재정의
    protected void accelrate() {
        System.out.println("sportscar accelrate");
        super.accelrate();
        super.accelrate();
    }
    //
}

Q. 그럼 어떤 메서드를 protected로 노출해야 할까? A. protected 클래스가 많아지면, 내부 접근 및 구현이 많아지므로 수를 줄여야 한다. 한편? 너무 적게 노출하면, 상속의 의미가 사라진다! 결론은 직접 하위 클래스를 만들어 보는 것만이 유일하다

  • 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다!

2-3. 생성자에서 재정의 가능 메서드를 호출하면 안된다 💥

  • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 실행된다 ➡️ NullPointerException

  • 따라서 부모 클래스의 생성자에서 재정의 가능 메서드를 호출하면 안됨!

    // WrongCar 클래스
    private int speed = 0;

    public WrongCar() {
        System.out.println("Car 생성자");
        // 부모 클래스 생성자에서 재정의 메서드를 호출할 경우 오류 발생
        accelrate();
    }

    protected void accelrate() {
        System.out.println("accelrate");
        speed += 10;
    }
// WrongCar를 상속하는 BrokenCar 클래스
public class BrokenCar extends WrongCar{
    // 이 위치에서는 초기화     X
    private Integer turboSpeed;

    public BrokenCar() {
        System.out.println("BrokenCar 생성자");
        // 초기화
        this.turboSpeed = 50;
    }

    @Override
    protected void accelrate() {
        System.out.println("BrokenCar accelrate");

        // turboSpeed가 초기화 이전이라 오류 발생
        setSpeed(getSpeed() + turboSpeed);
    }
}
// main 클래스
public class WrongCarMain {
    public static void main(String[] args) {
        System.out.println("BrokenCar test");
        BrokenCar car = new BrokenCar();
        car.drive();
    }
}
Image
Image

2-4. 상속을 원하지 않는 클래스는 상속을 금지시켜라!

상속은 고려해야 하는 것이 많으므로, 상속용이 아닌 클래스는 아예 상속이 불가능하게 만들어준다.

  • final로 선언하기

  • 모든 생성자를 private로 선언하고, public 정적 팩터리 메소드 만들기

public final class newCar {
}

💨 향후 확장 포인트

  • [템플릿 메서드 패턴(Template Method Pattern)]

    • 전체적인 흐름은 부모 클래스에서 정의

    • 하위 클래스는 hook 메서드를 사용하여 일부만 변경

    • 코드 중복을 줄이고, 확장성을 높이는 패턴


🤖 최종 결론

  • 상속을 고려한 클래스는 문서화(@implSpec)를 하라

  • Hook을 사용하여 하위 클래스를 유연하게 확장 시킬 수 있게 만들어라

  • 부모 클래스의 생성자에서 재정의 가능한 메서드는 호출하면 안된다


❗어려웠던 점

  1. '재정의 가능한 메서드를 부모 생성자에서 호출하지 마라'라는 말이 어려웠다. 실제 코드를 작성해봤을 때도, 단순히 보면 충분히 정상 작동될 수도 있다고 생각할 수도 있는 코드였다 ➡️ 하지만 상속과 관련된 코드를 작성할 땐, '부모 생성자가 실행되는 동안, 하위 클래스의 필드가 아직 초기화되지 않을 수도 있다'라는 실행 흐름을 정확하게 파악하는 것이 중요하다.


😶‍🌫️ 느낀점

  • 요약을 다 쓰고 나서 다시 읽어보니, 쉽게 이해할 수 있는 부분들도 너무 어렵게 생각해서 이해 과정에서 오래 걸린 것 같다. 내용을 읽을 때 너무 파고드는 것보다 먼저저 쉬운 예시 코드로 작성해보며 이해력을 높이자.

Last updated