Skip to content

리팩터링을 연습해보자 (with. 마틴 파울러)

Notifications You must be signed in to change notification settings

woo-wook/refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

66 Commits
 
 
 
 
 
 

Repository files navigation

refactoring

1. 기본

코드 조각을 찾아서 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙이기

메서드의 본문이 메서드의 명칭 만큼 명확한 경우에는 분리 된 메서드를 인라인한다.

표현식이 너무 복잡해서 이해하기 어려울 때 지역변수를 활용하여 표현식을 쪼개 관리하기 더 쉽게 만든다. 현재 함수 안에서만 의미가 있는 값일 때 변수로 추출하고, 함수를 벗어나서 넓은 문맥에서 의미가 있다면 함수로 추출하는것이 좋다.

이름이 원래 표현식과 다를 바 없을 때는 주변 코드를 리팩터링할 때 방해가 될 수 있다. 이럴 때 변수를 인라인한다.

메서드의 이름은 연결부에서 가장 중요한 요소이다. 이름이 좋으면 구현 코드를 보지 않고도 무슨 일을 하는지 명확히 알 수 있다. 매개변수도 마찬가지다. 예컨대 전화번호 포매팅 함수가 매개변수로 사람 객체를 받게 된다면, 회사 전화번호에는 사용할 수 없다. 번호 자체를 받도록 정의하면 함수의 활용 범위를 넓힐 수 있다.

리팩터링은 결국 프로그램의 요소를 조작하는 일. 메서드는 데이터보다 다루기가 수월하다. 데이터는 메서드보다 다루기가 어려운데, 메서드 선언 바꾸기와 같은 방식으로 전달 메서드를 만들기가 어렵기 때문이다. 접근할 수 있는 범위가 넓은 데이터는 데이터를 독점하는 함수를 만드는게 좋다(Getter) 또 데이터를 변경(Setter)하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어준다.

데이터 항목 여러개가 이 메서드에서 저 메서드로 몰려다니는 경우가 자주 있다. 이런 데이터 구조를 하나로 모아주자. 데이터를 데이터 구조로 묶으면 데이터 관계가 명확해진다. 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기에 일관성도 높다.

메서드 호출 시 매개변수로 전달되는 공통 데이터를 중심으로 긴밀하게 엮여 작동하는 메서드 목록이 있다면, 클래스 하나로 묶는다. 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있다.

메서드 호출 시 매개변수로 전달되는 공통 데이터를 중심으로 긴밀하게 엮여 작동하는 메서드 목록이 있다면, 클래스 하나로 묶는다. 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있다.

2. 캡슐화

레코드는 여러가지 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미있는 단위로 전달이 가능하다. 하지만 레코드는 단점이 있다. 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해서 저장해야 한다. 이로 인해 레코드보다 객체를 사용하자. 객체를 사용하면 어떻게 저장했는지를 숨긴 채 각각의 메서드로 값을 제공할 수 있다.

컬렉션을 직접 변경할 수 있으면 눈치채지 못하는 곳에서 컬렉션의 원소들이 바뀔 수 있다. 이러한 문제를 방지하기 위해 컬렉션을 변경하는 메서드를 만든다.

단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하자. 나중에 특별한 동작이 필요해지면, 이 클래스에 추가하면 되니 프로그램이 커질수록 점점 유용한 도구가 된다.

함수 안에서 어떤 코드의 결과값을 뒤에서 다시 참조할 목적으로 임시 변수를 사용하기도 한다. 임시 변수를 사용하면 값을 계산하는 코드가 반복되는것을 줄일 수 있어서 유용하다. 하지만 한걸음 더 나아가 함수로 만들어 사용하는 편이 나을 때가 많다. 변수 대신 함수로 만들어 두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.

메서드와 데이터가 너무 많은 클래스는 이해하기가 어려우니 잘 살펴보고 분리하는 것이 좋다. 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.

더 이상 제 역할을 못하는 클래스는 인라인 하자. 역할을 옮기는 리팩터링을 하고 나서 특정 클래스에 남은 역할이 거의 없을 때 이런 현상이 자주 생긴다. 두 클래스의 기능을 다르게 분배하고 싶을 때에도 클래스를 인라인 한 이후 다시 추출하면 더 쉬운 경우가 많다.

