자바 앱이 갑자기 꺼진다면?
그건 NPE 때문입니다

– Null Pointer Exception을 자동으로 탐지하는 삼성SDS의 기술

본 논문은 자바에서 가장 흔하고 치명적인 오류 중 하나인 Null Pointer Exception(NPE)을 효과적으로 탐지하기 위한 단위 테스트 생성 기법인 NpeTest를 소개합니다. 기존의 Randoop이나 EvoSuite와 같은 자동 테스트 생성 도구들은 코드 커버리지를 높이는데 집중하지만, NPE 탐지에는 충분히 효과적이지 않다는 문제가 있습니다. NpeTest는 정적 분석과 동적 분석을 결합하여 NPE가 발생할 가능성이 높은 코드 영역에 테스트 생성을 집중시키는 전략을 제시합니다. 96개의 실제 프로젝트에서 추출한 108개의 NPE 벤치마크에 대한 실험 결과, NpeTest는 78.9%의 NPE 탐지 재현율을 달성하였으며, 이는 EvoSuite 56.9% 대비 38.7% 더 높은 결과입니다. 또한 산업 프로젝트에서 89개의 이전에 알려지지 않은 NPE를 성공적으로 탐지하였습니다.

잠깐! 여기에서 논문 원문을 확인해보세요.
[Security, IEEE/ACM ASE 2024]
Effective Unit Test Generation for Java Null Pointer Exceptions

👉 논문 바로가기

NPE, 자바 개발자의 악몽

Null Pointer Exception(NPE)은 Java 애플리케이션에서 가장 흔하고 치명적인 오류 중 하나입니다. NPE는 null 포인터를 역참조할 때 프로그램이 항상 충돌하여 전체 시스템의 정의되지 않은 동작을 유발하기 때문에 심각한 소프트웨어 결함입니다. 최근 산업 보고서에 따르면 실제 Java 애플리케이션의 오류 원인 중 NPE가 가장 큰 비중을 차지하고 있어 소프트웨어 개발 과정에서 NPE 위험을 줄이기 위한 테스팅이 필수적입니다.

단위 테스팅의 복잡함과 NPE감지의 어려움

단위 테스팅은 Java와 같은 객체 지향 프로그래밍 언어에서 가장 널리 사용되는 소프트웨어 테스팅 기법 중 하나입니다. 잘 설계된 테스트 케이스를 통해 소프트웨어의 각 단위가 예상대로 작동하는지 검증하고, 버그를 감지하는 데 도움이 됩니다. 그러나 버그를 유발하는 단위 테스트를 찾는 것은 복잡하고 시간이 많이 소요되는 작업이며, 소프트웨어 시스템의 크기와 복잡성이 증가할수록 더욱 어려워집니다.

자동 테스트 케이스 생성 기술은 개발자의 단위 테스트 설계 부담을 줄이기 위해 제안되었으며, 랜덤 테스팅과 검색 기반 소프트웨어 테스팅이라는 두 가지 주요 접근 방식이 있습니다. 두 방법 모두 대상에 대한 메서드 호출 시퀀스 등을 자동으로 합성하여 대상 프로그램을 직접 실행하는 테스트를 생성합니다.

그러나 본 논문에서는 Randoop과 EvoSuite 같은 단위 테스트 자동생성 도구가 NPE를 포착하는데 충분히 효과적이지 않다는 것을 관찰했습니다. 이러한 단위 테스팅 기술은 주로 높은 코드 커버리지를 달성하는 데 중점을 두고 있지만, 높은 코드 커버리지를 달성하는 것이 반드시 더 나은 NPE 발견 성능으로 이어지지는 않습니다. 이는 NPE와 같은 소프트웨어 버그가 일반적으로 특정 조건에서 발생하기 때문입니다.

NPE예시를 통한 테스트케이스 생성 실패 사례

Apache Qpid Proton-j 프로젝트에서 발견된 NPE를 통해 문제를 살펴보겠습니다.

// MapType.java

250617_6_01 images

// EncoderImpl.java

250617_6_02 images

