이번 장에서 다룰 내용
리팩터링이 무엇이며 그것이 왜 기술적 관점에서 중요한지에 대한 튼튼한 기반을 만들기 위한 기술적 세부 사항을 살펴본다!
리팩터링이란 코드가 하는 일을 변경하지 않고 더 나은 코드를 만드는 것이다. 이 정의에서 더 나은 코드를 만드는 것과 코드가 하는 일을 변경하지 않는다는 중요한 두 가지 내용을 살펴본다.
가독성은 의도를 전달하기 위한 코드의 성질이다. 이 말은 곧 코드가 의도한 대로 작동한다는 가정이 있으면 코드가 무슨 일을 하는지 파악하기가 매우 쉽다는 뜻이다.
코드에서 의도를 전달하는 방법에는 여러가지가 있다. 코딩 컨벤션을 지정하고 따르는 것, 주석 달기, 변수, 메서드, 클래스 및 파일이름 지정, 공백 사용 등이다.
예제
//일기 힘든 코드의 예
function checkValue(str: boolean){// 바람직하지 않는 메서드명: 매개변수 타입이 불(Boolean)인데, 매개변수명이 str 임..
// 값 체크 <- 그저 이름만 반복하는 주석
if(str !== false) // 이중 부정은 읽기 어려움
// 반환 <- 코드를 반복하는 의미없는 주석
return true;
else; // 그렇지 않으면 <- 놓치기 쉬훈 세미콜론(;) 과 사소한 내용의 주석
return str; // 오해의 소지가 있는 들여쓰기, 이 시점에서 str은 반드시 false 이기 때문에 그냥 false 로 표시하는 것이 명확함!
}
// 읽기 쉽게 작성된 동일한 코드
function isTru(bool: boolean){
if(bool)
return ture;
else
return false;
}
// 정리하면 다음과 같이 간단하게 정리할 수 있다.
function isTrue(bool:boolean){
return bool;
}
버그를 고치거나 기능을 추가하기 위해 일부 기능을 변경해야 할 때마다 새 코드를 어디에 놓을지 후보 위치(context)를 조사하는 것으로 시작한다. 현재 코드가 무슨일을 하는지 파악하고, 새로운 목표를 수용하기 위해 코드를 안전하고 빠르며 쉽게 수정할 수 있는 방법은 무엇인지 찾으려고 시도한다.
유지보수성은 얼마나 많은 후보를 조사해야 하는지를 나타내는 표현이다.
읽고 살펴봐야 할 코드가 많을 수록 시간이 더 오래 걸리고 무언가를 놓칠 가능성이 높다는 것은 쉽게 알 수 있다. 따라서 유지보수성은 변경 시 발생하는 위험과 밀접하게 관련되어 있다.
조사 단계에서 시간이 오래 걸린다는 것은 코드 유지보수성이 나쁘다는 징후이며 개선을 위해 노력해야 한다.
어떤 시스템에서는 한군데서 무언가를 수정하면 관련 없어 보이는 다른 곳에서 문제가 발생한다. 추천 기능을 수정했는데 지불 시스템이 중단되는 온라인 쇼핑몰을 생각해보라! 그런 시스템을 취약하다(fragile)고 말한다.
이 취약성의 근원은 일반적으로 전역상태(global state)이다. 여기서 전역(global)은 우리가 고려한 범위(scope)를 벗어난 것을 의미한다. 메서드로 생각하면 내부 변수가 전역 변수를 참조하는 것이다. 상태(state)의 개념은 조금더 추상적이다. 프로그램이 실행되는 동안 변경될 수 있는 모든 것이다. 여기에는 변수뿐아니라, 데이터베이스의 데이터, 하드디스크 안의 파일, 그리고 하드웨어 자체도 포함된다.
전역상태를 생각하는 데 도움이 되는 유용한 트릭은 중괄호를 찾는 것이다. 중괄호 외부의 모든 것이 중괄호 내부의 모든 것에 대해 전역 상태로 간주된다.
🚨 전역 상태의 문제
내부 변수에 외부의 데이터를 할당하는 경우가 많다. 이떄 데이터가 전역적일 경우 데이터가 연결된 다른 변수를 통해 누군가가 읽거나 변경할 수 있어 실수로 데이터가 손상될 수 있다. 코드에서 상태(조건)를 명시적으로 확인하지 않는(가정 설정문으로만 확인하는) 속성을 불변속성(invariant)라고 한다.
"이 숫자는 절대 음수일 수 없습니다" 라든가" "이 파일은 확실히 존재합니다"는 불변속성의 예다.
안타깝게도 특히 시스템이 변경되거나 프로그래머들이 깜빡하거나 새로운 사람이 추가될 때 불 변속성이 유효한 상태로 유지되기란 거의 불가능하다...
변수를 명시적으로 체크해서 불변속을 제거함으로써 유지보수성을 향상시킬 수 있다. 그러나 이렇게 하면 리팩터링에서 해서는 안 되는. 코드가 수행하는 작업이 변경된다. 이런 경우 리팩터링은 불변속성을 더욱 쉽게 볼 수 있도록 서로 가깝게 이동시켜 유지보수성을 향상시킨다.
이를 가리켜 '함께 변하는 것은 함께 있어야 한다' 는 의미의 불변속성의 범위 제한 이라고 한다.
직감적으로 코드를 블랙박스로 생각하고 외부에 영향을 주지 않는 내부의 모든 작업을 변경할 수 있다고 생각할 수 있다. 값을 입력하면 리팩터링 전과 후에 동일한 결과를 얻어야한다. 결과가 예외인 경우에도 마찬가지 이다.
한 가지 주목할 만한 예외는 성능을 바꿀 수 있단느 것이다. 특히, 리팩터링 중에는 코드가 느려져도 거의 신경 쓰지 않는다.
대부분의 시스템에서 성능은 가독성과 유지보수성보다 가치가 떨어진다.
성능이 중요한 경우 프로파일링 도구나 성능 전문가의 지도를 받아 리팩터링과 다른 단계에서 처리해야 한다.
리팩터링을 할 떄는 블랙박스의 경계를 고려해야한다. 어느 정도의 코드를 변경할 예정인가? 더 많은 코드를 포함할수록 더 많은 것을 바꿀 수 있다. 협업할때 특히 중요하다.
적절한 리팩터링 범위를 결정하는 것은 어렵고 중요한 조정 작업니다.
좋은 코드베이스에서 작업할 때의 정점은 더 생산적이면서 더 적은 실수를 하고 더 편안하다.
범위가 제한되지 않은 불변속성은 유지보수하기 어렵다.
이 책에서 설명하는 대부부분의 리팩터링 패턴과 규칙은 구체적으로 객체 컴포지션을 돕기위한 것들이다. 즉, 객체가 내부에 다른 객체의 참조를 가지는 것이다.
//상속을 사용
interface Bird {
hasBeak(): boolean;
canFly(): boolean;
}
class CommonBird implements Bird {
hasBeak() {return true;}
canFly() {return true; }
}
class Penguin extends CommonBird { //상속
canFly() {return false;}
}
//컴포지션을 사용
interface Bird {
hasBeak(): boolean;
canFly(): boolean;
}
class CommonBird implements Bird {
hasBeak() {return true;}
canFly() {return true; }
}
class Penguin implements Bird {
private bird = new CommonBird(); //컴포지션
hasBeak() {return bird.hasBeak();} //호출을 직접 명시적으로 전달해야함
canFly() {return false}
}
이 책에서는 둘 중 아래쪽 코드가 더 좋다고 본다. 다른 점을 보여주기 위해 canSwim이라는 새로운 메서드를 Bird에 추가하는 것을 상상해보자. 두 경우 모두 CommonBird에 이 메서드가 추가된다.
//상속을 사용
class CommonBird implements Bird {
canSwim() {return false};
}
컴포지션 예에서 Penguin이 새로운 메서드 canSwim을 구현하지 않기 때문에 컴파일 오류가 발생한다.
따라서 이를 인지하고 수동으로 추가하고 펭귄이 수영할 수 있는지를 반환해야한다. Penguin이 다른 새처럼 동작하기를 원할 경우에는 hasBeak 처럼 간단하게 구현할 수 있다. 반대로 상속 예에서 Penquin이 수영할 수 없다고 가정하면 canSwim을 재정의 (override) 해야 한다는 것을 작업자가 기억해야한다. 사람의 기억력은 종종 의존 관계에 취약하다. 특히 다른 기능에 집중하고 있을 때는 이 같은 것을 간과하기 쉽다.
컴포지션을 중심으로 만들어진 시스템을 사용하면 다른 방식보다 더 깔끔하게 코드를 결합하고 재사용 할 수 있다. 컴포지션을 많이 사용하는 시스템으로 작업하는 것은 레고 블록을 가지고 노는 것과 같다.
모든 것이 잘 조립되어 있으면 부품을 교체하거나 기존 부품으로 새로운 것을 만드는 것이 놀라울 정도로 빠르다. 이런 유연성(flexibility)은 대부분의 시스템이 원래 프로그래머가 상상하지 못했던 방식으로 사용된다는 것을 알면 더욱 중요해 진다.
컴포지션의 가장 큰 장점은 추가(addition)로 변경이 가능하다.
이것은 기존 기능에 영향을 주지 않고 기능을 추가하거나 변경할 수 있음을 의미한다. 어떤 경우에는 기존 코드를 변경하지 않고도 가능하다.
이 속성을 개방-폐쇄(open-closed)원칙이라고 하는데, 소프트웨어 구성 요소들은 확장에 대해 열려 있어야하고, 수정에 대해 닫혀 있어야한다는 의미이다.
추가에 의한 변경 방식을 따르면 기존 코드를 항상 보존할 수 있다. 새 코드가 실패할 경우 이전 기능으로 대체하는 기능을 구현하는 것은 쉽다. 이를 통해 기존 기능에 새로운 오류가 발생하지 않게 할 수 있다. 이렇게 하면 불변속성의 범위를 제한해 오류를 줄이는 것에 더해서 시스템의 안정성이 더욱 향상된다.
항상 여러분이 왔을 떄보다 더 좋게 만들어 놓고 떠나세요
소프트웨어는 실생활의 특정 측면을 모델링한 것이다. 소프트웨어와 대응되는 실세계는 항상 존재한다. 이 실제 세계의 구성요소를 소프트웨어의 도메인 이라고한다.
소프트 웨어를 개발할 떄 도메인 전문가와 긴밀하게 협력해야 할 때가 많은데, 이는 그들의 영어와 문화를 배워야 한다는 것을 의미한다. 프로그래밍 언어는 모호성을 허용하지 않기 때문에 때떄로 전문자 조차도 낯선 새로운 코너 케이스를 찾아야한다. 결과적으로 프로그래밍은 주로 학습과 의사소통에 관한 것이다.