필드가 가리키는 객체의 메서드를 호출하면, 클라이언트는 위임받은 객체를 알아야 한다. 위임 객체의 인터페이스가 변경되면 이 인터페이스를 사용하는 모든 클라이언트가 코드를 수정해야 한다. 이러한 의존성을 없애기 위해 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨기면, 위임 객체가 수정되어도 객체만 수정하면 각 클라이언트는 영향을 받지 않는다.

클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 객체에 위임 메서드를 추가하게 되면, 단순히 전달만 하는 위임 메서드가 점점 많아진다. 그러다 보면 결국 중개자로 전락하기에 위임 객체를 직접 호출하는 게 나을 수 있다. 어느 정도로 숨겨야 할지 트레이드 오프가 필요하다.

목적을 달성하는 방법은 여러가지가 존재한다. 그 중에서도 다른 것보다 더 쉬운 방법이 존재한다. 알고리즘도 마찬가지다. 더 간명한 방법을 찾아내면 코드를 간명한 방식으로 고친다. 리팩터링은 복잡한 대상을 단순한 단위로 나눌 수 있지만, 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.

3. 기능 이동

어떤 함수가 자신이 속한 모듈보다 다른 모듈의 요소를 더 많이 참조한다면, 다른 모듈로 옮겨줘야 마땅하다. 이렇게 하면 캡슐화가 좋아져서, 이 소프트웨어의 나머지 부분은 다른 모듈의 세부사항에 덜 의존한다. 비슷하게 호출자들의 현재 위치나 다읍 업데이트에 바뀌리라 예상하는 위치에 따라서도 옮겨줘야 할 수 있다.

현재 데이터 구조가 적합하지 않음을 알게 되면 곧바로 수정하자. 고치지 않고 남은 것들은 추후 머리속을 혼란스럽게 한다. 예를 들어 함수에 어떤 레코드를 넘길 때 마다 또 다른 레코드의 필드도 같이 넘기고 있다면, 데이터의 위치를 옮기는것이 좋다. 또한 한 레코드를 변경할 때 다른 레코드의 필드도 같이 변경하여야 한다면 필드의 위치가 잘못 되었다는 신호이다.

중복 제거는 코드를 건강하게 한다. 이렇게 해두면 추후 반복되는 부분에서 무언가를 수정할 때 단 한곳만 수정하면 된다.

초기에는 응집도가 높았던 메서드가 어느새 둘 이상의 다른 일을 수행하도록 바뀔 수 있다. 개발자는 동작이 달라진 함수에서 꺼내 해당 호출자로 옮겨줘야 한다.

메서드는 여러 동작을 하나로 묶는다. 메서드 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 메서드를 활용하면 코드를 이해하기 쉬워진다. 또한 함수는 중복을 없애는데도 효율적이다. 이미 존재하는 메서드와 동일한 일을 하는 인라인 코드를 발견하면 보통은 해당 코드를 메서드 호출로 대체하자. 이름을 잘 지었다면 인라인 코드 대신에 메서드를 넣어도 말이 된다. 이것이 맞지 않다면, 이름이 적절하지 않거나 그 함수의 목적이 다르기 때문 일 것이다. 가능하다면 라이브러리가 제공하는 메서드로 대체하는 것이 훨씬 좋다. 메서드 본문조차 작성할 필요가 없다.

관련된 코드들이 가까이 모여 있으면 이해하기가 더 쉽다. 다른 데이터를 사용하는 사이에 흩어져 있기 보다는 한곳에 모여 있어야 좋다. 함수로 추출하는 리팩터링을 적용하기에 앞서 선행되는 경우가 많다.

