마이크로소프트웨어 2006년 4월 기고글입니다.


소프트웨어 개발 관련 기술은 빠른 속도로 발전하고 있지만, 여전히 많은 개발자들은 코드 속의 버그와 전쟁을 벌이고 있다. 소프트웨어는 다른 산업과 달리 이례적으로 개발 비용보다 유지 보수비용이 훨씬 크다. 개발 과정에서 유지보수가 차지하는 비중이 1980년대에 50%에서 2000년 이후에는 90%라는 조사가 있을 정도이다. 유지보수는 소프트웨어의 요구사항이 지속적으로 변화한다는 문제에서 기인하지만, 설계, 구현 과정에서 발생한 버그 수정도 상당 부분을 차지한다. 수많은 소프트웨어 전문가는 이런 버그, 달리 말해서 소프트웨어 결함을 줄이기 위해 상당한 노력을 기울여 왔으며, 최선의 소프트웨어 프로세스와 코드 감사 및 리뷰에 의존하던 수동적인 버그 발견 및 수정 절차를 어느 정도 자동화하기에 이르렀다. 그 정점에 서있는 기술이 정적 프로그램 분석(static program analysis) 기술이다. 이 글에서는 자바 언어의 프로그램 분석 도구인 FindBugs 프로젝트를 중심으로, 버그 없는 소프트웨어 작성 프로세스에 대해 이야기해 보겠다.


소프트웨어의 버그는 정말 무섭다. 잘못된 한 줄의 코드가 수천 수백억의 재산을 한순간에 날려버릴 수 있다. 실제로 1998년 화성 주변을 돌던 인공위성이 파괴되었다. 이 사건은 미국식 단위(feet)를 사용하던 한 소프트웨어모듈과 미터법을 사용하는 다른 모듈이 결합되어 생긴 결과였다. 나사(NASA)의 과학자들은 인공위성이 파괴되기 전까지 이런 버그가 존재한다는 사실조차 모르고 있었다.

상업용 소프트웨어도 사정이 다르지 않다. 2005년 7월 안티 바이러스 소프트웨어 벤더인 트렌드 마이크로(Trend Micro) 사는 자사 제품의 버그로 인해, 8백만 달러(약 80억 원)를 배상해야만 했다. 이 버그는 백신 업데이트를 다운받은 사용자의 CPU를 점유하여 성능을 떨어뜨렸다. 콜센터는 폭주했고, 기업은 명성을 잃고 엄청난 비용을 치러야만 했다.

소프트웨어는 버그와 전쟁을 벌이고 있다고 해도 과언이 아니다. 따라서 소프트웨어 결함을 조기에 발견하고 효과적으로 줄이기 위한 연구가 소프트웨어 공학을 중심으로 계속되고 있다. 또한 이와 별개로 소프트웨어의 버그를 자동으로 찾아주는 프로그램에 대한 연구도 무척 활발히 진행되고 있다. 이 글에서는 후자에 속하는 프로그램 분석 기술을 자세히 살펴보자.


프로그램 분석


소프트웨어 개발은 대부분의 일을 사람이 해왔지만, 근래의 소프트웨어의 눈부신 발전은 반복적이고 귀찮은 개발 과정의 상당수를 자동화하기에 이르렀다. 이제 우리는 요구사항 분석, 설계, 구현, 테스트, 유지 보수에 이르는 소프트웨어 개발의 전 과정을 CASE(Computer Aided Software Engineering) 도구의 도움을 받아서 수행하고 있다. 구현과 테스트 측면에서만 본다면, 작게는 디버깅, 테스팅, 프로파일링을 비롯한 개별 기능을 수행하는 개발도구가 있고, 크게는 이런 도구들을 통합하는 통합개발도구(IDE)가 있다.

