JS - 리펙토링과 테스트코드

sarang_daddy·2023년 4월 25일
0

Javascript

목록 보기
21/26
post-thumbnail
post-custom-banner

Intro

소프트웨어 개발 과정에서 처음부터 완벽한 코드를 작성하는 것은 어렵기 때문에,
우리는 지속적으로 리펙토링을 수행해야 한다.

과연 나는 제대로된 리펙토링을 수행한 것일까..?
전체적인 코드의 개선이나 불필요한 코드의 제거가 아닌 경우에도 코드가 수정되었으면
Reractor라는 커밋 타이블을 무분별하게 사용하고 있었다. 🥲

"리펙토링 어떻게 하는거지?", "리펙토링 멋지게 하는 방법" 등을 알기 전에

"왜 리펙토링이 필요하지?"
"테스트코드는 왜 필요하지? 언제 사용하는거지?"

필요성에 대해서 먼저 이해하고 리펙토링과 테스트코드에 대해 학습해보자.


리펙토링?

리펙토링이란 간단히 말하면 코드를 변경하여 가독성, 유지 보수성, 확장성 등을 향상시키는 과정이다.
즉, 새로운 코드를 작성하는게 아닌 기존 코드를 개선하여 좀 더 효율적으로 유지 보수가 쉬운 형태로 만드는 것이다.

리펙토링을 수행하면 코드의 가독성이 높아지고, 버그를 찾기 쉬워지며, 변경 사항을 적용하는 것이 쉬워진다.
이러한 이유로 리펙토링은 코드의 품질을 향상시키는 중요한 작업이다.

여기서 중요한 점은 리펙토링은 코드의 동작을 변경하지 않는 다는 것이다.
즉, 새로운 기능을 추가하거나 기능을 수정하는 것이 아니다.

그렇다면 방대한 양의 코드를 리펙토링하는 과정에서 기존의 코드 로직
즉, 기존의 동작이 변경되거나 오류가 발생하면 안된다는 것이다. (🚫 side-effect 방지)

코드의 품질을 향상 시키는 중요한 리펙토링, 하지만 방대한 양의 코드안에서
동작의 변경이나 예상치 못한 부작용을 피하기 위해서는 어떻게 해야할까?

테스트코드가 필요한 이유다.

리펙토링을 수행하기 전에 기존 코드의 테스트 커버리지를 확인하고,
변경 후에도 동일한 테스트를 수행하여 코드 변경의 영향을 확인하는 것이 중요하다.

리펙토링과 테스트코드는 떨어질 수 없으며 떨어져서도 안되는 "한몸" 이다.


테스트코드?

테스트코드는 이름 그대로 소프트웨어의 기능을 테스트하고 검증하기 위해 작성된 코드다.
주로 자동화된 테스트를 수행하며, 특정 함수 또는 모듈의 작동 여부를 확인하거나,
전체 시스템의 동작을 검증하는데 사용된다.

소프트웨어 테스트의 종류와 수준

소프트웨어 테스트에는 여러 가지 종류와 수준이 있다.
그 중 대표적인 것이 The Four Levels of Software Testing 이다.
참고자료 : The Four Levels of Software Testing

  • Unit Testing (단위 테스트)
    소스 코드의 각각의 단위(주로 함수 또는 메소드)를 분리된 환경에서 테스트하는 것이다.
    일반적으로 빠르고 자동화된 테스트이며, 코드의 버그를 빠르게 찾아내어 개발 생산성을 높여준다.

  • Integration Testing (통합 테스트)
    단위 테스트에서 통합된 코드의 상호작용을 테스트하는 것이다.
    서로 다른 단위 테스트를 통합하여 전체적인 기능이 동작하는지 확인한다.
    통합 테스트는 종종 모의 객체(Mock Object)와 같은 도구를 사용하여 다른 시스템 또는 구성 요소와의 상호작용을 시뮬레이트한다.

  • System Testing (시스템 테스트)
    전체 시스템의 기능과 성능을 테스트하는 것이다.
    시스템의 명세서와 요구사항을 기반으로 테스트 케이스를 작성한다.
    기능적, 비기능적인 측면에서 테스트를 수행한다.

  • Acceptance Testing (인수 테스트)
    최종 사용자가 시스템이 요구사항과 명세서에 따라 적합한지 테스트하는 것이다.
    이 테스트는 주로 고객과 사용자의 요구사항을 충족하는지 확인하는 데 사용된다.
    인수 테스트는 사용자 시나리오와 실제 데이터를 사용하여 수행될 수 있다.