반복문 하나에서 두가지 일을 수행하는 경우가 종종 있다. 그저 두 일을 한번에 처리할 수 있다는 이유에서 말이다. 이렇게 하면 하나의 반복문을 수정해야 할 때 마다 두가지 일을 모두 잘 이해하고 진행해야 한다. 하지만 반복문을 쪼개면 수정할 동작 하나만 이해하면 된다. 반복문을 두 번 실행해야 하므로 불편해 하는 프로그래머도 많다. 하지만 리팩토링과 최적화는 구분해야 한다. 최적화를 할 때 리팩토링으로 코드를 깔끔히 정리 한 이후에 최적화를 수행하자. 다시 하나로 합취는건 매우 쉬운 일이다. 또한, 반복문을 두번 실행해도 병목현상이 일어나는 경우는 매우 드물다.

객체를 순회할 때 반복문을 사용하라고 배웠다. 하지만 언어는 점점 더 나은 구조를 제공하는 쪽으로 발전해왔다. 이번 이야기의 주인공인 컬렉션 파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다. 이때 각 연산은 컬렉션을 입력받아 다른 컬렉션을 내뱉는다. 대표적인 연산으로 map, filter가 있다. 논리를 파이프라인으로 표현하면 이해하기가 훨씬 쉬워진다.
(Java 8+ 부터 stream API를 지원한다.)

4. 데이터 조직화

변수는 다양한 용도로 쓰인다. 그 중 변수에 값을 여러번 대입할 수 밖에 없는 경우도 있다. 예컨데 반복문 안에 있는 루프 변수는 반복문을 한번 돌 때 마다 값이 바뀐다. 그 외에도 변수는 긴 코드의 결과를 저장했다가 나중에 쉽게 참조하려는 목적으로 흔히 쓰인다. 이런 변수에는 값을 단 한번만 대입해야 한다. 대입이 두번 이상 이뤄진다면 여러가지 역할을 수행한다는 신호다. 역할이 두개 이상 있는 변수는 쪼개야한다. 예외는 없다.
(변수는 되도록 불변 상태로 생성하자)

이름은 중요하다. 특히 프로그램 곳곳에서 쓰이는 레코드 구조체의 이름은 더 중요하다. 데이터 구조는 프로그램을 이해하는 큰 역할을 한다. 데이터 구조가 중요한 만큼 반드시 깔끔하게 관리해야 한다. 다른 요소와 마찬가지로 개발을 진행할수록 데이터를 더 잘 이해하게 된다. 보다 더 적합한 이름이 생각나면 이름을 바꾸자.

가변 데이터는 소프트웨어에 문제를 일으키는 가장 큰 골칫거리에 속한다. 가변 데이터는 서로 다른 두 코드를 이상한 방식으로 결합하기도 한다. 가변 데이터를 완전히 배제하기란 불가능하지만, 가변 데이터의 유효 범위를 가능한 좁혀야 한다. 효과가 가장 좋은 방법은 값을 쉽게 계산해 낼 수 있는 변수를 모두 제거하자. 계산 과정을 보여주는 코드 자체가 데이터의 의미를 더 분명히 드러내는 경우가 많다.

객체를 다른 객체에 중첩하면 내부 객체를 참조 호는 값으로 취급할 수 있다. 참조냐 값이냐의 차이는 내부 객체의 속성을 갱신하는 방식에서 가장 극명하게 드러난다. 참조로 다루는 경우에는 대부 객체는 그대로 둔 채 그 객체의 속성만 갱신하며, 값으로 다루는 경우에는 새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체한다. 필드를 값으로 다룬다면 내부 객체의 클래스를 수정하여 값 객체로 만들 수 있다. 값 객체는 대체로 자유롭게 활용하기 좋다(불변)

하나의 데이터 구조 안에 논리적으로 똑같은 제 3의 데이터 구조를 참조하는 레코드가 여러개 있을 때가 있다. 이때 데이터를 참조로도, 값으로도 다룰 수 있다. 논리적으로 가장 문제가 되는 경우는 그 데이터를 갱신할 때 이다. 모든 복제본을 찾아서 다 갱신해야하며, 하나라도 놓치면 데이터 일관성이 깨진다. 이런 상황이라면 데이터를 참조로 관리하는것이 좋다.

