요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

‘테스트 주도 개발(Test-Driven Development, 이하 TDD)’은 자바(Java) 테스트 도구인 JUnit을 만든 켄트 벡이 만든 개발 방법으로, 아래와 같이 빨강/초록/리팩토링이라는 사이클을 반복하는 것이 특징이다.

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

TDD, 실패하는 테스트부터 작성해 얻는 것 5가지

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

‘테스트 주도 개발(Test-Driven Development, 이하 TDD)’은 자바(Java) 테스트 도구인 JUnit을 만든 켄트 벡이 만든 개발 방법으로, 아래와 같이 빨강/초록/리팩토링이라는 사이클을 반복하는 것이 특징이다.

 

출처: interra.com

 

TDD를 처음 접하는 사람들이 가장 놀라는 점은 작동하는 코드를 만들기 전에 실패하는 테스트를 먼저 작성한다는 것이다.

 

빨강 - 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다. - <테스트 주도 개발>

 

왜 처음에는 컴파일조차 되지 않을 수 있는 실패하는 테스트를 먼저 작성하는 것일까? 그럼으로써 무엇을 얻을 수 있을까?

 

1. 작동하는 깔끔한 코드로 이끈다

TDD는 테스트를 먼저 작성한다. 그러다 보니 많은 사람들이 TDD를 테스트를 잘 작성하기 위한 방법으로 오해한다. 켄트 벡이 자신의 책에서 밝혔듯이 TDD의 궁극적인 목표는 테스트가 아니라 작동하는 깔끔한 코드다.

 

작동하는 깔끔한 코드(clean code that works.) 론 제프리즈(Ron Jeffries)의 핵심을 찌르는 이 한마디가 테스트 주도 개발의 궁극적인 목표다. - <테스트 주도 개발>

 

여기서 말하는 ‘작동하는 깔끔한 코드’가 무엇인지 한 마디로 정의하기는 어렵다. 개발자 수만큼 다양한 맥락에 따른 정의가 있기 때문이다.* 그러나 한 가지 명확한 건 단순히 코딩 기술에서 한정되는 것이 아니라 분석, 설계까지 아우른다는 것이다.

* 엉클 밥(Uncle Bob)으로 잘 알려진 로버트 마틴의 <클린 코드>에는 소프트웨어 대가들의 깔끔한 코드에 관한 다양한 정의를 찾아볼 수 있다.

 

TDD의 아이러니 중 하나는 TDD가 테스트 기술이 아니라는 점이다. TDD는 분석 기술이며, 설계 기술이기도 하다. 사실은 개발의 모든 활동을 구조화하는 기술이다. - <테스트 주도 개발>

 

작동하는 깔끔한 코드가 무엇인지 명확하게 정의하기는 어렵지만 우리는 직관적으로 무엇이 깔끔한 코드인지 아닌지 알고 있다. 바로 ‘변경이 쉬운 코드’이다. 우리는 날마다 수없이 코드를 고친다. 깔끔하지 못한 코드는 고치기 어렵다. 여기서 고치기 어렵다는 것은 여러 가지 의미를 내포한다. 예를 들면 코드 읽기가 어렵다든지, 코드 중복 문제로 고쳤지만 다른 곳에서 버그를 만들거나 설계의 문제로 하나의 변경이 여러 레이어에 걸친 변경으로 이어진다든지, 코드 의도를 알 수 없다든지 등의 의미가 있을 수 있다. 작동하는 깔끔한 코드를 정의하기는 어렵지만 분명한 사실은 변경이 쉬운 코드라는 것이다.

 

우리는 변경 용이한 코드를 원한다. 어떻게 하면 변경이 용이한 코드를 만들 수 있을까? 다른 사람에게 배울 수 있을까? 그럴 수 있다면 행운이다. 왜냐하면 보통 배울 만한 사람을 찾기가 어렵고, 찾더라도 보통 메타인지가 낮아 왜 본인이 잘 하는지 다른 사람에게 설명하기 어렵기 때문이다. 또한 배우더라도 그 사람이 처한 맥락과 내 맥락이 다르기 때문에 배운 것을 응용하기 쉽지 않다.

 

