이 연재글은 웹 애플리케이션 부하 테스트 실습의 1번째 글입니다.

Gatling은 부하 테스트(Load Test)를 통해 웹 서버의 성능을 체크하는 OpenSource Solution입니다. 웹 사이트나 Rest API 등 HTTP 기반 서비스의 성능을 테스트하기 위해 사용되며 실제 서비스 환경과 유사하게 요청을 시뮬레이션하여 테스트할 수 있습니다. 신규 서비스를 오픈하기 전이나 새로운 기능을 개발하였을 때 서비스 투입 전 성능상 문제가 없는지 확인하고 개선하기 위해 사용합니다.

Gatling을 이용하면 수많은 유저가 정해진 시나리오대로 동작하는 것처럼 시뮬레이션할 수 있습니다. 모든 요청의 응답 시간은 수집되고 집계되어 보고서 작성 및 데이터 분석에 활용됩니다.

Gatling은 오픈소스 버전과 상용 버전으로 나뉘며 실습에서는 Opensource 버전으로 진행합니다.
https://gatling.io/open-source

상용버전은 다음 링크에서 확인해 주십시오.
https://gatling.io/gatling-frontline

Quick Start

실습에서는 로컬 PC에 Gatling 환경을 구성하여 테스트합니다. 실제 운영 중인 서버를 테스트하기 위해서는 별도 분리된 환경의 서버에서 Gatling이 실행되도록 구성하는 것을 권고드립니다.

사전 준비 사항

Java Version

Gatling은 64bit OpenJDK 8 및 11을 지원합니다. JDK 12+나 32bit System 또는 OpenJ9 같은 기타 JVM은 지원되지 않습니다.

$ java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.242-b08, mixed mode)

대부분의 시스템에 JAVA는 기본적으로 설치되어 있지만 버전이 문제가 되거나 새로 설치해야 한다면 다음 링크를 통해 JAVA를 받아 설치합니다.

https://adoptopenjdk.net/

Scala Version

Gatling은 테스트 시나리오를 Scala 언어로 작성합니다.

시나리오 작성을 위해 Scala를 사용해야 한다는 것에 되려 겁먹고 시작도 안 해보고 끝내는 경우가 많습니다. 하지만 실상은 Document를 보고 만들어진 시나리오를 복사, 붙여 넣기 하는 수준이 대부분이라 Scala 문법에 부담가질 필요는 없습니다.

Gatling 3.0 이후 버전에는 Scala 2.12가 필요합니다. 개틀링은 Scala 2.11 및 Scala 2.13과 호환되지 않습니다. Scala는 JVM기반에서 동작하는 언어이므로 Java 환경이 구성되었다면 Scala 언어로 프로그램을 작성하여 실행해 보면 됩니다.

테스트 환경 구성

Scala로 테스트코드를 작성하고 실행해 볼 수 있는 방법은 다음과 같이 두 가지가 있습니다.

  • Intellij 같은 IDE에 Scala 및 Gatling 환경을 구성하여 테스트하는 방법
  • 직접 Gatling 파일을 다운로드하여 테스트를 진행하는 방법

Intellij IDE Community로 환경 구성

아래 링크로 이동하여 OS에 맞는 Intellij를 다운로드하여 설치합니다.

https://www.jetbrains.com/idea/download

설치가 완료되면 IDE를 실행하고 메뉴 – File – Settings – Plugins로 이동하여 Scala를 검색하여 install 합니다.

플러그인이 설치되면 Scala 컴파일러의 Stack 크기를 늘려서 StackOverflowErrors가 발생하지 않도록 해야 합니다. Xss를 100M으로 설정하는 것이 좋습니다.

신규 프로젝트 생성

File – New – Project를 선택하여 신규 프로젝트를 생성합니다. 실습에서는 아래와 같이 Maven 기반 프로젝트를 생성합니다.

Maven 선택 – Create from archetype 체크 – Add Archetype… 클릭

GroupId : io.gatling.highcharts
ArtifactId : gatling-highcharts-maven-archetype
Version : 3.3.1

다음 화면에서 프로젝트 Name을 설정하고 Artifact Coordinates의 정보도 수정이 필요하면 수정하도록 합니다. 여기서는 default 값으로 진행합니다.

다음 화면에서 최종 설정된 내용을 확인하고 Finish를 눌러 프로젝트를 생성합니다.

