이 글은 마틴 파울러의 리펙터링을 읽으면서 배운 내용을 기록한 글입니다.
지금까지 리펙터링이란 설계를 수정하거나 결합도 높은 코드를 모듈로 분리하는 것들을 리펙터링이라고 생각했습니다. 그리고 이 작업들은 큰 규모로 진행되고 사이드 이펙트를 고려해서 조심스럽게 작업해야 했습니다.
사실 '리펙터링이란 무엇인지?', '어떻게 해야하는지?'에 대해 구체적인 기준없이 경험과 직감을 바탕으로 작업을 했었던 것 같습니다. 이 책을 읽으면서 지금까지 놓치고 있었던 리펙터링의 원리와 방법을 깨달았습니다. 그 결과 책을 읽기 전보다 훨씬 더 나은 코드를 작성하게 되었습니다.
리펙터링은 규모가 크고, 사이드 이펙트를 생각해야하는 작업이라는 인식이 있어서 다가가기 어려운 느낌입니다. 하지만 책에서는 리펙터링이란 작은 단위로 진행되는 것이며, 테스트 코드와 함께 실시하기 때문에 안전하다고 주장합니다. 지금까지 리펙터링을 막연하게 생각했던 이유는 대단한 작업을 해야 리펙터링이라고 생각했던 것과, 어려울 수 밖에 없는 환경에서 리펙터링을 진행했었기 때문인 것 같습니다.
리펙터링은 간단한 작업들의 모음이며 함수 이름을 바꾸는 것도 리펙터링입니다. 그리고 테스트 코드가 없는 환경에서 진행하는 리펙터링은 어려울 수 밖에 없는 리펙터링입니다. 리펙터링을 진행할때는 테스트 코드가 필수적입니다.
저자가 정의한 리펙터링의 특징은 다음과 같습니다.
위 정의에 따르면 작은 단위의 작업도 리펙터링이므로 틈틈이 진행할 수 있습니다. 또한 테스트 코드를 필수적으로 작성해야함도 알 수 있습니다.
특정 기능을 추가하기 전에 지저분한 코드를 리펙터링하면 빠르게 기능을 추가 할 수 있고, 버그 가능성도 낮다고 합니다. 이건 구체적인 통계가 있는건 아니지만, 실무를 경험한 개발자라면 대부분 공감할 것 같습니다. 저자는 작업을 하다가 리펙터링 할 만한 영역이 있다면 틈틈이 작은 단위로 진행할 것을 권장합니다.
테스트 케이스와 함께라면 작은 리펙터링들은 겉보기 동작에 아무런 영향을 주지 않음을 거의 확신할 수 있으니깐요. 즉 리펙터링은 작은 단위의 독립적인 작업들이 모여서 유의미한 결과를 나타내는 과정이라고 볼 수 있습니다. 그럼 리펙터링이 왜 좋은 건지? 리펙터링을 어떻게 해야 하는지?에 대해 알아보겠습니다.
저자가 말하길 리펙터링을 하는 근본적인 이유는 경제적 효과에 있습니다. 클린코드나 도덕적 이유로 리펙터링 하는 건 아니라고 합니다. 오히려 그런 가치관을 경계합니다. 프로에게는 클린코드보다는 정해진 기간 내에 신뢰성 있는 제품을 완성하는게 최우선 과제이며 그 과정에서 리펙터링을 통해 시간을 단축할 수 있기 때문에 진행하는 것입니다.
로직들을 함수로 쪼개고, 불필요한 변수를 제거하면서 코드의 가독성이 증가합니다. 가독성이 증가하면, 이해하기 쉬워지고, 테스트를 짜거나 코드를 변경하기에 용이해집니다. 이는 모듈화와 연결되는데 리펙터링을 통해 기능을 모듈들로 쪼개면 사이드 이펙트 가능성이 줄어들고 디버깅과 기능추가가 쉬워집니다. 이는 결과적으로 소프트웨어의 유지보수 비용을 감소시킵니다.
리펙터링의 이점은 알겠는데, 직감과 경험으로만 리펙터링을 하기엔 막막합니다. 과연 어떻게 리펙터링을 해야하는 걸까요?
우선 리펙터링을 작은 단위의 모음이라고 생각해야 합니다. 함수 이름을 바꾸거나, 안쓰는 변수를 제거하는 사소한 작업도 모두 리펙터링 입니다. 리펙터링을 큰 작업이라고 생각하고 며칠동안 진행하기보다는 작은 단위들을 틈틈이 진행한다는 마음 가짐을 가지는게 좋습니다.
책에서 나왔던 여러 기법들 중에서 제가 자주 사용했고, 유익했던 리펙터링 방법들을 소개하겠습니다.
const getPosts = () => {
const files = fs.readDir(path.join(₩/app/posts'), {recursive: true});
const posts = files.filter((file) => file.endsWith('.mdx'));
const sorted = posts.sorted((a, b) => a.date - b.date);
return sorted;
}
파일 시스템에서 .mdx 확장자 파일들을 정렬해서 리턴하는 함수입니다. 당장은 괜찮아 보이지만, 만약 기능이 더 추가되면 스파게티 코드가 될 가능성이 커보입니다. 저자는 왠만한 로직들은 함수로 분리하는것을 권장합니다. 처음에는 이렇게 작은 단위를 굳이 함수로 분리해야하나? 라고 의구심이 들었지만 막상 실천해보니 코드의 가독성이 월등히 높아졌습니다.
const fetchFiles = (path) => fs.readDir(path, { recursive: true });
const onlyMdx = (files) => files.filter((file) => file.endsWith('.mdx'));
const sortByDate = (posts) => posts.sorted((a, b) => a.date - b.date);
function getPosts() {
//변경 전 코드 -> 코드를 읽을때 각 줄의 역할을 이해하고 살펴봐야함.
//const files = fs.readDir(path.join('/app/posts'), {recursive: true});
//const posts = files.filter((file) => file.endsWith('.mdx'));
//const sorted = posts.sorted((a, b) => a.date - b.date);
//변경 후 코드 -> 코드를 읽을때 함수의 이름만 보고 각 줄의 역할을 알 수 있음.
const files = fetchFiles(path.join('/app/posts'));
const posts = onlyMdx(files);
const sorted = sortByDate(posts);
return sorted;
}
리펙터링으로 각 기능을 담당하는 작은 함수들을 만들었습니다. 또한 각 함수들은 모듈화 되어 있으므로 쉽게 테스트 할 수 있습니다.
class Order {
constructor(price: number, count: number, distance: number) {
this.price = price;
this.count = count;
this.distance = distance;
}
public calculateTotalPrice() {
const originalPrice = this.price * this.count;
const discountRate = originalPrice > 100 ? 0.95 : 1;
const shippingFee = this.distance > 100 ? 10 : 5;
return originalPrice * discountRate + shippingFee;
}
}
총 지불 금액을 계산해주는 Order 클래스입니다. 나중에 기능이 추가된다면 calculateTotalPrice 메서드가 점점 비대해지고 외부에서 해당 클래스 내부 정보를 효율적으로 사용하기 어려워질 것 같습니다. 위의 함수 분리하기와 원리는 똑같습니다. 차이점은 간단한 순수 로직들을 함수가 아닌 getter 로 분리한다는 것이죠.
class Order {
constructor(price: number, count: number, distance: number) {
this.price = price;
this.count = count;
this.distance = distance;
}
private get originalPrice() {
return this.price * this.count;
}
private get discountRate() {
return this.originalPrice > 100 ? 0.95 : 1;
}
private get shippingFee() {
return this.distance > 100 ? 10 : 5;
}
public calculateTotalPrice() {
return this.originalPrice * this.discountRate + this.shippingFee;
}
}
더 직관적이지 않나요? 개발자가 calculateTotalPrice() 메서드를 볼때 그냥 한줄로 쭉 읽으면 이해할 수 있는 코드가 되었습니다. 만약 세부 구현이 궁금하면 그때 각 getter 를 보면 되는 것이죠.
그 외에도 임시 변수 제거하기, 클래스 캡슐화하기, 중개자 추가하기 등 여러 방법이 있지만 위 2가지가 가장 기본적이면서도 많이 사용하는 방법이라서 소개했습니다.
중복을 제거하는건 크게 2가지 이점이 있습니다. 가독성 측면에서 중복 코드를 함수로 추출하면 코드를 읽을 때 이해하기 더 쉽습니다. 유지보수 측면에서는 중복 로직을 수정해야 할 경우에 함수로 추출한 로직만 수정하면 됩니다.
중복 제거는 위의 리펙터링 방법
에 소개했던 함수 추출하기를 적용하면 자연스럽게 따라옵니다.
글을 쓰는 것도 리펙터링과 닮은점이 많은 것 같습니다. 처음에 1시간 브레인스토밍 한 뒤 1시간은 초안을 작성하고 다음 1시간은 초안을 수정하여 글을 완성합니다. 마지막 1시간은 글을 가독성 좋게 다듬는다는 측면에서 리펙터링과 유사한 면이 있습니다. 글을 다듬을때 중복된 단어는 제거하고, 맞춤법을 수정하며 주어 서술어 호응을 맞춥니다. 그럼 한층 더 가볍고 읽기 좋은 글이 되죠.
글을 고쳐쓸때 중복을 제거하고 필요없는 단어와 조사를 제거하듯, 리펙터링할때도 똑같은 규칙을 적용할 수 있습니다.
예를 들어 화살표 함수에서 return 과 중괄호를 생략하는 것이죠.
const catch = () => {
return new Bird();
}
const catch = () => new Bird();
또한 널 병합 연산자나 삼항 연산자로 if 문과 임시변수를 제거할 수 있습니다.
let food;
if (bird) {
food = bird;
} else {
food = catch();
}
const food = bird ?? catch();
이런식으로 자바스크립트의 적절한 문법을 활용해 코드를 경량화 하면 가독성이 높아집니다. 가독성이 좋아지면 많은 긍정적 부수효과가 생기므로, 코드 경량화 과정은 사소하지만, 역설적으로 굉장히 중요합니다. 작은 디테일들이 모여서 완성도 있는 제품이 만들어지듯이, 작은 리펙터링들이 모여서 좋은 소프트웨어를 만듭니다.
이 책을 아직 읽지 않은 분들은 꼭 읽어보셨으면 좋겠습니다.