<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>누구나 개발할 수 있다</title>
    <link>https://mooncake1.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 12 Jun 2026 20:50:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>문케이크</managingEditor>
    <image>
      <title>누구나 개발할 수 있다</title>
      <url>https://tistory1.daumcdn.net/tistory/5715380/attach/c7b4389145dd4c77b7dcfab12fbbd1f4</url>
      <link>https://mooncake1.tistory.com</link>
    </image>
    <item>
      <title>[JVM 밑바닥까지 파헤치기] 4장 - 가상 머신 성능 모니터링과 문제 해결 도구</title>
      <link>https://mooncake1.tistory.com/330</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;* 가상 머신 상태 모니터링과 문제 해결에 쓰이는 주된 도구에는 상용 인증 도구 (JMC/ JFR. 개인 목적에서는 무료이나 상용 환경에서는 비용 지불 필요), 공식 지원 도구, 실험적 도구 세 범주가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 도구들은 크기가 매우 작은게특징, 자바로 구현됨. 프로덕션 환경에서 사용되기에 부담이 없도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. JPS&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* ps 명령어처럼, 동작 중인 가상 머신 프로세스 목록을 보여주며, 각 프로세스에서 가상 머신이 실행한 메인 클래스의 이름과 로컬 가상 머신 식별자(LVMID)를 알려줌.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. JSTAT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 가상 머신의 다양한 작동 상태 정보를 모니터링 하는데 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 가상 프로세스의 클랫스 로딩, 메모리, GC, JIT 컴파일과 같은 런타임 데이터 볼 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1778983756718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ jstat -gcutil 5404 (-gc 명령어는 힙 상태 모니터링 위주이지만, 이건 전체 공간 사용률에 초점)
