본문 바로가기
Develop/JavaScript

리팩터링 2판 1장

by 지오는 짱짱걸 입니다 2023. 6. 1.
반응형

최근 개발을 진행하면서 어떻게하면 더 효율적인 코드를 만들 수 있을까 생각해봤어요. 그러던중 이 분야에서는 리팩터링 책이 유명하다는 것을 듣고 읽어보며 따로 정리도 해보려고 해요. 그럼 이제 시작할까요?

오늘은 1장 리팩터링: 첫 번째 예시 파트를 시작해볼게요.


1장 리팩터링: 첫 번째 예시

이번 장은 첫 번째 장이라서 그런지 예시를 통해 설명하는 경향이 있어요. 전반적으로 이 책의 뒷부분에서 사용되는 여러 기법들을 전체적인 관점에서 설명해주고 있어요. 그래서 전체적으로 어떤 방식으로 리팩터링을 진행해야하는지 알아보는데 주의를 집중하는게 좋다고 판단했어요.

그럼 이제 시작해볼까요? 먼저 리팩터링이란 '프로그램이 새로운 기능을 추가하기에 편한 구조가 되게끔 먼저 기능을 추가하기 쉬운 형태로 변경하는 것' 을 의미합니다. 이번 장에서는 이 책에서 제공한 예시를 바탕으로 앞선 내용을 자세하게 설명을 해보겠습니다. 먼저 이 책에서는 공연료 청구서를 출력하는 코드로 예시를 들고 있습니다. 공연할 연극정보, 공연료 청구서에 들어갈 데이터 그리고 공연료 청구서를 출력하는 비즈니스 로직이 들어있는 함수까지 총 3가지가 처음에 제공됩니다. 저자는 이 코드를 더욱 효율적으로 리팩터링하여 다양한 변화에도 유연하게 대처하고 싶어 합니다.

리팩터링의 첫 단계는 리팩터링할 코드 영역을 검사해줄 테스트 코드들을 작성하는 것입니다. 리팩터링에서 테스트의 역할은 굉장히 중요한데, 이는 리팩터링 기법들을 사람이 수행하기 때문에 언제든 실수할 수 있기 때문입니다. 또한 프로그램이 클수록 수정 과정에서 다양한 문제가 발생할 가능성이 큽니다. 이러한 이유로 리팩터링의 첫 단계를 테스트 코드 작성이라고 할 수 있습니다. 여튼 이 부분은 4장에서 조금 더 자세히 알아볼 예정이니 넘어가겠습니다.

리팩터링을 할 때 테스트코드 작성 다음으로 중요한 것은 긴 함수의 내용을 쪼개는 것입니다. 함수를 쪼개기 전 먼저 함수 전체의 동작을 하나의 부분으로 나눌 수 있는 지점을 찾습니다(이 책의 예시에서는 switch문을 가장 먼저 나누어 놓아요.). 여기서는 하나의 부분으로 나눌때 별도의 함수로 추출하는 방식을 사용합니다(함수 추출하기 6.1절). 먼저 별도의 함수로 추출할 때 유효범위를 벗어나는 변수, 즉 새 함수에서는 곧바로 사용할 수 없는 변수가 있는지 확인합니다. 만약 존재한다면, 새롭게 추출한 함수 내부에서 변경이 필요한 변수와 아닌 변수로 나누어 놓고, 값을 변경하지 않으면, 매개변수로 전달하고, 값을 변경해야한다면 함수 내부에서 초기화를 진행한 다음 값을 반환하는 방법을 사용합니다.

이렇게 추출에 성공하고나면, 추출된 함수 코드를 살펴보면서 지금보다 명확하게 표현할 수 있는 간단한 방법은 없는지 검토합니다. 가장 먼저 할 수 있는 것은 변수의 이름을 더 명확하게 바꾸는 것이 있습니다(반환되어야하는 변수가 있다면, 변수명을 result와 같이 변경할 수 있겠죠). 좋은 코드라면 하는 일이 명확히 드러나야하며, 이때 변수 이름은 커다란 역할을 합니다.

다음으로 추출한 함수의 매개변수들이 꼭 필요한지 한 번 더 확인합니다. 이 책의 예에서는 function amountFor(aPerformance, play) 이렇게 매개변수를 받는데 여기서 play는 amountFor() 함수의 외부에서 plays[perf.playID(얘가 aPerformance와 같음)] 이렇게 전달됩니다. 이는 굳이 매개변수로 받지 않아도 되는 변수를 매개변수로 받는 것이기에 제거하는 것이 좋습니다(play 말고 함수 내부에서 plays[aPerformance] 이렇게 따로 사용해도 되기 때문입니다). 이러한 변수들을 제거하기 위해 '임시 변수를 질의 함수로 바꾸기 7.4절' 의 리팩터링 방법을 활용합니다(이러한 로컬 범위의 변수들을 최대한 제거시켜주어야 추출 작업이 수월해집니다).