이러한 네 가지 수준의 테스트는 소프트웨어 개발 과정에서 각각 다른 시기와 목적에 맞추어 적용된다.
이를 통해 소프트웨어의 품질을 개선하고, 버그를 발견하여 수정하는 등의 작업을 수행할 수 있다.

테스트코드 구현해보기

우선 프로그래밍의 최소단위(함수, 메서드)를 테스트 하는 Unit Test를 연습해보자.
테스트 코드 구현을 잘하기 위해서 아래 두가지의 기본적인 방법을 먼저 연습하자.

1. 최소단위 함수부터 테스트하기.
반환값이 명확히 존재하고, 다른 함수를 호출하지 않는 함수부터 테스트 한다.
즉, dependency 가 없는 함수.

const coffeeMakingMap = {
  americano: () => "물" + "," + "커피원두",
  cappuccino: () => "우유" + "," + "커피원두",
};

// 최소 단위의 함수
function makeCoffee(orderMenu, orderCount) {
  const result = [];
  for (let i = 0; i < orderCount; i++) {
    result.push(coffeeMakingMap[orderMenu]());
  }
  return result;
}

// 테스트 코드
let expected = ["물,커피원두"];
console.log(expected.toString() === makeCoffee("americano", 1).toString());
// 성공(true)

expected = ["우유,커피원두", "우유,커피원두", "우유,커피원두"];
console.log(expected.toString() === makeCoffee("cappuccino", 3).toString());
// 성공(true)

expected = ["우유,커피원두"];
console.log(expected.toString() === makeCoffee("americano", 1).toString());
// 실패(false)

🧐 더 알아가기

위 예제에서 expected.toString()을 사용한 이유는 === 연산자의 경우 두 피연산자가 같은 객체의 경우에만 true를 반환하기 때문이다.
배열은 객체이므로 두 객체의 값이 같더라도 다른 객체일 수 있다. (객체의 값은 참조에 의한 전달이기 때문에)

var person1 = { name: 'Lee'};
var person2 = { name: 'Lee'};
// person1 변수와 person2 변수가 가리키는 객체는 비록 내용은 같지만 다른 메모리에 저장된 별개의 객체다.
// 즉, person1 변수와 person2 변수의 참조 값은 전혀 다른 값이다.
console.log(person1 === person2); // false

위 예제 처럼 테스트 코드를 작성 및 호출하여 확인이 가능하지만,
사람이 직접 모든 함수를 호출하는 것은 불가능하다.
라이브러리를 통해 테스트코드를 조금 더 쉽게 구현할 수 있다.

아래 예제는 Node.js에서 기본으로 제공되는 내장 모듈인
node:testnode:assert를 사용하여 테스트 코드를 작성한 내용이다.

// ES module를 사용하기위해 package.json을 깔아준다. 

npm init -y     

node:test 모듈은 Node.js의 내장 테스트 프레임워크인 assert 모듈과 연동하여 테스트 코드를 작성하도록 도와주는 도구다.
assert 모듈은 테스트에서 사용되는 assert 함수를 제공하여, 예상되는 값과 실제 값이 일치하는지 비교할 수 있게 해준다.

2. given -> when- > then 패턴 사용하기.
일관된 방식의 테스트 코드 구현을 위해서 가장 많이 사용되는
given(테스트에 필요한 값 셋팅) -> when(실행) -> then(테스트)
방식으로 테스트를 수행해보자.

// index.js
const coffeeMakingMap = {
  americano: () => "물" + "," + "커피원두",
  cappuccino: () => "우유" + "," + "커피원두",
};

export function makeCoffee(orderMenu, orderCount) {
  const result = [];
  for (let i = 0; i < orderCount; i++) {
    result.push(coffeeMakingMap[orderMenu]());
  }
  return result;
}

// index.test.js
import test from "node:test";
import assert from "node:assert";

import { makeCoffee } from "./index.js";

test("makeCoffee 커피를 잘 만드는지 확인", () => {
  //given
  const orderCount = 3;
  const orderMenu = "americano";

  //when
  const result = makeCoffee(orderMenu, orderCount);

  //then
  const expected = ["물,커피원두", "물,커피원두", "물,커피원두"];
  assert.deepEqual(expected, result);
});
// Node.js의 watch 옵션으로 테스트 코드 실행이 가능하다.

node --watch index.test.js   

index.js 내용이 변경(리펙토링)될 때마다 자동으로 함수를 호출하여 테스트 결과를 보여준다.

