동시성 테스트의 핵심: 안전성(Safety)과 활동성(Liveness)
동시성 프로그램을 테스트하는 것은 순차적 프로그램보다 훨씬 더 까다롭고 어려운 작업입니다. 대부분의 동시성 클래스 테스트는 크게 안전성(Safety)과 활동성(Liveness)이라는 두 가지 고전적인 범주로 나눌 수 있습니다.
•
안전성(Safety) 테스트: "나쁜 일이 절대 일어나지 않음"을 보장하는지 확인하는 것입니다. 클래스의 동작이 명세(specification)를 잘 따르고 있는지, 데이터 경합으로 인해 객체의 불변성(invariants)이 깨지지 않는지를 검증합니다.
•
활동성(Liveness) 테스트: "좋은 일이 결국에는 일어남"을 확인합니다. 알고리즘이 데드락(Deadlock) 등과 같은 위험에 빠지지 않고 계속해서 진행되는지, 아니면 그저 느리게 실행되고 있는 것인지를 판단합니다.
여기에 더해 성능(Performance) 테스트도 매우 중요합니다.
성능은 완료되는 작업의 비율인 '처리량(Throughput)', 요청과 완료 사이의 지연 시간인 '응답성(Responsiveness)', 그리고 더 많은 자원(CPU 등)이 주어졌을 때 처리량이 얼마나 개선되는지를 보여주는 '확장성(Scalability)' 등으로 측정됩니다.
동시성 코드에 숨어 있는 타이밍 문제나 데이터 경합을 발견하기 위해서는 `Thread.yield`와 같은 메서드를 활용하여 다양한 스레드 실행 순서(인터리빙, Interleaving)를 강제로 유도하는 것이 효과적입니다.
성능 테스트 시 주의해야 할 3가지 함정
단순히 시나리오를 작성하고 시간을 재는 것만으로는 의미 있는 성능 테스트 결과를 얻기 힘듭니다. 자바와 같은 동적 컴파일 언어에서는 여러 가지 요인이 측정 결과를 왜곡할 수 있기 때문입니다.
1.
가비지 컬렉션 (Garbage Collection): 테스트 실행 중에 예측 불가능하게 발생하는 가비지 컬렉션은 타이밍 결과에 큰 영향을 미칠 수 있습니다. 따라서 가비지 컬렉터가 여러 번 실행될 수 있도록 테스트를 충분히 길게 구동하여 실제 환경의 성능을 반영하게 하는 것이 좋습니다.
2.
동적 컴파일 (Dynamic Compilation): JIT 컴파일러는 코드가 충분히 실행된 후에야 바이트코드를 기계어로 컴파일합니다 [6]. 컴파일되지 않은 인터프리터 코드의 실행 시간을 측정하는 것은 의미가 없으므로, 본격적인 측정 전에 코드가 충분히 실행되도록 '워밍업(Warm-up)' 단계를 거쳐야 합니다.
3.
데드 코드 제거 (Dead-code Elimination): 똑똑한 최적화 컴파일러는 프로그램의 출력에 아무런 영향을 주지 않는 연산을 아예 제거해버릴 수 있습니다. 이로 인해 벤치마크가 비정상적으로 빠르게 측정되는 것을 막으려면, 테스트에서 계산된 모든 결과를 동기화 오버헤드가 적은 방식(예: 연산 결과를 System.nanoTime()과 비교하여 무의미한 문자를 출력하는 방식)으로 반드시 소모(use)해야 합니다.
복잡한 동시성 프로그램에서 테스트만으로 모든 버그를 찾아낼 수는 없지만, 이러한 함정들을 피해서 설계된 정확한 성능 및 단위 테스트는 우리가 만든 동시성 클래스가 예상대로 견고하게 동작한다는 신뢰를 극대화해 줍니다. 추가로 코드 리뷰와 정적 분석 도구(Static analysis)를 병행하면 더욱 큰 효과를 볼 수 있습니다.
