대메뉴 바로가기 본문 바로가기

데이터 기술 자료

데이터 기술 자료 상세보기
제목 비용 감소를 위한 Bottom Up Test
등록일 조회수 5978
첨부파일  

비용 감소를 위한 Bottom Up Test


테스트 꾸준히 잘하는 법



많은 개발자들이 스스로 작성한 코드를 테스트하는 것이 바람직함을 알고 있다. 하지만 잘 하지 않으려고 한다. 코드나 스크립트를 통해 모듈을 테스트하는 것이 생각보다 어렵기 때문이다. 이것은 결국 테스트에 드는 비용 대비 효과가 낮다는 의미다. 이 글에서는 어떻게 하면 테스트에 드는 비용을 낮춰 개발자가 테스트를 꾸준히 할 수 있는지에 대해 이야기한다.



개발자가 꾸준히 테스트를 잘 하는 비법은 결국 테스트의 비용을 낮추는 비법과 동일하다. 테스트의 비용대비 효과가 크다면 하지 않을 명분이 없다. 그렇다면 문제는 “어떻게 하면 개발자가 수행하는 테스트의 비용을 낮출 수 있을까?”라는 질문으로 좁혀진다. 여기에는 두 가지 답이 있다. 첫 번째, 테스트하고자 하는 모듈의 디자인과 구현이 잘 되어 있어서 모듈간의 의존성이 낮아야 한다. 모듈을 테스트하기 위해서 각 모듈을 분리해야 할 필요가 있는데, 의존성이 잘 분리되어 있다면 테스트하는 것이 쉬워진다. 두 번째, 자신이 작성한 모든 부분에 대해 테스트해야 할 필요가 없고, 본인이 테스트를 집중적으로 해야 하는 부분을 알아야 한다. 테스트 작성 비용이 높고 효과가 낮다면 차후에 수동으로 테스트하거나 QA를 통해 테스트하는 것이 답이다. 그럼 각각에 대해 자세히 살펴보자.



모듈의 의존성을 줄이는 법



어떤 객체 B가 A에 의존성이 걸려 있다. 이 경우에 B를 테스트하기 위해서는 A 전체가 필요하다. 만약 A가 다른 여러 모듈에 의존성이 걸려 있다면 어떨까? B를 테스트하기 위해 전체 모듈을 가져다 쓰지 않으려는 이상, A를 대신하는 mock object를 만드는 것과 같은 부가적인 작업이 더 필요하게 된다. mock object를 사용하게 되면 테스트 제작도 오래 걸리게 되지만, 관리해야 하는 코드도 늘어나 결국 테스트에 들어가는 비용을 증가시킨다. 하지만 정말 B는 A 전체가 필요한 걸까?

경험상, B는 A 전체가 아닌 A의 부분에 의존성이 걸려 있는 경우가 많다. 만약 그렇다면 의존성이 있는 부분만을 떼어내서 새로 a를 만들고 A와 B 모두가 a에 의존성을 거는 것이 옳다. 이 경우에는 B를 테스트하기 위해 a만 있으면 되기 때문에 mock object를 만들 이유도 없고, 테스트하는 것도 훨씬 쉬워지게 된다. 하지만 많은 경우에, 개발자들은 알면서도 a를 분리하지 않는다. 귀찮기 때문이다. 불행히도 여기에도 깨진 유리창의 법칙이 적용된다. 이렇게 한번 부주의하게 걸려버린 의존성 때문에 B는 A 전체를 계속해서 사용하게 되고 A에 대한 의존성은 점점 증가해 나중에는 분리하기 어려운 지경에 놓이게 된다.

좋은 디자인을 위해 여러 가지 디자인 패턴을 사용하는 것도 좋지만, 이와 같이 기본적인 것들만이라도 잘 지킨다면 모듈간의 의존성은 훨씬 줄어들게 되고 결과적으로 테스트 작성 비용을 줄일 수 있게 된다.





테스트를 꼭 해야 하는 것은 아니다



