<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>TTK Dev log</title>
        <link>https://velog.io/</link>
        <description>개발을 좋아하는 taketheking 입니다.</description>
        <lastBuildDate>Thu, 20 Mar 2025 07:46:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>TTK Dev log</title>
            <url>https://velog.velcdn.com/images/take_the_king/profile/c28ae6b0-a92b-4c4f-8a5f-13ae1db71fa9/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. TTK Dev log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/take_the_king" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[정규화]]></title>
            <link>https://velog.io/@take_the_king/%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@take_the_king/%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Thu, 20 Mar 2025 07:46:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>간단 정리
정규화란 데이터를 안전하게 관리하기 위한 설계 방법이다. 
제1 정규화는 한 컬럼에 하나의 데이터만 존재하도록 만드는 것이다.
제2 정규화는 제1 정규화 + 현재 테이블의 주제와 관련이 없는 컬럼을 분리(다른 테이블)하는 것이다.
제3 정규화는 제2 정규화 + 일반 컬럼에만 종속된 컬럼을 분리(다른 테이블)하는 것이다.</p>
</blockquote>
<h1 id="db-정규화">DB 정규화</h1>
<p>데이터베이스 설계에서 정규화(Normalization) 는 데이터 중복을 제거하고, 데이터 무결성을 유지하며, 이상 현상(anomaly)을 방지하기 위해 사용되는 기법입니다. 이 글에서는 정규화의 기본 개념과 주요 정규형(1NF, 2NF, 3NF, BCNF 등)을 살펴보고, 실제 예제를 통해 정규화가 어떻게 적용되는지 확인해보겠습니다.</p>
<h2 id="1-정규화의-기본-개념">1. 정규화의 기본 개념</h2>
<p>정규화의 주요 목적은 다음과 같습니다</p>
<blockquote>
</blockquote>
<p>데이터 중복 제거: 동일한 정보를 여러 테이블에 반복 저장하지 않아 데이터 불일치를 방지합니다.
데이터 무결성 유지: 삽입, 갱신, 삭제 작업 시 발생할 수 있는 이상 현상을 최소화합니다.
효율적인 관리: 관련 데이터를 논리적으로 분리하여 유지보수와 쿼리 성능을 개선합니다.</p>
<h2 id="2-정규형의-단계">2. 정규형의 단계</h2>
<h3 id="-제1-정규화-">[ 제1 정규화 ]</h3>
<p>제1 정규화란 테이블의 컬럼이 원자값(Atomic Value, 하나의 값)을 갖도록 테이블을 분해하는 것이다. 예를 들어 아래와 같은 고객 취미 테이블이 존재한다고 하자. </p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/fa516981-e85e-4092-8e12-4f856dd9df7d/image.png" alt=""></p>
<p>위의 테이블에서 추신수와 박세리는 여러 개의 취미를 가지고 있기 때문에 제1 정규형을 만족하지 못하고 있다. 그렇기 때문에 이를 제1 정규화하여 분해할 수 있다. 제1 정규화를 진행한 테이블은 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/1e13afe3-af65-477c-bddc-262ff46585d8/image.png" alt=""></p>
<p> </p>
<h3 id="-제2-정규화-">[ 제2 정규화 ]</h3>
<p>제2 정규화란 제1 정규화를 진행한 테이블에 대해 완전 함수 종속을 만족하도록 테이블을 분해하는 것이다. 여기서 완전 함수 종속이라는 것은 기본키의 부분집합이 결정자가 되어선 안된다는 것을 의미한다. 예를 들어 아래와 같은 수강 강좌 테이블을 살펴보자. 
 
<img src="https://velog.velcdn.com/images/take_the_king/post/0106dcd9-854a-45b4-a3af-731a9d2b02ad/image.png" alt=""></p>
<p>이 테이블에서 기본키는 (학생번호, 강좌이름)으로 복합키이다. 그리고 (학생번호, 강좌이름)인 기본키는 성적을 결정하고 있다. (학생번호, 강좌이름) --&gt; (성적) 그런데 여기서 강의실이라는 컬럼은 기본키의 부분집합인 강좌이름에 의해 결정될 수 있다. (강좌이름) --&gt; (강의실)즉, 기본키(학생번호, 강좌이름)의 부분키인 강좌이름이 결정자이기 때문에 위의 테이블의 경우 다음과 같이 기존의 테이블에서 강의실을 분해하여 별도의 테이블로 관리하여 제2 정규형을 만족시킬 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/a76923f8-742c-4f28-8f1c-42bcae81d852/image.png" alt=""></p>
<p> 
 </p>
<h3 id="-제3-정규화-">[ 제3 정규화 ]</h3>
<p>제3 정규화란 제2 정규화를 진행한 테이블에 대해 이행적 종속을 없애도록 테이블을 분해하는 것이다. 여기서 이행적 종속이라는 것은 A -&gt; B, B -&gt; C가 성립할 때 A -&gt; C가 성립되는 것을 의미한다. 예를 들어 아래와 같은 계절 학기 테이블을 살펴보자. </p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/399d1174-9e3b-4ad2-a675-c9afe5053296/image.png" alt=""></p>
<p> 
 
기존의 테이블에서 학생 번호는 강좌 이름을 결정하고 있고, 강좌 이름은 수강료를 결정하고 있다. 그렇기 때문에 이를 (학생 번호, 강좌 이름) 테이블과 (강좌 이름, 수강료) 테이블로 분해해야 한다. 
이행적 종속을 제거하는 이유는 비교적 간단하다. 예를 들어 501번 학생이 수강하는 강좌가 스포츠경영학으로 변경되었다고 하자. 이행적 종속이 존재한다면 501번의 학생은 스포츠경영학이라는 수업을 20000원이라는 수강료로 듣게 된다. 물론 강좌 이름에 맞게 수강료를 다시 변경할 수 있지만, 이러한 번거로움을 해결하기 위해 제3 정규화를 하는 것이다. 즉, 학생 번호를 통해 강좌 이름을 참조하고, 강좌 이름으로 수강료를 참조하도록 테이블을 분해해야 하며 그 결과는 다음의 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/109f8838-faa7-49dc-9932-6b59aa363848/image.png" alt=""></p>
<p> 
 </p>
<h3 id="-bcnf-정규화-">[ BCNF 정규화 ]</h3>
<p>BCNF 정규화란 제3 정규화를 진행한 테이블에 대해 모든 결정자가 후보키가 되도록 테이블을 분해하는 것이다. 예를 들어 다음과 같은 특강수강 테이블이 존재한다고 하자.
 
<img src="https://velog.velcdn.com/images/take_the_king/post/08a987b5-645a-4599-991f-70d7d6a9bbd4/image.png" alt=""></p>
<p> 
특강수강 테이블에서 기본키는 (학생번호, 특강이름)이다. 그리고 기본키 (학생번호, 특강이름)는 교수를 결정하고 있다. 또한 여기서 교수는 특강이름을 결정하고 있다. 그런데 문제는 교수가 특강이름을 결정하는 결정자이지만, 후보키가 아니라는 점이다. 그렇기 때문에 BCNF 정규화를 만족시키기 위해서 위의 테이블을 분해해야 하는데, 다음과 같이 특강신청 테이블과 특강교수 테이블로 분해할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/1c164334-3eab-40d3-8003-6467241d0e60/image.png" alt=""></p>
<h2 id="3-실무-적용-예제-주문-관리-시스템">3. 실무 적용 예제: 주문 관리 시스템</h2>
<p>아래는 주문 관리 시스템에서의 비정규화된 테이블을 정규화하는 과정을 보여주는 예제입니다.</p>
<h3 id="31-비정규화된-테이블-구조">3.1 비정규화된 테이블 구조</h3>
<pre><code class="language-sql">
CREATE TABLE Orders (
    OrderID INT,
    CustomerName VARCHAR(100),
    ProductID INT,
    ProductName VARCHAR(100),
    Quantity INT,
    PRIMARY KEY (OrderID, ProductID)
);</code></pre>
<p>이 구조에서는 고객 정보와 상품 정보가 하나의 테이블에 함께 저장되어 데이터 중복 및 업데이트 이상 현상이 발생할 수 있습니다.</p>
<h3 id="32-정규화된-테이블-구조">3.2 정규화된 테이블 구조</h3>
<p>고객과 상품 정보를 별도의 테이블로 분리하여 2NF와 3NF를 만족시키는 예제입니다.</p>
<h4 id="고객-테이블">고객 테이블</h4>
<pre><code class="language-sql">CREATE TABLE Customers (
    CustomerID INT PRIMARY KEY,
    CustomerName VARCHAR(100)
);</code></pre>
<h4 id="상품-테이블">상품 테이블</h4>
<pre><code class="language-sql">
CREATE TABLE Products (
    ProductID INT PRIMARY KEY,
    ProductName VARCHAR(100)
);</code></pre>
<h4 id="주문-및-주문-상세-테이블">주문 및 주문 상세 테이블</h4>
<pre><code class="language-sql">CREATE TABLE Orders (
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE,
    FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID)
);

CREATE TABLE OrderDetails (
    OrderID INT,
    ProductID INT,
    Quantity INT,
    PRIMARY KEY (OrderID, ProductID),
    FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
    FOREIGN KEY (ProductID) REFERENCES Products(ProductID)
);</code></pre>
<p>이 구조를 통해 고객 정보와 상품 정보가 별도의 테이블로 분리되며, 데이터의 중복 저장을 방지하고, 각 테이블의 수정 및 유지보수가 용이해집니다.</p>
<h2 id="4-정규화의-장단점">4. 정규화의 장단점</h2>
<p>장점
데이터 무결성 보장
업데이트 이상(삽입, 삭제, 갱신) 최소화
효율적인 데이터 관리</p>
<p>단점
조인(Join) 연산 증가로 인한 성능 저하 가능성
복잡한 쿼리로 인한 관리 어려움
과도한 정규화 시 오히려 성능 저하 우려</p>
<p>장점: 데이터의 중복을 제거하여 무결성을 높이고, 변경 시 발생할 수 있는 이상 현상을 줄입니다.
단점: 여러 테이블로 분리되어 데이터를 조회할 때 조인이 많이 필요하므로 성능 저하가 발생할 수 있으며, 상황에 따라 일부 의도적인 비정규화(Denormalization)가 필요할 수 있습니다.</p>
<h2 id="결론">결론</h2>
<p>데이터베이스 정규화는 데이터 중복 제거와 무결성 유지를 위한 핵심 설계 기법입니다.
실무에서는 정규화와 비정규화 사이의 균형을 잘 맞추어 최적의 데이터베이스 성능을 확보하는 것이 중요합니다.</p>
<p>위의 예제와 같이, 비정규화된 데이터를 정규화하면 데이터의 일관성과 관리 효율성이 향상됩니다.
필요에 따라 SQL 코드와 마크다운 표 예시를 참고하여 자신의 시스템에 맞는 최적의 구조를 설계해 보시기 바랍니다.</p>
<hr>
<p>출처: <a href="https://mangkyu.tistory.com/110">https://mangkyu.tistory.com/110</a> [MangKyu&#39;s Diary:티스토리]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인덱스(Index)란?]]></title>
            <link>https://velog.io/@take_the_king/%EC%9D%B8%EB%8D%B1%EC%8A%A4Index%EB%9E%80</link>
            <guid>https://velog.io/@take_the_king/%EC%9D%B8%EB%8D%B1%EC%8A%A4Index%EB%9E%80</guid>
            <pubDate>Tue, 11 Mar 2025 14:47:33 GMT</pubDate>
            <description><![CDATA[<h1 id="1-인덱스index란">1. 인덱스(Index)란?</h1>
<p>인덱스란 데이터베이스에서 검색 성능을 향상시키기 위해 사용하는 자료구조이다.
마치 책의 목차처럼 특정 데이터를 빠르게 찾을 수 있도록 도와준다. 인덱스가 없을 경우, 데이터베이스는 원하는 데이터를 찾기 위해 전체 테이블을 조회(Full Table Scan)해야 하지만, 인덱스를 사용하면 원하는 데이터를 효율적으로 검색할 수 있다.</p>
<p>한마디로, <strong>정렬해놓은 컬럼 사본</strong>을 의미한다.</p>
<h1 id="2-인덱스의-동작-원리">2. 인덱스의 동작 원리</h1>
<p>인덱스는 일반적으로 B-Tree 또는 Hash 구조를 이용하여 데이터를 정렬하거나 해싱하여 관리한다. 예를 들어, B-Tree 인덱스는 정렬된 데이터를 유지하면서 검색, 삽입, 삭제 작업을 효율적으로 수행할 수 있도록 한다. 이를 통해 특정 값을 빠르게 찾고, 범위 검색도 효과적으로 수행할 수 있다.</p>
<h1 id="3-인덱스의-종류">3. 인덱스의 종류</h1>
<p>3.1 기본(Basic) 인덱스</p>
<p>클러스터형 인덱스(Clustered Index)</p>
<p>테이블의 데이터가 인덱스에 의해 정렬된 형태로 저장된다.</p>
<p>한 테이블당 하나의 클러스터형 인덱스만 생성 가능하다.</p>
<p>검색 속도가 빠르지만, 데이터 삽입/삭제 시 재정렬이 필요할 수 있다.</p>
<p>비클러스터형 인덱스(Non-Clustered Index)</p>
<p>데이터 자체는 정렬되지 않고, 별도의 인덱스 페이지가 존재하여 검색 시 실제 데이터 페이지로 매핑된다.</p>
<p>한 테이블에 여러 개의 비클러스터형 인덱스를 생성할 수 있다.</p>
<p>3.2 특수 인덱스</p>
<p>유니크 인덱스(Unique Index)</p>
<p>중복 값을 허용하지 않는 인덱스이다.</p>
<p>PRIMARY KEY 또는 UNIQUE 제약 조건을 적용할 때 사용된다.</p>
<p>복합 인덱스(Composite Index)</p>
<p>여러 개의 컬럼을 조합하여 생성하는 인덱스이다.</p>
<p>특정 컬럼 조합에 대한 검색 성능을 높일 수 있다.</p>
<p>해시 인덱스(Hash Index)</p>
<p>키-값 기반의 해시 테이블을 사용하여 데이터를 검색한다.</p>
<p>동등(=) 검색에는 빠르지만 범위 검색에는 적합하지 않다.</p>
<p>부분 인덱스(Partial Index)</p>
<p>테이블의 특정 부분만을 대상으로 생성하는 인덱스이다.</p>
<p>필요하지 않은 데이터까지 인덱싱하는 비용을 줄일 수 있다.</p>
<h1 id="4-인덱스의-장점과-단점">4. 인덱스의 장점과 단점</h1>
<p>4.1 장점</p>
<p>검색 속도 향상: 특정 컬럼에 대한 검색이 빠르게 수행됨.</p>
<p>정렬 속도 개선: ORDER BY 절을 사용할 때 성능 향상.</p>
<p>조인 성능 향상: 조인 시 필요한 키 값을 빠르게 찾을 수 있음.</p>
<p>4.2 단점</p>
<p>INSERT/UPDATE/DELETE 성능 저하: 인덱스가 많을수록 데이터 변경 시 추가적인 인덱스 관리 비용 발생.</p>
<p>디스크 공간 사용 증가: 인덱스는 별도의 저장 공간을 차지하므로 테이블 크기가 증가함.</p>
<p>과도한 인덱스 생성 시 오버헤드 발생: 너무 많은 인덱스는 오히려 성능을 저하시킬 수 있음.</p>
<h1 id="5-인덱스-사용-예제-mysql-기준">5. 인덱스 사용 예제 (MySQL 기준)</h1>
<p>5.1 기본 인덱스 생성</p>
<p>CREATE INDEX idx_user_name ON users(name);</p>
<p>5.2 클러스터형 인덱스 (PRIMARY KEY)</p>
<p>CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);</p>
<p>5.3 비클러스터형 인덱스 생성</p>
<p>CREATE INDEX idx_email ON users(email);</p>
<p>5.4 복합 인덱스 생성</p>
<p>CREATE INDEX idx_name_email ON users(name, email);</p>
<p>5.5 인덱스 삭제</p>
<p>DROP INDEX idx_user_name ON users;</p>
<h1 id="6-인덱스-활용-및-최적화-전략">6. 인덱스 활용 및 최적화 전략</h1>
<p>자주 조회되는 컬럼에 대해 인덱스를 설정.</p>
<p>중복되지 않는 유니크한 값이 많은 컬럼을 선택하여 인덱스를 생성.</p>
<p>불필요한 인덱스는 제거하여 데이터 삽입/수정 성능을 개선.</p>
<p>쿼리 실행 계획(EXPLAIN)을 사용하여 인덱스가 제대로 활용되는지 확인.</p>
<h1 id="7-결론">7. 결론</h1>
<p>인덱스는 데이터베이스 성능 최적화의 핵심 요소이며, 적절한 사용이 중요하다. 무분별한 인덱스 생성은 오히려 성능 저하를 초래할 수 있으므로, 데이터 특성과 조회 패턴을 고려하여 신중하게 인덱스를 설계해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA['숫자와 문자'라 쓰고 변수에 대한 노트]]></title>
            <link>https://velog.io/@take_the_king/%EC%88%AB%EC%9E%90%EC%99%80-%EB%AC%B8%EC%9E%90%EB%9D%BC-%EC%93%B0%EA%B3%A0-%EB%B3%80%EC%88%98%EC%97%90-%EB%8C%80%ED%95%9C-%EB%85%B8%ED%8A%B8</link>
            <guid>https://velog.io/@take_the_king/%EC%88%AB%EC%9E%90%EC%99%80-%EB%AC%B8%EC%9E%90%EB%9D%BC-%EC%93%B0%EA%B3%A0-%EB%B3%80%EC%88%98%EC%97%90-%EB%8C%80%ED%95%9C-%EB%85%B8%ED%8A%B8</guid>
            <pubDate>Tue, 11 Mar 2025 12:24:40 GMT</pubDate>
            <description><![CDATA[<h1 id="변수와-상수">변수와 상수</h1>
<h3 id="저장-공간">저장 공간</h3>
<p><img src="https://velog.velcdn.com/images/taketheking/post/c7d0e02a-e0e3-4865-a376-baf7cad4ea92/image.png" alt=""></p>
<ul>
<li>저장공간에 값을 저장(할당)하는 법<pre><code>// 1 방법
int number = 10;  // 저장공간 선언과 동시에 값을 할당 = 초기화
</code></pre></li>
</ul>
<p>// 2방벙
int number;        // 저장공간 선언
number = 10;    // 값을 따로 할당</p>
<pre><code>
###  변수 : 변하는 수
 &gt; 변수를 선언하고 언제든 값을 덮어씌울 수 있다.(바꿀 수 있다)</code></pre><p>int number = 10; // 1. 변수로 선언 및 초기화
number = 11; // 2. 변수의 값을 바꾼다. (덮어쓰기)</p>
<pre><code>


### 상수 : 변하지 않는 수
  &gt; 상수를 선언하면 그 이후로는 값을 바꿀 수 없다.</code></pre><p> final int number = 10; // 1. 상수로 선언 (데이터 타입 앞에 final 을 붙이면 됩니다.)
 number = 11; // e2. 변수의 값을 바꾸려고하면 에러가 납니다!</p>
<pre><code>
### 저장공간 타입 종류</code></pre><ol>
<li>논리형(bool) 타입</li>
</ol>
<p>boolean flag = true; // 1. 논리형 변수 boolean 으로 선언 및 True 값으로 초기화</p>
<p>flag = false; // 2. False 값으로도 저장할 수 있습니다.</p>
<ol start="2">
<li>문자형 타입</li>
</ol>
<p>char alphabet = &#39;A&#39;; // 문자 하나를 저장합니다. &#39;&#39;를 이용</p>
<p>// 문자열은 &quot;&quot; 를 이용
String message = &quot;Hello World!&quot;; </p>
<ol start="3">
<li>정수형 타입</li>
</ol>
<p>byte byteNumber = 127; // byte 는 -128 ~ 127 범위의 숫자만 저장 가능합니다.</p>
<p>short shortNumber = 32767; // short 는 -32,768~32,767 범위의 숫자만 저장 가능합니다.</p>
<p>int intNumber = 2147483647; // int 는 -21억~21억 범위의 숫자만 저장 가능합니다.</p>
<p>long longNumber = 2147483647L; // long 은 숫자뒤에 알파벳 L 을 붙여서 표기하며 매우 큰수를 저장 가능합니다.</p>
<ol start="4">
<li>실수형 타입</li>
</ol>
<p>float floatNumber = 0.123f; // float 는 4byte 로 3.4 * 10^38 범위를 표현하는 실수값
double doubleNumber = 0.123123123; // double 은 8byte 로 1.7 * 10^308 범위를 표현하는 실수값</p>
<pre><code>
## 문자와 숫자
메모리는 0과 1로만 이루어진다. 그럼 문자는 어떻게 저장될까? 바로 아스키코드를 통해 숫자로 변환하여 저장한다.
![](https://velog.velcdn.com/images/taketheking/post/986562f5-54f7-4ac6-89e0-563b94b67b05/image.png)


예시)</code></pre><p>// 숫자 -&gt; 문자
import java.util.Scanner;</p>
<p>public class Main {</p>
<pre><code>public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    int asciiNumber = sc.nextInt();
    char ch = (char)asciiNumber; // 문자로 형변환을 해주면 숫자에 맞는 문자로 표현됩니다.

    System.out.println(ch);
}</code></pre><p>}</p>
<p>// 입력
97</p>
<p>// 출력
a</p>
<pre><code></code></pre><p>// 문자 -&gt; 숫자</p>
<p>import java.util.Scanner;</p>
<p>public class Main {</p>
<pre><code>public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);

    char letter = sc.nextLine().charAt(0); // 첫번째 글자만 받아오기위해 charAt(0) 메서드를 사용합니다.
    int asciiNumber = (int)letter; // 숫자로 형변환을 해주면 저장되어있던 아스키 숫자값으로 표현됩니다.

    System.out.println(asciiNumber);
}</code></pre><p>}</p>
<p>// 입력
a</p>
<p>// 출력
97</p>
<pre><code></code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[float vs long 와 10진수 vs 2진수]]></title>
            <link>https://velog.io/@take_the_king/float-vs-long-%EC%99%80-10%EC%A7%84%EC%88%98-vs-2%EC%A7%84%EC%88%98</link>
            <guid>https://velog.io/@take_the_king/float-vs-long-%EC%99%80-10%EC%A7%84%EC%88%98-vs-2%EC%A7%84%EC%88%98</guid>
            <pubDate>Tue, 11 Mar 2025 12:23:24 GMT</pubDate>
            <description><![CDATA[<h2 id="float-vs-long">float vs long</h2>
<h3 id="float-의-바이트가-더-적은데-어떻게-long보다-넓은-범위를-표현할까">float 의 바이트가 더 적은데 어떻게 long보다 넓은 범위를 표현할까?</h3>
<p>실수 와 부동 소수점</p>
<p>실수 표현과 부동 소수점에 대해 이해하기 위해서는 먼저 &#39;실수&#39;와 &#39;부동 소수점&#39;이 무엇인지 알아야 합니다.</p>
<blockquote>
<p>실수(Real Number):
실수는 소수점이 있는 숫자를 말합니다. 예를 들어, 3.14, 0.01, -2.5 등이 실수입니다. 
이러한 실수는 컴퓨터에서 표현하기 위해 &#39;부동 소수점&#39; 방식을 사용합니다.</p>
</blockquote>
<blockquote>
<p>부동 소수점(Floating Point):
부동 소수점은 실수를 컴퓨터에서 표현하는 방식입니다. &#39;부동&#39;이란 단어가 의미하는 것은 
소수점의 위치가 고정되어 있지 않고, &#39;움직일 수 있다&#39;는 것입니다. 
이 방식은 매우 큰 수나 매우 작은 수를 표현하는 데 유용합니다.</p>
</blockquote>
<p>예를 들어, 0.000123과 123000000.0 두 수를 생각해봅시다.
이 두 수를 부동 소수점으로 표현하면, 각각 1.23 x 10^-4와 1.23 x 10^8로 표현할 수 있습니다. 여기서 1.23은 &#39;가수(mantissa)&#39;라고 하고, -4와 8은 &#39;지수(exponent)&#39;라고 합니다.
이처럼 부동 소수점은 가수와 지수로 이루어져 있습니다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>정수타입(long)은 지수를 사용하지 못하고, 실수타입(float)은 지수를 사용할 수 있다. =&gt; 제곱의 사용으로 훨씬 더 큰 수(범위)를 만들어 낼 수 있다.
long의 범위 : long은 64비트(8바이트)를 사용하여 정수를 표현합니다. 이는 2의 64승, 즉 약 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807까지의 정수를 표현할 수 있습니다.</p>
<p>약 -922경 3372조 368억 5477만 5807 ~ +922경 3372조 368억 5477만 5807</p>
<p>float의 범위 : 반면에 float는 32비트(4바이트)를 사용하여 실수를 표현합니다. 그러나 이 32비트는 부동 소수점 방식에 따라 &#39;가수&#39;와 &#39;지수&#39;로 나뉘어 사용됩니다. 이 방식 덕분에 float는 매우 큰 수(약 10^38)부터 매우 작은 수(약 10^-38)까지 표현할 수 있습니다.</p>
<p>약 +-100,000,000,000,000,000,000,000,000,000,000,000,000 &gt; 백간(^38)
억(^8) &lt; 조(^12) &lt; 경(^16) &lt; 해(^20) &lt; 자(^24) &lt; 자(^24) &lt; 양(^28) &lt; 구(^32) &lt; 간(^36) &lt;백간(^38)</p>
<pre><code>public class Main {
    public static void main(String[] args) {
        long a = 10000000000L;  // 10^10, long으로 표현 가능
        float b = 10000000000F;  // 10^10, float으로 표현 가능

        long c = 10000000000000000000L;  // 10^19, long으로 표현 불가능
        float d = 10000000000000000000F;  // 10^19, float으로 표현 가능

        long e = 0.1L;  // 0.1, long으로 표현 불가능
        float f = 0.1F;  // 0.1, float으로 표현 가능
    }
}</code></pre><h2 id="10진수의-2진수-변환">10진수의 2진수 변환</h2>
<h3 id="10진수---2진수-변환방법">10진수 -&gt; 2진수 변환방법</h3>
<p>1) 정수부분
<img src="https://velog.velcdn.com/images/taketheking/post/9052a635-41a6-4198-a6ee-8d827d463a1c/image.webp" alt=""></p>
<p><img src="https://velog.velcdn.com/images/taketheking/post/baedd70f-68df-4e71-9ca4-e546c03ef87e/image.png" alt=""></p>
<p>10진수를 2진법으로 변환하는 방법은 10진수 숫자를 2로 나눌 수 없을 때까지 나누고 아래에서부터 꺼꾸로 나열하면 변환할 수 있다.</p>
<p>** * 큰 숫자 를 변환할 때**
<img src="https://velog.velcdn.com/images/taketheking/post/6a912ecc-6e04-4961-a68a-2f3682834359/image.png" alt=""></p>
<p>2) 소수부분
<img src="https://velog.velcdn.com/images/taketheking/post/fb682f35-f678-434a-a6bc-245b2c0ba41c/image.webp" alt=""></p>
<p>소수 부분을 2로 계속 곱해서 정수 부분이 1이 되면 2진수에서 1로 변환, 정수 부분이 0이 되면 2진수에서 0으로 변환하고 소수부분이 0이 될 때까지 나머지 소수 부분에 계속 2를 곱한다.</p>
<h4 id="무한이-나누어-질때">무한이 나누어 질때</h4>
<p> 소수 부분이 0으로 나누어 떨어지지 않고 무한히 나누어지는 경우가 있다. 이때는 무한 소수로 나타낸다.</p>
<h3 id="2진수---10진수-변환방법">2진수 -&gt; 10진수 변환방법</h3>
<p> 1) 정수부분
 <img src="https://velog.velcdn.com/images/taketheking/post/5564eb4e-1c84-4ea2-a622-2389b624e9c2/image.png" alt=""></p>
