나의 노트

ESSENTIALS - Canon Test-Driven Development - Kent Beck | Craft 2025

번역 켄트벡의 tdd강연 스크립트를 번역한 내용입니다. gemini 3 pro를 이용했습니다. youtu.be/90VBvjYedWI?si=mmkdMq4_XFAmYZJW ↗

Canon TDD: 정석 테스트 주도 개발 - 켄트 벡(Kent Beck)

(사회자) 어제와 마찬가지로 보라색 무대(Purple stage)가 거의 꽉 찼네요. 다음 연사는 프로그래머이자 예술가, 음악가, 그리고 포커 플레이어입니다. 그의 직업적 사명은 괴짜(Geek)들이 이 세상에서 안전함을 느끼게 하는 것입니다. 오늘 "Canon TDD(정석 테스트 주도 개발)"라는 주제로 강연해 주실 켄트 벡을 큰 박수로 맞아주시기 바랍니다.

[박수]

(켄트 벡) 좋은 아침입니다. 아침 맞나요? 네. 제 영혼은 아직 대서양 어딘가를 건너고 있는 중이라 정신이 좀 없네요.

음, 잠깐만요. 이 옷 좀 벗겠습니다. 너무 덥네요. 자, 됐습니다.

오늘은 어제 키노트에서 나왔던 거창한 아이디어들이 아니라, 작고 구체적이며 아주 괴짜스러운(geeky) 아이디어를 이야기하려 합니다.

저는 지난 한 달 동안 정말 미친 듯이 코딩만 했습니다. 저의 '프로그래밍 요정(Genie, AI 코딩 도구를 비유)'과 함께 코딩하는 게 너무 재미있거든요. 다들 아시다시피 요정(지니)은 소원을 들어주긴 하는데, 우리가 생각하는 방식대로 들어주진 않죠. 그래서 저는 그 요정들과 함께 프로그래밍하고 있습니다. TDD(테스트 주도 개발)를 다시 적용해보고 있는데, 아, 정말 너무 즐거운 시간입니다. 그래서 제가 말하는 '테스트 주도 개발'이 원래 무엇이었는지 다시 한번 되짚어보기에 좋은 타이밍이라는 생각이 들었습니다.

요즘 젊은 친구들, 제가 어렸을 때는 없던 표현을 쓰더군요. '캐논(Canon)'이라는 단어를 '원작'이라는 의미로 씁니다. 그러니까 '캐논 반지의 제왕'이라고 하면 톨킨이 쓴 원작 소설을 말하는 것이지, 팬픽이나 버즈 라이트이어와 아라곤이 만나는 크로스오버 같은 잡다한 게 아니라는 거죠. '캐논'은 오리지널을 뜻합니다.

제가 왜 이 이야기를 하고 있을까요? 저 자신에게도 많이 물어본 질문입니다. "왜 아직도 이 이야기를 해야 하지?"

이유는 간단합니다. 저는 TDD에 대한 비판을 항상 듣는데, 그 비판들이 정작 TDD에 대한 것이 아닐 때가 많기 때문입니다. 그래서 생각했죠. "좋아, 좋아. 뉴스레터에 TDD가 무엇인지에 대한 에세이 시리즈도 썼으니, 이제 다시 기본 원칙(First Principles)으로 돌아가서 TDD가 무엇인지 정의해 보자."

오늘 제 목표는 여러분에게 프로그래밍 방법을 가르치는 게 아닙니다. 전혀요. 애초에 TDD의 목표도 그런 게 아닙니다. 제 목표는 비판할 수 있는 명확한 표적을 제시하는 것입니다.

사람들은 말합니다. "아, 저는 코드를 짜기 전에 테스트를 왕창 만드는 게 싫어서 TDD가 싫어요." 그러면 저는 생각하죠. "아니, 아니, 그건 TDD가 아니에요." 또 다른 사람은 "코드를 잔뜩 짜놓고 나중에 테스트를 몰아서 짜는 게 싫어서 TDD가 싫어요."라고 합니다. 저는 또 생각하죠. "아니, 아니, 그것도 아닙니다. 워크플로우가 틀렸어요."

저는 제 생각이 꽤 명확했다고 생각했지만, 실제로는 그렇지 않았던 모양입니다. 그래서 한 번 더, 말하겠습니다. 이번이 그냥 또 한 번의 반복이 될 수도 있겠지만요. 두고 봐야겠죠. 이것이 바로 Canon TDD(정석 TDD)입니다. 여러분이 꼭 이렇게 프로그래밍해야 한다는 건 아닙니다. 이것은 하나의 '개념(Thing)'입니다. 만약 비판하고 싶다면, 바로 이 '개념'을 비판해 주십시오. 그러면 거기서부터 파생된 변형들에 대해 이야기할 수 있을 테니까요.