이 NPE의 근본 원인은 deduceTypeFromClass 메서드에서 amqpType 변수에 할당된 null 리터럴이 정제 없이 반환되는 것입니다. NPE는 calculateSize 메서드의 t.getEncoding(k) 호출에서 발생합니다. 이때 getType 메서드가 내부적으로 deduceTypeFromClass 메서드를 호출하고 결과를 직접 반환합니다.

그러나 변수 amqpType이 실행 중에 정제되지 않는 조건은 간단하지 않습니다. 첫 번째 인수의 타입이 적절하게 설정되어야 하며, 이는 calculateSize 메서드의 인수에 의해 결정됩니다. 또한 calculateSize 메서드의 입력 맵에 적어도 하나의 요소가 포함되어야 합니다.

이러한 NPE를 유발하는 테스트 케이스를 생성하기 위해서는 단위 테스트 생성 도구가 Map의 제네릭 타입 매개변수에 대한 다양한 타입을 변형하고, deduceTypeFromClass의 분기 조건을 우회하여 amqpType 값이 정제되지 않도록 하는 적절한 타입을 찾는 데 집중해야 합니다. 그러나 EvoSuite와 Randoop은 테스트 케이스와 변형할 문장의 큰 공간으로 인해 이러한 테스트 케이스를 생성하는 데 실패했습니다.

삼성SDS의 해결책: "NpeTest"로 NPE를 더 효과적으로 감지하는 테스트 케이스를 생성

NpeTest는 정적 분석과 동적 분석을 모두 활용하여 NPE를 더 효과적으로 감지하는 테스트 케이스를 생성합니다.

검색 기반 소프트웨어 테스팅

NpeTest는 EvoSuite라는 검색 기반 소프트웨어 테스팅(SBST) 도구를 기반으로 합니다.
EvoSuite의 간소화된 테스트 케이스 생성 프로세스는 다음과 같습니다:

  • 커버리지 목표 식별
  • 초기 부모 인구 구축
  • 부모 인구에서 자식 인구 생성
  • 모든 테스트 케이스의 적합도 값 계산
  • 다음 부모 테스트 케이스 선택
  • 커버리지 목표 업데이트
  • 시간 예산이 소진될 때까지 반복
  • 최종 솔루션으로 테스트 케이스 세트 반환

NpeTest의 워크플로우는 다음과 같습니다:

  • 주어진 클래스에 대한 정적 분석 수행
  • NPE 목표 계산
  • NPE 함수 수집
  • 초기 인구 구축
  • NPE 테스트 케이스 생성
  • NPE 커버리지 목표에 따라 세트 업데이트
  • 메서드 업데이트
  • 테스트 케이스 점수 계산
  • 시간 예산이 소진될 때까지 반복
  • 최종 솔루션으로 테스트 케이스 세트 반환

정적 분석

정적 분석기는 (1) 테스트 대상 클래스(CUT)의 모든 NPE 취약 영역과 메서드를 식별하고 (2) 주어진 테스트 케이스에서 변형할 문장의 우선순위를 정하는 데 사용됩니다.

경로 구성을 통해 각 메서드의 제어 흐름 그래프(CFG)를 구성하고, 이를 기반으로 각 메서드에 대한 대상 표현식 집합을 계산합니다. 이 집합을 사용하여 메서드가 NPE-안전 메서드인지 분류합니다.

Nullable 경로 식별을 통해 주어진 경로에서 대상 표현식이 null이 될 수 있는지 분석합니다. 모든 경로에서 표현식 값이 false로 유지되면 해당 표현식을 역참조할 때 NPE가 발생하지 않는다고 결론지을 수 있습니다.

NPE 가능성 점수 계산을 통해 주어진 메서드에 대한 NPE 가능성 점수를 계산합니다. 이 점수는 나중에 변형 중 테스트 케이스 선택에 사용됩니다.

변형 대상 선택을 통해 변형을 위한 테스트 케이스가 주어지면, 무작위로 변형할 문장을 선택하는 대신 NPE를 유발할 수 있는 메서드에서 문장과 변수를 선택합니다.

동적 분석

동적 분석의 목표는 테스트 케이스의 실행 결과를 모니터링하여 NPE 취약 메서드를 적극적으로 탐색하도록 변형 생성 프로세스를 안내하는 것입니다.

