정체모를 존재에 의해 사람들이 집단자살하기 시작하면서 벌어지는 이야기. 처음에 산드라 블록은 산모로 나오는데 여동생과 함께 차를 타고 아수라장이 된 도로를 벗어나려다 여동생이 그 존재를 보게 되면서 사고가 나고 여동생은 산드라 블록이 보는 바로 앞에서 자살한다. 그러다 ‘문라이트’에 출연했던 트레반트 로즈의 도움으로 가까운 곳에 있던 집에 들어가게 되고, 그곳에서 한동안 머문다. 그 존재를 눈으로 보면(심지어 CCTV 화면을 통해 봐도) 자살하게 되기에 사람들은 살아남기 위해 눈가리개를 하고 다니게 된다. 그러다 함께 지내던 다른 산모가 낳은 아이까지 맡게 되면서 두 아이를 책임지고 좀 더 안전한 공동체로 데려다 주기 위해 고군분투하는 과정을 교대로 보여주면서 이야기를 끌어간다.
우선 아무런 정보가 없는 막연함이 주는 공포를 잘 살린 것 같다. 악령이라고 해야 할지 뭔가 어둠으로만 표현되는 존재가 나오는데, 눈으로 보게 되면 눈이 충혈되고 이상하게 바뀌면서 조종하는 것으로 그려진다. 악령 같은 존재가 가까이 오면 새들이 불안해하는데, 우연하게 이를 알게 된 산드라 블록이 새가 들어있는 상자를 아이들로 하여금 지니게 하고, 이것 때문에 제목이 ‘버드 박스’인 것 같다.
좀 허술한 모습도 몇 가지 보인다. 우연찮게 피신처에 머물게 된 사람들이 각자 내용 전개에 필요한 특징을 하나씩 가지고 있다든가, 사회 기반 시설이 몽땅 망가졌을 텐데 전기/수도가 멀쩡히 나오고, 사람들이 죽어나가면 야생 동물들의 세상이 될 텐데 그런 점도 반영되지 않은 듯하고, 마지막 설정도 조금 아쉬운 점이 있다.
허술한 점이 있긴 하지만 산드라 블록의 열연이나 설정 자체는 볼 만하다. 눈을 가린 상태에서 느껴지는 답답함이 잘 드러나고, 긴장감이 영화 끝까지 이어진다. 마지막 반전도 생각할 거리를 준다.
‘알렉시티미아’라는 감정 표현 불능증이라는 병을 가지고 태어난 아이의 성장기. 평범하지 않은 아이를 키우는 부모의 고난을 완벽하게 이해할 수는 없기에 이 책을 읽으면서 그저 우리 아이들이 건강하고 평범하게 태어났다는 사실만으로도 감격스럽고 고맙기만 하다는 생각이 든다.
아이가 생기기 전에는 몰랐는데 아이가 생긴 후에는 세상에 감사한 일들이 많아지는 신기한(?) 경험을 하게 된다. 가령 동감하기는 힘들지 모르겠지만 전쟁의 위험처럼 평소에는 전혀 생각할 일이 없는 것에도 다행스러움을 느끼곤 한다. 아내하고도 종종 이야기하지만 별다른 위험 없이, 무탈하게, 건강하게 사는 것이 얼마나 어렵고 확률 낮은 일인지, 지금처럼 사는 게 얼마나 축복인지에 대해 함께 입을 모은다.
올해 언젠가, 집안 가족 구성원 중 한 명이 흔히 이야기하는 ‘사이코패스’인 가족의 경험담을 읽을 적이 있는데, 이 책을 읽으면서 그 내용이 생각 났다. 어떤 이유에선지는 모르겠지만 감정 능력이 떨어지기 때문에 우리가 흔히 상식적으로 생각하는 윤리 기준을 일일이 가르쳐야 한다는 내용이 있었다. 지극히 당연한 거지만 동물을 죽여서는 안 된다라든가 다른 사람을 괴롭히면 안 된다, 라는 것 같은.
아몬드에서도 이와 비슷한 부분이 있긴 하지만, 아몬드의 주인공은 지속적으로 다른 아이들에게 다르다는 이유로 따돌림당하고 차별을 당한다. 그런데 오히려 주인공이 감정을 제대로 느끼기가 힘든 것이 오히려 결과적으로는 다행스럽다고 해야 하나. 괴롭힘이나 따돌림 때문에 잘못된 선택을 하는 일이 잦아서 차라리 이런 점에서는 오히려 도움이 되지 않을까 생각했다.
책의 중반부부터는 진정한 교우관계도 만들고 조금씩 감정적으로 성장하는 모습을 보여준다. 상처가 있는 여린 친구 곤이와 자신의 꿈을 향해 조금씩 나아가는 도라를 통해 새로운 관계와 감정들을 접해가는 모습이 그려진다. 후반부는 설정이나 흐름이 조금 거칠긴 하지만 따뜻한 결말로 마무리되어 책을 읽으면서 느껴졌던 안타까움이 덜어져서 홀가분했다.
청소년 소설이긴 하지만 청소년 소설이라는 테두리를 벗어나서 아이들의 부모로서도 동감할 만한 부분이 있어서 좋았다. 아이들이 태어나 성장하는 모습을 보면서 느끼는 감정들이 이 책 덕분에 좀 더 특별해지는 것 같다.
라이브러리는 본질적으로 호출 가능한 함수의 집합으로서, 오늘날에는 보통 클래스에 모여 있다.
클래스에 모여 있는 함수? 초면에 정말 죄송하지만 이건 틀렸다. 그리고 객체지향 프로그래밍의 클래스에 대해 흔히들 하는 오해다. 클래스는 함수를 모아두는 곳이 아니다. 그리고 객체는 자료구조가 아니다.
그럼 “적절한” 객체란 무엇일까? 무엇이 적절한 객체이고, 무엇이 적절한 객체가 아닐까? 차이점이 뭘까? 이것이 격렬한 비판을 받을 만한 주제이긴 하지만 아주 중요하다. 객체가 무엇인지 이해하지 않으면 어떻게 객체지향 소프트웨어를 작성할 수 있을까? 글쎄, 자바, 루비 등의 언어 덕분에 그렇게 하는 게 가능하다. 그런데 그럼 얼마나 좋을까? 안타깝게도 이것은 엄밀한 과학이 아니고, 사람마다 의견이 분분하다. 다음은 좋은 객체의 품질을 나열한 것이다.
클래스 vs. 객체
객체에 관해 이야기하기에 앞서 클래스란 무엇인가에 대해 정의해보자. 클래스는 객체가 태어나는(인스턴스화라고도 하는) 곳이다. 클래스의 주된 책임은 필요에 따라 새로운 객체를 생성하고 더는 사용하지 않을 때 객체를 파괴하는 것이다. 클래스는 해당 클래스의 자식들이 어떤 모습이어야 하고 어떻게 행동해야 하는가를 알고 있다. 다시 말해서, 해당 클래스의 자식들이 따라야 하는 계약에 대해 알고 있다.
이따금 클래스를 “객체 템플릿”으로 부르는 것(예를 들면 위키피디아에서 그렇게 하고 있다)을 듣곤 한다. 이 같은 정의는 정확하지 않은데, 이 정의에 따르면 클래스는 수동적인 위치에 있기 때문이다. 이 정의는 누군가가 템플릿을 가지고 그것을 사용해 객체를 만들어낸다고 가정한다. 그럴 수도 있지만 엄밀히 말하자면 개념적으로 틀린 말이다. 다른 아무도 여기에 관여해서는 안 된다. 클래스와 그것의 자식들만 있을 뿐이다. 객체는 클래스에게 또 다른 객체를 만들라고 요청하고, 클래스는 객체를 만들어낸다. 이게 전부다. 루비에서는 이 같은 개념을 자바나 C++보다 훨씬 잘 표현한다.
photo = File.new('/tmp/photo.png')
photo 객체는 File 클래스에 의해 생성된다(new는 클래스에 대한 진입점이다). 한번 생성된 객체는 스스로 동작한다. 자신을 누가 만들었고 클래스에 형제 자매가 얼마나 더 있는지 알아서는 안 된다. 그렇다. 리플렉션은 엉터리 같은 생각이며, 이후 글에서 이 주제에 대해 다루겠다 🙂 이제 객체 자체를 비롯해 객체의 가장 좋은 측면과 가장 나쁜 측면에 대해 알아보자.
1. 객체가 현실 세계에 존재한다
먼저, 객체는 생명체다. 게다가 객체는 의인화, 즉 인간처럼 취급해야 한다(또는 애완동물을 선호한다면 애완동물처럼). 이것은 기본적으로 객체가 자료구조나 함수의 집합이 아니라는 것을 의미한다. 그 대신 객체는 자신만의 생명주기, 행위, 습관을 지닌 독립적인 개체다.
직원, 부서, HTTP 요청, MySQL 안의 테이블, 파일 안의 한 줄, 파일 자체는 적절한 객체다. 그것들은 현실 세계에 존재하고, 심지어 우리가 소프트웨어를 종료하더라도 존재하기 때문이다. 좀 더 정확히 말하자면 객체는 현실 세계의 피조물을 대표하는 것이다. 객체는 다른 모든 객체 앞에 있는 그러한 현실 세계의 피조물의 대리자(proxy)다. 그러한 피조물이 없다면 당연히 객체도 존재하지 않는다.
이 예제에서는 File로 하여금 새로운 photo 객체를 생성하도록 요청하고 있는데, 이 객체는 디스크 상의 실제 파일을 대표할 것이다. 여러분은 파일이 무언가 가상적이고 컴퓨터를 켰을 때만 존재하는 것이라고 말할 수도 있다. 나는 이 말에 동의하고 “현실 세계”의 의미를 다음과 같이 수정하고자 한다: 객체가 살아가는 프로그램의 범위에서 벗어난 곳에 존재하는 모든 것. 디스크 파일은 위 프로그램의 범위 밖에 존재한다. 프로그램 내에서 그것의 대표자를 만들어내는 것이 완벽하게 맞는 것은 바로 그런 이유에서다.
컨트롤러, 파서, 필터, 검증기, 서비스 로케이터, 싱글턴, 팩터리는 좋은 객체가 아니다(그렇다, 대부분의 GoF 패턴은 안티패턴이다!). 이것들은 소프트웨어 밖의 현실 세계에는 존재하지 않는다. 단지 다른 객체와 함께 사용하기 위해 만들어낸 것이다. 그것들은 인공적이고 가짜 피조물이다. 그것들은 아무것도 대표하지 않는다. 진심으로 말하건대 XML 파서는 누구를 대표할까? 아무도.
그것들 중 일부는 이름을 바꾼다면 좋아질 수 있지만 일부는 자신의 존재를 증명할 길이 전혀 없다. 예를 들어, XML 파서는 “파싱 가능한 XML”로 이름을 바꾸고 우리가 말하는 범위 바깥에 존재하는 XML 문서를 대표할 수 있다.
늘 다음과 같이 자문해 보라. “내가 만든 객체 배후에 존재하는 현실 세계의 개체는 무엇일까?” 이 질문의 답을 찾을 수 없다면 리팩터링을 고려해 봐야 한다.
2. 객체가 계약에 따라 동작한다
좋은 객체란 언제나 계약에 따라 동작한다. 객체는 자신의 특성이 아니라 자신이 준수하는 계약 때문에 사용되길 예상한다. 한편으로 우리가 객체를 사용할 때는 특정 클래스의 일부 특정 객체가 우리를 위해 동작하도록 차별하거나 예상해서는 안 된다. 우리는 아무 객체라도 우리의 계약에 명시된 일을 하도록 예상해야 한다. 객체가 우리가 원하는 일을 하기만 한다면 해당 객체가 만들어진 클래스, 성별, 종교에 관심을 둬서는 안 된다.
예를 들어, 화면에 사진을 보여주고 싶다고 해보자. 사진을 PNG 형식의 파일로부터 읽어오고 싶다. 이를 위해 DataFile 클래스의 객체와 계약해서 해당 이미지의 바이너리 콘텐츠를 가져다 주도록 요청하려고 한다.
그런데 잠깐. 해당 콘텐츠를 정확히 어디에서 가져와야 할지(즉 디스크 상의 파일, HTTP 요청, 또는 드롭박스 내의 문서)에 대해 내가 관심이 있을까? 사실 그렇지 않다. 내가 관심 있는 것은 어떤 객체가 나에게 PNG 콘텐츠가 담긴 바이트 배열을 준다는 것이다. 따라서 내 계약은 다음과 같은 모습일 것이다.
interface Binary {
byte[] read();
}
이제, 어떤 클래스의 어떤 객체라도(DataFile만이 아니라) 나를 위해 일해줄 수 있다. 그 객체가 자격을 갖추기 위해 해야 하는 일이라곤 Binary 인터페이스를 구현함으로써 계약을 준수하는 것뿐이다.
여기서 발견할 수 있는 규칙은 간단하다. 좋은 객체 안에 담긴 모든 공용 메서드는 인터페이스 상에 선언된, 상응하는 메서드들을 구현해야 한다는 것이다. 여러분이 만든 객체에 어떤 인터페이스에서도 상속되지 않은 공용 메서드가 있다면 그 객체는 잘못 설계된 것이다.
여기엔 두 가지 실제적인 이유가 있다. 첫째, 계약 없이 동작하는 객체는 단위 테스트에서 목킹(mock)하는 것이 불가능하다. 둘째, 계약 없는 객체는 데코레이션을 통해 확장하는 것이 불가능하다.
3. 객체가 고유하다
좋은 객체는 언제나 고유하기 위해 무언가를 캡슐화해야 한다. 캡슐화할 것이 없다면 객체가 동일한 복제본을 가질 수도 있으며, 이것은 바람직하지 않다고 생각한다. 다음은 복제본을 가질 수 있는 나쁜 객체의 예다.
class HTTPStatus implements Status {
private URL page = new URL("https://localhost");
@Override
public int read() throws IOException {
return HttpURLConnection.class.cast(
this.page.openConnection()
).getResponseCode();
}
}
다음과 같이 HTTPStatus 클래스의 인스턴스를 몇 개 생성할 수 있으며, 모든 인스턴스는 서로 동일한 것으로 판단될 것이다.
first = new HTTPStatus();
second = new HTTPStatus();
assert first.equals(second);
당연히 정적(static) 메서드만이 담긴 유틸리티 클래스는 좋은 객체를 인스턴스화할 수 없다. 좀 더 일반적으로 말하자면 유틸리티 클래스는 이번 글에서 언급한 이점을 아무것도 갖지 않으며 “클래스”라고 부를 수조차 없다. 유틸리티 클래스는 단순히 객체 패러다임을 엉터리로 남용하는 것에 불과하며, 현대 객체지향 언어를 발명한 사람들이 정적 메서드를 사용할 수 있게 만들어 뒀다는 이유로 존재하는 것뿐이다.
4. 객체가 불변적이다
좋은 객체라면 자신이 캡슐화한 상태를 절대 변경하지 않을 것이다. 기억해 두자. 객체는 현실 세계에 존재하는 개체의 대표자이며, 이 객체는 객체의 전 생애에 걸쳐 변하지 않은 채로 머물러야 한다. 다시 말해서, 객체는 그것이 대표하는 것들을 절대 배신해서는 안 된다. 절대 주인을 바꿔서는 안 된다. 🙂
불변성(immutability)이 모든 메서드가 언제나 동일한 값을 반환한다는 것을 의미하지는 않는다는 점에 유의해야 한다. 오히려 좋은 불변 객체는 매우 역동적이다. 그럼에도 자신의 내부 상태는 절대 변경하지 않는다. 다음 예제를 보자.
@Immutable
final class HTTPStatus implements Status {
private URL page;
public HTTPStatus(URL url) {
this.page = url;
}
@Override
public int read() throws IOException {
return HttpURLConnection.class.cast(
this.page.openConnection()
).getResponseCode();
}
}
read() 메서드가 서로 다른 값을 반환할 수 있음에도 객체는 불변적이다. 이 객체는 특정 웹 페이지를 가리키고 다른 어떤 곳도 가리키지 않을 것이다. 자신의 캡슐화된 상태를 변경하지도 않을 테고, 자신이 나타내고 있는 URL을 배신하는 일 따윈 없을 것이다.
물론 좋은 객체는 객체의 상태를 변경해서 URL을 배신하도록 강제할 수도 있는 설정자 메서드를 갖지 않는다. 다시 말해서 setURL() 메서드를 도입할 경우 HTTPStatus 클래스에 심각한 실수를 저지르는 것이다.
게다가 불변 객체는 How Immutability Helps라는 글에서 설명하는 것과 같이 언제나 여러분으로 하여금 좀 더 응집력 있고, 견고하며, 이해하기 쉬운 설계를 하도록 강제한다.
5. 객체의 클래스에 정적 멤버가 없다
정적 멤버는 클래스의 행위를 구현하지, 객체의 행위를 구현하지 않는다. File이라는 클래스가 있고, 이 클래스의 자식으로 size() 메서드가 있다고 해보자.
final class File implements Measurable {
@Override
public int size() {
// 파일의 크기를 계산해서 반환
}
}
지금까지는 괜찮다. size() 메서드가 이곳에 있는 이유는 Measurable 계약 때문이며, File 클래스의 모든 객체는 자신의 크기를 측정할 수 있을 것이다. 끔찍한 실수는 이 클래스가 정적 메서드를 갖도록 설계하는 것일 것이다(이 같은 설계는 유틸리티 클래스로도 알려져 있으며, 자바, 루비를 비롯한 거의 대부분의 OOP 언어에서 흔히 사용된다).
// 끔찍한 설계, 사용하지 마시오!
class File {
public static int size(String file) {
// 파일의 크기를 계산해서 반환
}
}
이 설계는 객체지향 패러다임에 완전히 반한다. 왜일까? 정적 메서드는 객체지향 프로그래밍을 “클래스지향(class-oriented)” 프로그래밍으로 바꾸기 때문이다. size() 메서드는 객체가 아니라 클래스의 행위를 노출한다. 이것이 뭐가 잘못됐냐고 궁금할 수도 있겠다. 우리가 작성한 코드에 일급 시민으로 객체와 클래스 모두 두지 못할 이유가 있나? 객체와 클래스가 모두 메서드와 프로퍼티를 갖지 못할 이유가 있나?
문제는 클래스지향 프로그래밍의 경우 분해(decomposition)가 더는 작동하지 않는 데 있다. 복잡한 문제를 여러 부분으로 분해할 수 없는데, 전체 프로그램에 클래스의 단 하나의 인스턴스만이 존재하기 때문이다. OOP의 위력은 객체를 범위 분해(scope decomposition)를 위한 도구로서 사용할 수 있다는 것이다. 메서드 내에서 객체를 인스턴스화할 경우 해당 객체는 부여받은 특정 과업을 수행하기 위해 전념한다. 객체는 메서드 주변의 다른 모든 객체로부터 완벽하게 고립된다. 이 객체는 메서드 유효범위에서 지역 변수에 해당한다. 정적 메서드를 가진 클래스는 내가 해당 클래스를 어디에서 사용하건 언제나 전역 변수에 해당한다. 이러한 이유로 이 변수와의 상호작용을 다른 것들과 고립시킬 수가 없는 것이다.
객체지향 원칙에 개념적으로 반하는 것 외에도 공용 정적 메서드는 몇 가지 실제적인 단점을 지니고 있다.
첫째, 목킹하는 것이 불가능하다(PowerMock을 사용할 수도 있겠지만 이렇게 할 경우 자바 프로젝트에서 저지를 수 있는 가장 끔찍한 결정이 될 것이다… 몇 년 전 나도 딱 한 번 그렇게 한 적이 있다).
둘째, 정의상 스레드 안전하지 않다. 왜냐하면 모든 스레드에서 접근 가능한 정적 변수와 언제나 상호작용하기 때문이다. 스레드 안전하게 만들 수도 있겠지만 그러려면 언제나 명시적인 동기화가 필요할 것이다.
공용 정적 메서드를 보게 될 때마다 그 즉시 그것을 재작성하라. 정적(또는 전역) 변수가 얼마나 끔찍한지는 굳이 언급하고 싶지도 않다. 그건 굳이 말 안 해도 알 것이라 생각한다.
6. 객체의 이름이 직명을 나타내지 않는다
객체의 이름은 이 객체가 무엇인지 말해야 하고 무슨 일을 하는지 말해서는 안 된다. 이것은 마치 현실 세계의 객체에 이름을 부여하는 것과 마찬가지인데, ‘페이지 모음기’ 대신 ‘책’을, ‘물 보관기’ 대신 ‘컵’을, ‘몸 감싸개’ 대신 ‘티셔츠’라고 이름을 붙이는 것과 같다. 물론 프린터나 컴퓨터 같은 예외도 있지만 그것들은 아주 최근에 발명됐고 이 글을 읽지 않는 사람들에 의해 만들어진 것들이다. 🙂
예를 들어, 다음과 같은 이름은 그것의 소유자가 누구인지에 대해 우리에게 말해준다. 사과, 파일, 일련의 HTTP 요청, 소켓, XML 문서, 사용자 리스트, 정규 표현식, 정수, PostgreSQL 테이블, 제프리 레보스키(옮긴이-“위대한 레보스키”라는 영화의 주인공). 적절한 이름을 가진 객체는 언제나 작은 그림으로 그려질 수 있다. 심지어 정규 표현식조차 그려질 수 있다.
반대로 다음은 그것의 소유자가 무엇을 하는지 우리에게 알려주는 이름의 예다. 파일 리더, 텍스트 파서, URL 검증기, XML 프린터, 서비스 로케이터, 싱글턴, 스크립트 실행기, 자바 프로그래머. 이것들 중에서 그림으로 그릴 수 있는 것이 있는가? 그렇지 않을 것이다. 이러한 이름은 좋은 객체로 적합하지 않다. 그것들은 끔찍한 설계로 이어지는 끔찍한 이름들이다.
일반적으로, “-er”로 끝나는 이름은 피하라. 그것들 중 대부분은 좋지 않다.
“그럼 FileReader 대신 뭘 써야 할까요?”라고 묻고 싶을 것이다. 더 나은 이름은 무엇일까? 어디 한번 보자. 이미 디스크 상의 실제 파일을 나타내는 File을 가지고 있다. 이 이름만으로는 충분히 강력하지 않은데, 파일의 내용을 어떻게 읽어야 할지 알지 못하기 때문이다. 그러한 능력을 지닌 좀 더 강력한 것을 만들고 싶다. 그것을 뭐라고 불러야 할까? 기억해 두자. 이름은 그것이 무엇인지를 말해야 하고, 무슨 일을 하는지 말해서는 안 된다. 그 객체는 무엇인가? 그것은 데이터를 가진 파일이다. File 같은 단순한 파일이 아니라 데이터를 가진 좀 더 세련된 것이다. 그럼 FileWithData나 단순히 DataFile은 어떨까?
같은 논리를 다른 모든 이름에도 적용할 수 있을 것이다. 언제나 그것이 무슨 일을 하는지가 아니라 그것이 무엇인지에 관해 생각하자. 객체에 직명 대신 실제의, 유의미한 이름을 부여하라.
좋은 객체는 final 클래스나 abstract 클래스에서 온다. final 클래스는 상속을 통해 확장할 수 없는 클래스다. abstract 클래스는 인스턴스를 가질 수 없는 클래스다. 간단히 말해서, 클래스는 다음과 같이 말해야 한다. “넌 나를 분해할 수 없어. 난 블랙박스야”라거나 “난 이미 망가졌어. 날 먼저 고친 후에 사용해.”
그 사이에는 아무것도 없다. final 클래스는 어떤 식으로도 변경할 수 없는 블랙박스다. final 클래스는 원래 동작 그대로 동작하고, 그것을 사용하거나 사용하지 않으면 그만이다. final 클래스의 프로퍼티를 상속하는 또 다른 클래스는 만들 수 없다. final 수정자 때문에 이렇게 하는 것은 금지된다. 이러한 final 클래스를 확장하는 유일한 방법은 final 클래스의 자식을 데코레이션하는 것을 통해서다. HTTPStatus(앞에서 예로 든)라는 클래스가 있고, 이 클래스가 마음에 들지 않는다고 해보자. 음, 이 클래스가 마음에 들긴 하지만 원하는 만큼 강력하진 않다. 나는 HTTP 상태가 400 이상일 경우 이 클래스가 예외를 던지게 만들고 싶다. 그래서 read() 메서드에서 지금 하는 일보다 더 많은 일을 하길 바란다. 전통적인 방법은 클래스를 확장해서 이 메서드를 오버라이드하는 것이다.
class OnlyValidStatus extends HTTPStatus {
public OnlyValidStatus(URL url) {
super(url);
}
@Override
public int read() throws IOException {
int code = super.read();
if (code >= 400) {
throw new RuntimeException("Unsuccessful HTTP code");
}
return code;
}
}
이건 왜 잘못됐을까? 이 방법이 아주 잘못된 이유는 부모 클래스의 메서드 중 하나를 오버라이드함으로써 전체 부모 클래스의 로직을 망가뜨릴 위험을 무릅써야 하기 때문이다. 자식 클래스에서 read() 메서드를 오버라이드하게 되면 부모 클래스의 모든 메서드가 자식 클래스의 새로운 버전을 사용하게 된다는 점을 기억하자. 말 그대로 클래스에 새로운 “구현의 일부”를 곧바로 집어넣고 있는 것이다. 철학적으로 말하자면 이것은 범죄 행위다.
반면 final 클래스를 확장하려면 그것을 블랙박스로 간주하고 여러분만의 구현체로 그것을 장식(Decorator 패턴으로도 알려진)해야 한다.
final class OnlyValidStatus implements Status {
private final Status origin;
public OnlyValidStatus(Status status) {
this.origin = status;
}
@Override
public int read() throws IOException {
int code = this.origin.read();
if (code >= 400) {
throw new RuntimeException("Unsuccessful HTTP code");
}
return code;
}
}
이 클래스는 원본 클래스인 Status와 동일한 인터페이스를 구현하고 있음을 눈여겨보자. HTTPStatus의 인스턴스는 생성자를 통해 이 클래스로 전달되고 캡슐화될 것이다. 그런 다음, 모든 호출을 가로채어 필요한 경우 다른 방식으로 구현될 것이다. 이 설계에서는 원본 객체를 블랙박스로 취급하고 그것의 내부 로직은 전혀 건드리지 않는다.
final 키워드를 사용하지 않는다면 누군가(여러분 자신을 포함해서)가 클래스를 확장한 다음… 범죄 행위를 저지를 것이다 🙁 그러니 final을 사용하지 않는 클래스는 나쁜 설계다.
추상 클래스는 정확히 정반대의 경우다. 추상 클래스는 그것이 불완전하고 “지금 모습 그대로” 사용할 수 없음을 말해준다. 따라서 추상 클래스에 별도의 구현 로직을 집어넣어야 하며, 이때 우리가 건드릴 수 있게 허용한 곳에만 넣을 수 있다. 이러한 지점에는 명시적으로 abstract 메서드로 표시돼 있다. 예를 들어, HTTPStatus는 다음과 같을 것이다.
abstract class ValidatedHTTPStatus implements Status {
@Override
public final int read() throws IOException {
int code = this.origin.read();
if (!this.isValid()) {
throw new RuntimeException("Unsuccessful HTTP code");
}
return code;
}
protected abstract boolean isValid();
}
보다시피 이 클래스는 HTTP 코드의 유효성을 정확히 어떻게 검증해야 할지 알지 못하며, 그 부분은 상속과 isValid() 메서드의 오버라이드를 통해 로직을 주입하길 기대한다. 이 클래스를 상속하는 것으로는 이 클래스에 대해 범죄 행위를 저지르지 않는 셈인데, 다른 메서드는 모두 final로 방어했기 때문이다(메서드의 수정자를 눈여겨보라). 따라서 이 클래스는 우리의 범죄 행위에 대비하고 있고, 완벽하게 방어하고 있다.
정리하자면 클래스를 설계할 때는 final이나 abstract를 지정해야 하며, 그 중간은 없다.
업데이트(2017년 4월): 구현 상속이 나쁘다는 주장에 동의한다면 여러분이 작성하는 모든 클래스는 final 클래스여야 한다.