웹 애플리케이션을 개발하다 보면 반드시 개발한 기능을 검증하는 과정을 거친다.
테스트는 개발만큼이나 중요한 작업으로, 이용자의 예상 시나리오를 작성하여 해당 과정이 성공적으로 이루어지는지 판단하는 과정을 거치고는 한다.
테스트를 수행하는 툴은 다양하지만, 그중에서도 java 오픈 소스 툴인 Apache JMeter로 테스트 시나리오를 작성하는 방법을 알아보자.
본 포스팅에서는 JMeter가 이미 설치되어 있음을 가정한다. JMeter 설치 방법은 이미 여러 블로그에 있으니 검색해서 참고하면 좋겠다.
테스트 시나리오는 Jaeyeon Beak님의 '테스트 명장, Apache JMeter' 포스트를 참고하였다.
JMeter를 통해 통신할 서버 또한 해당 포스트의 FastAPI 서버 예제를 활용한다.
해당 포스트에서 공유된 Github에서 다운받을 수 있다.

fastapi 서버 실행을 위해서는 해당 코드가 있는 폴더의 터미널에서 다음 명령어를 실행한다.
uvicorn main:app
그 뒤 localhost/docs 또는 127.0.0.1:8000/docs 로 접속하면 fastapi 문서를 확인할 수 있다.

JMeter는 크게 세 가지의 계층 구조로 이루어져 있다.
Test Plan : 전체 시나리오를 관장하는 글로벌한 설정 영역이다.Thread Group : 하나의 시나리오 덩어리를 저장하는 영역이다. 실행하는 스레드 수, 램프업 시간, 테스트 수행 시간을 지정하며, 하나의 테스트 플랜에는 여러개의 Thread Group을 지정할 수 있다. 본 예제에서는 로그인해서 상품을 보고 결제하는 일련의 과정을 의미한다.Sampler : JMeter가 대상 시스템에 요청해야 하는 정보를 설정한다. 본 예제에서는 로그인, 상품 조회, 가격 조회 등 각각의 수행을 의미한다.
Test Plan에 우클릭을 하면 위와 같은 메뉴를 확인할 수 있다.
메뉴에 대한 설명은 다음과 같다.
Thread Group : 위에서 설명했듯 하나의 시나리오를 저장하는 영역이다.Config Element : Sampler의 요청 정보를 관리할 수 있다.Listener : JMeter를 통해 테스트하는 결과 정보 및 진행 상태 정보를 표시한다. 진행 추이를 그래프로 표시하거나 xml, csv 등으로 저장할 수 있다.Timer : 요청과 요청 사이에 특정 시간 간격이 필요한 경우 사용한다.Logical Controllers : JMeter가 언제 서버에 요청을 전달할지 결정한다.Assertions : JMeter의 HTTP 프로토콜을 활용해 성능 테스트를 할 경우 기본적으로 HTTP 응답 코드가 200이면 성공, 그외에 다른 코드 값은 실패로 판단한다. 하지만 업무적으로 200번 코드가 리턴되더라도 실패로 판단해야 하는 경우 Assertion를 이용해서 응답 정보에 특정한 메시지를 필터링해서 성공/실패 여부를 판단할 수 있다.Pre Processors : 샘플러를 실행하기 전에 수행해야 할 내용을 정의한다.Post Processors : 샘플러를 실행한 후에 수행해야 할 내용을 정의한다.
먼저 Config Element > User Defined Variables에서 HOST, PORT 값을 지정했다. 이 설정은 모든 Sampler에서 사용되므로 사용자 지정 변수를 활용하면 테스트 서버 주소가 변경될 시 번거로움을 덜어줄 수 있다.

다음으로 Thread (Users) > Thread Group 을 통해 Thread Group을 추가한다.
Action to be taken after a Sampler error : 테스트 도중 에러가 발생할 시 취할 액션을 정의한다. 'Continue'로 설정되어 있으므로 오류가 발생하더라도 무시하고 계속해서 테스트를 수행한다.Number of Threads (users) : 몇 명의 스레드 수(유저 수)로 테스트할지 정의한다.Ramp-up period (seconds) : Number of Threads 만큼의 요청을 몇 초에 걸쳐서 만들지 정의한다.예를 들어 Number of Threads가 10이고 Ramp-up period가 1일 때, 1초 동안 10개의 요청을 발생시킨다.요청 간의 간격은 일정하다고 보장할 수 없으며, 디테일한 시간 설정은 timer를 활용해야 한다.Loop Count : 요청을 몇 번 반복할지 정의한다.예를 들어 Number of Threads가 10이고 Loop Count가 5일 때, 10개의 스레드를 5번 반복해 발생시키므로 50번의 요청이 발생한다.
HTTP 요청을 발생시키기 위해 Thread Group 하위에 Sampler > HTTP Request를 추가한다.

로그인 API 테스트를 위해 위와 같이 설정해준다. Protocol은 http, IP와 Port Number은 User Defined Variables에서 지정했던 변수명을 ${HOST}, ${PORT} 처럼 셸스크립트의 문법으로 사용한다. Request Body를 이용한 요청이므로 Body data에 email, password를 넣어 준다.

모니터링을 위해서는 Listener가 필요하다. 전체적인 값을 한번에 확인하기 위해서 Test Plan 밑에 생성해 준다.

