10 상속과 코드 재사용

 

01 상속과 중복 코드

 

DRY 원칙

 

코드 중복 여부를 판단하는 기준은 변경이다. 요구사항이 변경됐을 때 두 코드를 함께 수정해야 한다면 이 코드는 중복이라 할 수 있다.

그리고 코드의 모양이 유사하다는 것은 중복의 징후 일 뿐, 결국 중복의 여부를 결정하는 기준은 코드가 변경에 반응하는 방식이다.

수정하기 쉽고 신뢰할 수 있는 소프트웨어를 만들기위해선 DRY(Don't Repeat Yourself) 원칙을 따르면 된다.

 

DRY원칙이란? 모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다.

 


중복과 변경

 

중복 코드 살펴보기

 

중복 코드의 문제점을 이해하기 위해 한달에 한 번씩 가입자별로 전화 요금을 계산하는 애플리케이션을 살펴보자.

 

 

전화 요금을 계산하는 규칙은 통화 시간을 단위 시간당 요금으로 나눠주면된다.

 

이제 통화 요금을 계산할 객체인 Call의 목록을 관리할 정보 전문가 Phone을 작성하자.

 

 

calculateFee 메서드는 amount, seconds, calls를 이용해 전체 통화 요금을 계산한다.

 

다음은 '10초당 5원씩'씩 부과되는 요금제에 가입한 사용자가 각각 1분 동안 두 번 통화를 한 경우의 통화 요금을 계산한 내용이다.

 

 

요구사항은 항상 변하기 때문에 '심야 할인 요금제(10시 이후의 통화에 대해 요금을 할인)'라는 새로운 요금 방식을 추가해야한다는 요구사항이 접수됐다고 가정하자.

 

이 요구사항을 해결하기 위해 쉽고 빠른 방법은 Phone의 코드를 복사해서 NightlyDiscountPohne이라는 새로운 클래스를 생성 후 수정하는 것이다.

 

 

NightlyDiscountPhone은 밤 10시를 기준으로 regularAmount와 nightlyAmount 중에서 기준 요금을 결정한다는 점을 제외하고는 Phone과 거의 유사하다. 위의 방법은 요구사항을 빠르게 처리해주지만 중복 코드가 존재하므로 미래에 지불해야할 비용은 크다.


 

중복 코드 수정하기

 

만약 가입자의 핸드폰별 세율이 서로 다르다고 가정하자.

Phone에 멤버변수로 taxRate를 추가해주어야 할 것이다.

문제는 NightlyDiscountPhone에도 동일하게 추가해주어야한다. 이렇듯 중복코드는 항상 함께 수정되어야하며 하나라도 빠트리면 버그로 이어질 것이다.


 

타입 코드 사용하기

 

두 클래스 사이의 중복을 제거하는 한가지 방법은 클래스를 하나로 합치는 것이다. 그러나 예상한 모습은 클래스안에서 요구되는 타입에 맞게 분기하여 로직이 수행되는 모습일 것이다. 이는 앞선 장에서 강조했던 것처럼 타입 코드를 사용하는 클래스는 낮은 응집도와 높은 결합도라는 문제를 발생시킨다.

 

그렇다면 중복 코드를 관리하는 방법은 무엇이 있을까? 바로 상속이다.

 


상속을 이용해서 중복 코드 제거하기

 

이미 존재하는 클래스와 유사한 클래스가 필요하다면 코드를 복사하지 말고 상속을 이용해서 코드를 재사용하자.

 

 
상속 또한 과연 문제가 없을까?

NightlyDiscountPhone클래스의 calculateFee 메서드를 자세히보면 super 참조를 통해 부모 클래스인 Phone의 calculateFee 메서드를 호출해서 일반 요금제에 따라 통화 요금을 계산한 후 이 값에서 통화 시작 시간이 10시 이후인 통화의 요금을 빼주는 부분이다.

개발자는 Phone의 코드를 최대한 많이 재사용하기 위해 위와 같이 작성했을 것이다. 또한 재사용을 위해 상속 계층 사이에서 무수히 많은 가정을 세웠을지 모른다. 

 

따라서 상속은 결합도를 높이며 코드 또한 수정하기 어렵게 만든다.

 

이처럼 상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 가리켜 취약한 기반 클래스 문제라고 부른다.

 

 

 

02 취약한 기반 클래스 문제

 

취한약 기반 클래스란? 부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상

 

취약한 기반클래스 문제는 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시킨다.

이처럼 상속은 코드의 재사용을 위해 캡슐화의 장점을 희석시키고 구현에 대한 결합도를 높임으로써 객체지향이 가진 강력함을 반감시킨다.

 

상속이 가지는 문제점을 몇가지 살펴보자.

 


 

불필요한 인터페이스 상속 문제

 

자바 초기 버전에 상속을 잘못 사용한 대표적인 사례로 java.util.Stack이 있다.

자바 초기 컬렉션 프레임워크 개발자들은 요소의 Stack을 구현하고자 요소의 추가, 삭제, 오퍼레이션을 제공하는 Vector를 상속하도록 작성했다.

 

 

 

안타깝게도 Stack이 Vectort를 상속받기 때문에 Stack의 퍼블릭 인터페이스에 Vector의 퍼블릭 인터페이스가 합쳐진다. 즉, Stack이 상속받은 퍼블릭 인터페이스를 통해서 임의의 위치에 요소를 추가/삭제할 수 있어 Stack 규칙을 쉽게 위반한다.

-> 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다.

 


메서드 오버라이딩의 오작용 문제

 

InstrumentedHashSet은 HashSet의 내부에 저장된 요소의 수를 셀 수 있는 기능을 추가한 클래로서 HashSet의 자식 클래스로 구현돼 있다.

 

 

 

대부분의 사람들은 위 코드를 실행한 후에 addCount의 값이 3이 될 거라고 예상하지만 HashSet의 addAll메서 안에서 add메서드를 호출 하고 있기 때문에 실제로는 6이된다.

 

이문제를 해결하기 위해선 InstrumentedHashSet의 addAll 메서드를 제거하고 HashSet의 addAll메서드를 사용하는 것이다. 하지만 나중에 HashSet의 addAll메서드가 add메세지를 전송하지 않도록 변경된다면 추가되는 요소들의 카운트가 누락될 것이다.

 

더 좋은 해결책은 InstrumentedHashSet의 addAll메서드를 오버라이딩하고 추가되는 각 요소에 대해 한 번씩 add 메세지를 호출하는 것이다.

 

 

하지만 오버라이딩된 addAll 메서드의 구현이 HashSet의 것과 동일하다는 문제가 생긴다.

 

즉, 클래스가 상속되가 원한다면 상속을 위해 클래스를 설계하고 문서화해야한다. 그렇지 않으면 위와 같은 사례들의 문제가 발생한다.

 

설계는 트레이드오프 활동이며 상속은 코드 재사용을 위해 캡슐화를 희생시킨다. 완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 방법을 모색해야한다.

 

 

 

03 Phone 다시 살펴보기

 

Phone클래스를 다시 살펴보면서 문제를 해결해보자.

 


추상화에 의존하자

 

추상클래스는 구체클래스 보다 변경가능성이 더욱 낮기 때문에 부모클래스 변경으로인한 자식클래스에 끼치는 영향을 최소화 시킬 수 있다.

(부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정해야 한다.)

 


차이를 메서드로 추출하라

 

가장 먼저 할 일은 중복 코드 안에서 차이점을 별도의 메서드로 추출하는 것이다.

서로 유사한 NightlyDiscountPhone과 Phone을 다시 살펴보자.

 

 

 

두 클래스의 메서드에서 다른 부분인 calculateFee를 별도의 메서드로 추출하고 하나의 Call에 대한 통화 요금을 계산하는 것이므로 메서드이름은 calculateCallFee라고 하자.

 

 

 


중복 코드를 부모 클래스로 올려라

 

부모 클래스를 추가하자. 앞서 말했듯이 목표는 모든 클래스들이 추상화에 의존하도록 만드는 것이기 때문에 이 클래스는 추상클래스가 적합하다.

 

 

 

 

리팩터링 후 상속 계층

 


의도를 드러내는 이름 선택하기

 

한 가지 아쉬운 점으로 NightlyDiscountPhone이라는 이름은 심야 할인 요금제와 관련된 내용을 구현한다는 사실을 명확히 전달하지만 그에 반해 Phone은 어떤 할인 요금제와 관련된건지 알 수 없는 추상적인 의미로 전달될 수 있다.

RegularPhone이라는 이름이 적절해 보인다.

 

 

 

 

04 차이에 의한 프로그래밍

 

상속은 코드 재사용과 관련된 대부분의 경우에 우아한 해결 방법이 아니다. 상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은 방법은 바로 '합성'이다.

 

 

 

 

 

 

'📚 Book > Object' 카테고리의 다른 글

12 다형성  (0) 2020.04.12
11 합성과 유연한 설계  (0) 2020.03.29
09 유연한 설계  (1) 2020.03.14
08 의존성 관리하기  (0) 2020.03.08
07 객체 분해  (0) 2020.02.29