매직 리터럴이란 소스코드에 등장하는 일반적인 리터럴 값을 의미한다. 예를 들어 움직임을 계산하는 코드라면 9.81이라는 숫자가 산재해 있을 것이다.(표준 중력을 의미함)
하지만 코드를 읽는 사람이 이 의미를 모른다면 숫자 자체로는 명확히 모르므로 매직 리터럴이라 할 수 있다. 코드 자체가 뜻을 분명하게 드러내도록 상수를 정의하고 상수를 사용하도록 바꾸자.

5. 조건부 로직 간소화

복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가장 흔한 원흉에 속한다. 긴 함수는 그 자체로 읽기도 어렵지만, 조건문은 그 어려움을 한층 더 가중시킨다. 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 무슨일이 일어나는지는 말해주지만, 왜 일어나는지는 말해주지 않을 때가 많다. 거대한 코드 블록이 주어지면 코드를 부위별로 분해 한 다음 코드 덩어리를 각 의도를 살린 함수의 호출로 바꾸자, 무엇을 분기했는지가 명백해진다.

비교하는 조건은 다르지만 그 결과로 수행하는 동작은 똑같은 코드들이 더러 있는데 어차피 같은 일을 할 거라면 조건 검사도 하나로 통합하자. 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 명확해진다. 또한, 통합하면 함수 추출하기까지 이어질 가능성이 매우 높다.
하지만, 같은 결과를 반환하더라도 독립적인 비교 조건인 경우 조건식을 통합해서는 안된다.

조건문은 주로 두가지 형태로 쓰인다. 참인 경로와 거짓인 경로가 모두 정상 동작인 경우와 한쪽만 정상인 형태다. 두 형태는 의도하는 바가 다르므로 코드에 의도가 드러나야 한다. 두 경로 모두 정상 동작이라면 if-else 구문을 사용하고, 한쪽만 정상이라면, if에서 비정상 조건을 검사 한 이후 함수에서 빠져나온다. 이러한 검사 형태를 보호 구문 이라고 한다.

복잡한 조건부 로직은 프로그래밍에서 가장 해성하기 난해한 대상에 속한다. 조건부 로직을 직관적으로 구조화 하는 방법이다. 흔한 예로 타입을 여러개 만들고 각 타입이 조건부 로직을 자신만의 방식으로 처리하도록 구성하는 방법이 있다. switch문이 포함된 함수가 여러개 보인다면 분명 이러한 상황이다. 이런 경우 케이스별로 클래스를 만들어 공통 스위치 로직의 중복을 없앨 수 있다.

데이터 구조의 특정한 값을 확인 한 후 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우가 종종 있다. 흔히 볼 수 있는 중복 코드 중 하나다. 이처럼 코드 베이스에서 특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 그 반응들을 한데로 모으는 게 효율적이다. 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴이라는 것이 있는데. 바로 이럴때 적용하기 좋은 메커니즘이다.

특정 가정이 코드에 항상 명시적으로 기술되어 있지 않아 알고리즘을 보고 연역해서 알아내야 할때가 있다. 이럴 때 어서션으로 코드에 삽입해놓으면, 어서션이 실패했다는 것으로 프로그래머가 잘못했다는 것을 의미한다. 어서션은 프로그래밍 기능의 정상 동작에 대해 영향을 주지 않아야 한다. 테스트를 잘 작성해놓으면 디버깅으로써의 효용성은 줄어들지만, 개발자들끼리의 소통 측면에서는 어서션이 매력적이다.
(Java 1.4+ 부터 assert keyword를 제공합니다. 해당 검증이 실패할 경우 Exception이 아닌 Error를 반환합니다. 또한 assert가 동작하기 위해서는 java 실행 시 -ea 옵션을 필요로 합니다.)

제어 플래그는 코드의 동작을 변경하는 데 사용하는 변수를 의미한다. 이런 코드는 리팩터링으로 충분히 간소화가 가능하다. 제어 플래그의 주 서식지는 반복문 안이다. return을 하나로만 사용하려고 하거나, break, continue 사용이 익숙하지 않은 사람이 심어놓기도 한다.

6. API 리팩터링