소프트웨어 개발 방법은 역사를 살펴보면, 개발 도구의 발전과 그 맥을 같이한다. 과거 컴파일러조차 없던 시절의 개발자들은 어셈블리 코드, 혹은 기계어를 직접 컴퓨터에 입력해야 했으며, 작은 실수조차도 원인을 알 수 없는 심각한 버그가 되는 경우가 많았다. C를 비롯한 다음 세대의 고급 언어의 출현은 컴파일러라는 자동화 도구가 있었기 때문에 가능했다. 컴파일러는 C에서 어셈블리 언어로의 번역 과정을 자동화 했을 뿐만 아니라, 개발자가 흔히 저지르는 실수와 오류를 즉시 교정해 주는 역할을 하였다. 예컨대, C언어에서 구문 끝에 세미콜론(;) 하나를 빠뜨려서 몇 시간씩 코드를 검토해야 하는 일은 없어진 것이다.

다시 말해서 우리가 알고 있는 단순히 언어 번역기로만 알고 있는 컴파일러는 사실 두 가지 기능을 수행한다. 첫째는, 컴파일러 고유의 기능인 한 언어를 또 다른 언어(보통 어셈블리 언어)로 번역하는 것이다. 둘째는, 번역하는 프로그램 언어 속에 존재하는 구문 오류를 찾아내서 즉시 개발자에게 알려준다. 즉 컴파일 과정은 런타임에 버그가 될 수 있었던 잠재적인 소프트웨어 결함을 조기에 발견하는 효과가 있다. 이때 컴파일러가 사용하는 기법에는 구문 검증 기술(parsing), 타입 시스템(type system) 등이 있다.

하지만 구문이 정확하고, 타입 오류가 없는 프로그램이 버그가 없는 프로그램은 아니다. 실제로 구문 검증과 타입 시스템은 수많은 소프트웨어 버그 중에서 극히 일부만을 찾아낼 뿐이다. 컴퓨터 과학자들은 좀 더 많은 버그를 찾아내고자 연구를 거듭했고, 그 결과로 나온 기술이 자동 증명(theorem proving)과 정적 프로그램 분석(static program analysis)(줄여서 정적 분석이라고도 함)이다.

프로그램 분석은 간단히 말해서 프로그램을 실제로 수행시켜 보기 전에 프로그램의 여러 성질을 파악해서, 버그가 있는지 없는지를 판별해 내는 기술이다. 우리가 버그를 찾아내기 위해 흔히 사용하는 기술인 테스팅은 런타임에만 버그를 찾을 수 있다. 하지만 테스팅이 모든 프로그램 수행 경로를 실행시켜 보는 것이 불가능하기 때문에 버그를 찾아내는데 한계가 있는 반면에, 프로그램 분석은 프로그램을 실제로 실행시키지 않으므로 탐지 가능한 모든 버그를 찾아낼 수 있다.


프로그램 분석(static program analysis)은 프로그램을 수행하기 전에 버그를 발견한다.


특히 타입 시스템이 풀지 못했던, 배열 경계를 벗어나는 오류, 널 포인터 오류, 레이스 컨디션 오류 등이 이런 프로그램 분석이 해결하고자 하는 주요 결함이다. 얼마 전에 마소에도 소개된 적이 있는 서울대 이광근 교수진에서 개발한 아이락(Airac)도 정적으로 C언어의 배열 경계 검사를 해주는 프로그램 분석 도구이다.

프로그램 분석 기술은 하나의 단어로 이야기하지만, 실제 분석 기술은 매우 다양하다. 단순히 프로그램의 특정 버그 패턴을 찾아내는 기술도 있고, 아이락처럼 프로그램을 요약해서 가상으로 수행시켜보는 요약 해석(abstract interpretation), 각종 데이터/제어 흐름 분석, 진보된 타입 시스템 등 다양한 기술이 있다.


Airac