네. 질문이 있으시면 스폰서인 'Slido'를 이용해 주세요. 그들이 아니었으면 제 영혼은 캘리포니아에 머물러 있었을 거고 비행기도 못 탔을 겁니다. 하지만 현장에서 바로 질문하셔도 됩니다. 지금 당장 질문하고 싶다면 언제든 제 말을 끊으셔도 됩니다. 저는 여러분이 40분 동안 궁금한 걸 참느라 제 강연을 못 듣고 앉아있는 것보다, 그 순간에 질문을 받는 게 더 좋습니다. 그러니 언제든 멈추셔도 됩니다. 제가 질문을 반복하고 진행하겠습니다. 당장 급한 질문이 아니라면 Slido에 올려주세요.

자, 현재 TDD를 정기적으로 사용하시는 분 얼마나 계신가요? 오, 꽤 되네요. 만약 여러분이 안 쓰는 분들을 좀 때려주신다면 우린 여기서 끝내도 될 텐데... 농담입니다. 제발 그러지 마세요.

좋습니다. 제 전형적인 스타일대로, 슬라이드를 전부 손으로 그려보겠습니다.

무슨 색으로 할까요? 맙소사. 자, 우리는 처음부터 새로운 프로그래밍 워크플로우를 도출해 볼 겁니다. 프로그래밍에 대해 아무것도 모른다고 가정하고, 기본 원칙부터 생각해 봅시다. 어떤 의사결정을 어떤 순서로 내려야 할까요?

가장 먼저 생각할 것은, 이 과정이 끝났을 때 우리가 무엇을 얻고 싶은가입니다. 우리는 코드(Code)를 원합니다. 기능이 구현되어야 하니까요. 하지만 이 코드를 계속 변경해야 하기 때문에, 테스트(Tests)도 원합니다.

테스트의 목표는 두 가지입니다. 하나는 자신감(Confidence)입니다. 코드가 우리가 생각한 대로 실제로 작동한다는 확신이죠. 나 자신을 위한 자신감이자, 동료를 위한 것입니다. "아, 코드 또 바꿨어? 제발 그러지 마."라는 반응 대신, 테스트가 있다면 제 작업에 신뢰를 가질 수 있습니다. 그리고 사용자를 위한 신뢰입니다. 며칠 전 BMW를 탔는데 "무선 업데이트 가능"이라는 알림이 뜨자 차 주인이 바로 "취소"를 누르더군요. 그냥 업그레이드 자체를 원치 않는 거예요. 슬픈 일이죠. 사람들이 우리가 무언가를 망가뜨리지 않을 거라 믿지 못한다는 뜻이니까요. 크게 보면 우리는 자신감을 갖고 싶고, 그 자신감을 다른 사람들과 나누고 싶습니다.

이것이 우리가 도달하고자 하는 곳입니다. 그럼 어떻게 가야 할까요?

하나의 순서는 이렇습니다. 코드를 좀 짜고, 코드를 더 짜고, 코드를 더 짭니다. 그러고 나서 테스트를 짜고, 또 테스트를 짜고, 또 테스트를 짭니다. 매우 합리적인 순서입니다. 사람들은 저에게 말합니다. "하지만 TDD는 제 워크플로우랑 안 맞아요. 저는 API 문서를 먼저 쓰고, 파일과 모듈을 어떻게 구성할지 정해요. (소스 코드를 파일에 넣는다는 게 좀 슬프긴 하지만, 그건 다른 주제니까 넘어가죠.) 그리고 어떤 함수가 어느 모듈에 갈지 정하고, 각 함수의 본문을 채워 넣은 다음 끝내거든요."

나쁘지 않은 워크플로우처럼 들립니다. 하지만 만약 여기서 실수를 하거나, 앞부분 코드에 영향을 주는 무언가를 발견하면 어떻게 되죠? 웁스(Oops). 이제 두 가지를 고쳐야 합니다. 그리고 여기서 발견한 실수가 그전에 쓴 코드, 또 그전에 쓴 코드를 고치게 만든다면요? 왜냐하면 결합도(Coupling) 때문이죠. 네, 이건 잠재적으로 비용이 많이 드는 시나리오입니다.

그럼 우리가 여기에 도달하기 전에 피드백을 받는다면 어떨까요? 여기 다른 순서가 있습니다. 코드를 좀 짜고, 테스트를 짭니다. 이제 첫 번째 코드가 작동하는지 피드백을 받습니다. 그리고 코드를 좀 더 짜고, 테스트를 짭니다. 두 테스트 셋을 다 돌려서 모두 작동하는지 확인합니다. 그리고 코드를 더 짜고, 또 테스트를 짭니다.

