Post

상속과 코드 재사용

10. 상속과 코드 재사용

객체지향 프로그래밍의 장점 중 하나는 코드를 재사용하기가 용이하다는 것

전통적인 패러다임에서 코드를 재사용하는 방법은 코드를 복사한 후 수정하는 것

객체지향에서는 코드를 재사용하기 위해 ‘새로운’ 코드를 추가

재사용 관점에서 상속이란 클래스 안에 정의된 인스턴스 변수와 메서드를 자동으로 새로운 클래스에 추가하는 구현 기법

새로운 클래스의 인스턴스 안에 기존 클래스의 인스턴스를 포함시키는 방법 -> 합성

10.1 상속과 중복 코드

DRY 원칙

중복 코드는 변경을 방해 -> 중복 코드를 제거해야 하는 가장 큰 이유

중복 코드가 가지는 가장 큰 문제는 코드를 수정하는 데 필요한 노력을 몇 배로 증가시킨다는 것

중복 여부를 판단하는 기준은 변경

중복 여부를 결정하는 기준은 코드가 변경에 반응하는 방식

신뢰할 수 있고 수정하기 쉬운 소프트웨어를 만드는 효과적인 방법 중 하나는 중복을 제거하는 것

DRY(Don’t Repeat Yourself) 원칙: 동일한 지식을 중복하지 말아라

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

DRY원칙은 한 번, 단 한번(Once and Only Once) 원칙 또는 단일 지점 제어 원칙(Single-Point Control)

중복과 변경

중복 코드 살펴보기

예시로 한 달에 한 번씩 가입자별로 전화 요금을 계산하는 애플리케이션

일반 요금의 요구사항에서 심야 할인 요금 요구사항이 생겨 코드 중복이 생김

코드를 복사하여 구현 시간을 절약한 대가로 지불해야 하는 비용은 예상보다 크다

중복 코드가 존재하기 때문에 언제 터질지 모르는 시한폭탄을 안고 있는 것과 같다

중복 코드 수정하기

세금 추가해야 하는 상황

실수로 한쪽에만 세금 기능을 추가한다면 다른 요금제 가입자에게는 세금이 부과되지 않는 상황 발생 -> 심각한 장애

제대로 수정했어도, 중복 코드를 서로 다르게 수정하기가 쉽다

중복 코드는 새로운 중복 코드를 부른다.

새로운 중복 코드를 추가하는 과정에서 코드의 일관성이 무너질 위험이 항상 도사리고 있다.

더 큰 문제는 중복코드가 늘어날수록 애플리케이션은 변경에 취약해지고 버그가 발생할 가능성이 높아진다는 것

중복 코드의 양이 많아질수록 버그의 수는 증가하며 그에 비례해 코드를 변경하는 속도는 점점 더 느려진다.

타입 코드 사용하기

두 클래스 사이의 중복 코드를 제거하는 한 가지 방법은 클래스를 하나로 합치는 것

구분하는 타입코드를 추가하고 타입 코드의 값에 따라 로직을 분리시킬 수 있음 -> 낮은 응집도와 높은 결합도라는 문제 발생

객체지향 프로그래밍 언어는 타입 코드를 사용하지 않고도 중복 코드를 관리할 수 있는 효과적인 방법 제공 -> 상속

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

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

상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하는 것은 생각처럼 쉽지 않다.

요구사항과 구현 사항의 차이가 크면 클수록 코드를 이해하기 어려워짐

상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다.

따라서 상속은 결합도를 높인다. -> 코드를 수정하기 어렵게 만든다.

강하게 결합된 Phone 과 NightlyDiscountPhone

super 참조 + 오버라이딩으로 인해 결합

세금을 추가할 때, 결국 두 클래스 모두 수정하게 됨

-> 코드 중복을 제거하기 위해 상속을 사용했음에도 새로운 로직을 추가하기 위해 새로운 중복 코드를 만들어야 함

-> 강하게 결합되어 생긴 문제

상속을 위한 경고1

