<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>IT - railroad</title>
    <link>https://sunro1994.tistory.com/</link>
    <description>IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다.
틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.</description>
    <language>ko</language>
    <pubDate>Thu, 27 Aug 2026 03:03:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>공부하고 기억하는 공간</managingEditor>
    <image>
      <title>IT - railroad</title>
      <url>https://tistory1.daumcdn.net/tistory/5934058/attach/5220f0ca23184fb4b7cb4120405416ae</url>
      <link>https://sunro1994.tistory.com</link>
    </image>
    <item>
      <title>[Stream 함수] Stream 함수 Reduce</title>
      <link>https://sunro1994.tistory.com/379</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;reduce&lt;/code&gt; 함수는 이름 그대로 &lt;b&gt;'줄여나가다'&lt;/b&gt;라는 뜻을 가지고 있습니다. 여러 개의 값이 담긴 컬렉션을 하나의 값으로 응축(Reduction)하는 것이 핵심입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &lt;code&gt;reduce&lt;/code&gt;의 3가지 인자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 &lt;code&gt;Stream.reduce&lt;/code&gt;는 용도에 따라 세 가지 형태로 제공됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;① &lt;code&gt;reduce(BinaryOperator&amp;lt;T&amp;gt; accumulator)&lt;/code&gt;&lt;/h3&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;/li&gt;
&lt;li&gt;&lt;b&gt;반환 타입:&lt;/b&gt; &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt; (스트림이 비어있을 수 있기 때문)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동작:&lt;/b&gt; 스트림의 첫 번째 요소를 초기값으로 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;② &lt;code&gt;reduce(T identity, BinaryOperator&amp;lt;T&amp;gt; accumulator)&lt;/code&gt;&lt;/h3&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;code&gt;identity&lt;/code&gt;)을 지정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반환 타입:&lt;/b&gt; &lt;code&gt;T&lt;/code&gt; (비어있어도 초기값이 있으므로 항상 값이 존재)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동작:&lt;/b&gt; 제공된 초기값과 스트림의 첫 번째 요소부터 연산을 시작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;③ &lt;code&gt;reduce(U identity, BiFunction&amp;lt;U, ? super T, U&amp;gt; accumulator, BinaryOperator&amp;lt;U&amp;gt; combiner)&lt;/code&gt;&lt;/h3&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; 병렬 처리(Parallel Stream)를 위한 &lt;code&gt;combiner&lt;/code&gt;가 추가됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동작:&lt;/b&gt; 각 스레드에서 생성된 중간 결과물들을 마지막에 하나로 합치는 역할을 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상세 프로세스&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시작:&lt;/b&gt; 누적값(Result)을 &lt;code&gt;identity&lt;/code&gt;로 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복:&lt;/b&gt; 스트림에서 다음 요소(Element)를 가져옵니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결합:&lt;/b&gt; &lt;code&gt;accumulator&lt;/code&gt; 함수를 실행하여 &lt;code&gt;(누적값, 현재 요소)&lt;/code&gt;를 계산하고, 그 결과로 누적값을 업데이트합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;종료:&lt;/b&gt; 스트림에 더 이상 요소가 없으면 최종 누적값을 반환합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 &lt;code&gt;reduce&lt;/code&gt;를 사용하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 합계라면 &lt;code&gt;sum()&lt;/code&gt;을 쓰면 되지만, &lt;code&gt;reduce&lt;/code&gt;는 훨씬 유연한 처리가 가능합니다.&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;/li&gt;
&lt;li&gt;&lt;b&gt;불변 객체 처리:&lt;/b&gt; 기존 객체를 변경하지 않고 새로운 결과를 만들어내는 함수형 프로그래밍 스타일을 유지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬성:&lt;/b&gt; 연산이 결합 법칙(Associative Law)을 만족한다면 병렬 스트림을 통해 대량의 데이터를 빠르게 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 주의사항&lt;/h2&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;code&gt;accumulator&lt;/code&gt; 내부에서 외부 변수의 상태를 변경하거나 스트림 요소를 수정해서는 안 됩니다. (Side-effect free)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결합 법칙:&lt;/b&gt; 병렬 처리를 할 경우 &lt;code&gt;(a + b) + c&lt;/code&gt;와 &lt;code&gt;a + (b + c)&lt;/code&gt;의 결과가 같아야 정확한 값이 보장됩니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>면접 준비/Java</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/379</guid>
      <comments>https://sunro1994.tistory.com/379#entry379comment</comments>
      <pubDate>Wed, 15 Apr 2026 16:20:47 +0900</pubDate>
    </item>
    <item>
      <title>[Claude] 토큰 최적화 + 구매할 옷 미리 입어보기 서비스(feat.개발자 관점)</title>
      <link>https://sunro1994.tistory.com/378</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 AI를 활용한 부업, 창업, 서비스 개발이 폭발적으로 늘어나면서 개발자들 사이에서 &lt;b&gt;클로드(Claude)&lt;/b&gt;의 점유율이 눈에 띄게 높아지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 클로드를 무분별하게 사용하다 보면 소모되는 '토큰' 비용과 컨텍스트 제한에 부딪히기 마련입니다. 오늘은 외부 API인 &lt;b&gt;구글 바나나(Google Banana)&lt;/b&gt;를 연동한 가상 피팅(Virtual Try-On) 서비스를 예시로, 클로드를 효율적으로 사용하기 위한 토큰 최적화 전략과 개발 자동화 구조를 공유해보고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 개요: AI 가상 피팅 서비스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 만들 서비스의 핵심은 간단하다. 사용자가 고른 옷 아이템을 자신의 사진에 합성하여, 실제로 입었을 때 어떤 느낌인지 미리 확인하는 서비스다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw83N8/dJMb9966kiP/fwAs3aBSKsDmRmt5G5Tom0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw83N8/dJMb9966kiP/fwAs3aBSKsDmRmt5G5Tom0/img.png&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;548&quot; data-is-animation=&quot;false&quot; width=&quot;389&quot; height=&quot;388&quot; data-widthpercent=&quot;57.92&quot; style=&quot;width: 57.2478%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw83N8/dJMb9966kiP/fwAs3aBSKsDmRmt5G5Tom0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw83N8%2FdJMb9966kiP%2FfwAs3aBSKsDmRmt5G5Tom0%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;550&quot; height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYy4Gn/dJMcahqxADp/5lx4vQKLVMKo2GYWN22Kbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYy4Gn/dJMcahqxADp/5lx4vQKLVMKo2GYWN22Kbk/img.png&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;1174&quot; data-is-animation=&quot;false&quot; width=&quot;468&quot; height=&quot;642&quot; style=&quot;width: 41.5894%;&quot; data-widthpercent=&quot;42.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYy4Gn/dJMcahqxADp/5lx4vQKLVMKo2GYWN22Kbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYy4Gn%2FdJMcahqxADp%2F5lx4vQKLVMKo2GYWN22Kbk%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;856&quot; height=&quot;1174&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트는 간단히 작성해도 결과적으로 좋은 퀄리티를 보여주는 것을 확인했다. 오른쪽 사진이 결과 사진이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/de0U9G/dJMcaaEXOpq/rC6u6AH0ionC5GUhmCuWZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/de0U9G/dJMcaaEXOpq/rC6u6AH0ionC5GUhmCuWZK/img.png&quot; data-origin-width=&quot;992&quot; data-origin-height=&quot;294&quot; data-is-animation=&quot;false&quot; style=&quot;width: 78.1544%; margin-right: 10px;&quot; data-widthpercent=&quot;79.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/de0U9G/dJMcaaEXOpq/rC6u6AH0ionC5GUhmCuWZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fde0U9G%2FdJMcaaEXOpq%2FrC6u6AH0ionC5GUhmCuWZK%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;992&quot; height=&quot;294&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVj8MM/dJMcacv0FDa/02JnfaCfutYGTHnqFLPzb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVj8MM/dJMcacv0FDa/02JnfaCfutYGTHnqFLPzb0/img.png&quot; data-origin-width=&quot;784&quot; data-origin-height=&quot;878&quot; data-is-animation=&quot;false&quot; width=&quot;395&quot; height=&quot;442&quot; style=&quot;width: 20.6829%;&quot; data-widthpercent=&quot;20.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVj8MM/dJMcacv0FDa/02JnfaCfutYGTHnqFLPzb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVj8MM%2FdJMcacv0FDa%2F02JnfaCfutYGTHnqFLPzb0%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;784&quot; height=&quot;878&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 서비스를 진행하기 위한 대략적인 기술스택과 구조는 아래와 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.클린 아키텍처 레이어와 토큰 절약의 상관관계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 흐름&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[ 외부 요청 ]
      │
      ▼