참고자료 : 프롱트

리펙토링 해보기

위에서 연습한 코드를 리펙토링 해보자.

const coffeeMakingMap = {
  americano: () => "물" + "," + "커피원두",
  cappuccino: () => "우유" + "," + "커피원두",
};

export function makeCoffee(orderMenu, orderCount) {
  const result = [];
  for (let i = 0; i < orderCount; i++) {
    result.push(coffeeMakingMap[orderMenu]());
  }
  return result;
}
  • makeCoffee(orderMenu, orderCount)의 매개변수 순서가 잘못되는 경우가 있을 수 있다.
  • 매개변수를 객체로 전달 할수 있다.
  • {orderMenu, orderCount}로 파라미터를 전달하고 받으면 순서의 오류를 피할 수 있다.
  • 이를 구조 분해 할당(destructuring) 이라 한다.

참고자료 : 구조 분해 할당

// index.js
const coffeeMakingMap = {
  americano: () => "물" + "," + "커피원두",
  cappuccino: () => "우유" + "," + "커피원두",
};

// 테스트를 위해 파리미터 순서 변경
export function makeCoffee({ orderCount, orderMenu }) {
  const result = [];
  for (let i = 0; i < orderCount; i++) {
    result.push(coffeeMakingMap[orderMenu]());
  }
  return result;
}

// index.test.js
import test from "node:test";
import assert from "node:assert";

import { makeCoffee } from "./index.js";

test("makeCoffee 커피를 잘 만드는지 확인", () => {
  //given
  const orderCount = 3;
  const orderMenu = "americano";

  //when
  const result = makeCoffee({ orderMenu, orderCount });

  //then
  const expected = ["물,커피원두", "물,커피원두", "물,커피원두"];
  assert.deepEqual(expected, result);
});

객체를 이용한 파라미터 전달로 순서가 달라도 오류가 없다.

Outro

리펙토링과 테스트코드에 대해서 간단히 알아보고 왜 필요한지 그리고 왜 "한몸"인지 확인해 보았다.

개발자에게 리펙토링은 필수 과정이며 이를 위해서는 테스트 코드가 함께 해야 한다.
그리고 테스크 코드를 구현하기 위해서는 코드가 간결하고 최소단위여야 하며 순수함수들로 이루어 질수록 구현이 용이함을 알 수 있었다.

순수함수, 함수형 프로그래밍, 클린코드들이 왜 중요한지도 알게되었다.

코드를 작성 할때는 항상 아래의 내용들을 고려하며 작성하도록 노력해보자.
참고 - 코드스쿼드 마스터즈 수업

가독성있는 코드

  • 매직 넘버 제거
  • 조건문도 없앨 수 있을지 고민하기(객체리터럴 활용 등)
  • 반복문을 줄이고 고차함수를 활용하기.
  • 네이밍은 그 변수, 함수, 클래스가 가진 역할을 빠짐없이 정확히 표현.

함수와 객체(클래스)의 역할

  • 함수, 객체 모두 한 가지 역할로 한정.(Single Responsibility Principle)
  • 한가지 역할을 정하고 그 역할에 맞는 네이밍인지 확인.
  • 함수의 경우 동일한 입력에 동일한 결과가 나오는 것인지 확인(pure function)
  • 로직 분리의 대상은 자주 변경되거나, 변경될 확률이 높은 부분을 선택함으로써 결함도 낮추기.
    - 따라서 현재 문제가 되는 부분이 무엇인지, 앞으로 어떤 부분이 변경될지를 탐구해야 함.

함수와 클래스의 선택

  • 어디에 함수를 사용하고, 클래스를 사용하는 것이 적당할지.
  • 생성자가 없는 클래스는 함수나 객체리터럴 선택을 고려
  • 객체를 여러개 생성하게 된다면 클래스표현을 활용

중복 기능 제거

  • 공통되는 코드를 분리한다.
  • 2~3번 중복이라면 분리하는 것이 좋은 편.
  • 객체마다 공통되는 메서드가 보이면 상속을 고려
  • 범용적인 기능이면 별도의 헬퍼객체에 함수를 포함

단순한 비동기 처리 코드

  • 비동기 로직도 동기적인 순서의 흐름과 비슷하게 동작하도록 구현.
  • 콜백 지옥이 아닌 Promise 패턴이나 await 방식.
  • Promise를 사용해도 콜백지옥으로 구현되어 있지 않은지 확인.
profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.
post-custom-banner

0개의 댓글