자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합 됨

-> super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라

상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상 -> 취약한 기반 클래스 문제

10.2 취약한 기반 클래스 문제

부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상 -> 취약한 기반 클래스 문제(Fragile Base Class Problem, Brittle Base Class Problem)

취약한 기반 클래스 문제는 상속이라는 문맥 안에서 결합도가 초래하는 문제점을 가리키는 용어

상속은 자식 클래스를 점진적으로 추가해서 기능울 확장하는 데는 용이하지만 높은 결합도로 인해 부모 클래스를 점진적으로 개선하는 것은 어렵게 만든다

취약한 기반 클래스 문제는 캡슐화를 약화시키고 결합도를 높인다.

상속을 사용하면 부모 클래스의 퍼블릭 인터페이스가 아닌 구현을 변경하더라고 자식 클래스가 영향을 받기 쉬워짐

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

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

자바의 초기 버전에서 상속을 잘못 사용한 대표적인 사례는 java.util.Properties와 java.util.Stack

Stack을 Vector의 자식 클래스로 구현 함

Stack이 Vector를 상속받기 때문에 임의의 위치에서 get과 add 가능 -> stack의 규칙 위반

Properties는 키와 값 타입을 오직 String만 가짐

Properties는 HashTable을 상속 받음

자바에 제네릭(generic)이 도입되기 이전에 만들어졌기 때문에 컴파일러카 키와 값의 타입이 String인지 여부를 체크할 수 있는 방법이 없음

상속을 위한 경고2

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

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

InstrumentedHashSet 문제

addAll(), add() 두가지 메서드 중 부모 클래스인 HashSet의 구현을 따라다녀야 함

상속을 위한 경고3

자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.

설계는 트레이드오프. 상속은 코드 재사용을 위해 캡슐화를 희생

부모 클래스와 자식 클래스의 동시 수정 문제

상속을 사용하면 자식 클래스가 부모 클래스의 구현에 강하게 결합되기 때문에 함께 수정해야 할 수 있음

결합도란 다른 대상에 대해 알고 있는 지식의 양

상속은 기본적으로 부모 클래스의 구현을 재사용한다는 기본 전제를 따르기 때문에 자식 클래스가 부모 클래스의 내부에 대해 속속들이 알도록 강요함

따라서 코드 재사용을 위한 상속은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황 역시 빈번하게 발생할 수밖에 없음

상속을 위한 경고4

클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수 밖에 없다.

10.3 Phone 다시 살펴보기

취약한 기반 클래스 문제를 완전히 없앨 수는 없지만 어느 정도까지 위험을 완화시키는 것은 가능 -> 추상화

추상화에 의존하자

부모 클래스와 자식 클래스 모두 추상화에 의존하도록 수정해야 함

코드 중복을 제거하기 위해 상속을 도입할 때 따르는 두 가지 원칙

  • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출
  • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라

차이를 메서드로 추출

중복 코드 안에서 차이점을 별도의 메서드로 추출

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

추상 부모 클래스를 추가

공통 코드를 옮길 때 인스턴스 변수보다 메서드를 먼저 이동시키는 게 편하다.

메서드를 옮기고 나면 그 메서드에 필요한 메서드나 인스턴스 변수가 무엇인지를 컴파일 에러를 통해 자동으로 알 수 있기 때문

메서드의 구현은 그대로 두고 공통 부분인 시그니처만 부모 클래스로 이동시켜야 함

시그니처만 이동시키므로 추상 메서드로 선언하고 자식 클래스에서 오버라이딩할 수 있도록 protected로 선언

자식 클래스들 사이의 공통점을 부모 클래스로 옮김으로써 실제 코드를 기반으로 상속 계층을 구성할 수 있다. -> 추상화에 의존

추상화가 핵심이다

공통 코드를 이동시킨 후에 각 클래스는 서로 다른 변경의 이유를 가짐

단일 책임 원칙을 준수하기 때문에 응집도가 높다.