외부에서 관찰할 수 있는 겉보기 부수 효과가 전혀 없이 값을 반환해주는 함수를 추구해야 한다. 이런 함수는 어느 때건 원하는 만큼 호출해도 아무 문제가 없다. 호출하는 문장의 위치를 어디로든 옮겨도 되고 테스트 하기도 쉽다. 겉보기 부수 효과가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋다. 이를 위한 방법이 '질의 함수는 모두 부수효과가 없어야 한다'라는 규칙을 따르는 것 이다. 이를 명령-질의 분리(Commend-Query Separation)라고 한다.

두 함수의 로직이 아주 비슷하고 리터럴 값만 다르다면, 그 다른 값만 매개 변수로 받아 처리하는 함수 하나로 합쳐서 중복을 없앨 수 있다. 이렇게 하면 매개변수 값만 바꿔서 여러 곳에서 쓸 수 있으니 매우 유용하다.

플래그 인수란 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다. 플래그 인수는 호출할 수 있는 함수가 무엇이고, 어떻게 호출해야 하는지를 이해하기 어렵게 만든다. 심지어 불리언 형태의 플래그는 코드를 읽는 이에게 뜻을 온전히 전달하지 못한다. 플래그 인수를 제거하면 코드가 깔끔해지고, 프로그래밍 도구에도 도움을 준다.

하나의 레코드에서 값 두어개를 가져와서 인수로 넘기는 코드가 있으면, 그 값들 대신 레코드를 통째로 넘기고 함수 본문에서 필요한 값을 꺼내도록 수정한다. 레코드를 통째로 넘기면 변화에 대응하기가 쉽다. 그 함수가 더 다양한 데이터를 사용하도록 바뀌어도 매개변수 목록을 수정 할 필요가 없다. 하지만 함수가 레코드 자체에 의존하기를 원치 않을 때는 이 리팩터링을 수행해서는 안된다.

매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다. 즉, 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다. 다른 코드와 마찬가지로 이 목록에서도 중복은 피하는게 좋으며 짧을수록 이해하기 쉽다. 피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개 변수로 건네는 것도 일종의 중복이다. 이런 함수를 호출할 때 매개변수의 값은 호출자가 정하게 되는데 이 결정은 사실 하지 않아도 되는 일이다. 매개변수가 있다면 결정 주체가 호출자가 되고, 매개변수가 없다면 피호출 함수가 된다. 습관적으로 호출하는 쪽을 간소하게 만들자. 물론 피호출 함수가 그 역할을 수행하기에 적합할 때만 그렇게 하자.

코드를 읽다 보면 함수 안에 두기에 거북한 참조를 발견할 때가 있다. 전역변수를 참조한다거나, 제거하기 원하는 원소를 참조하는 경우가 여기에 속한다. 이 문제는 해당 참조를 매개변수로 바꾸어 처리할 수 있다. 똑같은 값을 건네면 매번 똑같은 결과를 내는 함수는 다루기 쉽다. 이런 성질을 참조 투명성이라고 한다. 참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃게되는데. 이 문제는 해당 원소를 매개변수로 바꾸면 해결된다. 책임이 호출자로 옮겨진다는 단점이 있다.

세터 메서드가 있다는 것은 필드가 수정될 수 있음을 의미한다. 객체 생성 후에는 수정되지 않길 원하는 필드라면 세터를 제공하지 않았을 것이다. 세터 제거하기 리팩터링이 필요한 상황은 주로 두가지다. 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할때, 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때 이다. 이런 경우에는 세터를 제거하여 변하지 않는 값이라는 의도를 정확하게 전달하는 것이 좋다.

객체 지향 언어에서 제공하는 생성자는 객체를 초기화 하는 용도의 함수다. 실제로 새로운 객체를 생성할 때면 주로 생성자를 호출한다. 하지만 생성자에는 일반 함수에는 없는 이상한 제약이 따라붙기도 한다.(자바의 경우에는 생성자는 반드시 클래스 명과 일치해야 하고, 서브 클래스를 반환할 수 없다.) 팩터리 함수에는 이러한 제약이 없다. 팩터리 함수를 구현하는 과정에서 생성자를 호출할 수 있지만, 원한다면 다른 무언가로 대체할 수 있다.

