상세 컨텐츠

본문 제목

The Software Test Engineer's Handbook

테스팅 번역 자료들

by techbard 2019. 4. 5. 13:56

본문

반응형

pp. 109

 

8. 분석 기법

 

기술 테스트 분석가는 분석을 통해 테스트 중인 소프트웨어에 관해 많은 것을 알 수 있다. 우리가 분석을 시행할 때 이전 장에서 기술한 테스트 기법들을 사용해 찾기 어려울 수 있는 특정 유형의 결함을 찾을 수 있거나 우리의 테스팅 전략을 다듬는데 도움이 될 정보를 얻을 수 있다. 만일 우리가 분석을 고려하고 있지 않다면 이해관계자들을 불필요한 리스크에 노출시키게 된다. 대부분의 분석은 도구의 도움을 통해 수행되기 때문에 우리가 사용할 수 있는 분석 기법을 인지하고 있다면 상대적으로 낮은 비용으로 그런 리스크를 줄일 수 있다.

 

우리는 이 장에서 분석 기법의 두 가지 중요한 범주를 생각할 것이다.

 

  • 코드를 수행하지 않는 정적 분석

  • 코드를 수행하는 동적 분석

 

8.1 정적 분석

 

그 이름이 암시하듯이 정적 분석은 프로그램의 실행을 포함하지 않는다. 우리는 분석 가능하고 구조화된 형태로 이용할 수 있는 소프트웨어 관련 아이템들에 대해서 정적 분석을 수행할 수 있다. 대체로 이것은 코드를 의미한다. 하지만 복잡한 소프트웨어 설치에서 정의되는 것 또는 UML 같은 표준적인 모델링 언어를 통해 만들어지는 것 같은 절차에 대해서도 정적 분석을 수행할 수 있다.

 

이런 분석 형태의 정적인 속성은 장점과 단점을 모두 가지고 있다. 우리가 사용할 수 있는 여러 유형의 정적 기법들을 살펴보기 전에 이어지는 절에서 그 장점과 단점의 개요를 살펴보자.

 

8.1.1 장점

 

소프트웨어 개발 생명 주기에서 초반에 어떤 형태로든 테스팅을 수행할 수 있는 능력은 주요한 장점이다. 가장 잘 알려져 있고 가치있는 테스팅 원리 중 하나는 대체로 생명 주기 상에서 초반에 발견된 결함이 나중에 발견된 결함보다 고치는데 비용이 적게 든다는 것이다. 정적 분석은 실행 가능한 프로그램이 존재하지 전에 수행될 수 있으며 따라서 잠재적이거나 실제적인 결함을 초기에 발견할 수 있다.

 

정적 분석에 코딩 표준이 적용되었는지에 대한 확인이 포함되면우리 코드는 보다 유지보수성이 높고 보다 이식성이 높은 것을 통해 이익을 취할 수 있다 (우리는 15장과 16장에서 이 품질 특성을 논의할 것이다). 추가로 필요한 경우에 코딩 표준과의 합치성을 증명할 수도 있다.

 

본질적으로 정적 분석은 다수의 가용한 도구를 통해 오프라인으로 수행되는 비용 대비 효과가 좋은 활동이다. 정적 분석이 제공한 것을 당신의 개발과 생명 주기 동안의 테스팅에 포함시키고 주기적으로 수행하면 도구 지원과 라이선스 획득에 드는 비용보다 이익이 클 것이다.

 

정적 분석은 또 다른 다수의 테스팅 활동에 가치있는 지원을 제공한다.

 

  • 리뷰에서 동일 아이템에 (코드, 디자인 등) 집중할 때 정적 분석의 도움을 받을 수 있다. 정적 분석에서 얻은 결과는 리뷰에서 어디에 집중해야 하는지 아는데 사용되며 다른 형태의 결함을 찾는데 귀중한 리뷰 시간을 쓰지 않게 해준다. 코드 리뷰의 일정을 세울 때 가능한 한 많은 결함을 먼저 “제거”하고 리뷰 그 자체에 돌입하기 전에 다른 잠재적인 결함들을 강조하기 위해 정적 분석을 수행하는 것이 효과적인 접근법일 수 있다.

  • 정적 분석은 주로 정적 분석이 제공하는 메트릭을 통해 리스크 분석을 지원할 수 있다. 이러한 메트릭은 리스크에 대한 지시자로 (예를 들면 복잡도) 쓰이며 리스크 기밥 테스팅 접근법에 귀중한 입력이 될 수 있다.

 

교훈: 정적 분석 도구가 결함을 예방한다


내가 테스트 관리자였던 특수한 프로젝트에서 (아아 프로젝트가 어려움에 빠졌을 때만) 정적 분석 도구가 도입되었다. 시간이 조금 지나 내가 그 역할을 맡았다. 프로젝트의 C++ 프로그래밍 가이드라인이 적용되지 않았고 개발팀이 그 결과 고통을 받고 있음이 명확해 졌다 (코드 재사용 별로 없음 그리고 잘못된 분석성이 그 고통의 주 원인이었다). 정적 분석 도구가 그 문제들을 해결하기 위해 도움을 주는데 쓰였다고 발표한 이후 (하지만 실제 설치는 하지 않았다) 나는 흥미로운 효과를 관찰했다. 개발자에게서 전달되는 코드가 이전보다 가이드라인을 더 잘 준수하기 시작했다. 대체로 개발자들은 자신의 작업을 자랑스러워하고 도구가 나타내는 아이디어들을 좋아하지 않는다 (특히 새로운 테스트 관리자 앞에서는 더욱 그렇다). 그 도구가 제자리를 잡아간 이후 고품질의 가이드라인은 준수하는 코드를 작성하는데 있어 개발자들을 지원하는 좋은 효과를 보이는데 사용되었다. 촛점은 결함 예방에서 동기 부여로 이동해 갔으며 그 도구는 성공적으로 적용되었다 (비록 몇몇 불분명한 메트릭을 어떻게 실용적으로 사용할지 우리에게 의문점을 남기긴 했지만).

 

8.1.2 제약

 