<p> 2) 소수부분 + (정수 부분)
 <img src="https://velog.velcdn.com/images/taketheking/post/c3d02bc2-2baf-4dcf-8dea-d39bd7d991c6/image.png" alt=""></p>
<p><em>출처 : <a href="https://blog.hexabrain.net/357">https://blog.hexabrain.net/357</a></em>
<em>출처 : <a href="https://ourcalc.com/2%EC%A7%84%EC%88%98-%EB%B3%80%ED%99%98%EA%B8%B0/">https://ourcalc.com/2%EC%A7%84%EC%88%98-%EB%B3%80%ED%99%98%EA%B8%B0/</a></em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바의 기초 형태 뜯어보기]]></title>
            <link>https://velog.io/@take_the_king/%EC%9E%90%EB%B0%94%EC%9D%98-%EA%B8%B0%EC%B4%88-%ED%98%95%ED%83%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@take_the_king/%EC%9E%90%EB%B0%94%EC%9D%98-%EA%B8%B0%EC%B4%88-%ED%98%95%ED%83%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 11 Mar 2025 12:11:43 GMT</pubDate>
            <description><![CDATA[<h3 id="맨-처음-보는-코드-분석하기">맨 처음 보는 코드 분석하기</h3>
<pre><code>public class Main {
    public static void main(String[] args) {
        System.out.println(&quot;Hello world!&quot;);
    }
}</code></pre><pre><code> // () : 소괄호
 // {} : 중괄호
 // [] : 대괄호</code></pre><h4 id="클래스-정의">클래스 정의</h4>
<p> public class Main {...}</p>
<p> (1) public : 접근 제어자 - 접근 권한을 설정
 (2) class : 클래스라 정의하는 것
 (3) Main : 클래스 이름   - <em>첫글자는 대문자로 설정</em> 
 (4) {...} : 클래스의 내용
 (5) 클래스 : 객체의 틀 = (객체를 정의)</p>
<p> (6) 객체 : 하나의 물체처럼 특징와 행동을 가지고 있는 것
 (7) 객체의 특징 : 사람이면 MBTI, 키, 몸무게, 성격 등
 (8) 객체의 행동 : 먹기, 자기, 공부하기 등</p>
<h3 id="main-메소드">main 메소드</h3>
<p> public static void main(String[] args) {...}</p>
<p> (1) public : 접근 제어자 - 접근 권한을 설정
 (2) static : 정적 메소드 정의
 (3) void : 메소드 출력값 데이터 타입 정의 - 메소드 안에서 밖으로 값을 출력할 때 정의
 (4) main : 메소드 이름 - 소문자로만
 (5) (String[] args) : 매개변수(input) - 메소드 사용할 때, 메소드 안에 전달할 값이 들어있는 변수로 메소드 안에서만 사용가능</p>
<p> (6) {...} : 메소드 내용 - 보통 동작을 작성</p>
<h4 id="main-메소드-특징">main 메소드 특징</h4>
<p> 자바 프로젝트(앱)는 제일 먼저 클래스의 main 메소드를 실행시킨다. = <strong>JVM의 약속</strong></p>
<p> 초기에 항상 드는 의문이 &quot;왜?? 왜 이렇게 해야돼?&quot; 이다. 여기에 대한 답은 &quot;그냥 만든 사람이 그렇게 정했어&quot;이다. </p>
<p> 집에서 콩국수를 먹는다고 해보자. 그런데 엄마가 &quot;콩국수에는 소금을 뿌려먹는 거야!&quot; 라고 하면 소금을 뿌려먹어야 한다. 그게 집에서의 약속이자 룰이다.</p>
<p> 이처럼 개발도 엄마같은 &quot;사람이 이거는 이렇게 쓰는 거야&quot; 라고 정하기 때문에 그냥 그렇게 하면 된다.</p>
<p> 하지만 중요한 것은 왜 그렇게 약속을 정해졌는 지 알아야 한다. 예를 들면, 모든 프로그램의 표준을 제시하기 위해 약속이 정해졌을 수 있기 때문이다. 표준을 지켜야 코드의 유지, 보수가 쉽다.</p>
<ul>
<li>옛날 썰<blockquote>
<p> <del>처음 C언어를 공부할 때, 항상 마지막에 return 0; 을 붙이라고 배웠다. 근데 나는 항상 궁금했다. &quot;왜 return 0; 을 붙여야 되는거지?&quot;
그 당시, 다들 &quot;나중에 되면 알아. 그냥 외워.&quot; 라는 반응이었다.
나는 너무 궁금해서 집요한 서치를 통해서 결국 return 0; 이 부모 프로세스에게 해당 프로그램이 무사히 끝났다고 알려주는 하나의 약속이라는 사실을 알게 되었다.
그러면서 약속의 개념과 프로그램의 동작을 어렴풋이 이해하게 되었다.</del></p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 - CS 공부 2일차]]></title>
            <link>https://velog.io/@take_the_king/%EC%8A%A4%ED%84%B0%EB%94%94-CS-%EA%B3%B5%EB%B6%80-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@take_the_king/%EC%8A%A4%ED%84%B0%EB%94%94-CS-%EA%B3%B5%EB%B6%80-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 27 Feb 2025 07:29:54 GMT</pubDate>
            <description><![CDATA[<h2 id="자바의-제네릭-타입-안정성과-코드-재사용성의-향상">자바의 제네릭: 타입 안정성과 코드 재사용성의 향상</h2>
<p>제네릭은 자바에서 데이터 타입을 일반화하는 방법으로, 컴파일 시에 타입을 미리 지정하여 타입 안정성을 높이고 불필요한 타입 변환을 줄입니다1. 제네릭의 주요 장점은 다음과 같습니다:</p>
<p>타입 안정성 보장</p>
<p>불필요한 형 변환 제거</p>
<p>코드 재사용성 증가</p>
<p>제네릭 타입은 컴파일 시점, 즉 객체를 선언하고 new로 생성할 때 정해집니다. 제네릭을 사용하지 않으면 타입 안정성이 보장되지 않아 형 변환 오류가 발생할 수 있습니다.</p>
<p>자바의 동시성 문제와 멀티스레드
동시성 문제는 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제로, 데이터의 일관성을 해칠 수 있습니다4. 이를 해결하기 위한 방법으로는 synchronized 키워드 사용, java.util.concurrent 패키지의 활용, 그리고 락(lock) 메커니즘 등이 있습니다2.</p>
<p>멀티스레드의 과도한 생성은 다음과 같은 문제를 야기할 수 있습니다:</p>
<p>서버 리소스 과다 사용으로 인한 성능 저하</p>
<p>컨텍스트 스위칭 비용 증가</p>
<p>데드락 발생 가능성 증가</p>
<p>동기화와 비동기화
동기화는 작업이 끝날 때까지 기다리는 방식으로 작업의 순서를 보장하며, 비동기화는 작업을 병렬적으로 처리하여 효율성을 높이는 방식입니다3. 자바에서는 CAS(Compare-And-Swap)와 같은 메커니즘을 통해 효율적인 동기화를 구현할 수 있습니다.</p>
<p>캡슐화: 객체 지향 프로그래밍의 핵심
캡슐화는 객체의 상태나 메서드를 하나로 묶어 사용하기 쉽게 만드는 것입니다. 주로 접근 제어자를 통해 구현되며, 데이터 은닉과 보안을 강화합니다. 그러나 코드 복잡성 증가, 성능 저하 가능성, 테스트와 유지보수의 어려움 등의 단점도 있습니다.</p>
<p>Optional: null 처리의 안전성 향상
Optional은 null 처리를 보다 안전하게 할 수 있도록 도와주는 클래스입니다. 사용 시 주의할 점은 다음과 같습니다:</p>
<p>필드로 사용하지 않기</p>
<p>.get() 메소드 사용 자제</p>
<p>Optional을 남용하면 불필요한 객체 생성으로 인한 오버헤드가 발생할 수 있어 성능상 문제가 될 수 있습니다.</p>
<p>결론
자바의 다양한 기능과 개념들은 각각 고유한 장단점을 가지고 있습니다. 제네릭, 멀티스레딩, 동기화 메커니즘, 캡슐화, Optional 등을 적절히 활용하면 타입 안정성, 성능, 코드 품질을 크게 향상시킬 수 있습니다. 그러나 각 기능의 특성과 제약을 잘 이해하고 상황에 맞게 사용하는 것이 중요합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스터디 - CS 공부 1일차]]></title>
            <link>https://velog.io/@take_the_king/%EC%8A%A4%ED%84%B0%EB%94%94-CS-%EA%B3%B5%EB%B6%80-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@take_the_king/%EC%8A%A4%ED%84%B0%EB%94%94-CS-%EA%B3%B5%EB%B6%80-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 25 Feb 2025 14:27:38 GMT</pubDate>
            <description><![CDATA[<h2 id="1-싱글톤-패턴-singleton-pattern">1. 싱글톤 패턴 (Singleton Pattern)</h2>
<p>싱글톤 패턴은 객체를 하나만 생성하여 애플리케이션 전체에서 공유할 수 있도록 하는 디자인 패턴입니다.</p>
<p><strong>특징</strong>
하나의 인스턴스만 생성됨
전역적으로 접근 가능
메모리 절약 및 데이터 공유 용이</p>
<h3 id="구현-방법-lazy-initialization">구현 방법 (Lazy Initialization)</h3>
<pre><code class="language-java">public class Singleton {
    private static Singleton instance;

    private Singleton() {} // 외부에서 인스턴스 생성을 막음

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}</code></pre>
<p>하지만 위 방식은 멀티스레드 환경에서 안전하지 않기 때문에 double-checked locking 또는 static inner class 방식을 추천합니다.</p>
<h3 id="static-inner-class-방식-권장됨">static inner class 방식 (권장됨)</h3>
<pre><code class="language-java">public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}</code></pre>
<p>이 방식은 클래스가 로드될 때 인스턴스가 생성되지 않고, 최초 getInstance() 호출 시 생성되므로 효율적이며 스레드 안전합니다.</p>
<h2 id="2-jvm-java-virtual-machine">2. JVM (Java Virtual Machine)</h2>
<p><strong>JVM(Java Virtual Machine)</strong>은 자바 바이트코드를 실행하는 가상 머신입니다.</p>
<h3 id="jvm의-역할">JVM의 역할</h3>
<p>바이트코드 실행 → 자바 프로그램을 운영체제와 관계없이 실행 가능
메모리 관리 → GC(Garbage Collection)를 통해 자동으로 메모리 관리
플랫폼 독립성 제공 → Write Once, Run Anywhere</p>
<h3 id="jvm의-구성-요소">JVM의 구성 요소</h3>
<p>클래스 로더(Class Loader): .class 파일을 로드하고 메모리에 적재
메모리 영역 (Runtime Data Area): JVM이 관리하는 메모리 공간
실행 엔진(Execution Engine): 바이트코드를 기계어로 변환하여 실행
GC(Garbage Collector): 불필요한 객체를 정리</p>
<h2 id="3-자바-메모리-영역">3. 자바 메모리 영역</h2>
<p>JVM의 메모리 영역은 다음과 같이 구성됩니다.</p>
<h3 id="1-메서드-영역method-area-클래스-영역">1) 메서드 영역(Method Area, 클래스 영역)</h3>
<p>클래스 정보, static 변수, 상수, 메서드 코드가 저장됨
프로그램이 종료될 때까지 유지됨</p>
<h3 id="2-힙heap">2) 힙(Heap)</h3>
<p>객체(instance)들이 저장되는 공간
<strong>GC(Garbage Collector)</strong>가 관리</p>
<h3 id="3-스택stack">3) 스택(Stack)</h3>
<p>메서드 호출 시 지역 변수, 매개변수, 리턴값 저장
메서드 실행이 끝나면 제거됨 (LIFO 방식)</p>
<h3 id="4-pc-레지스터program-counter-register">4) PC 레지스터(Program Counter Register)</h3>
<p>현재 실행 중인 JVM 명령어의 주소를 저장</p>
<h3 id="5-네이티브-메서드-스택native-method-stack">5) 네이티브 메서드 스택(Native Method Stack)</h3>
<p>JNI(Java Native Interface)를 통해 호출된 네이티브 코드(C, C++)의 정보 저장</p>
<h2 id="4-오버로딩overloading과-오버라이딩overriding-차이">4. 오버로딩(Overloading)과 오버라이딩(Overriding) 차이</h2>
<p>구분    오버로딩 (Overloading)    오버라이딩 (Overriding)
의미    같은 이름의 메서드를 여러 개 정의    상속받은 메서드를 재정의
클래스 관계    같은 클래스 내에서 발생    부모 클래스의 메서드를 자식 클래스에서 재정의
매개변수    개수, 타입, 순서를 다르게 정의    동일해야 함
반환 타입    달라도 가능    동일해야 함
접근 제한자    제한 없음    부모보다 좁아질 수 없음
애너테이션    없음    @Override 사용</p>
<p>오버로딩 예제</p>
<pre><code class="language-java">
class MathUtils {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) { // 매개변수 타입이 다름
        return a + b;
    }
}</code></pre>
<p>오버라이딩 예제</p>
<pre><code class="language-java">
class Parent {
    void showMessage() {
        System.out.println(&quot;부모 클래스&quot;);
    }
}

class Child extends Parent {
    @Override
    void showMessage() { // 부모 메서드 재정의
        System.out.println(&quot;자식 클래스&quot;);
    }
}</code></pre>
<h2 id="5-클래스class-객체object-인스턴스instance-차이">5. 클래스(Class), 객체(Object), 인스턴스(Instance) 차이</h2>
<p>클래스(Class): 객체를 만들기 위한 설계도
객체(Object): 클래스에서 생성된 실제 대상
인스턴스(Instance): 객체가 메모리에 할당된 상태</p>
<p>예제</p>
<pre><code class="language-java">class Car {  // 클래스
    String model;
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car(); // 객체 생성 (인스턴스)
    }
}</code></pre>
<p>Car는 클래스
myCar는 객체이자 인스턴스
<strong>즉, &quot;인스턴스는 객체의 특정한 상태를 나타내는 개념&quot;</strong>이라고 볼 수 있습니다.</p>
<h2 id="6-원시-타입primitive-type과-참조-타입reference-type-차이">6. 원시 타입(Primitive Type)과 참조 타입(Reference Type) 차이</h2>
<p>구분    원시 타입 (Primitive Type)    참조 타입 (Reference Type)
데이터 저장 방식    값 자체를 저장    객체의 주소(참조값)를 저장
메모리 위치    스택(Stack)    힙(Heap)
크기    고정적    가변적
기본 값    int → 0, boolean → false    null
비교 방식    값 자체 비교 (==)    주소 비교 (==), 내용 비교 (equals())</p>
<p>원시 타입 예제</p>
<pre><code class="language-java">
int a = 10;
int b = a; // 값이 복사됨
b = 20;
System.out.println(a); // 10 (a의 값은 변하지 않음)</code></pre>
<p>참조 타입 예제</p>
<pre><code class="language-java">class Person {
    String name;
}

Person p1 = new Person();
p1.name = &quot;Alice&quot;;

Person p2 = p1; // 주소가 복사됨
p2.name = &quot;Bob&quot;;</code></pre>
<p>System.out.println(p1.name); // &quot;Bob&quot; (같은 객체를 참조하므로 값이 변경됨)
참조 타입은 같은 객체를 가리키므로 p1과 p2가 같은 데이터를 공유합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 트러블 슈팅 : 실시간 웨이팅 순서 조회 성능 개선]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%A8%EC%9D%B4%ED%8C%85-%EC%88%9C%EC%84%9C-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%9B%A8%EC%9D%B4%ED%8C%85-%EC%88%9C%EC%84%9C-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Fri, 21 Feb 2025 14:16:52 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제-배경-및-기존-방식의-한계">1. 문제 배경 및 기존 방식의 한계</h2>
<h3 id="기존-db-기반-방식">기존 DB 기반 방식</h3>
<p>초기 구현에서는 데이터베이스에서 직접 쿼리를 수행하여 &quot;실시간 웨이팅 순서&quot;를 계산했습니다.</p>
<p>예를 들어, 아래와 같이 특정 가게와 날짜, 그리고 본인의 웨이팅 번호보다 앞에 있는 대기 중인 팀의 수를 DB에서 직접 카운트하는 방식입니다.</p>
<pre><code class="language-java">/**
 * 실시간 웨이팅 순서 조회 메서드
 *
 * @param waitingId
 * @return WaitingNowSeqNumberResponseDto
 */
