<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Piotr Przybyl - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Piotr Przybyl - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/kr/search-labs/author/piotr-przybyl</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/author/piotr-przybyl</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/author/piotr-przybyl.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Tue, 22 Sep 2026 16:46:53 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch 샤드와 복제본: 실용적인 가이드]]></title>
    <description><![CDATA[Elasticsearch 샤드와 복제본의 개념을 마스터하고 이를 최적화하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>Elasticsearch는 확장성과 내결함성 문제를 해결하는 분산 시스템을 그 위에 구축하여 Lucene의 성능을 향상시킵니다. 또한 JSON 기반 REST API를 노출하여 다른 시스템과의 상호 운용성이 매우 간단합니다.</p><p>Elasticsearch와 같은 분산 시스템은 매우 복잡할 수 있으며, 성능과 안정성에 영향을 미칠 수 있는 많은 요인들이 존재합니다. <strong>샤드는</strong> Elasticsearch에서 가장 기본적인 개념 중 하나이며, 샤드의 작동 원리를 이해하면 Elasticsearch 클러스터를 효과적으로 관리할 수 있습니다.</p><p>이 문서에서는 기본 샤드와 복제 샤드가 무엇이며, Elasticsearch 클러스터에 미치는 영향과 다양한 요구 사항에 맞게 샤드를 조정할 수 있는 도구가 무엇인지 설명합니다.</p><h2>샤드 이해</h2><p>Elasticsearch 인덱스의 데이터는 엄청난 비율로 증가할 수 있습니다. 관리하기 쉽도록 모든 데이터는 인덱스에 보관되며, 인덱스는 여러 개의 <strong>샤드로</strong> 분할된 인덱스입니다. 각 Elasticsearch 샤드는 Apache Lucene 인덱스이며, 각 개별 Lucene 인덱스는 Elasticsearch 인덱스에 있는 문서의 하위 집합을 포함합니다. 이러한 방식으로 인덱스를 분할하면 리소스 사용량을 제어할 수 있습니다. Apache Lucene 인덱스의 문서 수 제한은 2,147,483,519개(2³¹ - 129개)입니다.</p><p>때로는 리밸런싱을 위해 노드 간에 인덱스를 이동해야 하는 경우가 있습니다. 이 프로세스는 시간과 리소스가 많이 소요될 수 있으므로 인덱스가 너무 커지지 않아야 복구 시간을 관리할 수 있습니다. 또한 인덱스는 지속적으로 병합해야 하는 Lucene 세그먼트로 구성되므로 세그먼트가 너무 커지지 않도록 하는 것이 중요합니다. 이러한 이유로 Elasticsearch는 인덱스 데이터를 <strong>기본 샤드라고</strong> 하는 관리하기 쉬운 작은 청크로 분할하여 여러 머신에 더 쉽게 분산할 수 있습니다. <strong>복제</strong> 샤드는 단순히 해당 기본 샤드의 정확한 복사본이며, 이 글의 뒷부분에서 복제 샤드의 기능을 살펴보겠습니다.</p><p>적절한 수의 샤드를 보유하는 것은 성능에 중요합니다. 따라서 미리 계획을 세우는 것이 현명합니다. 쿼리가 여러 샤드에서 병렬로 실행되는 경우, 단일 샤드로 구성된 인덱스보다 빠르게 실행되지만 각 샤드가 다른 노드에 있고 클러스터에 충분한 노드가 있는 경우에만 실행됩니다. 그러나 동시에 샤드는 인덱싱된 데이터와 클러스터 메타데이터 측면에서 메모리와 디스크 공간을 소비합니다. 샤드가 너무 많으면(오버샤딩이라고도 함) 쿼리, 인덱싱 요청 및 관리 작업 속도가 느려질 수 있으므로 적절한 균형을 유지하는 것이 중요합니다.</p><p>기본 샤드 수는 <strong>특정 인덱스 인스턴스에 대한</strong> 인덱스 생성 시 정의됩니다. 나중에 다른 수의 기본 샤드가 필요한 경우<strong> 크기 조정</strong><a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-split">API인</a> 분할(기본 샤드 수 증가), <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-shrink">축소</a> (기본 샤드 수 감소) 또는 <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-indices-clone">복제</a> (복제본에 대한 새로운 설정으로 동일한 수의 기본 샤드)를 사용할 수 있습니다. 이러한 작업은 Lucene 세그먼트를 복사하고 <strong>모든 문서의 전체 재색인을 피하며</strong>, 인덱스를 만들 때 인덱스의 설정으로 기본 및 복제 샤드 수를 설정할 수 있습니다:</p>PUT /sensor
{
   "settings" : {
       "index" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 2
       }
   }
}<p>(샤드 또는 복제본 수를 지정하지 않으면 Elasticsearch 7.0 기준 기본값은 둘 다 1입니다). 인덱스의 데이터 양에 따라 이상적인 샤드 수를 결정해야 합니다. 일반적으로<a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards">최적의 샤드에는 10~50GB의 데이터가 저장되어야 하며,</a> 샤드당 문서 수는 2억 개 미만이어야 합니다. 예를 들어, 하루에 약 300GB의 애플리케이션 로그가 누적될 것으로 예상되는 경우, 이를 호스팅할 충분한 양의 노드가 있다면 해당 인덱스에 약 10개의 샤드를 보유하는 것이 합리적입니다.</p><p>파편은 수명이 다하는 동안 다음과 같은 여러 상태를 거칠 수 있습니다:</p><ul><li><p><strong>초기화 중입니다:</strong> 샤드를 사용하기 전 초기 상태입니다.</p></li><li><p><strong>시작됨:</strong> 샤드가 활성화되어 요청을 받을 수 있는 상태입니다.</p></li><li><p><strong>재배치 중:</strong> 샤드가 다른 노드로 이동하는 중일 때 발생하는 상태입니다. 이는 특정 조건에서 필요할 수 있는데, 예를 들어 해당 노드의 디스크 공간이 부족한 경우입니다.</p></li><li><p><strong>할당되지 않음:</strong> 할당되지 않음: 할당되지 않은 샤드의 상태입니다. 예를 들어, 샤드를 호스팅하는 노드가 더 이상 클러스터에 속하지 않거나 <em>(NODE_LEFT)</em> 폐쇄 인덱스로 복원하는 경우 <em>(EXISTING_INDEX_RESTORED)</em>와 같이 이런 상황이 발생하면 사유가 제공됩니다.</p></li></ul><p>모든 샤드, 샤드의 상태 및 기타 메타데이터를 보려면 다음 요청을 사용하면 됩니다:</p>GET _cat/shards<p>특정 인덱스의 샤드를 보려면 URL에 인덱스 이름(예: sensor)을 추가하면 됩니다:</p>GET _cat/shards/sensor<p>이 명령은 다음 예제와 같은 출력을 생성합니다. 기본적으로 표시되는 열에는 인덱스의 이름, 이름(예 번호), 기본 샤드인지 복제본인지 여부, 샤드의 상태, 문서 수, 디스크의 크기, 샤드가 위치한 노드의 IP 주소와 노드 ID를 확인할 수 있습니다.</p>sensor 5 p STARTED    0  283b 127.0.0.1 ziap
sensor 5 r UNASSIGNED                  
sensor 2 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 2 r UNASSIGNED                  
sensor 3 p STARTED    3 7.2kb 127.0.0.1 ziap
sensor 3 r UNASSIGNED                  
sensor 1 p STARTED    1 3.7kb 127.0.0.1 ziap
sensor 1 r UNASSIGNED                  
sensor 4 p STARTED    2 3.8kb 127.0.0.1 ziap
sensor 4 r UNASSIGNED                  
sensor 0 p STARTED    0  283b 127.0.0.1 ziap
sensor 0 r UNASSIGNED<h2>복제본 이해</h2><p>각 샤드에는 데이터의 단일 사본이 포함되지만 인덱스에는 여러 개의 샤드 사본이 포함될 수 있습니다. 따라서 샤드에는 <strong>기본 샤드와</strong> <strong>복제본</strong>, 즉 복제본의 두 가지 유형이 있습니다. 기본 샤드의 각 복제본은 항상 다른 노드에 위치하므로 노드 장애 발생 시 데이터의 고가용성을 보장합니다. 복제본은 중복성과 데이터 손실 및 다운타임을 방지하는 역할 외에도 쿼리를 기본 샤드와 병렬로 처리하여 더 빠르게 처리함으로써 검색 성능을 향상시키는 데 도움이 될 수 있습니다.</p><p>기본 샤드와 복제본 샤드의 작동 방식에는 몇 가지 중요한 차이점이 있습니다. 둘 다 쿼리를 처리할 수 있지만 인덱싱 요청(예 인덱스에 데이터 추가)는 복제 샤드에 복제되기 전에 먼저 기본 샤드를 거쳐야 합니다. 위에서 언급했듯이 노드 연결이 끊기거나 하드웨어 장애 등으로 인해 기본 샤드를 사용할 수 없게 되면 복제본이 그 역할을 대신하도록 승격됩니다.</p><p>복제본은 노드 장애 발생 시 도움이 될 수 있지만, 인덱싱할 때 메모리, 디스크 공간, 컴퓨팅 성능을 소모하므로 복제본을 너무 많이 보유하지 않는 것이 중요합니다. 기본 샤드와 복제본의 또 다른 차이점은 인덱스가 생성된 후에는 기본 샤드의 수를 변경할 수 없지만, 복제본의 수는 인덱스 설정을 업데이트하여 언제든지 동적으로 변경할 수 있다는 점입니다.</p><p>복제본에서 고려해야 할 또 다른 요소는 사용 가능한 노드 수입니다. 복제본은 항상 기본 샤드와 다른 노드에 배치되는데, 동일한 노드에 동일한 데이터의 복사본이 두 개 있으면 노드에 장애가 발생할 경우 아무런 보호 기능을 제공하지 못하기 때문입니다. 따라서 시스템에서 <em>n개의</em> 복제본을 지원하려면 클러스터에 최소 <em>n + 1개의</em> 노드가 있어야 합니다. 예를 들어 클러스터에 노드가 2개 있고 인덱스가 6개의 복제본으로 구성된 경우, 복제본은 1개만 할당됩니다. 반면, 7개의 노드가 있는 시스템은 하나의 기본 샤드와 6개의 복제본을 완벽하게 처리할 수 있습니다.</p><h2>샤드 및 복제본 최적화</h2><p>기본 샤드와 복제본 샤드의 균형이 적절한 인덱스가 생성된 후에도 시간이 지남에 따라 인덱스 주변의 역학 관계가 변하기 때문에 이를 모니터링해야 합니다. 예를 들어 시계열 데이터를 다룰 때는 일반적으로 최근 데이터가 있는 인덱스가 오래된 데이터보다 더 활성화되어 있습니다. 이러한 인덱스를 조정하지 않으면 요구 사항이 매우 다름에도 불구하고 모두 동일한 양의 리소스를 소비하게 됩니다.</p><p>롤오버 인덱스 API는 최신 인덱스와 이전 인덱스를 분리하는 데 사용할 수 있습니다. 특정 임계값(디스크의 인덱스 크기, 문서 수 또는 기간 등)에 도달하면 자동으로 새 인덱스를 생성하도록 설정할 수 있습니다. 이 API는 샤드 크기를 제어하는 데도 유용합니다. 인덱스 생성 후에는 샤드 수를 쉽게 변경할 수 없기 때문에 롤오버 조건이 충족되지 않으면 샤드는 계속해서 데이터를 축적합니다. 자주 액세스하지 않는 오래된 인덱스의 경우, 인덱스 축소와 강제 병합은 메모리와 디스크 공간을 줄이는 두 가지 방법입니다. 전자는 인덱스의 샤드 수를 줄이고, 후자는 Lucene 세그먼트 수를 줄이며 삭제된 문서가 사용하는 공간을 확보합니다.</p><h2>기본 샤드와 복제본 샤드를 Elasticsearch의 기반으로 사용</h2><p>Elasticsearch는 방대한 양의 데이터를 위한 분산 저장, 검색 및 분석 플랫폼으로서 강력한 명성을 쌓아왔습니다. 그러나 이러한 규모로 운영할 때는 필연적으로 문제가 발생할 수밖에 없습니다. 따라서 기본 샤드와 복제본 샤드의 작동 방식을 이해하는 것은 플랫폼의 안정성과 성능을 최적화하는 데 도움이 될 수 있기 때문에 Elasticsearch에서 매우 중요하고 기본이 되는 이유입니다.</p><p>작동 방식과 최적화 방법을 아는 것은 보다 강력하고 성능이 우수한 Elasticsearch 클러스터를 달성하는 데 매우 중요합니다. 쿼리 응답이 느리거나 서비스 중단이 자주 발생하는 경우 이 지식이 이러한 장애물을 극복하는 열쇠가 될 수 있습니다.</p><p><a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/clusters-nodes-shards">클러스터, 노드 및</a> <a href="https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/size-shards"></a>샤드, <a href="https://www.elastic.co/docs/deploy-manage/distributed-architecture/shard-allocation-relocation-recovery">샤드 크기 조정 방법, 샤드 할당 및 복구에</a> 대해 자세히 알아보려면 Elasticsearch의 공식 설명서를 참조하세요.</p><p>이 주제는 <a href="https://youtu.be/sAySPSyL2qE">Elastic 커뮤니티 YouTube 채널에서</a>입문 과정으로도 제공됩니다.</p><p>마지막으로, 노드, 샤드 또는 복제본에 대해 걱정하고 싶지 않으시다면 <a href="https://www.elastic.co/docs/deploy-manage/deploy/elastic-cloud/serverless">Elastic Cloud Serverless를</a> 사용해 보세요. 이 Elastic Cloud 제품은 Elastic에서 완전히 관리하며 워크로드에 따라 확장할 수 있도록 자동화되어 있습니다. 무료 평가판을 통해 서버리스 접근 방식의 다른 이점에 대해 알아볼 수 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-shards-and-replicas-guide</guid>
    <category><![CDATA[기본]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71dd92d939d383a2/6a17e9d53e03d769c44f2cc7/7775c44f01f2516c4ff4cce6d6bbe9e7b2c38908-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 14 Aug 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[모의 및 실제 Elasticsearch로 Java 코드 테스트하기]]></title>
    <description><![CDATA[모의 테스트와 Testcontainer를 사용하여 Elasticsearch를 위한 자동화된 테스트를 작성하는 방법을 알아보세요.]]></description>
    <content:encoded><![CDATA[<p>이 포스팅에서는 외부 시스템 종속성으로 Elasticsearch를 사용하여 소프트웨어를 테스트하는 두 가지 방법을 소개하고 설명합니다. 모의 테스트와 통합 테스트를 사용하여 테스트하고, 두 테스트 간의 실질적인 차이점을 보여주고, 각 스타일에 대한 몇 가지 힌트를 제공합니다.</p><h2>시스템 신뢰도를 위한 좋은 테스트</h2><p>좋은 테스트는 IT 시스템을 만들고 유지 관리하는 과정에 참여하는 모든 사람의 자신감을 높여주는 테스트입니다. 테스트는 멋지거나 빠르거나 인위적으로 코드 커버리지를 늘리기 위한 것이 아닙니다. 테스트는 이를 보장하는 데 중요한 역할을 합니다:</p><ul><li><p>우리가 제공하고자 하는 것은 프로덕션에서 작동할 것입니다.</p></li><li><p>시스템은 요구 사항과 계약을 충족합니다.</p></li><li><p>앞으로는 퇴행이 없을 것입니다.</p></li><li><p>개발자(및 기타 관련 팀원)는 자신이 만든 콘텐츠가 제대로 작동할 것이라고 확신합니다.</p></li></ul><p>물론 그렇다고 해서 테스트가 멋지거나 빠르거나 코드 커버리지를 늘릴 수 없다는 의미는 아닙니다. 테스트 스위트를 더 빨리 실행할수록 좋습니다. 다만 테스트 스위트의 전체 기간을 단축하기 위해 자동화된 테스트가 제공하는 신뢰성, 유지보수성, 자신감을 희생해서는 안 된다는 것입니다.</p><p>좋은 자동화된 테스트는 다양한 팀원들의 자신감을 높여줍니다:</p><ul><li><p>개발자: 개발자는 작업 중인 코드가 컴퓨터를 떠나기 전에도 자신이 하고 있는 일이 제대로 작동하는지 확인할 수 있습니다.</p></li><li><p>품질 보증 팀: 수동으로 테스트할 일이 줄어듭니다.</p></li><li><p>시스템 운영자 및 SRE: 시스템 배포 및 유지 관리가 더 쉬워지므로 더 편안합니다.</p></li></ul><p>마지막으로 중요한 것은 시스템의 아키텍처입니다. 시스템이 체계적이고 유지 관리가 쉬우며 아키텍처가 깔끔하고 목적에 부합할 때 저희는 이를 좋아합니다. 그러나 때때로 우리는 "이 방법이 더 테스트하기 쉽다는 핑계로 너무 많은 것을 희생하는 아키텍처를 볼 수 있습니다". 시스템이 그 존재를 정당화하는 필요를 충족시키는 대신 주로 테스트 가능하도록 작성된 경우, 꼬리가 개를 흔드는 상황을 보게 됩니다.</p><h2>두 가지 종류의 테스트: 모의 &amp; 종속성</h2><p>테스트는 여러 가지 방법으로 볼 수 있으며, 따라서 분류할 수도 있습니다. 이 글에서는 테스트 분할의 한 가지 측면, 즉 모의(또는 스텁, 가짜 등)를 사용하는 것과 실제 종속성을 사용하는 것에 대해서만 집중적으로 살펴보겠습니다. 저희의 경우 종속성은 Elasticsearch입니다.</p><p>모의 테스트를 사용하는 테스트는 외부 종속성을 시작할 필요가 없고 모든 것이 메모리 내에서만 이루어지기 때문에 매우 빠릅니다. 자동화된 테스트에서 모킹은 실제 종속성을 사용하지 않고 프로그램의 일부를 테스트하기 위해 실제 객체 대신 가짜 객체를 사용하는 것을 말합니다. 이것이 바로 이러한 기능이 필요한 이유이며, 예를 들어 빠른 탐지 네트워크 테스트에서 빛을 발하는 이유입니다. 입력 유효성 검사. 예를 들어 요청에 음수가 허용되지 않는지 확인하기 위해서만 데이터베이스를 시작하고 호출할 필요가 없습니다.</p><p>하지만 모의고사를 도입하는 데에는 몇 가지 의미가 있습니다:</p><ul><li><p>모든 것을, 모든 시간을 쉽게 모킹할 수 있는 것은 아니므로 모킹은 시스템 아키텍처에 영향을 미칩니다(때로는 훌륭하지만 때로는 그렇지 않을 수도 있습니다).</p></li><li><p>모의 테스트에서 실행되는 테스트는 빠를 수 있지만, 모방하는 시스템을 깊이 반영한 모의 테스트는 일반적으로 무료로 제공되지 않기 때문에 이러한 테스트를 개발하는 데 상당한 시간이 걸릴 수 있습니다. 시스템이 어떻게 작동하는지 아는 사람이 적절한 방식으로 모의고사를 작성해야 하며, 이러한 지식은 실무 경험, 문서 공부 등을 통해 얻을 수 있습니다.</p></li><li><p>모의고사를 유지 관리해야 합니다. 시스템이 외부 종속성에 의존하고 있고 이 종속성을 업그레이드해야 하는 경우, 누군가는 종속성을 모방한 모의 프로젝트도 모든 변경 사항(문서화 및 미문서화)이 업데이트되도록 해야 합니다(시스템에도 영향을 미칠 수 있음). 종속성을 업그레이드하고 싶지만 (모의 테스트만 사용하는) 테스트 스위트로는 테스트한 모든 케이스가 작동한다는 확신을 줄 수 없을 때 특히 문제가 됩니다.</p></li><li><p>모의 테스트가 아닌 시스템 개발과 테스트에 집중할 수 있도록 절제된 노력이 필요합니다.</p></li></ul><p>이러한 이유로 많은 사람들이 모의(또는 스텁 등)를 사용하지 않고 실제 종속성에만 의존하는 정반대 방향을 옹호합니다. 이 접근 방식은 데모 또는 시스템의 규모가 작고 커버리지가 큰 테스트 케이스가 몇 개만 있는 경우에 매우 효과적입니다. 이러한 테스트는 통합 테스트(대략적으로 말하면 일부 실제 종속성에 대해 시스템의 일부를 확인하는 것) 또는 엔드투엔드 테스트(모든 실제 종속성을 동시에 사용하고 시스템이 사용 가능하고 성공적인 것으로 정의하는 사용자 워크플로우를 재생하면서 모든 끝에서 시스템의 동작을 확인하는 것)일 수 있습니다. 이 접근 방식을 사용하면 종속성에 대한 가정과 이를 작업 중인 시스템과 통합하는 방법을 (종종 의도치 않게) 검증할 수 있다는 분명한 이점이 있습니다.</p><p>그러나 테스트에서 실제 종속성만 사용하는 경우에는 다음과 같은 측면을 고려해야 합니다:</p><ul><li><p>일부 테스트 시나리오에서는 실제 종속성이 필요하지 않습니다(예: 요청의 정적 불변성을 확인하는 경우).</p></li><li><p>피드백을 기다리는 데 너무 많은 시간이 걸리기 때문에 이러한 테스트는 일반적으로 개발자의 컴퓨터에서 전체 제품군으로 실행되지 않습니다.</p></li><li><p>CI 머신에서 더 많은 리소스가 필요하며, 시간 낭비를 방지하기 위해 &amp; 리소스를 조정하는 데 더 많은 시간이 소요될 수 있습니다.</p></li><li><p>테스트 데이터로 종속성을 초기화하는 것은 간단하지 않을 수 있습니다.</p></li><li><p>실제 종속성이 있는 테스트는 주요 리팩토링, 마이그레이션 또는 종속성 업그레이드 전에 코드를 묶는 데 유용합니다.</p></li><li><p>테스트 대상 시스템의 내부에 대해 자세히 설명하지 않고 결과만 처리하는 등 불투명한 테스트일 가능성이 높습니다.</p></li></ul><h2>최적의 지점: 두 가지 테스트 모두 사용</h2><p>한 가지 유형의 테스트만 사용하여 시스템을 테스트하는 대신, 두 가지 유형 모두에 의존하여 두 가지 유형 모두의 사용법을 개선할 수 있습니다.</p><ul><li><p>모의 기반 테스트는 훨씬 빠르므로 먼저 실행하고, 모두 성공하면 그 후에야 느린 종속성 테스트를 실행하세요.</p></li><li><p>외부 종속성이 실제로 필요하지 않은 시나리오에서는 모의 작업을 선택하고, 모의 작업에만 너무 많은 시간이 소요되어 코드를 대규모로 변경해야 하는 경우에는 외부 종속성에 의존하세요.</p></li><li><p>두 가지 접근 방식을 모두 사용하여 코드를 테스트하는 것이 타당하다면 잘못된 것은 없습니다.</p></li></ul><h2>SystemUnderTest의 예</h2><p>다음 섹션에서는 <a href="https://github.com/pioorg/testing-elasticsearch">여기에서</a> 찾을 수 있는 예제를 사용하겠습니다. Java 21로 작성된 작은 데모 애플리케이션으로, 빌드 도구로 Maven을 사용하고, Elasticsearch 클라이언트에 의존하며, Elasticsearch의 최신 추가 기능인 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL</a> (Elastic의 새로운 절차적 쿼리 언어)을 사용하고 있습니다. Java가 프로그래밍 언어가 아니더라도 아래에서 설명할 개념을 이해하고 이를 스택에 적용하는 데 문제가 없을 것입니다. 실제 코드 예제를 사용하면 특정 사항을 더 쉽게 설명할 수 있습니다.</p><p><code>BookSearcher</code> 은 데이터를 검색하고 분석하는 데 도움이 되며, 우리의 경우에는 책이 됩니다( <a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">이전 게시물 중 하나에서</a> 설명한 것처럼).</p><ul><li><p>예를 들어, 코드가 이전 버전과 호환되는지 확실하지 않고 이전 버전과 호환되지 않는지 확실하지 않기 때문에 유일한 종속성( <code>isCompatibleWithBackend()</code> 참조)으로서 정확히 버전 <code>8.15.x</code> 의 Elasticsearch가 필요합니다. 프로덕션 환경의 Elasticsearch를 최신 버전으로 업그레이드하기 전에 먼저 테스트 대상 시스템의 동작이 동일하게 유지되는지 확인하기 위해 테스트에서 범프를 실행합니다.</p></li><li><p>이를 사용하여 특정 연도에 출판된 도서의 수를 검색할 수 있습니다( <code>numberOfBooksPublishedInYear</code> 참조).</p></li><li><p>데이터 집합을 분석하여 특정 연도 사이에 가장 많이 게시된 20명의 저자를 찾아야 할 때도 이 기능을 사용할 수 있습니다( <code>mostPublishedAuthorsInYears</code> 참조).</p></li></ul>public class BookSearcher {

    private final ElasticsearchClient esClient;

    public BookSearcher(ElasticsearchClient esClient) {
        this.esClient = esClient;
        if (!isCompatibleWithBackend()) {
            throw new UnsupportedOperationException("This is not compatible with backend");
        }
    }

    private boolean isCompatibleWithBackend() {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            show info
            | keep version
            | dissect version "%{major}.%{minor}.%{patch}"
            | keep major, minor
            | limit 1""")) {
            if (!rs.next()) {
                throw new RuntimeException("No version found");
            }
            return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int numberOfBooksPublishedInYear(int year) {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            from books
            | where year == ?
            | stats published = count(*) by year
            | limit 1000""", year)) {

            if (rs.next()) {
                return rs.getInt("published");
            }
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }


    public List&lt;MostPublished&gt; mostPublishedAuthorsInYears(int minYear, int maxYear) {
        assert minYear &lt;= maxYear;
        String query = """
            from books
            | where year &gt;= ? and year &lt;= ?
            | stats first_published = min(year), last_published = max(year), times = count (*) by author
            | eval years_published = last_published - first_published
            | sort years_published desc
            | drop years_published
            | limit 20
            """;

        try {
            Iterable&lt;MostPublished&gt; published = esClient.esql().query(
                ObjectsEsqlAdapter.of(MostPublished.class),
                query,
                minYear,
                maxYear);

            List&lt;MostPublished&gt; mostPublishedAuthors = new ArrayList&lt;&gt;();
            for (MostPublished mostPublished : published) {
                mostPublishedAuthors.add(mostPublished);
            }
            return mostPublishedAuthors;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public record MostPublished(
        String author,
        @JsonProperty("first_published") int firstPublished,
        @JsonProperty("last_published") int lastPublished,
        int times
    ) {
        public MostPublished {
            assert author != null;
            assert firstPublished &lt;= lastPublished;
            assert times &gt; 0;
        }
    }
}
<h2>모의 테스트를 통해 시작하기</h2><p>테스트에 사용되는 모형을 만들기 위해 Java 에코시스템에서 매우 인기 있는 모킹 라이브러리인 <a href="https://site.mockito.org/">Mockito를</a> 사용하겠습니다.</p><p>각 테스트 전에 모의고사를 초기화하기 위해 다음과 같이 시작할 수 있습니다:</p>public class BookSearcherMockingTest {

    ResultSet mockResultSet;
    ElasticsearchClient esClient;
    ElasticsearchEsqlClient esql;

    @BeforeEach
    void setUpMocks() {
        mockResultSet = mock(ResultSet.class);
        esClient = mock(ElasticsearchClient.class);
        esql = mock(ElasticsearchEsqlClient.class);

    }
}
<p>앞서 말했듯이 모의 테스트를 통해 모든 것을 쉽게 테스트할 수 있는 것은 아닙니다. 하지만 우리가 할 수 있고 해야만 하는 일들도 있습니다. 지금은 <code>8.15.x</code> 버전만 지원되는지 확인해 보겠습니다(향후 시스템이 향후 버전과 호환되는지 확인되면 범위를 확장할 수 있습니다):</p>@Test
void canCreateSearcherWithES_8_15() throws SQLException, IOException{
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(15);

    // then
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(esClient));
}
<p><code>BookSearcher</code> 이 아직 <code>8.16.x</code> 과 호환될지 확실하지 않기 때문에 다른 부 버전을 반환하여 비슷한 방식으로 이 작동하지 않는다는 것을 확인할 수 있습니다:</p>@Test
void cannotCreateSearcherWithoutES_8_15() throws SQLException, IOException {
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(16);

    // then
    Assertions.assertThrows(UnsupportedOperationException.class, () -&gt; new BookSearcher(esClient));
}
<p>이제 실제 Elasticsearch를 대상으로 테스트할 때 비슷한 결과를 얻을 수 있는 방법을 살펴보겠습니다. <a href="https://java.testcontainers.org/modules/elasticsearch/">이를 위해 테스트컨테이너의 Elasticsearch 모듈을</a> 사용하려고 하는데, 이 모듈의 요구 사항은 단 한 가지, 즉 Docker에 액세스할 수 있어야 한다는 것입니다. 어떤 각도에서 보면 테스트 컨테이너는 단순히 Docker 컨테이너를 작동하는 방법이지만, Docker 데스크톱(또는 이와 유사한), CLI 또는 스크립트에서 이를 수행하는 대신 사용자가 알고 있는 프로그래밍 언어로 요구 사항을 표현할 수 있습니다. 이를 통해 이미지 가져오기, 컨테이너 시작, 테스트 후 가비지 수집, 파일 앞뒤로 복사, 명령 실행, 로그 검사 등을 테스트 코드에서 직접 수행할 수 있습니다.</p><p>스텁은 다음과 같이 보일 수 있습니다:</p>@Testcontainers
public class BookSearcherIntTest {

    static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
    static final JacksonJsonpMapper JSONP_MAPPER = new JacksonJsonpMapper();

    RestClientTransport transport;
    ElasticsearchClient client;

    @Container
    ElasticsearchContainer elasticsearch = new ElasticsearchContainer(ELASTICSEARCH_IMAGE);

    @BeforeEach
    void setupClient() {
        transport = // setup transport here
        client = new ElasticsearchClient(transport);
    }

    @AfterEach
    void closeClient() throws IOException {
        if (transport != null) {
            transport.close();
        }
    }

}
<p>이 예에서는 <code>@Testcontainers</code> 및 <code>@Container</code> 과의 <a href="https://java.testcontainers.org/test_framework_integration/junit_5/">Testcontainers의 JUnit 통합을</a> 사용하므로, 테스트 전에 Elasticsearch를 시작하고 테스트 후에 중지하는 것에 대해 걱정할 필요가 없습니다. 각 테스트 전에 클라이언트를 생성하고 각 테스트 후에 클라이언트를 닫기만 하면 됩니다(더 큰 테스트 세트에 영향을 줄 수 있는 리소스 누수를 방지하기 위해).</p><p>정적이 아닌 필드에 <code>@Container</code> 주석을 달면 각 테스트마다 새 컨테이너가 시작되므로 오래된 데이터나 컨테이너의 상태 재설정에 대해 걱정할 필요가 없습니다. 그러나 많은 테스트에서 이 접근 방식은 성능이 좋지 않을 수 있으므로 다음 게시물 중 하나에서 다른 접근 방식과 비교해보겠습니다.</p><p><strong>참고:</strong></p><code>docker.elastic.co</code> (Elastic의 공식 Docker 이미지 리포지토리)를 사용하면 Docker 허브의 한계를 초과하지 않아도 됩니다.

또한 테스트 환경과 프로덕션 환경에서 동일한 버전의 종속성을 사용하여 호환성을 최대한 보장하는 것이 좋습니다. 또한 Elasticsearch 이미지에는 <code>latest</code> 태그가 없으므로 버전을 정확하게 선택하는 것이 좋습니다.<h2>테스트에서 Elasticsearch에 연결</h2><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html">Elasticsearch Java 클라이언트는</a> 보안 및 SSL/TLS가 활성화된 상태에서도 테스트 컨테이너에서 실행 중인 Elasticsearch에 연결할 수 있습니다(버전 8.x의 기본값이므로 컨테이너 선언에 보안과 관련된 내용을 지정할 필요가 없었습니다). 프로덕션에서 사용 중인 Elasticsearch에도 TLS와 일부 보안이 활성화되어 있다고 가정하면, 통합 테스트 설정은 가능한 한 프로덕션 시나리오에 가깝게 설정하는 것이 좋으므로 테스트에서 이를 비활성화하지 않는 것이 좋습니다.</p><p>컨테이너가 필드 또는 변수에 할당되었다고 가정하여 연결에 필요한 데이터를 얻는 방법 <code>elasticsearch</code>:</p><ul><li><p><code>elasticsearch.getHost()</code> 는 컨테이너가 실행 중인 호스트를 알려줍니다(대부분의 경우 <code>"localhost"</code> 이지만 설정에 따라 다른 이름일 수도 있으므로 호스트는 항상 동적으로 가져와야 합니다.) 하드코딩하지 마세요.</p></li><li><p><code>elasticsearch.getMappedPort(9200)</code> 는 컨테이너 내부에서 실행 중인 Elasticsearch에 연결하기 위해 사용해야 하는 호스트 포트를 제공합니다(컨테이너를 시작할 때마다 외부 포트가 달라지므로 이 역시 동적 호출이어야 합니다).</p></li><li><p>덮어쓰지 않는 한 기본 사용자 아이디와 비밀번호는 각각 <code>"elastic"</code> 및 <code>"changeme"</code> 입니다.</p></li><li><p>컨테이너 설정 중에 SSL/TLS 인증서가 지정되지 않았고 보안 연결이 비활성화되지 않은 경우(버전 8.x의 기본 동작), 자체 서명된 인증서가 생성됩니다. 신뢰하려면(예 <a href="https://curl.se/docs/manpage.html#--cacert"></a>처럼) 인증서는 <code>elasticsearch.caCertAsBytes()</code> ()를 사용하여 얻을 수 <code>Optional&lt;byte[]&gt;</code> 있으며, 또 다른 편리한 방법은 를 <code>SSLContext</code> 사용하여 를 얻는 <code>createSslContextFromCa()</code> 것입니다.</p></li></ul><p>전체 결과는 다음과 같습니다:</p>BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level rest client
RestClient restClient = RestClient.builder(new HttpHost(elasticsearch.getHost(), elasticsearch.getMappedPort(9200), "https"))
    .setHttpClientConfigCallback(httpClientBuilder -&gt;
        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            .setSSLContext(elasticsearch.createSslContextFromCa())
    )
    .build();

// The RestClientTransport is mainly for serialization/deserialization
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

// The official Java API Client for Elasticsearch
ElasticsearchClient client = new ElasticsearchClient(transport);
<p><code>ElasticsearchClient</code> 인스턴스 생성의 또 다른 예는 <a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherIntTest.java#L61">데모 프로젝트에서</a> 찾을 수 있습니다.</p><p><strong>참고</strong>:</p>프로덕션 환경에서 클라이언트를 생성하려면 <a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html#_verifying_https_with_a_certificate_fingerprint">문서를</a> 참조하세요.<h2>첫 번째 통합 테스트</h2><p>첫 번째 테스트인 Elasticsearch 버전 8.15.x를 사용하여 <code>BookSearcher</code> 을 생성할 수 있는지 확인하는 테스트는 다음과 같습니다:</p>@Test
void canCreateClientWithContainerRunning_8_15() {
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(client));
}
<p>보시다시피 다른 설정은 따로 할 필요가 없습니다. 우리가 해야 할 일은 테스트컨테이너에서 시작한 실제 Elasticsearch 인스턴스에 연결된 클라이언트를 <code>BookSearcher</code> 에 제공하기만 하면 됩니다.</p><h2>통합 테스트는 내부에 대한 관심이 적습니다.</h2><p>열 인덱스를 사용하여 결과 집합에서 데이터를 추출하는 것을 중단하고 열 이름에 의존해야 한다고 가정해 보겠습니다. 따라서 방법에서 <code>isCompatibleWithBackend</code> 대신</p>return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
<p>우리가 가질 것입니다:</p>return rs.getInt("major") == 8 &amp;&amp; rs.getInt("minor") == 15;
<p>두 테스트를 다시 실행하면 실제 Elasticsearch와의 통합 테스트가 여전히 문제 없이 통과된다는 것을 알 수 있습니다. 그러나 모의 호출을 사용한 테스트는 <code>rs.getInt(String)</code> 이 아닌 <code>rs.getInt(int)</code> 과 같은 호출을 모의했기 때문에 작동이 중지되었습니다. 이제 테스트 스위트에 있는 다른 사용 사례에 따라 두 가지를 대신 모의하거나 둘 다 모의해야 통과할 수 있습니다.</p><h2>통합 테스트는 파리를 잡는 대포가 될 수 있습니다.</h2><p>통합 테스트는 외부 종속성이 필요하지 않더라도 시스템의 동작을 검증할 수 있습니다. 그러나 이러한 방식으로 사용하면 일반적으로 실행 시간과 리소스가 낭비됩니다. 방법을 살펴보자 <code>mostPublishedAuthorsInYears(int minYear, int maxYear)</code>. 처음 두 줄은 다음과 같습니다:</p>assert minYear &lt;= maxYear;
String query = // here goes the query
<p>첫 번째 문은 조건을 확인하는 것으로, 어떤 식으로든 Elasticsearch(또는 다른 외부 종속성)에 의존하지 않습니다. 따라서 <code>minYear</code> 이 <code>maxYear</code> 보다 크면 예외가 발생한다는 것을 확인하기 위해 컨테이너를 시작할 필요가 없습니다.</p><p>빠르고 리소스를 많이 사용하지 않는 간단한 모의 테스트만으로도 이를 충분히 확인할 수 있습니다. 모의 테스트를 설정한 후에는 간단히 진행할 수 있습니다:</p>BookSearcher systemUnderTest = new BookSearcher(esClient);

Assertions.assertThrows(
    AssertionError.class,
    () -&gt; systemUnderTest.mostPublishedAuthorsInYears(2012, 2000)
);
<p><a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherMockingTest.java#L89">이 테스트 케이스에서는</a> 이 종속성에 대해 의미 있는 호출을 할 가능성이 없기 때문에 모킹 대신 종속성을 시작하는 것은 낭비입니다.</p><p>그러나 <code>String query = ...</code> 로 시작하는 동작을 확인하려면 쿼리가 올바르게 작성되었는지, 클라이언트 라이브러리가 적절한 요청과 응답을 보낼 수 있는지, 구문 변경이 없으므로 통합 테스트 등을 사용하여 예상대로 결과가 나오는지 등을 확인하는 것이 훨씬 쉽습니다:</p>@BeforeEach
void setupDataInContainer() {
    // here we initialise data in the Elasticsearch running in a container
}

@Test
void shouldGiveMostPublishedAuthorsInGivenYears() {
    var systemUnderTest = new BookSearcher(client);
    var list = systemUnderTest.mostPublishedAuthorsInYears(1800, 2010);
    Assertions.assertEquals("Beatrix Potter", list.get(12).author(), "Beatrix Potter was 13th most published author between 1800 and 2010");
}
<p>이렇게 하면 데이터 형식이 변경되지 않았고 쿼리가 여전히 유효하며 모든 미들웨어(클라이언트, 드라이버, 보안 등)가 계속 작동할 것이므로 데이터를 Elasticsearch에 공급할 때(이번 버전 또는 향후 마이그레이션하기로 선택한 버전에서) 쿼리가 예상했던 것과 정확히 일치하는 결과를 제공할 것이라는 확신을 가질 수 있게 됩니다. 모형을 최신 상태로 유지하는 것에 대해 걱정할 필요가 없습니다. <code>8.15</code> 이 바뀌게 될 것입니다:</p>static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
<p>다음과 같이 결정한 경우에도 마찬가지입니다. ES|QL 대신 기존의 QueryDSL을 사용하면 언어에 관계없이 쿼리에서 받는 결과는 여전히 동일해야 합니다.</p><h2>필요한 경우 두 가지 접근 방식 모두 사용</h2><p><code>mostPublishedAuthorsInYears</code> 메서드의 사례는 두 가지 메서드를 모두 사용하여 단일 메서드를 테스트할 수 있음을 보여줍니다. 그리고 어쩌면 그렇게 해야 할지도 모릅니다.</p><ul><li><p>모의 버전만 사용한다는 것은 시스템을 업그레이드할 때 모의 버전을 유지해야 하고 자신감이 없다는 뜻입니다.</p></li><li><p>통합 테스트만 사용하면 전혀 필요하지도 않은 리소스를 낭비하는 셈이 됩니다.</p></li></ul><h2>요약해 보겠습니다.</h2><ul><li><p>모의 테스트와 Elasticsearch와의 통합 테스트를 모두 사용할 수 있습니다.</p></li><li><p>모의 테스트를 빠른 탐지망으로 사용하고 성공적으로 통과한 경우에만 종속성 테스트를 시작합니다(예: <code>./mvnw test '-Dtest=!TestInt*' &amp;&amp; ./mvnw test '-Dtest=TestInt*'</code> 또는 <a href="https://maven.apache.org/surefire/maven-failsafe-plugin/">Failsafe</a> 및 <a href="https://maven.apache.org/surefire/maven-surefire-plugin/">Surefire</a> 플러그인 사용).</p></li><li><p>외부 종속성과의 통합이 중요하지 않거나 건너뛸 수 있는 시스템 동작("코드" 줄 )을 테스트할 때는 모의 테스트를 사용하세요.</p></li><li><p>통합 테스트를 사용하여 외부 시스템에 대한 가정과 통합을 검증하세요.</p></li><li><p>위의 요점에 따라 두 가지 접근 방식을 모두 사용하는 것이 합리적이라면 테스트하는 것을 두려워하지 마세요.</p></li></ul><p>버전(저희의 경우 <code>8.15.x</code>)을 너무 엄격하게 적용하는 것은 지나치다는 지적이 있을 수 있습니다. 버전 태그만 사용할 수도 있지만, 이 글에서는 버전 간에 변경될 수 있는 다른 모든 기능을 나타내는 역할을 한다는 점에 유의하세요.</p><p><a href="https://www.elastic.co/search-labs/blog/automated-integration-tests-faster-elasticsearch">시리즈의 다음 편에서는</a> 테스트 데이터 세트를 사용해 테스트 컨테이너에서 실행 중인 Elasticsearch를 초기화하는 방법을 살펴보겠습니다. 이 블로그를 기반으로 구축한 것이 있거나 궁금한 점이 있으면 <a href="https://discuss.elastic.co/">토론 포럼</a> 및 <a href="https://communityinviter.com/apps/elasticstack/elastic-community">커뮤니티 Slack 채널에</a> 알려주세요.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</guid>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e4d003f09dfbe50/6a1709aba929cf810aae0957/b6bb727815ebdb844aeb36d5c44cdf3657f0e4bc-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>