k6 vs JMeter, 어느 성능 테스트 도구를 써야 할까?

Tate 김용태·2023년 4월 24일
6
post-thumbnail

안녕하세요. 유니크굿컴퍼니의 백엔드 개발자 용태입니다. 지난번에 제가 작성했던 <온보딩 과정을 돌아보다> 글에 이어서, 이번에는 <성능 테스트 도구 비교>를 주제로 만나 뵙게 되어 반갑습니다. 이 글에서는 대표적인 서버 성능 테스트 도구인 k6JMeter를 비교하며 어느 도구를 사용할지 고민하고 결정했던 과정을 공유합니다.

서버 성능 테스트는 왜 해야 할까?

최근 저희 팀은 리얼월드 서버의 성능을 향상시키기 위해 많은 부분을 변경하는 프로젝트를 진행했습니다. 저는 그 과정에서 저희가 한 작업이 서버의 성능을 얼마나 향상시켰는지 궁금해졌습니다. 제가 측정하고 싶었던 점은 기본적으로 얼마나 요청과 응답 사이의 시간이 짧아졌는지, 또한 이용자가 많이 몰리는 특수한 상황에서 서버가 얼마나 잘 대처하여 오류를 최소화하는지 여부였습니다. 이런 질문들에 대답을 얻을 수 있다면, 저희는 우리가 한 작업의 효용을 정량화할 수 있으며, 더욱 다양한 상황에서 서버를 신뢰할 수 있게 됩니다.

Realworld, Everybody’s Playground

Realworld, Everyone’s Playground

한편 이러한 테스트를 위해서 사람이 직접 요청을 보내고 시간을 재서 결과를 얻을 수도 있지만, 시간이 오래 걸리고 요청을 보내는 자원이 많이 필요하다는 단점이 있습니다. 그래서 k6나 JMeter 같은 성능 테스트 도구를 사용하여 가상 사용자를 생성하고 요청을 보내게 하면, 성능을 더 효율적으로 측정할 수 있습니다. 성능 테스트는 부하 테스트를 비롯하여 여러 가지 종류가 있으며, 이러한 테스트 도구들을 사용해서 각 종류의 테스트 결과를 더 편리하게 얻을 수도 있습니다. 그럼 이제 JMeter부터 k6까지 서로의 장점을 비교하며 무엇을 사용할지 고민해보겠습니다.

JMeter, 전통적이고 GUI가 있는 성능 테스트 도구

JMeter는 아파치 재단에서 Java로 구축한 오픈소스 성능 테스트 도구입니다. 1998년에 처음 출시되었고, 20년이 넘는 시간을 지나 지금까지도 널리 사용되고 있습니다. JMeter도 k6를 비롯한 다른 테스트 도구들처럼 여러 가지 장점이 많지만, k6와의 대조적인 비교를 위해 상대적 장점을 위주로 서술하겠습니다. JMeter를 더 자세히 알고 싶으시다면 저와 같은 팀의 강현우 개발자께서 작성하신 <JMeter, 부하 테스트!> 글을 확인해 주세요.

JMeter

JMeter

GUI가 있다

여러분이 직접 스크립트 코드를 읽고 쓰는 데 어려움을 겪고 있다면, k6보다는 JMeter 사용을 권유 드립니다. JMeter의 GUI는 시각적으로 기능을 배치하기 때문에 처음 배울 때 더 쉽고, CLI에 익숙하지 않은 사람들에게 친숙하게 느껴집니다. k6도 k6 Test Builder라는 GUI가 있기는 하지만, JMeter처럼 모든 기능을 쓸 수는 없으며 기본적으로 스크립트 코드를 사용해야 하므로 온전한 GUI를 지원한다고 할 수 없습니다.

JMeter GUI

JMeter GUI

풍부한 문서 자료와 커뮤니티가 있다

JMeter는 1998년도에 출시된 오래된 도구입니다. 그렇기 때문에 20년 이상 발전해오면서 무수한 사용 사례를 거쳐왔습니다. 아파치 공식 문서도 좋지만, 많은 사용 경험을 가진 개발자들이 작성한 훌륭한 자습서나 책도 많습니다. 이러한 장점은 JMeter를 처음 사용하기 위한 어려움을 상당히 줄여주고 자료를 쉽게 찾을 수 있게 해줍니다.

JMeter books searched from amazon.com

https://www.amazon.com/s?k=jmeter&i=stripbooks-intl-ship

분산 부하 테스트 기능을 기본적으로 제공한다

분산 부하 테스트란 테스트에 사용하는 가상 사용자의 수를 늘리고 다른 부하 생성기를 사용하여 다중 인스턴스를 작동시키는 것을 말합니다. JMeter는 컨트롤러 노드와 워커 노드들을 분리하여 이러한 분산 부하 테스트를 수행할 수 있습니다. 반면 k6는 기본적으로 이 기능을 제공하지는 않지만, 쿠버네티스k6 Operator를 비롯한 외부 서비스를 연결하여 사용할 수는 있습니다.

Apache JMeter Distributed Testing Step-by-step

https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.html

k6, 개발자를 위한 효율적인 성능 테스트 도구