메서드 테스트 업데이트를 통해 런타임 예외 정보를 사용하여 테스트 대상 메서드 집합을 동적으로 정제합니다. 테스트 케이스 실행 중에 NPE가 발생하면, 해당 메서드와 NPE 유발 오류 위치에 대한 정보를 수집하고, 해당 대상 표현식을 제거합니다.

테스트 케이스 수준 NPE 가능성 점수 계산을 통해 각 테스트 케이스에 대한 테스트 케이스 수준 NPE 가능성 점수를 계산하고 유지합니다. 모든 테스트 케이스에 계산된 점수로 주석을 달고, 이 점수를 기반으로 가중치 샘플링을 수행하여 인구에서 변형할 테스트 케이스를 선택합니다.

실험 결과와 적용 가능성

평가 설정

NpeTest는 2024년 2월에 GitHub에서 마지막으로 업데이트된 EvoSuite의 최신 버전을 기반으로 구현되었습니다. 성능 비교를 위해 EvoSuite와 Randoop을 기준선으로 선택했습니다. 각 도구에 대해 5분의 시간 예산으로 벤치마크 클래스에서 25번의 반복 평가 실험을 수행했습니다.

벤치마크로는 실제 NPE 벤치마크를 문헌에서 수집했으며, 최종적으로 96개의 버그가 있는 프로젝트와 108개의 알려진 NPE를 수집했습니다.

NpeTest의 효과성

25번의 시도에서 NPE 유발 테스트 케이스를 생성하는 평균 재현율 결과를 보면, NpeTest는 평균적으로 Randoop과 EvoSuite보다 각각 45.2%와 22.4% 더 높은 재현율로 알려진 NPE를 감지하는 테스트 케이스를 생성하는 데 성공했습니다. 25번의 시도 중 어느 하나에서든 감지된 NPE 수 측면에서, NpeTest는 73개의 NPE를 발견한 반면 Randoop과 EvoSuite는 각각 25개와 59개의 NPE를 감지했습니다.

코드 커버리지와 NPE 감지 간의 상관관계

코드 커버리지와 NPE 감지 능력 간의 상관관계를 관찰하기 위해 서로 다른 옵션으로 EvoSuite를 평가했습니다. 미세 조정된 옵션은 코드 커버리지 성능을 크게 향상시킬 수 있었습니다. EvoSuite는 평균 77.8%의 라인 커버리지를 달성한 반면, 기본 옵션의 EvoSuite는 64.5%를 보여 20.8% 향상되었습니다. 그러나 NPE 감지의 재현율 측면에서는 미세 조정된 옵션을 사용해도 재현율이 55.7%에서 56.9%로 2.2% 향상되는 데 그쳤습니다.

흥미롭게도, NpeTest는 표 2에서 NPE 감지에 가장 좋은 성능을 보였지만 EvoSuite와 기본 옵션 EvoSuite보다 코드 커버리지가 낮았습니다. 이는 NpeTest가 EvoSuite보다 검색 공간(즉, 테스트 대상 메서드)이 작기 때문입니다.

산업 사례 연구

실용적인 실현 가능성을 비교하기 위해 IT 회사 내에서 사용되는 독점 암호화 라이브러리에 초점을 맞춘 사례 연구를 수행했습니다. 이 라이브러리는 84개의 공개 클래스와 13,669줄의 코드로 구성되어 있으며, 수동으로 작성된 단위 테스트를 통해 76%의 라인 커버리지를 달성했습니다.

250617_6_03 images

놀랍게도, 도구들은 총 91개의 이전에 알려지지 않은 NPE를 발견했으며, 이 모두는 라이브러리 개발팀에 의해 진짜 양성으로 확인되었습니다. NpeTest는 89개의 NPE를 발견했으며, 이 중 9개는 EvoSuite가 놓친 것이고 37개는 Randoop이 놓친 것입니다. 반면, EvoSuite와 Randoop은 각각 82개와 52개의 NPE를 감지했으며, EvoSuite는 NpeTest가 감지하지 못한 2개의 추가 NPE만 발견했습니다.

연구의 의의 및 결론

Lessons Learned