이 방식은 나중에 배운 사실이 이전 작업을 무효화시키는 시나리오를 방지하거나 줄여줍니다. 하지만 이 방식은 의존성을 만듭니다. (색깔을 바꿔서...) 이 아래쪽 순서를 따르려면, 코드를 짤 때 이미 실행 가능한 무언가를 짜야 합니다. 제 친구가 API 문서부터 쓰고 파일과 함수를 다 배치하는 방식은, 다 끝날 때까지 실행할 수도, 컴파일할 수도 없습니다.

여기에는 기술(Skill)이 필요합니다. 내가 작성할 코드를 분해하는 기술이죠. 원래라면 한 덩어리로 짰을 것을, 실행 가능한 작은 조각들로 나누는 것입니다. 마치 살라미 소시지를 얇게 썰어서, 다 먹기 전에(완성하기 전에) 한 조각씩 먹을 수 있게 순서를 정하는 것과 같습니다. 여러분의 프로그래밍 언어나 환경에 따라 쉬울 수도 어려울 수도 있지만, 이것은 테스트를 쪼개서 조금씩 진행할 때 발생하는 필연적인 결과입니다.

그 부분에 대한 테스트를 짤 방법을 알아내야 하고, 두 번째 코드 조각도 실행 가능하게 짜서 테스트해야 하고, 세 번째 조각도 짜서 전체를 테스트해야 합니다. 이 아래쪽 순서(코드-테스트 반복)에는 위쪽 방식에는 필요 없는 기술이 필요합니다. 위쪽 방식에서는 코드 1000줄을 짤 때 순서가 상관없습니다. 처음부터 하든, 끝에서부터 하든, 중간부터 하든 상관없죠. 하지만 아래쪽 방식, 코드와 테스트를 섞어서 진행하려면, 코드를 작성하는 순서를 조정하는 기술을 배워야 합니다. 1000줄의 코드가 있다면 1000 팩토리얼만큼의 순열이 있겠죠. 그중 어떤 순서는 말이 되고, 어떤 건 말이 안 됩니다. 조각조각 테스트한다는 목표를 달성하려면요.

분명히 여기엔 기술이 필요한데, 아무도 이 기술(어떤 순서로 코드를 짜야 하는지)에 대해 깊이 파고들지 않은 것 같습니다. 패턴은 무엇인지, 어떻게 가르칠지... 아무도요. 만약 연구 주제를 찾고 계신다면, '코드를 특정 순서로 작성한다는 것의 의미'를 파고들어 보세요. 훌륭한 프로젝트가 될 겁니다.

여기까지 질문 있나요? 좋네요. 그런 것 같네요.

또 다른 영향이 있습니다. (질문: 켄트? 소스 코드를 파일에 넣는 것에 대한 메모와 관련해서, 합리적인 대안은 무엇인가요?) 오. 오. 소스 코드를 파일에 안 넣으면 객체(Objects)에 넣으면 되죠. 40년 전부터 해왔던 겁니다. 정말 잘 작동하는데 아무도 듣고 싶어 하지 않는 대답이죠. 스몰토크(Smalltalk) 말이죠. 감사합니다. 그 얘기는 나중에 하죠.

자, 이제 질문에 도달했습니다. 코드 다음 테스트 순서에서, '코드' 부분은 다시 세 가지의 별도 결정으로 나뉩니다. 기능을 추가해서 시스템의 동작을 바꾸려 할 때, 세 가지 결정을 내려야 합니다.

API는 무엇인가? 이 기능을 어떻게 호출할 것인가?

입출력(Input/Output) 동작은 무엇인가? 로직에 어떤 입력이 들어가고 어떤 출력을 기대하는가? (도메인과 레인지)

구현(Implementation) 자체.

코딩을 하는 동안 이 세 가지를 다 생각해야 합니다. 테스트로 넘어가기 전에 이 세 가지를 다 끝내야 하죠. 제 근본 원칙 중 하나는 복잡성을 분해하는 것입니다. 크고 복잡한 문제를 상호작용하지 않는 두 개 이상의 단순한 문제로 나누는 거죠. '상호작용하지 않는'이라는 부분을 사람들이 자주 잊어버리는데 이게 꽤 중요합니다. 안 그러면 이 문제, 저 문제, 그리고 둘의 상호작용까지 해결해야 하니까 한 번에 하는 것보다 더 어려워지거든요. 이 '코드 다음 테스트' 순서(매우 합리적이지만)에서는, 내가 한 일에 대한 피드백을 받기 전에 이 세 가지 결정을 모두 내려야 합니다.