function playFor(aPerformance) {
    return palys[aPerformance.playID];
}

위의 코드와 같이 play 추출을 함수를 만들어 따로 진행합니다. 이렇게하면 추출한 함수 내부에서 playFor(aPerformance) 로 접근할 수 있으므로 play 매개변수는 사용할 필요가 없어지기에 제거해도 됩니다. 이러한 방식으로 하나 씩 지역 변수를 제거할 수 있습니다. 이렇게 지역 변수를 제거 함으로써 유효범위를 신경 써야 할 대상이 줄어들기 때문에 추출 작업이 훨씬 쉬워진다는 큰 장점이 있습니다.

그럼 이제 계속 리팩터링을 해봅시다. 이 책의 예에서 아래 코드의 volumeCredits 변수와 같이 계속 누산되는 지역 변수가 존재합니다.

let volumeCredits = 0;
for (let perf of invoice.performances) {
    volumeCredits += volumnCreditsFor(perf);

    ...(여러 코드들)
}

당연히 이 지역변수도 따로 함수로 추출해서 제거해야 합니다. 왜냐하면 지역 변수는 자신이 속한 루틴에서만 의미가 있고, local 범위에 존재하는 이름이 늘어나서 함수 추출작업이 복잡해질 수 있기 때문입니다(반복문의 함수 추출 부분이 가장 궁금했는데 계속 읽다보면 되게 비효율적으로 느껴질 수 있어요. 하지만 코드의 재생산성을 높이기 위해 저자는 필요한 과정이라고해요).

반복문을 돌면서 누적되는 변수인 volumeCredits 는 '반복문 쪼개기 8.7절' 을 활용해서 따로 함수로 추출할 수 있습니다. 먼저 아래와 같이 값 누적 로직을 별도 for문으로 분리합니다.

let volumeCredits = 0;
for (let perf of invoice.performances) {
    ...(여러 코드들)
}
for (let perf of invoice.performances) {
    volumeCredits += volumnCreditsFor(perf);
}

보면 ...(여러 코드들) 부분과 volumeCredits 변수의 누적 부분이 분리된 채로 두 번 반복문을 도는 것을 볼 수 있습니다(맞습니다. 이렇게 반복문을 따로 돌려버리면서 따로 추출합니다). 이렇게 분리된 반복문 부분은 이제 함수 추출하기가 용이해집니다. 아래 코드와 같이 따로 함수로 추출합니다.

function totalVolumeCredits() {
    let volumeCredits = 0;
    for (let perf of invoice.performances) {
        volumeCredits += volumnCreditsFor(perf);
    }
}

이렇게하면 성능은 조금 손해봤지만 이 함수를 불러오는 곳에서는 반복문 계산을 할 필요 없이 바로 불러오기만 하면 되기 때문에 코드의 가독성이 좋아지며, 추후 새로운 기능 추가 작업을 진행하기도 용이해진다. 저자는 '리팩터링으로 인한 성능 문제에 대해서 특별한 경우가 아니라면 일단 무시하자' 라고 말한다.

이 글에서는 말하지 않았지만 사실 저자는 이 반복문 하나를 쪼개기 위해서 아래와 같이 작업의 단계를 아주 잘게 나누어서 진행했다. 이 글을 읽는 우리 모두 리팩터링 시에 작은 단계로 나누어 진행해보길 권한다. 이렇게 작은 단계로 나누어야 예상치 못한 오류로부터 자유로울 수 있기 때문이다.

  1. '반복문 쪼개기 8.7절' 로 변수 값을 누적시키는 부분을 분리한다.
  2. '문장 슬라이드하기 8.6절' 로 변수 초기화 문장을 변수 값 누적 코드 바로 앞으로 옮긴다(위 예시에서는 let volumeCredits = 0; 부분).
  3. '함수 추출하기 6.1절' 로 적립 포인트 계산 부분을 별도 함수로 추출한다.
  4. '변수 인라인하기 6.4절' 로 volumeCredits 변수를 제거한다.

이렇게 리팩터링을 진행하면, 거의 모든 부분을 기능별로 함수 추출 가능합니다(저자는 거의 모든 기능을 추출하여 함수화 하려고해요). 여기서 추출된 함수는 내부 함수로 구현되어져 있습니다. 이제부터는 이 내부 함수들을 기능에 맞게 이동시키려고 합니다. 계속 나아가볼까요?

