Computer기본지식/리팩터링

리팩터링 Chapter 3 - 코드에서 나는 악취

HOONY_612 2021. 8. 26. 16:40
반응형

 

이번 포스팅은 코드에서 나는 악취라는 주제로 포스팅하겠습니다.

이번 챕터는 리팩터링을 언제 해야하고 언제 그만할지를 알려주는 챕터입니다.

그래서 다양한 리팩터링 기술들을 간단하게 코드로 소개했습니다.

 

3-1 기이한 이름

 

함수, 모듈, 변수, 클래스 등은 이름만 보고도 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 신경써야합니다.

대부분 함수 선언 바꾸기, 변수 이름 바꾸기, 필드 이름 바꾸기로 리팩터링.

 

3-2 중복 코드

한 클래스에 두 메서드가 똑같은 표현식 사용하는 경우 : 함수 추출하기

코드가 비슷한데 완전히 똑같진 않을 경우 : 문장 슬라이스하기

같은 부모로부터 파생된 서브 클래스에 중복 코드가 있을 경우 : 메서드 올리기

 

문장 슬라이스

비슷한 부분을 모아서 함수 추출하기를 쉽게 적용할 수 있는지 살펴보는 것

메서드 올리기

class Employee() {...}

class Salesperson extends Employee {
		get name() {}
}

class Engineer extends Employee {
		get name() {}
}
class Employee() {
	get name() {...}
}

class Salesperson extends Employee {
}

class Engineer extends Employee {
}

 

3-3 긴 함수

좋은 코드가 짧은 경우는 끝없이 위임하는 방식으로 작성되어 있기 때문이다.

함수를 짧게 만드는 방법은 대부분 함수 추출하기로 리팩터링.

그러나 매개변수가 많아짐. 어떻게 하면 좋을까?

임시 변수를 질의 함수로 바꾸기, 매가변수 객체 만들기, 객체 통째로 넘기기로 매개변수를 줄임.

추출할 코드 덩어리는 어디서 찾을까?

주석을 참고하거나 조건문이나 반복문에서 실마리를 찾음.

조건문은 조건문 분해하기, 조건문을 다형성으로 바꾸기 방법을 사용.

반복문은 반복문 쪼개기로 작업.

 

* 참고 : 임시 변수를 질의 함수로 바꾸기

class Employee() {
	get name() {...}
}

class Salesperson extends Employee {
}

class Engineer extends Employee {
}

장점은 매개변수를 줄일 수 있습니다. 왜냐하면 함수안에서 필요한 곳에 함수를 호출하면 됩니다.

 

3-4 긴 매개변수 목록

긴 매개변수 목록을 효과적으로 리팩터링하는 방식이 몇 가지 있습니다.

매개변수를 질의 함수로 바꾸기는 다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있을 경우,

객체 통째 넘기기는 사용 중인 데이터 구조에서 값들을 뽑아 각각을 별개의 매개변수로 전달할 경우,

매개변수 객체 만들기는 항상 함께 전달되는 매개변수들 묶을 경우,

플래그 인수 제거하기는 함수의 동작 방식을 결정하는 매개변수가 있는 경우 등이 있습니다.

 

3-5 전역 데이터

전역 데이터는 우리가 겪을 수 있는 악취 중 가장 지독한 축에 속한다.

왜 전역 데이터가 안 좋은 것일까요? 바로 코드베이스 어디서든 건드릴 수 있고 값을 누가 바꿨는지

찾아 낼 수 없다는게 큰 문제점입니다. 전역변수, 싱글톤에서 자주 발생하는 문제점들입니다.

이를 방지하기 위해서 변수 캡슐화하기 작업이 필요합니다.

이렇게 함으로써 접근 범위를 최소화합니다.

 

* 참고 : 변수 캡슐화하기

데이터를 읽고 쓰는 함수부터 정의

function getDefaultOwner() {return defaultOwner}
function setDefaultOwner(arg) {defaultOwner = arg;}