메서드는 프로그래밍의 기본 빌딩 블록 중 하나다. 그런데 메서드를 그 메서드만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다. 이런 객체를 가리켜 명령 객체 혹은 단순히 명령이라 한다. 명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다. 명령은 평범함 메서드 매커니즘 보다 훨씬 유연하게 메서드를 제어하교 표현할 수 있다. 명령은 되돌리기 같은 보조 연산을 제공할 수 있으며, 수명주기를 더 정밀하게 제어하는데 필요한 매개변수를 만들어주는 메서드를 제공할 수 있다. 객체는 지원하니만 일급 함수를 지원하지 않는 프로그래밍 언어를 사용할 때는 명령을 이용해 일급 함수에 기능 대부분을 흉내낼 수 있다.

명령 객체는 복잡한 연산을 다룰 수 있는 강력한 메커니즘을 제공한다. 구체적으로는 큰 연산 하나를 작은 메서드로 쪼개고 필드를 이용해 쪼개진 메서드들 끼리 정보를 공유할 수 있다. 하지만, 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는 것이 낫다.

데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나다. 특히 같은 데이터 블록을 읽고 수정하는 코드가 여러 곳이라면 데이터가 수정디는 흐름과 코드의 흐름을 일치시키기가 상당히 어렵다. 그래서 데이터가 수정된다면 그 사실을 명확히 알려주어서 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 일이 대단히 중요하다. 이 리팩터링은 값 하나를 계산한다는 분명한 목적이 있는 함수들에 가장 효과적이다.

예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 매커니즘이다. 오류가 발견되면 예외를 던진다. 그러면 적절한 예외 핸들러를 찾을 때 까지 콜스택을 타고 위로 전파된다. 예외를 사용하면 코드를 일일이 검서하거나 오류를 식별해 콜스택 위로 던지는 일을 신경쓰지 않아도 된다. 예외는 정확히 예상 밖의 동작일 때만 쓰여야 한다. 달리 말하면 프로그램의 정상 동작 범주에 들지 않는 오류를 나타낼때만 쓰여야 한다.

오류 코드를 연쇄적으로 전파하던 긴 코드를 예외로 바꿔 깔끔하게 제거할 수 있다. 하지만 좋은 것들은 늘 그렇듯 예외도 과용되곤 한다. 예외는 뜻밖의 오류라는, 말 그대로 예외적으로 동작할 때만 쓰여야 한다. 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야 한다.

7. 상속 다루기

중복 코드 제거는 중요하다. 중복된 메서드가 문제없이 동작하더라도 미래에는 버그가 꼬이는 쓰레기로 전락할 수 있다. 무언가 중복되었다는 것은 한쪽의 변경이 다른쪽에는 반영되지 않을 수 있다는 위험을 항상 수반한다. 그런데 일반적으로는 중복을 찾기가 그리 쉽지 않다. 메서드 올리기를 적용하기 쉬운 상황은 메서드들의 본문 코드가 똑같을 때다. 물론 세상이 언제나 이처럼 만만하지는 않다. 서로 다른 두 클래스의 두 메서드를 각각 매개변수화 하면 궁극적으로 같은 메서드가 되기도 한다.

서브 클래스들이 독립적으로 개발되었다가 하나의 계층 구조로 합쳐진 경우에는 일부 기능이 중복되어 있을 때가 종종 있다. 특히 필드가 중복되기 쉽다. 이런 필드들은 이름이 비슷한 게 보통이지만, 항상 그런것은 아니다. 그래서 어떤 일이 벌어지는지를 알아내려면 필드들이 어떻게 이용되는지 분석해봐야 한다. 분석 결과 필드들이 비슷한 방식으로 쓰인다고 판단되면 슈퍼클래스로 끌어올리자. 이렇게 사용하면 두가지 중복을 없앨 수 있다. 데이터 중복 선언과, 해당 필드를 사용하는 동작을 서브클래스에서 슈퍼클래스로 끌어 올릴 수 있다.