Airac(Static Analyzer for Automatic Verification of Array Index Ranges in C Programs)은 서울대에서 개발한 C 프로그램 분석 도구이다. 특히 Airac는 C 프로그램에서 발생 가능한 배열 인덱스 오류를 찾아준다. 배열 인덱스 오류는 잠재적인 버퍼 오버플로우 등의 주요 원인이므로, 이 문제를 미리 해결할 수 있으면 프로그램에 상당한 안전성을 확보할 수 있다. Airac은 nML이라는 함수형 프로그래밍 언어로 작성되었는데, 불행히 프로그램과 소스 코드는 라이선스 계약을 통해서만 얻을 수 있다.



버그를 찾아라. FindBugs!


프로그램 분석 도구의 개념은 참 근사해 보인다. 개발자들이 엄청난 시간을 지겨운 디버깅과 버그 수정에 보낸다는 사실을 생각하면, 자동으로 버그를 찾아주는 도구는 자동으로 코딩을 해주는 도구만큼이나 매력적이다. 하지만 개발자는 반대로 그런 도구가 존재할까라는 의구심을 가지며, 현재 기술 수준으로는 생산적인 소프트웨어 개발에 도움이 될 만큼 훌륭한 분석 도구가 없다고 생각한다. 프로그램 분석 도구가 개발 과정에 가져다 줄 이점을 느껴보려면 실제로 분석 도구를 사용해보자.

