독서이야기/엘레강트 오브젝트 - 새로운 관점에서 바라본 객체지향

[엘레강트 오브젝트] 4.3 final이거나 abstract이거나

사랑꾼이야 2023. 4. 9. 23:03
반응형

이 내용은 엘레강트 오브젝트 를 읽으면서 정리한 내용을 포함하고 있습니다.

  • 상속의 문제점
  • final이나 abstract
  • 정리

상속의 문제점

상속의 가장 큰 문제는 객체들의 관계를 너무 복잡하게 만든다는 것이다.

class Document {
    public int length() {
        return this.content().length();
    }
    public byte[] content() {
        // 문서의 내용을 바이트 배열로 로드한다.
    }
}
class EncryptedDocument extends Document {
    @Override
    public byte[] content() {
        // 문서를 로드해서, 즉시 복호화하고, 복호화한 내용을 반환한다.
    }
}
  • EncryptedDocument 클래스의 content() 메서드는 내용을 로드한 후 암호를 즉시 해독한다.
  • content() 매소드를 오버라이딩했기 때문에 Document 클래스로부터 상속된 length() 메서드의 행동이 변해버렸다.
    • EncryptedDocument 클래스의 length() 메서드는 디스크에 저장되어 있는 원래 문서의 길이가 아니라 해독한 문서의 길이를 반환한다.
    • 이 메서드를 호출한 사용자는 EncryptedDocument 클래스의 length() 메서드가 Document 클래스의 length() 메서드와 동일하게 문서가 차지하는 물리적인 스토리지 크기를 반환할 것이라고 기대했을 것이다.

자식이 부모의 유산에 접근하는 일반적인 상속과 달리, 메서드 오버라이딩은 부모가 자식의 코드에 접근하는 것을 가능하게 한다.

상속이 OOP를 지탱하는 편리한 도구에서 유지보수성을 해치는 골치덩어리로 추락하는 곳이 바로 이 지점이다.

  • 복잡성이 상승하고, 코드를 읽고 이해하기가 매우 어려워진다.

final이나 abstract

클래스와 메서드를 final이나 abstract 둘 중 하나로만 제한한다면 문제가 발생할 수 있는 가능성을 없앨 수 있다.

  • 먼저 Document 클래스가 final 이라면 상속을 받을 수 없다.
  • 반면에 content() 메서드가 abstract 라면 Document 클래스 안에서는 content() 메서드를 구현할 수 없기 때문에 length() 메서드를 이해하는데 혼란스럽지 않습니다.

final 클래스

  • 사용자 관점에서 블랙 박스
  • 상속을 통해 수정할 수 없다.
  • 불투명하고 독립적이며 자신이 어떻게 행동해야 하는지 알고 있고, 어떤 도움도 필요로 하지 않는다.
  • 기술적으로 final 클래스 안의 어떤 메서드도 오버라이딩할 수 없다. 메서드는 영원히 final이다.

abstract 클래스

  • 글래스 박스이고 불완전
  • 스스로 행동할 수 없기 때문에 누군가의 도움이 필요하며 일부 요소가 누락되어 있다.
  • 기술적인 관점에서 abstract 클래스는 아직 클래스가 아니며, 제대로 된 클래스를 생성하기 위해 사용할 수 있는 원재료라고 할 수 있다.
  • 기술적으로 abstract 클래스의 특정 메서드를 오버라이딩할 수 있지만 다른 메서드는 모두 final이다.

final 클래스를 통한 개선

final 앞의 수정자는 이 클래스 안의 어떤 메서드도 자식 클래스에서 오버라이딩할 수 없다는 사실을 컴파일러에게 알려준다.

final class Document {
    public int length() { /* 코드는 동일 */ }
    public byte[] content() { /* 코드는 동일 */ }
}

final 클래스인 Document 를 상속 받을 수 없기 때문에 인터페이스를 추가하였다.

interface Document {
    int length();
    byte[] content();
}

Document 의 이름을 DefaultDocument 로 변경하고, 이 클래스가 Document 인터페이스를 구현하게 만든다.

final Class DefaultDocument implements Document {
    @Override
    public int length() { /* 코드는 동일 */ }
    @Override
    public byte[] content() { /* 코드는 동일 */ }
}

DefaultDocument 를 재사용해서 EncryptedDocument 를 구현한다.

  • final 클래스를 상속 받는 것은 불가능하기 때문에 상속 대신 캡슐화를 사용한다.
final class EncryptedDocument implements Document {
    private final Document plain;
    EncryptedDocument(Document doc) {
        this.plain = doc;
    }
    @Override
    public int length() {
        return this.plain.length();
    }
    @Override
    public byte[] content() {
        byte[] raw = this.plain.content();
        return /* 원래 내용을 복호화 한다. */;
    }
}
  • 이 예제는 의무적으로 finalabstract 를 사용하도록 강제하면 대부분의 위치에서 상속을 사용할 수 없다는 사실을 잘 보여준다.
  • 만약 모든 클래스가 final 이라면 오로지 캡슐화만을 이용할 수 있다.

Q. 상속이 적절한 경우는 언제일까요?
클래스의 행동을 확장하지 않고 정제할 때이다.

  • 확장이란, 새로운 행동을 추가해서 기존의 행동을 부분적으로 보완하는 일을 의미한다.
  • 정제란, 부분적으로 불완전한 행동을 완전하게 만드는 일을 의미한다.

abstract 클래스를 통한 개선

abstract class Document {
    public abstract byte[] content();
    public final int length() {
        return this.content().length;
    }
}

디스크로부터 콘텐츠를 로드하는 방법을 알고 있는 DefaultDocument 클래스를 추가해서 Document 를 정제해야 한다.

final class DefaultDocument extends Document {
    @Override
    public byte[] content() {
        // 디스크에서 내용을 로드한다.
    }
}

Document 를 다른 방식으로 정제하는 EncryptedDocument 클래스를 추가하겠다.

final class EncryptedDocument extends Document {
    @Override
    public byte[] content() {
        public byte[] content() {
            // 디스크에서 내용을 로드하고, 내용을 암호화한 후 반환한다.
        }
    }
}

정리

  • Java를 비롯한 많은 언어에서 finalabstract 어느 쪽에도 해당되지 않는 클래스와 메서드를 만들 수 있도록 허용한 것은 실수이다.
  • 우리는 의도를 명확하게 표현해야 한다.
  • 다시 말해서 메서드는 올바른 방식으로 설계하거나, 아니면 아예 설계하지 말아야 한다. 그 사이에 어떤 것도 있어서는 안된다.
반응형