S0  S1  E   O   M   CCS   YGC ...
0   87  31  61  97  92    17  ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.19.19.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd1YRi/dJMcadWjUwj/1GRODF1O02qetW4ImY0kk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd1YRi/dJMcadWjUwj/1GRODF1O02qetW4ImY0kk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd1YRi/dJMcadWjUwj/1GRODF1O02qetW4ImY0kk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd1YRi%2FdJMcadWjUwj%2F1GRODF1O02qetW4ImY0kk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;735&quot; height=&quot;197&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.19.19.png&quot; data-origin-width=&quot;735&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 위와 같이 나온다면, 신세대의 에덴 공간을 31.15% 사용중, 두 생존자 공간 중 하나의 87% 정도를 사용 중이고, 구세대는 61% 사용중 이런 정보를 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. JINFO&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 가상 머신의 다양한 매개 변수를 실시간으로 확인 / 변경하는 도구&lt;/p&gt;
&lt;pre id=&quot;code_1778983883103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ jinfo -flag ConGCThreads 1444  (ConGCThreads 값 출력)
-XX:ConGCThreads=2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. JMAP&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 힙 스냅숏을 파일로 덤프해 주는 자바용 메모리 맵 명령어. LVMID 전달을 필요로 한다 ($ jmap [options vmid)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. JHAT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JMAP 에서 나온 힙 스냅숏 분석하는 도구. 작은 웹 서버를 내장하고 있어서 브라우저로 열 수 있지만 많이 사요앟진 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 힙 스냅숏 덤프 파일을 보통 다른 기기로 복사해서 분석하기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 분석이 단순한 편&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. JSTACK&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 스레드 스냅숏을 생성하는데 쓰이는, 스택 추적 도구. 스레드가 장기간 멈춰 있을 때 원인을 찾기 위해 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.19.27.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYhZY/dJMcab5iDFF/cRaKrhj7CKz9BWs42BT4T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYhZY/dJMcab5iDFF/cRaKrhj7CKz9BWs42BT4T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYhZY/dJMcab5iDFF/cRaKrhj7CKz9BWs42BT4T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYhZY%2FdJMcab5iDFF%2FcRaKrhj7CKz9BWs42BT4T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;742&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.19.27.png&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 위와 같이 정말 많은 정보 알려주고, 각 스레드별 상태를 알려줌 (우선 순위, CPU 사용 시간, 경과 시간, 스레드 상태, 호출 스택 등등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GUI 도구&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JDK는 GUI 도구 지원하는데, 대표적으로 JConsole, JHSDB, VisualVM, JMC&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. JHSDB&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* SA를 통해 프로세스 외부에서 디버깅할 수 있는 도구 (SA는 핫스팟 가상 머신이 제공하는 API 집합, JVM의 런타임 정보 제공)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JHSDB 를 실행하면 여러 옵션들이 있는데, Tools 에 &lt;span style=&quot;color: #006dd7;&quot;&gt;Heap Parameter&lt;/span&gt;, Inspector 등등의 기능들이 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778984717533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static class A {
    static ObjectHolder staticObj = new ObjectHolder();
    ObjectHolder instanceObj = new ObjectHolder();
    
    void foo(){
        ObjectHolder localObj = new ObjectHolder();
        ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 위와 같은 사례에서, staticObj, instanceObj, localObj 가 각각 어디에 저장되는지 (가르키는 객체가 아닌 변수 자체) 분석해볼 수 있다 (테스트 프로세스의 아이디를 얻어온 다음, &lt;i&gt;&lt;b&gt;$jhsdb hsdb - -pid {PID}&lt;/b&gt;&lt;/i&gt; 로 실행)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 그럼 JHSDB 창이 뜨는데, 여기서 에덴의 시작주소와 끝 주소를 알아둔 다음, CMD Line 에 &lt;i&gt;&lt;b&gt;$ scanoops &amp;lt;시작주소&amp;gt; &amp;lt;끝주소&amp;gt; ObjectHolder(클래스경로)&lt;/b&gt;&lt;/i&gt; 를 치면 해당 클래스로 생성된 객체들이 해당 주소 안에 어디어디 존재하는지 알려줌. 에덴 주소 안에 모두 static, instance, local Obj 들이 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;span style=&quot;color: #006dd7;&quot;&gt;Inspector&lt;/span&gt; 를 이번엔 실행해서, 위에서 얻은 객체의 주소를 입력해보면,&lt;b&gt; 객체 헤더와 객체 메타데이터&lt;/b&gt; (상속 관계, 인터페이스 관계, 필드 정보, 메서드 정보, 런타임 상수 풀 포인터 등등) 를 가리키는 포인터를 보여준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.40.31.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bs8t9Z/dJMb99M86fp/46K2AlYUVjI1XErnIgs2pK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bs8t9Z/dJMb99M86fp/46K2AlYUVjI1XErnIgs2pK/img.png&quot; data-alt=&quot;Inspector 를 통해 얻은 객체 내부 메타데이터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bs8t9Z/dJMb99M86fp/46K2AlYUVjI1XErnIgs2pK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbs8t9Z%2FdJMb99M86fp%2F46K2AlYUVjI1XErnIgs2pK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;570&quot; height=&quot;361&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.40.31.png&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Inspector 를 통해 얻은 객체 내부 메타데이터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;span style=&quot;color: #006dd7;&quot;&gt;Compute Reverse Ptrs 기능&lt;/span&gt;을 통해서는 해당 객체를 참조하는 포인터를 찾을 수 있다 (근데 명령어를 쓰는건지 메뉴를 실행하는건지 잘 모르겠음)&lt;/p&gt;
&lt;pre id=&quot;code_1778985500357&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hsdb&amp;gt; revptrs &amp;lt;위에서 찾은 객체 주소&amp;gt;
Computing reverse pointers ...
...
Oop for java/lang/Class @ &amp;lt;이 Class 인스턴스의 주소&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 이 부분 무슨 소린지 잘 모르겠음.. 참조를 타고 타면서 계속 정보를 알아내는 것 같은데.. 처음에 어디에 저장하는지 알 수 있다 부분부터,,, null 이 나왔으면 갑자기 뭘 더 하는데 그것도 뭔지 모르겠음.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. JConsole&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JMX에 기반한 GUI 모티러이 및 관리 도구. 실행시 실행중인 가상 머신 프로세스들을 찾아줘서, 로컬 프로세스를 연결하거나, 원격 서버 정보를 통해 원격 모니터링도 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.54.34.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UZP9P/dJMcacXqUPx/d2Y5kyU0PqJAZrcICblgyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UZP9P/dJMcacXqUPx/d2Y5kyU0PqJAZrcICblgyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UZP9P/dJMcacXqUPx/d2Y5kyU0PqJAZrcICblgyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUZP9P%2FdJMcacXqUPx%2Fd2Y5kyU0PqJAZrcICblgyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;384&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.54.34.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;메모리 모니터링&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778985969847&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fillHeap(){
    // 대충 자바 힙에 64KB/50ms 의 속도로 총 1000개의 데이터를 채워 넣는 코드
    // 다 채워넣은 이후에는 System.gc() 로 GC 호출 후 대기 (연결 유지를 위함)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.55.00.png&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v8XYY/dJMcahqWnwa/tE7ZkYSpOc34YivjwOgOyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v8XYY/dJMcahqWnwa/tE7ZkYSpOc34YivjwOgOyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v8XYY/dJMcahqWnwa/tE7ZkYSpOc34YivjwOgOyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv8XYY%2FdJMcahqWnwa%2FtE7ZkYSpOc34YivjwOgOyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;382&quot; data-filename=&quot;스크린샷 2026-05-17 오전 11.55.00.png&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 채워졌다 줄었다 (줄어드는 것은 아마 구세대로 보내는 것?) 를 반복하다가 GC 이후에는 꺠끗하게 신세대의 에덴 / 생존자 공간이 비워지는 것을 볼 수 있다. 이에 따른 두가지 질문을 생각해볼 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; Xms, Xmx 를 통해 자바 힙을 100MB 로 제한했는데, 이는 신세대 크기 명시 용도가 아니다. 이 상황에서 JConsole 을 통해 신세대의 용량을 예측할 수 있는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; System.gc 이후 힙 메모리가 최대치를 유지하는 이유는? gc 를 통해 힙의 객체를 회수하도록 하기 위해선?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 위 사진에 따르면 에덴의 크기는 27,328KB 임. 에덴과 생존자 공간의 비율은 기본 8:1 (SurvivorRatio 를 통해 변경은 가능) 이므로 신세대 전체 용량은 약 34,160KB (차이가 생존자 공간, 그 적은 부분 두개로 나눠서 마크 카피하던거)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* gc 호출 시점이 함수 내부라서 그렇다 (로컬인 List&amp;lt;OOMObject&amp;gt; 객체가 살아있어서). 메소드 밖으로 빼면 &quot;범위 안&quot;에 존재하는게 아니라, 회수된다 (범위란??)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 1.42.42.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0kGFW/dJMcaiXBHkQ/FJ9dqgvzJBSOsbOFWdu3Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0kGFW/dJMcaiXBHkQ/FJ9dqgvzJBSOsbOFWdu3Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0kGFW/dJMcaiXBHkQ/FJ9dqgvzJBSOsbOFWdu3Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0kGFW%2FdJMcaiXBHkQ%2FFJ9dqgvzJBSOsbOFWdu3Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;196&quot; data-filename=&quot;스크린샷 2026-05-17 오후 1.42.42.png&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 1.42.56.png&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfQHq7/dJMcad236Nf/ZLZWQ0TlQWcktCRZUeP8S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfQHq7/dJMcad236Nf/ZLZWQ0TlQWcktCRZUeP8S0/img.png&quot; data-alt=&quot;스레드들의 상태도 조회할 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfQHq7/dJMcad236Nf/ZLZWQ0TlQWcktCRZUeP8S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfQHq7%2FdJMcad236Nf%2FZLZWQ0TlQWcktCRZUeP8S0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;758&quot; height=&quot;193&quot; data-filename=&quot;스크린샷 2026-05-17 오후 1.42.56.png&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스레드들의 상태도 조회할 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* Thread 탭은 위에서 jstack 으로 스레드 덤프의 UI 와 같다. 위처럼 RUNNABLE 상태인지, WAITING 상태인지, 혹은 교착상태인지도 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. VisualVM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 일반적인 운영 및 대응, 성능 분석 등을 제공하는 자바 문제 해결 도구. agent sw 없이 활용할 수 있고, 운영 환경에서 바로 실행 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; jps, jinfo, jstat, jstack 기능, map, jhat 기능 지원 (플러그인으로 갖추고 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 메서드 수준에서 &lt;b&gt;가장 빈번히 호출&lt;/b&gt;되고 &lt;b&gt;긴 시간을 소모하는 메소드&lt;/b&gt; 확인 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 덤프나 런타임 설정 등을 &lt;b&gt;스냅샷하여 전송&lt;/b&gt;가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 원하는 애플리케이션에 대한 힙 덤프 생성이 가능하고, Summary (앱 시스템 속성 조회), Objects, Threads, OQL Console 등 다양한 항목을 지원한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* Profiler 항목을 통해서 프로그램이 동작하는 도중 CPU 사용 시간과 메모리 사용량을 &lt;b&gt;메소드 수준에서 분석 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 단, 해당 기능은 런타임 성능에 영향을 주기 때문에 운영에서&amp;nbsp; 직접 사용하지는 않음 (JMC 란건 부담이 적어서, 이걸 자주 쓴다고 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;BTrace 는 원래 코드에 없던 디버깅용 코드를 동적으로 삽입할 수 있는 도구&lt;/b&gt;, 보통 필요한 로그가 없던 상황이면 재배포를 해야함&lt;/p&gt;
&lt;pre id=&quot;code_1778994497746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 대충 엔텉 키를 누를 떄마다 1000 이하의 무작위 정수 두 개를 생성하고 더한 값을 출력하는 작업을 반복하는 코드&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 이 때 생성된 정수가 어떤 것들인지 로깅되어야 한다는 요구 사항 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778994792467&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BTrace
public class TracingScript {
    @OnMethod( // BTrace 로 추가하는 기능인 듯
        clazz=&quot;org.fenixsoft.jvm.chapter4.BtraceTest&quot;
        method=&quot;add&quot;,
        location=@Location(Kind.RETURN)
    )
    
    public static void func(@Self org.fenixsoft.jvm.chapter4.BTraceTest instance
        , int a, int b, @Return int result) { // 반환 값이 뭔지 캐치하는 듯
        
        println(&quot;콜 스택&quot;);
        jstack();
        println(&quot;메서드 매개 변수 A:&quot;, str(a));
        println(&quot;메서드 매개 변수 B:&quot;, str(b));
        println(&quot;메서드 결과:&quot;, str(result));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; BTrace 기능을 사용하면 IDE 같은 탭이 포함되어 있는데, 다음과 같이 디버깅 코드를 작성 후 돌리면, 원할 때마다 Outptut 패널에 추가한 로그가 출력된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;사진?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* BTrace 는 콜 스택, 매개 변수, 반환값 출력은 기본적인 쓰임이고, 성능 모니터링, 연결 누수 / 메모리 누수 찾기, 스레드 경합 해결 등에서 모두 활용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. JMC&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 오라클 유료 지원 서비스 중에는 AMC, JUT, JFR, JMC 등이 있고, 이 중 JMC 는 &lt;b&gt;자바 가상 머신 모니터링 시스템&lt;/b&gt;이다. 유료 라이센스지만, 개인은 무료.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 이클립스 느낌이 나는 JMC 는 따로 다운로드가 가능하며, 자동으로 가상 머신 목록들을 조회 후 Connection 으로 연결이 가능하다 (JMC는 MBean 과 JFR을 데이터 소스로 함, JFR은 첫 등장인듯)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 어느 정도 레코드를 할지 시간 설정이 가능하며, &lt;b&gt;GC, 컴파일러, 메서드 프로파일링 방법, 예외 기록 수준, 파일 및 소켓IO 등에 대한 레코드 옵션 및 빈도 설정&lt;/b&gt;도 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 그리고 나온 보고서는 다음과 같은 내용을 담고 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 애플리케이션: 앱이 쓴 스레드, 락, 메모리, 파일및 소켓IO, 메서드별 정보, 발생한 예외, 스레드 덤프 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; JVM 내부 정보: GC, JIT 컴파일 정보 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 환경: JVM이 실행 중인 프로세스, 환경 변수 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 이벤트: Event type 관련 정보?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 2.22.46.png&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cohQp5/dJMcaickoFV/VgkkRFzU5fdTDFGc6uysC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cohQp5/dJMcaickoFV/VgkkRFzU5fdTDFGc6uysC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cohQp5/dJMcaickoFV/VgkkRFzU5fdTDFGc6uysC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcohQp5%2FdJMcaickoFV%2FVgkkRFzU5fdTDFGc6uysC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;610&quot; height=&quot;460&quot; data-filename=&quot;스크린샷 2026-05-17 오후 2.22.46.png&quot; data-origin-width=&quot;795&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JFR은 MBean 보다 얻을 수 있는 데이터의 수준이 훨씬 높고, 프로세스 성능에 큰 영향을 주지 않음. JMC를 그래서 운영 프로덕션에는 더 활용할만 하다고 보는 듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핫스팟 가상 머신 플러그인과 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;1. HSDIS&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &amp;lt;JVM 명세서&amp;gt; 에는 JVM 세트에 속한 명령어들이 정의되어 있음. 이는 점점 개념 모델이 되어 갔고 (초반에 역사에서 많이 살펴본 부분인 듯), 이를 실행하는 구현체들은 더 다양해졌다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; &quot;프로그램 실행의 의미&quot;를 논할때는 바이트 코드 분석이 핑요하지만, &quot;동작 (어떻게 동작하고 성능은 어떤지)&quot;을 논할 때는 바이트 코드는 무의미 (??)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 보통 프로그램 동작 분석은 디버깅 도구로 중단점을 설정해 가며 수행, 하지만 JIT 컴파일러 등으로 많은 문제 -&amp;gt; HSDIS 의 등장&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* HSDIS 는 JIT 컴파일 코드용 디스어셈블리 플러그인 (핫스팟 VM 변수로 -XX:+PrintAssembly 로 가능) -&amp;gt; JIT 컴파일러가 동적으로 생성한 네이티브 코드를 &lt;b&gt;HSDIS 가 어셈블리 코드로 복원&lt;/b&gt;해 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 책에 예시가 있으니 살펴봐도 좋음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. JIT WATCH&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JIT 컴파일러용 로그 분석 및 시각화 도구, 매개변수로 로그 설정 및 JITWatch 가 읽어들일 로그를 저장해 주는 방식으로 진행&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 로그 파일을 읽어오면 자바 소스 코드, 바이트코드, JIT 컴파일러가 생성한 어셈블리 코드 등을 동시에 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-05-17 오후 8.26.42.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w5Lcg/dJMb99M9f5m/sR7KfmcPseG2j564m8enk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w5Lcg/dJMb99M9f5m/sR7KfmcPseG2j564m8enk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w5Lcg/dJMb99M9f5m/sR7KfmcPseG2j564m8enk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw5Lcg%2FdJMb99M9f5m%2FsR7KfmcPseG2j564m8enk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;317&quot; data-filename=&quot;스크린샷 2026-05-17 오후 8.26.42.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/330</guid>
      <comments>https://mooncake1.tistory.com/330#entry330comment</comments>
      <pubDate>Sun, 17 May 2026 11:16:41 +0900</pubDate>
    </item>
    <item>
      <title>[JVM 밑바닥까지 파헤치기] 2장 - 자바 메모리 영역과 메모리 오버플로</title>
      <link>https://mooncake1.tistory.com/328</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* C/ C++ 개발자는 객체의 소유권, 탄생부터 죽음까지 관리하는 책임을 진다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 자바 개발자는 VM이 제공하는 자동 메모리 관리 메커니즘으로 누수 / 오버플로 문제를 거의 겪지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 의존성이 높기 때문에, 문제 발생시 파악에 어려움이 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JVM 런타임 데이터&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-17 오후 10.47.22.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;777&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AeSH8/dJMcaiWyN8Q/wRe00p7F9qKNHCgwcQNnbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AeSH8/dJMcaiWyN8Q/wRe00p7F9qKNHCgwcQNnbK/img.png&quot; data-alt=&quot;JVM의 런타임 데이터 영역&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AeSH8/dJMcaiWyN8Q/wRe00p7F9qKNHCgwcQNnbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAeSH8%2FdJMcaiWyN8Q%2FwRe00p7F9qKNHCgwcQNnbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;749&quot; height=&quot;507&quot; data-filename=&quot;스크린샷 2026-02-17 오후 10.47.22.png&quot; data-origin-width=&quot;1148&quot; data-origin-height=&quot;777&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JVM의 런타임 데이터 영역&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;PC 레지스터&lt;/b&gt; - 현재 실행 중인 스레드의 줄 번호 표시기 (바이트 코드 줄 번호). 인터프리터는 이 카운터의 값을 바꿔서 다음에 실행할 바이트코드 명령어를 선택. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;스레드별로 존재&lt;/b&gt;&lt;/span&gt;하며, CPU에게 &lt;b&gt;멈춘 지점을 명확하게 복원&lt;/b&gt;시켜 주는 값 저장 담당.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;자바 가상 머신 스택&lt;/b&gt; - &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;스레드별로 존재&lt;/b&gt;&lt;/span&gt;. 스레드 내에서 메서드 호출시 스택에 지역 변수 테이블 (32비트 슬롯들 모음집), 피연산자 스택 (CPU의 계산용 임시공간), 동적 링크 (의존하는 메서드 호출 네비게이션), 메서드 반환값 등이 저장된 스택 프레임을 쌓음, FILO.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;스택 메모리 영역에서는 두가지 오류가 정의&lt;br /&gt;&lt;br /&gt;- StackOverflow : 스레드가 요청한 스택 깊이가 VM이 허용하는 깊이보다 큰 경우&lt;br /&gt;- OutOfMemory : VM에서 동적으로 스택 용량을 확장하려는 시점에 여유 메모리가 충분하지 않을 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;네이티브 메서드 스택&lt;/b&gt; - 네이티브 메서드를 실행할 때 사용되는 스택. 일반 스택과 하나로 합쳐져 있는 경우도 많음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;자바 힙&lt;/b&gt; - &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;모든 스레드가 공유&lt;/span&gt;&lt;/b&gt;, 자바의 가장 큰 메모리, VM 구동시 생성. 자바 세계의 거의 모든 객체 인스턴스가 할당되는 영역. 가비지 컬렉터가 관리하는 메모리 영역. 스레드 로컬에 할당되는 버퍼 여러 개로 나뉨 (효율 높이기 위함). 디스크에 파일이 저장되듯이, 메모리 내에서 물리적으로는 떨어져도 논리적으로 연속되어야 함. Xmx, Xms 매개변수를 활용하여 VM 들은 힙 크기를 제어할 수 있다. 힙을 확정할 수 없다면 OOM 이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;메서드 영역&lt;/b&gt; - &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모든 스레드가 공유&lt;/b&gt;&lt;/span&gt;. 정적 변수, 상수, (JIT 컴파일러가 컴파일한) 코드 캐시 등을 저장. GC 대상이긴 하지만 회수 효과가 상대적으로 적음. 메서드가 꽉 차서 메모리 할당 할 수 없다면 OOM 이 발생한다&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메서드에는 다음과 같은 정보들이 저장됨&lt;br /&gt;- 클래스 메타데이터, 필드 정보, 메서드 정보&lt;br /&gt;- 런타임 상수 풀 (.class 에는 클래스 파일 상수 풀이 있는데, JVM 이 로딩시 런타임 상수 풀로 변경, )&lt;br /&gt;- static 변수&lt;br /&gt;- 문자열 상수 풀&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;런타임 상수 풀&lt;/b&gt; - 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 정보 저장. 이 때, [.class] 파일에 있는 정보들은 정적인 테이블인 &lt;span style=&quot;color: #ee2323;&quot;&gt;클래스 파일 상수 풀&lt;/span&gt;에 저장되는데, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;클래스가 JVM에 로딩되면 클래스 파일 상수 풀이 런타임 상수 풀로 복사&lt;/b&gt;&lt;/span&gt;된다. 단, 이 런타임 상수 풀은 동적이다. 메서드 실행 중 new String(&quot;hello&quot;).intern() 을 수행시 상수풀에 추가된다 (그냥 String a = &quot;hello&quot; 는 이미 클래스 파일 상수 풀에 있는 상태라고 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;다이렉트 메모리&lt;/b&gt; - JVM에 속하지 않지만 자주 쓰이고 OOM의 원인이 되기도 함. 성능을 위해 힙을 거치지 않고 OS 메모리를 직접 사용하는 메모리로, 사용예시 ex) &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;ByteBuffer&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;buffer = &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;ByteBuffer&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;allocateDirect&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;1024&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 16px; letter-spacing: 0px;&quot;&gt;).&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;핫스팟 VM의 객체 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;객체 생성 과정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* JVM의 new 객체() 라는 바이트 코드를 만나면, 매개 변수가 상수 풀 안의 클래스를 가리키는 심벌 참조인지 확인. 그 다음 이 심벌 참조가 뜻하는 클래스가 로딩, 해석, 초기화되어 있는지 확인.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;어떤 클래스의 객체를 만들라는건지 아까 클래스 상수 풀 (.class 의 이름, 변수, 상수 등) 에서 확인하는데, 이 때 심벌 참조 (new &quot;이름&quot;)를 사용한다는 뜻. 그리고 메모리에 있는지 확인, 실제 메모리 참조하여 링크, 힙에 객체 공간 할당 등을 진행한다는 뜻&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 로딩이 완료된 클래스면 새 객체를 담을 메모리 할당 (힙에서 특정 크기의 메모리 블록 잘라 주는 일).&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 때, 자바 힙이 물리적으로 규칙적이라고 한다면, 새 객체의 크기만큼 포인터를 밀친 뒤 밀쳐진 공간을 주면 되는데, 이걸 &quot;포인터 밀치기&quot;라 함. 하지만, 당연히 뒤섞여있기 때문에 JVM은 가용 메모리 블록을 따로 관리, 공간을 찾아 할당 후 목록 refresh. 이걸 &quot;여유 목록&quot; 방식이라 함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;(추가- 스레드가 가용 공간을 어떻게 할당할까)&lt;span style=&quot;color: #000000;&quot;&gt;-&lt;/span&gt;&lt;/span&gt; 멀티 스레딩 환경에서는 여유 메모리의 시작 포인터 위치를 수정하는 단순한 일도 non-thread-safe. 여러 스레드가 동시에 객체를 생성하려 할 때 문제가 생길 수 있음&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;한 스레드가 요청한 객체 A를 위해 공유 공간인 힙 메모리를 할당하는 과정에서, 포인터의 값을 수정하기 전에 다른 스레드가 B를 요청할 때, 할당된 메모리가 A 공간 포인터 수정 전일 경우 문제가 발생한다&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;해법은 CAS, 혹은 ThreadLocal. 둘다 존재함. ThreadLocal 할당 버퍼 (TLAB) 방식은 스레드가 각각 힙 내의 작은 크기의 전용 공간을 미리 할당받아 놓는 방식을 말한다 (버퍼가 부족해지면, 그 때 동기화를 통해 새로운 버퍼를 할당받음). -XX:+/-UseTLAB 매개 변수를 JVM에게 전달하면 스레드 로컬 할당 버퍼를 사용하는 실행으로 인지한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 아무튼 이후 JVM은 정보들을 객체 헤더에 저장한다 (해시코드 정보, GC 세대 정보 등). 이 시점에서 JVM은 객체 생성을 완료 했으나, 프로그램 관점에서는 클래스의 &amp;lt;init&amp;gt; 이 실행되지 않은 상태.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;자바 컴파일러가 new 를 발견하면 바이트코드 명령어인 [new] 와 [invokespecial] 을 수행하는데, 후자가 &amp;lt;init&amp;gt; 메서드 호출을 담당. 둘은 연이어 수행된다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;객체의 메모리 레이아웃&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v7Uap/dJMcaa5kM9Y/EHfYk1kmMtqmXRWkqMCzak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v7Uap/dJMcaa5kM9Y/EHfYk1kmMtqmXRWkqMCzak/img.png&quot; data-alt=&quot;객체의 메모리 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v7Uap/dJMcaa5kM9Y/EHfYk1kmMtqmXRWkqMCzak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv7Uap%2FdJMcaa5kM9Y%2FEHfYk1kmMtqmXRWkqMCzak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;680&quot; height=&quot;307&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;객체의 메모리 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 핫스팟 VM은 객체를 세 부분으로 나눠 힙에 저장 (헤더, 인스턴스 데이터, 정렬 패딩)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;객체 헤더&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; &lt;b&gt;마크워드&lt;/b&gt;: 런타임 데이터 저장 (해시 코드, GC 세대, 현재 모니터 락 상태, 자주 쓰는 스레드 ID 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt;&amp;gt; 매우 많은 정보들이 담겨야 하기 때문에, 효율적으로 사용해야하고 플래그를 통해 정보를 저장하기도 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; &lt;b&gt;클래스워드&lt;/b&gt;: 클래스 포인터 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; &lt;b&gt;배열 길이&lt;/b&gt;: 배열 객체일 경우 길이도 저장 (int [10] : JVM은 이도 객체라고 판단함).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;인스턴스 데이터&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; 정의된 다양한 타입의 필드들, 부모 클래스 유모, 부모 클래스로 상속받은 필드 등 여기에 기록됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;정렬 패딩&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; 없을 수도 있음. 객체의 크기는 8바이트의 N(정수)배. 조건을 충족하지 못하는 경우 해당 패딩을 통해 채움.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;객체에 접근하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 객체들은 서로 연관되어 있으며, 스택내 참조 데이터를 통해 힙에 있는 객체들에 접근해서 조작함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 힙에서 객체의 정확한 위치를 알아내어 접근하는 방식은 보통&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; 핸들 / 다이렉트 포인터&lt;/b&gt;&lt;/span&gt;를 사용해 구현함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KTAWz/dJMcaibfbmz/4z6mAXyiOBKHSeoSXaLLk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KTAWz/dJMcaibfbmz/4z6mAXyiOBKHSeoSXaLLk0/img.png&quot; data-alt=&quot;핸들을 이용한 객체 접근&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KTAWz/dJMcaibfbmz/4z6mAXyiOBKHSeoSXaLLk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKTAWz%2FdJMcaibfbmz%2F4z6mAXyiOBKHSeoSXaLLk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;267&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;핸들을 이용한 객체 접근&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2KE5X/dJMcaioMe9t/T8z5lyUgoH64yf8l3aBoHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2KE5X/dJMcaioMe9t/T8z5lyUgoH64yf8l3aBoHK/img.png&quot; data-alt=&quot;다이렉트 포인터를 활용한 객체 접근&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2KE5X/dJMcaioMe9t/T8z5lyUgoH64yf8l3aBoHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2KE5X%2FdJMcaioMe9t%2FT8z5lyUgoH64yf8l3aBoHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;619&quot; height=&quot;281&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다이렉트 포인터를 활용한 객체 접근&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 핸들 방식은 자바 힙에 핸들 저장용 Pool 을 두어서, 인스턴스 데이터, 타입 데이터 등의 정확한 주소 정보를 담아두는 방식&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;자바 A 객체에서 B 객체를 참조할 때, 핸들 방식이란 해당 B 객체에 대해 핸들 풀 참조를 들고 있는 것. GC 과정에서 객체 이동은 매우 흔한데, A 객체에 참조는 손댈 필요 없이 핸들 풀의 인스턴스 데이터 포인터만 바꾸면 됨&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* 다이렉트 포인터은 힙에 인스턴스 데이터 뿐 아니라 타입 데이터 포인터도 제공해야 하고, &lt;b&gt;참조 데이터에도 힙 내 실제 객체의 인스턴스 주소&lt;/b&gt;가 저장되어 있음&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;핸들을 경유하지 않기 때문에 매우 빠른 접근 속도. 실행 시간에 실제로 유의미한 영향을 보인다&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;OutOfMemory 예외 살펴보기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;* 자바 힙 OOM&lt;/b&gt; - 객체를 무한정 생성시 생성만 빠르게 지속하면 OOM이 발생한다. 힙의 OOM은 진짜 메모리 부족, 객체를 너무 많이 생성, 힙 크기 제한, 메모리 누수의 이유로 발생한다. (메모리 누수는 메모리 부족과 마찬가지긴 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;스택 OOM 과 오버 플로우&lt;/b&gt; - 함수를 무제한 호출 loop 을 돌리면, 스택 크기 제한으로 인해 StackOverflow 에러가 발생한다. 그리고 지역변수 테이블을 크게하여 스택 크기 자체를 크게 해서 (지역변수 800 Byte 추가) 스택이 빨리 가득 차게 하면 더 빠르게 발생한다. 하지만 이 부분은, 스택의 크기를 동적으로 늘릴 수 있는 VM이라면 OOM 을 발생시킬 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;* &lt;b&gt;스레드로 인한 스택 OOM&lt;/b&gt; - 스레드를 생성하면 JVM 이 OS에게 직접 &quot;스택 메모리 주세요&quot; 한다. 이 때 OS 에서 &quot;이 프로세스에게는 더 못준다&quot;고 하면 발생함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;프로세스 전체 메모리 한도&lt;br /&gt;├─ Heap (-Xmx)&lt;br /&gt;├─ Thread stacks (-Xss &amp;times; 스레드 수)&lt;br /&gt;├─ Metaspace&lt;br /&gt;├─ Direct/native memory&lt;br /&gt;└─ JVM 내부 영역&lt;br /&gt;&lt;br /&gt;- 스택이 1MB 인데 2000개의 스레드가 돌고 있다면, Thread stacks 는 총 2GB 를 가져간다. 이 때, Heap 이 4GB 라면 6GB 이상을 프로세스가 점유하고 있는 것&lt;br /&gt;- Xmx 크기를 줄이고, -Xss 크기를 줄이면, 프로세스에게 &quot;더 많은 스레드&quot; 를 위한 공간이 확보되기 때문에, 오히려 위와 너무 많은 스레드로 인한 OOM 일 경우는 힙 크기 줄이기와 스택 크기 줄이기가 해결책이 되기도 한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;* 메서드 영역 (런타임 상수 풀)의 OOM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; (무한루프) hashSet.add(String.valueOf(i++).intern()); 을 수행하면, JDK 6까지는 PermGen (핫스팟의 메소드 영역) 에서 OOM 이 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;intern 을 사용하면 JVM 이 관리하는 문자열 상수 풀에 등록하게 된다. 따라서 메서드 영역을 잡아 먹는 것. 문자열 상수 풀은 JVM이 공유 / 관리 하는 것으로, 우리가 실제 쓸 일은 없다. JVM의 static String 들이라고 보면 편하다 (특정 문자열 비교가 굉장히 잦을 때, intern 을 사용하여 최적화 할 수도 있다)&lt;/blockquote&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; JDK 7 을 넘어서면서는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;문자열 상수 풀 + static 영역의 PermGen 이 힙으로 넘어왔기 때문에&lt;/b&gt;&lt;/span&gt;, 힙 영역 오류가 발생한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;String a = new StringBuilder(&quot;hello&quot;).append(&quot;world&quot;).toString();&lt;br /&gt;a.intern() == a;&lt;br /&gt;를 수행했을 때, JDK7 이후는 true 가 발생한다. JDK 6에서 문자열에 대한 intern()은 문자열 상수 풀에 복사한 다음 문자열 인스턴스의 참조를 반환한다. StringBuilder 를 통해 생성되면 일반 Heap 에 존재하고 문자열 상수 풀에 복사시키지 않기 때문에, 다른 주소를 참조. 따라서 둘이 다름.&lt;br /&gt;JDK 7 이후는 문자열 상수 풀이 자바 힙에 있으므로, intern 을 수행해도 첫 번째 인스턴스의 참조로 바꿔주면 된다 (JVM 입장에서는 공유중이기 때문)&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;------ 문자열 상수 풀이란 개념, 공유된다는 개념이 잘 잡히지는 않음. static 느낌과는 다름? String 은 자동 static 도 아닌데.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; 메서드 영역의 메타스페이스에 대한 OOM 도 테스트 하는데, 이 부분도 잘 이해가 안된다. 객체를 무한정 생성하는데, 메서드 영역의 OOM 나는 모습이 어떤 충돌이 일어난건지 잘 모르겠음&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;* 네이티브 다이렉트 메모리 OOM&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; Unsafe 라는 객체 인스턴스를 사용하면, 직접 운영 체제 단에서 메모리를 할당하는 방식을 사용하게 된다. unsafe.allocateMemory(1024*1024) 를 통해 1MB 씩 지속 할당받으면, OOM 이 발생하는데, 이는 운영체제에서 직접 더 이상 할당하지 않는 모습&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;힙 덤프에서는 이상한 점을 찾을 수 없다는 점이 다이렉트 메모리의 OOM&lt;/span&gt; 특징&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-18 오전 10.42.14.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wH4nU/dJMcaca11hI/a8yME57JSEh77SvhRF9aV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wH4nU/dJMcaca11hI/a8yME57JSEh77SvhRF9aV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wH4nU/dJMcaca11hI/a8yME57JSEh77SvhRF9aV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwH4nU%2FdJMcaca11hI%2Fa8yME57JSEh77SvhRF9aV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;377&quot; data-filename=&quot;스크린샷 2026-02-18 오전 10.42.14.png&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS 이론/JVM</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/328</guid>
      <comments>https://mooncake1.tistory.com/328#entry328comment</comments>
      <pubDate>Tue, 17 Feb 2026 22:31:12 +0900</pubDate>
    </item>
    <item>
      <title>[Elasticsearch] Custom Tokenizer 를 만들어 Elasticsearch 에 넣어보자 (+이론 설명)</title>
      <link>https://mooncake1.tistory.com/325</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Elasticsearch 는 검색에 대한 요구사항을 해결하기에 도입하기에 굉장히 유용하며, 이제 백엔드 개발 분야에서는 거의 필수적으로 가져가야 하는 지식이라고 생각이 된다. Elasticsearch 를 활용함에 있어서 가장 큰 핵심은 Analyzer, Tokenizer 를 사용하는 것이다.&amp;nbsp;많은 개발자들이 공감하겠지만, 회사나 프로젝트 내부 도메인 기반으로 검색 기능을 만들려면 존재하는 tokenizer 들로는 어려운 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;검색 자체가 형태소 같은 단위가 아니라 해쉬태그나 연관 검색어 위주로 검색할 때, 도메인 특화되어 있으면 도메인에 특화되게끔 Tokenize 해주는건 Elasticsearch 에선 당연히 어렵기 때문이다. 이번 포스트에선 Custom Tokenizer 적용을 위해 이해가 필요한 영역들과 방법에 대해 적었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Lucene 엔진과&amp;nbsp; Tokenizer 의 동작&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Lucene 엔진 소개와 Analyzer&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Elasticsearch 를 사용하면서 Tokenizer 개발을 해야겠다 생각한다면, Elasticsearch 내 핵심 엔진인 Lucene 라이브러리에 대해서 알아야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Lucene 은 역인덱스를 사용한 검색 엔진의 핵심&lt;/b&gt;&lt;/span&gt;이며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;Elasticsearch 는 이 Lucene 을 감싸서 하나의 서버로 배포되는 시스템&lt;/u&gt;이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Lucene 은 Java 라이브러리로, 직접 코드로 사용하는 구조&lt;/b&gt;&lt;/span&gt;이다. 따라서 Custom 한 동작을 정의하기 위해선 이 Lucene 라이브러리의 흐름을 어느 정도 알아야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-15 오후 7.54.53.png&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTqqpK/btsPUgMeLQY/luI6Tz1N54jW7EkMxrX2d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTqqpK/btsPUgMeLQY/luI6Tz1N54jW7EkMxrX2d0/img.png&quot; data-alt=&quot;Lucene 내에서 Data 가 색인되어 저장될 때 흐름도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTqqpK/btsPUgMeLQY/luI6Tz1N54jW7EkMxrX2d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTqqpK%2FbtsPUgMeLQY%2FluI6Tz1N54jW7EkMxrX2d0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;881&quot; height=&quot;271&quot; data-filename=&quot;스크린샷 2025-08-15 오후 7.54.53.png&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lucene 내에서 Data 가 색인되어 저장될 때 흐름도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Lucene 에서 디렉토리의 저장되는 모습은 위 구조와 같이 흐르게 되어 있고, 그 중 Analyzer 를 통과할 때 역인덱스 저장을 위한 토큰 처리가 진행된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://mooncake1.tistory.com/318&quot;&gt;기본 포스트&lt;/a&gt;에 있듯이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Analyzer 는 Char Filter, Tokenizer, TokneFilter 로 구성&lt;/span&gt;되어 있으며, 그 중 핵심이 되는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Tokenizer 와 TokeniFilter&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 구조를 알아보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Tokenizer 와 TokenFilter, 그리고 TokenStream&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-15 오후 7.55.03.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eiwea/btsPTAj2qIm/avzACKpM4WFmDLxMbvRIU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eiwea/btsPTAj2qIm/avzACKpM4WFmDLxMbvRIU0/img.png&quot; data-alt=&quot;TokenStream Pipeline 의 흐름도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eiwea/btsPTAj2qIm/avzACKpM4WFmDLxMbvRIU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEiwea%2FbtsPTAj2qIm%2FavzACKpM4WFmDLxMbvRIU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;166&quot; data-filename=&quot;스크린샷 2025-08-15 오후 7.55.03.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenStream Pipeline 의 흐름도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Analyzer 에서 CharFilter 이후에 Tokenizer &amp;amp; TokenFilter 를 처리하기 위해 input 을 전달하게 되는데, 이 때 이 둘의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;전체적인 묶음을 TokenStream&lt;/u&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라고 부른다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;TokenStream&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이란 위 그림 처럼 CharFilter 이후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&quot;토큰 처리를 위한 흐름&quot;이라는 역할 묶음에 대한 추상적인 표현&lt;/span&gt;&lt;/b&gt;이라고 보는게 이해가 쉽다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;실제로 코드내에서 TokenStream 이라는 객체 안에 Tokenizer 및 TokenFilter 들이 Pipeline 형태로 구성되어 있는데, 이는 관계도를 보면 바로 파악할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beJ8Vj/btsPTyzLjMS/rCzt7hM2V6CaXDWww3P8ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beJ8Vj/btsPTyzLjMS/rCzt7hM2V6CaXDWww3P8ZK/img.png&quot; data-alt=&quot;TokenStream, Tokenizer, TokenFIlter 의 데코레이터 패턴 관계도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beJ8Vj/btsPTyzLjMS/rCzt7hM2V6CaXDWww3P8ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeJ8Vj%2FbtsPTyzLjMS%2FrCzt7hM2V6CaXDWww3P8ZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;498&quot; height=&quot;215&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;215&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TokenStream, Tokenizer, TokenFIlter 의 데코레이터 패턴 관계도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;TokenStream 이 최상위 인터페이스로, TokenFilter는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Tokenizer를 전달받아 생성&lt;/b&gt;&lt;/span&gt;된 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;이에 부가 기능들을 입힐 수 있는 완벽한 Decorator 패턴으로 구성&lt;/b&gt;&lt;/span&gt;되어 있다(&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://mooncake1.tistory.com/206&quot;&gt;데코레이터 패턴 글&lt;/a&gt;). 즉 TokenStream을 사용하는 Client 객체는 어떻게 토큰화가 처리될지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;정의가 완료된 한 Pipeline (TokenStream) 을 가지고 토큰화를 진행&lt;/u&gt;하는 것인데, 이 때 Client에게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;적합한 TokenStream 을 생성해 전달하는 역할을 Analyzer&lt;/b&gt;가 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드를 살펴보자. Lucene Analyzer 를 사용하는 Client 측을 개발한다면 (우리의 경우 Elasticsearch 코드가 Client), 다음과 같은 형태로 설계하도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://www.baeldung.com/lucene-analyzers&quot;&gt;Baeldung 에서 안내&lt;/a&gt;하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972023&quot; class=&quot;lasso&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public List&amp;lt;String&amp;gt; analyze(String text, Analyzer analyzer) throws IOException {
    List&amp;lt;String&amp;gt; result = new ArrayList();
    TokenStream tokenStream = analyzer.tokenStream(FIELD_NAME, text);
    ...
    while(tokenStream.incrementToken()){
        result.add(attr.toString());
    }
   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이처럼 Client 는 완성된 TokenStream 을 Analyzer 에게 전달받고, Tokenize 요청인 incrementToken() 함수를 호출하게 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;중요한 점은 TokenStream 의 가장 Root 객체는 항상 Tokenizer 로 사용해야 정상적으로 동작&lt;/b&gt;&lt;/span&gt;한다. 다음 예시를 통해 데코레이터 패턴으로 형성된 TokenStream 이 어떻게 동작하는지 알아보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972024&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MooncakeTokenizer extends TokenStream {
    CharTermAttribute cta = addAttribute(CharTermAttribute.class);
    ...
    @Override
    public boolean incrementToken(){
        if(!isProcessing)){
            // 토큰화 및 Queue 저장
        }
        if(!tokens.isEmpty()){
            termAttr.append(tokens.poll());
            return true;  // 다음 필터에게 true 상태로 전달
        }
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Tokenizer의 incrementToken() 함수는 기본적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Token 화 및 Filter 들에게 Token 전달을 메인 동작&lt;/b&gt;으로 진행한다. 세부 동작은 실제 Tokenizer 클래스 만들면서 살펴보고, 지금 당장은 위 incrementToken 함수가 &quot;가장 먼저 실행되는 함수&quot; 임을 아는 것이다. 일단&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&quot;true 를 반환하면 토큰을 cta 에 담아 Filter 에게 전달&quot;&lt;/u&gt;한다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972025&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class LowerCaseTokenFilter extends TokenFilter {
    CharTermAttribute cta = addAttribute(CharTermAttribute.class);
    ...
    protected LowerCaseTokenFilter (TokenStream input){
        super(input); 
    }
 
    @Override
    public boolean incrementToken() throws IOException { 
        if(!input.incrementToken()){  // 상위에서 false 였으면 같이 false 반환
            return false;
        }
        String mine = termAttr.toString().toLowerCase(); // input 이 cta 에 토큰을 저장해둠
        cta.setEmpty().append(mine); // 자신이 할 일을 하고 새롭게 저장
        return true;
    }
}

---

// 실제 TokenFilter 클래스를 보면 기존 input 을 반드시 가지고 선언하라고 되어 있다
public abstract class TokenFilter extends TokenStream implements Unwrappable&amp;lt;TokenStream&amp;gt; {
    protected final TokenStream input;

    protected TokenFilter(TokenStream input) {
        super(input);
        this.input = input;
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TokenFilter 는 구조상 반드시 input 을 가지고 선언하게 되어 있는데, TokenStream Pipeline 에서 첫번째가 될 수 없기 때문이다. 상위 Tokenizer 혹은 TokenFilter 에서 토큰을 전달할 때, cta 를 통해 전달하는데, 여기서 토큰을 꺼낸 뒤 할 일을 수행 후 다시 저장하고 다음 Filter 로 보낸다. 참고로 addAttribute 는 AttributeSource 란 객체 내에서 관리되는데, 저장된 속성이 있다면 해당 속성을 반환하고 없다면 새로 빈 값을 만들어서 반환한다 (아래에서 더 살펴보게 됨).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972027&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;class MooncakeAnalyzer extends Analyzer {
    ...
    @Override
    public TokenStream tokenStream(String fieldName, Reader reader){  // 라이브러리 봤을 때 사실상 이렇게 흐르진 않아 보였는데, 역할적인 측면에서 이해하자
        ...
        TokenStream tokenizer = new MooncakeTokenizer();
        TokenStream lowerCasefilter = new LowerCaseTokenFilter(tokenizer);
        TokenStream htmlFilter = new HtmlTokeFilter(lowercaseFilter)
        ...
        return htmlFilter;
    }
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 Analyzer 까지 만들어야 할 일이 있다면, 아마 위와 같이 만들어질 것이다. Client 측에서 사용할 Analyzer 를 구성해서 전달해야 하므로, 위와 같이 TokenStream 을 tokenizer 를 시작으로 구성하여,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;전체 TokenStream Pipeline 을 전달&lt;/b&gt;&lt;/span&gt;하는 모습이다. 이 analyzer 를 위에서 Baeldung 예시와 연결지으면 되겠다. 지금 중요한건&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&quot;데코레이터 패턴으로 TokenStream이 동작하는 모습&quot;만 이해&lt;/b&gt;&lt;/span&gt;하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Elasticsearch 에 사용할 Custom Tokenizer 개발&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Tokenizer를 만들려면 Lucene을 사용해야 하기 때문에 Java 프로젝트를 만들어서 적절한 Library 들을 import 해야 한다. 나는 gradle 로 만들었고, Elasticsearch 에 넣어줄 것이기 때문에 다음과 같이 import 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972028&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# build.gradle 파일 dependencies 내부
...
// 설치된 elasticsearch 버전과 완전히 동일해야 함
implementation 'org.elasticsearch:elasticsearch:8.17.4'   

// Elastic 8 버전은 Lucene 9를 사용하도록 안내
implementation 'org.apache.lucene:lucene-analysis-common:9.7.0'   
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;AttributeSource 와 Attribute&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972028&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public abstract class TokenStream extends AttributeSource implements Closeable {
    public static final AttributeFactory DEFAULT_TOKEN_ATTRIBUTE_FACTORY;
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위에서도 등장했듯이, Attribute 은 Tokenize 개념에 있어서 빠질 수 없는 정보이기 때문에, 간략히 알고 가는게 좋다. 우선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Attribute 란, 각 토큰에 붙는 정보&lt;/b&gt;&lt;/span&gt;들이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CharTermAttribute&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;토큰 텍스트 값 자체&lt;/b&gt;를 말하며, 그 밖에도 Offset Attribute, Position Attribute 등이 있다고 한다 (토큰 부가정보로 같이 저장되기도 함). TokenStream 은 위와 같이 AttributeSource 를 상속받는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;AttributeSource 는 이 Attribute 들을 관리하는 객체&lt;/b&gt;&lt;/span&gt;이다 (addAttribute 요청을 처리하는 객체).&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;따라서 TokenStream 들은 자신의 역할에 맞게 AttributeImpl들 (다양한 Attribute들) 을 준비한다. 대부분 TokenStream 객체들은 Token 텍스트 값을 가지고 행동 들을 하기 때문에 CharTermAttribute 은 모두 들고 있다고 생각하면 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Tokenizer 에서 input 을 가지고 CharTermAttribute 을 AttributeSource (TokenStream 의 한 역할) 에 저장&lt;/b&gt;해두면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;후속 TokenFilter 들이 이를 꺼내와서 사용&lt;/b&gt;한다 (addAttribute 함수).&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AttributeSource 는 Attribute 들을 관리하는 역할을 주로 하고, 내부적으로 AttributeFactory 를 가지고 있는데 이 객체에게 각 Attribute 생산을 위임한다는 정도만 일단 알아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Custom Tokenizer&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972029&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MooncakeTokenizer extends Tokenizer {

    private final CharTermAttribute cta = addAttribute(CharTermAttribute.class);
    private final Queue&amp;lt;String&amp;gt; tokens = new ArrayDeque&amp;lt;&amp;gt;();
    private boolean isProcessing = false;

    @Override
    public boolean incrementToken() throws IOException {

        clearAttributes();

        if (!isProcessing) {

            isProcessing = true;

            char[] writingGround = new char[1024];
            StringBuilder rawBuilder = new StringBuilder();
            int length;
            while ((length = input.read(writingGround)) != -1) {
                rawBuilder.append(writingGround, 0, length);
            }

            String inputText = rawBuilder.toString(); // rawText 를 추출한다

            doMyExtract(tokens); // Token 을 모두 뽑아서 Queue에 넣어둔다

        }

        if (!tokens.isEmpty()) {
            termAttr.append(tokens.poll());
            return true;
        }

        return false;
    }

    @Override
    public void reset() throws IOException {
        super.reset();
        tokens.clear();
        isProcessing = false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Custom Tokenizer 를 만들려면 사실 이론을 몰라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;위 형태만 갖추면 바로 되긴 한다&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;(그냥 궁금해서 이것 저것 공부했다). doMyExtract() 함수 부분에서 도메인별로 토큰화하는 방식을 Java 코드로 개발해서 넣으면 된다. 필드들은 위에서 살펴 본 토큰을 담는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CharTermAttribute&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 있고, Token 화 이후 저장하는 공간인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Queue&lt;/b&gt;, 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;processing 현황&lt;/b&gt;이 있다. 함수별로 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;incrementToken()&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한가지 중요한 점은, Token 화 된 모든 텍스트를 Queue에 담아서 TokenFilter 로 전달하지 않고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;(1) isProcessing = false 일 때&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;전체를 토큰화 이후 Queue 에 보관&lt;/u&gt;하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;&lt;b&gt;(2) isProcessing = true 일 때&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하나씩 꺼내서 Filter 처리를 하는 방식&lt;/u&gt;인 것이다. 즉 10개의 토큰이 있다면 이 함수는 10번 호출된다. 다시 한번 Baeldung 예시 코드를 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972032&quot; class=&quot;lasso&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public List&amp;lt;String&amp;gt; analyze(String text, Analyzer analyzer) throws IOException {
    List&amp;lt;String&amp;gt; result = new ArrayList();
    TokenStream tokenStream = analyzer.tokenStream(FIELD_NAME, text);
    ...
    while(tokenStream.incrementToken()){
        result.add(attr.toString());
    }
   ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;while&amp;nbsp; 문으로 tokenStream 이 false 를 반환할 때까지 순환을 하는 모습을 확인할 수 있으며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;false 일 때는 Token Queue 에 더 이상 남은 토큰이 없는 경우&lt;/b&gt;&lt;/span&gt;이다. 그 때가지 Queue 에서 하나씩 꺼내고 Filter 처리를 한 이후, Client 단에서 토큰화된 결과 자료구조에(result) 담는 방식이다. 즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;CharTermAttribute 에는 토큰 한 개씩만 담기며&lt;/b&gt;&lt;/span&gt;, 다음 incrementToken 호출 때 다음 토큰이 담기기 때문에, 맨 위에서 clearAttribute() 라는 함수를 호출해 주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과적으로 최초 호출시 (isProcessing=true) Tokenizer 는 토큰화 대상 text 를 Tokenizer 부모 클래스의 Reader 로 부터 읽어오게 되고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;각자 토큰화(개발자가 직접 개발)&lt;/b&gt;를 하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Queue 에 저장&lt;/b&gt;&lt;/span&gt;을 한다. 이후 호출시에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Queue 에 있는 Token 들을 하나씩 꺼내면서 CTA에 담고 return true 하는 방식으로 개발&lt;/b&gt;&lt;/span&gt;하면 Custom Tokenizer 는 완성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;reset()&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;말&amp;nbsp;그대로&amp;nbsp;초기화를&amp;nbsp;담당한다.&amp;nbsp;TokenStream&amp;nbsp;은&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Analyzer&amp;nbsp;가&amp;nbsp;사용될때마다&amp;nbsp;인스턴스를&amp;nbsp;재활용&lt;/b&gt;&lt;/span&gt;하는데,&amp;nbsp;이&amp;nbsp;때&amp;nbsp;이전&amp;nbsp;상태로부터&amp;nbsp;초기화를&amp;nbsp;해주기&amp;nbsp;위해,&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&quot;TokenStream&amp;nbsp;사용&amp;nbsp;전&quot;에 자동으로 호출&lt;/span&gt;된다.&amp;nbsp;Custom&amp;nbsp;Tokenizer&amp;nbsp;에서&amp;nbsp;사용한&amp;nbsp;자원들을&amp;nbsp;초기화해주면&amp;nbsp;된다.&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;super.reset()&amp;nbsp;도&amp;nbsp;당연히&amp;nbsp;필수&lt;/b&gt;&lt;/span&gt;이며, Reader input 객체 (요청 input 읽는 친구) 를 초기화 해주기도 한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;clearAttribute 과 분리되어 있는 이유는 생각해보면 알 수 있는데, reset 함수는 TokenStream 이 사용되는 시점에 호출되지만, clearAttribute 은 Token 이 하나씩 Filtering 될 때마다 호출되기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Tokenizer Elasticsearch 에 넣기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Elasticsearch 에 plugin 설치하기&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;AnalysisPlugin 코드 추가&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 개발했으니 Elasticsearch 에 넣어줄 차례이다. Elasticsearch 는 plugin 형태로 customize 한 역할체들을 넣어줄 수 있다 (analyzer, similarity, tokenizer 모두 동일). Tokenizer 같은 경우는 프로젝트 내 다음과 같이 Plugin 선언을 해주면 되는데, 이 부분은 Elasticsearch 에게 알려주는 거라고 생각하고 넘어가자.. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754817972033&quot; class=&quot;scala&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyCustomPlugin extends Plugin implements AnalysisPlugin {
    @Override
    public Map&amp;lt;String, AnalysisProvider&amp;lt;TokenizerFactory&amp;gt;&amp;gt; getTokenizers() {
        return Map.of(
                &quot;mooncake_tokenizer&quot;, // 이 부분은 쿼리에 쓰일 text 
                (IndexSettings indexSettings, Environment environment, String name, Settings settings) -&amp;gt;
                        TokenizerFactory.newFactory(name, MooncakeTokenizer::new)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;인프라 적용 (Elasticsearch 재기동)&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ES에 plugin 들을 넣어주기 위해선&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;u&gt;1) Elasticsearch 용 Plugin 형태로 압축&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;2) Elasticsearch 의 bin 폴더에 .zip 이 있는 상태로 ES 재기동&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;이렇게 두 단계로 진행되면 된다. 압축은 폴더 자체를 압축하면 되는데 해당 폴더 안에 개발한 내용을 빌드한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;jar 파일&lt;/b&gt;&lt;/span&gt;을 두고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;descriptor properties 파일&lt;/b&gt;&lt;/span&gt;을 만들어주면 된다. (zip 파일 구성 때문에 꽤나 애먹은 구간)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;# plugin-descriptor.properties 파일&lt;/i&gt;&lt;br /&gt;name=mooncake-tokenizer&lt;br /&gt;description=This is tokenizer, not analyzer.&lt;br /&gt;version=0.0.1&lt;br /&gt;elasticsearch.version=8.17.4&lt;br /&gt;java.version=17&lt;br /&gt;classname=com.mooncake.MyCustomPlugin&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 구성은 Elasticsearch 가 저장할 메타데이터들로, 요청에 대한 응답에 표기되기도 하기 때문에 형식을 잘 지켜줘야 한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;필수 항목이 없으면 plugin 설치 실패&lt;/span&gt;한다. 이후 다음과 같이 진행하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ ls ~/custom-tokenizer&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;plugin-descriptor.properties&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;mooncake-tokenizer.jar&lt;br /&gt;---&lt;br /&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;$ zip -r ../mooncake-tokenizer.zip .&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/b&gt;--&amp;nbsp; &amp;nbsp;외부에 해당 폴더 전체를 압축한 mooncake-tokenizer.zip 을 생성&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/i&gt;---&lt;br /&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;$ unzip -l ../mooncake-tokenizer.zip&lt;/b&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; --&amp;nbsp; 압축한 폴더 내용 출력으로 확인 가능&lt;/i&gt;&lt;br /&gt;Archive:&amp;nbsp; mooncake-tokenizer.zip&lt;br /&gt;&amp;nbsp;&amp;nbsp;Length&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Date&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Time&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Name&lt;br /&gt;---------&amp;nbsp;&amp;nbsp;----------&amp;nbsp;-----&amp;nbsp;&amp;nbsp;&amp;nbsp;----&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;169&amp;nbsp;&amp;nbsp;2025-07-09&amp;nbsp;13:49&amp;nbsp;&amp;nbsp;&amp;nbsp;plugin-descriptor.properties&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;14652&amp;nbsp;&amp;nbsp;2025-08-01 14:42&amp;nbsp; mooncake-tokenizer.jar&lt;br /&gt;---------&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;-------&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;14821&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2&amp;nbsp;files&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;위와 같이 zip 파일을 준비했다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;해당 zip 파일을 Elasticsearch 설정 /bin 폴더에 넣어주고 기동&lt;/b&gt;&lt;/span&gt;시키면 된다. 만약 이미지를 사용해 ES를 사용하고 있다면, 다음과 같이 Dockerfile 을 작성해주고, 컨테이너를 기동시키면 Elasticsearch 에 성공적으로 plugin 을 설치할 수 있다. 이후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;이미지 생성하면 성공적으로 plugin 설치된 로그를 확인&lt;/span&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;FROM&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;elasticsearch/elasticsearch:{ver}&lt;br /&gt;&lt;br /&gt;&lt;b&gt;COPY&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{경로}/mooncake-tokenizer.zip&amp;nbsp;/tmp/&lt;br /&gt;&lt;br /&gt;&lt;b&gt;RUN&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bin/elasticsearch-plugin&amp;nbsp;install&amp;nbsp;file:///tmp/mooncake-tokenizer.zip&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Test 해보기&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Elasticsearch 에 요청(Kibana DevTools)을 보내서 설치된 플러그인을 확인할 수 있고, 특정 텍스트를 개발한 토크나이저를 통해 토큰화시켜볼 수 있다. 만약 둘 중 하나라도 응답하지 않는다면 제대로 설치된 것이 아니니 확인이 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ GET&amp;nbsp; /_cat/plugins&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;---&amp;nbsp; &amp;nbsp;HTTP 요청&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&lt;br /&gt;RESP:&lt;br /&gt;{노드-ID}&amp;nbsp;mooncake-tokenizer&amp;nbsp;0.0.1&lt;br /&gt;{노드-ID}&amp;nbsp;another-plugin&amp;nbsp;0.3.3&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;$ GET&amp;nbsp; /_analyze&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;tokenizer&quot;:&amp;nbsp;&quot;mooncake_tokenizer&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;text&quot;:&amp;nbsp;&quot;테스트&amp;nbsp;해볼&amp;nbsp;텍스트!&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-10 오후 6.29.14.png&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUMgOS/btsPPk7YJPW/PIfpFdauR6VvuJGFbrV7SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUMgOS/btsPPk7YJPW/PIfpFdauR6VvuJGFbrV7SK/img.png&quot; data-alt=&quot;확장에 열린 Elasticsearch 덕분에 도메인 특화 검색이 가능해진다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUMgOS/btsPPk7YJPW/PIfpFdauR6VvuJGFbrV7SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUMgOS%2FbtsPPk7YJPW%2FPIfpFdauR6VvuJGFbrV7SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;427&quot; height=&quot;326&quot; data-filename=&quot;스크린샷 2025-08-10 오후 6.29.14.png&quot; data-origin-width=&quot;427&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;확장에 열린 Elasticsearch 덕분에 도메인 특화 검색이 가능해진다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>인프라 기술/Elasticsearch</category>
      <category>analyzer</category>
      <category>custom tokenizer</category>
      <category>decorator</category>
      <category>descriptor</category>
      <category>elasticsearch</category>
      <category>Lucene</category>
      <category>TokenFilter</category>
      <category>tokenizer</category>
      <category>TokenStream</category>
      <category>일레스틱서치</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/325</guid>
      <comments>https://mooncake1.tistory.com/325#entry325comment</comments>
      <pubDate>Sun, 10 Aug 2025 18:27:42 +0900</pubDate>
    </item>
    <item>
      <title>[Elasticsearch 기본] - 2. Search 쿼리와 활용 가능 기능들</title>
      <link>https://mooncake1.tistory.com/320</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;398&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBGCw0/btsPLNKPYda/7I79mcFgChlQer67L0H12k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBGCw0/btsPLNKPYda/7I79mcFgChlQer67L0H12k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBGCw0/btsPLNKPYda/7I79mcFgChlQer67L0H12k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBGCw0%2FbtsPLNKPYda%2F7I79mcFgChlQer67L0H12k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;398&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;398&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Elasticsearch 의 Search 쿼리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Elasticsearch 검색의 핵심 쿼리 3대장&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;Elasticsearch&amp;nbsp;에&amp;nbsp;analyzer&amp;nbsp;들을&amp;nbsp;정의하여&amp;nbsp;index&amp;nbsp;를&amp;nbsp;생성하는&amp;nbsp;것까지&amp;nbsp;살펴봤고,&amp;nbsp;이제는&amp;nbsp;elasticsearch의&amp;nbsp;꽃인&amp;nbsp;검색&amp;nbsp;쿼리에&amp;nbsp;대해서&amp;nbsp;살펴보자.&amp;nbsp;기본적으로&amp;nbsp;ES에게&amp;nbsp;특정&amp;nbsp;index&amp;nbsp;내&amp;nbsp;검색을&amp;nbsp;요청하는&amp;nbsp;쿼리는&amp;nbsp;다음과&amp;nbsp;같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/{index_name}/_search&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;위와&amp;nbsp;같이&amp;nbsp;날리면&amp;nbsp;index&amp;nbsp;내의&amp;nbsp;모든&amp;nbsp;필드를&amp;nbsp;조회해준다.&amp;nbsp;기본&amp;nbsp;Pagination&amp;nbsp;이&amp;nbsp;적용되어&amp;nbsp;offset&amp;nbsp;0,&amp;nbsp;limit&amp;nbsp;10&amp;nbsp;으로&amp;nbsp;적용된다.&amp;nbsp;하지만&amp;nbsp;우리는&amp;nbsp;쿼리&amp;nbsp;형태의&amp;nbsp;요청을&amp;nbsp;날릴&amp;nbsp;것이기&amp;nbsp;때문에,&amp;nbsp;다음&amp;nbsp;쿼리들에&amp;nbsp;대해서&amp;nbsp;알아둬야&amp;nbsp;한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;match 쿼리 ⭐&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;원하는&amp;nbsp;내용이&amp;nbsp;포함된&amp;nbsp;모든&amp;nbsp;데이터를&amp;nbsp;조회하는,&amp;nbsp;Analyzer&amp;nbsp;를&amp;nbsp;사용하는&amp;nbsp;유연한&amp;nbsp;검색&amp;nbsp;쿼리이다.&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;match&amp;nbsp;쿼리는&amp;nbsp;&quot;text&quot;&amp;nbsp;타입의&amp;nbsp;필드에&amp;nbsp;대해서&amp;nbsp;적용을&amp;nbsp;해야&amp;nbsp;원하는대로&amp;nbsp;동작&lt;/b&gt;&lt;/span&gt;하며,&amp;nbsp;검색어를&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&quot;검색&amp;nbsp;Analyzer&quot;로&amp;nbsp;토큰화&lt;/b&gt;&lt;/span&gt;하여,&amp;nbsp;input&amp;nbsp;token&amp;nbsp;들을&amp;nbsp;사용하여&amp;nbsp;해당&amp;nbsp;token&amp;nbsp;들과&amp;nbsp;관련된&amp;nbsp;Document&amp;nbsp;들을&amp;nbsp;찾아준다.&amp;nbsp;또한,&amp;nbsp;match&amp;nbsp;쿼리는&amp;nbsp;이&amp;nbsp;찾은&amp;nbsp;Document&amp;nbsp;들에&amp;nbsp;대해&amp;nbsp;Score&amp;nbsp;을&amp;nbsp;해주는&amp;nbsp;시스템을&amp;nbsp;적용한다&amp;nbsp;(관련도가&amp;nbsp;높을&amp;nbsp;수록&amp;nbsp;점수가&amp;nbsp;높다.&amp;nbsp;내부적으로&amp;nbsp;BM25&amp;nbsp;함수를&amp;nbsp;사용한다.&amp;nbsp;사용했던&amp;nbsp;모습은&amp;nbsp;(ES기본1편)&amp;nbsp;의&amp;nbsp;Analzyer&amp;nbsp;를&amp;nbsp;소개하는&amp;nbsp;부분에서&amp;nbsp;간단히&amp;nbsp;소개되었다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;term 쿼리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;term 쿼리는 LIKE 나 = 를 활용한 쿼리라고 생각하면 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;text&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;외&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모든 필드에 대해 &quot;정확히 일치하는 값&quot;을 검색&lt;/b&gt;&lt;/span&gt;하게 된다. 따라서 문자열에 대해서 유연한 검색을 제공할 필요가 없는 값들 (ex: 비밀번호, 상품코드 등) 은 keyword type 으로 선언을 하게되고, 이 keyword 타입들은 term 쿼리를 사용해서 검색할 수 있는 것이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;참고로 term 쿼리는 keyword 만을 위함이 아닌, 'text 타입 외 모든 타입'의 검색에 사용된다. boolean 필드의 true/false, integer 필드의 숫자 일치 값 등등 모두 사용된다. boards 라는 게시판 index 안에 isActive 란 필드가 있다고 해보자. 다음과 같이 쿼리를 날려볼 수 있다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isActive&quot;:&amp;nbsp;false&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;br /&gt;---&lt;br /&gt;&lt;i&gt;&lt;b&gt;$ GET /boards/_search&amp;nbsp; &amp;nbsp; ------ X 틀림&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;writer&quot;:&amp;nbsp;&quot;mooncake&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;isActive&quot;:&amp;nbsp;false&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;위에서&amp;nbsp;1번과&amp;nbsp;같이,&amp;nbsp;이&amp;nbsp;쿼리는&amp;nbsp;게시판&amp;nbsp;index&amp;nbsp;내&amp;nbsp;&quot;비활성화된&quot;&amp;nbsp;(보이지&amp;nbsp;않는)&amp;nbsp;게시물들을&amp;nbsp;찾아보는&amp;nbsp;쿼리로&amp;nbsp;term&amp;nbsp;쿼리를&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;만약&amp;nbsp;여러개의&amp;nbsp;값&amp;nbsp;중&amp;nbsp;하나라도&amp;nbsp;일치하는게&amp;nbsp;있는지&amp;nbsp;조회하는&amp;nbsp;IN절&amp;nbsp;조회는,&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;term&amp;nbsp;이&amp;nbsp;아닌&amp;nbsp;terms&amp;nbsp;를&amp;nbsp;사용해서&amp;nbsp;array&amp;nbsp;를&amp;nbsp;전달&lt;/b&gt;&lt;/span&gt;하면 된다. 하지만, 2 번처럼 여러 필드에 대한 조건을 나열할 수는 없을까? 정답은 안 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;한 가지 이상의 조건을 적용하는 AND절 활용은, 다음 bool 쿼리를 사용&lt;/b&gt;&lt;/span&gt;해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;bool&amp;nbsp;쿼리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Bool 쿼리는 거의 필수적으로 사용되는 쿼리이다. 여러가지 조건을 나열하게 해주며, 내부적으로 여러 동작성을 제공해 줄 수 있는 다양한 부가 조건절들을 사용할 수 있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Bool 쿼리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;bool&amp;nbsp;쿼리는&amp;nbsp;한&amp;nbsp;검색&amp;nbsp;쿼리&amp;nbsp;내에서&amp;nbsp;여러&amp;nbsp;조건들을&amp;nbsp;조합하기&amp;nbsp;위해서&amp;nbsp;사용한다.&amp;nbsp;한&amp;nbsp;개&amp;nbsp;이상의&amp;nbsp;조건이&amp;nbsp;필요할시&amp;nbsp;바로&amp;nbsp;bool&amp;nbsp;쿼리를&amp;nbsp;생각하면&amp;nbsp;되고,&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;정말&amp;nbsp;99%&amp;nbsp;상황에서&amp;nbsp;사용할&amp;nbsp;쿼리&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;bool&amp;nbsp;쿼리&amp;nbsp;안에는&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;4가지&amp;nbsp;조건절이&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;filter 절&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;bool&quot;&amp;nbsp;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&quot;category&quot;&amp;nbsp;:&amp;nbsp;&quot;자유게시판&quot;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;range&quot;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&quot;created_at&quot;&amp;nbsp;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&quot;gte&quot;:&amp;nbsp;&quot;2024-01-01&quot;&amp;nbsp;&amp;nbsp;}&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;filter&amp;nbsp;절은&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;반드시&amp;nbsp;만족해야&amp;nbsp;하는&amp;nbsp;조건을&amp;nbsp;나열&lt;/b&gt;&lt;/span&gt;하기&amp;nbsp;위해&amp;nbsp;사용된다.&amp;nbsp;filter&amp;nbsp;는&amp;nbsp;말&amp;nbsp;그대로&amp;nbsp;&quot;정확한&amp;nbsp;해당&amp;nbsp;값&quot;을&amp;nbsp;필터링&amp;nbsp;하는&amp;nbsp;역할이며,&amp;nbsp;내부&amp;nbsp;캐싱이&amp;nbsp;동작하기&amp;nbsp;때문에&amp;nbsp;성능&amp;nbsp;최적화에&amp;nbsp;도움을&amp;nbsp;준다.&amp;nbsp;내부적으로&amp;nbsp;term&amp;nbsp;쿼리를&amp;nbsp;많이&amp;nbsp;사용한다.&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;filter&amp;nbsp;절은&amp;nbsp;&lt;u&gt;&quot;유연한&amp;nbsp;검색&quot;에&amp;nbsp;제공되는&amp;nbsp;점수에&amp;nbsp;아무&amp;nbsp;영향&lt;/u&gt;을&amp;nbsp;주지&amp;nbsp;않는다&lt;/b&gt;&lt;/span&gt;. 따라서, 유연한 검색을 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;text 타입 필드들에 대해서는 filter 절을 사용하지 않는다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;must 절&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;bool&quot;&amp;nbsp;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;must&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&quot;content&quot;&amp;nbsp;:&amp;nbsp;&quot;통신&amp;nbsp;기기&amp;nbsp;매장&quot;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&quot;title&quot;&amp;nbsp;:&amp;nbsp;&quot;Apple&amp;nbsp;2025&quot;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;must&amp;nbsp;절은&amp;nbsp;filter&amp;nbsp;절과&amp;nbsp;같이&amp;nbsp;쿼리와&amp;nbsp;&quot;반드시&amp;nbsp;만족&quot;하는&amp;nbsp;데이터를&amp;nbsp;찾아주지만,&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;점수&amp;nbsp;계산에&amp;nbsp;사용&lt;/b&gt;&lt;/span&gt;된다. 즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;유연한 검색에 사용되는 쿼리&lt;/b&gt;&lt;/span&gt;임이 가장 큰 차이가 있으며, text 타입을 위한 조건절임을 알 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&quot;text&quot;타입, &quot;match&quot; 쿼리, &quot;bool-must&quot; 쿼리는 형제&lt;/span&gt;&lt;/b&gt;들이라고 생각하면 된다. filter 절과 must 절은 합쳐서도 많이 사용한다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;예시를 한 번 들어보면, 자유게시판 내에서 &quot;검색 엔진&quot;과 관련된 글을 찾고 싶다. 이 때, 카테고리는 완전한 일치 조건으로, 제목은 유연한 검색으로, 그 중 공지에서 내려간 글에서 찾고 싶은 경우이다. 한 가지 이상의 조건이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;bool 쿼리&lt;/b&gt;, 유연한 검색과 정확한 일치 검색을 같이 사용해야 하니까&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;must &amp;amp; filter 절&lt;/b&gt;을 통해 다음과 같이 쿼리를 구성할 수 있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;bool&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;must&quot;:&amp;nbsp;[&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;:&quot;검색엔진&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;category&quot;:&quot;자유&amp;nbsp;게시판&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;is_notice&quot;:&amp;nbsp;false&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;must_not 절&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;bool&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;must_not&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;category&quot;:&amp;nbsp;&quot;광고&amp;nbsp;게시판&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;&amp;nbsp;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;is_notice&quot;:&amp;nbsp;true&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;must_not&amp;nbsp;절은&amp;nbsp;filter&amp;nbsp;의&amp;nbsp;반대&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;명시된&amp;nbsp;조건만&amp;nbsp;정확히&amp;nbsp;&lt;b&gt;&quot;제외&quot;한&amp;nbsp;필드들을&amp;nbsp;검색&lt;/b&gt;한다.&amp;nbsp;이름&amp;nbsp;때문에&amp;nbsp;헷갈릴&amp;nbsp;수&amp;nbsp;있는데,&amp;nbsp;유연한&amp;nbsp;검색&amp;nbsp;및&amp;nbsp;점수와&amp;nbsp;아무&amp;nbsp;상관&amp;nbsp;없고,&amp;nbsp;must&amp;nbsp;와도&amp;nbsp;먼&amp;nbsp;사이인&amp;nbsp;것을&amp;nbsp;확실히&amp;nbsp;알자.&amp;nbsp;위처럼&amp;nbsp;검색하면,&amp;nbsp;광고&amp;nbsp;게시판이&amp;nbsp;아니면서&amp;nbsp;공지된&amp;nbsp;모든&amp;nbsp;게시물을&amp;nbsp;가져오는&amp;nbsp;쿼리이고,&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;유연한&amp;nbsp;검색을&amp;nbsp;전혀&amp;nbsp;사용하지&amp;nbsp;않는&amp;nbsp;일치&amp;nbsp;여부&amp;nbsp;검색&amp;nbsp;쿼리&lt;/span&gt;&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;should 절&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;should 절은 위 세가지 절들과 성격이 조금 다른데, 한마디로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&quot;있으면 좋고 없으면 말고&quot;의 느낌&lt;/b&gt;&lt;/span&gt;이다. should 절은 유연한 검색을 위한 절이며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;score 에 가산점을 부여하기 위한 조건&lt;/span&gt;&lt;/b&gt;이다. 가령, 최근 출시된 제품, 평점이 좋은 제품, 광고 브랜드 제품 등을 상위에 노출시키는 요구사항을 해결할 때 좋은 조건이다. 내가 원하는 조건에 가산점을 주고 싶을 때, should 쿼리를 생각하면 된다. 따라서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;다양한 조건을 score system 과 연계시켜주기 위한 절&lt;/b&gt;&lt;/span&gt;이기도 하며, text 타입 외 타입들을 사용할 수 있는 절이다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;사용자가&amp;nbsp;상품을&amp;nbsp;검색하는데,&amp;nbsp;키워드로&amp;nbsp;관련&amp;nbsp;데이터를&amp;nbsp;조회한다고&amp;nbsp;해보자.&amp;nbsp;이&amp;nbsp;때,&lt;b&gt;&amp;nbsp;평점이&amp;nbsp;높고&amp;nbsp;좋아요&amp;nbsp;수가&amp;nbsp;많은&amp;nbsp;상품이&amp;nbsp;우선적으로&amp;nbsp;노출&lt;/b&gt;되게끔&amp;nbsp;해보자.&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;4.5점&amp;nbsp;이상,&amp;nbsp;100개의&amp;nbsp;좋아요&amp;nbsp;이상에&amp;nbsp;가산점을&amp;nbsp;주는&amp;nbsp;쿼리를&amp;nbsp;짜볼&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/products/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;bool&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;must&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;:&amp;nbsp;&quot;무선&amp;nbsp;이어폰&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;should&quot;: [&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;-------------&amp;nbsp; 평점이 4.5이상, 좋아요 100 이상이면 가산점, 없어도 괜찮다&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;range&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;rating&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;gte&quot;:&amp;nbsp;4.5&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;range&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;likes&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;gte&quot;:&amp;nbsp;100&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;결과를&amp;nbsp;살펴본다면,&amp;nbsp;4.5&amp;nbsp;이상의&amp;nbsp;평점과&amp;nbsp;100개의&amp;nbsp;좋아요&amp;nbsp;수를&amp;nbsp;넘는&amp;nbsp;상품들이&amp;nbsp;상단에&amp;nbsp;많이&amp;nbsp;배치되어&amp;nbsp;있는&amp;nbsp;것을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;지금까지의&amp;nbsp;절들과의&amp;nbsp;차이는,&amp;nbsp;4.5이하와&amp;nbsp;평점이&amp;nbsp;100개가&amp;nbsp;&lt;b&gt;안되어도&amp;nbsp;검색이&amp;nbsp;되며&lt;/b&gt;,&amp;nbsp;score&amp;nbsp;에&amp;nbsp;가산점을&amp;nbsp;얻지&amp;nbsp;못한채&amp;nbsp;검색된다는&amp;nbsp;차이가&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고로 위에서 사용된 쿼리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;range 쿼리&lt;/b&gt;&lt;/span&gt;이다. 기간이나 시간 등&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;범위에 대해 BETWEEN 절&lt;/b&gt;&lt;/span&gt;을 넣을 수 있다. 위처럼 should 절에 넣을 수 있지만, 기본적으론 당연히 filter 절에서 동작하며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;gte / lte / gt / lt 로 사용 가능&lt;/b&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런식으로 서브 쿼리들은 정말 많지만, 주된 쿼리들만 소개하셨다. 실제 서비스 구현하면서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;훨씬 복잡한 쿼리들을 제어&lt;/b&gt;해야 하는데, 모두 다 외울 수 없으니&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 때 그 때 가능한 쿼리들을 알아보고 조합&lt;/b&gt;해보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Elasticsearch 에서 활용 빈도가 높은 다양한 기능들&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Fuzziness&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리가 일상생활에서 검색을 하다보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;오타가 나도 잘 검색이 되는 모습&lt;/b&gt;&lt;/span&gt;을 볼 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;fuzziness&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 이를 지원해주는 elasticsearch의 유용한 기능 중 하나이다. 이 부분은 딥하게 다루지는 않지만, 다음과 같이 fuzziness 옵션을 두면, 오타가 발생해도 elasticsearch 란 키워드에 대해서 정상적으로 검색해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;: &quot;elastiksearch&quot;,&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;i&gt;&lt;b&gt;-----------&amp;nbsp; 오타가 났어도 fuzziness 옵션 덕분에 검색된다&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;fuzziness&quot;:&amp;nbsp;&quot;AUTO&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;multi_match&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;여러 필드에 대해 match 쿼리를 사용하고 싶을 때 사용하는 기능&lt;/b&gt;&lt;/span&gt;이다. 처음 봤을 때 &quot;그럼 bool 쿼리에 match 두 개 넣으면 된다&quot;고 생각할 수 있지만, 차이가 있다. bool 쿼리에 match 두 개를 넣는 것은, 각기 다른 조건으로 사용하는 것이다. A 필드에 &quot;hello&quot;, B 필드에 &quot;yellow&quot; 를 요청하는 것이다. 하지만&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;multi_match 는 A, B 필드에 모두 &quot;hello&quot; 를 검색하도록 요청&lt;/b&gt;하는 것이다.&amp;nbsp;&amp;nbsp;따라서 점수를 선정하는 방식도 다를 것이다. 다음과 같이 데이터가 들어가 있다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;(1) title&lt;/b&gt;:&amp;nbsp;엘라스틱&amp;nbsp;서치&amp;nbsp;적용&amp;nbsp;후기&amp;nbsp;/&amp;nbsp;&lt;b&gt;content&lt;/b&gt;:&amp;nbsp;엘라스틱&amp;nbsp;서치&amp;nbsp;후기&amp;nbsp;공유합니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(2) title&lt;/b&gt;:&amp;nbsp;검색&amp;nbsp;엔진&amp;nbsp;사례&amp;nbsp;/&amp;nbsp;&lt;b&gt;content&lt;/b&gt;:&amp;nbsp;엘라스틱&amp;nbsp;서치가&amp;nbsp;너무&amp;nbsp;좋아요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;(3) title&lt;/b&gt;:&amp;nbsp;레디스&amp;nbsp;캐시&amp;nbsp;사용기&amp;nbsp;/&amp;nbsp;&lt;b&gt;content&lt;/b&gt;:&amp;nbsp;캐시&amp;nbsp;시스템도&amp;nbsp;쓸&amp;nbsp;수&amp;nbsp;있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;multi_match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;&quot;엘라스틱서치&amp;nbsp;적용&amp;nbsp;후기&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;fields&quot;: [&quot;title&quot;, &quot;content&quot;]&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;i&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;---------- 위 검색어를 여러 필드에서 한 번에 검색&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이와 같이 쿼리를 날리면, (1)과 (2)이 순서대로 검색이 되고, 3번은 검색되지 않을 것이다. 위와 같이 두 text 타입 필드에 대해 모두 query 문에 대한 탐색을 허용해준다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;multi_match 쿼리는 가중치도 부여&lt;/b&gt;&lt;/span&gt;할 수 있다. 만약 위 사례에서 content 에 대해 두 배의 점수를 부여하고 싶다면, &quot;fields&quot;:[ &quot;title&quot;, &quot;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;content^2&lt;/b&gt;&lt;/span&gt;&quot;] 로 명시하면 되고, 이렇게 하면 순서에 영향을 줄 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;highlight 기능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;많은 검색 사이트를 보면 검색한 키워드에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;bold 처리가 되거나 노란색으로 하이라이트 되어서 '이 컨텐츠가 왜 검색되었는지'를 알려주는 역할&lt;/span&gt;을 해준다. 이런 기능도 Elasticsearch의 highlight 기능을 사용해서 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ GET /boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp; &quot;query&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;multi_match&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;query&quot;: &quot;엘라스틱 서치 적용 후기&quot;,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;fields&quot;: [&quot;title&quot;, &quot;content&quot;]&lt;br /&gt;&amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; },&lt;br /&gt;&amp;nbsp; &quot;highlight&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;fields&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;title&quot;:{&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;pre_tags&quot;: [&quot;&amp;lt;mark&amp;gt;&quot;],&amp;nbsp; &amp;nbsp;&lt;b&gt;&lt;i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;---------- title 필드에서 검색된 영역 앞뒤에 넣을 tag 들을 나열한다&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;post_tags&quot;: [&quot;&amp;lt;/mark&amp;gt;&quot;]&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; },&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;content&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;pre_tags&quot;: [&quot;&amp;lt;b&amp;gt;&quot;],&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;post_tags&quot;: [&quot;&amp;lt;/b&amp;gt;&quot;]&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; &amp;nbsp; }&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;i&gt;&lt;b&gt;-------- 결과 예시&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ...&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;_source&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;:&amp;nbsp;&quot;엘라스틱서치&amp;nbsp;적용&amp;nbsp;후기&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;:&amp;nbsp;&quot;회사&amp;nbsp;프로젝트에&amp;nbsp;엘라스틱서치를&amp;nbsp;적용한&amp;nbsp;후기를&amp;nbsp;공유합니다.&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;highlight&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;: [&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;i&gt;&lt;b&gt;-------- 각 토큰당 tag 를 달아주는 모습 확인&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;&amp;lt;mark&amp;gt;엘라스틱&amp;lt;/mark&amp;gt;&amp;lt;mark&amp;gt;서치&amp;lt;/mark&amp;gt;&amp;nbsp;&amp;lt;mark&amp;gt;적용&amp;lt;/mark&amp;gt;&amp;nbsp;&amp;lt;mark&amp;gt;후기&amp;lt;/mark&amp;gt;&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;],&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;...&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이처럼 웹 프런트에 바로 html 랜더리을 해줘야 하거나, 다른 서버에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&quot;검색 결과&quot;를 인지할 수 있는 pointer 를 넣어줘야 할 때&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;유용하게 사용될 수 있다. 물론 위 응답 예시처럼 원문이 오리지날로 전달되고, highlight 필드에 적용되어 전달된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Pagination&amp;nbsp;&amp;amp;&amp;nbsp;Sorting&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;페이지네이션은&amp;nbsp;서버&amp;nbsp;부하를&amp;nbsp;막기&amp;nbsp;위해&amp;nbsp;매우&amp;nbsp;중요한&amp;nbsp;기능이며,&amp;nbsp;Elasticsearch&amp;nbsp;에서도&amp;nbsp;당연히&amp;nbsp;이를&amp;nbsp;지원한다.&amp;nbsp;또한,&amp;nbsp;유연한&amp;nbsp;검색은&amp;nbsp;기본적으로&amp;nbsp;&quot;점수&quot;로&amp;nbsp;sorting&amp;nbsp;이&amp;nbsp;되지만,&amp;nbsp;이&amp;nbsp;조건을&amp;nbsp;원하는대로&amp;nbsp;바꿀&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ GET /boards/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;title&quot;:&amp;nbsp;&quot;글&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;size&quot;:&amp;nbsp;3,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;from&quot;:&amp;nbsp;6,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;sort&quot;:[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;likes&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;order&quot;:&quot;desc&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;b&gt;size&amp;nbsp;는&amp;nbsp;limit&amp;nbsp;이고,&amp;nbsp;from&amp;nbsp;은&amp;nbsp;offset&amp;nbsp;&lt;/b&gt;이다.&amp;nbsp;기본값은&amp;nbsp;각각&amp;nbsp;10,&amp;nbsp;0&amp;nbsp;이다.&amp;nbsp;sort&amp;nbsp;는&amp;nbsp;위처럼&amp;nbsp;특정&amp;nbsp;필드에&amp;nbsp;대해&amp;nbsp;asc&amp;nbsp;/&amp;nbsp;desc&amp;nbsp;를&amp;nbsp;정의해주면&amp;nbsp;된다.&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;네&amp;nbsp;가지&amp;nbsp;사항만&amp;nbsp;알아두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;likes 처럼 필드명을 명시하지 않고 [&quot;likes&quot;,&quot;hello&quot;] 로 명시하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본 asc 정렬&lt;/b&gt;을 사용한다&lt;/li&gt;
&lt;li&gt;위&amp;nbsp;쿼리처럼&amp;nbsp;match&amp;nbsp;같이&amp;nbsp;유연한&amp;nbsp;검색을&amp;nbsp;사용하는&amp;nbsp;쿼리는&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;기본적으로&amp;nbsp;score&amp;nbsp;을&amp;nbsp;사용하여&amp;nbsp;내림차순&amp;nbsp;정렬&lt;/span&gt;한다.&amp;nbsp;위와&amp;nbsp;같이&amp;nbsp;sort&amp;nbsp;조건을&amp;nbsp;명시하면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;score&amp;nbsp;은&amp;nbsp;무시&lt;/b&gt;&lt;/span&gt;하게&amp;nbsp;된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;score&amp;nbsp;이&amp;nbsp;같은&amp;nbsp;경우&amp;nbsp;likes&amp;nbsp;로&amp;nbsp;정렬&lt;/span&gt;&lt;/b&gt;하라&amp;nbsp;하고&amp;nbsp;싶으면,&amp;nbsp;&lt;b&gt;&quot;_score&quot;&amp;nbsp;필드와&amp;nbsp;&quot;likes&quot;&amp;nbsp;필드를&amp;nbsp;둘다&amp;nbsp;desc&amp;nbsp;로&amp;nbsp;정의&lt;/b&gt;하면&amp;nbsp;된다&lt;/li&gt;
&lt;li&gt;score 이 같은 경우는 잘 없다. 따라서, 다른 필드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;score 에 반영하라&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하고 싶으면, 기본적으로 should 를 쓰면 된다. 하지만, should 는 정확한 점수 제어가 어렵기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;세부 control 을 위해선 functional score query 를 사용&lt;/b&gt;&lt;/span&gt;해야 한다 (알고만 있자)&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;multi-field&amp;nbsp;로&amp;nbsp;여러&amp;nbsp;타입&amp;nbsp;저장&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어떤&amp;nbsp;필드에&amp;nbsp;대해서는&amp;nbsp;&lt;b&gt;유연한&amp;nbsp;검색에도&amp;nbsp;사용하고&amp;nbsp;싶고,&amp;nbsp;정확한&amp;nbsp;일치에도&amp;nbsp;사용&lt;/b&gt;하고&amp;nbsp;싶다.&amp;nbsp;이런&amp;nbsp;경우가&amp;nbsp;꽤나&amp;nbsp;흔하다&amp;nbsp;(변형&amp;nbsp;데이터를&amp;nbsp;같이&amp;nbsp;저장해야&amp;nbsp;하는&amp;nbsp;경우).&amp;nbsp;이럴&amp;nbsp;경우&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Multi-Field&amp;nbsp;선언&lt;/b&gt;&lt;/span&gt;을&amp;nbsp;해줄&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;//&amp;nbsp;mapping&amp;nbsp;정의시&amp;nbsp;&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;...&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;category&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;text&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analyzer&quot;:&amp;nbsp;&quot;nori&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;fields&quot;:&amp;nbsp;{&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;i&gt;&lt;b&gt;-------&amp;nbsp;multi-field&amp;nbsp;선언&amp;nbsp;(이름과&amp;nbsp;타입&amp;nbsp;등을&amp;nbsp;정의)&amp;nbsp;&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;raw&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;keyword&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;위와&amp;nbsp;같이&amp;nbsp;선언을&amp;nbsp;해두면,&amp;nbsp;category&amp;nbsp;는&amp;nbsp;기본적으로&amp;nbsp;text&amp;nbsp;타입&amp;nbsp;필드에&amp;nbsp;토큰화되어서&amp;nbsp;저장되지만,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;keyword&amp;nbsp;타입으로도&amp;nbsp;&quot;category.raw&quot;&amp;nbsp;라는&amp;nbsp;다른&amp;nbsp;필드에&amp;nbsp;함께&amp;nbsp;저장&lt;/b&gt;&lt;/span&gt;된다.&amp;nbsp;따라서,&amp;nbsp;category&amp;nbsp;는&amp;nbsp;&quot;text&quot;&amp;nbsp;타입임에도&amp;nbsp;불구하고,&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;term&amp;nbsp;쿼리에&amp;nbsp;사용할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/products/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;term&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;category.raw&quot;:&amp;nbsp;&quot;특수&amp;nbsp;가전제품&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;multi-field&amp;nbsp;기능은&amp;nbsp;정말&amp;nbsp;유용하고&amp;nbsp;데이터&amp;nbsp;수집을&amp;nbsp;위해&amp;nbsp;정말&amp;nbsp;많이&amp;nbsp;사용되는&amp;nbsp;기능이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Search&amp;nbsp;as&amp;nbsp;you&amp;nbsp;type,&amp;nbsp;검색어&amp;nbsp;추천&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;쿠팡&amp;nbsp;같은&amp;nbsp;곳에서&amp;nbsp;검색을&amp;nbsp;하면,&amp;nbsp;검색을&amp;nbsp;할&amp;nbsp;때마다&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;아래&amp;nbsp;추천&amp;nbsp;검색어들이&amp;nbsp;여러&amp;nbsp;개&amp;nbsp;조회&lt;/b&gt;&lt;/span&gt;되는&amp;nbsp;것을&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;당연히&amp;nbsp;이&amp;nbsp;정도&amp;nbsp;상용&amp;nbsp;서비스에는&amp;nbsp;훨씬&amp;nbsp;깊은&amp;nbsp;레벨의&amp;nbsp;구현이&amp;nbsp;있겠지만,&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;elasticsearch&amp;nbsp;의&amp;nbsp;searcy_as_you_type&amp;nbsp;이라는&amp;nbsp;필드를&amp;nbsp;사용해서&amp;nbsp;적용&lt;/b&gt;&lt;/span&gt;해볼&amp;nbsp;수도&amp;nbsp;있다.&amp;nbsp;이&amp;nbsp;타입은&amp;nbsp;&lt;b&gt;자동완성을&amp;nbsp;위해서&amp;nbsp;구현&lt;/b&gt;된&amp;nbsp;타입으로,&lt;b&gt;&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;t&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;e&lt;/span&gt;xt&amp;nbsp;타입처럼&amp;nbsp;Analyzer를&amp;nbsp;거쳐&amp;nbsp;토큰으로&amp;nbsp;분리된다.&amp;nbsp;이&amp;nbsp;타입은&amp;nbsp;multi&amp;nbsp;field&amp;nbsp;로&amp;nbsp;_2gram,&amp;nbsp;_3gram&amp;nbsp;을&amp;nbsp;같이&amp;nbsp;자동으로&amp;nbsp;만들어준다&lt;/b&gt;&lt;/span&gt;. 이는 토큰화 결과에서 2, 3개의 토큰을 묶어서 멀티 필드로 저장하는 기능을 말한다. 만약&amp;nbsp;&amp;nbsp;nori analyzer 가 적용된 필드에 search_as_you_type 을 선언하고, &quot;프리미엄 감귤 선물 세트&quot;를 저장한다면, 다음과 같이 토큰화해서 각 필드에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;name&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: &quot;프리미엄&quot; / &quot;감귤&quot;/ &quot;선물&quot; / &quot;세트&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;name._2gram&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: &quot;프리미엄 감귤&quot; / &quot;감귤 선물&quot; / &quot;선물 세트&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;name._3gram&quot;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: &quot;프리미엄 감귤 선물&quot; / &quot;감귤 선물 세트&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;유저가&amp;nbsp;검색어를&amp;nbsp;입력함에&amp;nbsp;따라서&amp;nbsp;(미입력&amp;nbsp;0.5초&amp;nbsp;단위),&amp;nbsp;다음과&amp;nbsp;같이&amp;nbsp;쿼리를&amp;nbsp;계속&amp;nbsp;날리고,&amp;nbsp;연관된&amp;nbsp;데이터들을&amp;nbsp;계~속&amp;nbsp;렌더링&amp;nbsp;해주는&amp;nbsp;방식으로&amp;nbsp;자동&amp;nbsp;완성을&amp;nbsp;구현해볼&amp;nbsp;수&amp;nbsp;있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$&amp;nbsp;GET&amp;nbsp;/products/_search&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;multi_match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:&amp;nbsp;&quot;___&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;bool_prefix&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;fields&quot;: [&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;i&gt;-----------&amp;nbsp; 검색하고 있는 내용에 대해&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;준비해둔 완성 조합으로 연관 검색어를 모두 조회&amp;nbsp;&lt;/b&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name._2gram&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name._3gram&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;위에서 배웠던 multi_match 쿼리와, multi_field에 대한 쿼리를 모두 사용하고 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;bool_prefix( )&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;u&gt;앞 쪽 단어는 match 조건&lt;/u&gt;&lt;/i&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;u&gt;마지막 단어는 입력 중인 prefix match 조건&lt;/u&gt;&lt;/i&gt;을 뜻한다 (이에 대해 딥하게 들어가진 않음). 가령,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;you have th&quot; 라고 검색 중이라면&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;앞쪽 단어인 you / have 는 토큰 검색을 하고 (기존 text 검색), 뒤쪽 단어인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;th는 th 로 시작하는 토큰&lt;/i&gt;을 찾는다 (일단 이 형태로 외우고 써도 된다. 예외가 발생하는 경우들도 생각나긴 하는데 일단 무시하자).&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;위 쿼리는 실제로 실무에서 많이 구현단에서 사용하기도 하는 쿼리이며, 당연히 서비스별로 최적화가 보통 진행되지만 기본적인 구현을 위한 뼈대이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;  참고 -&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;n_gram 과 shingle&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 대해&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;shingle 은 단어 기반으로, 연속된 단어를 묶어서 멀티 필드에 대해 토큰들을 저장&lt;/b&gt;&lt;/span&gt;하는 것이다. 즉, search_as_you_type 은 _2gram, _3gram 이라는 멀티 필드를 사용하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사실 내부적으로 shingle filter 를 사용&lt;/b&gt;한다. 그리고 shingle filter 만 사용하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;멀티 필드를 자동으로 만들어 주지 않기&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;때문에, multi_field 생성 기능까지 포함된 기능이라 볼 수 있다. shingle filter 를 원래 사용한다면 다음과 같이 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ PUT /{index_name}&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;settings&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analysis&quot;: {&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;i&gt;&lt;b&gt;------- analyzer 에 적용할 필터 정의&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;custom_shingle_filter&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;shingle&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;min_shingle_size&quot;: 2,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;---- 두 단어의 조합부터&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;max_shingle_size&quot;: 3,&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;---- 세 단어의 조합까지 만든다&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;output_unigrams&quot;:&amp;nbsp;false&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;.....&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;--------- 이후 analyzer 및 필드에 적용&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;해당 analyzer 를 쓰는 필드에 &quot;hello banana world&quot; 가 전달되었다고 하면,&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[&quot;hello banana&quot;, &quot;banana world&quot;, &quot;hello banana world&quot;] 토큰&lt;/b&gt;&lt;/span&gt;들이 같이 저장되며, 이게 shingle의 기능이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;output_unigram 을 true&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 하면 &quot;hello&quot;, &quot;banana&quot;, &quot;world&quot;세 가지 토큰도 함께 추가된다. 만약 search_as_you_type 처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;원본이 아닌 멀티 필드에 shingle 저장을 하고 싶으면, 당연히 멀티 필드 선언&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;후 해당 필드 analyzer 로 적용하면 된다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;그렇다면 search_as_you_type 에서 등장한 gram 은 무엇일까?&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;n_gram 은 원래 문자 기반&lt;/b&gt;&lt;/span&gt;이다. 즉, banana 에 대한 2_gram 저장은,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;[ba, na, na] 로 저장&lt;/b&gt;&lt;/span&gt;하는 것이다. 2gram, 3gram 을 모두 저장한다 하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;[ban, ana, nan, ana]&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 추가될 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&quot;settings&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analysis&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;custom_ngram_filter&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;nGram&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;min_gram&quot;:&amp;nbsp;2,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;max_gram&quot;:&amp;nbsp;3&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;........&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;&amp;nbsp; --------- 이후 analyzer 및 필드에 적용&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;n_gram 역시 shingle 과 똑같이 filter 로, analyzer 단에 적용&lt;/b&gt;하면 된다. 원본 필드 외 멀티 필드에 적용하려면, shingle 과 동일하게 멀티 필드를 선언 후 해당 필드의 analyzer 로 지정하면 된다. Search as you type 필드에서 사용하는 명칭들 때문에 조금 헷갈렸지만, 두 필터의 의미 차이는 알아두자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;정리하며&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Elasticsearch 에 대해서 간략하지만 어느 정도 심도 있게 알아볼 수 있었다. 강의를 넘어서 궁금한 것들을 계속 찾아보는게 학습에 도움이 되는 것 같다. Elasticsearch 에는 정말 강력한 Analyzer 들이 플러그인으로 많이 들어가 있다. 정말 찾으면 찾을 수록 정말 많다. 하지만 도메인이 특화된 분야에서는 직접 Analyzer 를 개발하는 일이 필요하기도 할 것 같고, 나는 이 쪽을 목적으로 하고 있다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;백엔드 분야에서 개발을 하면 정말 쉽게 들어오는 요청이 바로 검색과 추천이다. 클라이언트들은 당연하게 생각하지만 맨땅으로 구현하기엔 굉장히 복잡하고 난이도가 있는 검색과 추천,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;어찌보면 Elasticsearch 는 이제 백엔드 분야에서 정말 선택이 아닌 반드시 해야 하는 필수 소양&lt;/b&gt;&lt;/span&gt;인 것 같다. 꼭 앞으로도 병행하며 공부해야 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;[Elasticsearch 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 박재성님의&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[실전에서&amp;nbsp;바로&amp;nbsp;써먹는&amp;nbsp;Elasticsearch&amp;nbsp;입문&amp;nbsp;(검색&amp;nbsp;최적화편)] 강의&lt;/b&gt;를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inf.run/55Z3k&quot;&gt;https://inf.run/55Z3k&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751469374960&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의 | JSCODE 박재성 - 인프런&quot; data-og-description=&quot;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 'Elasticsearch 입문' 강의를 만들어봤습니다!,   Elasticsearch는 혼자서 공부하기 왜 이렇게 어려운거야?!비전공&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://inf.run/55Z3k&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8B%A4%EC%A0%84-elasticsearch-%EC%9E%85%EB%AC%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bUPGQd/hyZf2cIHWn/dnQKkKi7XlD4AoDmEy7D9K/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/bDkwan/hyZgaIBGdt/prdrYhsYzfhKe4LlFsw7j0/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/8oqiN/hyZfYuD68L/lgibN50FKPcTwKLskh6h9k/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525&quot;&gt;&lt;a href=&quot;https://inf.run/55Z3k&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inf.run/55Z3k&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bUPGQd/hyZf2cIHWn/dnQKkKi7XlD4AoDmEy7D9K/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/bDkwan/hyZgaIBGdt/prdrYhsYzfhKe4LlFsw7j0/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/8oqiN/hyZfYuD68L/lgibN50FKPcTwKLskh6h9k/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의 | JSCODE 박재성 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 'Elasticsearch 입문' 강의를 만들어봤습니다!,   Elasticsearch는 혼자서 공부하기 왜 이렇게 어려운거야?!비전공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>인프라 기술/Elasticsearch</category>
      <category>bool</category>
      <category>elasticsearch</category>
      <category>fuzziness</category>
      <category>MATCH</category>
      <category>multi field</category>
      <category>multi match</category>
      <category>search as you type</category>
      <category>search 쿼리</category>
      <category>should</category>
      <category>term</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/320</guid>
      <comments>https://mooncake1.tistory.com/320#entry320comment</comments>
      <pubDate>Thu, 3 Jul 2025 00:16:55 +0900</pubDate>
    </item>
    <item>
      <title>[Elasticsearch 기본] - 1. 기본 개념, 동작 원리와 Analyzer</title>
      <link>https://mooncake1.tistory.com/318</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;597&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uujRx/btsO3aru5x3/zr7raBo221bkkwJS2NnrhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uujRx/btsO3aru5x3/zr7raBo221bkkwJS2NnrhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uujRx/btsO3aru5x3/zr7raBo221bkkwJS2NnrhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuujRx%2FbtsO3aru5x3%2Fzr7raBo221bkkwJS2NnrhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1131&quot; height=&quot;597&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Elastic&amp;nbsp;Search&amp;nbsp;란&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;오픈 소스이며, Restful한 검색 및 분석 엔진, 확장 가능한 DB 저장소로, 쉽게 말해서 검색 / 데이터 분석에 최적화된 DB이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;대표적인 활용 사례로는 우선 데이터 수집 및 분석에 사용된다. 로그와 같은 대규모 데이터를 수집 및 분석하는데 최적화되어 있고, ELK 스택이 통합적으로 사용된다. 또한 Elasticsearch 는 자체적인 검색 엔진을 가지고 있어 검색 최적화에도 굉장히 많이 사용된다. 뛰어난 검색 속도를 자랑하며, 오타와 동의어 등 일반적으로 개발자들에게 높은 난이도의 개발을 요구하는 부분을 해결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eV2qOJ/btsO2qPCG3P/1mdUtpElb3Dgr6RKO5OXkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eV2qOJ/btsO2qPCG3P/1mdUtpElb3Dgr6RKO5OXkK/img.png&quot; data-alt=&quot;RDB에 SQL문이 있다면, Elasticsearch 에는 API 가 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eV2qOJ/btsO2qPCG3P/1mdUtpElb3Dgr6RKO5OXkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeV2qOJ%2FbtsO2qPCG3P%2F1mdUtpElb3Dgr6RKO5OXkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;690&quot; height=&quot;420&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RDB에 SQL문이 있다면, Elasticsearch 에는 API 가 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MySQL은 SQL 문으로 3306 포트에 있는 프로세스와 통신을 하는 것이며, Elasticsearch는 REST API 방식으로 9200 포트에 있는 프로세스와 통신을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;가령, 데이터 삽입을 하기 위해서는 MySQL 에서는 INSERT 문을 사용한다. Elasticsearch 에서는 우리에게 익숙한 POST 로 JSON Body 를 날리면 된다. SELECT는 GET 요청을 사용하면 된다. API 명세서는 Elasticsearch에서 제공해주는 것을 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이렇게 API 방식으로 DB를 사용할 수&amp;nbsp; 있어서, 복잡한 SQL 을 따로 구현하지 않아도 되어 개발자들이 사용하기 쉽다. 또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;확장성 및 커스터마이징이 좋다&lt;/b&gt;&lt;/span&gt;. 자체적인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;웹을 구축 후 사용하는 ES로는 API 를 활용해서 연계&lt;/b&gt;&lt;/span&gt;할 수 있기 때문이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;기본 용어 정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL은 저장을 시작하기 위해 Table을 만들고, Elasticsearch는 Index를 만든다. 처음에는 우리가 아는 그 &quot;index&quot;와 아예 아무 상관 없다고 생각하는게 좋다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;MySQL은 어떻게 DB에 넣을지 Table의 Schema 를 정의하고, Elasticsearch는 Mapping을 정의한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;MySQL은 Schema 내의 저장될 칼럼들을 정의하고, Elasticsearch는 Mapping 안에 필드들을 정의한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;MySQL은 Table에 실제 데이터인 레코드들을 넣고, Elasticsearch 는 Document들을 넣는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;인덱스 가지고 놀아보기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;인덱스 제어&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 Users 라는 인덱스 (테이블)을 만들고 싶으면, PUT 요청을 보내면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PUT&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;/users&amp;nbsp; -- 인덱스 생성&lt;/li&gt;
&lt;li&gt;GET&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;/users&amp;nbsp; -- 해당 인덱스에 대한 모든 정보 조회 (매핑 정보, 존재하는 데이터들 등)&lt;/li&gt;
&lt;li&gt;DELETE /users&amp;nbsp; -- 인덱스 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;매핑 정의&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Users 라는 인덱스에 매핑을 정의할 수 있다. 인덱스를 먼저 생성한 다음에 mapping 을 정의할 수도 있고, 위에서 PUT /users 요청으로 인덱스를 생성하는 시점에 Body로 매핑 정보를 보내줄 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;$ PUT /users/_mappings&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;properties&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;&amp;nbsp;:{&quot;type&quot;:&amp;nbsp;&quot;keyword&quot;},&amp;nbsp;&amp;nbsp;//&amp;nbsp;String&amp;nbsp;같은거라고&amp;nbsp;일단&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;age&quot;:&amp;nbsp;{&quot;type&quot;:&amp;nbsp;&quot;integer&quot;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;is_active&quot;:&amp;nbsp;{&quot;type&quot;:&amp;nbsp;&quot;boolean&quot;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;i&gt;$ PUT /users&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;mappings&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &quot;properties&quot;: {&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ...&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;다큐먼트 삽입&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;인덱스에 생성한 매핑에 맞게 다큐먼트를 넣어볼 수 있다 (RDB로 치면 생성한 테이블의 스키마에 맞게 레코드 삽입)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;$ POST /users/_doc&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;:&quot;Alice&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;age&quot;&amp;nbsp;:&amp;nbsp;28,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;is_active&quot;:&amp;nbsp;true&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고로 Document 생성은 POST /users/create 요청을 통해서도 진행할 수 있다. 이 요청은 보통 /users/create/22 와 같은 형태로 Elasticsearch 내 고유 id에 원하는 값을 사용해서 생성하기 위해 사용되는 요청이다. 하지만 _doc 요청에서도 동일하게 동작한다. 특히,&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;_doc 요청에서 뒤에 {id}를 붙이면, UPSERT를 시키는 요청&lt;/span&gt;이므로, version conflict 도 방지할 수 있으니, /_doc 명령어를 일반적으로 사용한다고 알아두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;다큐먼트 조회&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인덱스에 들어있는 다큐먼트들을 조회하는 명령어를 수행할 수 있다. 이 명령어는 elasticsearch 의 핵심 명령어로, 앞으로 다양한 쿼리들, 다양한 옵션들과 기능들을 활용하게 될 검색 API이다. 당장은 SELECT * FROM 의 역할을 수행하는 모습으로 알아두자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;$ GET /users/_search&lt;br /&gt;&lt;/i&gt;-- 응답 항목에는 hits 가 있는데, 내부에 반환할 데이터들이 들어 있고, score 라는 항목도 살짝 확인하고 넘어가자&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Spring 에서 사용하기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;# build.gradle - dependencies&lt;br /&gt;implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 Spring 에서 Elasticsearch 관련 Library를 사용하기 위해서 implementation으로 dependency를 추가해줘야한다. 그러면 코드 내부에서 Elasticsearch와 연계된 Library들을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;# application.yml&lt;br /&gt;spring:&lt;br /&gt;&amp;nbsp; elasticsearch:&lt;br /&gt;&amp;nbsp; &amp;nbsp; urls: http://{elasticsearch-ip}:{port}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일반적인 RDB를 연결할 때 해주듯이, application 설정 파일에 위와&amp;nbsp; 같이 elasticsearch 에&amp;nbsp; 대한 주소를 설정해 두면, Connection 을 준비할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750951598502&quot; class=&quot;kotlin&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Document(indexName = &quot;users&quot;)
public class UserDocument {

    @Id
    private String id;

    @Field(type = Keyword)
    private String name;

    @Field(type = Long)
    private Long age;

    @Field(type = Boolean)
    private Boolean isActive;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 설정들을 모두 추가해주면 위와 같이 Document를 (JPA에서 Entity처럼) Java 클래스로 선언할 수 있고, ORM처럼 매핑을 지원받으며 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750951598503&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface UserDocumentRepository extends ElasticsearchRepository&amp;lt;User, String&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;위와 같이 Repository Interface를 만들면 Jpa에서 사용하던 것처럼 제공되는 내장 함수들을 사용할 수 있다. 이렇게 설정을 해두면 Spring 이 기동되는 시점에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;@Document로 존재하는 객체에 정의된 indexName을 확인&lt;/span&gt;하여, 해당 index들이 실제로 존재하는지 확인하고, 기본적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;없으면 생성하는 요청을 날리며 동작&lt;/span&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아무튼 위와 같은 방식으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;평소에 하던 Spring 개발을 진행하되, Elasticsearch DB와 연계되어 Elasticsearch 엔진의 강력한 기술들을 사용하는 웹 서버&lt;/span&gt;를 만들 수 있는 것이다. 이렇게 확장성이 우수한 모습으로 Elasticsearch 의 API 형태의 DB 제어 방식의 장점을 확인할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;동작 원리와 Analyzer 소개&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리가 물건을 구매하려 할 때, 물건 이름을 정확하게 기억하고 검색하지 않는다. 예를 들면 맥북 M4 , 파란색 니트 이런식으로 검색한다. 따라서 단어의 순서가 맞지 않아도 검색이 되게끔 구축하는 시스템들인 것이다. Elasticsearch 는 이런 유연한 검색을 잘 지원해주며, 그 중&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;유연한 검색을 지원하고 싶은 필드는 &quot;text&quot;여야 지원&lt;/b&gt;&lt;/span&gt;이 된다. 참고로 String 과 같은 정확히 일치를 위한 타입은 &quot;keyword&quot;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;i&gt;$ PUT /products&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;properties&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;text&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;---&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;i&gt;GET&amp;nbsp;/products/_search&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;query&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;match&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;: &quot;맥북 에어 13&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 name 필드에 &quot;Apple 2025 맥북 에어 13 M4 10코어&quot; 라고 저장된 제품이 있다면, 해당 제품은 검색된다. 이와 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;text 타입은 검색 순서가 바뀌어도 검색이 되는 유연한 검색을 지원&lt;/b&gt;&lt;/span&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Elasticsearch 의 역 Index&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&quot;Apple 2025 맥북 에어 13 M4 10코어&quot; -&amp;gt; [Apple, 2025, 맥북, 에어, 13, M4, 10코어] 로 잘라서 저장한다. 이를 토큰화라고 한다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;각 토큰을 가지고 있는 Id 를 Value 로 저장한다. (Key Value 과 일반적인 경우와 반대된 상황)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;검색을 &quot;Apple 2024 아이패드&quot; 라고 검색하면, 이 검색을 다시 토큰화 한다 -&amp;gt; [Apple, 2024, 아이패드]&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Apple 토큰이 있는 ID 들, 2024 토큰을 가진 ID들, 아이패드 토큰을 가진 ID들을 통하 Document 들을 매우 빠르게 조회할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이처럼 포함 여부를 가지고 하기 때문에 유연한 검색 가능, 각 아이템 별로 score 를 부여하여 더 많이 가진건 높은 점수 (매우 간단하게 말하면)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Score 시스템은 매우 복잡하며 이 시스템 자체가 Elasticsearch 의 핵심 요소들이다. 가장 대표적으로 다음과 같은 계산 로직들을 통해 검색시 각 Document 의 점수들이 정해진다. 하지만 간단한 기본 적용을 위해서는 일단 위에서 말한 수준만으로도 충분하긴 하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Term&amp;nbsp;Frequency&amp;nbsp;-&amp;nbsp;검색어가&amp;nbsp;얼마나&amp;nbsp;Document&amp;nbsp;에&amp;nbsp;많이&amp;nbsp;있는지&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Inverse Document Frequency - 검색어가 전체 문서 중 얼마나 희귀한지&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Field&amp;nbsp;Length&amp;nbsp;Normalization&amp;nbsp;-&amp;nbsp;Document&amp;nbsp;가&amp;nbsp;짧을&amp;nbsp;수록&amp;nbsp;점수&amp;nbsp;높음&amp;nbsp;(짧은&amp;nbsp;문서에서&amp;nbsp;등장하면&amp;nbsp;더&amp;nbsp;관련성&amp;nbsp;높은)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;한 단계 더 들어가보는 Analyzer&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;아까 문자열을 토큰의 배열로 바꿔주는 장치를 Analyzer 라고 부른다 (사실 간단하게 단어 단위로만 자르는건 아니다). 위 그림과 같이 캐릭터 필터, 토크나이저, 토큰 필터로 나뉘어져 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐릭터 필터&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 문자열을&amp;nbsp;토큰으로&amp;nbsp;자르기&amp;nbsp;전&amp;nbsp;문자열을&amp;nbsp;다듬는다&amp;nbsp;(ex&amp;nbsp;:&amp;nbsp;html&amp;nbsp;태그&amp;nbsp;제거&amp;nbsp;-&amp;nbsp;포함된채로&amp;nbsp;저장하는&amp;nbsp;경우&amp;nbsp;많음)&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토크나이저&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 문자열을 토큰으로 자르는 역할, 대표적인 것이 Standard 토크나이저 (공백, ,.!?- 와 같은 문장 부호 기준으로 자름, 맨뒤의 . 까지 제거)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰 필터&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;- 잘린 토큰을 최종적으로 다듬는 역할, 자주 쓰는 3가지 필터로는 소문자 적용 필터, Stop 필터 (a,the,is 같은 특별한 의미 없는 단어 토큰 제거), -es, -ed 같은 단어들을 원래 단어 형태로 변경해주는 필터 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Standard Analyzer&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Char Filter 는 없고, Standard Tokenizer 를 사용하며, Lowercase Filter 가 적용된 Analyzer 가 가장 기본적으로 적용되는 default analyzer 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;Analyzer 의 종류&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Analyzer 는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;색인 analyzer 와 검색 analyzer&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;두 가지를 사용할 수 있다 (나중에 적용할 때 진짜 많이 고려하게 되더라). 색인 analyzer 는 지금까지 말한 analyzer, 일반적으로 말하는 analyzer 이다. 검색 analyzer 는 검색할 때 query 로 날린 input text 를 어떻게 토큰화할지 적용하는 analyzer 이다. 당연히&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;검색 analyzer 는 따로 지정하지 않으면 &quot;색인 analyzer&quot; 와 같은 값으로 적용&lt;/b&gt;&lt;/span&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이해가 안될 수 있는데, 만약 Product 라는 index (테이블) 이 있다고 하자. Product 안에 description 이라는 필드가 text 로 선언한다고 하면, 이 필드에는 analyzer 를 적용할 수 있다 (안하면 기본적으로 standard analyzer). 하지만, 만약 검색할 때 좀 다른 토큰화가 필요한 경우가 있다. 그런 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;index 생성시 해당 필드에 대하 search_analyzer 를 따로 지정&lt;/b&gt;&lt;/span&gt;할 수 있는 것.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Analyzer 는 필드 단위로 적용하는 것&lt;/b&gt;&lt;/span&gt;임을 확실히 이해해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Analyzer 실습해보기 (다양한 filter 들 실습)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ Get /_analyze&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp; &quot;text&quot;:&quot;적용 대상&quot;,&lt;br /&gt;&amp;nbsp; &quot;analyzer&quot;:&quot;standard&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 위와 같은 명령어를 통해, 특정 analyzer 가 넣은 text 를 어떻게 토큰화할지 미리 확인할 수 있다. 특히 이는 앱단에서 적용할 때 쿼리에 넣기 위해 사용되기도 하는 use case 까지 봤다. 물론 analyzer 를 char_filter, tokenizer, filter 세 가지 구성 요소를 각각 명시하여 custom 한 analyzer 를 테스트 해볼 수도 있다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ PUT /products&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;settings&quot;: { // 이번 요청 안에서 사용할 setting 정의&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analysis&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analyzer&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;products_name_analyzer&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;char_filter&quot;:&amp;nbsp;[],&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;tokenizer&quot;:&amp;nbsp;&quot;standard&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;[]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mappings&quot;: { // 테이블 정의 부분&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;properties&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;text&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analyzer&quot;:&amp;nbsp;&quot;products_name_analyzer&quot;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 모습 처럼 index 를 생성할 때, analyzer 를 생성 및 지정하고, 필드별로 analyzer를 설정할 수 있다. setting 에는 analyzer 를 명시해서, 원하는 이름 &quot;proucts_name_analyzer&quot;를 선언하였고, index 를 정의하는 mapping 부분에서 이를 사용하였다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;참고로 index 는 테이블 선언이기 때문에, analyzer 를 변경할 수 없다&lt;/b&gt;&lt;/span&gt;. 변경해야 하면 다시 생성 및 데이터 이전해야 한다. 이 때 검색 analyzer 도 지정할 수 있는 것이다 (필요시).&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ POST /products/_create/1&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;name&quot;:&quot;Apple&amp;nbsp;2025&amp;nbsp;맥북&amp;nbsp;에어&amp;nbsp;13&amp;nbsp;M4&amp;nbsp;10코어&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;위와 같이 index 를 생성 후 바로 위와 같이 데이터를 넣어줄 경우, 검색을 apple 로 하면 검색되지 않는다! index 생성 요청을 보면 Standard Analyzer 와 다르게&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;filter 에서 lowercase 를 제외&lt;/span&gt;하였기 때문이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ GET /boards/_analyze&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;field&quot;:&quot;content&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;text&quot;:&amp;nbsp;&quot;&amp;lt;h1&amp;gt;&amp;nbsp;Running&amp;nbsp;cats,&amp;nbsp;jumping&amp;nbsp;quckly&amp;nbsp;-&amp;nbsp;over&amp;nbsp;the&amp;nbsp;lazy&amp;nbsp;dogs!&amp;nbsp;&amp;lt;/h1&amp;gt;&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 board 라는 인덱스가 있는데, 위와 같이 html 에서 긁어져 오는 데이터들이라 태그들이 포함되어 있다고 해보자. 일반적인 Standard Analyzer 를 적용한다면, h1, h2 와 같은 태그들이 모두 저장된다. 따라서 검색할 때&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;h1 이라고 검색하면 다 검색이 되는 일&lt;/b&gt;&lt;/span&gt;이 벌어진다 (h1 같은 태그는 검색 대상이 당연히 아니다). 이럴 때 html_strip 필터를 넣어줄 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;...&lt;br /&gt;&quot;analyzer&quot;:{&lt;br /&gt;&amp;nbsp; &quot;boards_content_analyzer&quot;:{&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &quot;char_filter&quot;: [&quot;html_strip&quot;],&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;i&gt;---&amp;gt; 더 이상 태그들은 저장되지 않는다&lt;/i&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;tokenizer&quot; : &quot;standard&quot;,&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;filter&quot; : [&quot;lowercase&quot;]&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번엔 다른 경우를 살펴보자. 같은 index 에 다음과 같은 content 를 넣어본다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ POST /boards/_doc&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;content&quot;:&quot;The&amp;nbsp;cat&amp;nbsp;and&amp;nbsp;the&amp;nbsp;dog&amp;nbsp;are&amp;nbsp;friends&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 이처럼 많은 평문이 저장되는 경우라면, 별 의미 없는 a, the, and, are 같은 단어들을 검색하면 정~말 많은 양의 데이터가 검색될 것이다. 우리는 이런 의미 없는 단어들을 검색 점수를 판단함에 있어서 고려해주고 싶지 않을 수도 있는 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;br /&gt;...&lt;br /&gt;&quot;analyzer&quot;:{&lt;br /&gt;&amp;nbsp; &quot;boards_content_analyzer&quot;:{&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;char_filter&quot;: [],&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &quot;tokenizer&quot; : &quot;standard&quot;,&lt;br /&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; &quot;filter&quot; : [&quot;lowercase&quot;, &quot;stop&quot;]&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;-&lt;/i&gt;&lt;/b&gt;&lt;i&gt;--&amp;gt; a , an, the, or, but, and, ... 다양한 불용어들을 토큰에서 제외시킬 수 있다&lt;/i&gt;&lt;br /&gt;&amp;nbsp; }&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런식으로 만약에 filter 에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;stemmer&quot; 란 필터&lt;/b&gt;를 추가해주면, 위에서 언급했듯이, Running 같은 것은 run 으로, Jumped 같은 것은 jump 로, cats 같은 것은 cat 로 토큰을 &quot;원형&quot;으로 원복시켜 저장해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 &quot;stop&quot;, &quot;stemmer&quot;, &quot;html_strip&quot; 기능을 수행하는 친구들은 ES를 간단하게 사용함에 있어서 정말 많이 사용되는 char_filter, filter 들이라고 한다. 하나 더 알아두고 가면 좋은 기능, 정~말 많이 쓰는 기능은 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;synonym filter&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;이다. 약간의 노가다가 필요하며, 직접 synonym 들을 정의하는 과정도 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ PUT /products&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;settings&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analysis&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;products_syn_filter&quot;: {&amp;nbsp; &amp;nbsp;&lt;u&gt;&lt;i&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;----&amp;gt; synonym filter 를 정의할 수 있고, type 에 synonym 으로 선언해야 한다&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;type&quot;:&amp;nbsp;&quot;synonym&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;synonyms&quot;:&amp;nbsp;[&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;notebook, 노트북, 랩탑, 휴대용 컴퓨터, laptop&quot;,&amp;nbsp;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;samsung, 삼성&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;---&amp;gt; samsung 과 삼성은 같은 토큰으로 취급한다는 뜻&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analyzer&quot;:&amp;nbsp;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;products_name_analyzer&quot;:{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;char_filter&quot;:[],&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;tokenizer&quot;:&amp;nbsp;&quot;standard&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;filter&quot;:&amp;nbsp;[&quot;lowercase&quot;,&amp;nbsp;&quot;products_syn_filter&quot;]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;mappings&quot;: { // 이제부터 인덱스 정의&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;...&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;양방향 syn, 단방향 syn&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;강의에 나오지는 않지만, 위에처럼 정의하면 양방향 synonym 정의이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;samsung &amp;lt;-&amp;gt; 삼성 서로 호환&lt;/b&gt;된다는 것이다. 하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;검색 시 포함 관계 느낌으로 정의&lt;/b&gt;&lt;/span&gt;하고 싶을 수 있다. A를 검색하면 a, b 모두 나왔으면 좋겠지만, a 를 검색하면 b까지 나오게 하고 싶지는 않은 경우이다. 이럴 때는&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;A=&amp;gt;a,b 로 정의해주면, 단방향 synonym 정의&lt;/b&gt;&lt;/span&gt;를 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아무튼 위에서 만든&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;synonym filter&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용해서 index 내 특정 필드의 analyzer에 포함시켜서 사용하면, &quot;Samsung Notebook&quot; 이라고 검색만 해도 [&quot;삼성 노트북, 삼성 랩탑, 삼성 휴대용 컴퓨터, samsung labtop, ... &quot;] 등이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;모두 검색될 수 있는, 아주 강력한 기능&lt;/b&gt;&lt;/span&gt;이라 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;한글에서 적용해보기 (Nori 의 등장)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 위에서 선언한 boards의 content 필드에, &quot;백화점에서 쇼핑을 하다가 친구를 만났다&quot;라고 데이터를 넣는다고 해보자. 그리고 search 를 match query 로 사용해서, &quot;백화점&quot;을 검색하면, 위 데이터가 조회될까?&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;조회되지 않는다&lt;/b&gt;&lt;/span&gt;. 위 데이터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;토큰이 &quot;백화점에서&quot;로 들어가&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있기 때문이다. 누가 검색할 때 에서를 붙이는가?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이처럼 standard analyzer, 더 나아가 기본 Elasticsearch 는 한글에 최적화 되어 있지 않다. 사실, 영어를 제외한 어떤 언어에도 최적화 되어 있지 않다. 한글에 최적화된 Analyzer 는 여러개 있지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;대표적으로 Nori Analyzer 를 일반적으로 사용&lt;/b&gt;&lt;/span&gt;한다. 사용하려면 Elasticsearch 에 추가적인 plugin 을 넣어줘야 한다. 구글 검색해서 알아서들 설치해보자 ㅎㅎ (참고로 현재 사용하고 있는 Elasticsearch 버전과 정확히 일치해야 한다고 한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;## Nori Analyzer 의 구성&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp; &quot;char_filter&quot;: [],&lt;br /&gt;&amp;nbsp; &quot;tokenizer&quot;: &quot;nori_tokenizer&quot;,&lt;br /&gt;&amp;nbsp; &quot;filter&quot;: [&quot;nori_part_of_speech&quot;,&amp;nbsp;&quot;nori_readingform&quot;,&amp;nbsp;&quot;lowercase&quot;]&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 모습은 Nori Analyzer 의 기본적인 구성으로, 새로운 필터 및 토크나이저들은 plugin에 포함된 친구들이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;nori tokenizer 는 standard와 다르게 특수문자가 아닌 의미 단위로 자른다 (형태소 기준으로 자른다고 표현)&lt;/b&gt;&lt;/span&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;b&gt;$ GET /_analyze&lt;/b&gt;&lt;/i&gt;&lt;br /&gt;{&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;text&quot;:&quot;백화점에서&amp;nbsp;쇼핑을&amp;nbsp;하다가&amp;nbsp;친구를&amp;nbsp;만났다&quot;,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;analyzer&quot;:&amp;nbsp;&quot;nori&quot;&lt;br /&gt;}&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 nori analyzer 를 사용해서 토큰화를 시도해볼 수 있는데, 위 결과는 [백화 / 점 / 쇼핑 / 하 / 친구 / 만나] 로 토큰화가 진행된다. 이렇게 되기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&quot;백화점&quot;이라고 검색을 해도, &quot;쇼핑&quot;이라고 검색을 해도, &quot;친구&quot;라고 검색을 해도 데이터가 조회&lt;/b&gt;&lt;/span&gt;될 수 있다. nori analyzer 는 정말 많이 사용하고 custom 화 하는 구성 요소로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;한국에선 필수 plugin&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라고 봐도 될 정도라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;참고로 Elasticsearch 는 기본적으로 NoSQL이기 때문에, NoSQL 의 자연스러운 특성들을 가져간다. 대표적으로 굉장히 테이블이 가변적이고, 특정 column 이 없다고 error 을 던지지도 않는다. 그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;무조건 NULLABLE&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다 (&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Not Null 이라는 제약 조건이 없음&lt;/b&gt;&lt;/span&gt;). 또한 NoSQL 이기 때문에 필드 배열이 가능하다. 참고로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;배열로 들어오는 필드에 대해 text 타입&lt;/span&gt;&lt;/b&gt;으로 선언되었다면, 토큰화 시 [&quot;이젠&quot;, &quot;안녕&quot;] 이 들어올 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;이젠 안녕&quot; 으로 그냥 띄어쓰기로 만들어 버린 후 생각하고 토큰화&lt;/b&gt;한다고 한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 정도로 Elasticsearch 와 가벼운 첫 만남을 마칠 수 있다. 이후로는 Elasticsearch 로 검색하는 쿼리를 어떻게 만드는지, 그리고 이 쿼리를 돕기 위해 어떤 기능들을 사용할 수 있는지 살펴보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;출처&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;[Elasticsearch 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 박재성님의&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[실전에서&amp;nbsp;바로&amp;nbsp;써먹는&amp;nbsp;Elasticsearch&amp;nbsp;입문&amp;nbsp;(검색&amp;nbsp;최적화편)] 강의&lt;/b&gt;를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inf.run/55Z3k&quot;&gt;https://inf.run/55Z3k&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750951611459&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의 | JSCODE 박재성 - 인프런&quot; data-og-description=&quot;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 'Elasticsearch 입문' 강의를 만들어봤습니다!,   Elasticsearch는 혼자서 공부하기 왜 이렇게 어려운거야?!비전공&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://inf.run/55Z3k&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%8B%A4%EC%A0%84-elasticsearch-%EC%9E%85%EB%AC%B8&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/RSIC4/hyZcnhDx0k/09mxeh0xJ8ukb09CvhLVgK/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/fjJbs/hyZfyhAlhO/6zrXjc1QOAZCz5sWRswdOk/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/vUtOX/hyZce52bIZ/tEYKMkeDsP98jIvevGRhW0/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525&quot;&gt;&lt;a href=&quot;https://inf.run/55Z3k&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://inf.run/55Z3k&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/RSIC4/hyZcnhDx0k/09mxeh0xJ8ukb09CvhLVgK/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/fjJbs/hyZfyhAlhO/6zrXjc1QOAZCz5sWRswdOk/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525,https://scrap.kakaocdn.net/dn/vUtOX/hyZce52bIZ/tEYKMkeDsP98jIvevGRhW0/img.png?width=807&amp;amp;height=525&amp;amp;face=0_0_807_525');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편) 강의 | JSCODE 박재성 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;JSCODE 박재성 | 비전공자 입장에서도 쉽게 이해할 수 있고, 실전에서 바로 적용 가능한 'Elasticsearch 입문' 강의를 만들어봤습니다!,   Elasticsearch는 혼자서 공부하기 왜 이렇게 어려운거야?!비전공&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>인프라 기술/Elasticsearch</category>
      <category>analyzer</category>
      <category>elasticsearch</category>
      <category>elasticsearch 입문</category>
      <category>elk</category>
      <category>nori analyzer</category>
      <category>nosql</category>
      <category>박재성 강사</category>
      <category>역인덱스</category>
      <category>유연한 검색</category>
      <category>키바나</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/318</guid>
      <comments>https://mooncake1.tistory.com/318#entry318comment</comments>
      <pubDate>Fri, 27 Jun 2025 00:28:13 +0900</pubDate>
    </item>
    <item>
      <title>[음식 리뷰] 전라 광주 방문 식당 리뷰</title>
      <link>https://mooncake1.tistory.com/317</link>
      <description>&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 초돈 쉼표&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.52.33.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;665&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXAvlU/btsOLvqu8Tj/oaORQZw0xOROs6T3cHKG70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXAvlU/btsOLvqu8Tj/oaORQZw0xOROs6T3cHKG70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXAvlU/btsOLvqu8Tj/oaORQZw0xOROs6T3cHKG70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXAvlU%2FbtsOLvqu8Tj%2FoaORQZw0xOROs6T3cHKG70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;528&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2025-06-22 오후 12.52.33.png&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;665&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;37B06C4D-6950-4A29-80FE-C771D2B944F0.JPG&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfseDE/btsOKQWkVse/AmYAiuNc1CZuIP4Kxxeid0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfseDE/btsOKQWkVse/AmYAiuNc1CZuIP4Kxxeid0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfseDE/btsOKQWkVse/AmYAiuNc1CZuIP4Kxxeid0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfseDE%2FbtsOKQWkVse%2FAmYAiuNc1CZuIP4Kxxeid0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;393&quot; data-filename=&quot;37B06C4D-6950-4A29-80FE-C771D2B944F0.JPG&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;5EC48FEC-2EE8-4DB8-AD3D-847735973D4D.JPG&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSL6N5/btsOK7i8J41/v83ocMYn5wB0Wh3SYIruk0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSL6N5/btsOK7i8J41/v83ocMYn5wB0Wh3SYIruk0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSL6N5/btsOK7i8J41/v83ocMYn5wB0Wh3SYIruk0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSL6N5%2FbtsOK7i8J41%2Fv83ocMYn5wB0Wh3SYIruk0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;524&quot; height=&quot;393&quot; data-filename=&quot;5EC48FEC-2EE8-4DB8-AD3D-847735973D4D.JPG&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;맛:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;☆&lt;br /&gt;&lt;b&gt;서비스:&amp;nbsp;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;청결도:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;☆&lt;br /&gt;&lt;b&gt;주차:&lt;/b&gt;&lt;span&gt; 근처에 주차, 여유로움&lt;/span&gt;&lt;br /&gt;&lt;b&gt;대기:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;100% 예약제, 예약이 좀 힘듦&amp;nbsp;&lt;br /&gt;&lt;b&gt;한줄 평:&lt;/b&gt;&lt;span&gt; 그냥 맛있는 돼지고기 집인데, 불향으로 승부를 본 집. 부드럽고 불향이 있어서 다르긴 하다고 느꼈다. &lt;/span&gt;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;span&gt;유튜브에서 화제 맛집이라 조금 덧 붙이자면, &lt;/span&gt;손님들을 엄청 생각하는 느낌으로 마케팅이 더 잘 된 케이스라 생각하지만 사장님이 바빠서 그런지 교류도 없고, 사전 문의할 때나 매장 이용할 때 직원들 전체적으로 친절하다고 느껴지진 않음. &lt;/span&gt;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;개인적으로 삼겹살은 먹어본 곳 중에선 제일 맛있긴 하지만, 목살은 청주 나릿집이 압도. 영화관, 게임방 이용도 장점&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://naver.me/xY4Ld8aN&quot;&gt;https://naver.me/xY4Ld8aN&lt;/a&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1750488557215&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네이버 지도&quot; data-og-description=&quot;초돈 쉼표&quot; data-og-host=&quot;map.naver.com&quot; data-og-source-url=&quot;https://naver.me/xY4Ld8aN&quot; data-og-url=&quot;https://map.naver.com/p/entry/place/1254383195?placePath=/home&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/UH7og/hyZbqqML08/2eiEZR2nhYGgBF3lixlxkK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/bXW3Ky/hyZcjE5jTm/5XMYFKIfjyws2f7oXudnkK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256&quot;&gt;&lt;a href=&quot;https://naver.me/xY4Ld8aN&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://naver.me/xY4Ld8aN&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/UH7og/hyZbqqML08/2eiEZR2nhYGgBF3lixlxkK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/bXW3Ky/hyZcjE5jTm/5XMYFKIfjyws2f7oXudnkK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네이버 지도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;초돈 쉼표&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;map.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 대금육회&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CF3A16D8-4EF6-40FB-B2E8-A9F24880E45F.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cA6nPY/btsOMUJyWE8/knlaoiR3UzYe55yTDA1U8k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cA6nPY/btsOMUJyWE8/knlaoiR3UzYe55yTDA1U8k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cA6nPY/btsOMUJyWE8/knlaoiR3UzYe55yTDA1U8k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcA6nPY%2FbtsOMUJyWE8%2FknlaoiR3UzYe55yTDA1U8k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;591&quot; data-filename=&quot;CF3A16D8-4EF6-40FB-B2E8-A9F24880E45F.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;맛:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;서비스:&amp;nbsp;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;청결도:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;주차:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주차할 곳은 없다&lt;br /&gt;&lt;b&gt;대기:&lt;/b&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;20시 기준 없음&lt;/span&gt;&lt;br /&gt;&lt;b&gt;한줄 평:&lt;/b&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;안주들은 괜찮고, 하나만 시켜도 이것 저것 차려주는 구성이 좋다. 하지만 광주 첨단 거리는 다시 가고 싶진 않다. 거리뷰로 한번 주변에 뭐가 있는지 확인들 하시길 (첨단 전체적으로)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://naver.me/Flc97KU2&quot;&gt;https://naver.me/Flc97KU2&lt;/a&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1750489316367&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네이버 지도&quot; data-og-description=&quot;대금육회&quot; data-og-host=&quot;map.naver.com&quot; data-og-source-url=&quot;https://naver.me/Flc97KU2&quot; data-og-url=&quot;https://map.naver.com/p/entry/place/434827067?lng=126.8456958&amp;amp;lat=35.2150708&amp;amp;placePath=/home&amp;amp;entry=plt&amp;amp;searchType=place&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/U6ZWU/hyZcqqGXba/XUFbsj4aPbIpp0CkjTYrHK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/84Bxn/hyZclpn36K/RyLYiXXa2GKwgmh7ujv2r0/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256&quot;&gt;&lt;a href=&quot;https://naver.me/Flc97KU2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://naver.me/Flc97KU2&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/U6ZWU/hyZcqqGXba/XUFbsj4aPbIpp0CkjTYrHK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/84Bxn/hyZclpn36K/RyLYiXXa2GKwgmh7ujv2r0/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네이버 지도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;대금육회&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;map.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h4&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3.&amp;nbsp; 산수쌈밥&lt;/b&gt;&lt;/h2&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;2462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qHnQH/btsOLcESVUR/SvBqz7CM7KC3wKlAQe2PM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qHnQH/btsOLcESVUR/SvBqz7CM7KC3wKlAQe2PM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qHnQH/btsOLcESVUR/SvBqz7CM7KC3wKlAQe2PM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqHnQH%2FbtsOLcESVUR%2FSvBqz7CM7KC3wKlAQe2PM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;553&quot; height=&quot;605&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;2462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;text-align: center;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;맛:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;서비스:&amp;nbsp;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;청결도:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐&lt;span style=&quot;background-color: #ffffff; color: #474747; text-align: start;&quot;&gt;⭐☆&lt;/span&gt;&lt;/span&gt;☆☆&lt;br /&gt;&lt;b&gt;주차:&lt;/b&gt;&lt;span&gt; 별도 주차장은 없음, 매장 앞에 운 좋게 주차&lt;/span&gt;&lt;br /&gt;&lt;b&gt;대기:&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 13시 기준 약 10분 대기&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;b&gt;한줄 평:&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 가격 대비 구성이 괜찮다. 적당히 맛있는 한정식 한차림 먹은 느낌. 다만 전라 광주 여행하며 김치는 모두 맛있었는데, 이 집은 김치가 수도권 식당들처럼 평범했다&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;a href=&quot;https://naver.me/GalHxPu0&quot;&gt;https://naver.me/GalHxPu0&lt;/a&gt;&lt;/h4&gt;
&lt;figure id=&quot;og_1750490153659&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;네이버 지도&quot; data-og-description=&quot;산수쌈밥&quot; data-og-host=&quot;map.naver.com&quot; data-og-source-url=&quot;https://naver.me/GalHxPu0&quot; data-og-url=&quot;https://map.naver.com/p/entry/place/1491922305?lng=126.9242274&amp;amp;lat=35.1528788&amp;amp;placePath=/home&amp;amp;entry=plt&amp;amp;searchType=place&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/L8e24/hyY72rxcS6/sP7dGr6P1kHHGKyekaoYjK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/b3cxDL/hyZbC5Pmun/FTS1NnYGfosGFE0QURUBH0/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256&quot;&gt;&lt;a href=&quot;https://naver.me/GalHxPu0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://naver.me/GalHxPu0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/L8e24/hyY72rxcS6/sP7dGr6P1kHHGKyekaoYjK/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256,https://scrap.kakaocdn.net/dn/b3cxDL/hyZbC5Pmun/FTS1NnYGfosGFE0QURUBH0/img.jpg?width=256&amp;amp;height=256&amp;amp;face=0_0_256_256');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;네이버 지도&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;산수쌈밥&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;map.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;************&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 리뷰는 '평범하다' 기준을 2점으로 두고 있습니다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상/음식점 리뷰</category>
      <category>광주</category>
      <category>광주맛집</category>
      <category>광주여행</category>
      <category>대금육회</category>
      <category>맛집</category>
      <category>산수쌈밥</category>
      <category>전라도 맛집</category>
      <category>전라맛집</category>
      <category>초돈</category>
      <category>초돈쉼표</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/317</guid>
      <comments>https://mooncake1.tistory.com/317#entry317comment</comments>
      <pubDate>Sat, 21 Jun 2025 16:05:38 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional 내에서 Exception 처리 범위에 대하여</title>
      <link>https://mooncake1.tistory.com/313</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;회사에서 운영중인 프로젝트에서 저장되지 않도록 저장한 로직에서 저장이 진행되어 추후 요청에 오류가 발생하는 모습이 확인이 되었다. 살펴보니 Checked Exception 을 누수한 상황에서 Entity 정보가 저장이 되지 않을 것을 기대해서 발생한 오류였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;체크 예외도 Transactional 범위 안에서 발생시 당연히 롤백할 것이라 생각한 내 잘못이였다. 검색을 통해 알아보니 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional 범위와 Exception 종류 및 처리 방식에 대한 DB 반영 여부는 Case 별로 다양하기 때문&lt;/b&gt;&lt;/span&gt;에 확실히 이해해두고, 기억이 안날때마다 봐야할 필요성을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;901&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; width=&quot;195&quot; height=&quot;24&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot; width=&quot;260&quot;&gt;Case A&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot; width=&quot;299&quot;&gt;Case B&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot; width=&quot;147&quot;&gt;Case C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; height=&quot;24&quot;&gt;1번&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;체크 예외 누수&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot;&gt;언체크 예외 누수&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;언체크 예외 잡기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; height=&quot;24&quot;&gt;2번 (체크 예외)&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;자식 예외 누수, 부모 누수&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot;&gt;자식 예외 누수, 부모 잡음&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; height=&quot;24&quot;&gt;3번 (언체크 예외)&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;자식 예외 누수, 부모 누수&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot;&gt;자식 예외 누수, 부모 잡음&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;자식 예외 잡음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; height=&quot;24&quot;&gt;4번 (새 Tx, 체크 예외)&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;2번과 동일&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot;&gt;2번과 동일&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.186%;&quot; height=&quot;24&quot;&gt;5번 (새 Tx, 부모 언체크 예외)&lt;/td&gt;
&lt;td style=&quot;width: 26.2791%;&quot;&gt;자식 param 수정&lt;/td&gt;
&lt;td style=&quot;width: 32.4419%;&quot;&gt;자식 FK 형성&lt;/td&gt;
&lt;td style=&quot;width: 16.9767%;&quot;&gt;독립 관계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위와 같이 상황을 분기하여 테스트를 설계하였다. 당장 어떤 설계인지 몰라도 내용을 보면 이해할 수 있다. 우선 등장하는 Entity 객체들은 다음과 같다. Childcake 는 5번에서만 등장한다. 모든 상황은 정확한 test 를 위해 직접 API 요청을 보내며 test 진행하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756217792180&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
public class Mooncake {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    public Mooncake(){
        this.name = &quot;mooncake&quot;;
    }
}


@Entity
public class Childcake {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = &quot;mooncake_id&quot;, nullable = true)
    private Mooncake mooncake;
    
    private String name;
    
    public Childcake(Mooncake mooncake){
        this.mooncake = mooncake;
        this.name = &quot;childcake&quot;;
    }
    
    public Childcake(){
        this.name = &quot;No Parent childcake&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1번. 기본 체크 예외와 언체크 예외 동작성 차이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756218127424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void test1_caseA() throws SomeCheckedException { // 체크 누수
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    throw new SomeCheckedException();
}

@Transactional
public void test1_caseB() { // 언체크 누수
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    throw new SomeUncheckedException();
}

@Transactional
public void test1_caseC() { // 언체크 잡음
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    try {
        throw new SomeUncheckedException();
    } catch (SomeUncheckedException e) {
        System.out.println(&quot;caught unchecked exception&quot;);
    }
}
--------------
Table : Mooncake
1        mooncake
3        mooncake&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과를 보면 알 수 있듯이, CheckedException 인 case A는 저장이 되었고, UncheckedException 인 case B는 저장이 되지 않았다. Case C는 Uncheck 예외를 잡아줬으므로, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Transactional 입장에선 예외가 발생하지 않았으므로&lt;/b&gt;&lt;/span&gt; 저장을 진행해준 것을 알 수있다. 체크 예외와 언체크 예외의 큰 차이점이라 할 수 있고, 프로젝트에선 이 부분에서 미스가 났었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;&lt;u&gt;&lt;b&gt;추가 Point&lt;br /&gt;&lt;/b&gt;&lt;/u&gt;&lt;/i&gt;&lt;br /&gt;1 - PK 생성방식에 따라 과정에 차이가 있지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;결론적으로는 PK를 건너뛰게 된다&lt;/b&gt;&lt;/span&gt;. PK에 빈 값이 존재할 수 있는 경우 중 하나로 볼 수 있다.&lt;br /&gt;2 - 체크예외는 예상할 수밖에 없기 때문에 인지했다는 가정하에 커밋을 진행하게 되고, 언체크 예외는 예상하지 못할 수 있기 때문에 커밋을 진행하지 않는게 기본 동작이라고 예측된다고 한다 (&lt;a href=&quot;https://techblog.woowahan.com/2606/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;배민 블로그&lt;/a&gt;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;2번. 자식 함수에서 발생한 &lt;span style=&quot;color: #ee2323;&quot;&gt;체크 예외&lt;/span&gt;에 대한 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756218642315&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void test2_caseA() throws SomeCheckedException { // 자식 체크 예외 누수, 부모에서 누수
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_leak_checked(mc);
    // 이후로 바뀌어도 적용되지 않음
    mc.setName(&quot;parent changed name!&quot;);
}

@Transactional
public void test2_caseB() { // 자식 체크 예외 누수, 부모에서 잡음
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    try{
        child.child_leak_checked(frog); 
        mc.setName(&quot;parent changed name 2!&quot;); // 여기서 바꾼건 바뀐대로 저장되지 않는다
    } catch (SomeCheckedException e) { 
        mc.setName(&quot;parent changed name!&quot;); // 여기서 바꾼건 바뀐대로 저장된다
        System.out.println(&quot;parent caught checked exception!&quot;);
    }
}

-- Child Class
public void child_leak_checked(Mooncake mc) throws SomeCheckedException {
    mc.setName(&quot;Child Mooncake&quot;);
    throw new SomeCheckedException();
}

----------------
Table : Mooncake
1        Child Mooncake
3        parent changed name!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1번에서&amp;nbsp;알아본&amp;nbsp;바와&amp;nbsp;같이,&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;Checked&amp;nbsp;Exception&amp;nbsp;은&amp;nbsp;누수되어도&amp;nbsp;영속성&amp;nbsp;컨텍스트에&amp;nbsp;있는&amp;nbsp;객체에&amp;nbsp;대한&amp;nbsp;커밋은&amp;nbsp;진행&lt;/span&gt;시킨다.&amp;nbsp;잡지&amp;nbsp;않는다면&amp;nbsp;Service&amp;nbsp;단&amp;nbsp;이후로&amp;nbsp;예외가&amp;nbsp;누수되어&amp;nbsp;처리될&amp;nbsp;것이고,&amp;nbsp;catch&amp;nbsp;를&amp;nbsp;잡는다면&amp;nbsp;그냥&amp;nbsp;잡을&amp;nbsp;뿐이다.&amp;nbsp;당연히&amp;nbsp;예외가&amp;nbsp;발생한&amp;nbsp;함수&amp;nbsp;이후의&amp;nbsp;코드는&amp;nbsp;호출되지&amp;nbsp;않기때문에,&amp;nbsp;&quot;changed&amp;nbsp;name&amp;nbsp;2&quot;&amp;nbsp;로&amp;nbsp;변경되지&amp;nbsp;않는다.&amp;nbsp;참고로,&amp;nbsp;Child&amp;nbsp;에서&amp;nbsp;Checked&amp;nbsp;예외를&amp;nbsp;잡는&amp;nbsp;경우는&amp;nbsp;더&amp;nbsp;결과&amp;nbsp;예측이&amp;nbsp;쉽기&amp;nbsp;때문에&amp;nbsp;따로&amp;nbsp;진행하지&amp;nbsp;않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3번. 자식 함수에서 발생한 &lt;span style=&quot;color: #ee2323;&quot;&gt;언체크 예외&lt;/span&gt;에 대한 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756219009591&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void test3_caseA() {  // 자식 언체크 누수, 부모 언체크 누수
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_leak_unchecked(mc);
}

@Transactional
public void test3_caseB() {  // 자식 언체크 누수, 부모 잡음
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    try {
        child.child_leak_unchecked(mc);
    } catch (SomeUncheckedException exception) {
        mc.setName(&quot;parent changed name!&quot;);
        System.out.println(&quot;parent caught unchecked exception!&quot;);
    }
}

@Transactional
public void test3_caseC() {  // 자식 언체크 잡음
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_catch_unchecked(mc);
}

-- Child Class
public void child_leak_unchecked(Mooncake mc) {
    mc.setName(&quot;Child Mooncake&quot;);
    throw new SomeUncheckedException();
}

public void child_catch_unchecked(Mooncake mc) {
    mc.setName(&quot;Child Mooncake&quot;);
    try {
        throw new SomeUncheckedException();
    } catch (SomeUncheckedException e) { // 여기서 이름을 바꿀 수 있다 
        mc.setName(&quot;Child caught this mc unchk exc&quot;);
        System.out.println(&quot;child caught unchecked exception!&quot;);
    }
}

----------------
Table : Mooncake
2        parent changed name!
3        Child caught this mc unchk exc&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CaseA&amp;nbsp;는&amp;nbsp;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;Uncheck&amp;nbsp;가&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;Transactional&amp;nbsp;범위&amp;nbsp;밖으로&amp;nbsp;누수&lt;/span&gt;되었기&amp;nbsp;때문에,&amp;nbsp;Rollback&amp;nbsp;을&amp;nbsp;진행&lt;/span&gt;&lt;/b&gt;하게 된다. 1번 테스트에서 알아봤듯이 이 때도 PK는 사용되어 하나를 건너 뛰게 된다. Case B는 언체크 예외가 &lt;u&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Transactional 범위 밖으로 누수되지 않았기 때문에, 롤백이 진행되지 않는다&lt;/span&gt;&lt;/u&gt;. 또한, catch 구문에서 setName 을 통해 바꾼 이름 역시 반영이 되는걸 알 수 있다. Case C는 사실 &lt;span style=&quot;color: #ee2323;&quot;&gt;Parent 입장에서는 아무일도 일어나지 않은 것&lt;/span&gt;이기 때문에, 정상 저장이 된다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;위와&amp;nbsp;같이&amp;nbsp;체크&amp;nbsp;및&amp;nbsp;언체크&amp;nbsp;상황&amp;nbsp;여부와&amp;nbsp;try/catch&amp;nbsp;상황&amp;nbsp;여부의&amp;nbsp;차이로&amp;nbsp;인해&amp;nbsp;DB&amp;nbsp;반영에&amp;nbsp;차이가&amp;nbsp;있음을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;깔끔하게&amp;nbsp;구분하는&amp;nbsp;방법은&amp;nbsp;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Transactional 입장에서 Exception 을 감지했는가, 어떤 Exception 을 감지했는가?&lt;/b&gt;&lt;/span&gt; 만을 생각한다면 조금 더 깔끔하게 구분할 수 있다. 하지만, 이 경우는 Transactional 이 Child 의 범위로 전파되는 기본 속성 내일 경우였다. 만약, &lt;span style=&quot;color: #ee2323;&quot;&gt;Child 가 새로운 Transactional 을 가져가는 REQUIRES_NEW 가 적용되어 있을 경우&lt;/span&gt;에는 어떻게 다를지 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;4번. 자식 함수에서 REQUIRED_NEW Transaction, 2번 Test 와 동일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756219351461&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;... Test 로직 자체는 2번 Test 와 동일

-- Child Class
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void child_leak_checked(Mooncake mc) throws SomeCheckedException {
    mc.setName(&quot;Child Mooncake&quot;);
    throw new SomeCheckedException();
}

----------------
Table : Mooncake
1        Child Mooncake
3        parent changed name!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사실 지금까지 살펴본 것처럼 &lt;span style=&quot;color: #ee2323;&quot;&gt;Checked 는 기본적으로 Tx를 롤백하지 않기 때문에&lt;/span&gt;, &lt;b&gt;결과가 동일&lt;/b&gt;할 것임이 어느정도 예상할 수 있다. A Case 인 경우 체크 예외가 누수된채로 자식 Tx가 종료되고 부모 Tx도 누수된채로 종료될 뿐 결과는 차이가 없다. B Case 도 마찬가지이다. 체크 예외는 롤백 옵션이 아니기 때문에 차이를 만들 필요가 없기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;5번. 자식 함수에서 REQUIRED_NEW Transaction, &lt;span style=&quot;color: #ee2323;&quot;&gt;부모에서 언체크 예외&lt;/span&gt; 발생&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;체크 예외야 그렇다 쳐도, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;언체크 예외는 중요한 차이&lt;/b&gt;&lt;/span&gt;를 만들 수 있다. 하지만 &lt;b&gt;3번에서 한 예제&lt;/b&gt;들은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;자식에서 예외를 발생&lt;/b&gt;시키는 방식으로 test를 해서 새로운 Tx 옵션을 넣어도 DB상 결과 차이가 없다&lt;/span&gt; (생각해보면 알 수 있다). 따라서, 부모에서 예외가 발생할 수 있는 상황을 만들어 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1756219856877&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void test5_caseA() { // 자식 New Tx, 넘겨준 객체에 대해 변경, 부모에서 언체크 예외
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_change_name(mc);
    throw new SomeUncheckedException();
}

@Transactional
public void test5_caseB() { // 자식 New Tx, 넘겨준 객체에 대해 연관 객체 형성, 부모에서 언체크 예외
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_creates_related(frog);
    throw new SomeUncheckedException();
}

@Transactional
public void test5_caseA() { // 자식 New Tx, 각자 객체 형성, 부모에서 언체크 예외
    Mooncake mc = new Mooncake();
    mRepo.save(mc);
    child.child_creates_non_related();
    throw new SomeUncheckedException();
}

-- Child Class
@Transactional(Transactional.TxType.REQUIRES_NEW)
public void child_change_name(Mooncake mc) {
    mc.setName(&quot;Child Mooncake&quot;);
}

@Transactional(Transactional.TxType.REQUIRES_NEW)
public void child_creates_related(Mooncake mc) {
    Childcake cc = new Childcake(mc);
    cRepo.save(cc);
}

@Transactional(Transactional.TxType.REQUIRES_NEW)
public void child_creates_non_related() {
    Childcake cc = new Childcake();
    cRepo.save(cc);
}

----------------
Table : Mooncake
빈 테이블

Table : Childcake
2          No Parent childcake&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;A&amp;nbsp;Case&amp;nbsp;는&amp;nbsp;Child&amp;nbsp;에서&amp;nbsp;A가&amp;nbsp;생성한&amp;nbsp;속성을&amp;nbsp;바꾼&amp;nbsp;이후&amp;nbsp;flush&amp;nbsp;를&amp;nbsp;했더라도,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Parent&amp;nbsp;에서&amp;nbsp;잡고&amp;nbsp;있는&amp;nbsp;영컨이&amp;nbsp;롤백&lt;/b&gt;&lt;/span&gt;되기&amp;nbsp;때문에,&amp;nbsp;최종적으로는&amp;nbsp;저장되지&amp;nbsp;않는다.&amp;nbsp;&lt;b&gt;B&amp;nbsp;Case&amp;nbsp;는&amp;nbsp;&lt;/b&gt;재밌는&amp;nbsp;일이&amp;nbsp;일어나는&amp;nbsp;것을&amp;nbsp;알&amp;nbsp;수&amp;nbsp;있는데,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데드락이&amp;nbsp;발생&lt;/b&gt;&lt;/span&gt;한다. 이 경우는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;예외 상관 없이 데드락이 발생하고, 부모객체와 자식 객체 아무것도 최종적으로 저장되지 않은채 모두 롤백&lt;/b&gt;&lt;/span&gt;된다. 이는 FK 제약조건 때문이며, Parent row 를 한 Tx에서 추가하기 위해 대기 중 (자식 함수 종료 대기 중) 인데 다른 Tx에서 FK 제약조건으로 참조할 때 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;자식 Tx에선 해당 칼럼에 락을 얻기 위해 대기하기 때문에 발생&lt;/b&gt;&lt;/span&gt;한다. (&lt;u&gt;완료되지 않은 Tx를 다른 Tx가 참조&lt;/u&gt;하려 함, ACID 위배)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;C Case 는 REQUIRES_NEW 및 언체크 예외를 사용하기 적합한 Case&lt;/b&gt;&lt;/span&gt; 이다. 부모와 자식이 연관되어 호출하지만, 부모에서 롤백이 발생해도 자식에는 영향을 주지 않는 Case 이다. 자식은 완전히 독립적으로 Tx 를 종료하기 때문이다. 결과적으로 부모의 Mooncake 는 롤백되었지만, 자식의 &lt;span style=&quot;color: #ee2323;&quot;&gt;자체 Transaction 안에서는 Childcake 를 성공적으로 커밋&lt;/span&gt;하였다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;정리하며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Ii6p/btsP67PG91B/fwhekNP2enBYrRQHwYVH80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Ii6p/btsP67PG91B/fwhekNP2enBYrRQHwYVH80/img.jpg&quot; data-alt=&quot;편리한 라이브러리 사용에는 항상 대가가 따른다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Ii6p/btsP67PG91B/fwhekNP2enBYrRQHwYVH80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Ii6p%2FbtsP67PG91B%2FfwhekNP2enBYrRQHwYVH80%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;336&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;편리한 라이브러리 사용에는 항상 대가가 따른다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Transactional 입장에서 예외가 감지 되었는지, 감지 되었다면 어떤 종류의 예외인지를 파악하면서 로직을 짜야한다는 것을 알 수 있었다. 이 상황을 인지한다면 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;noRollbackFor 와 rollbackFor 옵션을 필요에 따라서 사용하며 더 완성도 있는 예외를 설계&lt;/b&gt;&lt;/span&gt;할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 REQUIRES_NEW&amp;nbsp;를&amp;nbsp;쓸&amp;nbsp;때는&amp;nbsp;굉장히&amp;nbsp;신중해야&amp;nbsp;할&amp;nbsp;것&amp;nbsp;같다.&amp;nbsp;반드시&amp;nbsp;필요한&amp;nbsp;경우에만&amp;nbsp;독립성을&amp;nbsp;보장해서&amp;nbsp;쓴다던가..&amp;nbsp;해야할&amp;nbsp;것&amp;nbsp;같다.&amp;nbsp;그&amp;nbsp;외에는&amp;nbsp;최종&amp;nbsp;Transactional&amp;nbsp;기준&amp;nbsp;어떤&amp;nbsp;예외가&amp;nbsp;감지되었는지&amp;nbsp;감지되지&amp;nbsp;않았는지를&amp;nbsp;잘&amp;nbsp;판단하면&amp;nbsp;앞으로&amp;nbsp;문제가&amp;nbsp;발생하진&amp;nbsp;않을&amp;nbsp;것&amp;nbsp;같다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/Spring 기본</category>
      <category>checked exception</category>
      <category>noRollbackFor</category>
      <category>REQUIRES_NEW</category>
      <category>rollbackFor</category>
      <category>transactional</category>
      <category>unchecked exception</category>
      <category>롤백</category>
      <category>언체크 예외</category>
      <category>체크 예외</category>
      <category>커밋</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/313</guid>
      <comments>https://mooncake1.tistory.com/313#entry313comment</comments>
      <pubDate>Fri, 30 May 2025 00:09:34 +0900</pubDate>
    </item>
    <item>
      <title>[Java Multi-Threading] 생산자 소비자 Queue 예제 연습해보기</title>
      <link>https://mooncake1.tistory.com/312</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;멀티 스레드 개발에 익숙해지는데 실전적인 상황에 대한 연습은 해보면 해볼 수록 도움이 된다. 실제로 메인 스레드에서 분리해서 비동기 작업을 하는 경우에 멀티 스레드를 활용해서 작업을 실행하는 경우는 많이 사용된다. 실제 개발 과제로 만날 법한 상황에 대한 시나리오를 한 번 마련해보고 이를 예제로 풀어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제 시작&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;멀티 스레드로 로그를 처리하는 시스템을 만든다고 하자. 여러 서비스로부터 로그를 앱이 수신하고, 모으다가 디스크에 일괄 저장하는 앱이다. 다음과 같은 요구사항들을 가져가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 서비스들이 스레드로 로그 메세지를 생성해서 전달한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;로그 메세지는 메세지 큐에 쌓이다가, 디스크에 저장을 처리하는 또다른 Writer 스레드로 인해 처리된다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Writer 스레드는 큐에서 하나씩 꺼낸다, 이 때, 메세지 큐에 저장된 순서대로 꺼내져야 한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Writer 스레드는 디스크에 저장할 때 flush 함수가 수행된다. 즉, 버퍼를 가져간다. 이 때, n개의 로그가 모이거나, t초 동안 주기적으로 로그를 디스크에 저장한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Writer 는 중간에 interrupt 될 수 있으며, interrupt 될 시 flush 시도된 데이터는 손실 없이 다시 시도 되어야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 가장 기본이 되는 Queue 공간을 만들어 준다. 일단 사실 Queue 는 내가 만들지 않아도 된다. 이미 만들어진 친구들을 사용하면 되기 때문이다. 하지만 연습이기도 하니까 직접 만들어보자. 또한, 위처럼 &quot;큐에 저장된 순서대로&quot;라는 요구사항이 있다면, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;소비자는 하나&lt;/b&gt;&lt;/span&gt;로 두는게 좋다 (newSingleThreadExecutor 같은 느낌으로). 안그러면 훨씬 복잡하기 때문이다.. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;실제로 풀어보실 분들은 여기까지 상황만 가지고 풀어보시면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고로 사용할만한 Queue 는 ConcurrentLinkedQueue 나, LinkedBlockingQueue 인데, 제공되는 자료구조를 사용하면 동기화 처리가 불필요하다. 전자는 큐에 제한이 없기 때문에, 생산자가 기다릴 필요가 없기 때문에고, 후자는 내부적으로 blocking 이 구현되어 있기 때문이다. 어쨌든 난 모니터 락을 사용해서 직접 큐를 아래와 같이 구성해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748700040503&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LogQueue {

    private final int MAX = 5;
    private final Queue&amp;lt;String&amp;gt; logQueue = new ArrayDeque&amp;lt;&amp;gt;(MAX);

    /*
     참고로 ArrayDeque 은 무한일 수 있고, 제한을 걸 수도 있다
     무한이면 wait 이 필요 없지 않나? 싶었지만, put 함수 자체에서도 저장하다 꼬일 수 있기 때문에 필요하다
    */

    public synchronized void submit(String log) {

        if (logQueue.size() &amp;gt;= MAX) { // 대기해야한다, 넣을 수 없음
            try {
                log(&quot;Log Queue 가득차서, 대기합니다&quot;);
                wait(); // 락 반납
            } catch (InterruptedException e) {
                log(&quot;대기중 인터럽트 발생, submit 은 예외처리됩니다&quot;);
                throw new RuntimeException(e);
            }
        }
        logQueue.add(log);
        notify(); // Worker 가 대기중이라면 로그에게 알린다
    }

    public synchronized String getLog() { // Writer 에서 뽑아간다

        if(logQueue.isEmpty()) {
            try {
                log(&quot;전달된 Log 없음, 대기합니다&quot;);
                wait();
            } catch (InterruptedException e) {
                log(&quot;대기중 인터럽트 발생, getLog 종료합니다&quot;);
                throw new RuntimeException(e);
            }
        }

        String log = logQueue.poll();
        notify(); // 대기중인 Producer 에게 알림
        return log;
    }

    public String printStatus() {
        return logQueue.toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 이번 경우는 ExecutorService 를 사용할 수 없다. ExecutorService 는 &quot;작업&quot; 단위로 전달하기에 특화되어 있기 때문에, 이번 경우는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;소비자를 직접 제어해야 하기 때문이다 (Buffer 를 제어해야 한다)&lt;/b&gt;&lt;/span&gt;. 다음과 같이 소비자를 만든다. Buffer Window 는 5로 잡자. (참고로 flush 함수는 처음에 LogSaver 안에 두었는데, FlushManager 라는 객체로 빠지게 된다. 아래 설명되어 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748700228730&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LogSaver implements Runnable {  // 버퍼를 관리하는 소비자 스레드

    private final int BUFFER_WINDOW = 5;
    private final List&amp;lt;String&amp;gt; logBuffer = new ArrayList&amp;lt;&amp;gt;();
    private final LogQueue queue;

    public LogSaver(LogQueue queue, FlushManager flushManager) {
        this.queue = queue;
        this.flushManager = flushManager;
    }

    public void writeToDisk() {
        String log = queue.getLog();
        logBuffer.add(log);

        if (logBuffer.size() == BUFFER_WINDOW) {
            log(&quot;소비자 스레드 로그 버퍼 가득참 :: FLUSH!&quot;);
            flushManager.flush(); // 추후 flushManager 가 도입된다. 글에 설명되어 있음
            logBuffer.clear();
        }

        System.out.println(&quot;현재 소비자 스레드내 로그 버퍼  모습: &quot; +  logBuffer);
    }

    @Override
    public void run() {
        while (true) {
            ThreadUtils.sleep(1000);
            log(&quot;주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다&quot;);
            writeToDisk();
        }
    }
}

----------

static class Producer implements Runnable { // 간단히 '로그'를 전달하는 생산자 스레드

    private final String log;
    private final LogQueue queue;

    public Producer(String log, LogQueue queue) {
        this.log = log;
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 로그를 씁니다
        queue.submit(log);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;LogSaver 는 우선 기본적인 요구사항인 &quot;Buffer Winow&quot; 관련된 요구사항을 해결해보자. 즉, 로그 버퍼에 N 개 만큼 쌓이면, flush 가 진행되는 것이다. 위 소비자 스레드는 지속적으로 Queue 에 생산되어 있는 &quot;로그&quot;를 뽑아서 버퍼에 저장한다 (너무 빨리 진행되면 보기 어려워서 1초 주기로 설정). 그리고 큐에서 뽑아올 로그가 없다면 WAITING 으로 진입, 생산자가 생산한 후 알려주면 다시 깨어날 것이다. 이와 같은 간단한 상황이 우선 해결되었는지 확인하기 위해 다음과 같이 테스트 해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748700680459&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LoggingGround {

    // 로그를 고른다. 임의로 생산자 스레드가 안에서 하나 고름
    private static final List&amp;lt;String&amp;gt; logInfo = List.of(&quot;이건 로그입니다&quot;, &quot;두번째 로그입니다&quot;, &quot;랜덤성 로그입니다&quot;, &quot;무슨 일이 발생했습니다&quot;
            , &quot;Exception Occured!!&quot;, &quot;mooncake 입니다&quot;, &quot;왓해픈?&quot;);

    public static void main(String[] args) throws InterruptedException {

        LogQueue logQueue = new LogQueue();
        FlushManager flushManager = new FlushManager(); // 글 읽다보면 뭔지 설명된다
        List&amp;lt;Thread&amp;gt; ths = new ArrayList&amp;lt;&amp;gt;();

        Thread t1 = new Thread(new Producer(pickLog(), logQueue), &quot;Producer 1&quot;);
        Thread t2 = new Thread(new Producer(pickLog(), logQueue), &quot;Producer 2&quot;);
        Thread t3 = new Thread(new Producer(pickLog(), logQueue), &quot;Producer 3&quot;);
        ...
        Thread t8 = new Thread(new Producer(pickLog(), logQueue), &quot;Producer 8&quot;);
        ths.add(t1);
        ths.add(t2);
        ths.add(t3);
        ...
        ths.add(t8);

        for (Thread th : ths) {
            th.start();
        }

        Thread.sleep(2000); // 실행까지 기다려 보자
        System.out.println(&quot;현재 LogQueue 모습: &quot; + logQueue.printStatus());

        // 로그들을 모두 저장했고, LogSaver 를 돌려보자.
        Thread logSaver = new Thread(new LogSaver(logQueue, flushManager));
        logSaver.start();

    }
}

--------------------

20:11:59.710 [Producer 6] Log Queue 가득차서, 대기합니다 --- 1)
20:11:59.714 [Producer 5] Log Queue 가득차서, 대기합니다
20:11:59.714 [Producer 7] Log Queue 가득차서, 대기합니다 
현재 LogQueue 모습: [랜덤성 로그입니다, 두번째 로그입니다, 무슨 일이 발생했습니다, 두번째 로그입니다, 두번째 로그입니다]
20:12:02.615 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [랜덤성 로그입니다]
20:12:03.620 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [랜덤성 로그입니다, 두번째 로그입니다]
20:12:04.624 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [랜덤성 로그입니다, 두번째 로그입니다, 무슨 일이 발생했습니다]
20:12:05.632 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [랜덤성 로그입니다, 두번째 로그입니다, 무슨 일이 발생했습니다, 두번째 로그입니다]
20:12:06.633 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
20:12:06.633 [ Thread-0] 소비자 스레드 로그 버퍼 가득참 :: FLUSH! --- 2)
현재 소비자 스레드내 로그 버퍼  모습: []
20:12:09.645 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [왓해픈?]
20:12:10.647 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [왓해픈?, 이건 로그입니다]
20:12:11.655 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
현재 소비자 스레드내 로그 버퍼  모습: [왓해픈?, 이건 로그입니다, 이건 로그입니다] --- 3)
20:12:12.670 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다
20:12:12.670 [ Thread-0] 전달된 Log 없음, 대기합니다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;결과는 1,2,3 부분만 확인하면 된다. 1) 에서는 큐에 가득 차서, 생산자 스레드 3개는 WAITING 으로 진입한 것을 확인할 수 있다. 그리고 이후로는 버퍼에서 1초에 한번씩 큐에서 뽑아와서 저장한다. 2) 에서는 Buffer Window 만큼 가득 차서, flush 가 수행되었다. &lt;b&gt;큐에서 로그를 뽑아내자마자 notify 받은 생산자 스레드는 다시 생산&lt;/b&gt;하기 시작하고, flush 가 발생한 이후 &lt;b&gt;3)에서는 세가지 로그만이 버퍼에 저장된 모습&lt;/b&gt;을 알 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;생산자들을 ExecutorService 로 해도 되지만, 그냥 직관적으로 하기 위해 위와 같이 진행했고, Buffer Window에 대한 요구사항은 해결되었고 로그는 성공적으로 잘 찍히는 모습을 확인할 수 있다. 문제는 다음 요구사항인 &quot;5초마다 flush&quot; 이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;이미 LogSaver 는 하나의 스레드로서 한가지 작업을 진행&lt;/b&gt;&lt;/span&gt;하고 있다. 하지만, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;이 스레드는 wait 상태를 반영&lt;/b&gt;&lt;/span&gt;하기 때문에, wait 상태로 진입하면 &quot;5초를 count&quot; 할 수가 없어 보인다. wait 한 곳에 TIMED 를 걸 수는 없는게 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;깨어나는 기준이 서로 다른 요구사항&lt;/b&gt;&lt;/span&gt;이기 때문이다. 데이터를 소모하기 위해 깨어나야 하는데, 5초 수행을 위해 깨어나면 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;데이터가 없어서 wait 시킨 의미가 없어진다&lt;/b&gt;&lt;/span&gt;. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;스레드로 수행하는 작업의 요구사항은 한 가지인 것이 책임 상 유리&lt;/b&gt;&lt;/span&gt;하기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;나는 많은 고민을 했지만, Log 큐의 소비자 스레드는 한가지였지, 다른 스레드를 쓰지 말란 말은 안했기 때문에.. 그냥 스레드를 하나 더 만들기로 했다. 그리고, FlushManager 를 도입하여, flush 를 독립적으로 수행하고 소비자 스레드끼리 서로 모르도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748702751558&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class FlushManager {

    public static void flush() throws Exception { // 원래는 DB에 저장할 객체를 전달 받을 것이다
        ...
        Thread.sleep(1000); // flush 하는데 걸리는 시간
        log(&quot;FLUSHED! Log saved and buffer emptied&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이 때, LogSaver 와 FlushManager 의 관계를 잘 생각해 볼 필요가 있다. FlushManager 가 LogSaver 의 필수적인 일부일 필요가 있을까? 우선 FlushManager 가 하는 일은, 사실 Logger 를 Flush 한다기보단 관리 중인 DB 쪽로&amp;nbsp;&amp;nbsp;Flush 를 해주는 친구이다. 즉, 쌓인 쿼리나 저장해야하는 대상을 전달받아 저장을 수행할 것이다. 가령, LogSaver는 자신의 Buffer 안에 있는 Log 를 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;DB로 밀어 넣어지는 객체로 생성해서 FlushManager에게 전달되던가 할 것&lt;/b&gt;&lt;/span&gt;이다. (그 사이에 Log 와 DB 화를 해주는 매개체는 생략된 모습)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;중요한건 FlushManager 는 LogSaver 와는 특별한 상관이 없다. FlushManager 는 그냥 쌓인걸 밀어 넣을 뿐이다. 따라서, LogSaver 가 FlushManager를 단순히 의존해야 한다. 또한, FlushManager 는 Spring 이였다면 Infra Bean 으로 존재했겠지만, 지금 예제는 Java 이므로 Main 함수에서 객체 하나를 생성해두는 방식으로 의존시키면 될 것이다. 따라서 Main 함수에 다음과 같이 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748701428983&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    ...
    // 로그들을 모두 저장했고, LogSaver 를 돌려보자.
    Thread logSaver = new Thread(new LogSaver(logQueue, flushManager));
    logSaver.start();

    // FlushManager 에게 독립적인 작업을 두는 친구도 존재한다
    Thread scheduledThread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                log(&quot;5초마다 스레드를 비웁니다&quot;);
                ThreadUtils.sleep(5000); // 최대한 단순하게 새로운 소비자 구현
                
                // ** LogSaver 를 의존해서, Buffer 에 있는 값을 DB 화해서 전달받는 로직 필요
                
                flushManager.flush();
            }
        }
    }, &quot;스케줄러스레드&quot;);
    scheduledThread.start();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고로 ExecutorService 에게 주기성을 부여하는 아래와 같은 친구를 만들 수도 있다. 단순히 5초마다 수행된다는 것을 직관적으로 짜기 위해 위와 같이 Thread.sleep 으로 구현하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748701547514&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(() -&amp;gt; queue.flush(), 5, 5, TimeUnit.SECONDS);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아무튼 이제 로그까지 확인하면, 다음과 같이 출력되는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;...&lt;br /&gt;현재&amp;nbsp;LogQueue&amp;nbsp;모습:&amp;nbsp;[mooncake&amp;nbsp;입니다,&amp;nbsp;Exception&amp;nbsp;Occured!!,&amp;nbsp;왓해픈?,&amp;nbsp;이건&amp;nbsp;로그입니다,&amp;nbsp;mooncake&amp;nbsp;입니다]&lt;br /&gt;20:16:13.676&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;5초마다&amp;nbsp;스레드를&amp;nbsp;비웁니다&lt;br /&gt;...&lt;br /&gt;20:16:17.703&amp;nbsp;[&amp;nbsp;Thread-0]&amp;nbsp;주기적인&amp;nbsp;LogSaver의&amp;nbsp;동작,&amp;nbsp;Queue&amp;nbsp;에서&amp;nbsp;Log&amp;nbsp;를&amp;nbsp;가져옵니다&lt;br /&gt;현재&amp;nbsp;소비자&amp;nbsp;스레드내&amp;nbsp;로그&amp;nbsp;버퍼&amp;nbsp;&amp;nbsp;모습:&amp;nbsp;[mooncake&amp;nbsp;입니다,&amp;nbsp;Exception&amp;nbsp;Occured!!,&amp;nbsp;왓해픈?,&amp;nbsp;이건&amp;nbsp;로그입니다]&lt;br /&gt;20:16:18.703&amp;nbsp;[&amp;nbsp;Thread-0]&amp;nbsp;주기적인&amp;nbsp;LogSaver의&amp;nbsp;동작,&amp;nbsp;Queue&amp;nbsp;에서&amp;nbsp;Log&amp;nbsp;를&amp;nbsp;가져옵니다&lt;br /&gt;20:16:18.703&amp;nbsp;[&amp;nbsp;Thread-0]&amp;nbsp;소비자&amp;nbsp;스레드&amp;nbsp;로그&amp;nbsp;버퍼&amp;nbsp;가득참&amp;nbsp;::&amp;nbsp;FLUSH!&lt;br /&gt;20:16:20.697&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;FLUSHED!&amp;nbsp;Log&amp;nbsp;saved&amp;nbsp;and&amp;nbsp;buffer&amp;nbsp;emptied&lt;br /&gt;20:16:20.697&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;5초마다&amp;nbsp;스레드를&amp;nbsp;비웁니다&lt;br /&gt;...&amp;nbsp;&amp;nbsp;&lt;br /&gt;20:16:22.729 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다 --- wait 인 생산자들 3개가 깨어남&lt;br /&gt;현재&amp;nbsp;소비자&amp;nbsp;스레드내&amp;nbsp;로그&amp;nbsp;버퍼&amp;nbsp;&amp;nbsp;모습:&amp;nbsp;[랜덤성&amp;nbsp;로그입니다,&amp;nbsp;mooncake&amp;nbsp;입니다]&lt;br /&gt;20:16:23.738&amp;nbsp;[&amp;nbsp;Thread-0]&amp;nbsp;주기적인&amp;nbsp;LogSaver의&amp;nbsp;동작,&amp;nbsp;Queue&amp;nbsp;에서&amp;nbsp;Log&amp;nbsp;를&amp;nbsp;가져옵니다&lt;br /&gt;현재&amp;nbsp;소비자&amp;nbsp;스레드내&amp;nbsp;로그&amp;nbsp;버퍼&amp;nbsp;&amp;nbsp;모습:&amp;nbsp;[랜덤성&amp;nbsp;로그입니다,&amp;nbsp;mooncake&amp;nbsp;입니다,&amp;nbsp;Exception&amp;nbsp;Occured!!]&lt;br /&gt;20:16:24.741 [ Thread-0] 주기적인 LogSaver의 동작, Queue 에서 Log 를 가져옵니다&amp;nbsp;&lt;br /&gt;20:16:24.742 [ Thread-0] 전달된 Log 없음, 대기합니다&amp;nbsp; ---- 더 이상 생산자들은 없다.&lt;br /&gt;20:16:27.707&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;FLUSHED!&amp;nbsp;Log&amp;nbsp;saved&amp;nbsp;and&amp;nbsp;buffer&amp;nbsp;emptied&lt;br /&gt;20:16:27.707 [&amp;nbsp;&amp;nbsp;스케줄러스레드] 5초마다 스레드를 비웁니다&amp;nbsp; &amp;nbsp; ---- 이 시점에서 남은 3가지가 flush 됨!!&lt;br /&gt;20:16:34.717&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;FLUSHED!&amp;nbsp;Log&amp;nbsp;saved&amp;nbsp;and&amp;nbsp;buffer&amp;nbsp;emptied&lt;br /&gt;20:16:34.718&amp;nbsp;[&amp;nbsp;&amp;nbsp;스케줄러스레드]&amp;nbsp;5초마다&amp;nbsp;스레드를&amp;nbsp;비웁니다&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이렇게 두 가지 스레드가 돌면서, 5초마다 그리고 가득찰 때마다 flush 를 수행하는 요구사항을 모두 해결한다. 소비자 스레드에 쌓인 버퍼를 flush 하기 때문에, FIFO 가 지켜진 이후라서 FIFO 요구사항은 지켜진다고 할 수 있다. 한계점도 생각해볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우선 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;다른 소비자 스레드가 LogSaver 에 있는 Log 를 비우라고 FlushManager 에게 말하는 것이 어색&lt;/b&gt;&lt;/span&gt;하다. FlushManager는 그냥 자신이 가지고 있는 &quot;DB 에 저장할 대상&quot;을 flush 한다. 따라서 위에 주석에 적어둔 것 처럼, 차라리 [LogSaver 소비자 스레드]안에서 Log 를 비우고 Entity 처럼 &quot;DB 저장 대상&quot;을 반환하는 함수를 두고, [스케줄러 스레드]도 이 LogSaver 함수를 사용하는 것이 어땠을까 하는 생각이 든다. 그러면 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;이 스레드도 flush 함수에게 &quot;DB 저장 대상&quot;을 전달하는 자연스러운 flow&lt;/b&gt;&lt;/span&gt; 가 만들어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어쨌든 5 개가 찼을 때 버퍼를 비워서 저장하는 요구사항, 그리고 혹시 5개 미만으로 찬 상태로 LogSaver 가 wait 상태로 진입했어도, T초 이후에는 자동으로 버퍼를 비워서 저장하는, 자연스러운 버퍼 요구사항들을 해결해볼 수 있었다. 사실 문제 자체가 어색하긴 하지만, 생산자/소비자 문제를 실전처럼 사용해볼 수 있었고, 좋은 연습이 되었던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;그밖에&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아직 해결하지 못한 요구사항이 있다. 마지막 요구사항이였던 &quot;Writer (소비자 스레드) 는 중간에 interrupt 될 수 있으며, interrupt 될 시 flush 시도된 데이터는 손실 없이 다시 시도 되어야 한다&quot; 를 사실 해결하지 않았다. 이걸 추가하자면, flush 함수에서 진행할 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748703195716&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void flush(){
    ...
    if(Thread.currentThread().isInterrupted()){
        // 저장시도하려던 객체를 반환, 혹은 DLQ 같은 공간에 저장
        throw RuntimeException();
    }
    // 실제 flush 작업
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어차피 최종 저장은 flush 가 진행될 때 수행되며, flush 함수는 소비자 스레드에서만 이루어진다. 그래서 &lt;span style=&quot;color: #006dd7;&quot;&gt;지금까지 인터럽트에 대한 요구사항은 &quot;제일 마지막에 한 번 최종 확인 할 수 있는 곳&quot;에서 하는게 가장 쉽게, 최대한 반영해 본 것 같다&lt;/span&gt;. 따라서, 저장 직전에 interrupt 상태를 조회하면 취소를 진행할 수 있다. 실제 flush 이후 상태까지 interrupt 를 반영하고 싶다면, &quot;DB에 삭제하는 로직&quot; 까지 더 반영하면 가능할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개인적으로 인터럽트 요구사항을 그렇게 이해하는 편은 아니다. Interrupt 는 스레드가 WAITING 상태일 때만&amp;nbsp; 말그대로 스레드를 중단(interrupt)시키기 때문이다 (ReentrantLock 의 lock&amp;nbsp; 함수 제외). 진행중인 스레드를 막기 위해선 위처럼 interrupt 체킹을 하는 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;programming-multitasking.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3upen/btsOl9HoAkL/LrlMUbCDouPy1JSVpOiwm1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3upen/btsOl9HoAkL/LrlMUbCDouPy1JSVpOiwm1/img.gif&quot; data-alt=&quot;내가 문제 풀 때랑 예제들을 볼 때랑은 너무 다르다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3upen/btsOl9HoAkL/LrlMUbCDouPy1JSVpOiwm1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/b3upen/btsOl9HoAkL/LrlMUbCDouPy1JSVpOiwm1/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;640&quot; height=&quot;480&quot; data-filename=&quot;programming-multitasking.gif&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내가 문제 풀 때랑 예제들을 볼 때랑은 너무 다르다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/312</guid>
      <comments>https://mooncake1.tistory.com/312#entry312comment</comments>
      <pubDate>Fri, 30 May 2025 00:03:48 +0900</pubDate>
    </item>
    <item>
      <title>[iSCSI] iSCSI 프로토콜을 통해 SAN 아키텍처 이해해보기</title>
      <link>https://mooncake1.tistory.com/310</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SAN 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-17 오전 12.08.58.png&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0idHt/btsN11JFiR1/wLLk0i29Gt93Ikv4X0U6C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0idHt/btsN11JFiR1/wLLk0i29Gt93Ikv4X0U6C1/img.png&quot; data-alt=&quot;SAN 스위치 허브를 기준으로 제공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0idHt/btsN11JFiR1/wLLk0i29Gt93Ikv4X0U6C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0idHt%2FbtsN11JFiR1%2FwLLk0i29Gt93Ikv4X0U6C1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;449&quot; height=&quot;414&quot; data-filename=&quot;스크린샷 2025-05-17 오전 12.08.58.png&quot; data-origin-width=&quot;716&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SAN 스위치 허브를 기준으로 제공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;컴퓨터 과학의 초창기에는 용량이 부족하면, 다른 컴퓨터에서 빼오든 새로 마련하든 물리적인 저장공간을 마련해서 추가해 주었다. 이런 &lt;b&gt;&quot;물리적&quot;인 측면에서 처음으로 벗어나게 해준 기술이 SAN 아키텍처 기술&lt;/b&gt;이다. SAN 아키텍처는 SAN 스위치 허브를 주심으로 스토리지들과 사용 PC 들을 연결해주는 구조를 말하며, 네트워크를 통해 PC에서 스토리지에 접속하는 개념이다. 오늘날&lt;b&gt; Block Storage 클라우드 서비스의 근간이 되는 구조가 바로 SAN 아키텍처&lt;/b&gt;이며, 대표적인 프로토콜로는 FC, iSCSI, iSER 이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;FC 는 Fibre Channel 로, 광케이블을 사용하는 통신 프로토콜을 말한다. 광케이블은 빛을 통해 데이터를 전달하기 때문에 전송 속도와 지연 시간이 비교할 수 없을 정도로 우수하며, 직접적으로 Storage 를 사용하는 아키텍처인만큼 빠른 처리가 중요했기 때문에 많이 광케이블 네트워크가 많이 사용되었다. 하지만 &lt;span style=&quot;color: #006dd7;&quot;&gt;광케이블은 인프라를 확보하는 비용이 크며 기간도 오래걸리는 등 불편함이 존재&lt;/span&gt;하기 때문에, NAS와 동일하게 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;이더넷 스위치를 기반으로 SAN 을 구축하는 iSCSI 프로토콜&lt;/b&gt;&lt;/span&gt;이 등장하게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Block Storage 와 SAN&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOdgJQ/btsN0xwBALg/033EKoj9NbwZQPGetguZYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOdgJQ/btsN0xwBALg/033EKoj9NbwZQPGetguZYK/img.png&quot; data-alt=&quot;Block Storage 는 SAN 이고 뭐고 모른다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOdgJQ/btsN0xwBALg/033EKoj9NbwZQPGetguZYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOdgJQ%2FbtsN0xwBALg%2F033EKoj9NbwZQPGetguZYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;389&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1536&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Block Storage 는 SAN 이고 뭐고 모른다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Block 이란 말은 정말 많이 듣는데, 간단하게라도 이게 뭔지 알고 가는게 좋다. 데이터는 Storage 에 저장될 때 &lt;b&gt;블록이나 레코드 단위로 저장&lt;/b&gt;되고, 관련된 것들을 모아서 논리적인 이름을 부여한게 파일이이며, 이 모아놓은 것들을 어떻게 관리하는지에 대한 얘기가 파일 시스템이다. &lt;b&gt;일정한 크기의 공간(4KB)으로 나누어 저장하는 방식이라는 뜻에서 Block 이라는 이름&lt;/b&gt;으로 부르고, Block Storage 역시 그냥 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;원래 일반 디스크 쓰듯이 Block 저장소로 쓸 수 있다고 해서 Block Storage 라고 부르는 것&lt;/b&gt;&lt;/span&gt;이다. Block Storage 와 SAN의 관계를 묻는다면, 아무 상관 없다고 보는게 맞는 것 같다. 로컬 디스크들도 모두 Block Storage 기 때문이다. 하지만 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Block Storage 를 &lt;span style=&quot;color: #006dd7;&quot;&gt;네트워크로 제공&lt;/span&gt;하기 위해선&lt;/b&gt; &lt;b&gt;SAN 아키텍처가 필요한 것&lt;/b&gt;&lt;/span&gt;이라고 볼 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;iSCSI 프로토콜&lt;/b&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞서 말했듯이 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;이더넷 스위치를 기반으로 SAN 아키텍처를 구성하는 iSCSI 프로토콜&lt;/b&gt;&lt;/span&gt;은 FC 보다 성능적으로는 떨어질 수 있지만, 이더넷 스위치는 가정집부터 어디에나 구축되어 있기 때문에 SAN 아키텍처를 구성하기 훨씬 쉬운데 확실한 정점이 있다. LAN 혹은 WAN 이 형성되어 있는 곳이면 쉽게 구축할 수 있기 때문이다. 이번 포스트에서도 iSCSI 프로토콜로 실습을 정리해놨는데, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;iSCSI 프로토콜 역시 &quot;이렇게 주고받자, 이런걸 정의하자&quot; 라는 약속&lt;/b&gt;&lt;/span&gt;이기 때문에, 어떤 약속들이 있는지 살짝 알아볼 필요가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IQN (iSCSI Qualified Name)&lt;/b&gt; - 프로토콜 상 &lt;span style=&quot;color: #ee2323;&quot;&gt;서버와 클라이언트를 서로 식별하는 고유 이름 (String)&lt;/span&gt; 으로, 약속된 형식이 있는데, 바로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&quot;iqn.yyyy-mm.domain-name:unique-name&quot;&lt;/b&gt; &lt;/span&gt;이 그것이다. domain-name 은 진짜 도메인일 필요 없다. 이 관계상 도메인 역할을 할 뿐이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LUN (Logical Unit Number) -&lt;/b&gt; 기억하면 좋다.&lt;span style=&quot;color: #ee2323;&quot;&gt; &quot;LUN은 Storage 제공측이 자신들이 제공할 디스크에 부여한 번호&quot;&lt;/span&gt;로, Client 입장에선 &lt;b&gt;가상 디스크 그 자체&lt;/b&gt;로 생각해도 좋다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ACL (Access Control List)&lt;/b&gt; - iSCSI 뿐만 아니라 많은 곳에서 사용되는 용어이다. &quot;나에게 접근을 허용하는 대상자 목록&quot;을 의미하며, iSCSI 같은 경우 &lt;span style=&quot;color: #ee2323;&quot;&gt;허용할 Client 의 IQN 목록&lt;/span&gt;이다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Target 과 Initiator&lt;/b&gt; - Target = Storage 제공 서버 = Target 에 대한 End Point 제공 서버. Initiator 는 이 Target 을 사용하는 Client 를 말한다. 쉽게 말하면 그냥 &lt;span style=&quot;color: #ee2323;&quot;&gt;서버 / 클라이언트를 iSCSI 에서 부르는 말&lt;/span&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brksWS/btsN1dKUi76/4qRWjbw4DB3NGA54vaJIB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brksWS/btsN1dKUi76/4qRWjbw4DB3NGA54vaJIB1/img.png&quot; data-alt=&quot;스토리지 제공받는 쪽이 Target 의 LUN들을 할당받아 사용한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brksWS/btsN1dKUi76/4qRWjbw4DB3NGA54vaJIB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrksWS%2FbtsN1dKUi76%2F4qRWjbw4DB3NGA54vaJIB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;531&quot; height=&quot;354&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;스토리지 제공받는 쪽이 Target 의 LUN들을 할당받아 사용한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실습하면서 더 이해가 가겠지만, iSCSI 서버에선 &quot;Storage 를 제공하는 객체&quot;를 만들 것이다. 이 객체는 식별하는게 IQN이며, 이 객체 안에는 TPG (Target Portal Group) 이라는 작은 주머니를 두게 된다. 이 TPG 안에는 IQN이 제공할 (자신이 갖고 있는) 디스크&amp;nbsp; (LUN), 여기에 허용된 친구들 (ACL) 등에 대한 정보가 들어있다고 보면 된다. 참고로 여러 TPG 를 둘 수도 있는데, 이는 보통 접속하는 경로를 달리하기 때문에 굳이 이것까지 들어가진 않겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SAN과 NAS의 차이점 (왜 SAN 이 그렇게 더 빠른가?)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한가지 또 생각해볼만한 부분은, NAS와 SAN의 차이인 부분이다. NAS 는 공유에 강하고, SAN 은 직접 마운트기 때문에 IOPS 와 Throughput 이 좋다는 사실은 유명하다. 하지만 의문이 들었다. NAS 가 이더넷 스위치를 기반으로 동작하기 때문에 IOPS 가 느리고, 파일 단위로 접근하기 때문에 느린 점은 이해했다. 하지만 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;iSCSI 프로토콜 역시 이더넷 기반으로 형성이 되는데, 왜 NAS 와 이렇게 차이가 나는걸까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이는 바로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;동작 계층의 차이&lt;/b&gt; &lt;/span&gt;때문이다. &lt;b&gt;NAS 는 &quot;서버&quot;가 파일 시스템을 직접 관리하고, &quot;클라이언트&quot;는 '파일 좀 제어할게요' 하면서 요청할 뿐&lt;/b&gt;이다. 하지만&lt;b&gt; iSCSI 는 SAN 아키텍처&lt;/b&gt;이기 때문에, 디스크 전체를 사용하도록 던지고 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;제공받는 쪽이 직접 파일 시스템을 관리&lt;/b&gt;&lt;/span&gt;한다. 이 모습 때문에 &quot;진짜 로컬 티스크처럼 사용&quot; 한다고 비유하는 것이다. NAS는 파일시스템이 해주는 일을 서버측에서 하기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;파일 열기/닫기, 파일 시스템의 관리 및 제어, 권한 확인 등등을 진행하는 시간에서의 결정적인 차이&lt;/b&gt;&lt;/span&gt;가 나는 것이다. (파일시스템이란 무엇인가는 좀 다른 주제로 갈 필요가 있을 것 같다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 이런 Storage 서비스 아키텍처들은 클라우드 시점에서 볼 필요가 있기 때문에, IaaS와 PaaS 관점에서 어디에 속하는지 생각해보는 것도 좋다. &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;NAS는 계속 뭔가 서버로서 제공&lt;/b&gt;&lt;/span&gt;해주는 느낌이 든다. 특히, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;공유 파일과 내부 파일 시스템 등을 서버가 관리하고&amp;nbsp; Client 는 &quot;파일 단위&quot;로만 서빙 받는 모습&lt;/b&gt;&lt;/span&gt;이기 때문이다. 따라서 PaaS인가 하는 생각이 들 수는 있지만, PaaS 에서 Platform 은 개발자가 오직 개발에만 집중하도록 지원해주는 역할이 가장 크다. NAS는 SAN 보단 서버가 개입하는게 많지만, 그래도 &lt;span style=&quot;color: #ee2323;&quot;&gt;Client 가 Storage mount 작업, 권한 관리, 백업 관리 등은 당연히 직접 제어를 해야 하는 수준&lt;/span&gt;이기 때문에, NAS 리소스 역시 IaaS 로 분류된다. SAN 은 당연히 기반 인프라를 제공하기 때문에 IaaS 이다. 따라서 둘은 목적과 특징에서 조금 차이가 있는 IaaS Level의 기술이라고 봐야 한다. 일반적으로 비교하는 모습은 다음과 같이 정리해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 177px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;NAS 아키텍처 기술&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;SAN 아키텍처 기술&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;클라우드 상 비유&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;파일 스토리지 서비스 (ex: AWS &lt;b&gt;EFS&lt;/b&gt;, Azure Files)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;블록 스토리지 서비스 (ex: AWS &lt;b&gt;EBS&lt;/b&gt;, Azure Disk Storage)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;접근 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;파일 단위 (파일 시스템이 제공됨)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;블록 단위 (디바이스처럼 제공됨, OS에서 포맷 필요)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;주로 연결되는 대상&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;여러 서버가 동시에 파일 공유&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;특정 인스턴스 (1:1 매핑으로 디스크처럼 붙음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;실제 사용 예&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;공유 설정 파일, 웹 콘텐츠, 로깅, 홈 디렉토리&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;데이터베이스, OS 디스크, 고성능 앱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;접근 시 프로토콜&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;NFS, SMB 등 (파일 시스템 계층 접근)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;iSCSI, NVMe-oF 등 (디스크처럼 접근)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;상대적으로 단순&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;상대적으로 복잡 (디스크 초기화 등 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;공유성,&lt;/span&gt;&lt;/b&gt; 간편함&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;성능, IO 처리량, &lt;b&gt;신뢰성 (특히 DB에 유리)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;확장성 측면에서&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;다수의 서버에 마운트 가능, 락 처리 고려 필요&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;보통 1:1,&lt;/span&gt; 확장하려면 디스크 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-17 오후 8.17.51.png&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wwVXo/btsN1913vDo/X6nB1ryhnwO8lJMPpFHgK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wwVXo/btsN1913vDo/X6nB1ryhnwO8lJMPpFHgK0/img.png&quot; data-alt=&quot;NAS는 공유가, SAN은 1:1 블록 디바이스 연결이 주 목적이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wwVXo/btsN1913vDo/X6nB1ryhnwO8lJMPpFHgK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwwVXo%2FbtsN1913vDo%2FX6nB1ryhnwO8lJMPpFHgK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;369&quot; height=&quot;367&quot; data-filename=&quot;스크린샷 2025-05-17 오후 8.17.51.png&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NAS는 공유가, SAN은 1:1 블록 디바이스 연결이 주 목적이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;iSCSI 프로토콜 실습&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;i&gt;# Server 10.123.123.1&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ lsblk&lt;/b&gt;&lt;br /&gt;...&lt;br /&gt;sdb&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8:16&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;600G&amp;nbsp;&amp;nbsp;0&amp;nbsp;disk&lt;br /&gt;└─sdb1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8:17&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;600G&amp;nbsp;&amp;nbsp;0&amp;nbsp;part&lt;br /&gt;&amp;nbsp;&amp;nbsp;├─vg001-appdata_lv&amp;nbsp;253:0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;100G&amp;nbsp;&amp;nbsp;0&amp;nbsp;lvm&amp;nbsp;&amp;nbsp;/appdata&lt;br /&gt;&amp;nbsp;&amp;nbsp;└─vg001-var_lv&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;253:1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;100G&amp;nbsp;&amp;nbsp;0&amp;nbsp;lvm&amp;nbsp;&amp;nbsp;/var/lib&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;예전에 여분의 LV 를 마련해둔 서버가 생각나서 이 서버에서 실습했다 ㅋㅋㅋㅋㅋ. 이 여분의 파티션이 존재하게 된 과정은 (&lt;a href=&quot;https://mooncake1.tistory.com/292&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mooncake1.tistory.com/292&lt;/a&gt;) 이 포스트 참고하면 된다. 결과적으로 위와 같은 상황이고 현 VG 내 400GB가 남으므로 간단히 10GB 를 공유해보자는 생각으로 새로운 LV 를 만들어서 이를 제공해보겠다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$ sudo lvcreate -L 10G -n san_test_lv vg001&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ lsblk&lt;/b&gt;&lt;br /&gt;sdb&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8:16&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;600G&amp;nbsp;&amp;nbsp;0&amp;nbsp;disk&lt;br /&gt;└─sdb1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8:17&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;600G&amp;nbsp;&amp;nbsp;0&amp;nbsp;part&lt;br /&gt;&amp;nbsp;&amp;nbsp;├─vg001-appdata_lv&amp;nbsp;&amp;nbsp;253:0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;100G&amp;nbsp;&amp;nbsp;0&amp;nbsp;lvm&amp;nbsp;&amp;nbsp;/appdata&lt;br /&gt;&amp;nbsp;&amp;nbsp;├─vg001-var_lv&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;253:1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;100G&amp;nbsp;&amp;nbsp;0&amp;nbsp;lvm&amp;nbsp;&amp;nbsp;/var/lib&lt;br /&gt;&amp;nbsp;&amp;nbsp;└─vg001-san_test_lv&amp;nbsp;253:6&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;&amp;nbsp;10G&amp;nbsp;&amp;nbsp;0&amp;nbsp;lvm&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하나만 더 알아두면 좋지만 Block Storage 자체는 LVM 시스템과는 별 상관이 없다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Block 디바이스인게 중요하기 때문에 실제 디스크, 디스크가 분할된 파티션 혹은 나처럼 LV로 준비&lt;/b&gt;&lt;/span&gt;해도 된다. 우선 Storage Server 부터 살펴보자.&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$ sudo apt update&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt install targetcli-fb&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;targetcli-fb 라는 프로그램을 설치한다. targetcli 란 리눅스 환경에서 iSCSI 서버를 구성하고 관리하는데 사용되는 스토리지 인터페이스이다. 그 안에서 여러가지 설정을 할 수 있는데, targetcli 명령어를 사용해서 해당 쉘 안에서 제어하면 된다. (쉽게 targetcli 프로그램을 사용할 수 있는 쉘을 사용할 수 있다). 우선 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;LV를 등록하는 것부터 시작할 것인데, 이 때 등록하는 &quot;경로&quot;는 블록 디바이스 경로&lt;/b&gt;&lt;/span&gt;여야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&amp;lt;개인적인 질문, 관심없으면&amp;nbsp; SKIP&amp;gt;&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;Q: 일반 경로 /appdata/appuser/hello 이런것과 /dev/vg~~ 이런걸 어떻게 구분해서 하는건지? targetcli 는 블록 디바이스만 등록할 수 있는데, 어떻게 이를 아는건지? 근본적으로는 어차피&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; LV 도 PV 에 의존해서 여기저기 분산해서 데이터를 저장하는 형태고, 일반 경로도 어떤 방식으로든 물리적 PV에 저장되는건데, 왜 LV 는 되고 일반 경로는 안되는건지 궁금&lt;/b&gt;&lt;/span&gt;했다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;A: LV는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;커널이 직접 접근 가능한 블록 자치의 인터페이스를 제공&lt;/b&gt;&lt;/span&gt;하지만, 일반 디렉토리는 그렇지 않기 때문이라고 한다. LV는 블록 장치로 커널이 직접 접근하지만,&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; 일반 디렉토리와 파일들은 &quot;파일시스템을 경유&quot;해서 접근&lt;/b&gt;&lt;/span&gt;하는 차이가 있다고 한다. &lt;span style=&quot;color: #ee2323;&quot;&gt;LV 나 다른 블록 디바이스들은 &quot;device-mapper&quot; 라는 커널 드라이버를 통해 커널이 사용할 수 있는 인터페이스를 제&lt;/span&gt;공하고, 일반 파일/디렉토리는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&quot;파일 시스템&quot;이 만든 객체&lt;/span&gt;이며, VFS 계층을 거쳐서 접근된다는 차이가 있고, 이를 targetcli 는 인식하기 때문에, &lt;b&gt;targetcli 에 등록할 때는 블록 디바이스만 가능&lt;/b&gt;하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;이제 우리가 만들어 둔 LV 를 등록하고, iSCSI 타겟을 만들어 IQN이 생성되는 모습을 확인해보자. targetcli 쉘을 실행해서 볼건데, 이 쉘 역시 내부적으로 ls, cd 같은 명령어는 기본적으로 지원된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$ sudo targetcli&lt;/b&gt;&amp;nbsp; &amp;nbsp;------ targetcli 쉘로 이동, 자체 프롬포트 띄워준다.&lt;br /&gt;targetcli&amp;nbsp;shell&amp;nbsp;version&amp;nbsp;2.1.fb43&lt;br /&gt;Copyright&amp;nbsp;2011-2013&amp;nbsp;by&amp;nbsp;Datera,&amp;nbsp;Inc&amp;nbsp;and&amp;nbsp;others.&lt;br /&gt;For&amp;nbsp;help&amp;nbsp;on&amp;nbsp;commands,&amp;nbsp;type&amp;nbsp;'help'.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;/&amp;gt;&amp;nbsp;ls&lt;/b&gt;&lt;br /&gt;o-&amp;nbsp;/&amp;nbsp;.........................................................................................................................&amp;nbsp;[...]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;backstores&amp;nbsp;..............................................................................................................&amp;nbsp;[...]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;block&amp;nbsp;..................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;fileio&amp;nbsp;.................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;pscsi&amp;nbsp;..................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;ramdisk&amp;nbsp;................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;iscsi&amp;nbsp;............................................................................................................&amp;nbsp;[Targets:&amp;nbsp;0]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;loopback&amp;nbsp;.........................................................................................................&amp;nbsp;[Targets:&amp;nbsp;0]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;vhost&amp;nbsp;............................................................................................................&amp;nbsp;[Targets:&amp;nbsp;0]&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 노드 내의 iSCSI 시스템의 현황을 확인할 수 있는 공간이며, backstores 는 제공할 storage 들이고, 우리가 관심있는 것은 block storage 이기 때문에 &quot;block&quot; 공간에 등록하며 된다. 명령어마다 출력되는 응답들도 주목해볼만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;/&amp;gt; /backstores/block create name=iscsi_sample_storage dev=/dev/vg001/san_test_lv&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;Created&amp;nbsp;block&amp;nbsp;storage&amp;nbsp;object&amp;nbsp;iscsi_sample_storage&amp;nbsp;using&amp;nbsp;/dev/vg001/san_test_lv.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;/&amp;gt;&amp;nbsp;/iscsi&amp;nbsp;create&amp;nbsp;iqn.2025-05.com.mooncake:iscsi.storage1&amp;nbsp;&amp;nbsp;##&amp;nbsp;iSCSI&amp;nbsp;타겟&amp;nbsp;(endpoint)&amp;nbsp;만들기&lt;/b&gt;&lt;br /&gt;Created&amp;nbsp;target&amp;nbsp;iqn.2025-05.com.mooncake:iscsi.storage1.&lt;br /&gt;Created&amp;nbsp;TPG&amp;nbsp;1.&lt;br /&gt;Global&amp;nbsp;pref&amp;nbsp;auto_add_default_portal=true&lt;br /&gt;Created&amp;nbsp;default&amp;nbsp;portal&amp;nbsp;listening&amp;nbsp;on&amp;nbsp;all&amp;nbsp;IPs&amp;nbsp;(0.0.0.0),&amp;nbsp;port&amp;nbsp;3260.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;iSCSI 와 연동될 block 을 /backstores/block 에 생성해줄 수 있다. 또한 iSCSI&amp;nbsp; 타겟을 형성하니까 해당 타겟에 대한 ACL, LUN, Portal 디렉토리가 형성되는 것을 확인할 수 있어서, 위에서 말했듯이 이들은 타겟별로 관리되는 정보들임을 알 수 있다. 이렇게 접속 경로, 인증 정보, LUN 구성 등을 모아놓은 단위 그룹인 TPG까지 확인할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size18&quot; data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다시 한 번 정리하면, 한 Endpoint (TG) 에는 TPG 를 부여할 수 있고, 이 EndPoint 에서 특정 TPG로 접근 (로그인) 할 경우, 요청자가 ACL에 있다면 해당 EndPoint 에서 제공하는 LUN을 사용하게 되는 것이다&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;/&amp;gt; ls&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; ####&amp;nbsp; 현재까지 확인할 수 있는 모습&lt;/b&gt;&lt;br /&gt;o- / ..................................................................................................................... [...]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;backstores&amp;nbsp;..............................................................................................................&amp;nbsp;[...]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;block&amp;nbsp;..................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;1]&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp; &amp;nbsp;o- iscsi_sample_storage ....................................... [/dev/vg001/san_test_lv (10.0GiB) write-thru deactivated]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;fileio&amp;nbsp;.................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;pscsi&amp;nbsp;..................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;ramdisk&amp;nbsp;................................................................................................&amp;nbsp;[Storage&amp;nbsp;Objects:&amp;nbsp;0]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;iscsi&amp;nbsp;............................................................................................................&amp;nbsp;[Targets:&amp;nbsp;1]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;o-&amp;nbsp;iqn.2025-05.com.mooncake:iscsi.storage1&amp;nbsp;...........................................................................&amp;nbsp;[TPGs:&amp;nbsp;1]&lt;br /&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;tpg1&amp;nbsp;...............................................................................................&amp;nbsp;[no-gen-acls,&amp;nbsp;no-auth]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp; &amp;nbsp; &amp;nbsp;o- acls .......................................................................................................... [ACLs: 0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;luns&amp;nbsp;..........................................................................................................&amp;nbsp;[LUNs:&amp;nbsp;0]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;portals&amp;nbsp;....................................................................................................&amp;nbsp;[Portals:&amp;nbsp;1]&lt;br /&gt;&amp;nbsp;&amp;nbsp;|&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;0.0.0.0:3260&amp;nbsp;.....................................................................................................&amp;nbsp;[OK]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;loopback&amp;nbsp;.........................................................................................................&amp;nbsp;[Targets:&amp;nbsp;0]&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;vhost&amp;nbsp;............................................................................................................&amp;nbsp;[Targets:&amp;nbsp;0]&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;이제 나의 iSCSI 프로토콜이 동작하도록 구성해주면 되어서, LUN, ACL 등을 구성해줄 차례다. &lt;b&gt;LUN을 생성한다는 것&lt;/b&gt;은&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; 해당 서버의 iSCSI 시스템에 등록된 내부 Storage 들을 해당 IQN 을 통해 사용할 수 있도록 매핑해주는 과정&lt;/b&gt;&lt;/span&gt;이라고 생각하면 된다. 즉, 서버가 특정 IQN(EndPoint, Target)에서 제공해줄 Storage 를 매핑(LUN 생성)해주면, Client 들이 왔을 경우 &quot;이 접근 경로로 왔어? ACL에 있네? 여기 LUN들을 줄게!&quot; 하고 연결해주는 거라고 생각하면 편하다. 이 부분은 Client 쪽까지 구성하면 훨씬 이해가 잘 가니, 계속 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;/&amp;gt; cd ~~&lt;/b&gt;&lt;br /&gt;&lt;b&gt;/iscsi/iqn.20...ge1/tpg1/luns&amp;gt;&amp;nbsp;create&amp;nbsp;/backstores/block/iscsi_sample_storage&lt;/b&gt;&lt;br /&gt;Created&amp;nbsp;LUN&amp;nbsp;0.&lt;br /&gt;&lt;b&gt;/iscsi/iqn.20...ge1/tpg1/luns&amp;gt; ls&amp;nbsp; &amp;nbsp; &amp;nbsp;#####&amp;nbsp; 생성된 lun 을 확인할 수 있고, 어떤 Storage 를 제공하는건지 확인 가능&lt;/b&gt;&lt;br /&gt;o-&amp;nbsp;luns&amp;nbsp;..................................................................................................................&amp;nbsp;[LUNs:&amp;nbsp;1]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;lun0&amp;nbsp;....................................................................&amp;nbsp;[block/iscsi_sample_storage&amp;nbsp;(/dev/vg001/san_test_lv)]&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 LUN 0 가 잘 만들어진 모습을 확인할 수 있다. LUN 은 여러개 만들 수 있는 것이고, 특정 iSCSI Target 은 여러 LUN 을 제공할 수 있다는 점을 알 수 있다. 이어서 ACL 을 생성해보겠다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;ACL에서는 허용할 클라이언트의 IQN이&lt;/b&gt;&amp;nbsp;&lt;b&gt;적혀있어야 한다&lt;/b&gt;&lt;/span&gt;. IQN 은 당연히 client 의 unique 함을 보장해야 하느니, 실무에서는 인증 과정이 당연히 더 필요하느니 하겠지만, 간단한 실습이기 때문에, 내 맘대로 IQN을 만들 것이고 이 값으로만 인증되게끔 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;/iscsi/iqn.20...ge1/tpg1/acls&amp;gt;&amp;nbsp;ls&lt;/b&gt;&lt;br /&gt;o-&amp;nbsp;acls&amp;nbsp;..................................................................................................................&amp;nbsp;[ACLs:&amp;nbsp;0]&lt;br /&gt;&lt;b&gt;/iscsi/iqn.20...ge1/tpg1/acls&amp;gt;&amp;nbsp;create&amp;nbsp;iqn.2025-05.com.mooncake:client1&lt;/b&gt;&lt;br /&gt;Created&amp;nbsp;Node&amp;nbsp;ACL&amp;nbsp;for&amp;nbsp;iqn.2025-05.com.mooncake:client1&lt;br /&gt;Created&amp;nbsp;mapped&amp;nbsp;LUN&amp;nbsp;0.&lt;br /&gt;&lt;b&gt;/iscsi/iqn.20...ge1/tpg1/acls&amp;gt;&amp;nbsp;ls&lt;/b&gt;&lt;br /&gt;o-&amp;nbsp;acls&amp;nbsp;..................................................................................................................&amp;nbsp;[ACLs:&amp;nbsp;1]&lt;br /&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;iqn.2025-05.com.mooncake:client1&amp;nbsp;.............................................................................&amp;nbsp;[Mapped&amp;nbsp;LUNs:&amp;nbsp;1]&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;o-&amp;nbsp;mapped_lun0&amp;nbsp;..........................................................................&amp;nbsp;[lun0&amp;nbsp;block/iscsi_sample_storage&amp;nbsp;(rw)]&lt;br /&gt;&lt;br /&gt;##&amp;nbsp; 설정이 완료된 시점에서 서버 설정 종료&lt;br /&gt;&lt;b&gt;&amp;gt; saveconfig&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;gt; exit&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;이 때 기본적으로 존재하는 LUN에 대해 해당 ACL을 매핑해줘서 해당 LUN을 쓸 수 있는 권한을 주게 된다. &lt;span style=&quot;color: #ee2323;&quot;&gt;해당 Client에게 제공하면 안되는 LUN이 있다면 해당 mapping 을 삭제&lt;/span&gt;하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이미 많이 헷갈리지만, Client 와의 상호작용 측면쪽으로 더 들어가보면 더 헷갈린다. 일단 들어가기 전에 다음과 같은 내용들을 인지한채로 Client 실습으로 들어가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방금 설정한 ACL의 정보는 &quot;IQN&quot; 뿐이다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Target 은 &quot;EndPoint에 접근할 수 있는 Client IQN과, 그 IQN이 자신의 어떤 LUN을 쓸 수 있는지&quot; 까지도 알고 있다! (3개 있어도 2개만 줄 수 있는 것)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Client가 iSCSI에 iSCSI 프로그램을 통해 로그인을 시도한다(EX: iscsiadm ~~ --login)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;해당 Endpoint에 LUN이 두개 매핑되어 있다고 해보자. 그렇다면 해당 Client IQN은 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;로그인 성공 시 &quot;자동으로 두 개의 디바이스를 할당받게 된다&quot;&lt;/b&gt;&lt;/span&gt;. 클라이언트가 ISCSI 에 연결되면 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&quot;나에게 허락된건 LUN0, LUN1 구나&quot; 하고 강제로 할당받아 버리고&lt;/b&gt;&lt;/span&gt;, 이에 대한&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt; 매핑된 내부 경로가 생성된다 (/dev/sdb, /dev/sdc 이런 가상 경로들 생성)&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이를 &quot;Client가 Target이 제공한 LUN에 대해 SCSI Inquiry를 진행한다&quot;고 표현한다&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이제 우리가 볼륨을 쓰던 방식대로 위 가상 경로들을 매핑해서 사용하면 된다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 위 절차들을 직접 살펴보기 위해 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Client 단 실습&lt;/b&gt;&lt;/span&gt;을 해보자. (참고로 Portal 설정도 있는데, portal 은 그냥 접근될 Server IP:3260 적으면 되는데, 일단 0.0.0.0 으로 유지하자)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;# Client 10.123.123.2&lt;/i&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt update&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt install open-iscsi&lt;br /&gt;&lt;/b&gt;...&lt;br /&gt;&lt;b&gt;$ sudo cat /etc/iscsi/initiatorname.iscsi&lt;br /&gt;&lt;/b&gt;&lt;br /&gt;##&amp;nbsp;DO&amp;nbsp;NOT&amp;nbsp;EDIT&amp;nbsp;OR&amp;nbsp;REMOVE&amp;nbsp;THIS&amp;nbsp;FILE!&lt;br /&gt;##&amp;nbsp;If&amp;nbsp;you&amp;nbsp;remove&amp;nbsp;this&amp;nbsp;file,&amp;nbsp;the&amp;nbsp;iSCSI&amp;nbsp;daemon&amp;nbsp;will&amp;nbsp;not&amp;nbsp;start.&lt;br /&gt;##&amp;nbsp;If&amp;nbsp;you&amp;nbsp;change&amp;nbsp;the&amp;nbsp;InitiatorName,&amp;nbsp;existing&amp;nbsp;access&amp;nbsp;control&amp;nbsp;lists&lt;br /&gt;##&amp;nbsp;may&amp;nbsp;reject&amp;nbsp;this&amp;nbsp;initiator.&amp;nbsp;&amp;nbsp;The&amp;nbsp;InitiatorName&amp;nbsp;must&amp;nbsp;be&amp;nbsp;unique&lt;br /&gt;##&amp;nbsp;for&amp;nbsp;each&amp;nbsp;iSCSI&amp;nbsp;initiator.&amp;nbsp;&amp;nbsp;Do&amp;nbsp;NOT&amp;nbsp;duplicate&amp;nbsp;iSCSI&amp;nbsp;InitiatorNames.&lt;br /&gt;InitiatorName=iqn.1993-08.org.debian:01:895b5c86fb8&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;iSCSI 접속 프로그램을 설치한 다음에 지금 사용하는 Client name 을 확인해볼 수 있다. 참고로 경고는 이거 수정하면 Server 단에서 너를 모를 수 있다 이런 내용인데, 지금은 간단한 Test 중이고 내마음대로 IQN 을 쓰고 있으니 수정해도 된다. 아까 내가 Server 해서 설정했던 Client 이름인 iqn.2025-05.com.mooncake:client1 으로 바꿔놓겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$&amp;nbsp;sudo&amp;nbsp;iscsiadm&amp;nbsp;-m&amp;nbsp;discovery&amp;nbsp;-t&amp;nbsp;sendtargets&amp;nbsp;-p&amp;nbsp;10.166.233.140:3260&lt;/b&gt;&lt;br /&gt;10.166.233.140:3260,1&amp;nbsp;iqn.2025-05.com.mooncake:iscsi.storage1&lt;br /&gt;&lt;br /&gt;&lt;b&gt;$ sudo iscsiadm -m node -T iqn.2025-05.com.mooncake:iscsi.storage1 -p 10.123.123.1:3260 --login&lt;/b&gt;&lt;br /&gt;Logging in to [iface: default, target: iqn.2025.mooncake~~, portal: 10.123.123.1,3260] (multiple)&lt;br /&gt;Login to [iface: default, target: iqn.2025.mooncake~~, portal: 10.123.123.1,3260] successful.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;iSCSI 서버 EndPoint 가 있는지 discovery 명령어를 통해 확인할 수 있고, 아까 우리가 설정해둔 Target IQN 을 확인할 수 있다. 그럼 이제 로그인을 하면, 위에서 말한대로 Inquiry가 진행되고 Client에 경로가 매핑될 것이다. 과연 디바이스 경로를 Client 에서 인식했을지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$ lsblk&lt;/b&gt;&lt;br /&gt;NAME&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MAJ:MIN&amp;nbsp;RM&amp;nbsp;&amp;nbsp;SIZE&amp;nbsp;RO&amp;nbsp;TYPE&amp;nbsp;MOUNTPOINT&lt;br /&gt;loop0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;7:0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;&amp;nbsp;62M&amp;nbsp;&amp;nbsp;1&amp;nbsp;loop&amp;nbsp;/snap/core20/1611&lt;br /&gt;...&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;sdc&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;8:32&amp;nbsp;&amp;nbsp;&amp;nbsp;0&amp;nbsp;&amp;nbsp;&amp;nbsp;10G&amp;nbsp;&amp;nbsp;0 disk&amp;nbsp; &amp;lt;&amp;lt;------------------ 마운트된 모습&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;lsblk 로 블록 디바이스를 확인해보면 맨 아래 sdc 로 10GB 짜리 디스크가 하나 추가된걸 확인&lt;/span&gt;&lt;/b&gt;할 수 있다. 이는 확실히 서버로부터 블록 스토리지를 제공받은 모습이며, disk type 으로 분류되며, 정말 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;물리적인 디바이스로 인식되기 때문에 PV 그룹에 포함시킬 수 있는 영역&lt;/b&gt;&lt;/span&gt;이 된 것이다!! (그냥 평소 &lt;span style=&quot;color: #006dd7;&quot;&gt;디스크랑 똑같이 파티션을 나눠서 사용해도 되고, 디스크 자체를 PV로 써도 된다&lt;/span&gt;). 파티션이든 디스크든 사용할 파일 시스템을 생성해준 이후 원하는 곳에 mount 해서 사용하면 되는 것이다. 일반 디스크처럼 &lt;b&gt;직접 마운트해서 사용해도 되고, LVM을 사용해도 된다 &lt;/b&gt;(&lt;b&gt;LVM은 볼륨 확장, 스냅샷, RAID 유사한 기능 등에서 이점&lt;/b&gt;을 얻기 때문에 이유가 있을 때 사용하는 것이라고 한다).&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고사항들을 정리해보자면, 우선 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;iSCSI 로그아웃을 하게되면 디바이스 자체가 사라진다&lt;/b&gt;&lt;/span&gt;. 따라서, 마운트된 채로 로그아웃을 진행하면, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;디스크 에러가 발생할 수 있고 이는 제법 치명적인 손상&lt;/b&gt;&lt;/span&gt;이다. 따라서, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;사용하는 경로를 Clean up 후 unmount 한 뒤에 로그아웃을 하는게 맞다&lt;/b&gt;&lt;/span&gt;. 또한, &lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;해당 디스크를 마운트해서 사용한다고 해서, 제공한 서버측에서 그 파일들을 확인할 수 있는 건 아니다&lt;/span&gt;&lt;/b&gt; (NFS와의 차이 인지). 이 모습은 진짜 디스크 자체를 그냥 넘겨준 모습으로 봐야하기 때문에, 마치 &lt;span style=&quot;color: #ee2323;&quot;&gt;타 서버의 파일을 그냥 보려는 행위&lt;/span&gt;와 똑같다고 보면 된다. 당연히 내 서버에 저장되어 있으므로 위치를 찾아서 강제로 읽을 수는 있겠지만, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;파일 시스템을 해석하거나 파일 내용을 보는 것은 불가능&lt;/b&gt;&lt;/span&gt;하다 (고 한다... 바이너리 블록 덩어리만 있을 뿐이다).&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;정리하며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NAS 아키텍처에 이어서 SAN 아키텍처까지 실습해보았다. 프로토콜들의 내용 부분이 조금 이해하기 어렵다는 생각은 들었지만, 어쨌든 아키텍처 구조의 전반적인 흐름을 이해하는데는 도움이 된 시간이였다. 기본기를 공부하는 과정은 항상 아깝지 않은 것 같다. 더 많은 Storage 기술들과 내용들을 이해하는데 기본이되는 내용들일테니, 잘 알아두기로 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnM6JC/btsN08JSME1/jgT4Kh00201mGKXMAxrlb0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnM6JC/btsN08JSME1/jgT4Kh00201mGKXMAxrlb0/img.jpg&quot; data-alt=&quot;FC, iSCSI 는 아직도 많이 사용되지만, NVMe-oF, RoCE, Object Storage 등을 많이 사용하는 시대이다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnM6JC/btsN08JSME1/jgT4Kh00201mGKXMAxrlb0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnM6JC%2FbtsN08JSME1%2FjgT4Kh00201mGKXMAxrlb0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;434&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;FC, iSCSI 는 아직도 많이 사용되지만, NVMe-oF, RoCE, Object Storage 등을 많이 사용하는 시대이다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #666666; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot;&gt;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747481895874&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;글루시스 기술 블로그&quot; data-og-description=&quot;A simple yet classy theme for your Jekyll website or blog.&quot; data-og-host=&quot;tech.gluesys.com&quot; data-og-source-url=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot; data-og-url=&quot;https://tech.gluesys.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bxodGa/hyYVaoEWGO/u7KMyKUUq0LudCnAlC2zlK/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/G4rde/hyYVa93bwC/fKHh71Abr6a6mLcUhg12k0/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/sEXoW/hyYWSObgDg/kpYy8JlgtfobwAkOt9tdfk/img.png?width=1536&amp;amp;height=1024&amp;amp;face=0_0_1536_1024&quot;&gt;&lt;a href=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bxodGa/hyYVaoEWGO/u7KMyKUUq0LudCnAlC2zlK/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/G4rde/hyYVa93bwC/fKHh71Abr6a6mLcUhg12k0/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/sEXoW/hyYWSObgDg/kpYy8JlgtfobwAkOt9tdfk/img.png?width=1536&amp;amp;height=1024&amp;amp;face=0_0_1536_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;글루시스 기술 블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A simple yet classy theme for your Jekyll website or blog.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.gluesys.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS 이론/Storage</category>
      <category>block storage</category>
      <category>Ebs</category>
      <category>FC</category>
      <category>iSCSI</category>
      <category>NAS</category>
      <category>San</category>
      <category>Storage</category>
      <category>블록 디바이스</category>
      <category>스토리지</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/310</guid>
      <comments>https://mooncake1.tistory.com/310#entry310comment</comments>
      <pubDate>Sat, 17 May 2025 00:07:52 +0900</pubDate>
    </item>
    <item>
      <title>[NFS] NFS 프로토콜을 통해 NAS 아키텍처 이해해보기</title>
      <link>https://mooncake1.tistory.com/309</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NAS 아키텍처&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.50.09.png&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diklpZ/btsNUOjq0ya/SriogiTeVpGKcPdmatVt6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diklpZ/btsNUOjq0ya/SriogiTeVpGKcPdmatVt6k/img.png&quot; data-alt=&quot;NAS 아키텍처 기본 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diklpZ/btsNUOjq0ya/SriogiTeVpGKcPdmatVt6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiklpZ%2FbtsNUOjq0ya%2FSriogiTeVpGKcPdmatVt6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;533&quot; height=&quot;304&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.50.09.png&quot; data-origin-width=&quot;705&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NAS 아키텍처 기본 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NAS 아키텍처&lt;/b&gt;는 Storage 개념이 강화되면서, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;업무 문서 및 주요 자료들에 대한 공유 및 동시 수정을 지원하기 위해 시작된 아키텍처&lt;/b&gt;&lt;/span&gt;이다. 일반 Storage 들은 블록이나 Record 단위로 파일들이 기록되지만, &lt;b&gt;NAS에서 데이터를 저장하고 사용하는 단위는 &quot;파일&quot;&lt;/b&gt;이다 (파일이란, 관련 데이터들을 묶어놓고 이름을 붙여놓은 논리적 단위이다).&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;NAS 의 강점은 쉬운 환경 조성&lt;/b&gt;&lt;/span&gt;에 있다. FC를 사용하는 SAN 과는 다소 다르게, 이더넷 허브를 기반으로 형성되기 때문에 지금 사용하고있는 LAN 환경에서 NFS 서버 / 클라이언트 프로그램만 있으면 간단하게 조성할 수 있고, PC에서 NFS 서버에 접근할 수 있다. 하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;TCP/IP 기반이며 파일 단위로 제어&lt;/span&gt;하기 때문에 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;낮은 throughput 과 IOPS&lt;/b&gt;&lt;/span&gt;, 그리고 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;높은 latency 는 고질적인 단점&lt;/b&gt;&lt;/span&gt;으로 여겨진다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번 포스트에서는 NAS 아키텍처를 직접 만들고, NAS 아키텍처가 동작하기 위한 근본이 되는 프로토콜인 NFS 프로토콜을&amp;nbsp; 간단히 알아보기 직접 사용해보면서 스토리지 기술들과 조금 더 친해져보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;헷갈리는 용어 정리하고 가기..&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 36px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;b&gt;NAS (Network Attached Storage)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;b&gt;분산 파일 시스템&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;b&gt;NFS (Network File System)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;공유 파일 시스템을 제공하기 위한 아키텍처&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;NAS 아키텍처 위에서, 여러 서버에 걸쳐 동작하는 파일 시스템을 말한다. 파일을 여러 서버, 여러 디스크에 분산하여 저장하는 시스템이다.&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;NAS 아키텍처에서 사용하는, 동작하기 위한&amp;nbsp; 프로토콜을 말한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NFS 프로토콜&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;NAS 아키텍처가 동작하는 대표적인 프로토콜은 그 유명한 NFS 프로토콜이다. 이 NFS 프로토콜에 대해서 너무 복잡하게 생각하지 말자. HTTP 프로토콜 처럼, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;그냥 파일에 대한 제어를 요청하는 NFS 클라이언트와, NFS 서버가 서로의 요청과 응답을 이해할 수 있는 약속&lt;/b&gt;&lt;/span&gt;이다. 보통 HTTP 를 우리가 만들 듯이 &lt;span style=&quot;color: #ee2323;&quot;&gt;NFS 를 막 만드는 경험은 드물고&lt;/span&gt; 보통 만들어진 프로그램들을 사용하기 때문에, 당연히 알면 좋겠지만 스토리지 접하는 시점에서 당장 구체적으로 알 필요는 없는 것 같다. 다만 기본적인 정보는 알고 있자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NFS&amp;nbsp;프로토콜은&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;File System 에 대한 작업을 원격으로 요청&lt;/b&gt;&lt;/span&gt;하는 프로토콜이다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;요청에는 파일 식별자, 요청 종류, (WRITE 경우) 데이터 등이 포함된다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;요청 종류로는 읽기, 쓰기, 생성, 삭제, 디렉토리 목록 조회, 복사, 이름 변경 등등등 우리가 맨날 서버에서 하는 것들이 있다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;NFS&amp;nbsp;프로토콜은&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;nbsp;OS에서&amp;nbsp;처리되도록&amp;nbsp;약속&lt;/b&gt;&lt;/span&gt;되어&amp;nbsp;있으며,&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;NFS 서버 프로그램은 NFSD 데몬을 통해서 요청을 처리&lt;/b&gt;&lt;/span&gt;한다&lt;br /&gt;(SSH 같은 프로토콜도 똑같은 것이다. 서버/클라이언트간 약속된 절차를 따르고, 데몬이 일을 수행해주는 구조이다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;NFS-Kernel-Server, NFS-common 프로그램 (NFS 서버 / 클라이언트)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NFS 프로토콜에 대한 실습을 해보기에 앞서, 실습에서 사용할 프로그램들을 통해 NFS 와 살짝은 더 친해져보자. &lt;b&gt;NFS-Kernel-Server 프로그램&lt;/b&gt;은 NFS 서버의 역할을 하는 대표적인 프로그램이다. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;&quot;서버&quot;의 역할이기 때문에, NFS 서버 데몬인 nfsd 가 프로세스로 띄워지고 역할을 수행&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제로 실무 Level 에서도 내부적인 공유 시스템을 간단하게 만들 때 사용되기도 한다. 이런 친구들이 NFS 요청 / 응답 다 만들어주기 때문에 우리는 그렇게 NFS 에 대해 당장 깊이 들어갈 필요 없다고 한 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;NFS-Common 프로그램&lt;/b&gt;은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;NFS 서버에서 공유하는 파일 시스템에 접근할 수 있는 실행 프로그램이다. &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;여러가지 명령어를 지원하며 명령어들로 process 를 실행하고 수행&lt;/b&gt;&lt;/span&gt;한다. NFS-Common 을 날 괴롭혔던 iptables 프로그램처럼&lt;b&gt; OS 수준에서 실행되는 프로그램&lt;/b&gt;이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;NFS Client 는 유저 요청에 대해 지정된 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;NFS 서버의 기본포트 2049 에게 연결해서 요청&lt;/b&gt;&lt;/span&gt;하려고 한다. &lt;b&gt;포트는 L4 계층 식별자이며 NFS 는 OS 단에서 제어&lt;/b&gt;되기 때문에, 2049 포트로 진입하면 NFSD 데몬이 소켓으로 이 요청을 수신한다는 것을 알 수 있다. 아무튼 파일을 생성하는 요청을 예시로 과정을 알아보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.23.35.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0oiUW/btsNUQ2CvMF/NTJ91LXL29H8ixOndLDpT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0oiUW/btsNUQ2CvMF/NTJ91LXL29H8ixOndLDpT0/img.png&quot; data-alt=&quot;우측부터 시작입니다.. 퀄리티 죄송&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0oiUW/btsNUQ2CvMF/NTJ91LXL29H8ixOndLDpT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0oiUW%2FbtsNUQ2CvMF%2FNTJ91LXL29H8ixOndLDpT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;699&quot; height=&quot;316&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.23.35.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우측부터 시작입니다.. 퀄리티 죄송&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;Client PC 에서 NFS 서버에 마운트된 경로에서 hello.txt 란 파일을 만든다고 해보자. 이 때, hello.txt 파일이 생성되는 시점에, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Client OS 는 당연히 로컬 파일 시스템 작업을 처리하기 때문에 디스크에 hello.txt 를 쓰려고 시도&lt;/b&gt;&lt;/span&gt;할 것이다. 하지만 NFS 클라이언트 프로그램이 있다면, &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;OS 는 로컬 파일 시스템 작업을 할 때 &quot;NFS 마운트 경로인지?&quot; 를 추가적으로 확인&lt;/b&gt;&lt;/span&gt;하며, 맞다면 이를 NFS 클라이언트에게 서버에 요청하라고 전달하는 구조이다. 그럼 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;CREATE 요청과 data에 hello.txt 의 내용이 들어간채로 NFS 서버 2049 포트로 전달&lt;/b&gt;&lt;/span&gt;되며, 해당 서버의 OS에서 자신의 로컬에 작업을 진행하는 구조이다. NFS 클라이언트와 서버는 이렇게 동작한다고 일단 이해하면 충분하다. 실습해보자.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실습&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;NFS 에 목적에 맞게 실습하는 것이 가장 좋은데, 처음에 하면 NFS 프로토콜이며, Client/Server 며 마치 NFS 프로토콜을 사용해서 파일을 다운받는 느낌인 줄 알았다. 하지만 막상 그런류가 아니라, 정말 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;말그대로 원격으로 파일을 제어하는 방식&lt;/b&gt;&lt;/span&gt;이다. 따라서 다음과 같이 NAS 아키텍처를 그려보면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;참고로 내가 헷갈렸던 이유는.. 그냥 볼륨 붙이고 하는 느낌인데 어떤 것을 읽든 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;계속 설명 중에 &quot;클라이언트의 요청&quot; 이라는 말&lt;/b&gt;&lt;/span&gt;이 나오기 때문이다 (그냥 내가 많이 부족해서 그렇다). 여기서 말하는 클라이언트의 요청이란, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;NFS 클라이언트가 내부적으로 수행하는 모든 요청&lt;/b&gt;&lt;/span&gt;을 말한다. 즉, ls, cat, echo, rm 등 많은 요청들이 날라가고, 이를 NFS 포로토콜로 패킷을 만들어서 서버에 전송하는 형태를 우린 보고있는 것이기 때문에 요청이라고 표현한다. 아무튼 다음과 같이 실습을 해보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.45.36.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yn82y/btsNTxQwrh2/d7i1MPmAWonuTjlij1csr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yn82y/btsNTxQwrh2/d7i1MPmAWonuTjlij1csr1/img.png&quot; data-alt=&quot;Server 경로에 Client 두 대가 각자의 경로에 Mount 되어있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yn82y/btsNTxQwrh2/d7i1MPmAWonuTjlij1csr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fyn82y%2FbtsNTxQwrh2%2Fd7i1MPmAWonuTjlij1csr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;666&quot; height=&quot;443&quot; data-filename=&quot;스크린샷 2025-05-12 오후 10.45.36.png&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Server 경로에 Client 두 대가 각자의 경로에 Mount 되어있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;한 NFS 서버에 Client 한 대가 파일을 넣으면, 다른 Client 에서는 해당 파일이 실시간으로 조회되는 모습을 실습해보자. 서버는 10.166.1.1 의 IP, Client 는 각자 10.166.2.1~2 의 IP 를 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;우선 NFS Server 역할 할 node 에 nfs-kernel-server 를 설치해보자. nfs-kernel-server 는 데몬 프로세스 이기 때문에, 바로 프로세스가 띄워져 있지는 않다. 다만, ps aux | grep nfsd 를 통해서 실행중인 데몬 프로세스들을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;# Server 10.166.1.1&lt;/i&gt;&lt;br /&gt;&lt;b&gt;$ mkdir /nfs-server/share&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ sudo chown nobody:nogroup /nfs-server/share&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt update&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt install nfs-kernel-server -y&amp;nbsp;&lt;/b&gt; ---------- nfs 서버 시스템 설치&lt;br /&gt;...&lt;br /&gt;&lt;b&gt;$ ps aux | grep nfsd&amp;nbsp; &amp;nbsp;&lt;/b&gt; -------- 데몬 프로세스 정상 실행중인지 확인&lt;br /&gt;root&amp;nbsp; &amp;nbsp; 2109086&amp;nbsp; &amp;nbsp; 0.0&amp;nbsp; 0.0&amp;nbsp; &amp;nbsp; 0&amp;nbsp; &amp;nbsp;0 ?&amp;nbsp; &amp;nbsp; S&amp;nbsp; &amp;nbsp; 18:18&amp;nbsp; &amp;nbsp; 0:00 [nfsd] &lt;br /&gt;root&amp;nbsp; &amp;nbsp; 2109087&amp;nbsp; &amp;nbsp; 0.0&amp;nbsp; 0.0&amp;nbsp; &amp;nbsp; 0&amp;nbsp; &amp;nbsp;0 ?&amp;nbsp; &amp;nbsp; S&amp;nbsp; &amp;nbsp; 18:18&amp;nbsp; &amp;nbsp; 0:00 [nfsd] &lt;br /&gt;root&amp;nbsp; &amp;nbsp; 2109088&amp;nbsp; &amp;nbsp; 0.0&amp;nbsp; 0.0&amp;nbsp; &amp;nbsp; 0&amp;nbsp; &amp;nbsp;0 ?&amp;nbsp; &amp;nbsp; S&amp;nbsp; &amp;nbsp; 18:18&amp;nbsp; &amp;nbsp; 0:00 [nfsd]&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;nogroup:nobody&amp;nbsp;권한&amp;nbsp;설정은&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&quot;익명&amp;nbsp;사용자에게&amp;nbsp;해당&amp;nbsp;경로에&amp;nbsp;대한&amp;nbsp;접근을&amp;nbsp;허용하는&amp;nbsp;것&quot;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;실제&amp;nbsp;실무&amp;nbsp;Level&amp;nbsp;에서는&amp;nbsp;더&amp;nbsp;fine-grained&amp;nbsp;한&amp;nbsp;user,&amp;nbsp;group&amp;nbsp;권한&amp;nbsp;control&amp;nbsp;이&amp;nbsp;필요할&amp;nbsp;것&lt;/span&gt;이다. 가령, 클라이언트가 user1 으로 nfs 서버에 연결하면, 이 user1 의 UID, GID 를 확인하여 해당 사용자의 권한을 확인한는 등... (상위 모듈과의 연계 필요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;계속 해보면, /etc/exportfs 파일은 nfs-kernel-server 가 NFS 프로토콜을 사용하여 공유시킬때 참조하는 파일이다. 다음과 같이 설정해 주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;$ sudo vim /etc/exportfs&lt;/b&gt;&lt;br /&gt;### (파일 맨 밑에 아래와 같이 추가, 공백 있으면 안된다)&lt;br /&gt;/nfs-server/share 10.166.2.1(rw,sync,no_subtree_check) 10.166.2.2(rw,sync,no_subtree_check)&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;# 설정 적용 후 재시작해준다&lt;br /&gt;&lt;b&gt;$ sudo exportfs -a&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ sudo systemctl restart nfs-kernel-server&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;느낌이 바로 오겠지만, 내가 만든 경로를 &quot;NFS 공유 경로&quot; 로 설정하는 일이다. 그리고 Client PC 들에게 해당 경로를 공유시키겠다는 설정을 적용하고, 옵션들을 넣어주는 것이다. 위에서 설정한 옵션들은 다음과 같고, 더 많은 옵션들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;rw&lt;/b&gt; : 읽고 쓰는 권한을 말한다&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sync&lt;/b&gt; : 동기식 모드로, CREATE 하는 요청을 받았을 경우 실제로 생성한 후에 응답하는 방식이다 (async 는 일단 대답하고 만듬)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;no_subtree_check&lt;/b&gt; : no 없이 적용하면, 하위 경로들에 대한 제어시 실제로 존재하는지 매번 검증해서, 비용이 크다고 한다 (보통 no)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;br /&gt;&lt;br /&gt;이제 NFS 서버는 준비가 되었다. 준비해둔 Client 두 대에는 nfs-common 을 설치한다. 그리고 위에서 제공한 NFS 서버의 경로와 mount 해줄 경로를 각각 준비한다( /nfs-client/share, /nfs-client2/share). 그리고 mount 명령어를 실행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;# Client PC - 10.166.2.1 / 10.166.2.2&lt;/i&gt;&lt;br /&gt;&lt;b&gt;$ sudo apt install nfs-common -y&amp;nbsp;&lt;/b&gt; &amp;nbsp; -------- 1, 2 번 Client 에 모두 설치&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;&lt;b&gt;$ sudo mount 10.166.1.1:/nfs-server/share /nfs-client/share&lt;/b&gt;&amp;nbsp; ------- 1번 Client 에서 실행&lt;br /&gt;&lt;b&gt;$ sudo mount 10.166.1.1:/nfs-server/share /nfs-client2/share&lt;/b&gt; ------- 2번 Client 에서 실행&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 2번 Client 에서 파일을 생성해보고, 1번 Client 에서 해당 경로를 lookup 해보겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;# Client PC 2 (10.166.2.2)&lt;/i&gt;&lt;br /&gt;&lt;b&gt;$ cd /nfs-client2/share&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ echo &quot;Hello From NFS Client2!&quot; &amp;gt; hello.txt&lt;/b&gt;&lt;br /&gt;...&lt;br /&gt;&lt;br /&gt;&lt;i&gt;# Client PC 1 (10.166.2.1)&lt;/i&gt;&lt;br /&gt;&lt;b&gt;$ cd /nfs-client/share&lt;/b&gt;&lt;br /&gt;&lt;b&gt;$ cat hello.txt&lt;/b&gt;&lt;br /&gt;Hello From NFS Client2!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이처럼 PC2에서 실행한 TXT 파일 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;Create 명령이 NFS-Client 프로그램을 통해 NFS 서버로 요청&lt;/b&gt;&lt;/span&gt;되었고, NFS 서버는 자신의 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;로컬 마운트 경로에 hello.txt 를 생성&lt;/b&gt;&lt;/span&gt;하였다. PC1에서는 마운트된 경로를 조회하고 특정 파일까지 조회하면 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;READ 명령이 다시 한번 NFS 서버로 날라가, 결과를 반환하고 콘솔에 출력&lt;/b&gt;&lt;/span&gt;해주는 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NFS 프로토콜과 이를 지원하는 프로그램들에 대해서 알아보았다. NFS 프로토콜을 기반으로 한 AWS의 EFS, NCP의 NAS Storage 와 같은 서비스로 실제 유저들이 많이 사용하는 유료 클라우드 서비스이며, 이와 같은 원리와 모습을 근간에 두고 있다. AWS 의 EFS 를 봐도, EFS 서버와 내 EC2 따위의 특정 경로를 마운트하는 과정을 필요로 한다. 그리고 &lt;b&gt;당연히 EFS에 마운트한 경로를 사용하는 EC2는 NFS 클라이언트를 설치&lt;/b&gt;해야한다 (&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/efs/latest/ug/mounting-fs-install-nfsclient.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AWS EFS 클라이언트 관련 Docs&lt;/a&gt;)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-12 오후 11.14.27.png&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vZ7A6/btsNVd4lQ3D/JViD4FHh6xaRQvWKOR4kgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vZ7A6/btsNVd4lQ3D/JViD4FHh6xaRQvWKOR4kgK/img.png&quot; data-alt=&quot;프로토콜들은 깊이 들어가면 어렵지만, 항상 너무 겁먹을 필요 없는 것 같다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vZ7A6/btsNVd4lQ3D/JViD4FHh6xaRQvWKOR4kgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvZ7A6%2FbtsNVd4lQ3D%2FJViD4FHh6xaRQvWKOR4kgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;479&quot; height=&quot;422&quot; data-filename=&quot;스크린샷 2025-05-12 오후 11.14.27.png&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로토콜들은 깊이 들어가면 어렵지만, 항상 너무 겁먹을 필요 없는 것 같다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot;&gt;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747054422901&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;글루시스 기술 블로그&quot; data-og-description=&quot;A simple yet classy theme for your Jekyll website or blog.&quot; data-og-host=&quot;tech.gluesys.com&quot; data-og-source-url=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot; data-og-url=&quot;https://tech.gluesys.com&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dwdjj6/hyYRoBf57O/KsetragFcyJo2ylkjTkS81/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/hVBho/hyYRpUspbG/QgR1xu8uIca5pwQ8GMpyx0/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/cfxawN/hyYRouuvGs/4WH43aKGMRdrGAzXch5d5K/img.png?width=1536&amp;amp;height=1024&amp;amp;face=0_0_1536_1024&quot;&gt;&lt;a href=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.gluesys.com/blog/2019/12/02/storage_1_intro.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dwdjj6/hyYRoBf57O/KsetragFcyJo2ylkjTkS81/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/hVBho/hyYRpUspbG/QgR1xu8uIca5pwQ8GMpyx0/img.png?width=612&amp;amp;height=605&amp;amp;face=0_0_612_605,https://scrap.kakaocdn.net/dn/cfxawN/hyYRouuvGs/4WH43aKGMRdrGAzXch5d5K/img.png?width=1536&amp;amp;height=1024&amp;amp;face=0_0_1536_1024');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;글루시스 기술 블로그&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A simple yet classy theme for your Jekyll website or blog.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.gluesys.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>CS 이론/Storage</category>
      <category>NAS</category>
      <category>NFS</category>
      <category>nfs client</category>
      <category>nfs protocol</category>
      <category>nfs server</category>
      <category>공유 파일</category>
      <category>분산 파일 시스템</category>
      <category>프로토콜</category>
      <author>문케이크</author>
      <guid isPermaLink="true">https://mooncake1.tistory.com/309</guid>
      <comments>https://mooncake1.tistory.com/309#entry309comment</comments>
      <pubDate>Mon, 12 May 2025 22:25:44 +0900</pubDate>
    </item>
  </channel>
</rss>