Extremely Agile/TDD2007.12.26 01:48
소프트웨어와 버그

소프트웨어에서 버그를 100% 제거하는 것이 거의 불가능에 가깝다는 것은 잘 알려진 이야기입니다. 소프트웨어 시스템의 복잡도가 날이 갈수록 증가하면서, 버그의 양상도 굉장히 다양해 졌습니다. 하지만 대다수의 버그들은 다음의 세 가지 범주들 가운데 하나에 해당합니다[각주:1].

  • 논리적 버그 (logical bugs)
  • 구조적 버그 (structural bugs)
  • 성능 버그 (performance bugs)

논리적 버그는 논리 오류(logical error)와 같은 말입니다. 구조적 버그는 '어떤 소프트웨어가 성취해야 할 가치를 저해하는, 소프트웨어 구조상의 결함'을 일컫는 말입니다. 성능 버그는 '어떤 소프트웨어가 성취해야 할 가치를 저해하는, 성능상의 문제'를 일컫는 말입니다.

통상 구조적 버그와 성능 버그는 서로 관계가 있는 것이 보통이겠습니다만(가령 처리율이 중요한 시스템의 프론트 엔드에 메시지 큐를 붙이지 않았다던가 하는), 확장성이 중요한 시스템처럼, 구조적 버그가 성능상의 문제를 야기하지 않는 경우도 있습니다. 확장성이 높은 시스템을 만들었어야 하는데 확장성이 떨어지는 시스템을 만들어 버렸다고 칩시다. 그런 경우, 성능상에는 아무런 문제가 없지만 '확장성이 높은 시스템'이라는 애초의 목표에 미달하였으므로 문제입니다.

사실 이런 이야기들은 굉장히 해묵은 이야기들입니다. 제가 굳이 이야기를 더 하지 않아도, 대부분의 소프트웨어 개발자들은 이미 잘 알고 있는 이야기죠. 중요한 것은 '어떤 버그가 있느냐' 하는 것이 아니라 '버그를 어떻게 해결할 것이냐'하는 것입니다.

어떻게 해결할 것인가

논리적 버그의 경우에는 해결 방법이 비교적 잘 알려져 있는 편입니다. 이런 버그가 시스템에 들어가는 이유는 단순합니다. 알고리즘을 잘 못 만들었거나, 알고리즘을 코드로 잘 못 옮겼기 때문입니다. 버그를 '잡는다'는 행위는 아마도 '버그 탐지'와 '버그 수정'의 두 가지 층위로 나누어 생각해 볼 수 있을 것인데, '버그 수정'은 결국 '애초의 알고리즘 상의 오류를 교정'하는 작업이거나 '알고리즘을 엉뚱한 코드로 옮긴 실수를 교정'하는 작업이 될 터이니, 결국 논리적 버그를 잡는 데 있어서 가장 중요한 것은 '버그 탐지'인 셈입니다. 논리적 버그를 '탐지'하는 가장 널리 알려진 솔루션은, 바로 디버거(debugger)입니다.

한편, 구조적 버그와 성능 버그의 경우에는 문제가 좀 까다롭습니다. 이런 문제를 해결하기 위해서는, 구조적 버그와 성능 버그가 나타나는 양태를 돌이켜 볼 필요가 있습니다. 대략 다음과 같은 범주들로 나눌 수 있을 것 같습니다.

  • 구조적 버그가 발견됨
  • 구조적 버그 및 구조적 버그가 야기한 성능 버그가 발견됨
  • 성능 버그가 발견됨

구조적 문제와 하등의 관련이 없는 성능 버그가 관측된 경우에는(위의 세번째 경우), 흔히 소프트웨어 개발 대가들이 하는 말 대로, '프로파일링을 열심히 한 다음에 문제가 되는 코드를 손보면' 됩니다. 프로파일러(profiler)를 잘 쓰고, 문제가 되는 지점을 탐지한 다음, 코드를 뜯어고치면 된다는 것이죠. (문제는 '탐지'는 상대적으로 쉬운 반면, '뜯어고치는 것'은 상대적으로 어렵다는 것이죠. 물론 경험이 쌓이면 나아지긴 합니다.) 이런 종류의 문제는 소프트웨어를 아무리 잘 설계하고 구현하더라도 피하기가 힘듭니다. 그러므로 회피(avoidance) 전략이 잘 먹히질 않습니다. '발견되면 해결한다'의 접근법을 취하는 것이 좋습니다.

반면, 성능 버그를 야기하지 않는 구조적 버그가 발견된 경우에는(위의 첫번째 경우) '설계가 잘못된 것'이 그 원인이므로, 설계를 바로잡는 것이 최선입니다. (소프트웨어 개발 후반부에 이런 일이 생겨버리면 좀 난감하긴 합니다.) 이런 종류의 문제는 '발견되었을 때 해결'하기가 어려운 경우가 많으므로(구조를 변경하기에는 너무 진도를 많이 나가 버린 경우에 그렇습니다), 회피 전략을 취하는 것이 효과적일 수 있습니다. 애초에 '목적에 맞는' 디자인 패턴을 도입하거나, '스파이크 솔루션(spike solution)'을 최대한 많이 만들어 보는 것이 그런 전략들 중 한가지가 될 것입니다.

