테스트 주도 개발이 뭘까?

October 25, 2023 (1y ago)

테스트 주도 개발을 하기 위해서는 코드의 응집도는 높고 결합도는 낮아야 한다. 그렇다면 여기서 응집도와 결합도는 무엇을 의미하는가? 이에 대해서 확실하게 짚고 넘어가야 한다.

코드의 응집도

응집도(cohesion)는 모듈 내부에 존재하는 구성 요소들 사이의 밀접한 정도를 이야기 한다. 이 뜻을 의미상으로 분석해보면 이러하다.

"선생님은 반 아이들의 순서를 이름의 가나다 순으로 정리했다.", "선생님은 정리된 순서에 따라 아이들에게 번호를 부여해주었다.","선생님은 반 아이들의 이름과 번호를 호출했다."

위의 예제는 응집도가 높은 말일까? 이를 판단하기 위해서 주체가 무엇을 하고 있는지를 봐보자. 여기서 주체는 누구일까? 선생님이다. 그렇다면 선생님이 집중하고 있는 대상은 누구인가? 학생들이다. 그렇다면 대상의 무엇에 집중하고 있는가? 그 중에서도 학생들의 정보를 정리하고 호출하는데 집중하고 있다. 현재 주체는 대상의 정보를 정리하는데 집중하고 있다는 것을 알 수 있다.

이렇듯 주체(코드에서는 class나 함수)가 하나의 목적을 달성하기 위해 집중하고 있을 경우 응집도가 높다고 표현한다.

코드의 결합도

결합도(coupling)은 모듈과 모듈 사이의 관계에서 관련 정도를 나타낸다. 모듈 간에는 관련이 적을수록 상호 의존성이 줄어 모듈의 독립성이 높아지고 독립성이 높으면 모듈 간에 영향이 적어 좋은 셜계가 된다.

예를들어 생각해보자

class Parent{
	givingPocketMoney() {
		Child.receivingPocketMoney();
		return pocketMoney;
	}
	...
}

class Child{
	static receivingPocketMoney(){...}
}

위의 경우 부모님에게 용돈을 받기 위해서 하기 위한 행동은 아이가 부모님에게 요청을 용돈을 요구하면 주는 형식으로 구성이 되어 있다. 아이가 모듈 내에서 어떠한 요구조건(메소드)를 추가하더라도 부모의 입장은 변하지 않는다. 반면에 아래의 경우를 봐보자.

class Parent{
	constructor(){
		this.mother = new Mother();
		this.father = new Father();
	}

	givingPocketMoney() {
		const child = new Child();
		const motherIsOk = this.mother.to(child.explainHowToUseIt());
		const fatherIsOk = mother.negotiateWith(this.father);

		if(fatherIsOk && motherIsOk) {
			return pockeMoney;
		}
	}
}

class Child{
	receivingPocketMoney() {

	}

	exlpainHowToUseIt() {
		this.explainWhenToUseIt();
		this.explainIsItAppropriateSum();
	}
	explainWhenToUseIt() {...}
	explainIsItAppropriateSum(){...}
}

위의 경우 아이가 용돈을 받기 위해서는 아빠의 허락이 떨어져야 하고 엄마의 허락까지 떨어져야지 용돈을 받을 수 있다. 만약 여기서 아빠의 요구는 그대로이지만 엄마의 요구 조건이 바뀐다면 어떻게 될까?

아이는 바뀐 요구 조건에 따라서 메소드들을 변경하여야 한다. 또한 아빠와 협상을 해야 하므로 아빠 또한 협상 내용에 따라 메소드를 변경해야 할 것이다. 이와 같이 하나의 행동의 변화가 서로에게 미치는 영향이 너무 클 경우 결합도가 높다고 한다.

위에서 설명한 것과 같이 서로가 서로에게 미치는 영향이 낮을 수록 결합도가 낮고 수정해야하는 필요가 줄어들기 때문에 코드 내에서 결합도는 낮게끔 유지해야 한다.

그래서 TDD가 뭘까?

TDD(Test Driven Development) 테스트 주도 개발이다. 이 개념을 이해하는것이 상당히 어려운 일이었다.

TDD는 개발의 순서를 바꾸자는 개념이다. 기존에 코드를 작성한다고 하면 유닛 단위로 필요한 기능들을 우선 작성하고 그 기능이 정상적으로 작동하는 단계를 거친다. 하지만 TDD는 기능 리스트를 작성하고 그 리스트에 맞춘 테스트 코드를 먼저 작성한다. 그리고 테스트 코드가 작동할 수 있게끔 코드를 수정해나간다. 단계가 아래의 그림과 같다.