지금까지는 프로그램의 논리적인 요소를 파악하기 쉽도록 코드의 구조를 보강하는데 주안점을 두고 리팩터링을 했습니다. 이는 리팩터링 초기 단계에서 흔히 수행하는 일입니다.

이제 구조는 충분히 개선됐으니 기능 변경, 즉 HTML로의 출력을 위한 버전을 만드는 작업을 살펴보겠습니다(이 책에서는 HTML로의 출력을 위한 기능을 추가해요. 여러분들은 여러분들 만의 기능을 추가하면 된답니다!). 지금은 계산 코드가 기능 별로 모두 분리됐기 때문에, 출력에 대응되는 HTML 버전만 작성하면 됩니다(현재 출력은 텍스트로 출력돼요). 일단 지금은 모든 기능 별 추출된 함수들이 큰 함수 내에 중첩 함수로 들어가있습니다. 이 모두를 복사해서 붙이는 방식으로 HTML 버전을 만들고 싶지는 않기 때문에 '단계 쪼개기 6.11절' 방법을 활용해서 문제를 해결해 보겠습니다. 먼저 전체 함수의 로직을 두 단계로 나눕니다. 첫 번째는 계산 로직을 위해 필요한 데이터를 처리합니다. 두 번째는 앞에서 처리한 결과를 텍스트나 HTML로 표현하도록 만듭니다. 다시 말해 첫 번째 단계에서는 두 번째 단계로 전달할 중간 데이터 구조를 생성합니다.

function statement(invoice, plays) {
    ...(출력 부분과 추출된 중첩 함수들)
}

단계를 쪼개려면 먼저 두 번째 단계가 될 코드들을 '함수 추출하기 6.1절' 로 추출해야 합니다. 현재 상황에서는 위 코드와 같이 출력 부분을 위해 중첩 함수 모두가 필요하므로 아래 코드와 같이 뽑아내야 합니다(이 책의 예에서는 외부 함수의 본문인 출력 부분과 추출된 각 중첩 함수들 모두 renderPlainText() 함수에 담깁니다).

function statement(invoice, plays) {
    return renderPlainText(invoice, plays);
}

function renderPlainText(invoice, plays) {
    ...(출력 부분과 추출된 중첩 함수들)
}

현재는 계산함수들도 같이 추출됐지만, 뒤에서 데이터 처리에 필요한 함수와 표현에 필요한 함수를 적절히 분리합니다.

다음 단계로는 두 개의 단계 사이에서 중간 데이터 구조 역할을 할 객체를 만들어서 renderPlainText() 에 전달합니다(아래 코드를 참고하세요).

function statement(invoice, plays) {
    const statementData = {};
    return renderPlainText(statementData, invoice, plays);
}
function renderPlainText(data, invoice, plays) {
    ...(출력 부분과 추출된 중첩 함수들)
}

위 코드에서 statementData = {}를 사용하면 statementData()함수의 매개변수인 invoice, plays를 제거할 수 있습니다. 방법은 간단합니다. 두 매개변수 모두 중간 데이터 구조인 statementData 객체로 옮기면 됩니다. 이렇게 되면 계산 관련 코드는 전부 statement() 함수로 모으로 renderPlainText()data 매개변수로 전달된 데이터만 처리하면 됩니다. 아래 코드를 통해 어떻게 renderPlainText() 함수에 존재하던 계산 관련 중첩 함수들을 statement() 함수로 옮겨서 statementData 중간 데이터 구조로 옮기는지 보여드리겠습니다.

function statement(invoice, plays) {
    const statementData = {};

    // 아래와 같이 중간 데이터 구조로 데이터 및 중첩 함수를 옮길 수 있음
    statementData.invoice.customer;
    statementData.OOO;
    ...

    return renderPlainText(statementData);
}
function renderPlainText(data) {
    ...(출력 부분 data.OOO으로 계산된 데이터에 접근)
}

위와 같은 중간 데이터 구조로 옮기는 과정이 끝나면, 이제 statement() 함수에서 중첩 데이터 구조를 위한 statementData 객체와 이와 관련된 중첩 함수들을 모두 새로운 함수 내부로 옮긴다음 파일을 새롭게 만든다. 그러면 statement() 함수는 단순히 renderPlainText() 함수만을 call하는 함수가 된다.
이제 HTML 출력을 위한 함수를 따로 만들고 계산 결과를 위한 함수를 불러와 사용하면 아래 코드와 같이 손쉽게 HTML 기능 추가를 할 수 있다.