내가 다년간 TDD를 경험하며 깨달은 것은 테스트를 먼저 작성하는 것이 변경이 용이한 코드로 나를 이끈다는 것이다. 작동하는 깔끔한 코드로 테스트가 주도(Test-Driven)하는 것이다.

 

 

2. 느슨한 결합을 만든다

앞서 나는 테스트가 나를 작동하는 깔끔한 코드로 이끈다고 했다. 구체적으로 어떻게 이끈다는 것일까? 소프트웨어 설계에서 말하는 ‘느슨한 결합’ 이라는 원칙이 있다.

 

첫 번째 정의는 “시스템의 구성요소(component)가 서로 약하게 연관돼 관계를 떼어낼 수 있고, 그때문에 한 구성요소에 변화가 생겼을 때 다른 구성요소의 성능이나 존재에 최소한의 영향을 끼치는 상태”라고 요약할 수 있을 듯합니다.


두 번째 정의는 조금 다른데요. “구성요소가 다른 구성요소의 정의에 대해 많은 지식이 없이도 사용할 수 있는 상황”을 칭한다고 말합니다. 전자가 구성요소 간의 결합의 양상을 말했다면, 후자는 그에 따른 효과와 결합의 범주에 대해 말합니다. 클래스, 인터페이스, 데이터, 서비스 등이 구성요소가 될 수 있다는 뜻이죠. - <느슨한 결합(loosely coupled) 원칙을 활용한 소프트웨어 설계>

 

앞서 나는 TDD의 궁극적인 목표는 작동하는 깔끔한 코드라고 말하며 핵심적인 요소로 ‘변경 쉬운 코드’를 뽑았다. 느슨한 결합은 변경과 관련이 깊다. 강하게 결합되어 있으면 그만큼 변경이 어렵기 때문이다. 그런데 내가 만든 코드의 결합도가 낮은지 높은지 어떻게 알 수 있을까? 바로 테스트를 작성해 보는 것이다.

 

테스트를 작성하기가 쉽지 않다면, 그것은 테스트가 아니라 설계에 문제 있다는 신호다. 결합도가 낮고 응집성이 높은 코드는 테스트하기 쉽다. - <익스트림 프로그래밍>

 

언뜻 글로만 읽으면 이해하기 어려운데 코드를 보면 이해하기 수월할 것이다.

 

웹 애플리케이션에서 HTTP 요청에 대한 응답 상태 코드 값이 4XX(클라이언트 에러), 5XX(서버 에러)이면 개발자에게 알림을 주는 간단한 모니터링 기능을 만든다고 가정해 보자.

 

HTTP 요청과 응답 사이에 미들웨어나 인터셉터를 만들어 구현할 수 있다. 아래 코드는 Go 언어의 웹 프레임워크인 Gin에서 제공하는 미들웨어로 구현한 것으로 HTTP 응답 상태 코드 값이 에러(4XX, 5XX)이면 슬랙(Slack) 메신저로 알림을 보낸다.

 

func HttpResponseStatusCodeErrorHandler() gin.HandlerFunc {
  return func(c *gin.Context) {
    c.Next()
    if c.Writer.Status() >= 400 && c.Writer.Status() <= 599 {
      SlackNotifier{}.SendMessage("HTTP 응답 오류")
    }
  }
}

 

이제 테스트 코드를 작성해 보자. 테스트를 위해 URL(/api/test)을 만들고 응답 상태 코드 500(Internal Server Error)을 반환했다.(테스트는 Given/When/Then 스타일로 작성했다)

 

func TestHttpResponseStatusCodeErrorHandler(t *testing.T) {
  // given
  router := gin.Default()
  router.Use(HttpResponseStatusCodeErrorHandler())   // 미들웨어 추가
  router.GET("/api/test", func(ctx *gin.Context) {   // 테스트를 위한 URL
    ctx.Status(http.StatusInternalServerError)
  })

  // when
  req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
  rec := httptest.NewRecorder()
  router.ServeHTTP(rec, req)
	
  // then
  // ?
}

 

