테스트코드, 리팩토링 입문기

hohooodo·2021년 11월 19일
20

웹 개발

목록 보기
2/3

들어가며

작년에 완성한 첫 프로젝트가 있었다. 1년이 지난 지금 자바스크립트에 대해서 더 알게 되었고, 프로젝트를 리팩토링 하고 싶은 마음이 생겼다. 이러한 마음을 가지고 예전에 작성한 코드를 보게 되었다.

코드를 보는 내내 한숨만 나왔다. 대략 700줄 정도 되는 코드를 읽고 있자니 내가 왜 이렇게 작성했는지 알 수 없는 이상한 코드뿐이었다. 기본적인 컨벤션도 지키지 않은 채 제멋대로 작성하고, 선언 키워드도 의미 있게 작성하지 못했다.

부끄럽지만 그런 코드 중 하나의 함수를 가져왔다.

여러 문제가 보이는가? 상수는 제멋대로 카멜 케이스, 매개변수는 하나도 안 받고 모두 함수 밖의 변수를 참조하고 있다. 반환문은 어떤가? state 식별자를 통해 의미 없는 할당 문을 남발하고 있다.

물론 로직은 원하는 대로 동작한다. 하지만, 위 코드는 아찔하게 쌓아놓은 젠가와 비슷하다. 기능을 추가하려고 또는 리팩토링을 하려고 손을 대는 순간 전체가 무너지고 만다. 전역으로 선언한 변수들이 이곳저곳에서 영향을 받았기 때문이다.

그러면 어떻게 함수를 리팩토링 해야할까? 원래의 로직을 보존하면서 함수를 변경하는 방법이 테스트코드를 작성하는것이다.

1. 여러 테스트

테스트에도 여러 가지 종류가 있다. 원하는 목적에 따라서 테스트를 정하게 된다.

E2E 테스트

End to End 테스트, 말 그대로 끝에서 끝까지 테스트하는 것을 말한다. 애플리케이션이 제대로 동작하는지 기능을 테스트하는 것이다. 사용자 관점에서 내가 구현한 클릭, 입력폼 등이 제대로 동작하는지 확인하는 테스트다.

통합 테스트

하나 이상의 시스템들이 제대로 상호작용하며 원하는 대로 작동하는지 테스트하는 것을 말한다. 가령 서버와 클라이언트가 제대로 상호작용하고 있는지 확인하는 테스트다.

유닛 테스트

소스 코드 중 가장 작은 단위의 코드를 테스트하는 것을 말한다. 함수, 메서드 단위로 잘 동작하는지 확인하는 테스트다.

2. 테스트와 리팩토링

유닛 테스트를 시작하기에 앞서 코드를 작성할 때 가져야 할 마음가짐에 대해 알아보자. 코드를 작성할 때 개발자는 단 하나의 모자만 쓸 수 있고 모자의 종류는 두 개로 나눌 수 있다.

  1. 기능개발 모자 쓰기🧢
  2. 리팩토링 모자 쓰기🎩

개발할 때는 두 가지 모자 중 단 한 가지만 쓸 수 있다는 것을 알아야 한다. 기능개발을 할 때는 새로운 기능을 개발하는 모자, 리팩토링할 때는 리팩토링 모자. 각 상태에서 어떤 자세를 가지고 개발을 해야 하는지 알아야 한다. 리팩토링 할 때는 소프트웨어의 기능을 변경하지 않도록 해야 한다.
-켄트 백-

위 이야기의 요점은 한 가지 일에만 집중하라는 것이다. 기능구현과 리팩토링을 할 때 각각 하나만 생각하고 있으면 더욱 개발에 몰입할 수 있다. 기능구현 하며 리팩토링까지 생각한다면 코드의 작성 시간은 더욱 느려지게 될 것이다. 지금 당장 작성한 코드가 최선의 코드인지 계속 생각해야 하기 때문이다.

3. 유닛 테스트 시작하기

3.1. Jest

페이스북에서 만든 테스팅 라이브러리다. 간단하게 다운로드 받을 수 있으며 공식문서를 보며 사용해보자.

  • jest 다운로드 받기
