카테고리 없음

2024-07-06 [nGrinder 성능 테스트]

Glen_check 2024. 7. 6. 23:40

성능테스트의 필요성

대부분 해당 포스트를 읽는 분들은 성능 테스트의 필요성을 인지하고 있을 것이라 생각하지만... 그래도 필요성을 간략하게 설명하면

애플리케이션이나 웹사이트에는 사용자가 존재합니다. 그리고 사용자는 요청의 응답이 늦어질수록 해당 서비스에서 이탈할 확률이 높아집니다.

2017년 구글에서 제시한 지표에서 모바일 환경에서 페이지의 로드 타임이 1초에서 3초 가까이 되면 서비스 사용자 이탈률이 32%까지 증가한다는 것을 확인할 수 있습니다. 사용자의 이탈을 방지하기 위해서는 서버의 성능이 중요하다는 것을 인지할 수 있습니다.

 

추가적으로 반드시 고려해야 할 부분은 사용자가 증가할수록 동시 접속자는 증가할 것이고, 서버의 자원은 한정적이지만 사용자는 매우 가변적이기 때문에 한정적 자원에 몰아 붙여지는 과도한 트래픽, 부하를 반드시 고려해야 합니다!

부하가 증가할수록 사용자 각각의 요청에 응답이 늦어지는 경우가 발생할 수 있으며, 과도한 부하는 서버에 장애를 발생시켜 서비스 자체를 사용할 수 없는 문제가 발생할 수 있습니다.

그래서 개발자는 반드시 성능 테스트 진행을 통해 더 나은 서비스 환경을 고려할 수 있어야합니다.

 

성능테스트 도구

Gatling
  • Scala를 통해 Test Script를 생성하는 부하 테스트 도구
  • 비동기식 아키텍처로, 가상 사용자를 스레드가 아닌 메시지로 생성하여 수천 명의 동시 사용자 재현이 가능한 테스트 도구
  • 분산 테스트 미지원
JMeter
  • GUI를 제공하여 쉽게 사용 가능 (GUI는 시각적 요소를 사용하여 직관적인 인터페이스를 제공하므로, 사용자들이 쉽게 이해하고 사용 가능)
  • 한 명의 가상 사용자를 하나의 스레드로 생성 (동시성 제한)
  • 분산 테스트 지원
nGrinder
  • Jython, Groovy 스크립트 활용 테스트 시나리오 작성 가능
  • 한 명의 가상 사용자를 하나의 스레드로 생성 (동시성 제한)
  • 분산 테스트 지원

대표적으로 다음과 같은 성능 테스트 도구들이 존재합니다.

이번 포스팅은 nGrinder 도구를 활용한 성능 테스트를 학습해 볼 예정입니다.

 

테스트 작성법  이해하기( nGrinder 스크립트 Groovy )

테스트를 작성하기 전 nGrinder 설치를 우선적으로 진행해야 합니다.

저의 경우 M1 Mac에 설치를 진행하였고 설치 시 아래의 포스팅을 참고하였으니, Mac의 경우 아래의 포스팅 내용을 참고해보시는 것을 추천드립니다.