프로그램 분석 도구는 널리 사용되고 있지 않아서 그렇지, 실제로 꽤 많은 프로젝트가 활발히 개발 중이다. 그중 가장 많은 사용자를 가진 자바 프로그램 분석 도구로 메릴랜드 대학에서 개발한 FindBugs(http://findbugs.sourceforge.net/)가 있다.

FindBugs 프로젝트는 다음과 같은 흥미로운 관찰 결과를 바탕으로 시작하였다.

 

◆ 훌륭한 개발자도 종종 바보 같은 실수를 한다.

◆ 여러 버그는 비슷한 성질을 가지고 있다.

◆ 버그 패턴: 빈번히 오류가 되는 코드 사용법(idiom)이 있다

◆ 이런 버그 패턴을 자동으로 찾을 수 있을까? Yes!


일단 훌륭한 개발자도 종종 바보 같은 실수를 한다는 점을 보자. 다음은 Eclipse 2.1.0에 실제로 있었던 코드이다.


if (entry == null) {

     IClasspathContainer container=

     JavaCore.getClasspathContainer(entry.getPath(),

     root.getJavaProject());

...

Eclipse 2.1.0 org.eclipse.jdt.internal.ui.javaeditor.ClassFileEditor 코드 일부


if (!searchQueryData.isBookFiltering()

&& (lastWS != null || lastWS.length() > 0)) {

Eclipse 2.1.0 org.eclipse.help.ui.internal.search.HelpSearchPage 코드 일부



이 코드의 버그는 매우 명백하다. entry가 null인지 확인한 후에 entry.getPath()를 부르므로, 이 코드에 들어가면 무조건 NullPointerException이 발생한다. lastWS의 경우도 마찬가지 문제가 있다. 이 코드를 작성한 사람은 절대 초보 개발자가 아니다. 많은 경험을 가진 훌륭한 개발자라도 가끔은 이런 실수를 하기 마련이다. 하지만 이런 실수를 했다는 사실보다 더 놀라운 것은, 이 코드가 Eclipse 2.1.0까지 발견되지 않고 그대로 코드 속에 남아있었다는 점이다.

하지만 FindBugs를 이용하면 이러한 NullPointer 버그는 프로그램을 수행해 보기도 전에 곧바로 잡아낸다. 데이트 흐름 분석(data flow analysis), 제어 흐름 분석(control flow analysis) 등의 기법을 사용하면 이런 문제를 정적으로 발견하는 것이 가능하기 때문이다.

이미 소프트웨어 공학은 소프트웨어 결함(버그) 발견의 시기와 비용간의 상관관계에 대한 많은 분석을 수행하였다. 여기서 얻은 결론은 버그를 빨리 발견할수록 수정하는데 비용이 적게 든다는 것이다. 마이크로소프트가 보안 결함이 있는 소프트웨어를 배포해놓고, 이를 패치하기 위에 들이는 노력을 생각한다면 같은 버그를 테스트 단계, 혹은 구현 단계에서 발견할 수 있다는 그 경제적 이익은 엄청나다.

다른 예제를 살펴보자. 자바는 언어 차원에서 멀티쓰레드 프로그래밍을 지원하며, 네트워크를 비롯한 여러 라이브러리에서 쓰레드 사용을 장려하고 있다. 하지만, 멀티쓰레드 프로그램의 버그는 프로그램 수행을 예측할 수 없게 만든다. 항상 잘되다가 가끔씩만 문제가 생기는 버그는 거의 대부분 이 멀티 쓰레드 버그의 속성 때문이다. 하지만 정작 버그 자체만 놓고 보면 정말 사소한 개발자의 실수로 밝혀지는 경우가 대부분이다.

 

while (!someCondition) {

     synchronized (lock) {

         lock.wait();

     }

}

위 예제는 개발자들이 자주 저지르는 멀티 쓰레드 프로그래밍 관련 오류이다. 원래는synchronized 블록에 들어가서 조건 검사를 해야 하는데, 반대로 조건부터 검사하고 락(lock)을 잡은 경우이다. while 조건 검사와 synchronized 블록 사이에 다른 쓰레드가 조건을 변경할 수 있으므로, 이 프로그램은 레이스 컨디션에 취약하다. 사소한 실수이지만, 이런 종류의 버그를 테스트로 찾아내는 것은 거의 불가능하다. 멀티쓰레드 버그는 타이밍에 민감하기 때문에 보통 소프트웨어가 릴리즈된 후에야 발견된다. 물론 그만큼 버그 파악과 해결에 걸리는 비용도 커진다.

FindBugs를 이용하면 일부 멀티 쓰레드 관련 버그를 매우 쉽게 발견할 수 있다. FindBugs는 자바 바이트 코드를 분석해서 버그를 찾는다. 위 버그의 경우 자바 바이트 코드에서 monitorenter와 Object.wait() 호출이 연달아 오는 패턴만 찾아내면 된다. 정적 분석 도구는 코드를 실제로 수행시켜 보는 것이 아니므로 멀티쓰레드 프로그램의 버그를 찾아내는데 가장 효율적인 방법이라 할 수 있다.

뿐만 아니라 FindBugs는 사용자들이 자바 언어/라이브러리를 잘못 이해하고 사용하는 경우도 탐지한다. 예를 들어 자바는 객체의 비교를 위해 equals라는 메쏘드를 이용하는데, 실수로 연산자(==)를 사용할 수도 있다. 라이브러리를 잘못 사용한 경우는 자바의 경우 해시 테이블(hash table)에 객체를 삽입하기 위해서는 hashCode와 equals를 모두 오버라이드해야 하는데 어느 한쪽만 오버라이드하는 경우도 있다. FindBugs는 이런 문제도 자동으로 찾아서 알려준다. 이런 패턴은 발견하는 데로 계속 추가가능하기 때문에 FindBugs의 탐지 기능은 지속적으로 발전하고 있다. 지면 관계상 여기에 모든 기능을 소개할 순 없지만, 보통 개발자들이 생각하는 것보다 훨씬 많은 버그를 잡아낸다.


PMD

PMD(http://pmd.sourceforge.net/)는 자바 프로그램에서 버그를 찾아주는 프로그램 분석 도구이다. FindBugs와는 달리 버그뿐만 아니라, 최적화되지 못한 코드, 데드 코드(dead code), 중복 코드 등도 찾아준다. 룰셋(ruleset)이라고 불리는 메커니즘으로 잠재적으로 버그나 문제가 될 수 있는 프로그램의 패턴을 지정하는 방식을 사용한다. PMD 역시 Eclipse를 비롯한 다양한 통합개발도구에 대한 플러그인을 제공하므로, 개발 과정에서 지속적인 피드백을 얻을 수 있다.


프로그램 분석 사용 사례


아직까지도 프로그램 분석이 일상 개발과는 거리가 먼 이야기로 들린다면, 실제로 많은 소프트웨어 개발 프로젝트가 이미 프로그램 분석 도구 사용을 개발 과정의 일환으로 생각하기 시작했다는 사실을 말해주고 싶다. DOM, JDOM의 문제점을 지적하며 새로운 오브젝트 모델을 지향하고 나선 XOM도 FindBugs를 개발에 차용하고 있다. XOM는 프로젝트 홈페이지에 디자인 원리를 잘 정리해놓았는데, 개발 스타일에 JUnit을 이용한 단위 테스트와 함께 정적 검사(FindBugs와 PMD)를 명시하고 있다. XOM은 초기부터 FindBugs, 이와 유사한 도구인 PMD를 이용해 버그를 줄이는 방법을 쓰고 있다.

FindBugs 외에도 나사와 하니웰 테크놀로지(Honeywell Technology Center)에서 개발한 반데라(Bandera)라는 정적 분석 도구도 있다. 이 도구는 멀티 쓰레드 프로그램의 오류를 찾아주는데, 항공 운항을 위한 실시간 운영체제인 DEOS(Digital Engine Operating System)의 오류를 찾아낼 수 있었다.

프로그램 분석 도구는 어떻게 활용하는 것이 가장 좋을까? 방법은 실로 다양하다. 어느 정도 기간을 두고 한두 번씩 프로그램 분석 도구를 사용하여 버그를 잡아내는 방법도 있고, 개개인의 개발자가 Eclipse 같은 통합 개발 환경에 FindBugs를 설치하여 컴파일할 때마다 어떤 버그를 만든 건 아닌지 확인하는 방법도 있다. 혹은 소스 코드 관리 도구(CVS 혹은 SubVersion 같은 도구)에 저장된 소스 코드를 자동으로 꺼내와 프로그램 분석 도구를 수행한 후에 결과를 관련 개발자들에게 이메일로 보내는 방법도 있다. 단위 테스트나, 매일 빌드(daily build)가 비슷한 과정을 거쳐서 도입되었다는 점에 유의할 필요가 있다.

결론은 프로그램 분석 도구는 비용대비 무척 효율적인 버그 발견 방법이라는 점이다. 버그를 조기에 자동으로 발견할 수 있다는 장점을 애써 무시할 필요는 없는 법이다. 실제로 일부 개발자들은 오픈 소스를 꺼내와 FindBugs를 돌린 결과에서 버그를 발견해 오픈 소스에 기여하고 있는 경우도 있다.


앞으로


지금까지 프로그램 분석 도구의 장점과 이를 도입했을 때 얻을 수 있는 이점에 대해 간략히 소개하였다. 하지만 소프트웨어 개발에 만병통치약이란 없다. 프로그램 분석 도구의 고질적인 문제로는 아직까지 완벽하게 버그만을 발견해내지 못한다는 점이다. 가끔은 버그가 아닌 코드를 버그라고 하는 허위 경보(false positive)를 내기도 하고, 아예 버그를 발견하지 못하는 탐지 실패(false negative) 문제도 있다.

하지만 소프트웨어 결함 제거는 일종의 전선이다. 제 1 전선이 무너지면, 코드 리뷰, 검토, 단위 테스트, 기능 테스트, QA라는 2, 3, 4 전선이 계속 남아있다. 프로그램 분석 도구는 코딩 단계에서 버그를 최대한 빨리 발견하기 위한 제 1 전선의 역할을 훌륭하게 해낸다. 프로그램 분석 도구가 한 명의 적군(버그)라도 쓰러뜨린다면 우리 개발자가 개발 전쟁에서 승리할 확률이 더 높아짐을 명심하자.