k6는 Grafana Labs가 운영하는 오픈소스 성능 테스트 도구로, 비교적 최근인 2017년에 출시되었습니다. 그런데도 K6는 뛰어난 성능과 개발 편의성을 발판으로 빠르게 점유율을 높여 나가고 있습니다. 그러면 이제 JMeter에 대한 k6의 상대적 장점을 서술하겠습니다.

k6

k6

스크립팅이 편리하다

k6 스크립트는 자바스크립트로 작성됩니다. 간단한 코드 에디터만 있다면 바로 테스트가 가능하다는 뜻이죠. 또한 JMeter의 스크립트는 XML로 작성되어 있어 가독성 측면에서 자바스크립트를 따라오지 못합니다. 직접 비교해보세요. 먼저 아래는 k6 자바스크립트 코드의 한 예시입니다.

import http from 'k6/http';
import { check } from 'k6';
export default function () {
    const response = http.get('<https://www.google.com>');
    check(response, {
        'response status 200' : (r) => r.status === 200
    });
}

k6 javascript example

다음은 위와 동일한 작업을 수행하는 JMeter XML 예시입니다.


<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.4.2">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <stringProp name="LoopController.loops">1</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
            <collectionProp name="Arguments.arguments"/>
          </elementProp>
          <stringProp name="HTTPSampler.domain">www.google.com</stringProp>
          <stringProp name="HTTPSampler.port">80</stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path"></stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree/>
        <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Assertion" enabled="true">
          <collectionProp name="Asserion.test_strings">
            <stringProp name="51508">400</stringProp>
          </collectionProp>
          <stringProp name="Assertion.custom_message"></stringProp>
          <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
          <boolProp name="Assertion.assume_success">false</boolProp>
          <intProp name="Assertion.test_type">1</intProp>
        </ResponseAssertion>
        <hashTree/>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

JMeter XML example

메모리를 적게 사용한다

3개의 요청을 보내는 간단한 테스트를 기준으로, JMeter는 600MB를 사용합니다. 반면에 k6는 동일한 조건에서 100MB 정도를 사용하죠. 이 장점은 k6가 더 많은 가상 사용자를 생성하고 더 많은 부하를 가할 수 있게 만들어줍니다. 그 결과, 더 적은 예산으로 성능 테스트를 가능하게 만듭니다. 비록 k6는 분산 부하 테스트가 불가능하지만, 그것이 없어도 k6는 효율적으로 CPU를 사용하기 때문에 단일 인스턴스만으로 초당 최대 300000 요청을 보낼 수 있습니다.

Memory consumption of k6 and JMeter

Memory consumption of k6 and JMeter

GUI가 없다?

k6는 JMeter과 다르게 GUI가 없습니다. 그러나 때로는 이것이 장점이 되기도 합니다. k6는 GUI가 없기 때문에 성능 테스트 도중 리소스 오버헤드가 일어나지 않습니다. 실질적인 성능 테스트에 있어서 GUI는 부가적인 요소이기 때문이며, JMeter 역시 이 점을 인정하고 성능 테스트를 할 때 GUI를 사용하는 것을 권장하지 않고 있습니다.

Don’t use GUI mode for load testing! message on the startup of JMeter

Don’t use GUI mode for load testing! message on the startup of JMeter

그래서 제가 선택한 성능 테스트 도구는요…

k6입니다. 그러나 이 선택이 k6가 JMeter보다 좋은 성능 테스트 도구라는 것을 의미하지는 않습니다. k6와 JMeter 모두 적합한 사용 환경이 서로 다르고, 제가 k6를 사용하기 적절한 상황에 있었을 뿐입니다. 대개 JMeter는 전통적인 테스트 팀이나 GUI 기반 테스트 도구를 원하는 개발자에게 유용합니다. 반대로 k6는 가볍고 기본 기능이 풍부한 성능 테스트 도구를 찾거나 메모리와 예산을 효율적으로 사용하고 싶은 개발자에게 유용합니다. 무엇보다 성능 테스트를 하면서 도구보다 중요한 것은 테스트의 목적과 요구 사항에 대한 이해, 그리고 충분한 협업과 의사소통일 것입니다.

k6 vs JMeter

지금까지 성능 테스트 도구의 비교 과정을 돌아본 결과, 저는 k6를 이용한 성능 테스트를 구성하기로 마음먹었습니다. 도구를 골랐으니 이제 실제로 테스트 단계로 돌입해야겠죠. 다음에는 k6를 이용한 테스트 준비 과정과 그 방법에 관한 글로 찾아뵙겠습니다. 감사합니다.

작성자의 다른 글도 읽어보고 싶으신가요? 네!

k6를 비롯한 다양한 기술을 사용하여 함께 리얼월드를 만들어 갈 당신을 기다리고 있습니다! 자세히 알아보기

참고자료: https://k6.io/blog/k6-vs-jmeter

profile
도전하는 뇌공학도 태잇 김용태입니다. 뇌공학은 어떤 미래를 만들 수 있을까요?

2개의 댓글

comment-user-thumbnail
2023년 9월 13일

좋은 글 감사합니다.

답글 달기
comment-user-thumbnail
2024년 10월 11일

성능테스트 앞두고 JMeter랑 k3중에 고민중이었는데 도움이 됐습니다. 팔로우 신청 했어요!

답글 달기