참조 코드를 get함수로 대입문은 set함수로 바꿔줍니다.

변수의 가시 범위를 제한. 접근자만 노출 시킵니다.

 

3-6 가변 데이터

가변 데이터의 큰 문제점은 버그가 발생해도 어디서 버그가 발생하는지 찾기가 어렵습니다.

그래서 원칙은 데이터를 변경하려면 반드시 복사본을 만들어서 반환하는게 기본 개념입니다.

하나의 변수는 하나의 역할만 해야합니다. 그렇지 않을 경우는 변수 쪼개기로 독립성을 부여합니다.

변수가 아닌 기능을 쪼개기 위해선 문장 슬라이스 + 함수 추출하기로 독립성을 부여합니다.

변수의 유효범위(변수가 접근 할 수 있는 변수)가 좁은 경우는 문제가 없습니다.

그러나 넓어질 경우는 여러 함수를 클래스로 묶기 또는 여러 함수를 변환 함수로 묶기를 활용합니다.

 

3-7 뒤엉킨 변경

코드를 수정할 때는 시스템에서 고쳐야 할 딱 한 군데를 찾아서 그 부분만 수정할 수 있기를 바란다.
이렇게 할 수 없다면 뒤엉킨 변경과 산탄총 수술 중 하나가 문제이다.

뒤엉킨 변경 : 단일 책임 원칙(SRP)가 지켜지지 않을 때 나타나는 현상으로 하나의 모듈이 서로 다른

이유들로 인해 여러가지 방식으로 변경되는 일이 많을 때 발생합니다.

예를 들어 데이터베이스에서 데이터를 가져와 금융 상품 로직을 처리해야하는 일을 살펴봅시다.

데이터베이스 연동과 금융 상품 처리는 다른 맥락에서 이뤄지므로 독립된 모듈로 분리해야합니다.

맥락에 필요한 데이터를 특정한 데이터 구조에 담아 전달하게 하는 식으로 단계를 분리합니다.

각 맥락에 해당하는 모듈들을 만들어 관련 함수들을 모읍니다.

 

* 참고 : 단일 책임 원칙

모든 클래스는 하나의 책임을 가지며 책임은 완전히 캡슐화되어야합니다.

예를 들면 운동화가 있는데 운동화를 나는 달리기 위해서 구입했지만 친구는 멋 부리기 위해서

구입했으므로 목적이 다릅니다. 이 때는 같은 액터가 아니므로 분리해줘야합니다.

 

3-8 산탄총 수술

산탄총 수술 : 코드를 변경할 때마다 자잘하게 수정해야하는 클래스가 많을 때 발생하는 악취.

이 경우에는 변경대상들을 함수 옮기기필드 옮기기를 이용하여 한 모듈로 묶어두면 좋습니다.

이렇게 묶은 모듈들의 출력 결과를 묶어서 다음 단계의 로직으로 전달합니다.

어설프게 분리된 로직은 함수 인라인하기 또는 클래스 인라인하기로 하나로 합칩니다.

 

인라인 함수란?

 

<인라인 하기 전>

int sum(int x, int y) {
	return x + y;
}

int main() {
	printf("%d", sum(1,2));
	printf("%d", sum(2,3));
}

<인라인 한 후>

int main() {
	printf("%d", 1 + 2);
	printf("%d", 2 + 3);
}

 

3-9 기능 편애

기능 편애 : 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호

작용 할 일이 더 많을 때 풍기는 냄새입니다. 예를 들면 중복된 게터(Getter)사용입니다.

이럴 경우는 함수를 데이터 근처로 옮겨주면 됩니다. 어디로 옮길지는 함수를 여러 조각으로 나눈 후

각각을 적합한 모듈로 옮기면 쉽게 해결됩니다.

 

3-10 데이터 뭉치

몰려다니는 데이터 뭉치들은 보금자리를 따로 마련해줘야합니다.

첫 번째로 필드 형태의 데이터 뭉치를 찾아 클래스 추출하기로 객채로 묶어줍니다.