80년대 후반쯤, 제가 첫 번째 테스팅 프레임워크를 만들었을 때(자바용 JUnit은 많은 분들이 써보셨죠. 그전에 스몰토크용을 만들었습니다), 저는 코드와 함께 테스트를 많이 작성하고 있었는데 멍청한 생각이 하나 떠올랐습니다. 멍청한 생각 맞아요. 그렇죠? 참고로, 멍청한 아이디어들을 시도해 보세요. 왜냐하면 만약 그게 맞는다면 정말 가치 있을 테고, 두 번째로는 아무도 그런 멍청한 생각을 시도할 만큼 멍청하지 않기 때문에 경쟁자가 없을 겁니다. 특히 지금처럼 우리 직업 세계가 빠르게 변할 때는 이 원칙이 중요합니다. 코딩 요정(AI)을 써봐야 할까? 당연하죠. 생각나는 모든 미친 실험을 다 해보세요. 만약 통한다면 엄청나게 강력할 겁니다.

아무튼, 저는 테스팅 프레임워크를 가지고 있었고 테스트를 쉽고 빠르게 돌릴 수 있었죠. 그때 이런 생각이 들었습니다. "순서를 바꾸면 어떨까?" 모두가 코딩하고 테스트합니다. 그게 말이 되니까요. 근데 만약 바꾸면? 그 생각을 했을 때 소리 내어 웃었던 기억이 납니다. 정말 멍청한 생각이었거든요. 말이 안 되잖아요. 테스트를 먼저 쓰면, 실패할 게 뻔하잖아요. 코드가 없으니까. 우리는 테스트가 통과하길 원하잖아요? 근데 왜 실패가 보장된 테스트를 짭니까? 코드를 짜고 테스트를 하면 통과할 수도 있고 아닐 수도 있죠. 통과하면 "우와 이겼다!", 실패하면 "오 슬퍼" 하겠죠. 근데 순서를 바꿔서 테스트를 먼저 쓰면? 무조건 실패하잖아요. "그럼 난 매번 울어야 하나?"

그래도 한번 해보자 싶었죠. 제 첫 예제는 스택(Stack)이었습니다. 비어있는 스택 생성. 푸시(Push)하고 팝(Pop)하면 같은 게 나옴. 두 개 넣고 두 개 빼면 순서대로 나옴. 빈 스택에서 팝 하려 하면? 다 했나? 네. 네. 더 이상 실패할 만한 테스트가 떠오르지 않더군요. 그 순간이 저에게는 마법 같았습니다. 저는 불안도가 높은 사람인데, 이건 마치 "프로그래머를 위한 자낙스(신경안정제)" 같았거든요. 불안을 완전히 잠재워줍니다. "또 뭐가 깨질 수 있지?" 자다가 깨서 생각하죠. "버튼 누르고 테스트 돌려. 다 통과하네? 다시 자자." 실제로 그런다는 건 아니지만, 안 그런다는 것도 아닙니다.

자, 여기서 멋진 점 중 하나는 이겁니다. 테스트를 먼저 작성하면, 이 세 가지 결정(API, 입출력, 구현)을 내리는 순서를 내가 선택할 수 있고, 매 단계마다 피드백을 받을 수 있습니다.

테스트를 짜려면 API를 결정해야겠죠. "스택 객체가 있고 push, pop 연산이 있어." 오케이. 테스트에 그렇게 씁니다. 하지만 어떻게 구현될지는 신경 안 씁니다. 링크드 리스트인지 배열인지 확률적 구조인지, 몰라요. 상관없어요. 그다음 입출력 동작을 결정합니다. "1을 넣고 1을 빼면 같은 게 나와야 해." 이게 입출력 동작입니다. 그러고 나서 구현을 시도합니다. "좋아, 링크드 리스트로 하자..." 하지만 꼭 이 순서일 필요는 없습니다. 입출력 결정부터 시작할 수도 있습니다. 제임스 뉴커크(James Newkirk)가 가르쳐준 'Assert First'라는 기법이 있습니다. 테스트의 마지막 줄부터 쓰는 겁니다. assert equals actual expected. 좋아, expected는 뭐지? 5를 넣었으니 5가 나와야 해. actual은 뭐지? 임시 변수 stack.pop의 결과야. 근데 그게 되려면 stack.push 5를 먼저 해야 해. 그게 되려면 stack = stack.new를 해야 해. 결국 같은 결과가 나오지만, 테스트 안에서 작업하면 완전히 다른 순서로 결정할 수 있습니다. 입출력부터 시작해서 API로 가고, 이제 구현을 하러 가는 거죠.

