문제가 있었다면, 서로 다른 환경의 프로젝트에 대해 애자일 방법론인 테스트 주도 개발(test-driven development; 이하 TDD)을 수행하도록 하거나 가르칠 때마다 동일한 혼란 또는 오해들과 마주치곤 했다는 거다. 개발자들은 어디서부터 시작할지, 어떤 것을 테스트하고 어떤 것은 하지 않을지, 한번에 얼마만큼을 테스트할지, 테스트에 어떤 이름을 붙일지 그리고 테스트가 왜 실패했는지 등을 고민했다.
- 내가 버그를 만들었다. 내가 나빠. 해결책: 버그 고치기.
- 의도된 그 행위는 여전히 적절하지만 다른 곳으로 옮겨졌다. 해결책: 테스트를 옮기고 필요하면 내용도 고친다.
- 시스템의 조건이 바뀌거나 해서 그 행위는 더이상 맞지 않다. 해결책: 테스트를 삭제한다.
애자일 프로젝트에선 그것에 대한 이해가 진전된 만큼 후자쪽의 경우가 더 많이 발생한다. 불행히도, TDD 초보자들은 테스트 코드들을 삭제하는 일이 마치 그들의 코드 품질을 조금이라도 낮추는 일이 될 것처럼 선천적인 두려움을 갖는다.
이제 "test" 단어를 지울 수 있는 도구(agiledox)와 테스트 메서드마다 적용할 탬플릿이 생겼다. 문득 TDD 에 대한 사람들의 오해들이 거의 대부분 "test" 란 단어에서 시작됐다는 게 떠올랐다.
테스팅을 TDD 의 본질이 아니라고는 할 수 없지만, 작성한 테스트 메서드 집합은 우리가 코딩 작업을 확실히 하도록 보장하는 효과적인 방법이다. 그럼에도 메서드들이 시스템의 행위를 완전하게 기술하고있지 못하면 결국 그것들은 우리들을 방심하게 만든다.
테스트에 관한 사유를 행위에 대한 사유로 바꾸는 것에 심각해지면서 나는 테스트 주도 개발(TDD)를 행위 주도 개발(BDD)로 부르기 시작했다.
JBehave 는 테스트에 있어 행위를 강조한다.
2003년 말에 내가 설파해온 것에 대해 돈 또는 시간을 투자하기로 했다. JUnit 을 대체할 JBehave 를 만들기 시작했는데, 테스트에 관한 것들을 행위를 검증하는 내용을 중심으로 한 용어들로 대체했다. 나의 새로운 행위 주도적 주문(呪文)들을 엄격하게 고수했을 때 그런 프레임워크가 어떻게 진화하는지 확인하기 위함이었다. 또한 그것이 태스트 주도적인 용어들로 인한 소동들을 배제한 채 TDD 와 BDD 를 소개하는 데 유용한 교육용 도구가 될 걸로 기대했다.
위에서 예시한 가상의 CustomerLookup 클래스의 행위를 정의한다면 CustomerLookupBehaviour 라는 클래스를 작성할 수 있겠고, 이는 "should" 로 시작되는 메서드들을 포함하게 된다. JUnit 이 테스트를 수행할 때 처럼, 특정 행위가 수행될 때 행위 클래스를 초기화하고 각각의 행위에 대한 메서드들이 차례로 호출되는 식이다. 그 수행되는 진행상황이 리포트될 거고 종국에 요약된 내용이 출력되게 된다.
나의 첫번째 이정표는 JBehave 에 자체 검증 기능을 만드는 것이었다. 그러기 위해 단지 스스로를 실행할 수 있도록 하는 행위를 추가했다. JUnit 의 모든 테스트들을 JBehave 의 행위들로 옮기고 JUnit 과 동일하게 즉각적인 응답을 얻을 수 있었다.
그 다음으로 가장 중요한 행위를 판단한다.
그런 후에 비지니스적 가치에 대한 개념을 확정했다. 물론 내가 소프트웨어를 만드는 목적에 대해 언제나 알고 있었지만 지금 당장 코딩하고 있는 것에 대한 가치에 대해 심각하게 생각해본 적은 없었다. 또한명의 동료인 비지니스 아날리스트 크리스 매츠(Chris Matts)는 행위 주도 개발의 맥락에서의 비지니스적 가치에 대해 생각해보도록 유도했다.
JBehave 가 그것에 대해서도 재귀적(self-hosting)으로 사용될 수 있도록 만들려고 작정한 상태에서, 그것에 계속 집중할 수 있도록 하는데 "이 시스템이 하지 않는 가장 중요한 것은 무엇인가?" 라고 질문하는 것이 유용하단 걸 알게 됐다.
이 질문은 우리가 아직 구현하지 않은 기능의 가치를 확인하고 우선순위를 메기도록 한다. 또한 행위에 관한 메서드 이름을 체계적으로 나타내는 데도 유용하다. 해당 시스템이 어떤 의미 있는 행위인 X 를 하지 않는 상태에서, 그 X 가 중요하여 행위하도록 해야 한다면, 그 다음의 행위 메서드는 단순히 아래와 같이 된다.
public void shouldDoX() { // ... }
이로써 TDD 에 대한 또다른 질문, 어디서 시작할 것인가에 대한 답을 얻었다.
요구사항 또한 행위이다.
이제 나는 TDD가 어떻게 수행되는지 내가 맞닥드린 함정들을 피할 수 있는 접근법을 이해함은 물론 그것을 설명할 수 있는 프레임워크를 갖게 됐다.
2004년이 끝나갈 무렵 나의 이런 새로운 발견인 행위 주도적 용어들을 맷츠에게 설명하는 중에 그는 "그건 그냥 분석 같은데?" 라는 말을 했다. 긴 침묵이 흐른 뒤에 우리는 모든 행위 주도적 생각을 요구사항 정의에 적용해보기로 했다. 만약 우리가 분석가들, 테스터들, 개발자들과 비지니스를 위한 일관적인 용어를 만들 수 있다면, 기술진이 비지니스 관련자들과 이야기할 때 발생하는 모호함과 소통 오류를 상당수 없앨 수 있는 방법이 될 것 같았다.
BDD는 분석가들에게 "공통언어(ubiquitous language)" 가 된다.
근래에 에릭 에반스(Eric Evans)는 그의 베스트셀러가 된 Domain-Driven Design 이란 책을 출간했다. 그 책에서 그는 모델링의 개념을 비지니스 도메인을 기반으로한 공통언어를 사용하는 시스템으로 기술하므로써 프로그램 코드의 근저에 비지니스 용어가 배어들도록 했다.
크리스와 나는 우리가 분석 과정 그 자체에 대한 공통언어를 정의하려고 시도하고 있다는 걸 실감했다. 우리는 훌륭한 출발점을 얻었다. 조직 안에서 공통적으로 사용되는 아래와 같은 스토리 템플릿이 이미 있었다.
[X] 로 하여금 (As a)
[Z] 를 얻게 하기 위하여 (so that)
[Y] 가 필요하다 (I want)
여기서 Y 는 어떤 기능이고, Z 는 그 기능에서 얻어지는 이득이거나 가치고, X 는 득을 보게 되는 사람이거나 역할이다. 이것들의 장점은 그것을 처음 정의할 때에 스토리를 배포하는 것의 가치를 명확히 하도록 강제한다는 데 있다. 어떤 스토리에 실질적인 비지니스적 가치가 없을 때 그것은 "... [단지 내가 하기] 때문에 나는 [그 기능]이 필요해" 템플릿을 적용하면 위 예시에서 두 가지 시나리오는 아래와 같이 표현될 수 있다. 2
시나리오 1: 계좌에 잔고가 있음
조건 계좌에 잔고가 있다
그리고 카드가 유효하다
그리고 출금기에 현금이 들어있다
만일 고객이 현금을 요청한다
그러면 계좌에 출금액을 기입한다
그리고 현금을 출금한다
그리고 카드가 반환한다
"그리고"는 다수의 기본 조건이나 다수의 결과를 상호 연결하기 위해 씌었다.
시나리오 2: 계좌가 당좌 대월 한도를 넘어 당좌 차월 상태
조건 계좌가 당좌 차월 상태다
그리고 카드가 유효하다
만일 고객이 현금 요청한다
그러면 출금거절 메시지가 출력된다
그리고 현금이 출금되지 않는다
그리고 카드가 반환된다
두 시나리도 모두 똑같은 이벤트 발생을 바탕으로 하고 일부 기본 조건들과 결과들 조차도 보편적인 것들이다. 기본 조건들과, 이벤트 발생, 그리고 그결과들이란 템플릿을 재사용하여 활용하길 바란다.
인수조건은 실행 가능해야 한다
이 시나리오의 요소들(조건, 사건, 결과)은 잘 다듬어져있어서 코드에 직접 재연될 수 있다. JBehave 는 시나리오의 요소들을 자바 클래스들로 곧바로 사상할 수 있는 객체 모델을 정의한다.
각각의 조건들을 재연하는 클래스를 작성해보자.
public class AccountIsInCredit implements Given { public void setup(World world) { ... } } public class CardIsValid implements Given { public void setup(World world) { ... } }
그리고 사건 발생을 위한 클래스를 작성한다.
public class CustomerRequestCash implements Event { public void occurIn(World world) { ... } }
그리고 결과들에 대해서도 마찬가지로 적용한다. JBehave 는 이 모든 것들을 연결하시켜 실행한다. 이는 말하자면 하나의 "세상"을 창조하는 식으로, 객체들을 저장하는 어딘가가 되기도 하면서 그것들을 각각의 조건에 순차적으로 전달하므로써 투명한 상태로 세상을 번창시킬 수 있도록 한다. 그런다음 JBehave는 "사건 발생"을 세상에 알리고, 그로써 그 시나리오의 실질적인 행위를 수행한다. 결국 그것은 우리가 정의한 스토리의 결과들로 제어된다.
각각의 요소들을 대변하는 클래스가 있다면 다른 시나리오들 또는 스토리들에 재사용할 수도 있다. 첫번째로 그런 요소들은 계좌에 잔고가 있거나 혹은 카드가 유효한 것으로 설정되어 가상으로 구현된다. 이것들은 행위를 구현하는 출발점을 형성한다. 응용프로그램을 구현하면서 특정 조건들과 결과들은 구현된 실상의 클래스들이 사용되도록 대체되는데, 그리하여 시간이 지남에 따라 해당 시나리오가 완성되고, 구석구석의 기능들을 적절하게 테스트하기에 적합해지는 것이다.
BDD의 현재와 미래
잠깐동안의 중단이 있었지만, JBehave 다시 활발한 개발이 이뤄졌다. 그것의 핵심부는 꽤 견고하게 완성된 상태다. 다음 단계는 IntelliJ IDEA 와 Eclipse 와 같은 보편적인 Java IDE 와 통합하는 작업이다.
데이브 아스텔스(Dave Astels)는 BDD 에 활발하게 진척시켜왔다. 그의 블로그와 다양한 기고들은 질풍같은 활동을 자극해왔는데, 특히 rspec 프로젝트가 루비 언어로 BDD 프레임워크를 만들도록 하는 데서 그랬다. 나는 JBehave 를 루비로 구현하는 rbehave 작업에 착수했다.
내 몇몇 동료들은 그들이 수행하고 있는 다양한 프로젝트들에 BDD 를 적용하고 있고 그것들은 매우 성공적이었다. JBehave 의 (인수조건을 검증하는) 스토리 실행기능은 개발이 활발히 진행중이다.
목표는 분석가들과 테스터들이 일반적인 문서편집기로 스토리들을 포착할 수 있도록 하기 위해, 비지니스 영역의 모든 언어들로 행위 클래스들에 대한 토막들을 생성할 수 있는 통합 에디터를 만드는 것이다. BDD 는 많은 사람들의 도움으로 발전했고 나는 그들 모두에게 크게 감사한다.
- [/footnote] 통한 점진적 숙달은 별로 없었다는 느낌이 들었다. 돌아보건데 내가 "와, 문이 열렸구다." 라고 앞으로를 생각했던 것에 비해 "누군가 그걸 말해줬더라면 지금 같진 않았을텐데!" 하며 뒤만 돌아보는 경우가 더 많았다. 그래서 나는 TDD 가 좋은 일들로 연결되면서 함정들을 피해가는 방법으로써 보여질 수 있도록 해야겠다고 생각했다.
public class CustomerLookupTest extends TestCase { testFindsCustomerById() { ... } testFailsForDuplicateCustomers() { ... } ... }
- finds customer by id
- fails for duplicate customers
- ...
public class ClientDetailsValidator { private final AgeCalculator ageCalc; public ClientDetailsValidator(AgeCalculator ageCalc) { this.ageCalc = ageCalc; } }
그런 출발점에서부터 매츠와 나는 모든 애자일 테스터들이 이미 알고 있는 것을 발견하기 시작했다. 어떤 스토리의 행위는 단순히 그것의 인수조건(acceptance criteria)이며, 그 시스템이 모든 인수조건을 만족할 때 그것은 정확히 동작하고 있으며 그렇지 않을 경우는 그 반대라는 것이다. 그래서 우리는 스토리의 인수조건을 포착한 하나의 템플릿을 만들어냈다.
이 템플릿은 어느정도 자유로워서 분석가들에게 인위적이거나 강제적이지 않으면서도 충분히 구조화 되어서 그 스토리의 구성 요소들에 접근하여 자동화할 수 있어야 했다. 우리는 시나리오라는 명목으로 그런 인수조건의 작성에 착수했는데 아래와 같은 형식을 채택했다.
만일 사건이 발생했을 때, (When)
그러면 어떤 일이 수행된다. (Then)
이를 설명하기 위해서 고전적인 ATM 기계의 예시를 들어보자. 스토리 카드들 중 하나는 아래와 같을 것이다.
제목: 고객이 현금을 출금한다
고객으로 하여금,
은행에서 줄을 서지 않아도 되기 위해,
ATM을 통한 현금을 출금이 필요하다.
이제 우리는 이 스토리가 어느 시점에 배포되어야 할지를 어떻게 알 수 있을까? 몇가지 시나리오들을 고려해볼 수 있다. 계좌에 잔고가 있을 수도 있고, 잔고 이상의 당좌 차월 상태지만 당좌 대월 한도를 넘지 않은 경우, 그리고 당좌 대월 한도보다 더 당좌 차월이 발생한 경우 등이다. 물론 그밖의 경우, 말하자면 계좌에 잔고는 있으나 출금 금액으로인해 당좌 차월이 된다거나, 출금기에 충분한 현금이 들어있지 않은 등의 상황도 있을 수 있다.
위의 조건-만일-그러면(given-when-then)[footnote] [본문으로]