다음으로 메서드 시그니처에있는 데이터 뭉치입니다.

매개변수 객체 만들기 또는 객체 통째로 넘기기를 적용해 매개변수를 줄입니다.

 

3-11 기본형 집착

예를 들어 전화번호를 출력하는 타입을 새로 생성하고 싶습니다.

그렇다면 기본형을 객체로 바꾸면서 만약 조건부 동작을 제어하는 타입 코드로 쓰였다면

타입 코드를 서브클래스로 바꾸기조건부 로직을 다형성으로 바꾸기를 차례로 적용합니다.

 

*참고 : 타입코드를 서브클래스로 바꾸기와 조건부 로직을 다형성으로 바꾸기

function createEmlpoyee(name, type) {
		return new Employee(name, type);
}
//타입코드를 서브클래스로 바꾸기
function createEmlpoyee(name, type) {
		switch (type) {
				case "engineer" : return new Engineer(name);
				case "salesperson" : return new Salesperson(name);
				case "manager" : return new Manager(name);
		}
}
//조건부 로직을 다형성으로 바꾸기
class Engineer {
	getClass() {
		return Engineer;
	}
}
...
class Salesperson {
	getClass() {
		return Salesperson;
	}
}
...
class manager {
	getClass() {
		return manager;
	}
}
...

 

3-12 반복되는 switch문

Switch문은 무조건 조건부 로직을 다형성으로 바꾸기를 적용해야합니다.

 중복된 Switch문은 조건절을 하나 추가 할 때마다 다른 Switch문들도 모두 찾아서 수정해야하기 때문입니다.

 

3-13 반복문

반복문은 반복문을 파이프라인으로 바꾸기를 적용해야합니다.

예를들어 filter와 map같은 연산이 이에 해당합니다.

 

* 참고 : 반복문을 파이프라인으로 바꾸기

const names = [];
for(const i of input) {
		if(i.job === "programmer") {
				names.push(i.name);
		}
}

//파이프라인으로 바꾸기
const names = input
	.filter(i => i.job === "programmer")
	.map(i => i.naem);

 

3-14 성의 없는 요소

프로그램 요소 중 실질적으로 메서드가 하나이거나 그대로 쓰는 것이 나을 때가 있습니다.

이럴 경우에는 함수 인라인하기 또는 클래스 인라인하기를 적용. 상속을 사용했다면 계층 합치기

적용하는 것이 훨씬 낫습니다.

 

3-15 추측성 일반화

추측성 일반화 : '나중에 필요할거야'라고 미래를 예측하고 작성하는 부분을 일컫는 말.

이런 추측성 일반화는 당장 지워야합니다. 왜냐하면 쓸데없는 낭비를 하게 되기 때문입니다.

 

3-16 임시 필드

특정 상황에서만 값이 설정되는 필드를 가진 클래스는 코드를 이해하기 어렵게 만듭니다.

이런 상황에서는 클래스 추출하기를 하여 모조리 새 클래스에 몰아넣습니다.

조건부 로직이 존재하는 경우에는 특이 케이스 추가하기로 대안 클래스를 만들어 제거합니다.

 

* 참고 : 특이 케이스 추가하기

const names = [];
for(const i of input) {
		if(i.job === "programmer") {
				names.push(i.name);
		}
}

//파이프라인으로 바꾸기
const names = input
	.filter(i => i.job === "programmer")
	.map(i => i.naem);

 

3-17 메시지 체인

메세지 체인 : 클라이언트가 한 객체를 통해 다른 객체를 얻고 방금 얻은 객체에 또 다른 객체를 요청.

예시를 들면 꼬리에 꼬리를 무는 Getter를 예시로 들 수 있습니다.

이러한 경우에는 결과 객체를 코드 일부로 뺴낸다음 체인을 숨겨줘야합니다.

//전체 호출구조
managerName = aPerson.department.manager.name;

//부분 위임 숨기기
managerName = aPerson.manager;

class aPerson {
	get manager() {
		 return this.department.manager.name;
	}
}

 