여러분은 꽤 숙련된 청중 같으니 좀 더 멋진 걸 파고들어 보죠. 네, 제가 좀 비행기를 태우고 있긴(flattering) 하지만 괜찮습니다. 진짜 경험 많아 보이시거든요. 실제로 의사결정 순서를 다르게 할 수도 있습니다. 구현부터 시작할 수도 있어요. 제 친구 키스 브레이스웨이트(Keith Braithwaite)가 'TDD as if you meant it(진심으로 하는 TDD)'라고 부르는 기법입니다. 테스트 안에서 바로 구현하는 겁니다. 나중에 다른 곳에서 쓸 push와 pop의 로직을 테스트 안에 구현해 버립니다. "알고리즘은 이거야. 배열을 만들고, 헤드 포인터를 두고, push는 배열에 추가하고 포인터 증가, pop은 가져오고 포인터 감소." 그리고 expected = actual을 확인합니다. 어떻게 호출할지는 아직 모릅니다. 함수일지, 객체일지, 모듈일지... 아직 결정하고 싶지 않아요. 그래도 구현 작업을 할 수 있고, 그다음 입출력 동작을 정하고, 나중에 테스트 안에 임시로 살고 있는 이 구현을 최종적으로 있을 곳으로 추출(Extract)해 낼 수 있습니다.

코드와 테스트의 순서를 바꿈으로써, 원하는 순서대로 결정을 내릴 수 있는 워크플로우의 자유를 얻은 겁니다. 꽤 멋지죠.

(질문: 배열을 먼저 사용한다면 배열 크기에 대한 암묵적인 가정을 하는 것이고, 나중에 오버플로우 위험이 있지 않나요?) 네, 맞습니다. 그러니 나중에 그것에 대한 테스트를 꼭 짜야죠. 테스트를 안 짜면 안 됩니다. "TDD를 쓰면 현재 테스트만 만족시키려고 해서 일반적인 경우에 코드가 작동 안 할 거야."라는 주장이 있는데, 그럼 그렇게 안 짜면 되잖아요. 저는 이 감정적인 반응의 강도를 이해 못 하겠습니다. 누가 설명해 주면 좋겠어요. 어떤 사람들은 깊은 부끄러움을 안고 저에게 옵니다. "전 TDD를 안 해요..." 제 얼굴이 뭐 신경 쓰는 것처럼 보입니까? 또 어떤 사람은 화를 내며 "나한테 이거 강요하지 마!"라고 합니다. 제 얼굴이 신경 쓰는 것처럼 보입니까? 아무튼 설명해 주실 분 찾습니다.

자, 하나만 더. Canon TDD로 넘어가기 전에(약속하는데 진짜 넘어갈 겁니다), 한 가지만 더 생각해 보죠. 우리는 대안이 있습니다. '코드 후 테스트'냐, '테스트 후 코드'냐. 이것의 경제성은 무엇일까요? 왜 코드를 짤 때 테스트를 섞어서 하고, 순서를 제약하는 부담을 져야 할까요?

가치와 비용 곡선을 그려보겠습니다. 여기가 시간이고, 여기가 '코드 작성 시점'입니다. 테스트를 코드 작성보다 훨씬 전에 짜면 가치가 낮습니다. 가정을 너무 많이 해야 하고, 나중에 다시 써야 하니까요. 구현도 안 할 수도 있고요. 버리는 일이 많아집니다. 구현 시점에 가까워질수록 테스트의 가치는 올라갑니다. 우리가 API, 입출력, 구현 결정을 내렸을 때 그게 맞는지 피드백을 주니까요. 코드를 짜는 그 시점에 테스트의 가치는 정점(Peak)을 찍습니다. 하지만 피드백을 늦게 받을수록 테스트 가치는 떨어집니다. 코드를 프로덕션에 배포한 후라면 테스트 가치는 별로 없죠.

비용도 비슷한 곡선을 그리는데, 반대입니다(음수). 너무 일찍 테스트를 짜면 재작업 때문에 비용이 높습니다. 구현 시점에 가까워질수록 비용은 낮아져서, 코드를 짜는 시점에 비용이 최소가 됩니다. 코드를 짤 때 테스트를 안 짜면, 기본적으로 코드는 테스트 불가능한 상태가 됩니다. 나중에 짜려면 점점 더 비싸지죠. 이 두 곡선을 합치면, 코드를 작성하는 바로 그 시점이 테스트를 작성하기에 가장 가치 있고 비용이 적게 드는 때입니다. 그래서 우리가 '코드-테스트-코드-테스트' 순서를 선택하는 것이고, 그렇다면 "어떤 순서로 시작할 것인가?"의 문제가 남는 거죠.