테스트는 미들웨어(HttpResponseStatusCodeErrorHandler)가 SlackNotifier의 SendMessage 함수를 호출했는지 확인해야 한다. 하지만 현재 코드에서는 SendMessage 함수가 호출되었는지 확인하기 어렵다. 테스트 가능하도록 코드를 수정해 보자.

 

먼저 Notifier 인터페이스를 만들고 SlackNotifier가 구현한다.*

* Go 언어는 덕 타이핑(Duck Typing) 기반이다. 덕 타이핑이란, 객체가 어떤 타입에 부합하는 변수와 메서드를 가질 경우 객체를 해당 타입에 속하는 것으로 간주하는 방식이다. 덕 테스트(The Duck Test)에서 유래되었는데, 다음과 같은 명제로 정의한다. “만약 어떤 새가 오리처럼 걷고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리로 부를 것이다.”

 

type Notifier interface {
  SendMessage(message string)
}

type SlackNotifier struct {
  //...
}

func (SlackNotifier) SendMessage(message string) {
  // ...
}

 

이제부터 미들웨어는 SlackNotifier를 직접 사용하는 것이 아니라 Notifier 인터페이스를 인자로 받아 사용한다.

func HttpResponseStatusCodeErrorHandler(notifier Notifier) gin.HandlerFunc {
  return func(c *gin.Context) {
    c.Next()
    if c.Writer.Status() >= 400 && c.Writer.Status() <= 500 {
      notifier.SendMessage("HTTP 응답 오류")
    }
  }
}

 

테스트 코드는 Notifier 인터페이스를 구현한 목 객체(MockObject)을 미들웨어에 주입하여 작성한다.

type MockNotifier struct {
  sentMessage string
}

func (m *MockNotifier) SendMessage(message string) {
  m.sentMessage = message
}

func TestHttpResponseStatusCodeErrorHandler(t *testing.T) {
  // given
  router := gin.Default()
  mock := MockNotifier{} // 목 객체
  router.Use(HttpResponseStatusCodeErrorHandler(&mock)) // 미들웨어에 목 객체를 전달
  router.GET("/api/test", func(ctx *gin.Context) {
    ctx.Status(http.StatusInternalServerError)
  })

  // when
  req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
  rec := httptest.NewRecorder()
  router.ServeHTTP(rec, req)
	
  // then
  assert.Equal(t, "HTTP 응답 오류", mock.sentMessage)
}

 

인터페이스를 사용하여 느슨한 결합을 만들었다. 테스트가 수월해졌으며, 결과적으로 변경이 용이해졌다. 예를 들어 알림을 슬랙이 아닌 메일로 받고 싶다면 미들웨어를 수정할 필요가 없고 메일로 알림을 주는 객체를 만들고 SendMessage 인터페이스를 구현하면 되기 때문이다.

 

 

3. 잘못을 일찍 깨달을 수 있다

어느 개발자가 차세대 프로젝트에 투입되어 코딩 없이 3개월 동안 분석/설계 작업을 하고 있다고 내게 말했다. 게다가 자신이 개발할 대상이 아닌 것을 분석하고 있다고 했다. 분석 → 설계 → 구현 → 테스트 → 오픈. 전형적인 폭포수 방법론에 따른 개발이다.

 

먼저 이러한 방식은 긴 시간에 대한 추정이 기저에 깔려 있다. 예를 들면 분석에 몇 개월 쓰고 설계에 몇 개월 쓴다는 것이다. 세상은 불확실성으로 가득 차 있다. 우리를 둘러싼 환경이 급격하게 변하는 상황에서 우리가 가진 빈약한 정보와 제한된 인지 능력으로는 추정이 부정확할 수밖에 없다. 그래서 설계 단계가 끝나고 구현 단계에서 분석/설계가 잘못된 것을 뒤늦게 깨닫는 것이 비일비재한 것이다. 그렇다고 프로젝트 기간이 정해져 있는 상황에서 분석/설계가 잘못되었는 이유로 뒤로 돌아갈 수 있을까? 몹시 어려운 일이다. 돌아간다 하여도 매우 큰 비용이 발생한다.

 

