<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>WBS</title>
        <link>https://velog.io/</link>
        <description>우측 상단 햇님모양 클릭하셔서 무조건 야간모드로 봐주세요!!</description>
        <lastBuildDate>Fri, 06 Feb 2026 05:54:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. WBS. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/choi-ju-yung" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[람다]]></title>
            <link>https://velog.io/@choi-ju-yung/%EB%9E%8C%EB%8B%A4</link>
            <guid>https://velog.io/@choi-ju-yung/%EB%9E%8C%EB%8B%A4</guid>
            <pubDate>Fri, 06 Feb 2026 05:54:48 GMT</pubDate>
            <description><![CDATA[<p>✅ <code>람다</code> 특징 </p>
<ul>
<li><code>람다</code> 는 익명함수를 지칭하는 일반적인 용어 = 이름 없이 함수 표현</li>
<li><code>람다식</code> 은 (매개변수) -&gt; {본문} 형태로 람다를 구현하는 구체적인 문법 표현을 지칭</li>
<li><code>람다</code> 를 사용할 때 new 키워드를 사용하지 않지만, 인스턴스가 생성된다</li>
<li><code>람다</code> 를 사용하면 표현이 간결하다</li>
<li><code>람다</code> 는 변수처럼 다룰 수 있다</li>
<li>대부분의 <code>익명 클래스</code> 는 <code>람다</code> 로 대체할 수 있다
참고로 <code>람다</code> 가 <code>익명 클래스</code> 를 완전히 대체할 수 있는 것은 아님<pre><code class="language-java">반환타입 메서드명(매개변수) { // 일반 함수 표현식
   본문
}
</code></pre>
</li>
</ul>
<p>public int add(int x){
    return x + 1;
}</p>
<p>(매개변수) -&gt; {본문} // 람다 표현식
(int x) -&gt; {return x + 1;}</p>
<pre><code>
람다 사용방법 : `함수명, 반환 타입은 생략하고 매개변수와 본문만 간단하게 적으면 됨`
***
✅ `함수형 인터페이스`
* 딱 하나의 추상 메서드를 가지는 인터페이스
* `람다` 는 추상 메서드가 하나인 인터페이스에만 할당할 수 있음
* 단일 추상 메서드를 줄여서 `SAM` (Single Abstract Method) 라고 부름
* 람다는 클래스나 추상클래스에 할당할 수 없으며, 단일 추상 메서드를 가지는  인터페이스에만 할당할 수 있음

```java
public interface NotSamInterface {
    void run(); // 메서드 앞에는 abstract 키워드가 생략되있음
    void go();
    // 여기서는 두개의 추상 메서드가 선언되 있으므로, 단일 추상메소드가 아니다
    // 그래서 이 인터페이스에는 람다를 할당할 수 없다
}</code></pre><p>왜 위의 코드에서 <code>람다</code> 를 할당할 수 없을까?
-&gt; 람다는 하나의 함수라서, 람다를 인터페이스에 담으려면 하나의 메서드 선언만 존재해야함
-&gt; 두개 이상이면 어떤 메서드에 할당해야하는지 알 수 없기 때문에 컴파일 오류 발생</p>
<p>인터페이스에 하나의 추상 메서드만 사용한다라고 표현하려면 <code>@FunctionalInterface</code> 를 사용
<code>@FunctionalInterface</code> : <code>함수형 인터페이스</code> 임을 선언
이 어노테이션을 사용하면 누군가 실수로 추상 메서드를 추가하면 컴파일 오류가 발생</p>
<p>따라서 람다를 사용할 <code>함수형 인터페이스</code> 라면 <code>@FunctionalInterface</code> 필수로 추가 권장</p>
<hr>
<p>✅ 람다와 생략</p>
<p>예제 (1)</p>
<pre><code class="language-java">@FunctionalInterface 
public interface MyFunction { // MyFunction 인터페이스
     int apply(int a, int b);
}</code></pre>
<p>위 <code>MyFunction</code> 인터페이스를 재정의하면 다음과 같이 표현할 수 있다</p>
<pre><code class="language-java">MyFunction function1 = (int a, int b) -&gt; {
     return a + b;
};</code></pre>
<p>위 function1 처럼 <code>단일표현식</code> 인 경우 중괄호와 리턴을 생략할 수 있다</p>
<pre><code class="language-java">MyFunction myFunction2 = (int a, int b) -&gt; a + b;</code></pre>
<p>하지만 여러문장이 있으면 (<code>단일표현식</code>) 이 아닌 경우는 생략하면 안된다</p>
<pre><code class="language-java">MyFunction myFunction3 = (int a, int b) -&gt;{
    System.out.println(&quot;hi&quot;);
    return a + b;
};</code></pre>
<p>함수형 인터페이스에 이미 <code>int</code> 형으로 정의되어 있으므로, 매개변수 타입을 생략할 수 있다</p>
<pre><code class="language-java">MyFunction myFunction4 = (a,b) -&gt; a + b;</code></pre>
<p>만약에 매개변수가 딱 하나라면 <code>()</code> 를 생략할 수 있다 (매개변수 없거나, 두개 이상이면 안됨)</p>
<pre><code class="language-java">MyFunction myFunction5 = a -&gt; a * 2;</code></pre>
<hr>
<p>다음은 <code>Procedure</code> 인터페이스의 예시를 보여주겠다</p>
<pre><code class="language-java">public interface Procedure {
     void run();
}</code></pre>
<p>위 문장을 재정의하면 다음과 같이 표현할 수 있다</p>
<pre><code class="language-java">Procedure procedure = () -&gt;{
    System.out.println(&quot;hi&quot;);
};</code></pre>
<p>하지만 위 문장은 <code>단일표현식</code> 이므로 중괄호를 생략할 수 있다</p>
<pre><code class="language-java">Procedure procedure2 = () -&gt; System.out.println(&quot;hi&quot;);</code></pre>
<hr>
<p>✅ 람다의 전달</p>
<ul>
<li>람다를 변수에 대입할 수 있음</li>
</ul>
<p>람다도 인터페이스를 사용하므로, 람다 인스턴스 참조값을 변수에 전달할 수 있다</p>
<pre><code class="language-java">MyFunction add = (a,b) -&gt; a+b;
MyFunction sub = (a,b) -&gt; a-b;
System.out.println(add.apply(2,3)); // 5
System.out.println(sub.apply(2,3)); // -1

// 변수를 통해 전달
MyFunction cal = add; // 람다를 변수에 대입
System.out.println(cal.apply(2,3)); // 5

// 람다를 직접 전달
calculate((a,b) -&gt; a + b);
calculate((a,b) -&gt; a - b);

static void calculate(MyFunction function) {
    int a = 1;
    int b = 2;

    int result = function.apply(a, b);
    System.out.println(result);    
}
</code></pre>
<p>또 람다를 반환할 수 도 있다</p>
<pre><code class="language-java">public static void main(String[] args) {
 MyFunction add = getOperation(&quot;add&quot;); 
 System.out.println(add.apply(1, 2)); // 3

 MyFunction sub = getOperation(&quot;sub&quot;);
 System.out.println(sub.apply(1, 2)); // -1

 MyFunction xxx = getOperation(&quot;xxx&quot;);
 System.out.println(xxx.apply(1, 2)); // 0
 }

 // 람다를 반환하는 메서드
 static MyFunction getOperation(String operator) {
     switch (operator) {
       case &quot;add&quot;:
       return (a, b) -&gt; a + b;
       case &quot;sub&quot;:
       return (a, b) -&gt; a - b;
       default:
       return (a, b) -&gt; 0;
     }
 }</code></pre>
<hr>
<p>✅ <code>고차함수</code></p>
<ul>
<li>함수를 값처럼 다루는 함수를 의미함</li>
</ul>
<p>다음 2가지 중 하나를 만족하면 <code>고차 함수</code> 라고 한다</p>
<ul>
<li>함수를 인자로 받는 함수</li>
<li>함수를 반환하는 함수</li>
</ul>
<pre><code class="language-java">static void calculate(MyFunction function) { // 함수(람다)를 인자로 받는 경우
     // ...
 }

static MyFunction getOperation(String operator) { // 함수(람다)를 반환하는 경우
     // ...    
    return (a, b) -&gt; a + b;
}</code></pre>
<p>보통의 일반 함수들은 인수로 값을 받고, 값을 반환한다
하지만 고차함수는 함수로 인자를 받거나 함수를 반환한다
고차함수는 함수라는 개념 자체를 값처럼 다룬다는 점에서 <code>추상화</code> 의 수준(계층)이
한 단계 높아진다고 해서  <code>고차함수</code> 라고 한다</p>
<hr>
<p>✅ 함수형 인터페이스에 제네릭 도입</p>
<pre><code class="language-java">@FunctionalInterface
interface GenericFunction&lt;T, R&gt; {
    R apply(T s); // T : 매개변수 타입  R : 반환 타입
}</code></pre>
<pre><code class="language-java">GenericFunction&lt;String, String&gt; toUpperCase = str -&gt; str.toUpperCase();
GenericFunction&lt;String, Integer&gt; stringLength = str -&gt; str.length();
GenericFunction&lt;Integer, Integer&gt; square = x -&gt; x * x;
GenericFunction&lt;Integer, Boolean&gt; isEven = num -&gt; num % 2 == 0;

    System.out.println(toUpperCase.apply(&quot;hello&quot;)); // HELLO
    System.out.println(stringLength.apply(&quot;hello&quot;)); // 5
    System.out.println(square.apply(3)); // 9
    System.out.println(isEven.apply(3)) // false</code></pre>
<p>제네릭을 사용하면 동일한 구조의 함수형 인터페이스를 다양한 타입에 재사용할 수 있다
제레닉을 넣음으로써, <code>apply()</code> 의 매개변수와 반환 타입을 유연할게 할 수 있다<br>위에서 선언한 GenericFunction()  은 매개변수가 1개고, 반환값이 있는 모든 람다에 사용 가능</p>
<p>💡 하지만 람다르 사용하려면 함수형 인터페이스가 필수이기 때문에, </p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kafka 장애 대비하기]]></title>
            <link>https://velog.io/@choi-ju-yung/Kafka-%EC%9E%A5%EC%95%A0-%EB%8C%80%EB%B9%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@choi-ju-yung/Kafka-%EC%9E%A5%EC%95%A0-%EB%8C%80%EB%B9%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 03 Feb 2026 17:40:27 GMT</pubDate>
            <description><![CDATA[<p><code>고가용성</code> : 시스템이 장애 상황에서도 멈추지 않고 정상적으로 서비스를 제공할 수 있는 능력</p>
<hr>
<p>✅ 노드 = 카프카 서버</p>
<ul>
<li>카프카가 설치되어 있는 서버 단위를 의미</li>
</ul>
<p>즉 쉽게말해서 Kafka가 설치되어 있는 서버를 <code>노드</code> 라고 부른다
이 <code>노드</code> 가 고장나면 메시지를 전달하는 것 자체가 막히기 때문에, 서비스 장애가 일어난다.
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/e250d8b3-a84d-48de-858b-543fc296b361/image.png" alt=""></p>
<p>이러한 서비스 장애를 막기 위해서, 위그림과 같이 <code>노드</code> 를 1개만 두지 않고, 최소 3개의 <code>노드</code>를
구성한다. 그러면 첫번째 노드가 고장나더라도, 다른 노드에서 메시지를 저장할 수 있다.
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/06bc7442-1caf-49e4-b4ae-f6003cef5a30/image.png" alt=""></p>
<hr>
<p>✅ 클러스터</p>
<ul>
<li>여러 대의 서버가 연결되어 하나의 시스템처럼 동작하는 서버들의 집합</li>
</ul>
<p>위 사진에서 나온 3대의 <code>노드</code> 사진을 보면, 각 노드들이 서로 유기적으로 작동하고 있다.
장애시 시스템 전체가 중단없이 작동되게 만든다. 이와같이 유기적으로 작동하는 <code>노드</code> 들을 묶어서
<code>클러스터</code> 라고 부른다.
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/acec10d8-ba49-47fe-837d-d130a4c55183/image.png" alt=""></p>
<hr>
<p>✅ 브로커, 컨트롤러
위에서 나온 노드(Kafka 서버) 는 크게 <code>컨트롤러</code> 와 <code>브로커</code> 로 구성되어 있다.</p>
<p><code>브로커</code> 는 메시지를 저장하고 클라이언트의 요청을 처리하는 역할을 한다.
<code>컨트롤러</code> 는 <code>브로커</code>들간의 연동과 전반적인 <code>클러스터</code> 상태를 총괄하는 역할을 한다.</p>
<p>쉽게 비유하면, <code>브로커</code> 는 직원, <code>컨트롤러</code> 는 총관리자로 비유하면 좋다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/fc367ff5-2ab0-4575-8ce0-3a80b3056f7d/image.png" alt="">
위 사진처럼 하나의 Kafka 서버에서 <code>컨틀로러</code> 와 <code>브로커</code> 를 동시에 셋팅을 할 수도 있지만
<code>컨트롤러</code> 와 <code>브로커</code> 를 분리해서 Kafka 서버를 구성할 수 있다.</p>
<p>기본적으로 Kafka 서버에서 <code>브로커</code> 는 9092번 포트에서 실행되고
<code>컨트롤러</code> 는 9093번 포트에서 실행된다.
하나의 Kafka 서버를 띄우면  프로세스가 2개가 실행된다 (9092, 9093) = 별개의 프로세스로 실행된다.</p>
<hr>
<p>✅ 레플리케이션</p>
<ul>
<li>데이터의 안정성과 가용성을 높이기 위해, 토픽의 파티션을 여러 Kafka 서버에 복제하는 것을 의미</li>
</ul>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/db4a6997-3cbd-41fa-947e-ad47e3e9738c/image.png" alt="">
위 사진처럼 첫번째 Kafka 서버에 있는 파티션을 각각 노드2, 노드3에 복제한것을 볼 수 있다.</p>
<p>여기서 <span style="color:red">원본</span> 파티션을 <code>리더 파티션</code> 이라고 부르며, <span style="color:red">복제</span>한 파티션을 <code>팔로워 파티션</code> 이라 부른다.
<code>리더 파티션</code> 은 프로듀서나 컨슈머 서버가 직접적으로 메시지를 쓰고 있는 파티션이며
<code>팔로워 파티션</code> 은 프로듀서나 컨슈머 서버가 메시지를 쓰고 있지 않다.</p>
<p>ex) 프로듀서 서버는 메시지를 <code>리더 파티션</code> 으로만 넣는다.
ex) 컨슈머 서버가 메시지를 꺼내서 처리할 때는 <code>리더 파티션</code> 에 있는 메시지만 꺼내서 처리한다.</p>
<p><code>팔로워 파티션</code> 은 <code>리더 파티션</code> 의 메시지를 실시간으로 복제하며 유지한다.
ex) 프로듀서 서버가 오프셋 2번 메시지를 <code>리더파티션</code> 에 넣는 순간, <code>팔로워 파티션</code> 에 그 메시지를 가져온다.</p>
<p><code>리더 파티션</code> 이 장애가 발생하면, <code>팔로워 파티션</code> 이 대신 <code>리더 파티션</code> 역할을 담당한다.
그로 인해 장애가 발생해도, <code>팔로워 파티션</code> 이 메시지를 정상적으로 처리할 수 있다. 
= 복제한 파티션이 리더 역할을 하게됨 = <code>팔로워 파티션</code> 이 <code>리더 파티션</code> 으로 승격</p>
<p><code>레플리케이션</code> 개수는 kafka 서버 수만큼 설정할 수 있지만, 
실무에서는 <code>레플리케이션</code> 개수를 2나 3으로 설정해서 활용한다.
<code>레플리케이션</code> 개수를 많이 설정하면, 복제가 너무 많이되서 디스크 공간이 부족해지고, 속도가 느려진다.
ex) kafka 서버를 3대로 설정했는데, <code>레플리케이션</code> 을 3개보다 많은 개수로 설정할 수 없다.</p>
<p>✅ 연습 (강의참고)</p>
<p>우선 하나의 EC2에 3대의 Kafka 노드를 구축했다고 가정하자 
노드 안에는 각각의 (브로커, 컨트롤러) 가 존재</p>
<p>Kafka 노드에 토픽을 생성하면서 (레플리케이션) 까지 생성하기
<code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic 토픽명 
partitions 1 --replication-factor 3</code> -&gt; email.send 토픽을 생성하면서 파티션은 1개, 복제파티션은 3개 생성</p>
<p>위에서 만든 토픽 세부정보 값을 출력하면 아래와 같이 나온다
<code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic email.send</code>
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/72f660ee-1f08-4ee9-a80c-9e2615bd1846/image.png" alt=""></p>
<p><code>PartitionCount</code> : 해당 토픽의 파티션 수</p>
<p><code>ReplicationFactor</code> : 해당 토픽의 레플리케이션 수</p>
<p><code>Partition</code> : 파티션 번호 (0 부터 시작임)</p>
<p><code>Leader</code> : 해당 토픽의 리더 파티션을 가지고 있는 노드 id (노드번호는 1부터 시작)
= 1번 Kafka 서버에 리더파티션의 정보를 가지고 있다는 뜻</p>
<p><code>Replicas</code> : 해당 토픽의 파티션을 복제하기로 설정된 노드들의 id </p>
<p><code>Isr</code> : 리더 파티션과 똑같은 상태로 복제(동기화)가 완료된 노드들의 id</p>
<p>💡 <code>ISR</code> 의 개수와 <code>Replicas</code> 개수가 맞지 않으면 장애가 있다는 신호로 받아들일 수 있다.</p>
<br>

<p>만약 <code>리더 파티션</code> 이 아닌 <code>팔로워 파티션</code> 에 메시지를 넣으면 어떻게 될까?</p>
<p>정답부터 말하면, 리더파티션과 팔로워 파티션들에 모두 다 메시지가 들어있는것을 확인할 수 있다
그 이유는 Kafka <code>프로듀서</code> 서버는 메시지를 보내기전에, 파티션의 리더가 누구인지 확인하고
자동으로 <code>리더 파티션</code> 한테 메시지를 전송해준다.</p>
<p>결국은 <code>팔로우 파티션</code> 에 메시지를 전송하게 아니라 <code>리더 파티션</code> 에  메시지가 전송되었고
그 후에 복제가되서 <code>팔로우 파티션</code> 에 메시지가 들어있었던 것이다.</p>
<p>이게 가능한 이유는 Kafka 서버끼리 서로 연동되어 있어서, <code>리더 파티션</code> 을 가진 노드가 
어떤것인지에 대해 서로 주고 받을 수 있기 때문이다.</p>
<pre><code>Producer → broker 2
broker 2 → (아 이건 내가 리더 아님)
broker 2 → broker 1 (리더)에게 전달
broker 1 → 메시지 저장

그 후에

리더 파티션에 write
↓
ISR 팔로워들에게 복제
↓
팔로워 파티션에도 데이터 생김</code></pre><hr>
<p>✅ 리더 파티션에 장애가 발생하면 어떻게 될까?
기존 리더 파티션을 가지고 있는 노드를 조회하면 아래 사진과 같이 나온다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/42a0bc1c-b835-49ab-864c-8bcbd814e00a/image.png" alt="">사진과 같이 현재 1번 노드가 <code>리더 파티션</code>을 가지고 있다. 테스트를 위해 1번 노드를 종료시켜보자</p>
</blockquote>
<p>그 후 다른 노드의 주로를 조회하면</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/99bbe443-f155-4479-afe2-37141f274cc7/image.png" alt="">사진과 같이 <code>리더 파티션</code> 이 2번 노드로 승격된것을 확인할 수 있다.
또 <code>Isr</code> 에 1번 노드가 빠져 있는것을 보면, 문제가 있음을 확인할 수 있다.
여기에 메시지를 넣으면, 정상적으로 토픽에 메시지를 넣었음을 확인할 수 있다.</p>
</blockquote>
<p>그 후에 다시 1번 노드를 복구시키고, 다시 2번노드를 조회하면</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/2c9f0a17-6a2d-416e-ba46-46d4a5dc6bcb/image.png" alt="">
다시 <code>리더 파티션</code> 이 1번으로 바뀌었으며, <code>Isr</code> 에도 다시 1번노드가 들어온것을 확인할 수 있다.</p>
</blockquote>
<hr>
<p>✅ 정리</p>
<p>Kafka 서버를 3대로 구성하면, 특정 브로커에 장애가 발생하더라도 서비스가 중단되지 않는다.
<code>브로커</code> 간 <code>파티션</code> 복제를 통해 데이터 손실을 방지하며, <code>리더 파티션</code> 에 장애가 발생할 경우 
<code>팔로워 파티션</code> 이 자동으로 <code>리더</code> 로 승격되어 시스템은 지속적으로 운영된다.
이러한 구조는 Kafka의 대표적인 <code>고가용성</code> (High Availability) 구성 방식이다.</p>
<p>쉽게 말하면
<code>Kafka</code> 는 <code>다중 브로커</code> 와 <code>파티션</code> 복제를 통해 장애 상황에서도 자동 복구 및 무중단 서비스를 제공한다.</p>
<p><code>Kafka</code> 서버는 초기 단계 서비스의 경우는 1대 서버로도 충분하다 (비용 절감의 단계) 추후 점진적 확장
하지만 서비스 안정성이 중요한 단계의 경우 최소 <code>3</code> 대의 서버를 구성하는 것이 좋다.</p>
<hr>
<p>✅ Spring Boot에 Kafka 서버 3대를 연결해서 사용하는 방법</p>
<p><code>Producer</code> 서버와  <code>Consumer</code> 서버에서 각각 application.yml 파일을 수정해줘야함</p>
<pre><code class="language-xml">&lt;!-- Producer --&gt;
spring:
 kafka:
  bootstrap-servers:
   - {Kafka 서버 IP 주소}:9092
   - {Kafka 서버 IP 주소}:19092
   - {Kafka 서버 IP 주소}:29092
.....</code></pre>
<pre><code>&lt;!-- Consumer --&gt;
spring:
 kafka:
  bootstrap-servers:
   - {Kafka 서버 IP 주소}:9092
   - {Kafka 서버 IP 주소}:19092
   - {Kafka 서버 IP 주소}:29092
.....</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[kafka 메시지 처리성능 높이기 및 장애 대비]]></title>
            <link>https://velog.io/@choi-ju-yung/kafka-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%B2%98%EB%A6%AC%EC%84%B1%EB%8A%A5-%EB%86%92%EC%9D%B4%EA%B8%B0-%EB%B0%8F-%EC%9E%A5%EC%95%A0-%EB%8C%80%EB%B9%84</link>
            <guid>https://velog.io/@choi-ju-yung/kafka-%EB%A9%94%EC%8B%9C%EC%A7%80-%EC%B2%98%EB%A6%AC%EC%84%B1%EB%8A%A5-%EB%86%92%EC%9D%B4%EA%B8%B0-%EB%B0%8F-%EC%9E%A5%EC%95%A0-%EB%8C%80%EB%B9%84</guid>
            <pubDate>Wed, 28 Jan 2026 15:35:43 GMT</pubDate>
            <description><![CDATA[<p>이전에 만들어놓은 API 요청을 한번이 아니라 세번 연속으로 실행해보자
보내고나서 로그를 확인해보면, 한 번에 하나의 API 요청을 처리하는 것을 확인할 수 있다</p>
<p>SpringBoot는 멀티 쓰레드 기반으로 여러개의 요청을 병렬적으로 처리할 수 있는 구조를 가지고 있는데도, 왜 비효율적으로 한번에 하나의 요청만 처리하고 있는걸까? 
이 문제의 원인은 <code>파티션</code> 과 밀접한 관련이 있다</p>
<p>✅  파티션</p>
<ul>
<li><code>큐</code> (메시지를 임시로 저장할 수 있는 공간)을 여러개로 늘려서 병렬 처리를 가능하게 하는 기본 단위</li>
</ul>
<p>✅ 파티션의 특징</p>
<p>각 토픽은 1개 이상의 파티션으로 구성할 수 있다.
토픽을 생성할 때 별도의 옵션을 주지 않으면, 아래와 같이 파티션을 한개만 생성한다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/8c336b2f-defd-488c-9b88-3f744e35f72c/image.png" alt="">
하지만 토픽을 생성할 때, 파티션을 여러개 만들 수 있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/6909f216-3868-4504-9d16-09f66541b13d/image.png" alt="">
<code>Producer</code> 가 특정 토픽에 메시지를 넣으면, 여러 파티션에 메시지가 적절하게 분산된다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/f9defce6-0e34-4394-9323-ce9b73288fb0/image.png" alt=""></p>
<p>하나의 <code>파티션</code> 은 하나의 컨슈머에게만 할당한다
즉 여러 <code>컨슈머</code> 가 하나의 <code>파티션</code> 의 메시지를 처리할 수 없다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/17033072-9d99-4e11-9ef2-4df284b9bf1b/image.png" alt=""></p>
<p>여러 <code>컨슈머</code> 서버가 하나의 <code>파티션</code> 의 메시지를 같이 처리할 수는 없지만, 
하나의 <code>컨슈머</code> 서버가 여러 <code>파티션</code> 을 처리할 수 있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/285078e7-2d7e-4aa1-9910-8a3a946e8c93/image.png" alt=""></p>
<p>하나의 <code>파티션</code> 에 할당된 하나의 <code>컨슈머</code> 서버는 메시지를 순서대로 처리한다.
아래 사진과 같이 오프셋이 0,1 메시지를 병렬적으로 처리하지 않는다
그 이유는 <code>파티션</code>     단위로 메시지의 처리 순서를 보장하기 때문이다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/d272af32-1586-4fd5-a853-9fb2ce54acdb/image.png" alt=""></p>
<p>💡 정리하면 <code>컨슈머</code> 서버를 여러개 실행시켜도, 하나의 <code>컨슈머</code> 서버에서만 메시지를 처리한다
하나의 <code>파티션</code> 은 하나의 <code>컨슈머</code> 서버에게만 할당된다.</p>
<hr>
<p>✅ 특정 토픽의 파티션 수 <code>설정 / 조회 / 변경</code></p>
<ul>
<li>토픽 생성할 때 파티션 수 설정 (토픽 생성 + 파티션 수 설정)
<code>$ bin/kafka-topics.sh --bootstrap-server &lt;kafka 주소&gt;</code> 
<code>--create --topic &lt;토픽명&gt; --partitions &lt;파티션 수&gt;</code></li>
</ul>
<p>ex) <code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092</code>
<code>--create --topic joo.topic --partitions 3</code>  -&gt; joo 토픽을 생성하고 파티션을 3개로 생성</p>
<ul>
<li>생성한 토픽에서 파티션 수 조회
<code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic joo.topic</code></li>
</ul>
<blockquote>
<p>아래 사진처럼 3개의 파티션이 생성된 것을 볼 수 있다
토픽을 생성할 때 별도의 옵션을 주지 않으면 파티션은 1개만 생성된다<img src="https://velog.velcdn.com/images/choi-ju-yung/post/950494b9-8350-4e64-801c-aa54db92f030/image.png" alt=""></p>
</blockquote>
<p>이번에는 생성했던 토픽의 파티션 수를 늘려보자</p>
<ul>
<li>기존 토픽의 파티션 수 변경
<code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092</code>
<code>--alter --topic joo.topic --partitions 5</code> -&gt; joo토픽의 파티션 수를 5개로 늘림
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/35d95591-d694-4bb1-9d75-e2f2fdc33deb/image.png" alt=""></li>
</ul>
<p>파티션 수를 늘릴 수는 있지만, 파티션 수를 <span style="color:red">줄일</span> 수는 없다
명령어를 입력해서 파티션 수를 줄이면, 에러가 난다
그 이유는 파티션을 줄이는 과정에서 내부적으로 (데이터 손실, 성능 저하) 등의 문제가 발생하기 때문이다
만약 파티션 수를 줄여야 한다면, 새로운 토픽을 다시 생성해서 파티션수를 다시 설정해야한다
그리고 기존 토픽에 있던 내용들을 새로운 토픽에 마이그레이션 시켜야한다.</p>
<hr>
<p>✅ 여러개의 파티션에 메시지가 골고루 들어가는지 확인해보기
특정 토픽에 메시지를 넣으면 여러 파티션에 메시지가 적절하게 분산된다.
이 때 메시지의 형태에 따라 파티션에 분배되는 방식이 달라진다.</p>
<p>1 . Key가 포함되지 않는 value 만 있는 형태의 메시지를 넣을 경우
<code>스티키 파티셔닝</code> 방식으로 메시지를 분산한다 
-&gt; 하나의 파티션에 메시지가 일정량 채워지면 그 다음 파티션에 메시지를 저장하는 방식
-&gt; ex) 1 파티션에 10개의 메시지가 채워지면 그 다음부터 2파티션에 메시지 10개 채워짐 ....
Kafka 2.4 버전 이전에는 <code>라운드 로빈 방식</code> (파티션을 번갈아가면서 하나씩 넣는 방식) 으로 분배했었다
하지만 대규모 메시지를 처리할 때는 <code>스티키 파티셔닝</code> 방식이 효율적이라서 변경되었다.</p>
<p>2 . Key 포함된 메시지를 넣을 경우
Key의 해시 값을 기반으로 파티션을 결정하여 메시지를 분배한다.
같은 Key 값을 가진 메시지는 같은 파티션에 들어간다.</p>
<p>이제 위에서 만든 joo 토픽을 기반으로 실시간으로 토픽의 파티션에 저장되는 메시지를 확인해보자
<code>$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092</code>
<code>--topic joo.topic --from-beginning --property print.partition=true</code></p>
<p>= joo.topic 토픽의 모든 메시지를 조회 ( + 파티션 정보도 같이 출력)
이렇게 명령어를 입력한 후 Api 요청을 여러번 보내면 하나의 파티션에만 메시지가 저장되는것이 확인된다
그 이유는 <code>스티키 파티셔닝</code> 방식이기 때문이다.
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/efe6aa0f-1dc5-4f83-bcc3-d9d42d0acb54/image.png" alt=""></p>
<p><code>스티키 파티셔닝</code> 방식이 대규모 데이터를 처리할 때는 유리하지만, 작은 규모의 데이터를 처리할 때는
하나의 파티션에만 메시지가 몰리는것은 좋지 않다.</p>
<p>이번에는 <code>라운드 로빈</code> 방식으로 메시지가 저장되도록 변경해보자</p>
<p>Producer 역할을 하는 Springbooat 서버의 <code>application.yml</code> 파일을 수정하면 된다
아래와 같이 properties 부분과 partitioner.class 부분을 수정하면 된다</p>
<pre><code class="language-yml">spring:
 kafka:
  bootstrap-servers: 15.164.96.71:9092
    producer:
   key-serializer: org.apache.kafka.common.serialization.StringSerializer
   value-serializer: org.apache.kafka.common.serialization.StringSerializer
   properties:
    partitioner.class: org.apache.kafka.clients.producer.RoundRobinPartitioner ## 라운드로빙 방식</code></pre>
<p>이렇게 수정한 후 다시 api 요청을 여러번 보내면 아래와 같이 파티션에 메시지가 골고루 쌓인다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/4c2ffd4c-3954-4de9-98e1-cef007af2476/image.png" alt=""></p>
<hr>
<p>✅ 여러개의 컨슈머 서버로 메시지를 병렬적으로 처리하기</p>
<p>파티션이 여러개 있는 상태에서, 컨슈머 서버를 여러개 추가하면
컨슈머 서버의 개수에 맞게 알아서 파티션을 분배하는 것을 확인할 수 있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/f61f6ee8-e5d8-4f13-a67f-af33beb9fbcb/image.png" alt=""></p>
<p>왼쪽 서버에는 파티션 0,1 로 분배되어있고, 오른쪽 서버에는 파티션 2가 분배되어있다
이와 같이 컨슈머 서버가 한대일때보다 여러대일 경우 메시지를 병렬적으로 처리하는것을 확인할 수 있다</p>
<blockquote>
<p>아까 파티션이 하나였을 경우는 컨슈머 서버가 여러대여도 소용이 없었다
그 이유는 하나의 파티션은 하나의 컨슈머 서버에서만 담당하기 때문이다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/e2735768-6c08-44c2-8a57-34eb936fa551/image.png" alt=""></p>
</blockquote>
<p>하지만 파티션을 늘리면, 각 컨슈머 서버가 담당할 수 있는 파티션이 생기기 때문에
컨슈머 서버도 늘리면 메시지를 병렬적으로 처리할 수 있다. 대형마트로 비유하면 계산할 수 있는 카운터가 많아진 것이고 계산을 기다리는 손님을 빠르게 처리할 수 있는 것이다.
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/a5509ed5-44b1-4e3f-86ba-f7a8c7ffd9e4/image.png" alt=""></p>
<p>💡  하지만 컨슈머 서버가 메시지를 처리할 때 사용하는 리소스(CPU, 메모리) 가 부족한 상태이면
컨슈머 서버를 늘리는 것이 맞다. 하지만 무거운 작업이 아니고, 리소스가 부족하지 않은 상태라면
하나의 컨슈머 서버에서 여러 파티션을 병렬적으로 처리하는 방법을 사용해야한다.</p>
<br>

<p>✅ 하나의 컨슈머 서버로 메시지를 병렬적으로 처리</p>
<p>위에서 만들어놓은 코드를 수정하면 된다 -&gt; (멀티쓰레드 개수 증가)</p>
<pre><code class="language-java">@KafkaListener(
     topics = &quot;email.send&quot;,
     groupId = &quot;email-send-group&quot;,
     concurrency = &quot;3&quot; // 멀티 쓰레드를 활용해 병렬적으로 처리할 파티션의 개수
)</code></pre>
<p>이렇게 설정한 후 다시 API 요청을 3번 보내면, 동시에 처리되는것을 확인할 수 있다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/0c96bf78-98dd-404a-9dc6-46b3a916500d/image.png" alt=""></p>
<p>그럼 API 요청을 5번 보내면 어떻게 될까?
쓰레드가 3개이기때문에 3번의 요청은 바로 처리가 될것이고, 나머지 2개의 요청은, 메시지의 후순위에 있기 때문에, 다 처리된 후에 작업이 실행될 것이다.</p>
<p>그럼 쓰레드를 더 늘리면 되는것인가? 
현재 파티션은 3개이기 때문에, 쓰레드를 더 늘린다고 해도 병렬적으로 메시지를 처리할 수 있는
메시지의 개수는 최대 3개밖에 안된다. 그러므로 5개요청을 병렬적으로 처리하고 싶다면
파티션의 개수부터 증가시키고 그 이후 쓰레드도 증가시키면 된다</p>
<p>💡 그럼 병렬적으로 처리하기 위해서 파티션을 많이 넣으면 되는것이 아닌가?
무작정 과도하게 파티션 수를 증가시키면 반대로 성능에 비효율성을 가져올 수 있다
적절한 파티션 수를 설정하는 것이 중요하다.</p>
<hr>
<p>✅ 적정 파티션 개수 계산하는 방법
<code>프로듀서가 보내는 메시지량 ≤ 하나의 쓰레드가 처리하는 메시지량 x 파티션 수</code></p>
<p>1 . 몇개의 쓰레드를 사용해야 처리량이 가장 높아지는 지 측정하기
SpringBoot 서버는 멀티 쓰레드 기반이기 때문에 동시에 요청을 처리할 수 있다.
이 때, 몇개의 쓰레드를 사용해야 요청을 가장 많이 처리할 수 있는지 측정해야한다.
부하 테스트를 통해서 확인하면된다 
→ <code>100</code> 개의 쓰레드를 활용하는 게 가장 효율적이라고 측정했다고 가정하자</p>
<p>2 . 하나의 컨슈머 서버가 처리할 수 있는 최대 처리량을 알아내기
컨슈머 서버가 적절한 쓰레드 개수를 기반으로 요청을 처리한다고 했을 때, 최대 처리량(Throughput)이
얼마나 되는지 측정해야한다.
-&gt; 하나의 컨슈머 서버가(<code>100</code> 개의 쓰레드 활용) 1초에 처리할 수 있는 처리량이 30이라고 가정하자
-&gt; 즉 1개의 쓰레드가 1초에 <code>0.3</code> 개의 요청을 처리한다는 뜻이다 </p>
<p>3 . 프로듀서 서버가 보내는 평균 메시지량 알아내기
사용자가 API 요청을 얼마나 보내는 지와 같은 의미로. 사용자가 1초당 평균적으로 얼마나 요청을 보낼지
측정하거나 예상해야한다
-&gt;  사용자가 평균적으로 1초당 100개의 메일을 보낸다고 가정</p>
<p>4 . 처리가 지연되지 않는 선에서 파티션 개수 계산
처리가 지연되지 않으려면 프로듀서에서 들어오는 메시지의 수보다 더 빨리 처리할 수 있어야 한다.
그리고 평균 메시지량이 어느 정도 초과할 것도 예상해서 계산해야 한다.</p>
<p>-&gt; 평균 메시지량이 어느 정도 초과할 것을 예상해서 1초당 <code>120</code> 개 정도를 처리할 수 있게 만드려면, 
아래 공식에 의해 적정 파티션 수는 <code>400</code> 개라는 걸 알 수 있다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/93bab2cd-67f6-410f-8978-54f57cd2c9b0/image.png" alt=""></p>
<hr>
<p>컨슈머 서버가 메시지를 지연 없이 잘 처리하고 있는지 확인해보자</p>
<p>✅ Lag</p>
<ul>
<li>컨슈머 서버가 아직 처리하지 못한 메시지 (지연된 메시지)의 개수이다</li>
<li>Lag는 지연(deley) 의미를 가지며, 평소에 컴퓨터가 느려지면 렉걸린다 표현을 할 때 그 렉이다</li>
<li><code>컨슈머 렉</code> 이라고도 부른다</li>
</ul>
<p>그럼 <code>컨슈머 렉</code> 은 언제 발생할까?</p>
<p>프로듀서 서버의 메시지 생산량 보다 
컨슈머의 서버의 메시지 처러량이 작을 때 <code>컨슈머 렉</code> 이 발생한다.
예를들어 메시지가 1초에 3개씩 생기는데, 1초에 메세지를 1개밖에 처리를 못한다면
1초당 메시지 2개씩 계속해서 쌓일것이다.</p>
<p>현업에서는 주로 컨슈머 서버에 장애가 있을 경우 <code>컨슈머 렉</code>  이 발생하는 경우가 많다
이를 해결하려면 <code>컨슈머 렉</code> 을 지속적으로 모니터링 해야한다</p>
<p>먼저 CLI에서 모니터링을 해보자
CLI 에서 컨슈머 서버를 종료시키고 API요청을 보낸 후
그 후에 컨슈머 그룹 세부 정보를 조회하면
<code>$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092</code>
<code>--group email-send-group --describe</code>
아래 사진과 같이 LAG 의 개수가 들어있는것을 확인할 수 있다.
이 말은 파티션1 에는 처리되지않는 메시지가 2개, 파티션2,3은 처리되지않은 메시지가 1개씩 들어있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/3c820a78-6b17-456d-be03-adb693ce7ac7/image.png" alt=""></p>
<p>하지만 24시간 내내 CLI로 <code>컨슈머 렉</code> 이 발생하는지 확인 할 수 없다.
그래서 외부 모니터링 툴을 사용해서 알림을 발생하도록 하는것이 좋은 방법이다.</p>
<p>오픈소스 추천 : <code>프로메테우스</code> , <code>그라파나</code> </p>
<p>지금까지는 카프카를 직접 구축해서 사용했지만, 실제 현업에서는 클라우드(AWS)의 카프카 서비스를
사용하는 경우가 많다. 이 서비스를 사용하면 자체적인 <code>컨슈머 렉</code> 에 대한 모니터링 기능을 제공해준다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[kafka 의 개념 및 메시지 처리 방법]]></title>
            <link>https://velog.io/@choi-ju-yung/kafka</link>
            <guid>https://velog.io/@choi-ju-yung/kafka</guid>
            <pubDate>Tue, 27 Jan 2026 14:37:11 GMT</pubDate>
            <description><![CDATA[<p>✅ kafka</p>
<ul>
<li>대규모 데이터를 처리할 수 있는 <code>메시지 큐</code> = (임시저장소)</li>
<li><code>메시지 큐</code> 의 종류중 하나이다</li>
</ul>
<p><code>메시지 큐</code> : 큐 형태에 데이터를 일시적으로 저장하는 임시 저장소
<code>매시지 큐</code> 를  활용하면 비동기적으로 데이터를 처리할 수 있어서 효율적이다</p>
<hr>
<p>✅ 그림으로 보는 <code>메시지 큐</code> 를 활용한 통신
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/a1c8bd0c-c4c8-413d-9317-362f15f2d81a/image.png" alt=""></p>
<p>위 그림에서 순차적으로 보면</p>
<ol>
<li><p>사용자가 요청을 보내면 <code>Producer</code> 라는 서버에 보낸다</p>
</li>
<li><p><code>Producer</code> 서버는 처리해야할 요청의 정보들을 메시지로 만들어 <code>메시지 큐</code> 에  전달한다</p>
</li>
<li><p><code>Producer</code> 서버는 <code>메시지 큐</code> 에 메시지를 넣자마자 사용자에게 성공응답을 한다 (비동기방식)
💡 비동기 방식으로 처리하기 때문에 효율적이며, 대규모 트래픽을 처리할 때도 유리한 구조이다</p>
</li>
<li><p><code>메시지 큐</code> 는 <code>Producer</code> 로부터 받은 메시지(요청)을 보관하고 있으며
처리해야할 메시지들을 보관하는 임시 저장소 역할을 한다.
메시지를 전달받으면 kafka는 <code>메시지 큐</code>에 <code>토픽</code> 별로 구분해 메시지를 저장해둔다
💡 토픽이란? : <code>메시지 큐</code> 에 넣을 메시지의 종류를 구분하는 개념</p>
</li>
<li><p><code>Consumer</code> 서버에서는 <code>메시지 큐</code> 가 보관하고 있는 메시지(요청)을 꺼내서 실제 작업을 수행한다
즉 실제로 비즈니스 로직을 수행하는 서버를 <code>Consumer</code> 라고 한다</p>
</li>
</ol>
<hr>
<p>✅ 토픽(TOPIC) 관련 명령어</p>
<p>💡 토픽 관련 작업은 모두 <code>bin/kafka-topics.sh</code> 스크립트로 수행한다</p>
<p>(1) 토픽 생성 : <code>bin/kafka-topics.sh --bootstrap-server &lt;kakfa 주소&gt; --create --topic &lt;토픽명&gt;</code>
ex) <code>bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic email.send</code></p>
<p>(2) 토픽 조회 : <code>bin/kafka-topics.sh --bootstrap-server &lt;kakfa 주소&gt; --list</code>
ex) <code>bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</code></p>
<p>(3) 특정 토픽 조회 : <code>bin/kafka-topics.sh --bootstrap-server &lt;kakfa 주소&gt; --describe --topic &lt;토픽명&gt;</code>
ex) <code>bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic email.send</code></p>
<p>(4) 토픽 삭제 : <code>bin/kafka-topics.sh --bootstrap-server &lt;kafka 주소&gt; --delete --topic &lt;토픽명&gt;</code>
ex) <code>bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic email.send</code></p>
<hr>
<p>✅ 프로듀서(producer) 관련 명령어</p>
<p>💡 프로듀서는 메시지큐에 메시지를 보내는 역할을 한다
프로듀서 관련 작업은 모두 <code>bin/kafka-console-producer.sh</code> 스크립트로 수행한다</p>
<p>우선 email.send 라는 토픽을 만들었다고 가정</p>
<p>(1) Kafka의 특정 토픽에 메시지 넣기<br><code>bin/kafka-console-producer.sh --bootstrap-server &lt;kafka 주소&gt; --topic &lt;토픽명&gt;</code>
ex) <code>bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic email.send</code></p>
<p>위 명령어를 입력 후 넣을 메시지를 입력하고 엔터누르면 토픽에 메시지가 잘 들어감
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/333b190c-546f-4fab-bf8f-c8a32f96352d/image.png" alt="">
<br></p>
<p>(2) Kafaka에서 메시지 조회하기
ex) <code>bin/kafka-console-consumer.sh --bootstrap-server localhost:9092</code>
<code>--topic email.send --from-beginning</code></p>
<p>💡 <code>--from-beginning</code> : 토픽에 저장된 가장 처음 메시지부터 출력해온다</p>
<p>위 명령어는 <code>email.send</code> 토픽에 저장된 메시지를 전부 조회한다. 추가로 <code>email.send</code> 토픽에 메시지가 추가되어도 메시지가 실시간으로 조회하는 상태로 변경된다</p>
<p>전통적인 메시지 큐(RabbitMQ, SQS) 는 메시지를 조회하면, 해당 메시지를 큐에서 제거하는 구조이지만
Kafka는 메시지를 조회하면, 읽기만 하고 제거가 되지 않는다</p>
<p>즉 Kafka는 같은 메시지를 여러번 읽는게 가능하다
<br>
(3) 메시지를 어디까지 읽었는지 기억하고, 다음 메시지부터 처리하기
Kafka에서는 <code>컨슈머 그룹</code> 을 활용하여 어디까지 메시지를 읽었는지 <code>오프셋</code> 이라는 번호로 기록해둔다
<code>컨슈머 그룹</code> 에 속해있는 <code>컨슈머</code> 들은 안 읽은 메시지로부터 순차적으로 메시지를 읽게 된다</p>
<p><code>컨슈머</code> : Kafka 의 메시지를 처리하는 주체
<code>컨슈머 그룹</code> : 1개 이상의 컨슈머를 하나의 그룹으로 묶은 단위
<code>오프셋</code> : 메시지의 순서를 나타내는 고유 번호 (0부터 시작) = 배열 인덱스와 비슷</p>
<p>컨슈머 그룹을 지정해서 메시지 읽기
ex) <code>bin/kafka-console-consumer.sh --bootstrap-server localhost:9092</code>
<code>--topic email.send --from-beginning --group email-send-group</code></p>
<p><code>--group email-send-group</code> → 컨슈머 그룹 지정</p>
<p>위 명령어처럼 <code>--group email-send-group</code> 와 <code>--from-beginning</code> 을 같이 사용하면
<code>컨슈머 그룹</code> 의 <code>오프셋</code> 기록이 없으면 첫 메시지부터 다 읽고, 기록이 있으면 그 이후 오프셋부터 읽는다</p>
<p>컨슈머 그룹이 잘 생성됬는지 조회
ex) <code>bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list</code></p>
<p>특정 컨슈머 그룹 세부정보 조회하기
ex) <code>bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092</code>
<code>--group email-send-group --describe</code>
세부정보를 조회하면 현재 컨슈머 그룹의 오프셋을 확인할 수 있다</p>
<p>만약 <code>email.send</code> 토픽에 메시지를 추가로 넣고, <code>컨슈머 그룹</code> 으로 메시지를 조회하면
기존에 읽었던 메시지는 제외하고 방금전에 추가한 메시지만 조회하는것을 알 수 있다</p>
<p>실무에서는 똑같은 요청을 중복해서 여러번 호출하면 안된다. 그래서 반드시 <code>컨슈머 그룹</code> 을 활용하여
메시지를 어디까지 읽었는지 <code>오프셋</code> 값으로 기억해뒀다가, 처리하지 않은 그 다음 메시지부터 처리해야 한다.</p>
<hr>
<p>위에서 <code>Producer</code> 서버는 <code>메시지 큐</code> 에 메시지를 넣자마자 사용자에게 성공응답을 한다고 했다
이 방식은 비동기방식인데, 비동기방식으로 처리하면 제대로 작업을 처리했는지 알 수 있을까?</p>
<p>즉 Kafka에 메시지만 넣고, 바로 응답할 수 있기 때문에 사용자 입장에서는 기다림 없이 빠르게 처리가 된 것 처럼 느껴진다. 하지만 이 구조는 사용자에게 실제 성공여부를 확인하지 않고 응답을 먼저 보내고 있다.</p>
<p>이러한 비동기 구조의 단점을 보완하기 위해서, 두가지 방법을 주로 많이 활용한다</p>
<ul>
<li>메시지 처리 중 실패가 발생했을 때, 자동으로 <code>재시도 (retry)</code> 하는 방식</li>
<li>여러번의 재시도 끝에도 실패한 메시지를 별도로 보관하는 <code>Dead Letter Topic (DLT)</code> 방식</li>
</ul>
<p>✅ <code>재시도</code> 방식</p>
<p><code>재시도</code> 전략 방식은 별도의 설정을 하지 않아도 이미 기본값으로 세팅이 되어있다
<code>interval</code> : 재시도를 하는 시간 간격으로 (기본값 0) = 즉시 재시도한다는 뜻
<code>maxAttempts</code> : 최대 재시도 횟수로 (기본값 9)
<code>currentAttemps</code> : 지금까지 시도한 횟수로 (기본값 : 최초 시도 횟수 + 최대 재시도 횟수)</p>
<p>위와 같은 <code>재시도</code> 전략의 기본값을 바꾸려면 어떻게 해야할까?
<code>@RetryableTopic</code> 어노테이션을 사용</p>
<pre><code class="language-java"> @RetryableTopic(
     attempts = &quot;5&quot;,  //  attemps는 총 시도횟수로 (최초 시도횟수를 제외한 4번까지 재시도를 한다는 뜻)
     backoff = @Backoff(delay = 1000, multiplier = 2)  // 재시도 간격으로 처음에는 1000ms로 재시도하다가
     // 재시도 할때마다 시간이 2배로 증가함 (1000ms -&gt; 2000ms -&gt; 4000ms -&gt; 8000ms 순으로 증가한다.)
 )
 public void consume(String message) {
    ...
 }</code></pre>
<p>💡 현업에서는 보통 재시도횟수를 3~5 회 사이로 정한다.  재시도를 너무 많이 하면 시스템 부하가 커지고
너무 적으면 일시적인 네트워크 장애에 대응하기 어렵기 때문이다</p>
<p>하지만 이렇게 여러번 <code>재시도</code> 를 했는데도 실패했을 경우 어떻게 해야할까?
사용자 입장에서는 <code>성공</code> 메시지가 보여졌지만, 실제로는 실패한 작업으로 남게된다.
이럴 경우 <code>Dead Letter Topic (DLT)</code> 라는 별도 토픽을 활용해서, <code>재시도</code> 까지 실패한 메시지를
안전하게 보관하여 나중에 관리자가 확인하여 수동적이라도 처리할 수 있도록 구성할 수 있어야한다</p>
<p>✅ <code>Dead Letter Topic</code>     </p>
<ul>
<li>오류로 인해 처리할 수 없는 메시지를 임시로 저장하는 토픽</li>
<li>kafka에서는 재시도까지 실패한 메시지를 
다른 토픽에 저장해서 유실을 방지하고 후속 조치를 가능하게 한다</li>
</ul>
<p><code>DLT</code> 를 사용하는 이유?</p>
<ul>
<li>실패한 메시지가 유실되는것을 방지</li>
<li>사후에 실패 원인을 분석할 수 있음</li>
<li>처리되지 못한 메시지를 수동으로 처리할 수 있음</li>
</ul>
<p>✅ <code>DLT</code> 를 활용해 재시도에 실패한 메시지 따로 보관하기</p>
<p>Spring Kafka는 위에서 선언한 <code>@RetryableTopic</code> 을 사용하면 자동으로 <code>DLT</code> 토픽을 생성하고
메시지를 전송해준다. 기본적으로 <code>(기존토픽명) - dlt</code> 형태로 DLT 토픽이 만들어진다</p>
<p>하지만 아래 코드와 같이 내가 원하는 형태로 DLT 토픽의 명칭을 바꿀 수 있다 -&gt; <code>.dlt</code> 로 변경</p>
<pre><code class="language-java"> @RetryableTopic(
     attempts = &quot;5&quot;,  
     backoff = @Backoff(delay = 1000, multiplier = 2)  
    dltTopicSuffix = &quot;.dlt&quot; // DLT 토픽 이름에 붙일 접미사
 )
 public void consume(String message) {
    ...
 }</code></pre>
<blockquote>
<p>이후 CLI 에서 토픽을 조회하면 <code>DLT</code> 토픽이 생성된 것을 확인할 수 있다
<code>$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --list</code>
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/931a469e-bb8b-44fa-b0af-80950b9177dd/image.png" alt="">메시지 재시도를 하면서 생긴 토픽들도 보인다. 만들어진 .dlt 토픽을 확인해보자
<code>$ bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic email.send.dlt --from-beginning</code> 을 입력하면 재시도에 실패한 메시지가 .dlt 토픽에 저장되어 있음을 확인 가능
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/be205963-3eef-4501-94db-23477993a980/image.png" alt="">
전체적인 그림
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/7b57a3c7-9231-43e0-b1d5-1bd50aa139b8/image.png" alt=""></p>
</blockquote>
<p>이렇게 <code>재시도</code> 조차 실패한 메시지를 <code>DLT</code> 토픽에 저장한 후, 이 실패한 메시지를 조치하는 방법에 대해서 알아보자</p>
<ol>
<li><code>DLT</code> 에 저장된 실패 메시지를 로그 시스템에 전송하여 장애 원인을 추적해야한다</li>
<li><code>DLT</code> 에 저장되자마자 관리자에게 알림을 갈 수 있도록 설정한다.</li>
<li>알림을 받은 관리자는 로그에 쌓인 내용을 보고 장애 원인을 분석하고, <code>수동</code> 으로 처리한다.</li>
</ol>
<p>여기서 <code>수동</code> 으로 처리한다는 방식은 
일시적인 네트워크 장애였을 경우 메시지를 원래 토픽으로 다시 보내는 방법
메시지의 내용이 잘못됬을 경우 그 메시지를 폐기하는 방법
잘못된 메시지가 Kafka에 들어가지 않도록, <code>Producer</code> 의 검증 로직을 보완하는 방법이 있다</p>
<pre><code class="language-java"> @KafkaListener(
     topics = &quot;email.send.dlt&quot;,
     groupId = &quot;email-send-dlt-group&quot;
 )
 public void consume(String message) {

 System.out.println(&quot;로그 시스템에 전송 : &quot; + message);  // 대충 로그 시스템에 전송하는 로직 .. 

 System.out.println(&quot;Slack에 알림 발송&quot;); // 대충 Slack에 알림 발송하는 로직..
}</code></pre>
<p>위 코드처럼, <code>DLT</code> 토픽으로 빠진 메시지는 관리자에게 알림이 발송되어 관리자가 수동으로 처리할 수 있다</p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[문자열 관련 문제]]></title>
            <link>https://velog.io/@choi-ju-yung/%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B4%80%EB%A0%A8-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@choi-ju-yung/%EB%AC%B8%EC%9E%90%EC%97%B4-%EA%B4%80%EB%A0%A8-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Mon, 01 Dec 2025 14:57:40 GMT</pubDate>
            <description><![CDATA[<p>✅ 문자 찾기</p>
<p>문자열 입력 : <code>객체.next()</code>
문자 입력 : <code>객체.next().charAt(0)</code>
공백 포함 문자열 입력 : <code>객체.nextLine()</code></p>
<p>문자 -&gt; 문자배열로 변환 
<code>char[] 변수명</code> = <code>문자열값.toCharArray()</code></p>
<p>대소문자 구분 없이 라는 말이 나오면 
문자열은 <code>toUpperCase()</code> 또는 <code>toLowerCase()</code> 로 통일시키고
문자는 <code>Character.toUpperCase(문자값)</code> 또는 <code>Character.toLowerCase(문자값)</code> 로 통일시킨다</p>
<p>💡 <code>Character</code> : 문자(특히 유니코드 관련) 작업을 도와주는 클래스</p>
<p>String 은 배열처럼 인덱스로 접근할 수 없다  ⮕ <code>charAt(index)</code> 로 접근해야함</p>
<p>아래에 나오는 코드는 for문 방식과 foreach 방식을 둘다 표현하였다</p>
<pre><code class="language-java">for(int i=0; i&lt;str.length()-1; i++) {
        if(str.charAt(i) == t) {
            answer++;
        }
}

for(char x : str.toCharArray()){
    if(x=t) cnt++;
}</code></pre>
<hr>
<p>✅ 대소문자 변환</p>
<pre><code class="language-java">    public String solution(String str) {
        String answer = &quot;&quot;;

        char[] charArray = str.toCharArray(); // 문자열을 문자배열로 변환
        for(char s : charArray) {
            if(Character.isLowerCase(s)) { // 소문자이면
                answer += Character.toUpperCase(s);
            }else {  // 대문자이면 
                answer += Character.toLowerCase(s);
            }
        }        
        return answer;
    }</code></pre>
<p>대문자와 소문자는 숫자값으로 표현할 수 있으며, 같은 대소문자 차이는 32 크기가 차이난다</p>
<hr>
<p>✅  문장 속 단어
한 개의 문장이 주어지면 그 문장 속에서 가장 긴 단어를 출력하는 프로그램
입력 : <code>it is time to study</code>   출력 : <code>study</code></p>
<pre><code class="language-java">public String solution(String str) {
        String answer = &quot;&quot;;
        int m = Integer.MIN_VALUE; // int) 변수가 가질 수 있는 가장 작은 값
        String[] split = str.split(&quot; &quot;);
        for(String x : split) {
            if(m &lt; x.length()) {
                m = x.length();
                answer = x;
            }
        }
        return answer;
    }
</code></pre>
<hr>
<p>✅ 알파벳 뒤집기
N개의 단어가 주어지면 각 단어를 뒤집어 출력하는 프로그램</p>
<pre><code>입력 : 
3
good
Time
Big

출력 :
doog
emiT
giB</code></pre><pre><code class="language-java">public ArrayList&lt;String&gt; solution(int n, String str[]) {

    ArrayList&lt;String&gt; answer = new ArrayList&lt;&gt;();

    for(String x : str) { // 문자열은 불변이라 문자열 내부의 순서를 바꿀수 없음
        char[] charArray = x.toCharArray(); // 문자열을 -&gt; 문자 배열로 변환
        int lt = 0;
        int rt = charArray.length-1;
        char tmp; // 문자 자리를 바꾸기 위해서 생성

        while(lt &lt; rt) { // 짝수, 홀수개일경우 다 성립
            tmp = charArray[lt];
            charArray[lt] = charArray[rt];
            charArray[rt] = tmp;
            lt++;
            rt--;
        }

        String value = String.valueOf(charArray); // 다시 문자열로 변환
        answer.add(value);
    }
    return answer;
}

// main
int n = in.nextInt();
String str[] = new String[n];
for(int i=0; i&lt;n; i++) {
    str[i] = in.next(); // 문자 배열개수 만큼 입력
}

for(String x : T.solution(n, str)){
    System.out.println(x);
}</code></pre>
<hr>
<p>✅ 특정 문자 뒤집기
영어 알파벳과 특수문자로 구성된 문자열이 주어지면 영어 알파벳만 뒤집고,
특수문자는 자기 자리에 그대로 있는 문자열을 만들어 출력하는 프로그램
입력 : <code>a#b!GE*T@S</code>  출력 : <code>S#T!EG*b@a</code></p>
<pre><code class="language-java">public String solution(String str) {

    String answer = &quot;&quot;;

    char[] charArray = str.toCharArray();

    int lt = 0;
    int rt = charArray.length - 1;
    char tmp;

    while (lt &lt; rt) {

        if(!Character.isAlphabetic(charArray[lt])) lt++; // 왼쪽에서부터 오는 문자가 알파벳이 아니면 
        else if(!Character.isAlphabetic(charArray[rt])) rt--; // 오른쪽에서부터 ~~ 알파벳이 아니면
        else { // 왼쪽 오른쪽 둘다 알파벳이면
            tmp = charArray[lt];
            charArray[lt]  = charArray[rt];
            charArray[rt] = tmp;
            lt++;
            rt--;
        }
    }
    answer = String.valueOf(charArray); // 문자배열 상태를 문자열로 변환
    return answer;
}</code></pre>
<hr>
<p>✅ 중복문자 제거
소문자로 된 한개의 문자열이 입력되면 중복된 문자를 제거하고 출력하는 프로그램
입력 : <code>ksekkset</code>  출력 : <code>kset</code></p>
<p>3번째 인덱스에 있는 k 값을 보면 
i = 3일 때 <code>str.indexOf(str.charAt(3))</code> 즉 <code>str.indexOf(str.charAt(k))</code> 는  0 이 된다
즉 앞에 이미 k가 있기때문에  i값과 <code>indexOf</code> 값이 일치하지 않으면 값이 이미 중복된다는 소리이다
그래서 i값과 <code>indexOf</code> 값이 일치하는 문자만 따로 빼서 저장해야한다</p>
<pre><code class="language-java">public String solution(String str) {

    String answer = &quot;&quot;;
    char[] charArray = str.toCharArray();

    for(int i=0; i&lt;str.length(); i++) { 
        if(i == str.indexOf(str.charAt(i))) { 
            answer += str.charAt(i);
        }
    }
    return answer;
}</code></pre>
<hr>
<p>✅ 회문문자열
앞에서 읽을 때나 뒤에서 읽을 때나 같은 문자열을 회문 문자열이라 함
해당 문자열이 회문 문자열이면 &quot;YES&quot;, 회문 문자열이 아니면 “NO&quot;를 출력하는 프로그램
단 회문을 검사할 때 대소문자를 구분 X</p>
<pre><code class="language-java">// 첫번째 방법
public String solution(String str) {
    String answer = &quot;YES&quot;;
    int lt = 0;
    int rt = str.length()-1;

    str = str.toLowerCase(); // 대소문자를 구분하지 않아야하므로 다 소문자로 변환
    char[] charArray = str.toCharArray(); // 문자열을 순회하기위해 문자배열로 변환

    while(lt &lt; rt) { // 문자열개수가 짝수개든 홀수개든 다 성립
        if(charArray[lt] != charArray[rt]) { // 양쪽의 문자를 비교해서 한번이라도 같지않은 순간
             answer = &quot;NO&quot;;  // 순회문자열이 아니므로 NO로 바꾼 후 반복문 탈출
            break;
        }
        lt++;
        rt--;
    }
    return answer;
}

// 두번째 방법
public String solution(String str) {

    String answer = &quot;YES&quot;;
    // StringBuilder 클래스로 문자열을 변형시켜서 뒤집은 후 문자열로 형태로 바꿔주기
    String tmpString = new StringBuilder(str).reverse().toString(); 

     // 뒤집은 문자열과 원본 문자열이 동일하면 순회문자!
    if(!str.equalsIgnoreCase(tmpString)) {  // equalsIgnoreCase : 대소문자 구분없이 비교할 떄 많이 사용
        answer = &quot;NO&quot;;
    }
    return answer;
}</code></pre>
<hr>
<p>✅ 팰린드롬
회문문자열과 동일한 방식인 문자열을 팰린드롬이라고 한다 (앞, 뒤에서 읽을 때 같은 문자열)
문자열이 입력되면 해당 문자열이 팰린드롬이면 &quot;YES&quot;, 아니면 “NO&quot;를 출력하는 프로그램
단 회문을 검사할 때 알파벳 이외의 문자들의 무시하고, 대소문자를 구분하지 않음</p>
<p>💡 정규표현식은 <code>replaceAll</code> 로 가능하며 replace 로는 불가능</p>
<pre><code class="language-java">public String solution(String str) {

    String answer = &quot;YES&quot;;

    // A-Za-z : 영어 대문자 A~Z, 소문자 a~z
    // ^ : not(부정) 을 의미
    str = str.replaceAll(&quot;[^A-Za-z]&quot;,&quot;&quot;); // 문자열이 대,소문자가 아닌 다른경우 다 공백으로 치환 
    str = str.toLowerCase(); // 대소문자만 남은 문자열을 다 소문자로 치환
    String reverseStr = new StringBuilder(str).reverse().toString();

    if(!str.equalsIgnoreCase(reverseStr)) {
        answer = &quot;NO&quot;;
    }
    return answer;
}</code></pre>
<hr>
<p>✅ 숫자만 추출
문자와 숫자가 섞여있는 문자열이 주어지면 그 중 숫자만 추출하여 그 순서대로 자연수 만듬
만약 “tge0a1h205er”에서 숫자만 추출하면 0, 1, 2, 0, 5이고 이것을 자연수를 만들면 1205
하지만 0이 먼저나오면 생략해야함 ex) <code>g0en2T0s8eSoft</code> -&gt; <code>208</code></p>
<pre><code class="language-java">// 첫번째 방법
public int solution(String str) {
    String replaceStr = str.replaceAll(&quot;[^0-9]&quot;, &quot;&quot;); // 숫자이외의 문자들은 다 공백으로 치환 

    int result = Integer.parseInt(replaceStr); // 문자열을 기본형 정수로 변환 (자동으로 앞자리 0 생략)
    return result;
}

// 두번째 방법
public int solution(String str) {
    String answer = &quot;&quot;;
    for(char s : str.toCharArray()) { // 문자열 조작을 위해 문자배열로 변환 후 문자 하나씩 조작
        if(Character.isDigit(s)) answer += s;   // 문자가 숫자형태이면 문자열객체에 더해줌
    }
    return Integer.parseInt(answer); // 문자열을 정수형태로 변환
}</code></pre>
<hr>
<p>✅ 가장 짧은 문자거리
한 개의 문자열 s와 문자 t가 주어지면 문자열 s의 각 문자가 문자 t와 떨어진 최소거리를 출력</p>
<pre><code class="language-java">public int[] solution(String str, char c) {

    int answer[] = new int[str.length()]; // 정수배열 생성
    int s = 1000; // 초기값은 “문자열 길이보다 충분히 큰 값”이어야 한다
    for(int i=0; i&lt;str.length(); i++) { // 왼쪽에서부터 비교
        if(str.charAt(i) == c) {
            s = 0;
               answer[i] = s;
        }else {
            s++;
            answer[i] = s;
        }
    }

    s = 1000; // 초기값은 “문자열 길이보다 충분히 큰 값”이어야 한다
    for(int j= str.length()-1; j&gt;=0; j--) { // 오른쪽에서 부터 비교

        if(str.charAt(j) == c) s = 0;  // 이미 0이므로 또 넣을필요 x
        else {
            s++;
            answer[j] = Math.min(answer[j], s);  // 정수 두개중 더작은값 뽑기
        } 
    }

    return answer;
}</code></pre>
<p>💡 이 문제의 핵심은 첫 값을 크게 잡는것과 왼쪽에서 끝까지 순환했다가
오른쪽에서 처음으로 순환하는것이 포인트</p>
<hr>
<p>✅ 문자열 압축</p>
<pre><code class="language-java">    public String solution(String str) {

        String answer = &quot;&quot;;
        int sum = 1;
        for(int i=0; i&lt;str.length(); i++) {

            if(i == str.length()-1) { // 마지막문자일경우 그 문자를 붙이고 현재 sum값에 따라 붙여줌
                answer = answer + str.charAt(i); 
                if(sum &gt; 1) { // 마지막문자에 현재 누적된 sum값이 1이상일때만 붙여줌
                    answer = answer + sum;
                }
                break;
            }

            if(str.charAt(i) == str.charAt(i+1)) { // 다음문자와 동일한 문자이면
                sum += 1; // sum값 누적
            }else {
                if(sum == 1) { // 동일한 문자가 아닌데, 현재 누적된 sum값이 1이면 
                    answer = answer + str.charAt(i); // 문자만 붙이기
                    sum = 1;
                    continue; // 다시 for문으로 이동
                }

                answer = answer + str.charAt(i) + sum;  // sum이 2이상이면 sum값도 붙여주기
                sum = 1;
            }
        }
        return answer;
    }</code></pre>
<hr>
<p>✅ 암호
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/87bceb50-b5a2-41af-94d9-2610b592f461/image.png" alt=""></p>
<pre><code class="language-java">    public String solution(int n,String str) {

        String answer = &quot;&quot;;

        // 7개씩추출 + #은 1로, *은 0으로 치환
        for(int i=0; i&lt;n; i++) {
            String tmp = str.substring(0,7).replace(&quot;#&quot;, &quot;1&quot;).replace(&quot;*&quot;, &quot;0&quot;); 

            int num = Integer.parseInt(tmp,2); // 2진수 문자열값을 10진수로 변환

             answer = answer + (char)num; // 정수를 문자형태로 변환하면 아스키코드로변환
            str = str.substring(7); // 뽑아온 문자열부터 다시 시작

        }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[다중컬럼 인덱스]]></title>
            <link>https://velog.io/@choi-ju-yung/%EB%8B%A4%EC%A4%91%EC%BB%AC%EB%9F%BC-%EC%9D%B8%EB%8D%B1%EC%8A%A4</link>
            <guid>https://velog.io/@choi-ju-yung/%EB%8B%A4%EC%A4%91%EC%BB%AC%EB%9F%BC-%EC%9D%B8%EB%8D%B1%EC%8A%A4</guid>
            <pubDate>Mon, 13 Oct 2025 16:11:24 GMT</pubDate>
            <description><![CDATA[<p>앞서 하나의 컬럼으로 구성된 <code>단일 인덱스</code> 에 대해서 정리했다
이번에는 여러 조건을 조합하여 쿼리의 성능을 최적화하는 <code>다중 컬럼 인덱스</code> 다른 말로 <code>복합 인덱스</code> 를 정리하겠다</p>
<p>✅ 다중컬럼 인덱스 : 두 개 이상의 컬럼을 묶어서 하나의 인덱스로 만드는 것</p>
<p><code>다중컬럼 인덱스</code> 원칙 3가지</p>
<ul>
<li>인덱스는 순서대로 사용하기</li>
<li>등호(=) 조건을 앞으로, 범위(&gt;, &lt;) 조건은 뒤로 보내기</li>
<li>정렬(ORDER BY)도 인덱스의 순서를 따르기</li>
</ul>
<p>ex) category와 price 순으로 복합 인덱스 생성하기
<code>CREATE INDEX idx_items_category_price ON items (category, price);</code></p>
<p>이렇게 인덱스를 생성하고 조회하면 -&gt; <code>SHOW INDEX FROM items;</code>
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/fa680710-f833-4e7e-bf18-39af0edb1b65/image.png" alt=""></p>
<p>사진과 같이 2개의 인덱스가 생성된것을 볼 수 있다 여기서 Seq_in_index 는 컬럼 순서이다</p>
<p>이제 복합 인덱스를 사용하는 조회 쿼리를 날려보자
<code>카테고리가 &#39;전자기기&#39;이면서, 가격이 정확히 120,000원인 상품을 찾기</code>
<code>EXPLAIN SELECT * FROM items WHERE category = &#39;전자기기&#39; AND price = 120000;</code>
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/515052be-2c5e-40df-8ed2-36f9ad2d5477/image.png" alt="">
사진처럼 인덱스를 생성할 때 Category와 price 순으로 이미 정렬이 되어있다
만약 category가 동일하면 price 순으로 정렬이 된다</p>
<p>카테고리가 전자기기 인 섹션을 찾고, 그 안에서 price 가 12000 인 지점을 탐색하게 된다
전자기기 섹션 내부에 이미 price 순으로 정렬되어 있어서, 데이터를 빠르게 접근할 수 있다</p>
<p>만약 그러면 <code>카테고리가 &#39;전자기기&#39;이면서 100,000원 초과인 상품을 가격 오름차순으로 정렬</code> 
가격 순으로 정렬해서 조건을 추가해서 EXPLAIN 하면 어떻게 될까?
<code>EXPLAIN SELECT * FROM items WHERE category = &#39;전자기기&#39; AND price &gt; 100000
ORDER BY price;</code>
-&gt; category = &#39;전자기기&#39; 인 것들을 찾음 -&gt; &#39;패션&#39; 전 까지만 찾음 -&gt;  그 찾은것들 중에서 가격 100000 찾기 -&gt; 찾은 가격보다 비싼 데이터들만 찾기 -&gt; 가격 순으로 정렬 = 이미 정렬이 되어있음</p>
<p>이미 인덱스가 생성될 때 price 순으로 이미 정렬되어 있기 때문에, <code>Using filesort</code> 가 보이지 않는다</p>
<p>하지만 만약 인덱스의 순서와 무관한 컬럼으로 정렬을 하면? =&gt; <code>ORDER BY stock_quantity</code>
그러면 결과를 가져온 뒤 별도로 정렬해야하는 불필요한 작업을 해야한다</p>
<hr>
<p>이번에는 복합 인덱스를 실패하는 경우를 알아보겠다</p>
<p>첫번째로 <code>인덱스 순서 무시</code> 상황이다</p>
<p>만약 복합 인덱스의 첫번째 컬럼인 category를 건너뛰고, 두번째 컬럼인 price 만으로 데이터를 검색하면
어떻게 될까? =&gt; 카테고리와 상관없이 가격이 80,000원인 상품을 찾기
<code>EXPLAIN SELECT * FROM items WHERE price = 80000;</code> </p>
<p>결과는 인덱스가 있음에도 옵티마이저는 <code>풀 테이블 스캔</code> 을 선택한다
그 이유는 <code>인덱스 왼쪽 접두어 규칙</code> 때문이다. 
가격이 80000인 상품은 &#39;전자기기&#39; 에도 &#39; 패션&#39; 에도 존재할 수 도 있기 때문이다
price 값이 인덱스 전체에 흩어져 있기 때문이다</p>
<p>결국 데이터베이스 입장에서 price 만으로 데이터를 찾으려면, &#39;도서&#39; 카테고리부터 &#39;패션&#39; 카테고리 까지
모든 섹션을 다 뒤져봐야한다. 이는 인덱스 전체를 스캔하는 작업으로, 차라리 풀 테이블 스캔이 나을것이다</p>
<p>쉽게말하면 전화번호부에서 성은 모르지만 이름이 &#39;주영&#39; 이라는 사람을 찾으려면
성이 ㄱ부터 ㅎ까지 다 찾아봐야 하는 경우이다. 이처럼 복합인덱스는 <code>선행 컬럼 조건</code> 없이는 제 역할을 하지 못한다</p>
<p>💡 (category, price) 이렇게 두개의 복합 인덱스를 만들면 (categry) 의 단일 인덱스를 만들 필요 없음
-&gt; 이미 복합인덱스에서 category를 기준으로 정렬이 되어있기 때문이다</p>
<p>두번째 실패하는 경우 
<code>범위 조건을 먼저 사용</code> 하는 경우이다
선행컬럼에 범위조건을 사용하면 그 뒤에오는 컬럼은 인덱스를 제대로 활용할 수 없다
<code>카테고리명이 &#39;패션&#39; 이상인 상품들 중에서, 가격이 정확히 20,000원인 상품을 찾기</code>
라는 조건이 있다고 가정하자</p>
<p><code>EXPLAIN SELECT * FROM items WHERE category &gt;= &#39;패션&#39; AND price = 20000</code> 와 같이 작성할 것이다
이것을 실행하면 카테고리가 이름순으로 패션보다 같거나 큰 카테고리 들로 1차 필터링을 한다
하지만 각 카테고리마다 가격들이 정렬되어있기 때문에 두번째로 나와있는 price는 인덱스를 사용하지 않고 직접 price = 20000 을 필터링해서 하나하나 검사한다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/9abe4bee-838b-40dc-94a1-8008c36331f5/image.png" alt=""></p>
<p>따라서 인덱스를 설계할 때는 <code>=</code> 조건으로 사용될 컬럼을 <code>범위</code> 조건으로 사용될 컬럼보다
앞에 배치하는것이 최적화 전략이다</p>
<p>ex) <code>CREATE INDEX idx_items_price_category_temp ON items (price, category);</code>
위 예시처럼 price가 동등조건이기 때문에 인덱스를 생성할 때 첫번째 컬럼으로 넣으면 된다
위에서 원칙 중 2번째 원칙에 해당한다 (등호(=) 조건은 앞으로, 범위조건은 뒤로)</p>
<p>하지만 이방법도 있지만 실무에서는 <code>IN</code> 절을 활용하여 많이 해결한다</p>
<p><code>WHERE category &gt;= &quot;패션&quot;</code> 이 부분을 <code>WHERE category IN (&#39;패션&#39;, &#39;헬스/뷰티&#39;)</code> 로 바꾸면 된다
왜냐하면 <code>IN</code> 절은 여러 개의 개별 지점에 대해 동등(=) 비교의 묶음으로 처리되기 때문이다</p>
<hr>
<p>✅ 인덱스 설계 가이드라인</p>
<p><span style="color:red">첫 번째로</span>
인덱스를 만들때 중요한 것은, <code>어디에</code> 인덱스를 만들어야 하는지 아는 것이다
잘못된 인덱스는 오히려 시스템 성능을 떨어뜨릴 수 있다</p>
<p>인덱스를 어디에 걸지 판단하는 가장 중요한 기준은 <code>카디널리티</code> 이다
<code>카디널리티</code> 는 컬럼에 저장된 값들의 <code>고유성</code> 을 나타내는 지표로
EX) category 컬럼에  &#39;도서&#39; &#39;패션&#39; &#39;운동&#39; &#39;차량&#39;  이렇게 있다면 <code>카디널리티</code> 는 4이다
EX) is_active 컬럼에 &#39;true&#39; &#39;false&#39; 이렇게 있다면 <code>카티널리티</code> 는 2이다</p>
<p>이 <code>카티널리티</code> 가 높은 컬럼에 생성하는 것이 좋다
왜냐하면, 특정키워드를 찾았을 때 검색범위가 확 줄어야 효율적이므로
비교하는 대상의 값이 여러개로 분포되어있어야 한쪽에 몰리지 않기 때문이다
만약 <code>where is_active = true</code> 의 데이터가 전체의 80%라고 가정하면
is_active 컬럼에 인덱스가 있더라도 풀테이블 스캔이 나을수도 있다</p>
<p>💡 <code>카디널리티</code> 높다 = 중복도가 낮다</p>
<p><span style="color:red">두 번째로</span>
<code>JOIN</code> 의 연결고리가 되는 컬럼(외래키) 에 인덱스를 생성해야한다</p>
<pre><code class="language-sql">EXPLAIN SELECT
   s.seller_name,
   i.item_name,
   i.price
 FROM items i
 JOIN sellers s ON i.seller_id = s.seller_id
 WHERE s.seller_name = &#39;행복쇼핑&#39;;</code></pre>
<p>위 예제를 보면 <code>seller_id</code> 컬럼을 대상으로 조인이 되고 있다
MYSQL 에서는 외래키 제약조건을 설정하면 자동으로 외래키 인덱스가 생성된다
하지만 ORACLE의 경우 자동으로 생성되지 않는다</p>
<p>💡 PK는 ORACLE, MYSQL 둘다 자동으로 인덱스가 생성됨</p>
<p>현재 sellers 테이블의 pk가 seller_id 이고  items 테이블의 fk가 seller_id 이다
따라서 oracle의 기준으로 조인을할때 풀테이블 스캔을 피하기 위해서 외래키 인덱스를 생성해줘야한다</p>
<p><code>CREATE INDEX idx_items_seller_id ON items(seller_id);</code> : 외래키 인덱스 생성 (JOIN용 = 자식테이블)</p>
<p>추가로 WHERE 절에 seller_name 컬럼도 인덱스를 추가해줘야한다
<code>CREATE INDEX idx_sellers_name ON sellers(seller_name);</code> : WHERE 절용</p>
<p><span style="color:red">세번째로</span>
ORDER BY 에 사용된 컬럼에 인덱스를 추가하자
정렬은 데이터의 양이 많을수록 매우 비용이 큰 작업이다.
<code>ORDER BY 대상컬럼</code> -&gt; 대상컬럼에 대해서 인덱스를 만들어 놓으면, 이미 데이터가 정렬된 상태로
저장되어 있기 때문에 ORDER BY 대상 컬럼 작업을 진행해도, 굳이 정렬작업 없이, 인덱스에
저장된 순서 그대로 데이터를 읽기만 하면 된다.</p>
<p>EX) <code>ORDER BY registered_date DESC LIMIT 10</code> 
여기에서는 대상 컬럼이 <code>registered_date</code> 이므로 
<code>registered_date</code> 컬럼에 인덱스를 생성해야한다</p>
<hr>
<p>✅ 인덱스의 단점과 주의사항</p>
<p>인덱스는 공짜가 아님 = 인덱스를 생성하고 유지하는 비용이 큼</p>
<p>인덱스의 단점으로는 첫번째로 <code>저장공간</code> 이다
인덱스는 원본테이블과 별개로, B-Tree 구조를 가진 물리적인 파일로 디스크에 저장된다</p>
<p>두번째로는 <code>쓰기성능(INSERT, UPDATE, DELETE)</code> 이다
인덱스는 <code>SELECT</code> 의 속도를 높이지만, <code>INSERT, UPDATE, DELETE</code> 의 속도를 희생시킨다
그 이유는 데이터의 변경이 일어날 때 마다 원본 테이블 뿐 아니라 이와 관련된
모든 인덱스를 함께 수정해야 하기 때문이다
EX) items 테이블을 추가하면, 이 테이블에 생성된 모든 인덱스를 추가해야한다
만약 인덱스가 5개라면, 테이블 삽입 1번에 인덱스 삽입 5번의 작업이 추가되는 것이다</p>
<p>실무에서는 이런식으로 사용한다</p>
<ol>
<li>조회가 빈번한 기능이라면, 인덱스를 자유롭게 생성해도 좋다</li>
<li><code>INSERT, UPDATE</code> 가 빈번한 기능이라면, 인덱스 생성에 신중하게 해야한다</li>
</ol>
<p>혹시 나중에 필요할까봐? 인덱스를 미리 만들지말고, 나중에 필요하면 만들자
사용하지 않는 인덱스를 주기적으로 정리하자</p>
<p>💡  인덱스 컬럼은 가공하면 안된다 
인덱스는 가공되지 않은 <code>원본</code> 값을 기준으로 만들어지기 때문이다</p>
<p><code>WHERE SUBSTRING(item_name, 1, 5) = &#39;게이밍&#39;</code>
<code>WHERE indexed_column * 10 = 100</code> 
위 예시처럼 인덱스가 적용된 컬럼을 함수로 감싸거나, 계산식을 넣으면서 가공을 하면
인덱스가 적용되지 않는다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단일 인덱스]]></title>
            <link>https://velog.io/@choi-ju-yung/%EB%8B%A8%EC%9D%BC-%EC%9D%B8%EB%8D%B1%EC%8A%A4</link>
            <guid>https://velog.io/@choi-ju-yung/%EB%8B%A8%EC%9D%BC-%EC%9D%B8%EB%8D%B1%EC%8A%A4</guid>
            <pubDate>Sun, 12 Oct 2025 13:13:49 GMT</pubDate>
            <description><![CDATA[<p><code>인덱스</code> 가 왜 필요할까?</p>
<p>데이터 양이 수십만개, 수백만개로 많아진 상태에서
데이터를 조회하면 서비스의 속도가 느려진다</p>
<p>인덱스가 없는 테이블에서 특정 데이터를 찾는 과정은
100만 페이지짜리의 거대한 책에서, 특정 단어 하나를 찾기위해
책의 첫페이지부터 마지막 페이지까지 한장씩 넘겨보는것과 같다</p>
<p>이러한 과정을 <code>풀 테이블 스캔</code>  (Full Table Scan) 이라고 한다</p>
<p>조인의 실무팁</p>
<ul>
<li><code>WHERE</code> 절에 자주 사용하는 검색 조건 컬럼에는 인덱스를 생성해서 풀테이블 스캔을 방지</li>
<li>대용량 데이터에 대한 풀스캔이 불가피한 통계/배치 작업이 있다면, 서비스 이용자가 적은 시간에 실행</li>
</ul>
<hr>
<p>✅  인덱스</p>
<p>인덱스는 <code>특정 컬럼</code> 을 정렬해서 저장하고, 이를 통해
원본데이터를 빠르게 접근할 수 있는 특별한 목차이다</p>
<p>한마디로 데이터를 빨리 찾기 위해 <code>특정 컬럼</code> 을 기준으로 미리 정리해놓은 표이다
실제로 DB에서 인덱스를 생성한다고 해서 정렬된 표를 직접 눈으로는 볼 수 없다
시스템 내부적으로 생성되기 때문이다</p>
<p>인덱스의 특징은 정렬이 되어있어야함
원본 데이터의 위치에는 실제 데이터 행의 위치(주소값, 포인터, PK값) 을 저장한다
이 값을 통해서 원본 데이터에 빠르게 접근할 수 있다</p>
<p>대부분의 관계형 데이터베이스는 인덱스를 구현하기 위해
이진트리를 개선한 <code>B-</code> 트리 또는 <code>B+</code> 트리를 사용한다 이는 <code>밸런스 트리</code> 의 한 종류이다</p>
<hr>
<p>✅  인덱스 생성하기
<code>CREATE INDEX 인덱스이름 ON 테이블이름 (컬럼1, 컬럼2, ...);</code>
인덱스 이름은 대부분 <code>idx_테이블명_컬럼명</code> 과 같은 규칙으로 지은다</p>
<p>ex) : <code>create index idx_items_item_name on items(item_name);</code>
위와 같이 인덱스를 생성하고</p>
<blockquote>
<p>💡 인덱스 조회 : <code>show index from 테이블명</code>
<code>show index from items;</code> 로 items 테이블과 관련된 인덱스를 모두 조회하면 아래와 같은 결과가 나온다<img src="https://velog.velcdn.com/images/choi-ju-yung/post/5cebd134-348d-4bb0-8d12-e85a57ebe893/image.png" alt=""></p>
</blockquote>
<p><code>NON_unique</code> : 1이면 중복을 허용하는 인덱스이며, 0이면 허용하지 않는 인덱스
<code>Cardinality</code> : 인덱스에 저장된 유니크 값의 개수에 대한 추정치이며, 높을수록 중복도가 낮고 성능이 좋다고 판단 </p>
<p>ex) 만약 데이터가 남자, 여자 로만 되어있으면 Cardinality 가 2이며, 효율성이 없음</p>
<p>위 결과와 같이 데이터베이스는 <code>PK</code> <code>FK</code> <code>유니크키</code> 를 설정하면 자동으로 인덱스를 만들어줌
💡  이유는 무엇일까?  PK를 대상으로 말하면</p>
<p>만약 데이터가 수십만개의 데이터가 있는상태에서 하나의 데이터를 INSERT 한다고 가정하자
그러면 INSERT를 할 때 PK가 중복이 되는지 비교를 해야하기 때문이다
그래서 자주사용하는 PK나 FK 그리고 중복을 확인해야하는 유니크키는 자동으로 인덱스가 생성된다
💡 FK, 유니크 키의 인덱스 자동생성은 DBMS마다 다름 -&gt; ORACLE (X) , MYSQL (O)</p>
<p><code>PK</code> 는 인덱스가 자동으로 적용되어 있으므로, <code>PK</code> 를 기준으로 데이터가 정렬된다</p>
<p><code>PK</code> = <code>클러스터링 인덱스</code> 
= 실제 데이터(원본데이터) 의 저장순서가 인덱스 순서와 동일하도록 정렬한 인덱스</p>
<p>✅ 인덱스 삭제
<code>DROP INDEX 인덱스이름 ON 테이블이름;</code>
ex) : <code>DROP INDEX idx_items_item_name ON items;</code></p>
<p>데이터베이스에는 쿼리를 어떤 방식으로 최적화해서 실행할지 계획하는 기능이 있는데
<code>옵티마이저</code> 라고 부른다</p>
<p>인덱스를 만들어도, 항상 모든 SELECT 문을 실행하면 그 인덱스를 사용하는것은 아니다
데이터의 분포나, 쿼리의 형태에 따라 <code>옵티마이저</code> 가 판단해서 인덱스를 사용할지, 풀스캔을 할지 결정한다
또 여러개의 인덱스가 있다면, 어떤 인덱스를 사용할지도 판단한다</p>
<p>실제로 쿼리에 인덱스가 잘 사용되고 있는지 확인하려면 <code>EXPLAIN</code> 를 쿼리 앞에 붙이면 된다
ex) : <code>EXPLAIN SELECT * FROM items WHERE item_name = &#39;게이밍 노트북&#39;;</code></p>
<p><code>EXPLAIN SELECT * FROM items WHERE item_name = &#39;게이밍 노트북&#39;;</code> 를 실행하면
아래와 같이 실행되는것을 확인할 수 있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/8d0ca503-ae0e-4a6e-be67-dc8e682655e5/image.png" alt="">
하지만 이결과는 데이터베이스가 통계 데이터를 기반으로 예측한 정보이므로 정확하지는 않음</p>
<p><code>type</code> : 데이터베이스가 테이블을 어떻게 접근하는지 나타낸다 </p>
<ul>
<li><code>ALL</code> : 풀테이블 스캔을 의미</li>
<li><code>index</code> : 풀인덱스 스캔 = 인덱스 테이블을 처음부터 끝까지 다 뒤져서 찾는 방식으로 인덱스 테이블은 실제 테이블보다 크기가 작기 때문에, 풀 테이블보다는 효율적이지만, 아주 효율적인건 X</li>
<li><code>const</code> : 조회하고자 하는 1건의 데이터를 한번에 찾을 때 나옴 (고유인덱스 or 기본키로 조회한경우)
ex) where <code>pk값</code>  = 2</li>
<li><code>ref</code> : 비고유 인덱스를 사용한경우 (unique 가 아닌 컬럼의 인덱스를 사용한 경우) 
ex) where name = &#39;최주영&#39;</li>
<li><code>range</code> : 범위검색 (BETWEEN, &gt;, &lt;, &gt;=) 에서 인덱스를 사용했다는 의미</li>
<li>위 결과는 인덱스를 사용하지 않았으므로, 풀테이블 스캔을 사용한다고 예측</li>
</ul>
<p><code>key</code> : 쿼리를 실행할때 사용한 인덱스 이름이 나옴
    하지만 값이 <code>NULL</code> 이라는 의미는 어떤 인덱스도 사용하지 않을것이라고 예측</p>
<p>   <code>rows</code> : 옵티마이저가 쿼리를 처리하기 위해 탐색할 것으로 예측하는 행의 수
   이미 <code>type</code>에서 풀스캔을 한다고 예측했으므로, 모든 데이터의 행의 수인 25개로 예측</p>
<p>   <code>fitered</code> : 필터링 후 최종적으로 조회될 예측되는 행의 비율
   모든 행의개수가 25개일 경우  10%이면 2.5개 정도의 행이 나올것이라고 예측한다
   하지만 실제로는 1개가 나오는데 이 부분은 예측이기 때문에 정확하지 않다</p>
<p>   <code>Extra</code> : 데이터를 가져온 후 추후의 작업을 의미
    위 결과에서 Using where 은, 현재 인덱스가 없어서 모든 데이터를 가져온 후
    조건에  맞는지 WHERE 절의 조건을 사용할것이라고 예측</p>
<hr>
<p>데이터베이스에서 인덱스는 3가지 상황에서 사용한다</p>
<ol>
<li>동등 비교 (=)</li>
<li>범위검색 (BETWEEN, &gt;, &lt;, &gt;=, LIKE ) 등</li>
<li>ORDER BY를 통한 정렬 작업</li>
</ol>
<p>✅ 동등 비교
인덱스를 만든 후 <code>EXPLAIN</code> 로 동등 조건을 비교하면 
<code>type</code> 이 <code>ref</code> 로 바뀐것을 확인할 수 있다</p>
<pre><code class="language-sql">CREATE INDEX idx_items_item_name ON items (item_name); // 상품이름 대상의 인덱스 생성
EXPLAIN SELECT * FROM items WHERE item_name = &#39;게이밍 노트북&#39;; // 쿼리 실행 계획을 확인</code></pre>
<p>위 쿼리실행 계획을 조회하면 아래와 같은 사진이 나온다</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/b6ad6eeb-6408-4c5b-b28a-d77157940994/image.png" alt="">우선 <code>type</code> 은 <code>ref</code> 로 바뀌었으며, <code>key</code> 는 NULL이 아닌 생성한 인덱스 명으로 바뀌었고 
<code>possible_keys</code> 는 현재 쿼리에서 사용한 모든 인덱스의 후보가 나오지만 현재 하나이므로 
그것만 나오는 상태이다. <code>rows</code> 는 인덱스를 사용한 결과 1개로 줄어든것을 볼 수 있다
<code>filterd</code> 는 100%로 인덱스로 찾는 1개의 행의 100%이므로 1개가 조회될것을 예측한다
<code>EXTRA</code> 는  인덱스를 사용해서 이제 데이터를 가져온 후 추후의 작업이 필요없으므로 NULL로 예측</p>
</blockquote>
<p>✅ 범위 검색
인덱스를 만든 후 <code>EXPLAIN</code> 로 범위검색을 비교하면 
<code>type</code> 이 <code>range</code> 로 바뀔 것이다</p>
<pre><code class="language-sql">CREATE INDEX idx_items_price ON items (price); // 가격 컬럼 대상의 인덱스 생성
EXPLAIN SELECT * FROM items WHERE price BETWEEN 50000 AND 100000; // 쿼리 실행 계획을 확인</code></pre>
<p>위 쿼리실행 계획을 조회하면 아래와 같은 사진이 나온다</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/60baa238-48b4-4c7d-a65c-eef0410a99be/image.png" alt="">우선 <code>type</code> 은 <code>range</code> 로 바뀌었으며, <code>key</code> 는 NULL이 아닌 생성한 인덱스 명으로 바뀌었고 
<code>possible_keys</code> 는 현재 쿼리에서 사용한 모든 인덱스의 후보가 나오지만 현재 하나이므로 
그것만 나오는 상태이다. <code>rows</code> 는 인덱스를 사용한 결과 5개로 줄어든것을 볼 수 있다
<code>filterd</code> 는 100%로 인덱스로 찾는 5개의 행의 100%이므로 5개가 조회될것을 예측한다
<code>EXTRA</code> 는  인덱스를 사용해서 대략적인 범위만 찾고, 이후 <code>WHERE</code> 조건을 사용했다는 뜻
즉 인덱스로 조건을 미리 걸러낸 후 <code>WHERE</code> 을 사용했다는 것이다</p>
</blockquote>
<p>💡 범위검색의 작동순서는 어떻게 되는걸까?</p>
<blockquote>
<p>현재 조건이 5만이상 10만이하의 조건이므로 우선 가격이 5만이상인 조건을 찾는다
그리고 차례대로 5만원이상의 데이터를 찾다가, 10만원 초과의 데이터가 발견되면 탐색을 종료한다 가격이 정렬되어 있기 때문에 가능하다<img src="https://velog.velcdn.com/images/choi-ju-yung/post/c2ba8130-ca83-4f37-9c2b-a6652bf24420/image.png" alt=""></p>
</blockquote>
<p>✅ LIKE 범위 검색
우선 LIKE 의 경우 와일드카드(%) 가 앞에붙은 경우는 사용할 수 없다
<code>&#39;%게이밍&#39;</code> (X)
<code>&#39;게이밍%&#39;</code> (O)
<code>&#39;%게이밍%&#39;</code> (X)
LIKE도 범위이므로 <code>type</code> 이 <code>range</code> 로 바뀔것이다</p>
<pre><code class="language-sql">// 위에서 이미 인덱스 생성해서 생략
EXPLAIN SELECT * FROM items WHERE item_name LIKE &#39;게이밍%&#39;; // 쿼리 실행 계획을 확인</code></pre>
<p>위 쿼리실행 계획을 조회하면 아래와 같은 사진이 나온다</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/892f032e-6284-4653-a7e9-e0a0f1877a70/image.png" alt="">여기에 대한 설명은 위에서 설명한 범위검색과 동일하다</p>
</blockquote>
<p>💡  LIKE 범위 검색 또한 다음과 같이 동작한다</p>
<blockquote>
<p>현재 글자가 국어사전 순으로 정렬되어 있으므로, <code>게이밍</code> 으로 시작하는 조건을 찾는다.
이렇게 찾다가 <code>게이밍</code> 으로 시작하지 않는 고급 가죽 지갑이 나오자마자 탐색을 종료한다<img src="https://velog.velcdn.com/images/choi-ju-yung/post/5957bef3-4b38-4dfc-b272-ba3cdc718f69/image.png" alt=""></p>
</blockquote>
<p>💡  그러면 와일드카드가 앞에붙은 경우의 LIKE는 어떻게 동작할까?
<code>SELECT * FROM items WHERE item_name LIKE &#39;%게이밍%&#39;;</code></p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/652b05e6-f3eb-497a-bc15-fd1eb83c2a7e/image.png" alt="">풀테이블 스캔이 되는것을 확인할 수 있다. 그 이유는 국어사전에서 중간에 <code>게이밍</code> 이라는 단어를 찾는것 자체가 쉽지 않기 때문이다. 시작글자를 알 수 없기 때문에 정렬된 인덱스가 도움이되지 않는다.</p>
</blockquote>
<p>그러면 어떻게 해결해야할까?
실무에서는 데이터가 많아질수록 <code>LIKE &#39;%검색어%&#39;</code> 방식은 사용하기 어렵다
이러한 문제를 해결하려면 전문 검색 <code>Full-Text Search</code> 를 사용해야 한다
이 방식은 텍스트를 단어(토큰) 단위로 쪼개서 인덱싱하는 방식이다
이러한 비슷한 방식으로 <code>ElasticSearch</code> 가 있다</p>
<hr>
<p>✅ 정렬
데이터베이스에서 <code>ORDER BY</code> 는 생각보다 많은 처리가 필요한 작업이다
-&gt; 테이블을 다 조회한 후 정렬작업까지 하기 때문에
-&gt; 인덱스를 사용하지 않고 ORDER BY 를 할 때, <code>filesort</code> 라는 정렬 작업을 한다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/9e05d68d-3b68-4c62-b08a-0a34919c6b36/image.png" alt=""></p>
<p>하지만 <code>인덱스</code> 를 잘 활용하면 이 <code>filesort</code> 작업을 생략할 수 있다
이미 정렬된 인덱스를 순서대로 읽기만 하면 되기 때문이다</p>
<p><code>WHERE</code> 절의 조건과 <code>ORDER BY</code> 절의 정렬 기준이 같을 때
인덱스 하나로 검색과 정렬을 모두 해결할 수 있다</p>
<pre><code class="language-sql">CREATE INDEX idx_items_price ON items (price); // 가격을 대상으로 인덱스 생성

EXPLAIN SELECT * FROM items WHERE price BETWEEN 50000 AND 100000
ORDER BY price; // 조건문에 대한 컬럼과 정렬 대상 기준컬럼이 같음 (price)</code></pre>
<p>위처럼 <code>인덱스</code> 를 사용해서 실행 계획을 분석해보면, <code>filesort</code>  작업이 없어졌을 것이다
그 이유는 위에서 만든 인덱스에서 이미 price(가격) 순서로 정렬을 했기 때문이다</p>
<p>그러면 이번엔 오름차순이 아닌 내림차순(DESC) 정렬을 사용하면 <code>filesort</code> 작업이 발생할까?
데이터베이스 옵티마이저는 인덱스를 거꾸로 읽는 <code>역방향 스캔</code> 을 할 수 있어서
내림차순 또한 <code>filesort</code> 없이 효율적인 처리가 가능하다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/d97139fc-a6cd-41ce-9d7a-ca18e21f256f/image.png" alt=""></p>
<pre><code class="language-sql">-- price 컬럼에 내림차순 인덱스 생성
CREATE INDEX idx_items_price_desc ON items (price DESC);</code></pre>
<p>위와 같이 사실 내림차순 인덱스를 생성할 필요 없다 
그냥 price 컬럼을 대상으로 인덱스를 생성하면 
price로 내림차순을 하든 오름차순을 하든  둘 다 인덱스로 처리된다 (역방향 스캔효과)</p>
<hr>
<p>✅ 옵티마이저의 선택</p>
<p><code>옵티마이저</code> 는 인덱스를 사용하는 것이 비효율적이라고 판단하면, 인덱스가 존재하더라도
과감히 포기하고 <code>풀 테이블 스캔</code> 방법을 선택한다</p>
<p>그 이유는 인덱스를 사용하면 검색 대상의 양은 줄어들지만, 여러위치에 흩어진 데이터에 접근해야 하기 때문이다</p>
<p>일반적으로 전체 데이터의 20~25% 이상을 조회해야하는 쿼리는 인덱스를 통해
테이블의 각 행에 개별적으로 접근하는 것 보다, 테이블 전체를 순차적으로 스캔하는것이 더 효율적이라고 알려져 있다.</p>
<p>그러면 왜 흩어진 데이터를 접근할 때 시간이 오래걸릴까?
EX) 책에서 5, 25, 400 페이지를 접근할 때 5페이지를 갔다가, 다시 25페이지를 갔다가, 다시 400페이지를 가야한다. 그러면 데이터를 읽을 때 마다 물리적으로 해당 위치까지 이동해야하기 때문이다
이렇게 데이터의 위치를 찾는데 걸리는 시간(탐색 시간)이 추가되기 때문이다</p>
<hr>
<p>위에서 데이터 파일에 접근하는데 시간이 걸린다는 점에서 문제점이 있었다
쿼리에서 필요한 결과값(컬럼) 을 포함하고 있는 인덱스를 만들면 해결이 될 것이다</p>
<p>✅ 커버링 인덱스 : 말 그대로 인덱스 하나로, 쿼리의 요구사항 전체를 덮는다는 의미</p>
<p>한마디로 SQL문을 실행할 때 필요한 모든 컬럼을 가지고 있는 인덱스이다</p>
<p>특정 인덱스가 <code>SELECT , WHERE , ORDER BY , GROUP BY</code> 절에
사용되는 모든 컬럼을 가지고 있다면, 원본 테이블에 전혀 접근하지 않고 
오직 인덱스에 접근하여 읽어서 쿼리를 처리한다</p>
<p>예를들어 현재 items 테이블에 price 컬럼에 인덱스가 있다고 가정하자
이상태에서
<code>EXPLAIN SELECT item_id, price, item_name FROM items WHERE price BETWEEN 50000
AND 100000;</code> 
쿼리를 실행하면 아래와 같이 나온다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/a8f782a6-a07a-4779-bf16-3772e4b1d471/image.png" alt="">
여기서 <code>Using index condition</code> 은 WHERE 조건절을 필터링하는 데 인덱스를 효율적으로 사용했지만, 
최종 데이터를 가져오기 위해서는 추가 작업이 필요하다는 뜻이다</p>
<p>즉 <code>item_name</code> 컬럼이 인덱스에 포함되어 있지 않기 때문에 
<code>item_name</code> 컬럼을 가져오기 위해서 원본 데이터를 접근해야한다</p>
<p>그러면 인덱스에 다 포함되어 있는 컬럼값만 조회하면 어떤 결과가 나올까?
<code>EXPLAIN SELECT item_id, price FROM items WHERE price BETWEEN 50000 AND 100000;</code>
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/1914e559-1c21-4d90-a503-2a832556f2fd/image.png" alt=""></p>
<p>다음과 같이 <code>Using index</code> 는 쿼리에 필요한 모든 데이터를 오직 인덱스에서만 읽어서
처리했음을 의미한다. 옵티마이저가 인덱스만 스캔하여 price, item_id 를 얻었고
items 테이블에는 접근할 필요가 없다</p>
<p>결론적으로 이 실행계획은 <code>커버링 인덱스</code> 를 활용하여 테이블 접근을 피했고, 인덱스 내에서 
WHERE 절의 조건으로 필터링을 수행한 훌륭한 쿼리이다 
(WHERE 절의 조건 = price = 인덱스에 있는 컬럼)</p>
<p>그러면 item_name 을 포함한 쿼리는 어떻게 해야할까?
다음과 같이 복합 인덱스를 만들면된다
<code>CREATE INDEX idx_items_price_name ON items (price, item_name);</code></p>
<blockquote>
<p>💡  컬럼이 여러 개인 복합 인덱스에서 컬럼의 순서는 매우 중요하다. WHERE 절에서 동등 비교나 범위 검색에 사용되는 컬럼을 가장 앞에 두어야 인덱스를 효율적으로 사용할 수 있다</p>
</blockquote>
<p>커버링 인덱스의 장단점</p>
<p> 장점</p>
<ul>
<li>압도적인 SELECT 성능 향상: 테이블 접근을 위한 랜덤 I/O를 제거하여 조회 성능을 극적으로 개선한다.<ul>
<li>특히 COUNT 쿼리 최적화: SELECT COUNT(*) 와 같은 쿼리에서 테이블 전체가 아닌, 크기가 훨씬 작은
인덱스만 스캔하여 결과를 빠르게 반환할 수 있다.</li>
</ul>
</li>
</ul>
<p>단점</p>
<ul>
<li>저장 공간 증가: 인덱스는 원본 데이터와 별도의 저장 공간을 차지한다. 인덱스에 포함되는 컬럼이 많아질수록 인덱스의 크기도 커진다.</li>
<li>쓰기 성능 저하: INSERT , UPDATE , DELETE 작업 시, 테이블 데이터뿐만 아니라 인덱스도 함께 수정해야 한다. 인덱스가 많고 복잡할수록 쓰기 작업에 대한 부하가 커진다.</li>
</ul>
<p><code>커버링 인덱스</code> 는 조회(읽기)가 빈번하고, 쓰기 작업인 적은 테이블에 유리하며
조회하는 컬럼의 개수가 적을 때 유리하다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[View]]></title>
            <link>https://velog.io/@choi-ju-yung/View</link>
            <guid>https://velog.io/@choi-ju-yung/View</guid>
            <pubDate>Mon, 29 Sep 2025 16:59:16 GMT</pubDate>
            <description><![CDATA[<p>💡  <code>View</code> 를 왜사용할까?</p>
<p>다른 부서에서 많이 사용하는 유용한 긴 쿼리가 있는데,이 복잡한 쿼리 자체를
데이터베이스에 하나의 &#39;바로가기&#39; 처럼 저장해두고, 필요할때마다 간단한 이름으로 호출하는 방법</p>
<p>왜 이렇게 사용할까?</p>
<ul>
<li>쿼리를 잘 모르는 사람들은 이 쿼리를 매번 정확하게 입력하기 힘들다.</li>
<li>로직이 변경되면 모든 사람들에게 해당 쿼리를 다 바꿔서 전송해야하는 불편한점이 있다.</li>
<li>개인정보와 같은 민감한 정보는 노출되지 않고, 필요한 내용만 보이게 해야한다</li>
<li><blockquote>
<p> 원본 테이블에 대한 접근권한을 주지 않고, 뷰를 통해서 제한된 컬럼에만 접근하게 할 수 있음</p>
</blockquote>
</li>
<li><blockquote>
<p>VIEW는 데이터베이스에서 <code>권한제어</code> 를 가능하게 함</p>
</blockquote>
</li>
</ul>
<p>✅ View</p>
<ul>
<li>실제 데이터를 가지고 있지 않은 <code>가상의 테이블</code> 이다</li>
<li>데이터베이스에 이름과 함께 저장된 하나의 <code>SELECT</code> 쿼리문</li>
<li>쉽게말해 바탕화면의 바로가기 아이콘이라고 생각하면 된다</li>
</ul>
<p>즉 <code>View</code> 는 데이터를 저장하는 테이블이 아니며, 단지 복잡한 <code>SELECT</code> 쿼리문 자체를 저장하고 있다</p>
<p>그러면 복잡한 쿼리 실행 없이
<code>SELECT * FROM 바로가기_뷰;</code>  라는 간단한 명령어로 복잡한 쿼리의 결과를 얻을 수 있다
사용자는 마치 그냥 테이블을 조회한 것처럼 느껴진다</p>
<hr>
<p>✅ VIEW 생성 방법
<code>CREATE VIEW 뷰이름 AS SELECT 쿼리문;</code></p>
<pre><code class="language-sql">// ex)
create view v_category_count as 
select u.name, count(o.order_id) as &#39;total_orders&#39;,
    count(case when category = &#39;전자기기&#39; then 1 end) as &#39;elcetronic_orders&#39;,
    count(case when category = &#39;도서&#39; then 1 end) as &#39;book_orders&#39;,
    count(case when category = &#39;패션&#39; then 1 end) as &#39;fasion_orders&#39;
  from users u
  left join orders o 
  on u.user_id = o.user_id
  left  join products p
  on o.product_id = p.product_id
group by u.name;</code></pre>
<p>이 쿼리를 실행하면 <code>v_category_count</code> 라는 view 객체가 생성된다</p>
<p>✅ VIEW 조회 방법
위에서 만든 view 객체를 조회하는 방법은 간단한다
<code>SELECT * FROM v_category_count;</code></p>
<p>✅ VIEW 수정 방법
<code>ALTER VIEW</code> 를 사용하고 뒤에 <code>SELECT</code> 문만 바꿔주면 된다
💡 View 이름은 그대로 둬야함</p>
<pre><code class="language-sql">alter view v_category_count as  // alert view 이름은 그대로
select u.name, count(o.order_id) as &#39;total_orders&#39;, 
    count(case when category = &#39;전자기기&#39; then 1 end) as &#39;elcetronic_orders&#39;,
    count(case when category = &#39;도서&#39; then 1 end) as &#39;book_orders&#39;,
    count(case when category = &#39;패션&#39; then 1 end) as &#39;fasion_orders&#39;,
    1 as &#39;alert_view&#39; // 하나의 컬럼 추가
  from users u
  left join orders o 
  on u.user_id = o.user_id
  left  join products p
  on o.product_id = p.product_id
group by u.name;</code></pre>
<p>✅ VIEW 삭제 방법
<code>DROP VIEW</code> 로 삭제가 가능
<code>DROP VIEW v_category_count;</code></p>
<hr>
<p>실무에서는 VIEW를 사용하다보면  장단점이 있다
장점은 VIEW를 사용하는 이유에 대해서 알것이고
단점은 VIEW 안에 쿼리가 실제로 크면 많은 성능이 있을수도 있으며
뷰를 중첩에서 사용하면, 성능저하의 원인이 될 수 있다</p>
<p>VIEW는 기본적으로 <code>조회용</code> 생각하는것이 일반적인 원칙이다</p>
<p>VIEW에도 데이터를 수정 삽입할 수 있지만
<code>JOIN</code> <code>집계함수</code> <code>GROUP BY</code> <code>DISTINCT</code> 등 을 사용한 복잡한 뷰는 일반적으로
INSERT, DELETE, UPDATE 가 불가하다</p>
<p>데이터를 수정하는 일이 필요하다면, 뷰가 아닌 원본테이블에서 수행해야 한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CASE 문]]></title>
            <link>https://velog.io/@choi-ju-yung/CASE-%EB%AC%B8</link>
            <guid>https://velog.io/@choi-ju-yung/CASE-%EB%AC%B8</guid>
            <pubDate>Thu, 25 Sep 2025 17:22:28 GMT</pubDate>
            <description><![CDATA[<p>✅ CASE</p>
<ul>
<li>데이터 자체를 동적으로 가공하고 새로운 의미를 부여</li>
<li><code>IF ELSE THEN</code> 처럼, 특정조건에 따라 다른 값을 출력하게 하는 SQL의 강력한 조건부 로직</li>
<li>원본 데이터는 건드리지 않고, 비즈니스 로직에 따라 새로운 값을 동적으로 생성</li>
</ul>
<p><code>WHEN</code> 조건들 중에서 어느것도 하나도 참이 아닌 경우, <code>ELSE</code> 뒤의 결과를 반환
만약 <code>ELSE</code> 를 생략했는데, 모든 <code>WHEN</code> 조건이 거짓이면 <code>NULL</code> 이 반환됨</p>
<p>💡 위에서 아래 순서로 조건을 평가하며, 먼저 참이되는 <code>WHERE</code> 절을 만나면
<code>THEN</code>의 결과를 반환하고 다른 조건은 보지 않는다</p>
<p>CASE 문의 문법은 다음과 같다 </p>
<pre><code class="language-sql">// 문법  (CASE WHEN ELSE END)
CASE 비교대상_컬럼_또는_표현식
     WHEN 값1 THEN 결과1
     WHEN 값2 THEN 결과2
     ...
     ELSE 그_외의_경우_결과
END</code></pre>
<p><code>CASE</code> 문은 크게 <code>단순 CASE 문</code> 과 <code>검색 CASE 문</code> 으로 나뉜다
<code>단순 CASE문</code> : 표현식의 값에 따라 결과를 다르게 하고 싶을 때 사용</p>
<pre><code class="language-sql">select status , 
        case 
            when status = &#39;COMPLETED&#39; then &#39;완료&#39;
            when status = &#39;SHIPPED&#39; then &#39;배송중&#39;
            when status = &#39;PENDING&#39; then &#39;대기&#39;
            else &#39;알수없음&#39;
        end as status_korean // 별칭부여
from orders;</code></pre>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/0c569999-2b31-4bc8-aa64-88148a9b5ab6/image.png" alt=""></p>
<p><code>검색 CASE문</code> : <code>WHEN</code> 절에 독립적인 조건식을 사용하여 복 잡한 논리를 구현할 때 사용
비교 연산자와 논리 연산자를 사용할 수 있다 (AND OR NOT)</p>
<pre><code class="language-sql">SELECT
     name,
    price,
     CASE
         WHEN price &gt;= 100000 THEN &#39;고가&#39;
         WHEN price &gt;= 30000 THEN &#39;중가&#39;
         ELSE &#39;저가&#39;
     END AS price_label
FROM products;</code></pre>
<hr>
<p><code>CASE</code> 문은 <code>SELECT</code> 절 외에도 <code>ORDER BY</code> <code>GROUP BY</code> <code>WHERE</code> 등 다양한 곳에서 사용할 수 있다
만약 상품을 &#39;고가&#39; &#39;중가&#39; &#39;저가&#39; 순으로 정렬하고 싶다고 가정해보자</p>
<pre><code class="language-sql">SELECT
     name,
     price,
     CASE
         WHEN price &gt;= 100000 THEN &#39;고가&#39;
         WHEN price &gt;= 30000 THEN &#39;중가&#39;
         ELSE &#39;저가&#39;
     END AS price_label
FROM products
ORDER BY
     CASE
        WHEN price &gt;= 100000 THEN 1 -- 고가: 1
        WHEN price &gt;= 30000 THEN 2 -- 중가: 2
        ELSE 3 -- 저가: 3
     END ASC, -- 숫자가 작은 순서대로 정렬 (1순위 정렬 기준)
    price DESC; -- 같은 등급 내에서는 가격 내림차순 (2순위 정렬 기준)</code></pre>
<p>이번엔 <code>CASE</code> 문을 <code>GROUP BY</code> 에 넣은 예시를 확인하자
<code>고객들을 출생 연대에 따라 &#39;1990년대생&#39;, &#39;1980년대생&#39;, &#39;그 이전 출생&#39;으로 분류하고,
각 그룹에 고객이 총 몇 명씩 있는 조회</code> 라는 문제가 있다고 가정하자</p>
<p>문제를 해결하면 <code>ORDER BY</code> 에서 해결한 것처럼 <code>GROUP BY</code> 뒤에 CASE문을 그대로 넣어줘야한다</p>
<pre><code class="language-sql">SELECT
     CASE
         WHEN YEAR(birth_date) &gt;= 1990 THEN &#39;1990년대생&#39;
         WHEN YEAR(birth_date) &gt;= 1980 THEN &#39;1980년대생&#39;
         ELSE &#39;그 이전 출생&#39;
      END AS birth_decade,
      COUNT(*) AS customer_count
FROM users
GROUP BY
     CASE
         WHEN YEAR(birth_date) &gt;= 1990 THEN &#39;1990년대생&#39;
         WHEN YEAR(birth_date) &gt;= 1980 THEN &#39;1980년대생&#39;
         ELSE &#39;그 이전 출생&#39;
   END;</code></pre>
<p>하지만 CASE문으로 조회한 결과값의 별칭을  GROUP BY 뒤에 넣어줘도 그룹핑이 된다
원래의 SQL 표준 논리적 실행순서에 따르면 <code>GROUP BY</code> 절이 <code>SELECT</code> 절보다 먼저 처리되기 때문에</p>
<p>CASE문으로  조회한 결과값 (birth_decade) 을 <code>GROUP BY</code> 절에 사용할 수 없지만
편의를 위해서 많은 데이터베이스들이 이러한 별칭 사용은 예외적으로 허용하게 해놨다</p>
<pre><code class="language-sql">SELECT
     CASE
         WHEN YEAR(birth_date) &gt;= 1990 THEN &#39;1990년대생&#39;
         WHEN YEAR(birth_date) &gt;= 1980 THEN &#39;1980년대생&#39;
         ELSE &#39;그 이전 출생&#39;
      END AS birth_decade,
      COUNT(*) AS customer_count
FROM users
GROUP BY birth_decade</code></pre>
<hr>
<p>✅ 조건부 집계</p>
<ul>
<li><code>CASE</code> 문이 집계함수 (SUM, COUNT) 등의 안으로 들어가는 방법</li>
</ul>
<p>EX) <code>하나의 쿼리로, 전체 주문 건수와 함께 &#39;결제 완료(COMPLETED)&#39;, &#39;배송(SHIPPED)&#39;, &#39;주문 대기(PENDING)&#39; 상태의 주문이 각각 몇 건인지 별도의 컬럼으로 나누기</code></p>
<p>이 문제는 두가지 방법으로 해결할 수 있다</p>
<ol>
<li><code>COUNT</code> 안에 <code>CASE</code> 문 작성하기<pre><code class="language-sql">SELECT
  COUNT(*) AS total_orders,
  COUNT(CASE WHEN status = &#39;COMPLETED&#39; THEN 1 END) AS completed_count,
 COUNT(CASE WHEN status = &#39;SHIPPED&#39; THEN 1 END) AS shipped_count,
 COUNT(CASE WHEN status = &#39;PENDING&#39; THEN 1 END) AS pending_count
FROM
orders</code></pre>
</li>
<li><code>SUM</code> 안에 <code>CASE</code> 문 작성하기<pre><code class="language-sql">SELECT
COUNT(*) AS total_orders,
  SUM(CASE WHEN status = &#39;COMPLETED&#39; THEN 1 ELSE 0 END) AS completed_count,
  SUM(CASE WHEN status = &#39;SHIPPED&#39; THEN 1 ELSE 0 END) AS shipped_count,
  SUM(CASE WHEN status = &#39;PENDING&#39; THEN 1 ELSE 0 END) AS pending_count
FROM
orders;</code></pre>
<code>COUNT</code> 에서 ELSE 0 을 안적은 이유는  <code>COUNT</code> 는 NULL 일 경우 세지 않음
즉 <code>WHEN</code> 이 거짓이면 NULL로 나와서 자동으로 세지 않음
하지만 ELSE 0을 적어버리면 0은 NULL이 아니기 때문에 세버림</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[UNION]]></title>
            <link>https://velog.io/@choi-ju-yung/UNION</link>
            <guid>https://velog.io/@choi-ju-yung/UNION</guid>
            <pubDate>Wed, 24 Sep 2025 16:41:29 GMT</pubDate>
            <description><![CDATA[<p>이전에 정리한 <code>JOIN</code> 과 <code>서브쿼리</code> 의 공통점은 기존 테이블의 정보를 조합하거나 필터링해서
원하는 형태의 하나의 결과 집합으로 만들어 낸다는 점이다</p>
<p> <code>JOIN</code> 은 테이블을 옆으로 이어붙여서 더 많은 정보들의 컬럼들을 만드는 기술이면
 이번에 정리할 <code>UNION</code> 은 여러개의 결과 집합을 아래로 이어 붙여서 더 많은 행을 가진
 하나의 집합으로 만드는 기술이다</p>
<p>✅ UNION</p>
<ul>
<li>흩어진 집합들을 하나로 만드는 기술 </li>
<li><code>JOIN</code> 과 달리 테이블들이 서로 연결된 관계가 아닐 때 사용</li>
</ul>
<p><code>UNION</code> 을 사용할 때는 다음과 같은 규칙이 있다</p>
<ul>
<li><code>UNION</code> 으로 연결되는 모든 <code>SELECT</code> 문은 컬럼의 개수가 동일해야한다 (컬럼이름은 달라도됨)</li>
<li>각 <code>SELECT</code> 문의 같은 위치에 있는 컬럼들은 서로 호환이 가능한 데이터 타입이어야 한다</li>
<li>최종 결과의 컬럼 이름은 첫 번째 SELECT 문의 컬럼 이름을 따른다</li>
<li>두 결과를 합친뒤 중복되는 행은 하나만 빼고 제거된다</li>
</ul>
<pre><code class="language-sql">SELECT name, email FROM users // 컬럼 개수가 동일하고 각 컬럼들이 동일한 데이터 타입임
UNION
SELECT name, email FROM retired_users;</code></pre>
<pre><code class="language-sql">select user_id ,email from users u // 컬럼이 달라도되고, 같은 데이터 타입이기만 하면 합치기 가능
union 
select id,name from retired_users ru;</code></pre>
<blockquote>
<p>아래 결과처럼 중복이 제거되는 것은 합치는 모든 컬럼이 동일할때만 제거된다
 1, <a href="mailto:Sean@example.com">Sean@example.com</a> -&gt; 두개가 중복되어서 제거됨
 <code>1, sean@example.com</code> 과 , <code>1, 션</code> 은 다른 행으로 인식되기 때문에 제거 X<img src="https://velog.velcdn.com/images/choi-ju-yung/post/f96c792d-086f-49c1-9665-275b11a0d8e9/image.png" alt=""></p>
</blockquote>
<hr>
<p>✅ UNION ALL</p>
<ul>
<li><code>UNION</code> 과 다르게 중복되는 행을 제거하지 않고 모두 이어붙임</li>
</ul>
<p><code>마케팅팀에서 두 종류의 고객에게 이벤트 안내 메일을 보내려고 한다. 첫 번째 그룹은 &#39;전자기기&#39; 카테고리의 상품을 구매한 이력이 있는 고객이고, 두 번째 그룹은 &#39;서울&#39;에 거주하는 고객이다. 두 그룹의 명단을 합쳐서 전체 발송 목록을 만든다 했을 때</code> 문제가 있다고 하자</p>
<p>위와 같은 비즈니스 요구사항에서는 <code>서울에 살면서 전가기기를 구매한 고객</code> 은 두 그룹에 모두 속하기 때문에 중복을 허용해야한다</p>
<pre><code class="language-sql">select u.name , u.email
  from users u
  join orders o 
    on u.user_id = o.user_id 
  join products p 
    on p.product_id = o.product_id 
 where p.category = &#39;전자기기&#39;
union all
select u.name, u.email 
  from users u
 where u.address like &#39;서울%&#39;;</code></pre>
<p>그럼 실무에서는 <code>UNION</code> vc <code>UNION ALL</code> 중에 어떤것을 사용할까?</p>
<p><code>UNION ALL</code> 이 <code>UNION</code> 보다 속도가 빠르다 
그 이유는 <code>UNION</code> 은 데이터베이스 내부적으로 중복을 제거하기 위해 전체 결과를 정렬한 다음
인접한 행들을 비교하여 중복을 찾아내는 과정을 거친다
만약 데이터 양이 많다면 이 정렬과 비교 작업은 엄청난 성능 저하가 발생한다</p>
<p>따라서 중복을 제거해야하는 명확한 요구사항이 있을때만 <code>UNION</code> 을 사용하자
그 외의 모든 경우는 <code>UNION ALL</code> 을 우선적으로 사용하자</p>
<hr>
<p>✅ UNION 정렬</p>
<p><code>UNION</code> 과 <code>UNION ALL</code> 을 사용하여 행을 합칠 때 최종 결과 집합에 대해서 정렬이 가능하다</p>
<p>각 <code>SELECT</code> 문에 정렬 (ORDER BY) 를 사용할 수는 있지만, 예상과 다른 결과가 발생하거나 오류가 생긴다
그러므로 개별적인 정렬이아닌 합쳐진 최종 결과에 대해서 맨 마지막에 <code>ORDER BY</code> 를 사용하자</p>
<pre><code class="language-sql">SELECT name, email FROM users
UNION
SELECT name, email FROM retired_users 
ORDER BY name; -- 최종 결과에 대한 정렬</code></pre>
<p><code>UNION</code> 과 <code>UNION ALL</code> 을 사용하여 합치면 최종결과는 항상 첫번째 <code>SELECT</code> 문의 컬럼이름이나
별칭만을 사용할 수 있다</p>
<pre><code class="language-sql">SELECT name, email, created_at FROM users
UNION ALL
SELECT name, email, retired_date FROM retired_users 
ORDER BY retired_date; -- (x)

SELECT name, email, created_at FROM users 
UNION ALL // created_at 과 retired_date 와 같이 다른이름의 컬럼이 있으면 항상 추상적인 별칭으로 통일하자
SELECT name, email, retired_date FROM retired_users 
ORDER BY created_at; -- (o)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[서브쿼리]]></title>
            <link>https://velog.io/@choi-ju-yung/%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC</link>
            <guid>https://velog.io/@choi-ju-yung/%EC%84%9C%EB%B8%8C%EC%BF%BC%EB%A6%AC</guid>
            <pubDate>Mon, 22 Sep 2025 17:12:28 GMT</pubDate>
            <description><![CDATA[<p>✅ 서브쿼리</p>
<ul>
<li>하나의 <code>SQL 쿼리</code> 문 안에 포함된 또 다른 <code>SELECT 쿼리</code> 를 의미</li>
</ul>
<p><code>메인쿼리</code> : 최종적으로 결과를 보여주는 쿼리
<code>서브쿼리</code> : 메인쿼리 안에서 필요한 값을 먼저 계산·조회해 주는 쿼리</p>
<p>왜 서브쿼리를 사용할까?  예시를 보자</p>
<p>쇼핑몰에서 판매하는 상품들의 평균 가격보다 비싼 상품을 조회하라는 문제가 있다고 가정하자
이 문제를 해결하기 위해서는 이런식으로 해결할것이다</p>
<ol>
<li><code>AVG</code> 집계함수를 사용하여  평균을 구하기<pre><code class="language-sql">SELECT AVG(price) FROM products; </code></pre>
</li>
<li>나온 평균값을 WHERE 뒤에 조건을 넣기<pre><code class="language-sql">SELECT *
FROM products
WHERE price &gt; 평균값;</code></pre>
</li>
</ol>
<p>이렇게 두번의 쿼리를 통해 원하는 결과를 얻을 수 있지만
매번 이렇게 쿼리를 실행하면 번거롭고, 1단계와 2단계의 쿼리를 실행하는 순간에도 값이 변할 수 있어서
데이터를 잘못 조회할 수도 있다</p>
<p>이러한 이유로 하나의 작업 단위로 묶기 위해서 <code>서브쿼리</code> 를 사용한다</p>
<hr>
<p>✅ 서브쿼리의 종류</p>
<ul>
<li>단일행 서브쿼리 (스칼라 서브쿼리)</li>
<li>다중행 서브쿼리</li>
<li>다증컬럼 서브쿼리</li>
<li>상관 서브쿼리</li>
<li>SELECT 서브쿼리</li>
<li>테이블 서브쿼리</li>
</ul>
<hr>
<p>✅ 단일행 서브쿼리 (스칼라 서브쿼리)</p>
<ul>
<li>서브쿼리를 실행했을 때, 결과가 오직 <code>하나의 행과 컬럼</code> 으로 나오는 경우</li>
<li>결과가 하나의 값이기 때문에 비교 연산자 ( = &gt; &lt; &gt;= &lt;= &lt;&gt;) 과 함께 사용할 수 있다</li>
</ul>
<p>💡  <code>스칼라</code> : 수학용어로 &#39;단 하나의 값&#39; 을 의미</p>
<p>EX) 특정 주문을 (order_id = 1) 인 고객과 같은 도시에 사는 모든 고객을 찾기</p>
<p>우선 서브쿼리 없이 1,2 단계로 문제를 해결하기</p>
<ol>
<li>order_id 가 1인 고객의 도시 알아내기<pre><code class="language-sql">SELECT u.address
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id = 1; </code></pre>
</li>
<li>해당 도시에 사는 모든 고객 찾기<pre><code class="language-sql">SELECT name, address
FROM users
WHERE address = 고객의 도시;</code></pre>
</li>
</ol>
<p>위 단계처럼 두개의 단계를 서브쿼리로 해결할 수 있다</p>
<pre><code class="language-sql">SELECT name, address
FROM users
WHERE address = (SELECT u.address
                    FROM users u
                   INNER JOIN orders o ON u.user_id = o.user_id
                   WHERE o.order_id = 1);</code></pre>
<p>이처럼 서브쿼리를 사용하면 여러 번의 쿼리를 하나로 합쳐서 코드를 간결하게 하고
애플리케이션과 데이터베이스의 통신 횟수를 줄여 성능상 이점을 얻을 수 있다</p>
<p>하지만 서브쿼리의 결과가 반드시 하나의 행만 반환해야만 쿼리가 정상적으로 동작된다
만약 두개 이상의 행을 반환하는 서브쿼리라면?
<code>Subquery returns more than 1 row</code> 에러를 발생시키며 실행을 멈춘다</p>
<hr>
<p>✅ 다중행 서브쿼리</p>
<ul>
<li>서브쿼리의 결과가 <code>다중 행</code> <code>하나의 컬럼</code> 으로 반환될 때 사용</li>
<li>다중행 서브쿼리의 결과를 처리하기 위해서는 <code>=</code> 와 같은 단일 행 연산자가 아닌
목록을 다룰 수 있는 <code>IN</code> <code>ANY</code> <code>ALL</code> 을 사용한다</li>
</ul>
<p><code>IN</code> 은 목록에 포함된 값과 일치하는지 확인하는 연산자로 
가장 흔하게 사용되며, 특정 컬럼의 값이 목록 중 하나라도 일치하면 참을 반환
<code>IN</code> 의 예시는 위에 단행일 서브쿼리와 예시와 비슷함</p>
<p><code>ANY</code> : 목록의 일부 값과 비교
<code>ALL</code> : 목록의 모든 값과 비교
<code>ALL</code> 과 <code>ANY</code> 는 비교 연산자와 함께 사용되며, 서브쿼리가 반환한 여러 값들과 비교하는 역할을 함</p>
<p><code>&gt; ANY</code> : 서브쿼리가 반환한 여러 결과값 중 어느 하나보다만 크면 참 = 최소값보다 크면 참
<code>&gt; ALL</code> : 서브쿼리가 반환한 여러 결과값 모두보다 커야만 참 = 최대값보다 커야 참
<code>&lt; ANY</code> : 최대값보다 작으면 참
<code>&lt; ALL</code> : 최소값보다 작으면 참
<code>= ANY</code> : IN 과 완전히 동일한 의미 = 목록 중 어느 하나와 같으면 참</p>
<p>EX) 전자기기 카테고리의 어떤 상품보다도 비싼 상품을 찾기</p>
<pre><code class="language-sql">SELECT name, price
FROM products
WHERE price &gt; ANY (SELECT price FROM products WHERE category = &#39;전자기기&#39;);</code></pre>
<p>서브쿼리의 결과가 (75000, 12000, 35000, 28000) 의 가격 목록을 반환된다
즉 위 결과는 <code>WEHRE price &gt; 75000</code> 의 결과와 동일하다</p>
<p>실무에서는 <code>ANY</code> 와 <code>ALL</code> 보다는 <code>MIN</code> 과 <code>MAX</code> 를 더 많이 사용한다
코드를 더 이해하기 쉽기 때문이다</p>
<p>위 예시를 <code>MIN</code> 으로 사용하여 바꾸면</p>
<pre><code class="language-sql">SELECT name, price
FROM products
WHERE price &gt; (SELECT MIN(price) FROM products WHERE category = &#39;전자기기&#39;);</code></pre>
<hr>
<p>✅ 다중컬럼 서브쿼리</p>
<ul>
<li>서브쿼리의 조회 값 중 컬럼이 <code>두개</code> 이상이 포함되는 경우를 말한다</li>
<li>이 기법은 메인쿼리의 <code>WHERE</code> 절에서 여러 컬럼을 동시에 비교해야할 때 유용</li>
</ul>
<p>EX) 한 쇼핑몰의 고객이 주문한 주문ID가 있다 (order_id = 3)
이주문과 동일한 고객이면서 주문 처리 상태도 같은 모든 주문을 찾기</p>
<ol>
<li>비교 기준이 될 고객 ID와 주문상태를 조회<pre><code class="language-sql">SELECT user_id, status FROM orders WHERE order_id = 3; // 2개의 열(컬럼) 이 조회됨</code></pre>
</li>
<li>다중 컬럼 비교<pre><code class="language-sql">SELECT order_id, user_id, status, order_date
FROM orders
WHERE (user_id, status) = (SELECT user_id, status  // WHERE 절에 비교할 컬럼들을 괄호로 묶어서 비교
                         FROM orders
                         WHERE order_id = 3);</code></pre>
</li>
</ol>
<p>서브쿼리의 결과값이 (2, &#39;SHIPPED&#39;) 라고 조회됬을 때 위 결과는
<code>WHERE user_id = 2 AND status = &#39;SHIPPED&#39;</code> 와 논리적으로 같다.</p>
<p>위 예시는 하나의 행일 경우의 예시이며
두개이상의 행일 때는 <code>IN</code> 연사자를 사용하여 동일하게 처리하면 된다</p>
<hr>
<p>✅ 상관 서브쿼리</p>
<ul>
<li>서브쿼리를 독립적으로 실행할 수 없음</li>
<li>메인쿼리와 서브쿼리가 서로 &#39;연관&#39; 관계를 맺고 동작하는 서브쿼리</li>
</ul>
<p>💡 상관 : 메인쿼리와 서브쿼리가 서로 영향을 준다는 뜻</p>
<p>위에서 나왔던 서브쿼리들은 독립적으로 단 한번 실행된 후 그 결과를 메인쿼리에 사용했다
그 서브쿼리들을 <code>비상관</code> 서브쿼리라고 한다</p>
<p>하지만 <code>상관 서브쿼리</code> 는 다음과 같이 동작한다</p>
<ol>
<li>메인쿼리가 한 행을 읽는다</li>
<li>읽혀진 행의 값을 서브쿼리에 전달하여, 서브쿼리가 실행된다</li>
<li>서브쿼리 결과를 이용해 메인쿼리의 WHERE 조건을 판단한다</li>
<li>메인쿼리가 다음 행을 읽고, 그 과정을 반복한다</li>
</ol>
<p>즉 서브쿼리가 메인쿼리의 행 수만큼 반복 실행된다
<br>
아래 예제를 보면서 확인하자
<code>각 상품별로, 자신이 속한 카테고리의 평균 가격 이상의 상품들을 찾기</code></p>
<p>이 문제의 핵심은 
전체 평균 가격이 아닌, 자신이 속한 바로 카테고리의 평균 가격과 비교해야 한다는 점이다</p>
<p>이처럼 서브쿼리가 메인쿼리에서 처리중인 특정 값을 알아야한 계산을 수행할 수 있을 때
<code>상관서브쿼리</code> 를 사용해야 한다</p>
<p>정리하면 메인쿼리의 각 행마다 다음 서브쿼리를 실행해야 한다
{catrgory} 값은 메인쿼리의 각 행마다 다른 category 값을 사용해야한다
<code>SELECT AVG(price) FROM products WHERE category = {category}</code></p>
<p>위 문제를 해결하면 </p>
<pre><code class="language-sql">select product_id,
       name,
       category,
       price
  from
    products p1
 where price &gt;= (select AVG(price)
                   from products p2
                  where p2.category = p1.category);</code></pre>
<p>여기서 가장 중요한 부분은 <code>WHERE p2.category = p1.category</code> 이다
서브쿼리에서 비교하는 부분을 메인쿼리의 <code>p1.category</code> 를 대상으로 하고 있다
이부분이 바로 <code>연관</code> 의 핵심이다</p>
<pre><code>p1 한행 읽음
   ↓
그 행의 category 확인
   ↓
같은 category 평균 가격 구함
   ↓
p1의 price &gt;= p1 카테고리의 평균가격 비교</code></pre><p>동작순서는 메인쿼리의 각 행을 수 만큼 서브쿼리를 반복하여
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/f0109702-1bf2-4e10-81a3-393c13e2fa11/image.png" alt="">
<code>WHERE 75000 &gt;= 206250</code>
<code>WHERE 120000 &gt;= 206250</code>
<code>WHERE 350000 &gt;= 206250</code>
<code>WHERE 28000 &gt;= 28000</code>
... 이런식으로 반복된다</p>
<p>하나의 문제를 더 풀어보자
<code>한번이라도 주문된 상품 조회하기</code> 
이 문제를 일반 서브쿼리 문제로 해결하면 아래와 같이 해결이 가능하다</p>
<pre><code class="language-sql">select * from products p 
 where p.product_id in (select product_id  // IN으로 해결한 방법
                            from orders o
                         where order_id is not null);</code></pre>
<p>이렇게 IN 방식으로 문제를 해결하면 원하는 결과를 직관적으로 얻을 수 있다
하지만 주문 테이블(orders) 테이블의 로우 개수가 수천만, 수억 건이 넘으면 성능상 문제가 생긴다
그럴 때는 <code>EXISTS</code> 를 사용하면 더 효율적이다</p>
<p><code>EXISTS</code> 는 서브쿼리 결과 행이 <code>1개 이상이면 TRUE</code> , <code>0개이면 FALSE</code> 가 된다
즉 <code>EXISTS</code> 는 서브쿼리가 반환하는 결과값 자체에는 관심이 없고 서브쿼리의 결과로
행이 하나라도 존재하는지의 여부만 체크한다</p>
<p>💡 반대로 특정 조건의 데이터가 존재하지 않는 것을 확인하고 싶을 때는 <code>NOT EXISTS</code> 를 사용</p>
<pre><code class="language-sql">select * 
  from products p 
 where exists ( // EXISTS 로 해결한 방법
                  select 1 
                    from orders o
                   where o.product_id = p.product_id); // 메인쿼리의 행 수만큼 서브쿼리를 반복</code></pre>
<p>위 방법은 <code>o.product_id = p.product_id 라는 조건</code>이 있으며, 서브쿼리가 독립적으로 실행되지않고
메인쿼리의 p 테이블의 값 (p.product_id) 에 의존하여 실행되고 있으므로 상관 서브쿼리 방식이다</p>
<p>서브쿼리의 <code>SELECT 1</code> 은 서브쿼리의 결과값이 의미가 없고, 행이 존재하는지 여부만 보기때문에
관례적으로 1 과 같은 상수를 사용해서 불필요한 데이터 조회를 피하는 것이 좋다</p>
<p>예를들어 product_id 가 1인 주문이 10000개가 존재해도, 1개만 먼저 발견하면 바로 <code>TRUE</code> 를 리턴하기 때문에 나머지 9999개를 찾지 않아도 되는 효율을 갖고 있다</p>
<p>그럼 실무에서의 <code>IN vs EXISTS</code> 중 언제 사용해야하나?
<code>IN</code> 의 경우 서브쿼리의 결과 데이터의 개수가 작을 때 사용하는 것이 좋고
<code>EXISTS</code> 의 경우 서브쿼리의 결과 데이터가 클때와 메인쿼리의 행 수가 적을 때 효율적이다</p>
<hr>
<p>✅ SELECT 서브쿼리</p>
<ul>
<li>서브쿼리를 SELECT 절에다 사용</li>
<li>서브쿼리는 필터용이 아닌, 그 자체가 하나의 컬럼처럼 동작</li>
<li>하나의 행과 하나의 컬럼을 반환하는 <code>스칼라 서브쿼리</code> 를 사용해야 한다</li>
</ul>
<p><code>SELECT 서브쿼리</code> 에서도 비상관, 상관 서브쿼리로 나눌 수 있다</p>
<p>우선 <code>비상관 서브쿼리</code> 의 경우
<code>모든 상품 목록을 조회하는데, 각 상품의 가격과 함께 전체 상품의 평균 가격을 모든 행에 함께 표시</code>
여기서 전체 상품의 평균 가격은 어떤 특정 상품 행에 종속되는것이 아니라, 모든 상품에 대해
동일하게 적용되는 고정 값이다</p>
<pre><code class="language-sql">SELECT
     name,
     price,
     (SELECT AVG(price) FROM products) AS avg_price // 모든행에 대한 고정값
  FROM products;</code></pre>
<p>이렇게 비상관 서브쿼리로 작성하는 경우 
데이터베이스는 메인서브쿼리를 실행하기 전에, <code>SELECT</code> 절의 서브쿼리를 단 한번 먼저 실행한다
그 값을 기억해뒀다가 메인쿼리를 실행하고 각 행을 가져올때마다 값들을 추가해준다</p>
<p>하지만 <code>상관 서브쿼리</code> 의 경우는 어떨까?
<code>전체 상품 목록을 조회하면서, 각 상품별로 총 몇 번의 주문이 있었는지 &#39;총 주문 횟수&#39; 표시</code>
이 문제는 각 행마다 주문 횟수 값이 달라져야 한다</p>
<pre><code class="language-sql">SELECT
     p.product_id,
     p.name,
     p.price,
     (SELECT COUNT(*) FROM orders o WHERE o.product_id = p.product_id) AS order_count
  FROM products p;</code></pre>
<p>여기서 <code>WHERE o.product_id = p.product_id</code> 이 부분은 메인쿼리 테이블인 p에 종속받고 있기 때문에
상관서브쿼리이다</p>
<p>위에서도 말했지만, 상관서브쿼리의 경우 메인 쿼리가 반환하는 행의 수만큼 반복 실행하기 때문에
메인쿼리의 행의 수가 많으면 성능저하가 일어날 수 있다</p>
<hr>
<p>지금까지 서브쿼리는 <code>WHERE</code> 절에서는 동적필터로 <code>SELECT</code> 절에는 새로운 컬럼으로 추가되는 것을
확인하였다. 이번에는 <code>FROM</code> 절에서 사용하는 방법을 알아본다</p>
<p>✅ 테이블 서브쿼리 (인라인 뷰)</p>
<ul>
<li><code>FROM</code> 절에 위치하는 서브쿼리로, 실행결과가 마치 하나의 독립된 가상 테이블처럼 사용됨</li>
<li><code>FROM</code> 절에 들어가는 서브쿼리는 반드시 별칭을 붙여줘야 활용이 가능하다</li>
<li>복잡한 <code>SELECT</code> 문의 결과를 하나의 명확한 데이터 집합으로 만들어놓고, 그 집합을 대상으로
다시 한번 <code>SELCET</code> 할 수 있게 해준다</li>
</ul>
<p>EX) <code>각 상품 카테고리별로, 가장 비싼 상품의 이름과 가격을 조회</code></p>
<pre><code class="language-sql">SELECT p.product_id,
        p.name,
       p.price
  FROM products p
  JOIN ( // 원본테이블과 가상테이블을 조인
          SELECT category, 
               MAX(price) AS max_price // 카테고리별 최고가격을 서브쿼리로 미리 구하기
           FROM products
          GROUP BY category) AS cmp // cmp 라는 임시 테이블을 메모리에 생성
    ON p.category = cmp.category AND p.price = cmp.max_price;</code></pre>
<p>위 쿼리의 동작방식은 다음과 같다
데이터베이스에서 <code>FROM</code> 절의 서브쿼리(인라인 뷰)를 먼저 실행 후, cmp라는 임시테이블을 메모리에 생성
그 다음 메인쿼리가 실행되면서, 상품테이블과 임시테이블을 조인하여
임시테이블의 값으로 필터를 할 수 있다</p>
<hr>
<p>그럼 실무에서의 <code>JOIN</code> 과 <code>서브쿼리</code> 중 어떤것을 사용해야 좋을까?
정확한 답은 없지만 이러한 가이드로 진행하자</p>
<ol>
<li><code>JOIN</code> 을 우선 고려</li>
</ol>
<p>일반적으로 데이터베이스는 <code>JOIN</code>이 <code>서브쿼리</code> 보다 성능이 더 좋거나, 최소한 동일한 경우가 많다</p>
<ol start="2">
<li><code>JOIN</code> 으로 표현하기 복잡하거나, <code>서브쿼리</code> 의 가독성이 더 좋을 땐 <code>서브쿼리</code> 로 사용</li>
</ol>
<p>성능이 중요하지 않은 쿼리의 경우, 동료가 이해하기 쉬운 코드를 작성하는것이 좋다</p>
<ol start="3">
<li><code>EXISTS</code> 를 활용</li>
</ol>
<p>서브쿼리를 활용할 때 <code>IN</code> 연산자 대신 <code>EXISTS</code> 를 사용하면 더 효율적으로 동작할 때가 있다</p>
<ol start="4">
<li>성능의 의심될 때는 측정하기</li>
</ol>
<p><code>JOIN</code> <code>서브쿼리</code> 등 여러 방법으로 쿼리를 작성해보고, <code>EXPLAIN</code> 과 같은 도구로 실행시간을 측정하자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[조인]]></title>
            <link>https://velog.io/@choi-ju-yung/%EC%A1%B0%EC%9D%B8</link>
            <guid>https://velog.io/@choi-ju-yung/%EC%A1%B0%EC%9D%B8</guid>
            <pubDate>Sun, 21 Sep 2025 12:13:48 GMT</pubDate>
            <description><![CDATA[<p>✅ 조인이 필요한 이유</p>
<p>현재 테이블이 <code>users</code> <code>products</code> <code>orders</code>  (사용자, 상품, 주문) 테이블이 있다고 가정
여기서  최근 주문현황의 고객 이름과 상품명을 포함해서 보고서로 만들어달라는 요구사항이 생김</p>
<p>주문현황이기 때문에 <code>orders</code> 테이블을 조회했더니 아래와 같이 나온다</p>
<table>
<thead>
<tr>
<th>order_id</th>
<th align="left">user_id</th>
<th align="center">product_id</th>
<th align="center">order_date</th>
<th align="center">quantity</th>
<th align="right">status</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td align="left">1</td>
<td align="center">1</td>
<td align="center">2025-06-10</td>
<td align="center">10:00:00</td>
<td align="right">1</td>
</tr>
<tr>
<td>1</td>
<td align="left">1</td>
<td align="center">4</td>
<td align="center">2025-06-10</td>
<td align="center">10:05:00</td>
<td align="right">2</td>
</tr>
</tbody></table>
<p>하지만 위 표에서는 고객 이름과 상품명이 없다
user_id 가 1인 고객이 누구인지, product_id 가 1인 상품이 무엇인지 알 수 없다</p>
<p>그러면 <code>orders</code> 테이블 안에 모든 데이터를 저장하면 되는것이 아닌가?</p>
<table>
<thead>
<tr>
<th>order_id</th>
<th align="left">order_date</th>
<th align="center">user_name</th>
<th align="center">user_email</th>
<th align="left">product_name</th>
<th align="center">price</th>
<th align="right">데이터 많아서 생략...</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td align="left">2025-06-10</td>
<td align="center">션</td>
<td align="center">sean@...</td>
<td align="left">삼성키보드</td>
<td align="center">1</td>
<td align="right">데이터 많아서 생략...</td>
</tr>
</tbody></table>
<p>이런식으로 하면 당장 조회할 때는 편하지만 많은 문제점이 있다</p>
<p><code>1</code>  : 데이터 중복
션이라는 사람이 상품을 100번 주문하면  그 사람의 이메일과 이름 등이 100번이나 불필요하게 
반복 저장된다.</p>
<p><code>2</code> : 갱신 이상
션이라는 사람이 이메일 주소를 변경하면,  션이 주문한 모든 데이터를 모두 찾아서 
이메일 정보를 일일이 새로운 정보로 변경해야한다. 
하지만 만약 실수로 하나라도 누락된다면 주문마다 주소가 다르기 때문에 데이터의 일관성이 깨진다.</p>
<p><code>3</code> : 삽입 이상
쇼핑몰에 아직 아무도 주문하지 않은 노트북을 등록하려고 하는데, 위 테이블 구조에서는 
주문이 발생해야만 데이터를 추가할 수 있다. 하지만 주문한 사람이 없어서
상품 정보조차 등록할 수 없는 상황이 발생된다</p>
<p><code>4</code> : 삭제 이상
션이 주문한 기록 하나를 삭제한다고 가정하자. 만약 이 주문 기록을 삭제하면
션이라는 고객의 이름, 이메일, 주소 정보까지 삭제되는 현상이 발생한다</p>
<p>이러한 문제들 때문에 데이터베이스를 설계할 때 <code>정규화</code> 라는 과정을 거친다
💡 <code>정규화</code><br>-&gt; 데이터의 일관성을 해치는 &#39;이상 현상&#39;들을 방지하기 위해 데이터를 논리적인 단위로 분리하는 과정</p>
<p>위에서 <code>users</code> <code>products</code> <code>orders</code> 테이블로 나눈 것이 정규화의 예시이다
이렇게 분리한 테이블에서 흩어진 데이터에서 내가 필요한 데이터들을 다시 갖고오려면 (통합)
<code>조인</code> 을 사용해야한다.</p>
<hr>
<p>✅ 내부조인</p>
<p>내부 조인( INNER JOIN )은 두 테이블을 연결할 때
양쪽 테이블에 모두 <span style="color:indianred">공통</span>으로 존재하는 데이터만을 결과로 보여줌
<code>* (전체)</code> 로 조회하면  조인한 모든 테이블의 데이터들이 합쳐져서 모두 출력된다
<code>WHERE</code> 조건 뒤에 적을때 or 조회대상 컬럼은 컬럼 앞에 테이블명을 정확하게 명시해주는것이 좋다
ex) <code>WHERE P.PRODUCT_ID = 1</code> <code>SELECT O.ORDER_ID ....</code></p>
<ul>
<li><p>문법</p>
<pre><code class="language-sql">SELECT 컬럼1, 컬럼2, ...
FROM 테이블A // FROM :  기준이 되는 첫 번째 테이블을 지정
INNER JOIN 테이블B   // INNER JOIN : 연결할 두 번째 테이블을 지정
ON 테이블A.연결컬럼 = 테이블B.연결컬럼; // ON : 두 테이블을 어떤 조건으로 연결할지 명시하는 연결고리</code></pre>
</li>
<li><p>동작 순서</p>
</li>
</ul>
<p><code>JOIN</code> 을 통해 여러 테이블을 먼저 합친 가상의 테이블을 만든 후
<code>WHERE</code> 절의 조건에 따라 필요한 행을 걸러내고 최종적으로 원하는 필드를 <code>SELECT</code> 하는 순서로 동작
💡  <code>FROM/JOIN (테이블결합) -&gt; WHERE(조건 필터링) -&gt; SELECT (컬럼선택)</code></p>
<ul>
<li>결론</li>
</ul>
<p><code>내부 조인</code> 은 벤 다이어그램에서 둘의 겹친 영역 (내부영역) 인 교집합 영역을 의미한다
결국 두 테이블의 <code>교집합</code> 을 찾는 것과 같다
두 집합(테이블) 에서 연결 컬럼의 값이 일치하는 데이터만을 결과로 반환한다
A집합과 B집합에 모두 포함된 값이 해당하는 데이터만을 결합하여 보여줌
ex) orders 테이블 : {1,2,3,4,5}     users 테이블  : {1,2,3,4,5,6}</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/a192151b-701b-49e6-b800-e64d67e9f5b5/image.png" alt="">사진에서 보는 것처럼 users 테이블에는 6번 유저가 있지만, 한번도 주문한 적이 없어서
orders 테이블에는 6이 존재하지 않는다. 따라서 6번 유저는 INNER JOIN (내부조인) 결과에서 제외됨</p>
</blockquote>
<p>내부 조인은 <code>양방향</code> 이기 때문에 
A -&gt; B 조인하든, B -&gt; A로 조인하든 그 결과는 항상 동일하다</p>
<p>어떤 순서로든 조인해도 상관없지만 실무에서는 가독성을 높이기 위해서 다음과 같이 순서를 정한다
<code>주문 목록</code> 을 중심으로 고객 정보를 추가하고 싶다면 -&gt;  <code>FROM orders JOIN users</code>
<code>고객 목록</code> 을 중심으로 주문 정보를 조회하고 싶다면 -&gt; <code>FROM users JOIN orders</code>
즉 쿼리를 읽는 사람의 입장에서 어떤 데이터가 중심이 되는가에 따라서 정하면 된다</p>
<p>가독성을 높이려면 <code>테이블 별칭</code> 과 <code>컬럼 별칭</code> 을 사용하는 것이 좋다
실무에서는 <code>테이블 별칭</code> 은 AS를 생략하고, <code>컬럼 별칭</code> 은 AS를 써주는것이 좋다
INNER JOIN으로 풀네임으로 적지 않고 JOIN 으로만 작성해주는것이 편하고 좋다</p>
<pre><code class="language-sql">SELECT
 u.user_id AS u_user_id
 o.user_id AS o_user_id
 o.order_date
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id</code></pre>
<p>내부조인으로는 주문기록이 없는 고객을 찾을 수 없다 
그 이유는 내부조인은 양쪽 테이블에서 짝이 맞는 데이터들을 연결하기 때문이다</p>
<p>그럴때는 외부조인을 사용해야한다</p>
<hr>
<p>✅ 외부조인 (OUTER JOIN)</p>
<p>한쪽에만 데이터가 있고, 다른 한쪽에는 없는 데이터까지 모두 포함해서 보고싶을 때 외부조인을 사용
쉽게말해서 외부조인은 한쪽 테이블에만 존재하는 데이터를 결과에 포함시킬 수 있다</p>
<p>기준이 되는 테이블이 어느쪽이냐에 따라서 <code>LEFT OUTER JOIN</code> 과 <code>RIGHT OUTER JOIN</code> 으로 나눈다
교지합 영역 + 기준이 되는 테이블의 영역을 합친것이 결과에 포함된다</p>
<p>양쪽 모두를 사용하는 <code>풀 외부조인</code> 이라는 방법도 있는데, 실무에서는 잘 사용하지 않으며
UNION ALL를 사용하는 편이다
<code>풀 외부조인</code> 은 MYSQL 에서는 지원하지 않는다 </p>
<blockquote>
<p>✅ LEFT OUTER JOIN  과 RIGHT OUTER JOIN
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/bb670252-3865-42e3-9da3-9188669f6de1/image.png" alt=""></p>
</blockquote>
<p>사용할 때는 <code>OUTER</code> 명령어는 생략하는것을 권장한다
<code>LEFT OUTER JOIN</code> -&gt; <code>LEFT JOIN</code>
<code>RIGHT OUTER JOIN</code> -&gt; <code>OUTER JOIN</code></p>
<p><code>LEFT JOIN</code></p>
<ul>
<li><code>LEFT JOIN</code> 구문의 왼쪽(FROM 절)에 있는 테이블이 기준이 된다</li>
<li>왼쪽 테이블의 모든 데이터를 결과에 포함시킨다</li>
<li>ON 조건에 맞는 데이터를 오른쪽 테이블에서 찾아 옆에 붙여준다</li>
<li>오른쪽 테이블에 짝이맞는 데이터가 없으면, 그 자리는 NULL로 채워진다</li>
</ul>
<p><code>RIGHT JOIN</code> 은 위 방법과 반대로 동작한다</p>
<p>위에서 주문기록이 없는 고객을 찾을 때 내부조인으로 찾지 못했는데
OUTER JOIN으로는 찾을 수 있게되었다</p>
<pre><code class="language-sql">SELECT * 
  FROM users u
  LEFT JOIN orders o  // JOIN을 기준으로 왼쪽 테이블이 기준 (유저 테이블이 기준)
    ON u.user_id = o.user_id;</code></pre>
<p>아래 사진과 같이 레오나르도 다빈치는 주문기록이 없지만, USERS 테이블이 기준이기 때문에
모든 행들을 포함하고 주문기록이 없기 때문에 NULL로 채워져 있다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/a1bdb7f5-eb0e-4e46-bc82-cd50d58b7de3/image.png" alt=""></p>
<p>만약 위 예제를 RIGHT 조인을 사용하면 테이블의 위치만 변경해주면 동일한 결과가 생긴다
실무에서는 <code>LEFT JOIN</code> 이 <code>RIGHT JOIN</code> 보다 훨씬 더 많이 사용된다
그 이유는 분석의 기준이 되는 테이블을 먼저 사용하고 (사람은 왼쪽에서 -&gt; 오른쪽으로 글을 읽음)
필요한 정보를 담은 테이블들을 <code>LEFT JOIN</code> 으로 하나씩 붙여나가는 방식으로 쿼리를 작성하는 것이 직관적이기 때문이다
<br>
조인을 하다보면 행이 늘어날 때도 있는데, 각 테이블의 관계에 따라 다르다</p>
<p><code>행 개수 유지</code></p>
<ul>
<li>FK -&gt; PK 조인</li>
<li>FROM <code>자식테이블</code> JOIN <code>부모테이블</code></li>
<li>자식테이블의 모든 행은 부모 테이블의 단 하나의 행과 매칭됨</li>
<li>기준이 되는 자식 테이블의 행 개수가 그대로 유지 </li>
</ul>
<p><code>행 개수 증가 가능</code></p>
<ul>
<li>PK -&gt; FK 조인</li>
<li>FROM <code>부모테이블</code> JOIN <code>자식 테이블</code></li>
<li>부모 테이블의 한 행은 자식 테이블의 여러 행과 매칭될 수 있음</li>
<li>부모 행이 자식 행의 개수만큼 복제되면서 전체 행의 개수가 늘어날 수 있다</li>
</ul>
<hr>
<p>✅ 셀프조인</p>
<ul>
<li>하나의 테이블과 자기 자신을 조인하는 기법</li>
<li>INNER JOIN, OUTER JOIN 처럼  <code>JOIN</code> 기법의 명령어가 아님</li>
<li>테이블 별칭을 활용하여 자기 참조 관계를 풀어내는 기법</li>
</ul>
<p>EX) 조직도, (카테고리, 서브카테고리), 게시판 원본글과 답변글 같은 <code>계층형</code> 데이터를 다룰 때 사용함</p>
<p>다음 <code>EMPLOYEES</code> 테이블 예시를 보자
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/43252ffb-16d5-4201-b50d-31f5ceb61e19/image.png" alt=""></p>
<p>이 테이블에는 모든 직원의 정보가 들어있다. 각 직원의 상사를 찾으려고 하는데
각 직원의 상사도 직원이다. 이럴 경우 자기 자신의 테이블 조인하면 된다</p>
<pre><code class="language-sql">select e.name as &#39;직원이름&#39;,
       m.name as &#39;상사이름&#39;
  from employees e   // 하나의 테이블에 서로 다른 별칭을 두개 부여하여, 마치 두개의 테이블인것처럼 인식
  join employees m on e.manager_id = m.employee_id;</code></pre>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[ORA-12541 %s에 리스너가 없습니다]]></title>
            <link>https://velog.io/@choi-ju-yung/ORA-12541-s%EC%97%90-%EB%A6%AC%EC%8A%A4%EB%84%88%EA%B0%80-%EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@choi-ju-yung/ORA-12541-s%EC%97%90-%EB%A6%AC%EC%8A%A4%EB%84%88%EA%B0%80-%EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Wed, 03 Sep 2025 17:31:24 GMT</pubDate>
            <description><![CDATA[<p> ✅ 문제점 </p>
<p> 로컬환경에서 디비버를 연결하려고 하는데 접속할 수 없음</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/40231e91-f9a5-4504-94d9-596e8fe0da84/image.png" alt=""></p>
</blockquote>
<p>과거에 Oracle Database XE 설치를 저장 공간이 부족하여 D드라이브에 설치했었다
그래서 환경변수 경로도 꼬인 관계로 다시 처음부터 설치하는 방향으로 진행하였다</p>
<blockquote>
<p><code>1</code> : C 드라이브나 D 드라이브에 설치한 오라클 경로를 삭제하기
EX)  <code>C:\app\사용자계정\product\버전</code> 
EX) <code>D:\app\사용자계정\product\버전</code>
오라클을 설치하면 디폴트경로로 위 예시처럼 저장이 된다
그래서 <code>\app</code> 폴더를 통째로 삭제한다<img src="https://velog.velcdn.com/images/choi-ju-yung/post/70d2a909-d778-49ff-a4f6-669f13c4d213/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>2</code> : 제어판 → 프로그램 및 기능 → Oracle Database 21c XE 제거
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/80bae0bd-d2f3-48bf-9c97-db2fa74dc450/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>3</code> : cmd 접속 → services.msc 입력 -&gt; Oracle 관련 서비스 모두 제거됐는지 확인
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/aa7d351a-3a7f-4d99-80d6-361d11ee3655/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>4</code> : 윈도우 시작 -&gt; regedit (레지스트리 편집기) → HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE 삭제
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/8be2a554-2018-4001-a7e2-33e8df0d559c/image.png" alt=""></p>
</blockquote>
<blockquote>
<p><code>5</code> : 환경 변수 ORACLE_HOME, TNS_ADMIN, PATH 내 Oracle 관련 항목 제거
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/4455fd13-8340-4714-9dbc-45c013a0e9e5/image.png" alt=""></p>
</blockquote>
<p>이렇게 5가지 과정을 진행하면 남아 있는 오라클이 흔적이 없을것이다</p>
<hr>
<p>✅ 재설치 후 확인</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/38dc8462-4fdb-40cb-ab5a-50ac896b3e4e/image.png" alt="">위 사진처럼 다시 오라클을 설치한다</p>
</blockquote>
<p>설치가 다 완료되면 위에서 언급한 <code>C:\app\사용자계정\product\버전</code> 에 설치가 되있을 것이다
그러면 하위폴더로 <code>C:\app\bogdu\product\21c\homes\OraDB21Home1\network\admin</code> 에 가서
<code>lister.ora</code> <code>tnsnames.ora</code> 가 있으면 잘 설치된 것이다</p>
<p>이렇게 하고 디비버에서 다시 연결하려고하는데 동일한 리스너 문제가 발생했다..
CMD에서 lsnrctl status 명령어를 입력하여 리스너 상태를 확인해봤더니</p>
<pre><code>(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=220.74.13.47)(PORT=1521))) 
(DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(PIPENAME=\\.\pipe\EXTPROC1521ipc))) 
(DESCRIPTION=(ADDRESS=(PROTOCOL=tcps)(HOST=127.0.0.1)(PORT=5500))
(Security=(my_wallet_directory=C:\APP\BOGDU\PRODUCT\21C\admin\XE\xdb_wallet))
(Presentation=HTTP)(Session=RAW)) 서비스 요약...</code></pre><p>즉 HOST는 220.74.13.47 (공인 IP)로 잡혀 있기 때문이다</p>
<p>이렇게 된 이유는 오라클을 설치할 때, <code>네트워크 자동 설정</code> 이 활성화 된 상태에서
외부 IP를 자동 감지하여 리스너에 등록된 것 같다</p>
<p>자세하게는 모르겠고 수동으로 해결을 진행해보겠다</p>
<hr>
<p>✅ 해결 방법</p>
<blockquote>
<p>위에서 언급한 <code>listener.ora</code> 와 <code>tnsnames.ora</code>  안에 들어가면
HOST 주소가 다른 외부주소로 적혀 있는 경우가 있다 
이런 경우 <code>HOST = localhost</code> 또는 <code>HOST = 127.0.0.1</code> 로 바꿔주면 된다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/9def1d4f-e348-45ff-be31-da7eed2ac28a/image.png" alt=""><img src="https://velog.velcdn.com/images/choi-ju-yung/post/aeb6d759-e92f-477e-b1b4-5728e0cd493b/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>추가로 <code>C:\Windows\System32\drivers\etc\hosts</code> 로 들어가서
<code>127.0.0.1 localhost</code>  <code>::1 localhost</code> 이렇게 2개가 주석되어있으면 풀어준다
💡  참고 : hosts를 그냥 메모장에 연결해서 수정하면 권한이 없기 때문에 수정이 불가능
기본 메모장을 오른쪽 우클릭해서 관리자모드로 실행 -&gt; 상단 파일 클릭 -&gt; 열기 -&gt; hosts 선택
해서 하면 수정이 가능하다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/a56c9c33-8384-47b6-97e0-65fa7ed25a41/image.png" alt=""></p>
</blockquote>
<p>그 후 관리자 모드로  CMD를 들어가서
<code>lsnrctl stop</code> -&gt; <code>lsnrctl start</code> 순으로 입력한 후
컴퓨터를 재시작한 다음 <code>lsnrctl status</code> 를 해서 
HOST가 <code>127.0.0.1</code> 또는 <code>localhost</code> 로  바뀌었는지 확인한다</p>
<p>이렇게 진행하니 로컬 환경에서 디비가 잘 붙는다
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/4ea3252b-a25a-43c3-a439-86509cb5e54e/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자주 사용하는 검색 기능]]></title>
            <link>https://velog.io/@choi-ju-yung/%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@choi-ju-yung/%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Mon, 11 Aug 2025 17:14:46 GMT</pubDate>
            <description><![CDATA[<p>✅ <code>match</code></p>
<ul>
<li>검색 키워드가  포함된 데이터를 조회하고 싶을 때 사용</li>
<li>💡 <code>match</code> 쿼리는 <code>text</code> 타입의 필드에서만 사용하는 쿼리</li>
<li><code>match</code> 쿼리는 검색 키워드가 포함된 모든 도큐먼트를 조회</li>
</ul>
<p>EX)</p>
<pre><code class="language-js">PUT /boards // 인덱스 생성 및 매핑 정의
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;title&quot;: {
        &quot;type&quot;: &quot;text&quot;
      }
    }
  }
}

POST /boards/_doc // 데이터 삽입
{
  &quot;title&quot;: &quot;벨로그 짱짱 멋짐&quot;
}

GET /boards/_search // 도큐먼트로 검색 (아래와 같이 조회하면 결과값이 조회됨)
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;벨로그 멋짐&quot; 
    }
  }
}</code></pre>
<hr>
<p>✅ <code>term</code></p>
<ul>
<li>특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때 사용</li>
<li><code>term</code> 쿼리는 특정 값과 <code>정확히</code> 일치하는 모든 도큐먼트를 조회</li>
<li><code>term</code> 쿼리는 <code>text</code> 를 제외한 모든 타입에서 사용</li>
</ul>
<p>EX) </p>
<pre><code class="language-js">PUT /boards // 인덱스 생성 및 매핑 정의
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;board_id&quot;: {
        &quot;type&quot;: &quot;long&quot;
      },
      &quot;category&quot;: {
        &quot;type&quot;: &quot;keyword&quot;
      }
    }
  }
}

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;
}

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

GET /boards/_search // 조회1
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;category&quot;: &quot;자유&quot;
    }
  }
}

GET /boards/_search // 조회2
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;category&quot;: &quot;자유게시판&quot;
    }
  }
}

GET /boards/_search // 조회3
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;category&quot;: &quot;자유 게시판&quot;
    }
  }
}</code></pre>
<p>위와같이 조회를 3번 해봤을 때 어떤것이 조회될까?
조회3만 도큐먼트가 조회된다</p>
<p>조회1 -&gt; 자유  (데이터가 정확하게 일치하지 않음)
조회2 -&gt; 자유게시판 (공백이 있어서 데이터가 일치하지 않음)</p>
<p>즉 데이터가 가진 값과 정확히 일치하게 검색하지 않으면 도큐먼트가 조회되지 않는다</p>
<p>SQL문으로 표현하면 <code>SELECT * FROM boards WHERE category = &quot;자유 게시판&quot;</code> 와 동일하다</p>
<p>그럼 여러개 값중 하나라도 일치하면 도큐먼트를 조회하고 싶을 때 어떻게 해야할까?</p>
<p>✅ <code>terms</code></p>
<ul>
<li><code>terms</code> 쿼리는 여러 개의 값 중 하나라도 일치하는 모든 도큐먼트를 조회</li>
<li>SQL문의 <code>IN</code> 과 비슷한 역할</li>
</ul>
<p>EX)</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;terms&quot;: {
      &quot;category&quot;: [&quot;자유 게시판&quot;, &quot;익명 게시판&quot;]
    }
  }
}</code></pre>
<p>위와 같이 조회를 하면 <code>자유 게시판</code> <code>익명 게시판</code> 둘다 조회되는것을 확인할 수 있다
이 문장은 <code>SELECT * FROM boards WHERE category IN (&quot;자유 게시판&quot;, &quot;익명 게시판&quot;)</code> 와 동일하다</p>
<p>그러면 여러개의 조건을 동시에 만족하는 데이터를 조회할 때는 어떻게 해야할까?
<code>SELECT * FROM boards WHERE category = “자유 게시판” AND board_id = 1</code> 와 같이 
2가지 이상의 조건을 활용해서 검색하려 한다
아래와 같이 조회하면 오류가 발생한다
즉 <code>term</code> 쿼리에서는 2개이상 필드를 사용하는 것을 막아놨다</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;category&quot;: &quot;자유 게시판&quot;,
      &quot;board_id&quot;: 1
    }
  }
}</code></pre>
<hr>
<p>✅ 2가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때 사용하는 쿼리</p>
<p><code>bool</code> 쿼리에서 <code>must</code> or <code>filter</code> 를 사용하면 된다
즉 SQL에서 AND 역할을 한다</p>
<p><code>filter</code> 와 <code>must</code> 의 큰 차이점은 
<code>filter</code> 는 Score(점수) 영향을 주지않고, <code>must</code> 는 Score(점수) 영향을 준다</p>
<p>Score(점수)는 사용자가 입력한 값이 얼마나 잘 일치하는지 수치로 표현한 관련도 점수이며
역인덱스를 이용해서 계산된 관련도 점수이다</p>
<p>역인덱스는 필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록이다</p>
<p>여기서 전에 배운 <code>text</code> 타입과 역인덱스와 Score(점수) 는 밀접한 관련이 있다</p>
<p><code>text</code> 타입으로 필드를 만든 후 데이터를 넣으면 <code>Analyzer</code> 에 의해서 <code>역인덱스</code> 가 생성된다 
<code>역인덱스</code> 에 저장된 단어들을 <code>토큰</code> 이라고 부른다. 그리고 검색 쿼리가 오면 <code>역인덱스</code> 를 통해 데이터를 찾고 Score(점수)를 계산한다</p>
<p>결론은 <code>text</code> 타입으로만 Score(점수) 가 사용되며 계산되며
<code>text</code> 타입의 경우 <code>match</code>  쿼리를 사용하기 때문에 Score(점수)와 상관있는 쿼리의 경우 <code>match</code> 사용</p>
<p> text 이외의 타입의 경우는 Score(점수)와 상관없기 때문에 <code>term</code> 을 사용</p>
<p>💡 정리
유연한 검색이 필요하면
-&gt; <code>text</code> 타입, <code>match</code> 쿼리, <code>bool</code> 의 <code>must</code> 사용</p>
<p>정확한 검색이 필요하면
-&gt; text 이외의 타입, <code>term</code> 쿼리, <code>bool</code> 의 <code>filter</code> 사용</p>
<p>예시를 보면서 이해해보자</p>
<p>board_id : long 타입
title : text 타입
category : keyword 타입
is_notice : boolean 타입
created_at : date 타입</p>
<p>위처럼 boards라는 인덱스를 생성하고 매핑까지 했다고 가정하자
그 후 데이터를 삽입</p>
<pre><code class="language-js">POST /boards/_doc
{
  &quot;board_id&quot;: 1,
  &quot;title&quot;: &quot;엘라스틱서치는 정말 강력한 검색엔진이에요&quot;,
  &quot;category&quot;: &quot;자유 게시판&quot;,
  &quot;is_notice&quot;: false,
  &quot;created_at&quot;: &quot;2025-05-01T12:00:00&quot;
}</code></pre>
<p>여기서 검색쿼리를  사용할 때 다음과 같은 조건을 걸고 조회하려면 어떻게 해야할까?
<code>자유 게시판</code> 의 게시글 중에서 <code>검색엔진</code> 과 관련된 글을 찾고 싶다
그런데 <code>공지글</code> 이 아닌 게시글 중에서만 검색하고 싶다</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        { &quot;match&quot;: { &quot;title&quot;: &quot;검색엔진&quot; } } // 유연한 검색이 필요 (must)
      ],
      &quot;filter&quot;: [
        { &quot;term&quot;: { &quot;category&quot;: &quot;자유 게시판&quot; } }, // 정확한 검색이 필요 (filter)
        { &quot;term&quot;: { &quot;is_notice&quot;: false } } // 정확한 검색이 필요 (filter)
      ]
    }
  }
}</code></pre>
<p>자유 게시판의 게시글 중에서라는 뜻 -&gt; 자유 게시판 이라고 정확한 검색이 필요 (filter 사용)
검색엔진과 관련된 글  뜻 -&gt;  유연한 검색이 가능함 (must 사용)
공지글이 아닌 게시글 뜻 -&gt; 공지글이 아닌것은 false 로 정확히 구분 (filter 사용)</p>
<hr>
<p>✅ 특정 조건을 만족하지 않는 데이터를 조회하고 싶을 때</p>
<p><code>bool</code> 쿼리에서 <code>must_not</code> 을 사용하면 된다
즉 SQL에서 NOT 역할을 한다</p>
<p>직전 예시를 그대로 구현해보자</p>
<p>EX) <code>광고 게시판</code> 의 글이 아니면서, <code>공지 글</code> 이 아니면서, <code>검색엔진</code> 의 키워드와 관련된 게시글을 조회</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        { &quot;match&quot;: { &quot;title&quot;: &quot;검색엔진&quot; } } // 유연한 검색이 필요 (must)
      ],
      &quot;filter&quot;: [
        { &quot;term&quot;: { &quot;is_notice&quot;: false } } // 정확한 검색이 필요 (filter)
      ],
      &quot;must_not&quot;: [
        { &quot;term&quot;: { &quot;category&quot;: &quot;광고 게시판&quot; } }
      ]
    }
  }
}</code></pre>
<p>광고 게시판의 글이 아니라는 뜻 -&gt; 광고 게시판이 아니여야 한다는 정확한 검색이 필요 (must_not 사용)
공지 글이 아니면서 뜻 -&gt; 공지글이 아니여야 한다는 정확한 검색 필요 (filter 사용)
검색엔진 의 키워드와 관련된 뜻 -&gt; (must_ 사용)</p>
<p>💡 <code>must_not</code> 은 <code>term</code> 과 <code>match</code> 를 둘 다 사용할 수 있지만 조건에 따라 사용이 다르다
ex) 어떤 필드값이 광고가 아니여야 할 때 -&gt; <code>term</code> 사용
ex) 어떤 필드값이 &#39;광고’ 관련 내용을 유사하게라도 포함한 글이 아니여야할 때 -&gt; <code>match</code> 사용</p>
<hr>
<p>✅ 숫자/날짜의 값에 대한 범위 조건으로 데이터를 조회하고 싶을 때
<code>range</code> 쿼리를 사용하면 된다</p>
<p><code>range</code> 쿼리에서 사용하는 연산자</p>
<ul>
<li><code>gte</code> : 이상 <code>lte</code> : 이하 <code>gt</code> 초과 <code>lt</code> 미만</li>
<li><code>gte</code> = <code>Greater Than or Equal to</code>  (크거나 같음)</li>
<li><code>lte</code> = <code>Less-than or equal to</code> (작거나 같음)</li>
</ul>
<p>ex) 나이가 30살 이상이면서 회원가입 날짜가 2025년 1월 1일 이후인 사용자를 조회</p>
<pre><code class="language-js">GET /users/_search
{
  &quot;query&quot;: { 
    &quot;bool&quot;: { // 2가지 이상의 조건이 있기때문에 bool 쿼리 사용
      &quot;filter&quot;: [ // 정확한 검색을 해야하기때문에 filter 사용
        {
          &quot;range&quot;: {
            &quot;age&quot;: {
              &quot;gte&quot;: 30
            }
          }
        },
        {
          &quot;range&quot;: {
            &quot;created_at&quot;: {
              &quot;gte&quot;: &quot;2025-01-01&quot;
            }
          }
        }
      ]
    }
  }
}</code></pre>
<hr>
<p>✅ 특정 조건을 만족하는 데이터 위주로 상위 노출시키고 싶을 때
<code>boole</code> 쿼리에서 <code>should</code> 를 사용하면 된다</p>
<p> 위에서 배운 <code>must</code>와 <code>filter</code> 는 반드시 조건을 만족하는 데이터만 조회하지만
<code>should</code> 는 조건을 만족하지 않는 데이터도 조회된다</p>
<p>그 이유는 <code>should</code> 는 조건을  추가해서 그 조건이 맞을경우 <code>score(점수)</code> 에 가산점을 부여해서
데이터가 상위로 노출될 가능성이 높아진다</p>
<p>ex) 검색 결과 중 <code>평점이 높고</code> <code>좋아요 수가 많은 글</code> 을 상위에 노출시키고 싶은 경우</p>
<p>인덱스를 다음과 같이 생성했다고 가정하자</p>
<pre><code class="language-js">PUT /products
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;:   {  // 제품이름
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;nori&quot;
      },
      &quot;rating&quot;: { &quot;type&quot;: &quot;double&quot; }, // 평점
      &quot;likes&quot;:  { &quot;type&quot;: &quot;integer&quot; } // 좋아요 수
    }
  }
}</code></pre>
<p>데이터는 아래와 같이 넣었다고 가정하자
&quot;name&quot;: &quot;무선 충전기 C타입&quot;, &quot;rating&quot;: 4.9, &quot;likes&quot;: 300
&quot;name&quot;: &quot;소니 무선 이어폰 WF&quot;, &quot;rating&quot;: 3.8, &quot;likes&quot;: 15
&quot;name&quot;: &quot;갤럭시 버즈2 무선 이어폰&quot;, &quot;rating&quot;: 4.8, &quot;likes&quot;: 310
&quot;name&quot;: &quot;삼성 노트북 13인치&quot;, &quot;rating&quot;: 5.0, &quot;likes&quot;: 1000</p>
<p><code>should</code> 조건 없이 데이터를 조회하면 <code>무선 이어폰</code> 키워드가 없는 데이터는 아예 조회되지도 않고
평점이 낮거나 좋아요 수가 적은 상품이 상위에 노출되는 것을 확인할 수 있다</p>
<p>아래처럼 <code>should</code> 를 사용해서 조회를 해보자</p>
<pre><code class="language-js">GET /products/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [ // 특정키워드로 연관성이 있는 것 즉 score를 매겨야하기 때문에 must 사용
        {
          &quot;match&quot;: {
            &quot;name&quot;: &quot;무선 이어폰&quot; 
          }
        }
      ],
      &quot;should&quot;: [
        {
          &quot;range&quot;: {
            &quot;rating&quot;: {
              &quot;gte&quot;: 4.5 // 4.5 이상의 평점의 상품일 경우 score에 가산점 부여
            }
          }
        },
        {
          &quot;range&quot;: {
            &quot;likes&quot;: {
              &quot;gte&quot;: 100 // 좋아요 수가 100개 이상인 상품일 경우 score에 가산점 부여
            }
          }
        }
      ]
    }
  }
}</code></pre>
<p>결과는 어떻게 나올까?
1 순위 : 갤럭시 버즈2 무선 이어폰 -&gt; 검색 키워드도 관련성 높으면서 평점도 좋고 좋아요 수도 많아서 1위
2 순위 : 무선 충전기 C타입 -&gt; 검색 키워드는 &#39;무선&#39; 만 일치하지만  평점과 좋아요 수가 높아서 2위
3 순위 : 소니 무선 이어폰 WF -&gt; 검색 키워드는 관련성이 높지만 평점과 좋아요 수가 낮아서 3위</p>
<p>삼성 노트북 13인치는 관련 키워드가 아예 없기 때문에 조회되지 않음</p>
<hr>
<p>✅ 오타가 있더라도 유사한 단어를 포함한 데이터를 조회하고 싶을 때
<code>fuzziness</code> 를 사용하면 된다</p>
<p>데이터에 name 필드가 =&gt;  메이플 캐논슈터   일때
아래와 같이 조회하면 데이터가 잘 조회되는 것을 확인할 수 있다</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;메이플&quot;
    }
  }
}</code></pre>
<p>하지만 마이플 이라고 검색하면 데이터가 조회되지 않는다
이럴 때 오타가 어느정도 있어도 검색되게 만들기 위해서 <code>fuzziness</code> 를 사용한다</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: {
        &quot;query&quot;: &quot;마이플&quot;,
        &quot;fuzziness&quot;: &quot;AUTO&quot;
      }
    }
  }
}</code></pre>
<p><code>fuzziness : AUTO</code> : 단어 길이에 따라 오타 허용 개수를 자동으로 설정</p>
<hr>
<p>✅ 여러 필드에서 검색 필드가 포함된 데이터를 조회하고 싶을 때  (ex : 제목, 내용)
<code>multi_match</code> 를 사용하면 된다</p>
<p>예시로 데이터를 4개 삽입했다고 가정하자</p>
<pre><code class="language-js">POST /boards/_doc // (1) title, content 둘 다에 키워드 포함
{
  &quot;title&quot;: &quot;엘라스틱서치 적용 후기&quot;,
  &quot;content&quot;: &quot;회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다.&quot;
}

POST /boards/_doc // (2) title 에만 키워드 포함
{
  &quot;title&quot;: &quot;엘라스틱서치를 사용해보니&quot;,
  &quot;content&quot;: &quot;검색 엔진 도입 후 성능이 향상되었습니다.&quot;
}

POST /boards/_doc // (3) content에만 키워드 포함
{
  &quot;title&quot;: &quot;검색엔진 도입 사례&quot;,
  &quot;content&quot;: &quot;이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다.&quot;
}

POST /boards/_doc // (4) title, content 둘 다 포함 안 됨
{
  &quot;title&quot;: &quot;레디스 캐시 사용기&quot;,
  &quot;content&quot;: &quot;서비스 속도 개선을 위해 캐시 시스템을 사용했습니다.&quot;
}</code></pre>
<p>여기서 아래와 같이 <code>multi_match</code> 를 이용해서 조회하면</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: { // 두가지 이상 필드에서는 multi_match 사용
      &quot;query&quot;: &quot;엘라스틱서치 적용 후기&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;content&quot;] // 찾고자하는 여러개의 필드를 배열로 지정
    }
  }
}</code></pre>
<p>(1) &lt;-  (3) &lt;- (2) 순으로 상위노출되서 조회되는것을 확인할 수 있다
(4)는 title과 content에 둘 다 키워드가 없어서 조회되지 않는다</p>
<p>왜 위와 같은 순서로 상위노출 조회 됬을까?
다음과 같은 조건으로 <code>score</code> 로 점수를 매긴다</p>
<ul>
<li>검색 키워드가 문서에서 자주 등장할 수록 점수가 높게 측정됨</li>
<li>전체 문서중 희귀한 검색어가 일치할수록 점수가 높게 측정됨</li>
<li>필드 값의 길이가 작은데도 불구하고 키워드가 등장하면 점수가 높게 측정</li>
</ul>
<p>(1) 은 title과 content에 키워드가 둘 다 들어있기 때문에 <code>score</code> 를 높게 받아 제일 먼저 조회된다</p>
<p>그러면 내가 원하는 필드에 검색어가 포함될 때 점수를 더 높게 주려면 어떻게 해야할까? (가중치)
다음과 같이 작성하면 된다</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;엘라스틱서치 적용 후기&quot;,
      &quot;fields&quot;: [&quot;title^2&quot;, &quot;content&quot;] // title에 2배 더 높은 score를 부여
    }
  }
}</code></pre>
<p>위와 같이 조회한다면 (1) &lt;- (2) &lt;- (3) 순으로 조회되는 것을 확인할 수 있다</p>
<hr>
<p>✅ 검색한 키워드를 하이라이팅 처리하고 싶을 때
<code>highlight</code> 를 사용하면 된다</p>
<p>바로 위 예시로 사용해서 작성해보자</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;엘라스틱서치 적용 후기&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;content&quot;]
    }
  },
  &quot;highlight&quot;: {
    &quot;fields&quot;: {
      &quot;title&quot;: {
        &quot;pre_tags&quot;: [&quot;&lt;mark&gt;&quot;],
        &quot;post_tags&quot;: [&quot;&lt;/mark&gt;&quot;]
      },
      &quot;content&quot;: {
        &quot;pre_tags&quot;: [&quot;&lt;b&gt;&quot;],
        &quot;post_tags&quot;: [&quot;&lt;/b&gt;&quot;]
      }
    }
  } 
}</code></pre>
<p>위처럼 title에서 특정 키워드가 일치하면은 
<code>pre_tags</code> (키워드 앞에는) mark 태그를 붙이고 <code>post_tags</code> (키워드 뒤에는) /mark 태그를 붙인다</p>
<p>또 content에서 키워드가 일치하면
<code>pre_tags</code> (키워드 앞에는) b 태그를 붙이고 <code>post_tags</code> (키워드 뒤에는) /b 태그를 붙인다</p>
<p>조회 결과는 아래와 같이 HTML 태그로 감싸져서 결과 값이 반환된다</p>
<pre><code class="language-js">&quot;title&quot;: [ &quot;&lt;mark&gt;엘라스틱&lt;/mark&gt;&lt;mark&gt;서치&lt;/mark&gt; &lt;mark&gt;적용&lt;/mark&gt; &lt;mark&gt;후기&lt;/mark&gt;&quot; ],
&quot;content&quot;: [ &quot;회사 프로젝트에 &lt;b&gt;엘라스틱&lt;/b&gt;&lt;b&gt;서치&lt;/b&gt;를 &lt;b&gt;적용&lt;/b&gt;한 &lt;b&gt;후기&lt;/b&gt;를 공유합니다.&quot; ]</code></pre>
<hr>
<p>✅ 페이징과 정렬</p>
<p>우선 페이징은 <code>size</code> 와 <code>from</code> 을 사용하면 된다</p>
<p>데이터를 총 7개 (1번글 ~ 7번글) 을 생성했다고 가정하자 </p>
<pre><code class="language-js">POST /boards/_doc
{
  &quot;title&quot;: &quot;1번 글&quot;,
  &quot;likes&quot;: 12
}

POST /boards/_doc
{
  &quot;title&quot;: &quot;2번 글&quot;,
  &quot;likes&quot;: 35
}
..... ~ 7번글까지</code></pre>
<p>여기서 페이징 처리를 적용시켜서 데이터를 조회해보자</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;글&quot;
    }
  },
  &quot;size&quot;: 3, // 3개만 불러오기
  &quot;from&quot;: 0 // 첫번째부터 
}</code></pre>
<p><code>size</code> : 한 페이지에 불러올 데이터 개수 (SQL문의 LIMIT과 동일)
<code>from</code> : 몇 번째 데이터부터 불러올 지 (SQL문의 OFFSET과 동일, 0부터 시작)</p>
<p>그럼 2페이지는 어떻게 조회하는가? 
<code>size</code> 는 그대로 3이고 <code>from</code> 을 3으로 바꾸면 된다</p>
<p>다음은 정렬을 해보자
likes(좋아요) 필드를 기준으로 정렬해보자</p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;글&quot;
    }
  },
  &quot;sort&quot;: [
    {
      &quot;likes&quot;: {
        &quot;order&quot;: &quot;desc&quot; // 내림차순
      }
    }
  ]
}</code></pre>
<hr>
<p>✅ 하나의 필드에 <code>text</code> 와 <code>keyword</code> 타입을 동시에 사용하고 싶을 때</p>
<p><code>text</code> 는 유연한 검색, <code>keyword</code> 는 정확한 검색이 필요할 때 사용한다
유연한 검색과 정확한 검색을 둘 다 하고 싶을 때 사용 한다</p>
<p>아래 예시는 category 필드에 두 가지 타입을 사용한 경우이다</p>
<pre><code class="language-js">PUT /products
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;nori&quot;
      },
      &quot;category&quot;: {
        &quot;type&quot;: &quot;text&quot;, // text 타입 선언
        &quot;analyzer&quot;: &quot;nori&quot;,
        &quot;fields&quot;: { 
          &quot;raw&quot;: { // 서브 필드명 (다른 이름으로 설정해도 됨)
            &quot;type&quot;: &quot;keyword&quot; // keyword 타입 선언
          }
        }
      }
    }
  }
}</code></pre>
<p>위처럼 인덱스를 생성하고 매핑 한 후 데이터를 다음과 같이 삽입하면</p>
<pre><code class="language-js">POST /products/_doc
{
  &quot;name&quot;: &quot;삼성 세탁기&quot;,
  &quot;category&quot;: &quot;특수 가전제품&quot;
}</code></pre>
<p>text 타입은 토큰으로 분리해서 저장한다 -&gt; <code>특수</code> <code>가전</code> <code>제품</code>
keyword 타입은 값 자체를 통째로 저장한다 -&gt; <code>특수 가전제품</code></p>
<p>이 상태에서 유연하게 검색을 하면</p>
<pre><code class="language-js">POST /products/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;가전&quot;,
      &quot;fields&quot;: [&quot;name&quot;, &quot;category&quot;] // name필드나 category 필드에 가전이 들어가면 조회됨
    }
  }
}</code></pre>
<p>이번엔 특수 가전제품 카테고리의 상품만 검색하고 싶으면</p>
<pre><code class="language-js">POST /products/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: { // 정확하게 일치하는 데이터를 찾기위해서 term 사용
      &quot;category.raw&quot;: &quot;특수 가전제품&quot;
    }
  }
}</code></pre>
<hr>
<p>✅ 검색어를 추천해주는 기능 (    자동 완성 기능)</p>
<p>여러가지 방법이 있지만 자주 사용하고 가성비 좋은 <code>search_as_you_type</code> 을 활용해보자</p>
<p><code>search_as_you_type</code></p>
<ul>
<li>Elasticsearch에서 자동 완성 기능을 쉽게 구현할 수 있게 설계된 <code>데이터 타입</code></li>
<li>text 타입처럼 애널라이저를 거쳐 토큰으로 분리된다</li>
<li>이 타입을 활용해서 필드를 만들면 내부적으로<br><code>_2gram</code> , <code>_3gram</code> 이라는 멀티 필드(Multi Field)도 같이 만든다.</li>
</ul>
<p><code>_2gram</code> : 두 단어씩 묶어서 토큰을 만든다
<code>_3gram</code> : 세 단어씩 묶어서 토큰을 만든다</p>
<p>예를 들어 아래와 같이 <code>search_as_you_type</code> 타입으로 인덱스를 생성하고</p>
<pre><code class="language-js">PUT /products
{
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;: {
        &quot;type&quot;: &quot;search_as_you_type&quot;,
        &quot;analyzer&quot;: &quot;nori&quot;
      }
    }
  }
}</code></pre>
<p>Analyze로 토큰을 분석해보자</p>
<pre><code class="language-js">GET /products/_analyze
{
  &quot;field&quot;: &quot;name&quot;,
  &quot;text&quot;: &quot;You have the big banana&quot;
  // 일반 text 타입처럼 동일한 방식으로 분리됨 -&gt; &#39;you&#39; &#39;have&#39; &#39;the&#39; &#39;big&#39; &#39;banana&#39;
}

GET /products/_analyze
{
  &quot;field&quot;: &quot;name._2gram&quot;,
  &quot;text&quot;: &quot;you have the big banana&quot;
  // 두 단어씩 분리됨 -&gt; &#39;you have&#39; &#39;have the&#39; &#39;the big&#39; &#39;big banana&#39;
}

GET /products/_analyze
{
  &quot;field&quot;: &quot;name._3gram&quot;,
  &quot;text&quot;: &quot;you have the big banana&quot;
  // 세 단어씩 분리됨 -&gt; &#39;you have the&#39; &#39;have the big&#39; &#39;the big banana&#39;
}</code></pre>
<p>이제 자동완성 기능을 사용해보자</p>
<p>데이터를 <code>곱창 돌김생김</code> <code>구운 돌김</code> <code>완도 곱창 돌김 100매</code> <code>삼성 노트북</code> <code>Nike 신발</code> 을 삽입했다고 가정</p>
<p>위 데이터를 바탕으로 아래처럼 조회를 하면 데이터가 자동완성 된다</p>
<pre><code class="language-js">GET /products/_search
{
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;돌김&quot;, 
      &quot;type&quot;: &quot;bool_prefix&quot;, 
      &quot;fields&quot;: [
        &quot;name&quot;,
        &quot;name._2gram&quot;,
        &quot;name._3gram&quot;
      ]
    }
  }.
  &quot;size&quot; : 5  // 5개까지만
}</code></pre>
<p>여기서 <code>bool_prefix</code> 앞쪽 단어는 match, 마지막 단어는 prefix match로 처리한다
ex) <code>you have the</code> 라고 검색하면 <code>you</code> <code>have</code> 는 연인덱스에 저장된 토큰과 일치하는 데이터를 찾고
마지막 단어인 <code>the</code> 로 시작하는 데이터를 조회하는 것이다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[매핑]]></title>
            <link>https://velog.io/@choi-ju-yung/%EB%A7%A4%ED%95%91</link>
            <guid>https://velog.io/@choi-ju-yung/%EB%A7%A4%ED%95%91</guid>
            <pubDate>Mon, 14 Jul 2025 16:47:57 GMT</pubDate>
            <description><![CDATA[<p>✅ 매핑</p>
<ul>
<li>도큐먼트의 각 필드가 어떤 데이터 타입을 가지고 있는지 정의하는 설정</li>
</ul>
<p>RDBMS에서 테이블을 만들고 어떤 유형의 데이터를 넣는지 정의 = 스키마를 정의</p>
<hr>
<p>✅ Elasticsearch 의 데이터 타입 (중요한것만)</p>
<p>[숫자]</p>
<ul>
<li>10억 이하의 정수를 저장할 때 : <code>integer</code></li>
<li>10억이 넘어가는 정수를 저장할 때 : <code>long</code></li>
<li>실수(소숫점을 가진 숫자 포함) 을 저장할 때 : <code>double</code></li>
</ul>
<p>[문자]</p>
<ul>
<li>문자열을 토큰으로 쪼개서 저장할 때 : <code>text</code> 
= 유연한 검색이 필요할 때 (완전히 일치하지 않아도, 비슷한 데이터를 조회할 때)</li>
<li>정확한 검색이 필요할 때와 문자열 그대로 저장할 때 : <code>keyword</code>
= 데이터가 정확하게 일치할 때 ex) 휴대폰번호 <code>010-1234-5678</code> 이메일 <code>abc@naver.com</code></li>
</ul>
<p>[기타]</p>
<ul>
<li>날짜 데이터 저장할 때 : <code>date</code></li>
<li>true, false 저장할 때 : <code>boolean</code></li>
</ul>
<hr>
<p>✅ 매핑의 특징</p>
<ul>
<li>(1) : <code>null</code> 을 허용</li>
</ul>
<p>Elasticsearch는 매핑을 정의하더라도 
해당 필드가 반드시 존재하거나 null이면 안 된다는 제약을 두지 않는다</p>
<p>ex) 
다음 예시처럼 매핑을 정의했을 때</p>
<pre><code class="language-js">&quot;mappings&quot;: {
  &quot;properties&quot;: {
    &quot;title&quot;: { &quot;type&quot;: &quot;text&quot; }
    &quot;content&quot;: { &quot;type&quot;: &quot;text&quot; }
  }
}</code></pre>
<p>데이터를 이런식으로 넣어도 문제없이 들어간다</p>
<pre><code class="language-js">{ 
  &quot;title&quot;: null,
  &quot;content&quot;: &quot;hihi&quot;
}

{ 
  &quot;content&quot;: &quot;hihi&quot;
}</code></pre>
<ul>
<li>(2) : 배열을 허용</li>
</ul>
<p>Elasticsearch는 별도의 설정 없이 배열(array) 형태의 데이터를 삽입할 수 있다</p>
<p>다음과 같이 매핑을 정의했을 때</p>
<pre><code class="language-js">&quot;mappings&quot;: {
  &quot;properties&quot;: {
    &quot;hashtags&quot;: { &quot;type&quot;: &quot;text&quot; }
  }
}</code></pre>
<p>이런식으로 데이터를 넣을 수 있다</p>
<pre><code class="language-js">{ 
  &quot;hashtags&quot;: &quot;여행&quot;
}

{ 
  &quot;hashtags&quot;: [&quot;여행&quot;, &quot;요리&quot;]
}</code></pre>
<p>배열에 있는 값이 각각 토큰으로 분리되서 역인덱스로 저장이되서
<code>여행</code> 이라는 키워드로 검색하면 위 2가지 데이터 전부 다 조회된다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[엘라스틱 서치 검색 기능 및 작동원리]]></title>
            <link>https://velog.io/@choi-ju-yung/%EC%97%98%EB%9D%BC%EC%8A%A4%ED%8B%B1-%EC%84%9C%EC%B9%98-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%B0%8F-%EC%9E%91%EB%8F%99%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@choi-ju-yung/%EC%97%98%EB%9D%BC%EC%8A%A4%ED%8B%B1-%EC%84%9C%EC%B9%98-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%B0%8F-%EC%9E%91%EB%8F%99%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Thu, 10 Jul 2025 17:35:42 GMT</pubDate>
            <description><![CDATA[<p>✅ 역 인덱스</p>
<ul>
<li>필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록</li>
</ul>
<p>✅ 역 인덱스의 원리</p>
<pre><code class="language-sql">PUT /products  // products 인덱스(테이블)을생성하면서 매핑(스키마) 하기
{
    &quot;mappings&quot;: {
        &quot;properties&quot;: {
            &quot;name&quot; : {
                &quot;type&quot;: &quot;text&quot;
            }
        }
    }
}

POST /products/_create/1 // id : 1
{
  &quot;name&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;
}

POST /products/_create/2 // id : 2
{
  &quot;name&quot;: &quot;Apple 2024 에어팟 4세대&quot;
}

POST /products/_create/3 // id : 3
{
  &quot;name&quot;: &quot;Apple 2024 아이패드 mini A17 Pro&quot;
}


GET /products/_search // 특정 단어(문장)을 포함한 도큐먼트를 조회하기
{
    &quot;query&quot;: {
        &quot;match&quot;: {
          &quot;name&quot; : &quot;Apple 2024 아이패드&quot;
        }
    }
}</code></pre>
<p>위와 같이 3개의 도큐먼트를 엘라스틱 서치에 저장한다고 가정해보자
그러면 내부적으로 데이터가 단어단위로 잘라지고 <code>역인덱스</code> 로  저장이 된다</p>
<ol>
<li>단어 단위로 자르기 
[Apple, 2025, 맥북, 에어, 13, M4, 10코어]<br>[Apple, 2024, 에어팟, 4세대]<br>[Apple, 2024, 아이패드, mini, A17, Pro]</li>
</ol>
<p>💡 필드값에서 추출되어 역 인덱스에 저장된 단어를 <code>토큰</code> 이라고 부름
💡 생성된 역 인덱스는 시스템 내부적으로만 생성이 되어서 눈으로 확인 불가능</p>
<ol start="2">
<li><p>역 인덱스로 저장</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/d8c38b05-b6cf-48fb-8b7e-bd32f44a159d/image.png" alt=""></p>
</blockquote>
</li>
<li><p>검색을 할 경우 역 인덱스 활용
EX) <code>Apple 2024 아이패드</code> 를 검색하면 역 인덱스를 활용하여 일치하는 단어가 많은
도큐먼트를 우선적으로 조회함<br>
id가 1 도큐먼트는 토큰이 1개 일치
id가 2 도큐먼트는 토큰이 2개 일치
id가 3 도큐먼트는 토큰이 3개 일치<br>
즉 id가 3인 도큐먼트가 제일 먼저 조회됨
ElasticSearch 가 자체적인 로직으로 점수를 매겨서 높은순으로 도큐먼트를 조회한다</p>
</li>
</ol>
<p>대략적으로 점수를 계산하는 로직에는 3가지 정도로 판단된다</p>
<ol>
<li>문서 내에서 검색어가 얼마나 자주 등장하는지? -&gt; 많이 등장할수록 점수 UP</li>
<li>검색어가 전체 문서 중 얼마나 희귀하는지? -&gt; 희귀할수록 점수 UP</li>
<li>문서(필드)가 짧을수록 점수 UP</li>
</ol>
<p>ElasticSearch는 역 인덱스의 기능을 가지고 있기 때문에, 단어의 순서랑 상관없이 도큐먼트를 조회 가능함
참고로 이러한 구조는 데이터 타입의 <code>text</code> 타입 한해서만 해당한다</p>
<hr>
<p>✅ 애널라이저 (Analyzer)</p>
<ul>
<li>텍스트 데이터를 검색 가능하게 만들기 위해 분석하고 처리하는 도구로
쉽게 말해 문장을 잘게 쪼개고 정리해서 검색하기 쉽게 바꾸는 도구이다</li>
</ul>
<blockquote>
<p>문자열 -&gt; 토큰으로 만들어주는 도구
캐릭터 필터, 토크나이저, 토큰필터 이렇게 3가지 전부 합쳐서 애널라이저라 함<img src="https://velog.velcdn.com/images/choi-ju-yung/post/6fc7f018-2ce6-40d9-aad0-ec19b9486adb/image.png" alt=""></p>
</blockquote>
<p><code>캐릭터 필터</code> :  문자열을 토큰으로 자르기전에 문자열을 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 캐릭터 필터가 설정되어 있지 않음
ex) <code>html_strip</code> 필터 적용 (html 태그 제거) 
<code>&lt;h1&gt;아이폰 15 사용 후기&lt;/h1&gt;</code> -&gt; <code>아이폰 15 사용 후기</code>
<br>
<code>토크나이저</code> : 문자열을 토큰으로 자르는 역할을 함
💡 기본값으로 <code>standard 토크나이저</code> 가 적용되어 있으며
공백 <code>,</code> <code>.</code> <code>!</code> <code>?</code> 와 같은 문장 부호를 기준으로 자름
ex) <code>The Brown-Foxes jumped over the roof.</code>
→ [<code>The</code>, <code>Brown</code>, <code>Foxes</code>, <code>jumped</code>, <code>over</code>, <code>the</code>, <code>roof</code>]
<br>
<code>토큰 필터</code> : 잘린 토큰을 최종적으로 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 <code>lowercase</code> 필터가 적용되어 있음
ex) lowercase 필터 적용 (소문자로 변환)
[<code>The</code>, <code>Brown</code>, <code>Foxes</code>, <code>jumped</code>, <code>over</code>, <code>the</code>, <code>roof</code>]
    → [<code>the</code>, <code>brown</code>, <code>foxes</code>, <code>jumped</code>, <code>over</code>, <code>the</code>, <code>roof</code>]</p>
<p>ex) stop 필터 적용 (<code>a</code>, <code>the</code>, <code>is</code>와 같은 특별한 의미를 가지지 않는 단어 제거)
[<code>the</code>, <code>brown</code>, <code>foxes</code>, <code>jumped</code>, <code>over</code>, <code>the</code>, <code>roof</code>]
 → [<code>brown</code>, <code>foxes</code>, <code>jumped</code>, <code>roof</code>]</p>
<p>ex) stemmer 필터 적용 (단어의 원래 형태로 변환)
[<code>brown</code>, <code>foxes</code>, <code>jumped</code>, <code>roof</code>]
→ [<code>brown</code>, <code>fox</code>, <code>jump</code>, <code>roof</code>]</p>
<hr>
<p>✅ 애널라이저가 토큰을 나누는 방식</p>
<p>방법 1 :  <code>&quot;analyzer&quot;: &quot;standard&quot;</code>
 -&gt; analyzer 를 standart 기본값으로 명시하는 방법</p>
<p> 방법 2</p>
<pre><code>&quot;char_filter&quot;: [], 
&quot;tokenizer&quot;: &quot;standard&quot;, 
&quot;filter&quot;: [&quot;lowercase&quot;]</code></pre><p>-&gt;  standard analyer의 구성을 직접 명시하는 방법</p>
<p>아래와 같이 입력하면 토큰이 나눠지는 것을 볼 수 있다
<code>GET /_analyze</code> 란?
Elasticsearch에서 텍스트가 Analyzer에 의해 어떻게 처리되는지 직접 확인할 수 있게 해주는 분석 도구 API</p>
<pre><code>// 방법 1 
GET /_analyze
{
  &quot;text&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;,
  &quot;analyzer&quot;: &quot;standard&quot;
}

// 방법 2
GET /_analyze
{
  &quot;text&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;,
  &quot;char_filter&quot;: [],
  &quot;tokenizer&quot;: &quot;standard&quot;,
  &quot;filter&quot;: [&quot;lowercase&quot;]
}</code></pre><hr>
<p>✅ 대소문자 구분없이 검색하는 방법 (예시)</p>
<pre><code class="language-js">PUT /products // 인덱스 생성 + 매핑 정의 + Custom Analyzer 적용
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;analyzer&quot;: {
        &quot;products_name_analyzer&quot;: {
          &quot;char_filter&quot;: [],
          &quot;tokenizer&quot;: &quot;standard&quot;,
          &quot;filter&quot;: [&quot;lowercase&quot;] // lowercase token filter 추가
        }
      }
    }
  },
  &quot;mappings&quot;: {
      &quot;properties&quot;: {
        &quot;name&quot;: {
          &quot;type&quot;: &quot;text&quot;,
          &quot;analyzer&quot;: &quot;products_name_analyzer&quot;
        }
      }
    }
}

POST /products/_create/1 // id가 1값인 데이터 삽입
{
  &quot;name&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;
}</code></pre>
<p> 이런식으로 데이터를 삽입한 후 조회해보도록 하겠다</p>
<pre><code class="language-js">GET /products/_search // 조회 잘됨
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;name&quot;: &quot;apple&quot; 
    }
  }
}

GET /products/_search // 조회 잘됨
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;name&quot;: &quot;Apple&quot;
    }
  }
}</code></pre>
<p>둘다 조회가 잘 되는것을 확인할 수 있다
이제 Analyze API 사용해서 분석을 해보자</p>
<pre><code class="language-js">GET /products/_analyze
{
  &quot;field&quot;: &quot;name&quot;
  &quot;text&quot;: &quot;Apple 2025 맥북 에어 13 M4 10코어&quot;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/a50804e6-8d82-42f0-8168-a5084e80c7df/image.png" alt="">
위 사진처럼  도큐먼트를 생성할 때 Analyzer가 문자열을 분리해서 역인덱스를 생성하기 때문에
토큰이 나눠지는것을 확인할 수 있다
그러면 토큰에는 소문자로 apple 이 저장되있어서 text를 apple로 검색했을 때 나오는 것은 알겠는데
왜 대문자로 Apple 을 검색했을 때는 조회가 잘 되는가?</p>
<p>💡 그 이유는 검색을 할때의 Text 값도 Analyzer을 사용하여 토큰을 분리시킨 다음
실제 역인덱스 값이랑 비교하기 때문에 조회할 수 있는 것이다</p>
<p>그래서 사용자는 대소문자를 신경쓰지 않고 데이터를 조회할 수 있다</p>
<hr>
<p>✅ <code>html_strip</code>  (HTML 태그 제거) 
💡 전체적인 예시는 위에서 Custom Analyzer 적용할 때 
<code>character filter</code> 값만 바꿔주면 되기때문에 생략하겠다 =&gt; <code>character filter : [&quot;html_strip&quot;]</code></p>
<p>게시글 서비스에는 굵게, 기울임, 링크 등을 포함해서 글을 작성하기 때문에
HTML태그를 포함해서 DB에 저장하는 경우가 있다</p>
<pre><code class="language-js">POST /boards/_doc
{
  &quot;content&quot;: &quot;&lt;h1&gt;이 물품 팝니다/h1&gt;&quot;
}</code></pre>
<p>위처럼 HTML 태그가 포함되있는 상태에서 데이터가 저장된 상태에서 h1을 검색하면 </p>
<pre><code class="language-js">GET /boards/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;content&quot;: &quot;h1&quot;
    }
  }
}</code></pre>
<p>데이터가 조회되는것을 확인할 수 있다 
하지만 실제 글과 상관없는 h1이라는  키워드로 게시글이 조회된다면 검색 품질이 떨어지는 것이다
실제로 어떤식으로 토큰이 저장되어있는지 확인해보자</p>
<pre><code class="language-js">GET /boards/_analyze
{
  &quot;field&quot;: &quot;content&quot;,
  &quot;text&quot;: &quot;&lt;h1&gt;Running cats, jumping quickly — over the lazy dogs!&lt;/h1&gt;&quot;
}</code></pre>
<p>결과값으로 앞뒤로 h1 태그들도 역 인덱스에 저장되는것을 확인할 수 있어서 디스크 공간을 낭비한다
이러한 이유로 <code>html_strip</code> 를 사용하여 HTML 태그를 제거하고 저장을 해야한다</p>
<hr>
<p>✅ 검색할 때 필요없는 불용어 제거하기 = <code>stop</code></p>
<p>사용자가 검색할 때 <code>그 사과는 맛있다</code> 라고 입력하면 
사실 검색에서 중요한 단어는 <code>사과</code> 와 <code>맛있다</code> 고 <code>그</code>는 필요없는 단어이기 때문에 제거하는게 좋다
필요없는 단어를 제거해야 역인덱스의 저장공간을 절약할 수 있다</p>
<p>영어: <code>&quot;a&quot;, &quot;the&quot;, &quot;is&quot;, &quot;at&quot;, &quot;which&quot;, &quot;on&quot;, &quot;in&quot;</code> 등</p>
<p>한글: <code>&quot;이&quot;, &quot;그&quot;, &quot;저&quot;, &quot;그리고&quot;, &quot;하지만&quot;, &quot;또는&quot;, &quot;은&quot;, &quot;는&quot;, &quot;이&quot;, &quot;가&quot;, &quot;에&quot;, &quot;의&quot;</code> 등</p>
<p>적용방법은 <code>filter</code> 부분에 <code>stop</code> 을 추가하면 된다
<code>filter&quot;: [&quot;lowercase&quot;, &quot;stop&quot;]</code></p>
<hr>
<p>✅ 단어의 형태에 상관없이 검색하는 방법 = <code>stemmer</code>
<code>stemmer</code> :  어미, 접미사 등을 제거해서 어근만 남기는 것</p>
<p>ex) 영어
<code>running, ran, runs</code> -&gt; <code>run</code>
<code>playing, played, player</code> -&gt; <code>play</code></p>
<p>ex) 한글
<code>먹었다, 먹고, 먹는</code> → <code>먹다</code>
<code>사랑한다, 사랑해서, 사랑할</code> → <code>사랑</code></p>
<p>이렇게 형태는 달라도 같은 의미인 단어들을 하나의 형태로 통일시켜서
검색 시 더 정확한 매칭이 되도록 도움을 준다</p>
<p>적용방법은 <code>filter</code> 부분에 <code>stemmer</code>  를 추가하면 된다</p>
<p><code>stemmer</code> 를 적용하면 영단어나 한글을 기본형으로 변환한 후 토큰에 저장한다
그러면 토큰에는 <code>사랑</code> 이라고 저장되있는데 왜 <code>사랑한다</code> 를 검색하면 조회될까?
아까 위에서 말한 예시와 동일하다
<code>사랑한다</code> 라고 검색했을 때, <code>stemmer</code> 에 의해 기본형인 <code>사랑</code> 으로 바뀐채로 검색을 하기 때문이다</p>
<hr>
<p>✅ 동의어로 검색하는 방법 = <code>synonym</code>
<code>synonym</code> 필터는 동의어 필터로 동일하거나 유사한 의미의 단어들을 하나처럼 취급한다
적용방법은 <code>filter</code>에 <code>synonym</code> 추가하고나서 세부적으로 매칭을 추가시켜야함
ex) <code>나는 랩탑을 새로 샀다</code> 에서 <code>노트북</code> 이라고 검색했을 때, 인식을 시키고 싶으면?</p>
<pre><code class="language-js">PUT /korean_synonym_test
{
  &quot;settings&quot;: {
    &quot;analysis&quot;: {
      &quot;filter&quot;: {
        &quot;korean_synonym_filter&quot;: {
          &quot;type&quot;: &quot;synonym&quot;,
          &quot;synonyms&quot;: [
            &quot;노트북, 랩탑&quot;, // 노트북, 랩탑 입력시 다 매칭
            &quot;핸드폰, 휴대폰, 스마트폰&quot;, // 핸드폰, 휴대폰, 스마트폰 입력시 다 매칭
            &quot;자동차, 차량, 차&quot; // 자동차, 차량, 차 입력시 다 매칭
          ]
        }
      },
      &quot;analyzer&quot;: {
        &quot;korean_synonym_analyzer&quot;: {
          &quot;tokenizer&quot;: &quot;standard&quot;,
          &quot;filter&quot;: [&quot;lowercase&quot;, &quot;korean_synonym_filter&quot;]
        }
      }
    }
  },
  &quot;mappings&quot;: {
    &quot;properties&quot;: {
      &quot;name&quot;: {
        &quot;type&quot;: &quot;text&quot;,
        &quot;analyzer&quot;: &quot;korean_synonym_analyzer&quot;
      }
    }
  }
} </code></pre>
<p>다음과 같이 저장했을 때, 토큰을 분석하면 사진과 같이 동의어들이 토큰에 저장되는것을 확인할 수 있다</p>
<pre><code class="language-js">GET /products/_analyze
{
    &quot;field&quot;: &quot;name&quot;,
    &quot;text&quot;: &quot;노트북 스마트폰 차량&quot;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/a7b6409a-ca87-4e4d-b2e8-f33b4445f6bc/image.png" alt=""></p>
<hr>
<p>✅ <code>Nori Analyzer</code> </p>
<p><code>&quot;filter&quot;: [&quot;lowercase&quot;, &quot;stop&quot;, &quot;stemmer&quot;]</code> 로 인덱스를 생성했을 때
영어와 같은 경우 띄어쓰기로 단어가 명확하게 구분되어 토큰으로 저장이 된다
ex) <code>&quot;text&quot;: &quot;I like bananas&quot;</code> -&gt; <code>i</code> <code>like</code> <code>banana</code></p>
<p>하지만 한글과 같은 경우 ex) <code>&quot;text&quot;: &quot;백화점에서 쇼핑을 하다가 친구를 만났다.&quot;</code> 
-&gt; <code>백화점에서</code> <code>쇼핑을</code> <code>하다가</code> <code>친구를</code> <code>만났다</code> 단순히 띄어쓰기로 토큰이 저장된다
이럴경우 사용자가 <code>백화점</code> <code>쇼핑</code> <code>친구</code> 라는 키워드로 검색하면 조회되지가 않는 문제가 발생한다</p>
<p>이러한 문제점을 해결하기 위해 한글에 맞는 <code>Nori Analyzer</code> 를 써야 한다. 
<code>Nori Analyzer</code>  를 사용하려면 플러그인을 설치해야한다</p>
<p>도커 예시</p>
<pre><code class="language-docker"># Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:8.17.4 

# Nori Analyzer 플러그인 설치
RUN bin/elasticsearch-plugin install analysis-nori </code></pre>
<pre><code class="language-docker">services:
  elastic:
    build:
      context: .
      dockerfile: Dockerfile  # 위에 저장한 Dockerfile 넣기
    ports:
      - 9200:9200 # 9200번 포트에서 Elasticsearch 실행
    environment:
      # 아래 설정은 개발/테스트 환경에서 간단하게 테스트하기 위한 옵션 (운영 환경에서는 설정하면 안 됨)
      - discovery.type=single-node # 단일 노드 
      - xpack.security.enabled=false # 보안 설정
      - xpack.security.http.ssl.enabled=false # 보안 설정
  kibana:
    image: docker.elastic.co/kibana/kibana:8.17.4 # 8.17.4 버전
    ports:
      - 5601:5601 # 5601번 포트에서 kibana 실행
    environment:
      - ELASTICSEARCH_HOSTS=http://elastic:9200 # kibana에게 통신할 Elasticsearch 주소 알려주기</code></pre>
<p>이런식으로 바꿔서 컨테이너를 띄우고 Analyze API 활용해 디버깅 해보면</p>
<pre><code class="language-js">GET /_analyze
{
  &quot;text&quot;: &quot;백화점에서 쇼핑을 하다가 친구를 만났다.&quot;,
  &quot;analyzer&quot;: &quot;nori&quot;
}

## 위 아래 동일한 방법이다 (아래 방법은 Nori Analyze 와 동일한 애널라이저이다)

GET /_analyze
{
  &quot;text&quot;: &quot;백화점에서 쇼핑을 하다가 친구를 만났다.&quot;,
  &quot;char_filter&quot;: [], 
    &quot;tokenizer&quot;: &quot;nori_tokenizer&quot;, 
    &quot;filter&quot;: [&quot;nori_part_of_speech&quot;, &quot;nori_readingform&quot;, &quot;lowercase&quot;]
}
# `nori_part_of_speech` : 의미 없는 조사(`을`, `의` 등), 접속사 등을 제거
# `nori_readingform` : 한자를 한글로 바꿔서 토큰으로 저장</code></pre>
<p><code>백화</code> <code>점</code> <code>쇼핑</code> <code>하</code> <code>친구</code> <code>만나</code> 로 토큰이 저장되어 있다
이제 <code>백화점</code> <code>쇼핑</code> <code>친구</code> 키워드로 검새하면 잘 조회되는것을 확인할 수 있다</p>
<p>그러면 한글과 영어를 섞어서 검색하려면 어떻게 해야할까?</p>
<p>정답은 <code>Nori analyzer</code> 를 사용하고 필드값의 특징에 따라 
<code>character filter</code> 나 <code>token filter</code> 를 추가해주면 된다</p>
<p>ex) <code>Nori analyzer</code> 에 불용어 제거와  단어의 기본형태로 저장하기</p>
<pre><code class="language-js">// Nori analyzer의 구성을 직접 명시
GET /_analyze
{
  &quot;text&quot;: &quot;오늘 영어 책에서 &#39;It depends on the results.&#39;이라는 문구를 봤다.&quot;,
  &quot;char_filter&quot;: [], 
    &quot;tokenizer&quot;: &quot;nori_tokenizer&quot;, 
    &quot;filter&quot;: [&quot;nori_part_of_speech&quot;, &quot;nori_readingform&quot;, &quot;lowercase&quot;, &quot;stop&quot;, &quot;stemmer&quot;]
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Elasticsearch 기본개념]]></title>
            <link>https://velog.io/@choi-ju-yung/Elasticsearch-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@choi-ju-yung/Elasticsearch-%EA%B8%B0%EB%B3%B8%EA%B0%9C%EB%85%90</guid>
            <pubDate>Mon, 07 Jul 2025 14:33:20 GMT</pubDate>
            <description><![CDATA[<p>✅ <code>Elasticsearch</code></p>
<ul>
<li>RESTful 검색 및 분석 엔진, 데이터 분석에 최적화된 엔진</li>
</ul>
<p>✅ 용도</p>
<ul>
<li>주로 <code>Elasticsearch</code> 는 크게 데이터 수집 및 분석, 검색 최적화 이렇게 두가지 용도로 사용한다</li>
</ul>
<p><code>데이터 수집 및 분석</code> </p>
<ul>
<li>대규모 데이터를 수집 및 분석하는데 최적화</li>
<li>주로 데이터 저장(Elasticsearch), 데이터 수집 및 가공(Logstash), 데이터 시각화(Kibana) 를 활용해 데이터 수집 및 분석</li>
</ul>
<p><code>검색 최적화</code></p>
<ul>
<li>데이터가 많더라도 뛰어난 검색속도를 가지고 있음</li>
<li>오타나 동의어를 고려하여 유연하게 검색할 수 있는 기능을 가지고 있음</li>
</ul>
<hr>
<p>✅ <code>Elasticsearch</code> 작동 방식
일반 데이터베이스 (MySQL, ORACLE 등)는 SQL문으로 통신을 해야한다
하지만 <code>Elasticsearch</code> 데이터베이스는 <code>REST API</code> 방식으로 통신을 해야한다 (만든 사람 마음)</p>
<p>EX)  일반 DB에서 데이터 삽입과 조회할 때
<code>INSERT INTO users (name, email) VALUES (&#39;Alice&#39;, &#39;alice@example.com&#39;);</code>
<code>SELECT * FROM users;</code></p>
<p>EX) <code>Elasticsearch</code> 에서 데이터 삽입과, 조회할 때</p>
<pre><code class="language-sql">curl -X POST &quot;localhost:9200/users/_doc&quot; -H &#39;Content-Type: application/json&#39; -d&#39;
{
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;email&quot;: &quot;alice@example.com&quot;
}&#39;

curl -X GET &quot;localhost:9200/users/_search&quot; -H &#39;Content-Type: application/json&#39; -d&#39;
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  }
}&#39;</code></pre>
<p>일반 DB에서는 매번 CLI로 SQL문을 입력하기 불편해서 Dbeaver, Sql Developer 와 같은 GUI 툴을 활용한다
<code>Elasticsearch</code> 에서도 매번 API 요청을 보내기 불편하기 때문에   GUI 툴이 존재한다 -&gt; <code>Kibana</code></p>
<hr>
<p> 도커를 사용하면 <code>Elasticsearch</code> 와 <code>kibana</code> 컨테이너를  각각 띄워서 편하게 사용할 수 있다</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/ebdfc2ac-004c-4ef5-a883-dbeb0666f9a3/image.png" alt="">Kibana, Elasticsearch 아키텍처 -&gt; kibana를 사용하여 엘라스틱서치를 조작할 수 있음</p>
</blockquote>
<hr>
<p>✅ RDBMS와 Elasticsearch 차이</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/b8116034-094c-45af-b927-e20d45be612c/image.png" alt="">사진처럼 두개의 차이는 명칭에서 다르다
참고로 RDBMS의 인덱스와 Elasticsearch의 인덱스는 다르다</p>
</blockquote>
<hr>
<p>✅ <code>Elasticsearch</code> 에서 인덱스 생성 = RDBMS에서 테이블 생성</p>
<ul>
<li><p>인덱스 생성
<code>PUT /{인덱스명}</code>
EX) <code>PUT /users</code></p>
</li>
<li><p>인덱스 조회
<code>GET /{인덱스명}</code>
💡  되도록이면 뒤에 _search 를 붙여서 사용하자 -&gt; <code>GET /{인덱스명}/_search</code>
EX) <code>GET /users/_search</code></p>
</li>
</ul>
<p>생성하지 않은 인덱스를 조회하면 <code>index_not_found_exception</code> 발생</p>
<ul>
<li>인덱스 삭제
<code>DELETE /{인덱스명}</code>
EX) <code>DELETE /{users}</code>
생성하지 않은 인덱스를 삭제하면 <code>index_not_found_exception</code> 발생</li>
</ul>
<hr>
<p>✅ <code>Elasticsearch</code> 에서 <code>매핑</code> 을 정의 = RDBMS에서 테이블의 스키마 정의</p>
<ul>
<li>매핑 정의하기
💡 매핑을 정의하기 전에 <code>인덱스</code> 를 반드시 생성해야함
<code>PUT /{인덱스명}/_mappings</code><pre><code class="language-java">// EX
PUT /users/_mappings
{
&quot;properties&quot;: {
&quot;name&quot;: { &quot;type&quot;: &quot;keyword&quot; }, // 문자열
&quot;age&quot;: { &quot;type&quot;: &quot;integer&quot; }, // 정수형
&quot;is_active&quot;: { &quot;type&quot;: &quot;boolean&quot; } // 불린형
}
}</code></pre>
</li>
</ul>
<hr>
<p>✅ <code>Elasticsearch</code> 에서 <code>도큐먼트</code> 삽입 = RDBMS에서 테이블에 데이터를 삽입</p>
<ul>
<li>id를 자동으로 생성해서 도큐먼트 삽입하기
💡 <code>도큐먼트</code> 를 삽입하기전에 반드시 <code>인덱스 생성</code> 은 해야함
💡 <code>도큐먼트</code> 를 삽입하기전에 <code>매핑</code> 을 하지 않아도 자동으로 타입 추론하여 매핑을 생성함
-&gt; 의도하지 않은 타입 매핑일 생길 수 있기 때문에 되도록이면 <code>매핑</code> 한 후 <code>도큐먼트</code>를 삽입하자</li>
</ul>
<ul>
<li>도큐먼트 삽입방법 </li>
</ul>
<p>  아래와  같이  도큐먼트를 삽입하면  랜덤한 ID  값으로 도큐먼트가 삽입됨
  <code>POST /{인덱스명}/_doc</code></p>
<pre><code class="language-java">// 도큐먼트 저장 (랜덤 id 자동 생성)
 POST /users/_doc
{
 &quot;name&quot;: &quot;Alice&quot;,
 &quot;age&quot;: 28,
 &quot;is_active&quot;: true
}</code></pre>
<br>

<p>아래와 같이 맨 뒤에 ID 값을 직접 지정해서 도큐먼트 삽입하면 
ID 가 없으면 새로 생성, 이미 있으면 전체를 덮어씀
<code>PUT /{인덱스명}/_doc/{id}</code></p>
<pre><code class="language-java">// 도큐먼트 저장 및 업데이트
PUT /users/_doc/2
{
 &quot;name&quot;: &quot;jason&quot;,
 &quot;age&quot;: 30,
 &quot;is_active&quot;: true
}</code></pre>
<br>

<p><code>_create</code> 를 활용하여 ID를 직접 지정해서 도큐먼트를 삽입하면
해당 ID 값이 없으면 새로 생성 되지만
해당 ID 값이 없으면 (409 ) 에러가 발생
즉 중복생성이 불가능하다
<code>POST /{인덱스명}/_create/{id}</code></p>
<pre><code class="language-java">// 도큐먼트 저장 (id 직접 지정)
POST /users/_create/1
{
 &quot;name&quot;: &quot;jscode&quot;,
 &quot;age&quot;: 30,
 &quot;is_active&quot;: true
}</code></pre>
<p>💡 즉 <code>_doc</code> 와 <code>_create</code> 의 차이는 중복된 ID 값이 있으면, 덮어쓰거나, 에러가 난다</p>
<br>

<ul>
<li>도큐먼트 조회 방법
모든 도큐먼트 조회 : <code>GET /{인덱스명}/_search</code> = ex) <code>GET /users/_search</code>
id로 특정 도큐먼트 조회 : <code>GET /{인덱스명}/_doc/{id}</code> = ex) <code>GET /users/_doc/1</code></li>
</ul>
<br>

<ul>
<li>도큐먼트 수정 방법</li>
</ul>
<p>도큐먼트를 통째로 덮어쒸우기 = <code>PUT /{인덱스명}/_doc/{id}</code> = ex)</p>
<pre><code class="language-java">PUT /users/_doc/1
{
 &quot;name&quot;: &quot;new&quot;
}</code></pre>
<p>특정 도큐먼트만 수정하기 = <code># POST /{인덱스명}/_update/{id}</code> = ex )</p>
<pre><code class="language-java">POST /users/_update/2
{
 &quot;doc&quot;: {
 &quot;age&quot;: 10,
 &quot;is_active&quot;: false
 }
}</code></pre>
<br>

<ul>
<li>도큐먼트 삭제 방법
<code>DELETE /{인덱스명}/_doc/{id}</code>
ex) <code>DELETE /users/_doc/2</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis로 캐싱 성능조회]]></title>
            <link>https://velog.io/@choi-ju-yung/Redis%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EC%84%B1%EB%8A%A5%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@choi-ju-yung/Redis%EB%A1%9C-%EC%BA%90%EC%8B%B1-%EC%84%B1%EB%8A%A5%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Sun, 08 Jun 2025 13:00:52 GMT</pubDate>
            <description><![CDATA[<p>✅ 주의 해야할점</p>
<ul>
<li><code>Reids</code>에 객체를 저장하거나 읽을려면 항상 직렬화와 역직렬화 과정이 필요</li>
<li><blockquote>
<p><code>Redis</code> 가 이해할 수 있는 형태로 변환하기 위해서 </p>
</blockquote>
</li>
</ul>
<pre><code class="language-java">@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Board implements Serializable{
    private static final long serialVersionUID = 1L;
    private int boardNo;
    private String boardWriter;
    private String boardTitle;
    private String boardContent;
    private Date boardDate;
    private String boardCategory;
    private String noticeYn;
    private String boardOriginalFileName;
    private String boardRenamedFileName;
    private int rnum; // rownum 추가 필요
}</code></pre>
<ul>
<li><p><code>@EnableCaching</code> 을 사용하는 <code>@Configuration</code> 파일 만들기</p>
</li>
<li><blockquote>
<p>Spring이 내부적으로 캐시 관련 어노테이션(<code>@Cacheable, @CachePut, @CacheEvict</code> 등)
을 인식할 수 있도록 캐시 기능을 활성화</p>
</blockquote>
<pre><code class="language-java">@Configuration
@EnableCaching
public class RedisConfig {
   @Bean
   public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
       // 캐시의 기본 동작 방식을 설정하는 객체를 만들기
       RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 캐시에 저장할 값을 JSON 형태로 직렬화(변환)하도록 설정
               .serializeValuesWith(RedisSerializationContext.SerializationPair
                       .fromSerializer(new GenericJackson2JsonRedisSerializer()))
               .entryTtl(Duration.ofMinutes(10)) // 캐시의 유효기간을 10분으로 지정
               .disableCachingNullValues(); // // null 값(값이 없는 경우)은 캐시에 저장하지 않도록 설정

       // 위에서 만든 설정(config)과 Redis 연결 정보를 사용해서 RedisCacheManager 객체를 생성해서 반환
       return RedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();
   }
}</code></pre>
</li>
<li><p><code>@Cacheable</code> 활용해서 <code>Redis</code> 캐싱 적용</p>
<pre><code class="language-java">  @Cacheable(value = &quot;boardList&quot;,key = &quot;&#39;allBoards&#39;&quot;) // api로 redis 전체 데이터 호출 테스트
  public List&lt;Board&gt; getBoardList(){
      System.out.println(&quot;db안거침&quot;);
      return boardDao.getBoardList();
  }

  // api로 redis 검색 데이터 호출 테스트
  @Cacheable(value = &quot;boardList&quot;, key = &quot;&#39;category:&#39; + #category&quot;) 
  public List&lt;Board&gt; getBoardListByCategory(String category) {
      System.out.println(&quot;db안거침&quot;);
      return boardDao.getBoardListByCategory(category);
  }</code></pre>
</li>
</ul>
<hr>
<p>✅ 성능 비교
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/4c0e2d63-d01f-4de5-9ef1-f1bb903ee3fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/265c5fc6-6289-41dc-9aaa-20f5a31cfd7b/image.png" alt=""></p>
<hr>
<p>도커로 <code>Redis</code> 접속 후 레디스 접속하기
<code>docker exec -it [컨테이너명] redis-cli</code>
<code>docker exec -it e4d redis-cli</code> </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis]]></title>
            <link>https://velog.io/@choi-ju-yung/Redis</link>
            <guid>https://velog.io/@choi-ju-yung/Redis</guid>
            <pubDate>Thu, 05 Jun 2025 04:42:46 GMT</pubDate>
            <description><![CDATA[<p>✅ <code>Redis</code> 란?</p>
<ul>
<li><p>Remote Dictionary Server의 약자로, <code>키-값</code> 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터베이스 관리 시스템 (DBMS) 이다</p>
</li>
<li><p>쉽게 말해서 데이터 처리  속도가 빠른 <code>NoSQL</code> (비관계형 데이터베이스)</p>
</li>
</ul>
<p>Redis는 메모리 안에 모든 데이터를 저장하기 때문에 데이터의 처리 성능이  빠르다는 장점이 있다
-&gt; 관계형 데이터베이스들은 대부분 <code>디스크</code> 에 데이터를 저장하고 Redis는 <code>메모리</code> 에  데이터를 저장한다
-&gt; <code>메모리</code> 가 <code>디스크</code> 보다 데이터 처리속도가 빠르기 때문에 Redis 의 처리 성능이 빠르다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/cf3eb203-7b57-459f-a827-f6f21118f9ee/image.png" alt=""></p>
<p>✅  Redis 주요  사례
<code>캐싱</code> <code>세션관리</code> <code>실시간 분석 및 통계</code> <code>메시지 큐</code> <code>지리공간 인덱싱</code> <code>속도 제한</code> <code>실시간 채팅 및 메시징</code></p>
<p><code>캐싱</code> (데이터 조회 성능 향상)</p>
<hr>
<p>✅ Redis 기본 명령어</p>
<ul>
<li><p>데이터 저장
<code>set [key 이름] [value]</code>
ex) <code>set jooyoung:name &quot;jooyoung choi&quot;</code>  -&gt; value 값이 띄어쓰기가 있으면 쌍따옴표로 묶어주기
ex) <code>set jooyoung:hobby netflix</code></p>
</li>
<li><p>데이터 조회
<code>get [key 이름]</code>
ex) <code>get jooyoung:hobby</code> 
만약 없는 데이터를 조회하면 <code>nill</code> 이라고 출력됨</p>
<ul>
<li>저장된 모든 key 조회
<code>keys *</code></li>
</ul>
</li>
<li><p>key로 데이터 삭제
<code>del [key 이름]</code>
ex) <code>del jooyoung:hobby</code></p>
</li>
<li><p>모든 데이터 삭제
<code>flushall</code></p>
</li>
<li><p>데이터 저장시 만료시간 TTL(TIME TO LIVE) 정하기
<code>set [key 이름] [value] ex [만료시간(초]</code>
ex) <code>set jooyoung:hobby netflix ex 30</code>
Redis는 특성상 메모리 공간이 한정적이기 때문에, 모든 데이터를 레디스에 저장할 수 없다
따라서 만료시간을 활용해서 자주 사용하는 데이터만 레디스에 저장하고 
그렇지 않은 데이터는 일정 시간이 되면 데이터가 삭제되도록 셋팅한다</p>
</li>
<li><p>만료시간 (TTL) 확인
<code>ttl [key 이름]</code>
ex) <code>ttl jooyoung:hobby</code>
만료시간이 몇초 남았는지 반환
키는 존재하지만 만료설정이 되어있지 않은 데이터는 -1을 반환
키가 없는 경우와 만료되어서 키가 없는 경우 -2를 반환</p>
</li>
</ul>
<hr>
<p>✅ Key 네이밍 컨벤션</p>
<p>주로 콜론 <code>:</code>  을 활용해서 계층적으로 의미를 구분</p>
<p>ex) <code>users:100:profile</code> : 사용자들 중에서 PK가 100인 사용자의 프로필
<code>products:123:details</code> : 상품들 중에서 PK가 123인 상품의 세부사항</p>
<hr>
<p>✅ 캐시와 캐싱</p>
<p><code>캐시</code> 란? -&gt; 원본 저장소보다 빠르게 가져올 수 있는 임시 데이터 저장소</p>
<p><code>캐싱</code> ? -&gt; 캐시(임시 데이터 저장소) 에 접근해서 데이터를 빠르게 가져오는 방식</p>
<hr>
<p>✅ 캐싱 전략 <code>Cache-Aside</code>  <code>Write-Around</code> </p>
<p><code>Cache Aside</code> 전략은 데이터를 어떻게 조회할 지에 대한 전략임
<code>Cache-Aside</code> 의 동작원리는 
<code>Catch Hit</code> 와 <code>Catch Miss</code> 만 알아두면 된다</p>
<p><code>Catch Hit</code> 는 요청한 데이터가 캐시에 이미 존재하는 경우를 말한다 
이 경우 DB조회를 생략하고 빠르게 응답이 가능하다</p>
<p><code>Cache Miss</code> 는 요청한 데이터가 캐시에 없는 경우를 말한다
이 경우 캐시에 데이터가 없기 때문에 DB에 가서 데이터를 받아온 후
받은 데이터를 캐시에 저장하여 다음 요청부터는 Catch Hit를 유도 한다</p>
<p>한 문장으로 요약하면 
<code>Cache Aside</code> 전략은 캐시(Cache)에서 데이터를 확인하고, 없다면 DB를 통해 조회해오는 방식이다</p>
<p><img src="https://velog.velcdn.com/images/choi-ju-yung/post/cbaf6d25-1ff0-42b6-a3d5-191ddec004a1/image.png" alt=""></p>
<p><code>Write-Around</code> 방식은 데이터를 어떻게 사용(저장, 수정, 삭제) 할지에 대한 전략
이 방식은 데이터를 조회할 때는 위 <code>Cache Aside</code>  방식과 동일하지만
데이터를 저장할 때는 캐시에 저장하지 않고 데이터베이스에만 저장하는  방식이다</p>
<p>한마디로 정리하면
<code>Write Around 전략</code> 은 쓰기 작업(저장, 수정, 삭제)을 캐시에는 반영하지 않고, DB에만 반영하는 방식
<img src="https://velog.velcdn.com/images/choi-ju-yung/post/06c1f9a9-c85e-4785-859b-dea409537ae6/image.png" alt=""></p>
<p>현업에서는 <code>Cache Aside</code> 전략과 <code>Write-Around</code> 전략을 같이 사용한다
하지만 같이 사용했을 경우 <code>한계점</code> 이 존재한다</p>
<p>한계점 (1)  캐시 데이터와 DB 데이터가 일치하지 않을 수가 있다</p>
<p><code>Write Around</code> 전략을 사용하면 데이터를 수정할 때 DB 데이터만 업데이트 하기 때문이다
그럼 데이터를 수정할 때 캐시데이터와 DB데이터 둘 다 업데이트 하면 되지 않나? 라고 하면
성능이 더 느려지기 때문에 할 수가 없다</p>
<p>이러한 이유로 캐시를 적용시키기 적절한 데이터는 
자주 조회되는 데이터, 잘 변하지 않는 데이터, 실시간으로 정확하게 일치하지 않아도 되는 데이터 등의 조건에 맞는 것을 선택해주는것이 좋다</p>
<p>하지만 이럼에도 문제가 될수 있기 때문에 이러한 <code>해결방법</code> 으로 
Redis의 <code>TTL</code> (만료시간 설정 기능)을 사용한다. 일정 시간이 지나 데이터가 삭제되면
특정 사용자가 조회를 하는  순간 <code>Cache Miss</code> 가 발생해서 DB 데이터를 새로 조회하여
캐시에 데이터를 넣게 된다. 즉 데이터가 새롭게 갱신되는 효과가 발생한다</p>
<p>한계점 (2)  캐시에 저장할 수 있는 공간이 비교적 작다
이 문제점 역시 <code>TTL</code> 기능을 사용하면 캐시의 공간을 효율적으로 사용할 수 있다
자주 사용하지 않은 데이터는 만료 시간에 의해 데이터가 삭제되기 때문이다</p>
<p>정리하면 
<code>Cache Aside</code> , <code>Write Around</code> 전략을 사용할 때 주로 TTL을 같이 활용해야 한다    </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 시큐리티 로그아웃 기능]]></title>
            <link>https://velog.io/@choi-ju-yung/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@choi-ju-yung/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Thu, 29 May 2025 07:59:14 GMT</pubDate>
            <description><![CDATA[<p>✅ 스프링 시큐리티 로그아웃 기능 이란?</p>
<ul>
<li><p><code>인증</code> 된 사용자가 로그아웃 요청을 했을 때 
(서버세션 무효화, 인증정보와 쿠키 삭제, <code>SecurityContext</code> 도 비워주는) 보안 기능이다</p>
</li>
<li><p>이 과정을 통해 사용자는 안전하게 로그아웃 되고, 이후에는 인증이 필요한 리소스에 접근 불가능</p>
</li>
</ul>
<p><code>SecurityContext</code> :  현재 인증된 사용자의 <code>정보</code> 와 <code>권한</code> 을 담고 있는 컨테이너(저장소)</p>
<hr>
<p>✅ 기본적인 코드 구조</p>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout()
            .logoutUrl(&quot;/logout&quot;) // 로그아웃 처리 URL
            .logoutSuccessUrl(&quot;/login&quot;) // 로그아웃 성공 후 이동할 URL
            .invalidateHttpSession(true) // 세션 무효화
            .deleteCookies(&quot;JSESSIONID&quot;, &quot;remember-me&quot;) // 쿠키 삭제
            .addLogoutHandler(logoutHandler()) // 추가 로그아웃 핸들러(선택)
            .logoutSuccessHandler(logoutSuccessHandler()); // 커스텀 성공 핸들러(선택)
    }
}</code></pre>
<p><code>logoutUrl(&quot;/logout&quot;)</code> : 로그아웃 요청을 받을 URL 지정</p>
<p><code>logoutSuccessUrl(&quot;/login&quot;)</code> : 로그아웃 후 이동할 URL 지정</p>
<p><code>invalidateHttpSession(true)</code> : 세션 무효화</p>
<p><code>deleteCookies(...)</code> : 쿠키 삭제</p>
<p><code>addLogoutHandler(...)</code> : 커스텀 핸들러 추가(선택)</p>
<p><code>logoutSuccessHandler(...)</code> : 로그아웃 성공 시 실행할 핸들러(선택)</p>
<p>여기서 커스텀 로그아웃 핸들러는 따로 작성하면 된다</p>
<pre><code class="language-java">.addLogoutHandler((request, response, authentication) -&gt; {
    // 추가 로그아웃 작업
})
.logoutSuccessHandler((request, response, authentication) -&gt; {
    response.sendRedirect(&quot;/login&quot;);
})</code></pre>
<hr>
<p>✅ Oauth 로그아웃과 일반 로그아웃 함께 구현</p>
<pre><code class="language-java">.logout(logout -&gt; logout
            .logoutUrl(&quot;/logout&quot;) // 로그아웃을 수행할 엔드포인트 URL을 지정 (사용자가 /logout으로 접속하면 이 설정이 실행)
            .logoutSuccessHandler((request, response, authentication) -&gt; { // 로그아웃이 성공적으로 처리된 후 실행되는 직접 구현한 로직

        // 현재 세션 종료 및 인증 해제
            HttpSession session = request.getSession(false);
                if (session != null) { // 현재 세션이 존재하면
                    session.invalidate();  // 세션을 강제로 무효화
                }

            SecurityContextHolder.clearContext(); // Spring Security의 SecurityContext (로그인 정보 저장소) 를 초기화

            // 로그아웃 후 리다이렉트 주소에 사용하기 위해 서버의 주소를 동적으로 생성
            String currentHost = request.getScheme() + &quot;://&quot; + request.getServerName() + &quot;:&quot; + request.getServerPort();

                 // 인증이 null인 경우 → 로그인 상태 아님 → 그냥 로그인 페이지로 보냄
                if (authentication == null) {
                        response.sendRedirect(currentHost + &quot;/login&quot;); // /login 페이지로 리다이렉트
                        return;
                }

                // OAuth 로그인인지 확인 (카카오/구글 같은 OAuth2 로그인 사용자일 경우 true)
                boolean isOauthUser = authentication.getPrincipal() instanceof org.springframework.security.oauth2.core.user.DefaultOAuth2User;

                if (isOauthUser) {
                    // 🔒 카카오 로그아웃 URL로 리다이렉트
                    String kakaoLogoutUrl = &quot;https://kauth.kakao.com/oauth/logout&quot; +
                                            &quot;?client_id=&quot; + KakaoConfig.CLIENT_ID +
                                            &quot;&amp;logout_redirect_uri=&quot; + URLEncoder.encode(currentHost + &quot;/login&quot;, &quot;UTF-8&quot;);
                                    response.sendRedirect(kakaoLogoutUrl);
                 } else {
                         response.sendRedirect(currentHost + &quot;/login&quot;); // 일반 로그인 사용자 → 일반 로그아웃 처리
                    }

                    })
                        .invalidateHttpSession(true) // true -&gt; 로그아웃 시 세션을 자동으로 무효화 (위에서 수동으로 했지만 이중으로 함)
                        .deleteCookies(&quot;JSESSIONID&quot;) // 로그아웃할 때 클라이언트에 저장된 세션 쿠키(JSESSIONID) 도 삭제 (보안을 위해서)
                        .permitAll()  // 누구나 접근 가능 
                    );</code></pre>
]]></description>
        </item>
    </channel>
</rss>