테스트는 개발이 아니라 습관이다
개발자들의 개발방법론 중에 TDD(Test Driven Development)라는 방식이 있다.
실패하는 테스트를 만들고, 그 테스트가 통과하는 제품을 만들면 결국은 원하는 기능이 동작하게 된다는 개발방법론이다. 예를 들어, 일반적으로 자동차를 만든다고 생각했을 때, '엑셀을 만들고', '브레이크를 만들자'라고 개발을 시작하게 될 것이다. 하지만 TDD라는 방법론으로 진행하게 되면 '엑셀을 밟으면 차가 앞으로 가는가', '엑셀을 놓으면 차가 서서히 서는가', '브레이크를 밟으면 차가 서는가'라는 체크리스트를 사전에 만들게 되는데 그것이 Test라고 보면 된다. 그리고 해당 체크리스트가 통과하도록 로직을 만들게 되면 우리가 원했던 '엑셀'과 '브레이크'를 만들었다고 볼 수 있는 것이다.
뭔가, 개발의 순서만 바뀐 것 같은데 도대체 TDD라는 것이 왜 필요한 것일까?
일반적으로 개발자들이 생각했을 때 TDD가 필요한 이유는, 회사에서 성과지표로 사용하기 때문일수도 있고, 나중에 유지보수를 편하게 하기 위한 측면이 될 수도 있다. 테스트를 만들면 내 로직을 잘 검증할 수 있다고 얘기하는 사람이 있을 수도 있다. 하지만 마지막의 경우도 결국 릴리즈하는 로직에 대해서 괜찮다라고 내심 스스로 안심하기 위함이거나, 혹시 잘못되었을 때 테스트를 했지만 놓쳤다라고 책임회피용일 가능성이 있다.
필자가 얘기하고 싶은 TDD의 의미도 사실 마지막에 얘기한 것이긴 하다. 하지만 릴리즈라는 나중 얘기가 아니라 설계라는 앞의 얘기를 하려고 한다. 위에서 얘기했던 자동차를 만드는 케이스로 생각을 해보자. 만약, 제품을 먼저 만드는 단계라고 한다면, '엑셀'을 만들고, 밟으면 앞으로 가고, 놓으면 서는 기능을 넣을 것이다. '브레이크'를 만들고 밟으면 서고, 놓으면 아무런 기능을 하지 않도록 만들 것이다. 어떤 기능 구현에 대해 생각하다보면 결합(integration)에 대한 부분을 놓치기 쉽다. 만약, '엑셀과 브레이크를 동시에 밟으면 어떻게 될까?'와 같은 케이스를 생각해보기 어렵다. 하지만 체크리스트라는 기준으로 생각하게 되면, 아무래도 위와 같은 케이스를 생각하기가 조금은 쉬워진다. (물론 테스트를 먼저 만들어도 생각하지 않는다면 방법이 없...)
그러나 사실상 더 중요한 TDD의 의미가 있다. 아래의 케이스를 살펴보자.
기획자 : 저번에 유리창을 닫을 때, 버튼을 2초간 꾹 눌렀다가 떼면, 창문이 자동으로 닫혀야 된다고 말씀드렸는데, 정상적으로 동작하지 않는 것 같아요.
개발자 : 어? 유리창을 열 때, 버튼을 2초간 꾹 눌렀다가 떼면, 창문이 자동으로 열려야한다고 해서, 닫힐 때는 해당 기능이 동작하는 게 아니라고 이해했는데 아닌가요? 그렇다면 수정이 필요한데요.
사람 사이에 커뮤니케이션은 굉장히 복잡하고 미묘하고 어렵다. 내가 A라는 얘기를 했지만, 상대방은 B로 들을 수 있다. 그나마 커뮤니케이션이 잘된다고 생각하는 케이스가 A' 정도로 이해한 사람일 것이다. 그런 차이로 인해서 상대방(기획자, 고객, 사장님)이 원하는 기능을 개발했지만 정작 그 사람들의 요구사항을 정확하게 반영하지 못 할 수도 있다. 그렇기 때문에 개발서버테스트, 베타테스트 등을 하기도 하고, Agile이라는 방법론을 통해서 주기적으로 요구사항들을 계속 확인하는 단계를 가지기도 한다. 즉, 커뮤니케이션의 실수를 통한 문제점들은 아무리 TDD라고 하더라도 잡기가 어렵다. TDD가 필요하는 이유는 사실 다음과 같은 케이스때문이다.
기획자 : 저번에 개발한 기능이요, 엑셀을 밟으면 간다고 했는데, 엑셀을 밟아도 차가 안 가요. 왜그렇죠?
개발자 : 글쎄요, 차가 가야되는데 왜 그렇지? 한 번 확인이 필요할 것 같아요.
기획자와 개발자는 '엑셀을 밟으면 자동차가 앞으로 간다'라는 기능을 100% 정확하게 서로 이해하고 있었다. 하지만 결과적으로 제품은 생각대로 동작하지 않는 경우이다. 개발자는 적어도 내가 이해한대로 로직이 동작하고 있다는 것을 보증할 수 있어야 한다. 그것이 테스트라는 것이고, TDD라는 개발방법론으로 진행하게 된다.
정리를 하면, TDD를 해야하는 이유는...
1. 개발자가 의도한대로 로직이 동작하는지 명확하게 알 수 있고, 로직에 대해 보증할 수 있다.
2. 사전에 다양한 케이스를 고려해봄으로써 문제가 될 수 있는 잠재적 오류들을 방어할 수 있다.
3. 아키텍처와 로직이 깔끔해진다. (이 부분은 본문에서 기술하지 않았지만, 테스트가 어려운 코드는 좋은 코드가 아니다)
4. 이후에 다른 사람이 로직을 수정하게 될 때, 로직의 변경에 대한 영향도가 명확하게 보여진다. (이 부분 역시 기술하지 않았지만, 명확하다는 얘기는 변경에 드는 비용이 적다는 걸 의미한다)
마지막으로 필자의 경험을 공유하려고 한다.
TDD에 심취해서 열심히 개발을 했는데도 불구하고, 서비스에 장애가 발생했다. nginx로그를 통해서 입력 파라미터를 알게 되었지만, 서비스의 로직이 어떻게 동작할지는 팀의 그 누구도 확신이 없었다. 그래서 테스트케이스를 만들어 추가하였고, 다음과 같은 결과를 얻게 되었다.
개발은 필자의 의도대로 잘 되었지만, 해당 케이스는 사전에 전혀 고려하지 못한 것이었다. 위에는 confidential 이라고 나와있는데, 해당 케이스는 String (문자열)을 받아서 DB에 저장하는 Api였는데, 모바일에서 타이핑을 할 경우 이모티콘을 쓰는 경우가 있었다. 예를 들어, 감사합니다~� (← 하트이모티콘이예요, 브런치도 제대로 안나오네요;;) 라는 리뷰를 남기게 되면, 구형 MySQL DB에서 해당 문자열을 받아주지 못해서 발생하는 문제였다. 그리고 이렇게 오류를 해결해 나가게 되면, 로직은 점점더 단단(Robust)해지게 된다.