아마 정적 분석의 가장 큰 제약은 실제 결함을 발견하는 능력일 것이다. 매우 빈번하게 정적 분석은 실제 결함이 있는지 결정하기 전에 테스터나 개발자가 추가 조사를 해야 하는 코드 영역을 “의심스러운 영역”으로 경고한다. 그러한 경고의 가치에도 불구하고 가치있는 개발자나 테스터의 시간이 낭비되지 않도록 어떤 경고를 조사할지 결정하는 활동은 신중해야 한다. 이런 맥락에서 원하는 경우 낮은 수준의 경고를 필터링할 수 있도록 해주는 도구가 도움이 된다.

 

정적 분석은 상대적으로 사소한 코드나 디자인 영역을 제외하고는 모든 경우 복잡해 질 수 있다. 그러므로 도구 지원이 필수적이다. 하지만 당신은 그 도구의 결과가 무엇을 말해주는지 이해할 필요가 있으며 이것 때문에 몇몇 전문가가 필요하다. 이 장의 나머지 부분은 몇 가지 주요 정적 분석 기법의 개요를 제공하고 그것이 발견한 결함 유형을 소개해서 이 작업에서 당신을 도울 것이다.

 

8.1.3 제어 흐름 분석

 

제어 흐름 분석은 구조를 검사한다. 대체로 코드 구조를 구성하는 아이템들은 결정 포인트나 (if-then-else) 루프를 포함한다. 이러한 논리적인 구조가 어떻게 쓰였는지에 따라 그 코드는 보다 복잡할 수도 심지어 결함을 포함할 수도 있다. 제어 흐름이란 용어는 실행될 때 “제어를 담당하는” 구문이 존재한다는 사실에서 온 것이다. 결정을 통해 만들어진 경로에 따라 제어 흐름이 변한다.

 

우리는 제어 흐름 분석을 통해 이어지는 유형의 결함들을 발견하려고 시도한다.

 

  • 몇몇 잘못된 로직 때문에 실행되지 않는 코드 (가끔 죽은 코드로 불림)

  • 루프에 진입하지만 빠져나가지 못하는 코드 (무한 루프)

 

또한 제어 흐름 분석은 과도하게 복잡한 영역을 강조해 보다 많은 결함이 들어 있을 수 있는 영역에 관심을 가지는데 도움을 준다. 제어 흐름 분석에 대한 자세한 내용은 [Beizer 95]를 참고하라.

 

이어지는 (버그가 있는) 의사코드 영역을 생각해 보자. 이것은 변수 “sum”의 값을 계산해야 한다. 특별히 잘 작성되지 않았다는 (소수의 주석문, 나쁜 프로그래밍 스타일 등) 것과 별개로 여기에는 두 가지 제어 흐름 결함이 들어 있다. 당신은 그곳을 지적할 수 있는가?

 

그림 8-1. 예제 코드

integer sum, count, var1, subtotal1, subtotal2

integer bonus = 10000


// subtotal1과 2의 절대값을 취한다

// 이것은 절대 음수가 될 수 없다


subtotal1 = abs(calculate_subtotal(1))

subtotal2 = abs(calculate_subtotal(2))


sum = subtotal1 + subtotal2

count = subtotal1 / subtotal2


if (sum < count) then

  count = 0

endif


do while count >= 0

  sum = sum + bonus

end do


count = count -1


write (sum)

 

우리는 이 코드의 논리적인 흐름에 관심이 있다는 걸 기억하라. 그림 8-1에서 제어 흐름은 단일 결정 지점과 단일 루프로 결정된다. 첫 번째 결정 지점에서 우리는 if-endif 블럭이 결코 실행되지 않는 코드처럼 그 결정 지점이 “참”이 되지 않을 가능성이 존재하는지 자문해봐야 한다. 보다 자세히 확인하면 변수 “sum”은 변수 “count” 보다 적을 수가 없다. count = 0 구문은 결코 실행되지 않는다. 우리는 제어 흐름 결함인 죽은 코드를 가지고 있다.

 

루프 안에서 걸리나?

 

이제 루프를 생각해 보자. 루프에 한 번 진입하고 빠져나가는게 가능한가? 루프 변수 “count”는 0 또는 그 이상의 값을 가질 수 있다. 따라서 그 루프에 진입하는 건 가능하다. 하지만 한 번 진입한 후 어떻게 그 루프를 빠져나올 것인가? 루프 변수 “count”는 루프 안에서 절대 변하지 않는다. 따라서 무한 루프 제어 흐름 결함이 존재한다. 지속적인 동작에서 변수 “sum”의 값은 결국 정수에 저장될 수 있는 값을 초과하여 특정 유형의 예외를 발생시키고 아마 루프가 종료될 것이다. 어떻게 되든간에 우리는 수정이 필요한 명백한 제어 흐름 결함을 가지고 있다.

 

동적 테스팅에서 찾을 수 없는 것

 

이와 같은 결함을 동적 테스팅 기법을 사용해 코드를 실행해서 쉽게 발견할 수 있을까? 우리 예제에서는 우리 테스트가 완료되지 않기 때문에 무한 루프의 효과를 확실히 인지할 수 있다.  우리는 에러메시지나 시스템이 크래쉬 되는 것을 인지할 수 있다. 무한 루프 그 자체는 이어지는 결함 분석의 일부분으로 찾아낼 수 있고 아마 그 결함을 국지화하는데 제어 흐름 분석이 사용될 것이다.

 

죽은 코드를 포함한 결함은 실행되지 않는 코드가 우리의 기대 테스트 결과인지에 따라 동적으로 발견하기 쉽지 않을 수 있다. 동적 테스팅 동안에 커버리지 측정 도구를 사용하면 실행되지 않은 코드 영역을 보여주며 도달하지 않는 코드 영역을 식별하는데 도움이 된다. 다시 한 번 이런 영역의 조사에는 제어 흐름 분석이 포함된다.

 

