'Introducing BDD'에 해당되는 글 1건

  1. 2009.09.13 Introducing BDD (Behaviour Driven Development) 3
We can work IT out2009. 9. 13. 18:03
이 글은 댄 놀쓰(Dan North)가 2006년 3월에 Better Software 에 기고한 Introducing BDD 의 번역입니다. (번역 : 이홍주)

문제가 있었다면, 서로 다른 환경의 프로젝트에 대해 애자일 방법론인 테스트 주도 개발(test-driven development; 이하 TDD)을 수행하도록 하거나 가르칠 때마다 동일한 혼란 또는 오해들과 마주치곤 했다는 거다. 개발자들은 어디서부터 시작할지, 어떤 것을 테스트하고 어떤 것은 하지 않을지, 한번에 얼마만큼을 테스트할지, 테스트에 어떤 이름을 붙일지 그리고 테스트가 왜 실패했는지 등을 고민했다.

TDD 라는 계곡에 깊히 들어갈 수록 내 스스로의 여정이 점점 더 어두운 계곡의 연속이었던 것에 비해, 반복 수련을[각주:1]를 사용할 때 유용하다.


의미심장한 테스트 이름은 테스트가 실패했을 때 도움이 된다.

얼마 후, 코드를 변경한 것이 테스트 실패로 이어졌을 때 테스트 메서드 이름을 보는 걸로 그 코드에 의도된(부여된) 행위를 확인할 수 있다는 걸 알았다. 대체로 아래 셋 중 하나의 경우였다.

  • 내가 버그를 만들었다. 내가 나빠. 해결책: 버그 고치기.
  • 의도된 그 행위는 여전히 적절하지만 다른 곳으로 옮겨졌다. 해결책: 테스트를 옮기고 필요하면 내용도 고친다.
  • 시스템의 조건이 바뀌거나 해서 그 행위는 더이상 맞지 않다. 해결책: 테스트를 삭제한다.

애자일 프로젝트에선 그것에 대한 이해가 진전된 만큼 후자쪽의 경우가 더 많이 발생한다. 불행히도, TDD 초보자들은 테스트 코드들을 삭제하는 일이 마치 그들의 코드 품질을 조금이라도 낮추는 일이 될 것처럼 선천적인 두려움을 갖는다.

형태 변화인 will 또는 shall 과 비교하면 should 란 단어에 대한 애매함이 명확해진다. should 는 해당 테스트의 조건에 대해 "진짜 그래야 하나?" ("Should it? Really?") 라는 암시적인 질문을 하도록 한다. 이는 테스트 실패가 우리가 만든 버그에서 비롯된 것인지 아니면 단지 시스템의 행위에 대한 과거의 가정들이 현재에는 맞지 않기 때문인지를 쉽게 판별하도록 해준다.


"Behaviour" 는 "test" 보다 더 유용한 단어다.

이제 "test" 단어를 지울 수 있는 도구(agiledox)와 테스트 메서드마다 적용할 탬플릿이 생겼다. 문득 TDD 에 대한 사람들의 오해들이 거의 대부분 "test" 란 단어에서 시작됐다는 게 떠올랐다.

테스팅을 TDD 의 본질이 아니라고는 할 수 없지만, 작성한 테스트 메서드 집합은 우리가 코딩 작업을 확실히 하도록 보장하는 효과적인 방법이다. 그럼에도 메서드들이 시스템의 행위를 완전하게 기술하고있지 못하면 결국 그것들은 우리들을 방심하게 만든다.


나는 TDD 를 하면서 "test" 대신 "behaviour" 라는 말을 쓰기 시작했고 그것은 단지 적합할뿐만아니라 훈련과정에서 발생하는 모든 영역의 의문들을 풀어주기도 했다. 이제 나는 그런 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 는 많은 사람들의 도움으로 발전했고 나는 그들 모두에게 크게 감사한다.




  1. [/footnote] 통한 점진적 숙달은 별로 없었다는 느낌이 들었다. 돌아보건데 내가 "와, 문이 열렸구다." 라고 앞으로를 생각했던 것에 비해 "누군가 그걸 말해줬더라면 지금 같진 않았을텐데!" 하며 뒤만 돌아보는 경우가 더 많았다. 그래서 나는 TDD 가 좋은 일들로 연결되면서 함정들을 피해가는 방법으로써 보여질 수 있도록 해야겠다고 생각했다.

내가 찾은 해답은 행위 주도 개발(behaviour-driven development; 이하 BDD)이었다. BDD는 기정 애자일 방법론들로부터 진화한 것이면서 그런 방법론들이 애자일적인 소프트웨어의 출하를 처음 접하는 팀들에게 접근하기 쉽고 효율적이도록 디자인 되었다. 시간이 지남에 따라 BDD 는 테스팅에 대한 애자일적 분석과 자동화된 합격판정 등의 더 넓은 영역을 포함하도록 확장되어왔다.


테스트 메서드 이름은 문장이 되어야 한다.

나의 첫번째 깨달음의 순간은 내 동료인 크리스 스티븐슨(Chris Stevenson)이 개발한 agiledox 라는 믿을 수 없이 단순한 도구를 보았을 때였다. 그것은 JUnit 의 테스트 클래스를 사용하여 메서드 이름들을 평범한 문장들로 출력하는데, 예를 들어 하나의 테스트 케이스는 아래와 같은 모양을 하고 있다. 
public class CustomerLookupTest extends TestCase {
    testFindsCustomerById() { ... }
    testFailsForDuplicateCustomers() { ... }
    ...
}
그리고 이를 아래와 같이 풀어서 표현한다.

CustomerLookup
- finds customer by id
- fails for duplicate customers
- ...