Canon TDD (정석 TDD) 드디어 Canon TDD입니다. 이 모든 결정을 내리기 위한 과정이죠. 2단계 프로세스입니다.

1단계: 시나리오 (Scenarios) 어떤 기능을 구현할 때 생각합니다. "오버플로우, 언더플로우, 요소 1개, 2개... 더 생각나는 거 없나?" 타입 체크 같은 것도 테스트의 일부가 되겠죠. 시나리오는 로직을 통과하는 경로들입니다. 이자 계산법이 두 가지라면 적어도 각각 하나씩 테스트가 필요하겠죠. 저는 항상 목록(List)으로 시작합니다. 이건 저의 불안감과 ADD 때문일 수도 있습니다. 참고로 저는 '주의력 결핍 장애'라는 말을 싫어합니다. 저는 주의력이 결핍된 게 아니라 주의력이 넘치거든요. 단지 아주 쉽게 지루해질 뿐이죠. 그래서 '지루함 민감성(Boredom Sensitivity)'이라고 부르는 걸 좋아합니다. 땅콩 알레르기처럼 지루함 알레르기가 있는 거죠. 넘치는 주의력을 집중시킬 방법이 필요한데, TDD가 그걸 도와주고 시나리오 목록도 도와줍니다. 시나리오가 도와주는 두 번째는 '언제 끝나는지 알게 해 준다'는 것입니다. 저는 불안한 사람이라 목록의 항목들을 하나씩 지워나가는 게 불안을 관리하는 방법입니다. 평생 그렇게 살았어요. 논리적 경로들의 목록으로 시작합니다. 오늘 내가 아는 것들이죠.

2단계 A: 테스트 작성 (Write a test) 프로젝트에 진전이 있다면 '초록색(Green)', 진전이 없다면(내가 진전이 있다고 생각하든 말든) '빨간색(Red)'이 될 일련의 문장들을 작성합니다. 이게 테스트 주도(Test-Driven) 부분입니다.

2단계 B: 통과시키기 (Make it pass) 일단 빨간 테스트가 생기면, 그걸 초록으로 만드는 것 외에는 아무것도 신경 안 씁니다. 어떻게 하면 최대한 빨리 초록으로 만들까? 여기서 저는 제 불안을 유리하게 이용합니다. "다른 시나리오 백만 개는 어쩌지? 구현은 어떻게 하지?" 하며 걱정이 꼬리를 물고, 제가 좋아하는 회피 활동인 손톱 다듬기로 빠질 수 있습니다. 저는 클래식 기타리스트라 손톱을 다듬거든요. 코딩 걱정되네? "아 안돼." 하고 화장실 가서 손톱이나 갈고 있는 거죠. 코드는 한 줄도 안 나오는데 말이죠. 테스트가 있고 빨간불입니다. 초록불로 만듭니다. 제대로 된 코드로 초록불을 만들고 싶겠지만, 그게 너무 복잡할 수도 있습니다. (기차 소음) 오, 훌륭한 컨퍼런스, 훌륭한 장소네요. 제 발표 중간에 딱 맞춰 기차를 보내주는 환대라니 정말 감사합니다. 네, 원하는 코드를 쓰고 싶겠지만, 그게 너무 어려우면 '통과시키기' 단계에서는 온갖 죄악(Sins)을 저질러도 됩니다. 하드코딩을 하든 뭘 하든 초록불만 보면 됩니다. 왜냐하면 우리는 언제나 C 단계로 갈 거니까요.

2단계 C: 일반화 (Generalize) 테스트 주도 개발은 구체적인 시나리오에서 출발하지만, 우리는 일반적인 상황에서 작동하는 코드를 원합니다. 다른 입력값들도 다 작동할 거라는 걸 암시하는 테스트들을 고르는 거죠. 이 일반화 단계에서는 다시 초록불 상태입니다. 새로운 테스트도 통과했습니다. 이제 초록불을 유지하면서 일반화합니다. 여러 방법이 있습니다. 이것도 훌륭한 연구 주제죠. "테스트가 통과한 후 일반화하는 모든 방법." 예를 들어 디자인을 일반화할 수 있습니다. "테스트는 통과했지만 코드를 복사-붙여넣기(Copy pasta) 했네." (헝가리에도 복붙 있나요? 코드를 복사해서 붙이고 수정하면 결합도가 생기죠. 안 한다고요? 믿어드리죠.) 복붙을 했다면, "이 코드랑 저 코드에 중복이 있네. 헬퍼를 추출해서 쓰자." 이게 디자인 일반화입니다. 때로는 구현을 일반화합니다. 제가 링크드인에 올렸던 내용인데, "100유로가 있고 5% 이자면 1년 뒤 105유로"라는 테스트가 있습니다. '통과시키기' 단계에서는 그냥 return 105라고 썼을 겁니다. 하지만 아직 안 끝났죠. 105는 사실 원금 더하기 이자입니다. 그래서 return 105를 return 원금 + 이자로 바꿉니다. 원금은 return 100, 이자는 return 5라고 되어 있겠죠. 그리고 다시 들어가서, "원금은 사실 초기 예치금이야."라며 테스트에 있던 값을 연결하고, "이자는 이율 곱하기 원금이야."라고 연결합니다. 그러면 완벽하게 일반화된 코드가 됩니다. 하지만 구현 결정의 대부분을 테스트가 모두 초록인 상태에서 내렸습니다. 불안한 사람에게는 마법 같은 일이죠. 때로는 단순화(Simplify)하거나, 추상화(Abstract)합니다. 구체적인 클래스를 인터페이스로 바꾼다거나요.