┌─────────────────────────────────────────────────────────────┐
│  Adapter (IN)  &amp;larr;── Controller, DTO                          │
│       │                                                     │
│       ▼                                                     │
│  Port (IN)     &amp;larr;── UseCase Interface                        │
│       │                                                     │
│       ▼                                                     │
│  Application Service  &amp;larr;── 비즈니스 로직, 트랜잭션 경계       │
│       │                                                     │
│       ▼                                                     │
│  Port (OUT)    &amp;larr;── Repository/AI/Storage Interface          │
│       │                                                     │
│       ▼                                                     │
│  Adapter (OUT) &amp;larr;── JPA, S3, AI API 구현체                   │
└─────────────────────────────────────────────────────────────┘
      │
      ▼
[ DB / S3 / 외부 API ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처를 고른 이유 또한 토큰 절약을 위한 전략 수단 중 하나였다. 그 근거는 아래 기술 블로그에서도 자세히 나와있으니 한 번 읽어보면 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로드는 대화 시 프로젝트 전체 컨텍스트를 파악해야 하므로 파일 구조가 비대할수록 토큰 소모량이 급증한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이어 세분화: 각 계층과 유즈케이스별로 역할을 명확히 분리함으로써, 클로드가 현재 작업 중인 특정 파일의 컨텍스트에만 집중할 수 있도록 유도한다.&lt;/li&gt;
&lt;li&gt;인터페이스 기반 설계: 인터페이스와 구현체가 분리되어 있어, 클로드는 전체 시스템을 이해하지 않고도 주어진 인터페이스 명세만으로 정확한 코드를 생성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://helloworld.kurly.com/blog/oms-claude-ai-workflow/#-ai-context%EC%97%90-%EC%9C%A0%EB%A6%AC%ED%95%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot;&gt;OMS에서 Claude AI를 활용하여 변화된 업무 방식&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 클로드 활용의 시작: CLAUDE.md 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 과정의 첫 시작은 CLAUDE.md를 작성하는 것이다. Root 폴더의 CLAUDE.md 파일을 프로젝트의 전체 개요를 보여주는 내용이며 CLAUDE를 시작하고 프로젝트를 읽기 시작할 때 가장 먼저 확인하는 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/init명령어를 작성하면 CLAUDE가 자동으로 생성해준다. 해당 파일이 거대해질 수록 클로드가 읽고 소모하는 토큰량이 방대해지므로 대략 300줄 이하로 구성하는것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;---
&amp;gt; AI 에이전트와 개발자 모두를 위한 프로젝트 구조 안내서.
&amp;gt; 새 파일을 추가하거나 기능을 확장할 때 반드시 이 문서를 먼저 참고할 것.

## 프로젝트 개요

**서비스명**: Work Lobby &amp;mdash; Virtual Try-On (가상 피팅 서비스)

**핵심 기능**: 사용자가 전신 사진과 옷 사진을 업로드하면, AI가 해당 옷을 입은 가상 피팅 이미지를 생성해 반환.

### 기술 스택

| 영역 | 기술 |
|------|------|
| Backend | Spring Boot 3.4.5, Java 17 |
| ORM | Spring Data JPA |
| Database | PostgreSQL (주 DB), Valkey (캐시) |
| Image Storage | AWS S3 |
| AI Fitting | Google Banana API (어댑터 구현 대기) |
| AI Agent (에러 분석) | Spring AI + Claude API (Tool Calling) |
| 알림 | Slack Webhook |
| CI/CD | GitHub Actions |
| MCP | GitHub MCP (PR 리뷰), Jira MCP (티켓 자동화) |

### 학습 목표

- 클린 아키텍처 (Hexagonal Architecture) 구조 이해 및 실습
- AI Agentic 프로그래밍 (Tool Calling, 에러 자동 분석)
- 외부 API 연동 패턴 (어댑터 교체 용이성)
- 장애 대응 자동화 (에러 &amp;rarr; AI 분석 &amp;rarr; Jira/GitHub 자동 티켓)
- GitHub MCP를 통한 PR 자동 리뷰
- Slack을 통한 팀 알림 및 진행도 공유

---

## 2. 멀티 모듈 구조 &amp;amp; 포트

work-lobby/ &amp;larr; 루트 프로젝트
├── api-gateway/ [ 예정 ] 포트: 8080
├── user-service/ [ 구현됨 ] 포트: 8081
├── fitting-service/ [ 구현됨 ] 포트: 8082
├── image-service/ [ 예정 ] 포트: 8083
├── notification-service/ [ 예정 ] 포트: 8084
└── error-monitor-agent/ [ 예정 ] 포트: 8085

| 모듈 | 포트 | 상태 | 역할 |
|------|------|------|------|
| `api-gateway` | 8080 | 예정 | Spring Cloud Gateway. 라우팅, 인증 토큰 검증, Rate Limiting |
| `user-service` | 8081 | 구현됨 | 회원 가입/조회. 인증 정보 관리 |
| `fitting-service` | 8082 | 구현됨 | 핵심 서비스. AI 가상 피팅 요청 처리, 이미지 히스토리 |
| `image-service` | 8083 | 예정 | 이미지 전처리 (리사이징, 포맷 변환), S3 업로드 위임 |
| `notification-service` | 8084 | 예정 | Slack Webhook 알림. Jira 티켓 상태 변경 시 팀 공유 |
| `error-monitor-agent` | 8085 | 예정 | 에러 로그 수집 &amp;rarr; Claude AI 분석 &amp;rarr; Jira/GitHub 자동 티켓 생성 |

---

## 개발 규칙

### 절대 규칙
- **main 브랜치 직접 Push 금지** &amp;rarr; 반드시 PR을 통해 머지
- **Private 값(API Key, DB 비밀번호 등) Git Push 금지** &amp;rarr; 환경변수 사용
- **Production 환경 직접 쿼리 금지** &amp;rarr; Dev 환경에서 먼저 검증
- **에러는 사람이 아닌 AI가 1차 분석** &amp;rarr; error-monitor-agent가 자동으로 Jira/GitHub 티켓 생성

### 코드 규칙
- 새 기능은 항상 **도메인 &amp;rarr; 포트 &amp;rarr; 서비스 &amp;rarr; 어댑터** 순서로 설계
- 서비스 간 데이터 공유는 **ID(UUID)** 만 사용 (엔티티 직접 참조 금지)
- JPA 엔티티는 도메인 계층에 노출 금지
- Controller는 변환과 위임만 담당 (비즈니스 로직 작성 금지)

### Git 브랜치 전략

main &amp;larr; 배포 브랜치 (직접 Push 금지)  
└── develop &amp;larr; 통합 브랜치  
└── feature/{모듈명}/{기능명} &amp;larr; 기능 개발  
└── fix/{모듈명}/{버그명} &amp;larr; 버그 수정

--- 

### Details
- /docs/api-spec.md (API 문서)
- /docs/db-schema.md (DB 스키마)
- /docs/architecture.md (아키텍처)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.그렇다면 나머지 내용은 어디에 적는것이 좋을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도 docs 패키지를 만들어서 기타 내용들을 세분화하는 것이 좋다. 그 예시로 API 스펙, ERD, 개인 의사결정, 아키텍처 등이 있을 것이다. 위 내용들에 대한 이정표를 세부사항에 적고 읽을수 있도록 세분화하고 필요시 해당 문서들을 읽게 하는 것이 좋다.&lt;br /&gt;위 내용에 대해서는 /memory 명령어를 사용하여 클로드가 어디까지 읽고 기억하고 있는지 확인할 수 있으며, 팀 단위 / 개인 단위로 메모리 내용을 관리할 수 있을 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.토큰을 줄일 수 있는 그 외의 필수 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;! shift+ tab을 사용하여 plan mode 유지하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;plan mode는 곧바로 실행시키는 것이 아닌 실행할 내용에 대해 한 번 더 확인하는 과정을 거쳐, 무분별한 토큰 사용을 줄일 수 있다. 만약 불필요한 행위를 하게 둔다면 최소 1000에서 많게는 몇만 토큰이 나도 모르게 소모될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;! /context로 사용량 수시로 체크하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/context를 사용해서 사용량을 확인하고 사용량이 80%이상이거나 모호한 답변을 사용하면 /compact, /clear를 사용해서 이전 내용을 압축하거나 불필요한 경우 삭제하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;! 복잡한 로직, 계획에는 Opus를 사용하고 간단한 반복 동작, 문서 작업에는 Sonnet을 사용하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Opus는 복잡한 사고시 적절한 플랜이지만 그만큼 토큰을 많이 소모한다는 단점이 있다. 신중히 사용하지 않으면 Pro 플랜에서는 금방 토큰이 소모된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>AI/Claude</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/378</guid>
      <comments>https://sunro1994.tistory.com/378#entry378comment</comments>
      <pubDate>Tue, 14 Apr 2026 21:03:43 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] 안정적인 이벤트 스트리밍 방식 설계하기</title>
      <link>https://sunro1994.tistory.com/377</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;면접에 앞서 카프카를 다시 한번 정리하면서 내가 카프카를 요구사항에 알맞게 설계하면서도 낮은 지연속도, 높은 대역폭등 효율적인 설계를 했는지 확인하기 위해 다시 한번 정리해보려고 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기/비동기 차이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 비동기 처리가 필요할까? 동기처리에는 대표적인 단점이 2개 존재한다.&lt;br /&gt;첫 번째, 느린 응답 시간이다. 동기 방식에서는 유저 정보 저장, 이메일 발송, 환영 쿠폰 지급 등의 작업이 모두 완료되어야 사용자에게 &quot;완료&quot; 응답을 제공할 수 있따. 만약 서버에 어떠한 문제가 발생하여 지연이 발생한다면 사용자는 한참을 기다려야 하고 사용자의 경험에 부정적인 영향을 미칠 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째, 각각의 서비스 서버에 영향을 미칠 수 있는 강한 결합상태가 된다. 회원 관련 도메인을 담당하는 사용자 서버와 이메일을 발송하는 서버가 따로 있다고 생각해보자. 회원가입 서비스와 이메일 서비스가 직접적으로 엮여있다. 이메일 서버에 장애가 발생하면 전체 회원가입 프로세스가 실패하는 등 독립성이 떨어지고, 하나를 수정하면 다른 서비스에 영향을 줄 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 처리와 메세지 큐&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간이 오래 걸리지만 당장 처리되지 않아도 되는 작업을 별도의 공간에 요청으로 발행하고 메인 서비스는 사용자에게 즉시 응답을 주는 방식이다.&lt;br /&gt;이를 통해 회원가입 요청이 완료되었다고 즉시 응답하여 사용자 대기 시간을 줄이고, 만약 작업량이 많아지거나 이메일 서비스에 장애가 생기더라도 사용자 서버에서 회원가입은 정상적으로 이뤄질 수 있도록 독립적인 대처가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 정리하자면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 요청에 대한 응답 시간을 단축하고, 마이크로 서비스 간의 결합도를 낮춰 시스템 전체의 안정성과 확장성을 확보하기 위해 도입한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다른 메세지 큐 방식과 차이점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 카프카를 사용할 것인지, 다른 기술과 어떤 차이점이 있어 결정하게 되었는지 선택의 근거가 필요하다.&lt;br /&gt;대표적인 메세지 큐는 RabbitMQ, Kafka, Redis Pub/SUb 등이 있을 것이다.&lt;br /&gt;카프카는 단순한 메시지 큐가 아닌, &lt;b&gt;대용량 실시간 데이터를 처리하기 위한 분산 이벤트 스트리밍 플랫폼&lt;/b&gt;이다. 예를 들어, RabbitMQ는 발행자가 메시지를 소비자에게 직접 배달(Push)하고 지우는 방식이라면, 카프카는 도서관처럼 데이터를 주제별(Topic)로 진열해 두고 소비자가 필요할 때 와서 읽어가는(Pull) 방식이다.&lt;br /&gt;내가 개발한 티켓 예매 시스템은 실시간으로 다수의 인원이 요청을 하고 이를 실시간으로 빠르게 처리해야 하는 요구사항이 있기 때문에 Kafka를 사용했다. 또한 데이터 재처리, 순서 보장이 가능하다는 강력한 장점에 끌리기도 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전형적인 메세지 큐방식과 이벤트 스트리밍 방식의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 큐와 이벤트 스트리밍의 큰 차이는 &lt;b&gt;데이터를 소비하는 방식, 데이터의 영속성&lt;/b&gt;에 있다.&lt;br /&gt;메시지 큐는 To-Do 목록처럼 한 번 해야 할 일을 하고 나서 지워버리는 반면에, 이벤트 스트리밍 방식은 일어난 일을 기록하여 보관하는 기간 동안 여러 번 다시 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메세지메시지 큐는 Queue 방식으로 FIFO의 특징을 갖고 한 번 소비자가 처리하면 큐에서 사라진다. 보통 point-to-point 방식으로 일대일 관계를 갖는다. 하나의 메시지는 하나의 소비자에게만 전달되고 사라지는 것이다. 대표적인 기술로는 RabbitMQ, SQS 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 스트리밍 방식은 로그를 사용하고 Append-only 특징을 갖고 소비자가 읽어도 데이터는 그대로 남아있다. 이는 Pub/Sub방식으로 하나의 이벤트를 구독하는 여러 소비자가 이벤트가 발행될 때마다 Polling 해서 읽는다. 설정된 보관 기간 동안 메시지를 유지할 수 있고 대표적인 기술로는 Apache Kafka, Kinesis 등이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 더 나아가 Flink라는 기술도 있다. 이는 실시간으로 데이터를 분석하고 복잡한 이벤트를 &lt;b&gt;변환/집계&lt;/b&gt;할 수 있는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 언제 무엇을 사용하는 것이 좋을까? 그 예시를 들어보겠다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메세지 큐 : &quot;이미지 변환 요청이 들어오면, 워커 중 하나가 받아서 처리해 줘. 누가 처리하는지는 상관없고, 한 번만 처리하면 돼&quot;처럼 단순히 작업을 분산시키고 싶을 때 사용한다.&lt;/li&gt;