현재의 단위 테스트 생성기는 NPE 감지에 충분하지 않습니다. EvoSuite는 Randoop이 쉽게 감지할 수 있는 NPE를 감지하지 못했으며, 총 108개의 NPE 중 59개만 감지했습니다. NpeTest는 73개의 고유한 NPE를 감지할 수 있었습니다.

높은 코드 커버리지 달성이 NPE 감지 능력을 향상시키는 데 필수적이지 않습니다. EvoSuite의 미세 조정된 옵션 매개변수는 달성된 라인 커버리지를 20.8% 증가시켰지만, NPE 감지의 재현율은 2.2%만 향상시켰습니다. 반면, NpeTest는 EvoSuite보다 18.8% 적은 라인 커버리지를 달성했지만, EvoSuite가 찾지 못한 15개의 더 많은 고유한 NPE를 감지할 수 있었고, 평균적으로 22.4% 더 높은 재현율을 보였습니다.

산업 소프트웨어 개발에서 NPE를 감지하기 위한 통합 접근 방식의 중요성. 사례 연구는 산업 소프트웨어 개발에서 NPE를 감지하기 위한 포괄적인 접근 방식을 채택하는 것의 중요성을 강조합니다. 엄격한 테스트 및 개발 프로세스가 있음에도 불구하고, 세 가지 주제 도구는 이전에 알려지지 않은 상당한 수의 NPE를 감지할 수 있었습니다.

한계

물론 NPE 이외의 다른 유형의 버그를 감지하는 데 있어 NpeTest는 고유한 한계가 있습니다. 정적 및 동적 분석을 통해 NpeTest는 NPE가 없거나 테스트 과정에서 모든 NPE가 감지된 메서드를 의도적으로 건너뛰어, 건너뛴 메서드에 존재하는 버그를 놓칠 가능성이 있습니다.

타당성에 대한 위협

EvoSuite의 최고 성능을 평가하기 위해 SBST'22에서 미세 조정된 옵션 세트를 사용했습니다. 그러나 이러한 옵션 값이 일부 벤치마크에서 EvoSuite의 최고 성능을 달성하는 데 적절하지 않을 수 있습니다.

실험 설정에서 빌드하지 못한 프로그램을 제거했습니다. 이러한 프로그램이 적절하게 빌드되고 평가에 사용되었다면 실험 결과가 달라질 수 있습니다.

각 벤치마크에 대해 25번의 시도로 5분 동안 실험을 수행했습니다. 실험을 위한 시간 예산이 EvoSuite와 NpeTest 모두의 최고 성능을 달성하기에 충분하지 않을 수 있습니다.

결론

본 논문에서는 Java null 포인터 예외(NPE)를 더 효과적으로 찾기 위한 자동 단위 테스트 생성 향상에 대한 경험을 공유했습니다. NPE는 Java 애플리케이션에서 가장 흔하고 중요한 오류 중 하나이지만, Randoop 및 EvoSuite와 같은 기존 단위 테스트 생성 도구는 NPE를 포착하는 데 충분히 효과적이지 않습니다.

높은 코드 커버리지를 달성하는 기존 도구의 주요 전략이 실제로 다양한 NPE를 유발하는 데 반드시 결과로 이어지지는 않습니다. 본 논문에서는 NPE 감지 측면에서 현재 최신 단위 테스팅 도구의 한계에 대한 관찰을 상세히 설명하고 그 효과를 향상시키기 위한 새로운 전략을 소개했습니다.

우리의 전략은 정적 및 동적 분석을 모두 활용하여 테스트 케이스 생성기가 특히 NPE를 유발할 가능성이 있는 시나리오에 집중하도록 안내합니다. 이 전략을 EvoSuite 위에 구현하고, 96개의 실제 프로젝트에서 수집한 108개의 NPE 벤치마크에서 우리의 도구인 NpeTest를 평가했습니다.

결과는 우리의 NPE 안내 전략이 EvoSuite의 NPE 재현율을 56.9%에서 78.9%로, 38.7% 향상시킬 수 있음을 보여줍니다. 또한, NpeTest는 산업 프로젝트에서 89개의 이전에 알려지지 않은 NPE를 성공적으로 감지하였습니다.



👉 논문 바로가기

공유하기