복잡한 문제를 제대로 이해는 경우는 거의 없지만, 이해하고 착각하는 경우는 많으며 그 착각은 재앙으로 가는 확실한 길이다. 그렇기 때문에 사람들에게 문제에 대해 끊임없이 다양한 가정을 하도록 권장해야 한다. - <테크니컬 리더>

 

분석/설계를 하며 우리는 착각한다. 문제를 알고 있다고, 이해하고 있다고. 하지만 현실은 너무나도 다르다. 심리학에서 더닝-크루거 효과라고 부르는 것이 있다. 실력이 떨어지는 사람일수록 자기 평가가 부정확하게 높다는 것이다. 아는 것이 없을 때 자기 확신이 높을 가능이 있다.

 

출처: https://orale.co.kr/더닝-크루거-현상/

 

하지만 진짜 문제는 잘못 아는 데서 생기는 것이 아니다. 잘못을 늦게 깨닫는 데서 생긴다. 누구나 잘못할 수 있다. 잘못을 일찍 깨달을 수 있다면 상황은 변할 수 있지만 너무 늦게 깨닫는다면 돌이킬 수 없게 되는 것이다.

 

잘못을 늦게 깨닫지 않으려면 빨리 써야 한다. 소프트웨어의 가치는 쓰일 때 비로소 나타나기 때문이다. 그래서 쓰임새가 중요한 것이다. 테스트 코드를 먼저 작성하는 것은 쓰임새를 정의한다는 것이고 테스트 코드를 실행한다는 것은 ‘쓴다’는 의미이다. 테스트는 빨리 써보게 하고 잘못 일찍 깨달을 수 있게 만든다.

 

 

4. 과업을 정의하고 필요 없는 일을 제거한다

HBR에 실린<지식 근로자의 생산성을 어떻게 끌어올릴 것인가>에서 피터 드러커는 지식 근로자의 생산성을 가장 크게 높일 수 있는 방법으로 과업을 정의하고 해야 할 필요가 없는 일들을 제거하라고 말했다.

 

지식과 서비스 산업에서 생산성을 끌어올리고 똑똑하게 일하기 위해서는 먼저 이 질문부터 해야 한다. “과업이 무엇인가? 우리는 무엇을 달성하고자 하는가? 우리는 이것을 대체 왜 하는가?” 지식 서비스 업무에서 가장 쉽고 아마도 가장 크게 생산성을 높일 수 있는 방법은 과업을 정의하고, 해야 할 필요가 없는 일들을 제거하는 것이다. - <HBR - 지식 근로자의 생산성을 어떻게 끌어올릴 것인가>

 

이와 비슷하게도, 요즘 많은 조직에서 도입한 OKR 역시 낭비를 줄이고 조직의 성과를 달성하기 위해 목표를 명확히 하는 것이다. 프로그램을 만들 때도 마찬가지이다. 목표를 명확히 해야 시간을 낭비하지 않는다.

 

테스트를 먼저 작성한다는 것은 프로그램이 무엇을 해야 하는지 명시적으로 정의하는 것이다. 이런 행위는 내가 무엇을 아는지와 무엇을 모르는지를 인지하게 만든다. 즉, 메타인지를 높이는 것이다. 테스트를 작성하다 보면 “아! 이런 경우 있었구나.” 하고 모르는 것을 발견하는 경우가 수두룩하기 때문이다. 모르는 것을 인지하고 집중함으로써 더닝-크루거 효과에서 보여주는 ‘무지함’에서 벗어나 ‘깨달음’으로 갈 수 있는 것이다.

 