켄트벡의 ‘Where, Oh Where to Test?’라는 에세이가 있다. 여기서 그는 테스트를 위해 System Level Test를 해야 하는지 Unit Level Test를 해야 하는지 판단할 때에 Cost, Stability, Reliability를 반드시 고려해야 한다고 말한다. 여기서는 이 세 가지 요소를 테스트해야 할지 말아야 할지를 판단하는 요소로 사용한다.

첫 번째로 고려해야 할 것은 Cost이다. Cost는 매우 포괄적인 개념으로 Stability와 Reliability를 포함한다. 이것은 테스트를 작성하고 수행하는 데 걸리는 시간, 그리고 유지보수에 들어가는 비용을 말한다. 테스트 그 자체는 결과 제품에 어떠한 가치도 더하지 않기 때문에 가능한 한 최소한의 비용을 들여서 주요 부분을 검증하는 것이 좋다.

비용을 줄이기 위한 가장 좋은 방법은 테스트 스크립트 수를 줄이고, mock object와 같이 부가적으로 필요한 코드도 최소한으로 줄이는 것이다. 테스트 스크립트 수를 줄이면 테스트 작성 및 유지보수 비용도 줄어들게 된다. 물론 어딘가에 존재하는 테스트와 코딩에 뛰어난 개발자들은 테스트 역시도 잘 디자인하고 작성해 변화에 최소한의 영향을 받게끔 한다고 들었다. 하지만 테스트는 본질적으로 버그를 검출하기 위한 수단이기 때문에, 보통의 개발자들이 테스트에까지 집중을 유지하는 것은 어렵다. 그래서 문제가 없다고 판단됨에도 불구하고 계속 테스트가 깨진다면, 그 테스트는 주석 처리되어 관리에서 멀어질 확률이 높다. 그리고 그런 스크립트가 많아진다면 본래의 회기 테스트로서의 의미를 가지지 못하게 된다. 그렇기 때문에 애초에 스크립트를 최소한으로 작성하는 것이 작성과 유지보수 비용을 줄이는 좋은 방법이다.

두 번째로 Stability를 고려해야 한다. 시스템을 조금 변경했고 문제가 없는데도 불구하고 테스트가 깨져버린다거나, 심지어 시스템을 변경하지 않았는데도 테스트가 깨지는 경우가 발생한다. 이러면 개발자는 계속 테스트에 신경을 써주어야 하기 때문에 테스트 관련 Cost가 올라간다. 만약 중요하지 않은 부분이면서 Stability를 갖춘 테스트를 작성하기가 어렵다면 작성하지 않는 것이 좋다. 이런 테스트는 개발자가 스크립트를 통해 직접 하는 것보다, 수동으로 하거나 이후에 QA에 의해 테스트하는 것이 비용대비 효과가 높다.

마지막으로 고려해야 할 것은 Reliability다. 테스트는 본질적으로 문제가 없음을 보장할 수 없을 뿐만 아니라, 우리에게 주어진 리소스에도 한계가 있기 때문에 우리는 문제가 있을 만한 곳과 버그가 발생했을 때 큰 피해가 예상되는 곳에 집중적으로 테스트해야 한다. 다시 말해 ‘버그가 있을 확률 × 버그에 의한 피해’가 높은 순서대로 테스트를 작성해야 한다. 하지만 두 가지 변수 모두 모듈을 작성한 개발자의 직감과 경험에 크게 의존하는 부분이라 쉽게 익힐 수 없는 부분이기도 하다.

경험에서 나온 Reliability 관련 몇 가지 팁이 있다. 보통 로직이 복잡한 부분은 문제가 있을 확률이 많다. 그리고 멀티스레드 환경인 경우에는 동시성 검증도 철저히 해야 한다. 동시성으로 인한 버그는 발생 확률도 높고, 피해 역시 치명적인 경우가 많기 때문이다. 하지만 오픈소스를 사용한다거나 기존의 잘 동작하는 모듈을 사용하는 경우에는 테스트를 작성하지 않는다. 그런 부분은 이미 필드에서 검증됐기 때문이다. 그리고 기존의 모듈을 수정해서 새로운 기능을 넣어야 하는 경우에는 가능한 한 기존의 함수를 수정하지 않고, 새로운 함수를 만들어서 기존 코드를 수정 없이 조합하는 방식을 사용한다.