성능 버그와 관련된 구조적 버그가 발견된 경우에는(위의 두번째 경우) '성능 요구사항에 부합하지 않는 구조가 선택된 것'이 원인입니다. 이런 문제는 해결하기도 어렵고, 회피하기도 어렵습니다. 이런 종류의 문제를 회피하려면, 굉장히 많은 know-how가 필요합니다. 문제가 발생한 도메인(domain)에 흔히 사용되는 기술들에 대한 깊이있는 이해가 요구되는 것이죠. 그리고 이런 버그에 대한 해결책은, 그 규모가 앞선 두 가지 종류의 버그에 비해 굉장히 큰 경우가 많습니다. (무슨무슨 미들웨어를 써야 한다거나, 무슨무슨 데이터베이스를 써야 한다거나, 무슨무슨 부하분산 기법을 써야 한다거나 하는 종류의 솔루션들이 그 예가 되겠습니다.)

당신이 초/중급의 프로그래머라면

초/중급의 프로그래머는 '구조적 문제와 관련이 있는 성능 문제'같은 것을 겪을 기회가 사실 별로 없을 것입니다. 있다면, 주변에서 같이 일하는 프로그래머들이 전부 해당 도메인에 그다지 큰 지식이 없는 경우겠죠. 대부분의 경우에는 '그런 문제가 생길 소지가 있다'고 판단되는 순간, 경험이 있는 다른 프로그래머들이 '그런 문제는 이런 저런 식의 솔루션을 도입해서 해결한다'는 답을 내놓습니다. (물론 그 답이 항상 옳지는 않습니다.) 그러니, 이 글에서도 그런 종류의 '거창한' 버그는 논외로 하겠습니다.

자. 앞 단락에서 잠깐 언급을 했습니다만, 버그를 해결하는 방법에는 두 가지가 있습니다. 버그를 아예 만들지 않거나(회피 전략), 아니면 만들어진 버그를 굉장히 빠른 시간 안에 잘 수정하거나.

회피 전략을 극도로 밀어부치면 '아예 프로그램을 짜지 않는 것이 최선'이라는 이야기가 나올 법도 합니다만, 그러면 아예 프로그래머라는 직종이 사라지는 수가 생기므로 곤란하죠. 그렇다면 '코딩을 하되 버그는 회피하는' 좋은 수단으로는 어떤 것이 있을까요? 위에서도 언급했습니다만 '디자인 패턴'이나 '스파이크 솔루션' 같은 것이 유용한 수단이 될 수 있습니다. 사실, '디자인 패턴'과 '스파이크 솔루션' 사이에는 공통점이 있습니다. 둘 다 '선험적 지식'이라는 점에 있어서 공통적이라는 것이죠. 다지안 패턴은 GoF가 만든 이래로 많은 사람들이 갈고 닦아온 코딩 패턴이고, 스파이크 솔루션은 '실제 시스템을 만들 때 중요한 지식을 미리 시험하고 체험해 볼 수 있는 솔루션'이라는 점에서, 전부 '선험적 지식'에 기대고 있습니다.

그렇다면, '이미 검증된 라이브러리 상에서 작업하는 것'도 유용한 회피 전략 중 하나가 될 수 있겠군요. 네. 그렇습니다. 이미 검증된 라이브러리 자산 위에서 작업하면 생산성은 배 이상 올라갑니다. 사람들이 STL에 그렇게 열광하는 것도 그래서죠. 뭐가 잘 되고 뭐가 문제점인지 다 알고 있는 라이브러리 위에서 작업하는데, 당연히 생산성이 올라갈 수 밖에요.

회피 전략 이야기는 이쯤 하구요. 그렇다면 버그를 '굉장히 빠른 시간 안에 잘 수정'하는 좋은 방법은 뭐가 있을까요? 디버거가 '버그의 위치를 탐지하고 교정'하는 데 도움을 준다는 것은 분명하지만, 그렇다고 디버거가 모든 일을 다 잘 해주는 것은 아니에요. 메모리 문제라면 (적어도 Linux에서는) valgrind같은 툴을 쓰면 더 빨리 버그의 위치를 탐지해 날 수 있다는 것은 잘 알려져 있는 사실이죠. 고치는 문제라면, TDD같은 방법론에 의존하는 편이 더 낫습니다. (TDD에 대해서는 켄트 벡의 저서를 읽어보시기 바랍니다.)