function htmlStatement(invoice, plays) {
    return renderHtml(계산 관련 함수(invoice, plays));
}

이렇게 리팩터링을 진행하면, 전체 로직을 구성하는 요소는 각각이 더 뚜렷이 부각되고, 계산하는 부분과 출력 형식을 다루는 부분이 분리됐다. 이렇게 모듈화하면 각 부분이 하는 일과 그 부분들이 맞물려 돌아가는 과정을 파악하기 쉬워진다. 모듈화한 덕분에 계산 코드를 중복하지 않고도 HTML 버전을 만들 수 있었다.


이번에는 다형성을 활용해 계산 코드 재구성하기를 알아보겠다.

먼저 다형성의 사전적 의미란 여러개의 형태를 가질 수 있다는 의미이다. 프로그래밍에서 다형성이란 하나의 부모타입참조변수가 여러 자식 타입의 인스턴스를 가질 수 있는 것이다. 다형성에는 오버로딩과 오버라이딩이 포함됩니다.

그럼 여기서 다형성은 왜 사용하는지 알아봅시다. 우선 여기서는 다형성을 사용해서 조건문을 제거하는데 사용합니다. 예를 들어서 연극 장르를 추가하고 장르마다 공연료와 적립 포인트 계산법을 다르게 지정하도록 기능을 수정한다고 가정하겠습니다. 그러려면 현재 이 계산을 수행하는 함수에서 조건문을 수정해야하는데, 이런 형태의 조건부 로직은 코드 수정 횟수가 늘어날수록 유지보수하기 힘들어집니다. 그래서 이를 구조적인 요소로 적절히 보완해야하는데, 이때 다형성이 사용됩니다. 이 책에서는 이러한 과정을 '조건부 로직을 다형성으로 바꾸기 10.4절' 에서 자세히 설명합니다.

다형성을 구성하기 위해서는 상속 계층을 구성해서 하나의 슈퍼 클래스와 기능 별 각각 서브 클래스를 만들고, 각자의 구체적인 계산 로직을 정의하는 것입니다. 이렇게되면 호출하는 쪽에서는 다형성 버전의 계산 함수를 호출하기만 하면 되고, 정확한 계산 로직을 연결하는 작업은 언어 차원에서 처리하게 됩니다.
그런데 JavaScript는 다른 언어와 다르게 생성자가 서브 클래스의 인스턴스를 반환할 수 없기에 추가적으로 팩터리 함수를 만들어서 알맞는 객체를 반환할 수 있게 만듭니다. 여기서 팩터리 함수란 생성자의 역할을 대신 수행하며, 함수가 객체를 반환합니다. 보통 팩터리 함수는 이름이 create로 시작합니다. 책에는 이에 대한 자세한 사항이 '생성자를 팩터리 함수로 바꾸기 11.8절' 에서 나옵니다. 다형성 적용하여 조건문을 적절히 분리하는 방식은 아래의 코드를 통해 간단히 알아보겠습니다.

// 팩터리 함수
function createPerformanceCalculator(aPerformance, aPlay) {
    switch (aPlay.type) {
        case "OOO": return new TragedyCalculator(aPerformance, aPlay);
        case "OOO": return new ComedyCalculator(aPerformance, aPlay);
        default:
            throw new Error('에러 표시');
    }
}

// 슈퍼 클래스
class PerformanceCalculator {
    constructor(aPerformance, aPlay) {
        this.performance = aPerformance;
        this.play = aPlay;
    }

    // 이 함수 없어도 되지만 추후 관리를 위하여 남겨놓음
    get amount() {
        throw new Error('서브 클래스에서 처리하도록 설계되었습니다.');
    }

    get volumeCredits() {
        return OOO;
    }
}

// 서브 클래스1
class TragedyCalculator extends PerformanceCalculator {
    get amount() {
        ...원래 조건문이 었던 부분이 여기 들어옴(TragedyCalculator에 해당하는 부분)
        관련된 조건문의 코드가 여기 들어옴
    }
}

// 서브 클래스2
class ComedyCalculator extends PerformanceCalculator {
    get amount() {
        ...원래 조건문이 었던 부분이 여기 들어옴(ComedyCalculator에 해당하는 부분)
        관련된 조건문의 코드가 여기 들어옴
    }

    get volumeCredits() {
        return super.volumeCredits + OOO;
    }
}