Top Down vs. Bottom Up



테스트 스크립트 수를 줄이면서 효과적으로 테스트할 수 있는 방법으로 Bottom Up Test를 소개하고자 한다. Bottom Up Test는 간단히 말하면 Bottom Up 방식으로 개발하면서 점차적으로 테스트하는 방식이다. 이 방식은 모듈의 재사용성에도 도움을 주고 요구사항의 변화에도 유연하게 대처할 수 있게 해주는 등 많은 장점을 가지고 있다.

Top Down은 전체적인 모습을 먼저 그린 뒤, 세부적인 부분을 완성시켜 나가는 방식이다. 그렇기 때문에 각 Layer의 요구사항은 상위 Layer로부터 나온다. 다시 말해, 상위 Layer에서 만들라고 하는 것만 만드는 방식이다. 이 방식에서는 최종 제품을 위해 필요한 것만 디자인하기 때문에 빠르다는 장점이 있지만, 상위 Layer의 변경이 하위 Layer의 변경을 유발한다는 단점이 있다. 구현할 때는 Main과 같이 전체적인 부분부터 작성을 시작한다. 그리고 모듈을 작성할 때는 해당 모듈을 사용하는 부분이 미리 만들어져 있는 경우가 많다.

반대로 Bottom Up은 먼저 여러 용도로 사용 가능한 부품들을 만들어 놓고 그것을 통해서 제품을 만들어 간다. 각 Layer는 나름의 독립된 의미를 가지게 된다. 그렇기 때문에 상위 Layer의 요구가 바뀌어도 아래 Layer는 변화에 비교적 안전하다. 하지만 Bottom Up은 각 Layer가 나름의 의미를 가져야 하기 때문에 디자인하는 데 많은 시간이 소요된다는 단점이 있다. 구현할 때는 Library부터 잘 작성해 놓고 시작하고, 모듈을 작성할 때는 그 모듈이 사용하는 모듈이 이미 존재해, 그것을 조합해서 작성해 나가게 된다.

Bottom Up Test를 할 때는 구현은 반드시 Bottom Up 방식으로 해야 하지만 디자인은 Top Down으로 해도 상관없다. 필자 역시도 디자인은 Top Down으로 하여 어떤 컴포넌트들을 어떻게 만들어야 할지 결정하고 난 다음 Bottom Up으로 구현을 진행한다. 이렇게 되면 명확하고 빠르게 디자인할 수 있는 장점을 얻는다.



Bottom Up으로 구현하면 의존성이 낮은 코드를 작성할 수 있다는 것이 가장 큰 장점이다. Top Down으로 구현할 경우 그 모듈을 호출하는 모듈, 즉 상위 Layer 모듈이 이미 존재하게 된다. 그래서 조금만 부주의하게 작성하게 되면 상위 Layer로 불필요한 의존성이 걸려버려 테스트하기 어렵고 나쁜 디자인의 코드가 만들어진다. 하지만 Bottom Up으로 구현하면 상위 Layer 모듈이 존재하지 않기 때문에 어지간히 잘못하지 않는 이상 상위 Layer로 불필요한 의존성이 걸리지 않는다. 그래서 테스트하기 쉽고 좋은 디자인의 코드가 만들어진다.



Bottom Up Test



Bottom Up으로 구현하면서 테스트를 진행하는 것이 무엇인지, 그리고 어떤 장점을 가지는지 예를 통해 알아보자. 디자인 결과 <그림 2>와 같은 구조의 모듈을 만들기로 결정됐다고 하자.



먼저, 가장 아래 C부터 작성을 시작한다. 이때 그 C 모듈 자체만으로도 하나의 독립적인 의미를 가지도록 작성한다. 이것은 누구든 사용할 수 있도록 작성한다는 것과 같은 의미이고 상위 모듈에 의존성이 없다는 것과도 같은 의미이다.