‘아는 것에서 모르는 것으로’는 우리가 어느 정도의 지식과 경험을 가지고 개발을 시작한다는 점, 개발 하는 와중에 새로운 것을 배우게 될 것임을 예상한다는 점 등을 암시한다. 이 두 가지를 합쳐보자. 우리는 아는 것에서 모르는 것으로 성장하는 프로그램을 갖게 된다. - <테스트 주도 개발>

 

위에서 켄트 벡은 ‘성장하는 프로그램’이라는 은유를 썼다. 성장의 핵심은 학습이다. 효과적인 학습 방법 중 하나로 ‘시험 효과(Testing Effect)’라는 것이 있다. 간단하게 말하면 반복해서 같은 것을 공부하는 것보다는 한 번 공부하고 여러 번 반복해서 시험을 보는 것이 효과적이라는 것이다. 그 이유는 뇌와 관련이 있는데 연구에 따르면 우리의 뇌는 어떤 것을 떠올리려고 노력할수록 기억이 오래간다고 한다. 테스트는 학습에 효과적인 도구이다.

 

모든 시스템을 만들고 오픈 전에 테스트하는 것이 아니라, 부담이 적은 작은 테스트를 자주 실행하다 보면 나중에 생기는 문제에 대한 불안을 낮출 수 있다. 또 잘 모르고 있거나 잘못 알고 있는 점을 발견하고 그것을 바로잡는 방향으로 시간을 쓸 수 있다.

 

 

5. ‘테스트할 시간이 없다’는 죽음의 나선에서 벗어날 수 있다

내가 아는 개발자는 동료에게 테스트 코드 작성을 제안했다고 한다. 하지만 돌아오는 대답은 “테스트할 시간이 없다”라는 것이었다. 내 경험으로도 사람들에게 테스트 작성을 제안하면 열의 아홉은 시간이 없다고 말한다. <테스트 주도 개발>에서는 ‘테스트할 시간이 없다’의 죽음의 나선을 보여준다.

 

출처: <테스트 주도 개발> 갈무리

 

스트레스를 많이 받으면 테스트를 점점 더 뜸하게 한다. 테스트를 뜸하게 하면 당신이 만드는 에러는 점점 많아질 것이다. 에러가 많아지면 더 많은 스트레스를 받게 된다. - <테스트 주도 개발>

 

“테스트할 시간이 없다”라고 말하는 동료의 또 다른 말이 있다.

포스트맨(Postman)으로 테스트하고 있어요.

 

UI 도구를 이용한 수동 테스트를 말하고 있었다. 하지만 수동 테스트만으로는 죽음의 나선에서 벗어날 수 없다. 프로젝트 초반에는 코드가 얼마 되지 않기 때문에 수동으로 테스트하는 것이 충분할 수 있다. 하지만 코드는 슬금슬금 늘어난다. 내가 참여했던 프로젝트에서는 1년이 지나자 개발자의 생산성이 급격하게 떨어졌다. 한 부분을 수정하면 다른 부분들이 문제가 생기고 있었다. 늘어난 코드에 개발자들은 내가 이 부분을 고치면 다른 곳에서 에러가 발생하여 시스템 장애로 이어질 수 있다는 생각에 변경에 두려움을 갖기 시작했고, 실제 개발하는 시간 보다 변경 영향도 분석에 시간을 더 쓰고 있었던 것이다. 결국 개발자의 생산성이 뒤집어진 U자 곡선을 그리게 된 것이다.

 

출처: 작가

 

만약 당신이 코드를 자신 있게 변경할 수 없다면, 당신의 테스트는 (혹은 좋은 테스트) 충분하지 못 하다는 것을 의미합니다. 지나치다는 신호는 당신이 코드를 변경할 때, 코드를 변경하는 것 보다 테스트를 변경하는 노력이 더 많이 든다고 느낄 때를 말하는 거죠. - <TDD는 죽었는가?>

 