위와 같이 다형성을 사용하면 계산 코드들을 함께 묶어둘 수 있습니다. 앞으로는 이렇게 명확하게 분리된 코드를 수정하기만 하면 됩니다. 이렇게 하지 않으면 각 새로운 무언가가 추가되면 새로운 조건을 따로 만들어야하고 나중에는 조건부 로직이 복잡해지게 되어 유지보수의 어려움이 발생합니다.

번외로 React에서 이러한 다형성을 최대한 모방해 봤습니다.

Card 컴포넌트를 가지고, 이를 확장하여 UserCardProductCard 컴포넌트를 만듭니다.

// 기본 "Card" 컴포넌트
function Card({ title, description }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
    </div>
  );
}

// "UserCard" 컴포넌트는 "Card"를 확장하여 사용자 정보를 표시합니다.
function UserCard({ user }) {
  return <Card title={user.name} description={user.email} />;
}

// "ProductCard" 컴포넌트는 "Card"를 확장하여 제품 정보를 표시합니다.
function ProductCard({ product }) {
  return <Card title={product.name} description={product.description} />;
}

// 이제 어떤 데이터를 표시할지에 따라 UserCard 또는 ProductCard를 사용할 수 있습니다.
function App() {
  const user = { name: 'John Doe', email: 'john@example.com' };
  const product = { name: 'Apple', description: 'Fresh and juicy!' };

  return (
    <div>
      <UserCard user={user} />
      <ProductCard product={product} />
    </div>
  );
}

export default App;

위 코드에서 UserCardProductCard 컴포넌트는 모두 Card 컴포넌트를 활용하지만, 각기 다른 방식으로 내용을 표현합니다. 이렇게 특정 컴포넌트를 기반으로 서로 다른 동작을 하는 컴포넌트를 생성하는 것이 다형성과 유사한 개념입니다. 이를 통해, 개발자는 컴포넌트의 재사용성을 높이고 코드를 보다 효율적으로 관리할 수 있습니다.

HOC(Higher-Order Component)를 사용해서도 컴포넌트의 재사용성을 높일 수 있습니다. HOC는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수입니다. 이를 통해 공통적인 로직을 HOC로 추출하고, 이를 다양한 컴포넌트에서 재사용할 수 있습니다. 아래 코드는 그 예시입니다.

// 고차 컴포넌트 (HOC) withCard
const withCard = (Component) => ({ title, description, ...props }) => {
  return (
    <div>
      <h2>{title}</h2>
      <p>{description}</p>
      <Component {...props} />
    </div>
  );
};

// 일반 컴포넌트
const UserDetail = ({ user }) => <p>Email: {user.email}</p>;
const ProductDetail = ({ product }) => <p>Price: {product.price}$</p>;

// HOC를 사용하여 컴포넌트 확장
const UserCard = withCard(UserDetail);
const ProductCard = withCard(ProductDetail);

// 이제 어떤 데이터를 표시할지에 따라 UserCard 또는 ProductCard를 사용할 수 있습니다.
function App() {
  const user = { name: 'John Doe', email: 'john@example.com' };
  const product = { name: 'Apple', description: 'Fresh and juicy!', price: 20 };

  return (
    <div>
      <UserCard title={user.name} description="User Info" user={user} />
      <ProductCard title={product.name} description={product.description} product={product} />
    </div>
  );
}

export default App;

위 예제에서 UserCardProductCard 컴포넌트는 각각 다른 컴포넌트(UserDetail, ProductDetail)를 기반으로 withCard HOC를 사용해 생성되었습니다. 이렇게 고차 컴포넌트를 사용하면 특정 컴포넌트의 기능을 확장하고 다른 컴포넌트로부터 이를 분리함으로써 코드의 재사용성을 높이고 다형성을 어느 정도 달성할 수 있습니다.


마치며

리팩터링은 크게 세 단계로 진행합니다.

  1. 원본 함수를 중첩 함수 여러 개로 나눈다.
  2. '단계 쪼개기 6.11절' 를 적용해서 게산 코드와 출력 코드를 분리한다.
  3. 계산 로직을 다형성으로 표현한다.

또한 리팩터링을 효과적으로 하려면 아래 세 가지를 따르면 됩니다.

  1. 단계를 잘게 나눠 더 빠르게 처리할 수 있게 한다.
  2. 코드는 절대 깨지지 않는다.
  3. 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 깨닫는다.

저자는 좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가' 라고 합니다.

우리 모두 이 책을 읽고 가독성이 좋고 수정하기 쉬운 좋은 코드를 작성해나갑시다.

반응형

'Develop > JavaScript' 카테고리의 다른 글

Refactoring 2판 3장  (0) 2023.08.29
TypeScript의 구조적 타이핑과 Brading  (0) 2023.08.23