최초 실행 시 Maven에 의해 라이브러리 다운로드 및 프로젝트 구성이 진행되며 다소 시간이 소요됩니다. 생성된 디렉터리 구조는 다음과 같습니다. 시뮬레이션을 위한 테스트코드는 src/test/scala/org/example 하위에 scala로 작성합니다. feeder file 같은 리소스 관련 파일은 src/test/resources 하위에 위치합니다.

GATLING-PROJECT
├─.idea
│  └─codeStyles
├─src
│  └─test
│      ├─resources
│      │  ├─bodies
│      │  └─data
│      └─scala
│          └─org
│              └─example
└─target
    ├─gatling
    └─test-classes

Launcher 실행

recorder를 실행하려면 프로젝트에서 Recorder를 마우스 오른쪽 메뉴를 호출하여 실행하면 됩니다. 혹시 안될 경우 maven의 Plugin – gatling – gatling:recorder를 한번 실행해 줍니다. scala로 작성된 테스트 코드를 실행해보려면 프로젝트에서 Engine을 마우스 오른쪽 메뉴를 호출하여 실행하면 됩니다. 역시 실행시 오류가 발생하면 maven의 Plugin – gatling – gatling:test를 한번 실행해 줍니다.

Gatling 파일로 환경 구성

IDE로 환경을 구성할 것이 아니면 Gatling 파일을 다운로드하여 사용하면 됩니다. Gatling은 설치형 애플리케이션이 아니므로 파일을 다운로드하여 압축 해제하고 별다른 설정 없이 코드를 작성하고 테스트를 진행하면 됩니다.

아래 링크에서 Gatling을 다운로드 받을수 있습니다.
https://gatling.io/open-source/

실습에서는 Gatling 3.3.1을 다운로드하여 적당한 위치에 압축 해제하였습니다.

https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.3.1/gatling-charts-highcharts-bundle-3.3.1-bundle.zip

압축 해제한 디렉터리 구조는 다음과 같습니다.

├─bin
├─conf
├─lib
├─results
└─user-files
    ├─resources
    └─simulations
        └─computerdatabase
            └─advanced
  • bin – 녹화를 통해 스크립트를 자동으로 생성하는 recorder.sh, 작성한 스크립트를 실행하는 gatling.sh 가 있습니다. (각각 window용 .bat, linux/mac용 .sh)
  • conf/lib – 설정 파일과 라이브러리가 들어있으며 딱히 수정할 일이 없습니다.
  • results – 스크립트 실행 후 테스트 결과가 html 파일로 생성되는 공간입니다.
  • user-files – 녹화한 테스트코드가 들어가 있습니다. feeder file 같은 리소스 관련 파일은 user-files/resources 하위에 생성합니다. 직접 작성한 scala 테스트 코드는 user-files/simulations 하위에 저장합니다.

시뮬레이션을 위한 테스트 코드 작성

IDE 나 Gatling 파일을 다운로드하여 테스트 환경을 구축했다면 시뮬레이션을 위한 테스트 코드를 작성하여 부하 테스트를 진행할 수 있습니다. 테스트 코드를 작성하는 방법에는 두 가지 방법이 존재합니다.

  • recorder를 사용하여 테스터의 액션을 캡처하고 테스트 코드(scala)를 자동으로 생성하는 방법
  • scala로 직접 시뮬레이션을 위한 테스트 코드(scala)를 작성하는 방법

왜 2가지 방식을 제공할까요? 제 의견은 다음과 같습니다. Gatling은 웹 애플리케이션을 실제로 동작하는 것처럼 가상으로 실행해보는 것입니다.

그런데 웹 애플리케이션은 개발자의 입장에서 볼때 두가지 경우가 있습니다. 웹 프런트. 즉 실제 웹페이지를 테스트하는 경우와 백엔드. 즉 프런트 클라이언트에 데이터를 제공하는 api를 테스트하는 경우입니다.

첫 번째 웹페이지를 테스트 하는 경우는 캡처 방식을 사용하는것이 시나리오 작성에 유리합니다. 하지만 백엔드 api의 경우는 캡처보다는 api call을 직접 scala로 작성하는 것이 보다 명확한 테스트 코드를 작성할 수 있습니다. 그러므로 상황에 맞게 두가지 방식을 적절히 선택하여 테스트 코드를 작성하면 됩니다.

recorder를 이용한 테스트 코드 작성

gatling에서 제공하는 recorder는 웹브라우저에서 요청한 http call을 캡처하여 테스트 코드를 작성합니다. 따라서 웹브라우저에서의 요청을 gatling recorder에서 캡처할 수 있도록 브라우저의 proxy setting을 변경해야 합니다.

Proxy Setting 변경