npm install --save-dev jest
  • package.json 추가하기
{
  "scripts": {
    "test": "jest"
  }
}

3.2. 더하기 함수

1. 실패하는 테스트코드 작성하기

function add(a, b) {}

test('3 + 7 은 10 입니다.', () => {
	expect(add(3, 7)).toBe(10);
});

test('2 + 3 은 5 입니다.', () => {
	expect(add(2, 3)).toBe(5);
});
npm test

아직 add함수 안에 코드를 작성하지 않았기 때문에 FAIL이 나오게 된다.

2. 기능개발 모자 쓰기🧢

실패하는 테스트 코드를 잘 작성했으니 이제 기능구현을 해보자.

function add(a, b) {
	return a + b;
}

test('3 + 7 은 10 입니다.', () => {
	expect(add(3, 7)).toBe(10);
});

test('2 + 3 은 5 입니다.', () => {
	expect(add(2, 3)).toBe(5);
});

3. 리팩토링 모자 쓰기🎩

구현한 기능이 제대로 동작하는 것을 알았으니 리팩토링 단계로 넘어가자. 기존 함수 형식을 보면 함수 선언문으로 이루어져 있다. 이를 함수 표현식으로 바꾸고, 간단한 함수이므로 화살표 함수로 변경해보자.

const add = (a, b) => a + b;

test('3 + 7 은 10 입니다.', () => {
	expect(add(3, 7)).toBe(10);
});

test('2 + 3 은 5 입니다.', () => {
	expect(add(2, 3)).toBe(5);
});

원하는 기능이 제대로 동작하고, 보기 좋은 코드로 리팩토링까지 잘 된 모습을 볼 수 있다. 이렇게 완성한 함수는 메인 로직에서 자신감을 가지고 사용할 수 있다. 다른 예제로 테스트 코드를 작성해보자.

3.3. 프로퍼티 값 뽑기 함수

1. 실패하는 테스트코드 작성하기

배열안에 객체가 담겨있고, 해당 객체에서 원하는 프로퍼티 의 값만 빼서 따로 배열을 만들어보자.

const todos = [
  { id: 3, content: 'HTML', completed: false },
  { id: 2, content: 'CSS', completed: true },
  { id: 1, content: 'Javascript', completed: false }
];

function f(todos, key) {

}

test('id 프로퍼티로 배열을 뽑으면 [3,2,1]', () => {
  expect(f(todos, 'id')).toEqual([3, 2, 1]);
});

2. 기능 개발 모자 쓰기🧢

todos배열을 순회하며 프로퍼티 값을 뽑아 새로운 arr 배열에 담아야 하므로, for...of 문으로 순회하며 push를 통해 요소를 추가하면 제대로 동작할 것이다.

const todos = [
  { id: 3, content: 'HTML', completed: false },
  { id: 2, content: 'CSS', completed: true },
  { id: 1, content: 'Javascript', completed: false }
];

function f(todos, key) {
	const arr = [];
	for(const a of todos){
		arr.push(a[key]);
	}
	return arr;
}

test('id 프로퍼티로 배열을 뽑으면 [3,2,1]', () => {
  expect(f(todos, 'id')).toEqual([3, 2, 1]);
});

3. 리팩토링 모자 쓰기🎩

원하는 대로 동작하는 코드를 완성했다. 이제 여기서 코드를 리팩토링 해보자.

  • 의미 있는 함수 이름
  • 배열을 리턴하므로 map 고차함수 사용하기
  • 화살표 함수로 간단하게 표현하기
const todos = [
  { id: 3, content: 'HTML', completed: false },
  { id: 2, content: 'CSS', completed: true },
  { id: 1, content: 'Javascript', completed: false }
];

const getValues = (todos, key) => todos.map(obj => obj[key]);

test('id 프로퍼티로 배열을 뽑으면 [3,2,1]', () => {
  expect(getValues(todos, 'id')).toEqual([3, 2, 1]);
});