또 이것은 테스트하기 쉽다는 것과도 같은 의미다. 예를 들어 RPC 모듈을 작성한다고 할 때 가장 먼저 작성되는 모듈은 패킷을 시리얼라이즈, 디시리얼라이즈하는 부분이다. 만약 이 모듈을 독립적으로 작성했다면 RPC와 네트워크 외에 다른 어떤 곳에서도 쓰일 수 있을 것이다.

구현이 끝나면, 작업한 모듈에 Unit Test를 작성한다. Unit Test를 작성하면서 실제로 구현한 모듈을 처음으로 사용해 보게 된다. 이 과정을 통해 모듈의 기능에 대한 테스트뿐만 아니라 인터페이스까지도 테스트해 볼 수 있게 된다. 사용하기 불편하다면 수정해 가면서 테스트를 작성한다. 이런 과정에서 더 좋은 인터페이스를 가지게 되고, 그렇기 때문에 Unit Test는 다음에 작성할 모듈 B의 프로토타이핑 역할도 하게 된다. 이것이 Bottom Up Test가 가지는 또 다른 장점이다.



테스트까지 완료된 모듈, 즉 <그림 3>에서 사각형으로 둘러싸인 부분은 그 자체로도 독립된 의미를 가지고 있으면서 어떻게 사용되는지, 어떤 일을 하는지도 알려주기 때문에 좋은 재사용 단위가 된다. 만약 팀내 review 프로세스를 가지고 있다면 이는 좋은 review 단위가 된다.

그 다음 B 모듈을 작성할 때에는 이미 작성한 C 모듈을 사용해서 작성한다. 앞서 Unit Test를 통해 프로토타이핑을 해보았기 때문에 빠르게 작성할 수 있고, 더 좋은 품질의 코드가 나오게 된다. 역시나 구현이 완료되면 Unit Test를 붙여서 테스트한다. 그리고 B 모듈, C 모듈 그리고 Unit Test는 또 좋은 재사용 단위가 된다. 이런 방식으로 가장 상위 모듈까지 작성해나가면 전체 모듈을 완성시킬 수 있다.



같은 것을 Top Down 방식으로 구현해보자. B 모듈을 작성할 때는 A 모듈은 이미 완성되고 테스트된 이후다. 그리고 A가 필요로 하는 것을 B 모듈에서 작성한다. 이때 자칫 부주의하게 작성하면 B 모듈이 A 모듈에 불필요한 의존성을 가지게 된다. 다 작성하고 Unit Test를 작성할 때, 아직 C 모듈이 없기 때문에 그들의 행동을 흉내 내는 Mock Object를 작성해야 한다. 만약 B 모듈이 A 모듈에 의존성이 걸려버렸다면 A 모듈과 분리해서 테스트하기 위해 별도의 작업도 필요하다. 심지어 A 모듈에 대한 Mock Object가 필요할 수도 있다. 이런 것들이 테스트 Cost를 증가시키는 요인이 된다.





코드 커버리지



코드 커버리지는 테스트가 코드의 얼마나 많은 부분을 수행했는지를 나타내는 지표다. 보통 Statement 커버리지를 많이 사용하는데, 코드의 전체 라인대비 테스트가 수행한 라인을 백분율로 나타낸다. 만약 스스로 테스트에 대한 의지가 있는 개발자라면, 테스트에서 코드 커버리지는 신경 쓰지 않는 것이 좋다. 앞에서 이야기한 것처럼 테스트가 중요한 부분이 있고 그렇지 않은 부분이 있다. 하지만 코드 커버리지는 코드의 중요도는 전혀 고려하지 않는다. 테스트가 중요한 코드의 Statement 커버리지를 100% 달성한다고 해서 버그가 없다고 말할 수 없다. 심지어 동시성 테스트가 중요한 코드에서의 Statement 커버리지 100%는 전혀 의미가 없다. 그리고 커버리지 수치에 신경 쓰게 되면 중요한 부분을 집중적으로 테스트하기보다는 중요하지 않은 부분을 테스트하게 될 우려도 있다. 하지만 커버리지가 좋은 측면도 있다. 만약 테스트에 대한 의지가 없는 개발자를 둔 관리자라면 개발자에게 테스트에 대해 압박을 주는 용도로 사용할 수 있다.