&lt;li&gt;이벤트 스트리밍 : &quot;모든 사용자 클릭 이벤트를 중앙에 모아서, 사기 탐지팀, 실시간 대시보드팀, 데이터 분석팀이 각자 필요에 맞게 가져다 쓰게 하자&quot;와 같이 하나의 이벤트를 여러 시스템에서 활용해야 할 때 사용한다.&lt;/li&gt;
&lt;li&gt;플링크 : &quot;사용자 클릭 이벤트 스트림을 실시간으로 받아와서, 5분 단위로 페이지별 체류 시간을 계산하고, 비정상적인 패턴이 보이면 즉시 경고를 보내줘&quot;와 같이 흘러들어오는 데이터를 실시간으로 복잡한 계산이나 분석이 필요할 때 이벤트 스트리밍 시스템과 함께 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽, 파티션, 브로커 - 핵심 개념&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Topic, Partition, Broker&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 &lt;b&gt;토픽&lt;/b&gt;에 보관된다. 토픽에 보관되는 데이터의 양이 많아진다면 서버 한대로 감당하기 힘든 상황이 발생할 수 있는데 이를 해결하기 위해 &lt;b&gt;파티션&lt;/b&gt;을 사용한다. 파티션은 샤딩과 동일한 기법이다. 토픽을 여러 파티션으로 분할한 다음에 메시지를 모든 파티션에 균등하게 나눠 보낸다.&lt;br /&gt;파티션은 토픽에 보낼 메세지의 작은 부분집합인 것이다. 파티션은 메시지 큐 클러스터 내부에서 고르게 분산 배치된다.&lt;br /&gt;파티션을 유지하는 서버는 &lt;b&gt;브로커&lt;/b&gt;에 의해 관리된다.&lt;br /&gt;각 토픽 파티션은 FIFO 큐처럼 동작하며 같은 파티션 안에서는 메시지가 유지된다. 파티션 내에서 위치는 오프셋을 통해 확인이 가능하다.&lt;br /&gt;ID키 값을 가진 메세지는 같은 파티션으로 보내도록 설정할 수 있다.(hash % numPartitions)&lt;/p&gt;
&lt;h1&gt;Consumer Group&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 소비자 그룹은 여러 토픽을 구독할 수 있고 오프셋을 별도로 관리한다. 목적에 따라 정산과 관련된 이벤트를 받을 경우 과금용 , 회계용 , 그룹 등으로 나눠서 동일한 토픽을 여러 곳에서 받을 수 있는 것이다. 이를 병렬로 소비하여 효율적으로 사용할 수 있다.&lt;br /&gt;이 Consumer Group에서 주의해야 할 점은 메시지 순서의 보장이다. 그룹 내 소비자가 2개 이상일 경우 데이터를 병렬로 읽으면 대역폭 측면에서는 장점을 가져갈 수 있지만, 메시지를 각각의 소비자가 나눠가져 가므로 순서를 보장하는 것은 힘들기 때문이다.&lt;br /&gt;추가로 소비자 그룹의 소비자수는 파티션 개수와 동일하거나 더 작아야 한다. 소비자 수가 파티션 개수보다 더 클 경우 나머지 소비자는 해당 토픽에서 발행되는 메시지를 수신할 수 없기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;카프카의 상세 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카를 사용하면서 중요한 점은 데이터를 장기적으로 보관하면서 높은 대역폭을 제공하기 위한 설계를 하는 것이다.&lt;br /&gt;첫 번째, 회전 디스크의 높은 순차 탐색 성능, 적극적 디스크 캐시 전략을 활용한다.&lt;br /&gt;두 번째, 메시지가 생산자로부터 소비자에게 전달되는 동안 중간과정 없이 곧바로 전송이 가능하도록 메세지 자료 구조를 설계하고 활용해야 한다. 전송되는 데이터가 대량일 경우 메세지를 복사하는 비용도 막대하기 때문이다.&lt;br /&gt;세 번째, 일괄 처리를 우선시하는 시스템을 만들어야 한다. 일괄 처리가 되지 않는 경우 소량의 I/O가 빈번하게 일어날 텐데 이 경우에는 높은 대역폭을 보장하기가 어렵다. 생산자는 메시지를 일괄 전송하고, 메세지 큐는 그 메세지들을 더 큰 단위로 묶어서 보관한다. 소비자도 가능하면 메세지를 일괄 수신하도록 해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 저장소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 개발한 티켓 예매 서비스는 읽기와 쓰기가 빈번하게 일어나며, 순차적인 읽기/쓰기가 대부분이다.&lt;br /&gt;이때 데이터를 어디에 저장하는 것이 좋을까? 일반적인 선택지는 DB, NoSQL이 있을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDB: 토픽별로 테이블을 만들고 토픽이 보내는 메시지는 해당 테이블에 새로운 레코드로 추가한다.&lt;/li&gt;
&lt;li&gt;NoSQL: 토픽별로 컬렉션을 만든다. 토픽에서 보내는 메세지는 하나의 문서가 된다.&lt;br /&gt;하지만 이는 고려하기 어렵다. 읽기 연산과 쓰기 연산이 동시에 다량으로 들어오게 될 경우 이러한 데이터베이스는 정합성을 맞추기 위해 최신 정보로 쓰기 작업이 이루어진 후에 읽기 연산이 이루어지도록 lock을 사용할 수 있는데, 이때 지연이 발생하기 때문에 병목 구간이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 고려할 수 있는 방법은 &lt;b&gt;쓰기 우선 로그(Write Ahead Log, WAL)이다.&lt;/b&gt; 메시지 큐에서 새로운 메시지는 파티션의 마지막에 추가되며, 오프셋은 점진적으로 증가한다. 이 방식을 사용하게 되면 로그 파일 줄 번호를 오프셋으로 사용할 수 있다. 하지만 파일의 크기가 무한정으로 커지는 것은 서버에 부담을 줄 수 있으니 세그먼트 단위로 나눠서 고려하는 것이 좋다.&lt;br /&gt;세그먼트를 사용하는 경우 크기가 일정 한계에 도달하면 새 세그먼트 파일을 추가하도록 하고, 나머지 세그먼트는 비활성 상태로 바꾼다. 비활성 상태 세그먼트는 읽기 요청만 처리한다.&lt;br /&gt;이러한 방식의 이점은 WAL에 대한 접근 패턴은 모두 읽기/쓰기가 순차적이라는 것이다. 순차적이면 디스크의 이곳저곳을 이동하며 찾는 게 아니라 해당 위치에서 바로 옆에 찾는 값이 있기 때문에 좋은 성능을 보이는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 자료 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;Key : byte []&lt;/li&gt;
&lt;li&gt;value : byte [ ]&lt;/li&gt;
&lt;li&gt;topic : sting&lt;/li&gt;
&lt;li&gt;partition : integer&lt;/li&gt;
&lt;li&gt;offset : long&lt;/li&gt;
&lt;li&gt;timestamp : long&lt;/li&gt;
&lt;li&gt;size : integer&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Producer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생산자가 어떤 파티션에 메시지를 보내야 하는지 결정하는 과정은 어떻게 이루어질까? 이는 중개인를 통해 이루어지는데 브로커 중에도 &lt;b&gt;리더&lt;/b&gt; 브로커에게로 전달된다. 그 과정에서는 라우팅 계층이 사용된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;생산자는 메시지를 라우팅 계층으로 보낸다.&lt;/li&gt;
&lt;li&gt;라우팅 계층은 메타데이터 저장소에서 사본 분산 계획(각 파티션의 사본 분산 배치 정보)을 읽어 자기 캐시에 보관한다. 메시지가 도착하면 라우팅 계층은 파티션의 리더 사본에 보낸다.&lt;/li&gt;
&lt;li&gt;리더 사본이 우선 메시지를 받고 해당 리더를 따르는 다른 사본은 해당 리더로부터 데이터를 받는다.&lt;/li&gt;
&lt;li&gt;충분한 개수의 사본이 동기화되면 리더는 데이터를 디스크에 기록하고 이때부터 데이터는 소비 가능 상태가 된다. 기록이 끝나면 생산자에게 회신을 보낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 리더와 사본에 데이터를 공유하고 소비 가능 상태에 이르게 하는 이유는 장애에 대한 대처를 위함이다. 리더 브로커가 장애가 발생하게 되면 이를 대체할 브로커를 다른 사본 브로커중에서 찾는데 이 브로커들 또한 리더와 동일한 데이터를 갖고 있어야 하기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소비자는 특정 파티션의 오프셋을 반환하고 해당 위치에서부터 이벤트를 묶어 가져온다.&lt;br /&gt;소비자가 데이터를 소비하는 방식은 위에서 말했던 것처럼 Push 방식과 Pull 방식이 있는데 Kafka에서는 Pull 방식을 사용했다.&lt;br /&gt;Pull 모델의 장점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 소비하는 속도를 소비자가 직접 결정하기 때문에 각각의 소비자에 대한 소비 속도를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;일괄 처리에 적합하다. 생산자 중심으로 메시지 처리를 하게되면 소비자가 정상적으로 처리할 수 있는 속도인지 확인이 불가능하지만 소비자 중심이기 때문에 마지막으로 가져간 로그 위치 다음에 오는 메세지 혹은 설정된 최대 개수만큼을 한 번에 가져가서 조금 더 공격적인 일괄 처리를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점도 존재한다. 지속적으로 소비자는 데이터를 Pull 하는 스케줄링이 필요하기 때문에 자원이 낭비될 수 있다. 이를 해결하기 위해서 롱 폴링 모드를 사용할 수 있는데 당장은 가져갈 메시지가 없다면 일정 시간은 기다리도록 하는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pull 모델의 동작 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 것은 파티션 배치 정책이다. 기본적으로 우리가 알고 있는 RR(Round Robin)이나 범위 기반 정책, Sticky 정책 등 여러 가지가 있다. 이는 내가 이전에 블로그 스터디를 하면서 정리한 내용을 따로 포스팅하려고 한다.&lt;br /&gt;그럼 새로운 컨슈머를 그룹에 추가시키면 어떻게 파티션을 배정하고 수행하는지 알아보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;토픽 A를 구독하는 그룹 A가 있다. 새로운 소비자가 그룹 A에 추가되었다.&lt;/li&gt;
&lt;li&gt;소비자는 그룹 이름을 해싱하여 접속할 브로커 노드를 찾는다.&lt;/li&gt;
&lt;li&gt;이 브로커(코디네이터)는 해당 소비자를 그룹에 참여시키고 파티션을 새로운 소비자에게 할당한다.&lt;/li&gt;
&lt;li&gt;소비자는 마지막으로 소비한 오프셋 이후 메시지를 가져온다&lt;/li&gt;
&lt;li&gt;소비자는 메세지를 처리하고 새로운 오프셋을 브로커에게 보낸다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 언급한 &lt;b&gt;코디네이터&lt;/b&gt;는 파티션을 책임지는 브로커이다. 이 브로커는 소비자로부터 오는 &lt;b&gt;heartbeat&lt;/b&gt;를 통해 각 소비자의 생존 여부를 파악하고 각 소비자의 파티션 내 오프셋 정보를 관리한다. 만약 소비자 측에 장애가 ㅂ라생하여 박동 신호가 일정 시간 동안 사라지면 이는 사용할 수 없는 소비자라 판단하여 코디네이터는 재조정 프로세스를 시작한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메타 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서는 Zookeeper 또는 Kraft를 통해 메타데이터 및 상태를 저장할 수 있다. 현재 버전 이후로는 Zookeeper에 대한 유지보수가 종료되었고 Kraft만 지속적으로 업데이트되고 있는 것으로 알고 있다.&lt;br /&gt;&lt;a href=&quot;https://www.confluent.io/blog/zookeeper-to-kraft-with-confluent-kubernetes/&quot;&gt;참고 자료&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주키퍼는 Kafka 내부가 아닌 외부에서 관리를 해주는 서비스이다. 주키퍼에는 메타데이터, 상태 저장이 되어 있고 각 브로커들에 대한 조정이 이루어진다.&lt;br /&gt;즉, 주키퍼를 통해 브로커 클러스터의 리더 선출 과정을 돕는다.&lt;br /&gt;각 파티션은 이 기본적으로 장애에 대한 대처를 위해 3개의 사본을 갖는데 이 사본들은 서로 다른 브로커 노드에 분산되어 있다.&lt;br /&gt;각 각 파티션들은 리더를 갖고 있고 나머지는 리더에서 새 메시지를 지속적으로 가져와서 동기화를 진행한다.&lt;br /&gt;메세지를 완전히 동기화한 사본(ISR)들이 모두 동기화가 완료되면 리더는 생산자에게 메세지를 잘 받았다는 응답을 보낸다.&lt;br /&gt;이때 우리가 다루는 ack에 대한 값이 여기와 관련이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사본 동기화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화된 사본(ISR)은 In-Sync Replicas라는 뜻으로 리더와 사본이 모두 동일한 정보를 갖고 있는 상태를 말한다. 이때 우리가 다루는 속성 값 중 replica.lag.max.messages를 통해 사본과 리더 사이의 오프셋 차이를 설정할 수 있다.&lt;br /&gt;예를 들어 해당 값이 5로 정해졌다면 리더와 사본의 오프셋 차이는 4 이내가 되어야 한다는 뜻이다.&lt;br /&gt;리더의 오프셋이 15이고 사본 중 하나의 오프셋이 10인 상태이다. 그렇다면 해당 사본은 아직 ISR인 상태가 아닌 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ACK&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ack=1 : 리더가 메시지를 저장하고 나면 바로 응답을 받는다. 데이터가 동기화 될 때까지 응답을 기다리지 않으니 리더에 장애가 생기면 복구할 수 있는 방법이 없어지는 단점이 있다. 데이터가 사라져도 상관없는 대신 낮은 응답 지연을 보장하는 시스템에 적절하다&lt;/li&gt;
&lt;li&gt;ack=0 : 수신 확인 메세지를 기다리지 않고 계속 메세지를 전송하며 어떤 재시도도 하지 않는다. 온전히 낮은 응답 지연을 달성하기 위해 메세지 손실을 감수하는 설계이며, 지표 수집이나 데이터 로깅 등 메세지 양이 많지만 손실이 발생해도 크게 문제가 없는 경우 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;ack=all : 현재는 all이 기본값이다. 리더와 사본이 모두 응답을 받은 후에 ack 응답을 받는다. 가장 느리다는 단점이 있지만 메시지의 영속성 측면에서는 가장 보장받는 방법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메세지 전달 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달 방식에는 총 3가지가 있다. &lt;b&gt;최대 한 번/ 최소 한 번/ 정확히 한 번이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대 한 번 : 메시지를 최대 한 번(at-most once)만 전달하는 방식으로 메세지가 전달 과정에서 소실되더라도 다시 전달되지 않는다. ACK=0인 설정이며 소비자는 메세지를 읽고 처리하기 전에 오프셋부터 갱신한다. 만약 소비자가 장애로 죽더라도 메세지는 다시 소비할 수 없다.&lt;/li&gt;
&lt;li&gt;최소 한 번 : 같은 메시지가 한 번 이상 전달(at-least once)될 수는 있으나 메세지 소실은 발생하지 않는 방식이다. ack=1 or ack=all의 구성을 이요한다. 메세지가 브로커에게 전달되었음을 반드시 확인하고 전달이 실패할 경우 또는 타임아웃이 발생한 경우 계속 재시도를 한다.&lt;br /&gt;소비자는 데이터를 성공적으로 처리한 뒤에만 오프셋을 갱신하지만 만약 소비자가 오프셋을 갱신하지 못한 경우에는 다시 시작되어 중복 처리가 된다.&lt;/li&gt;
&lt;li&gt;정확히 한 번(exactly once) : 중복을 허용하지 않으며 항상 같은 결과(idempotency)를 내놓도록 구현되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정 외에도 Kafka의 이벤트와 Transaction의 일광성을 유지하도록 하는 Outbox 패턴, 직렬화/역직렬화를 통일하기 위한 매핑 방식의 전략, lag Monitoring을 확인하고 처리를 관리하는 방식등을 추가로 고려할 수 있을 것이다. 이 내용은 이전에 포스팅했던 내용 또는 기술 블로그 한 내용들을 통해 재포스팅할 예정이다.&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Kafka</category>
      <category>message queue</category>
      <category>Pub/Sub</category>
      <category>RabbitMQ</category>
      <category>zookeeper</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/377</guid>
      <comments>https://sunro1994.tistory.com/377#entry377comment</comments>
      <pubDate>Thu, 9 Oct 2025 01:42:09 +0900</pubDate>
    </item>
    <item>
      <title>[ELK / term] 특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때</title>
      <link>https://sunro1994.tistory.com/376</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 지난번에 사용했던 match 키워드가 있던 자리에 term을 사용해보려고 한다. 이전 블로그에서 사용했던 쿼리를 다시 사용하기 위해 일단 스니펫을 적어두겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 코드와 다른게 있다면 이번에는 하나가 아닌 두개의 데이터를 타입을 정의하고 넣을것이다. 왜냐하면 term과 terms를 통해 하나 이상의 값이 일치하는 데이터를 조회하는 과정이기 때문이다.&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;boards_id 와 categories 이렇게 두 개를 생성하고자 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758820055149&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELETE /boards