여전히 테스트 코드는 잘 동작하고 클린한 코드도 완성했다고 할 수 있다. 이제 이 함수를 가지고 원래 로직에 사용하면 될 것이다.

마치며

꼭 해야 할까요?

위 예제를 테스트코드 기반으로 리팩토링하며 잘 동작하고 이전보다 더 나은 코드라고 할 수 있다. 아주 작은 개선사항이라도 괜찮다. 이런 식으로 코드를 계속 클린하게 만든다면 이들이 모여서 많은 코드가 깨끗해질 것이다.

물론 유닛 테스트가 항상 옳은 것은 아니다. 테스트 코드를 작성하기 위해서는 순수함수로 이루어져 있어야 한다. 하지만 개발을 하다 보면 비 순수함수를 만들 수밖에 없는 상황이 나오곤 한다. 이때는 통합 테스트, E2E 테스트 등 다양한 테스팅 방법을 사용해야 한다.

그럼에도 불구하고 테스트는 해야할까? 테스트도 모두 비용이다. 테스트 코드를 작성할 시간에 새로운 기능을 만들 수 있는 기회비용을 포기하고 테스트 코드를 작성하는 것이다. 마틴 파울러는 리팩토링의 이유에 대해 이렇게 말한다.

리팩토링을 하는것은 경제성 때문이다. 개발자가 하는 모든 것들은 더 많은 기능을 더 빠르게 적용하기 위함이다. 이게 바로 유일한 '리팩토링을 해야 하는 이유' 리팩토링을 했을 때 기간을 길게 보자. 나중 기능추가는 더욱 빨라질 수밖에 없다.
그러므로 기억하자. 리팩토링은 경제성 때문에 해야 한다. 깔끔한 코드는 더 빠른 기능개발을 할 수 있다."
-마틴 파울러-

개발자는 글을 쓰는 작가.

개발자가 코드를 작성하는 것, 작가가 글을쓰는 것 이 둘은 매우 비슷하다고 생각한다. 나만의 코드(글)를 작성하고 작은 코드들이 모여 하나의 제품(책)을 만들어간다. 글쓰기의 세계에서도 리팩토링과 비슷한 '퇴고'가 있다.

퇴고는 초고를 바탕으로 수정, 보완하고 정리하는 작업을 뜻한다. 퇴고를 꼼꼼하게 하는 것은 좋은 글을 만드는 기본 요건에 해당하므로 초고 작성 못지않게 중요한 과정이다. 퇴고의 3원칙을 아는가?

  1. 쓰인 글에서 빠진 부분과 부족하다고 느껴지는 부분을 찾아 보완해야 한다.
  2. 불필요한 부분이 들어가 있거나 지나치게 많이 들어간 것들을 찾아 삭제해야 한다.
  3. 글의 순서를 바꾸었을 때 더욱 효과적일 부분은 없는지 살펴보고, 문장 구성을 변경하여 주제 전개의 양상을 부분적으로 고친다.

어디서 많이 본 원칙이지 않은가? 이는 리팩토링의 단계와도 비슷하다. 좋은 글(코드)을 작성하기 위해 퇴고는 필요충분조건이다. 글쓰기 대가들도 끊임없이 고치는 것을 반복한다.

어니스트 헤밍웨이는 자신의 역작 '노인과 바다'를 400번 이상 고쳐 쓴 뒤 퓰리처상과 노벨문학상을 받을 수 있었다. 그는 초고를 완성한 뒤 몇 번이고 다시 읽으며 불필요한 단어 하나, 문장 한 줄, 쉼표와 마침표 하나까지 덜어내기 위해 고심했다. 언젠가 나는 헤밍웨이가 되고 싶다. 내 코드 하나하나 고심하며 완성된 코드를 작성하고 싶다.

참고자료
Martin Fowler. 2014년07월. Workflows of Refactoring. OOP2014. 미국

profile
글을 잘쓰는 개발자가 되고싶습니다

1개의 댓글

comment-user-thumbnail
7일 전

잘보고 갑니다~!

답글 달기