간편하게 설정하기 위해 google chrome에서 제공하는 Proxy SwitchyOmega extension을 설치합니다.

https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif

설치 후 extension의 설정 화면으로 이동하여 profiles – proxy로 들어가 proxy servers를 세팅합니다. 브라우저에서 특정 사이트로 이동하면 localhost:8000을 무조건 거쳐가라는 의미입니다. 여기서 localhost:8000은 Gatling recorder입니다.

  • protocol – HTTP
  • server : localhost
  • port : 8000

설정을 저장하고 나온 후 chrome 상단의 동그라미 모양을 눌러 Proxy를 선택합니다. 선택하면 위에서 설정한 Proxy설정이 적용됩니다.

테스트가 종료된 후에는 반드시 설정을 Direct로 변경하시기 바랍니다. recorder가 종료된 상태에서는 인터넷이 안되므로 당황할 수 있습니다.

이제 recorder를 실행합니다. 구축한 환경에 따라 ide에서 Recorder를 실행하던지 gatling 압축 해제한 디렉터리의 bin/recorder.sh (Linux/Mac) 또는 recorder.bat(Windows)을 실행하여 recorder를 띄웁니다.

다음과 같은 화면이 나오는데 별다른 설정은 필요 없고 맨 아래 No static resources를 클릭하여 캡처시 image나 javascript같은 정적 리소스의 요청은 캡처되지 않도록 처리한 후 Start!를 클릭하여 recorder를 실행합니다.

recorder가 실행되면 다음과 같은 화면으로 변경되는데 이제 브라우저로 이동하여 주소를 입력하고 여러 가지 액션을 행하면 관련 내용이 녹화되는 것을 확인할 수 있습니다. proxy서버가 https를 제대로 지원하지 않아 브라우저에서 관련 경고 메시지가 출력될 수 있습니다. 녹화할 액션이 완료되면 Stop & Save를 클릭하여 녹화를 종료합니다.

이제 작성된 scala 코드를 확인해 보겠습니다. 자동으로 생성된 테스트 코드는 IDE에서 생성했을 경우 scala/org/example/RecordedSimulation.scala로 생성됩니다. Gatling을 다운로드하여 실행했을 경우는 user-files/simulation/RecordedSimulation.scala로 생성됩니다.

테스트 시나리오 실행

자동으로 작성된 코드는 대략 아래처럼 생성됩니다.

import scala.concurrent.duration._

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._

class RecordedSimulation extends Simulation {

	val httpProtocol = http
		.baseUrl("https://gatling.io")
		.inferHtmlResources(BlackList(""".*\.js""", """.*\.css""", """.*\.gif""", """.*\.jpeg""", """.*\.jpg""", """.*\.ico""", """.*\.woff""", """.*\.woff2""", """.*\.(t|o)tf""", """.*\.png""", """.*detectportal\.firefox\.com.*"""), WhiteList())
		.acceptEncodingHeader("gzip, deflate")
		.acceptLanguageHeader("ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")
		.userAgentHeader("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36")

	val headers_0 = Map(
		"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
		"Sec-Fetch-Dest" -> "document",
		"Sec-Fetch-Mode" -> "navigate",
		"Sec-Fetch-Site" -> "none",
		"Sec-Fetch-User" -> "?1",
		"Upgrade-Insecure-Requests" -> "1")

	val headers_1 = Map(
		"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
		"Sec-Fetch-Dest" -> "document",
		"Sec-Fetch-Mode" -> "navigate",
		"Sec-Fetch-Site" -> "same-origin",
		"Sec-Fetch-User" -> "?1",
		"Upgrade-Insecure-Requests" -> "1")

	val headers_3 = Map(
		"Accept-Encoding" -> "gzip, deflate",
		"Pragma" -> "no-cache",
		"Proxy-Connection" -> "keep-alive")

    val uri2 = "http://www.gstatic.com/generate_204"

	val scn = scenario("RecordedSimulation")
		.exec(http("request_0")
			.get("/")
			.headers(headers_0))
		.pause(4)
		.exec(http("request_1")
			.get("/open-source")
			.headers(headers_1))
		.pause(2)
		.exec(http("request_2")
			.get("/docs/current/")
			.headers(headers_1))
		.pause(65)
		.exec(http("request_3")
			.get(uri2)
			.headers(headers_3))

	setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

작성된 코드를 다시 테스트해 보려면 IDE에서는 Engine을 실행하면 됩니다. Gatling을 다운받아 실행한 경우는 bin/gatling.sh(Linux/Mac) 또는 bin/gatling.bat(Windows)를 실행하면 됩니다. 테스트 시나리오가 여러 개인 경우 어떤 시뮬레이션을 선택할지 결정하는 화면이 나올 수 있습니다. 테스트를 선택하면 진행 과정이 console에 출력되면서 실행됩니다.

Choose a simulation number:
     [0] org.example.FirstSimulation
     [1] org.example.RecordedSimulation
1 [엔터]
Select run description (optional) [엔터]
Simulation org.example.RecordedSimulation started...

================================================================================
2020-06-21 23:42:22                                           5s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=1      KO=0     )
> request_0                                                (OK=1      KO=0     )

---- RecordedSimulation --------------------------------------------------------
[--------------------------------------------------------------------------]  0%
          waiting: 0      / active: 1      / done: 0     
================================================================================


================================================================================
2020-06-21 23:42:27                                          10s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=5      KO=0     )
> request_0                                                (OK=1      KO=0     )
> request_1                                                (OK=1      KO=0     )
> request_1 Redirect 1                                     (OK=1      KO=0     )
> request_2                                                (OK=1      KO=0     )
> request_3                                                (OK=1      KO=0     )

---- RecordedSimulation --------------------------------------------------------
[--------------------------------------------------------------------------]  0%
          waiting: 0      / active: 1      / done: 0     
================================================================================


================================================================================
2020-06-21 23:42:27                                          10s elapsed
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=6      KO=0     )
> request_0                                                (OK=1      KO=0     )
> request_1                                                (OK=1      KO=0     )
> request_1 Redirect 1                                     (OK=1      KO=0     )
> request_2                                                (OK=1      KO=0     )
> request_3                                                (OK=1      KO=0     )
> request_3 Redirect 1                                     (OK=1      KO=0     )

---- RecordedSimulation --------------------------------------------------------
[##########################################################################]100%
          waiting: 0      / active: 0      / done: 1     
================================================================================

Simulation org.example.RecordedSimulation completed in 10 seconds
Parsing log file(s)...
Parsing log file(s) done
Generating reports...

================================================================================
---- Global Information --------------------------------------------------------
> request count                                          6 (OK=6      KO=0     )
> min response time                                    129 (OK=129    KO=-     )
> max response time                                   1032 (OK=1032   KO=-     )
> mean response time                                   367 (OK=367    KO=-     )
> std deviation                                        301 (OK=301    KO=-     )
> response time 50th percentile                        260 (OK=260    KO=-     )
> response time 75th percentile                        265 (OK=265    KO=-     )
> response time 95th percentile                        841 (OK=841    KO=-     )
> response time 99th percentile                        994 (OK=994    KO=-     )
> mean requests/sec                                  0.545 (OK=0.545  KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                             5 ( 83%)
> 800 ms < t < 1200 ms                                   1 ( 17%)
> t > 1200 ms                                            0 (  0%)
> failed                                                 0 (  0%)
================================================================================

Reports generated in 0s.
Please open the following file: C:\Users\jayden\Downloads\gatling-project\target\gatling\recordedsimulation-20200621144216196\index.html
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  19.973 s
[INFO] Finished at: 2020-06-21T23:42:28+09:00
[INFO] ------------------------------------------------------------------------

여러 가지 정보가 출력되는데 요청에 대한 성공 실패 여부 및 처리에 얼마만큼의 시간이 걸렸는지 확인할 수 있습니다.

만약 여기서 처리가 오래 걸리는 요청이 발견된다면 웹 애플리케이션을 튜닝한 후 다시 테스트를 실행하여 개선되었는지 확인하면 됩니다.

위의 로그 내용을 자세히 보면 마지막에 결과 리포트를 HTML 파일로도 제공하는 것을 확인할 수 있습니다.

C:\Users\jayden\Downloads\gatling-project\target\gatling\recordedsimulation-20200621144216196\index.html

해당 위치의 html을 열면 다음과 같은 형태의 결과 리포트를 확인할 수 있습니다. 결과 리포트는 계속 축적되어 쌓이므로 테스트를 여러번 진행했다면 애플리케이션의 성능 변화를 비교할 수 있습니다.

다음 실습에서는 직접 scala로 테스트 코드를 작성하는 방법에 대해 알아보겠습니다. 그리고 어떤 방식으로 부하를 주입하여 테스트할 수 있는지도 같이 살펴보겠습니다.

연재글 이동
[다음글] Gatling을 이용한 웹 애플리케이션 부하 테스트(2) – Scala로 테스트 코드 작성하기