이 일반화 단계는 TDD 비평가들이 자주 놓치는 부분입니다. 제가 일반화에 얼마나 많은 시간을 쓰는지 모릅니다. 유튜브에 'Test Commit Revert'라는 변종 TDD를 시도한 스크린캐스트가 있습니다. 테스트가 실패하면 마지막 통과 이후의 모든 변경사항을 즉시 지워버리는 끔찍한 아이디어인데, 정말 재밌으니 해보세요. 거기서 로프(Rope) 데이터 구조를 만드는데, 한 시간 중 마지막 30분을 오로지 리팩터링(일반화)에만 씁니다. 파이썬 코드처럼 보이게 만들고, 깔끔하게 정리하는 거죠. 다음 단계를 위해서요.

이것이 바로 신사 숙녀 여러분, 그리고 그 사이의 모든 분들, Canon TDD입니다.

이 단계를 따라야 할까요? "절대 아니다"라고 말하려다 말았습니다. 아마도요. 아마도 이 단계를 따라야 할 겁니다. TDD를 비판하고 싶다면 이것을 비판하세요. "난 코드 짜기 전에 테스트 왕창 짜는 게 싫어."라는 사람과는 대화하고 싶지 않습니다. 그건 TDD가 아니니까요. 이게 기본입니다. 만약 TDD 워크숍을 한다면 저는 이걸 시작점으로 가르칠 겁니다. 실용주의 프로그래머 데이브(Dave)가 뉴스레터에서 자기 워크플로우를 이야기했는데, 그는 때로는 테스트를 안 쓰거나 나중에 씁니다. 그건 거장(Mastery)의 단계입니다. 의식적으로 결정하지 않고 판단과 직관에 맡기는 단계죠. 강력한 단계입니다. 하지만 거기에 도달하려면 먼저 음계(Scales)를 연습해야 합니다. 이 워크플로우를 따르는 것은 프로그래밍의 음계 연습입니다. 음악 그 자체가 아니고요. 시작점이고, 비판의 대상이 될 수 있는 지점입니다. TDD를 이렇게 이해하지 않으셨다면 한번 시도해 보시길 바랍니다. 시도해 보고 작동하든 안 하든, 문제를 발견하든 저에게 알려주세요. 이것이 제가 원래 TDD 책에서 설명하려고 했던, 지난 25년 동안 설명하려고 했던 것입니다.

시간 내주셔서 감사합니다.

[박수]

(사회자) 정말 감사합니다, 켄트. 질문이 엄청 많은데 시간이 부족해서 다는 못 하겠네요. 짧게 답변해 주시면 감사하겠습니다.

(사회자) 스택 예제에서 처음에 테스트를 몇 개나 미리 작성하나요? (켄트 벡) 하나.

(사회자) 이 답변에 놀라신 분? (웃음) 자, 다음. TDD는 단위(Unit), 통합(Integration), E2E 등 다른 추상화 레벨의 테스트와 어떻게 작동하나요? (켄트 벡) 저는 신경 안 씁니다. 저는 스케일 사이를 계속 왔다 갔다 합니다. 분해(Decomposition)에는 분명 기술이 필요합니다. 큰 문제를 이 부분과 저 부분으로 나누어 해결하면 전체가 해결된다는 식이죠. 잘못 나눌 수도 있습니다. 그래서 저는 전체 문제를 먼저 해결하고 나무가 자라듯 하위로 나누는 걸 선호합니다. 하지만 같은 원칙을 적용할 수 있습니다. 제가 이 강연을 준비할 때도 같은 원칙을 썼습니다. "어디서 끝내고 싶지? 코드와 테스트, 그리고 자신감. 그럼 어떻게 거기에 도달하지?" 같은 사고방식입니다.