public WaitingNumberResponseDto getNowSeqNumber(Long waitingId) {

    Waiting waiting = waitingRepository.findByIdOrElseThrow(waitingId);

    // 웨이팅 날짜만 가져오기
    LocalDate waitingDate = waiting.getCreatedAt().toLocalDate();

    // 앞에 대기중인 웨이팅 개수 가져오기
    int nowSeqNum = waitingRepository.countByStoreAndWaitingStatusAndCreatedAtBetweenAndWaitingNumberLessThanEqual(
            waiting.getStore(),
            WaitingStatus.PENDING,
            waitingDate.atTime(0, 0, 0), waitingDate.atTime(23, 59, 59),
            waiting.getWaitingNumber()
    );

    return new WaitingNumberResponseDto(nowSeqNum);
}
</code></pre>
<p><strong>문제점:</strong></p>
<ul>
<li><strong>DB 부하 증가:</strong> 실시간으로 사용자의 순서를 확인할 때마다 복잡한 조건을 가진 쿼리를 실행하기 때문에, 사용자가 많아질 경우 DB에 큰 부하가 발생합니다.</li>
<li><strong>성능 이슈:</strong> 매번 DB를 조회함으로써 응답 속도가 느려질 수 있으며, 동시 요청이 많은 경우 트래픽 폭주 상황에서 병목 현상이 발생할 수 있습니다.</li>
</ul>
<hr>
<h2 id="2-redis-기반-개선-방식">2. Redis 기반 개선 방식</h2>
<h3 id="redis와-redisson을-활용한-설계">Redis와 Redisson을 활용한 설계</h3>
<p>Redis를 활용하면 메모리 기반 데이터 저장소의 장점을 살려 실시간 조회 및 업데이트를 빠르게 처리할 수 있습니다. Redisson 라이브러리를 이용하여 Java 애플리케이션에서 Redis의 자료구조(특히, 정렬된 집합)를 효과적으로 사용할 수 있도록 구현했습니다.</p>
<h3 id="주요-개선-포인트">주요 개선 포인트</h3>
<ul>
<li><strong>빠른 읽기/쓰기:</strong> Redis는 메모리 내 데이터 처리를 통해 낮은 지연 시간으로 실시간 순서 조회에 적합합니다.</li>
<li><strong>TTL (Time-To-Live) 설정:</strong> 대기열에 TTL을 설정하여 일정 시간이 지난 데이터는 자동으로 삭제되도록 함으로써, 메모리 낭비를 줄이고 데이터의 신선도를 유지합니다.</li>
<li><strong>정렬된 집합 사용:</strong> RScoredSortedSet을 이용해 대기열에 등록된 웨이팅 항목을 정렬된 형태로 관리하고, 이를 통해 순위(rank)를 효율적으로 계산할 수 있습니다.</li>
</ul>
<hr>
<h2 id="3-코드-상세-분석">3. 코드 상세 분석</h2>
<h3 id="31-웨이팅-큐에-등록하는-메서드-addtowaitingqueue">3.1. 웨이팅 큐에 등록하는 메서드: <code>addToWaitingQueue</code></h3>
<pre><code class="language-java">public void addToWaitingQueue(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getWaitingType().name() + &quot;:&quot; + waiting.getStore().getStoreId();
    RScoredSortedSet&lt;Long&gt; waitingQueue = redissonClient.getScoredSortedSet(key);

    // 웨이팅 번호를 score 로 추가 (혹시라도 있을 중복을 위해 날짜로 번호 구분)
    waitingQueue.add(LocalDate.now().getDayOfYear() * 1000000L + waiting.getWaitingNumber(), waiting.getWaitingId());

    // TTL 설정 (6시간 후 자동 삭제)
    waitingQueue.expire(Duration.ofHours(6));
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li><strong>Key 구성:</strong> 대기열 키는 <code>waiting:store:[웨이팅타입]:[가게ID]</code> 형식으로 생성하여, 가게와 웨이팅 타입에 따라 분리된 저장소를 사용합니다.</li>
<li><strong>Score 계산:</strong> score는 오늘 날짜의 일자(예, <code>dayOfYear</code>)에 대기 번호를 곱하여 생성합니다. 이 방식은 동일한 번호가 중복 등록되는 상황을 방지하고, 정렬 기준으로 활용됩니다.</li>
<li><strong>TTL 설정:</strong> 해당 대기열은 6시간 후 자동으로 삭제되도록 설정하여 오래된 데이터가 남지 않도록 관리합니다.</li>
</ul>
<h3 id="32-현재-순서를-조회하는-메서드-getnowseqnumber">3.2. 현재 순서를 조회하는 메서드: <code>getNowSeqNumber</code></h3>
<pre><code class="language-java">public Integer getNowSeqNumber(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getWaitingType() + &quot;:&quot; + waiting.getStore().getStoreId();
    RScoredSortedSet&lt;Long&gt; waitingQueue = redissonClient.getScoredSortedSet(key);

    // 현재 웨이팅의 번호 조회
    Integer rank = waitingQueue.rank(waiting.getWaitingId());
    if (rank == null) {
        return 0;
    }
    // 0-indexed 이므로 1을 더해 실제 순서 계산
    return rank + 1;
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li><strong>순위 계산:</strong> <code>rank()</code> 메서드는 해당 웨이팅 ID가 정렬된 집합 내에서 몇 번째에 위치하는지를 0부터 계산합니다. 따라서 사용자에게 보여줄 순서는 <code>rank + 1</code>이 됩니다.</li>
<li><strong>결과 반환:</strong> 만약 해당 대기 항목이 존재하지 않는다면 0을 반환하여 예외 상황을 처리합니다.</li>
</ul>
<h3 id="33-대기열에서-제거하는-메서드-removefromwaitingqueue">3.3. 대기열에서 제거하는 메서드: <code>removeFromWaitingQueue</code></h3>
<pre><code class="language-java">public void removeFromWaitingQueue(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getWaitingType() + &quot;:&quot; + waiting.getStore().getStoreId();
    RScoredSortedSet&lt;Long&gt; waitingQueue = redissonClient.getScoredSortedSet(key);

    // Redis 의 대기열에서 해당 웨이팅 ID 제거
    waitingQueue.remove(waiting.getWaitingId());
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li>해당 웨이팅 항목이 처리되었거나 취소되었을 경우, Redis 대기열에서 제거하여 실시간 순위 계산에서 제외합니다.</li>
</ul>
<h3 id="34-남은-팀-수-저장-메서드-savewaitingleft">3.4. 남은 팀 수 저장 메서드: <code>saveWaitingLeft</code></h3>
<pre><code class="language-java">public void saveWaitingLeft(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getStore().getStoreId() + &quot;:&quot; + waiting.getWaitingType() + &quot;:left&quot;;
    RMap&lt;Long, Integer&gt; waitingLeftMap = redissonClient.getMap(key);

    Integer leftNow = getNowSeqNumber(waiting);
    waitingLeftMap.put(waiting.getWaitingNumber(), leftNow);
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li><strong>Map 사용:</strong> 특정 가게와 웨이팅 타입에 대한 남은 대기 팀 수를 별도의 Redis Map에 저장합니다.</li>
<li><strong>저장 방식:</strong> 웨이팅 번호를 key로, 현재 순위를 value로 저장하여 나중에 통계나 모니터링 용도로 활용할 수 있습니다.</li>
</ul>
<h3 id="35-대기-소요-시간-관련-메서드">3.5. 대기 소요 시간 관련 메서드</h3>
<h3 id="351-1팀-당-소요-시간-저장-savetakentimewaiting">3.5.1. 1팀 당 소요 시간 저장: <code>saveTakenTimeWaiting</code></h3>
<pre><code class="language-java">public void saveTakenTimeWaiting(Long waitingId) {
    Waiting waiting = waitingRepository.findByIdOrElseThrow(waitingId);
    Integer takenTime = (int) Duration.between(waiting.getCreatedAt(), waiting.getModifiedAt()).toMinutes(); // 등록 - 입장 소요시간

    String key = &quot;waiting:store:&quot; + waiting.getStore().getStoreId() + &quot;:&quot; + waiting.getWaitingType() + &quot;:time&quot;;
    RScoredSortedSet&lt;Long&gt; timeQueue = redissonClient.getScoredSortedSet(key);

    String leftKey = &quot;waiting:store:&quot; + waiting.getStore().getStoreId() + &quot;:&quot; + waiting.getWaitingType() + &quot;:left&quot;;
    RMap&lt;Long, Long&gt; waitingLeftMap = redissonClient.getMap(leftKey);

    Long left = waitingLeftMap.get(waiting.getWaitingId());
    timeQueue.add((int) (takenTime / left), waiting.getWaitingId()); // 1팀 당 소요시간 저장

    // 데이터 수가 많아지면 오래된 데이터 제거
    if (timeQueue.size() &gt; 150) {
        timeQueue.remove(0);
    }
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li><strong>소요 시간 계산:</strong> 등록 시각과 입장 시각의 차이를 계산하여, 전체 소요 시간을 구합니다.</li>
<li><strong>평균 계산:</strong> 전체 소요 시간을 현재 대기 팀 수(저장된 left 값)로 나누어 1팀 당 소요 시간을 구합니다.</li>
<li><strong>데이터 관리:</strong> 일정 크기(150개)를 초과하면 오래된 데이터를 제거하여 메모리 사용량을 관리합니다.</li>
</ul>
<h3 id="352-소요-시간-삭제-deletetakentimewaiting">3.5.2. 소요 시간 삭제: <code>deleteTakenTimeWaiting</code></h3>
<pre><code class="language-java">public void deleteTakenTimeWaiting(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getStore().getStoreId() + &quot;:&quot; + waiting.getWaitingType() + &quot;:time&quot;;
    RScoredSortedSet&lt;Long&gt; timeQueue = redissonClient.getScoredSortedSet(key);

    timeQueue.remove(waiting.getWaitingId());
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li>처리된 웨이팅 항목의 소요 시간 데이터를 정리하기 위한 메서드입니다.</li>
</ul>
<h3 id="353-예상-대기-시간-조회-gettakentimewaiting">3.5.3. 예상 대기 시간 조회: <code>getTakenTimeWaiting</code></h3>
<pre><code class="language-java">public Integer getTakenTimeWaiting(Waiting waiting) {
    String key = &quot;waiting:store:&quot; + waiting.getStore().getStoreId() + &quot;:&quot; + waiting.getWaitingType() + &quot;:time&quot;;
    RScoredSortedSet&lt;Long&gt; timeQueue = redissonClient.getScoredSortedSet(key);

    long leftNow = getNowSeqNumber(waiting);
    int sum = 0;
    int time;

    if (timeQueue.size() &lt; 10) {
        time = 15; // 데이터가 충분하지 않을 경우 기본값 15분
    } else {
        for (Long num : timeQueue) {
            sum += num.intValue();
        }
        time = sum / timeQueue.size();
    }
    return time * (int) leftNow;
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li><strong>데이터 안정성:</strong> 데이터 포인트가 충분하지 않은 경우 기본값(15분)을 적용합니다.</li>
<li><strong>평균 소요 시간:</strong> 저장된 각 웨이팅 항목의 1팀 당 소요 시간을 평균내어, 이를 현재 대기 팀 수와 곱해 전체 예상 대기 시간을 산출합니다.</li>
</ul>
<h3 id="36-최종-결과-통합-getnowsequenceandtime">3.6. 최종 결과 통합: <code>getNowSequenceAndTime</code></h3>
<pre><code class="language-java">public WaitingSequenceDto getNowSequenceAndTime(Long waitingId) {
    Waiting waiting = waitingRepository.findByIdOrElseThrow(waitingId);

    // 현재 나의 순서 조회
    Integer nowSeqNumber = getNowSeqNumber(waiting);

    // 예상 대기 시간 조회
    Integer waitingTime = getTakenTimeWaiting(waiting);

    return new WaitingSequenceDto(nowSeqNumber, waitingTime);
}</code></pre>
<p><strong>설명:</strong></p>
<ul>
<li>DB에서 웨이팅 정보를 불러온 후, Redis에 저장된 데이터(순서 및 소요 시간)를 활용하여 사용자에게 실시간 순서와 예상 대기 시간을 함께 제공합니다.</li>
<li>이로써 DB 부하를 최소화하면서도, 사용자에게 빠르고 정확한 정보를 제공할 수 있습니다.</li>
</ul>
<hr>
<h2 id="4-전체-코드-동작-흐름">4. 전체 코드 동작 흐름</h2>
<ol>
<li><strong>등록 시 처리:</strong><ul>
<li>사용자가 웨이팅에 등록하면 <code>addToWaitingQueue</code>를 호출하여 Redis의 정렬된 집합에 등록합니다.</li>
<li>동시에 현재 남은 팀 수를 <code>saveWaitingLeft</code> 메서드로 Redis Map에 저장합니다.</li>
</ul>
</li>
<li><strong>순서 및 예상 대기 시간 조회:</strong><ul>
<li>사용자가 자신의 순서와 예상 대기 시간을 조회할 때 <code>getNowSequenceAndTime</code> 메서드를 호출합니다.</li>
<li>내부적으로 <code>getNowSeqNumber</code>로 현재 순위를, <code>getTakenTimeWaiting</code>으로 대기 시간을 계산합니다.</li>
</ul>
</li>
<li><strong>완료/취소 시 처리:</strong><ul>
<li>웨이팅이 완료되거나 취소되면, <code>removeFromWaitingQueue</code>와 <code>deleteTakenTimeWaiting</code>을 통해 관련 데이터를 Redis에서 정리합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="5-결론">5. 결론</h2>
<p>Redis를 활용한 이번 개선 방식은 다음과 같은 장점을 제공합니다.</p>
<ul>
<li><strong>실시간 응답 속도 개선:</strong> 메모리 기반의 정렬된 집합을 사용하여, 사용자에게 빠른 순서 계산 및 예상 대기 시간 제공이 가능해졌습니다.</li>
<li><strong>DB 부하 완화:</strong> DB 대신 Redis를 통해 많은 계산 및 조회 작업을 처리함으로써, DB 서버의 부하를 크게 줄일 수 있습니다.</li>
<li><strong>데이터 관리:</strong> TTL, 데이터 수 제한 등의 전략을 통해 Redis 메모리 사용을 효율적으로 관리합니다.</li>
</ul>
<p>이와 같이 Redis 기반의 대기열 관리 방식을 활용하면, 대규모 사용자가 동시에 접근하는 상황에서도 안정적이고 빠른 실시간 정보 제공이 가능해집니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 트러블 슈팅 : 조건 검색 성능 개선]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%A1%B0%EA%B1%B4-%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%A1%B0%EA%B1%B4-%EA%B2%80%EC%83%89-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Mon, 17 Feb 2025 14:55:26 GMT</pubDate>
            <description><![CDATA[<h1 id="조건-검색-성능-개선">조건 검색 성능 개선</h1>
<h2 id="1-배경">1. 배경</h2>
<p>데이터 수가 증가함에 따라 현재 <code>Store</code> 엔티티와 연관된 검색 기능에서 성능 문제가 발생하고 있습니다.</p>
<h2 id="2-문제">2. 문제</h2>
<p><code>searchStoreQuery</code> 메서드에서 다수의 조인과 조건 필터링이 이루어지며 응답 속도가 저하됩니다. 이로 인해 사용자가 검색 결과를 확인하는 데 시간이 오래 걸리고, 서버의 리소스 사용량이 증가하는 문제가 발생합니다.</p>
<h3 id="21-원인">2.1 원인</h3>
<ul>
<li><strong>조건 검색의 비효율성</strong>: <code>BooleanExpression</code>을 이용한 필터링에서 인덱스 활용 부족</li>
<li><strong>반복적인 DB 접근</strong>: Redis 캐싱 미적용으로 인해 동일한 검색이 반복적으로 DB에 접근</li>
<li><strong>Lazy Loading으로 인한 N+1 문제 가능성</strong></li>
<li><strong>복잡한 검색 필터링 로직</strong>: 최적화가 필요</li>
</ul>
<hr>
<h2 id="3-성능-개선-방법">3. 성능 개선 방법</h2>
<h3 id="30-데이터-준비">3.0 데이터 준비</h3>
<ul>
<li>store 더미 데이터 50만 개 생성<ul>
<li><code>net.datafaker.Faker</code>를 사용하여 랜덤 데이터를 생성하고, 이를 DB에 저장합니다.</li>
</ul>
</li>
</ul>
<pre><code class="language-java">package com.gotcha.earlytable.domain.store;

import com.gotcha.earlytable.domain.file.FileService;
import com.gotcha.earlytable.domain.store.entity.Store;
import com.gotcha.earlytable.domain.store.enums.StoreCategory;
import com.gotcha.earlytable.domain.store.enums.StoreStatus;
import com.gotcha.earlytable.domain.user.UserRepository;
import com.gotcha.earlytable.domain.user.entity.User;
import com.gotcha.earlytable.global.enums.RegionBottom;
import com.gotcha.earlytable.global.enums.RegionTop;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import net.datafaker.Faker;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;

@Component
@RequiredArgsConstructor
public class StoreDummyDataInitializer {

    private final StoreRepository storeRepository;
    private final UserRepository userRepository;
    private final FileService fileService;

    private final Random random = new Random();
    private final Faker faker = new Faker(Locale.KOREAN);

    private static final int STORE_COUNT = 500_000;

    @PostConstruct
    @Transactional
    public void init() {
        List&lt;User&gt; users = userRepository.findAll();

        if (users.isEmpty()) {
            throw new IllegalStateException(&quot;User가 존재하지 않습니다.&quot;);
        }

        List&lt;Store&gt; stores = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; STORE_COUNT; i++) {
            stores.add(createRandomStore(users));

            if (i % 10_000 == 0) { // 10,000개씩 배치 저장
                storeRepository.saveAll(stores);
                stores.clear();
                System.out.println(i + &quot; 개의 데이터를 저장 완료...&quot;);
            }
        }

        if (!stores.isEmpty()) {
            storeRepository.saveAll(stores);
        }

        System.out.println(&quot;총 &quot; + STORE_COUNT + &quot; 개의 더미 데이터 저장 완료!&quot;);
    }

    private Store createRandomStore(List&lt;User&gt; users) {
        return new Store(
                faker.company().name(), // 랜덤 상점명
                faker.phoneNumber().phoneNumber(), // 랜덤 전화번호
                faker.lorem().sentence(), // 랜덤 설명
                faker.address().fullAddress(), // 랜덤 주소
                randomEnum(StoreStatus.class), // 랜덤 StoreStatus
                randomEnum(StoreCategory.class), // 랜덤 StoreCategory
                randomEnum(RegionTop.class), // 랜덤 RegionTop
                randomEnum(RegionBottom.class), // 랜덤 RegionBottom
                getRandomElement(users), // 랜덤 User
                fileService.createFile() // New File
        );
    }

    private &lt;T extends Enum&lt;?&gt;&gt; T randomEnum(Class&lt;T&gt; enumClass) {
        T[] enumConstants = enumClass.getEnumConstants();
        return enumConstants[random.nextInt(enumConstants.length)];
    }

    private &lt;T&gt; T getRandomElement(List&lt;T&gt; list) {
        return list.get(random.nextInt(list.size()));
    }
}</code></pre>
<ul>
<li>build.gradle에 의존성 추가</li>
</ul>
<pre><code class="language-java">implementation &#39;net.datafaker:datafaker:2.0.2&#39;</code></pre>
<ul>
<li>더미 데이터 예시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/8dac0c49-7111-400d-bcfd-a07bad819f35/image.png" alt=""></p>
<h3 id="31-인덱스-최적화">3.1 인덱스 최적화</h3>
<p>현재 <code>storeName</code>, <code>regionTop</code>, <code>regionBottom</code>, <code>storeCategory</code> 등에 대해 적절한 인덱스가 없어 DB에서 검색하는 데 시간이 오래 걸립니다.</p>
<h3 id="해결-방법">해결 방법</h3>
<p>아래와 같이 인덱스를 추가하여 검색 성능을 개선할 수 있습니다</p>
<pre><code class="language-java">@Entity
@Table(name = &quot;store&quot;, indexes = {
@Index(name = &quot;idx_store_name&quot;, columnList = &quot;storeName&quot;),
@Index(name = &quot;idx_region_top&quot;, columnList = &quot;regionTop&quot;),
@Index(name = &quot;idx_region_bottom&quot;, columnList = &quot;regionBottom&quot;),
@Index(name = &quot;idx_store_category&quot;, columnList = &quot;storeCategory&quot;)
})
public class Store extends BaseEntity {

    // ...이하 생략

}
</code></pre>
<ul>
<li><p>기본 키와 외래 키는 이미 데이터베이스에서 자동으로 인덱스를 생성함</p>
<ul>
<li>JPA에서는 @Id(storeId) 와 @manyToOne(UserId), @OneToOne(fileId) 관계의 필드를 인덱스로 자동 생성</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/cc10fa6b-e0c0-4c34-8a45-9eac2ae248d6/image.png" alt=""></p>
<ul>
<li>조건 중 검색어는 가게 이름과 대표 메뉴 이름을 LIKE 조건으로 검색하기 때문에 “%검색어” 나 “%검색어%” 처럼 정렬이 무의미한 경우는 인덱스가 의미가 없음 → 실제로 시간이 더 오래 걸림(인덱스 수정)</li>
<li>조건 중 가격은 자주 변경될 가능성이 높고, BETWEEN 조건이라 성능 개선이 크게 이루어지지 않을 가능성이 높음</li>
</ul>
<h3 id="적용-전후-성능-비교">적용 전/후 성능 비교</h3>
<ul>
<li><p>인덱스 적용 전</p>
<p>  <img src="https://velog.velcdn.com/images/take_the_king/post/7735e57b-2177-4b99-bf94-3a786d4c89d0/image.png" alt=""></p>
</li>
</ul>
<ul>
<li><p>인덱스 적용 후</p>
<p>  <img src="https://velog.velcdn.com/images/take_the_king/post/e1300f3f-41b5-4745-a7a4-ed714dfd7771/image.png" alt=""></p>
</li>
</ul>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/a351803f-aae8-43cc-84db-4503f536194b/image.png" alt=""></p>
<p>전체적으로 성능이 개선되긴 했지만 큰 지역(31,813개)과 카테고리(85,001개)처럼 많은 양의 데이터를 가져오는 상황에서는 크게 유의미한 결과를 보여주진 못하고, <strong>작은 지역과 작은 지역(2,259개) + 카테고리(5,303개) 10% 이하의 데이터를 가져올 때는 큰 성능 향상이 있었습니다.</strong></p>
<h3 id="32-redis-캐싱-적용">3.2 Redis 캐싱 적용</h3>
<p>동일한 검색이 반복적으로 발생할 경우, 캐싱이 없으면 매번 DB에서 조회해야하는 문제가 있습니다.</p>
<h3 id="해결-방법-1">해결 방법</h3>
<p>동일한 검색이 반복적으로 발생할 경우, DB에서 매번 조회하는 대신 Redis를 이용해 캐싱함으로써 성능을 개선할 수 있습니다.</p>
<pre><code class="language-java">/**
     * 가게 조건 검색 메서드
     *
     * @param requestDto
     * @return
     */
    public List&lt;StoreSearchResponseDto&gt; searchStore(StoreSearchRequestDto requestDto) {
        String cacheKey = &quot;store_search:&quot; + getCacheKey(requestDto);

        // RBucket을 JsonJacksonCodec과 함께 사용
        RBucket&lt;String&gt; cachedResult = redissonClient.getBucket(cacheKey, JsonJacksonCodec.INSTANCE);

        Instant start = Instant.now(); // 시작 시간 기록

        // 캐시된 결과가 있으면 반환
        String resultJson = cachedResult.get();
        List&lt;StoreSearchResponseDto&gt; result = null;
        if (resultJson != null &amp;&amp; !resultJson.isEmpty()) {
            try {
                result = objectMapper.readValue(resultJson, objectMapper.getTypeFactory().constructCollectionType(List.class, StoreSearchResponseDto.class));
            } catch (Exception e) {
                log.error(&quot;Error deserializing cached result&quot;, e);
            }
        }

        if (result == null || result.isEmpty()) {
            // 캐시된 결과가 없으면 DB에서 조회 후 캐시에 저장
            result = storeRepository.searchStoreQuery(requestDto);

            try {
                String resultJsonToCache = objectMapper.writeValueAsString(result); // List to JSON
                cachedResult.set(resultJsonToCache, 10, TimeUnit.MINUTES); // 캐시 만료 시간은 10분으로 설정
            } catch (Exception e) {
                log.error(&quot;Error serializing result to cache&quot;, e);
            }
        }

        Instant end = Instant.now(); // 종료 시간 기록
        long elapsedTime = Duration.between(start, end).toMillis(); // 실행 시간(ms)

        log.info(&quot;searchStoreQuery 실행 시간: {} ms, 결과 개수: {}&quot;, elapsedTime, result.size());

        return result;
    }

    // 캐시 키를 위한 핵심 파라미터들만 조합하는 메소드
    private String getCacheKey(StoreSearchRequestDto requestDto) {
        return requestDto.getSearchWord() + &quot;:&quot; +
                requestDto.getRegionTop() + &quot;:&quot; +
                requestDto.getRegionBottom() + &quot;:&quot; +
                requestDto.getStoreCategory();
    }</code></pre>
<ul>
<li><p>적용 전</p>
<ul>
<li><p>테스트 1</p>
<p>  <img src="https://velog.velcdn.com/images/take_the_king/post/48f12384-b65b-4b86-8ab3-ddc3d96933ec/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<pre><code>    ![](https://velog.velcdn.com/images/take_the_king/post/6a23e3c5-d1ed-4845-93fb-21cccb90359a/image.png)


- 테스트 2

    ![](https://velog.velcdn.com/images/take_the_king/post/6c7da1b5-56d0-4dee-9092-5c60c405f43b/image.png)


    ![](https://velog.velcdn.com/images/take_the_king/post/d6b82e5e-d348-4819-8449-7910a38a4287/image.png)</code></pre><ul>
<li><p>적용 후</p>
<ul>
<li><p>테스트 1 - 캐싱 후</p>
<p>  <img src="https://velog.velcdn.com/images/take_the_king/post/769e0bea-f50b-457e-9888-a40133d36e7c/image.png" alt=""></p>
</li>
</ul>
</li>
</ul>
<pre><code>- 테스트 2 - 캐싱 후

    ![](https://velog.velcdn.com/images/take_the_king/post/ecf41371-32f8-4272-8509-229b4252cfc7/image.png)</code></pre><h3 id="결과-1">결과</h3>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/961be8ac-e58e-47b6-958a-1993cfee910f/image.png" alt=""></p>
<p><strong>데이터를 캐싱한 결과 평균 95% 이상의 성능 개선이 이루어졌습니다.</strong> 자주 조회되는 조건들에 경우에는 아주 효과적인 개선 방법이 될 수 있습니다. 하지만 캐시 적중률이 높지 않다면 결국에는 DB 조회가 일어날 것입니다.</p>
<p>또한, 모든 조회들을 캐싱에 담아두고 긴 시간동안 보관하면 메모리 부하가 발생할 것입니다. 그렇기 때문에 적절한 만료시간과 자주 조회되는 컬럼을 적용시키면 아주 효과적인 성능 개선이 이루어질 것입니다.</p>
<h3 id="주의사항"><strong>주의사항</strong></h3>
<ul>
<li><strong>변경 빈도 고려</strong>: 만약 가게 데이터가 자주 변경된다면, 캐시에 저장된 데이터와 DB의 데이터 간에 불일치(데이터 정합성 문제)가 발생할 수 있습니다. 이런 경우 캐시를 자주 무효화하거나 업데이트해야 하므로, 캐시 업데이트 작업이 빈번해져 오히려 Redis에 부하를 줄 수 있습니다.</li>
<li><strong>캐시 적중률 고려</strong>: 캐시 적중률이 높다면 대부분의 요청이 Redis에서 처리되어 DB 부하가 줄어들지만, 캐시 적중률이 낮으면 DB 조회가 빈번하게 발생합니다. 특히 캐시 미스가 잦은 경우, Redis와 DB 간의 네트워크 트래픽이 증가할 수 있습니다.</li>
</ul>
<h3 id="33-n--1-문제-해결">3.3 N + 1 문제 해결</h3>
<p><code>@OneToMany(fetch = FetchType.LAZY)</code> 관계에서 N+1 문제가 발생할 가능성이 있습니다.</p>
<h3 id="해결-방법-1hibernate-batch-size-조정">해결 방법 1(Hibernate Batch Size 조정)</h3>
<p><code>@BatchSize(size = 100)</code>를 사용하여 성능을 개선할 수 있습니다.</p>
<ul>
<li>store 엔티티</li>
</ul>
<pre><code class="language-java">@Entity
@Table(name = &quot;store&quot;, indexes = {
@Index(name = &quot;idx_store_name&quot;, columnList = &quot;storeName&quot;),
@Index(name = &quot;idx_region_top&quot;, columnList = &quot;regionTop&quot;),
@Index(name = &quot;idx_region_bottom&quot;, columnList = &quot;regionBottom&quot;),
@Index(name = &quot;idx_store_category&quot;, columnList = &quot;storeCategory&quot;)
})
public class Store extends BaseEntity {

    // ...

    @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = &quot;file_id&quot;, nullable = false)
  private File file;

    @BatchSize(size = 100)
  @OneToMany(mappedBy = &quot;store&quot;, cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
  private final List&lt;Menu&gt; menuList = new ArrayList&lt;&gt;();

    @BatchSize(size = 100)
  @OneToMany(mappedBy = &quot;store&quot;, cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
  private final List&lt;Review&gt; reviewList = new ArrayList&lt;&gt;();
}
</code></pre>
<ul>
<li>file 엔티티</li>
</ul>
<pre><code class="language-java">@Getter
@Entity
@Table(name = &quot;file&quot;)
@BatchSize(size = 100)
public class File extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long fileId;

    @BatchSize(size = 100)
    @OneToMany(mappedBy = &quot;file&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;FileDetail&gt; fileDetailList = new ArrayList&lt;&gt;();

    public File() {

    }

}</code></pre>
<h3 id="결과-2">결과</h3>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/8d2362ed-f33d-4223-8e33-4b9800084d67/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/5bf46778-c205-433d-9868-4806fc1519a3/image.png" alt=""></p>
<p>이처럼 수십 번 발생할 쿼리를 몇 번의 쿼리로 데이터를 가져오게 됩니다.</p>
<p>실제로  <strong>76번의 쿼리가 4개의 쿼리로 줄어들었습니다.</strong></p>
<p>또한 실행 시간도 약간이지만 단축되는 효과가 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/ca5a60c6-b1c1-4a54-95b9-c383426e6c21/image.png" alt=""></p>
<p>이것은 batchsize를 적용하기 전의 실행 시간이다. batchsize를 적용 후 3% 정도 단축되었습니다.</p>
<p>추가 조회 실행 후에도</p>
<ul>
<li>batchsize 적용 전 (2차)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/aa850639-fc3a-40e4-8b3d-d0745cebc1f7/image.png" alt=""></p>
<ul>
<li>batchsize 적용 후 (2차)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/012a5154-1106-4790-91e4-c886656eb00d/image.png" alt=""></p>
<p>약 10% 정도 단축되는 것을 볼 수 있습니다. </p>
<p><strong>이를 통해 batchsize를 알맞은 상황에 적용하면 쿼리도 최적화를 하고 성능도 개선할 수 있다는 것을 알 수 있습니다.</strong></p>
<h3 id="해결-방법-2fetchtype-설정">해결 방법 2(fetchType 설정)</h3>
<p>가게를 조회할 때 항상 사용되는 필드는 조인해서 하나의 쿼리에서 데이터를 가져오도록 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/3cebc9da-8d09-430d-a01a-d1de1f559a7a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/73430c4b-92b4-467c-9b8f-75d264f4522e/image.png" alt=""></p>
<h3 id="결과-3">결과</h3>
<p>왼쪽은 fetchType 이 Lazy 일 때 쿼리가 3번 발생하는 상황이고, fetchType 을 Eager로 설정해서 1번의 쿼리만 발생하는 상황입니다. 항상 같이 조회되는 데이터의 경우는 조인을 통해 같이 가져오는 방법이 성능 개선이 도움을 줄 수 있습니다.</p>
<hr>
<h2 id="4-결론">4. 결론</h2>
<ul>
<li><strong>인덱스 최적화, 데이터 캐싱, N+1 문제 해결</strong>을 통해 성능 개선을 이룰 수 있었습니다.</li>
<li>특히 <strong>인덱스 최적화와 Redis 캐싱</strong>이 성능 향상에 중요한 역할을 했습니다.</li>
<li>이러한 최적화 방법들은 각 상황에 맞게 사용하면, 시스템 성능을 크게 개선할 수 있습니다.</li>
</ul>
<table>
<thead>
<tr>
<th>개선 방법</th>
<th>성능 향상 비율</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 최적화</td>
<td>10% ~ 80% 개선</td>
<td>작은 데이터에서 큰 성능 향상</td>
</tr>
<tr>
<td>Redis 캐싱</td>
<td>95% 개선</td>
<td>반복적인 조회에 매우 효과적</td>
</tr>
<tr>
<td>N+1 문제 해결</td>
<td>3% ~ 10% 개선</td>
<td>불필요한 쿼리 수를 줄여 성능 개선</td>
</tr>
<tr>
<td>FetchType 조정</td>
<td>20% 개선</td>
<td>하나의 쿼리로 데이터 조회</td>
</tr>
</tbody></table>
<h2 id="5-추후-고려-사항">5. 추후 고려 사항</h2>
<h3 id="51--elasticsearch-도입-고려">5.1  Elasticsearch 도입 고려</h3>
<p>검색 성능을 더욱 향상하기 위해 <strong>Elasticsearch</strong> 도입을 고려.</p>
<ul>
<li>빠른 텍스트 검색 가능</li>
<li>복합적인 필터링 및 랭킹 기능 제공</li>
</ul>
<h3 id="52-인덱스-추가">5.2 인덱스 추가</h3>
<p>검색 성능을 위해 더 다양한 인덱스 추가 고려</p>
<ul>
<li>알러지에 대한 인덱스 추가</li>
<li>복합 인덱스 추가</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 트러블슈팅 : SSE (Server-Sent Events) 연결 오류]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-SSE-Server-Sent-Events-%EC%97%B0%EA%B2%B0-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-SSE-Server-Sent-Events-%EC%97%B0%EA%B2%B0-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Tue, 04 Feb 2025 04:36:33 GMT</pubDate>
            <description><![CDATA[<h3 id="📌-react-프로젝트에서-sse-server-sent-events-트러블-슈팅"><strong>📌 React 프로젝트에서 SSE (Server-Sent Events) 트러블 슈팅</strong></h3>
<p>SSE (Server-Sent Events)를 React 애플리케이션에 적용하면서 발생한 문제들과 해결 방법을 정리</p>
<hr>
<h2 id="🚨-1-로그인회원가입-페이지에서도-sse가-실행되는-문제"><strong>🚨 1. 로그인/회원가입 페이지에서도 SSE가 실행되는 문제</strong></h2>
<h3 id="🔍-문제-원인"><strong>🔍 문제 원인</strong></h3>
<ul>
<li>SSE를 알림기능에 사용하기 위해 전역 페이지에 적용함.</li>
<li><code>SSEProvider</code>가 <code>App.js</code>의 <code>Router</code> 전체를 감싸고 있어 <strong>로그인/회원가입 페이지에서도 SSE가 실행됨</strong>.</li>
<li>로그인하지 않은 사용자에게는 불필요한 SSE 연결이 발생함.</li>
</ul>
<h3 id="💡-해결-방법"><strong>💡 해결 방법</strong></h3>
<p>✅ <strong>로그인 및 회원가입 페이지는 <code>SSEProvider</code>에서 제외하고, 인증된 페이지에만 적용</strong></p>
<pre><code class="language-jsx">&lt;Router&gt;
  &lt;Routes&gt;
    {/* ✅ 로그인 &amp; 회원가입에서는 SSEProvider 제외 */}
    &lt;Route path=&quot;/register&quot; element={&lt;Register /&gt;} /&gt;
    &lt;Route path=&quot;/login&quot; element={&lt;Login /&gt;} /&gt;

    {/* ✅ 나머지 페이지에서는 SSEProvider 적용 */}
    &lt;Route
      path=&quot;/*&quot;
      element={
        &lt;SSEProvider&gt;
          &lt;Routes&gt;
            &lt;Route path=&quot;/&quot; element={&lt;Home /&gt;} /&gt;
            &lt;Route path=&quot;/store/:storeId&quot; element={&lt;StoreDetails /&gt;} /&gt;
            {/* ...생략 */}
          &lt;/Routes&gt;
        &lt;/SSEProvider&gt;
      }
    /&gt;
  &lt;/Routes&gt;
&lt;/Router</code></pre>
<hr>
<h2 id="🚨-2-중복-sse-연결이-발생하는-문제"><strong>🚨 2. 중복 SSE 연결이 발생하는 문제</strong></h2>
<h3 id="🔍-문제-원인-1"><strong>🔍 문제 원인</strong></h3>
<ul>
<li><code>SSEProvider</code>의 <code>useEffect</code>가 여러 번 실행되어 <strong>기존 SSE 연결을 닫지 않은 채 새로운 연결이 계속 생성됨</strong>.</li>
<li>페이지 이동 시 <strong>기존 SSE 연결이 유지되지 않고 새로운 연결이 생성되어 중복 발생</strong>.</li>
</ul>
<h3 id="💡-해결-방법-1"><strong>💡 해결 방법</strong></h3>
<p>✅ <strong><code>eventSourceRef</code>를 <code>useRef</code>로 관리하여 중복 연결 방지</strong></p>
<p>✅ <strong>기존 SSE 연결이 있으면 새 연결을 생성하지 않도록 조건 추가</strong></p>
<pre><code class="language-jsx">onst eventSourceRef = useRef(null);

const connectSSE = () =&amp;gt; {
  if (eventSourceRef.current) {
    console.log(&quot;⚠️ 기존 SSE 연결 존재, 중복 연결 방지&quot;);
    return;
  }

  eventSourceRef.current = new EventSourcePolyfill(
    &quot;http://localhost:8080/notifications/subscribe&quot;,
    {
      headers: { Authorization: `Bearer ${localStorage.getItem(&quot;accessToken&quot;)}` },
      withCredentials: true,
    }
  );

  eventSourceRef.current.onopen = () =&amp;gt; {
    console.log(&quot;✅ SSE 연결 성공&quot;);
  };

  eventSourceRef.current.onerror = () =&amp;gt; {
    console.error(&quot;❌ SSE 연결 오류 발생, 재연결 시도...&quot;);
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
    setTimeout(connectSSE, 3000); // 3초 후 재연결
  };
};

useEffect(() =&amp;gt; {
  connectSSE();

  return () =&amp;gt; {
    console.log(&quot;🛑 SSE 연결 해제&quot;);
    eventSourceRef.current?.close();
    eventSourceRef.current = null;
  };
}, []);</code></pre>
<p>✅ <strong>이제 페이지 이동 시에도 기존 SSE가 유지되며, 중복 연결이 방지됨!</strong></p>
<hr>
<h2 id="🚨-3-sse-연결-끊김-시-자동-재연결되지-않는-문제"><strong>🚨 3. SSE 연결 끊김 시 자동 재연결되지 않는 문제</strong></h2>
<h3 id="🔍-문제-원인-2"><strong>🔍 문제 원인</strong></h3>
<ul>
<li>SSE 연결이 끊기면 새로운 연결을 시도하지 않고 종료됨.</li>
<li>브라우저의 자동 재연결 기능이 동작하지 않는 경우가 발생함.</li>
</ul>
<h3 id="💡-해결-방법-2"><strong>💡 해결 방법</strong></h3>
<p>✅ <strong>SSE 에러 발생 시 자동 재연결 로직 구현</strong></p>
<p>✅ <strong>최대 3회까지 재연결을 시도하고, 실패 시 토큰 갱신 후 재시도</strong></p>
<pre><code class="language-jsx">const retryCountRef = useRef(0);

const connectSSE = () =&amp;gt; {
  if (eventSourceRef.current) {
    console.log(&quot;⚠️ 기존 SSE 연결 존재, 중복 연결 방지&quot;);
    return;
  }

  eventSourceRef.current = new EventSourcePolyfill(
    &quot;http://localhost:8080/notifications/subscribe&quot;,
    {
      headers: { Authorization: `Bearer ${localStorage.getItem(&quot;accessToken&quot;)}` },
      withCredentials: true,
    }
  );

  eventSourceRef.current.onopen = () =&amp;gt; {
    console.log(&quot;✅ SSE 연결 성공&quot;);
    retryCountRef.current = 0; // 재연결 카운트 초기화
  };

  eventSourceRef.current.onerror = async (error) =&amp;gt; {
    console.error(&quot;❌ SSE 연결 오류 발생:&quot;, error);
    eventSourceRef.current?.close();
    eventSourceRef.current = null;

    if (retryCountRef.current &amp;gt;= 3) {
      console.warn(&quot;🚨 SSE 재연결 3회 실패, 토큰 갱신 후 재시도&quot;);
      const success = await getRefreshToken();
      if (success) connectSSE();
    } else {
      setTimeout(() =&amp;gt; {
        retryCountRef.current += 1;
        console.log(`🔄 SSE 재연결 (${retryCountRef.current}번째 시도)`);
        connectSSE();
      }, 3000);
    }
  };
};</code></pre>
<p>✅ <strong>이제 SSE 연결이 끊겨도 자동으로 재연결됨!</strong></p>
<hr>
<h2 id="🚨-4-액세스-토큰-만료-시-401-오류가-반복되는-문제"><strong>🚨 4. 액세스 토큰 만료 시 401 오류가 반복되는 문제</strong></h2>
<h3 id="🔍-문제-원인-3"><strong>🔍 문제 원인</strong></h3>
<ul>
<li>액세스 토큰 만료 시 새로운 토큰을 받아오지 못해 SSE 연결이 실패함.</li>
<li>SSE 요청 시 <strong>만료된 액세스 토큰을 계속 사용하여 401 에러가 반복됨</strong>.</li>
</ul>
<h3 id="💡-해결-방법-3"><strong>💡 해결 방법</strong></h3>
<p>✅ <strong>401 오류 발생 시 자동으로 리프레시 토큰을 요청하고, 성공하면 SSE 재연결</strong></p>
<p>✅ <strong>토큰 갱신 요청의 중복 실행을 방지하도록 <code>isRefreshingRef</code> 활용</strong></p>
<pre><code class="language-jsx">const isRefreshingRef = useRef(false);

const getRefreshToken = async () =&amp;gt; {
  if (isRefreshingRef.current) return false;
  isRefreshingRef.current = true;

  try {
    console.log(&quot;🔄 토큰 갱신 시도...&quot;);
    const response = await instance.post(&quot;/users/refresh&quot;, {}, {
      headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
      withCredentials: true,
    });

    const newAccessToken = response.data.accessToken;
    if (!newAccessToken) throw new Error(&quot;새로운 토큰 없음&quot;);

    localStorage.setItem(&quot;accessToken&quot;, newAccessToken);
    console.log(&quot;✅ 토큰 갱신 성공&quot;);
    return true;
  } catch (err) {
    console.error(&quot;❌ 토큰 갱신 실패, 로그인 페이지로 이동&quot;);
    navigate(&quot;/login&quot;);
    return false;
  } finally {
    isRefreshingRef.current = false;
  }
};

eventSourceRef.current.onerror = async (error) =&amp;gt; {
  console.error(&quot;❌ SSE 연결 오류 발생:&quot;, error);
  eventSourceRef.current?.close();
  eventSourceRef.current = null;

  if (error.status === 401) {
    const success = await getRefreshToken();
    if (success) {
      console.log(&quot;🔄 SSE 재연결 시도...&quot;);
      connectSSE();
    }
  } else {
    setTimeout(() =&amp;gt; {
      console.log(&quot;🔄 SSE 자동 재연결...&quot;);
      connectSSE();
    }, 3000);
  }
};</code></pre>
<p>✅ <strong>이제 토큰이 만료되어도 자동으로 갱신되고 SSE가 재연결됨!</strong></p>
<hr>
<h2 id="🎯-정리"><strong>🎯 정리</strong></h2>
<table>
<thead>
<tr>
<th><strong>문제</strong></th>
<th><strong>해결 방법</strong></th>
</tr>
</thead>
<tbody><tr>
<td>로그인/회원가입에서도 SSE 실행됨</td>
<td>로그인/회원가입에서는<code>SSEProvider</code>제외</td>
</tr>
<tr>
<td>중복 SSE 연결 발생</td>
<td><code>useRef</code>를 사용해 중복 연결 방지</td>
</tr>
<tr>
<td>SSE가 끊겨도 자동 재연결 안 됨</td>
<td>최대 3번 재연결 후 토큰 갱신 후 재시도</td>
</tr>
<tr>
<td>액세스 토큰 만료 시 401 오류 반복</td>
<td>401 발생 시 토큰 갱신 후 SSE 재연결</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 트러블슈팅 : 자동 로그인의 필요성]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%9E%90%EB%8F%99-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%9E%90%EB%8F%99-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1</guid>
            <pubDate>Tue, 04 Feb 2025 04:31:30 GMT</pubDate>
            <description><![CDATA[<h2 id="1-문제">1. 문제</h2>
<p>초기에는 엑세스토큰만을 사용하여 로그인을 관리했는데, 엑세스토큰의 만료 시간이 짧아 사용자가 자주 로그인을 다시 해야 하는 불편함이 발생했습니다.</p>
<p>즉, 사용자가 로그인한 후에도 짧은 시간마다 토큰 만료로 인해 인증이 끊기고, 다시 로그인을 해야 하는 상황이 문제였습니다.</p>
<hr>
<h2 id="2-원인">2. 원인</h2>
<ul>
<li><p><strong>엑세스토큰 만료 시간</strong></p>
<p>  엑세스토큰은 보통 보안상의 이유로 만료 시간이 짧게 설정됩니다. 이로 인해, 사용자가 서비스를 이용하는 도중에 토큰이 만료되면 인증 상태가 풀려 다시 로그인을 요구하게 됩니다.</p>
</li>
<li><p><strong>엑세스토큰 단독 관리</strong></p>
<p>  기존에는 엑세스토큰만 관리하다 보니, 토큰 갱신 로직이 없어서 만료되면 새 토큰을 발급받지 못하고 로그인이 끊기는 문제가 발생했습니다.</p>
</li>
</ul>
<hr>
<h2 id="3-해결책">3. 해결책</h2>
<p>해결책으로는 <strong>리프레쉬 토큰(Refresh Token)</strong>을 도입하는 것입니다.</p>
<ul>
<li><p><strong>리프레쉬 토큰 발급:</strong></p>
<p>  사용자가 로그인할 때 서버는 엑세스토큰과 함께 리프레쉬 토큰을 발급합니다.</p>
</li>
<li><p><strong>리프레쉬 토큰 저장:</strong></p>
<p>  리프레쉬 토큰은 보안상의 이유로 HttpOnly 쿠키로 저장합니다. 이 쿠키는 JavaScript에서 접근할 수 없으므로, XSS 공격에 안전합니다.</p>
</li>
<li><p><strong>엑세스토큰 저장:</strong></p>
<p>  엑세스토큰은 브라우저의 로컬스토리지에 저장하여 클라이언트 측에서 사용합니다.</p>
</li>
<li><p><strong>자동 토큰 갱신:</strong></p>
<p>  클라이언트에서는 엑세스토큰을 사용하다가 만료되면, HttpOnly 쿠키에 저장된 리프레쉬 토큰을 사용하여 서버에 새 엑세스토큰 발급을 요청합니다. 이 과정은 axios 인터셉터 등을 통해 자동으로 처리할 수 있습니다.</p>
</li>
</ul>
<hr>
<h2 id="4-적용-코드-예시">4. 적용 (코드 예시)</h2>
<h3 id="41-서버에서-refreshtoken-추가-발급">4.1. 서버에서 RefreshToken 추가 발급</h3>
<pre><code class="language-java">@PostMapping(&quot;/login&quot;)
    public ResponseEntity&lt;JwtAuthResponse&gt; loginUser(@Valid @RequestBody UserLoginRequestDto requestDto,
                                                     HttpServletResponse response) {

        String accessToken = userService.loginUser(requestDto);

                // 쿠키에 refresh token 담기
        response.addCookie(userService.craeteCookie(requestDto.getEmail()));

        return ResponseEntity.status(HttpStatus.OK).body(new JwtAuthResponse(AuthenticationScheme.BEARER.getName(), accessToken));
    }</code></pre>
<ul>
<li>refresh Token 을 생성해서 쿠키에 담는 메서드 (UserService)</li>
</ul>
<pre><code class="language-java">/**
     * refresh Token 을 쿠키에 담기
     *
     * @return Cookie
     */
    public Cookie craeteCookie(String email) {

        String cookieName = &quot;refreshToken&quot;;
        String cookieValue = jwtProvider.generateRefreshToken(email); // 쿠키벨류엔 글자제한이 이써, 벨류로 만들어담아준다.

        // refreshToken db 저장
        refreshTokenService.saveRefreshToken(email, cookieValue);

        Cookie cookie = new Cookie(cookieName, cookieValue);
        // 쿠키 속성 설정
        cookie.setHttpOnly(true);  //httponly 옵션 설정
        // cookie.setSecure(true); //https 옵션 설정
        cookie.setPath(&quot;/&quot;); // 모든 곳에서 쿠키열람이 가능하도록 설정
        cookie.setMaxAge(60 * 60 * 24); //쿠키 만료시간 설정
        return cookie;

    }</code></pre>
<ul>
<li>Redis에 refresh Token을 저장 (만료 기간 30일)</li>
</ul>
<pre><code class="language-java">@Service
public class RefreshTokenService {

    private final RedissonClient redissonClient;

    public RefreshTokenService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    // RefreshToken 저장
    public void saveRefreshToken(String username, String refreshToken) {
        RBucket&lt;String&gt; bucket = redissonClient.getBucket(&quot;refreshToken:&quot; + username);
        bucket.set(refreshToken, Duration.ofDays(30));
    }

    // RefreshToken 가져오기
    public String getRefreshToken(String username) {
        RBucket&lt;String&gt; bucket = redissonClient.getBucket(&quot;refreshToken:&quot; + username);
        return bucket.get();
    }

    // RefreshToken 삭제
    public void deleteRefreshToken(String username) {
        RBucket&lt;String&gt; bucket = redissonClient.getBucket(&quot;refreshToken:&quot; + username);
        bucket.delete();
    }

    // RefreshToken 검증
    public boolean validateRefreshToken(String username, String refreshToken) {
        String storedToken = getRefreshToken(username);
        return storedToken != null &amp;&amp; storedToken.equals(refreshToken);
    }
}
</code></pre>
<h3 id="42-axios-인터셉터를-활용한-자동-토큰-갱신">4.2. Axios 인터셉터를 활용한 자동 토큰 갱신</h3>
<pre><code class="language-jsx">import axios from &quot;axios&quot;;

// 🔹 Axios 인스턴스 생성
const instance = axios.create({
  baseURL: &quot;http://localhost:8080&quot;, // Spring Boot 서버 주소
  withCredentials: true, // 쿠키 포함 요청
});

// 🔹 액세스 토큰을 가져오는 함수
const getAccessToken = () =&gt; localStorage.getItem(&quot;accessToken&quot;);

// 🔹 요청 인터셉터: 헤더에 액세스 토큰 추가
instance.interceptors.request.use(
  (config) =&gt; {
    const accessToken = getAccessToken();
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) =&gt; Promise.reject(error)
);

// 🔹 리프레시 토큰을 사용해 새로운 액세스 토큰을 가져오는 함수
const refreshAccessToken = async () =&gt; {
  try {
    const response = await instance.post(
      &quot;/users/refresh&quot;,
      {},
      {
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        withCredentials: true, // HttpOnly 쿠키 포함
      }
    );

    const newAccessToken = response.data.accessToken;
    if (!newAccessToken) throw new Error(&quot;새로운 액세스 토큰 없음&quot;);

    localStorage.setItem(&quot;accessToken&quot;, newAccessToken);
    return newAccessToken;
  } catch (error) {
    console.error(&quot;❌ 리프레시 토큰 만료: 로그인 페이지로 이동&quot;);
    localStorage.removeItem(&quot;accessToken&quot;);
    window.location.href = &quot;/login&quot;; // 로그인 페이지로 리디렉션
    return null;
  }
};

// 🔹 응답 인터셉터: 401 응답 처리 (액세스 토큰 갱신 후 요청 재시도)
instance.interceptors.response.use(
  (response) =&gt; response,
  async (error) =&gt; {
    const originalRequest = error.config;

    if (error.response?.status === 401 &amp;&amp; !originalRequest._retry) {
      originalRequest._retry = true; // 재시도 방지 플래그 설정

      const newAccessToken = await refreshAccessToken();
      if (newAccessToken) {
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        return instance(originalRequest); // 요청 재시도
      }
    }

    return Promise.reject(error);
  }
);

export default instance;
</code></pre>
<h3 id="43-서버에서-엑세스-토큰-재발급-userservice">4.3 서버에서 엑세스 토큰 재발급 (UserService)</h3>
<pre><code class="language-java">/**
     * refreshToken 확인 및 accessToken 재발급
     *
     * @param email
     * @return accessToken
     */
    public String refresh(String email, String refreshToken) {

        if(!refreshTokenService.validateRefreshToken(email, refreshToken)) {
            throw new UnauthorizedException(ErrorCode.UNAUTHORIZED);
        }

        return jwtProvider.generateAccessToken(email);
    }</code></pre>
<hr>
<h2 id="5-자동-로그인-플로우-설명">5. 자동 로그인 플로우 설명</h2>
<ol>
<li><strong>로그인 시 토큰 발급</strong><ul>
<li>사용자가 로그인하면 서버는 엑세스토큰과 리프레쉬 토큰을 함께 발급합니다.</li>
<li>엑세스토큰은 로컬스토리지에 저장되고, 리프레쉬 토큰은 HttpOnly 쿠키에 저장됩니다.</li>
</ul>
</li>
<li><strong>요청 시 엑세스토큰 사용</strong><ul>
<li>클라이언트는 axios 인터셉터를 통해 로컬스토리지의 엑세스토큰을 HTTP 헤더에 포함하여 요청합니다.</li>
</ul>
</li>
<li><strong>엑세스토큰 만료 시</strong><ul>
<li>서버로부터 401 Unauthorized 응답을 받으면 axios 응답 인터셉터가 동작합니다.</li>
</ul>
</li>
<li><strong>토큰 갱신 요청</strong><ul>
<li>인터셉터에서 <code>/users/refresh</code> API를 호출하여 리프레쉬 토큰을 사용해 새 엑세스토큰을 발급받습니다.</li>
</ul>
</li>
<li><strong>새 토큰으로 재시도</strong><ul>
<li>발급받은 새 엑세스토큰을 로컬스토리지에 저장하고, 원래의 요청을 재시도하여 자동 로그인을 유지합니다.</li>
</ul>
</li>
<li><strong>갱신 실패 시</strong><ul>
<li>새 토큰 발급에 실패하면 사용자를 로그인 페이지로 리디렉션하여 다시 로그인하도록 합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="결론"><strong>결론</strong></h2>
<p>엑세스토큰의 짧은 만료 시간 문제를 해결하기 위해 리프레쉬 토큰을 도입하였습니다.</p>
<p>리프레쉬 토큰은 HttpOnly 쿠키에 안전하게 저장되고, 엑세스토큰은 로컬스토리지에 저장되어 사용됩니다.</p>
<p>axios 인터셉터를 활용해 401 에러 발생 시 자동으로 리프레쉬 토큰을 사용해 새 엑세스토큰을 발급받아 원래의 요청을 재시도하도록 구현하여, 사용자가 자주 로그인할 필요 없이 자동 로그인이 유지되도록 해결했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 기술적 의사 결정 : 결제 API]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC-%EA%B2%B0%EC%A0%95-%EA%B2%B0%EC%A0%9C-API</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0%EC%A0%81-%EC%9D%98%EC%82%AC-%EA%B2%B0%EC%A0%95-%EA%B2%B0%EC%A0%9C-API</guid>
            <pubDate>Tue, 04 Feb 2025 01:56:42 GMT</pubDate>
            <description><![CDATA[<h1 id="결제-api--kg-이니시스-vs--kakaopay--vs--tosspay">결제 API : KG 이니시스 vs  KakaoPay  vs  TossPay</h1>
<h3 id="kg-이니시스-🏦💳">KG 이니시스 🏦💳</h3>
<ul>
<li><strong>🟩장점</strong><ul>
<li><strong>다양한 결제 수단 지원💳</strong> : 신용카드, 가상계좌, 계좌이체, 휴대폰 결제 등 다양한 결제 옵션을 제공</li>
<li><strong>API 안정성 높음🔒</strong> : 오랜 기간 운영된 PG사인만큼 장애 발생 가능성이 적음</li>
<li><strong>정기 결제 &amp; B2B 기능 제공📅🔄</strong> : 정기 결제, 자동 결제 같은 고급 기능 지원</li>
<li><strong>대량 거래 처리 가능📊</strong> : 트래픽이 많거나 B2B 환경에서도 안정적</li>
</ul>
</li>
<li><strong>🟥단점</strong><ul>
<li><strong>JSON기반이 아닌 key=value방식 사용📝</strong> : RESTful API를 제공하지만, 최신 표준인 JSON이 아닌 key = value 형태로 데이터를 주고 받음</li>
<li><strong>불편한 테스트 환경⚠️</strong> : API를 테스트 하려면 실제 가맹점 가입 &amp; 승인 절차가 필요</li>
<li><strong>UI/UX 개선 필요🖥️</strong> : 다른 결제 서비스 대비 사용자 경험이 다소 불편할 수 있음</li>
</ul>
</li>
</ul>
<h3 id="kakaopay💛💬💰">KakaoPay💛💬💰</h3>
<ul>
<li><strong>🟩장점</strong><ul>
<li><strong>높은 사용자 접근성🌍</strong> : 카카오톡 기반으로 가입자가 많아 결제 전환율이 높음</li>
<li><strong>모바일 친화적인 UX/UI📱</strong> : 카카오톡 기반 결제로 사용자에게 익숙한 환경</li>
<li><strong>편의성🔑</strong> : 결제 연동이 간편하고 문서화가 잘 되어 있음</li>
<li><strong>손쉬운 테스트 환경🧪</strong> : 별도의 심사 없이 테스트 가능</li>
</ul>
</li>
<li><strong>🟥단점</strong><ul>
<li><strong>카카오톡 중심📲</strong> : 카카오톡을 사용하지 않는 사용자에게는 접근성이 낮음, 카카오톡 장애 발생시 결제 불가</li>
<li><strong>일부 결제 수단 제한 🌍💳</strong> : 해외 결제 및 일부 신용카드 결제 제한</li>
<li><strong>제한된 UI커스텀🛠️</strong> : 카카오 페이에서 제공하는 방식으로만 창을 띄울 수 있고 PC환경에서는 결제를 위해 QR코드 스캔 또는 전화번호 입력의 번거로움이 있음</li>
</ul>
</li>
</ul>
<h3 id="tosspay-💙⚡💰">TossPay 💙⚡💰</h3>
<ul>
<li><strong>🟩장점</strong><ul>
<li><strong>손쉬운 테스트 환경🧪</strong> : 별도의 심사 없이 테스트 가능</li>
<li><strong>결제창 UI 커스텀 가능🎨</strong> : Toss Paymemts SDK를 활용하여 자체 UI구성이 가능</li>
<li><strong>광범위한 결제 수단 지원 💳📱</strong> : 신용카드, 계좌이체, 가상계좌, 휴대폰결제 등 다양한 결제 수단을 제공</li>
</ul>
</li>
<li><strong>🟥단점</strong><ul>
<li><strong>제한적인 사용자층👥</strong> : 토스 사용자가 빠르게 증가하였지만, 카카오페이에 비해 인지도나 이용자가 적을 수 있음 (4050세대 이상)</li>
<li><strong>PC환경에서의 결제 경험 제한💻</strong> : 모바일 중심 서비스라 PC웹에서 UX가 최적화 되지 않았을 수도 있음</li>
</ul>
</li>
</ul>
<h2 id="왜-kakaopay를-선택했는가-💛"><strong>왜 KakaoPay를 선택했는가?</strong> 💛</h2>
<p>이번 프로젝트에서 KakaoPay를 선택한 이유는 주로 사용자 접근성과 편의성에 큰 장점이 있기 때문입니다. 여러 PG사들이 각각의 장단점이 있지만, KakaoPay는 다음과 같은 이유로 더 적합한 선택이었습니다.</p>
<h3 id="1-높은-사용자-접근성-🌍">1. 높은 사용자 접근성 🌍</h3>
<p> KakaoPay는 <strong>카카오톡</strong>과 깊게 통합되어 있기 때문에, 이미 <strong>많은 사용자들이 익숙</strong>하게 사용하고 있는 환경입니다. 결제 과정에서 사용자가 별도의 앱을 설치할 필요 없이 <strong>카카오톡만 있으면 바로 결제</strong>가 가능합니다. 이는 사용자 경험을 크게 향상시키며, <strong>결제 전환율</strong>을 높이는 데 유리한 요소입니다. 토스도 마찬가지로 많은 사용자와 모바일에 친숙하지만 토스 이용자 수 보다 카카오톡을 이용하는 이용자 수가 더 높을 것으로 예상되었습니다.</p>
<h3 id="2-모바일-친화적인-uxui📱">2. 모바일 친화적인 UX/UI📱</h3>
<p>KakaoPay는 <strong>모바일 환경에서 최적화된 UI/UX</strong>를 제공하여, 사용자가 결제 과정을 더욱 직관적으로 진행할 수 있습니다. 특히 카카오톡 사용자들에게 매우 익숙한 UI를 제공하여 <strong>사용자의 불편함을 최소화</strong> 할 수 있습니다. 이런 점에서 <strong>모바일 중심의 비즈니스</strong>에 특히 유리합니다. 완성까지의 시간을 고려하여 앱이 아닌 웹으로 유저의 UI를 제작하게 되었지만 기존에 앱을 생각하며 구상하였기 때문에 더욱 적합하다 생각하였습니다.</p>
<h3 id="3-편리한-테스트-환경-🧪">3. 편리한 테스트 환경 🧪</h3>
<p>다른 PG사들과 달리 KakaoPay는 <strong>심사 없이 테스트가 가능</strong>하다는 점이 큰 장점으로 와닿았습니다.  개발 및 테스트가 용이하고, 별도의 절차 없이 바로 테스트를 시작할 수 있어 <strong>개발 속도와 효율성</strong>을 크게 개선할 수 있었습니다.</p>
<h2 id="선택의-아쉬운-점-😮💨"><strong>선택의 아쉬운 점</strong> 😮‍💨</h2>
<h3 id="1-결제-수단의-제한🚫💳"><strong>1. 결제 수단의 제한🚫💳</strong></h3>
<p>KakaoPay는 카카오톡 기반이라 편리하지만, <strong>카카오톡을 사용하지 않거나 카카오페이 결제에 익숙하지 않은 사용자에게는 접근성이 떨어질 수 있습니다</strong>. 또한, 특정 해외 결제 및 일부 신용카드 사용이 제한되는 점도 아쉬운 부분입니다.</p>
<h3 id="2-ui-커스터마이징의-한계🎨🔒">2. UI 커스터마이징의 한계🎨🔒</h3>
<p>KakaoPay의 결제 화면은 <strong>카카오가 제공하는 방식 그대로 사용</strong>해야 하므로, 서비스에 맞게 자유롭게 <strong>커스터마이징하기 어렵습니다</strong>. 특히 PC 환경에서는 <strong>QR코드 스캔 또는 전화번호 입력 과정이 다소 번거롭게 느껴졌습니다</strong>.</p>
<h3 id="3-여러-pg사-연동의-어려움🔄🛠️">3. 여러 PG사 연동의 어려움🔄🛠️</h3>
<p>이번 프로젝트에서는 KakaoPay만을 고려하여 개발을 진행했기 때문에 당장은 문제가 없지만, 추후 다른 결제 서비스를 추가하려면 <strong>각각 개별로 연동해야 하는 번거로움</strong>이 발생할 수 있습니다.</p>
<p>이러한 경험을 바탕으로, 다음 프로젝트에서는 &quot;포트원(PortOne)&quot;을 활용하여 여러 PG사를 <strong>하나의 API로 통합하여 연동하는 방식</strong>을 시도할 계획입니다. 이를 통해 다양한 <strong>결제 수단을 유연하게 추가하고, 유지보수의 부담을 줄일 수 있을 것</strong>으로 기대됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 기술적 의사결정 2 : CI/CD]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-2-CICD</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-2-CICD</guid>
            <pubDate>Tue, 04 Feb 2025 00:00:48 GMT</pubDate>
            <description><![CDATA[<h1 id="1-기술적-의사-결정-github-actions-선택-이유">1. 기술적 의사 결정: GitHub Actions 선택 이유</h1>
<blockquote>
<p>프로젝트 배포 파이프라인을 구축할 때, Jenkins와 GitHub Actions 두 가지 옵션을 검토했습니다. 여러 측면에서 비교한 결과, GitHub Actions를 선택한 주요 이유는 다음과 같습니다.</p>
</blockquote>
<h2 id="1-통합-및-사용-편의성">1) 통합 및 사용 편의성</h2>
<p>GitHub Actions:
GitHub Actions는 GitHub 리포지토리와 완벽하게 통합되어 있어 별도의 CI/CD 서버를 구성할 필요 없이, 소스 코드 변경과 동시에 자동화된 배포 파이프라인을 바로 구축할 수 있습니다. 또한, YAML 기반의 워크플로우 설정이 직관적이어서 빠르게 학습하고 적용할 수 있습니다.</p>
<p>Jenkins:
Jenkins는 강력한 플러그인 에코시스템과 커스터마이징 가능성을 제공하지만, 별도의 설치, 관리, 유지보수가 필요합니다. GitHub와의 연동도 플러그인을 추가해야 하는 등 초기 설정과 관리에 더 많은 노력이 요구됩니다.</p>
<h2 id="2-유지보수-및-비용-효율성">2) 유지보수 및 비용 효율성</h2>
<p>GitHub Actions:
GitHub Actions는 클라우드 기반으로 운영되며, GitHub 리포지토리 내에서 관리되기 때문에 별도의 서버 관리 부담이 없습니다. 또한, 사용량에 따라 무료 제공 범위가 있어 소규모부터 중규모 프로젝트에 비용 효율적으로 적용할 수 있습니다.</p>
<p>Jenkins:
Jenkins는 자체 호스팅이 일반적이므로, 서버 관리, 보안 업데이트, 백업 등의 추가 유지보수 작업이 필요하며, 인프라 비용이 발생할 수 있습니다.</p>
<h2 id="3-확장성과-커뮤니티-지원">3) 확장성과 커뮤니티 지원</h2>
<p>GitHub Actions:
GitHub Actions는 GitHub의 활발한 커뮤니티와 풍부한 오픈소스 액션들이 이미 제공되고 있어, 필요에 따라 다양한 액션을 쉽게 활용할 수 있습니다. 또한, GitHub의 인프라를 활용하여 높은 확장성을 확보할 수 있습니다.</p>
<p>Jenkins:
Jenkins 또한 오랜 기간 널리 사용되며 방대한 플러그인을 보유하고 있지만, 플러그인 간의 호환성 문제나 업데이트 관리 등에서 복잡성이 증가할 수 있습니다.</p>
<hr>
<h1 id="2-docker-및-docker-hub를-활용">2. Docker 및 Docker Hub를 활용</h1>
<blockquote>
<p>배포 프로세스의 효율성과 일관성을 높이기 위해, 애플리케이션을 Docker 이미지로 빌드하고 Docker Hub에 업로드한 후, EC2 서버에서 해당 이미지를 다운로드하여 컨테이너로 실행하는 방식을 채택했습니다.</p>
</blockquote>
<h2 id="docker-사용-이유">Docker 사용 이유</h2>
<p>애플리케이션을 이미지 파일로 패키징하여 환경 간 일관성을 보장
로컬, 스테이징, 프로덕션 등 다양한 배포 환경에서 동일한 이미지를 사용할 수 있어 배포 및 유지보수가 용이</p>
<h2 id="docker-hub-사용-이유">Docker Hub 사용 이유</h2>
<p>중앙화된 이미지 저장소를 통해 배포 파이프라인에서 손쉽게 이미지를 관리
CI/CD 파이프라인과의 원활한 연계를 통해 빌드된 이미지를 자동으로 푸시하고, EC2 서버에서는 이를 풀(Pull) 받아 최신 버전으로 배포 가능</p>
<h2 id="3-결론">3. 결론</h2>
<p>GitHub Actions를 선택함으로써, GitHub와의 완벽한 통합과 간편한 YAML 설정을 통해 CI/CD 파이프라인을 효율적으로 구축할 수 있습니다. 또한, Docker와 Docker Hub를 활용한 배포 전략은 애플리케이션의 일관된 환경 제공 및 배포 자동화를 가능하게 하여, EC2 서버에서 최신 이미지를 손쉽게 배포할 수 있도록 합니다. 이러한 기술적 의사 결정은 초기 설정과 유지보수의 부담을 줄이고, 배포 과정에서의 효율성과 확장성을 극대화하는 데 기여합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 기술적 의사결정 1 : 실시간 가게 조회수 변경 기능]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-1-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B0%80%EA%B2%8C-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B8%B0%EC%88%A0-%EC%9D%98%EC%82%AC%EA%B2%B0%EC%A0%95-1-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EA%B0%80%EA%B2%8C-%EC%A1%B0%ED%9A%8C%EC%88%98-%EB%B3%80%EA%B2%BD-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Mon, 03 Feb 2025 14:47:49 GMT</pubDate>
            <description><![CDATA[<h1 id="실시간-가게-조회수-기능">실시간 가게 조회수 기능</h1>
<p>서비스에서는 사용자가 특정 가게 상세 조회 페이지에 진입하면 해당 가게의 조회수가 증가하고, 페이지를 벗어나면 감소하는 동적 기능이 필요합니다. 이를 위해 여러 통신 기술(SSE, WebSocket, RabbitMQ, Kafka)을 고려하였으며, 각 기술의 장단점을 아래와 같이 비교했습니다. 또한 이 조회수를 다른 사용자에게 전달하기 위해 메시징 이벤트가 필요합니다. 이 메시징 이벤트를 구현하기 위해 Redis의 Pub/Sub 기능을 활용했습니다.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/c18198dc-59ac-4fd5-af06-77f055636b3d/image.JPG" alt=""></p>
<h1 id="1-기술-비교">1. 기술 비교</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>SSE</th>
<th>WebSocket</th>
<th>RabbitMQ</th>
<th>Kafka</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점</strong></td>
<td>- 서버에서 클라이언트로의 단방향 실시간 알림 전송에 최적화됨<br>- HTTP 기반으로 브라우저에서 기본 지원되어 구현이 단순함<br>- 연결 유지 오버헤드가 낮아 조회수 업데이트와 같이 빈번한 이벤트 전송에 적합함</td>
<td>- 양방향 통신 지원으로 서버와 클라이언트가 자유롭게 데이터 교환 가능<br>- 실시간성이 매우 뛰어나며 지속적인 데이터 교환이 가능함</td>
<td>- 메시지 큐 기반으로 신뢰성 있는 비동기 메시징 제공<br>- 복잡한 메시지 라우팅 및 보증 기능 우수</td>
<td>- 대규모 데이터 스트림 처리에 최적화됨<br>- 확장성과 처리량 면에서 강점을 가짐</td>
</tr>
<tr>
<td><strong>단점</strong></td>
<td>- 클라이언트에서 서버로의 양방향 통신이 불가능하여 양방향 데이터 교환이 필요한 경우 부적합함</td>
<td>- 단순 조회수 업데이트와 같이 서버 → 클라이언트 단방향 알림만 필요한 경우, 구현 복잡성과 리소스 관리 부담이 큼</td>
<td>- 단순 실시간 알림 전송보다는 비동기 작업 처리에 적합하여 설정 및 운영이 복잡하고 오버헤드가 큼</td>
<td>- 설치 및 운영 부담이 크며, 단순 실시간 이벤트 전송에는 과도한 리소스 소모가 발생함</td>
</tr>
</tbody></table>
<h1 id="2-sse-선택-이유">2. SSE 선택 이유</h1>
<p>이 서비스에서는 실시간 단방향 알림 기능만 필요하므로, SSE(Server-Sent Events)를 선택했습니다.</p>
<p>조회수는 단순히 증감하는 이벤트이므로 양방향 통신이 필요 없습니다.</p>
<p>서비스에서 조회수 기능은 “사용자가 페이지에 들어갔을 때 증가, 나갔을 때 감소”하는 이벤트로, 클라이언트로 실시간 알림을 보내면 되므로 복잡한 양방향 통신이 필요하지 않습니다.</p>
<p>SSE는 HTTP 기반으로 브라우저에서 기본적으로 지원되며, 추가적인 라이브러리 없이 쉽게 구현 가능합니다.</p>
<p>지속적인 연결 유지가 필요하지만, WebSocket처럼 복잡한 핸드셰이크 및 연결 유지 관리가 필요 없습니다.</p>
<p>서버에서 클라이언트로만 데이터 전송이 가능하므로, 실시간 조회수 갱신에 적합합니다.</p>
<h1 id="3-redis-pubsub-활용">3. Redis Pub/Sub 활용</h1>
<p>사용자가 특정 가게의 상세 조회 페이지에 들어가면 Redis의 Pub 이벤트가 발생합니다. 해당 페이지에 이미 접속해 있는 사용자들은 Sub 이벤트를 수신하여 실시간으로 조회수를 업데이트 받습니다.</p>
<p><strong>실시간 반영</strong>: 사용자가 페이지에 들어가거나 나갈 때마다 즉각적으로 조회수가 업데이트되어 사용자 경험을 향상시킵니다.</p>
<p><strong>경량화</strong>: 별도의 무거운 메시지 큐 시스템 없이, Redis의 경량 Pub/Sub 기능을 통해 빠른 메시지 전달이 가능해집니다.</p>
<p><strong>확장성</strong>: 여러 서버 인스턴스 간에도 이벤트 전달이 원활하여, 트래픽이 증가하더라도 안정적인 성능을 유지할 수 있습니다.</p>
<h1 id="4-결론">4. 결론</h1>
<p>실시간 가게 조회수 기능은 사용자가 현재 보고있는 가게의 인기와 경쟁도를 확인하고 예약을 촉진시킬 수 있는 기능입니다. </p>
<p>이 기능을 구현하기 위해서는 서버와 클라이언트가 지속적으로 연결이 유지되는 통신 기술이 필요하고 조회 수 변경 감지에 따른 조회 수 업데이트가 필요하기 때문에 메세지 이벤트 시스템이 필요했습니다.</p>
<p>이 요구사항에 맞게 여러 기술을 비교해보고, SSE와 Redis의 Pub/Sub 기술을 이 서비스에 적합하다고 판단하여 적용하게 되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[얼리테이블 - 프로젝트 설계]]></title>
            <link>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%A3%BC%EC%A0%9C-%EC%84%A0%EC%A0%95</link>
            <guid>https://velog.io/@take_the_king/%EC%96%BC%EB%A6%AC%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%A3%BC%EC%A0%9C-%EC%84%A0%EC%A0%95</guid>
            <pubDate>Sun, 02 Feb 2025 12:18:42 GMT</pubDate>
            <description><![CDATA[<h1 id="얼리테이블--실시간-음식점-웨이팅예약-어플">얼리테이블 : 실시간 음식점 웨이팅&amp;예약 어플</h1>
<img src = "https://github.com/user-attachments/assets/374b47bd-1017-458d-89f6-4ebf05833575" width = "500">  


<h3 id="줄서기-스트레스는-그만-효율적인-웨이팅의-시작">&quot;줄서기 스트레스는 그만! 효율적인 웨이팅의 시작&quot;</h3>
<p>외식을 자주 즐기는 요즘 인기 맛집에서 길게 줄 서는 시대는 끝! 얼리 테이블로 미리 예약하고, 웨이팅을 잡아 스마트하게 식사 준비하세요. 시간도 아끼고, 더 맛있게!!</p>
<h2 id="❤️-서비스프로젝트-소개">❤️ 서비스/프로젝트 소개</h2>
<h3 id="서비스-개요">서비스 개요</h3>
<aside>
본 서비스는 음식점 예약 및 웨이팅 기능을 제공하여 사용자 경험을 개선한 프로젝트입니다. 기존 테이블링 앱의 낮은 사용성을 보완하고, 보다 편리한 예약 및 웨이팅 시스템을 제공합니다.

</aside>

<h3 id="기획-배경">기획 배경</h3>
<h4 id="현재-많은-음식점이-테이블링-앱을-활용하고-있지만-사용자들은-여러-문제점을-겪고-있습니다">현재 많은 음식점이 테이블링 앱을 활용하고 있지만, 사용자들은 여러 문제점을 겪고 있습니다.</h4>
<ul>
<li><strong>부정확한 대기 시간 예측</strong>: 기존 서비스에서는 대기 시간이 미제공 또는 부정확하게 제공되어 사용자 불편이 발생</li>
<li><strong>개인화된 서비스 부족</strong>: 사용자의 식습관(예: 알러지 등)에 대한 필터링 기능이 미비</li>
<li><strong>일행 관리 서비스 부족</strong>: 일행이 예약에 대한 정보를 알고싶을 때 예약자를 통해 확인하는 번거로움 발생</li>
</ul>
<h3 id="개선-사항">개선 사항</h3>
<h4 id="본-서비스는-기존의-문제를-해결하고-보다-편리한-예약-및-웨이팅-환경을-제공하기-위해-다음과-같은-기능을-제공합니다">본 서비스는 기존의 문제를 해결하고, 보다 편리한 예약 및 웨이팅 환경을 제공하기 위해 다음과 같은 기능을 제공합니다.</h4>
<ol>
<li><strong>실시간 예상 대기 시간 제공</strong><ul>
<li>정확한 대기 시간 정보 제공</li>
<li>실시간 대기열 업데이트로 사용자의 대기 불편 최소화</li>
</ul>
</li>
<li><strong>맞춤형 필터링 기능</strong><ul>
<li>알러지, 특정 식재료 제외 등 사용자의 조건을 고려한 음식점 필터링 제공</li>
</ul>
</li>
<li><strong>향상된 사용자 경험 제공</strong><ul>
<li>손쉬운 예약 및 웨이팅 기능 제공</li>
<li>알림 기능을 활용한 예약 및 웨이팅 상태 실시간 업데이트</li>
<li>일행을 초대하고 관리할 수 있는 서비스 제공</li>
<li>실시간 특정 가게 조회수 조회 기능 제공</li>
</ul>
</li>
</ol>
<h2 id="📊-주요-기능">📊 주요 기능</h2>
<details>
<summary>실시간 웨이팅</summary>
<div markdown="1">

<ul>
<li>실시간 남은 웨이팅 순서 정보 + 실시간 예상 대기 시간 제공</li>
</ul>
<img src = "https://github.com/user-attachments/assets/46615295-da6f-4312-b68c-3b4431e21d7a" width = "180">  

</div>
</details>

<details>
<summary>실시간 예약 및 결제 API 연동</summary>
<div markdown="1">

<ul>
<li>동시성 제어 - Redis 분산락 적용</li>
<li>카카오 페이의 API를 연동하여 결제 정보처리 및 결과 DB에 반영</li>
</ul>
<img src = "https://github.com/user-attachments/assets/ac77ae79-095e-4c3f-9b58-39a4f7923ffc" width = "520">  


</div>
</details>

<details>
<summary>실시간 가게 조회수</summary>
<div markdown="1">

<ul>
<li>SSE와 Redis Pub/Sub 기능 이용</li>
</ul>
<img src = "https://github.com/user-attachments/assets/71d2ae6a-5791-4ffe-a50d-08173128329d" width = "180">  

</div>
</details>

<details>
<summary>알림 서비스</summary>
<div markdown="1">

<ul>
<li>FCM과 SSE, Redis Pub/Sub 기능 이용</li>
</ul>
<img src = "https://github.com/user-attachments/assets/524a9604-1df0-450c-8b52-4c3a74c18b2e" width = "480">  

</div>
</details>

<details>
<summary>맞춤형 조건 검색</summary>
<div markdown="1">

<ul>
<li>지역, 금액, 음식 카테고리, 알러지 및 검색어로 맞춤 검색 기능 제공</li>
</ul>
<img src = "https://github.com/user-attachments/assets/ed79f86e-bb8d-4257-b2ec-395c498deb23" width = "680">  


</div>
</details>

<h2 id="🛠️-적용-기술">🛠️ 적용 기술</h2>
<img src = "https://github.com/user-attachments/assets/2c066087-101a-41b6-b756-d581c28b387b" width = "500"> 


<h2 id="🛣️-인프라-설계도">🛣️ 인프라 설계도</h2>
<img src = "https://github.com/user-attachments/assets/055a7208-eb87-43be-b6e9-21ce1723ab19" width = "700"> 

<p>이 프로젝트는 AWS 인프라 클라우드 환경에서 구축했습니다. 
사용자가 처음 접속하면 Route 53을 통해 연결된 도메인으로 접근하며, 정적 콘텐츠는 S3에서 제공됩니다. API 요청이 들어오면 ALB가 트래픽을 분산시키고, EC2 인스턴스에서 동작하는 Spring Boot 서버와 RDS(MySQL)가 연동하여 요청을 처리합니다. ALB를 통해 분산 서버에 대한 확장성을 고려할 수 있습니다. 
성능 최적화를 위해 Redis의 캐싱과 분산락, Pub/Sub 기능을 사용합니다. 
GitHub Actions + Docker를 이용한 CI/CD 파이프라인을 구축하여, 코드 변경 시 자동으로 빌드 및 배포가 이루어지도록 만들었습니다.</p>
<h2 id="erd">ERD</h2>
<img src = "https://github.com/user-attachments/assets/4da306f5-644a-42d5-b484-3e0af8d981ff" width = "900"> 

<p>이 ERD는 식당 예약/웨이팅 시스템을 중심으로 설계되었습니다. 
핵심 테이블인 가게를 중심으로 메뉴, 예약, 웨이팅 테이블을 구성하여 시스템의 주요 기능을 구현했습니다. 부가적으로 알러지, 가게 키워드, 리뷰, 이미지 파일 및 가게 설정 관련 테이블을 구성하여 다양한 기능을 지원할 수 있도록 구성했습니다.
정규화를 준수하여 데이터 중복을 최소화하고, 테이블 간의 관계 설정을 통해 데이터 일관성을 유지하도록 설계했습니다. 
JPA를 활용하여 객체와 테이블 간의 연관관계를 매핑하고, QueryDSL을 사용하여 타입-세이프한 방식으로 동적 쿼리를 최적화하여 유지보수성과 성능을 향상시켰습니다.</p>
<h2 id="업무-타임라인">업무 타임라인</h2>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/8d5649fe-c757-4c99-a0e1-e165f8f9d831/image.JPG" alt=""></p>
<ul>
<li>wbs
<img src="https://velog.velcdn.com/images/take_the_king/post/d448454d-dc09-4036-b1b4-942e1bc49445/image.JPG" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL(1) 세팅 및 설정]]></title>
            <link>https://velog.io/@take_the_king/QueryDSL1-%EC%84%B8%ED%8C%85-%EB%B0%8F-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@take_the_king/QueryDSL1-%EC%84%B8%ED%8C%85-%EB%B0%8F-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 30 Dec 2024 03:29:41 GMT</pubDate>
            <description><![CDATA[<h1 id="1-querydsl-세팅-및-설정">1. QueryDSL 세팅 및 설정</h1>
<h2 id="1-의존성-추가">1) 의존성 추가</h2>
<ul>
<li><p>build.gradle</p>
<pre><code class="language-java">dependencies {
//QueryDSL
  implementation &quot;com.querydsl:querydsl-jpa:5.0.0:jakarta&quot;
  annotationProcessor &quot;com.querydsl:querydsl-apt:5.0.0:jakarta&quot;
  annotationProcessor &quot;jakarta.annotation:jakarta.annotation-api&quot;
  annotationProcessor &quot;jakarta.persistence:jakarta.persistence-api&quot;

  ...
}</code></pre>
</li>
</ul>
<p>build.gradle에 dependencies 안에 queryDSL 의존성 추가한다.</p>
<h2 id="2-qclass-빌드">2) QClass 빌드</h2>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/cad7fc23-a709-4c0e-9459-dbe1641cf2d1/image.png" alt="">
<img src="https://velog.velcdn.com/images/take_the_king/post/b84272d8-7ab8-47b3-99f6-0b31aa747c7b/image.png" alt="">
Gradle의 Task에서 [build &gt; clean] -&gt;[other &gt; compileJava] 과정을 차례로 수행해보자.</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/7e99fa05-fc9a-4100-b213-d2792ac48d3f/image.png" alt="">
 
다음 사진과 같이 build 디렉터리에 Entity에서 생성된 Q Class가 성공적으로 빌드되었는지 확인해보자.
컴파일 단계에서 Entity와 형태가 같은 Static Class로 QClass을 생성하고, QueryDSL은 해당 클래스를 기반으로 쿼리 메서드를 실행시키게 된다. 따라서 타입의 불일치에 대한 에러 캐치에 관련하여 좋은 장점을 가질 수 있다.</p>
<h3 id="qclass-뜯어보기">QClass 뜯어보기</h3>
<p> </p>
<pre><code class="language-java">/**
 * QMember is a Querydsl query type for Member
 */
@Generated(&quot;com.querydsl.codegen.DefaultEntitySerializer&quot;)
public class QMember extends EntityPathBase&lt;Member&gt; {

    private static final long serialVersionUID = 1003972608L;

    public static final QMember member = new QMember(&quot;member1&quot;);

    public final ListPath&lt;Category, QCategory&gt; categories = this.&lt;Category, QCategory&gt;createList(&quot;categories&quot;, Category.class, QCategory.class, PathInits.DIRECT2);

    public final StringPath email = createString(&quot;email&quot;);

    public final NumberPath&lt;Long&gt; memberId = createNumber(&quot;memberId&quot;, Long.class);

    public final StringPath name = createString(&quot;name&quot;);

    public final StringPath password = createString(&quot;password&quot;);

    public final EnumPath&lt;com.example.tosshelperappserver.common.constant.RoleType&gt; role = createEnum(&quot;role&quot;, com.example.tosshelperappserver.common.constant.RoleType.class);

    public QMember(String variable) {
        super(Member.class, forVariable(variable));
    }

    public QMember(Path&lt;? extends Member&gt; path) {
        super(path.getType(), path.getMetadata());
    }

    public QMember(PathMetadata metadata) {
        super(Member.class, metadata);
    }

}</code></pre>
<p> 
생성된 QClass 내부를 살짝만 한번 들여다보고 가보자.
EntityPathBase<T>를 상속받는데, 이는 엔티티에 대한 경로를 나타내는 QueryDSL의 추상 클래스이다.
멤버 변수로 매핑된 엔티티를 기반으로 자동 생성된 속성들이 존재한다.
 
Type으로 그냥 String이 아니라, StringPath, 그리고 createString 따위의 메서드를 사용하는 것을 볼 수 있는데, 이는 Builder Pattern을 활용하여 QType의 속성을 변경할 수 있도록 설계되었기 때문이다.</p>
<h2 id="2-querydsl-사용을-위한-factory를-bean으로-등록">2) QueryDSL 사용을 위한 Factory를 Bean으로 등록</h2>
<pre><code class="language-java">@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}</code></pre>
<p>JPAQueryFactory는 QueryDSL에서 제공하는 주요 클래스 중 하나이다. 해당 Config 파일을 만들어 JPAQueryFactory를 QueryDSL을 이용한 JPA 쿼리를 빌드하는 Factory 역할로서 사용할 수 있도록 Bean으로 등록시켜 두자.</p>
<h2 id="3-repository에서-사용하기">3) Repository에서 사용하기</h2>
<p> 
 </p>
<pre><code>@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {


    // 쿼리 메서드
    Member findMemberByEmail(String email);

    // 동적 쿼리 생성
    @Query(value= &quot;select m from Member m where m.memberId = :id and m.email = :email&quot; )
    Member findMemberByIdAndEmail(@Param(&quot;id&quot;) Long id, @Param(&quot;email&quot;) String email);


}</code></pre><p> 
기존에 JpaRepository를 사용하여 다음과 같은 쿼리들을 작성해 두었다고 가정해보자, 이제 해당 Repository에서 QueryDsl을 사용한 메서드를 작성할 수 있도록 해 보자.</p>
<h2 id="4-custom-repository에서-querydsl을-사용하여--쿼리-빌드">4) Custom Repository에서 QueryDSL을 사용하여  쿼리 빌드</h2>
<p> </p>
<pre><code class="language-java">public interface MemberCustomRepository {

    Member findAllLeftFetchJoin(Long id);
}</code></pre>
<pre><code class="language-java">package com.example.tosshelperappserver.repository;
import com.example.tosshelperappserver.domain.Member;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Repository;
import static com.example.tosshelperappserver.domain.QMember.member;


@Repository
@AllArgsConstructor
public class MemberCustomRepositoryImpl implements MemberCustomRepository{

    private final JPAQueryFactory jpaQueryFactory;


    @Override
    public Member findAllLeftFetchJoin(Long id) {
        return jpaQueryFactory.selectFrom(member)
                .where(member.memberId.eq(id))
                .leftJoin(member.categories)
                .fetchJoin()
                .fetchOne();
    }
}</code></pre>
<p> 
JpaRepository는 인터페이스이기 때문에 코드를 구현할 수 없다. 따라서 CustomRepository를 작성해주자.
내부에서는 JPAQueryFactory를 Injection하여 사용하고 있다.
 
쿼리문은 Builder 패턴으로 작성된다. 쿼리문을 작성하는 내용에 대해서는 다음에 다루어볼 예정이지만 해당 쿼리문은 memberId와 일치하는 Member를 Categories와 함께 Join하는 쿼리문이다. 
눈썰미가 있다면 눈치챘을 수도 있겠지만, jpaQueryFactory에서 사용되는 쿼리문의 내부에는 QClass가 사용되고 있다.</p>
<h2 id="5-jparepository에-추가-및-service에서의-사용">5) JpaRepository에 추가 및 Service에서의 사용</h2>
<p> </p>
<pre><code class="language-java">@Repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberCustomRepository {


    // 쿼리 메서드
    Member findMemberByEmail(String email);

    // 동적 쿼리 생성
    @Query(value= &quot;select m from Member m where m.memberId = :id and m.email = :email&quot; )
    Member findMemberByIdAndEmail(@Param(&quot;id&quot;) Long id, @Param(&quot;email&quot;) String email);

}</code></pre>
<p> 
이제 해당 Repository를 JpaRepository에서 상속받아 QueryDsl을 사용한 메서드들을 사용할 수 있도록 하자.
 
 
 </p>
<pre><code class="language-java">@Override
public MemberWithCategoryDto getMemberInfoWithOwnCategory(Long id) {
    Member member = memberJpaRepository.findAllLeftFetchJoin(id);
    MemberWithCategoryDto dto = modelMapper.map(member, MemberWithCategoryDto.class);
    return dto;
}</code></pre>
<p> </p>
<p>출처: <a href="https://sjh9708.tistory.com/174">https://sjh9708.tistory.com/174</a> [데굴데굴 개발자의 기록:티스토리]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL(2) 기본 문법]]></title>
            <link>https://velog.io/@take_the_king/QueryDSL2-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@take_the_king/QueryDSL2-%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Mon, 30 Dec 2024 02:33:36 GMT</pubDate>
            <description><![CDATA[<h1 id="2-querydsl-의-기본문법">2. Querydsl 의 기본문법</h1>
<h2 id="1-querydsl과-jpql-비교">1) Querydsl과 JPQL 비교</h2>
<p>먼저 Querydsl과 JPQL을 비교해보겠다.</p>
<p>Querydsl vs JPQL</p>
<pre><code class="language-java">JPAQueryFactory qf;

  @Test
  public void startJPQL(){

  // JPQL을 사용한 member1 찾기

    String qlString = &quot;select m from Member m &quot; +
                      &quot;where m.username = :username&quot;;

        Member findMember = em.createQuery(qlString, Member.class)
         .setParameter(&quot;username&quot;, &quot;member1&quot;)
         .getSingleResult();

         assertThat(findMember.getUsername()).isEqualTo(&quot;member1&quot;);

  }

  @Test
  public void startQuerydsl(){

  //Querydsl 사용한 member1 찾기
    JPAQueryFactory qf = new JPAQueryFactory(em);

    Memeber findMember =qf
                          .select(Qmember.member)
                          .from(Qmember.member)
                          .where(Qmember.member.name.eq(&quot;member1&quot;) //파라미터 바인딩 처리
                          .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo(&quot;member&quot;1);

  }</code></pre>
<p>EntityManager 로 JPAQueryFactory 생성</p>
<p>Querydsl 은 JPQL 빌더</p>
<p>JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)
JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리</p>
<p>JPAQueryFactory 를 필드로 설정 할 수도 있다.</p>
<blockquote>
</blockquote>
<p>*<em>JPQQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? *</em>
동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntiryManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도
트랙재션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.</p>
<h2 id="2-qclass">2) Qclass</h2>
<p>기본적으로 QueryDsl을 사용할때 QClass가 생성이 된다.  초기에 QueryDSL을 사용하면서 궁금했던 내용은 그냥 Entity를 사용해도 될거같은데 굳이 QClass를 만들어서 사용을 할까? 어떻게 만드는거지? 라는 기본적인 궁금증에서 래퍼런스 문서 부터 많은 블로그의 내용을 찾아봤으며 해당 내용을 정리 해보려고 한다.
 
JPA_APT(JPAAnnotationProcessorTool)가 @Enttiy 와 같은 특정 어노테이션을 찾고 해당 클래스를 분석해서 QClass를 만들어 준다.  빌드 도구를 통해서 만드는 방법은 다른곳을 찾아봐도 나오니 생략한다. 
(Gradle의 경우, 버전별로 설정을 하는 방식이 다르기 때문에 버전에 맞게 잘 찾아서 사용 해야 한다.)
 </p>
<h3 id="✋-apt-란-">✋ APT 란 ?</h3>
<p>Annotation 이 있는 기존코드를 바탕으로 새로운 코드와 새로운 파일들을 만들 수 있고, 이들을 이용한 클래스에서 compile 하는 기능도 지원해준다.
쉬운 예시로는 Lombok의 @Getter, @Setter가 있다. 해당 어노테이션을 사용하는 경우 apt가 컴파일 시점에 해당 어노테이션을 기준으로 getter 와 setter를 만들어 주기 때문에 코드를 작성하지 않고 사용이 가능해진다.
 </p>
<h3 id="✋-qclass-란">✋ QClass 란?</h3>
<p>엔티티 클래스의 메타 정보를 담고 있는 클래스로, Querydsl은 이를 이용하여 타입 안정성(Type safe)을 보장하면서 쿼리를 작성할 수 있게 된다.
 
QClass는 엔티티 클래스와 대응되며  엔티티의 속성을 나타내고 있다. 이러한 QClass를 사용하여 쿼리를 작성하면 엔티티 속성을 직접 참조하고 조합하여 쿼리를 구성할 수 있다. QClass를 사용하면 컴파일 시점에 오류를 확인할 수 있고, IDE의 자동완성 기능을 활용하여 쿼리 작성을 보다 편리하게 할 수 있다.
 
그렇다면 굳이 엔티티 클래스 대신 Q클래스를 만들어서 사용하는 이유에 대해서 정리 하려고 한다. 
 
QClass와 엔티티 클래스는 많은 장점을 공유하고 있지만 그럼에 QClass를 사용하는 이유는 다음과 같다.</p>
<p>QClass는 엔티티 속성을 정적인 방식으로 표현하므로 IDE의 자동 완성 기능을 활용할 수 있고, 속성 이름을 직접 기억하거나 확인하지 않아도 된다는 장점을 가지고 있다. 
QClass는 엔티티 속성의 타입을 정확하게 표현하므로, 타입에 맞지 않는 연산이나 비교를 시도하면 컴파일러가 오류를 감지할 수 있다.</p>
<p>QClass는 엔티티 클래스의 확장으로 생각할 수 있다. 엔티티 클래스는 데이터베이스 테이블의 매핑을 담당하고, QClass는 쿼리 작성을 위한 편의성과 안전성을 제공을 해주면서 유지보수의 편의성 및 실수 방지를 하지 않도록 해준다고 생각한다.</p>
<p>기본 Q-Type 활용
Q클래스 인스턴스를 사용하는 3가지 방법</p>
<pre><code class="language-java">
QMember qMember = new QMember(&quot;m&quot;); //별칭 직접 설정
QMember qMember = QMember.member; //기본 인스턴스 사용

import static kbds.querydsl.domain.QMember.member; //static 상수 설정
</code></pre>
<blockquote>
<p>참고 : 같은 테이블을 조인해야하는 경우가 아니면 기본 인스턴스를 사용하자.
검색 조건 쿼리</p>
</blockquote>
<pre><code class="language-java"> @Test
 public void search(){

  Member findMember =  qf
    .selectFrom(member)
    .where(member.username.eq(&quot;member1&quot;)
    .and(member.age.eq(10))
    .fetchOne();

 }</code></pre>
<h2 id="2-기본-문법">2) 기본 문법</h2>
<h3 id="1-셋팅">(1) 셋팅</h3>
<p>사용할 데이터</p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/45f5525f-0855-496a-aeb0-d079e45e17fc/image.png" alt=""></p>
<p> 
 </p>
<ol>
<li>Author : Book = 1 : N
Author(저자)는 여러 개의 Book(책)을 가진다.
 </li>
<li>Author : Organization = N : 1
Author(저자)는 한곳의 Organization(조직)에 속한다.
 </li>
<li>Book : Review = 1 : N
Book(책)은 여러 개의 Review(리뷰)를 가진다.</li>
</ol>
<pre><code class="language-java">@Entity
@Table(name = &quot;Organization&quot;)
@Getter
@Setter
public class Organization {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String orgName;

    @OneToMany(mappedBy = &quot;organization&quot;, cascade = CascadeType.ALL)
    private List&lt;Author&gt; authors = new ArrayList&lt;&gt;();

}</code></pre>
<pre><code class="language-java">@Entity
@Table(name = &quot;Author&quot;)
@Getter
@Setter
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = &quot;author&quot;, cascade = CascadeType.ALL)
    private List&lt;Book&gt; book = new ArrayList&lt;&gt;();

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = &quot;organization_id&quot;)
    private Organization organization;

}</code></pre>
<pre><code class="language-java">@Entity
@Table(name = &quot;Book&quot;)
@Getter
@Setter
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = &quot;author_id&quot;)
    private Author author;

    @OneToMany(mappedBy = &quot;book&quot;, cascade = CascadeType.ALL)
    private List&lt;Review&gt; reviews = new ArrayList&lt;&gt;();

}</code></pre>
<pre><code class="language-java">@Entity
@Table(name = &quot;Review&quot;)
@Getter
@Setter
public class Review {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String comment;

    @ManyToOne
    @JoinColumn(name = &quot;book_id&quot;)
    private Book book;

}</code></pre>
<h3 id="2-검색select">(2) 검색(Select)</h3>
<p> </p>
<h4 id="리스트-조회">리스트 조회</h4>
<pre><code>public List&lt;Book&gt; findBookList() {
    List&lt;Book&gt; result = queryFactory
            .selectFrom(book)
            .fetch();
    return result;
}</code></pre><p> 
 </p>
<h4 id="단일-조회">단일 조회</h4>
<pre><code>public Book findBookByTitle(String title) {
    Book result = queryFactory.selectFrom(book)
            .where(book.title.eq(title))
            .fetchOne();
    return result;
}</code></pre><p> </p>
<ul>
<li>selectFrom(QType) : 해당 엔티티에서 모든 컬럼 조회</li>
<li>fetch() : 리스트를 조회, 데이터가 존재하지 않을 시 빈 리스트를 반환</li>
<li>fetchOne() : 단일 조회, 결과가 없으면 Null, 결과가 단일이 아닐 시 Exception 발생</li>
<li>fetchFirst() : 단일 조회, 결과가 여러개여도 맨 처음 레코드 반환</li>
</ul>
<p> </p>
<h4 id="컬럼-선택하여-조회">컬럼 선택하여 조회</h4>
<pre><code>public List&lt;String&gt; findBookListTitle() {
    List&lt;String&gt; result = queryFactory.select(book.title)
            .from(book)
            .fetch();
    return result;
}</code></pre><ul>
<li>select(QType.Column1, QType.Column2) : 엔티티의 해당 컬럼(들) 조회</li>
<li>from(QType) : 조회 대상 엔티티</li>
</ul>
<h3 id="3-조건where">(3) 조건(Where)</h3>
<p> </p>
<pre><code class="language-java">public Book findBookByTitle(String title) {
    Book result = queryFactory.selectFrom(book)
            .where(book.title.eq(title))
            .fetchOne();
    return result;
}</code></pre>
<p> </p>
<ul>
<li>where(조건) : 해당 조건과 일치하는 레코드(들)을 반환</li>
</ul>
<p> 
 
 </p>
<h4 id="다양한-조건-함수">다양한 조건 함수</h4>
<p> </p>
<pre><code class="language-java">// 동일 여부
author.name.eq(&quot;John&quot;); // 일치
author.name.ne(&quot;John&quot;); // 일치X
author.name.isNotNull(); // NullX

// 포함
author.age.in(20, 30, 40); // 포함
author.age.notIn(25, 35, 45); // 미포함

// 문자열
author.name.like(&quot;J%&quot;); // LIKE : J로 시작
author.name.startsWith(&quot;J&quot;); // J로 시작
author.name.contains(&quot;Jo&quot;); // J 포함

// 수 비교
author.age.between(25, 35); // 25 ~ 35
author.age.lt(30); // &lt; 30
author.age.loe(30); // &lt;= 30
author.age.gt(30); // &gt; 30
author.age.goe(30); // &gt;= 30</code></pre>
<p> 
 </p>
<h4 id="복합-조건-연산">복합 조건 연산</h4>
<p> </p>
<pre><code class="language-java">public List&lt;Author&gt; findAuthorByCondition() {

    List&lt;Author&gt; result = queryFactory.selectFrom(author)
            .where(
                    author.age.notBetween(20, 30)
                            .and(author.age.gt(10))
                            .and(author.age.lt(50))
            )
            .fetch();

    return result;
}</code></pre>
<pre><code class="language-java">select * from author
where age NOT BETWEEN 20 and 30 
and age &gt; 10 and age &lt; 50;</code></pre>
<p> </p>
<ul>
<li>and(조건) : AND 복합 조건</li>
<li>or(조건) : OR 복합 조건</li>
</ul>
<p> 
 
 </p>
<pre><code class="language-java">public List&lt;Author&gt; findAuthorByCondition2() {

    List&lt;Author&gt; result = queryFactory.selectFrom(author)
            .where(
                    (
                            author.age.notBetween(20, 30)
                                    .and(author.age.gt(10))
                                    .and(author.age.lt(50))
                    ).or(
                            author.name.like(&quot;%John%&quot;)
                    )
            )
            .fetch();


    return result;

}</code></pre>
<pre><code class="language-java">select * from author
where (age NOT BETWEEN 20 and 30 
and age &gt; 10 and age &lt; 50) or
(name LIKE &quot;%John%&quot;);</code></pre>
<p> </p>
<ul>
<li>() 로 조건들을 묶어 표현식 만들 수 있다.</li>
</ul>
<p> </p>
<pre><code class="language-java">.where(
    boardIdEq(boardId),
    cardTitleEq(cardTitle),
    cardExplanationEq(cardExplanation),
    endAtEq(endAt),
    cardManagerNicknameEq(cardMangerName)
)

...

private BooleanExpression boardIdEq(Long boardId) {
        return boardId != null ? card.boardList.board.id.eq(boardId) : null;
    }

    private BooleanExpression cardTitleEq(String cardTitle) {
        return cardTitle != null ? card.cardTitle.contains(cardTitle) : null;
    }

    private BooleanExpression cardExplanationEq(String cardExplanation) {
        return cardExplanation != null ? card.cardExplanation.contains(cardExplanation) : null;
    }

    private BooleanExpression endAtEq(String endAt) {
        if (endAt == null) {
            return null;
        }
        LocalDate ConvertedEndAt = LocalDate.parse(endAt);
        return card.endAt.eq(ConvertedEndAt);
    }

    private BooleanExpression cardManagerNicknameEq(String userNickname) {
        return userNickname != null ? user.nickName.eq(userNickname) : null;
    }</code></pre>
<ul>
<li>메서드로 조건을 추출하며 적용할 수 있다.</li>
</ul>
<p> 
 
 </p>
<h4 id="연관된-엔티티의-컬럼을-조건으로-사용">연관된 엔티티의 컬럼을 조건으로 사용</h4>
<p> </p>
<pre><code class="language-java">public List&lt;Book&gt; findBooksByAuthorName(String name) {
    List&lt;Book&gt; result = queryFactory
            .selectFrom(book)
            .where(book.author.name.eq(name))
            .fetch();
    return result;
}</code></pre>
<p> </p>
<ul>
<li>book.author.name 을 보면 연관된 엔티티의 컬럼을 조건으로 사용할 수 있다. (Book과 Author은 N:1 관계)</li>
<li>SQL 쿼리문으로는 아래와 같다.</li>
</ul>
<p> </p>
<pre><code class="language-java">SELECT Book.*
FROM Book
INNER JOIN Author ON Book.author_id = Author.id
WHERE Author.name = &#39;John Doe&#39;;</code></pre>
<p> </p>
<h3 id="4-정렬-order-by">(4) 정렬 (Order By)</h3>
<p> </p>
<pre><code>public List&lt;Book&gt; findBookListOrderBy() {
    List&lt;Book&gt; result = queryFactory
            .selectFrom(book)
            .orderBy(book.title.desc())
            .fetch();
    return result;
}</code></pre><p> </p>
<ul>
<li>orderBy(QType.Column.(정렬조건)) : 해당 컬럼을 정렬조건에 따라 정렬 (디폴트는 asc)</li>
</ul>
<p>정렬조건</p>
<ul>
<li>desc() : 내림차순</li>
<li>asc() : 올림차순</li>
</ul>
<p> </p>
<pre><code class="language-java">public List&lt;Book&gt; findBookListOrderBy() {
    List&lt;Book&gt; result = queryFactory
            .selectFrom(book)
            .orderBy(book.title.desc().nullsLast())
            .fetch();
    return result;
}</code></pre>
<p> 
 
 </p>
<ul>
<li>nullsLast(), nullsFirst() : Null 데이터에 대한 순서를 부여한다(끝, 시작)</li>
</ul>
<p> </p>
<h3 id="5-페이지네이션-offset-limit">(5) 페이지네이션 (Offset, Limit)</h3>
<p> </p>
<pre><code class="language-java">public List&lt;Book&gt; findBookListPagenation(int offset, int limit) {
    List&lt;Book&gt; result = queryFactory
            .selectFrom(book)
            .offset(offset)
            .limit(limit)
            .fetch();
    return result;
}</code></pre>
<p> </p>
<ul>
<li>offset(long x) : 0부터 시작하는 결과에 대한 오프셋(시작 위치)</li>
<li>limit(long x) : 쿼리 결과에 대한 최대치 제한(Limit)</li>
</ul>
<p> 
 </p>
<pre><code class="language-java">public List&lt;Book&gt; findBookListPagenation(int offset, int limit) {
    QueryResults&lt;Book&gt; res = queryFactory
            .selectFrom(book)
            .offset(offset)
            .limit(limit)
            .fetchResults();

    System.out.println(&quot;Total : &quot; + res.getTotal());
    System.out.println(&quot;Limit : &quot; + res.getLimit());
    System.out.println(&quot;Offset : &quot; + res.getOffset());
    List&lt;Book&gt; result = res.getResults();
    return result;
}</code></pre>
<ul>
<li>fetchResults : 결과를 가져올 때 페이지네이션 정보를 함께 가져온다. (total, limit, offset) 결과는 getResults()를 호출 시 얻을 수 있다.</li>
</ul>
<p> </p>
<h3 id="6-집계함수aggregation">(6) 집계함수(Aggregation)</h3>
<p> 
그룹 함수와 함께 주로 사용된다.</p>
<pre><code class="language-java">public List&lt;Tuple&gt; findAuthorAggregation() {
    List&lt;Tuple&gt; result = queryFactory
            .select(author.count(), author.age.avg())
            .from(author)
            .fetch();
    return result;
}</code></pre>
<p> </p>
<ul>
<li>count() : 집합의 행 수 계산</li>
<li>sum() : 합 계산</li>
<li>avg() : 평균 계산</li>
<li>max() : 최대 계산</li>
<li>min() : 최소 계산</li>
</ul>
<p> 
 </p>
<h3 id="7-그룹화-group-by-having">(7) 그룹화 (Group By, Having)</h3>
<p> 
 </p>
<pre><code class="language-java">public List&lt;Tuple&gt; findAuthorGroupByGender() {
    List&lt;Tuple&gt; result = queryFactory
            .select(author.gender, author.count(), author.age.avg())
            .from(author)
            .groupBy(author.gender)
            .fetch();
    return result;
}</code></pre>
<pre><code class="language-java">SELECT
    author.gender,
    COUNT(author.id),
    AVG(author.age)
FROM
    author
GROUP BY
    author.gender;</code></pre>
<p> </p>
<ul>
<li>groupBy(컬럼1, 컬럼2..) : 해당 컬럼을 기준으로 그룹화한다.</li>
</ul>
<p> 
 </p>
<pre><code class="language-java">public List&lt;Tuple&gt; findAuthorGroupBy() {
    List&lt;Tuple&gt; result = queryFactory
            .select(author.organization.orgName, author.count(), author.age.avg())
            .from(author)
            .groupBy(author.organization.id)
            .having(author.age.avg().gt(10))
            .fetch();
    return result;
}</code></pre>
<pre><code class="language-java">SELECT
    author_organization.org_name,
    COUNT(author.id),
    AVG(author.age)
FROM
    author
JOIN
    organization AS author_organization ON author.organization_id = author_organization.id
GROUP BY
    author.organization_id
HAVING
    AVG(author.age) &gt; 10;</code></pre>
<p> </p>
<ul>
<li>having(조건) : 특정 조건을 만족하는 그룹을 필터링한다.</li>
<li>해당 예시는 연관된 엔티티의 컬럼을 기준으로 GroupBy를 수행한 예시이다.</li>
</ul>
<p> </p>
<p>출처: <a href="https://ssow93.tistory.com/60">https://ssow93.tistory.com/60</a> [soTech:티스토리]
출처: <a href="https://sjh9708.tistory.com/175">https://sjh9708.tistory.com/175</a> [데굴데굴 개발자의 기록:티스토리]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 낙관적 락, 비관적 락에 대해]]></title>
            <link>https://velog.io/@take_the_king/JPA-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%97%90-%EB%8C%80%ED%95%B4</link>
            <guid>https://velog.io/@take_the_king/JPA-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-%EB%B9%84%EA%B4%80%EC%A0%81-%EB%9D%BD%EC%97%90-%EB%8C%80%ED%95%B4</guid>
            <pubDate>Sun, 29 Dec 2024 06:49:22 GMT</pubDate>
            <description><![CDATA[<h1 id="낙관적-락-비관적-락에-대해">낙관적 락, 비관적 락에 대해</h1>
<p>JPA를 사용하여 데이터베이스와 연결된 애플리케이션을 개발할 때, 동시성 처리와 관련된 이슈가 발생할 수 있다. 이러한 이슈를 해결하기 위한 방법 중 하나는 락(lock)을 사용하는 것이다. 이번 포스팅에는 낙관적 락과 비관적 락에 대해 알아보고, 예제코드와 이를 사용하는 이유 및 장단점을 함께 써보겠다.</p>
<h2 id="낙관적-락optimistic-lock">낙관적 락(Optimistic Lock)</h2>
<p>낙관적 락은 충돌이 거의 발생하지 않을 것이라고 가정하고, 충돌이 발생한 경우에 대비하는 방식이다. 낙관적 락은 JPA에서 버전(Version) 속성을 이용하여 구현할 수 있다. 낙관적 락의 특징으로는 충돌 발생확률이 낮고, 지속적인 락으로 인한 성능저하를 막을 수 있다.아래는 예시 코드이다.</p>
<p><strong>1. Entity</strong></p>
<pre><code class="language-java">@Entity
public class SampleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Version
    private Long version;

    private String data;
}</code></pre>
<p><strong>2. Repository</strong></p>
<pre><code class="language-java">@Repository
public interface SampleEntityRepository extends JpaRepository&lt;SampleEntity, Long&gt; {
}</code></pre>
<p><strong>3. Service</strong></p>
<pre><code class="language-java">@Service
public class SampleEntityService {
    @Autowired
    private SampleEntityRepository sampleEntityRepository;

    public SampleEntity updateData(Long id, String newData) {
        SampleEntity sampleEntity = sampleEntityRepository.findById(id).orElseThrow();
        sampleEntity.setData(newData);
        return sampleEntityRepository.save(sampleEntity);
    }
}</code></pre>
<p>작동원리로는 SampleEntity 클래스에 @Version 어노테이션을 이용하여 version 필드로 지정한다. 데이터를 수정할 때 같은 id 값이지만 다른 사용자에 의한 변경이 발생하면 version 값이 다르게 되고, 이때 예외가 발생하므로 충돌로부터 안전하게 처리할 수 있다.</p>
<h3 id="예제">예제</h3>
<p>@Version Annotation
JPA에서 version 속성을 정의할때 지켜야하는 몇가지 규칙이 있습니다.</p>
<p>각 Entity Class에는 @Version 속성이 하나만 있어야 한다
여러 테이블에 매핑된 Entity의 경우 기본 테이블에 배치되어야 한다
버전에 타입은 int , Integer , long , Long , short , Short , java.sql.Timestamp 중 하나여야 한다
이 field 의 값 혹은 시간이 처음 조회될 때의 버전과 commit될때의 버전이 서로 다르다면 이는 충돌이 발생한 것으로 판단하고 예외를 발생시킵니다.</p>
<p>재고를 차감하는 예를 들어보겠습니다.
치킨A라는 재고는 현재 단 한개가 남아 있습니다.</p>
<blockquote>
</blockquote>
<p>[transaction-1] : 치킨A의 재고를 확인 / 치킨A 재고: 1개, version: 1
[transaction-2] : 치킨A의 재고를 확인 / 치킨A 재고: 1개, version: 1</p>
<blockquote>
</blockquote>
<p>-- 이때 두 트랜잭션 중 transaction-1 가 먼저 완료되었다고 가정해보겠습니다.</p>
<blockquote>
</blockquote>
<p>[transaction-1] : 치킨A를 구매 / 치킨A 재고: 0개, version: 2 로 업데이트하고 커밋
[transaction-2] : 치킨A를 구매 / 치킨A 재고: 0개, version: 2 로 업데이트하고 커밋하려는데 version이 처음 조회했던 1이 아니라 [transaction-1]에서 2로 변경되어 현재 조회한 버전과 다르므로 업데이트 실패</p>
<blockquote>
</blockquote>
<pre><code>update stock
set
    availableStock = ?,
    version = 2
where
    id = ?
    and version = 1</code></pre><p>위와 같은 쿼리가 발생하지만 해당 재고의 version은 transaction-1 으로 인해 이미 2로 증가된 상태입니다. 이때 처음 조회했던 version값인 1을 전달하게 되니 업데이트할 대상을 찾지 못해 예외가 발생합니다.</p>
<h2 id="비관적-락pessimistic-lock">비관적 락(Pessimistic Lock)</h2>
<p>비관적 락은 충돌이 발생할 확률이 높다고 가정하여, 실제로 데이터에 액세스 하기 전에 먼저 락을 걸어 충돌을 예방하는 방식이다. 비관적 락은 JPA에서 제공하는 LockModeType을 사용하여 구현할 수 있다.아래는 예시코드이다1. </p>
<p><strong>Repository</strong></p>
<pre><code class="language-java">@Repository
public interface SampleEntityRepository extends JpaRepository&lt;SampleEntity, Long&gt; {
}
2. Service
@Service
public class SampleEntityService {
    @Autowired
    private SampleEntityRepository sampleEntityRepository;

    @Autowired
    private EntityManager entityManager;

    @Transactional
    public SampleEntity updateDataWithPessimisticLock(Long id, String newData) {
        SampleEntity sampleEntity = entityManager.find(SampleEntity.class, id, LockModeType.PESSIMISTIC_WRITE);
        sampleEntity.setData(newData);
        entityManager.flush();
        return sampleEntity;
    }
}</code></pre>
<p> 
위 코드를 설명해보면 EntityManager의 find 메소드에 락 타입(LockModeType.PESSIMISTIC_WRITE)을 지정하여 데이터에 락을 걸어두고, 변경 작업이 끝난 후에 락을 해제한다. 이를 통해 다른 트랜잭션이 동시에 수정할 수 없어 동시성 처리 이슈를 방지할 수 있게 된다.</p>
<hr>
<h2 id="장단점">장단점</h2>
<p>이제 낙관적 락과 비관적 락에 대해 알아보았으니 이들의 장단점에 대해서도 알아보자</p>
<h3 id="낙관적-락">낙관적 락</h3>
<p>장점: 리소스 경쟁이 적고 락으로 인한 성능저하가 적다.
단점: 충돌 발생 시 처리해야 할 외부 요인이 존재한다.</p>
<h3 id="비관적-락">비관적 락</h3>
<p>장점: 충돌 발생을 미연에 방지하고 데이터의 일관성을 유지할 수 있다.
단점: 동시 처리 성능 저하 및 교착상태(Deadlock) 발생 가능성이 있다.</p>
<p>위에서 알아본 내용처럼 낙관적 락과 비관적 락은 각각의 사용 상황에 따라 선택할 수 있다. 충돌 발생 확률이 낮고 성능 저하를 예방하려면 낙관적 락을 사용하면 되고, 충돌을 미연에 방지하고 데이터의 일관성을 유지하려면 비관적 락을 사용하면 된다. 선택한 락 방식에 따라 JPA를 효과적으로 사용하여 동시성 처리 이슈를 해결할 수 있다.</p>
<p>출처: <a href="https://mozzi-devlog.tistory.com/37">https://mozzi-devlog.tistory.com/37</a> [우당탕탕:티스토리]
출처: <a href="https://devoong2.tistory.com/entry/JPA-%EC%97%90%EC%84%9C-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDOptimistic-Lock%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0">https://devoong2.tistory.com/entry/JPA-%EC%97%90%EC%84%9C-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDOptimistic-Lock%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인덱스 설계 (최적화)]]></title>
            <link>https://velog.io/@take_the_king/%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%84%A4%EA%B3%84-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@take_the_king/%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%84%A4%EA%B3%84-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Fri, 27 Dec 2024 21:06:34 GMT</pubDate>
            <description><![CDATA[<h1 id="최적화-문제">최적화 문제</h1>
<p>Trello에서 멤버는 특정 키워드로 카드를 검색하거나, 프로젝트별로 카드를 필터링하고 있습니다. 하지만 데이터가 대량으로 축적됨에 따라 카드 검색 속도가 느려질 수 있습니다. 카드 검색 속도를 향상시킬 수 있는 최적화 방법을 적용해주세요. </p>
<p><strong>요구사항</strong></p>
<ul>
<li>카드 검색 속도를 향상시키기 위해 적절한 인덱스를 설계해주세요.</li>
<li>자주 사용되는 조건에 맞는 컬럼에 인덱스를 적용하여 대용량 데이터에서도 빠른 검색을 지원하세요.</li>
</ul>
<p><strong>발표 자료 준비</strong></p>
<ul>
<li>개선 대상 쿼리와 해당 쿼리를 선택한 이유</li>
<li>인덱스 설정 DDL 쿼리</li>
<li>쿼리 속도 비교(before, after)</li>
</ul>
<hr>
<h1 id="1-카드-도메인의-쿼리-3가지">1. 카드 도메인의 쿼리 3가지</h1>
<p>카드 도메인에서 발생하는 쿼리는 3가지이다.</p>
<pre><code class="language-java">// 1번
Card card = cardRepository.findByIdOrElseThrow(cardId);

// 2번
List&lt;Card&gt; cardList = cardRepository.findAllByBoardListId(listId);

// 3번
List&lt;Card&gt; cards = cardRepository.findAllSearchByConditions(pageable,
                cardSearchRequestDto.getBoardId(),
                cardSearchRequestDto.getCardTitle(),
                cardSearchRequestDto.getCardExplanation(),
                cardSearchRequestDto.getEndAt(),
                cardSearchRequestDto.getCardManagerName()
        );</code></pre>
<ul>
<li>3번 쿼리 (QueryDsl)<pre><code class="language-java">queryFactory.selectFrom(card)
              .leftJoin(card.boardList, boardList)
              .leftJoin(card.cardManagers, cardManager)
              .leftJoin(cardManager.user, user)
              .fetchJoin()
              .where(
                      boardIdEq(boardId),
                      cardTitleEq(cardTitle),
                      cardExplanationEq(cardExplanation),
                      endAtEq(endAt),
                      cardManagerNicknameEq(cardMangerName)
              )
              .offset(pageable.getOffset())
              .limit(pageable.getPageSize())
              .orderBy(card.modifiedAt.desc())
              .fetch();</code></pre>
</li>
</ul>
<h2 id="1번-쿼리">1번 쿼리</h2>
<p>1번 쿼리는 PK 컬럼으로 조회하기 때문에 이미 존재하는 인덱스를 통해 검색할 것이다.</p>
<h2 id="2번-쿼리">2번 쿼리</h2>
<p>2번 쿼리는 사용자가 보드에 접속할 때마다 사용되는 쿼리로 굉장히 자주 사용될 것이다. 그리고 listId 컬럼은 card 테이블에서 중복도가 낮은 컬럼이고 수정될 가능성이 낮은 컬럼이기 때문에 인덱스를 적용하기 적합하다.</p>
<h2 id="3번-쿼리">3번 쿼리</h2>
<p>3번 쿼리는 여러 조건을 통해 부합하는 데이터를 조회하는 쿼리로 각 조건들은 null일 수도 있다. 
일반적으로 카드를 조회하는 대표적인 조건으로는</p>
<blockquote>
</blockquote>
<p>1) 제목과 내용에 어떤 단어나 문장이 포함되어있는 지
2) 오늘까지 마감일인 카드가 있는 지
3) 특정 보드 안에 있는 모든 카드를 검색할 때 </p>
<p>이라고 생각한다.</p>
<p>1번에서는 내용의 경우는 수정이 아주 빈번하게 발생할 가능성이 높기 때문에 오히려 성능 저하를 일으킬 수 있다. 왜냐하면 수정을 할 때 인덱스에도 추가 작업을 수행해야하기 때문이다.
그래서 1번에서는 제목에만 인덱스를 적용하면 좋을 것이다.</p>
<p>2번은 수정의 빈도수가 적당하고, 조건으로 자주 사용될 것이라 생각하기 때문에 인덱스를 적용하면 좋을 것이다.</p>
<p>3번은 어차피 보드에 진입시 볼 수 있기 때문에, 특정 보드 조건은 다른 조건과 함께 사용될 가능성이 높다.</p>
<h1 id="2-인덱스-설정-및-데이터-셋팅">2. 인덱스 설정 및 데이터 셋팅</h1>
<h2 id="1-jpa-엔티티에-설정">1) JPA 엔티티에 설정</h2>
<pre><code class="language-java">@Getter
@Entity
@Table(name = &quot;card&quot;, indexes = {
        @Index(name = &quot;idx_list_Id&quot;, columnList = &quot;list_id&quot;),
        @Index(name = &quot;idx_endAt&quot;, columnList = &quot;end_at&quot;),
        @Index(name = &quot;idx_card_title&quot;, columnList = &quot;card_title&quot;)
//        @Index(name = &quot;idx_card_title_card_explanation&quot;, columnList = &quot;card_title, card_explanation&quot;)
})
public class Card extends BaseEntity {

...

}</code></pre>
<h2 id="2-인덱스-설정-ddl-쿼리">2) 인덱스 설정 DDL 쿼리</h2>
<pre><code class="language-sql">-- 리스트 식별자(list_id)에 대한 인덱스 생성
CREATE INDEX idx_list_Id ON card (list_id);

-- 마감일(end_at)에 대한 인덱스 생성
CREATE INDEX idx_endAt ON card (end_at);

-- 카드 제목(card_title)에 대한 인덱스 생성
CREATE INDEX idx_card_title ON card (card_title);

-- 카드 제목(card_title)과 카드 설명(card_explanation)에 대한 복합 인덱스
-- CREATE INDEX idx_card_title_card_explanation ON card (card_title, card_explanation);
</code></pre>
<h2 id="3-데이터-셋팅">3) 데이터 셋팅</h2>
<p> -데이터 10만 건을 셋팅한다.</p>
<pre><code class="language-java">@Component
public class CardInitConfig {

    private final String[] array1 = {&quot;바나나&quot;, &quot;사과&quot;, &quot;귤&quot;, &quot;포도&quot;, &quot;키위&quot;, &quot;멜론&quot;, &quot;두리안&quot;, &quot;배&quot;, &quot;감&quot;, &quot;망고&quot;};
    private final String[] array2 = {&quot;하나&quot;, &quot;둘&quot;, &quot;셋&quot;, &quot;넷&quot;, &quot;다섯&quot;, &quot;여섯&quot;, &quot;일곱&quot;, &quot;여덟&quot;, &quot;아홉&quot;, &quot;열&quot;};
    private final String[] colors = {&quot;빨강&quot;, &quot;주황&quot;, &quot;노랑&quot;, &quot;초록&quot;, &quot;파랑&quot;, &quot;남색&quot;, &quot;보라&quot;, &quot;검정&quot;, &quot;흰색&quot;, &quot;회색&quot;};

    private final WorkspaceRepository workspaceRepository;
    private final BoardRepository boardRepository;
    private final BoardListRepository boardListRepository;
    private final CardRepository cardRepository;


    public CardInitConfig(WorkspaceRepository workspaceRepository, BoardRepository boardRepository, BoardListRepository boardListRepository, CardRepository cardRepository) {
        this.workspaceRepository = workspaceRepository;
        this.boardRepository = boardRepository;
        this.boardListRepository = boardListRepository;
        this.cardRepository = cardRepository;
    }

    @PostConstruct
    @Transactional
    public void init() {

        // 워크스페이스 생성
        Workspace workspace = workspaceRepository.save(new Workspace(&quot;워크스페이스1&quot;, &quot;환영&quot;));

        // 리스트 생성
        List&lt;Board&gt; boards = new ArrayList&lt;&gt;();
        for (int i=0; i&lt;1000; i++){
            for (int j=0; j&lt;10; j++){
                Board board = new Board(workspace, &quot;보드&quot;, &quot;#000000&quot;, &quot;image.jpg&quot;);
                boards.add(board);
            }

        }
        boardRepository.saveAll(boards);


        // 리스트 생성
        List&lt;BoardList&gt; boardLists = new ArrayList&lt;&gt;();
        for (int i=0; i&lt;100; i++){
            for (int j=0; j&lt;10; j++){
                BoardList boardList = new BoardList(boards.get(i*10+j), &quot;리스트&quot;+i+array1[j]);
                boardList.addArrayNumber(i);
                boardLists.add(boardList);
            }

        }
        boardListRepository.saveAll(boardLists);

        // 카드 생성
        List&lt;Card&gt; cardList = new ArrayList&lt;&gt;();
        for(int i=0; i&lt;10; i++){
            for (int j=0; j&lt;10; j++){
                for(int k=0; k&lt;100; k++){
                    for (int j2=0; j2&lt;10; j2++){
                        // 카드 생성
                        Card card = new Card(&quot;제목 &quot;+ array1[i] + array2[j]+colors[j2],
                                &quot;내용 &quot;+ array1[i] + array2[j],
                                LocalDate.parse(&quot;2024-01-01&quot;).plusYears(i).plusMonths(j).plusDays(j2)
                        );
                        card.addBoardList(boardLists.get(k*10+j2));

                        // 카드 리스트에 추가
                        cardList.add(card);
                    }
                }

            }
        }
        cardRepository.saveAll(cardList);
    }

}</code></pre>
<h1 id="3-최적화-시도">3. 최적화 시도</h1>
<h2 id="1-fk도-자동-인덱스-적용">1) FK도 자동 인덱스 적용</h2>
<p>2번 쿼리와 3번 쿼리의 3) 조건은 각 테이블의 FK로 설정되어있는 컬럼을 조건으로 조회하는데 FK는 mysql에서 기본적으로 인덱스를 적용하기 때문에 최적화가 이미 되어있다고 할 수 있다.</p>
<pre><code class="language-sql">// 2번 쿼리
SELECT * FROM card WHERE list_id = 2;

// 3번의 3번째 조건 쿼리
SELECT * FROM card c JOIN board_list bl on c.list_id = bl.id WHERE bl.board_id = 93;</code></pre>
<h2 id="2-제목과-내용-기준-최적화">2) 제목과 내용 기준 최적화</h2>
<pre><code class="language-sql">SELECT * FROM card WHERE card_title LIKE &#39;%바나나하나회색%&#39;;</code></pre>
<p>위의 쿼리를 통해 조회했을 때 인덱스가 있을 때와 차이가 나지 않았고 어떤 경우에는 더 많은 시간이 소요되기도 했다. 이는 내용이 많고 수정이 자주 일어나는 컬럼이고 LIKE 문으로 조건을 확인하기 때문이라 생각된다.</p>
<ul>
<li><p>제목 인덱스 적용 전
<img src="https://velog.velcdn.com/images/take_the_king/post/ebc24ced-04de-4e72-9194-01000326d118/image.PNG" alt=""></p>
</li>
<li><p>제목 인덱스 적용 후
<img src="https://velog.velcdn.com/images/take_the_king/post/a5ac268a-23c2-469d-9ea0-fc440d686d27/image.PNG" alt=""></p>
</li>
</ul>
<pre><code class="language-sql">SELECT * FROM card WHERE card_title LIKE &#39;%바나나둘회색%&#39; AND  card_explanation LIKE &#39;%바나나%&#39;;</code></pre>
<p>마찬가지로 제목과 내용을 조건으로 적용해도 차이가 발생하지 않았다.</p>
<ul>
<li>제목과 내용 인덱스 적용 전
<img src="https://velog.velcdn.com/images/take_the_king/post/e1c7f9e5-9c25-4808-988c-b560188755d7/image.PNG" alt=""></li>
</ul>
<ul>
<li>제목과 내용 인덱스 적용 후
<img src="https://velog.velcdn.com/images/take_the_king/post/edf0cf0f-b6b3-44db-8681-befc5198cfb6/image.PNG" alt=""></li>
</ul>
<h2 id="3-마감일-기준-최적화">3) 마감일 기준 최적화</h2>
<pre><code class="language-sql">SELECT * FROM card WHERE end_at = &#39;2024-08-01&#39;;</code></pre>
<p>위의 쿼리를 통해 조회했을 때는 인덱스의 유무에 따라 엄청난 차이가 발생했다. </p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/bbba6f2e-e5b9-4188-924d-4dc2d0ec61e8/image.PNG" alt=""></p>
<p><img src="https://velog.velcdn.com/images/take_the_king/post/b8485309-c7a7-4898-889c-8fb042725496/image.PNG" alt=""></p>
<h1 id="4-결론">4. 결론</h1>
<p>PK와 FK는 mysql 에서 자동으로 인덱스를 적용하여 최적화한다. 
내용이 길고 수정이 많은 문자열로 이루어진 컬럼에 LIKE 문을 적용하면 오히려 성능저하가 올 수 있다.
딱 떨어지는 문자나 특정 타입의 컬럼에 인덱스를 적용하면 최적화를 할 수 있다.
오히려 많은 수의 컬럼을 인덱스에 적용하려고 하면 인덱스 테이블이 거대해져서 많은 저장공간을 차지한다.
그리고 성능도 떨어진다.
인덱스가 적용된 컬럼에 INSERT, UPDATE, DELETE가 수행된다면 원본 테이블 뿐만 아니라 
인덱스에도 다음과 같이 추가 작업을 수행해줘야 하기 때문에 3가지 쿼리가 자주 발생하는 컬럼은 적용하면 안된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA 데이터 일괄 수정하는 법]]></title>
            <link>https://velog.io/@take_the_king/JPA-%EC%9D%BC%EA%B4%84-%EC%88%98%EC%A0%95%ED%95%98%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@take_the_king/JPA-%EC%9D%BC%EA%B4%84-%EC%88%98%EC%A0%95%ED%95%98%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Fri, 13 Dec 2024 19:55:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>intro</strong>
프로젝트를 하는 과정에서 많은 데이터의 상태를 모두 변경해야되는 문제에 직면했다.</p>
</blockquote>
<p>예를 들어, 사람들이 신고한 사용자의 id를 하루동안 받았다가 한 번에 처리할 때나 쿠폰이 만료 기간이 지나서 한번에 상태를 만료로 변경해야 할 때가 있다.</p>
<h2 id="문제-발생">문제 발생</h2>
<blockquote>
<p>수 백만 건의 데이터를 수정하려니 수백 만 개의 sql문이 데이터베이스에 날아가는 문제가 발생했다.</p>
</blockquote>
<p>보통은 데이터를 수정할 때, 데이터베이스에서 특정 데이터를 가져와서 해당 데이터의 상태를 변경하고 저장하는 단건 수정 방식을 사용한다. 그런데 이 방법을 쓰면 수정하려는 데이터가 수백만, 수천만 개가 되면 데이터베이스가 마비될 것이다.</p>
<pre><code class="language-java">@Transactional
    public void reportUsers(List&lt;Long&gt; userIds) {
        for (Long userId : userIds) {
            User user = userRepository.findById(userId).orElseThrow(() -&gt; new IllegalArgumentException(&quot;해당 ID에 맞는 값이 존재하지 않습니다.&quot;));

            user.updateStatusToBlocked();

            userRepository.save(user);
        }
    }</code></pre>
<p>위에 경우에 백만 개를 처리한다고 하면 백만 번의 select문 처리와 백만 번의 update문 처리가 이루어져야 된다. </p>
<p>그래서 위 문제를 해결하기 위해 한 번의 데이베이스 접근으로 모두 수정하는 방법을 찾아보았다.</p>
<h2 id="해결방법">해결방법</h2>
<blockquote>
<p>벌크 연산을 사용하자!</p>
</blockquote>
<p>벌크 연산은 데이터베이스에서 UPDATE, DELETE 시 대량의 데이터를 한 번에 처리하기 위한 작업이다. 
즉, JPA에서 벌크 연산은 단 건 데이터를 변경(더티 체킹)하는 것이 아닌, 여러 데이터에 변경 쿼리를 날리는 작업을 말한다.</p>
<p>@Modifying을 변경이 일어나는 쿼리와 함께 사용해야 JPA에서 변경 감지와 관련된 처리를 생략하고 더 효율적인 실행이 가능하다.</p>
<pre><code class="language-java">@Modifying(clearAutomatically = true)
@Query(&quot;UPDATE User u SET u.status = :status where u.id IN :userIds&quot;)
void updateStatusByIds(@Param(&quot;status&quot;) String status, @Param(&quot;userIds&quot;) List&lt;Long&gt; userIds);</code></pre>
<p>where ... in ... 조건으로 수정할 데이터들을 정하고 set 으로 수정할 내용으로 수정한다.
여기서 중요한 것은 Modifying 어노테이션이다.</p>
<p>JPA에서는 원래 수정할 때 영속성 컨텍스트가 변화를 감지(1차 캐시와 비교)하여 업데이트를 데이터베이스에 보내게 되는데, 위의 경우는 직접 쿼리문을 보내기 때문에 영속성 컨텍스트와 데이터베이스의 정보가 일치하지 않는다.
그래서 잘못해서 영속성 컨텍스트에 있는 데이터를 조회하면 실제 데이터와 다른 정보를 얻게 되는 불상사가 생기게 된다.</p>
<p>Modifying 어노테이션은 그래서 영속성 컨텍스트를 초기화하는 방법을 제공하는데 (clearAutomatically = true)라는 옵션을 통해 초기화할 수 있게 해준다.</p>
<pre><code class="language-java">...

entityManager.flush();
entityManager.clear();</code></pre>
<p>기본적으로는 영속성 컨텍스트를 초기화 해주는 방법이 있다.</p>
<pre><code class="language-java">@Transactional
    public void reportUsers(List&lt;Long&gt; userIds) {

//        List&lt;User&gt; users = userRepository.findAllById(userIds).stream().toList();
//
//        if(users.isEmpty()) {
//            throw new IllegalArgumentException(&quot;해당 ID에 맞는 값이 존재하지 않습니다.&quot;);
//        }

        // 위의 user 조회가 없어도 동작함
        // 해당 id의 레코드가 없어도 실행됨
        userRepository.updateStatusByIds(&quot;BLOCKED&quot;, userIds);
    }</code></pre>
<p>이 위의 코드는 한 번의 select문과 한 번의 update문으로 많은 데이터를 한 번씩의 접근으로 해결했다.</p>
<h2 id="주의점">주의점</h2>
<h3 id="변경-쿼리-동기화-문제">변경 쿼리 동기화 문제</h3>
<p>JPA에서는 1차 캐시라는 기능이 있다.
1차 캐시를 간단하게 설명하면 영속성 컨텍스트에 있는 1차 캐시를 통해 엔티티를 캐싱하고, DB의 접근 횟수를 줄임으로써 성능 개선 한다.</p>
<p>그런데 @Modifying과 @Query 를 사용한 벌크 연산에서 1차 캐시와 관련하여 문제가 발생한다.
JPA에서 조회를 실행할 시에 1차 캐시를 확인해서 해당 엔티티가 1차 캐시에 존재한다면 DB에 접근하지 않고, 1차 캐시에 있는 엔티티를 반환한다.
하지만 벌크 연산은 1차 캐시를 포함한 영속성 컨텍스트를 무시하고 바로 Query를 실행하기 때문에 영속성 컨텍스트는 데이터 변경을 알 수가 없다.
즉, 벌크 연산 실행 시, 1차 캐시(영속성 컨텍스트)와 DB의 데이터 싱크가 맞지 않게 되는 것이다.</p>
<p>그래서 데이터를 사용하기 전에 영속성 컨텍스트를 비워주는 작업이 필요한데,
@Modifying의 clearAutomatically=true 속성을 사용해 변경 후 자동으로 영속성 컨텍스트를 초기화 할 수 있다. 
해당 속성을 추가하게 되면, 조회를 실행할 때 1차캐시에 해당 엔티티가 존재하지 않기 때문에 DB 조회 쿼리를 실행하게 된다. ( 데이터 동기화 문제를 해결 )</p>
<h3 id="트랜잭션-관리">트랜잭션 관리</h3>
<p>@Modifying 애노테이션은 기본적으로 @Transactional과 함께 사용된다.
변경 작업은 트랜잭션 내에서 실행되어야 하며, 완료되지 않은 변경 작업이 여러 작업에 영향을 줄 수 있기 때문이다.
이를 통해 데이터베이스에 대한 변경 작업을 수행할 때 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 지속성(Durability)을 보장할 수 있게 된다.</p>
]]></description>
        </item>
    </channel>
</rss>