당신은 지금껏 죽은 코드가 가진 문제가 무엇인지 의아하게 생각할 것이다. 결국 죽은 코드는 그냥 죽은 것이다. 그것은 아무것도 하지 않는다. 코드가 실행될 때 로드되면서 관련된 오버헤드가 약간 발생한다. 하지만 당신의 메모리가 심각하게 제약받고 있지 않다면 아마 그것은 문제가 안 될 것이다. 죽은 코드와 관련한 리스크는 유비보수의 리스크이다. 다음 번에 프로그래머가 이 코드를 보게되면 그는 이전 적용했던 변화에서 그 코드가 무엇을 했는지 알아내야 할 것이다. 불충분한 분석을 통해 그가 문제를 수정하는 과정으로 그 코드에 접근하려는 위험성이 항상 존재한다. 따라서 죽은 코드 그 자체는 그것이 죽었기 때문에 리스크는 아니지만 이후 생명을 얻거나 그냥 자리만 차지할 리스크가 있다.

 

8.1.4 데이터 흐름 분석

 

데이터 흐름 분석은 코드 내의 데이터 변수들에 집중한다. 다음과 같은 두 가지 답변이 필요한 질문이 존재한다.

  • 어디에서 변수들이 정의되나 (즉 값을 획득하나)?

  • 그것들이 어디에서 사용되나?

제어 흐름 분석과 비교했을 때 데이터 흐름 분석을 수행하는 것은 실제 결함을 확인하기 보다는 결함이 존재한다는 걸 제시하는 비정상을 찾아낸다. 데이터 흐름 분석이 찾아낼 수 있는 비정상의 유형은 다음과 같다.

  • 프로그램이 사용하려고 시도하는 정의되지 않은 결함 (즉 값이 없는 것들)

  • 정의된 변수지만 사용 전에 정의되지 않게 변하거나 무효하게 됨

  • 변수의 최초값을 사용하기 전에 재정의된 변수

변수가 정의되었지만 쓰이지 않는 비정상은 (즉 이전 목록에서 마지막 두 개) 가능하면 빨리 발견해야 한다. 왜냐하면 그것들이 그 코드가 몇 가지 방식에서 잘못되었다는 명확한 신호이기 때문이다. 여기에 사용된 기법은 그 변수의 정의와 사용 사이의 경로를 검사하는 것이다 (대개 du-pair 또는 set-use pair와 관련이 있다). 이어지는 의사코드 예제는 (다시 한 번 최고의 프로그램 스타일도 아니고 데모를 보일 목적으로도 효율적이지는 않지만) 몇 가지 지역 변수를 정의하고 달성한 주문량, 개인 목표 그리고 회사의 이익에 근거해 영업직의 개인 보너스 지불 금액을 계산하는데 그것을 사용하는 모듈을 나타낸다.

 

“CompanyBonus”와 “LocalBonus”의 (밑줄친) 변수에 대해 set-use pair를 고려해 본다. 둘다 기본값으로 초기에 0이 설정된다. 만일 영업직이 주문량을 달성하면 “CompanyBonus”의 기본값이 회사의 이익을 반영한 값으로 재설정된다. 이후 그 변수는 “PersonalBonus”를 계산하는데 쓰인다. set-use pair는 완전하며 어떤 데이터 흐름 상의 비정상도 존재하지 않는다.

 

그림 8-2. 예제 코드