서브클래스에서 기능이 같은 메서드들을 발견하면 함수 추출하기와 메서드 올리기를 차례로 적용하여 말끔히 슈퍼클래스로 옮기자. 근데 그 메서드가 생성자라면 스텝이 꼬인다. 생성자는 할 수 있는 일과 호출 순서에 제약이 있기 때문에 조금 다른 식으로 접근해야 한다.

특정 서브클래스 하나와만 관련된 메서드는 슈퍼클래스에서 제거하고 해당 서브클래스에 추가하는 것이 깔끔하다. 다만 이 리팩터링은 해당 기능을 제공하는 서브클래스가 정확히 무엇인지를 호출자가 알고 있을때만 적용할 수 있다.

서브클래스 하나에서만 사용하는 필드는 해당 서브클래스로 옮긴다.

소프트웨어 시스템에서는 비슷한 대상들을 특정 특성에 따라 구분할 때가 자구 있다. 예를들어 직원을 담당 업무로 구분하는것이 하나의 예이다. 이런 일을 다루는 수단으로 타입 코드 필드를 사용한다. 타입 코드를 서브클래스로 바꾸면 조건에 따라 다르게 동작하는 다형성 부분에서 이점을 얻을 수 있고, 특정 타입에서만 의미가 있는 값을 사용하는 필드나 메서드가 있을 때 서브 클래스만 필드를 가지도록 정리할 수 있다.

서브클래스는 원래 데이터 구조와는 다른 변종을 만들거나 종류에 따라 동작이 달라지게 할 수 있는 유용한 매커니즘이나, 시스템이 성장함에 따라 다른 모듈로 이동하거나 사라지면서 가치가 없어지기도 한다. 서브 클래스는 결국 한번도 활용되지 않기도 하며, 때론 필요로 하지 않는 방식으로 만들어진다. 이러한 서브클래스는 슈퍼클래스의 필드로 대체하여 제거하는것이 최선이다.

비슷한 일을 수행하는 두 클래스가 보이면 상속을 이용해 비슷한 부분을 공통의 슈퍼클래스로 옮길 수 있다.

클래스 계층 구조를 리팩터링 하다보면 계층 구조가 진화하면서 어떤 클래스와 부모가 비슷해져 독립적으로 존재해야 할 이유가 사라지는 경우가 생기기도 한다. 바로 그 둘을 하나로 합쳐야 할 시점이다.

상속에는 단점이 있다. 단 한 번만 쓸 수 있는 카드라는 것이다. 무언가 달라져야 하는 이유가 여러 개 여도 상속에서는 그중 단 하나의 이유만 선택해 기준으로 삼아야 한다. 예를 들어 사람이라는 객체의 동작을 '나이대'와 '소득 수준'에 따라 달리 하고 싶다면 서브 클래스는 젊은이, 어르신이 되거나, 부자 혹은 서민이 되어야 한다. 둘 다는 안된다. 또 다른 문제로 상속은 클래스들의 관계를 아주 긴밀하게 결합한다. 부모를 수정하면 이미 존재하는 자식들의 기능들을 해치기가 쉽다. 그래서 자식들이 슈퍼클래스를 어떻게 상속해 쓰는지를 이해해야 한다. 위임은 이 두 문제를 모두 해결해준다. 다양한 클래스에 서로 다른 이유로 위임할 수 있다. 위임은 객체 사이의 일반적인 관계이므로 상호작용에 필요한 인터페이스를 명확히 정의할 수 있다. (상속보다 결합도가 훨씬 약하다.)
디자인 패턴에 익숙한 사람이라면 서브 클래스를 전략 패턴으로 대체한다고 생각하면 도움이 될 것 이다.

슈퍼클래스의 기능들이 서브클래스에는 어울리지 않는다면 그 기능들을 상속을 통해 이용하면 안된다는 신호이다. 제대로 된 상속이라면 서브클래스가 슈퍼클래스의 모든 기능을 사용함은 물론 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 수 있어야 한다. 이 뜻은 슈퍼클래스가 사용되는 모든 곳에서 서브클래스의 인스턴스를 사용해도 이상없이 동작해야 한다.

About

리팩터링을 연습해보자 (with. 마틴 파울러)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages