<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>GohRuth</title>
        <link>https://velog.io/</link>
        <description>Backend Developer</description>
        <lastBuildDate>Sun, 29 Jun 2025 09:03:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>GohRuth</title>
            <url>https://velog.velcdn.com/images/go_ruth/profile/274ef601-10b1-4792-b3bc-64be291aaca5/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. GohRuth. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/go_ruth" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[ArrayList.contains() vs HashSet.contains()]]></title>
            <link>https://velog.io/@go_ruth/ArrayList.contains-vs-HashSet.contains</link>
            <guid>https://velog.io/@go_ruth/ArrayList.contains-vs-HashSet.contains</guid>
            <pubDate>Sun, 29 Jun 2025 09:03:22 GMT</pubDate>
            <description><![CDATA[<h1 id="1-백준-알고리즘">1. 백준 알고리즘</h1>
<h2 id="11-문제">1.1. 문제</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c3510c4d-bc5f-4336-ae07-105722fb6e66/image.png" alt=""></p>
<h2 id="12-문제설명">1.2. 문제설명</h2>
<p>이번 문제는</p>
<blockquote>
<ol>
<li>출력값을 나온 DFS 결과가 올바른 순서 여부 판단</li>
</ol>
</blockquote>
<p>하는 문제였습니다.</p>
<p>💡 여기서 주의할 점!</p>
<ul>
<li>입력 순서대로 진행되지 않아도 된다는 것입니다. (예제 2, 3)</li>
</ul>
<h2 id="13-접근법">1.3. 접근법</h2>
<ol>
<li>Stack을 통해 DFS 로직 구현</li>
<li><code>st.hasNextTokens()</code>을 통해, 예상 Index 파악</li>
<li><code>contains()</code>을 통해, 존재 여부 판단 + count 함수를 통한 접근 제어</li>
</ol>
<h2 id="14-list--contains">1.4. List + contains()</h2>
<h3 id="141-코드">1.4.1. 코드</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
  static class Node {
    int idx, count;

    Node(int idx, int count) {
      this.idx = idx;
      this.count = count;
    }
  }

  public static void main(String[] args) throws Exception {
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    int seq = Integer.parseInt(br.readLine());
    List&lt;List&lt;Integer&gt;&gt; list = new ArrayList&lt;&gt;();

    for(int i = 0; i &lt; seq + 1; i++) {
      list.add(new ArrayList&lt;&gt;());
    }
    StringTokenizer st;
    for(int i = 0; i &lt; seq - 1; i++) {
      st = new StringTokenizer(br.readLine());
      int a = Integer.parseInt(st.nextToken());
      int b = Integer.parseInt(st.nextToken());
      list.get(a).add(b);
      list.get(b).add(a);
    }

    boolean flag = false;
    ArrayDeque&lt;Node&gt; stack = new ArrayDeque&lt;&gt;();
    st = new StringTokenizer(br.readLine());
    if (Integer.parseInt(st.nextToken()) == 1) {
      stack.push(new Node(1, 0));

      while(st.hasMoreTokens()) {
        Node node = stack.peek();

        if(list.get(node.idx).size() &lt;= node.count) {
          stack.pop();
          continue;
        }

        int nextIdx = Integer.parseInt(st.nextToken());

        if (list.get(node.idx).contains(nextIdx)) {
          node.count++;
          stack.push(new Node(nextIdx, 1));
          continue;
        }

        flag = true;
        break;
      }

      System.out.println(!flag ? 1 : 0);
    } else {
      System.out.println(0);
    }
  }
}</code></pre>
<h3 id="142-결과">1.4.2. 결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c3544a88-9f6e-42c7-8385-21d784544579/image.png" alt=""></p>
<h3 id="143-시간초과-원인">1.4.3. 시간초과 원인</h3>
<ul>
<li>ArrayList는 배열 기반 순차 탐색으로, 시간 복잡도가 <code>O(N)</code>으로 비효율적</li>
<li>즉, 최악으로는 <code>O(N^2)</code>만큼 진행, 정점은 100,000까지 가능</li>
<li>100,000 * 100,000 =&gt; 무조건 2초 이상 =&gt; 시간 초과</li>
</ul>
<h3 id="144-해결방안">1.4.4. 해결방안</h3>
<ul>
<li>HashSet의 <code>contains()</code>는 시간 복잡도가 평균 <code>O(1)</code>, 최악일 때만 <code>O(N)</code>으로 효율적</li>
</ul>
<h2 id="15-set--contains">1.5. Set + contains()</h2>
<h3 id="151-코드">1.5.1. 코드</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
  static class Node {
    int idx, count;

    Node(int idx, int count) {
      this.idx = idx;
      this.count = count;
    }
  }

  public static void main(String[] args) throws Exception {
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    int seq = Integer.parseInt(br.readLine());
    List&lt;Set&lt;Integer&gt;&gt; list = new ArrayList&lt;&gt;();

    for(int i = 0; i &lt; seq + 1; i++) {
      list.add(new HashSet&lt;&gt;());
    }
    StringTokenizer st;
    for(int i = 0; i &lt; seq - 1; i++) {
      st = new StringTokenizer(br.readLine());
      int a = Integer.parseInt(st.nextToken());
      int b = Integer.parseInt(st.nextToken());
      list.get(a).add(b);
      list.get(b).add(a);
    }

    boolean flag = false;
    ArrayDeque&lt;Node&gt; stack = new ArrayDeque&lt;&gt;();
    st = new StringTokenizer(br.readLine());
    if (Integer.parseInt(st.nextToken()) == 1) {
      stack.push(new Node(1, 0));

      while(st.hasMoreTokens()) {
        Node node = stack.peek();

        if(list.get(node.idx).size() &lt;= node.count) {
          stack.pop();
          continue;
        }

        int nextIdx = Integer.parseInt(st.nextToken());

        if (list.get(node.idx).contains(nextIdx)) {
          node.count++;
          stack.push(new Node(nextIdx, 1));
          continue;
        }

        flag = true;
        break;
      }

      System.out.println(!flag ? 1 : 0);
    } else {
      System.out.println(0);
    }
  }
}</code></pre>
<h3 id="152-결과">1.5.2. 결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/a868314b-b224-452a-9c6e-dfda43ecc335/image.png" alt=""></p>
<hr>
<h2 id="2-arraylist와-hashset의-contains">2. ArrayList와 HashSet의 contains()</h2>
<h3 id="21-왜-hashset이-빠름">2.1. 왜 HashSet이 빠름?</h3>
<ul>
<li>HashSet은 내부적으로 HashMap을 사용.</li>
<li>객체의 <code>hashCode()</code>를 기반으로 해시 버킷을 찾아가고, 그 안에서 <code>equals()</code>로 최종 비교.</li>
<li>해시 충돌이 적고 <code>hashCode()</code> 구현이 잘 되어 있으면 평균 O(1)의 성능을 보장함.</li>
</ul>
<h3 id="22-list-hashset-정리-표">2.2. List, HashSet 정리 표</h3>
<table>
<thead>
<tr>
<th>자료구조</th>
<th>contains() 시간복잡도</th>
<th>내부 구조</th>
</tr>
</thead>
<tbody><tr>
<td>List</td>
<td>O(N)</td>
<td>배열 기반 순차 탐색 (ArrayList)</td>
</tr>
<tr>
<td>HashSet</td>
<td>평균 O(1) / 최악 O(N)</td>
<td>Hash Table 기반, key의 hashCode와 equals로 탐색</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-a--hashcode는-어디서-정의되어-있을까">3. +a : <code>hashCode()</code>는 어디서 정의되어 있을까?</h2>
<h3 id="31-hashcode">3.1. <code>HashCode()</code>?</h3>
<ul>
<li><code>hashCode()</code> Object 클래스에 정의된 메소드로, 객체를 식별하는 정수형 해시값(int)을 반환</li>
<li>객체 일치 ⭕<ul>
<li>항상 같은 hashCode를 반환</li>
</ul>
</li>
<li>객체 일치 ❌ <ul>
<li>가능하면 서로 다른 값을 반환하는 것이 이상적</li>
</ul>
</li>
</ul>
<h3 id="32-왜-필요할까">3.2. 왜 필요할까?</h3>
<ul>
<li>해시 자료구조에서 탐색 성능을 향상시키기 위해!</li>
</ul>
<h3 id="33-object내의-hashcode-함수">3.3. Object내의 hashCode 함수</h3>
<p>자바의 모든 객체는 <code>Object</code> 클래스를 상속, hashCode함수는 아래와 같이 정의되어 있음</p>
<pre><code class="language-java">public native int hashCode();</code></pre>
<h3 id="34-native-키워드">3.4. native 키워드?</h3>
<ul>
<li>Java 코드가 아닌 <strong>C/C++로 구현된 메서드</strong>임을 나타냄.</li>
<li>JVM 내부의 구현체와 연결되어 있고, 자바에서는 바디가 없음.</li>
<li>대표적인 메서드: <code>hashCode()</code>, <code>wait()</code>, <code>notify()</code>, <code>clone()</code> 등</li>
</ul>
<h3 id="35-why-native로-구현">3.5. Why native로 구현?</h3>
<ul>
<li>성능이 중요한 로직 (ex. 해시 연산)</li>
<li>운영체제 또는 메모리 관리 등 플랫폼 의존 기능 호출</li>
<li>JVM의 core 기능과 밀접한 부분이기 때문에 자바 외부에서 구현됨</li>
</ul>
<hr>
<h2 id="4-느낀-점--깨달은-점">4. 느낀 점 + 깨달은 점</h2>
<ol>
<li><strong>contains의 시간복잡도</strong> </li>
</ol>
<p>-&gt; 각 자료구조에 따라서 <code>contains()</code>의 시간복잡도가 다르다는 걸 까달음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSAFY) 실시간 채팅 구축 2 - Redis]]></title>
            <link>https://velog.io/@go_ruth/SSAFY-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%EC%B6%95-2-Redis</link>
            <guid>https://velog.io/@go_ruth/SSAFY-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%EC%B6%95-2-Redis</guid>
            <pubDate>Fri, 27 Jun 2025 09:05:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 시리즈는 채팅 시스템을 구축하면서 겪은 경험을 정리한 기술 회고로,
해당 글은 채팅에서 RDB 기반 채팅 시스템에 Redis를 추가 및 구조 변경에 대해 다룹니다.</p>
</blockquote>
<h1 id="redis-vs-mysql-rdb">Redis vs MySQL (RDB)</h1>
<h2 id="mysql-기반-시스템의-특징">MySQL 기반 시스템의 특징</h2>
<ul>
<li>모든 메시지를 chat_message 테이블에 시간순으로 저장</li>
<li>각 사용자의 읽음 여부는 read_point로 관리</li>
<li>채팅방 마지막 메시지, 읽지 않은 메시지 수 등을 매 요청마다 쿼리로 처리</li>
<li>실시간 수신은 WebSocket + STOMP로 처리<h2 id="문제점">문제점</h2>
</li>
</ul>
<ol>
<li>메시지 수가 많아질수록 조회 쿼리의 부하가 커짐</li>
<li>최근 메시지 + 읽음 정보 + 마지막 메시지를 한 번에 처리하기 위한 조인이 복잡함</li>
<li>수십 개 채팅방에 대해 동시에 실시간 정보를 갱신할 때 성능 저하 발생<h2 id="redis-기반-구조의-기대-효과">Redis 기반 구조의 기대 효과</h2>
</li>
<li>실시간성이 중요한 채팅에 적합한 In-Memory 처리</li>
<li>채팅방별 최신 메시지, 읽음 여부, 안 읽은 메시지 수 등을 ZSet, Hash, Pub/Sub으로 빠르게 관리 가능</li>
<li>빈번한 읽기/쓰기에도 빠른 처리 속도 보장</li>
</ol>
<h1 id="redis-적용-로직">Redis 적용 로직</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/5ec3f0d1-7b16-4f08-ac6b-76e4d4dc5213/image.png" alt=""></p>
<h1 id="변경-사항">변경 사항</h1>
<ol>
<li>메시지 저장<ul>
<li>메시지 수신 시 RDB뿐 아니라 Redis에도 저장</li>
<li>Redis ZSet으로 채팅 메시지를 시간 기준 정렬 저장</li>
<li>Redis Hash로 각 채팅방 유저들의 읽은 시간 관리</li>
</ul>
</li>
<li>읽음 처리<ul>
<li>사용자별 마지막 읽은 메시지 index를 Redis에 저장</li>
<li>특정 메시지 index를 기준으로 ZSet에서 count하여 안 읽은 메시지 수 계산</li>
</ul>
</li>
<li>채팅방 리스트 조회<ul>
<li>Redis에서 각 채팅방의 마지막 메시지, 안 읽은 수, 시간 정보 등 조회 RDB 쿼리 없이 바로 응답 가능</li>
</ul>
</li>
</ol>
<h1 id="한계">한계</h1>
<ol>
<li>Spring을 재시작하면 Redis 데이터가 초기화되는 문제가 발생했습니다.</li>
<li>채팅하는 과정에서 Redis와 RDB에 둘 다 동기적으로 입력을 진행하므로, 채팅하는 과정이 비효율적으로 진행됩니다.</li>
</ol>
<h1 id="next">Next</h1>
<p>Redis를 통한 채팅을 구현했으니, Redis Stream을 통한 비동기 저장을 구현하여, 채팅 과정에서의 속도를 높여보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바에서의 비트마스킹 + 비트 Set]]></title>
            <link>https://velog.io/@go_ruth/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%ED%8A%B8%EB%A7%88%EC%8A%A4%ED%82%B9-%EB%B9%84%ED%8A%B8-Set</link>
            <guid>https://velog.io/@go_ruth/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%ED%8A%B8%EB%A7%88%EC%8A%A4%ED%82%B9-%EB%B9%84%ED%8A%B8-Set</guid>
            <pubDate>Sun, 22 Jun 2025 08:17:28 GMT</pubDate>
            <description><![CDATA[<h1 id="bit">Bit</h1>
<h2 id="개념">개념</h2>
<ul>
<li>컴퓨터에서 비트(bit)는 0 또는 1의 값을 가지는 가장 작은 단위다. </li>
<li>여러 개의 비트가 모여 바이트(Byte), 워드(Word) 등을 구성<h2 id="기본-문법-및-예시">기본 문법 및 예시</h2>
</li>
</ul>
<ol>
<li><strong>(&amp; : AND)</strong> </li>
</ol>
<p>-&gt; <strong>1100 &amp; 0110 :</strong> 0100
2. <strong>(| : OR)</strong> 
-&gt; <strong>1100 | 0110 :</strong> 1110
3. <strong>(^ : XOR)</strong> 
-&gt; <strong>1100 ^ 0110 :</strong> 1010
4. <strong>(~ : NOT)</strong> 
-&gt; <strong>~1100 :</strong> 0011
5. <strong>(&lt;&lt; : Left Shift)</strong> 
-&gt; <strong>1100 &lt;&lt; 1 :</strong> 11000
6. <strong>(&gt;&gt; : Right Shift)</strong> 
-&gt; <strong>1100 &gt;&gt; 1 :</strong> 110
7. <strong>(&gt;&gt;&gt; : 부호 없이 Right Shift)</strong> 
-&gt; <strong>1100 &gt;&gt;&gt; 1 :</strong> 0110</p>
<h2 id="비트-연산을-알고리즘에서-사용하는-이유">비트 연산을 알고리즘에서 사용하는 이유</h2>
<ul>
<li>비트 연산은 CPU 수준에서 수행<ul>
<li>매우 빠름</li>
<li>메모리도 훨씬 적게 사용</li>
</ul>
</li>
</ul>
<hr>
<h1 id="bit-masking">Bit Masking</h1>
<h2 id="개념-1">개념</h2>
<ul>
<li>비트마스킹은 하나의 정수형 변수(int, long 등)에 여러 상태를 저장</li>
<li>각 비트는 하나의 플래그(flag)를 의미, 비트 연산을 통해 값을 설정하거나 확인<h2 id="장단점">장단점</h2>
<h3 id="장점">장점</h3>
</li>
</ul>
<ol>
<li>메모리 사용이 매우 적음 (비트 단위)</li>
<li>연산 속도가 매우 빠름 (O(1))<h3 id="단점">단점</h3>
</li>
<li>범위가 크면 long으로도 부족함</li>
<li>가독성이 낮고, 디버깅이 어려움<h3 id="활용할-때">활용할 때</h3>
</li>
</ol>
<ul>
<li>32개 이하의 상태라면 int 하나로 충분</li>
<li>64개까지는 long, 그 이상은 BitSet 고려</li>
</ul>
<h2 id="문제를-활용한-사용-예시">문제를 활용한 사용 예시</h2>
<ul>
<li>가르침 : <a href="https://www.acmicpc.net/problem/1062">https://www.acmicpc.net/problem/1062</a><ul>
<li>알파벳에 중복여부를 비트마스킹으로 check</li>
</ul>
</li>
<li>성곽 : <a href="https://www.acmicpc.net/problem/2234">https://www.acmicpc.net/problem/2234</a><ul>
<li>벽을 비트마스킹으로 setting</li>
</ul>
</li>
</ul>
<hr>
<h1 id="bit-set">Bit Set</h1>
<h2 id="개념-2">개념</h2>
<ul>
<li>Java의 java.util.BitSet은 가변 크기의 비트 배열을 제공하는 표준 클래스</li>
<li>내부적으로 long 배열을 사용, 인덱스로 각 비트를 다룸<h2 id="기본-문법-및-예시-1">기본 문법 및 예시</h2>
<pre><code class="language-java">// 생성
BitSet bs1 = new BitSet();        // 기본크기
BitSet bs2 = new BitSet(1000000); // 초기 크기 지정
</code></pre>
</li>
</ul>
<p>// 선택 설정
bs1.set(5);       // 5번 비트를 1로 설정
bs1.clear(5);     // 5번 비트를 0으로 설정
bs1.flip(5);      // 5번 비트를 반전 (0이면 1, 1이면 0)</p>
<p>// 전체 설정
bs1.clear();      // 전체 비트를 0으로 초기화
bs1.flip(0, 10);  // 0번부터 9번까지 비트를 반전</p>
<p>// 조회
boolean on = bs1.get(5);  // 5번 비트가 켜져 있는지 확인</p>
<p>// 논리연산
bs1.and(bs2);      // AND 연산 (교집합)
bs1.or(bs2);       // OR 연산 (합집합)
bs1.xor(bs2);      // XOR 연산 (차집합)
bs1.andNot(bs2);   // 차집합 a - b</p>
<p>int i = bs1.nextSetBit(0);            // 0번 인덱스부터 처음 1인 비트 위치
int cardinality = bs1.cardinality();  // 켜진 비트의 수
int length = bs1.length();            // 가장 높은 1비트 + 1 위치
int size = bs1.size();                // 내부 배열의 크기 (최대 인덱스 아님)</p>
<pre><code>## 장단점
### 장점
1. 크기 제약 없이 수백만 개 상태 표현 가능
2. Java 표준 API로 사용이 직관적이고 안정적
3. HashSet보다 훨씬 적은 메모리 사용
### 단점
1. 비트마스킹보다 다소 느릴 수 있음
2. synchronized가 아니므로 멀티스레드 환경에 주의 필요
### 활용할 때
1. 데이터가 수천~수백만 개 이상이면서 중복 여부만 체크할 때
2. 비트마스킹보다 범용성이나 가독성이 중요할 때

## 문제를 활용한 사용예시
- 중복제거 : https://www.acmicpc.net/problem/13701
  - BitSet을 활용해서, 중복없이 순서에 맞춘 출력 구현

# HashSet vs 비트마스킹 vs BitSet
| 방법 | 메모리 사용량 | 처리 속도 |
| --- | --- | --- |
| HashSet | 높음 | 느림 |
| BitSet | 낮음 | 빠름 |
| 비트마스킹 | 가장 낮음 | 가장 빠름 (단 범위 제한 있음) |</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Comparable vs Comparator]]></title>
            <link>https://velog.io/@go_ruth/Comparable-vs-Comparator</link>
            <guid>https://velog.io/@go_ruth/Comparable-vs-Comparator</guid>
            <pubDate>Sat, 14 Jun 2025 03:04:04 GMT</pubDate>
            <description><![CDATA[<h2 id="1-자바에서의-정렬방식">1. 자바에서의 정렬방식</h2>
<p>Java에서 정렬 기준을 정의하는 방법은 두 가지.</p>
<ul>
<li><strong><code>Comparable</code> 인터페이스 (<code>Comparable&lt;T&gt;</code>)</strong><ul>
<li>자기 자신이 정렬 기준 </li>
<li>클래스 내부에 <code>compareTo()</code> 메서드를 구현해 정렬 기준을 정의.</li>
</ul>
</li>
<li><strong><code>Comparator</code> 인터페이스 (<code>Comparator&lt;T&gt;</code>)</strong><ul>
<li>외부에서 정렬 기준을 주입. </li>
<li>주로 익명 클래스나 람다식으로 사용. </li>
</ul>
</li>
</ul>
<h2 id="2-동작-과정">2. 동작 과정</h2>
<p>자바의 정렬 메커니즘은 객체 간의 우선순위를 비교하여 결정하는 방식으로 작동. </p>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/ec42919e-a599-4314-8562-4186bf121020/image.png" alt=""></p>
<h2 id="3-comparable--comparator">3. Comparable &amp; Comparator</h2>
<h3 id="comparable">Comparable</h3>
<ul>
<li>객체가 Comparable<T> 인터페이스를 구현하고 있다면, compareTo() 메서드가 객체 간 비교에 사용됨</li>
<li>ex) PriorityQueue, Collections.sort() 등은 내부에서 자동으로 compareTo()를 호출해 순서 결정</li>
<li>내부적으로는 힙 구성, 정렬 알고리즘 수행 등에서 다수의 비교가 발생, 매번 compareTo()를 통해 정렬 기준을 판단</li>
</ul>
<h3 id="comparator">Comparator</h3>
<ul>
<li>객체가 Comparable을 구현하지 않은 경우 또는 다른 정렬 기준이 필요할 때, 외부에서 Comparator<T>를 주입함</li>
<li>주로 PriorityQueue, Arrays.sort, Collections.sort 등에서 두 번째 인자로 전달함</li>
<li>Comparator는 함수형 인터페이스, compare(x, y) 메서드를 통해 정렬 우선순위를 정의함</li>
<li>일반적으로 람다식으로 많이 사용, 특정 필드 기준 정렬, 역순 정렬, 복합 정렬 등 다양한 패턴에 유용함</li>
</ul>
<h2 id="4-각-동작마다-차이점">4. 각 동작마다 차이점</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Comparable</th>
<th>Comparator</th>
</tr>
</thead>
<tbody><tr>
<td>비교 위치</td>
<td>클래스 내부</td>
<td>외부에서 정의</td>
</tr>
<tr>
<td>코드 유지보수</td>
<td>클래스 수정 필요</td>
<td>외부에서 유연하게 변경 가능</td>
</tr>
<tr>
<td>다중 정렬 기준</td>
<td>불가능 (1개만 정의)</td>
<td>가능 (여러 Comparator 조합 가능)</td>
</tr>
<tr>
<td>성능</td>
<td>빠름 (직접 구현, 메서드 호출 적음)</td>
<td>약간 느림 (람다/익명 클래스 오버헤드)</td>
</tr>
<tr>
<td>재사용성</td>
<td>낮음</td>
<td>높음</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-실제-문제로-알아보기-백준-11286---절댓값-힙">4. 실제 문제로 알아보기 (백준 11286 - 절댓값 힙)</h2>
<ul>
<li><p>문제 링크: <a href="https://www.acmicpc.net/problem/11286">https://www.acmicpc.net/problem/11286</a></p>
</li>
<li><p>문제 요구 조건:</p>
<ul>
<li>절댓값이 작은 수가 먼저 나오고</li>
<li>절댓값이 같으면 더 작은 수가 먼저 나와야 함</li>
</ul>
</li>
</ul>
<h3 id="두-방식으로-모두-구현-가능">두 방식으로 모두 구현 가능</h3>
<h4 id="1-comparable-사용">1) Comparable 사용</h4>
<pre><code class="language-java">static class Numbers implements Comparable&lt;Numbers&gt; {
    int value;

    Numbers(int value) {
      this.value = value;
    }

    public int compareTo(Numbers other) {
      if (Math.abs(value) == Math.abs(other.value)) {
        return Integer.compare(value, other.value);
      } else {
        return Integer.compare(Math.abs(value), Math.abs(other.value));
      }
    }
  }</code></pre>
<ul>
<li>장점: 성능 빠름, 가독성 좋음</li>
<li>단점: 한 클래스에 하나의 정렬 기준만 넣을 수 있음</li>
</ul>
<h4 id="2-comparator-사용">2) Comparator 사용</h4>
<pre><code class="language-java">PriorityQueue&lt;Integer&gt; pq =
        new PriorityQueue&lt;&gt;((x, y) -&gt; {
          return Math.abs(x) == Math.abs(y)
              ? (x &gt; y ? 1 : -1)
              : (Math.abs(x) &gt; Math.abs(y) ? 1 : -1);
        });</code></pre>
<ul>
<li>장점: 코드가 짧고 유연함, 정렬 기준을 외부에서 바꿀 수 있음</li>
<li>단점: 성능적으로 비교가 많을 경우 약간 느릴 수 있음</li>
</ul>
<h3 id="속도-비교-체감-기준">속도 비교 (체감 기준)</h3>
<h4 id="comparable-1">Comparable</h4>
<p>  <img src="https://velog.velcdn.com/images/go_ruth/post/e6c2f882-f9a4-41da-9ad7-3c4ac1d8842f/image.png" alt=""></p>
<h4 id="comparator-1">Comparator</h4>
<p>  <img src="https://velog.velcdn.com/images/go_ruth/post/bd43da21-c14b-4c94-bb8c-8e01b7e466a3/image.png" alt=""></p>
<ul>
<li>Comparable 방식이 일반적으로 약간 더 빠름 (정렬 기준이 객체 내부에 고정되어 있기 때문!)</li>
<li>Comparator는 유연하지만, 매번 비교 시마다 람다 호출 오버헤드가 있음</li>
</ul>
<hr>
<h2 id="5-느낀점">5. 느낀점</h2>
<p>처음에는 람다의 편의성과 큰 차이점을 못느껴서 항상 Comparator를 사용했었지만, Comparable를 사용한 정렬방식이 훨씬 속도가 빠르다는 점을 알게 되어 추후에는 Comparable를 구현한 정렬방식을 더 많이 사용할 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[누적합 - 23829 인문예술탐사주간]]></title>
            <link>https://velog.io/@go_ruth/%EB%88%84%EC%A0%81%ED%95%A9-23829-%EC%9D%B8%EB%AC%B8%EC%98%88%EC%88%A0%ED%83%90%EC%82%AC%EC%A3%BC%EA%B0%84</link>
            <guid>https://velog.io/@go_ruth/%EB%88%84%EC%A0%81%ED%95%A9-23829-%EC%9D%B8%EB%AC%B8%EC%98%88%EC%88%A0%ED%83%90%EC%82%AC%EC%A3%BC%EA%B0%84</guid>
            <pubDate>Wed, 11 Jun 2025 10:55:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/050b27f4-bf2d-4ae2-963e-9a272193e90a/image.png" alt=""></p>
<h2 id="문제설명">문제설명</h2>
<p>이번 문제는</p>
<blockquote>
<ul>
<li>사진찍은 위치로부터의 나무 위치의 절댓값을 합친 값 =&gt; 사진 점수</li>
<li>주어진 위치의 사진 점수를 출력</li>
</ul>
</blockquote>
<p>을 해야하는 문제입니다.</p>
<h2 id="접근법">접근법</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/e33c5dac-57b3-4a0b-ba77-e138d8463bad/image.png" alt=""></p>
<ul>
<li><strong>0번 째 위치 :</strong> 1 + 3 + 7 + 9 + 10 = 30<ul>
<li>현재 위치에서 나무까지의 거리입니다.</li>
</ul>
</li>
<li><strong>1번 째 위치 :</strong> 0 + 2 + 6 + 8 + 9 = 25<ul>
<li>이전 위치에서 5개의 나무에 가까워졌기에, -5를 해주면 됩니다.</li>
</ul>
</li>
<li><strong>2번 째 위치 :</strong> 1 + 1 + 5 + 7 + 8 = 22<ul>
<li>이전 위치에서 4개의 나무에 가까워졌고 1개의 나무와 멀어졌으니, -3 을 해주면 됩니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡즉, 누적합을 통해 나무까지와의 거리를 구하면 됩니다!</p>
<ul>
<li>** 내 위치 &lt;= 🌲 : 이전 위치 - 1**</li>
<li>** 내 위치 &gt; 🌲 : 이전 위치 + 1**</li>
</ul>
</blockquote>
<h2 id="누적합--순차탐색---실패">누적합 + 순차탐색 -&gt; 실패</h2>
<h3 id="코드">코드</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
  public static void main(String[] args) throws Exception {
    StringBuilder sb = new StringBuilder();
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st =
        new StringTokenizer(br.readLine());
    int size = Integer.parseInt(st.nextToken());
    int seq = Integer.parseInt(st.nextToken());
    List&lt;Integer&gt; list = new ArrayList&lt;&gt;();

    st = new StringTokenizer(br.readLine());
    for(int i = 0; i &lt; size; i++) {
      list.add(Integer.parseInt(st.nextToken()));
    }
    list.sort((x, y) -&gt; x - y);

    int min = list.get(0);
    int max = list.get(list.size() - 1);

    long[] dp = new long[max + 1];

    for(int a : list) {
      dp[min] += Math.abs(a - 1);
    }

    for(int i = min + 1; i &lt; max + 1; i++) {
      int count = 0;
      for(int a : list) {
        if(a &lt; i) {
          count++;
        } else {
          break;
        }
      }
      dp[i] = dp[i - 1] + count - (size - count);
    }

    for(int i = 0; i &lt; seq; i++) {
      int idx = Integer.parseInt(br.readLine());

      if(idx &lt; min) {
        sb.append(dp[min] + ((min - idx) * size)).append(&quot;\n&quot;);
      } else if(idx &gt; max) {
        sb.append(dp[max] + ((idx - max) * size)).append(&quot;\n&quot;);
      } else {
        sb.append(dp[idx]).append(&quot;\n&quot;);
      }
    }

    System.out.println(sb);
  }
}</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c0855aad-3b04-46df-989c-8332dc5d3562/image.png" alt=""></p>
<h3 id="이유">이유</h3>
<ul>
<li>0번째부터 시작하기 때문에, 1번 탐색에 시간복잡도가 최대 O(N)으로 커짐</li>
<li>N과 Q은 최대 10^5까지 가능! 즉, 10^5 * 10^5 으로 2초내에 실행되는 게 불가능!</li>
</ul>
<h2 id="누적합--이분-탐색">누적합 + 이분 탐색</h2>
<h3 id="코드-1">코드</h3>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
  public static void main(String[] args) throws Exception {
    StringBuilder sb = new StringBuilder();
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st =
        new StringTokenizer(br.readLine());
    int size = Integer.parseInt(st.nextToken());
    int seq = Integer.parseInt(st.nextToken());
    List&lt;Integer&gt; list = new ArrayList&lt;&gt;();

    st = new StringTokenizer(br.readLine());
    for(int i = 0; i &lt; size; i++) {
      list.add(Integer.parseInt(st.nextToken()));
    }
    list.sort((x, y) -&gt; x - y);

    int min = list.get(0);
    int max = list.get(list.size() - 1);

    long[] dp = new long[max + 1];

    for(int a : list) {
      dp[min] += Math.abs(a - min);
    }

    for(int i = min + 1; i &lt; max + 1; i++) {
      int l = 0, r = list.size();
      while (l &lt; r) {
        int mid = (l + r) / 2;
        if (list.get(mid) &lt; i) {
          l = mid + 1;
        } else {
          r = mid;
        }
      }
      int count = l;

      dp[i] = dp[i - 1] + count - (size - count);
    }

    for(int i = 0; i &lt; seq; i++) {
      int idx = Integer.parseInt(br.readLine());

      if(idx &lt; min) {
        sb.append(dp[min] + ((long)(min - idx) * size)).append(&quot;\n&quot;);
      } else if(idx &gt; max) {
        sb.append(dp[max] + ((long)(idx - max) * size)).append(&quot;\n&quot;);
      } else {
        sb.append(dp[idx]).append(&quot;\n&quot;);
      }
    }

    System.out.println(sb);
  }
}</code></pre>
<h3 id="결과-1">결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/680b5d20-7d14-4063-b788-ea53a7948867/image.png" alt=""></p>
<h3 id="이유-1">이유</h3>
<ul>
<li>이분 탐색으로 1번 탐색에 시간복잡도가 최대 O(logN)으로 커진다.</li>
<li>즉, 2초내에 실행가능!</li>
</ul>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>개념으로만 알고 있는 지식을 문제에 적용하는 건 다르다는 걸 다시 한 번 깨달았음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[이분탐색, 2포인터]]></title>
            <link>https://velog.io/@go_ruth/%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-2%ED%8F%AC%EC%9D%B8%ED%84%B0</link>
            <guid>https://velog.io/@go_ruth/%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-2%ED%8F%AC%EC%9D%B8%ED%84%B0</guid>
            <pubDate>Mon, 09 Jun 2025 11:54:30 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="이분-탐색">이분 탐색</h2>
<h3 id="개념">개념</h3>
<ul>
<li>정렬된 배열에서 특정 값을 찾거나 조건을 만족하는 값을 찾는 데 사용. </li>
<li>배열을 절반씩 나눠가며 탐색 -&gt; <strong>O(log N)</strong>의 시간 복잡도.</li>
</ul>
<h3 id="적용-조건">적용 조건</h3>
<ul>
<li>데이터가 오름차순 또는 내림차순으로 정렬되어 있어야 함</li>
<li>값 존재 여부 판단 문제, 최솟값/최댓값을 찾는 결정 문제에 적합</li>
</ul>
<h3 id="기본-구현-java-기준">기본 구현 (Java 기준)</h3>
<pre><code class="language-java">public class BinarySearchExample {
    public static int binarySearch(int[] arr, int target) {
        int left = 0, right = arr.length - 1;

        while (left &lt;= right) {
            int mid = (left + right) / 2;

            if (arr[mid] == target) return mid;
            else if (arr[mid] &lt; target) left = mid + 1;
            else right = mid - 1;
        }

        return -1;
    }

    public static void main(String[] args) {
        int[] arr = {1, 3, 5, 7, 9, 11};
        int target = 7;
        System.out.println(binarySearch(arr, target));
    }
}</code></pre>
<h3 id="대표-문제-유형">대표 문제 유형</h3>
<ul>
<li>특정 수가 배열에 존재하는가?</li>
<li>최소/최대 조건을 만족하는 값 찾기</li>
<li>Lower Bound / Upper Bound 위치 찾기</li>
</ul>
<hr>
<h2 id="투-포인터">투 포인터</h2>
<h3 id="개념-1">개념</h3>
<ul>
<li>두 개의 포인터를 배열의 양 끝 또는 특정 위치에 두고, 서로를 좁히거나 밀어가며 조건을 만족하는 구간을 탐색. </li>
<li>일반적으로 <strong>O(N)</strong>의 시간 복잡도.</li>
</ul>
<h3 id="적용-조건-1">적용 조건</h3>
<ul>
<li>데이터가 정렬되어 있거나, 정렬할 수 있는 경우</li>
<li>부분합, 쌍 찾기, 구간 문제에 적합</li>
</ul>
<h3 id="기본-구현">기본 구현</h3>
<pre><code class="language-java">public class TwoPointerExample {
    public static boolean hasTwoSum(int[] arr, int target) {
        Arrays.sort(arr);
        int left = 0, right = arr.length - 1;

        while (left &lt; right) {
            int sum = arr[left] + arr[right];

            if (sum == target) return true;
            else if (sum &lt; target) left++;
            else right--;
        }

        return false;
    }

    public static void main(String[] args) {
        int[] arr = {1, 4, 2, 7, 11};
        int target = 9;
        System.out.println(hasTwoSum(arr, target));
    }
}</code></pre>
<h3 id="대표-문제-유형-1">대표 문제 유형</h3>
<ul>
<li>두 수의 합이 특정 값을 만드는 쌍 찾기</li>
<li>연속된 부분 수열의 합 문제</li>
<li>슬라이딩 윈도우와 결합하여 최대 길이/최대 합 구하기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Stack - 9935 문자열 폭발]]></title>
            <link>https://velog.io/@go_ruth/%EC%A1%B0%ED%95%A9-9935-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%8F%AD%EB%B0%9C</link>
            <guid>https://velog.io/@go_ruth/%EC%A1%B0%ED%95%A9-9935-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%8F%AD%EB%B0%9C</guid>
            <pubDate>Sun, 01 Jun 2025 01:52:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/3da847e4-b746-413c-9fef-0dca926741c4/image.png" alt=""></p>
<h2 id="문제-설명">문제 설명</h2>
<p>이번 문제는 </p>
<blockquote>
<ol>
<li>폭발 문자열을 포함하는 경우 폭발</li>
<li>남은 문자열은 이어붙여 새로운 문자열을 만듬</li>
<li>폭발 문자열이 없을 때까지, 1ㆍ2 반복</li>
<li>폭발 문자열이 없는 문자열 print / 문자열이 없다면 &quot;FAULA&quot; print </li>
</ol>
</blockquote>
<p>을 해야하는 문제입니다.</p>
<h2 id="replaceall---실패">replaceAll() -&gt; 실패</h2>
<h3 id="코드">코드</h3>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str = br.readLine();
        String s = br.readLine();

        while (!str.equals(str.replaceAll(s, &quot;&quot;))) {
            str = str.replaceAll(s, &quot;&quot;);
        }

        System.out.println(str.isEmpty() ? &quot;FRULA&quot; : str);
    }
}</code></pre>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c55dc2d2-616a-494e-b5e0-5d741ac97fac/image.png" alt=""></p>
<h3 id="이유">이유</h3>
<blockquote>
<p><strong>1. replaceAll 로직</strong>
-&gt; Pattern.compile(regex) -&gt; Matcher.find() -&gt; 문자열 새로 생성
<strong>2. 반복적으로 새 문자열을 만듬</strong> 
-&gt; 매번 문자열 전체 복사 + 패턴 매칭 진행
<strong>3. Java의 String은 불변 객체</strong>
-&gt; 매 replaceAll마다 새 문자열을 생성해야 합니다.</p>
</blockquote>
<p>즉, replaceAll()을 사용하면 반복적으로 새 문자열을 만들면서 메모리가 초과되는 상황이 발생하게 됩니다.</p>
<h2 id="stringbuilder---성공">StringBuilder() - 성공</h2>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
  public static void main(String[] args) throws Exception {
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    String str = br.readLine();
    String target = br.readLine();
    int tLen = target.length();

    StringBuilder result = new StringBuilder();

    for (int i = 0; i &lt; str.length(); i++) {
      result.append(str.charAt(i));

      // 마지막 부분이 폭탄 문자열과 같다면 삭제
      if (result.length() &gt;= tLen) {
        boolean isMatch = true;

        for (int j = 0; j &lt; tLen; j++) {
          if (result.charAt(result.length() - tLen + j) != target.charAt(j)) {
            isMatch = false;
            break;
          }
        }

        if (isMatch) {
          result.delete(result.length() - tLen, result.length());
        }
      }
    }

    System.out.println(result.length() == 0 ? &quot;FRULA&quot; : result);
  }
}</code></pre>
<h3 id="결과-1">결과</h3>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/11275a4f-b8ff-4f8b-87ec-7a8a0a8d0ff7/image.png" alt=""></p>
<h3 id="이유-1">이유</h3>
<blockquote>
<p><strong>1. 코드 로직 (append -&gt; 폭탄 유무 판단 -&gt; delete)</strong>
-&gt; 새로운 문자열을 만드는 작업 ❌
<strong>2. append(), delete(), insert() 등의 동작의 System.arraycopy()</strong>
-&gt; 배열을 메모리 단위로 복사, 덮어쓰기를 진행 (불필요한 객체 생성은 피하려고 설계)</p>
</blockquote>
<p>즉, StringBuilder()을 사용하면 주기적으로 문자열을 수정해도 새로운 객체를 생성하지 않아서 메모리 초과가 발생하지 않습니다.</p>
<h2 id="stringbuilder">StringBuilder()</h2>
<h3 id="내부구조">내부구조</h3>
<pre><code class="language-java">char[] value;
int count; // 현재 길이</code></pre>
<h3 id="메소드">메소드</h3>
<pre><code class="language-java">append(value) -&gt; 맨 뒤에 value 저장
insert(idx, value) -&gt; idx위치에 value 저장

charAt(index) -&gt; 해당 index의 데이터 return
indexOf(str) -&gt; str이 처음으로 나오는 index return
indexOf(str, index) -&gt; index부터 시작해서 str이 처음으로 나오는 index return

delete(startIdx, endIdx) -&gt; startIdx부터 endIdx - 1까지 삭제
deleteCharAt(index) -&gt; 해당 index의 데이터 삭제

length() -&gt; 현재 길이</code></pre>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>replaceAll()이 어떤 로직으로 움직이는 지 알게 되었고, StringBuilder() 전체적인 구성과 다양한 메소들를 알게 되었습니다.</li>
<li>이를 통해, StringBuilder()을 좀 더 자유롭게 사용할 수 있게 된 것 같습니다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[조합 - 20529 가장 가까운 세 사람의 심리적 거리 & 조합 방식]]></title>
            <link>https://velog.io/@go_ruth/%EC%A1%B0%ED%95%A9-20529-%EA%B0%80%EC%9E%A5-%EA%B0%80%EA%B9%8C%EC%9A%B4-%EC%84%B8-%EC%82%AC%EB%9E%8C%EC%9D%98-%EC%8B%AC%EB%A6%AC%EC%A0%81-%EA%B1%B0%EB%A6%AC-%EC%A1%B0%ED%95%A9-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@go_ruth/%EC%A1%B0%ED%95%A9-20529-%EA%B0%80%EC%9E%A5-%EA%B0%80%EA%B9%8C%EC%9A%B4-%EC%84%B8-%EC%82%AC%EB%9E%8C%EC%9D%98-%EC%8B%AC%EB%A6%AC%EC%A0%81-%EA%B1%B0%EB%A6%AC-%EC%A1%B0%ED%95%A9-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Fri, 30 May 2025 03:02:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/ebbb361b-6fd2-4b65-acce-4e51821d1a85/image.png" alt=""></p>
<h2 id="문제-설명">문제 설명</h2>
<p>이번 문제는 세 사람의 MBIT 심리적인 거리를 구하는 문제로</p>
<blockquote>
<ul>
<li>(A,B 사이 거리) + (B,C 사이 거리) + (A,C 사이 거리) 의 최솟값</li>
</ul>
</blockquote>
<p>을 구해야합니다.</p>
<p><strong>💡 즉, 조합으로 경우의 수를 찾고, 최솟값을 찾아야 합니다.</strong></p>
<h2 id="3중-for문으로-풀기">3중 For문으로 풀기</h2>
<p><strong>중첩 For 문은 적은 소규모 조합일 때, 가장 알맞는 조합방식입니다.</strong></p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        String str = br.readLine();
        int len = str.length();
        int[] dp = new int[len + 1];
        dp[0] = 1;

        for(int i = 1; i &lt; len + 1; i++) {
            char c = str.charAt(i - 1);
            if(c != &#39;0&#39;) {
                dp[i] += dp[i - 1];
            }

            if(i &gt;= 2) {
                int num = 
                    Integer.parseInt(str.substring(i - 2, i));

                if(10 &lt;= num &amp;&amp; num &lt;= 34) {
                    dp[i] += dp[i - 2];
                }
            }
        }

        System.out.println(dp[len]);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/a18e0ef4-c703-4c20-ad2a-99637fb46ff3/image.png" alt=""></p>
<h2 id="재귀로-풀기">재귀로 풀기</h2>
<p><strong>재귀는 조합의 전형적 방식으로, 중첩 For문보다는 느리지만 간결하고 직관적이라는 장점이 있습니다.</strong></p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {

  static int answer;
  static int[] arr;

  public static void main(String[] args) throws Exception {
    BufferedReader br =
        new BufferedReader(new InputStreamReader(System.in));
    StringBuilder sb = new StringBuilder();
    int freq = Integer.parseInt(br.readLine());

    next:
    for (int z = 0; z &lt; freq; z++) {
      answer = Integer.MAX_VALUE;
      arr = new int[3];

      int seq = Integer.parseInt(br.readLine());
      StringTokenizer st =
          new StringTokenizer(br.readLine(), &quot; &quot;);
      Map&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
      List&lt;String&gt; list = new ArrayList&lt;&gt;();

      // map + set 저장
      for (int i = 0; i &lt; seq; i++) {
        String str = st.nextToken();
        if (map.containsKey(str)) {
          map.put(str, map.get(str) + 1);
        } else {
          map.put(str, 1);
        }
      }

      // 리스트 setting + key 3개 이상이면 0 insert
      for (String key : map.keySet()) {
        list.add(key);

        if (map.get(key) &gt;= 3) {
          sb.append(0).append(&quot;\n&quot;);
          continue next;
        }
      }

      comb(list, map, arr, list.size(), 0);

      sb.append(answer).append(&quot;\n&quot;);
    }

    System.out.print(sb);
  }

  static void comb(List&lt;String&gt; list, Map&lt;String, Integer&gt; map,
      int[] arr, int seq, int depth) {

    if (depth == 3) {
      String aKey = list.get(arr[0]);
      String bKey = list.get(arr[1]);
      String cKey = list.get(arr[2]);

      if (arr[0] == arr[1] &amp;&amp; arr[1] == arr[2]) {
        return;
      }

      if ((aKey.equals(bKey) &amp;&amp; map.get(aKey) == 2)
          || (bKey.equals(cKey) &amp;&amp; map.get(bKey) == 2)
          || (aKey.equals(cKey) &amp;&amp; map.get(aKey) == 2)
          || (!aKey.equals(bKey) &amp;&amp; !aKey.equals(cKey) &amp;&amp; !cKey.equals(bKey))) {
        answer =
            Math.min(comp(aKey, bKey, cKey), answer);
      }

      return;
    }

    for (int i = 0; i &lt; seq; i++) {
      arr[depth] = i;
      comb(list, map, arr, seq,depth + 1);
    }
  }

  public static int comp(String aKey, String bKey, String cKey) {
    int returnValue = 0;

    for (int i = 0; i &lt; 4; i++) {
      returnValue += aKey.charAt(i) != bKey.charAt(i) ? 1 : 0;
      returnValue += bKey.charAt(i) != cKey.charAt(i) ? 1 : 0;
      returnValue += cKey.charAt(i) != aKey.charAt(i) ? 1 : 0;
    }

    return returnValue;
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/e8fef357-b47c-417d-96da-2b0c8bbf9846/image.png" alt=""></p>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>재귀와 중복 반복문으로 조합을 각각 구현해보니, 각각의 장단점을 알 수 있었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSAFY) 실시간 채팅 구축 1 - 설계 및 RDB]]></title>
            <link>https://velog.io/@go_ruth/SSAFY-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%EC%B6%95-1%ED%8E%B8-%EC%84%A4%EA%B3%84-%EB%B0%8F-RDB</link>
            <guid>https://velog.io/@go_ruth/SSAFY-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EA%B5%AC%EC%B6%95-1%ED%8E%B8-%EC%84%A4%EA%B3%84-%EB%B0%8F-RDB</guid>
            <pubDate>Fri, 30 May 2025 01:49:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 시리즈는 채팅 시스템을 구축하면서 겪은 경험을 정리한 기술 회고로, 
해당 글은 채팅에서 사용한 기술을 선택한 이유와 MVP를 위해 가장 먼저 구축했던 RDB 기반 채팅 시스템에 대해 다룹니다.</p>
</blockquote>
<hr>
<h2 id="채팅-개발한-이유">채팅 개발한 이유</h2>
<p>팀 프로젝트를 진행하면서, AI 기반 전자기기 중고거래 플랫폼을 개발하게 되었습니다. 저는 백엔드를 담당하며, 채팅파트를 맡아 진행했습니다.</p>
<p>이에, 가장 먼저 사용할 기술에 대해 고려했고, 여러 후보 중 STOMP와 Redis를 선택했습니다.</p>
<h2 id="stomp--redis를-선택한-이유">STOMP &amp; Redis를 선택한 이유!</h2>
<ul>
<li><strong>STOMP</strong><ul>
<li>기획단계에서 단체방에 대한 고려 ⭕ -&gt; <strong>메시지 라우팅이 자동으로 가능한 구조가 필요</strong>했습니다.<ul>
<li><strong>@MessageMapping + @SendTo을 통해 편하게 구축가능!</strong></li>
</ul>
</li>
<li>읽음 유무 판단, 마지막 메세지 제공 같은 실제 채팅 서비스를 목표로 진행했고, <strong>구현 과정에서의 최대한 리소스를 줄일 필요</strong>가 있었습니다.<ul>
<li>메시지 타입 분기, 목적지 라우팅, 세션/유저 추적 등 <strong>다양한 기능을 기본 제공하여 직접 구현해야 할 로직이 최소화!</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
<ul>
<li><strong>Redis</strong><ul>
<li>6주 안에 채팅에 읽은 시간, 채팅 메세지 캐싱 등을 도입 및 실제 사용이 가능한 완성도가 나와야 했기에, <strong>러닝커브가 적어야 했음.</strong><ul>
<li>MongoDB가 가장 많은 레퍼런스를 가지고 있지만, 채팅 이외의 작업도 많았던 상황에서 학습을 진행하기에는 무리라는 생각에 제외</li>
<li>가장 많이 사용해보고 익숙한 NoSQL이 Redis였음</li>
</ul>
</li>
<li>채팅에서 <strong>가장 중요한 건 속도</strong>라고 생각<ul>
<li>Redis의 <strong>ZSet을 통해 score를 만들어 구축</strong>하여, 채팅을 순차적으로 저장!</li>
<li><strong>In-Memory 구조로 NoSQL 중에서 가장 빠른 속도</strong>를 가짐!</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="요구사항">요구사항</h2>
<p>기획단계에서 나왔던 요구사항이 다음과 같이 나왔고, 이에 맞춰서 진행했습니다.</p>
<p><strong>1. 메시지 실시간 송수신
2. 채팅방 단위로 마지막 메세지를 보여주기
3. 각 사용자별로 읽은 메시지 정보 제공
4. 실시간으로 읽음여부를 파악</strong></p>
<h2 id="로직">로직</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/77441fe1-2cdd-4b02-a0e0-e315a8bc81a5/image.png" alt="">
<img src="https://velog.velcdn.com/images/go_ruth/post/d79e8a6d-e521-4364-874c-3918830cf4bb/image.png" alt=""></p>
<h3 id="고려사항">고려사항</h3>
<ul>
<li>메시지는 <code>chat_message</code> 테이블에 시간 순으로 쌓입니다.</li>
<li><code>read_point</code>는 사용자가 마지막으로 읽은 메시지를 기준으로 읽음 여부를 계산하는 데 사용됩니다.</li>
<li>채팅방 생성 후, 초기 데이터가 없기에 해당 상황에서의 기본 return값 지정이 필요합니다.</li>
</ul>
<hr>
<h2 id="🔨-기술-스택-및-구현">🔨 기술 스택 및 구현</h2>
<ul>
<li>Java 17</li>
<li>Spring Boot 3.x</li>
<li>Spring Data JPA</li>
<li>MySQL 8.0</li>
</ul>
<p>메시지는 WebSocket으로 수신되며, 바로 JPA를 통해 DB에 저장합니다.</p>
<ol>
<li><p>stomp 구성</p>
<pre><code class="language-java">@Slf4j
@Component
@RequiredArgsConstructor
public class StompChannelInterceptor implements ChannelInterceptor {
 private final Map&lt;String, Set&lt;String&gt;&gt; sessions = new ConcurrentHashMap&lt;&gt;();

 @Override
 public Message&lt;?&gt; preSend(@NonNull Message&lt;?&gt; message, @NonNull MessageChannel channel) {
     StompHeaderAccessor accessor =
             MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

     // 올바른 접근인 지 판단

     switch (accessor.getCommand()) {
         case CONNECT -&gt; {
             handleConnect(accessor);
         }
         case SEND -&gt; {
             handleSend(accessor, message);
         }
         case DISCONNECT -&gt; {
             handleDisconnect(accessor);
         }
     }

     return message;
 }

 private void handleDisconnect(StompHeaderAccessor accessor) {
     //연결 종료 시, 행동할 메소드
 }

 private void handleSend(StompHeaderAccessor accessor, Message&lt;?&gt; message) {
     // 전송 시, RDB에 저장

 private void handleConnect(StompHeaderAccessor accessor) {
     // 연결 시, jwt 검증 시도
 }
}
</code></pre>
</li>
</ol>
<pre><code>
&gt; SSAFY 프로젝트로 아직 반출신청을 못해서... 
지금은 큰 로직만 보여드리고, 추후에 자세한 코드를 첨부하겠습니다.
&gt; 



## 예상되는 한계

### 한계

* 메시지가 많아질수록 쿼리 성능이 저하될 가능성이 매우 높습니다.
* 실시간 요청을 하지만 RDB에 저장/조회되는 과정이 Redis보다 상대적으로 느립니다.



## 트래픽이 올라가면서…
![](https://velog.velcdn.com/images/go_ruth/post/ead62def-8baf-4d2c-b35e-a34e4b4b49e5/image.png)

![](https://velog.velcdn.com/images/go_ruth/post/ccfa4c93-0663-432a-9cf8-32e35c356c16/image.png)


예상대로 Chatting 조회 테스트를 진행하면서, 메시지 건수를 늘렸더니 속도가 저하되었습니다. 그래서 1차적으로 Slice&lt;&gt;를 통한 필요한 데이터만 return하도록 구성했고, 캐시 구성을 위한 **Redis** 기반의 채팅으로 전환을 진행했습니다.



## Next

RDB로 간단한 MVP를 구성했으니, 이제는 RDB에서 Redis로 전환하면서 속도를 높인 로직으로 바꿔보겠습니다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Https -> Http 에서 생겼던 오류 및 해결방법]]></title>
            <link>https://velog.io/@go_ruth/Https-Http-%EC%97%90%EC%84%9C-%EC%83%9D%EA%B2%BC%EB%8D%98-%EC%98%A4%EB%A5%98-%EB%B0%8F-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@go_ruth/Https-Http-%EC%97%90%EC%84%9C-%EC%83%9D%EA%B2%BC%EB%8D%98-%EC%98%A4%EB%A5%98-%EB%B0%8F-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 26 May 2025 01:28:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/0661ea47-f620-4ea4-bba2-534545df5558/image.png" alt=""></p>
<p>AI기반 전자기기 중고거래 플랫폼 프로젝트를 진행하면서, 프론트엔드에서 기기 이미지를 별도의 AI서버에 업로드하면, 해당 사진 판독 결과와 결과값을 받아오는 기능이 있었습니다.</p>
<p>로컬에서 해당 기능이 정상작동하는 걸 확인 후, https가 적용되있는 서버에 배포를 진행했습니다.</p>
<hr>
<h1 id="왜-갑자기-에러">왜 갑자기 에러?</h1>
<p>배포 후, Test를 진행했는데 갑자기 아래와 같은 에러가 뜨고 Api와의 연동이 되지 못했습니다.</p>
<pre><code>Mixed Content: The page at &#39;https://{웹 url}&#39; was loaded over HTTPS, 
but requested an insecure XMLHttpRequest endpoint &#39;http://{ai 서버 url}:port/api&#39;. 
This request has been blocked; the content must be served over HTTPS.</code></pre><p>HTTPS 페이지에서 HTTP로 요청을 보냈기 때문에 요청이 차단되었던 것입니다.</p>
<hr>
<h1 id="왜-block을-하는-걸까">왜 Block을 하는 걸까?</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/495b7a33-031e-4ab8-a6ed-11ed68bf92da/image.png" alt=""></p>
<blockquote>
<p>🔒 HTTPS는 암호화된 채널이지만, HTTP는 평문 전송이기 때문에
중간자 공격(MITM)이나 데이터 변조가 발생할 수 있습니다.</p>
</blockquote>
<p>현대의 브라우저는 <strong>HTTPS 페이지에서 HTTP 리소스를 요청하는 경우</strong>에, 
이를 보안 위협으로 간주하고 요청을 차단합니다.</p>
<p>결과적으로 <strong>HTTPS → HTTP 요청은 기본적으로 허용 ❌</strong></p>
<hr>
<h1 id="해결방법">해결방법</h1>
<h2 id="테스트만-하기-위한-임시-방편">테스트만 하기 위한 임시 방편</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/7109e828-d8d8-4f40-aa8e-80cde725306d/image.png" alt="Https -&gt; Http (Direct Request)"></p>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/2fae1feb-efbd-454b-95dd-4e3f59de856c/image.png" alt="Https -&gt; Http (Reverse Proxy -&gt; Use Nginx)"></p>
<p>바로, <strong>프론트엔드에서 직접 HTTP 서버로 요청 
-&gt;  Nginx 서버를 Proxy로 사용해서 내부적으로 Http 요청</strong>으로 변경하는 것입니다!</p>
<p>해당 로직이 왜 Https -&gt; Http에서 발생하는 에러를 방지하냐면, </p>
<ol>
<li>프론트에서는 <code>https://url/ai/api</code>와 같이 HTTPS API로 요청</li>
<li>Nginx 설정에서 해당 요청을 내부적으로 <code>http://{ai 서버 url}/api</code>로 proxy_pass를 진행</li>
</ol>
<blockquote>
<p>이렇게 함으로써 브라우저는 HTTPS → HTTPS 요청만 진행하고,
Nginx가 내부적으로 HTTP 요청을 처리해 Mixed Content 오류가 발생하지 않도록 우회할 수 있었습니다.</p>
</blockquote>
<p>다만, 결국에는 Https -&gt; Http 요청으로, <strong>데이터 보안이 보장 ❌</strong>
-&gt; 테스트 or 로컬에서만 사용해야 합니다.</p>
<h2 id="문제-해결하기-위한-해결책">문제 해결하기 위한 해결책</h2>
<p>실제 운영 환경에서는 AI 서버에도 <strong>Let&#39;s Encrypt 등의 무료 인증서를 적용</strong>하여 Https &lt;-&gt; Https 통신이 되도록 해야합니다.</p>
<h1 id="느낀-점">느낀 점</h1>
<p>이번에 인프라를 팀원들과 함께 구축하면서 개념으로만 알고 있던, Https와 Http의 차이를 제대로 파악할 수 있었고 보안을 위해서 무조건 Https를 적용해야 안정성을 제공할 수 있다는 걸 알 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DP - 2591 숫자카드]]></title>
            <link>https://velog.io/@go_ruth/DP-2591-%EC%88%AB%EC%9E%90%EC%B9%B4%EB%93%9C</link>
            <guid>https://velog.io/@go_ruth/DP-2591-%EC%88%AB%EC%9E%90%EC%B9%B4%EB%93%9C</guid>
            <pubDate>Sun, 11 May 2025 07:53:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/4a8e768b-04b6-4ad9-9b70-5c7197d658d6/image.png" alt=""></p>
<h2 id="문제-설명">문제 설명</h2>
<p>이번 문제는 가능한 카드배열의 수를 구하는 문제로,</p>
<blockquote>
<ul>
<li>1 ~ 34까지의 숫자를 가진 카드들로 배열을 구성.</li>
<li>최대 40자 이하의 숫자로 이뤄짐</li>
</ul>
</blockquote>
<p>이 되어야 합니다.</p>
<h2 id="접근법-dp">접근법 (DP)</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/1603bd73-9e4e-46a2-bcc3-7d2d5c9309a6/image.png" alt=""></p>
<ol>
<li>각 Index에 따라서 조건<ul>
<li>한자리 : 1~9인 지 파악<ul>
<li>두자리 : 10 ~ 34인 지 파악</li>
</ul>
</li>
</ul>
</li>
<li>조건에 맞다면 이전의 경우의 수를 +</li>
<li>모든 조건을 계산 후, 가장 마지막 DP Data를 return</li>
</ol>
<h2 id="코드">코드</h2>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        String str = br.readLine();
        int len = str.length();
        int[] dp = new int[len + 1];
        dp[0] = 1;

        for(int i = 1; i &lt; len + 1; i++) {
            char c = str.charAt(i - 1);
            if(c != &#39;0&#39;) {
                dp[i] += dp[i - 1];
            }

            if(i &gt;= 2) {
                int num = 
                    Integer.parseInt(str.substring(i - 2, i));

                if(10 &lt;= num &amp;&amp; num &lt;= 34) {
                    dp[i] += dp[i - 2];
                }
            }
        }

        System.out.println(dp[len]);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c6eb17c3-08c9-4a7e-9a04-8dd24940027f/image.png" alt=""></p>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>DP는 확실히 문제유형이 DP라는 걸 파악하는 게 중요하다고 생각이 들었음.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[DP - 1495 기타리스트]]></title>
            <link>https://velog.io/@go_ruth/DP-1495-%EA%B8%B0%ED%83%80%EB%A6%AC%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@go_ruth/DP-1495-%EA%B8%B0%ED%83%80%EB%A6%AC%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Sat, 10 May 2025 03:39:46 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/c145eab7-4186-4225-9a32-e7f96f15c3ff/image.png" alt=""></p>
<h2 id="문제-설명">문제 설명</h2>
<p>이번 문제는 마지막 곡의 최대 볼륨을 구하는 문제로, </p>
<ol>
<li>i번째를 연주하려면, 이전의 볼륨에서 정해진 변경 볼륨대로 + or -를 해야한다.</li>
<li>볼륨이 0 &lt;= volume &lt;= M 을 충족해야 한다.</li>
<li>모든 곡을 리스트에 적힌 순서대로 연주해야 한다.</li>
</ol>
<h2 id="1-dfs">1. DFS</h2>
<p>가장 먼저 DFS를 통해 최대값을 도출하려 했습니다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int answer = -1;

    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = 
            new StringTokenizer(br.readLine());
        int seq = Integer.parseInt(st.nextToken());
        int start = Integer.parseInt(st.nextToken());
        int limit = Integer.parseInt(st.nextToken());
        int[] arr = new int[seq];
        st = new StringTokenizer(br.readLine());
        for(int i = 0; i &lt; seq; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        dfs(arr, start, limit, 0, seq);

        System.out.println(answer);
    }

    public static void dfs(int[] arr, int start, int limit, int idx, int seq) {
        if(idx == seq) {
            answer = Math.max(answer, start);
            return;
        }

        if(start - arr[idx] &gt;= 0) {
            dfs(arr, start - arr[idx], limit, idx + 1, seq);
        }
        if(start + arr[idx] &lt;= limit) {
            dfs(arr, start + arr[idx], limit, idx + 1, seq);
        }
    }
}</code></pre>
<p>그리고 당연하게도... 시간초과가 발생했습니다.
<img src="https://velog.velcdn.com/images/go_ruth/post/d73e5ea3-a314-4045-88ed-ee11d5fa6ba9/image.png" alt=""></p>
<p>이유는 바로 <strong>상태저장 없이 DFS를 진행</strong>했기 때문입니다.</p>
<p>아래 그림을 통해 더 자세히 설명하자면 
<img src="https://velog.velcdn.com/images/go_ruth/post/5341cc5a-625d-414c-a435-776fac85fdf3/image.png" alt=""></p>
<ul>
<li>초기 상태에서 1번째 연주를 진행하면서 볼륨 증감 시도 (1 * 2) </li>
<li><blockquote>
<p><strong>2^1</strong></p>
</blockquote>
</li>
<li>1번째 연주 끝난 상태에서 2번째 연주를 진행하면서 볼륨 증감 시도 (2 * 2) </li>
<li><blockquote>
<p><strong>2^2</strong></p>
</blockquote>
</li>
<li>2번째 연주 끝난 상태에서 3번째 연주를 진행하면서 볼륨 증감 시도 (4 * 2) </li>
<li><blockquote>
<p><strong>2^3</strong></p>
</blockquote>
</li>
<li>마지막으로 N - 1번째 연주 끝난 상태에서 N번째 연주를 진행하면서 볼륨 증감 시도 (2^N-1 * 2)</li>
<li><blockquote>
<p><strong>2^N</strong></p>
</blockquote>
</li>
</ul>
<blockquote>
<p>💡 무조건 증감을 시도하기 때문에, 항상 <strong>O(2^N)</strong>의 시간 복잡도가 생기면서 시간초과 발생</p>
</blockquote>
<h2 id="2-dfs--visited배열">2. DFS + visited배열</h2>
<p>이에, 중복되는 상황을 제거하기 위해, 2차원 boolean배열을 통해 중복되는 접근을 차단했습니다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int answer = -1;

    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = 
            new StringTokenizer(br.readLine());
        int seq = Integer.parseInt(st.nextToken());
        int start = Integer.parseInt(st.nextToken());
        int limit = Integer.parseInt(st.nextToken());
        boolean[][] v = new boolean[seq + 1][limit + 1];
        int[] arr = new int[seq];
        st = new StringTokenizer(br.readLine());
        for(int i = 0; i &lt; seq; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        dfs(arr, start, limit, 0, seq, v);

        System.out.println(answer);
    }

    public static void dfs(int[] arr, int start, int limit, int idx, int seq, boolean[][] v) {
        if(idx == seq) {
            answer = Math.max(answer, start);
            return;
        }

        if(start - arr[idx] &gt;= 0 &amp;&amp; !v[idx + 1][start - arr[idx]]) {
            v[idx + 1][start - arr[idx]] = true;
            dfs(arr, start - arr[idx], limit, idx + 1, seq, v);
        }
        if(start + arr[idx] &lt;= limit &amp;&amp; !v[idx + 1][start + arr[idx]]) {
            v[idx + 1][start + arr[idx]] = true;
            dfs(arr, start + arr[idx], limit, idx + 1, seq, v);
        }
    }
}</code></pre>
<p>그리고 결과는... 정답이 였습니다.
<img src="https://velog.velcdn.com/images/go_ruth/post/a76f4f94-88f9-4953-88ee-b07791b71407/image.png" alt=""></p>
<p>아래의 그림으로 성공하는 이유를 설명하자면,</p>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/c4b93eff-308e-477d-a4fe-ff5adaf1d73c/image.png" alt=""></p>
<blockquote>
<p>💡 이전에는 중복이 가능했지만, 2차원 배열을 통해 이미 방문한 곳을 차단했기 때문에 중복 없이 탐색이 가능!
-&gt; 시간복잡도가 <strong>O(N * (M + 1)) 로 대폭감소</strong> / <em>N = 곡 수, M = 최대 볼륨</em></p>
</blockquote>
<h2 id="3-dp-2차원-배열">3. DP (2차원 배열)</h2>
<p>정답 후 다른 사람들의 정답을 찾아 파악해봤는데, 대부분 DP를 통해 문제해결을 도모. 차이점 비교를 위해서 코드를 작성해봤습니다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int answer = -1;

    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = 
            new StringTokenizer(br.readLine());
        int seq = Integer.parseInt(st.nextToken());
        int start = Integer.parseInt(st.nextToken());
        int limit = Integer.parseInt(st.nextToken());
        int[] arr = new int[seq];
        boolean[][] dp = new boolean[seq + 1][limit + 1];
        dp[0][start] = true;

        st = new StringTokenizer(br.readLine());
        for(int i = 0; i &lt; seq; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        for(int i = 0; i &lt; seq; i++) {
            for(int j = 0; j &lt; limit + 1; j++) {
                if(dp[i][j]) {
                    if(j + arr[i] &lt;= limit) {
                        dp[i + 1][j + arr[i]] = true;
                    }
                    if(j - arr[i] &gt;= 0) {
                        dp[i + 1][j - arr[i]] = true;
                    }
                }
            }
        }

        int answer = -1;
        for(int j = limit; j &gt;= 0; j--) {
            if(dp[seq][j]) {
                answer = j;
                break;
            }
        }

        System.out.println(answer);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/3ba95356-6617-4ab7-b2d0-7d38562aa6c0/image.png" alt=""></p>
<p>DP를 풀면서 이전의 DFS + 2차원 배열 코드와 다른 점으로는 </p>
<ul>
<li>DFS + 2차원 배열<ul>
<li>방문 처리를 통한 중복 방지</li>
<li>재귀를 통한 다음 index로 이동</li>
<li>끝까지 이동한 후, 최대 볼륨을 일일히 비교</li>
</ul>
</li>
<li>DP<ul>
<li>가능한 모든 경우를 boolean 2차월 배열로 초기 세팅</li>
<li>시작 위치 true</li>
<li>true인 곳을 다음 볼륨 비교 후 true로 업데이트</li>
<li>모든 방문 후, 최대 index를 파악 후 return</li>
</ul>
</li>
</ul>
<p>였습니다. </p>
<h2 id="4-dp-1차원-배열">4. DP (1차원 배열)</h2>
<p>더 나아가, 1차원 배열로도 진행이 가능합니다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int answer = -1;

    public static void main(String[] args) throws Exception {
        BufferedReader br = 
            new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = 
            new StringTokenizer(br.readLine());
        int seq = Integer.parseInt(st.nextToken());
        int start = Integer.parseInt(st.nextToken());
        int limit = Integer.parseInt(st.nextToken());
        int[] arr = new int[seq];
        boolean[] preDP = new boolean[limit + 1];
        boolean[] nextDP = new boolean[limit + 1];
        preDP[start] = true;

        st = new StringTokenizer(br.readLine());
        for(int i = 0; i &lt; seq; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        for(int z = 0; z &lt; seq; z++) {
            Arrays.fill(nextDP, false);

            for(int i = 0; i &lt; limit + 1; i++) {
                if(preDP[i]) {
                    if(i + arr[z] &lt;= limit) {
                        nextDP[i + arr[z]] = true;
                    }
                    if(i - arr[z] &gt;= 0) {
                        nextDP[i - arr[z]] = true;
                    }
                }
            }

            boolean[] temp = nextDP;
            nextDP = preDP;
            preDP = temp;
        }

        int answer = -1;
        for(int j = limit; j &gt;= 0; j--) {
            if(preDP[j]) {
                answer = j;
                break;
            }
        }

        System.out.println(answer);
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/cbad115e-bb7c-48db-ad52-fa7392b26278/image.png" alt=""></p>
<p>이전의 2차원 배열 DP과 지금의 1차원 배열이 다른 점으로는</p>
<ul>
<li>2차원 배열<ul>
<li>모든 경우의 수를 세팅</li>
<li>모든 작업 후 마지막 곡 index에서 최대 볼륨 index를 find</li>
</ul>
</li>
<li>1차원 배열<ul>
<li>이전 곡의 배열, 다음곡의 배열로 2개의 1차원 배열만 생성</li>
<li><blockquote>
<p>이전 곡에서 볼륨 조절을 진행하기 때문에!</p>
</blockquote>
</li>
<li>다음 곡 볼륨 로직 후, 배열 주소 변경</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 이를 통해, 공간 복잡도를 O(N * M) → O(2M)로 줄일 수 있게 됩니다!</p>
</blockquote>
<h2 id="느낀-점">느낀 점</h2>
<ul>
<li>해당 문제를 풀면서 다양한 접근이 가능하다는 걸 알게 되었습니다.</li>
<li>특히, 해당 문제에서 방식에 따른 장점을 파악할 수 있게 되었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바의 ObjectMapper]]></title>
            <link>https://velog.io/@go_ruth/%EC%9E%90%EB%B0%94%EC%9D%98-ObjectMapper</link>
            <guid>https://velog.io/@go_ruth/%EC%9E%90%EB%B0%94%EC%9D%98-ObjectMapper</guid>
            <pubDate>Mon, 05 May 2025 10:17:16 GMT</pubDate>
            <description><![CDATA[<p><code>ObjectMapper</code>는 Jackson 라이브러리에서 제공하는 JSON 처리 유틸리티로, Spring에서 <strong>객체(Object)와 JSON 간의 직렬화 및 역직렬화</strong> 작업에 널리 사용됩니다.</p>
<hr>
<h2 id="1-objectmapper란">1. ObjectMapper란?</h2>
<p><code>ObjectMapper</code>는 Java 객체를 JSON으로 직렬화하거나, JSON을 Java 객체로 역직렬화할 수 있게 해주는 Jackson의 핵심 클래스입니다.</p>
<pre><code class="language-java">ObjectMapper mapper = new ObjectMapper();

// 직렬화
String json = mapper.writeValueAsString(myObject);

// 역직렬화
MyObject obj = mapper.readValue(json, MyObject.class);</code></pre>
<hr>
<h2 id="2-직렬화와-역직렬화의-내부-동작-원리">2. 직렬화와 역직렬화의 내부 동작 원리</h2>
<h3 id="직렬화-writevalueasstring">직렬화 (<code>writeValueAsString</code>)</h3>
<ol>
<li><strong>BufferRecycler 준비</strong>
→ Jackson이 내부 버퍼 메모리 확보</li>
<li><strong>SegmentedStringWriter 준비</strong>
→ JSON 데이터를 쓸 준비</li>
<li><strong>JsonGenerator</strong> 생성
→ 실제 JSON 생성 작업 담당</li>
<li><strong>객체 직렬화</strong>
→ Java 객체를 JSON 형태로 변환</li>
<li><strong>결과 문자열 반환</strong> 및 메모리 정리</li>
</ol>
<h3 id="역직렬화-readvalue">역직렬화 (<code>readValue</code>)</h3>
<ol>
<li><code>readValue(String, Class&lt;T&gt;)</code> 
→ 일반 클래스 변환</li>
<li><code>readValue(String, new TypeReference&lt;List&lt;T&gt;&gt;(){})</code> 
→ 제네릭/컬렉션 처리</li>
</ol>
<hr>
<h2 id="3-실전-사용-예시">3. 실전 사용 예시</h2>
<h3 id="31-객체-→-json-문자열">3.1. 객체 → JSON 문자열</h3>
<pre><code class="language-java">User user = new User(&quot;John&quot;, 30);
String json = mapper.writeValueAsString(user);</code></pre>
<h3 id="32-json-문자열-→-객체">3.2. JSON 문자열 → 객체</h3>
<pre><code class="language-java">String json = &quot;{\&quot;name\&quot;:\&quot;John\&quot;,\&quot;age\&quot;:30}&quot;;
User user = mapper.readValue(json, User.class);</code></pre>
<h3 id="33-json-문자열-→-map-등-복잡한-구조">3.3. JSON 문자열 → Map 등 복잡한 구조</h3>
<pre><code class="language-java">Map&lt;String, Object&gt; map = mapper.readValue(json, new TypeReference&lt;Map&lt;String, Object&gt;&gt;() {});</code></pre>
<hr>
<h2 id="4-커스터마이징-기능">4. 커스터마이징 기능</h2>
<h3 id="41-필드명-커스터마이징">4.1. 필드명 커스터마이징</h3>
<pre><code class="language-java">@JsonProperty(&quot;username&quot;)
private String name;</code></pre>
<h3 id="42-필드-제외">4.2. 필드 제외</h3>
<pre><code class="language-java">@JsonIgnore
private String password;</code></pre>
<h3 id="43-날짜-포맷">4.3. 날짜 포맷</h3>
<pre><code class="language-java">@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)
private LocalDateTime createdAt;</code></pre>
<h3 id="44-null-필드-제외">4.4. Null 필드 제외</h3>
<pre><code class="language-java">mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);</code></pre>
<h3 id="45-알-수-없는-필드-무시">4.5. 알 수 없는 필드 무시</h3>
<pre><code class="language-java">mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);</code></pre>
<h3 id="46-snake_case-자동-변환">4.6 snake_case 자동 변환</h3>
<pre><code class="language-java">mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);</code></pre>
<hr>
<h2 id="5-고급-기능">5. 고급 기능</h2>
<h3 id="컬렉션-역직렬화-시-typereference-필수">컬렉션 역직렬화 시 TypeReference 필수</h3>
<pre><code class="language-java">List&lt;User&gt; list = mapper.readValue(json, new TypeReference&lt;List&lt;User&gt;&gt;() {});</code></pre>
<h3 id="javatimemodule-등록-localdatetime-등">JavaTimeModule 등록 (LocalDateTime 등)</h3>
<pre><code class="language-java">mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);</code></pre>
<h3 id="커스텀-serializer">커스텀 Serializer</h3>
<pre><code class="language-java">public class MaskingSerializer extends JsonSerializer&lt;String&gt; {
  public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    gen.writeString(&quot;***&quot;);
  }
}</code></pre>
<pre><code class="language-java">@JsonSerialize(using = MaskingSerializer.class)
private String phoneNumber;</code></pre>
<hr>
<h2 id="6-spring에서-objectmapper가-선호되는-이유">6. Spring에서 ObjectMapper가 선호되는 이유</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>기본 내장</strong></td>
<td>Spring Boot는 기본적으로 Jackson(ObjectMapper)을 채택</td>
</tr>
<tr>
<td><strong>통합성</strong></td>
<td><code>@RequestBody</code>, <code>@ResponseBody</code> 등과 자동 통합</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>빠른 JSON 처리 성능 (Gson 대비)</td>
</tr>
<tr>
<td><strong>유연성</strong></td>
<td>다양한 포맷 지원 (JSON, YAML, XML 등)</td>
</tr>
<tr>
<td><strong>설정 용이성</strong></td>
<td>전역 커스터마이징 설정이 간단함</td>
</tr>
</tbody></table>
<h3 id="spring-설정-예시">Spring 설정 예시</h3>
<pre><code class="language-java">@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new JavaTimeModule());
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return mapper;
}</code></pre>
<hr>
<h2 id="7-objectmapper의-사용-영역-in-spring">7. ObjectMapper의 사용 영역 in Spring</h2>
<ul>
<li><code>@RequestBody</code>, <code>@ResponseBody</code> 
→ HTTP 요청/응답 자동 변환</li>
<li><code>RestTemplate</code>, <code>WebClient</code> 
→ JSON API 데이터 처리</li>
<li><code>RedisTemplate</code>, <code>KafkaTemplate</code> 
→ 메시지 직렬화/역직렬화</li>
<li>단위 테스트 (<code>MockMvc</code>) → JSON 검증 및 비교</li>
</ul>
<hr>
<h2 id="8-대체-가능한-json-처리-도구-비교">8. 대체 가능한 JSON 처리 도구 비교</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Jackson</strong></td>
<td>Spring 기본 내장, 빠름, 커스터마이징 강력</td>
<td>복잡한 타입 처리 시 세팅 필요</td>
</tr>
<tr>
<td><strong>Gson</strong></td>
<td>가볍고 사용법 단순</td>
<td>성능 낮고 기능 제약 많음</td>
</tr>
<tr>
<td><strong>Json-B</strong></td>
<td>Java 표준 기반</td>
<td>실무 적용 빈도 낮음</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSAFY) Websocket에서의 Spring Security + Interceptor]]></title>
            <link>https://velog.io/@go_ruth/SSAFY-Websocket-oAuth%EC%97%90%EC%84%9C-%EC%83%9D%EA%B8%B4-Error-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@go_ruth/SSAFY-Websocket-oAuth%EC%97%90%EC%84%9C-%EC%83%9D%EA%B8%B4-Error-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Sat, 03 May 2025 06:38:35 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하며, Oauth + STOMP으로 사용하며 보완했던 경험을 같이 공유하고자 간단하게 포스팅을 작성해보려 합니다.</p>
<h2 id="websocket--spring-security">Websocket + Spring Security</h2>
<ul>
<li>WebSocket 연결은 아래와 같은 과정으로 진행이 됩니다.<blockquote>
<ol>
<li>HTTP 프로토콜을 사용해 접근 </li>
<li>Websocket으로 Upgrade를 진행하며 연결 시도</li>
<li>연결 유무 응답을 return</li>
</ol>
</blockquote>
</li>
<li>해당 과정에서 Spring Security는 <strong>HTTP 요청까지만 인증 및 권한 파악</strong>을 진행하며, WebSocket 메시지 이후의 통신에는 적용되지 않습니다.</li>
</ul>
<div align="center">
  <img src="https://velog.velcdn.com/images/go_ruth/post/a48273a9-d153-4a09-9e6c-67d5f9b3140b/image.png" alt="MySQL 접속">
  <small><em>use Spring Security</em></small>
</div>

<ul>
<li>따라서 WebSocket 인증은 <strong><code>HandshakeInterceptor</code>에서 1차적으로 인증 정보를 저장하고</strong>, 이후 메시지 처리 과정에서는 <code>ChannelInterceptor</code>를 통해 권한 검사를 수행하는 방식으로 구현해야 합니다.</li>
</ul>
<div align="center">
  <img src="https://velog.velcdn.com/images/go_ruth/post/4cc07dcd-3b57-423e-9f0f-3ef04a41cf20/image.png" alt="MySQL 접속">
  <small><em>use HandshakeInterceptor</em></small>
</div>

<hr>
<h2 id="보완한-점">보완한 점</h2>
<p>이에, 저는 Spring Security만 사용하던 코드를</p>
<p><strong>1. 웹소켓 요청 시, web.ignoring()을 통해 Security의 확인 과정 생략
2. STOMP에 HandshakeHandler을 설정하여 websocket 연결 전 확인
3. ChannelInterceptor를 통한 메세지 핸들링</strong></p>
<p>를 적용하며 보완했습니다.</p>
<p>구체적으로는 아래와 같이 사용했습니다.</p>
<blockquote>
<p><strong>preSend를 통해 command 파악</strong> </p>
<ol>
<li>CONNECT 일 때</li>
</ol>
<p>-&gt; 비정상적인 세션 및 데이터 관리를 위한 Redis에 저장 로직
2. SEND일 때
-&gt; 각 JSON의 type의 값을 조건으로 알맞는 DTO로 변경 후 데이터 처리
3. DISCONNECT일 때
-&gt; Redis에 저장된 값 제거</p>
</blockquote>
<blockquote>
<p><strong>💡 Tip. 
ChannelInterceptor에서 accessor의 command를 사용해서 CONNECT일 때, 권한 및 인증을 확인할 수 있습니다.</strong></p>
<p>다만, 그 과정을 위해 <strong>무조건 Websocket이 연결</strong>이 되야만 하기 때문에 *<em>handshaking를 사용한 연결 전 처리보다는 상대적으로 비효율적 *</em> 인 구조가 됩니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 서버나 컴퓨터를 끄고 키면 docker의 MySQL을 인식하지 못할까?]]></title>
            <link>https://velog.io/@go_ruth/%EC%99%9C-%EC%84%9C%EB%B2%84%EB%82%98-%EC%BB%B4%ED%93%A8%ED%84%B0%EB%A5%BC-%EB%81%84%EA%B3%A0-%ED%82%A4%EB%A9%B4-docker%EC%9D%98-mysql%EC%9D%84-%EC%9D%B8%EC%8B%9D%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@go_ruth/%EC%99%9C-%EC%84%9C%EB%B2%84%EB%82%98-%EC%BB%B4%ED%93%A8%ED%84%B0%EB%A5%BC-%EB%81%84%EA%B3%A0-%ED%82%A4%EB%A9%B4-docker%EC%9D%98-mysql%EC%9D%84-%EC%9D%B8%EC%8B%9D%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Fri, 02 May 2025 13:33:04 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행하며 Spring Boot에서 각종 오류가 발생했는데, 특히 Docker를 재시작할 때마다 Spring에서 Key 인식을 못 한다는 오류가 반복되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/4a293f29-562d-44e5-a0fd-4a0640e7577a/image.png" alt=""></p>
<p>이에, 처음에는 단순히 &quot;Key가 손상된 건가?&quot; 싶어, Docker 내부에서 직접 <code>mysql -u 사용자명 -p</code> 명령어를 통해 DB 내 설정 상태를 확인했습니다.</p>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/e29a28d5-2c99-472b-b35f-725812a8616f/image.png" alt="">
<img src="https://velog.velcdn.com/images/go_ruth/post/03b6e86f-7cb1-4e4c-8195-539711e390fb/image.png" alt=""></p>
<p>확인하면서 문제는 없었고, 해당 과정이 이후로 Spring 서버도 정상 실행됐습니다.</p>
<p>하지만 매번 Docker이 restart 될 때 해당 과정을 반복해야 했고, 꽤나 번거로웠습니다.</p>
<p>그래서 원인을 본격적으로 파고들었는데, 생각보다 아주 간단한 설정 하나로 문제를 해결할 수 있었습니다.</p>
<hr>
<h2 id="문제-원인-mysql-인증-방식">문제 원인: MySQL 인증 방식</h2>
<p>MySQL에는 대표적으로 두 가지 인증 방식이 있습니다.</p>
<ul>
<li>mysql_native_password</li>
<li>caching_sha2_password (MySQL 8부터 기본)</li>
</ul>
<p>이 중, caching_sha2_password 인증방식을 사용하면서 에러가 발생했습니다.</p>
<p><code>Public Key Retrieval is not allowed</code></p>
<p>말 그대로 &quot;클라이언트가 서버의 공개키를 받아오는 것이 허용되지 않아&quot; 발생하는 에러.
-&gt; caching_sha2_password방식에서는 공개키 기반 인증이 필요한데, 이를 허용하지 않게 JDBC 설정한 것이 원인이였습니다.</p>
<hr>
<h2 id="해결방법">해결방법</h2>
<ol>
<li><strong>JDBC URL에 옵션 추가 (권장)</strong></li>
</ol>
<pre><code class="language-yml">spring:
  datasource:
    url: jdbc:mysql://localhost:{port}/{db_name}?allowPublicKeyRetrieval=true&amp;useSSL=false</code></pre>
<ul>
<li>application.yml 또는 application.properties에 아래 설정을 추가합니다.</li>
</ul>
<ol start="2">
<li><strong>인증 방식을 변경 (비권장)</strong></li>
</ol>
<pre><code>ALTER USER &#39;usedTrade&#39;@&#39;%&#39; IDENTIFIED WITH mysql_native_password BY &#39;yourpassword&#39;;</code></pre><ul>
<li>이 방법은 보안이 약한 방식으로 회귀하는 것이고, Docker 자동화에도 불리하므로 추천하지 않아요.</li>
</ul>
<hr>
<h2 id="그런데-왜-docker-내부에서-로그인하면-잘-될까">그런데 왜 Docker 내부에서 로그인하면 잘 될까?</h2>
<p>이건 caching_sha2_password의 캐시 특성 때문으로, Docker 내부에서 <code>mysql -u usedTrade -p</code> 등으로 한 번 접속하고 Spring에서도 접속이 잘 되는 듯 보이는 현상이 나타납니다.</p>
<p>자세한 흐름은 아래와 같습니다.</p>
<blockquote>
<p>최초 접속
🔻
서버가 클라이언트에 공개키를 제공하고, 클라이언트가 암호화하여 응답
🔻
이후 접속
🔻
서버는 클라이언트 IP/User 정보를 바탕으로 인증 정보를 캐싱
🔻
캐싱된 인증을 통해 빠르게 접속 허용</p>
</blockquote>
<p>즉, 기존했던 작업들은 정상 동작처럼 보이지만 캐시 덕분에 &#39;우연히&#39; 통과된 상태로, </p>
<ol>
<li>캐시가 만료</li>
<li>Docker 또는 MySQL이 재시작</li>
<li>Spring 서버의 IP가 변경</li>
</ol>
<p>같은 동작이 발생한다면 다시 오류가 발생합니다.</p>
<p><strong>즉, allowPublicKeyRetrieval=true 설정을 추가해야 Spring이 올바르게 DB에 접근이 가능합니다.</strong></p>
<hr>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/98c6aa98-6774-4a9b-995a-7fdff918af54/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA ) 자바 ORM 표준 JPA 프로그래밍 - 8]]></title>
            <link>https://velog.io/@go_ruth/JPA-ORM-JPA-8</link>
            <guid>https://velog.io/@go_ruth/JPA-ORM-JPA-8</guid>
            <pubDate>Sun, 27 Apr 2025 03:26:24 GMT</pubDate>
            <description><![CDATA[<h1 id="프록시">프록시</h1>
<ul>
<li>엔티티를 조회할 때, 연관된 엔티티들이 항상 사용되는 것은 아님</li>
<li><blockquote>
<p>DB에서 함께 조회해두는 것은 비효율!</p>
</blockquote>
</li>
<li>해당 문제 해결을 위해 엔티티가 실제 사용 전까지 DB 조회를 지연하는 방법 제공 </li>
<li><blockquote>
<p>지연 로딩!</p>
</blockquote>
</li>
<li>지연로딩을 사용하기 위해서는 조회 지연을 위한 가짜 객체가 필요</li>
<li><blockquote>
<p>프록시 객체</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/122f9dc7-9f40-471f-ac67-0e029544f930/image.png" alt=""></p>
<h2 id="특징">특징</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/dd5c34bd-b49f-4b51-adc3-a650629eaa93/image.png" alt="">
<img src="https://velog.velcdn.com/images/go_ruth/post/99252155-539c-4517-85a4-a23000dcd754/image.png" alt=""></p>
<h2 id="초기화">초기화</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/1a9c82ab-4064-4ad2-919e-4e3b6c04b86e/image.png" alt=""></p>
<pre><code>Member member = em.getReference(Member.class, &quot;id1&quot;);
member.getName();

**내부 예상 로직**
1. 초기화 요청
2. DB 조회
3. 실제 Entity 생성 및 참조 보관
4. 실제 값 return</code></pre><h2 id="프록시와-식별자">프록시와 식별자</h2>
<ul>
<li>엔티티를 프록시로 조회 시, 식별자 값을 파라미터로 전달</li>
<li><blockquote>
<p>프록시 객체는 해당 식별자값을 보관</p>
</blockquote>
</li>
</ul>
<p><strong>💡 연관관계 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 DB 접근 횟수를 줄일 수 있다.</strong></p>
<h1 id="즉시로딩-지연로딩">즉시로딩, 지연로딩</h1>
<h2 id="개념">개념</h2>
<ul>
<li>즉시로딩<ul>
<li>엔티티를 조회할 때 연관된 엔티티도 함께 조회.</li>
<li>@ManyToOne(fetch = FetchType.EAGER)</li>
</ul>
</li>
<li>지연로딩<ul>
<li>연관된 엔티티를 실제 사용할 때 조회.</li>
<li>@ManyToOne(fetch = FetchType.LAZY)</li>
</ul>
</li>
</ul>
<h2 id="즉시로딩">즉시로딩</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/fb60b0be-f346-4451-abe9-6baca0dc228d/image.png" alt=""></p>
<pre><code class="language-sql">SELECT
 M.MEMBER_ID AS MEMBER_ID,
 M.TEAM_ID AS TEAM_ID,
 M.USERNAME AS USERNAME,
 T.TEAM_ID AS TEAM_ID,
 T.NAME AS NAME
FROM
 MEMBER M LEFT OUTER JOIN TEAM T
 ON M.TEAM_ID=T.TEAM_ID
WHERE
 M.MEMBER_ID=&#39;member1&#39;</code></pre>
<h2 id="지연로딩">지연로딩</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/6427b552-f180-43dd-8230-a0b0fc74168b/image.png" alt=""></p>
<h2 id="유의점">유의점</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/a04dc05b-b54a-4704-8ec7-23a06b013966/image.png" alt=""></p>
<blockquote>
<p>Lazy 로딩되는 Entity에 즉시 로딩이 있다면 해당 Entity도 로딩!</p>
</blockquote>
<ul>
<li>Porxy 객체는 실제 자신이 사용될 때까지 DB를 조회하지 않음</li>
<li><strong>컬랙션 래퍼</strong><ul>
<li>하이버네이브는 Entity를 영속 상태로 만들 때, Entity에 컬랙션이 있으면 컬랙션 추적 및 관리 목적으로 하이버네이트로의 내장 컬랙션으로 변경</li>
<li>지연로딩 시, 컬랙션은 컬랙션 래퍼가 지연 로딩 처리</li>
</ul>
</li>
</ul>
<h2 id="jpa-기본-전략">JPA 기본 전략</h2>
<ul>
<li>ManyToOne, OneToOne (즉시로딩)</li>
<li>OneToMany, ManyToMany (지연로딩)</li>
</ul>
<p><strong>💡 추천은 모든 연관관계를 지연로딩 후, 상황에 따라서 변동!</strong></p>
<h2 id="즉시로딩-시-주의점">즉시로딩 시, 주의점</h2>
<ol>
<li>컬렉션 하나 이상 즉시 로딩은 권장 ❌</li>
<li>컬렉션 즉시 로딩은 항상 외부조인!</li>
</ol>
<h1 id="영속성-전이">영속성 전이</h1>
<ul>
<li><p>특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용함</p>
</li>
<li><p>JPA에서는 엔티티를 저장할 때, 연관된 모든 엔티티는 영속 상태여야 함</p>
<ul>
<li>영속성 전이 ❌ : 부모 영속 후 자식도 각각 영속으로 만들어야 했음</li>
<li>영속성 전이 ⭕ : 부모 영속만 영속상태로 만들어도 자식도 영속상태가 됨</li>
</ul>
</li>
</ul>
<h2 id="cascade-옵션">CASCADE 옵션</h2>
<ul>
<li>ALL,            // 모두 적용</li>
<li>PERSIST,        // 영속
<img src="https://velog.velcdn.com/images/go_ruth/post/5d01d4e6-be7d-43fe-8081-1067f1485d44/image.png" alt=""></li>
</ul>
<pre><code class="language-java">Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
child1.setParent(parent);
child2.setParent(parent);

parent.getChildren().add(child1);
parent.getChildren().add(child2);

//부모 저장, 연관된 자식들 저장
em.persist(parent);</code></pre>
<ul>
<li><p>MERGE,        // 병합</p>
</li>
<li><p>REMOVE,        // 삭제</p>
<pre><code class="language-java">Parent findParent = em.find(Parent.class, 1L);
부모와 연관 자식도 모두 제거
em.remove(findParent);</code></pre>
</li>
<li><p>REFRESH,        </p>
</li>
<li><p>DETACH</p>
</li>
</ul>
<h1 id="고아객체">고아객체</h1>
<ul>
<li>JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공</li>
<li><blockquote>
<p>orphanRemoval = true</p>
</blockquote>
</li>
</ul>
<p>💡 참조하는 곳이 하나일 때만 사용해야 함!
💡 @OneToMany에만 사용 가능</p>
<h1 id="영속성-전이--고아객체---생명주기-controll">영속성 전이 + 고아객체 -&gt; 생명주기 Controll</h1>
<ul>
<li><p>일반적으로 엔티티는 persist로 영속, remove를 통해 제거</p>
</li>
<li><blockquote>
<p>엔티티 스스로 생명주기를 관리한다는 뜻!</p>
</blockquote>
</li>
<li><p>CascadeType.ALL + orphanRemoval = true</p>
</li>
<li><blockquote>
<p>부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다는 뜻</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Algorithm) 백준 21922]]></title>
            <link>https://velog.io/@go_ruth/Algorithm-%EB%B0%B1%EC%A4%80-21922-</link>
            <guid>https://velog.io/@go_ruth/Algorithm-%EB%B0%B1%EC%A4%80-21922-</guid>
            <pubDate>Thu, 24 Apr 2025 12:02:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/go_ruth/post/d80605bf-7fc2-4697-a824-97e37866c970/image.png" alt=""></p>
<h3 id="문제-설명">문제 설명</h3>
<p>해당 문제는 말 그대로 에어컨의 모든 바람 이동 위치를 파악 후 해당 갯수를 구해는 구현문제였습니다.</p>
<p>다만 고려해야 할 몇가지가 있었는데,</p>
<ol>
<li>에어컨은 여러개가 가능하다.</li>
<li>바람이 이동한 횟수를 count ❌, 바람이 지나가는 자리의 ⭕</li>
<li>에어컨, 물건이 있는 자리 모두 바람 ⭕</li>
</ol>
<p>라는 것이였습니다. </p>
<p>위에 3가지 사항은 사실 문제 지문과 예시 그림만 봐도 파악이 가능했었기 때문에 로직만 잘 파악하고 있다면 비교적 쉽게 구현이 가능한 문제였습니다.</p>
<h3 id="내가-풀었던-방식">내가 풀었던 방식</h3>
<pre><code class="language-java">public class Main {
    // 방향을 2차원 배열로 생성
    static int[][] dirs = 
        {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    // y, x값을 입력받는 내부 class 생성
    static class Node {
        int y, x;

        Node(int y, int x) {
            this.y = y;
            this.x = x;
        }
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br =
                new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st =
                new StringTokenizer(br.readLine());
        int row = Integer.parseInt(st.nextToken());
        int col = Integer.parseInt(st.nextToken());
        // 에어컨을 저장할 List 생성 -&gt; 여러개 에어컨 가능!
        List&lt;Node&gt; airController = new ArrayList&lt;&gt;();
        // 자리들을 저장하는 2차원 배열
        int[][] arr = new int[row][col];

        // 자리 input 및 에어컨을 List에 저장
        for (int i = 0; i &lt; row; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; col; j++) {
                int value = Integer.parseInt(st.nextToken());
                arr[i][j] = value;
                if (value == 9) {
                    airController.add(new Node(i, j));
                }
            }
        }

        // 접근했던 방향 판단용 3차원 boolean 배열 생성
        boolean[][][] v = new boolean[row][col][4];

        int result = 0;
        // 에어컨 별 bfs 실행
        for (Node air : airController) {
            bfs(arr, v, air.y, air.x, row, col);
        }

        // 3차원 배열을 통해 count
        for (boolean[][] vv : v) {
            next:
            for (boolean[] vvv : vv) {
                for (boolean vvvv : vvv) {
                    if (vvvv) {
                        result++;
                        continue next;
                    }
                }
            }
        }

        //출력
        System.out.println(result);
    }

    static void bfs(int[][] arr, boolean[][][] v, int preY, int preX, int row, int col) {
        // queue 생성 및 초기 세팅
        // 4방향으로 이동 가능하니, 0 ~ 3까지 입력
        ArrayDeque&lt;int[]&gt; queue = new ArrayDeque&lt;&gt;();
        for (int i = 0; i &lt; 4; i++) {
            queue.offer(new int[]{preY, preX, i});
            v[preY][preX][i] = true;
        }

        // queue가 비일 때까지 실행
        while (!queue.isEmpty()) {
            int[] qr = queue.poll();
            int d = qr[2];
            int ny = qr[0] + dirs[d][0];
            int nx = qr[1] + dirs[d][1];

            // 배열 내에서 움직일 때
            if (ny &gt;= 0 &amp;&amp; ny &lt; row &amp;&amp; nx &gt;= 0 &amp;&amp; nx &lt; col) {
                // 방향 변환 메소드
                int nd = changeDir(arr, ny, nx, d);

                // 이미 왔던 방향이면 continue
                if (v[ny][nx][nd]) {
                    continue;
                }

                // queue에 input, 방문여부 true
                queue.add(new int[]{ny, nx, nd});
                v[ny][nx][nd] = true;
            }
        }
    }

    // 방향전환 메서드
    static int changeDir(int[][] arr, int y, int x, int d) {
        if (arr[y][x] == 0) {
            return d;
        }

        switch (arr[y][x]) {
            case 1:
                if (d == 0) {
                    d = 2;
                } else if (d == 2) {
                    d = 0;
                }
                break;
            case 2:
                if (d == 3) {
                    d = 1;
                } else if (d == 1) {
                    d = 3;
                }
                break;
            case 3:
                if (d == 0) {
                    d = 3;
                } else if (d == 1) {
                    d = 2;
                } else if (d == 2) {
                    d = 1;
                } else if (d == 3) {
                    d = 0;
                }
                break;
            case 4:
                if (d == 0) {
                    d = 1;
                } else if (d == 1) {
                    d = 0;
                } else if (d == 2) {
                    d = 3;
                } else if (d == 3) {
                    d = 2;
                }
                break;
        }

        return d;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/ed0d825c-e831-495a-9c60-87dff31db891/image.png" alt=""></p>
<h3 id="문제점">문제점</h3>
<ol>
<li>switch문 같은 조건문 사용을 통해 일일히 방향전환 진행</li>
<li>3차원 boolean 배열 사용으로 비효율적인 메모리 사용</li>
</ol>
<h3 id="다른-사람들의-방식">다른 사람들의 방식</h3>
<ol>
<li>조건문 대신 방향전환용 2차원 배열 사용<pre><code class="language-java">static int[][] convert = { { 0, 0, 2, 1, 3 }, { 0, 3, 1, 0, 2 }, { 0, 2, 0, 3, 1 }, { 0, 1, 3, 2, 0 } };</code></pre>
</li>
<li>3차원 boolean 배열 대신 2차원 Byte배열 + 비트마스킹을 통한 visited 파악<pre><code class="language-java">// 4방향 각각 방문 
boolean[][][] v = new boolean[row][col][4];  
// 각 칸마다 4비트를 사용 (0000 ~ 1111)
byte[][] v = new byte[row][col]; </code></pre>
</li>
</ol>
<h3 id="수정-후-코드">수정 후 코드</h3>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

public class Main {
    static int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    static int[][] turns = 
        { { 0, 2, 0, 3, 1 }, 
          { 1, 1, 3, 2, 0 }, 
          { 2, 0, 2, 1, 3 }, 
          { 3, 3, 1, 0, 2 } };

    static class Node {
        int y, x;

        Node(int y, int x) {
            this.y = y;
            this.x = x;
        }
    }

    public static void main(String[] args) throws Exception {
        BufferedReader br =
                new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st =
                new StringTokenizer(br.readLine());
        int row = Integer.parseInt(st.nextToken());
        int col = Integer.parseInt(st.nextToken());
        List&lt;Node&gt; airController = new ArrayList&lt;&gt;();
        int[][] arr = new int[row][col];

        for (int i = 0; i &lt; row; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; col; j++) {
                int value = Integer.parseInt(st.nextToken());
                arr[i][j] = value;
                if (value == 9) {
                    airController.add(new Node(i, j));
                }
            }
        }

        byte[][] v = new byte[row][col];

        int result = 0;
        for (Node air : airController) {
            bfs(arr, v, air.y, air.x, row, col);
        }

        for (int i = 0; i &lt; row; i++) {
            for (int j = 0; j &lt; col; j++) {
                if (v[i][j] != 0) result++;
            }
        }

        System.out.println(result);
    }

    static void bfs(int[][] arr, byte[][] v, int preY, int preX, int row, int col) {
        ArrayDeque&lt;int[]&gt; queue = new ArrayDeque&lt;&gt;();
        for (int i = 0; i &lt; 4; i++) {
            queue.offer(new int[]{preY, preX, i});
            v[preY][preX] |= (1 &lt;&lt; i); 
        }

        while (!queue.isEmpty()) {
            int[] qr = queue.poll();
            int d = qr[2];
            int ny = qr[0] + dirs[d][0];
            int nx = qr[1] + dirs[d][1];

            if (ny &gt;= 0 &amp;&amp; ny &lt; row &amp;&amp; nx &gt;= 0 &amp;&amp; nx &lt; col) {
                if(arr[ny][nx] == 9) {
                    continue;
                }

                int nd = turns[d][arr[ny][nx]];

                if ((v[ny][nx] &amp; (1 &lt;&lt; nd)) != 0) {
                    continue;
                }

                queue.add(new int[]{ny, nx, nd});
                v[ny][nx] |= (1 &lt;&lt; nd);
            }
        }
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/807c6126-25ae-4ef7-be5a-5b1ae7ae0bd0/image.png" alt=""></p>
<h3 id="느낀-점">느낀 점</h3>
<ul>
<li>그 동안 boolean배열을 습관적으로 사용해왔는데, 비트마스킹의 속도와 메모리 절약을 보고 앞으로는 비트마스킹을 더 적극적으로 사용해야 겠다는 생각이 들어습니다.</li>
<li>Index를 통한 방향전환은 조건문이 아닌 다른 방식으로 풀 수 있다는 생각의 전환방식을 알게 되었습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA ) 자바 ORM 표준 JPA 프로그래밍 - 7]]></title>
            <link>https://velog.io/@go_ruth/JPA-ORM-JPA-7</link>
            <guid>https://velog.io/@go_ruth/JPA-ORM-JPA-7</guid>
            <pubDate>Tue, 22 Apr 2025 10:45:31 GMT</pubDate>
            <description><![CDATA[<h1 id="1-상속관계-매핑">1. 상속관계 매핑</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/4433f0be-c05e-4891-9520-5df375159371/image.png" alt=""></p>
<ul>
<li>DB에는 상속이라는 개념 ❌</li>
<li><blockquote>
<p>대신 그림과 같이 슈퍼타입 서브타입 관계라는 모델링 기법이 가장 유사</p>
</blockquote>
</li>
<li>실제 구현 방법<ul>
<li>각각의 Table로 변환 (Join 전략)<ul>
<li>각각을 모두 테이블로 만듬</li>
<li>조회 시 조인사용</li>
</ul>
</li>
<li>통합 테이블로 변환 (단일 테이블 전략)<ul>
<li>테이블을 하나만 사용해서 통합</li>
</ul>
</li>
<li>서브타입 테이브로 변환 (구현 클래스마다 테이블 전략)<ul>
<li>서브 타입마다 하나의 테이블을 만듬</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="11-조인전략">1.1. 조인전략</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/aeca32de-fc0b-4d75-be89-64e54ca4ed5a/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)
public abstract class Parent {
 @Id @GeneratedValue
 @Column(name = &quot;Parent_ID&quot;)
 private Long id;
 private String name; //이름
 private int price; //가격
}
@Entity
@DiscriminatorValue(&quot;C1&quot;)
public class Child1 extends Parent {
 private String c_1;
}
@Entity
@DiscriminatorValue(&quot;C2&quot;)
public class Child2 extends Parent {
 private String c_2; //감독
 private String c_22; //배우
}
@Entity
@DiscriminatorValue(&quot;C3&quot;)
public class Child3 extends Parent {
 private String c_3; //배우
}</code></pre>
<ul>
<li>@Inheritance(strategy = InheritanceType.JOINED)<ul>
<li>상속 매핑은 부모 클래스에 @Inheritance를 사용해야 함. </li>
<li>매핑 전략을 지정해야 하는데, 조인 전략을 사용하므로InheritanceType.JOINED를 사용했다.</li>
</ul>
</li>
<li>@DiscriminatorColumn(name = &quot;DTYPE&quot;)<ul>
<li>부모 클래스에 구분 컬럼을 지정</li>
<li>이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 됨</li>
</ul>
</li>
<li>@DiscriminatorValue(&quot;M&quot;)<ul>
<li>엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정. </li>
<li>만약 Child3 엔티티를 저장하면 구분 컬럼인 DTYPE에 값 C3이 저장된다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong>장점</strong><ul>
<li>테이블 정규화</li>
<li>외래 키 참조 무결성 제약조건을 활용 가능</li>
<li>저장 공간을 효율적으로 사용</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>조회할 때, 조인이 많이 사용되므로 성능이 저하 가능</li>
<li>조회 쿼리가 복잡</li>
<li>데이터를 등록할 Insert SQL를 두 번 실행</li>
</ul>
</li>
</ul>
<h2 id="12-단일-테이블-전략">1.2. 단일 테이블 전략</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/25d10e93-79b9-40b7-a708-1cdd6200d626/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = &quot;DTYPE&quot;)
public abstract class Parent {
 @Id @GeneratedValue
 @Column(name = &quot;Parent_ID&quot;)
 private Long id;
 private String name;
 private int price;
}

@Entity
@DiscriminatorValue(&quot;C1&quot;)
public class Child1 extends Parent {
 private String c_1;
}

@Entity
@DiscriminatorValue(&quot;C2&quot;)
public class Child2 extends Parent {
 private String c_2;
 private String c_22;
}

@Entity
@DiscriminatorValue(&quot;C3&quot;)
public class Child3 extends Parent {
 private String c_3;
}</code></pre>
<ul>
<li><p><strong>장점</strong></p>
<ul>
<li>조인이 필요 ❌ -&gt; 조회 성능이 빠름</li>
<li>조회 쿼리가 단순</li>
</ul>
</li>
<li><p><strong>단점</strong></p>
<ul>
<li>자식 Entity가 매핑한 컬럼은 모두 null 허용</li>
<li>단일 테이블에 모든 것을 저장하여, 테이블이 커질 수 있음</li>
<li><blockquote>
<p>상황에 따라서는 조회 성능이 오히려 느려짐</p>
</blockquote>
</li>
</ul>
</li>
<li><p><strong>특징</strong></p>
<ul>
<li>구분 컬럼을 꼭 사용해야 함</li>
<li><blockquote>
<p>@DiscriminatorColumn 설정 필수</p>
</blockquote>
</li>
<li>@DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용</li>
</ul>
</li>
</ul>
<h2 id="13-구현-클래스마다-테이블-전략">1.3. 구현 클래스마다 테이블 전략</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/f32768c4-a9cf-4567-b461-534a81ac7dcf/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Parent {
 @Id @GeneratedValue
 @Column(name = &quot;Parent_ID&quot;)
 private Long id;
 private String name;
 private int price;
}

@Entity
public class Child1 extends Parent {
 private String c_1;
}

@Entity
public class Child2 extends Parent {
 private String c_2;
 private String c_22;
}

@Entity
public class Child3 extends Parent {
 private String c_3;
}</code></pre>
<ul>
<li><strong>장점</strong><ul>
<li>서브 타입을 구분해서 처리할 때, 효과정</li>
<li>not null 제약조건을 사용</li>
</ul>
</li>
<li><strong>단점</strong><ul>
<li>여러 자식 테이블을 함께 조회할 때, 성능이 느려짐</li>
<li>자식 테이블을 통합해서 쿼리하기 어려움</li>
</ul>
</li>
<li><strong>특징</strong><ul>
<li>구분 컬럼을 사용 ❌</li>
</ul>
</li>
</ul>
<h1 id="2-mappedsuperclass">2. @MappedSuperclass</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/1a06fdb4-7b08-45c8-8221-7fd6e053e370/image.png" alt=""></p>
<pre><code class="language-java">@MappedSuperclass
public abstract class Base {
 private String name;
 private Long price;
 ...
}
@Entity
public class Child_1 extends Base {
 private Long id;
 private String c_1;
 ...
}
@Entity
public class Child_2 extends Base {
 private Long id;
 private String c_2;
 private String c_22;
}
@Entity
public class Child_3 extends Base {
 private Long id;
 private String c_3;
}</code></pre>
<ul>
<li>부모 클래스는 테이블 매핑 ❌, 자식 클래스에게 매핑 정보만 제공!</li>
<li><blockquote>
<p>@MappedSuperclass</p>
</blockquote>
</li>
<li>@MappedSuperclass를 비유하자면 추상클래스와 비슷</li>
<li><blockquote>
<p>매핑 정보를 상속할 목적으로만 사용</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA ) 자바 ORM 표준 JPA 프로그래밍 - 6]]></title>
            <link>https://velog.io/@go_ruth/JPA-ORM-JPA-6</link>
            <guid>https://velog.io/@go_ruth/JPA-ORM-JPA-6</guid>
            <pubDate>Sun, 20 Apr 2025 10:38:07 GMT</pubDate>
            <description><![CDATA[<h1 id="1-다대일">1. 다대일</h1>
<h2 id="11-단방향">1.1. 단방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/6d7c72f6-5c85-4bdc-b2fb-38dc8acfd08b/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    private String bb;
}
</code></pre>
<h2 id="12-양방향">1.2. 양방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/e96ded18-d736-4003-9442-0770e7e1bd97/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @OneToMany(mappedBy = &quot;b&quot;)
    private List&lt;A&gt; a_list = new ArrayList&lt;&gt;();

    private String bb;
}
</code></pre>
<h1 id="2-일대다">2. 일대다</h1>
<h2 id="21-단방향">2.1. 단방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/b912efd6-2787-4164-b672-5b7d272583f7/image.png" alt=""></p>
<blockquote>
<p>⭐ <strong>단점</strong>
매핑한 객체가 관리하는 외래 키가 다른 테이블에 있음
연관관계 처리를 위해 UPDATE SQL이 추가로 실행</p>
</blockquote>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @OneToMany(mappedBy = &quot;b&quot;)
    private List&lt;A&gt; a_list = new ArrayList&lt;&gt;();

    private String bb;
}
</code></pre>
<h2 id="22-양방향">2.2. 양방향</h2>
<ul>
<li>일대다 양방향 매핑은 존재하지 않는다. </li>
<li>불가능은 아니지만 일대다의 단점을 그대로 가짐</li>
</ul>
<h1 id="3-일대일">3. 일대일</h1>
<ul>
<li>양쪽이 서로 하나의 관계만 가짐</li>
<li>주 Table, 대상 Table 둘 중 어느 곳이나 외래 키를 가질 수 있음</li>
<li>주 Table 외래 키<ul>
<li>확인해도 대상 테이브과 연관관계가 있는 지 알 수 있음</li>
</ul>
</li>
<li>대상 Table 외래 키<ul>
<li>DB 관계를 일대일에서 일대다로 변경할 때, 테이블 구조를 그대로 유지가능</li>
</ul>
</li>
</ul>
<h2 id="31-주-테이블-단방향">3.1. 주 테이블 단방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/b6b08e35-89e9-4b4e-b878-914a48b1f63f/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @OneToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    private String bb;
}</code></pre>
<h2 id="32-대상-테이블-단방향">3.2. 대상 테이블 단방향</h2>
<ul>
<li>일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않음</li>
</ul>
<h2 id="33-주-테이블-양방향">3.3. 주 테이블 양방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/3653c1b7-49a1-4340-9a36-1097e6d64998/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @OneToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @OneToOne(mappedBy = &quot;b&quot;)
    private A a;

    private String bb;
}
</code></pre>
<h2 id="34-대상-테이블-양방향">3.4. 대상 테이블 양방향</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/b0bbd6b6-2a1e-4742-8962-7f3f3d567765/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @OneToOne(mappedBy = &quot;a&quot;)
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @OneToOne
    @JoinColumn(name = &quot;A_ID&quot;)
    private A a;

    private String bb;
}</code></pre>
<h1 id="4-다대다">4. 다대다</h1>
<ul>
<li>RDB는 정규화된 Table 2개로 다대다 관계를 표현할 수 없다</li>
<li><blockquote>
<p>그래서 보통 다대일, 일대다로 풀어내는 연결 테이블 사용</p>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/081b655a-3c8a-4b8a-aea3-7fd0b595458c/image.png" alt="">
<img src="https://velog.velcdn.com/images/go_ruth/post/6afe8b69-84a1-4f44-9627-90f25a888429/image.png" alt=""></p>
<h2 id="41-단방향">4.1. 단방향</h2>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToMany
    @JointTable(name = &quot;B&quot;,
                joinColums = @JoinColumn(name = &quot;A_ID&quot;),
                inverseJoinColumns = @JoinColumn(name = &quot;B_ID&quot;))
    private List&lt;B&gt; b_list = new ArrayList&lt;&gt;();

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    private String bb;
}</code></pre>
<ul>
<li>@ManyToMany와 @JoinTable을 사용해서 연결 테이블을 바로 매핑</li>
<li>@JoinTable<ul>
<li>name : 연결할 테이블 지정</li>
<li>joinColumns : 현재 방향인 A와 매핑할 조인 컬럼 정보 지정</li>
<li>inverseJoinColumns : 반대 방향인 B와 매핑할 조인 컬럼 정보 지정</li>
</ul>
</li>
</ul>
<h2 id="42-양방향">4.2. 양방향</h2>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToMany
    @JointTable(name = &quot;B&quot;,
                joinColums = @JoinColumn(name = &quot;A_ID&quot;),
                inverseJoinColumns = @JoinColumn(name = &quot;B_ID&quot;))
    private B b;

    private String aa;
}

-- 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @ManyToMany(mappedBy = &quot;a_list&quot;)
    private List&lt;A&gt; a_list = new ArrayList&lt;&gt;();

    private String bb;
}</code></pre>
<h2 id="43-연결-엔티티-사용">4.3. 연결 엔티티 사용</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/8d6e8911-8109-42d2-b80b-acc5021a8b82/image.png" alt=""></p>
<ul>
<li>@ManyToMany를 사용하면 연결 테이블을 자동 처리해주므로 도메인 모델이 단순</li>
<li><blockquote>
<p>실무에서 사용하기에는 한계가 있음</p>
</blockquote>
</li>
<li>위 그림처럼 컬럼을 추가하면 @ManyToMany를 사용 ❌</li>
<li><blockquote>
<p>A or B Entity에는 추가한 컬럼을 매핑 ❌</p>
</blockquote>
</li>
</ul>
<blockquote>
<p>💡 양방향으로 진행해야 함</p>
</blockquote>
<h2 id="44-새로운-기본-키-사용">4.4. 새로운 기본 키 사용</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/5e40e1fb-92fc-47de-a18a-a8acff2e0514/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class AB {
    @Id
    @Column(name = &quot;AB_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;A_ID&quot;)
    private A a;

    @ManyToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA ) 자바 ORM 표준 JPA 프로그래밍 - 5]]></title>
            <link>https://velog.io/@go_ruth/JPA-ORM-JPA-5</link>
            <guid>https://velog.io/@go_ruth/JPA-ORM-JPA-5</guid>
            <pubDate>Wed, 16 Apr 2025 00:47:12 GMT</pubDate>
            <description><![CDATA[<h1 id="연관관계-매핑-전-핵심-키워드">연관관계 매핑 전 핵심 키워드</h1>
<ol>
<li><strong>방향</strong><ul>
<li>단방향 : 둘 중 한 쪽만 참조하는 것</li>
<li>양방향 : 양쪽 모두 서로 참조하는 것</li>
</ul>
</li>
<li><strong>다중성 (M / N)</strong><ul>
<li>다대일 : M 여러개가 N 하나에 속한다.</li>
<li>일대다 : N 여러개가 M 하나에 속한다.</li>
<li>일대일 : M 하나가 N 하나에 속한다.</li>
<li>다대다 : M 여러개가 N 여러개에 속한다.</li>
</ul>
</li>
<li><strong>연관관계의 주인</strong><ul>
<li>연관관계의 주인 : 객체를 양방향 연관관계로 만들면 주인을 정해야 한다.</li>
</ul>
</li>
</ol>
<h1 id="단방향에서의-연관관계">단방향에서의 연관관계</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/b0e3bce3-6821-404d-9970-fc96f4a77977/image.png" alt=""></p>
<h2 id="객체">객체</h2>
<ul>
<li>참조(주소)로 연관관계를 맺음</li>
<li>객체 A는 A.b필드로 B객체와 연관관계</li>
<li>A는 B 파악 가능, B는 A 파악 불가능</li>
<li><strong>객체 그래프 탐색</strong><ul>
<li>객체는 참조를 사용해서 연관관계를 탐색</li>
</ul>
</li>
</ul>
<h2 id="테이블">테이블</h2>
<ul>
<li>테이블은 외래키로 연관관계를 맺음</li>
<li>A 테이블은 B_ID 외래 키로 팀 테이블로 연관관계</li>
<li>A와 B는 양방향, 양쪽은 외래키로 Join 가능</li>
</ul>
<blockquote>
<p>객체와 테이블의 연관관계의 차이!</p>
<ol>
<li>참조를 통한 연관관계는 언제나 <strong>단방향</strong></li>
</ol>
<p>-&gt; 결국, 연관관계를 추가해서 양쪽 참조를 해야 함
2. 단, 참조를 통한 양쪽 참조 != 양방향 관계
-&gt; 서로 다른 단방향 2개</p>
</blockquote>
<h2 id="객체-관계-매핑">객체 관계 매핑</h2>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/8fe7766b-1a68-4de3-8046-0bf905274cf3/image.png" alt=""></p>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

// 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    private String bb;
}</code></pre>
<ul>
<li>@ManyToOne<ul>
<li>다대일 관계라는 매핑 정보</li>
<li>연관관계를 매핑할 때, 다중성을 나타내는 Annotation 필수</li>
</ul>
</li>
<li>@JoinColumn<ul>
<li>조인 컬럼은 오래키를 매핑할 때 사용</li>
<li>name속성 </li>
<li><blockquote>
<p>매핑할 외래키 지정</p>
</blockquote>
</li>
<li>생략가능</li>
<li><blockquote>
<p>생략 시, 찾는 기본 전략 (필드명_참조테이블컬럼명)</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<h2 id="연관관계-사용">연관관계 사용</h2>
<h3 id="저장">저장</h3>
<ul>
<li>A Entity에서 B Entity를 참조 후 저장한다고 가정</li>
<li><blockquote>
<p>JPA는 참조한 B의 식별자를 외래 키로 사용해서 등록쿼리 생성</p>
</blockquote>
</li>
<li><blockquote>
<p>실행된 SQL에는 B의 식별자가 입력</p>
</blockquote>
</li>
</ul>
<h3 id="제거">제거</h3>
<ul>
<li>삭제 전 기존의 연관관계를 먼저 제거해야 함</li>
<li><blockquote>
<p>안하면 외래 키 제약조건으로 DB Error</p>
</blockquote>
</li>
</ul>
<h1 id="양방향-연관관계">양방향 연관관계</h1>
<p><img src="https://velog.velcdn.com/images/go_ruth/post/23268d45-5202-4c24-8193-e01cbddbd74a/image.png" alt=""></p>
<h2 id="객체-1">객체</h2>
<ul>
<li>일대다 관계는 여러 건과 연관관계를 맺을 수 있음</li>
<li><blockquote>
<p>Collection을 사용해야 함 (List, Set, Map 등등)</p>
</blockquote>
</li>
</ul>
<h2 id="테이블-1">테이블</h2>
<ul>
<li>DB은 외래 키 하나로 양방향 조회가능, 이전 관계와는 차이 ❌</li>
</ul>
<h2 id="객체-관계-매핑-1">객체 관계 매핑</h2>
<pre><code class="language-java">@Entity
@Data
public class A {
    @Id
    @Column(name = &quot;A_ID&quot;)
    private Long id;

    @ManyToOne
    @JoinColumn(name = &quot;B_ID&quot;)
    private B b;

    private String aa;
}

// 🔺는 A 클래스 , 🔻는 B클래스

@Entity
@Data
public class B {
    @Id
    @Column(name = &quot;B_ID&quot;)
    private Long id;

    @OneToMany(mappedBy = &quot;b&quot;)
    private List&lt;A&gt; b = new ArrayList&lt;&gt;();

    private String bb;
}</code></pre>
<h2 id="연관관계의-주인">연관관계의 주인</h2>
<ul>
<li><p>테이블은 외래 키 하나로 연관관계 관리</p>
</li>
<li><p>객체는 두 객체간 서로를 참조 </p>
</li>
<li><blockquote>
<p>어떤 관계를 사용해서 외래 키를 관리해야할까?
JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키 관리를 지시</p>
</blockquote>
</li>
<li><p>두 연관관계 중 하나를 연관관계의 주인으로 정해야 함</p>
<ul>
<li>주인 ⭕ : DB 연관관계와 Mapping, 외래키를 관리 (CUD)</li>
<li>주인 ❌ : Only Read</li>
<li>mappedBy로 Setting -&gt; 주인이 아닌 Entity에서 사용 
💡 <strong>다대일, 일대다 관계는 항상 다 쪽이 주인!</strong></li>
</ul>
</li>
</ul>
<h2 id="양방향-연관관계의-주의점">양방향 연관관계의 주의점</h2>
<ul>
<li>연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것</li>
</ul>
<pre><code class="language-java">
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf =
            Persistence.createEntityManagerFactory(&quot;myJpaUnit&quot;);
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();

        try {
            transaction.begin();

            B bc = new B(1L, new ArrayList&lt;&gt;(), &quot;firstB&quot;);
            A ac1 = new A(1L, null, &quot;firstA&quot;);
            A ac2 = new A(2L, null, &quot;secondA&quot;);

            bc.getA().add(ac1);
            bc.getA().add(ac2);

            // 비영속 -&gt; 영속
            em.persist(bc);
            // DB에 데이터 저장
            transaction.commit();

            // find -&gt; 1차 캐시에 영속 Entity 존재하므로 1차캐시에서 Get
            /*
            B(
              id=1, 
              a=[
                A(id=1, b=null, aa=firstA), 
                A(id=2, b=null, aa=secondA)
              ], 
              bb=firstB
            )
            */ 
            System.out.println(em.find(B.class, 1L));

            // 영속 -&gt; 준영속
            em.detach(bc);

            // find -&gt; 1차 캐시에 영속 Entity ❌, DB에서 1차 캐시에 영속 Entity 저장 후 1차 캐시에서 Get
            // B(id=1, a=[], bb=firstB)
            System.out.println(em.find(B.class, 1L));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            em.close();
        }
    }
}</code></pre>
<h2 id="그럼-주인-아닌-곳에-설정-x">그럼 주인 아닌 곳에 설정 X?</h2>
<ul>
<li>객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안정</li>
<li><blockquote>
<p>순수한 객체 상태에서 심각한 문제 발생!
💡 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>