Bottom Test의 커버리지



<그림 6>은 Bottom Up 방식으로 개발했을 때, 각 단계에서의 Unit Test가 커버하는 코드의 커버리지를 보여준다. 여기서 말하는 커버리지는 위에서 말한 Statement 커버리지가 아니라, 구현한 로직에 대해 얼마나 테스트됐는지를 의미한다. C Unit Test를 수행하게 되면 C 모듈의 중요한 부분을 테스트하게 된다.



그리고 B Unit Test를 수행하다 보면 C Unit Test를 할 때는 수행하지 않았던 부분까지도 덩달아서 테스트가 수행되는 효과를 누릴 수 있다. 운이 좋다면 C Unit Test에서 찾지 못했던 C 모듈의 버그도 찾을 수 있다. 그러나 이런 방식이 장점만 있는 것은 아니다. Unit Test를 하다 보면 필연적으로 많은 에러를 만나게 된다. A Unit Test를 하다 에러가 나오면 이것이 A 모듈에서 발생된 것인지 아니면 B 모듈 또는 C 모듈의 테스트되지 않았던 부분이 테스트되면서 발생한 버그인지를 식별해야 한다. 만약 B 모듈 대신에 Mock Object를 사용했다면 문제는 분명 A 모듈에 존재하는 것을 바로 알 수 있다. 이처럼 버그를 찾는 데 다소 시간이 소요될 수 있다는 것이 단점으로 작용할 수 있다. 하지만 경험적으로 대부분의 버그는 Unit Test가 목표로 하는 그 모듈에 있다. 왜냐하면, 하위 Layer 모듈의 중요한 부분들은 이미 테스트됐기 때문에, 설령 버그가 있다고 하더라도 찾기 쉬운 버그인 경우가 많다. 그리고 테스트의 목적은 버그가 있음을 알려주는 것이지, 버그 트래킹에 걸리는 시간을 줄여주는 것이 아니다.



Big Bang Test



지금까지는 개발하는 도중에 점차적으로 테스트를 진행하는 방식을 알아보았다. 그것과는 다르게 다 개발되고 난 후 마지막에 최상위 인터페이스를 통해서 한번에 테스트하는 것을 Big Bang Test라고 한다. 그러나 이 방식은 많은 단점을 가진다. 먼저 너무나 많은 문제가 한번에 나타나기 때문에 버그가 어떤 부분에 있는지 찾는 것이 어렵다. 그리고 구조적인 버그일 경우 이미 작성한 많은 부분을 수정해야 할 수도 있어 오히려 개발 시간을 더 늘리게 된다. 하지만 Big Bang Test가 단점만 가지고 있는 것은 아니다. 작성한 코드에 사소한 버그만 있다고 가정하면 굉장히 빠르다는 장점을 가진다. 이미 익숙한 모듈을 작성한다고 가정해보자. 이 경우에는 구조적인 버그가 생길 가능성이 거의 없고, 문제가 있더라도 어디에 문제가 있는지 찾기가 쉽다. 그렇기 때문에 이런 경우에는 오히려 Big Bang Test가 굉장히 효율적이다. 따라서 익숙하지 않은 부분을 작성한다면 Bottom Up Test를 적용하고, 이미 익숙한 부분을 작성한다면 Big Bang Test를 적용하라.





은 탄환은 없다



지금까지 테스트를 꾸준히 하기 위해서는 테스트 비용을 감소시켜야 함을 강조했다. 그리고 테스트 비용을 줄일 수 있는 방법으로 Bottom Up Test를 소개하고, 이로 인한 장점에 대해 설명했다. ‘은 탄환은 없다’라는 말은 여기에도 적용된다. 여기서 서술한 방법이 모든 사람들에게 딱 맞아 떨어질 수는 없기 때문이다. 그러므로 이 글을 참고해서 자신에게 가장 잘 맞는 방법을 찾아 꾸준히 테스트하는 개발자가 되길 바란다.









출처 :마이크로소프트웨어 1월호

제공 : DB포탈사이트 DBguide.net