자. 그러면 과연 초/중급의 프로그래머는 '디버깅을 잘' 하기 위해 무엇을 공부해야 하는 것일까요?

  • 디버깅 관련 유틸리티를 만들고 사용하는 방법
  • 디버거를 사용하는 방법
  • 버그 탐지 소프트웨어들을 사용하는 방법
  • TDD

'디버깅(debugging)'은 '있는 버그를 없애는'것이니까 회피 전략에 해당하는 부분들은 뺐습니다. (디자인 패턴이나 스파이크 솔루션 같은 것들요. 디자인 패턴에 대해서는 이미 시중에 좋은 책들이 많으니 참고하시면 될것 같고, 스파이크 솔루션에 대해서는 XP관련 사이트를 찾아보시는 게 더 나을 것 같습니다. 아니면 XPE나 XPI같은 책을 읽어보시거나요.) 그냥 순수하게, '있는 버그의 위치를 찾아내고 교정하는'것에 대한 부분만 살펴보도록 하겠습니다.

따라서, 저는 이 글의 2부, 3부, 4부에서 다음과 같은 내용들을 살펴보려고 합니다.

  • 직접 만드는 디버깅 유틸리티
  • 디버거(gdb)
  • 잘 알려진 버그 탐지 소프트웨어들과, 기타 등등의 탐지 기법

TDD(Test-Driven Development)에 대해서는 이미 좋은 책들이 있으니까 굳이 다루지는 않겠습니다만, 시간이 허락하는대로 그 실제 예제를 한 번 살펴보도록 하겠습니다. 사실 TDD는 버그의 '위치'를 탐지하는 데 도움을 주는 기법이라고 하긴 좀 뭐합니다만, 테스트를 하는 주기를 굉장히 짧게 줄여 나가다보면 '버그가 어디에서 발생했는지'를 거의 정확하게 파악할 수 있습니다. 테스트가 끝난 부분에서는 버그가 발생할 가능성이 낮으니, 새로 추가된 부분에 버그가 있을 것이다라는 가정을 하면, 버그의 위치를 빨리 찾아낼 수 있다는 것이죠. 거기다 TDD에 기반해서 개발을 하게 되면 결함 비용이 굉장히 낮아지게 될 뿐 아니라, 소프트웨어의 '구조적 결함'까지도 프로젝트 초기에 찾아낼 수 있다는 장점이 있습니다.

[2부로 이어짐]



  1. "Bug"의 정의에 대한 의견이 올라와서 각주를 답니다. Wikipedia에 따르면, Bug는 다음과 같이 정의됩니다. "error, flaw, mistake, "undocumented feature", <A title=Failure href="http://en.wikipedia.org/wiki/Failure">failure</A>, or <A title="Fault (technology)" href="http://en.wikipedia.org/wiki/Fault_%28technology%29">fault</A> in a <A title="Computer program" href="http://en.wikipedia.org/wiki/Computer_program">computer program</A> that prevents it from behaving as intended." 본 글에서는 이 정의를 다소 유연하게 해석하여, 버그의 정의가 아우르는 외연을 좀 확장하였습니다. 따라서 다른 분들이 보시기에 이 글에 등장하는 구조적 버그나 성능 버그는 요구사항requirement이나 만족 조건conditions of satisfaction같아 보일 수도 있습니다. 그런 면에서 보면 이 글에서의 버그는 <A href="http://en.wikipedia.org/wiki/ISO_9126" target=_blank>ISO 9126</A>에서 사용하는 버그의 정의와 같거나 유사하다고 볼 수도 있겠습니다. [본문으로]
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 일반적으로 버그가 오류(error)라는 뜻으로 통용된다는 상식을 받아들인다면, 구조적 버그나 성능버그는 버그로 분류될 종류의 것이 아닌것 같군요. 이 두 버그는 차라리 구조 및 성능에 대한 요구사항 미달에 가깝기 때문에 "소프트웨어 품질"에 포함시키는것이 더 나아보입니다. 당연히 품질이라는 개념에도 버그가 포함되겠지만, 버그는 프로그래머들에게나 통용되는 대단히 협소한 은어정도에 해당하는것이라 뜻이 제한적인 반면 품질이라는 단어는 훨씬 고 수준에서 고객들과 함께 사용할 수 있는 보다 일반화된 용어라는 차이가 있겠습니다. 때문에 두 종류의 버그를 구조적 요구사항 미달, 성능 요구사항 미달정도로 대치해서 생각해보는게 좋을것 같습니다.

    (구조적 요구사항이란건 개발자들에겐 중요해도 고객들에겐 크게 이슈화될 게 아니기 때문에 요구사항이란 단어를 붙이는 경우라면 "구조"를 "기능"이란 단어로 대치하는것이 좋겠습니다.)

    2008.04.26 22:50 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 낭만고양이

      좋은 의견이십니다. ^^ 다시 한번 정리해보도록 하죠.

      2008.04.27 12:51 신고 [ ADDR : EDIT/ DEL ]