부제: 테스트는 필수! TDD는 상황에 따라!
한빛N MSA(Micro Seminar Assemble)는 한빛미디어 디지털콘텐츠개발연구소에서 준비한 세미나 시리즈다. 학교와, 학원 등에서 다루지는 않지만, 현업에서 일을 할 때 필요한 지식, 기술, 정보 등을 전달하는 걸 목표로 진행되는 세미나다. 각 세미나는 격주 목요일마다 진행되며(공휴일 제외), 홍대에서 세미나가 열려 접근성도 좋았다.
이번 세미나의 주제는 TDD.
정보람님의 "주니어 개발자를 위한 TPO for TDD" 세션을 듣고 일부 정리해봤다.
게시글에 사용된 강의 자료는 한빛미디어의 지원을 받았습니다.
경험을 압축한 알고리즘은 없다.
10년 전이나, 지금이나 아직도 논란의 중심에 있는 TDD.
TDD를 책이나 다른 매체를 통해 접하게 되면 '어 너무 좋은데? 당장 써야겠는데?'라고 생각하게 되지만, 막상 프로젝트를 시작하면 책에서 못보던 수많은 문제들을 마주치게 된다. 학습으로 어려운 부분이 경험이기에, 책에서 마주칠 수 없는 경험들을 공유하고자 해당 세션을 준비하게 됐다고 한다.
우리가 어떤 서비스를 만든다고 가정했을때, 생각해볼 수 있는 일반적인 개발 방식은 다음과 같다.
- 비즈니스 요구사항이 나오고, '@@를 개발해야겠다'는 계획서가 나온다.
- 되게 다양한 형태로 나올 수 있는 계획서의 요구사항을 분석해 요구사항 분석서가 나오게 되고,
- 이 요구분석서를 가지고 '어떻게 개발을 할 것인지'에 대한 설계서도 나온다.
- 이걸로 열심히 뚝딱뚝딱 개발을 한다.
- 잘 구현이 됐는지 테스트한다!
→ 이때 테스트 코드나, 검증팀을 통해 화이트박스/블랙박스 테스트를 진행할 수 있다.- 실제로 프로덕션 환경에 떨어지게 되면 운영, 유지보수를 하는 프로세스를 거친다.
시장은 굉장히 빠르게 바뀌고 복잡하게 변한다.
그럼 개발해야될 요구사항과 로직도 빠르게 바뀔 수 있을텐데, 여기서 TDD의 개념이 등장한다.
TDD는 설계, 구현, 테스트 단계를 복잡해지는 로직, 빠르게 변하는 요구사항에 맞춰서 좀 더 잘 대응할 수 있도록 하는 방법론이다.
TDD(Test Driven Development)는 이름에서 알 수 있듯이 '테스트가 주도를 해서 개발을 한다'는 뜻이다. 어느정도 TDD를 공부해본 사람이라면 사진의 사이클이 익숙할텐데, TDD 프로세스는 다음과 같다.
아래의 사이클을 반복하며 개발하는 것을 TDD라 한다.
먼저 요구사항을 분석하고, 요구사항을 테스트 할 수 있는 테스트 코드를 작성한다. 그리고 테스트를 돌리게 되면 검증할 코드가 없으니 당연히 테스트가 실패할텐데, 이 상태를 RED라고 한다.
그 다음, 실패한 테스트를 통과하는 상태로 만들기 위한 비즈니스 로직을 작성한다. 이를 GREEN이라 한다. 이때 주의해야할 부분은 테스트를 통과하기 위해 최소한의 코드만 작성한다는 점이다.
테스트가 통과하는 상태를 유지하며 리팩토링을 진행하는 과정을 REFACTOR라 한다.
💡 정리
동작을 검증하기 위해 실패하는 테스트 코드를 추가하는게 RED,
통과하게 하는 최소한의 검증 코드를 작성하는 과정이 GREEN,
테스트에 통과한 코드를 리팩토링 하는 것이 REFACTOR.
개발자는 코드로 대화한다 했던가.
TDD계에서 헬로월드급으로 취급되는 유명한 예시인 볼링게임을 예제로 가져오셨다.
일반적으로 볼링 게임을 떠올려보면, '프레임이 있고, 롤이라는 던지는게 있고, 핀 점수가 있겠구나'라는 접근 방식이 일반적인데, 이걸 테스트 주도 방식으로 바꿔 생각해야 한다.
보통 개발을 하면 main을 먼저 만들지만, TDD에서는 테스트 코드를 먼저 작성한다.
참고로 TDD는 굉장히 빠른 생산성을 중요시해서 IDE가 제공하는 shortcut 사용을 권장한다.
create_game()
을 생성한다.이렇게 만들고 테스트를 돌리면 통과하는데, 그럼 한 사이클이 돌았다고 생각하면 된다.
다음 사이클도 같은 방식으로 진행된다.
roll()
이 있다는 것만 테스트하는 함수 can_roll()
을 작성한다.roll()
을 만들어주자.여기서 뭔가 roll()
에는 쓰러트린 핀 만큼 점수를 더해주는 로직이 있어야 할 것 같지만, 이때는 점수 계산같은 기능은 머리에서 지우고 'roll 함수만 있다!'로 접근해야 한다.
def can_roll(self):
game = Game() #here
game.roll(0)
def create_game(self):
game = Game() #here
create_game()
과 방금 짠 테스트 can_roll()
을 놓고 보면 Game 클래스를 생성하는 코드가 중복된게 보인다. 이제 이 부분을 리팩토링해야 되는데, GREEN 상태를 유지하며 리팩토링을 할 때는 테스트 코드 로직과 비즈니스 로직을 같이 보며 진행해야 한다!💡 TIP
TDD를 잘하는 메커니즘 몇가지가 있다.
테스트코드가 호출되기 전에 실행되는 함수, 끝날때 실행되는 함수, 의존성을 주입해주는 함수등 다양하게 제공 되는데 여기선 setUp()을 통해 중복을 제거했다.
최종 목표는 이 게임의 점수를 계산하는 것인데, 해당 로직을 작성하기 위해 간단히 생각할 수 있는 케이스는 거터 게임(10프레임 전부 공이 옆으로 빠져 총 점수가 0점이 되는 게임)이다.
gutter_game()
)로 작성한다.score()
를 호출하는데, 단순히 0만 리턴시켜주자.아까는 그냥 0을 리턴했다면, 이젠 실제로 점수가 계산이 되도록 요구사항을 구현해보자.
가장 간단하게 모든 라운드에서 전부 1개만 쳤을때를 가정해보면,
all_ones()
)로 작성한다.score()
의 로직을 구현할 수 있게 된다!roll()
의 인자로 받은 pin
을 score
변수에 더해주는 로직을 짜주자.💡 TIP
TDD에서 제안되는 리팩토링 기법 몇가지가 있다. 대표적으로 코드를 작성하며 생기는 중복 코드를 없애 바로 공통 변수와 공통함수를 뽑아내는 것이다. 여기서는gutter_game()
과all_ones()
테스트 코드에서 20번을 던지기 위해 작성한 for문을 공통함수로 뽑아낸다.
결과적으로 두 테스트 코드에서 roll()
을 몇 번 호출할건지, 쓰러트린 pin
이 몇개인지 인자로 받아 처리하는 공통함수 roll_many()
가 따로 빠져나온다. 이렇게 코드의 중복을 제거한 뒤, 새로 만들어준 공통함수를 통해 처리하도록 변경해준다.
이때 가장 중요한 것은 리팩토링 할때 테스트가 깨지면 안된다.
지금까지 구현한 내용이다. 왼쪽이 테스트, 오른쪽이 비즈니스 로직이다.
비즈니스 로직은 단지 Game 클래스를 생성하고, 공을 굴릴때 쓰러트린 pin의 개수만큼 최종 점수에 더해주는 역할을 하는데, 이 코드를 완전하게 작성하기 위해 테스트 코드가 왼쪽에 보이는 만큼 나온거다.
여기까지 하면 다음과 같은 생각이 들거라고 하셨다.
'음... 이게 내가 알던 TDD?'
세미나를 듣는 중간에 내 생각을 들킨 느낌이라 뜨끔했는데, 잠시 이런 마음을 접어두고 남은 스페어와 스트라이크는 직접 구현해보면 아직 등장하지 못한 프레임도 구현하게 될거고, 리팩토링 과정에서 중복함수를 제어하는 부분을 짜는 재미를 느낄 수 있다고 하셨다.
참가자들에게 내준 일종의 숙제라 생각하고 돌아와 직접 해봤는데, 머리로는 '테스트 케이스가 뭐가 있을까?'라 생각했지만 막상 키보드를 앞에 가져오고 나서 로직부터 짜려는 내 모습을 보고 많은 노력이 필요해보였다.
TDD 정말 쉽지 않구나!
TDD의 장점을 알아보자.
만약 프론트엔드 개발자가 백엔드에 요청해서 뭔가 받아야 하는 상황을 가정해보자.
프론트엔드 개발자는 다음과 같은 고민을 할 것이다.
'이 테스트 코드를 어떻게 짜야하지..? 백엔드 개발자에게 짜달라 해야하나?'
TDD는 다양한 대안기술을 제안하는데, 이런 경우 사용 가능한게 Stub과 Mock이다.
테스트를 실제로 시도하다보면 관계가 많이 엮여있어 테스트 코드를 작성하는게 매우 힘들다.
이때 사용할 수 있는게 이런 객체들인데, Dummy, Spy, Fake 등 다양한 객체들이 존재한다.
“TDD의 장점들 너무 좋고, 단점이 있는데 이를 보완하는 것도 다 제안이 되네요.”
”그럼 TDD 역시 짱인가?”
지금부턴 TDD의 단점을 알아보자.
score = score + 1
을 짜면 되는데 TDD식으로 접근해 계속 최소 기능단위로 개발해야 하고, 결론적으로 한번 작성할 코드를 여러번에 나눠 작성해야 한다. 그렇지만 이 메커니즘은 TDD가 기대하는 '효율성이 높은 코드'를 작성하게 하는 방법이라는 사실.“TDD로 프로젝트를 진행하면서 어려운 예외가 생길 수 있는데 그것 때문에 고민하는 순간이 찾아오게 된다. 원칙을 깰 수는 없고 꼼수가 있기는 한데 그 꼼수를 위해서 구조를 바꾸자니 이건 아무래도 아닌 것 같고, 테스트는 말 그대로 테스트일 뿐 실제 코드가 더 중요한 상황인데도 불구하고 테스트 원칙 때문에 쉽게 넘어가지 못하는 그런 경우다.”
정말 테스트는 말 그대로 테스트일 뿐. 우리는 실제 코드가 더 중요한데,
TDD에 얽매이다 보면 실제 코드를 작성하는데 어려움이 생긴다.
그럼 TDD는... 사용을 하는게 맞는걸까?
“무조건 우린 애자일을 해야해!”
“스크럼을 해야해!”
“우리 요구사항은 스프린트로 관리가 되야해!”
애자일을 공부해봤다면 알텐데, 위와 같이 방법론 자체가 목적이 되면 안된다.
TDD 역시 내가 선택할 수 있는 도구 중 하나다. 고도화가 예상되고, 복잡성이 굉장히 높은 코드를 짜야하는 경우에 해당 부분만 TDD로 짜고 나머지는 그냥 개발하던대로 하면 된다.
TDD에서 요구하는 리팩토링 사항이 굉장히 많은데 다 안해도 된다. 오히려 리팩토링에 집착하게 되면 코드는 안짜고 리팩토링만 주구장창하는 모습을 볼 수 있다.
개발을 하는 이유는 서비스를 빨리 만들어 사용자가 사용을 할 수 있게 하기 위함이지, '엄청 아름다운 코드를 짜자!' 이게 아니니 취할것은 취하고, 안맞는 것 같다고 생각되는건 쳐내자.
TDD는 아래와 같은 상황에 쓰면 좋다.
💡 모든 경우에 TDD가 답은 아니지만, 테스트는 반드시 짜야 한다.
모든 방법론이 그렇듯 정답이 될 수 있는 방법론은 존재하지 않는다.
현재 상황이 고려되지 않은채 무조건 적용하려 하는게 패인이며, 유연하게 적용해야 한다.
TDD를 접해본건 이번이 처음이라 봐도 무방하지만, 강의를 듣는 도중에 누군가 짜놓고 간 '테스트도 없고, 주석 하나 없이 돌아가는 코드'를 울며 겨자먹기로 하나씩 테스트하며 새로운 기능을 쌓아 올렸던 경험이 자꾸 떠올랐다. 이런 경험 때문인지, 내가 개발하는 이 코드를 누가 유지보수할지 모른다면 테스트 코드를 분명히 해줘야 한다는 내용이 정말 공감됐다. 내 경험도 일종의 TDD가 아닐까?
최근 이것저것 공부하며 내가 지금 이걸 왜 공부하고 있는지 의문이 들었지만, 명확한 답을 내놓지 못했는데 우연히 이번 세미나를 통해 답을 찾아낸듯 하다. 코테를 준비해본 사람이라면 '나만 모르는 웰노운'을 경험해봤을 것이다.
알면 쉽지만 모르면 매우 골치아픈 경우를 방지하기 위해 다양한 알고리즘을 공부하는 것 처럼, 새로운 기술/방법론을 공부하는 것은 '특정 상황에서 내가 선택할 수 있는 선택지를 많이 늘리기 위함'이라는 사실을 다시 일깨웠던 것 같다.
이 게시글 외에도 이번 세미나에서는 BDD, DDD, 실제로 TDD를 적용했을때 왜 실패하는지, TDD를 적용해 성공한 사례들을 추가로 소개했다. 해당 내용을 들으며 TDD외의 방법론과, TDD를 어떤 경우에 적용하면 좋은가에 대한 인사이트를 얻어갈 수 있었다.
위 내용들과 더불어 자세한 내용은 아래 링크로 들어가 한빛미디어에서 공개한 VOD, 세미나 자료를 참고하자. TDD 외적으로도 분명 얻어갈 내용이 있다고 본다.
추가로 이 글을 작성하는 시점에서 다음 세미나는 Copilot을 주제로 09.14에 진행되는데,
아래 링크에서 참가 신청이 가능하다!