Ch10 상속과 코드 재사용
들어가며
앞선 8장, 9장을 통해서 우리는 재사용 가능한 설계
, 유연한 설계
를 위한 의존성 관리 기법
들에 대해 학습하였다.
이번 장에서는 코드 재사용
을 위해 상속
을 활용했을 때 어떤 부작용들이 있는지, 왜 이러한 부작용들이 생기는지에 대해서 학습하고 이러한 부작용을 추상화에 의존
하는 방법을 통해 어떻게 최소화 할 수 있는지 살펴본다.
이번 챕터의 제목이 상속과 코드 재사용
이지만 내용은 코드 재사용을 위해서 상속을 사용하면 좋지 않다. 라는 점을 부각시킨다.
핵심.
-
중복 코드는 변경을 방해한다.
-
중복 코드는 수정과 테스트에 드는 비용을 증가시킨다.
-
중복 여부를 판단하는 기준은
변경
이다. -
요구사항이 변경
되었을 때 두 코드를함께 변경
해야 한다면 이 코드는중복
이다. 함께 수정할 필요가 없다면 중복이 아니다. -
중복 여부를 결정하는 기준은 코드가 변경에 반응하는 방식이다.
-
중복 코드
는 항상 함께 수정해야 하기 때문에 하나라도 빠트리면 버그로 이어진다. -
중복 코드가 늘어날 수록 애플리케이션은 변경에 취약해지고 버그가 발생할 가능성이 높아진다.
-
객체지향 개발에 있어 가장 큰 오해와 안티패턴 중 하나는 코드 재사용(중복 제거)를 위해 상속을 사용하는 것이다.
-
상속
을 염두에 두지 않고 설계된 클래스를상속
을 이용해재사용
하는 것은 생각보다 쉽지 않다. 개발자는재사용
을 위해 상속 계층 사이에 무수히 많은 가정을 세웠을지도 모른다. 그리고 이가정
은 코드를 이해하기 어렵게 만들 뿐만 아니라 직관에도 어긋날 수 있다. -
상속을 이용해 코드를 재사용하기 위해서는 부모 클래스 개발자가 세웠던 가정이나 추론 과정을 정확히 이해해야 한다. 이것은 자식 클래스 개발자가 부모 클래스의 세부 구현에 대해 정확한 지식을 가져야 함을 의미하기 때문에 상속은 결합도를 높인다.
-
상속은 결합도를 높인다. 즉, 자식 클래스는 부모 클래스의 세부 구현에 대해 정확히 알고있어야 한다. 이러한
결합도
가 코드를 수정하기 어렵게 만든다. -
자식 클래스
의 메서드 안에서super
참조를 이용해부모 클래스
의 메서드를 직접 호출하는 경우 두 클래스는 강하게 결합된다. 따라서super
호출을 제거할 수 있는 방법을 찾아 결합도를 낮춰야 한다. -
자식 클래스
가부모 클래스
의 구현에 강하게 결합된 경우부모 클래스
의 변경에 의해자식 클래스
가 영향을 받는다. -
상속 관계로 연결된 자식 클래스가
부모 클래스의 변경에 취약
해지는 현상을 가리켜취약한 기반 클래스 문제
라고 부른다. -
취약한 기반 클래스 문제
는 코드 재사용을 목적으로 상속을 사용할 때 발생하는 가장 대표적인 문제다. -
즉,
코드 재사용을 위해 상속을 사용
하게 되면부모 클래스
의 세부 구현에자식 클래스
가 강하게 결합되기 때문에부모 클래스의 변경
에자식 클래스가 영향
을 받을 가능성이 커진다. -
상속은 자식 클래스와 부모 클래스의 결합도를 높이고 이 결합도로 자식 클래스는 부모 클래스의 불필요한 세부 사항에 엮이게 된다.
-
취약한 기반 클래스 문제
는캡슐화
를 약화시키고결합도
를 높인다. -
상속
은 자식 클래스가 부모 클래스의구현 세부사항에 의존
하도록 만들기 때문에캡슐화
를 약화시킨다. -
객체지향의 기반은
캡슐화를 통한 변경의 통제
다.상속
은코드 재사용
을 위해 캡슐화의 장점을 희석시키고 구현에 대한 결합도를 높인다. -
상속
으로 인해불필요한 인터페이스 상속 문제
가 발생할 수 있다. -
단순히 코드를 재사용하기 위해 불필요한 오퍼레이션이 인터페이스에 스며들도록 방치해서는 안된다.
-
상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨뜨릴 수 있다.
-
상속
으로 인해메서드 오버라이딩의 오작용 문제
가 발생할 수 있다. -
자식 클래스가 부모 클래스의 메서드를 오버라이딩 할 경우 부모 클래스의 메서드 구현 세부사항에 자식 클래스가 결합될 수 있다.
-
코드 재사용
을 위한상속
은캡슐화를 희생
한다. -
완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른 방법을 사용해야 한다.
-
상속
으로 인해 부모 클래스를 수정할 때 자식 클래스도 함께 수정해야 하는 경우가 생길 수 있다. -
상속을 사용하면 자식 클래스가 부모 클래스의 구현에 강하게 결합되기 때문에 동시 수정 문제를 피하기 어렵다.
-
결합도
란다른 대상에 대해 알고 있는 지식의 양
이다. 상속은 기본적으로 부모 클래스의 구현을 재사용한다는 기본 전제를 따르기 때문에 자식 클래스가 부모 클래스의 내부에 대해 자세히 알도록 강요한다. -
결국 “코드 재사용을 위한 상속”은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황이 빈번하게 발생한다.
-
자식 클래스
가부모 클래스
의구현
이 아닌추상화
에 의존하도록 만듦으로써캡슐화 약화
문제를 어느정도 해결할 수 있다. -
부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정하라.
-
차이를 메서드로 추출하라.
-
중복 코드를 부모 클래스로 올려라
-
추상화
가 핵심이다. -
상속 계층이 걸림돌이 된다면 추상화를 찾아내고 상속 계층 안의 클래스들이 그 추상화에 의존하도록 코드를 리팩터링 하라.
-
차이점을 메서드로 추출하고 공통적인 부분은 부모 클래스로 이동하라.
상속과 중복 코드
DRY 원칙
중복 코드는 변경을 방해한다. 이것이 중복 코드를 제거해야 하는 가장 큰 이유다. 프로그램의 본질은 비즈니스와 관련된 로직을 코드로 변환하는 것
이다. 그리고 이 지식은 항상 변하며, 그에 맞춰 지식을 표현하는 코드 역시 변경
시켜야 한다.
중복 코드가 가지는 가장 큰 문제는 코드를 수정하는 데 필요한 노력을 몇 배로 증가시킨다는 것이다. 중복되는 지점을 하나하나 찾아야하고 모든 중복코드를 개별적으로 테스트해서 동일한 결과를 내놓는지 확인해야만 한다. 또한 중복 코드는 수정과 테스트에 드는 비용을 증가
시킨다.
중복 여부를 판단하는 기준은 ‘변경’이다. 어떤 요구사항이 변경되었을 때 두 코드를 함께 수정
해야 한다면 이 코드는 중복
이다. 함께 수정할 필요가 없다면 중복이 아니다. 여기서 말하는 함께 수정
이란 응집도
의 관점이 아님을 유의하자.
중복 여부를 결정하는 기준은 코드의 모양이 아니다. 코드가 변경에 반응하는 방식이다.
중복과 변경
어떤 두 코드가 중복이 되었을 때를 가정해보자. (코드를 이용한 예시는 본문을 확인하자.) 중복 코드가 적용되고 나서 얼마 후 요구사항 변경
이 발생하였다. 개발자는 요구사항을 반영하기 위해 중복 코드
를 하나하나 찾아가며 수정하고 테스트를 해야한다. 다시 말해 하나의 요구사항 변경에 대해서 중복되는 지점을 모두 찾아 수정하고 테스트 하는 비용이 늘어난다.
많은 코드 더미 속에서 어떤 코드가 중복인지를 파악하는 일은 쉬운 일이 아니다. 중복 코드는 항상 함께 수정
돼야 하기 때문에 수정할 때 하나라도 빠뜨린다면 버그
로 이어질 것이다.
중복 코드는 새로운 중복 코드를 부른다. 중복 코드를 제거하지 않은 상태에서 요구사항 변경을 반영할 수 있는 유일한 방법은 새로운 중복 코드를 추가
하는 것 뿐이다. 중복 코드가 생기기 시작하면 점점 늘어나는 악순환에 빠지게 된다.
민첩하게 변경하기 위해서는 중복 코드를 추가하는 대신 제거해야 한다. 기회가 생길 때 마다 코드를 DRY하게 만들도록 노력하라.
타입 코드를 이용한 중복 제거
두 클래스 사이의 중복 코드를 제거하는 한 가지 방법은 클래스를 하나로 합치는 것
이다. 그리고 타입
에 따라 분기
를 함으로써 중복 코드를 제거할 수 있다.
하지만 이 방법은 근본적으로 문제점을 갖고 있다. 앞선 장에서 설명한 바와 같이 타입 코드를 사용하는 클래스는 ‘낮은 응집도’와 ‘높은 결합도’의 문제에 시달리게 된다.
-
낮은 응집도 : 서로 다른 이유로 변경되는 코드가 하나의 클래스 내에 공존하게 된다. 다시 말해 서로 다른 이유로 서로 다른 시점에 변경되는 응집도 낮은 책임을 갖는 클래스가 탄생한다.
-
높은 결합도 : 타입을 구분하기 위해 구체 타입에 대한 정보를 알아야 한다. 다시말해
타입
에 대해 알아야 할 지식이 많아지기 때문에(세부 타입이 캡슐화 되지 않고 노출
) 타입의 종류가 변경되면 파급 효과가 미친다.
객체지향 프로그래밍 언어는 타입 코드
를 사용하지 않고도 중복 코드
를 관리할 수 있는 효과적인 방법을 제공한다. 상속
이 바로 그것이다.
상속을 이용한 중복 제거
상속
의 기본 아이디어는 매우 간단한다. 이미 존재하는 클래스와 유사한 클래스가 필요하다면 코드를 복사하지 말고 상속을 이용해 코드를 재사용하라 는 것이다.
상속
을 이용해서 코드를 재사용하게 되면 어떤 요구사항이 변경되었을 때 하나의 클래스(부모 클래스
)만 수정하면 된다.
상속의 문제점
상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하지 말라.
코드 재사용을 위한 상속은 코드의 품질을 낮춘다.
말로만 들으면 상속
은 중복 제거
를 위해, 즉 코드 재사용
을 위한 완벽한 도구인 것 같다.
결론부터 이야기 하자면 상속은 코드 재사용을 위한 도구가 아니다. 정확히 이야기 하자면 코드(구현) 재사용을 위한 상속은 코드의 품질을 현격히 낮추게 된다.
코드 중복을 제거하기 위해 어떤 클래스를 상속한다고 가정해보자. 자식 클래스
의 개발자는 부모 클래스
의 세부 구현 내용에 대해 정확한 지식을 가져야 한다. 또한 부모 클래스의 코드를 재사용 하기 위해 직관적이지 않은 가정을 세우게 될 수 있다.
단순히 하나의 상속 계층으로 구성된 코드라면 그렇게까지 큰 문제가 되지 않을 수 있다. 하지만 상속 계층이 상당히 깊다면 이 계층에 걸쳐 각 개발자들이 세운 무수한 가정들에 대해 이해해야 하기 떄문에 코드 이해가 쉽지 않을 수 있다.
중요한 것은 개발자의 가정을 이해하기 전에는 코드를 이해하기가 어렵다. 상속
을 염두에 두고 설계되지 않은 클래스를 상속
하여 재사용 하는 것은 생각처럼 쉽지 않다.
더 큰 문제는 자식 클래스가 부모 클래스의 세부 구현에 대해 자세히 알아야 한다는 것이다. 다시 말해 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다. 이것은 자식 클래스의 개발자가 부모 클래스의 구현 방법에 대한 정확한 지식을 가져야 한다는 것을 의미한다.
따라서 상속
은 결합도
를 높인다. 그리고 상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합이 코드를 수정하기 어렵게 만든다.
자식 클래스
가 부모 클래스의 구현에 강하게 결합
될 경우 부모 클래스의 변경에 의해 자식 클래스가 영향
을 받는다.
이처럼 상속 관계
로 연결된 자식 클래스
가 부모 클래스의 변경에 취약
해지는 현상을 가리켜 취약한 기반 클래스 문제
라고 부른다.
취약한 기반 클래스 문제는 코드 재사용을 목적으로 상속을 사용할 때 발생하는 가장 대표적인 문제다.
취약한 기반 클래스 문제
상속은 자식 클래스와 부모 클래스를 강하게 결합되게 만든다. 이 강한 결합도로 인해 자식 클래스는 부모 클래스의 불필요한 세부사항에 엮이게 된다. 부모 클래스의 작은 변경에도 자식 클래스는 컴파일 오류와 실행 에러라는 고통에 시달려야 할 수도 있다.
이처럼 부모 클래스의 변경에 의해 자식 클래스가 영향
을 받는 현상을 취약한 기반 클래스 문제
라고 일컫는다. 이 문제는 상속을 사용한다면 피할 수 없는 객체지향 프로그래밍의 근본적인 취약성이다.
구현을 상속(extends 사용)
한 경우 파생 클래스는 기반 클래스에 강하게 결합
되며, 이 둘 사이의 연결은 바람직하지 않다. 만일 기반 클래스
를 수정하게 되는 경우 새로운 행동이 파생 클래스에게 상속될 경우 파생 클래스의 잘못된 동작을 초래 할 수 있기 때문에 기반 클래스는 취약
하다.
‘취약한 기반 클래스 문제’는 상속에 따라 발생하는 문제인 결합도를 가리키는 용어다. 즉 상속에 따른 결합도
가 취약한 기반 클래스 문제
다.
상속 관계를 추가할수록 전체 시스템의 결합도가 높아진다는 사실을 알고있어야 한다. 상속은 자식 클래스를 점진적으로 추가해서 기능을 확장
하는데는 용이하지만 높은 결합도
로 인해 부모 클래스를 점진적으로 개선하는 것은 어렵게
만든다.
취약한 기반 클래스
문제는 캡슐화를 약화
시키고 결합도를 증가
시킨다. 상속은 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시키고 결합도를 증가시킨다.
우리가 객체
를 사용하는 이유는 구현
과 관련된 세부사항을 퍼블릭 인터페이스 뒤로 캡슐화
할 수 있기 떄문이다. 캡슐화
는 변경에 의한 파급효과를 제어
할 수 있기 때문에 가치가 있다. 객체는 변경될지도 모르는 불안정한 요소를 캡슐화 함으로써 파릅효과를 걱정하지 않고 자유롭게 내부를 변경할 수 있다.
안타깝게도 상속
을 사용하면 부모 클래스의 퍼블릭 인터페이스
가 아닌 구현
을 변경하더라도 자식 클래스가 영향
을 받기 쉬워진다.
개체지향의 기반은 캡슐화를 통한 변경의 통제다. 코드 재사용을 위한 상속
은 캡슐화의 장점을 희석
시키고 구현에 대한 결합도를 증가
시킴으로써 객체지향이 가진 강력함을 반감시킨다.
이렇게 상속
이 가진 캡슐화 약화
로 인해 발생하는 문제점에 대해서 살펴보자.
-
불필요한 인터페이스 상속 문제
-
메서드 오버라이딩 오작용 문제
-
부모 클래스와 자식 클래스의 동시 수정 문제
불필요한 인터페이스 상속 문제
상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨뜨릴 수 있다.
자바 초기 버전에서 상속
을 잘못 사용한 대표적인 사례는 java.util.Properties
와 java.util.Stack
이다. 두 클래스의 공통점은 부모 클래스에서 상속받은 메서드를 사용할 경우 자식 클래스의 규칙이 위반될 수 있다는 것이다.
먼저 Stack
을 살펴보자. Stack
은 LIFO
구조의 자료구조를 구현한 클래스다. Vector
는 임의의 위치에서 요소를 추출 및 삽입할 수 있는 리스트 자료구조의 구현체로써 List
의 초기 버전이라고 할 수 있다. 자바의 초기 컬렉션 프레임워크의 개발자들은 요소의 추가, 삭제 오퍼레이션을 제공하는 Vector를 재사용
하기 위해서 Stack
을 Vector의 자식 클래스
로 구현하였다.
이 때문에 Stack
은 Stack
으로써의 규칙을 위반하는 인터페이스를 갖게 된다. Stack
은 put
, pop
을 이용한 값의 삽입 및 추출만을 허용해야 한다는 규칙을 가지는데, Vector
의 퍼블릭 인터페이스가 스며드는 바람에 임의의 위치에서 값을 삽입하고 삭제할 수 있게 된다.
문제의 원인은 Stack
의 규칙을 무너뜨릴 여지가 있는 위험한 Vector
의 퍼블릭 인터페이스까지도 함께 상속받았기 때문이다. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 만들어야 한다.
또 다른 예로 java.util.Properties
를 살펴보자. 이 역시 잘못된 유산을 물려받는 클래스이다. Properties
클래스는 키/값의 쌍을 보관한다는 점에서 Map
과 유사하지만 다양한 타입을 저장할 수 있는 Map
과 달리 키/값의 타입으로 오직 String
만 가질 수 있다.
하지만 Properties
는 HashTable
을 상속받기 때문에 put
메서드를 사용하면 String
타입 이외의 키와 값이라도 저장할 수 있다.
Properties
의 getProperty()
메서드는 반환할 값의 타입이 String
이 아닌 경우 null
을 반환하도록 구현되어 있기 때문에 위 코드는 테스트를 통과하지 못한다.
Stack과 Properties의 예는 퍼블릭 인터페이스에 대한 고려 없이 단순히 코드 재사용을 위해 상속을 이용하는 것이 얼마나 위험한지를 잘 보여준다.
객체지향의 핵심은 객체들의 협력
이다. 단순히 코드를 재사용하기 위해 불필요한 오퍼레이션이 인터페이스에 스며들도록 방치해서는 안된다.
메서드 오버라이딩의 오작용 문제
자식 클래스가 부모 클래스의 메서드를 오버라이딩 할 경우 부모 클래스의 메서드 구현 세부사항에 대해 자식 클래스가 결합될 수 있다.
여기 한 가지 예를 살펴보자. HashSet
의 구현에 강하게 결합된 InstrumentedHashSet
클래스이다. InstrumentedHashSet
은 HashSet
의 내부에 저장된 요소의 수를 셀 수 있는 기능을 추가한 클래스로서 HashSet
의 자식 클래스로 구현되어 있다.
이제 아래와 같은 코드를 실행해보자.
대부분의 사람들은 위 코드를 실행한 후 addCount
의 값이 3
이 될 거라고 예상할 것이다. 하지만 실제로 실행한 후의 addCount
의 값은 6
이다. 그 이유는 부모 클래스인 HashSet
이 addAll
메서드 안에서 add
메서드를 호출하기 때문이다.
InstrumentedHashSet.addAll()
이 호출되어addCount
에3
이 더해진다.super.addAll()
메서드가 호출되고 제어는 부모 클래스인HashSet
으로 이동한다.HashSet
은 각각의 요소를 추가하기 위해add
메서드를 호출하고, 다시 제어는InstrumentedHashSet
으로 넘어간다.InstrumentedHashSet.add()
가 총3
번 호출되어addCount
에 또3
이 더해져 총6
이 된다.
이처럼 코드 재사용
을 위해 상속
을 사용할 경우, 부모 클래스
의 구현 세부사항에 대해 자세히 알아야 하는 캡슐화 약화
를 맞게 된다.
조슈아 블로치는 클래스가 상속되기를 원한다면 상속을 위해 클래스를 설계하고 문서화해야 하며 그렇지 않은 경우엔 상속을 금지시켜야 한다고 주장한다.
지금 블로치는 우리에게 상속을 사용할 경우 내부 구현을 문서화
하라고 말하고 있다. 객체지향의 핵심이 구현을 캡슐화
하는 것인데도 이렇게 내부 구현을 문서화
하는 것이 옳은가?
API 문서는 메서드가 무엇(what)을 하는지를 기술해야 하고, 어떻게(how) 하는지를 설명해서는 안된다. 하지만 결국 상속이 캡슐화를 위반하기 때문에 초래된 문제이다.
설계는 트레이드오프
활동임을 기억하라. 상속은 코드 재사용을 위해 캡슐화를 희생한다.
부모 클래스와 자식 클래스의 동시 수정 문제
상속
을 사용하면 자식 클래스
가 부모 클래스의 구현에 강하게 결합
되기 때문에 동시 수정
문제는 피하기가 어렵다.
결합도
란 다른 대상에 대해 알고있는 지식의 양
이다. 상속은 기본적으로 부모 클래스의 구현을 재사용한다는 기본 전제를 따르기 때문에 자식 클래스가 부모 클래스의 내부에 대해 속속들이 알도록 강요한다. 따라서 코드 재사용을 위한 상속은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황 역시 빈번하게 발생할 수 밖에 없는 것이다.
조슈아 블로치는 이 문제에 대해서 다음과 같이 조언한다.
서브 클래스는 올바른 기능을 위해 슈퍼 클래스의 세부적인 구현에 의존한다. 슈퍼 클래스의 작성자가 확장될 목적으로 클래스를 설계하지 않았다면 서브 클래스는 슈퍼 클래스의 변경에 발맞춰 같이 변경해야 하는 동시 수정 문제가 발생할 수 밖에 없다.
추상화에 의존
지금까지 상속
이 초래하는 문제에 대해 살펴보았다. 코드 재사용
을 위한 상속
은 자식 클래스
가 부모 클래스
의 세부 구현에 대해 자세히 알아야 하는 강한 결합도
로 인해 캡슐화 약화
문제를 피할 수 없다. 이로 인해 부모 클래스
의 사소한 변경 하나에도 자식 클래스
는 이리저리 휘둘릴 수 밖에 없다.
이렇게 상속
으로 발생하는 피해를 최소화 할 수 있는 열쇠는 바로 추상화다.
추상화에 의존하자.
자식 클래스
가 부모 클래스
의 구현이 아닌 추상화에 의존
하도록 만드는 것이 해결책이다. 정확하게 말하자면 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정해야 한다.
코드 중복을 제거하기 위한 상속의 도입
을 위해 다음과 같이 두 가지 원칙을 제시한다.
-
두 메서드가 유사하게 보인다면
차이점을 메서드로 추출
하자.메서드 추출
을 통해 두 메서드를동일한 형태
로 보이도록 만들 수 있다. -
부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라. 부모 클래스의 구체적인 메서드를 자식 클래스로 내리는 것 보다.(
자식 클래스가 상속을 통해 부모 클래스의 코드를 재사용
) 자식 클래스의추상적인 메서드
를 부모 클래스로 올리는 것이재사용성
과응집도
측면에서 더 뛰어난 결과를 얻을 수 있다.
아래에서 이 두 가지 원칙을 자세히 살펴보자.
차이를 메서드로 추출하라.
가장 먼저 할 일은 중복 코드 안에서 차이점을 별도의 메서드로 추출하는 것이다. 이것은 흔히 말하는 변하는 것으로부터 변하지 않는 것을 분리하라.
또는 변하는 부분을 식별하여 이를 캡슐화하라
라는 조언을 메서드 수준에 적용
한 것이다.
중복 코드를 가진 Phone
과 NightlyDiscountPhone
클래스에서 시작하자.
먼저 할 일은 두 클래스의 메서드에서 다른 부분을 별도의 메서드로 추출
하는 것이다. 이 경우에는 calculateFee
의 for
문 안에 구현된 개별 Call
요금 계산 로직이 서로 다르다는 사실을 알 수 있다. 이 부분을 동일한 이름을 가진 메서드
로 추출하자. 하나의 Call
에 대한 통화 요금을 계산하는 것이므로 메서드의 이름은 CalculateCallFee
로 하자
이제 두 클래스의 calculateFee
메서드는 완전히 동일해졌고 서로 다른 부분은 추출하여 calculateCallFee
로 격리시켜 놓았다. 이제 같은 코드를 부모 클래스로 올리는 일만 남았다.
중복 코드를 부모 클래스로 올려라.
부모 클래스를 추가
하자. 목표는 모든 클래스들이 추상화에 의존하도록 만드는 것이기 때문에 추상 클래스로 구현하는 것이 적합할 것이다. 그 다음 중복 코드(공통 코드)
를 부모 클래스로 올리자.
위 코드를 살펴보면 중복 코드
인 calculateFee
메서드를 부모 클래스로 올렸음을 알 수 있다. 하지만 calculateCallFee
의 경우 시그니처
만 이동시켰는데 이는 Phone
과 NightlyDiscountPhone
의 calculateCallFee
메서드가 시그니처는 동일하지만 서로 다른 내부 구현
을 갖기 때문이다. 따라서 메서드의 구현은 그대로 두고 공통 부분인 시그니처만 부모 클래스로 이동시켜야 한다.
공통 코드(calculateFee 메서드, calculateCallFee 시그니처)
를 모두 부모 클래스
로 이동시켰다. 이제 Phone
은 일반 요금제를 처리하는데 필요한 인스턴스 변수와 메서드만 존재
하고 NightlyDiscountPhone
은 심야 할인 요금제와 관련된 인스턴스 변수와 메서드만 존재
한다.
지금까지 살펴본 것 처럼 자식 클래스를 부모 클래스로 옮김
으로써 실제 코드를 기반으로 상속 계층을 구성할 수 있다. 이제 우리의 설계는 추상화에 의존하게 된다.
추상화가 핵심이다.
공통 코드를 부모 클래스로 이동시킨 이후에는 각 클래스는 서로 다른 변경의 이유를 갖는다는 점에 주목하자. AbstractPhone
은 전체 통화 목록을 계산하는 방법이 바뀔 경우에만 변경된다. Phone
은 일반 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다. NightlyDiscountPhone
은 심야 할인 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다. 세 클래스는 각각 하나의 변경 이유만을 가진다. 이 클래스는 단일 책임 원칙
을 준수하기 때문에 응집도
가 높다.
Phone
과 NightlyDiscountPhone
은 부모 클래스인 AbstractPhone
의 구체적인 구현에 의존하지 않는다. 오직 추상화에만 의존한다. 정확하게는 부모 클래스에서 정의한 추상 클래스인 ‘calculateCallFee’에만 의존한다. 이는 자식 클래스가 오직 추상 메서드인 calcualteCallFee
를 구현해야 한다는 사실만 알면 된다. (부모 클래스의 세부 구현 사항을 모두 알 필요가 없다.)
이제 calculateCallFee
의 시그니처가 변경되지 않는 한 부모 클래스의 내부 구현이 변경되더라도 자식 클래스는 영향을 받지 않는다. 이 설계는 낮은 결합도를 유지하고 있다.
사실 부모 클래스 역시 자신의 내부에 구현된 추상 메서드(calculateCallFee)를 호출하기 때문에 추상화에 의존한다고 말할 수 있다. 때문에 의존성 역전 원칙
도 준수하는데, 요금 계산과 관련된 상위 수준의 정책을 구현하는 AbstractPhone
이 세부적인 요금 계산 로직을 구현하는 Phone과 NightlyDiscountPhone
에 의존하지 않고 그 반대로 Phone과 NightlyDiscountPhone이 추상화인 AbstractPhone에 의존하기 때문이다.
새로운 요금제를 추가하기도 쉽다. 새로운 요금제가 필요하다면 AbstractPhone
을 상속받는 새로운 클래스를 추가한 후 calcualteCallFee
만 오버라이딩 하면 된다. 다른 클래스들을 수정할 필요가 없다. 현재의 설계는 확장에는 열려있고 수정에는 닫혀있기 때문에 개방-폐쇄 원칙
역시 준수한다.
지금까지 살펴본 모든 장점은 클래스들이 추상화에 의존하기 때문에 얻어지는 장점이다. 상속 계층이 코드를 진화시키는데 걸림돌이 된다면 추상화
를 찾아내고 상속 계층 안의 클래스들이 추상화에 의존하도록 코드를 리팩터링
하라.
또한 차이점(세부 구현)을 메서드로 추출
하고 공통적인 부분(중복/공통 부분)은 부모 클래스로 이동
하라
참고 및 출처
- 오브젝트