![[./tdd.png]]

  • Red: 테스트 단언 만을 이용하여 테스트 코드를 위한 조건만을 적어 놓은 상태
  • Green: 정적인 정답값, 입력 값, 아직 정의되지 않은 클래스 등등을 이용하여 코드가 컴파일을 통과할 수 있게 작성한 후 코드가 컴파일 뿐만이 아니라 작동할 수 있도록 메소드, 클래스, 함수등을 추가한 경우
  • Refactor: 앞선 Green 단계로 인해서 반복되는 중복을 제거하고 이름을 가다듬는 과정

위의 경우를 흔히 TDD 과정이라고 이야기 한다. 하지만 위의 과정에서 끝나는 것이 아닌 더 디테일 한 부분들이 많이 남아 있다.

RED

처음 코드를 작동하지 않게끔 빨리 작성하라는 이유는 우선 만들고자 하는 애플리케이션의 기능을 단순히 머리로 정리하는 것이 아닌 글로써 정리하여 수 많은 기능들을 동시에 생각하는 것이 아닌 쪼개서 생각하자는 취지이다. 이 과정들에 대해서 이야기 해보자.

  • 최대한 러프하게 작성하여 빠르게 테스트 한다.
  • 기능리스트를 어떤 방향으로 작성할 것인가? 아는것에서 모르는것? 위에서 아래로? 아래에서 위로?
  • 필요한 기능리스트는 무엇이 있는가?
  • 기능리스트들을 어떻게 잘게 쪼개서 테스트를 진행할 것인가?

Divide and Conquer, 알고리즘 문제들을 풀어나가다 보면 자연스럽게 만나게 되는 방법을 코드 작성에서도 사용하자는 것이다. 이러한 작업에서 중요한 부분은 각각의 부분은 독립적이어야 한다는 것이다. 모든 테스트가 서로에게 영향을 주어서는 안된다는 것을 잊지 말자!

  • 오퍼레이션을 어디에 두어야 하나?
  • 적절한 입력 값은 무엇인가?
  • 이 입력들이 주어졌을 때 적절한 출력은 무엇인가?

GREEN

개인적으로 책을 읽으면서 느낀 것은 이 부분이 굉장히 중요하다는 것이다. 이 과정에서는 결정해야 하는 여러가지 부분이 있다.

  • 작성한 테스트 코드를 위한 메소드의 이름을 뭐라고 할 것인가?
  • 메소드의 기능을 포함한 class의 이름은 뭐라고 할 것인가?
  • 테스트 코드를 통과하기 위한 데이터는 무엇인가?
  • 의미 있는 데이터는 무엇인가?

이외에도 신경써야 하는 부분은 수 없이 많다. 데이터는 실제로 사용되는 데이터를 사용해야 하며 여러가지 디자인 패턴도 사용해줄 필요가 있다. 이보다 더 한 추가 사항은 추후에 업데이트 하기로 하겠다.

Refactor

위에서 한 말은 거짓말이다 Refactor가 더 중요하다. 사실 둘다 너무 중요하다. 하지만 난이도로 따지면 GREEN의 난이도가 더 높다. 왜냐면 이름을 지어야 하기 때문이다. Refactro에 관한 부분은 추후에 다른 포스트로 다루기로 하겠다.

이외에도 알아야할 사항들

테스트는 우리가 만들 기능에 대한 설명을 포함해야 한다. 예를 들면 이런 것이다.

"Foo를 이런 식으로 설정하고 Bar를 설정하면 76이 나와야 한다."

하지만 이런 식의 설명은 우리가 추구해야 하는 질문과는 거리가 있다. 우리가 추구해야할 설명의 방식은 아래와 같다.

"Foo를 이런 식으로 설정하고 Bar를 이런 식으로 설정하면 76이 나와야 한다. Foo가 이렇고 Bar가 이렇게 되면 답은 67이 된다."

위와 아래의 차이는 무엇일까? 단순히 값의 설정만을 다룬 것이 아닌 다른 경우에는 테스트 코드가 어떻게 작동할 지에 대한 설명 또한 포함되어 있다는 것을 알 수 있다. 하지만 중요한 것은 대상은 변화하지 않았다는 것이다. Bar와 Foo에 집중하고 있되 다른 경우에 어떻게 작동하는지에 대한 설명 또한 포함되어 있다는 것을 잊지말자.

테스트를 완전히 자동화 하려면 결과를 평가하는데 개입되는 인간의 판단을 접누 끄집어 내야 한다.

테스트 코드도 코드다. 테스트 코드에서도 중복은 좋지 못하다.