View Results Tree, Summart Report, View Results in Table, 그리고 jp@gc 플러그인의 Transcations per Second를 적용해주었다. jp@hc 플러그인 설치는 포스트를 참고하면 된다.
View Results Tree는 각각의 요청에 대해 HTTP 프로토콜의 Request와 Response data의 생김새를 살펴볼 수 있다. 크롬 개발자모드 Network 탭과 유사하다고 생각하면 된다.
이제 Listener도 추가했으니 가운데 상단의 녹색 플레이 버튼만 눌러주면 테스트를 시행할 수 있다.

테스트 시행 후 View Results Tree를 보자, 빨간색으로 422 에러가 발생했다. Response data 탭을 통해 에러 메시지를 확인할 수 있다. 이 경우 Request body의 형식이 맞지 않아 생기는 문제이다.

해당 형식을 맞춰주기 위해 로그인 API Request 하위에 HTTP Header Manager를 추가해 준다. HTTP Header에 값을 직접 추가할 수 있는 기능이다.

User Defined Variable을 추가할 때처럼 content-type을 application/json으로 설정해 준다.

드디어 오류가 수정되고 로그인 API를 테스트할 수 있게 되었다. 유저 id와 토큰 값이 정상적으로 출력된 것을 확인할 수 있다. fastapi 문서에 같은 파라미터를 넣고 실행한 뒤 비교해보면, 같은 형태로 출력된 것을 알 수 있다.

이제 다시 Thread Group으로 돌아가 비회원 상품 조회 Sampler를 생성하도록 하자. 로그인 Sampler를 생성할 때와 똑같이 ${HOST}, ${PORT}로 서버 정보를 지정해 주고, HTTP Request를 Get으로, Path를 /products로 지정해주면 된다.
다만 이 경우, 똑같은 서버 정보를 계속해서 입력해주어야 하는 번거로움이 있다. 지금은 비교적 간단한 예제이지만, 테스트가 복잡해지고 Request가 많아지면 오류 발생 가능성이 높아질 수 있다.

Config Element > HTTP Request Defaults를 추가해 주면 HTTP Request에서의 기본값을 설정할 수 있다. Thread의 모든 HTTP Request에 적용되는 값이 있다면, 일일히 입력할 필요 없이 이곳에만 적용해주면 되는 것이다.

이곳에 서버 정보를 지정해 주면 로그인과 비회원 가격 확인 테스트 Sampler에 서버 정보를 따로 지정하지 않아도 된다.

회원 상품 조회 Sampler는 login API의 결과값인 token을 헤더에 입력해 동작한다. 따라서 다음으로는 login API에서 내려준 token 값을 사용할 수 있도록 설정해줘야 한다. login Sampler 하위에 Post Processors > JSON Extractor을 추가해 준다.

JSON Extractor는 JSON형식 내부의 값을 파싱해서 변수로 사용할 수 있다. 여기서는 token값을 파싱한다.

다음으로 회원 상품 조회 Sampler을 추가해 준다. 회원 상품 조회 Sampler는 비회원 상품 조회 Sampler를 복제해서 사용한다.

login Sampler에서와 같이 회원 상품 조회 Sampler 하위에 HTTP Header Manager를 추가하고, login Sampler 하위의 JSON Extractor에서 파싱했던 token값을 넣어 준다. 이렇게 되면 회원 상품 조회 Sampler의 헤더에 token값을 직접적으로 추가해 요청할 수 있다.

회원가는 10% 할인이 적용된 가격으로 출력되어야 하므로, HTTP 프로토콜이 200이더라도 10% 할인된 가격이 출력되지 않으면 테스트에 실패한 것으로 간주할 수 있다. Assertions > Response Assertion으로 테스트 조건을 추가적으로 설정해 주자.


회원 가격을 조회했을 때 비회원 가격과 같은 가격이 출력되어서는 안되므로, 비회원 가격 조회 Sampler의 결과값을 가져와 Substring, Not으로 설정해주었다. 이렇게 되면 비회원 가격의 결과값에 회원 가격이 포함되지 않을 때에만 테스트 성공으로 간주하게 된다.

이렇게 로그인, 비회원 상품 가격 조회, 회원 상품 가격 조회 Sampler가 전부 완성되었다. 그러나 이 흐름은 이용자의 사용 측면에서 자연스럽지가 않다. ' 비회원 상품 가격 조회 > 로그인 > 회원 상품 가격 조회 '의 흐름이 더욱 자연스럽다. 드래그 앤 드롭으로 순서를 조정해 준다.

이제 가운데 위쪽의 초록색 재생 버튼을 눌러 테스트를 진행한다. View Result Tree 확인 결과, 성공적으로 테스트가 수행된 것을 확인할 수 있다.
이제 테스트 코드 작성이 끝났으니 저장 버튼을 눌러 꼭 jmx파일로 저장해두도록 하자. 저장하지 않고 cmd가 꺼질 시 내용이 모두 날아간다(!)


나머지 Listener를 통해서도 결과값을 확인할 수 있다. 나머지 결과값과 부하 테스트에 대해서는 다음 포스팅에서 알아보도록 하자.
참고자료
Apache JMeter 공식 홈페이지 https://jmeter.apache.org/
먹세 - [JMeter] 제이미터 사용방법 https://mosei.tistory.com/entry/JMeter-%EC%A0%9C%EC%9D%B4%EB%AF%B8%ED%84%B0-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95
Jaeyeon Beak - 테스트 명장, Apache JMeter https://jybaek.tistory.com/889
ysk(0soo) - Apache JMeter를 사용해보자 https://0soo.tistory.com/220