(사회자) 감사합니다. 내 코드보다 테스트를 더 어떻게 신뢰할 수 있나요? (켄트 벡) 신뢰 못 합니다. 회계사들은 '크로스 푸팅(Cross-footing, 교차 검증)'이라고 부릅니다. 숫자 행렬이 있을 때 가로로 더해보고 세로로 더해봐서 같은 답이 나오면, 정확히 상쇄되는 에러를 범했을 수도 있지만 확률은 훨씬 낮죠. 저는 제 테스트도 안 믿고 제 코드도 안 믿습니다. 그래서 두 가지 다른 경로로 같은 답에 도달하고 싶은 겁니다. 그래서 코드를 짜고 결과를 출력해서 복사한 다음 테스트에 붙여넣는 건 TDD에 대한 죄악입니다. 아무것도 더블 체크한 게 아니잖아요. 그냥 답이 안 바뀌는지만 확인한 거죠. 계리사가 계산하게 하고 우리 코드로 계산해서 맞으면 확신하는 것과 같습니다. 테스트가 실패했는데 놀랍다면, 제 경험상 1/3은 테스트 문제고 2/3는 코드 문제입니다. 이것도 훌륭한 석사 논문 주제겠네요. 테스트 실패의 분류 체계를 만들어보세요.

(사회자) 감사합니다. AI가 명세서에서 적절한 단위 테스트를 생성해 줄 수 있다면 TDD는 여전히 유효한가요? (켄트 벡) 현재로서는 AI가 적절한 단위 테스트를 생성하지 못합니다. 어제 확인했어요. 내일은 다를 수도 있겠죠. TDD는 여전히 유효한가? 저는 요정(AI)이 TDD의 재탄생이라고 생각합니다. 복잡성 이론 관점에서 초기 조건에 민감한 비결정적 시스템(AI)이 있을 때, 통제할 수 있는 방법은 항상 '억제(Inhibition)'입니다. 무언가가 일어나지 않도록 막는 거죠. 테스트는 요정이 멍청한 짓을 하는 걸 막는 훌륭한 방법입니다. 물론 요정이 "테스트를 지우거나 주석 처리하면 통과하네?"라는 걸 알아내면 곤란하죠. 저랑 요정이랑 면담 좀 해야겠지만요. 하지만 테스트를 통해 솔루션의 범위를 정해주는 것, "아니 아니, 이 요구사항들을 만족하는 코드만 좋은 코드야"라고 말해주는 것은 여전히 유효합니다.

(사회자) 감사합니다. 마지막 질문입니다. 많은 시나리오를 이미 알고 있다면 테스트를 처음에 왕창 써도 되나요? (켄트 벡) 시간이 없지만 이건 해야겠네요. 답은 너무 간단합니다. 하지 마세요. 시나리오를 안다면 T1, T2, T3... 그냥 순서대로 짜면 되잖아요. 미리 짜면 비용만 들지 이득이 없습니다. 기다리면? 저는 항상 제가 무지하고 무언가를 배우게 될 것이라는 쪽에 겁니다. 테스트 작성을 미루면 배울 기회가 생기고, 진행하면서 마찰도 줄어듭니다.

(사회자) 빠르네요. 하나 더 가죠. 속성 기반 테스트(Property-based testing)가 TDD와 잘 어울리나요? 아니면 테스트가 초록일 때 일반화하는 단계를 너무 고통스럽게 만드나요? (테스트 통과를 위해 실제 구현이 필요할 수 있으니까요) (켄트 벡) 제게 속성 기반 테스트의 문제는 복잡성 분할(Complexity Partitioning)입니다. 복잡성을 한 덩어리씩 처리하는 것 말이죠. 속성 테스트를 미리 짜면, 도파민(테스트 통과)을 얻기 위해 구현을 끝까지 다 해야 합니다. 저는 속성 기반 테스트를 싫어하지 않습니다. 어제 B+ 트리 라이브러리를 짤 때도 마지막엔 속성 테스트로 끝냈습니다. 하지만 시작은 속성 테스트로 하지 않았어요. 1개 넣을 때, 2개 넣을 때... 구체적으로 시작했죠. 속성 기반 테스트는 자리가 있지만, 발견의 워크플로우에서는 부분적인 구체적 진전을 만들기 어렵게 한다고 생각합니다. 제 생각은 그렇고 다른 분들은 다를 수 있겠죠.

(사회자) 정말 감사합니다, 켄트. 큰 박수 부탁드립니다.

[박수]

(켄트 벡) 감사합니다. 작은 선물이 있네요. 감사합니다.