클래스 이름과 메서드 이름들에 쓰인 "test" 란 단어는 벗겨 없어졌고, 캐멀 케이스(camel-case) 메서드 이름은 일반적인 구문으로 바뀌었다. 이것이 전부지만 그 영향은 놀라울 정도다.

개발자들은 이것이 적어도 그들의 문서화 작업에 유용하겠다는 생각으로 테스트 메서드의 이름들을 실상의 문장들처럼 작성하게 됐다. 더욱이 메서드 이름에 비지니스 용어를 사용한다면 생성된 문서가 비지니스 사용자들이나 분석가들 그리고 테스터들까지 이해할 수 있게 된다는 것도 알게 됐다.


간단한 문장 템플릿이 테스트 메서드에 집중하게 한다.

그래서 나는 테스트 메서드 이름이 "should" 로 시작되도록 하는 관습을 만들고 따르기 시작했다. "이 클래스가 어떤 행위를 해야 한다(should do something)" 라는 식의 문장 템플릿은 현재 다루는 클래스에 대해서만 정의될 수 있음을 의미한다. 이때문에 집중하게 되고, 만약 이런 템플릿에 어울리지 않는 테스트 코드를 작성하고 있다면 그것이 엉뚱한 행위란 걸 알게 한다.

실례로, 내가 화면에서 입력받은 내용에 대한 유효성 검사(validation)를 수행하는 클래스를 작성할 때, 모든 필드들은 이름과 성 따위의 일반적인 고객에 대한 상세정보였는데, 거기엔 생일 필드와 나이 필드도 함께 있었다. 나는 ClientDetailsValidatorTest 라는 클래스를 작성하면서 testShouldFailForMissingSurname 과 testShouldFailForMissingTitle 등의 메서드들을 명명했다.

그런 후에 나이를 계산하는 코드를 짜면서 판단하기 묘한 문제에 직면했는데 이런 것들이다. 나이와 생일이 빠짐 없이 있다 해도 서로 맞지 않으면? 생일 값이 오늘 날짜를 나타내고 있다면? 연도 없이 월일만 있어서 나이를 계산하지 못하는 경우라면? 이런 문제들이 생각날 때마다 그런 행위들에 대해 성가신 테스트 메서드 이름들 추가해야 했기 때문에 손 떼고 다르게 해보기로 마음먹었다. 결국 AgeCalculatorTest 라는 테스트 클래스를 동반한 새로운 클래스 AgeCalculator 를 작성하게 됐다. 나이를 계산하기 위한 모든 행위들은 나이 계산기 역할을 하는 새로운 클래스로 옮겨갔고, 결국 유효성 검사에서 클래스의 나이 계산이란 행위에 대해서는 새로운 클래스와 함께 올바로 작용하는지만을 테스하면 되게 되었다.

만약 어떤 클래스가 하나 이상의 행위를 하고 있다면, 보통 내가 다른 클래스들을 만들어서 그것들을 처리하도록 해야 한다는 징조라고 본다. 그럴 땐 무엇을 한다 를 기술하는 인터페이스로써 새로운 서비스를 정의한 다음 그 서비스를 클래스의 생성자에 전달한다.
public class ClientDetailsValidator {
    private final AgeCalculator ageCalc;
    public ClientDetailsValidator(AgeCalculator ageCalc) {
        this.ageCalc = ageCalc;
    }
}
객체들을 서로 연결하는, dependency injection 으로 알려진 이러한 방법은 특히 시뮬레이션을 위한 모의 객체[footnote] [본문으로]
  • [/footnote] 라는 식으로 귀착되곤 한다. 이런 접근은 일부 더 난해한 요구사항들에 대한 유효범위를 판단하기 쉽게 해주기도 한다.

    그런 출발점에서부터 매츠와 나는 모든 애자일 테스터들이 이미 알고 있는 것을 발견하기 시작했다. 어떤 스토리의 행위는 단순히 그것의 인수조건(acceptance criteria)이며, 그 시스템이 모든 인수조건을 만족할 때 그것은 정확히 동작하고 있으며 그렇지 않을 경우는 그 반대라는 것이다. 그래서 우리는 스토리의 인수조건을 포착한 하나의 템플릿을 만들어냈다.

    이 템플릿은 어느정도 자유로워서 분석가들에게 인위적이거나 강제적이지 않으면서도 충분히 구조화 되어서 그 스토리의 구성 요소들에 접근하여 자동화할 수 있어야 했다. 우리는 시나리오라는 명목으로 그런 인수조건의 작성에 착수했는데 아래와 같은 형식을 채택했다.


    조건이 주어짐, (Given)
    만일 사건이 발생했을 때, (When)
    그러면 어떤 일이 수행된다. (Then)


    이를 설명하기 위해서 고전적인 ATM 기계의 예시를 들어보자. 스토리 카드들 중 하나는 아래와 같을 것이다.

    제목: 고객이 현금을 출금한다
    고객으로 하여금,
    은행에서 줄을 서지 않아도 되기 위해,
    ATM을 통한 현금을 출금이 필요하다.

    이제 우리는 이 스토리가 어느 시점에 배포되어야 할지를 어떻게 알 수 있을까? 몇가지 시나리오들을 고려해볼 수 있다. 계좌에 잔고가 있을 수도 있고, 잔고 이상의 당좌 차월 상태지만 당좌 대월 한도를 넘지 않은 경우, 그리고 당좌 대월 한도보다 더 당좌 차월이 발생한 경우 등이다. 물론 그밖의 경우, 말하자면 계좌에 잔고는 있으나 출금 금액으로인해 당좌 차월이 된다거나, 출금기에 충분한 현금이 들어있지 않은 등의 상황도 있을 수 있다.

    위의 조건-만일-그러면(given-when-then)[footnote] [본문으로]
  • Posted by Lyle