리팩터링을 언제 시작하고 언제 그만할지를 판단하는 일은 리팩터링의 작동 원리를 아는 것 못지않게 중요합니다. 이 장에서는 리팩터링하면 해결할 수 있는 문제의 징후를 제시할 뿐 이런 일들을 언제 해야하는지는 제시하지 않습니다. 즉, 인스턴스 변수는 몇 개가 적당한지와 같은 것들은 각자 경험을 통해 감을 키워야한다고 합니다.
기이한 이름(Mysterious Name)
코드는 단순하고 명료하게 작성해야 한다.
함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각 어떻게 사용되는지 혹은 어떻게 사용해야 하는지 명확히 알 수 있도록 해야합니다.
중복 코드(Duplicated Code)
똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합하여 더 나은 프로그램을 만들 수 있다.
예시를 들어보겠습니다. 한 클래스에 딸린 메서드가 같은 표현식을 사용하면, 메서드를 추출해서 추출된 메서드를 호출하게 변경하면 됩니다.
class Calculator:
def add_tax(self, price):
tax = 0.1
return price + (price * tax)
def add_tip(self, price):
tax = 0.1
tip = 0.15
total_with_tax = price + (price * tax)
return total_with_tax + (total_with_tax * tip)
위의 예시에서 add_tax와 add_tip 두 메서드 모두 price + (price * tax)라는 표현식을 사용하고 있습니다. 이런 중복을 제거하기 위해 메서드를 추출하겠습니다.
class Calculator:
def add_tax(self, price):
return price + self.calculate_tax(price)
def add_tip(self, price):
total_with_tax = price + self.calculate_tax(price)
return total_with_tax + (total_with_tax * 0.15)
def calculate_tax(self, price):
tax = 0.1
return price * tax
위 예시에서는 calculate_tax라는 메서드를 새로 생성하여 중복되는 price + (price * tax) 계산 로직을 이 메서드 안으로 옮겼습니다. 이후에 add_tax와 add_tip에서는 calculate_tax 메서드를 호출하도록 변경했습니다.
또 다른 상황으로는 다음과 같은 상황이 있습니다. 같은 부모로부터 파생된 서브클래스들에 코드가 중복되어 있다면, 각자 따로 호출되지 않도록 부모로 공통된 메서드를 올리는 방법을 생각할 수 있습니다.
이런 식으로 중복 코드를 별도의 메서드로 추출하는 것은 코드의 가독성을 향상시키고, 유지 보수를 용이하게 합니다.
긴 함수(Long Function)
저자의 경험에 비춰보면 오랜 기간 잘 활용되는 프로그램들은 하나같이 짧은 함수로 구성됐다.
함수를 쪼개야하는 몇 가지 관점에 대해 알아보겠습니다. 먼저 주석을 달아야 할 만한 부분은 무조건 함수로 만들어야 합니다. 그 함수 본문에는 원래 주석으로 설명하려던 코드가 담기고, 함수 이름은 동작 방식이 아닌 의도가 드러나게 짓습니다(이러한 함수의 본문은 여러 줄일 수도 있고 단 한 줄일 수도 있어요). 이렇게 함수를 분리하는 것의 핵심은 함수의 길이가 아닌, 무엇을 하는지를 코드가 잘 설명해주지 못할수록 함수로 만드는데 있습니다.
긴 매개변수 목록(Long Parameter List)
매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 많다.
다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있는데, 이러한 매개변수는 질의 함수로 바꿔서 제거할 수 있습니다.
질의 함수(query function)는 객체의 상태를 조회하여 어떤 값을 반환하지만, 객체의 상태를 변경하지 않는 함수를 의미합니다. 다시 말해, 이 함수는 부작용(side-effect) 없이 객체의 정보만을 반환하는 역할을 합니다.
또한 객체 통째로 넘기기, 매개변수 객체 만들기 그리고 플래그 인수 제거하기와 같은 이 책의 뒷 부분에서 소개하는 여러 리팩터링 기법들을 사용해서 긴 매개변수 목록이라는 문제를 해결할 수 있습니다.
전역 데이터(Global Data)
전역 데이터는 코드베이스 어디에서든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 메커니즘이 없다는 것이 문제다.
이러한 문제를 방지하기 위해 변수 캡슐화하기 리팩터링 기업을 사용합니다. 다른 코드에서 오염시킬 가능성이 있는 데이터를 발견할 때마다 이 기법을 가장 먼저 사용해야합니다. 이런 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있습니다. 또한 접근자(private, protected와 같은 것) 함수들을 클래스나 모듈에 집어넣고 그 안에서만 사용할 수 있도록 접근 범위를 최소로 줄이는 것도 좋습니다.
가변 데이터
데이터를 변경했더니 예상치 못한 결과나 골치 아픈 버그로 이어지는 경우가 종종 있다.
함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시(원래 데이터는 그대로 둔 채) 변경하려는 값에 해당하는 복사본을 만들어서 반환한다는 개념을 기본으로 합니다(불변성 원칙).
이 책에서는 함수형 프로그래밍의 관점이 아니라 다른 관점에서 말해줍니다. 여러가지 리팩터링 기법들을 토대로 문제 해결 방법을 알려주는데, 이 중 개인적으로 이 절의 핵심이라고 생각되는 두 가지 방법에 대해서 알아보겠습니다.
첫 번째는 변수 캡슐화하기 리팩터링 기법을 적용해서 정해놓은 함ㅅ를 거쳐야만 값을 수정할 수 있도록 합니다. 이렇게하면, 값이 어떻게 수정되는지 감시하거나 코드를 개선하기 쉽습니다.
두 번째는 변수 쪼개기 리팩터링 기법을 사용하는 것입니다. 이를 통해 하나의 변수에 용도가 다른 값들을 저장하는 경우에 해당하는 문제를 해결할 수 있습니다. 변수 쪼개기를 사용하면 용도별로 독립 변수에 저장하게 하여 값 갱신이 문제를 일으킬 여지를 없앱니다.
위의 방법들 외에도 문장 슬라이드하기, 함수 추출하기, 질의 함수와 변경 함수 분리하기, 세터 제거하기, 파생 변수를 질의 함수로 바꾸기, 여러 함수를 클래스로 묶기, 여러 함수를 변환 함수로 묶기 그리고 참조를 값으로 바꾸기 와 같은 리팩터링 기법들을 사용해 특정 문제에 맞는 해결법으로 사용을 할 수 있습니다.
뒤엉킨 변경(Divergent Change)
뒤엉킨 변경은 단일 책임 원칙(Single Responsibility Principle -> SRP)이 제대로 지켜지지 않을 때 나타난다. 즉, 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다(한 코드에 여러 모듈).
예를 들면, 데이터베이스가 추가될 때마다 함수 세 개를 바꿔야하고, 금융 상품이 추가될 때마다 또 다른 함수 네 개를 바꿔야 하는 모듈이 있다면 뒤엉킨 변경이 발생했다는 뜻입니다. 데이터베이스 연동과 금융 상품 처리는 서로 다른 맥락에서 이뤄지므로 독립된 모듈로 분리해야합니다. 그래야 무언가를 수정할 때 해당 맥락의 코드만 이해해도 진행할 수 있습니다.
산탄총 수술(Shotgun Surgery)
산탄총 수술은 뒤엉킨 변경과는 비슷하면서도 정반대다. 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 보여진다. 즉, 변경할 부분이 코드 전반에 퍼져 있다는 것이다(한 모듈이 여러 코드에 흩어진 것).
이런 문제를 해결하기 위해서는 함께 변경되는 대상들을 한 모듈에 묶어두면 좋다고 합니다. 이 책에서는 작은 함수와 클래스를 추천하지만, 코드를 재구성하는 중간 과정에서는 큰 덩어리로 뭉쳐지는데 개의치 않다고 합니다.
뒤엉킨 변경과 산탄총 수술은 결국 맥락을 잘 구분하지 못하는데서 생기는 문제입니다. 이러한 문제를 해결하려면 맥락을 명확히 구분하는 것이 중요합니다.
기능 편애(Feature Envy)
프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다. 기능 편애는 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때 발생한다.
이 문제를 해결하려면 함수에서 사용하는 외부 데이터 근처로 옮겨주면 됩니다. 함수의 일부에서만 기능을 편애한다면, 그 부분만 독립 함수로 빼낸 다음 원하는 모듈로 보내줍니다. 만약 옮겨야하는 모듈이 명확하지 않으면, 함수가 사용하는 모듈 중 가장 많은 데이터를 포함하는 모듈로 옮깁니다.
데이터 뭉치(Data Clumps)
데이터 항목 서너 개가 여러 곳에서 항상 함께 뭉쳐 다니는 모습을 볼 수 있다. 이렇게 뭉쳐다니는 데이터 뭉치는 따로 빼주어야 한다.
먼저 필드 형태의 데이터 뭉치를 찾아서 클래스 추출하기 리팩터링 기법을 사용해서 하나의 객체로 묶습니다. 다음은 메서드 시그니처에 있는 데이터 뭉치를 추출하기 위해 매개변수 객체 만들기나 객체 통째로 넘기기를 적용해서 매개변수 수를 줄여봅니다. 이렇게하면, 메서드 호출 코드가 간결해질 것입니다.
데이터 뭉치인지 판별하려면 값 하나를 삭제해보면 됩니다. 그랬을 때 나머지 데이터만으로는 의미가 없다면 데이터 뭉치 입니다(데이터 뭉치는 객체로 변경시켜요).
기본형 집착(Primitive Obsession)
프로그래머 중에는 자신에게 주어진 문제에 딱 맞는 기초 타입(화폐, 좌표, 구간 등)을 직접 정의하기를 몹시 꺼리는 사람이 많다. 이러한 문제는 문자열을 다루는 코드에서 특히 흔하다. 전화번호를 단순히 문자 집합으로만 표현하기에는 아쉬움이 많다. 최소한 사용자에게 보여줄 때는 일관된 형식으로 출력해주는 기능이라도 갖춰야 한다.
이런 문제를 해결하기 위해서 기본형을 객체로 바꾸기 리팩터링 기법을 사용할 수 있습니다. 기본형을 객체로 바꾸기를 적용하면 기본형만이 존재하는 곳을 의미 있는 자료형이 있는 곳으로 변경할 수 있습니다. TypeScript에서는 타입 브랜딩이라는 기본형을 방법을 사용하는 것도 괜찮을거 같다고 생각합니다.
타입 브랜딩(Branding) 기법은 주로 값의 원시 타입만으로는 충분하지 않을 때 혹은 두 개 이상의 값이 같은 원시 타입을 갖지만 서로 다른 용도나 의미를 갖게 하고 싶을 때 사용됩니다.
반복되는 switch문(Repeated Switches)
똑같은 조건부 로직(switch/case문이나 길게 나열된 if/else문)이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다.
다형성을 사용하여 여러 곳에서 사용되는 switch문의 단점을 해결할 수 있습니다.
반복문(Loops)
반복문은 프로그래밍 언어가 등장할 때부터 함께 한 핵심 프로그래밍 요소다.
현대 시대에는 일급 함수(first-class function)를 지원하는 언어가 많아졌기 때문에 반복문을 파이프라인으로 바꾸기 리팩터링 기법을 적용해서 반복문을 제거할 수 있습니다. 필터(filter)나 맵(map) 같은 pipeline 연산을 사요하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있습니다.
성의 없는 요소(Lazy Element)
우리는 코드의 구조를 잡을 때 프로그램 요소(프로그래밍 언어가 제공하는 함수(메서드), 클래스, 인터페이스 등 코드 구조를 잡는 데 활용되는 요소)를 이용하는 걸 좋아한다. 그래야 그 구조를 변형하거나 재활용할 기회가 생기고, 혹은 단순히 더 의미 있는 이름을 가졌기 때문이다.
클래스의 메서드가 하나뿐이라면, 이러한 구조는 함수 인라인하기나 클래스 인라인하기 리팩터링 기법을 통해서 제거할 수 있습니다. 상속을 사용했다면 계층 합치기 기법을 적용하여 제거합니다.
추측성 일반화(Speculative Generality)
추측성 일반화는 '나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 특이 케이스 처리 로직을 작성해둔 코드에서 발생한다.
미래를 대비해 작성한 부분을 실제로 사용하게 되면 다행이지만, 그렇지 않는다면 쓸데없는 낭비일 뿐입니다. 이러한 코드는 제거하는 것을 추천합니다.
추상 클래스는 계층 합치기 리팩터링 기법을 사용하여 제거합니다. 쓸데없는 위임이 있는 코드는 함수 인라인하기나 클래스 인라인하기로 제거합니다. 본문에서 사용되지 않는 매개변수는 함수선언 바꾸기로 없앱니다. 나중에 다른 버전을 만들 때 필요할 거라는 생각에 추가했지만 한 번도 사용한 적 없는 매개변수도 이 기법으로 제거합니다.
임시 필드(Temporary Field)
특정 상황에서만 값이 설정되는 필드를 가진 클래스가 존재한다.
코드를 임시 필드를 갖도록 작성하면 코드를 이해하기 힘들게 됩니다. 그래서 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 시간을 많이 쓰게 됩니다.
이러한 필드들을 발견하면 클래스 추출하기 리팩터링 기법을 사용해서 따로 추출해 줍니다. 그런 다음 함수 옮기기로 임시 필드들과 관련된 코드를 새 클래스에 몰아넣습니다. 또한, 임시 필드들이 유효한지를 확인한 후 동작하는 조건부 로직이 있을 수 있는데, 특이 케이스 추가하기로 필드들이 유효하지 않을 때를 위한 대안 클래스를 만들어서 제거할 수 있습니다.
메시지 체인(Message Chains)
메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.
예를 들어, getSomething() 같은 게터가 꼬리에 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드가 있는데, 이는 클라이언트가 객체 내비게이션 구조에 종속됐음을 의미합니다. 그래서 내비게이션 중간 단계를 수정하면 클라이언트 코드도 수정해야 합니다.
이 문제는 위임 숨기기 리팩터링 기법을 사용해서 해결합니다. 이 리팩터링 기법은 체인을 구성하는 모든 객체에 적용할 수 있지만, 그러다 보면 중간 객체들이 모두 중개자(아래에서 설명)가 돼버리기 쉽습니다. 그러니 최종 결과 객체가 어떻게 쓰이는지부터 살펴보는게 좋습니다. 결과 객체를 사용하는 코드 일부를 빼낸 다음 체인을 숨길 수 있는지 살표보는 것이 좋습니다. 체인을 구성하는 객체 중 특정 하나를 사용하는 클라이언트 중 그 이후의 객체들도 사용하길 원하는 클라이언트가 제법 된다면, 이 요구를 처리해줄 메서드를 추가합니다.
위의 말을 좀 더 쉽게 말하면 다음과 같습니다. "종속성 문제를 해결하기 위한 리팩터링 방법입니다. 주요 아이디어는 클라이언트가 객체의 내부 구조나 다음 객체를 직접 호출하는 대신, 그 객체의 기능만을 이용할 수 있도록 중간 단계를 숨기는 것입니다." 라고 말할 수 있습니다.
class Address {
getStreet() {
return "123 Main St.";
}
}
class User {
constructor() {
this.address = new Address();
}
getAddress() {
return this.address;
}
}
const user = new User();
console.log(user.getAddress().getStreet()); // "123 Main St."
위의 예제에서 user.getAddress().getStreet()
처럼 호출하면, 클라이언트 코드는 User
객체의 내부 구조인 Address
객체에 대한 지식을 가져야 합니다. 이는 객체 내비게이션 구조에 종속되었음을 의미합니다. 이 문제를 위임 숨기기 리팩터링 기법을 사용해서 해결하면 다음과 같습니다.
class User {
constructor() {
this.address = new Address();
}
getStreet() {
return this.address.getStreet();
}
}
const user = new User();
console.log(user.getStreet()); // "123 Main St."
여기서 User
클래스는 Address
객체의 getStreet()
메서드를 직접 호출하고, 클라이언트 코드는 User
객체의 getStreet()
메서드만을 호출합니다. 이로써 클라이언트는 Address
객체의 존재를 모르게 되어 객체 내비게이션 구조에 종속되지 않게 됩니다.
하지만 이런 리팩터링이 과도하게 진행되면, 많은 객체들이 실제 기능을 수행하지 않고 단순히 다른 객체에게 요청을 전달하는 중개자 역할만을 하게 될 수 있습니다. 따라서 리팩터링을 진행할 때는 신중해야 하며, 클라이언트가 필요로 하는 기능과 객체 내비게이션 구조를 잘 이해하고 적절한 메서드를 추가하는 것이 중요합니다.
중개자(Middle Man)
객체의 대표적인 기능 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화(encapsulation)가 있다. 캡슐화하는 과정에서 위임(delegation)이 자주 활용된다. 이러한 위임을 하는 객체가 중개자다.
클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면, 중개자 제거하기 리팩터링 기법을 사용해서 실제로 일을 하는 객체와 직접 소통하게 해야합니다.
내부자 거래(Insider Trading)
모듈 사이의 데이터 거래가 많으면 결합도(coupling)가 높아진다고 말한다. 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.
거대한 클래스(Large Class)
한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어난다. 그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.
클래스 추출하기 리팩터링 기법을 사용해서 필드들 일부를 묶습니다. 이때는 같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들을 선택하면 됩니다.
코드량이 너무 많은 클래스도 중복 코드와 혼동을 일으킬 여지가 큽니다. 가장 간단한 해법은 그 클래스 안에서 자체적으로 중복을 제거하는 것입니다.
클라이언트들이 거대 클래스를 어떻게 이용하는지 패턴을 파악하여 그 클래스를 쪼갤 단서를 얻을 수도 있습니다. 먼저 클라이언트들이 거대 클래스의 특정 기능 그룹만 주로 사용하는지 살핍니다. 이때 각각의 기능 그룹이 개별 클래스로 추출될 후보입니다. 유용한 기능 그룹을 찾았다면 클래스 추출하기, 슈퍼클래스 추출하기, 타입 코드를 서브클래스로 바꾸기등의 리팩터링 기법을 활용해서 여러 클래스로 분리합니다.
서로 다른 인터페이스와 대안 클래스들(Alternative Classes with Different Interface)
클래스를 사용할 때의 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다는 것이다. 단, 교체하려면 인터페이스가 같아야 한다.
데이터 클래스(Data Class)
데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다. 그저 데이터 저장 용도로만 쓰이다 보니 다른 클래스가 너무 깊이까지 함부로 다룰 때가 많다.
다른 클래스에서 데이터 클래스의 게터나 세터를 사용하는 메서드를 찾아서 함수 옮기기 리팩터링 기법을 사용해서 그 메서드를 데이터 클래스로 옮길 수 있는지 살펴봅시다. 메서드를 통째로 옮기기 어렵다면 함수 추출하기 리팩터링 기법을 사용해서 옮길 수 있는 부분만 별도 메서드로 뽑아내면 됩니다.
데이터 클래스는 필요한 동작이 엉뚱한 곳에 정의돼 있다는 신호일 수 있습니다. 이런 경우라면 클라이언트 코드를 데이터 클래스로 옮기기만 해도 많이 개선됩니다. 예외도 있는데, 다른 함수를 호출해 얻은 결과 레코드(데이터 객체)로는 동작 코드를 넣지 않아도 됩니다.
이렇게 리팩터링 2판의 3장에 대해 정리해 보았습니다. 앞으로의 장에서는 각각의 리팩터링 기법에 대해 자세히 살펴보고 적용 방법에 대해 살펴볼 예정입니다.
'Develop > JavaScript' 카테고리의 다른 글
TypeScript의 구조적 타이핑과 Brading (0) | 2023.08.23 |
---|---|
리팩터링 2판 1장 (0) | 2023.06.01 |