구체적인 구현에 의존하지 않고, 추상화에만 의존 -> 낮은 결합도

의존성 역전 원칙도 준수

개방-폐쇄 원칙도 준수

상속 계층이 코드를 진화시키는 데 걸림돌이 된다면 추상화를 찾아내고 상속 계층 안의 클래스들이 그 추상화에 의존하도록 코드를 리팩터링

차이점을 메서드로 추출하고 공통적인 부분은 부모 클래스로 이동하라

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

명확하게 전달하는 것이 좋다

세금 추가하기

실제로 더 쉽게 변경가능한지 확인

클래스라는 도구는 메서드뿐만 아니라 인스턴스 변수도 함께 포함

따라서 클래스 사이의 상속은 자식 클래스가 부모 클래스가 구현한 행동뿐만 아니라 변수에 대해서도 결합되게 만든다

인스턴스 변수의 목록이 변하지 않는 상황에서 객체의 행동만 변경된다면 상속 계층에 속한 각 클래스들을 독립적으로 진화시킬 수 있음

하지만 인스턴스 변수가 추가되는 경우는 다르다.

자식 클래스는 자신의 인스턴스를 생성할 때 부모 클래스에 정의된 인스턴스 변수를 초기화해야 하기 때문에 자연스럽게 부모 클래스에 추가된 인스턴스 변수는 자식 클래스의 초기화 로직에 영향을 미침

책임을 아무리 잘 분리하더라도 인스턴스 변수의 추가는 종종 상속 계층 전반에 걸친 변경을 유발

하지만 인스턴스 초기화 로직을 변경하는 것이 코드 중복보다 현명한 선택

객체 생성 로직에 대반 변경을 막기보다는 핵심 로직의 중복을 막아라

핵심 로직은 한 곳에 모아 놓고 조심스럽게 캡슐화해야 한다.

공통적인 핵심 로직은 최대한 추상화해야 한다.

상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없음

메서드 구현에 대한 결합은 추상 메서드를 추가함으로써 어느 정도 완화할 수 있지만 인스턴스 변수에 대한 잠재적인 결합을 제거할 수 있는 방법은 없다.

10.4 차이에 의한 프로그래밍

상속을 사용하면 이미 존재하는 클래스의 코드를 기반으로 다른 부분을 구현함으로써 새로운 기능을 쉽고 빠르게 추가 가능

상속이 강력한 이유는 익숙한 개념을 이용해서 새로운 개념을 쉽고 빠르게 추가할 수 있기 때문

차이에 의한 프로그래밍(programming by difference): 기존 코드와 다른 부분만 추가함으로써 애플리케이션의 기능을 확장하는 방법

차이에 의한 프로그래밍의 목표는 중복 코드를 제거하고 코드를 재사용하는 것

중복을 제거하기 위해서는 코드를 재사용 가능한 단위로 분해하고 재구성해야함

코드를 재사용하기 위해서는 중복 코드를 제거해서 하나의 모듈로 모아야 함

중복 코드를 제거하기 위해 최대한 코드를 재사용해야 함

객체지향 세계에서 중복 코드를 제거하고 코드를 재사용할 수 있는 가장 유일한 방법은 상속

여러 클래스에 공통적으로 포함돼 있는 중복 코드를 하나의 클래스로 모은다.

원래 클래스들에서 중복 코드를 제거한 후 중복 코드가 옮겨진 클래스를 상속 관계로 연결

상속을 이용하면 새로운 기능을 추가하기 위해 직접 구현해야 하는 코드의 양을 최소화 가능

하지만, 맹목적으로 상속을 사용하는 것은 위험함

상속이 코드 재사용이라는 측면에서 매우 강력한 도구인 것은 사실이지만 잘못 사용할 경우 돌아오는 피해 역시 크다

상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만든다.

정말로 필요한 경우에만 상속 사용

상속은 코드 재사용과 관련된 대부분의 경우에 우아한 해결 방법 X -> 주로 상속보다는 합성

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.