(https://curiousjinan.tistory.com/entry/apple-m1-ngrinder-setup)

 

nGrinder의 경우 Test Script를 Groovy라는 언어로 작성하는데, Junit5 기반으로 Java와 유사하여 비교적 사용이 편리합니다.

스크립트 작성 전, nGrinder의 구조는 다음과 같습니다.

Controller
  • 컨트롤러는 Ngrinder의 GUI 를 제공해주는 부분을 의미합니다.
  • 스크립트 생성과 테스트 명령을 Agent에 전달하는 역할을 합니다.
Agent
  • Controller 로 부터 요청을 받아 Target에 요청을 보내는 역할을 합니다.
Target
  • Agent로 부터 요청을 받아 스트레스 테스트를 해야하는 대상을 의미합니다.

Agent 설치까지 진행이 된 이후, QuickStart 안 Test를 원하는 API의 Url을 입력해주면 자동으로 다음의 스크립트를 생성해 주는 것을 확인할 수 있습니다.

(진행하고자 하는 Test의 Url은 'http://127.0.0.1:8080/api/v1/keywords/last-day'로, "조회 시점에서 하루동안의 인기검색어를 조회하는 API"입니다. cache를 사용하지 않은 Version 1으로, Cache를 사용하기 전, 성능 비교용 테스트입니다.)

 

@RunWith()
@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test
	public static HTTPRequest request
	public static Map<String, String> headers = [:]
	public static Map<String, Object> params = [:]
	public static List<Cookie> cookies = []

Spring의 테스트 컨텍스트 프레임워크의 JUnit 확장기능 지정
JUnit은 각각의 테스트 메소드가 서로 영향을 주지 않고 독립적으로 실행하도록 설계되어 각 테스트 메소드 실행 시마다 새로운 인스턴스를 생성합니다. 하지만 이로 인하여 각 테스트 클래스를 지정한 ApplicationContext도 매번 새로 로드해야 하는 비효율적인 상황이 발생할 수 있습니다.

이를 방지하기 위해 @RunWith annotation은 각 테스트 별로 오브젝트가 생성되더라고 싱글톤으로 유지되며, 이후 테스트 클래스들이 동일한 컨텍스트를 공유할 수 있도록 하여 컨텍스트를 매번 새롭게 로드하는 오버헤드를 줄일 수 있습니다.

 

여기서 '@RunWith(GrinderRunner)' Annotation은 해당 클래스가 ngrinder 환경에서 실행된다는 것을 명시합니다.

 

@BeforeProcess
	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test = new GTest(1, "127.0.0.1")
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

프로세스가 생성될 때 프로세스 단위로 실행해야 하는 동작을 사전 설정합니다.

설정 내용은,

  • 'HTTPRequestControl.setConnectionTimeout(300000)' 연결 타임 아웃을 300초(5분)로 설정하여 해당 시간을 초과할 시, 연결되지 않는 것으로 인식합니다.
  • 'test = new GTest(1, "127.0.0.1")'를 통해 테스트 객체를 초기화하며, 'request = new HTTPRequest()'로 HTTP 요청 객체를 초기화합니다.
  • 'grinder.logger.info("before process.")' 설정 완료를 로그로 기록합니다.
@BeforeThread
	@BeforeThread
	public void beforeThread() {
		test.record(this, "test")
		grinder.statistics.delayReports = true
		grinder.logger.info("before thread.")
	}

Thread가 시작되기 전 실행되어야 할 부분을 설정합니다. 보통 로그인 같은 테스트 사전 처리 코드를 작성합니다. 

설정 내용은,

  • 'test.record(this, "test")' ngrinder에서 테스트 메서드 실행을 기록하기 위한 설정입니다.
  • 'grinder.statistics.delayReports = true' 성능 테스트 시작 시 초기 단계에서 발생하는 불안정한 데이터를 배제하고, 시스템이 안정된 후의 데이터를 수집하기 위해 성능 통계 보고를 지연시킵니다.
  • 'grinder.logger.info("before thread.")' 설정 완료를 로그로 기록합니다.
@Test
	@Test
	public void test() {
		HTTPResponse response = request.GET("http://127.0.0.1:8080/api/v1/keywords/last-day", params)

		if (response.statusCode == 301 || response.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
		} else {
			assertThat(response.statusCode, is(200))
		}
	}

실제 테스트를 수행하는 메서드로 테스트 내용은,

  • 'HTTPResponse response = request.GET("http://127.0.0.1:8080/api/v1/keywords/last-day", params)' 지정된 URL로 GET 요청을 보내고 응답을 받습니다.
  • 'if (response.statusCode == 301 || response.statusCode == 302)' 서버가 요청된 자원을 다른 위치로 리다이렉트되어 테스트 결과에 영향을 주지 않도록, 응답 코드가 301(영구 이동) 또는 302(임시 이동)일 경우 경고 로그를 남깁니다.
  • 'assertThat(response.statusCode, is(200))' 응답 코드가 200(성공)인지 확인합니다.

 

이 클래스는 ngrinder를 사용하여 지정된 URL로 HTTP 요청을 보내고, 응답 코드가 200인지 확인하는 테스트를 수행합니다. 각 설정은 테스트를 안정적이고 일관되게 수행할 수 있도록 준비합니다.

 

위의 스크립트 전체 흐름을 정리해보면,

test() 메서드에서는 HTTPRequest 객체를 사용하여 http://127.0.0.1:8080/api/v1/keywords/last-day URL로 GET 요청을 보낸 뒤, 이 URL에서의 응답 코드를 확인합니다. 응답 코드가 301 또는 302인 경우에는 경고 로그를 남기며, 그 외의 경우에는 응답 코드가 200인지 확인합니다.

 

결과적으로 중요한 부분은,

nGrinder 도구를 활용한 테스트는 어렵지 않았습니다. 다만 '테스트를 통해 정확히 도출하고 싶은 결과가 무엇인지' 사전 정의를 하고 목적에 맞는 테스트를 진행하는 것이 중요해 보였습니다.

이를 위해 아래의 이미지 속 각 환경 설정이 가지는 의미를 명확하게 이해하고, 가설을 세우는 것이 다음의 미션이라고 생각했습니다.

 

 

그러면 Test를 어떻게 끝 마쳤고, 어떤 결론을 도출할 수 있었는지는 다음주 포스팅 내에서 찾아뵙도록 하겠습니다.. 꾸벅... 너무 어려운 것..