수동 테스트는 시간이 지나고 범위가 늘어남에 따라 인간의 인지 능력의 한계와 망각으로 테스트 케이스를 모두 기억할 수 없게 되며, 누락이 발생하기 시작한다. 게다가 코드들은 반드시 의존성을 가지기 때문에 하나를 고치면 예상하지 못하는 곳에서 오류가 발생하는 경우가 허다하다. 따라서 모든 테스트 케이스를 수행하는 회귀 테스트를 해야 하지만, 시간 제약으로 모든 테스트 케이스를 실행해 볼 수 없어 예상치 못한 에러가 발생하는 악순환이 반복되는 것이다. 결국 코드를 신뢰할 수 없게 된다.

 

혹자는 “시간이 없으니 일단 작동하는 코드를 만들고 나중에 자동화된 테스트를 작성하면 되지 않냐”라고 반문할 수 있겠다. 내가 주니어 시절 테스트를 나중에 작성한 경험이 있다. 먼저 심리적으로 시간 내기가 어려웠다. 이미 작동하고 있는 코드였기 때문이었다. 문제의식을 갖기 어려워 동기 부여가 되지 않았다. 시간을 내어 테스트를 작성하더라도 앞서 ‘느슨한 결합’에서 살펴보았듯이 기존 코드가 테스트하기 어려웠다.

 

작동하는 코드를 만들기 전에 먼저 테스트를 작성하라. 테스트를 먼저 작성함으로써 ‘테스트할 시간 없다’는 죽음의 나선에서 벗어날 수 있다.

 

테스트를 언제 작성하는 것이 좋을까? 테스트 대상이 되는 코드를 작성하기 직전에 작성하는 것이 좋다. 코드를 작성한 후에는 테스트를 만들지 않을 것이다. (중략) 테스트를 먼저 하면 스트레스가 줄고, 따라서 테스트를 더 많이 하게 된다. - <테스트 주도 개발>

 

 

마치며

테스트를 먼저 작성한다는 것은 어색하고 이상하다. 직관에 반하기 때문이다. 하지만 현실은 직관에 반하는 경우가 많다. 가장 대표적인 것이 테스트를 먼저 작성하는 것이다.

 

소프트웨어는 사람이 만든다. 사람은 관계를 중요시하고 관계의 핵심은 신뢰이다. 신뢰는 만드는 방법은 많지만, 코드를 통해 신뢰를 만드는 방법은 테스트라고 생각한다. 테스트를 먼저 작성한다는 단순하고 놀라운 방법으로 나는 함께 만드는 사람들의 신뢰뿐만 아니라 사용하는 사람들의 신뢰를 얻을 수 있었다.

 

작동하지 않는 코드를 작성한 사람을 신뢰하기는 힘들다. 작동하는 깨끗한 코드를 작성하고 자동화된 테스트로 의도를 드러내면, 팀원들이 당신을 신뢰할 근거가 생긴다. - <익스트림 프로그래밍>

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 0
실수투성이 개발자의 성장 이야기
133
명 알림 받는 중

작가 홈

실수투성이 개발자의 성장 이야기
133
명 알림 받는 중
솔루션 회사를 시작으로 컨설팅 회사 그리고 서비스 회사를 거쳐 지금은 스타트업에서 개발자로 일하고 있습니다. 주로 오픈소스 제품 개발, 기술 자문 및 코칭을 하고 있으며 기술과 인간의 상호작용에 관심이 많습니다.

좋아요

댓글

스크랩

공유

공유

지금 회원가입하고,
요즘IT가 PICK한 뉴스레터를 받아보세요!

회원가입하기
요즘IT의 멤버가 되어주세요! 요즘IT의 멤버가 되어주세요!
요즘IT의 멤버가 되어주세요!
모든 콘텐츠를 편하게 보고 스크랩해요.
모든 콘텐츠를 편하게 보고 스크랩 하기
매주 PICK한 콘텐츠를 뉴스레터로 받아요.
매주 PICK한 콘텐츠를 뉴스레터로 받기
로그인하고 무료로 사용하기