3-18 중개자

객체의 대표적인 기능으로 외부로부터 세부사항을 숨겨주는 캡슐화가 있습니다.

이 과정에서 위임이 자주 활용됩니다.

객체마다 책임을 확실하게 정의해서 실제 일을 하는 객체와 직접 소통하게 합니다.

 

* 참고 : 중개자 제거하기 : 반대는 위임 숨기기

manager = aPerson.manager;

class Person {
	get manager() {return this.department.manager;}

 

3-19 내부자 거래

모듈 사이의 데이터 거래가 많아지면(책임이 많다) 결합도가 높아지는 문제가 발생합니다.

어쩔 수 없지만 우리는 그 양을 최소로 줄이고 모두 투명하게 처리해야합니다.

만약 여러 모듈이 같은 관심사를 공유한다면 제 3의 모듈을 새로 만들거나 위임 숨기기를 이용하여

다른 모듈이 중간자 역할을 하게 만듭니다.

자식 클래스가 부모 클래스의 데이터를 접근하려고 할때는 결합도를 낮춰줘야합니다.

이럴 때는 인터페이스를 이용하여 결합도를 낮출 수 있습니다.

 

3-20 거대한 클래스

너무 많은 일을 하는 클래스는 중복 코드가 생기기 쉽습니다.이럴 때는 제 3의 클래스로 추출합니다.

한 클래스내에서 접미사,접두사가 같은 경우에 그 후보가 될 수 있습니다.

특정 기능 그룹만 조로 사용하는지 알아보고 그것을 기준으로 개별 클래스를 생성합니다.

클래스를 구분하고 슈퍼클래스 추출하기, 타입 코드를 서브클래스로 바꾸기등을 활용해서 여러 클래스로 분리합니다.

 

3-21 서로 다른 인터페이스의 대안 클래스들

클래스의 큰 장점 중 하나는 언제든 다른 클래스로 교체 할 수 있다는 것입니다.

조건은 인터페이스가 같아야한다는 점입니다. 방법은 다음과 같습니다.

함수 선언 바꾸기로 메서드 시그니처 일치 →

함수 옮기기로 필요 동작들을 밀어 넣기 →

중복 코드 발생 시 슈퍼클래스 추출하기.

 

3-22 데이터 클래스

데이터 클래스데이터 필드와 게터/세터로 구성된 클래스를 말합니다.

데이터 클래스는 특별한 경우가 아니라면 PUBLIC필드는 숨기고 세터를 제거하여 접근을 봉쇄하는게 좋습니다.

그리고 다른 클래스에서 데이터 클래스의 게터 또는 세터를 이용한다면 그 코드를 찾아서 데이터 클래스로 가능하다면 가져옵니다.

 

3-23 상속 포기

서브클래스가 부모클래스의 속성을 몇 개만 받고 싶은 경우는 어떻게 해야할까요?

부모의 인터페이스를 따르고 싶지 않을 경우에는 서브클래스를 위임으로 바꾸기, 슈퍼클래스를 위임

으로 바꾸기를 이용하여 아예 상속을 벗어나게 하는 것을 추천합니다.

 

3-24 주석

주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.

확실하지 않은 부분은 무조건 주석으로 달아두면 좋습니다. 일단 함수 추출하기를 해보고

그래도 필요하다면 함수 선언 바꾸기, 마지막으로 어서션 추가하기를 적용해봅니다.

 

이렇게 3장을 읽어봤습니다.내용도 많고 스터디 할 때 조금 힘들었던 부분은 뒤의 내용들을 총집합시켜놔 왔다갔다하면서 진행하였습니다.책에도 애매한 예시를 들어놔서 이해하는데 힘들었습니다. 그래도 어떻게 리팩터링을 시행해야하는지 감이 잡힙니다.스터디 내용은 따로 포스팅하도록 하겠습니다. 25%정도 읽고 스터디했네요.. 75%남았습니다..ㅎㅎ 화이팅!긴 글 읽어주셔서 감사합니다.

반응형