PUT /boards
{
    &quot;mappings&quot;: {
        &quot;properties&quot;: {
            &quot;boards_id&quot; : {
                &quot;type&quot;: &quot;long&quot;
            },
            &quot;categories&quot; : {
                &quot;type&quot; : &quot;keyword&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1758820130885&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /boards/_doc
{
  &quot;board_id&quot;: 1,
  &quot;category&quot;: &quot;익명 게시판&quot;
}

POST /boards/_doc
{
  &quot;board_id&quot;: 2,
  &quot;category&quot;: &quot;실명 게시판&quot;
}

POST /boards/_doc
{
  &quot;board_id&quot;: 3,
  &quot;category&quot;: &quot;공용 게시판&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 term을 먼저 사용해보겠다. term의 특징은 keyword와 연관되어있다. text는 일부 텍스트가 포함된 검색도 가능하지만(영어는 standard 에서 가능하지만 한글은 nori_tokenizer를 사용해야함) keyword는 정확히 일치하는 데이터를 조회한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758820234033&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;term&quot;: {
          &quot;boards_id&quot;: 2
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 조회하면 예상대로 딱 한개의 데이터가 hit된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bD9K9Y/btsQOk19Wva/SKDDECKXs3VK9zqleorOLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bD9K9Y/btsQOk19Wva/SKDDECKXs3VK9zqleorOLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bD9K9Y/btsQOk19Wva/SKDDECKXs3VK9zqleorOLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbD9K9Y%2FbtsQOk19Wva%2FSKDDECKXs3VK9zqleorOLk%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;652&quot; height=&quot;620&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;620&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이번에는 두 개 이상의 데이터를 조회해서 일치하는 것을 찾아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상하기로는 아래처럼 쿼리를 쓰면 될까...?하지만 이렇게 쓰면 에러가 발생한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758820318333&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;term&quot;: {
          &quot;categories&quot;: &quot;익명 게시판&quot;,
          &quot;boards_id&quot; : 1
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;term은 오직 하나의 데이터를 조회한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래와 같이 terms와 함께 must를 사용해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 새롭게 보이는 bool이 등장한다. must는 bool의 속성으로 must, must_not, should, filter가 있다.&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;must&lt;/b&gt; : SQL의 IN&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;must_not&lt;/b&gt; : SQL의 NOT&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;filter&lt;/b&gt; : SQL의 AND&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;should&lt;/b&gt; : SQL의 AND&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 SQL의 동작과정과 유사한 작동을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758820428082&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;bool&quot;: {
            &quot;must&quot;: [
              {
                &quot;term&quot; : {
                &quot;categories&quot; : &quot;광고 게시판&quot;
                }
              },{
                &quot;term&quot;: {
                    &quot;boards_id&quot; : 3
                }
              }
            ]
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TIFFd/btsQQyYRyss/bsruL00fCnH1ceGL5U4DOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TIFFd/btsQQyYRyss/bsruL00fCnH1ceGL5U4DOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TIFFd/btsQQyYRyss/bsruL00fCnH1ceGL5U4DOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTIFFd%2FbtsQQyYRyss%2FbsruL00fCnH1ceGL5U4DOK%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;712&quot; height=&quot;668&quot; data-origin-width=&quot;712&quot; data-origin-height=&quot;668&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter를 사용해도 결과는 동일한 것을 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758820619195&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;bool&quot;: {
            &quot;filter&quot;: [
              {
                &quot;term&quot; : {
                &quot;categories&quot; : &quot;공용 게시판&quot;
                }
              },{
                &quot;term&quot;: {
                    &quot;boards_id&quot; : 3
                }
              }
            ]
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x5X9g/btsQQH9mxvI/C6p7FEH2P1TJs7qyTeeiqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x5X9g/btsQQH9mxvI/C6p7FEH2P1TJs7qyTeeiqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x5X9g/btsQQH9mxvI/C6p7FEH2P1TJs7qyTeeiqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx5X9g%2FbtsQQH9mxvI%2FC6p7FEH2P1TJs7qyTeeiqK%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;676&quot; height=&quot;676&quot; data-origin-width=&quot;676&quot; data-origin-height=&quot;676&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에도 강조했지만 그렇다면 must와 filter는 어떤 상황에서 써야 하는 것일까?&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;ElasticSearch에는 Index를 조회하고 가져오는 과정에서 역색인을 수행하는데 이때 TF(Term Frequency)가 기본값으로 설정되어 있어서 빈번하게 조회되는 인덱스를 조회하는데 이때 score라는 것이 높은 것을 먼저 반환한다.&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;1. must는 score에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;must는 score를 사용하기 때문에 match와 함께 text 타입의 데이터를 조회힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. filter는 socre에 영향을 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter는 score에 영향을 주지 않기 때문에 term, terms와 함께 text를 제외한 키워드등의 데이터 타입을 조회한다.&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;</description>
      <category>ElasticSearch</category>
      <category>ElasticSearch</category>
      <category>ELK</category>
      <category>elk match</category>
      <category>elk must</category>
      <category>ELK 조건</category>
      <category>Kibana</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/376</guid>
      <comments>https://sunro1994.tistory.com/376#entry376comment</comments>
      <pubDate>Sat, 27 Sep 2025 03:20:38 +0900</pubDate>
    </item>
    <item>
      <title>[ELK / match] 검색 키워드가 포함된 데이터를 조회하고 싶을 때</title>
      <link>https://sunro1994.tistory.com/375</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 할때 사용할 수 있는 속성은 &lt;b&gt;match&lt;/b&gt;가 있습니다. &lt;b&gt;match&lt;/b&gt;는 &lt;b&gt;must&lt;/b&gt;와 함께 사용하고, &lt;b&gt;text&lt;/b&gt;타입의 데이터를 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;text&lt;/b&gt;는 유연한 검색에 쓰이는데 &lt;b&gt;must&lt;/b&gt;는 &lt;b&gt;score&lt;/b&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;must&lt;/b&gt; - &lt;b&gt;score&lt;/b&gt;에 영향을 주므로 &lt;b&gt;match&lt;/b&gt; 사용 - &lt;b&gt;text&lt;/b&gt; 타입 조회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;filter&lt;/b&gt; - &lt;b&gt;score&lt;/b&gt;에 영향을 주지 않으므로 &lt;b&gt;term &lt;/b&gt;또는 &lt;b&gt;terms&lt;/b&gt; 사용 - &lt;b&gt;text&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/boards라는 인덱스를 생성하기 전에 이전에 존재하는 boards를 삭제하고 새로 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 매핑을 정의한다. 매핑에는 title이라는 데이터를 생성하고 type은 text로 설정&lt;/p&gt;
&lt;pre id=&quot;code_1758819249595&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DELETE /boards

PUT /boards
{
    &quot;mappings&quot;: {
        &quot;properties&quot;: {
            &quot;title&quot;:{
                &quot;type&quot;: &quot;text&quot;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HjQXy/btsQQzQZUnj/YUJIXZ0a35H42P0KbPMp10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HjQXy/btsQQzQZUnj/YUJIXZ0a35H42P0KbPMp10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HjQXy/btsQQzQZUnj/YUJIXZ0a35H42P0KbPMp10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHjQXy%2FbtsQQzQZUnj%2FYUJIXZ0a35H42P0KbPMp10%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;540&quot; height=&quot;212&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;212&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;_doc 을 통해 새로운 title 데이터를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758819340860&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST /boards/_doc

{
  &quot;title&quot;: &quot;티스토리 블로그 글쓰기&quot;
}&lt;/code&gt;&lt;/pre&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckEO9V/btsQPKlcU9H/vjX62GNkbVlVkH8xLsZEwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckEO9V/btsQPKlcU9H/vjX62GNkbVlVkH8xLsZEwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckEO9V/btsQPKlcU9H/vjX62GNkbVlVkH8xLsZEwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckEO9V%2FbtsQPKlcU9H%2FvjX62GNkbVlVkH8xLsZEwK%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;544&quot; height=&quot;524&quot; data-origin-width=&quot;544&quot; data-origin-height=&quot;524&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 _search를 통해 생성한 title을 검색해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758819510098&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;match&quot;: {
          &quot;title&quot;: &quot;블로그&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 성공적으로 1개를 hit로 확인가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이때는 nori_tokenizer가 아닌 standard를 사용중이기에 일부 단어를 포함하는 검색은 할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t860L/btsQPgxTmvZ/V3bl1yckjxwTbqcqvnQ1F0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t860L/btsQPgxTmvZ/V3bl1yckjxwTbqcqvnQ1F0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t860L/btsQPgxTmvZ/V3bl1yckjxwTbqcqvnQ1F0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft860L%2FbtsQPgxTmvZ%2FV3bl1yckjxwTbqcqvnQ1F0%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;640&quot; height=&quot;670&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;670&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;
&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;pre id=&quot;code_1758819629696&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /boards/_search
{
    &quot;query&quot;: {
        &quot;match&quot;: {
          &quot;title&quot;: &quot;블로&quot;
        }
    }
}&lt;/code&gt;&lt;/pre&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;664&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NYljk/btsQO9llRF3/5l2FKd2gIkeFntkxvvCEGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NYljk/btsQO9llRF3/5l2FKd2gIkeFntkxvvCEGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NYljk/btsQO9llRF3/5l2FKd2gIkeFntkxvvCEGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNYljk%2FbtsQO9llRF3%2F5l2FKd2gIkeFntkxvvCEGK%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;664&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;664&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 공간을 간단히 도식화하면 다음과 같다.&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;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMcI1/btsQOkOAQJ7/n1abmDnxFvOToaTPZhS33K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMcI1/btsQOkOAQJ7/n1abmDnxFvOToaTPZhS33K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMcI1/btsQOkOAQJ7/n1abmDnxFvOToaTPZhS33K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMcI1%2FbtsQOkOAQJ7%2Fn1abmDnxFvOToaTPZhS33K%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;542&quot; height=&quot;661&quot; data-origin-width=&quot;690&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ElasticSearch</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/375</guid>
      <comments>https://sunro1994.tistory.com/375#entry375comment</comments>
      <pubDate>Fri, 26 Sep 2025 02:05:29 +0900</pubDate>
    </item>
    <item>
      <title>데이터 분석을 위한 ElastiSearch의 Query</title>
      <link>https://sunro1994.tistory.com/374</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ElasticSearch에서 데이터를 추가하거나 삭제, 수정하는 과정에서는 다양한 과정이 거쳐진다. 여기서 다룰 것은 기본적인 CRUD 명령어와 내부 옵션에 대한 설명이다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 인덱스 생성 / 삭제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;PUT /products
PUT /boards
PUT /products_reviews&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 새로운 인덱스를 생성 (테이블 생성과 유사).&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&amp;rarr; &lt;/span&gt;settings&lt;span&gt; 와 &lt;/span&gt;mappings&lt;span&gt; 지정 가능.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;DELETE /products
DELETE /boards&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 인덱스를 완전히 삭제 (문서 + 매핑 모두 제거).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 매핑(Mapping) 정의&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;PUT /products/_mapping
POST /products/_mapping&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 기존 인덱스에 필드 타입 정의 (단, 기존 필드 타입 변경은 불가)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;PUT /products
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;: { &quot;type&quot;: &quot;text&quot; }
    }
  }
}&lt;/code&gt;&lt;/pre&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;keyword&lt;/span&gt;: 정확 매칭/집계용&lt;/li&gt;
&lt;li&gt;&lt;span&gt;text&lt;/span&gt;: 풀 텍스트 검색용&lt;/li&gt;
&lt;li&gt;integer&lt;span&gt;, &lt;/span&gt;long&lt;span&gt;, &lt;/span&gt;double&lt;span&gt;: 숫자&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;boolean&lt;/span&gt;: true/false&lt;/li&gt;
&lt;li&gt;date&lt;span&gt;: 날짜&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 문서(Document) 조작&lt;/b&gt;&lt;/h2&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /products/_doc
{ &quot;name&quot;: &quot;Apple 2025 맥북 에어 13 M4&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;ID 지정하여 생성&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /products/_create/1
{ &quot;name&quot;: &quot;...&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 존재하지 않는 경우만 생성 (중복 ID는 에러)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;PUT /products/_doc/1
{ &quot;name&quot;: &quot;sunro2&quot; }&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 해당 문서 전체를 덮어씀.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;POST /products/_update/2
{
  &quot;doc&quot;: { &quot;age&quot;: 45 }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 해당 필드만 변경.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 조회 / 검색&lt;/b&gt;&lt;/h2&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /products/_doc/1&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; &lt;span&gt;_id=1&lt;/span&gt; 문서 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&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;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /products/_search&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;조건 검색(match)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET /products/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: { &quot;name&quot;: &quot;맥북 13 에어 M4&quot; }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; analyzer로 토큰화된 단어 단위 매칭.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건 검색 예시&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&quot;삼성&quot;&lt;/span&gt; &amp;rarr; &lt;span&gt;&quot;Samsung&quot;&lt;/span&gt;과 동의어 매칭 가능 (synonym filter 적용 시)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&quot;run&quot;&lt;/span&gt; &amp;rarr; &lt;span&gt;&quot;running&quot;&lt;/span&gt;과 매칭 가능 (stemmer 적용 시)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 텍스트 분석(Analyze API)&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 analyzer 확인&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /_analyze
{
  &quot;text&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;,
  &quot;analyzer&quot;: &quot;standard&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;사용자 정의 analyzer 확인&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GET /products/_analyze
{
  &quot;field&quot;: &quot;name&quot;,
  &quot;text&quot;: &quot;Samsung Notebook&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;rarr; 인덱스에서 설정한 analyzer 기준으로 토큰화 결과 확인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;char_filter / tokenizer / filter 직접 지정&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /_analyze
{
  &quot;text&quot;: &quot;백화점에서 친구를 만났다.&quot;,
  &quot;tokenizer&quot;: &quot;nori_tokenizer&quot;,
  &quot;filter&quot;: [&quot;nori_part_of_speech&quot;,&quot;nori_readingform&quot;,&quot;lowercase&quot;]
}&lt;/code&gt;&lt;/pre&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 커스텀 Analyzer 설정 예시&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&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;Lowercase 변환&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;filter&quot;: [&quot;lowercase&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;Stopwords 제거&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;filter&quot;: [&quot;stop&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;Stemming (동사 변형 통합)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;filter&quot;: [&quot;stemmer&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;Synonym (동의어 처리)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;filter&quot;: {
  &quot;product_synonym_filter&quot;: {
    &quot;type&quot;: &quot;synonym&quot;,
    &quot;synonyms&quot;: [&quot;notebook, 노트북, 랩탑, 휴대용 컴퓨터, laptop&quot;]
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&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;한글 형태소 분석기 (Nori)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;&quot;tokenizer&quot;: &quot;nori_tokenizer&quot;,
&quot;filter&quot;: [&quot;nori_part_of_speech&quot;,&quot;nori_readingform&quot;,&quot;lowercase&quot;]&lt;/code&gt;&lt;/pre&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;span&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&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;span&gt;&lt;b&gt;인덱스 관리&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;PUT / {index}&lt;span&gt;, &lt;/span&gt;DELETE / {index}&lt;span&gt;, &lt;/span&gt;GET / {index}&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;매핑 관리&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;PUT|POST /{index}/_mapping&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;문서 CRUD&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;POST _doc&lt;span&gt;, &lt;/span&gt;PUT _doc/{id}&lt;span&gt;, &lt;/span&gt;POST _update/{id}&lt;span&gt;, &lt;/span&gt;GET _doc/{id}&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;검색&lt;/b&gt;&lt;/span&gt;: &lt;span&gt;_search&lt;/span&gt; (match, match_all 등)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;텍스트 분석&lt;/b&gt;&lt;/span&gt;: &lt;span&gt;_analyze&lt;/span&gt;, 커스텀 analyzer&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;고급 분석&lt;/b&gt;&lt;/span&gt;: stopwords, stemming, synonym, nori 형태소 분석기&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>ElasticSearch</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/374</guid>
      <comments>https://sunro1994.tistory.com/374#entry374comment</comments>
      <pubDate>Fri, 26 Sep 2025 00:50:26 +0900</pubDate>
    </item>
    <item>
      <title>[설계 질문] Exactly-once 보장을 위한 설정과 전략</title>
      <link>https://sunro1994.tistory.com/372</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 순서 보장뿐 아니라 중복 없이 &lt;span&gt;&lt;b&gt;정확히 한 번만 처리되는(Exactly-once)&lt;/b&gt;&lt;/span&gt; 이벤트 시스템을 구성하기 위해 다음과 같은 설정과 전략을 적용했습니다.먼저 프로듀서 측에서는 &lt;span&gt;acks=all&lt;/span&gt;, &lt;span&gt;enable.idempotence=true&lt;/span&gt;, &lt;span&gt;transactional.id&lt;/span&gt;를 설정해 중복 전송 방지와 트랜잭션 컨텍스트를 유지했습니다.&lt;span&gt;컨슈머 측에서는 &lt;/span&gt;enable.auto.commit=false&lt;span&gt;로 수동 커밋을 적용하고, &lt;/span&gt;isolation.level=read_committed&lt;span&gt;로 트랜잭션이 커밋된 메시지만 읽도록 설정했습니다.&lt;/span&gt;또한 메시지를 읽은 후 &lt;span&gt;&lt;b&gt;DB 작업과 offset 커밋을 동일 트랜잭션 내에서 처리&lt;/b&gt;&lt;/span&gt;하여, 중간에 실패가 발생해도 중복 소비나 손실이 없도록 구성했습니다.외부 시스템(DB 등)과의 종단 간 정합성을 유지하기 위해 Outbox Pattern도 병행하여, Kafka 메시지 발행이 DB 상태 변경과 원자적으로 처리되도록 했습니다&lt;/p&gt;</description>
      <category>면접 준비</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/372</guid>
      <comments>https://sunro1994.tistory.com/372#entry372comment</comments>
      <pubDate>Tue, 27 May 2025 22:19:11 +0900</pubDate>
    </item>
    <item>
      <title>[설계 면접] 동시에 두 명이 같은 좌석을 예매하려고 시도했을 때 어떻게 처리하나요?</title>
      <link>https://sunro1994.tistory.com/371</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;예매 시스템에서 하나의 좌석을 두 명 이상이 동시에 요청하는 경우, 선착순으로 하나의 요청만 성공해야 하는 문제가 있었습니다.초기에 Redisson Lock을 사용해 동시성 제어를 구현했지만, 부하 테스트 결과 lock 경합에 따른 응답 지연이 심해지는 문제가 발생했습니다.이를 해결하기 위해 Redis LuaScript를 사용해 &lt;span&gt;&lt;b&gt;선점 가능한지 검사 &amp;rarr; 선점 상태 등록&lt;/b&gt;&lt;/span&gt;까지의 작업을 원자적으로 수행하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예: &lt;span&gt;SETNX seat:123 reservedUserId EX 600&lt;/span&gt; 같은 방식으로 10분 간 예약된 상태를 유지하며, 실패 시에는 즉시 실패 응답을 주도록 구현했습니다.결과적으로 동시에 요청이 들어와도 한 명만 선점할 수 있도록 정합성을 보장하면서, 성능도 개선되었습니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;꼬리 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&lt;b&gt;만약 Redis가 장애 난다면 어떻게 정합성을 유지하나요?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 성능과 원자성을 위해 사용되지만, 단일 장애점이 되지 않도록 &lt;span&gt;&lt;b&gt;Sentinel 또는 Cluster 구성&lt;/b&gt;&lt;/span&gt;으로 고가용성을 확보했습니다.그럼에도 불구하고 장애가 발생한 경우를 대비해, 좌석 예약 요청은 Kafka로 비동기 처리되며, Redis 선점 실패 시에는 좌석 정보를 DB에 직접 접근하여 선점 가능 여부를 재검사하도록 fallback 로직을 마련했습니다. 이때 병목 방지를 위해 DB에서 선점 여부를 체크할 때는 &lt;span&gt;seat_instance&lt;/span&gt; 테이블의 &lt;span&gt;status&lt;/span&gt; 필드에 대해 Optimistic Lock (version 기반)을 사용하거나, &lt;span&gt;UNIQUE&lt;/span&gt; 제약 조건을 활용하여 중복 선점을 방지했습니다.&lt;/p&gt;</description>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/371</guid>
      <comments>https://sunro1994.tistory.com/371#entry371comment</comments>
      <pubDate>Tue, 27 May 2025 22:01:50 +0900</pubDate>
    </item>
    <item>
      <title>[설계 면접] 주문 생성 중 결제가 실패했을 때, 주문 상태와 결제 상태를 어떻게 동기화하나요?</title>
      <link>https://sunro1994.tistory.com/370</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;주문 생성과 결제는 하나의 비즈니스 트랜잭션이지만, 기술적으로는 DB 트랜잭션(주문)과 외부 API 호출(결제)이 분리되기 때문에 정합성 보장이 필요합니다.&lt;br /&gt;저는 이 문제를 Outbox Pattern + Kafka를 사용하여 해결했습니다. 주문 생성 시 결제 요청 이벤트를 저장한 후, Kafka를 통해 결제 서비스를 호출하고, 결제 성공 시 주문 상태를 &amp;lsquo;확정&amp;rsquo;으로 업데이트합니다.&lt;br /&gt;결제가 실패하면 일정 시간 동안 &lt;span&gt;&lt;b&gt;Retry 정책&lt;/b&gt;&lt;/span&gt;(최대 3회, 10분 간격)을 적용하고, 모두 실패한 경우에는 주문 상태를 &amp;lsquo;결제 실패&amp;rsquo;로 변경하여 고객에게 알림을 보냅니다.&lt;br /&gt;이처럼 분산 환경에서는 Saga 패턴이나 보상 트랜잭션을 통해 각 상태를 명시적으로 관리하고, Kafka 기반의 메시지 재처리 및 DLQ를 함께 활용했습니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;Saga pattern과 보상 트랜잭션의 차이는?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보상 트랜잭션은 Saga패턴의 일부이다. 하나의 트랜잭션이 실패한 경우 해당 트랜잭션에 대한 보상으로 롤백시키는 과정을 일컫는 말이며 전체적인 흐름을 관리하는 것이 Saga Pattern이다. Saga Pattern은 Choreography, Orchestration이라는 두 개념으로 나뉠 수 있는데 Choreography는 각각의 서버에서 롤백을 수행하여 흐름파악은 어렵지만 확장 및 유연성에는 용이하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orchestration은 흐름을 한 번에 볼 수 있다는 장점이 있지만 중앙 집중화 되어 의존도가 높다는 단점이 있다.&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;</description>
      <category>면접 준비</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/370</guid>
      <comments>https://sunro1994.tistory.com/370#entry370comment</comments>
      <pubDate>Tue, 27 May 2025 21:56:09 +0900</pubDate>
    </item>
    <item>
      <title>[설계 면접] Spring Cloud Gateway나 Config Server가 죽으면 어떻게 되나요?</title>
      <link>https://sunro1994.tistory.com/369</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Spring Cloud Gateway나 Config Server는 MSA에서 요청 라우팅 및 설정 전달을 담당하는 핵심 인프라이기 때문에, 이들의 장애가 전체 시스템에 영향을 줄 수 있습니다.&lt;/span&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;span&gt;&lt;b&gt;Config Server&lt;/b&gt;&lt;/span&gt;는 애플리케이션 부팅 시 설정을 받아오는 역할을 하며, 이후에는 대부분의 경우 &lt;span&gt;&lt;b&gt;로컬 메모리에 설정이 캐싱&lt;/b&gt;&lt;/span&gt;되어 있기 때문에 서버가 죽더라도 &lt;span&gt;&lt;b&gt;기동 중인 인스턴스에는 영향이 없습니다&lt;/b&gt;&lt;/span&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;다만 Config Server 장애 시 신규 배포 또는 설정 refresh 시도 시 실패하게 되므로, 이를 대비해 &lt;span&gt;&lt;b&gt;Spring Cloud Config의 Fail Fast 모드&lt;/b&gt;&lt;/span&gt;를 해제하고, &lt;span&gt;&lt;b&gt;Retry 및 Timeout 설정&lt;/b&gt;&lt;/span&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;span&gt;&lt;b&gt;Gateway 장애 시&lt;/b&gt;&lt;/span&gt;는 전체 외부 요청이 막히기 때문에 &lt;b&gt;이중화 배포(HAProxy, ALB + 다중 인스턴스)&lt;/b&gt;를 통해 단일 장애점을 제거했습니다. 또한 각 서비스 내에는 &lt;span&gt;&lt;b&gt;Resilience4j 기반 Circuit Breaker&lt;/b&gt;&lt;/span&gt;를 적용하여, 특정 서비스가 다운되었을 때 유저에게 대체 응답이나 대기 화면을 제공하도록 구성했습니다.이처럼 각 컴포넌트는 장애 감지, fallback, 캐시, 이중화를 통해 유저 경험을 지키는 방향으로 설계했습니다.&lt;/p&gt;</description>
      <category>면접 준비</category>
      <author>공부하고 기억하는 공간</author>
      <guid isPermaLink="true">https://sunro1994.tistory.com/369</guid>
      <comments>https://sunro1994.tistory.com/369#entry369comment</comments>
      <pubDate>Tue, 27 May 2025 21:46:52 +0900</pubDate>
    </item>
  </channel>
</rss>