Integer PersonalBonus (integer: Year, Personal-ID

{

  Integer LocalBonus = 0

  Integer BonusPoints = 0

  Integer CompanyBonus = 0

  Integer OrderEntry = 0

  Integer PersonalTarget = 0


  // 데이터베이스에서 개인 데이터 가져오기

  OrderEntry = GetOE(Personal-ID, Year)

  PersonalTarget - GetPT(Personal-ID, Year)

  LocalBonus = GetLB(Personal-ID, Year)


  if (OrderEntry > 0) then

     BonusPoints - OrderEntry / PersonalTarget

     CompanyBonus = CompanyProfit(Year)/100

  endif


  if (BonusPoints > 0) then

     PersonalBonus = BonusPoints * CompanyBonus

  endif

}

이제 변수 “LocalBonus”를 생각해 보자. 다시 한 번 기본값이 재설정되지만 “사용”은 발생하지 않는다. 최선의 경우 이 비정상은 코드의 유지보수성을 떨어뜨릴 수 있다. 하지만 최악의 경우 이것은 실제 결함의 지시자가 될 수 있다 (프로그래머가 그 변수를 가지고 무언가를 하려는 의도가 있다면?).

 

이 예제에서 set-use pair를 검사할 때 우리는 제어 흐름도 고려해야 한다는 점이 명백해진다. 심지어 우리는 실제 데이터 흐름 분석을 추구하고 있지만 제어 흐름 비정상을 발견할 수도 있다.

 

8.1.5 코딩 표준 준수

 

코딩 표준을 적용하는 것은 이어지는 장점들을 만들어 내는 효과적인 방법이 된다.

  • 개발자들 사이에 쉽게 코드를 공유하는 능력

  • 그 코드를 쉽게 테스트 할 수 있는 능력을 포함한 코드 유지보수성 강화

  • 보다 이식성이 높은 코드

  • 보다 보안성이 높은 코드

  • 건설적인 품질 보증을 수행하는 능력 즉 결함 발견보다 결함 예방에 강조를 둠

  • 대체로 코딩 에러의 리스크가 적어짐 (특히 코딩 표준이 개발자의 통합 개발 환경의 일부가 되는 경우)

  • 증감식으로 보다 엄격한 규칙 세트를 적용함으로써 시간이 지나면서 개선되는 능력

많은 조직들은 비록 업계 표준이 사용 가능하지만 자신만의 코딩 표준을 개발한다. 우리가 코딩 표준이 우리 조직이나 프로젝트에 도입된 코딩 표준이 코드에 올바르게 적용되었는지 분석하면 그런 표준을 위반했는지 그리고 어디에 필요한지 찾아서 표준 준수를 증명할 수 있다.

 

여기에 우리가 코딩 표준을 도입할 때 피할 수 있는 나쁜 코딩 관례의 예가 있다. 이후 우리 코드에서 정적 분석을 수행한다.

  • 주석의 부재. 특히 루프나 결정 지점 같은 몇몇 코딩 요소 이전에

  • 과도하게 복잡한 코딩 구조 (들여쓰기 단계)

  • 잘못된 프로그래밍 스타일. 즉 결함의 원천이 된다 (예를 들어 묵시적인 형변환).

  • 프로그래밍 언어에 특정한 이슈. C++에서 동적으로 예약된 메인 메모리를 (“new”를 통해) 해제하지 않음 (“delete”)

  • 보안 취약성을 나타내는 코딩 관례의 사용 (예를 들면 제한없는 데이터 접근)

표준은 디자인과 아키텍쳐에 적용될 수 있다. 이런 것들은 코드가 적용을 받는 것과 유사하게 정적 테스팅의 적용을 받는다. 하지만 서로 다른 이슈들에서 핵심은 다음과 같다.

  • 신규 개발된 라이브러리 대신에 표준 소프트웨어 라이브러리를 사용한다

  • 외부 시스템과의 상호작용을 위한 가이드라인 (예를 들어 직접 시스템 레벨을 통하는 대신에 구체적인 연결성을 제공하는 소프트웨어 [미들웨어]를 통한다)

코딩 표준을 강제하는 도구의 사용

 

정적 분석을 수행하고 코드에 구체화된 메트릭을 (아래를 보라) 생산하는 도구는 대개 프로그래밍 언어에 종속적이며 (예를 들면 C, C++, 자바) 분석을 행하는 특정 측면을 목표로 한다 (예를 들면 보안성, 웹 사이트).

 

대개의 도구는 특정 코딩 표준을 나타내는 규칙의 집합을 사전 정의하고 있으며 당신이 선택한 코드에 대해 이것을 적용한다. 이것은 당신이 최소한의 설정 공수로 코드의 정적 분석을 수행할 수 있다는 걸 의미한다.

 

만일 당신이 도구에 포함된 규칙 세트를 개정하거나 확장할 필요가 있으면 그 도구가 큰 공수없이 이것을 할 수 있게 해주는지 신중히 확인해야 한다. 필요한 변경을 위한 사용자 인터페이스를 갖춘 대부분의 선도적인 도구는 효율적으로 작동한다.

 

몇몇 도구 또한 무시할 수 있는 에러를 “가르쳐” 줄 수 있는 인터페이스를 통해 오탐지에 (false-fail로 부르는) 관해 학습할 수 있는 능력을 가지고 있다.

 

8.1.6 코드 메트릭 생성

 

우리가 정적 분석을 수행하면 여러 가지 방법으로 테스팅에 기여할 수 있는 정보를 수집할 수 있게 된다. 그것들 중 일부를 살펴보자.

 

구조적인 복잡도 측정

 

구조적인 복잡도에 관한 정보는 결함을 가지고 있을 리스크가 있는 코드 영역을 식별하는데 도움을 준다. 만일 우리가 리스크 기반의 접근법을 따르고 있다면 이 영역은 보다 정밀한 테스팅을 위한 목표가 될 것이다. 하지만 이 부분에서 신중을 기해야 한다. 왜냐하면 비즈니스 가치와 같은 다른 요소들은 복잡도 관련 이슈보다 이해관계자들에게 보다 중요하게 고려되어야 하기 때문이다. 구조적인 복잡도를 측정하는데 가장 널리 쓰이는 메트릭 중 하나는 맥케이브 순환 복잡도이며 이것은 코드 조각을 관통하는 독립적인 경로들의 숫자를 기반으로 한다 (경로 테스팅의 상세한 내용은 5.4.7절을 참고한다).

 

주석 빈도 측정

 

주석 빈도의 측정과 통계적으로 도출된 메트릭은 코드의 유지보수성을 판단하는 유용한 지표가 될 수 있다 (15장 “유지보수성 테스팅”을 참고).

 

코드 크기 측정

 

일반적으로 코드 크기의 측정은 유용성이 최소한이지만 코드에서 가장 자주 쓰이는 메트릭이다. 이것은 객체 기행 코딩과 같이 코드 재사용성이 높은 영역에서는 특히 의미가 없다.

 

메트릭을 사용한 구조적인 접근법

 

정적 분석에서 광범위한 메트릭이 생성될 수 있다. 대개 어떤 것은 적용 가능하고 나머지는 시스템 유형이나 특정 프로그래밍 패러다임에 종속적이다.

 

그것들은 생성하는 것이 저렴하기 때문에 경험이 부족한 테스트 분석가는 다수의 메트릭을 생성하는 도구를 가지려는 유혹이 있을 수 있다 (즉 우리가 얻은 것을 그냥 확인한다). 이후 실질적인 비용은 생성된 어지러운 다차원 메트릭을 이해하고 적용하는 것에서 발생한다.

 

그러므로 메트릭을 사용할 때 구체적인 목표게 기반해 구조적인 접근법을 쓰고 (예를 들면 유지보수 비용의 감소) 그러한 목표를 지원하는 메트릭을 생성하는 것이 (예를 들면 주석 빈도) 바람직하다. 목표 질문 메트릭은 (Goal-Question-Metric_GQM) 이런 개념을 지원하는 접근법의 훌륭한 예이다. 그 이름이 제안하듯이 첫째 단계는 질문을 하는 것이다. 우리가 달성하고자 하는 목표는 무엇인가? 이후 질문에 답을 하고 어떻게 질문된 것을 측정할 것인지 결정한다. 그리고 이러한 메트릭에 대한 측정을 지원하는 메트릭을 선정한다. GQM 접근법은 1900년대에 바실리, 칼데이라 그리고 롬바치가 처음 개발했다. GQM 접근법의 예는 [Burnstein 03]에 제시되어 있다.

 

8.1.7 웹사이트의 정적 분석

 

웹사이트의 정적 분석은 하이퍼링크 도구 같은 고유한 도구로 다루는 특별한 영역이다. 대체로 웹사이트는 많은 변화를 (아마 심지어 매일매일) 이러한 도구들이 테스팅 목적으로 쓰일 뿐만 아니라 그 사이트를 유지보수하는 사람도 사용한다 (웹마스터와 연관성이 있다). 또한 그 사이트의 특정 속성을 최적화하기 원하는 개발자들도 사용한다.

 

이 도구가 발견할 수 있는 유형의 결함은 무엇일까?

  • 사용자를 의도된 웹사이트로 안내하지 못하는 웹페이지에 쓰인 하이퍼링크. 이것은 잘못 프로그래밍된 하이퍼링크나 바람직하지 않은 리다이렉션 때문이다.

  • 웹사이트로 전혀 연결하지 못하는 하이퍼링크 (즉 HTTP 에러 404)

  • 잘못되어 있거나 존재하지 않는 웹페이지의 구체적인 내용

  • 웹페이지를 표시하거나 (시각적인 컨텐츠 때문에) 다운로드를 수행하는데 과도하게 시간이 걸림

코드의 정적 분석처럼 웹사이트의 분석 또한 웹마스터, 개발자와 테스터들에게 유용할 수 있는 정보를 당신이 수집하게 해준다. 여기 제공할 수 있는 정보의 유형의 예가 있다.

  • 웹페이지의 크기 (홈 페이지느 자주 사용되는 페이지처럼 표시 시간이 중요한 경우 유용하다).

  • 웹사이트의 전체적인 구조 (대체로 사이트 맵과 관련이 있다). 이것은 사용자가 웹사이트를 “탐색”하기 쉬운 정도와 그 구조의 입도에 (granularity) 관한 전체적인 “균형” 같은 사용성 이슈를 분석하는데 쓸 수 있다. 만일 사용자가 원하는 정보를 얻기 위해 너무나 많은 “클릭”을 필요로 하거나 그 사이트의 구조가 어떤 지점에서 너무나 상세하며 다른 영역은 너무 높은나 고수준이며 그 사용자 경험은 나빠질 것이며 사용자는 그 사이트를 떠나게 된다.

대체로 그러한 도구들은 (가끔은 웹 스파이더로 부름) 앞에서 언급한 정보를 제공하고 결함을 발견하기 위해 웹사이트와 인터넷과 상호작용할 필요가 있음을 주의한다. 이런 맥락에서 우리는 이 장의 어딘가에서 논의한 코드에 대한 “순수한” 정적 테스팅과 (실행하지 않는) 비교했을 때 다소간 “정적” 테스팅의 정의를 확장할 수 있다.

 

8.1.8 호출 그래프

 

시스템 디자인 내의 호출 구조를 분석하는 것은 기술 테스트 분석가에게 두 가지 면에서 유용할 수 있다.

  • 그 디자인을 더 낫게 모듈화해서 유지보수성을 개선할 수 있다.

  • 다른 모듈과 강하게 상호작용하는 프로그램 내부의 해당 모듈을 강조해 주는 정보를 얻을 수 있다 (즉 다른 많은 모듈들을 호출하거나 그 자신이 많은 호출을 당하는). 이러한 모듈들은 시스템 내부에서 과도하게 많이 사용되는 영역을 나타내며 효율성이 떨어지는 경우 병목으로 발전할 수 있다.

대체로 단일 소프트웨어 모듈에서 수집한 정보는 팬 인 (호출당한) 그리고 팬 아웃으로 (호출하는) 부른다. 이라는 용어는 그 모듈에서 집중하고 있는 라인을 (호출) 나타내는데 쓰이며 이것은 다이어그램으로 그리는 경우에 팬 효과의 원인이 된다.

 

마찬가지로 호출 그래프는 완전한 시스템 아키텍쳐의 인터페이스들을 보여주는 방법을 제공한다. 따라서 내부 모듈 구조를 나타낼 수 있다 (그림 8-3을 보라. 최초의 예제와 다른 훌륭한 예제는 [URL: Aisee]에서 찾을 수 있다).

 

우리가 통합 테스트를 계획하고 있을 때 호출 그래프는 어떤 모듈이 효과적으로 통합되었는지를 확인하는데 도움을 주며 동시에 적절한 통합 전략을 결정하도록 해준다. 예를 들어 우리는 아마도 상향식 통합 전략이나 하향식 통합 전략이 적절하다고 결정할 수 있다. 페어 와이즈 전략이나 이웃하는 통합 전략 같은 다른 가능성도 [Jorgensen 02]에 언급되어 있다.

 

일반적으로 호출 그래프는 우리 프로그램의 아키텍쳐에 대한 “큰 그림”을 볼 수 있게 도와준다. 다음 그림은 모듈과 (큰 사각형) 그들 사이의 호출을 나타낸다. 또한 호출 그래프에서 반드시 필요한 것은 아니지만 부가 정보를 위해 그 모듈의 내부 구조 또한 나타내고 있다.

 

그림 8-3. 호출 그래프의 예

 

통합 순서가 정의된 이후 통합의 순서가 개발팀과 합의할 수 있는 배포 일정을 협의할 때 사용할 수 있게 제안된다. 또한 이것과 병행해서 호출 그래프는 필요한 시뮬레이션을 빠르게 확인하고 (예를 들면 스텁이나 드라이버) 그 구현을 계획하기 위해 개발자와 테스터를 지원한다.

 

추가적인 장점을 특정 시스템 요구사항에 관한 호출 그래프에서 얻을 수도 있다. 여기에 몇 가지 예가 있다.

  • 실시간 시스템은 호출 메커니즘이 사용하는 CPU 싸이클을 줄이기 위해 사용하는 호출의 숫자를 최소화하는데 필요하다. 호출 그래프는 머지를 위한 후보자가 되는 모듈을 확인하는데 도움을 줄 것이다.

  • 호출 그래프는 테스트 자동화를 위한 훌륭한 목표 지점을 확인하는데 도움을 준다 (대체로 동적 분석과 결합해서)

  • 호출 그래프는 실패의 내결함성을 테스팅할 훌륭한 후보들을 찾는데 도움을 준다. 예를 들어 이것들은 시스템들 사이의 통신을 처리하는 모듈이 될 수도 있다. 그러므로 이것은 특히 예상치 못한 입력에 강건해야 한다.

8.2 동적 분석

 

동적 분석을 수행하는 것은 소프트웨어 프로그램을 실행할 것을 요구한다. 정적 분석에 관해 생각해 보면 우리는 도구의 지원없이 동적 분석을 수행하지 않을 것이다. 이 도구들은 테스터와 개발자들을 위해 정보를 제공하며 이어지는 중요한 결함 유형을 발견할 수 있다.

  • 메모리 누출

  • 리소스 누출

  • 포인터 문제

동적 분석은 어떤 테스트 레벨이던 간에 적절하다 하지만 대체로 유닛 테스팅의 낮은 단계나 통합 테스팅에 적용된다. 이것은 발견하는 결함의 유형이 태생적으로 기능적이기 보다는 기술적이며 이런 테스팅 레벨에서 마스터 테스트 계획에 집어넣기 쉽기 때문이다. 초기 단계에서 이러한 문제들을 발견하고 제거함으로써 시스템 테스트 또한 메모리 누출의 원인이 되는 혼란을 줄여 이익을 누릴 수 있다.

 

8.3.1 이익

 

일반적으로 동적 분석을 사용하는 큰 장점은 사용하지 않았을 때에 비해 아주 어려웠을 수 있고 다른 유형의 테스팅에서는 발견하는데 비용이 많이 드는 결함을 발견해 내는 (불가능하지는 않겠지만) 능력이다.

 

결함 그 자체의 속성으로 볼 때 그것들은 재현하기 어려운 실패로 이어지고 심지어 장기가 알아차리지 못하게 잠복해 있게 된다. 가끔 이러한 실패는 그 문제의 실제 원인과 그 실패 자체가 멀리 떨어져 있게 조작한다. 실패 자체가 본질적으로 심각할 수 있으므로 예를 들면 특히 안전 제일의 시스템이 포함된 경우에 (예를 들어 크래쉬, 시스템 오동작, 데이터 망실) 신뢰할 수 있는 운영에 큰 리스크를 나타낸다. 그러므로 동적 분석을 통해 발견하는 결함 유형을 조사하는 것은 어떤 기술 테스트 분석가에게도 필수적이다 (이것은 8.2.3절 “메모리 누출” 그리고 8.2.4절 “포인터와 관련된 문제”에서 논의할 것이다).

 

도구 기반의 동적 분석은 비용 대비 효과가 높다. 이것은 테스트 계획에서 도구가 필요한지에 대해 결정을 내릴 때 중요한 요소이다. 그 도구는 설치하고 쓰기 쉬워야 하며 그 도구를 통해 손쉽게 발견할 수 있는 결함 중 일부가 출시까지 이어지지 않는다면 그 도구의 라이선스 비용은 그 결함을 발견해 내는 것에서 복구될 수 있다. 기능 테스트를 수행하는 동안 이 도구를 “백그라운드에서” 실행할 수 있기 때문에 동적 분석 이슈를 위한 빈번한 회귀 테스팅과 소프트웨어 품질의 확신이 점차 증가하는 상황에서 쓸 수 있는 저렴한 이슈가 된다. 개발자의 통합 개발 환경에 포함된 동적 분석 도구를 찾는 것은 일반적이다.

 

동적 분석 도구에서 시각적인 표현을 (이 경우 실행 시점에) 포함하는 동적 분석 도구는 소프트웨어가 사용하는 시스템과 네트워크 모두에 대해 더 잘 이해할 수 있게 해준다. 이 정보는 개선을 통해 이익을 얻을 수 있거나 (예를 들면 성능) 상세 테스팅에 필요한 코드 영역 을 확인하는데 사용할 수 있다. 어떤 경우 이 정보는 8.2.5절에 논의한 대로 정적 분석의 결과의 (예를 들면 동적 호출 그래프) 보충이 될 수도 있다.

 

8.2.2 제약

 

동적 분석으로 얻는 이익은 그 제약을 훨씬 뛰어넘는다. 하지만 우리는 동적 분석을 시행하기 전에 몇 가지 지점을 인식할 필요가 있다.

 

이 도구가 런타임 정보를 얻기 위해 데이터를 뽑아내는 메커니즘을 필요로 한다. 이것은 대체로 코드에 조작 코드를 삽입해 테스트를 수행하기 전에 당신의 오브젝트 코드와 그 도구를 연결해 구현된다. 이것은 동적 분석을 활성화하는 고상한 방법이다. 하지만 대체로 이것이 당신의 애플리케이션이 동작하는 속도보다 더 느리게 코드를 실행하는 것을 의미하지는 않는다. 일반적으로 이것은 문제가 안 된다. 하지만 당신이 실시간 시스템에서 시간에 민감한 테스트를 수행하거나 특별한 성능 테스트를 수행한다면 동적 분석 도구가 시스템 성능에 영향을 주는지 (이른바 탐침 효과) 먼저 점검하는 것이 매우 필요하다.

 

동적 분석 도구는 당신이 애플리케이션의 구현에 쓰는 프로그래밍 언어에 종속적이다. C와 C++ 같은 특정 언어들이 도구로 발견되는 결함이 더 많은 경향이 있으므로 비용 대비 편익 관계는 자바 같은 덜 영향 받는 것에 비해 크게 달라진다.

 

8.2.3 메모리 누출

 

프로그래머는 그 프로그램이 얼마나 많은 메모리를 (RAM) 필요로 할 지 프로그램 실행 전에는 알 수 없다. 예를 들어 외부 인적 리소스 시스템에서 온 파일에 들어 있는 각 레코드에 대해 우리 프로그램에서 “사람” 객체를 생성할 필요가 있다고 하자. 그런 상황에서 그 프로그래머는 얼마나 많은 “사람” 객체가 생성될 필요가 있는지 예측할 수 없고 최대 예상치를 (예를 들면 2,000명의 사람) 처리하는데 충분히 큰 RAM의 정적 크기를 예약해서 RAM 리소스를 낭비할 수도 있다. 그런 경우 개발자는 종종 필요할 때 동적으로 RAM을 정렬하고 (즉 각 개별 “사람” 객체를 생성) 이후 더 이상 필요 없을 때 명시적으로 해제한다 (예를 들어 계산이 완료된 특정 사람의 범부에 대한 누적 통계를 사용해). RAM에 대한 이런 동적인 예약과 해제는 RAM 리소스의 사용의 유연성과 효율성을 제공한다. 하지만 또한 이것이 메모리 누출의 원천이 된다. 그러한 누출은 잘못된 프로그래밍 때문에 메모리가 동적으로 예약되고 필요 없을 때 해제되지 않게 된다.

 

그런 누출을 찾기

 

도구 없이 메모리 누출을 찾는 것은 특히 누출 당 잃어버리는 메모리의 양이 작거나 가용 램의 크기가 큰 경우 극도로 어렵다. 그 결과는 며칠 또는 심지어 몇 주 동안 점차로 쌓여가는 시스템 성능의 극히 미미한 감소로 이어질 수 있다. 테스팅을 하는 동안 우리의 시스템은 재시작되기도 해서 이러한 희미한 효과가 RAM이 재초기화되기 전에 누적될 기회를 얻지 못하기도 한다. 만일 그 시스템이 주기적으로 구동되는 운영환경에 출고되었다면 RAM의 부정적인 효과는 명백해 질 때까지 조만간 성장할 것이다. 이 단계에서 그 시스템의 재시작은 심각한 재정적인 또는 안전과 관련된 처벌로 이어질 수 있다.

 

메모리 누출을 효과적이고 효율적으로 발견하기 위해 도구는 불가피하다. 대체로 그 도구는 당신의 프로그램의 실행 코드와 연결되어 있으며 기능 테스트의 과정에서 RAM의 예약과 해제를 지속적으로 감시한다. 만일 메모리 누출이 발견되면 그 도구는 이런 일이 발생한 코드 내의 지점을 지정하는 리포트를 만들어 낸다. 어떤 도구는 당신의 애플리케이션이 사용하는 (맞다. 그들 역시 메모리 누출 문제를 가질 수 있다) 써드파티 제품을 위해 이것을 수행할 수도 있다.

 

비로 이 절에서의 논의가 RAM 누출에 집중하고 있지만 실질적으로 프로그램이 사용하는 어떤 제한적인 리소스라도 부족과 심지어 잘못된 프로그래밍의 결과로 인한 실패에 관심을 가지는 것은 언급할 가치가 있다. 예를 들어 파일 핸들, 커넥션 풀 (예를 들면 네트워크 연결) 그리고 프로그램 제어를 위해 사용하는 세마포어가 이 경우에 해당한다.

 

교훈: 시작부터 분석 도구 사용하기


내가 참여했던 C++ 프로젝트에서 우리는 3일 정도 지속적으로 그 애플리케이션을 사용한 후 전형적인 메모리 누출 문제를 만났었다. 그 메모리 손실은 부족한 성능과 크래쉬의 원인이 되었다. 비록 시스템 리부팅으로 그 문제를 해결하곤 했지만 이 해결책은 지속적으로 돌아가는 안전 제일 애플리케이션에는 거의 적합하지 않았다. 무언가 조치가 필요했다.


비록 우리가 메모리 누출이 있다는 걸 인지했지만 그 원인을 실제로 찾는데는 엄청나게 오랜 시간이 걸렸다. 며칠 뒤 우리는 동적 분석 도구를 구매했다. 그리고 몇 분 안에 문제를 목도했다. 우리는 특정 대화상자 유형이 열리고 닫힐 때마다 각각 1KB 가량의 누출이 있는 시각적인 객체의 써드파티 패키지를 쓰고 있었다. 그 써드파티 소프트웨어의 공급사는 그 문제를 전혀 몰랐고 불행히도 우리는 우리 프로그램에 우회방법을 강제로 적용해야 했다. 우리는 그 프로젝트 시작 시점에 그 도구를 구매해 많은 공수를 절약할 수 있었고 개발 초기부터 주기적으로 그것을 올바르게 사용할 수도 있었다.

8.2.4 포인터와 관련한 문제

 

포인터는 명령, 데이터 그리고 프로그래밍 쓰는 객체의 저장 위치를 가리키는 (“지시하는”) 주 메모리 내의 (RAM) 주소이다. 다수의 문제가 (시스템 실패를 포함해) 프로그래밍 에러로 인해 잘못된 방법으로 포인터를 쓰거나 올바른 메모리 사용을 통제하는 규칙을 위반했을 때 발생한다 (메모리 사용 문제의 원인이 되는 포인터는 와일드 포인터와 관련이 있다).

 

메모리 사용과 관련된 전형적인 규칙 중 일부를 그림 8-4에 있는 다이어그램에 나타내었다.

 

그림 8-4. 메모리 상태

 

이 그림은 메모리 위치가 존재하는 상태, 그 상태들 간에 발생할 수 있는 전이 그리고 특정 상태일 때 메모리에서 발생하는 정확하고 부정확한 액션을 나타낸다. 다이어그램에서 malloc은 메모리 할당을 나타내며 이것은 동적인 메모리 확보를 위한 C 프로그래밍 언어의 메커니즘이다.

 

이 다이어그램의 표 형태에서 다수의 액션이 회피해야 (OK가 아님) 하는 것으로 나타나며 동적 분석 도구는 실행 시점에 그것을 발견할 것이다. 특히 할당되지 않은 메모리에 쓰기 액션을 하는 활동은 심각하다. 왜냐하면 이것이 원치 않는 값으로 메모리 영역에 쓰기를 하는 결과를 가져오기 때문이다. 무슨 일이 일어나는 지는 해당 메모리 영역의 본래 의도에 달려있다.

  • 덮어쓰기가 된 메모리 영역이 사용되고 있지 않다면 우리에게 행운이며 (이 경우) 그 프로그램은 예상한 동작을 계속한다.

  • 덮어쓰기가 된 메모리 영역이 시스템의 제어 흐름에 중요한 역할을 한다면 (예를 들어 운영 시스템의 예약 영역) 그 시스템은 빈번하게 크래쉬 될 것이다. 만일 이런 일이 테스팅에서 발생하면 그 실패는 최소한 매우 가시적이 되어 우리가 그 문제의 존재에 대해 경고하게 될 것이다.

  • 덮어쓰기 된 메모리 영역이 프로그램이 사용하는 데이터나 객체를 저장하는데 쓰이면 아마 그 프로그램은 계속 동작을 유지하지만 대체로 제한된 기능성을 나타낼 것이다. 우리는 한 객체를 찾이 못하게 되면 에러 메시지를 보게 될 수도 있고 이제는 잘못된 데이터를 사용하고 있게 된다. 다시 한 번 이러한 문제의 결과가 가시화되면 우리는 대응 조치를 취할 수 있다. 하지만 만일 그 결과가 애매하다면 (데이터를 덮어쓰는 경우처럼) 그 문제는 인지되지 않고 지나갈 것이다. 우리는 기능 테스팅에서 실제 결과와 기대 결과 사이의 약간의 불일치를 발견할 수도 있다. 하지만 이런 일이 항상 발생할 것이라 장담할 수는 없다.

여기에 나열된 항목은 특히 테스팅에서 이것을 발견하는 경우 운이 좋다고 말할 수 있는 찾기 힘든 포인터/메모리 결함을 나타낸다. 사실 도구 없이 그 문제의 근원을 찾으려고 시도하는 것은 어려움이 따르는 매우 시간 소모적인 프로세스이다. 본질적으로 그 실패 조건을 재현하기는 어려우며 문제 그 자체가 아니라 그 문제의 증상을 쫓는데도 상당한 공수가 들어간다.

 

포인터 문제의 심각한 속성은 세 번째 경우를 나열해서 추가로 설명한다. 즉 덮어 쓰기 된 메모리는 초기에 프로그램의 기능에 영향을 주지않고 코드 내의 결함이 처음에는 실제적인 실패로 이어지지 않는다. 이것은 그냥 폭발을 기다리는 코드 내의 시한 폭탄이다. 이것들은 휴지 상태로 유지되어 절대 실패로 드러나지 않는다. 이들은 테스팅에서 갑작스레 튀어 오르거나 (악몽 시나리오) 운영 영역에서 쓰인 이후 수 년 이후에 시스템 실패의 원인이 될 것이다. 이것은 우리가 만든 변화가 서로 다르게 설명한 RAM 사용 패턴을 쓰기 때문이다. 소프트웨어의 변경 사항이 구현된 후 이제 덮어 쓰여진 RAM의 위치가 잘 알려져 있으며 앞에 적용될 수 있는 또 다른 실패 조건 중 하나가 적용된다. 또한 이것은 개발자가 문제를 찾으려고 해당 코드의 디버깅 버전을 로드할 때 그것이 이동하면서 발생하기도 한다. 독자들이여 만일 당신의 시스템이 포인터 문제나 실패가 발생했을 때 결과가 심각하다면 위험할 가능성이 있는 도구와 함께 동적 분석을 수행하지 말라.

 

8.2.5 성능 분석

 

정적 분석에서 쓰인 도구처럼 동적 분석에서 쓸 수 있는 도구도 테스트 중인 소프트웨어에 관해 다양하고 유용한 정보를 제공한다. 일반적으로 이것은 실행 정보의 형태이며 특히 성능상의 병목을 지적하는데 유용하다.

 

그림 8-5. 정적 호출과 동적 호출 그래프 비교

 

호출 그래프에서 동적 분석이 제공한 정보를 떠올려보면 (모듈 사이의 호출 관계를 나타내는) 애플리케이션이 실행될 때 모듈 사이에 얼마나 많은 실제 호출이 발생하는지의 대한 상세 정보를 포함한 정적 정보를 확장하는 것이 가능하다. 이와 같은 결과에서 테스터는 성능에서 최대한의 장점을 가져올 수 있게 개선할 모듈을 찾을 수 있다. 그림 8.5는 정적인 형태와 동적인 형태의 호출 그래프를 나타낸다. 동적 호출 그래프는 테스트 수행 동안 이루어진 실제 호출 숫자를 추가하고 호출 인터페이스의 넓이를 적절하게 측정한 것이다. 정적인 정보만 가지고는 모듈 그룹 중에서 모듈 E가 주요 병목으로 확인할 수 있지만 추가적인 동적 정보를 확인하면 테스팅 그룹에서는 모듈 G를 병목으로 지명할 것이다.

 

8.3 실용적이 되자

 

어떻게 마라톤 애플리케이션에 분석 기법을 적용할 수 있을까? 먼저 시스템 다이어그램을 살펴보자.

 

그림 8-6. 마라톤 시스템

 

우리가 최초로 마라톤 시스템의 요구사항과 명세를 분석한 후 이어지는 측면들이 정적 분석 기법과 동적 분석 기법을 사용하는 접근법에 영향을 줄 수 있는 것으로 확인했다.

  • 내부에서 개발한 시스템 영역은 자바 프로그래밍 언어를 사용할 것이다. 자바 프로그래밍 가이드라인은 개발팀이 확립했다.

  • 통신 서버는 주자가 보내는 모든 수신 SMS 메시지를 처리하며 기존에 개발되어 있는 C로 프로그래밍된 코드를 재사용한다.

  • 리포트 생성기는 개발 외주로 C++로 구현하는 것으로 알려져 있다. 우리는 그 소스 코드에 접근할 권한이 없다.

  • 통신 서버는 주자 100,00명까지 주자 당 분당 한 개를 보내는 속도로 SMS 메시지를 처리할 필요가 있다.

 

이런 측면을 고려했을 때 우리는 이어지는 접근법을 적용할지 선택할 것이다. 기술 테스트 분석가는 그 자신이 결정을 내릴 수 없고 적합한 접근법에 관해 테스트 관리자와 프로젝트 리더의 조언을 반드시 받아야 함을 주의한다.

  • 자바 프로그래밍 언어를 위해 정적 분석 도구를 선택하고 개발자들이 자바 프로그래밍 가이드라인을 적용하는지 확인한다. 이 도구를 통합 개발 환경에 (IDE) 포함시킬 것을 고려한다.

  • C 프로그래밍 언어를 위해 동적 분석 도구를 선택하고 그것을 통신 서버의 성능이 최적화될 수 있는 영역을 찾는데 활용한다.

  • C 코드에서 메모리 누수나 포인터 문제를 주기적으로 점검하기 위해 동일한 동적 분석 도구를 사용한다.

  • 프로젝트 리더에게 리포트 생성기 소프트웨어를 공급하는 공급자가 전달할 소프트웨어에 동적 분석을 수행하고 그 결과를 검토할 것을 협상하도록 요청한다. 만일 이러한 리스크 감소 방법이 가능하지 않다면 이 제품 리스크를 줄일 수 있는 다른 방법을 고려한다 (예를 들어 공급사 변경, C++를 위한 동적 분석 도구의 자체 구매, 자체적인 모니터링 수행)

 

EOD

반응형

관련글 더보기

댓글 영역