<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>diense_kk.log</title>
        <link>https://velog.io/</link>
        <description>개발하다 독거노인 유망주</description>
        <lastBuildDate>Sat, 14 Feb 2026 07:55:50 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. diense_kk.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/diense_kk" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[SQL Tuning Day 7]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-7</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-7</guid>
            <pubDate>Sat, 14 Feb 2026 07:55:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/diense_kk/post/95175559-b34e-4d13-ad08-037cb9a2c371/image.png" alt=""></p>
<h3 id="single-block-io--multi-block-io">Single Block I/O / Multi Block I/O</h3>
<p>기본적으로 데이터들을 Disk에 저장하고, 읽어 올 때 내부 버퍼 캐시에 데이터가 없는 경우 Disk I/O를 발생시켜 데이터를 일거온 뒤 이를 DB 내부 버퍼 캐시에 적재하여 다양한 처리를 진행한다.
이때 Disk I/O의 최소 단위가 되는 것이 Block이다.
디스크에서 Block 단위로 데이터를 읽어와서 버퍼 캐시에 적자하는 과정을 Block I/O라 칭한다.</p>
<p>Oracle을 포함한 모든 DBMS의 I/O는 블록 단위로 이루어진다.
하나의 레코드를 읽더라도 레코드가 속한 블록 전체를 읽는 것인데, 그렇기 때문에 가장 중요한 성능 지표는 액세스하는 블록의 개수이며 옵티마이저 판단에 가장 큰 영향을 미친다.</p>
<h4 id="single-block-io">Single Block I/O</h4>
<p>한 번에 한 블록씩 요청해서 메모리에 적재
인덱스가 보통 Single Block I/O 방식으로 디스크에 접근하여 데이터를 가져온다.</p>
<ol>
<li>Index Range Scan, </li>
<li>Index Full Scan(인덱스를 통해 추가적인 테이블 스캔 소요가 있는 경우에 한함), </li>
<li>Index Unique Scan</li>
<li>Index Skip Scan
위 4개의 스캔 방식들의 공통점은 인덱스의 논리적 구조에 의존해 정렬된 인덱스를 순차적으로 탐색하는 스캔 방식이라는 점이다.</li>
</ol>
<p>인덱스는 항상 정렬된 상태를 유지하는데, 여기에서 나타나는 정렬의 개념이 특정 블록 레벨에서는 무의미해진다. (논리적 정렬 구조 != 물리적 구조)</p>
<p>즉, 하나의 인덱스 구조에 대한 요소들은 여러 Block에 나위어 저장되고 이는 인덱스가 논리적으로 정렬된 순서와 상관이 없다는 의미다.</p>
<p>따라서 논리적으로 정렬된 인덱스 구조를 따라 탐색을 진행하려면 Single Block I/O를 사용해야된다.</p>
<h4 id="single-block-io의-한계">Single Block I/O의 한계</h4>
<p>Single Block I/O 방식을 통해 읽어내야 할 데이터의 수가 많다면, 호출 건당 비효윻이 누적된다.
따라서 읽어내야 할 데이터가 전체 데이터에 비해 꽤 많다면 해당 방식을 사용하는 것이 오히려 더 느릴 수 있다.</p>
<p>인덱스를 이용한 스캔이 모든 경우에 대해 더 빠른 선택지가 아닌 이유가 바로 이 것이다.</p>
<p>이런 경우에는 Multi Block I/O를 통해 호출을 줄이는 것이 더 나은 선택지가 된다.</p>
<p>인덱스를 활용할 수 있는 경우에 옵티마이저가 전체 테이블을 읽는 이유이다.</p>
<h4 id="index-fast-full-scan">Index Fast Full Scan</h4>
<p>Index Fast Full Scan 방식은 인덱스를 활용하면서도 Multi Block I/O 방식을 사용한다.</p>
<p>인덱스의 논리적 정렬 구조를 완전히 무시한 채, 여러 Block을 한 번에 버퍼에 가져와 일거내는 방식이다.</p>
<p>정렬 구조를 무시하기 때문에 버퍼를 읽어왔을 땐 정렬 상태가 유지되지 않는다.</p>
<p>정렬이 유지되지 않는 인덱스 스캔이 무슨 의미일까 싶지만, Query에 포함된 모든 칼럼이 인덱스에 존재하는 경우 이를 Multi Block I/O 방식으로 읽어올 수 있기 때문에 활용되는 방식이다.</p>
<h3 id="multi-block-io">Multi Block I/O</h3>
<p>I/O Call이 발생한 시점에 인접한 블록들을 같이 읽어 메모리에 적재한다.
Full Scan과 같이 저장된 순서에 따라 읽을 때는 허용하는 범위 내에서 인접한 블록을 읽는 것이 유리하다.
인접한 블록은 한 Extent 범위 내의 블록을 말한다. 즉, Multi Block I/O를 하더라도 Extent의 범위를 넘지 못한다.</p>
<h4 id="remark">Remark</h4>
<p>Q. 인덱스를 사용해 조회할 데이터의 데이터 셋들의 ROWID 목록을 가지고 Multi Block I/O를 통해 한 번에 여러 블록을 읽어올 수는 없을까?
A. 인덱스를 통해 얻은 ROWID는 논리적으로 정렬되어 있지만, 물리적으로는 서로 다른 파일/Extent/블록에 산개되어 있다.
Multi Block I/O는 한 번의 I/O Call로 물리적으로 연속된 블록만 읽을 수 있기 때문에, 이러한 산개된 ROWID들을 하나의 Multi Block I/O로 묶는 것은 구조적으로 불가능하다.
따라서 인덱스 기반 테이블 접근은 Single Block I/O(랜덤 I/O) 방식으로 수행한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 6]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-6</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-6</guid>
            <pubDate>Sat, 07 Feb 2026 05:07:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/diense_kk/post/84bf7e20-23f9-499f-af65-f928134211f0/image.png" alt=""></p>
<h3 id="1-index-skip-scan-활용">1. Index Skip Scan 활용</h3>
<p>BETWEEN 조건을 IN-List 조건으로 변환하면 도움이 되는 상황에서 굳이 조건절을 바꾸지 않고도 Index Skip Scan을 사용하여 같은 효과를 낼 수 있다.</p>
<pre><code>CREATE TABLE 월별고객별판매집계
AS
SELECT rownum 고객번호
    , &#39;2026&#39; || lpad(ceil(rownum/100000), 2, &#39;0&#39;) 판매월
    , decode(mod(rownum, 12), 1, &#39;A&#39;, &#39;B&#39;) 판매구분
    , round(dbms_random.value(1000,100000), -2) 판매금액
    from dual
connect by level &lt;= 1200000;</code></pre><p>월별 10만개 판매 데이터를 생성했다.</p>
<pre><code>SELECT COUNT(*)
    FROM 월별고객별판매집계
WHERE 판매구분 = &#39;A&#39;
    AND 판매월 BETWEEN &#39;202601&#39; AND &#39;202612&#39;</code></pre><p>이 쿼리를 최적으로 수행하려면 &#39;=&#39; 조건인 판매구분이 선두컬럼에 위치하도록 인덱스를 구성해야 된다.</p>
<pre><code>CREATE INDEX 월별고객별판매집계_IDX1 ON 월별고객별판매집계(판매구분, 판매월);</code></pre><p>IDX1을 사용하면 인덱스를 스캔하면서 281개의 블록 I/O가 발생했다.</p>
<pre><code>CREATE INDEX 월별고객별판매집계_IDX2 ON 월별고객별판매집계(판매월, 판매구분);</code></pre><p>판매구분 = &#39;A&#39;인 레코드는 각 판매월 앞쪽에 위치하며, 전체에서 8.3%(=10/120)에 불과하므로 서로 멀리 떨어지게 된다.
IDX2를 사용하면 3090개 블록I/O가 발생한다.</p>
<p>테이블을 전혀 방문하지 않았는데도 I/O가 많이 발생한 이유는 인덱스 선두 컬럼이 BETWEEN 조건이어서 판매구분이 &#39;B&#39;인 레코드가지 모두 스캔하고서 버렸기 때문이다.</p>
<p>다시 BETWEEN 조건을 IN-LIST로 전환해보자</p>
<pre><code>WHERE 판매월 IN (&#39;202601&#39;, &#39;202602&#39;, &#39;202603&#39;, &#39;202604&#39;, &#39;202605&#39;, &#39;202606&#39;
                , &#39;202607&#39;, &#39;202608&#39;, &#39;202609&#39;, &#39;202610&#39;, &#39;202611&#39;, &#39;202612&#39;)</code></pre><p>3090개이던 블록 I/O가 314개로 감소하였다.
인덱스 브랜치 블록을 열두번 반복 탐색했지만, 리프 블록을 스캔할 때의 비효율을 제거함으로써 성능이 열 배 좋아졌다.</p>
<p>다시 WHERE 조건을 BETWEEN으로 수정 후, INDEX_SS(INDEX SKIP SCAN)을 사용하여 조회했을 때, 큰 비효율 없이 블록I/O가 300개로 감소했다.</p>
<p>선두컬럼이 BETWEEN이고, 나머지 검색 조건을 만족하는 데이터들이 서로 멀리 떨어져 있을 때 Index Skip Scan은 성능이 좋다.</p>
<h3 id="in-조건은-인가">IN 조건은 &#39;=&#39;인가</h3>
<p>인덱스 구성 1. [상품ID + 고객번호]
인덱스 구성 2. [고객번호 + 상품ID]
두 구성이 차이가 있는가?</p>
<pre><code>SELECT *
    FROM 고객별가입상품
WHERE 고객번호 = CustSeq
    AND 상품ID IN (&#39;CNC102&#39;, &#39;CNC103&#39;, &#39;CNC104&#39;)</code></pre><p>고객별가입상품 테이블에서 고객번호의 평균 카디널리티는 3이라고 가정한다.
만약 인덱스 구성이 상품ID + 고객번호로 구성돼 있다면, 상품은 고객번호 순으로 정렬된 상태이다.
그렇다면 상품ID 조건절이 IN-List Iterator 방식으로 풀리는 것이 효과적이다.
고객번호 = 1 조건을 만족하는 레코드가 서로 멀리 떨어져 있기 때문이다.</p>
<pre><code>SELECT *    
    FROM 고객별가입상품
WHERE 고객번호 = 1
    AND 상품ID = &#39;CNC102&#39;
UNION ALL
SELECT *    
    FROM 고객별가입상품
WHERE 고객번호 = 1
    AND 상품ID = &#39;CNC103&#39;
UNION ALL
SELECT *    
    FROM 고객별가입상품
WHERE 고객번호 = 1
    AND 상품ID = &#39;CNC104&#39;</code></pre><p>위 코드는 수직적 탐색 3번으로 총 아홉개 블록을 읽는다.
(상품ID를 기준으로 수직적 탐색 3번, 고객번호 1을 찾는 과정 3번으로 3X3)</p>
<p>상품ID가 인덱스 선두 컬럼인 상황에서 IN-LIST ITERATOR 방식으로 풀지 않으면, 상품ID는 필터 조건이므로 테이블 전체 또는 인덱스 전체를 스캔하면서 필터링해야 된다.</p>
<p>이번에는 인덱스 구성 2. [고객번호 + 상품ID]이다.
이러한 인덱스 구성에서 IN-LIST ITERATOR 방식을 사용하면 비효율적이다. 고객번호를 기준으로 상품ID들이 모여있어서 수직적 탐색을 1번(또는 2번)만 하면 되는데 3번의 수직적 탐색을 해야되기 때문이다.</p>
<p>고객번호 1이 한 블록에 모여 있다면, 블록I/O는 수직적 탐색 과정을 포함해 총 3번만 발생한다.</p>
<p>그렇기 때문에 IN조건은 &#39;=&#39;이 아니다.
IN조건이 &#39;=&#39;이 되려면 IN-LIST ITERATOR 방식으로 풀려야만 한다. 그렇지 않으면 IN 조건은 필터 조건이다. </p>
<h3 id="between과-like-스캔-범위-비교">BETWEEN과 LIKE 스캔 범위 비교</h3>
<p>범위 조건을 사용할 때 BETWEEN보다 LIKE를 많이 사용하게 된다.
결론부터 말하면 LIKE보다는 BETWEEN을 사용하는게 낫다.</p>
<p><strong>인덱스 구성 [판매월 + 판매구분]</strong></p>
<pre><code>조건절 1.
WHERE 판매월 BETWEEN &#39;202601&#39; AND &#39;202612&#39;
    AND 판매구분 = &#39;B&#39;
조건절 2.
WHERE 판매월 LIKE &#39;2026%&#39;
    AND 판매구분 = &#39;B&#39;</code></pre><p>위 코드에서 조건절 1은 판매월 = &#39;202601&#39;이고 판매구분 &#39;B&#39;인 첫 번째 레코드에서 스캔을 시작한다.
반면, 조건절 2는 판매월 = &#39;202601&#39;인 첫 번째 레코드에서 스캔을 시작한다.
혹시라도 202600이 저장돼 있다면 해당 레코드도 읽어야 되기 때문에 판매구분 = &#39;B&#39;인 지점으로 바로 내려갈 수 없다.
또한 &#39;202613&#39; 값이 저장돼 있다면 그 값도 읽어야 하므로 중간에 멈출 수 없다.</p>
<h3 id="범위검색-조건을-남용할-때의-비효율">범위검색 조건을 남용할 때의 비효율</h3>
<p>가입상품 테이블에 인덱스 구성을 [회사코드 + 지역코드 + 상품명] 이렇게 구성하였다.
이때 사용자가 데이터 조회를 위해 회사코드 + 지역코드 + 상품명을 입력할 수도 있고, 회사코드 + 상품명만을 이용해서 데이터 조회할 수도 있다.</p>
<pre><code>쿼리 1)
SELET *
    FROM 가입상품
WHERE 회사코드 = CompanySeq
    AND 지역코드 = RegionCd
    AND 상품명 LIKE ProdNm + &#39;%&#39;
쿼리 2)
SELCT *
    FROM 가입상품
WHERE 회사코드 = CompanySeq
    AND 상품명 LIKE ProdNm + &#39;%&#39;</code></pre><p>인덱스 중간 컬럼에 대한 조건이 없는 쿼리 2는 어쩔 수 없이 넓은 범위를 스캔하지만,
쿼리 1에서는 세 컬럼 모두 액세스 조건이므로 아주 적은 범위만 스캔하고 빠르게 결과를 출력할 수 있다.</p>
<p>그런데 만약 두 가지 상황을 하나의 SQL로 처리한다면 어떻게 변할까?</p>
<pre><code>SELET *
    FROM 가입상품
WHERE 회사코드 = CompanySeq
    AND 지역코드 = RegionCd + &#39;%&#39;
    AND 상품명 LIKE ProdNm + &#39;%&#39;</code></pre><p>해당 SQL 쿼리를 사용한다면 지역코드를 입력하지 않은 경우는 쿼리2와 동일한 결과를 얻겠지만, 지역코드가 입력된 상황에서는 RegionCd가 &#39;02&#39;인 경우에도 &#39;021&#39;, &#39;022&#39;와 같은 데이터가 있는 것을 염두해두고 인덱스 스캔 범위가 늘어날 것이다. 앞서 액세스 조건이던 상품명이 필터 조건으로 바뀌면서 생긴 변화다.</p>
<p>물론 해당 쿼리를 사용하면 SQL 하나로 모든 상황에 대한 처리를 할 수 있지만, 코딩을 쉽게 하려고 이처럼 인덱스 컬럼에 범위검색을 남용하면 인덱스 스캔 비효율이 생긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 5]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-5</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-5</guid>
            <pubDate>Thu, 25 Dec 2025 05:54:08 GMT</pubDate>
            <description><![CDATA[<h2 id="인덱스-스캔-효율화">인덱스 스캔 효율화</h2>
<h3 id="인덱스-탐색">인덱스 탐색</h3>
<p>인덱스 스캔 효율화 튜닝을 이해하려면 인덱스 수직적 탐색, 수평적 탐색을 깊이있게 이해해야된다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/5e7df2da-67ad-4c84-a270-fb6fa344a41c/image.jpg" alt=""></p>
<p>LMC(Leftmost Child)는 루트 블록에서 키 값을 갖지 않는 특별한 레코드이다.
LMC는 자식 노드 중 가장 왼쪽 끝에 위치한 블록을 가리킨다.
LMC가 가리키는 주소로 찾아간 블록에는 키값을 가진 첫번째 레코드보다 작거나 같은 값을 갖는 레코드가 저장돼 있다.</p>
<p>만약 해당 그림에서 WHERE C1 = &#39;B&#39;의 조건으로 검색한다면 루트 블록 스캔 과정에서 레코드를 찾을 때 그것이 가리키는 리프 블록3으로 내려가면 안된다. 그 직전 C1=&#39;A&#39; 레코드가 가리키는 리프 블록 2로 내려가야 된다. 
수직적 탐색은 스캔 시작점을 찾는 과정이다.</p>
<h3 id="액세스-조건--필터-조건">액세스 조건 &amp; 필터 조건</h3>
<p>인덱스를 스캔하는 단계에 처리하는 조건절은 액세스 조건과 필터 조건으로 나뉜다.</p>
<h4 id="액세스-조건">액세스 조건</h4>
<p>인덱스 스캔 범위를 결정하는 조건절이다.
인덱스 수직적 탐색을 통해 스캔 시작점을 결정하는 데 영향을 미치고, 인덱스 리프 블록을 스캔하다가 어디서 멈출지를 결정하는 데 영향을 미치는 조건절이다.</p>
<h4 id="필터-조건">필터 조건</h4>
<p>테이블로 액세스할지를 결정하는 조건절이다.
인덱스로 구성되지 않은 컬럼
인덱스를 이용하든, 테이블을 Full Scan하든, 테이블 액세스 단계에서 처리되는 조건절은 모두 필터 조건이다. 테이블 필터 조건은 쿼리 수행 다음 단계로 전달하거나 최종 결과 집합에 포함할지를 결정한다.</p>
<blockquote>
<p>옵티마이저의 비용 계산
비용 = 수직적 탐색 비용 + 수평적 탐색 비용 + 테이블 랜덤 액세스 비용 
    (= 루트와 브랙치 레벨에서 읽는 블록 수 + 리프 블록을 스캔하는 과정에 읽는 블록 수 + 테이블 액세스 과정에 읽는 블록 수)</p>
</blockquote>
<h3 id="비교-연산자-종류와-컬럼-순서에-따른-군집성">비교 연산자 종류와 컬럼 순서에 따른 군집성</h3>
<p>테이블과 달리 인덱스에는 같은 값을 갖는 레코드들이 서로 군집해있다.
인덱스 컬럼을 앞쪽부터 누락없이 = 연산자로 조회하면 조건절을 만족하는 레코드는 모두 모여있다.
하지만, 어느 하나를 누락하거나 = 조건이 아닌 연산자로 조회하면 조건절을 만족하는 레코드가 서로 흩어진 상태가 된다.</p>
<p>선행 컬럼이 모두 = 조건인 상태에서 첫 번째 나타나는 범위검색 조건까지만 만족하는 인덱스 레코드는 모두 연속해서 모여있지만, 그 이하 조건까지 만족하는 레코드는 비교 연산자 종류에 상관없이 흩어진다. (우연히 모여있을 수는 있다.)</p>
<pre><code>조건 EX) WHERE C1 = 1 AND C2 = 2 AND C3  = &#39;나&#39; AND C4 = 4</code></pre><p>만약 C3이 &#39;다&#39; 이상인 값이 없다면 모여있고, 그게 아니라면 흩어져 있을 것이다.</p>
<h4 id="조건절">조건절</h4>
<pre><code>1) WHERE C1 = 1 AND C2 = &#39;A&#39; AND C3 = &#39;나&#39; AND C4 = &#39;A&#39;</code></pre><pre><code>2) WHERE C1 = 1 AND C2 = &#39;A&#39; AND C3 = &#39;나&#39; AND C4 &gt;= &#39;A&#39;</code></pre><pre><code>3) WHERE C1 = 1 AND C2 = &#39;A&#39; AND C3 BETWEEN &#39;가&#39; AND &#39;다&#39; AND C4 = &#39;A&#39;</code></pre><pre><code>4) WHERE C1 = 1 AND C2 &lt;= &#39;A&#39; AND C3 = &#39;나&#39; AND C4 BETWEEN &#39;A&#39; AND &#39;B&#39;</code></pre><pre><code>5) WHERE C1 BETWEEN 1 AND 3 AND C2 &lt;= &#39;A&#39; AND C3 = &#39;나&#39; AND C4 = &#39;A&#39;</code></pre><table>
<thead>
<tr>
<th align="left"></th>
<th align="center">액세스 조건</th>
<th align="center">필터 조건</th>
</tr>
</thead>
<tbody><tr>
<td align="left">조건절 1</td>
<td align="center">C1, C2, C3, C4</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">조건절 2</td>
<td align="center">C1, C2, C3, C4</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">조건절 3</td>
<td align="center">C1, C2, C3</td>
<td align="center">C4</td>
</tr>
<tr>
<td align="left">조건절 4</td>
<td align="center">C1, C2</td>
<td align="center">C3, C4</td>
</tr>
<tr>
<td align="left">조건절 5</td>
<td align="center">C1</td>
<td align="center">C2, C3, C4</td>
</tr>
</tbody></table>
<h3 id="인덱스-선행-컬럼이--조건이-아닐-때-생기는-비효율">인덱스 선행 컬럼이 = 조건이 아닐 때 생기는 비효율</h3>
<p>인덱스 스캔 효율성은 인덱스 컬럼을 조건절에 모두 = 조건으로 사용할 때 가장 좋다.
리프블록을 스캔하면서 읽는 레코드는 하나도 걸러지지 않고 모두 테이블 액세스로 이어지므로 인덱스 스캔 단계에서의 비효율은 전혀 없다.
인덱스 컬럼 중 일부가 조건절에 사용되지 않거나 = 조건이 아니더라도, 그것이 뒤쪽 컬럼일 때는 비효율이 없다.</p>
<p>만약, 지금 A, B, C, D 인덱스에서 A, B, C는 = 를 사용하고 D를 BETWEEN을 사용한다면 적은 횟수의 스캔을 하지만, D가 선두컬럼이였다면 얘기가 달라질 것이다.</p>
<p>인덱스 선행 컬럼이 모두 = 조건일 때 필요한 범위만 스캔하고 멈출 수 있는 것은, 조건을 만족하는 레코드가 모두 한데 모여있기 때문이다.</p>
<h3 id="between을-in-list로-전환">BETWEEN을 IN-List로 전환</h3>
<p>범위검색 컬럼이 맨 뒤로 가도록 인덱스를 변경하면 좋지만 운영 시스템에서 인덱스 구성을 바꾸기는 쉽지 않다. 이럴 때 BETWEEN 조건을 IN-List로 바꿔주면 큰 효과를 얻을 수 있다.</p>
<pre><code>WHERE D IN (&#39;A&#39;, &#39;B&#39;, &#39;C&#39;) AND A = &#39;1&#39; AND B = &#39;2&#39; AND C = &#39;3&#39;</code></pre><p><img src="https://velog.velcdn.com/images/diense_kk/post/dc1d145f-ea7e-490d-a498-3d987e7e56dd/image.jpg" alt=""></p>
<p>해당 인덱스 구성에서 BETWEEN을 사용했을 때와 IN-List로 바꾸었을 때 스캔하는 양을 생각해보기를 바란다.</p>
<p>해당 그림에서 왼쪽에서 화살표가 3개인 이유는 수직적 탐색이 3번 발생했기 때문이다.</p>
<h4 id="주의할-점">주의할 점</h4>
<p>BETWEEN 조건을 IN-List 조건으로 전환할 때 주의할 점은, IN-List 개수가 많지 않아야 된다는 것이다. IN-List 개수가 많으면 수직적 탐색이 많이 발생한다. 그러면 BETWEEN 조건 때문에 리프 블록을 많이 스캔하는 비효율보다 IN-List 개수만큼 브랜치 블록을 반복 탐색하는 비효율이 더 크다. 루트에서 브랜치 블록까지 Depth가 깊을 때 특히 그렇다.</p>
<p>스캔 과정에서 조회되는 레코드들이 서로 멀리 떨어져 있을 때만 유용하다.
부서코드, 직급 순으로 구성한 인덱스에서 &quot;직급 = 과장&quot; 조건을 만족하는 레코드가 서로 멀리 떨어져 있을 때만 BETWEEN 조건을 IN-List로 전환하는 기법이 유용하다.</p>
<p>BETWENE 조건으로 인덱스를 비효율적으로 스캔하더라도 블록 I/O 측면에서는 소량에 그치는 경우가 많다.
리프블록에는 테이블 브록과 달리 매우 많은 레코드가 담기기 때문이다.
게다가 IN-List 개수가 많으면 수직적 탐색 과정에서 이미 많은 블록을 읽게 된다. 데이터 분포나 수직적 탐색 비용을 따져보지도 않고 BETWEEN을 IN-List로 변환하는 실수를 하면 안된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 4]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-4</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-4</guid>
            <pubDate>Sat, 20 Dec 2025 06:14:42 GMT</pubDate>
            <description><![CDATA[<h2 id="sql-tuning-day-4">SQL Tuning Day 4</h2>
<h3 id="효율적인-코드">효율적인 코드</h3>
<p>코드 1.</p>
<pre><code>SELECT 장비번호, 장비명, 상태코드
     , (SELECT MAX(변경일자)
             FROM 상태변경이력
        WHERE 장비번호 = P.장비번호) AS 최종변경일자
     , (SELECT MAX(변경순번)
             FROM 상태변경이력
        WHERE 장비번호 = P.장비번호
              AND 변경일자 = (SELECT MAX(변경일자)
                                FROM 상태변경이력
                          WHERE 장비번호 = P.장비번호)) AS 최종변경순번
    FROM 장비 AS P
WHERE 장비구분코드 = &#39;A001&#39;</code></pre><p>코드 2.</p>
<pre><code>SELECT 장비번호, 장비명, 상태코드
    ,SUBSTR(최종이력, 1, 8) AS 최종변경일자
    ,SUBSTR(최종이력, 9) AS 최종변경순번
    FROM(
        SELCT 장비번호, 장비명, 상태코드
            ,(SELCT MAX(변경일자||변경순번)
                FROM 상태변경이력
              WHERE 장비번호 = P.장비번호) AS 최종이력
            FROM 장비 AS P
        WHERE 장비구분코드 = &#39;A0001&#39;
    )</code></pre><p>인덱스를 &quot;장비번호 + 변경일자 + 변경순번&quot;으로 구성했을 경우 두 쿼리 중 어느 쿼리가 더 효율적일까?
각 장비당 이력이 많지 않으면 크게 상관없지만, 이력이 많다면 쿼리2가 성능이 문제가 될 수 있는 패턴이다.
인덱스 컬럼을 가공했기 때문이다. 각 장비에 속한 과거 이력 데이터를 모두 읽어야하므로 장비당 이력 레코드가 많다면 코드1 보다 성능이 더 안좋을 수 있다.</p>
<h3 id="index-skip-scan">Index Skip Scan</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/4c560a5a-c17f-4a6b-ac68-5f3c28359582/image.png" alt=""></p>
<p>인덱스 선두 컬럼을 조건절에 사용하지 않으면 옵티마이저는 기본적으로 Table Full Scan을 선택한다. Table Full Scan보다 I/O를 줄일 수 있거나 정렬된 결과를 쉽게 얻을 수 있다면, Table Full Scan을 사용하기도 한다.
오라클은 인덱스 선두 컬럼이 조건절에 없어도 인덱스를 활용하는 새로운 스캔 방식을 선보였는데,  Index Skip Scan이 바로 그것이다.
이 스캔은 조건절에 빠진 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많을 때 유용하다.
예를 들면 급여 테이블에서 Distinct Value가 적은 것은 부서, 많은 것은 급여일 것이다.
이때 인덱스 선두 컬럼이 없을 때 Index Skip Scan이 작동한다. (물론 중간 컬럼이 없을 때도 작동한다.)</p>
<p>Index SKip Scan은 루트 또는 브랜치 블록에서 읽은 컬럼 값 정보를 이용해 조건절에 부합하는 레코드를 포함할 &#39;가능성이 있는&#39; 리프 블록만 골라서 액세스하는 스캔 방식이다.</p>
<h3 id="partition-pruning">Partition Pruning</h3>
<p>파티션 프루닝(Partition Pruning)은 시스템에서 불필요한 파티션을 읽지 않고 건너뛰어 성능을 향상시키는 기술로, SQL 쿼리의 조건절을 분석해 &quot;가지치기&quot;처럼 액세스 대상이 아닌 데이터를 제거하는 데이터베이스 최적화 기법이다. 이를 통해 대용량 데이터처리 시 디스크 I/O를 줄이고 쿼리 속도를 크게 향상시킬 수 있다.</p>
<h3 id="인덱스-구조-테이블">인덱스 구조 테이블</h3>
<p>랜덤 액세스가 아예 발생하지 않도록 테이블을 인덱스 구조로 생성한 것을 IOT(오라클), 클러스터형 인덱스(MS-SQL)라고 부른다.
테이블을 찾아가기 위한 ROWID를 갖는 일반 인덱스와 달리 IOT는 그 자리에 테이블 데이터를 갖는다. 즉, 테이블 블록에 있어야 할 데이터를 인덱스 리프 블록에 모두 저장하고 있다. 
IOT에서는 인덱스 리프 블록이 곧 데이터 블록이다.</p>
<p>인덱스 구조로 테이블을 생성하는 방법이다.</p>
<pre><code>CREATE TABLE index_org_t(
    A NUMBER,
    B VARCHAR(10),
    CONSTRAINT index_org_t_pk PRIMARY KEY(a)
)
ORGANIZATION INDEX;
</code></pre><p>참고로, 일반 테이블은 힙 구조 테이블이라고 부른다.
일반 힙 구조 테이블에 데이터를 입력할 때는 랜덤 방식을 사용한다. 즉, FreeList로부터 할당 받은 블록에 정해진 순서 없이 데이터를 입력한다. 반면 IOT는 인덱스 구조 테이블이므로 정렬 상태를 유지하며 데이터를 입력한다.</p>
<p>IOT는 인위적으로 클러스터링 팩터를 좋게 만드는 방법 중 하나이다. 같은 값을 가진 레코드들이 100% 정렬된 상태로 모여 있으므로 랜덤 액세스가 아닌 시퀀셜 방식으로 데이터를 액세스한다.
이 때문에 BETWEEN이나 부등호 조건으로 넓은 범위를 읽을 때 유리하다.</p>
<h3 id="클러스터-테이블">클러스터 테이블</h3>
<p>클러스터 테이블에는 인덱스 클러스터와 해시 클러스터 두 가지가 있다.</p>
<h4 id="인덱스-클러스터-테이블">인덱스 클러스터 테이블</h4>
<p>인덱스 클러스터 테이블은 값이 같은 레코드를 한 블록에 모아서 저장하는 구조이다.
한 블록에 모두 담을 수 없을 때는 새로운 블록에 할당해서 클러스터 체인으로 연결한다.</p>
<p>여러 테이블 레코드를 같은 블록에 저장할 수도 있다. 이것을 다중 테이블 클러스터라고 부른다.
일반 테이블은 하나의 데이터 블록을 여러 테이블이 공유할 수 없다.</p>
<p>클러스터형 인덱스는 IOT와 가깝다.
오라클 클러스터는 키 값이 같은 데이터를 같은 공간에 저장해 둘 뿐, IOT나 SQL Server의 클러스터형 인덱스처럼 정렬하지는 않는다.</p>
<p>클러스터에 테이블을 담기 전에 클러스터 인덱스를 반드시 정의해야 된다. 클러스터 인덱스는 데이터 검색 용도로 사용할 뿐만 아니라 데이터가 저장될 위치를 찾을 때도 사용되기 때문이다.</p>
<p>클러스터 인덱스도 일반 B*Tree 인덱스 구조를 사용하지만, 테이블 레코드를 일일이 가리키지 않고 해당 키 값을 저장하는 첫 번째 데이터 블록을 가리킨다는 점이 다르다. 즉, 일반 테이블에 생성한 인덱스 레코드는 테이블 레코드와 1:1 대응 관계를 갖지만, 클러스터 인덱스는 테이블 레코드와 1:M 관계를 갖는다. 따라서 클러스터 인덱스의 키 값은 항상 Unique이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/7e14ffb8-ee35-4c4b-a2b5-1de4f7ac0185/image.jpg" alt=""></p>
<p>이런 구조적 특성 때문에 클러스터 인덱스를 스캔하면서 값을 찾을 때는 랜덤 액세스가 값 하나당 한 번씩만 발생한다. 클러스터에 도달해서 시퀀셜 방식으로 스캔하기 때문에 넓은 범위를 읽더라도 비효율이 없다는 것이 핵심 원리이다.</p>
<h4 id="해시-클러스터-테이블">해시 클러스터 테이블</h4>
<p>해시 클러스터는 인덱스를 사용하지 않고 해시 알고리즘을 사용해 클러스터를 찾아간다는 점만 다르다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DBMS 랜덤 I/O]]></title>
            <link>https://velog.io/@diense_kk/DBMS-%EB%9E%9C%EB%8D%A4-IO</link>
            <guid>https://velog.io/@diense_kk/DBMS-%EB%9E%9C%EB%8D%A4-IO</guid>
            <pubDate>Sat, 20 Sep 2025 08:14:02 GMT</pubDate>
            <description><![CDATA[<p>쿼리에서 참조되는 컬럼이 인덱스에 모두 포함되는 경우가 아니라면 인덱스 스캔 이후 테이블 랜덤 액세스가 발생한다.
이는 잦은 블록 I/O를 발생시켜 성능 저하의 원인이 될 수 있다.</p>
<h3 id="디스크-io-종류">디스크 I/O 종류</h3>
<p>디스크의 순차 I/O는 데이터를 연속적인 순서로 접근하는 방식이며, 디스크의 랜덤 I/O는 데이터를 임의의 순서로 접근하느 방식이다.</p>
<p>하드디스크에서 파일을 읽을 떄, 랜덤 I/O는 파일의 특정 부분만 읽거나 쓰기 위해 디스크 헤더를 움직이는 방식이고, 순차 I/O는 파일의 처음부터 끝까지 읽거나 쓰기 위해 디스크 헤더를 한 방향으로 움직이는 방식이다.</p>
<p>DB서버에서 순차 I/O와 랜덤 I/O가 발생하는 상황은 아래와 같다.</p>
<h4 id="순차-io">순차 I/O</h4>
<p>1, 테이블의 모든 데이터를 조회하는 상황
2, 대량의 데이터를 정렬하거나 그룹화 하는 상황
3, 풀 테이블 스캔</p>
<h4 id="랜덤-io">랜덤 I/O</h4>
<p>1, WHERE 조건이 포함된 쿼리를 실행해 데이터를 조회하는 상황
2, WHERE 조건이 포함된 쿼리를 실행해 데이터를 삭제하거나 수정하는 상황
3, 인덱스 레인지 스캔</p>
<p>순차 I/O는 디스크에서 연속적인 데이터를 읽거나 쓰기 떄문에, 대량의 데이터를 처리하는 데 좋은 성능을 보인다.
랜덤 I/O는 순차 I/O보다 원하는 데이터를 빠르게 찾을 수 있지만, 디스크의 헤드가 여러 위치를 탐색해야 하기 때문에, 대량의 데이터를 처리하는데 비교적 느린 작업이다.
따라서 디스크의 성능은 얼마나 헤드의 이동 없이 많은 데이터를 순차적으로 저장하는가에 달려있다.
즉, 랜덤 I/O를 줄이는 것이 성능 개선에 중요합니다.</p>
<h3 id="랜덤-io-종류">랜덤 I/O 종류</h3>
<h4 id="1-확인-랜덤-액세스">1. 확인 랜덤 액세스</h4>
<p>WHERE, HAVING 조건의 컬럼이 인덱스에 존재하지 않아 테이블을 액세스하는 랜덤 액세스이다.
확인 랜덤 액세스의 특징은 랜덤 액세스의 횟수보다 최종 결과가 동일하거나 적게 추출된다.</p>
<blockquote>
<p>SELECT * FROM 사원 테이블 WHERE 이름 = &quot;&quot; AND 사업장코드 = &quot;&quot;</p>
</blockquote>
<p>사원 테이블에 &quot;이름&quot; 컬럼에만 인덱스가 존재한다면, 위 SQL이 실행되면 이름 컬럼에 의해 인덱스를 액세스하고 처리 범위가 좁혀질 것이다.
그러나, &quot;사업장코드&quot;는 인덱스로 설정되어 있지 않기 때문에 결국 &quot;이름&quot; 조건을 만족하는 모든 데이터에 대해 테이블을 액세스 하여 &quot;사업장코드&quot; 컬럼의 값을 확인하여 조건을 부합하는 값을 찾게 된다.</p>
<p>이처럼 WHERE 조건의 컬럼이 인덱스에 존재하지 않아 테이블 랜덤 액세스를 발생시키는 것을 확인 랜덤 액세스라고 한다.
테이블을 액세스 한 후 버려지는 데이터가 존재하기 떄문에, <strong>랜덤 액세스의 3가지 종류 중에서도 확인 랜덤 액세스의 제거는 성능에 있어 매우 중요하다.</strong></p>
<h4 id="2-추출-랜덤-액세스">2. 추출 랜덤 액세스</h4>
<p>인덱스 액세스 후 SELECT 절의 컬럼을 결과로 추출하기 위해 추가로 테이블을 액세스한다. 추출 랜덤 액세스의 특징은 랜덤 액세스 횟수와 추출 데이터의 양이 동일하며, SELECT 절에서 발생한다는 것이다.</p>
<p>WHERE 조건에 사용되는 컬럼들이 모두 인덱스에 존재하지만, SELECT 절의 컬럼들에 인덱스가 포함되지 않는 컬럼이 있다면 인덱스 액세스 이후에 추가로 테이블에 액세스 해야된다. 이와 같은 현상이 추출 랜덤 액세스이다.</p>
<p>SELECT 절의 컬럼들은 추출되는 데이터를 감소시키거나 증가시키지 않기 때문에, 랜덤 액세스 횟수와 추출 데이터의 양이 동일하다. 따라서 추출 랜덤 액세스는 WHERE 조건의 조회 결과만큼 발생하게 된다.</p>
<h4 id="3-정렬-랜덤-액세스">3. 정렬 랜덤 액세스</h4>
<p>ORDER BY, GROUP BY절 컬럼이 인덱스에 존재하지 않아 추가적으로 테이블 액세스한다. 정렬 랜덤 액세스의 특징은 랜덤액세스와 추출 데이터의 양이 동일하며, ORDER BY, GROUP BY 절에서 발생한다.</p>
<p>SELECT 이후에, ORDER BY 절이나 GROUP BY절에 사용되는 컬럼에 인덱스가 존재하지 않을 때에 발생한다.
정렬 랜덤 액세스의 양도 추출 랜덤 액세스와 마찬가지로 결과의 양과 동일하다.
그렇기 떄문에, 랜덤 액세스 중 추출되는 데이터를 감소시키는 확인 랜덤 액세스를 감소시키는 방안이 성능 측면에서 가장 중요하다.</p>
<h3 id="랜덤-액세스-최소화">랜덤 액세스 최소화</h3>
<h4 id="확인-랜덤-액세스">확인 랜덤 액세스</h4>
<p>WHERE 조건에는 인덱스로 설정된 컬럼을 사용하는 것이 좋다. 만약, 인덱스로 설정되지 않은 컬럼을 사용해야 된다면 그 컬럼을 인덱스로 설정하는 것을 고려해야된다.
하지만, 인덱스의 추가 또는 삭제가 운영 중인 시스템에서는 매우 위험한 작업일 수 있다.</p>
<p>인덱스를 생성 시 인덱스를 생성하는 과정에서 CPU, 메모리 I/O 부하가 크다.
인덱스를 삭제 시 내부적으로 데이터 사전과 옵티마이저 통계가 바뀌고, 관련 객체에 락이 걸리 수 있기 떄문이다.</p>
<p>*데이터 사전(Data Dictionary AKA System Catalog)</p>
<p>대부분 읽기전용으로 제공되는 테이블 및 뷰들의 집합으로, 데이터베이스 전반에 대한 정보를 제공한다. 데이터베이스의 데이터를 제외한 모든 정보-데이터에 대한 데이터(메타데이터)가 들어있다. 데이터 사전의 내용을 변경하는 권한은 시스템이 가진다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/38be81d1-3cb3-420d-bdb6-8072d974681a/image.png" alt=""></p>
<h4 id="데이터-사전에-저장되는-내용">데이터 사전에 저장되는 내용</h4>
<p>데이터베이스 사용자 정보, 권한과 롤 정보, 데이터베이스 스키마 객체(TABLE, VIEW 등)의 정보</p>
<p>데이터 사전에는 데이터베이스 운영에 중요한 데이터들이 저장되기 때문에, 데이터 사전에 문제가 발생할 시 데이터베이스 사용이 불가능해질 수 있다.
데이터 사전도 데이터를 저장하는 데이터베이스의 일종이기 때문에 시스템 데이터베이스라고도 ㅎ산다. 데이터 사전은 DBMS가 스스로 생성하고 유지하는 것으로, DBMS가 주로 접근하짐나 일반 사용자도 접근 가능하다. 단, 조회만 가능</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 3]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-3</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-3</guid>
            <pubDate>Sat, 06 Sep 2025 07:35:55 GMT</pubDate>
            <description><![CDATA[<h2 id="테이블-액세스-최소화">테이블 액세스 최소화</h2>
<p>SQL 튜닝은 랜덤 I/O와의 전쟁이라는 말이 있다. 그만큼 중요하다.</p>
<p>SQL이 참조하는 컬럼을 인덱스가 모두 포함하는 경우가 아니면, 인덱스를 스캔한 후에 반드시 테이블을 액세스한다.
인덱스를 스캔하는 이유는, 검색 조건을 만족하는 소량의 데이터를 인덱스에서 빨리 찾고 거기서 테이블 레코드를 찾아가기 위한 주소값(ROWID)을 얻으려는 데 있다.</p>
<p>인덱스 ROWID는 논리적 주소에 가깝다. 물리적으로 직접 연결되지 않고 테이블 레코드를 찾아가기 위한 논리적 주소 정보를 담고 있기 떄문이다.
정확하게는 디스크 상에서 테이블 레코드를 찾아가기 위한 <strong>&quot;위치 정보&quot;</strong>를 담는다.
데이터베이스의 인덱스를 설명할 떄 항상 도서 색인에 비유한다. 색인에 기록된 페이지 번호가 ROWID에 해당한다.</p>
<h3 id="메인-메모리-db">메인 메모리 DB</h3>
<p>메인 메모리 DB는 말 그대로 데이터를 모두 메모리에 로드해 놓고 메모리를 통해서만 I/O를 수행하는 DB이다.
잘 튜닝된 OLTP성 데이터베이스 시스템이라면 버퍼 캐시 히트율이 99% 이상이다. 디스크를 경유하지 않고 대부분 데이터를 메모리에서 읽는다는 뜻이다. 그런데도 메인 메모리 DB만큼 빠르지는 않다. 특히 대량 데이터를 인덱스로 액세스할 떄는 엄청난 차이가 난다.</p>
<p>어떤 메인 메모리 DB의 경우 인스턴스를 기동하면 디스크에 저장된 데이터를 버퍼캐시로 로딩하고 이어서 인덱스를 생성한다. 이떄 인덱스는 오라클처럼 디스크 상의 주소정보를 갖는게 아닌 메모리상의 주소정보, 즉 포인터를 갖는다. 따라서 인덱스를 경유해 테이블을 액세스하는 비용이 오라클과 비교할 수 없을 정도로 낮다.</p>
<blockquote>
<p>FLOW
SQL 실행 → 메모리 확인 → 없으면 디스크 읽기 후 메모리 적재 → 결과 반환
오라클의 인덱스가 &quot;디스크 주소 참조형&quot;이라면 메모리 DB의 인덱스는 &quot;메모리 포인터 참조형&quot;이다.</p>
</blockquote>
<h3 id="io-매커니즘">I/O 매커니즘</h3>
<p>DBA(=데이터 파일 번호 + 블록번호)는 디스크 상에서 블록을 찾기 위한 주소 정보이다.
그렇다고 매번 디스크에서 블록일 읽을 수는 없다. I/O 성능을 높이려면 버퍼캐시를 활용해야된다.
그래서 블록을 읽을 떄는 디스크로 가기 전에 버퍼캐시부터 찾아본다. 읽고자 하는 DBA를 해시함수에 입력해서 해시 체인을 찾고 거기서 버퍼 헤더를 찾는다.
캐시에 적재할 떄와 읽을 떄 같은 해시 함수를 사용하므로 버퍼 헤더는 항상 같은 해시 체인에 연결된다.</p>
<p>해싱 알고리즘으로 버퍼 헤더를 찾고, 거기서 얻은 포인터로 버퍼 블록을 찾아가는 것이다.
모든 데이터가 캐싱돼 있더라도 테이블 레코드를 찾기 위해 매번 DBA 해싱과 래치 획득 과정을 반복해야 된다. 동시 액세스가 심할 때는 캐시버퍼 체인 래치와 버퍼 Lock에 대한 경합까지 발생한다. 이처럼 인덱스 ROWID를 이용한 테이블 액세스는 생각보다 고비용 구조다.</p>
<h3 id="인덱스-클러스터링-팩터">인덱스 클러스터링 팩터</h3>
<p>클러스터링 팩터는 군집성 계수이다.
특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정도를 의미한다.
당연하게도 CF가 좋은 컬럼에 생성한 인덱스는 검색 효율이 매우 좋다.
거주지역 = &#39;대한민국&#39;인 데이터가 물리적으로 근접해 있으면 흩어져 있을 때보다 데이터를 찾는 속도가 빠르다.
<img src="https://velog.velcdn.com/images/diense_kk/post/e56355ba-9c7e-4275-85ad-320d518559f4/image.png" alt="">
인덱스 클러스터링 팩터가 가장 좋은 상태를 도식화 한 이미지이다.
CF가 좋은 컬럼에 생성한 인덱스는 검색 효율이 좋다고 했는데, 이는 테이블 액세스량에 비해 블록 I/O가 적게 발생함을 의미힌다.</p>
<h3 id="인덱스-손익분기점">인덱스 손익분기점</h3>
<p>인덱스 ROWID를 이용한 테이블 액세스는 생각보다 고비용 구조다. 따라서 읽어야 할 데이터가 일정량을 넘는 순간, 테이블 전체를 스캔하는 것보다 오히려 느려진다.
Index Range Scan에 의한 테이블 액세스가 Table Full Scan보다 느려지는 지점을 흔히 인덱스 손익분기점이라고 부른다.
Table Full Scan은 성능이 일정하다. 몇 건을 조회하든 차이가 거의 없다.
인덱스를 이용해 테이블을 액세스할 떄는 전체 데이터 중 몇 건을 추출하느냐에 따라 성능이 크게 달라진다. 당연히 추출 건수가 많을수록 느려진다. 바로 테이블 랜덤 액세스 떄문이다.
인덱스를 이용한 테이블 액세스가 Table Full Scan보다 더 느려지게 만드는 가장 핵심적인 두 가지 요인은 다음과 같다.</p>
<ol>
<li>Table Full Scan은 시퀀셜 액세스인 반면, 인덱스 ROWID를 이용한 테이블 액세스는 랜덤 액세스 방식이다.</li>
<li>Table Full Scan은 MultiBlock I/O인 반면, 인덱스 ROWID를 이용한 테이블 액세스는 Single Block I/O방식이다.</li>
</ol>
<p>이런 요인에 의해 인덱스 손익분기점은 보통 5~20%의 낮은 수준에서 결정된다. 인덱스 CF가 나쁘면 같은 테이블 블록을 여러번 반복 액세스하면서 논리적 I/O 횟수가 늘고, 물리적 I/O 횟수가 늘기 떄문이다. CF가 나쁘면 손익분기점은 5% 미만에서 결정되며, 심할 떄는 1% 미만으로 낮아진다. 반대로 CF가 좋으면 90% 수준까지 올라간다.
<img src="https://velog.velcdn.com/images/diense_kk/post/8ef9b269-a177-4915-b48e-009f70ecb146/image.jpeg" alt=""></p>
<h3 id="인덱스-손익분기점과-버퍼캐시-히트율">인덱스 손익분기점과 버퍼캐시 히트율</h3>
<p>5~20% 수준의 손익분기점은 10만 건 이내, 많아봐야 100만 건 이내 테이블에 적용되는 수치이다. 더 많은 건을 가진 테이블에선 손익분기점이 더 낮아진다.
10만 건 기준으로 10%는 1만 건이다. 만 건 정도면 버퍼캐시에서 데이터를 찾을 가능성이 어느정도 있다. 게다가 이정도 크기의 테이블이면 인덱스 컬럼 기준으로 값이 같은 테이블 레코드가 근처에 모여 있을 가능성이 있다. 
따라서 인덱스를 스캔하면서 테이블을 액세스하다 보면 어느 순간부터 대부분 테이블 블록을 캐시에서 찾게된다.</p>
<h3 id="온라인-프로그램-튜닝-vs-배치-프로그램-튜닝">온라인 프로그램 튜닝 VS 배치 프로그램 튜닝</h3>
<p>온라인 프로그램은 보통 소량 데이터를 읽고 갱신하므로 인덱스를 효과적으로 활용하는 것이 무엇보다 중요하다. 조인도 대부분 NL 방식을 이용한다. NL조인은 인덱스를 이용하는 조인 방식이다.
반면, 대량 데이터를 읽고 갱신하는 Batch 프로그램은 항상 전체범위 처리 기준으로 튜닝해야된다. 즉, 처리대상 집합 중 일부를 빠르게 처리하는 것이 아니라 전체를 빠르게 처리하는 것을 목표로 삼아야 된다. 대량 데이터를 빠르게 처리하려면, 인덱스와 NL 조인보다 Full Scan과 해시 조인이 유리하다.</p>
<p>대량 Batch 프로그램에서는 인덱스보다 Full Scan이 효과적이다. 따라서 파티션 활용 전략이 매우 중요한 튜닝 요소이고, 병렬 처리까지 더할 수 있으면 좋다. 테이블을 특정 조건으로 파티셔닝 하면, 해당 파티션만 골라서 Full Scan하므로 부담을 크게 줄일 수 있다.
테이블을 파티셔닝 하는 이유는 결국 Full Scan을 빠르게 처리하기 위해서다.</p>
<h3 id="인덱스-컬럼-추가">인덱스 컬럼 추가</h3>
<p>테이블 액세스 최소화를 위해 가장 일반적으로 사용하는 튜닝 기법은 인덱스에 컬럼을 추가하는 것이다.
DeptNo + JobTask 순으로 구성한 인덱스가 있다고 가정해보자.</p>
<pre><code>SELECT /*index(emp emp_x01)*/
    FROM EMP
WHERE DeptNo = &#39;00137&#39;
    AND Sal &gt;= 2000</code></pre><p>위 조건을 만족하는 사원이 단 한명인데, DeptNo = &#39;00137&#39;인 데이터가 6건이 있다면 테이블을 여섯번 액세스 해야된다.
인덱스 구성을 변경하면 좋겠지만, 실무에서 인덱스 구성을 변경하는 것은 절대 쉽지 않다.
인덱스 구성을 새로 만드는 건? 이런식으로 인덱스를 추가하다 보면 테이블마다 인덱스가 수십 개씩 달려 배보다 배꼽이 더 커지게 된다. 인덱스 관리 비용이 증가함은 물론 DML 부하에 따른 트랜잭션 성능 저하가 생길 수 있다.
이런 경우, 기존 emp_x01 인덱스에 Sal 컬럼을 추가하는 것만으로도 큰 효과를 얻을 수 있다. 인덱스 스캔량은 줄지 않지만, 테이블 랜덤 액세스 횟수를 줄여주기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 2]]></title>
            <link>https://velog.io/@diense_kk/SQL-Tuning-Day-2</link>
            <guid>https://velog.io/@diense_kk/SQL-Tuning-Day-2</guid>
            <pubDate>Sun, 03 Aug 2025 06:13:25 GMT</pubDate>
            <description><![CDATA[<h3 id="인덱스-구조-및-탐색">인덱스 구조 및 탐색</h3>
<p>테이블에서 데이터를 찾는 방법은 Table Full Scan과 Index를 사용하는 두 가지의 방법이 있다.</p>
<p>인덱스 탐색 과정은 수직적 탐색과 수평적 탐색, 두 단계로 이루어진다.</p>
<ul>
<li>수직적 탐색 : 인덱스 스캔 시작지점을 찾는 과정</li>
<li>수평적 탐색 : 데이터를 찾는 과정</li>
</ul>
<p>만약 유저 테이블에서 특정 유저를 직접 찾는다면 이름순으로 정렬된 상태에서 데이터를 찾는 것이 빠를 것이다. 이것이 &quot;인덱스&quot;이다.
인덱스는 큰 테이블에서 소량 데이터를 검색할 때 사용된다. 온라인 트랜잭션 처리(OLTP) 시스템에서는 소량 데이터를 주로 검색하므로 인덱스 튜닝이 중요하다.</p>
<p>인덱스 튜닝의 두 번째 핵심 요소는 테이블 액세스 횟수를 줄이는 것이다. 인덱스 스캔 후 테이블 레코드를 액세스할 때 랜덤 I/O 방식을 사용하므로 이를 &quot;랜덤 액세스 최소화 튜닝&quot;이라고 한다.</p>
<p>유저 테이블에서 주소가 &#39;서울&#39;이고 이름이 &#39;KGJ&#39;인 사용자를 찾는다고 하자.
전체 유저 중 서울에 사는 사람은 약 10만 명이지만, 이름이 &#39;KGJ&#39;인 사람은 30명밖에 없다.
이 경우 이름 컬럼에 인덱스를 사용하는 것이 훨씬 효율적이다.
이름이 &#39;KGJ&#39;인 유저 30명만 먼저 조회한 후, 그 중에서 주소가 &#39;서울&#39;인지만 확인하면 되기 때문이다.
반면 주소에 인덱스를 사용하면 10만 건을 먼저 읽고 다시 이름 조건을 확인해야 한다.</p>
<h4 id="인덱스-구조">인덱스 구조</h4>
<p>DBMS는 일반적으로 B<em>Tree 인덱스를 사용한다.
B</em>Tree는 트리 형태로, Root가 위쪽에 있고, Branch를 거쳐 맨 아래에 Leaf가 있다.
<img src="https://velog.velcdn.com/images/diense_kk/post/26bc69ce-7120-47dd-a11b-cecda8c434a8/image.jpg" alt=""></p>
<p>Root와 Branch 블록에 있는 레코든즌 하위 블록에 대한 주소값을 갖는다. 키값은 하위 블록에 저장된 키값의 범위를 나타낸다.
예를 들어, &quot;500&quot; 레코드가 가리키는 하위 블록에는 &quot;500&quot;보다 크거나 같은 레코드가 저장돼 있다는 뜻이다.
Leaf 블록에 저장된 각 레코드는 키값 순으로 정렬돼 있을 뿐만 아니라 테이블 레코드를 가리키는 주소값, 즉 ROWID를 갖는다. 인덱스 키값이 같으면 ROWID 순으로 정렬된다. 인덱스를 스캔하는 이유는, 검색 조건을 만족하는 소량의 데이터를 빨리 찾고 거기서 ROWID를 얻기 위해서다.</p>
<p>ROWID가 갖는 테이블 블록 주소(Data Block Address)</p>
<ul>
<li>ROWID : 데이터 블록 주소 + 로우 번호</li>
<li>데이터 블록 주소 : 데이터 파일 번호 + 블록 번호</li>
<li>블록 번호 : 데이터파일 내에서 부여한 상대적 순번</li>
<li>로우 번호 : 블록 내 순번</li>
</ul>
<h4 id="인덱스-수직적-탐색">인덱스 수직적 탐색</h4>
<p>정렬된 인덱스 레코드 중 조건을 만족하는 첫 번째 레코드를 찾는 과정이다. 즉, 인덱스 스캔 시작지점을 찾는 과정이다.
인덱스 수직적 탐색은 Root 블록에서부터 시작된다. 루트를 포함해 Branch 블록에 저장된 각 인덱스 레코드는 하위 블록에 대한 주소값을 갖기 떄문에 루트에서 시작해 리프블록까지 수직점 탐색이 가능하다.</p>
<h4 id="인덱스-수평적-탐색">인덱스 수평적 탐색</h4>
<p>수직적 탐색을 통해 스캔 시작점을 찾았다면, 찾고자 하는 데이터가 더 안 나타날 때까지 인덱스 리프 블록을 수평적으로 스캔한다. 인덱스가 본격적으로 데이터를 찾는 과정이다.
인덱스 리프 블록끼리는 서로 옆의 블록에 대한 주소값을 갖는다. 즉, Double Linked List 구조다. 좌에서 우로, 우에서 좌로 수평적 탐색이 가능한 이유다.</p>
<p>인덱스를 수평적으로 탐색하는 이유는 다음과 같다.</p>
<ol>
<li>조건절에 만족하는 데이터를 모두 찾기 위해서이다.</li>
<li>ROWID를 얻기 위해서이다.
조회하고 싶은 컬럼을 모든 인덱스가 모두 갖고 있어 인덱스만 스캔하고 끝나는 경우도 있지만, 일반적으로 인덱스를 스캔하고서 테이블도 액세스한다. 이때 ROWID가 필요하다.
테이블을 액세스 한다는 것은, 인덱스만으로는 원하는 데이터를 다 가져올 수 없기 때문에, 실제 테이블까지 가서 데이터를 더 읽어야 한다는 뜻이다.</li>
</ol>
<h4 id="결합-인덱스-구조와-탐색">결합 인덱스 구조와 탐색</h4>
<p>두 개 이상의 컬럼을 결합해서 인덱스를 만들 수도 있다.
예를 들어, 성별 이름으로 인덱스를 만든다면 Male &amp; Kim라는 레코드가 생성된다.
주목할 것은, 인덱스를 &quot;성별 &amp; 이름&quot;으로 구성하는 것과 &quot;이름 &amp; 성별&quot;로 구성하는 것은 읽는 블록 개수가 똑같기 때문에 성능이 같다.
비교 연산 횟수가 줄어드는 건 사실이지만 성능에서 차이는 없다.
<del>애매하네 이건</del></p>
<h4 id="balanced">Balanced?</h4>
<p>DELETE 작업 떄문에 인덱스가 UnBalanced 상태에 놓일 수 있다고 설명하는 자료들이 있다.
하지만, B<em>Tree 인덱스에서 이런 현상은 절대 발생하지 않는다. B</em>Tree의 &quot;B&quot;가 &quot;Balanced&quot;의 약자임을 기억하자.</p>
<h3 id="인덱스-기본-사용법">인덱스 기본 사용법</h3>
<p>데이터베이스에서는 인덱스 컬럼을 가공하지 않아야 인덱스를 정상적으로 사용할 수 있다.
인덱스를 정상적으로 사용한다는 표현은 리프 블록에서 스캔 시작점을 찾아 거기서부터 중간에 멈추는 것을 의미한다.
즉, 리프 블록 일부만 스캔하는 Index Range Scan을 의미한다.
인덱스 컬럼을 가공 하더라도 인덱스를 사용할 수는 있지만, 스캔 시작점을 찾을 수 없고 멈출 수도 없어 리프 블록 전체를 스캔해야된다.(Index Full Scan)</p>
<h4 id="인덱스를-range-scan-할-수-없는-이유">인덱스를 Range Scan 할 수 없는 이유</h4>
<p>인덱스를 가공하면 인덱스를 정상적으로 사용할 수 없다라는 것은 기본 중에 기본이다.
Index Range Scan에서 &quot;Range&quot;는 &quot;범위&quot;를 의미한다. Range Scan은 인덱수에서 일정 범위를 스캔한다는 뜻이다. 일정 범위를 스캔하기 위해서는 &quot;시작지점&quot;과 &quot;끝지점&quot;이 명확하게 있어야 된다.</p>
<p>인덱스를 Range Scan하기 위한 가장 첫 번째 조건은 인덱스 선두 컬럼이 조건절에 있어야 된다.
인덱스 선두 컬럼이 가공되지 않은 상태로 조건절에 있으면 인덱스 Range Scan이 무조건 가능하다는 것이다.</p>
<pre><code>EX)
SELECT ~
    FROM TableName AS A
WHERE A.TeamCd = &#39;003&#39;
    AND (범위, OR, ISNULL 등)</code></pre><p>해당 쿼리에서 두번째 WHERE에 범위, OR, ISNULL의 함수가 사용되더라도 선두 컬럼인 TeamCd에 &quot;=&quot;를 사용했기 때문에 Range Scan이 가능한 것이다.
하지만, 인덱스 Range Scan 한다고 해서 항상 성능이 좋은 것은 아니다.</p>
<p>인덱스를 정말 잘 타는지는 리프 블록에서 스캔하는 양을 따져봐야 알 수 있다.</p>
<h4 id="order-by-절에서-컬럼-가공">ORDER BY 절에서 컬럼 가공</h4>
<p>조건절이 아닌 ORDER BY 또는 SELECT-LIST에서 컬럼을 가공함으로 인해 인덱스를 정상적으로 사용할 수 없는 경우도 종종 있다.</p>
<p>PK가 &quot;상품번호 &amp; 생성날짜&quot;로 이루어졌다고 가정했을 떄, 상품번호가 같은 레코드는 생성날짜를 기준으로 정렬돼있다. 그래서 상품번호에 &quot;=&quot;조건으로 검색할 때 PK 인덱스를 사용하면 결과집합은 생성날짜 순으로 출력된다.</p>
<p>옵티마이저는 이러한 속성을 활용해 SQL에 &quot;ORDER BY 상품번호, 생성날짜&quot;가 있어도 정렬 연산을 따로 수행하지 않는다.
그런데 만약, &quot;ORDER BY 상품번호 || 생성날짜&quot;로 작성했다면 정렬 연산을 생략할 수 없다. 가공하지 않은 상태로 값을 저장했지만 가공한 값 기준으로 정렬을 요청했기 때문이다.</p>
<h4 id="자동-형변환">자동 형변환</h4>
<p>코드값으로 데이터를 조회하는 쿼리를 보자.</p>
<pre><code>SELECT TeamCd, TeamNm
    FROM Team AS A
WHERE A.TeamCd = 123</code></pre><p>해당 쿼리에서는 조건절에 컬럼을 가공하지 않았는데도 Table Full Scan을 선택한다.
그 이유는, 옵티마이저가 해당 쿼리를 아래와 같이 변환했기 때문이다.</p>
<pre><code>SELECT TeamCd, TeamNm
    FROM Team AS A
WHERE TO_NUMBER(A.TeamCd) = 123</code></pre><p>각 조건절에서 양쪽 값의 데이터 타입이 서로 다르면 값을 비교할 수 없다.</p>
<h3 id="인덱스-확장기능-사용법">인덱스 확장기능 사용법</h3>
<h4 id="index-range-scan">Index Range Scan</h4>
<p>Index Range Scan은 B*Tree 인덱스의 가장 일반적이고 정상적인 형태의 액세스 방식이다.
인덱스 Root에서 Leaf 블록까지 수직적으로 탐색한 후에 필요한 Range만 스캔한다.
인덱스를 Range Scan하려면 선두 컬럼을 가공하지 않은 상태로 조건절에 사용해야 된다.
성능은 인덱스 스캔 범위와 테이블 액세스 횟수를 얼마나 줄일 수 있느냐로 결정된다.</p>
<h4 id="index-full-scan">Index Full Scan</h4>
<p>수직적 탐색 없이 인덱스 Leaf 블록을 처음부터 끝까지 수평적으로 탐색하는 방식이다.
데이터 탐색을 위한 최적의 인덱스가 없을 때 차선으로 선택된다.
인덱스 선두 컬럼이 조건절에 없으면 옵티마이저는 먼저 Table Full Scan을 고려한다.
만약 인덱스 스캔 단계에서 대부분 레코드를 필터링하고 아주 일부만 테이블을 액세스 하는 상황이라면, 면적이 큰 테이블보다 인덱스를 스캔하는 쪽이 유리하다. 그럴 때 옵티망지ㅓ는 Index Full Scan 방식을 선택한다.</p>
<p>하지만 대부분의 데이터가 조건을 만족하는 상황에서 Index Full Scan을 선택하면, 거의 모든 레코드에 대해 테이블 액세서가 발생하므로 Table Full Scan보다 오히려 효율적이지 못하다.</p>
<pre><code>SELECT /*+ first_rows */ *</code></pre><p>FIRST_ROWS 힌트는 전체 결과보다 처음 몇 행을 빠르게 보여주는 것이 목표이다.
그래서 옵티마이저는 정렬을 피하고, 인덱스를 이용한 빠른 접근 방식(Index Full Scan 등)을 선택할 수 있다.</p>
<p>이런 상황에서 인덱스만으로 원하는 순서가 맞춰진다면, 불필요한 정렬 없이 바로 앞부분 데이터만 읽게 돼서 성능이 크게 좋아질 수 있다.</p>
<p>주의할 점은, 사용자가 처음 의도와 달리 Fetch를 멈추지 않고 데이터를 끝까지 읽는다면 Table Full Scan보다 훨씬 더 많은 I/O를 일으키고 결과적으로 수행 속도도 훨씬 더 느려진다.</p>
<h4 id="index-unique-scan">Index Unique Scan</h4>
<p>Index Unique Scan은 수직적 탐색만으로 데이터를 찾는 스캔 방식으로, Unique 인덱스를 &quot;=&quot; 조건으로 탐색하는 경우에 작동한다.
Unique 인덱스가 존재하는 컬럼은 중복 값이 입력되지 않게 DBMS가 데이터 정합성을 관리해준다.
따라서 해당 인덱스 키 컬럼을 모두 &quot;=&quot; 조건으로 검색할 때는 데이터를 한 건 찾는 순간 더 이상 탐색할 필요가 없다.
Unique 인덱스라도 범위검색 조건으로 검색할 때는 Index Range Scan으로 처리된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL Tuning Day 1]]></title>
            <link>https://velog.io/@diense_kk/SQL-%ED%8A%9C%EB%8B%9D-1W</link>
            <guid>https://velog.io/@diense_kk/SQL-%ED%8A%9C%EB%8B%9D-1W</guid>
            <pubDate>Sat, 02 Aug 2025 08:10:44 GMT</pubDate>
            <description><![CDATA[<p>SQL은 기본적으로 구조적(Structed)이고 집합적(Set-Based)이고 선언적(Declarative)인 질의 언어이다.
원하는 결과집합을 구조적, 집합적으로 선언하지만, 그 결과집합을 만드는 과정은 절차적일 수 밖에 없다.
즉, 프로시저가 필요한데, 그런 프로시저를 만들어 내는 DBMS 내부 엔진이 SQL 옵티마이저다.</p>
<h3 id="sql-최적화">SQL 최적화</h3>
<h4 id="sql-파싱">SQL 파싱</h4>
<p>사용자로부터 SQL을 전달받으면 가장 먼저 SQL 파서(Parser)가 파싱을 진행한다.</p>
<ul>
<li>파싱 트리 생성 : SQL 문을 이루는 개별 구성요소를 분석해서 파싱 트리 생성</li>
<li>Syntax 체크 : 문법적 오류가 없는지 확인한다. 사용할 수 없는 키워드를 사용했거나 순서가 바르지 않거나 누락된 키워드가 있는지 확인한다.</li>
<li>Semantic 체크 : 의미상 오류가 없는지 확인한다. 존재하지 않는 테이블 또는 컬럼을 사용했는지, 사용한 오브젝트에 대한 권한이 있는지 확인한다.</li>
</ul>
<h4 id="sql-최적화-1">SQL 최적화</h4>
<p>SQL 최적화는 옵티마이저(Optimizer)가 맡는다.
SQL 옵티마이저는 미리 수집한 시스템 및 오브젝트 통계정보를 바탕으로 다양한 실행경로를 생성해서 비교한 후 가장 효율적인 하나를 선택한다.</p>
<h4 id="로우-소스-생성">로우 소스 생성</h4>
<p>SQL 옵티마이저가 선택한 실행경로를 실제 실행 가능한 코드 또는 프로시저 형태로 포맷팅 하는 단계이다. 로우 소스 생성기가 그 역할을 맡는다.</p>
<h3 id="sql-옵티마이저">SQL 옵티마이저</h3>
<p>SQL 옵티마이저는 사용자가 원하는 작업을 가장 효율적으로 수행할 수 있는 최적의 데이터 액세스 경로를 선택해 주는 DBMS의 핵심 엔진이다.</p>
<h4 id="옵티마이저의-최적화-단계-요약">옵티마이저의 최적화 단계 요약</h4>
<ol>
<li>사용자로부터 전달받은 쿼리를 수행하는 데 후보군이 될만한 실행계획들을 찾아낸다.</li>
<li>데이터 딕셔너리(Data Dictionary)에 미리 수집해 둔 오브젝트 통계 및 시스템 통계정보를 이용해 각 실행계획의 예상비용을 산정한다.</li>
<li>최저 비용을 나타내는 실행계획을 선택한다.
<img src="https://velog.velcdn.com/images/diense_kk/post/3123ab2f-3619-4a6e-86cd-33e518a71fc5/image.png" alt=""></li>
</ol>
<h4 id="실행계획과-비용">실행계획과 비용</h4>
<p>실행계획(Execution Plan)이 SQL 실행경로 미리보기 기능과 같다.
SQL 옵티마이저가 생성한 처리절차를 사용자가 확인할 수 있게 트리 구조로 표현한 것이 실행계획이다.
미리보기 기능을 통해 자신이 작성한 SQL이 테이블을 스캔하는지 인덱스를 스캔하는지, 인덱스를 스캔한다면 어떤 인덱스인지를 확인할 수 있고, 예상과 다른 방식으로 처리된다면 실행경로를 변경할 수 있다.
<img src="https://velog.velcdn.com/images/diense_kk/post/88007a05-1ca7-432d-b645-24f38cbed2f6/image.png" alt=""></p>
<p>미리보기 기능을 통해 자신이 작성한 SQL이 테이블을 스캔하는지 인덱스를 스캔하는지, 인덱스를 스캔한다면 어떤 인덱스인지를 확인할 수 있고, 예상과 다른 방식으로 처리된다면 실행경로를 변경할 수 있다.</p>
<p>옵티마이저가 인덱스를 선택하는 근거는 &quot;Cost&quot;이다.
Cost는 쿼리를 수행하는 동안 발생할 것으로 예상하는 I/O 횟수 또는 예상 소요시간을 표현한 값이다.
SQL 실행계획에 표시되는 Cost도 어디까지나 예상치다. 실행경로를 선택하기 위해 옵티마이저가 여러 통계정보를 활용해서 계산해 낸 값이다. 실측치가 아니므로 실제 수행할 때 발생하는 I/O 또는 시간과 많은 차이가 난다.</p>
<h4 id="옵티마이저-힌트">옵티마이저 힌트</h4>
<p>SQL 옵티마이저는 대부분 좋은 선택을 하는 것이지, 완벽하지는 않다. SQL이 복잡할수록 실수할 가능성도 크다.
그렇기 떄문에 옵티마이저 힌트를 이용해 데이터 액세스 경로를 바꿀 수 있다.
힌트에는 인덱스 명을 입력하면 된다.
아래와 같이 주석 기호를 &#39;+&#39;를 붙이면 된다.</p>
<pre><code>SELECT /*+ INDEX(A A_X01) INDEX(B B_X03)*/
    A.CustomerNm, A.Phone, A.Address, B.OrderId
    FROM Customer AS A
        LEFT OUTER JOIN Order AS B
WHERE A.CustomerID = &#39;Y20251982&#39;</code></pre><p>FROM 절 테이블명 옆에 ALIAS를 지정했다면, 힌트에도 반드시 ALIAS를 사용해야 된다.</p>
<h3 id="sql-공유-및-재사용">SQL 공유 및 재사용</h3>
<p>SQL의 내부 최적화 과정의 복잡성을 알고 나면, 동시성이 높은 온라인 트랜잭션 처리 시스템에서 바인드 변수가 왜 중요한지 자연스럽게 이해하게 될 것이다.</p>
<h4 id="소프트-파싱-vs-하드파싱">소프트 파싱 VS 하드파싱</h4>
<p>SQL 파싱, 최적화, 로우 소스 생성 과정을 거쳐 생성한 내부 프로시저를 반복 재사용할 수 있도록 캐싱해 두는 메모리 공간을 &quot;라이브버리 캐시(Library Cache)&quot;라고 한다. 라이브러리 캐시는 SGA 구성요소다. SGA(System Global Area)는 서버 프로세스와 백그라운드 프로세스가 공통으로 액세스하는 데이터와 제어 구조를 캐싱하는 메모리 공간이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/e26b7d45-7b49-4ebd-b37c-b9e1a4b02392/image.jpeg" alt=""></p>
<p>사용자가 SQL문을 전달하면 DBMS는 SQL을 파싱한 후 해당 SQL이 라이브러리 캐시에 존재하는지부터 확인한다.
캐시에 존재하면 곧바로 실행 단계로 넘어가지만, 찾지 못하면 최적화 단계를 거치게 된다. SQL을 캐시에서 찾아 곧바로 실행단계로 넘어가는 것을 &quot;소프트 파싱(Soft Parsing)&quot;이라 하고, 찾는 데 실패해 최적화 및 로우 소스 생성 단계까지 모두 거치는 것을 &quot;하드 파싱(Hard Parsing)&quot;이라고 한다.</p>
<p>옵티마이저가 SQL을 최적화할 때 많은 일을 수행한다.
예를 들어, 5개의 테이블을 JOIN하는 쿼리문 하나를 최적화하는 데도 무수히 많은 경우의 수가 존재한다. 조인 순서만 고려해도 120(=5!)가지다. 여기에 NL Join, Soft Merge Join, Hash Join 등 다양한 Join 방식이 있다. Full Scan할지, 인덱스를 이용할지를 결정해야 되고, 인덱스 스캔에도 여러 방식이 제공된다.
이렇게 SQL 옵티마이저는 순식간에 엄청나게 많은 연산을 한다. 그 과정에서 옵티마이저가 사용하는 정보는 다음과 같다.</p>
<ul>
<li>테이블, 컬럼, 인덱스 구조에 관한 기본 정보</li>
<li>오브젝트 통계 : 테이블 통계, 인덱스 통계, 히스토그램을 포함한 컬럼 통계</li>
<li>시스템 통계 - CPU 속도, Single Block I/O 속도, MultiBlock I/O 속도 등</li>
<li>옵티마이저 관련 파라미터</li>
</ul>
<p>하나의 쿼리를 수행하는 데 있어 후보군이 될만한 무수히 많은 실행경로를 도출하고, 짧은 순간에 딕셔너리와 통계정보를 읽어 각각에 대한 효율성을 판단하는 과정은 결코 가벼울 수 없다. 
데이터베이스에서 이루어지는 처리 과정은 대부분 I/O 작업에 집중되는 반면, 하드 파싱은 CPU를 많이 소비하는 몇 안 되는 작업 중 하나다.
이렇게 여러운(=hard) 작업을 거쳐 생성한 내부 프로시저를 한 번만 사용하고 버린다면 이만저만한 비효율이 아니다. 라이브러리 캐시가 필요한 이유가 바로 여기에 있다.</p>
<h4 id="이름없는-sql-문제">이름없는 SQL 문제</h4>
<p>사용자 정의 함수/프로시저, 트리거, 패키지 등은 생성할 때부터 이름을 갖고, 컴파일된 상태로 딕셔너리에 저장되며, 사용자가 삭제하지 않는 한 영구적으로 보관된다.
반면, SQL은 이름이 따로 없다. 전체 SQL 텍스트가 이름 역할을한다.
오라클, SQL Server 같은 DBMS는 이름없는 SQL을 영구 저장하지 않는다.
그 이유는, 일회성 또는 무효화된 SQL까지 모두 저장하려면 많은 공간이 필요하고, 그만큼 SQL을 찾는 속도도 느려진다.</p>
<h3 id="데이터-저장-구조-및-io-매커니즘">데이터 저장 구조 및 I/O 매커니즘</h3>
<p>I/O 튜닝이 곧 SQL 튜닝이라고 해도 과언이 아니다.</p>
<h4 id="sql이-느린-이유">SQL이 느린 이유</h4>
<p>SQL이 느린 이유는 십중팔구 디스크 I/O 때문이다.
&quot;I/O = 잠(Sleep)&quot;이라고 설명한다. OS 또는 I/O 서브시스템이 I/O를 처리하는동안 프로세스는 잠을 자기 때문이다. 프로세스가 일하지 않고 잠을 자는 이유는 여러 가지가 있지만, I/O가 가장 대표적이고 절대 비중을 차지한다.</p>
<p><del>대부분 대학교 3학년 떄 죽어라 하는 운영체제 PTSD가 올 것이다.</del></p>
<p>프로세스는 실행 중인 프로그램이며, 다음과 같은 생명주기를 가진다.
생성 이후 종료 전까지 준비와 실행과 대기 상태를 반복한다. 프로세스는 Interrupt에 의해 수시로 실행 준비 상태로 전환했다가 다시 실행 상태로 전환한다. 여러 프로세스가 하나의 CPU를 공유할 수 있지만, 특정 순간에는 하나의 프로세스만 CPU를 사용할 수 있기 떄문에 이런 매커니즘이 필요하다.
<img src="https://velog.velcdn.com/images/diense_kk/post/105c0adf-f98d-4376-9acd-1a9b220c3b89/image.png" alt=""></p>
<p>프로세스가 디스크에서 데이터를 읽어야 할 때는 CPU를 OS에 반환하고 잠시 수면 상태에서 I/O가 완료되기를 기다린다. 정해진 OS 함수를 호출하고 CPU를 반환한 채 알람을 설정하고 대기 큐에서 잠을 자는 것이다. 이러한 이유로 I/O가 많으면 성능이 느린 것이다.</p>
<h3 id="table-full-scan-vs-index-range-scan">Table Full Scan VS Index Range Scan</h3>
<p>테이블에 저장된 데이터를 읽는 방식은 두 가지다. 테이블 전체를 스캔해서 읽는 방식과 인덱스를 이용해서 읽는 방식이다. 
인덱스를 이용한 테이블 액세스는 인덱스에서 &quot;일정량&quot;을 스캔하면서 얻은 ROWID로 테이블 레코드를 찾아가는 방식이다. ROWID는 테이블 레코드가 디스크 상에 어디 저장됐는지를 가리키는 위치 정보다.</p>
<p>한 번에 많은 데이터를 처리하는 집계용 SQL과 배치 프로그램은 인덱스를 사용할 경우 SQL 성능을 떨어뜨린다.
그래서 이들 프로그램에서 사용하는 SQL은 온라인 트랜잭션 처리 시스템에서 사용하는 SQL보다 튜닝하기가 비교적 쉽다. 상당수가 Table Full Scan으로 유도하면 성능이 빨라진다. 조인을 포함한 SQL이면, 조인 메소드로 해서 조인을 선택해주면 된다.</p>
<h4 id="인덱스를-이용하는데-왜-성능이-더-느릴까">인덱스를 이용하는데 왜 성능이 더 느릴까?</h4>
<p>Table Full Scan은 시퀀셜 액세스와 MultiBlock I/O 방식으로 디스크 블록을 읽는다. 한 블록에 속한 모든 레코드를 한 번에 읽어 들이고, 캐시에서 못 찾으면 &quot;한 번의 수면을 통해 인접한 수십 수백 개 블록을 한꺼번에 I/O하는 매커니즘&quot;이다. 이 방식을 사용하는 SQL은 스토리지 스캔 성능이 좋아지는 만큼 성능도 좋아진다.</p>
<p>시퀀셜 액세스와 MultiBlock I/O가 아무리 좋아도 수십 수백 건의 소량 데이터를 찾기 위해 수백만 수천만 건 데이터를 스캔하는 건 비효율적이다. 큰 테이블에서 소량 데이터를 검색할 때는 반드시 인덱스를 이용해야 된다.
Index Range Scan을 통한 테이블 액세스는 랜덤 액세스와 Single Block I/O 방식으로 디스크 블록을 읽는다. 캐시에서 블록을 못 찾으면 레코드 하나를 읽기 위해 매번 잠을 자는 I/O 매커니즘이다. 따라서 많은 데이터를 읽을 때는 Table Full Scan보다 불리하다.</p>
<p>인덱스는 큰 테이블에서 아주 작은 일부 데이터를 빨리 찾기 위한 도구일 뿐이므로 모든 성능 문제를 인덱스로 해결하려 해서는 안된다. 읽을 데이터가 일정량을 넘으면 인덱스보다 Table Full Scan이 유리하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSSQL Execution Plan, DB Architecture]]></title>
            <link>https://velog.io/@diense_kk/MSSQL-Execution-Plan-DB-Architecture</link>
            <guid>https://velog.io/@diense_kk/MSSQL-Execution-Plan-DB-Architecture</guid>
            <pubDate>Sat, 12 Jul 2025 08:45:27 GMT</pubDate>
            <description><![CDATA[<h3 id="execution-plan실행계획">Execution Plan(실행계획)</h3>
<p>실행계획은 SQL Server 엔진이 쿼리를 어떻게 실행할지를 결정하고 설명한 &quot;실행 시나리오&quot;이다.
비유하자면, 옵티마이저는 대본을 쓰는 감독(혹은 작가), SQL Server 엔진은 그 대본을 따라 연기하는 배우이다.</p>
<p>구문분석 &gt; 표준화 &gt; 최적화 &gt; 컴파일 &gt; 실행
위 프로세스는 쿼리 처리 과정이다.</p>
<p>실행계획은 최적화 단계에서 통계, 조각정보 등을 바탕으로 만들어 지고 이때 만들어지는 플랜을 재사용을 위해 플랜 캐시하여, 실행계획은 <strong>실행계획</strong>과 <strong>실제 실행계획</strong>으로 구분된다.</p>
<blockquote>
<p>예상 실행계획은 이전에 생성된 통계정보를 바탕으로 플랜을 구성하고, 실제 실행계획은 현재 상태의 통계정보를 바탕으로 플랜을 구성한다.</p>
</blockquote>
<p>실행계획은 다음의 경우 기존의 실행계획을 사용하지 않고 새로운 실행계획을 생성한다.</p>
<ul>
<li>쿼리에서 참조하는 테이블이나 뷰가 ALTER된 경우</li>
<li>단일 프로시저가 ALTER된 경우, 이 경우 해당 프로시저의 모든 계획이 캐시에서 삭제된다.</li>
<li>실행계획에 사용되는 인덱스가 변경, 삭제 된 경우</li>
<li>UPDATE STATISTICS 등의 명령문에서 명시적으로 생성되거나 자동으로 생성되어 실행계획에 사용되는 통계가 업데이트 된 경우</li>
<li>SP_RECOMPILE에 대한 명시적 호출이 있던 경우</li>
</ul>
<p><strong>UPDATE STATISTICS</strong>는 특정 테이블의 통계 정보를 업데이트 하는 것으로, 데이터베이스가 알맞은 인덱스를 선택하도록 관리하는 작업이다.
테이블 통계 자료는 데이터베이스 테이블과 관련된 정보를 분석하고 수집하는 것으로, 주로 SQL 성능 최적화에 활용된다.
이러한 정보는 데이터베이스 옵티마이저가 최적의 실행계획을 수립하는 데 사용되며, 데이터의 양, 테이블 구조, 인덱스 정보 등을 포함한다. 테이블 통계 자료가 부정확하거나 오래되면 쿼리 성능 저하의 원인이 될 수 있으므로 주기적인 갱신이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/35f4474b-f75d-4cf1-994e-51053ca5b0e1/image.png" alt="">
실행계획은 위에서 아래로, 오른쪽에서 왼쪽으로 확인한다.
이는 쿼리가 실행되는 순서이다.
실행계획의 노드를 선택하면 다음과 같은 속성을 확인 할 수 있다.</p>
<table>
<thead>
<tr>
<th>속성</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td>Physical Operation</td>
<td align="left">논리 연산자의 지시에 따라 연산을 구현하는 연산자이다. 모든 물리 연산자는 일반적으로 작업을 수행하는 개체이다. Clustered Index Scan, Index Seek 등이 있다.</td>
</tr>
<tr>
<td>Logical Operation</td>
<td align="left">이 연산자는 쿼리를 처리하는 데 사용되는 실제 대수 연산을 설명한다. Right Anti, Semi Join, Hash Join 등이 있다.</td>
</tr>
<tr>
<td>Estimated Execution Mode</td>
<td align="left">Actual Execution Mode와 유사하나 추정값을 보여준다.</td>
</tr>
<tr>
<td>Storage</td>
<td align="left">쿼리 최적화 프로그램이 쿼리에 의해 추출되는 결과를 저장하는 방법을 알려준다.</td>
</tr>
<tr>
<td>Estimated I/O Cost</td>
<td align="left">결과 집합의 입출력 작업 비용을 알려준다.</td>
</tr>
<tr>
<td>Estimated Number of Executions</td>
<td align="left">Number of Executions와 유사하지만 추정 값이다.</td>
</tr>
<tr>
<td>Object</td>
<td align="left">작업이 수행되는 테이블을 나타낸다.</td>
</tr>
<tr>
<td>Estimated Number of Rows Per Execution</td>
<td align="left">옵티마이저가 연산자에 의해 반환될 것이라고 생각하는 행 수를 나타낸다.</td>
</tr>
<tr>
<td>Estimated Number of Rows to be Read</td>
<td align="left">옵티마이저가 운영자가 읽을 것이라고 생각하는 행 수를 나타낸다.</td>
</tr>
<tr>
<td>Estimated Number of Rows for All Executions</td>
<td align="left">Number of Executions 와도 유사하지만 추정 값이다.</td>
</tr>
<tr>
<td>Estimated Row Size</td>
<td align="left">연산자의 각 행에 대한 저장 크기이다.</td>
</tr>
<tr>
<td>Estimated Rebinds</td>
<td align="left">반복 실행되는 연산자(EX) Nested Loops)의 외부 참조 값이 변경되어 다시 바인딩되는 예상 횟수를 나타낸다. Rebind는 루프의 각 반복마다 외부 값이 변경될 때 발생한다.</td>
</tr>
<tr>
<td>Estimated Rewinds</td>
<td align="left">외부 참조 값이 변경되지 않고 반복 실행될 때(즉, 재사용되는 경우)의 예상 횟수를 나타낸다. Nested Loops와 같은 연산자에서 내부 쿼리를 반복 실행할 때 발생한다.</td>
</tr>
<tr>
<td>Defined Values</td>
<td align="left">해당 연산자가 정의(생성)하는 컬럼이나 표현식 값을 보여준다.</td>
</tr>
<tr>
<td>Output List</td>
<td align="left">해당 연산자가 출력하는 컬럼 목록을 나타낸다. 이 정보는 상위 연산자에게 전달된다.</td>
</tr>
<tr>
<td>Parallel</td>
<td align="left">연산자가 병렬 실행계획의 일부인지 여부를 나타낸다. 병렬 실행이 가능하면 &quot;True&quot; 또는 병렬 스레드 수가 표시된다.</td>
</tr>
<tr>
<td>Ordered</td>
<td align="left">작업을 수행할 데이터 세트가 정렬된 상태인지 여부를 결정한다.</td>
</tr>
<tr>
<td>Forced Index</td>
<td align="left">인덱스 힌트 또는 옵티마이저 지시에 따라 강제로 사용된 인덱스를 나타낸다. 강제되지 않은 경우 비어있을 수도 있다.</td>
</tr>
<tr>
<td>Node ID</td>
<td align="left">오른쪽에서 왼쪽, 위에서 아래로 읽는 Execution Plan에서 오퍼레이터가 호출된 순서대로 번호를 자동 할당한다.</td>
</tr>
<tr>
<td>Table Cardinality</td>
<td align="left">테이블의 전체 행 수를 나타낸다. 쿼리 최적화 시 통계 기반으로 사용된다.</td>
</tr>
<tr>
<td>Force Scan</td>
<td align="left">옵티마이저가 인덱스 사용 대신 전체 테이블 또는 전체 인덱스 스캔을 강제하도록 설정되었는지를 나타낸다.</td>
</tr>
<tr>
<td>NoExpandHint</td>
<td align="left">뷰에 대해 NOEXPAND 힌트가 사용되어 뷰가 확정되지 않고 그대로 사용되었는지를 나타낸다. 인덱스가 뷰를 사용할 떄 유용하다.</td>
</tr>
</tbody></table>
<h3 id="sql-server-실행계획-연산자">SQL Server 실행계획 연산자</h3>
<p>옵티마이저가 실행계획을 결정할 때 사용하는 <strong>“물리적 연산(Physical Operation)”</strong>이다.</p>
<h4 id="옵티마이저가-물리적-연산을-선택하는-기준">옵티마이저가 물리적 연산을 선택하는 기준</h4>
<table>
<thead>
<tr>
<th>기준</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>비용 기반 최적화</strong></td>
<td>CPU, I/O, 메모리 등 연산 비용을 계산해 총 비용이 가장 낮은 실행계획을 선택한다.</td>
</tr>
<tr>
<td><strong>통계 정보</strong></td>
<td>데이터 분포, 행 수, 선택도(selectivity) 등을 참고해 효율적인 연산자를 판단한다.</td>
</tr>
<tr>
<td><strong>필터 조건(WHERE 절)</strong></td>
<td>조건절이 인덱스 키와 얼마나 일치하는지, 인덱스가 필터링에 얼마나 효과적인지 평가한다.</td>
</tr>
<tr>
<td><strong>커버링 인덱스 여부</strong></td>
<td>인덱스가 쿼리에서 필요한 모든 컬럼을 포함하면 추가 조회 없이 인덱스만으로 작업이 가능해 비용이 절감된다.</td>
</tr>
<tr>
<td><strong>테이블 크기</strong></td>
<td>작은 테이블일 경우, 인덱스 사용보다 전체 스캔이 더 빠를 수 있다.</td>
</tr>
<tr>
<td><strong>병렬 처리 가능성</strong></td>
<td>병렬 처리로 작업이 빠르게 수행될 수 있는 연산자를 선택한다.</td>
</tr>
<tr>
<td><strong>메모리 및 시스템 부하</strong></td>
<td>시스템 상태나 메모리 상황에 따라 실행계획을 달리 선택할 수 있다.</td>
</tr>
<tr>
<td><strong>힌트 및 옵션</strong></td>
<td>사용자가 지정한 힌트가 있으면 옵티마이저가 이를 우선적으로 반영한다.</td>
</tr>
</tbody></table>
<h3 id="물리적-연산자의-종류">물리적 연산자의 종류</h3>
<h4 id="테이블-스캔table-scan">테이블 스캔(Table Scan)</h4>
<p>생성된 SQL 쿼리 실행계획에서 SQL Server 엔진이 데이터를 검색하기 위해 Table Scan 연산자를 사용하여 모든 전체 테이블 행을 스캔한다는 것을 의미한다. 
SQL Server 엔진은 WHERE 절을 추가하여 특정 레코드 집합을 가져오려고 할 때 해당 테이블에 생성된 인덱스가 없으면 Table Scan 연산자를 사용하여 모든 전체 테이블 행을 스캔한다.</p>
<blockquote>
<p>옵티마이저는 이미 캐시에 실행계획이 있다면 그 계획을 그대로 사용한다.
만약 캐시에 실행계획이 없다면 옵티마이저가 새 계획을 만드는데,
아래와 같은 경우 인덱스를 사용하지 않고 테이블 스캔을 선택할 수 있다.</p>
</blockquote>
<ul>
<li>인덱스가 유용하지 않은 경우
인덱스가 있어도 <strong>선택도(필터링 효과)</strong>가 낮은 경우이다.</li>
<li>테이블에 적은 수의 행이 포함된 경우
테이블 자체가 작아서 인덱스를 타는 것보다 그냥 한 번에 다 읽는 게 더 빠르다.</li>
<li>쿼리가 대부분의 행을 반환하는 경우
인덱스를 통해 조건을 걸더라도 어차피 대부분의 데이터를 읽어야 한다면, 여러 번 랜덤 I/O를 하는 인덱스 탐색보다 한 번에 쭉 읽는 테이블 스캔이 더 빠르다.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>클러스터형 인덱스 스캔 (Clustered Index Scan)</strong></td>
<td>클러스터형 인덱스를 <strong>처음부터 끝까지</strong> 쭉 읽음<br>→ 조건 없이 전체 읽기거나, 조건이 인덱스 필터로 적합하지 않을 때</td>
</tr>
<tr>
<td><strong>클러스터형 인덱스 검색 (Clustered Index Seek)</strong></td>
<td>클러스터형 인덱스를 <strong>조건에 따라 빠르게 탐색</strong><br>→ 주로 기본키나 조건절이 인덱스 키와 일치할 때</td>
</tr>
<tr>
<td><strong>비클러스터형 인덱스 스캔 (Nonclustered Index Scan)</strong></td>
<td>비클러스터 인덱스를 처음부터 끝까지 순차적으로 스캔<br>→ 부분적으로 조건을 거는 경우, 또는 커버링 쿼리</td>
</tr>
<tr>
<td><strong>비클러스터형 인덱스 검색 (Nonclustered Index Seek)</strong></td>
<td>비클러스터 인덱스를 통해 조건에 맞는 데이터 위치를 <strong>빠르게 찾음</strong></td>
</tr>
<tr>
<td><strong>RID 조회 (RID Lookup)</strong></td>
<td>비클러스터형 인덱스로 찾은 행이 Heap(클러스터 인덱스 없는 테이블)에 있을 때, 해당 Row ID를 사용해 본문 데이터를 조회</td>
</tr>
<tr>
<td><strong>키 조회 (Key Lookup)</strong></td>
<td>비클러스터형 인덱스로는 찾을 수 없는 나머지 컬럼을 <strong>클러스터형 인덱스를 통해 추가 조회</strong>하는 연산<br>→ 주 테이블로부터 나머지 정보 읽기 (Bookmark Lookup이라고도 불림)</td>
</tr>
</tbody></table>
<h3 id="sql-server-architecture">SQL Server Architecture</h3>
<p>MSSQL은 기본적으로 클라이언트-서버 아키텍처이다.
MSSQL의 프로세스는 클라이언트 Application이 Request를 보내는 것으로 시작된다.
이 요청은 MSSQL과 Client 간에 연결된 네티워크 인터페이스를 통해 들어온다.
SQL Server는 처리된 데이터를 가지고 Acceptance, Processing, Reponse한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/dd41fcb2-1a1e-4919-b00b-6bf7d537851a/image.png" alt="">
SQL Server Architecture Diagram</p>
<p>SQL Server의 다이어그램은 크게 3가지의 주요 모듈로 이루어져있다.</p>
<h4 id="프로토콜-계층---sni">프로토콜 계층 - SNI</h4>
<p>MSSQL Server 프로토콜 계층은 3가지 유형의 클라이언트 서버 아키텍처를 지원한다.</p>
<ul>
<li><strong>공유 메모리(Shared Memory)</strong>
클라이언트와 MSSQL Server는 동일한 시스템에서 실행된다. 둘 다 공유 메모리 프로토콜을 통해 통신할 수 있다.
로컬 개발 또는 테스트 개발에서 사용한다.</li>
<li><strong>TCP/IP</strong>
클라이언트와 MSSQL 서버는 원격이며 별도의 시스템에 설치된다.
가장 일반적이고 보편적인 통신 프로토콜이다.</li>
<li><strong>Named Pipe</strong>
클라이언트와 MSSQL Server는 LAN을 통해 연결된다.
Named Pipe에서 구성 및 설치데스크 옵션이 기본적으로 비활성화되어 있으며, SQL 구성 관리자에서 활성화해야 된다. 클라리언트와 서버가 동일한 LAN에 있을 때 사용되며, TCP 445 포트를 사용한다. TCP/IP 프로토콜이 없는 환경에서는 사용할 수 없다.</li>
<li><strong>TDS</strong>
TDS는 테이블 형식의 데이터 스트림을 나타낸다.
3가지 프로토콜 모두 TDS 패킷을 사용한다. TDS는 네트워크 패킷에 캡슐화된다. 이를 통해 클라리언트 컴퓨터에서 서버 컴퓨터로 데이터를 전송할 수 있다.</li>
</ul>
<h4 id="관계형-엔진relational-engine">관계형 엔진(Relational Engine)</h4>
<p>관계형 엔진은 Query Processor라고도 불린다.
사용자가 작성한 SQL 쿼리를 분석, 최적화, 실행 계획 생성 및 실행 요청하는 역할을 한다.
쿼리가 수행해야 할 작업이 무엇인지 파악하고, 이를 <strong>가장 효율적으로 수행할 수 있는 방법(실행계획)</strong>을 결정하는 SQL Server의 핵심 구성 요소이다.</p>
<ul>
<li><strong>CMD Parser</strong>
프로토콜 계층에서 수신된 데이터는 관계형 엔진으로 전달된다. CMD Parser는 쿼리 데이터를 수신하는 관계형 엔진의 첫 번째 구성요소이다. CMD Parser의 주요 작업은 구문 및 의미 오류에 대한 쿼리를 확인하는 것이다. 그리고는 쿼리 트리를 생성한다.</li>
</ul>
<p><strong>구문검사</strong>
다른 프로그래밍 언어와 마찬가지로 MSSQL에는 사전 정의된 키워드 세트가 있다. 또한 SQL Server에는 SQL Server가 이해하는 자체 문법이 있다.
SELECT, INSERT, UPDATE 및 기타 다수의 문법은 MSSQL 사전 정의 키워드 목록에 속한다.
CMD Parser는 구문 검사를 수행하고 사용자 입력이 언어 구문이나 문법 규칙을 따르지 않으면 오류를 반환한다.</p>
<p><strong>의미검사</strong>
의미검사는 Normalizer에 의해 수행된다.
가장 간단한 형태로 조회 중인 Column명, Table명이 Schema에 존재하는지 확인한다. 존재하는 경우 쿼리에 Binding한다.
사용자 쿼리에 View가 포함되면 복잡성이 증가한다. Normalizer는 내부적으로 저장된 뷰 정의 등으로 대체를 수행한다.</p>
<p><strong>쿼리 트리 생성</strong>
쿼리를 실행할 수 있는 다른 실행 트리를 생성하는 단계이다.
다른 모든 트리는 동일의 원하는 출력을 가진다.</p>
<ul>
<li><strong>Optimizer</strong>
최적화 프로그램의 작업은 사용자 쿼리에 대한 실행 또는 계획을 만드는 것이다.</li>
</ul>
<p>쿼리 비용은 CPU 사용량, 메모리 사용량 및 입출력 요구와 같은 조건을 기반으로 계산된다.
MSSQL 옵티마이저는 내장된 Exhaustive/Heuristic 알고리즘에서 작동한다. 목표는 쿼리 시간을 최적화하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/1138eecd-30af-4196-bd6c-5a7cd7ca8b52/image.png" alt=""></p>
<p><strong>Phase0 - 일반적인 계획 탐색</strong>
사전 최적화 단계라고도 한다.
어떤 경우에는 일반적인 계획으로 알려진 실행 가능한 계획이 하나만 있을 수도 있다.
그 이유는 더 많이 검색하면 동일한 런타임 실행 계획을 찾을 수 있기 때문이다.
전혀 필요하지 않은 최적화된 계획을 찾는데 추가 비용이 발생하기 때문에 최적화된 계획을 만들 필요가 없다.
어떠한 계획도 찾을 수 없는 경우 Phase1이 시작된다.</p>
<p><strong>Phase1 - 트랜잭션 처리 탐색 계획</strong>
여기에는 단순 및 복합 계획 검색이 포함된다.
단순 계획 검색 - 쿼리에 관련된 컬럼 및 인덱스의 과거 데이터를 통계 분석에 사용한다. 이는 보통 테이블당 하나의 인덱스로 구성되지만 이에 제한되지는 않는다.
그래도 단순 계획이 없으면 더 복잡한 계획이 검색된다. 테이블당 다중 인덱스를 포함한다.</p>
<p><strong>Phase2 - 병렬 처리 및 최적화</strong>
위의 전략 중 어느 것도 작동하지 않으면 옵티마이저는 병렬 처리 가능성을 검색한다. 이것은 기계의 처리 능력과 구성에 따라 다르다.
그래도 가능하지 않으면 최종 최적화 단계가 시작된다. 최종 최적화 목표는 최상의 방법으로 쿼리를 실행하기 위해 가능한 다르 모든 옵션을 찾는 것이다. 최종 최적화 단계의 알고리즘은 MicroSoft 소유이다.</p>
<ul>
<li><strong>쿼리 실행기(Query Executor)</strong>
옵티마이저가 생성한 실행계획을 이용하여 단계별로 쿼리를 실행하는 부분이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/92a4ebd6-3efe-40f0-a633-63907cf88d0d/image.png" alt=""></li>
</ul>
<p>쿼리 실행자는 액세스 방법을 호출한다. 실행에 필요한 SELECT 로직에 대한 실행 계획을 제공한다. Storage Engine에서 데이터를 수신하면 결과가 프로토콜 계층에 게시된다.</p>
<h4 id="storage-engine">Storage Engine</h4>
<p>스토리지 엔진의 작업은 디스크 또는 SAN과 같은 스토리지 시스템에 데이터를 저장하고 필요할 때 데이터를 검색하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/4241db56-1175-4528-9afe-f7516f98b288/image.jpeg" alt=""></p>
<p>데이터 파일은 물리적으로 데이터 페이지의 형태로 데이터를 저장하며 각 데이터 페이지 크기는 8KB이다. SQL Serever에서 가장 작은 저장 단위를 형성한다. 이러한 데이터 페이지는 논리적으로 그룹화되어 익스텐트를 형성한다. (페이지 8개가 모여 1개의 익스텐트를 만든다.)
페이지에는 페이지 유형, 페이지 번호, 사용된 공간 크기, 여유 공간 크기 및 다음 페이지 및 이전 페이지에 대한 포인터와, 같은 페이지에 대한 데이터 정보를 전달하는 96Byte 크기의 페이지 헤더라는 섹션 등이 있다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/e23b5966-7652-4ca0-8dc0-b94279a5577e/image.png" alt=""></p>
<ol>
<li><p>기본파일
모든 데이터베이스에는 하나의 기본 파일이 있다.
테이블, 뷰, 트리거 등과 관련된 모든 중요한 데이터를 저장한다.
확장자 - .mdf</p>
</li>
<li><p>보조파일
데이터베이스는 여러 개의 보조 파일을 포함할 수 있고 포함하지 않을 수도 있다.
선택사항이며 사용자별 데이터를 포함한다.
확장자 - .ndf</p>
</li>
<li><p>로그파일
미리 스기 로그라고도 한다.
트랜잭션 관리에 사용된다.
원치 않는 인스턴스에서 복구하는데 사용된다. 커밋되지 않은 트랜잭션으로 롤백하는 중요한 작업을 수행한다.
확장자 - .ldf</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQL] 인덱스(Index)]]></title>
            <link>https://velog.io/@diense_kk/SQL-%EC%9D%B8%EB%8D%B1%EC%8A%A4Index</link>
            <guid>https://velog.io/@diense_kk/SQL-%EC%9D%B8%EB%8D%B1%EC%8A%A4Index</guid>
            <pubDate>Sat, 21 Jun 2025 08:33:58 GMT</pubDate>
            <description><![CDATA[<p>2025년 01월 중견기업 ERP 개발자로 취업했다.
DB를 다룰 일이 상당히 많다.
SQLD &gt; SQLP 순으로 준비하며, DBA쪽으로 나아갈까도 생각중이다. <del>아니면 SAP?</del>
곧 해외 지사 ERP와 본사(한국) ERP를 통합하는 글로벌 ERP 프로젝트가 시작될 예정이며, 이에 따라 데이터베이스 역시 통합될 예정이다.</p>
<h2 id="index">INDEX</h2>
<p>인덱스란 추가적인 쓰기 작업과 저장 공간을 활용하여 DB 테이블의 검색 속도를 향상시키기 위한 자료구조이다.
우리가 책에서 원하는 내용을 찾으려고 책의 모든 페이지를 찾아 보는 것은 오랜 시간이 걸린다.
그렇기 때문에 책의 저자는 책의 맨 앞 또는 맨 뒤에 색인을 추가하는데, DB의 인덱스는 책의 색인과 같다.
DB에서 하나의 데이터를 찾기 위해 테이블을 Full-Scan 하면 시간이 오래 걸리기 때문에 데이터와 데이터의 위치를 포함한 자료구조를 생성하여 빠르게 조회할 수 있도록 돕고있다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/054686e3-b2a7-472d-a462-9cfe4c5a953c/image.jpeg" alt=""></p>
<p>인덱스를 사용하면, 데이터를 조회하는 SELECT 외에도 UPDATE나 DELETE의 성능이 함께 향상된다. UPDATE, DELETE도 해당 작업을 수행하기 전에 데이터를 조회하는 선행 작업을 수행하기 때문이다.</p>
<p>만약 Index를 사용하지 않은 컬럼을 조회해야 하는 상황이라면 전체를 탐색하는 Full Scan을 수행해야 된다. Full Scan은 이름 그대로 테이블 전체를 탐색하기 때문에 처리 속도가 느리다.</p>
<p>DBMS는 Index를 항상 최신의 정렬된 상태로 유지해야 원하는 값을 빠르게 탐색할 수 있다. 그렇기 때문에 인덱스가 적용된 컬럼에 INSERT, UPDATE, DELETE가 수행된다면 각각 다음과 같은 연산을 추가적으로 해주어야 하며 그에 따른 오버헤드가 발생한다.
INSERT - 새로운 데이터에 대한 인덱스를 추가한다.
DELETE - 실제로 삭제하는 데이터의 인덱스를 삭제하지 않고, 사용하지 않음 처리한다.
UPDATE - 기존의 인덱스를 사용하지 않음 처리하고, 갱신된 데이터에 대한 인덱스를 추가한다.
이러한 이유로 인덱스를 설정할 때에는, 변경이 잦지 않은 속성에 설정을 하는 것이 적절하다.</p>
<p>만약 INSERT, DELETE, UPDATE가 빈번한 속성에 인덱스를 걸게 되면 인덱스의 크기가 비대해져, 성능이 저하되는 역효과가 발생한다. 주요 원인은 DELETE, UPDATE이다.
앞에서 설명한대로, UPDATE와 DELETE는 기존의 인덱스를 삭제하지 않고 <strong>사용하지 않음</strong> 처리를 하기 때문이다.
만약, 어떤 테이블에 UPDATE, DELETE가 빈번하게 발생된다면 실제 데이터는 10만건이지만 인덱스는 훨씬 많이 존재하게 되어 SQL문 처리 시 비대해진 인덱스에 의해 오히려 성능이 떨어지게 될 것이다.</p>
<h4 id="인덱스의-장단점">인덱스의 장/단점</h4>
<p>인덱스를 사용한다면 테이블을 조회하는 속도와 그에 따른 성능을 향상시킬 수 있으며, 전반적인 시스템의 부하를 줄일 수 있다.
반면 인덱스를 관리하기 위해 DB의 약 10%의 저장공간이 필요하다. 또한, 인덱스를 관리하기 위해 추가 작업이 필요하며, 인덱스를 잘못 사용할 경우 오히려 성능이 저하되는 역효과가 발생할 수 있다.</p>
<h4 id="인덱스-사용이-적절한-case">인덱스 사용이 적절한 CASE</h4>
<ol>
<li>규모가 작지 않은 테이블
규모가 작은 테이블(기준정보 테이블)에 조회를 하는 경우, 인덱스가 설정된 것과 Full Scan을 하는 것에는 큰 차이가 없다. 결과적으로는 불필요하게 DB의 저장공간만을 차지하게 된다.</li>
<li>INSERT, DELETE가 자주 발생하지 않는 속성
앞서 얘기한 내용이다.</li>
<li>JOIN, WHERE 또는 ORDER BY에 자주 사용되는 컬럼
컬럼 입장에서는 테이블의 레코드는 순서가 없이 저장된다. 이때 Where절의 특정 조건에 맞는 데이터를 찾기 위해서는 Full Scan을 하면서 조건에 부합하는지 비교해야 된다. 하지만, Index는 데이터가 정렬되어 있기 때문에 Where절의 조건에 맞는 데이터를 빠르게 찾아낼 수 있다.
또한, 인덱스를 사용하지 않을 경우 전체 테이블을 대상으로 Order By에 의한 정렬을 해야 된다. 하지만 인덱스를 사용할 경우 이미 정렬되어 있기 때문에 정렬에 필요한 자원을 소모할 필요가 없다.</li>
</ol>
<h3 id="인덱스index의-자료구조">인덱스(Index)의 자료구조</h3>
<p>인덱스를 구현하기 위해서는 다양한 자료구조를 사용할 수 있는데, 가장 대표적으로 해시 테이블과 B+Tree가 있다.</p>
<h4 id="해시-테이블hash-table">해시 테이블(Hash Table)</h4>
<p>해시 테이블(Key, Value)로 데이터를 저장하는 자료구조 중 하나로 빠른 데이터 검색이 필요할 때 유용하다. 해시테이블은 Key값을 이용해 고유한 Index를 생성하여 그 Index에 저장된 값을 꺼내오는 구조이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/e5e358ab-9908-489a-abc8-835f84084ea4/image.png" alt=""></p>
<p>해시 테이블 기반의 DB 인덱스는 데이터 -&gt; 컬럼의 값, 데이터의 위치를 Key, Value로 사용하여 컬럼의 값으로 생성된 해시를 통해 인덱스를 구현하였다. 키로 인덱스의 위치를 찾을 수 있는 해시 테이블의 시간복잡도는 O(1)이다.
하지만, DB 인덱스에서 해시 테이블이 사용되는 경우는 매우 제한적이다. 그러한 이유는 해시가 등호(=)에만 특화되었기 때문이다. 해시 함수는 값이 1이라도 달라지면 완전히 다른 해시 값을 생성하는데, 이러한 특성에 의해 부등호 연산이 자주 사용되는 DB 검색에는 해시 테이블이 적합하지 않다.
이러한 이유로 DB의 Index에는 B+Tree가 일반적으로 사용된다.</p>
<h4 id="btree">B+Tree</h4>
<p>B+Tree는 DB의 인덱스를 위해 자식 노드가 2개 이상인 B-Tree를 개선시킨 자료구조이다. B+Tree는 모든 노드에 데이터를 저장했던 B-Tree와 다른 특성을 가지고있다.</p>
<p><strong>B+Tree의 특성</strong></p>
<ul>
<li>Leaf Node(데이터 노드)만 인덱스와 함께 데이터를 가지고 있고, other Nodes(인덱스 노드)들은 데이터를 위한 인덱스만을 갖는다.</li>
<li>Leaf Node들은 LinkedList로 연결되어 있다.</li>
<li>데이터 노드 크기는 인덱스 노드의 크기와 같지 않아도 된다.</li>
</ul>
<p>앞서 말 했듯이 DB의 인덱스 컬럼은 부등호를 이용한 순사 검색 연산이 자주 발생된다. 이러한 이유로 B+Tree의 리프노드는 서로 LinkedList로 연결되어, 형제 노드끼리도 옮겨가며 조회할 수 있다. 연결된 리프노드의 리스트를 따라가면서 범위 쿼리를 할 수 있어서 범위 검색 성능이 있다.
물론, B-Tree의 BEST CASE에 대해 리프노드까지 가지 않아도 탐색할 수 있는 것에 비해 B+Tree는 무조건 리프노드까지 가야되는 단점이 있다.
이러한 이유로 B+Tree는 O(𝑙𝑜𝑔2𝑛)의 시간복잡도를 갖지만 해시테이블보다 인덱싱에 적합한 자료구조이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/2e5cbbbc-56e5-47c2-bdf5-77e906ee8e4f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/28cd6f79-a688-4a31-bf48-9d69edf126ac/image.png" alt=""></p>
<p>Internal노드에는 데이터의 포인터가 없으며, 오로지 키만 저장된다.</p>
<ul>
<li>데이터를 찾기 위한 포인터도 리프노드에만 있다.</li>
<li>Internal 노드의 크기를 줄여 메모리 사용이 효율적이다.
B+Tree의 모든 노드의 키는 항상 정렬된 상태를 유지한다.</li>
<li>Internal 노드의 키와 Leaf Node의 키는 모두 오름차순 정렬되어 있다.
새로운 데이터의 삽입 및 삭제가 비교적 간단하다.</li>
<li>INSERT 시에는 Leaf Node에 새로운 데이터를 추가한다.</li>
<li>DELETE 시에는 데이터를 제거하면서 B+Tree의 균현을 유지하고, 저장공간이 절약된다.</li>
</ul>
<h3 id="clustered-index-non-clustered-index">Clustered Index, Non-Clustered Index</h3>
<h4 id="clustered-index">Clustered Index</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/874e5513-217a-45ab-9709-ab182def014a/image.jpg" alt=""></p>
<p>Clustered Index는 테이블의 레코드를 지정된 컬럼에 대해 물리적으로 재배열한다. Clustered Index는 테이블 당 한 개만 존재할 수 있고, PK 제약조건을 지정하는 컬럼에 대해 자동으로 생성된다. 그렇기 때문에 우리가 일반적으로 테이블을 생성할 때 특정 컬럼에 PK 제약조건을 지정한다면, 데이터가 자동으로 정렬되는 것이다.
Clustered Index를 생성한 컬럼을 기준으로 테이블의 데이터가 정렬되어 있기 때문에 속도면에서 우수한 성능을 보인다. 하지만, 데이터의 추가/수정/삭제 시 매번 레코드를 정렬해야 하기 때문에 추가/수정/삭제의 성능이 저하된다.</p>
<h4 id="clustered-index의-문제점">Clustered Index의 문제점</h4>
<p>만약 ID값이 1,3,4인 데이터를 가지고 있는 상태에서 ID값이 2인 데이터를 추가 저장한다고 생각해보자.
Clustered Index는 지정된 컬럼을 기준으로 데이터를 정렬하기 때문에 ID값이 2인 데이터를 추가할 경우 ID가 2보다 큰 데이터는 한 칸씩 아래로 이동하고, 2번째 위치에 데이터가 추가된다.
이 예시에서는 데이터 2개만 뒤로 밀려나지만, 데이터가 100만 건이 있다고 생각해보면 INSERT에 소모되는 비용이 굉장히 크다.
그렇기 때문에 PK를 어떤 컬럼으로 선택하는가에 따라 DB의 성능이 좌우된다.
이러한 이유로 ID라는 별도의 필드를 PK로 설정하고 Auto_Increment 옵션을 주어 Clustered Index에서 발생할 수 있는 문제점을 해결한다.</p>
<h4 id="clustered-index-구조">Clustered Index 구조</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/e3ac1d8f-d5f3-4685-b669-1a88fccfab35/image.png" alt=""></p>
<p>Clustered Index를 구성하기 위해 레코드를 해당 컬럼으로 정렬한 후에, 루트 페이지를 만들게 된다. Clustered Index는 Root Page와 Leaf Page로 구성되며, Leaf Page는 데이터 그 자체이다. 즉, Index 자체에 데이터가 포함된다.
Index Page를 키 값과 데이터 페이지 번호로 구성하고, 검색하고자 하는 데이터의 키 값으로 페이지 번호를 검색하여 데이터를 찾는다.</p>
<h4 id="non-clustered-index">Non-Clustered Index</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/ea731710-d07c-4fc2-b766-8d6233473056/image.jpg" alt=""></p>
<p>Non-Clustered Index는 물리적으로 레코드를 정렬하지 않은 상태로 Data Page가 구성된다. 즉, 테이블의 레코드는 그대로두고 지정된 컬럼에 대해 정렬된 인덱스를 만든다.
물리적으로 레코드를 정렬하지 않기 때문에 Clustered Index보다 속도면에서 성능이 떨어지지만, 추가/수정/삭제의 성능이 더 뛰어나다.
Non-Clustered Index는 Unique 제약조건을 설정한 컬럼에 대해 자동으로 Non-Clustered Index를 생성한다. 따라서 테이블 당 여러개 존재 가능하다. 하지만 함부로 남용하면 오히려 시스템 성능이 저하될 수 있다.</p>
<h4 id="non-clustered-index-구조">Non-Clustered Index 구조</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/986aa496-47ca-4cc6-9326-fafa19aeeaa3/image.png" alt=""></p>
<p>Non-Clustered Index는 데이터 페이지를 건들지 않고, 별도의 장소에 인덱스 페이지를 생성한다. Non-Clustered Index의 인덱스 페이지는 키값과 위치 포인터(ROWID)로 구성된다.
ROWID는 <strong>&#39;파일그룹번호-데이터페이지번호-데이터페이지오프셋&#39;</strong> 으로 구성되는 포인팅 정보이다.
우선 인덱스 페이지의 리프 페이지에 인덱스로 구성된 컬럼을 정렬하고 ROWID를 생성한다. ROWID는 Clustered Index와 달리 <strong>&#39;페이지번호+#오프셋&#39;</strong> 이 기록되어 바로 데이터 위치를 가리킨다.</p>
<h4 id="clustered-index--non-clustered-index">Clustered Index &amp; Non-Clustered Index</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/347be788-981f-4b11-aa55-c3ade31a539b/image.jpg" alt=""></p>
<h4 id="multi-column-index">Multi-Column Index</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/e895a6de-881d-469f-a23b-af89e523bfb1/image.png" alt=""></p>
<p>다중 컬럼 인덱스는 두 개 이상의 컬럼을 조합하여 생성한 인덱스이다.
다중 컬럼 인덱스에서 가장 중요한 것은 인덱스의 두 번째 컬럼은 첫 번째 컬럼에 의존해서 정렬되어 있다는 것이다. 
즉, 두 번째 컬럼은 첫 번째 컬럼의 값이 같은 레코드에서만 정렬되어 있다. 따라서 다중 컬럼 인덱스에서는 컬럼의 순서가 상당히 중요하다.
&#39;=&#39; 조건과 같이 개수가 적은 데이터를 조회하는 컬럼을 앞에 설정하고, 범위 검색과 같이 개수가 많은 데이터를 조회하는 컬럼을 뒤쪽에 설정해야 효율적이다.
또한, 다중 컬럼 인덱스는 단일 컬럼 인덱스보다 추가/수정/삭제 시 더 비효율적이기 때문에 가급적으로 추가/수정/삭제를 하지 않는 컬럼을 선정하는 것이 더 좋다.</p>
<h4 id="multi-column-index-사용시기">Multi-Column Index 사용시기</h4>
<p>데이터 조회 시 단일 컬럼 인덱스를 여러 개를 사용해야 하는 경우가 많다면 다중컬럼 인덱스를 고려해볼 수 있다.
예를 들어 A, B 컬럼을 조건절에 포함한 검색을 자주한다고 가정해보자.
A, B 컬럼 각각 인덱스를 설정할 경우 Optimizer는 A 컬럼과 B 컬럼 중 어떤 컬럼이 더 빠르게 검색되는지 판단하고 더 빠른 컬럼의 인덱스를 통해 레코드를 탐색하고 이 레코드에서 B 컬럼을 탐색한다.
만약, 각각의 인덱스를 복합 인덱스로 설정할 경우 인덱스에 A와 B 컬럼의 정보가 있기 때문에 바로 탐색이 가능하므로 위의 방식보다 빠르다.
하지만 Where절에 B 컬럼만 사용할 경우, 이 복합 인덱스는 B가 A에 의존적으로 정렬되기 때문에 해당 인덱스를 탐색하지 않는다.</p>
<p>EX) 테이블 T에 컬럼 A,B가 PK일 때, WHERE 조건에 B만 사용하면 인데스를 타지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Bean 생성 순서와 Life Cycle]]></title>
            <link>https://velog.io/@diense_kk/Spring-Bean-%EC%83%9D%EC%84%B1-%EC%88%9C%EC%84%9C%EC%99%80-Life-Cycle</link>
            <guid>https://velog.io/@diense_kk/Spring-Bean-%EC%83%9D%EC%84%B1-%EC%88%9C%EC%84%9C%EC%99%80-Life-Cycle</guid>
            <pubDate>Thu, 02 Jan 2025 14:48:26 GMT</pubDate>
            <description><![CDATA[<h2 id="bean-생성-순서">Bean 생성 순서</h2>
<h3 id="spring">Spring</h3>
<p>설정 파일을 통해 등록된 Bean을 자동적으로 위에서 아래로 bean들을 스캔하여 생성한다.</p>
<h3 id="spring-boot">Spring Boot</h3>
<p>어노테이션을 이용해서 Bean을 등록하게 되면 패키지의 알파벳 순서대로 스캔하여 Bean을 생성한다. 이렇게 하면 생성 순서를 맞춰주지 못하는 문제가 생길 것 같지만, 특정 Bean을 생성하는 도중에 해당 Bean에 주입되는 새로운 Bean을 만나면 이 Bean부터 생성하기 떄문에 문제가 되지 않는다. 단, 생성자 주입 방식일 경우에만 해당된다.</p>
<p>컴포넌트 스캔 순서와 관계없이 의존 관계를 기반으로 빈을 생성하므로 문제가 되지 않습니다.</p>
<h3 id="스프링-부트에서-컴포넌트-스캔방식생성자-주입-방식인-경우">스프링 부트에서 컴포넌트 스캔방식(생성자 주입 방식인 경우)</h3>
<h4 id="bean-생성자-호출-순서">Bean 생성자 호출 순서</h4>
<p>Controller -&gt; Service -&gt; Repository (패키지 알파벳 순)</p>
<h4 id="bean-생성완료-순서">Bean 생성완료 순서</h4>
<p>Repository -&gt; Service -&gt; Controller</p>
<p>Controller를 Bean으로 등록하기 위해 Controller의 생성자를 호출할 때 Controller의 생성자에 있는 파라미터(객체)를 먼저 Bean으로 생성해야 된다.</p>
<h2 id="bean-life-cycle">Bean Life Cycle</h2>
<p>2번부터 5번이 Bean Life Cycle</p>
<p>1) 스프링 컨테이너 생성
SpringApplication.run()을 호출하여 실행된다. 애플리케이션 설정 정보(application.properties) 로드, @Configuration 클래스 처리 등을 함</p>
<p>2) 스프링 빈 생성 및 의존관계 주입
@Component, @Service, @Repository, @Controller 등 어노테이션을 가진 클래스들을 스캔하여 빈으로 등록한다.</p>
<p>3) 초기화 콜백 - 빈이 완전히 생성된 후 호출
애플리케이션 실행에 필요한 리소스를 준비한다. 주로 데이터베이스 연결 초기화나 외부 API나 설정값 확인 등을 함</p>
<p>4) 빈 사용
HTTP 요청이 들어오거나, 이벤트가 발생했을 때 컨트롤러나 서비스 빈이 호출된다.</p>
<p>5) 소멸 전 콜백 - 빈이 소멸되기 직전에 호출
데이터베이스 연결 종료, 외부 API와의 연결 해제 등을 함</p>
<p>6) 스프링 종료 - Bean 파괴는 생성의 역순
애플리케이션을 종료한다. 리소스 정리와 함께, 컨테이너 내 모든 리소스(캐시, 연결 풀 등)를 종료함</p>
<h2 id="di-시점과-생성자-기반의-di">DI 시점과 생성자 기반의 DI</h2>
<p>스프링에서는 생성자 주입 방식을 권장하는데, 생성자 주입 방식을 사용하면 실질적인 DI가 애플리케이션 부팅 시점에 이루어지기 때문이다.
즉, 생성자 기반의 DI를 사용하면 Spring IoC Container가 생성되면서 Bean들을 생성하고, Bean이 생성된 후에 해당 Bean과 관련된 의존성을 주입한다.</p>
<p>또한, 생성자 기반으로 의존성을 주입하면 final 키워드를 사용하여 의존관계를 갖는 객체를 불변으로 관리할 수 있다. 즉, 해당 객체의 상태를 불변으로 관리하여 멀트쓰레드 환경에서 공유하는 heap 영역의 Bean을 Thread-Safety하게 관리하고 잠재적 버그의 여지를 줄인다.</p>
<p>생성자 인자가 많은 객체는 많은 의존성을 가졌다는 것을 의미한다. 즉, 관심사의 분리가 필요한 객체이다.</p>
<h2 id="di를-사용하는-이유">DI를 사용하는 이유</h2>
<h3 id="유연성-확보">유연성 확보</h3>
<p>의존 관계 설정이 컴파일이 아닌 런타임 시에 이루어지도록 하여 모듈 간 결합도를 낮춘다. 
특정 객체를 필요로 하는 클래스 내에서 직접 객체를 생성한다면, 클래스를 특정 객체에 확정 짖는 것이고, 이후에 해당 객체가 다른 객체로 대체된다면 클래스의 수정이 요구된다. 
즉, 다른 객체를 필요로 하는 경우 클래스를 재사용 할 수 없다는 것이다.
만약 의존성을 외부로부터 주입받도록(DI) 하면 코드의 재사용성을 높이고, 모듈 간 결합도를 낮출 수 있다.</p>
<h3 id="테스트-코드-작성-용이">테스트 코드 작성 용이</h3>
<p>특정 객체를 해당 클래스에서 직접 생성한다면 실제 객체를 모의 객체로 대체할 수 없기 때문에 클래스를 테스트 하기 어렵게 만든다.
생성자 기반 DI를 사용하면 테스트 시 생성자 주입을 통해서 목 객체를 쉽게 전달할 수 있다. 
즉, Mock 객체를 이용해 생성자로 주입해주면 테스트할 클래스만을 테스트할 수 있게 된다.</p>
<h2 id="spring이-singleton인-이유">Spring이 Singleton인 이유</h2>
<p>결론부터 말하면, 대규모 트래픽을 처리하기 위함이다.
스프링은 엔터프라이즈급 애플리케이션을 목표로 만들어졌다. 엔터프라이즈급 애플리케이션에서 수 많은 요청이 들어올때 반복적으로 Bean 객체를 만드는 것은 매우 비효율적이다.
아무리 GC성능이 좋아졌어도 부하를 감당하기는 힘들 것이다.
따라서 스프링은 초기 로딩 시 시간이 걸리더라도 Singleton으로 Bean을 만든다.
Spring IoC Container 생성 시에 IoC Container는 모든 Bean들을 만들고, Bean 설정에 대한 예외처리를 초기에 검증한다.</p>
<h2 id="bean은-thread-safe한가">Bean은 Thread-Safe한가?</h2>
<p>클라이언트로부터 HTTP 요청이 오면, 서블릿 컨테이너는 쓰레드 풀에서 요청 당 쓰레드를 할당한다.
기본적으로 객체는 Heap 영역에 존재하므로 별다른 설정이 없으면 Thread-Safe 하지 않다.
그래서 Bean 자체를 불변으로 관리하는 것이 좋다. Bean을 불변으로 관리하기 위한 방법 중 하나로 필드에 final 키워드를 사용하는 것이고 이것이 Spring이 생성자 주입 방식을 권장하는 이유이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] ETC]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Thu, 26 Dec 2024 06:10:17 GMT</pubDate>
            <description><![CDATA[<h2 id="프로세스와-스레드">프로세스와 스레드</h2>
<h3 id="프로세스">프로세스</h3>
<p>운영체제로부터 자원을 할당받은 작업의 단위이다.</p>
<p>프로세스는 프로그램을 실행시켜 정적인 프로그램이 동적으로 변하여 프로그램이 돌아가고 있는 상태를 말한다. 즉, 컴퓨터에서 작업중인 프로그램을 의미한다.</p>
<p>모든 프로그램은 운영체제가 실행되기 위한 메모리 공간을 할당해줘야 실행될 수 있다. 프로그램을 실행하는 순간 파일은 컴퓨터 메모리에 올라가게 되고, 운영체제로부터 시스템 자원(CPU)을 할당받아 프로그램 코드를 실행시켜 우리가 서비스를 이용할 수 있게 된다.</p>
<h3 id="스레드">스레드</h3>
<p>프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위이다.
스레드는 하나의 프로세스 내에서 동시에 진행되는 작업 갈래, 흐름의 단위이다.
스레드끼리 프로세스의 자언을 공유하면서 프로세스 실행 흐름의 일부가 되기 때문에 동시 작업이 가능한 것이다.</p>
<p>예를 들자면, 크롬을 실행하면 프로세스 하나가 실행 될 것이다. 그런데 우리는 브라우저에서 동영상을 틀어놓고 온라인 쇼핑을 즐기면서 게임을 하기도 한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/25205b47-eeee-4661-ba53-98d7a0a3b24e/image.png" alt=""></p>
<p>이것은 하나의 프로세스 안에서 여러가지 작업들 흐름이 동시에 진행되기 떄문에 가능한 것이다. 이러한 일련의 작업 흐름들을 스레드라고 하며 여러 개가 있다면 이를 멀티 스레드라고 부른다.</p>
<h3 id="프로세스--스레드의-메모리">프로세스 &amp; 스레드의 메모리</h3>
<h4 id="프로세스의-자원-구조">프로세스의 자원 구조</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/2b4c1d90-adaf-4d7c-a777-91926e02f8dc/image.png" alt=""></p>
<p>1) 코드 영역(Code / Text) - 프로그래머가 작성한 프로그램 함수들의 코드가 CPU가 해석 가능한 기계어 형태로 저장되어 있다.
2) 데이터 영역(Data) - 코드가 실행되면서 사용하는 전역 변수나 각종 데이터들이 모여있다.
3) 스택 영역(Stack) - 지역 변수와 같은 호출한 함수가 종료되면 되돌아올 임시적인 자료를 저장하는 독립적인 공간이다. Stack은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸된다.
4) 힘 영역(Heap) - 생성자, 인스턴스와 같은 동적으로 할당되는 데이터들을 위해 존재하는 공간이다. 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.</p>
<p>위 그림에서 Stack과 Heap 영역이 위아래로 화살표가 그려진 이유는 코드영역, 데이터 영역은 정적 영역이지만, Stack, Heap은 프로세스가 실행되는 동안 크기가 동적으로 변하기 때문이다.</p>
<h4 id="스레드의-자원-구조">스레드의 자원 구조</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/150e70c2-2509-4e00-a745-962ec1c89d00/image.png" alt=""></p>
<p>스레드는 프로세스의 4가지 메모리 영역 중 Stack만 할당받아 복사하고 Code, Data, Heap은 프로세스 내의 다른 스레드들과 공유된다.
독립적인 스택을 가졌다는 것은 독립적인 함수 호출이 가능하다는 의미이다. 또한, 독립적인 함수 호출이 가능하다는 것은 독립적인 실행 흐름이 추가된다는 말이다.
즉, Stack을 가짐으로써 스레드는 독립적인 실행 흐름을 가질 수 있게 되는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/7eb00415-8872-498b-b449-6ff87605125d/image.png" alt=""></p>
<h2 id="base64">Base64</h2>
<p>Base64란 Binary Data를 Text로 바꾸는 Encoding의 하나로써 Binary Data를 Character Set에 영향을 받지 않는 공통 ASCII 영역의 문자로만 이루어진 문자열로 바꾸는 Encoding이다.</p>
<h3 id="인코딩">인코딩</h3>
<p>문자나 파일을 약속도니 규칙에 따라 컴퓨터가 이해하는 언어로 이루어진 코드로 바꾸는 것을 통틀어 일컫는다. 즉, 인코딩이란 정해진 규칙에 따라 코드화, 암호화, 부호화 하는 것을 말한다. 이렇게 인코딩 하는 이유는 정보의 형태 표준화, 보안, 저장 공간 절약 등을 위해서이다.</p>
<h3 id="디코딩">디코딩</h3>
<p>디코딩이란 인코딩의 역과정으로, 이진 형식의 데이터를 사람이 해석할 수 있는 데이터로 변환하는 작업이다.</p>
<h2 id="bcryptpasswordencoder">BCryptPasswordEncoder</h2>
<p>스프링 시큐리티 프레임워크에서 제공하는 클래스 중 하나로 비밀번호를 암호화하는 데 사용할 수 있는 메서드를 가진 클래스이다.
BCryptPasswordEncoder는 PasswordEncoder 인터페이스를 구현한 클래스이다.</p>
<p>BCryptPasswordEncoder는 BCrypt 해싱 함수를 사용해서 비밀번호를 인코딩 해주는 메서드와 사용자에 의해 제출된 비밀번호와 저장소에 저장되어 있는 비밀번호의 일치 여부를 확인해주는 메서드를 제공한다.</p>
<p>encode 메서드는 솔트(salt)를 지원한다. 똑같은 비밀번호를 해당 메서드를 통하여 인코딩하더라도 매번 다른 인코딩 된 문자열을 반환한다.
입력받은 패스워드에 랜덤하게 생성된 salt를 더해서 해싱한 값을 데이터베이스 저장하므로, 가능한 모든 문자열 조합을 해시함수에 넣어서 결과를 저장한 테이블인 Rainbow Table을 활용한 브루트 포스 공격을 막는다.</p>
<pre><code>String encode = bCryptPasswordEncoder.encode(password);
$2a$10$areHbojzw3eHObSfiKGay.66OFxaJiEKy8d.n5CqvCyjY25ZVIfha

String encode2 = bCryptPasswordEncoder.encode(password);
$2a$10$m1pTFfDjNTpgx9RgR7dnr.4/KTRSRMHSbrtXrTYlUd4LFHCWaQXtW</code></pre><p>같은 값에 대해 2번의 encode 결과는 서로 다르다.</p>
<p>그럼 이 salt값은 어디에 저장되어 있는 것인가?
salt 값은 해시값에 이어붙여서 함께 저장되고 있다.</p>
<pre><code>$2a$10$7EqJtq98hPqEX7fNZaFWoOeX3ZrloBHZEE5pcT9N/0GbE3Kw6hJD.</code></pre><h4 id="구성">구성</h4>
<p>$2a$ - 알고리즘 버전
10 - cost factor (반복 횟수)
7EqJtq98hPqEX7fNZaFWoO - 솔트 값
eX3ZrloBHZEE5pcT9N/0GbE3Kw6hJD. - 해시 값</p>
<p>matches 메서드는 암호화된 값에서 추출한 솔트를 사용하여 평문 비밀번호를 다시 해싱한다. 이때, 동일한 알고리즘(BCrypt)과 동일한 cost factor를 사용해 비밀번호를 처리한다.</p>
<h2 id="배열과-리스트">배열과 리스트</h2>
<h3 id="배열-array">배열 (Array)</h3>
<p>배열은 원소들을 연속적인 메모리 공간에 저장하는 자료구조이다. 배열의 크기는 고정되어 있으며, 선언 시에 크기를 지정해야 된다.</p>
<h3 id="리스트-list">리스트 (List)</h3>
<p>리스트는 원소들을 연결하여 저장하는 자료구조이다. 원소의 개수가 가변적이며, 삽입과 삭제가 자유롭다.</p>
<h3 id="차이점">차이점</h3>
<p>1) 메모리 할당
배열은 연속적인 메모리 공간에 할당되고, 리스트는 비연속적인 메모리 공간에 할당된다.</p>
<p>2) 크기
배열은 크기가 고정되어 있으며, 리스트는 가변적이다.</p>
<p>3) 접근 방법
배열은 인덱스를 통한 빠른 접근이 가능하지만, 리스트는 순차적으로 접근해야 된다.
Java에서 ArrayList는 인덱스를 통한 접근이 가능하지만, LinkedList와 같은 경우에만 순차적으로 접근해야 된다.</p>
<p>4) 삽입과 삭제
배열은 삽입과 삭제가 번거롭고 시간이 오래 걸리지만, 리스트는 삽입과 삭제가 빠르다.</p>
<h4 id="linkedlist">LinkedList</h4>
<p>데이터 요소(Node)들이 링크(포인터)로 연결되어 선형 자료구조를 형성하는 자료구조이다.
인덱스를 통한 접근이 힘들기 때문에, 순차적으로 탐색해야 된다. 최악의 경우 시간 복자도는 O(n)이다.
Node에 데이터 뿐만 아니라 포인터도 저장해야 되기 때문에 추가적인 메모리 공간이 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/491d7bf9-de4c-4a43-b67f-12fbc5bbb665/image.png" alt=""></p>
<h2 id="컴파일러-vs-인터프리터">컴파일러 VS 인터프리터</h2>
<p>컴퓨터는 고급 언어로 작성한 코드를 바로 인식하지 못하기 때문에 이를 번역하는 과정이 컴파일이다.</p>
<h3 id="컴파일러">컴파일러</h3>
<p>컴파일러는 프로그램 전체를 스캔하여 이를 모두 기계어로 변역한다. 전체를 스캔하기 때문에 컴파일러는 초기 스캔 시간이 오래 걸린다. 하지만 전체 실행 시간만 따지고 보면 인터프리터 보다 빠르다.
컴파일러는 초기 스캔을 마치면 실행파일을 만들어 놓고 다음에 실행할 때 이전에 만들어 놓았던 실행파일을 실행하기 때문이다.</p>
<p>단점으로는 컴파일러가 인터프리터 보다 더 많은 메로리를 사용한다. 컴파일러가 고급 언어로 작성된 소스를 기계어로 번역하고 이 과정에서 오브젝트 코드라는 파일을 만드는데 이 오브젝트 코드를 묶어서 하나의 실행 파일로 다시 만드는 링킹이라는 작업을 해야 되기 때문이다.</p>
<h3 id="인터프리터">인터프리터</h3>
<p>인터프리터는 컴파일러와 다르게 프로그램 실행시 한 번에 한 문장씩 번역한다. 그렇기 때문에 한번에 전체를 스캔하고 실행파일을 만들어서 실행하는 컴파일러보다 실행시간이 더 걸린다. 한 문장 읽고 번역하여 실행시키는 과정을 반복하는게 만들어 놓은 실행파일을 한 번 실행시키는 것보다 빠르긴 힘들다.</p>
<p>인터프리터는 메모리 효율이 좋다. 링킹 과정을 거치지 않기 때문이다. 인터프리터는 메모리 사용에 컴파일러보다 더 효율적인 모습을 보인다.</p>
<h4 id="정리">정리</h4>
<h4 id="컴파일러-1">컴파일러</h4>
<p>1) 전체 파일을 스캔하여 한꺼번에 번역한다.
2) 초기 스캔시간이 오래 걸리지만, 한 번 실행 파일이 만들어지고 나면 빠르다.
3) 기계어 번역과정에서 더 많은 메모리를 사용한다.
4) 전체 코드를 스캔하는 과정에서 모든 오류를 한꺼번에 출력해주기 때문에 실행 전에 오류를 알 수 있다.</p>
<h4 id="인터프리터-1">인터프리터</h4>
<p>1) 프로그램 실행 시 한 번에 한 문장씩 번역한다.
2) 한 번에 한 문장씩 번역 후 실행 시키기 때문에 실행 시간이 느리다.
3) 오브젝트 코드 생성과정이 없기 때문에 메모리 효율이 좋다.
4) 프로그램을 실행시키고 나서 오류를 발견하면 바로 실행을 중지 시킨다. 실행 후에 오류를 알 수 있다.</p>
<h3 id="java는-컴파일러-인터프리터">Java는 컴파일러? 인터프리터?</h3>
<p>Java는 컴파일 시 전체 코드를 바이트 코드로 변환한 뒤, 클래스 로더를 통해 관리한다. 필요한 시점에 해당 코드를 JVM의 메모리 영역에 올려 사용한다. JVM은 바이트코드를 OS에 맞게 번역해 실행한다.
그럼 Java는 과연 컴파일러일까 인터프리터일까?
정답은 자바는 컴파일러와 인터프리터 둘 다 사용하는 것으로 간주된다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/ccc8ead6-22cd-4c62-b30f-d4046e470559/image.png" alt=""></p>
<p>자바 컴파일러가 소스 코드를 자바 바이트 코드로 컴파일하고, 자바 인터프리터는 바이트 코드를 기계어가 이해할 수 있는 기계코드로 변환하거나 번역한다.
바이트 코드를 기계 코드로 변환하기 위해 JVM에 .class 파일을 배포하면 JVM은 자바 인터프리터를 사용하 여 그 코드를 기계 코드로 변환하거나 변역한다.</p>
<ul>
<li><strong>빌드란?</strong> - 빌드란 소스 코드 파일을 실행 가능한 소프트웨어 산출물로 변환하는 과정이다. 이 과정에서는 소스 코드 파일을 컴파일하고, 링크를 거쳐 실행 파일이나 라이브러리 파일 등을 생성한다. 즉, 빌드는 소스 코드 파일을 실행 가능한 형태로 변환하는 과정으로, 컴파일 이후 링크 과정을 포함한다.
빌드하면 JAR 파일 생성됨. 빌드 툴로는 Maven과 Gradle이 있음</li>
</ul>
<h2 id="인터페이스">인터페이스</h2>
<p>인터페이스는 추상 메서드와 상수만을 가질 수 있는 기본 설계도이다.</p>
<pre><code>public interface TestInterface {
    public final int a = 10;

    void test();
}</code></pre><h3 id="인터페이스를-사용하는-이유는">인터페이스를 사용하는 이유는?</h3>
<h4 id="1-개발-기간을-단축-시킬-수-있다">1) 개발 기간을 단축 시킬 수 있다.</h4>
<p>인터페이스를 사용하면 이 틀을 사용해서 프로그램을 작성할 수 있다.</p>
<h4 id="2-표준화가-가능하다">2) 표준화가 가능하다.</h4>
<p>인터페이스로 틀을 잡아놓고 개발 하면 여러 명의 개발자가 작업을 할 때도 일관된 틀 안에서 그 안의 내용을 구현하면서 개발이 진행되므로 정형화된 작업이 가능하다.</p>
<h3 id="단점은">단점은?</h3>
<h4 id="1-인터페이스의-모든-메서드를-구현해야-된다">1) 인터페이스의 모든 메서드를 구현해야 된다.</h4>
<p>만약 인터페이스의 추상화가 제대로 이루어지지 않은 경우에는 불필요한 메서드까지 구현해야 될 수 있다.</p>
<h4 id="2-변경이-어렵다">2) 변경이 어렵다.</h4>
<p>인터페이스를 많은 클래스에서 사용하고 있는 상태에서 인터페이스에 메소드가 추가된다면 해당 인터페이스를 사용하는 모든 클래스를 수정해야 된다.</p>
<h2 id="디자인-패턴">디자인 패턴</h2>
<h3 id="싱글톤singleton-패턴">싱글톤(Singleton) 패턴</h3>
<p>인스턴스가 프로그램 전체에서 단 하나만 생성되도록 보장하는 디자인 패턴이다.</p>
<h4 id="싱글톤-패턴의-특징">싱글톤 패턴의 특징</h4>
<p>1) 전역 접근 가능 - 클래스의 유일한 인스턴스에 대해서 전역 접근을 제공한다.
2) 리소스 공유 - 여러 부분에서 동일한 객체를 공유할 수 있어 리소스 사용을 최적화 할 수 있다.
3) 상태 유지 - 프로그램 실행 중 객체를 일관되게 유지할 수 있다.</p>
<h4 id="싱글톤-패턴의-장점">싱글톤 패턴의 장점</h4>
<p>싱글톤 패턴을 사용함으로써 얻을 수 있는 이점 중 하나는 메로리 낭비를 방지할 수 있다.
같은 객체 여러개를 만들 필요 없이 하나의 객체만을 생성하여 사용한다.</p>
<h4 id="싱글톤-패턴의-단점">싱글톤 패턴의 단점</h4>
<p>모든 곳에서 같은 상태를 가지기 때문에 값의 변경에 대해 민감해진다. 변수를 수정하게 될 경우 다른 코드에 의도하지 않은 영향을 줄 수 있다.</p>
<h3 id="팩토리factory-패턴">팩토리(Factory) 패턴</h3>
<p>팩토리 패턴은 객체 생성을 처리하는 디자인 패턴이다.
이 패턴은 객체 생성 로직을 클라이언트 코드에서 분리하여 유연성을 높이고 코드 재사용성을 향상시킨다.</p>
<h4 id="팩토리-패턴의-특징">팩토리 패턴의 특징</h4>
<p>1) 객체 생성 로직을 캡슐화한다.
2) 구체적인 클래스에 의존하지 않고 인터페이스를 통해 객체를 다룬다.
3) 새로운 제품 유형을 쉽게 추가할 수 있다.</p>
<h4 id="팩토리-패턴의-장점">팩토리 패턴의 장점</h4>
<p>1) 코드의 유연성과 확장성이 향상된다.
2) 객체 생성 로직과 사용 로직을 분리하여 결합도를 낮춘다.
3) 코드 재사용성이 증가한다.</p>
<h4 id="팩토리-패턴의-단점">팩토리 패턴의 단점</h4>
<p>새로 생성할 객체가 늘어날 때마다 Factory 클래스에 추가해야 되기 때문에 클래스가 많아짐</p>
<pre><code>//인터페이스
public interface IsSpeaker {
    void speak();
}

//구현체
@Service
public class EnglishSpeaker implements IsSpeaker{

    @Override
    public void speak(){
        System.out.println(&quot;I&#39;m english speaker&quot;);
    }
}

@Service
public class GermanSpeaker implements IsSpeaker{

    @Override
    public void speak(){
        System.out.println(&quot;I&#39;m German Speaker&quot;);
    }
}

@Service
public class KoreanSpeaker implements IsSpeaker{

    @Override
    public void speak(){
        System.out.println(&quot;I&#39;m Korean Speaker&quot;);
    }

}

//ENUM
public enum Language {
    ENGLISH, GERMAN, KOREAN
}

//Factory
public class SpeakerFactory {

    public static IsSpeaker createSpeaker(Language language){
        switch(language){
            case ENGLISH -&gt; {
                return new EnglishSpeaker();
            }
            case GERMAN -&gt; {
                return new GermanSpeaker();
            }
            case KOREAN -&gt; {
                return new KoreanSpeaker();
            }
            default -&gt; {
                throw new ApplicationContextException(&quot;Unsupported language &quot; + language);
            }
        }
    }
}
</code></pre><h3 id="파사드facade-패턴">파사드(Facade) 패턴</h3>
<p>파사드 패턴은 객체를 생성하는 패턴이 아닌, 강력한 결합 구조를 해결하기 위해 코드의 의존성을 줄이고 느슨한 결합으로 구조를 변경한다.
메인 시스템과 서브 시스템 중간에 위치하여, 새로운 인터페이스 계층을 추가하며 시스템 간 의존성을 해결한다. 인터페이스 계층은 메인 시스템과 서브 시스템의 연결 관계를 대신 처리한다.
서브 시스템을 호출, 결합할 수 있는 인터페이스를 제공한다. 인터페이슨느 한 개일 수 있고 여러 개일 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/2e81a79a-7177-4eeb-802d-20c35bbd3ac2/image.png" alt=""></p>
<p>클라이언트가 특정 회사에 어떠한 작업을 의뢰한다고 생각하자.
클라이언트가 직접 디자이너, 밑그림 작업자, 채색자 등의 작업 순서를 알고 통제할 필요가 없다.
클라이언트는 단순히 특정 회사에 의뢰하는 인터페이스를 호출하도록 하는 것이 좋을 것이다.</p>
<h4 id="파사드-패턴의-장점">파사드 패턴의 장점</h4>
<p>1) 복잡성 감소 - 클라이언트 코드의 복잡성을 크게 줄인다.
2) 유지보수 용이성 - 서브시스템의 변경이 클라이언트에 미치는 영향을 최소화한다.
3) 코드 재사용 - 파사드를 통해 서브 시스템을 다양한 컨텍스트에서 쉽게 재사용할 수 있다.
4) 의존성 감소 - 클라이언트가 저체 서브시스템이 아닌 파사드에만 의존하게 된다.</p>
<h4 id="파사드-패턴의-단점">파사드 패턴의 단점</h4>
<p>1) 유연성 제한 - 파사드가 제공하는 인터페이스로 인해 서브시스템의 세부 기능에 대한 접근이 제한될 수 있다.
2) 파사드 클래스의 복잡성 - 서브시스템이 매우 복잡한 경우, 파사드 클래스 자체가 복잡해질 수 있다.</p>
<pre><code>@Component
public class Designer {

    public void analysis() {
        System.out.println(&quot;디자이너가 요구사항 분석중&quot;);
    }

    public void design() {
        System.out.println(&quot;디자이너가 초안 구상중&quot;);
    }

}

@Component
public class Drawer {

    public void draw() {
        System.out.println(&quot;드로어가 밑그림 그리는 중&quot;);
    }

    public void linePick() {
        System.out.println(&quot;드로어가 선 따는 중&quot;);
    }
}

@Component
public class Painter {

    public void colorScheme() {
        System.out.println(&quot;채색자가 배색하는 중&quot;);
    }
    public void paint() {
        System.out.println(&quot;채색자가 칠하는 중&quot;);
    }
}


@Service
@RequiredArgsConstructor
public class FacadeCompany {
    private final Designer designer;
    private final Drawer drawer;
    private final Painter painter;

    public void work(){
        designer.analysis();
        designer.design();
        drawer.draw();
        drawer.linePick();
        painter.colorScheme();
        painter.paint();
    }
}


    public void facadeTest(){
        facadeCompany.work();
    }</code></pre><h3 id="프록시proxy-패턴">프록시(Proxy) 패턴</h3>
<p>프록시 패턴은 대상 원본 객체를 대리하여 대신 처리하게 함으로써 로직의 흐름을 제어하는 행동 패턴이다.</p>
<p>프록시의 사전적 의미는 대리인이라는 뜻이다. 누군가에게 어떤 일을 대신 시키는 것을 의미한다.
OOP에 접목해보면 클라이언트가 대상 객체를 직접 쓰는게 아니라 중간에 프록시를 거쳐서 쓰는 코드 패턴이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/4c40f3c9-06d2-4715-8bdd-9dff748a2349/image.png" alt=""></p>
<p>대상 클래스가 민감한 정보를 가지고 있거나 인스턴스화 하기에 무겁거나 추가 기능을 넣고 싶은데, 원본 객체를 수정할 수 없는 상황일 때를 극복하기 위해서 사용한다.</p>
<p>1) 보안 - 프록시는 클라이언트가 작업을 수행할 수 있는 권한이 있는지 확인하고 검사 결과가 긍정적인 경우에만 요청을 대상으로 전달한다.
2) 캐싱 - 프록시가 내부 캐시를 유지하여 데이터가 캐시에 존재하지 않는 경우에만 대상에서 작업이 실행되도록 한다.
3) 데이터 유효성 검사 - 프록시가 입력을 대상으로 전달하기 전에 유효성을 검사한다.</p>
<p>접근을 제어하거나 기능을 추가하고 싶은데, 기존의 특정 객체를 수정할 수 없는 상황일때 사용된다.</p>
<h4 id="프록시-패턴의-장점">프록시 패턴의 장점</h4>
<p>1) 개방 폐쇄 원칙(OCP) 준수 - 기존 객체를 수정하지 않고 일련의 로직을 프록시 패턴을 통해 추가할 수 있다.
2) 단일 책임 원칙(SRP) 준수 - 대상 객체는 자신의 기능에만 집중 하고, 그 이외 부가 기능을 제공하는 역할을 프록시 객체에 위임하여 다중 책임을 회피할 수 있다.</p>
<h4 id="프록시-패턴의-단점">프록시 패턴의 단점</h4>
<p>1) 코드의 복잡도가 증가한다. 로직이 난해해져 가독성이 떨어질 수 있다.
2) 성능이 저하될 수 있다. 객체를 생성할 때 한 단계를 거치게 되므로, 빈번한 객체 생성이 필요한 경우</p>
<pre><code>public interface ISubject {
    void action();
}

@Component
public class RealSubject {

    public void action(){
        System.out.println(&quot;action&quot;);
    }
}

@Service
@RequiredArgsConstructor
public class Proxy implements ISubject{

    private final RealSubject realSubject;

    public void action(){
        System.out.println(&quot;데이터 전처리 또는 보안 관련 로직 수행&quot;);
        realSubject.action(); // 위임
        System.out.println(&quot;Proxy action&quot;);
    }
}

    public void testProxy(){
        proxy.action();
        return ResponseEntity.ok().build();
    }</code></pre><p>대상 객체와 Proxy 클래스에서 인터페이스를 Implements한다.</p>
<h2 id="병렬-프로그래밍">병렬 프로그래밍</h2>
<p>병렬 프로그래밍은 여러 스레드를 사용하여 작업을 동시에 수행하는 것을 의미한다. 자바에서는 쓰레드와 실행자(Executor)를 사용하여 병렬 프로그래밍을 구현할 수 있다.</p>
<h3 id="병렬성-vs-동시성">병렬성 VS 동시성</h3>
<p>동시성 - 하나의 시스템이 여러 작업을 동시에 처리하는 것처럼 보이게 하는 것이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/ea38eb9e-13a5-4257-9596-3572c5a93707/image.png" alt="">
동시성은 스레드, 비동기 프로그래밍 등의 방법을 사용하여 구현된다.
여러 작업을 번갈아가며 처리하므로 작업이 빠르게 완료될 수 있다.</p>
<p>병렬성 - 여러 작업을 실제로 동시에 처리하는 것이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/407bd609-3a32-4e47-9271-f9adf96f88eb/image.png" alt="">
병렬로 처리할 작업들은 병렬처리기에서 실행되며, 각각의 작업은 별도이 프로세스나 스레드에서 실행된다.
병렬처리기는 여러 개의 CPU 또는 CPU 코어가 있어서, 각각의 작업이 서로 다른 CPU 또는 CPU 코어에서 병렬적으로 실행된다.</p>
<p>병렬성은 여러 작업이 동시에 실행되는 것이지만, 작업들은 각각이 독립적으로 실행되며 서로 영향을 주지 않는다.
동시성은 서로 다른 작업들이 서로 영향을 주면서 동시에 실행되는 것처럼 보인다.</p>
<p>동시성은 <strong>싱글 코어</strong>에서 멀티 쓰레드를 동작 시키는 방식,
병렬성은 <strong>멀티 코어</strong>에서 멀티 쓰레드를 동작 시키는 방식이다.</p>
<h2 id="동기-통신-vs-비동기-통신">동기 통신 VS 비동기 통신</h2>
<p>동기 통신은 요청 후 결과 응답을 기다렸다가 받고, 비동기 통신은 요청 후 결과 응답을 기다리지 않고 나중에 받거나 생략합니다.
결과를 나중에 받는 방법은 이벤트 리스너를 등록하는 방법이 있다.</p>
<h3 id="동기-통신">동기 통신</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/6336f83d-f189-406d-96da-ba14f63c2daf/image.png" alt=""></p>
<p>동기 통신에서는 A는 B에게 직접 요청을 보내고 해당 작업이 완료될 때까지 기다려야된다.
이떄 A는 B의 작업이 완료될 때까지 다른 작업을 수행할 수 없으며, B도 C에게 요청을 동기 통신으로 작업 요청을 한 상태이기 때문에 C의 작업이 완료될 때까지 기다려야된다.</p>
<p>동기 통신의 단점 중 하나는 장애 전파에 대한 취약성이다.
여러 개의 서비스가 동기적으로 의존하고 있는 상황에서 하나의 서비스가 장애를 겪으면 이로 인해 전체 시스템에 장애가 전파될 수 있다.</p>
<p>예를 들어, B가 C에게 요청을 보낸 상황에서 에러가 발생한다면 A까지 B에게 보낸 작업 요청까지 장애가 전파된다. 결과적으로 전체 시스템의 가용성과 안정성이 저하될 가능성이 있다.</p>
<h3 id="비동기-통신">비동기 통신</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/683b4c8a-dcca-410d-9dca-39557536f343/image.png" alt="">
비동기 통신에서는 A가 작업을 수행하다가 B에게 작업 요청을 보낼 때, 해당 요청(이벤트)을 Message Q에 넣어둔다. A는 이후 작업을 계속 이어나가고 B는 Message Q를 주기적으로 확인하여 새로운 이벤트가 있는 경우 작업을 수행한다.
이와 같은 방식으로 각 서비스는 독립적으로 작업을 수행하면서 중간에 Message Q를 통해 정보를 교환한다.</p>
<p>이러한 비동기 통신의 장점은 장애가 발생해도 다른 서비스에 영향을 미치지 않을 가능성이 높다는 것이다.</p>
<p>하지만 비동기 통신만을 사용하는 것은 옳은 선택은 아니다. 동기적인 작업이 필요한 경우나 응답이 필요한 경우에는 API 호출과 같은 동기적인 방식을 사용하는 것이 적절하다.</p>
<p>이벤트를 한쪽으로 전달만 하면 되는 경우에는 Message Q를 두는 것이 더 안정적인 서비스 운영을 위한 서버 아키텍쳐가 될 수 있다.</p>
<h2 id="블로킹--논블로킹">블로킹 / 논블로킹</h2>
<p>다른 요청의 작업을 처리하기 위해 현재 작업을 Block(차단, 대기) 하냐 안하냐의 유무를 나타내는 프로세스의 실행 방식이다.</p>
<p>동기/비동기가 전체적인 작업에 대한 순차적인 흐름 유무라면, 블로킹/논블로킹은 전체적인 작업의 흐름 자체를 막냐 안막냐로 볼 수 있다.</p>
<h3 id="블로킹">블로킹</h3>
<p>블로킹은 A함수가 B함수를 호출하면, 제어권을 A가 호출한 B함수에 넘겨준다.
<img src="https://velog.velcdn.com/images/diense_kk/post/2d3b288e-620a-48bd-b67e-eaa8241f392d/image.png" alt=""></p>
<h3 id="논블로킹">논블로킹</h3>
<p>논블로킹은 A함수가 B함수를 호출해도 제어권은 그대로 자신이 가지고 있는다.
<img src="https://velog.velcdn.com/images/diense_kk/post/f69939c8-ccb3-4a2f-b099-3553f1b670f0/image.png" alt=""></p>
<h2 id="동기-비동기-블로킹-논블로킹">동기 비동기, 블로킹 논블로킹</h2>
<p>블로킹 / 논블로킹이 현재의 작업 상태에 따라 동작이 결정되는 것이라면, 동기 / 비동기는 결과를 기다리는 주체가 누구인가에 대한 이야기이다.
동기는 결과를 기다리는 주체가 요청을 호출한 스레드이다. 해당 스레드는 요청에 대한 결과가 돌아오기까지 아무것도 하지 않게 될 것이다.
위에 있는 블로킹과 논블로킹은 사실 동기인 경우를 가정하여 설명한 것이다.</p>
<p>1) Sync+Blocking
<img src="https://velog.velcdn.com/images/diense_kk/post/257eccec-3318-4256-aa48-ab630d84a399/image.png" alt="">
함수 A는 함수 B의 리턴값을 필요로 한다(동기). 그래서 제어권을 함수 B에게 넘겨주고, 함수 B가 실행을 완료하여 리턴값과 제어권을 돌려줄때까지 기다린다(블로킹).</p>
<p>2) Sync+Non_Blocking
<img src="https://velog.velcdn.com/images/diense_kk/post/d9b374df-0d61-4fb1-ace3-d75c3ca7e548/image.png" alt="">
A 함수는 B 함수를 호출한다. 이때 A함수는 B함수에게 제어권을 주지 않고, 자신의 코드를 계속 실행한다(논블로킹).
그런데 A함수는 B함수의 리턴값이 필요하기 때문에, 중간중간 B함수에게 함수 실행을 완료했는지 물어본다(동기).</p>
<p>3) Async+Blocking
<img src="https://velog.velcdn.com/images/diense_kk/post/bec2cabe-255f-44f1-b461-0fb6ecddd500/image.png" alt="">
A함수는 B함수의 리턴값에 신경쓰지 않고, 콜백함수를 보낸다(비동기)
근데, B함수의 작업에 관심없음에도 불구하고, A함수는 B함수에게 제어권을 넘긴다(블로킹).
그래서, A함수는 관련없는 B함수의 작업이 끝날 때까지 기다려야 된다.</p>
<p>4) Async+Non_Blocking
<img src="https://velog.velcdn.com/images/diense_kk/post/0504e69c-3b2b-4482-bf0c-ea2c1849c898/image.png" alt="">
A함수가 B함수를 호출한다. 제어권은 B함수에게 주지 않고, 자신이 계속 가지고 있는다(논블로킹).
따라서 B함수를 호출한 이후에도 멈추지 않고 자신의 코드를 계속 실행한다.
그리고 B함수를 호출할 때 콜백함수를 함께 준다. B함수는 자신의 작업이 끝나면 A함수가 준 콜백 함수를 실행한다(비동기).</p>
<h2 id="네트워크">네트워크</h2>
<h3 id="http">HTTP</h3>
<p>HTTP(Hyper Text Transfer Protocol)란 데이터를 주고 받기 위한 프로토콜이며, 서버/클라이언트 모델을 따른다.
HTTP는 상태 정보를 저장하지 않는 Stateless의 특징과 클라이언트의 요청에 맞는 응답을 보낸 후 연결을 끊는 Connectionless의 특징을 가지고 있다.</p>
<h4 id="http-메서드">HTTP 메서드</h4>
<p>GET - 데이터 조회
조회할 때 POST도 사용할 수 있지만, GET 메서드는 캐싱이 가능하기에 GET을 사용하는 것이 유리하다.
바디를 가질 수 없음. 쿼리스트링을 통해서 데이터를 전달한다
POST - 요청 데이터 처리, 주로 등록에 사용된다.
PUT - 리소스를 대체(덮어쓰기), 해당 리소스가 없으면 생성
PATCH - 리소스를 부분 변경 (PUT이 전체 변경, PATCH는 일부 변경)
DELETE - 리소스 삭제</p>
<blockquote>
<p>GET은 URL에 데이터가 노출되므로 보안적으로 중요한 데이터를 포함해서는 안된다.
POST가 완전히 안전하다는 것은 아니지만, URL에 데이터가 노출되지 않아 GET보다는 안전하다.</p>
</blockquote>
<h3 id="https">HTTPS</h3>
<p>HTTP는 평문 데이터를 전송하는 프로토콜이기 때문에, HTTP로 중요한 정보를 주고 받으면 제 3자에 의해 조회될 수 있다.
이러한 문제를 해결하기 위해 HTTP에 암호화가 추가된 프로토콜이 HTTPS이다.</p>
<p>HTTPS는 SSL의 껍질을 덮어쓴 HTTP라고 할 수 있다.
SSL(Secure Socket Layer) - 인터넷을 통해 전달되는 정보를 보호하기 위해 개발한 통신 규약</p>
<p>HTTP는 원래 TCP와 직접 통신했지만, HTTPS에서 SSL과 통신하고, SSL이 TCP와 통신함으로써 암호화와 증명서, 안전성 보호를 이용할 수 있게 된다.</p>
<h3 id="wwwnavercom에-접속하는-과정"><a href="http://www.naver.com%EC%97%90">www.naver.com에</a> 접속하는 과정</h3>
<p>1) 사용자가 브라우저에 URL을 입력한다.
2) DNS 서버에 도메인 네임으로 서버의 진짜 주소(IP)를 찾는다.
3) 찾은 IP 주소로 웹 서버에 TCP 3 handshake로 연결을 수립한다.
4) 클라이언트는 웹 서버로 HTTP 요청 메시지를 보낸다.
5) 웹 서버는 HTTP 응답 메시지를 보낸다.
6) 도착한 HTTP 응답 메시지는 웹 페이지 데이터로 변환되고, 웹 브라우저에 의해 출력된다.</p>
<h3 id="tcp와-udp">TCP와 UDP</h3>
<p>TCP와 UDP는 데이터를 보내기 위해 사용하는 프로토콜이다.</p>
<h4 id="tcp">TCP</h4>
<p>1) 연결 지향 방식으로 패킷 교환 방식을 사용한다.
2) 3-way handshaking 과정을 통해 연결을 설정하고 <strong>4-way handshaking</strong>을 통해 해제된다.
3) 높은 신뢰성을 보장한다.
4) UDP보다 속도가 느리다.
5) 전송 순서를 보장한다.
6) 수신 여부를 확인한다.</p>
<h4 id="udp">UDP</h4>
<p>1) 비연결형 서비스이다.
2) 3-way handshaking 과정을 통해 연결을 설정하고 해제하는 과정이 존재하지 않는다.
3) UDP헤더의 CheckSum 필드를 통해 최소한의 오류만 검출한다.
4) 신뢰성이 낮다.
5) TCP보다 속도가 빠르다.
6) 전송 순서를 보장하지 않는다.
7) 수신 여부를 확인하지 않는다.</p>
<h3 id="tcp-연결-및-연결-해제-과정">TCP 연결 및 연결 해제 과정</h3>
<h4 id="연결-3-way-handshaking">연결 3-way HandShaking</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/dffcabd5-78e0-4d6a-9595-eab93f9c5fee/image.png" alt=""></p>
<p>#1. Client -&gt; Server : 들림?
#2. Server -&gt; Client : 잘 들림 내 말은 들리나?
#3. Client -&gt; Server : 잘 들림</p>
<p>SYN(synchronize sequence numbers) - 연결 확인을 보내는 무작위의 숫자 값
ACK(acknowledgements) - 클라이언트 혹은 서버로부터 받은 SYN에 1을 더해 SYN을 잘 받았다는 ACK</p>
<p>1) 클라이언트가 SYN를 보내고 SYN_SENT 상태로 대기한다.
2) 서버는 SYN_RECEIVED 상태로 바꾸고 SYN과 응답 ACK을 보낸다.
3) SYN과 응답 ACK를 받은 클라이언트는 ESTABLISHED 상태로 변경하고 서버에게 응답 ACK를 보낸다.
4) 응답 ACK를 받은 서버는 ESTABLISHED 상태로 변경한다.</p>
<h4 id="해제-4-way-handshaking">해제 4-way HandShaking</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/4d3f6c8e-e368-444e-8e4b-ce0a16947899/image.png" alt=""></p>
<p>#1. Client -&gt; Server : 다 보냄 끊자
#2. Server -&gt; Client : ㅇㅋ
#3. Server -&gt; Client : 나도 끊을게
#4. Client -&gt; Server : ㅇㅋㅇㅋ</p>
<p>1) 데이터를 다 보낸 클라이언트가 FIN(연결 끊음)을 보내고 FIN-WAIT-1 상태로 대기한다.
2) 서버는 CLOSE_WAIT로 바꾸고 응답 ACK를 전달한다. 동시에 해당 포트에 연결되어 있는 애플리케이션에게 close를 요청한다.
3) ACK를 받은 클라이언트는 상태를 FIN-WAIT-2로 변경한다.
4) close 요청을 받은 서버 애플리케이션은 종료 프로세스를 진행하고 FIN을 클라이언트로 보낸 LAST_ACK 상태로 바꾼다.
5) FIN을 받은 클라이언트는 ACK를 서버에 다시 전송하고 TIME-WAIT로 상태를 바꾼다.
TIME-WAIT에서 일정 시간이 지나면 CLOSE된다. ACK를 받은 서버도 포트를 CLOSED로 바꾼다.</p>
<h3 id="osi-7계층">OSI 7계층</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/a187381b-9897-46b1-b9c0-9758f93f5ab0/image.jpeg" alt=""></p>
<p>7계층(응용 계층) - 사용자에게 통신을 위한 서비스 제공. 인터페이스 역할을 한다.
6계층(표현 계층) - 데이터의 형식을 정의하는 계층 (코드 간의 번역을 담당)
5계층(세션 계층) - 컴퓨터끼리 통신을 하기 위해 세션을 만드는 계층
4계층(전송 계층) - 최종 수신 프로세스로 데이터의 전송을 담당하는 계층(TCP, UDP)
3계층(네트워크 계층) - 패킷을 목적지까지 가장 빠른 길로 전송하기 위한 계층(Router)
2계층(데이터링크 계층) - 데이터의 물리적인 전송과 에러 검출, 흐름 제어를 담당하는 계층(이더넷)
1계층(물리 계층) - 데이터를 전기 신호로 바꾸어주는 계층(케이블, 리피터, 허브)</p>
<h2 id="대칭키와-비대칭키-암호화">대칭키와 비대칭키 암호화</h2>
<p>대칭키와 비대칭키는 양방향 암호화 방식이다.</p>
<h4 id="대칭키">대칭키</h4>
<p>대칭키는 암호화와 복호화에 같은 암호 키를 쓰는 알고리즘이다.
이는 중간에 누군가 암호 키를 가로채면 암호화된 정보가 유츌될 수 있다는 단점이 있는데, 이런 문제를 보완한 새로운 방식이 비대칭키(공개키)이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/1abc224e-d183-4d5e-b7c4-1626d5cbf485/image.png" alt=""></p>
<h4 id="비대칭키">비대칭키</h4>
<p>암호화와 복호화에 서로 다른 키를 사용하는 암호화 알고리즘이다.
타인에게 노출되어서는 안되는 개인키(Private Key)와 공개적으로 개방되어 있는 공개키(Public Key)를 쌍으로 이룬 형태이다.</p>
<p><strong>부인방지 기능</strong>
보내는 사람이 자신의 Private Key로 데이터 암호화해서 PublicKey와 함께 보내면 받는 사람이 암호화된 문서를 PublicKey로 복호화 한다.
보낸 사람은 자신이 보낸게 아니라고 부인할 수 없다.</p>
<p><strong>기밀성 보장</strong>
보내는 사람이 받는 사람의 PublicKey로 암호화 해서 보내면 받는 사람은 자신의 PrivateKey로 복호화한다.
이때는 이걸 받는 사람 외에는 문서를 열 수 없음</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/9ccc57e2-110b-4482-8d7c-d72ab177dd09/image.png" alt=""></p>
<h2 id="connection-timeout과-read-timeout">Connection Timeout과 Read Timeout</h2>
<p>서버 자체에 클라이언트가 어떤 사유로 접근을 실패했을 시 적용되는 것이 Connection Timeout이다.
즉, 접근을 시도하는 시간 제한이 Connection Timeout 되는 것을 말한다.</p>
<p>클라이언트가 서버에 접속을 성공 했으나 서버가 로직을 수행하는 시간이 너무 길어 제대로 응답을 못 준 상태에서 클라이언트가 연결을 해제하는 것이 Read Timeout이다.
이 경우는 클라이언트는 해당 상황을 오류로 인지하고, 서버는 계속 로직을 수행하고 있어 성공으로 인지해 양 사이드 간 싱크가 맞지 않아 문제가 발생할 확률이 높다.</p>
<h2 id="공인-ip와-사설-ip의-차이">공인 IP와 사설 IP의 차이</h2>
<p>공인 IP는 ISP(인터넷 서비스 공급자)가 제공하는 IP주소이며, 외부에 공개되어 있는 IP주소이다.
공인 IP는 전세계에서 유일한 IP 주소를 갖는다.
공인 IP 주소가 외부에 공개되어 있기에 인터넷에 연결된 다른 PC로부터의 접근이 가능하다. 따라서 공인 IP주소를 사용하는 경우에는 방화벽 등의 보안 프로그램을 설치할 필요가 없다.</p>
<p>사설 IP는 일반 가정이나 회사 내에 할당된 네트워크 IP주소이며, IPv4 주소 부족으로 인해 서브넷팅 된 IP이기 때문에 라우터(공유기)에 의해 로컬 네트워크 상의 PC나 장치에 할당된다.</p>
<p>사설 IP 주소는 주소대역으로 고정되어 있다.
Class A : 10.0.0.0 ~ 10.255.255.255
Class B : 172.16.0.0 ~ 172.31.255.255
Class C : 192.168.0.0 ~ 192.168.255.255</p>
<p>사설 IP 주소만으로는 인터넷에 직접 연결할 수 없고, 라우터를 통해 1개의 공인 IP를 할당하고, 라우터에 연결된 개인 PC는 사설 IP를 각각 할당 받아 인터넷에 접속할 수 있다.</p>
<p>💻➡🌏 : 사설 IP를 할당받은 스마트폰 혹은 개인 PC가 데이터 패킷을 인터넷으로 전송하면, 라우터(공유기)가 해당 사설 IP를 공인 IP로 바꿔서 전송한다.</p>
<p>🌏➡💻 : 인터넷에서 오는 데이터 패킷의 목적지도 해당하는 사설 IP로 변경한 후 개인 스마트폰 혹은 PC에 전송한다.</p>
<p>인터넷 상에서 서버를 운영하고자 할 때는 공인 IP를 고정 IP로 부여해야 한다.
공인 IP를 부여받지 못하면 다른 사람이 내 서버에 접속할 수 없고, 고정 IP를 부여하지 않으면 내 서버가 아닌 다른 사람의 서버로 접속이 될 수도 있기 때문이다.</p>
<p>집에서 사용하는 인터넷 서비스 업체는 각 가정마다 공인 IP를 유동 IP로 부여하고, 공유기 내부에서는 사설 IP를 유동 IP로 부여하는 것이 일반적이다.</p>
<h2 id="restful-api">Restful API</h2>
<p>RESTful은 자원을 이름으로 구분해 해당 자원의 상태를 주고 받는 모든 것을 의미하는 REST의 설계 규칙을 잘 지켜서 설계된 API를 RESTful한 API라고 한다.</p>
<h4 id="rest">Rest</h4>
<p>자원을 이름으로 구분해 해당 자원의 상태를 주고 받는 모든 것을 의미한다.</p>
<h4 id="rest의-구성요소">Rest의 구성요소</h4>
<p>1) 자원 - URI
모든 자원에는 고유한 ID가 존재하고, 이 자원은 Server에 존재한다.
자원을 구별하는 ID는 &#39;/example?exampleId=1&#39;와 같은 HTTP URI이다.</p>
<p>2) 행위 - Method
HTTP 프로토콜을 GET, POST, PUT, PATCH, DELETE의 Method를 제공한다.</p>
<p>3) 표현 - Representation of Resource
Client와 Server가 데이터를 주고받는 형태로 JSON, XML등이 있다.</p>
<h3 id="rest-api">Rest API</h3>
<p>REST의 특징을 기반으로 서비스 API를 구현한 것
REST API의 가장 큰 특징은 각 요청이 어떤 동작이나 정보를 위한 것인지를 그 요청의 모습 자체로 추론이 가능한 것이다.</p>
<h4 id="rest-api-설계-규칙">Rest API 설계 규칙</h4>
<p>1) URI는 명사를 사용한다.
2) 슬래시(/)로 계층 관계를 표현한다.
3) URI 마지막 문자로 슬래시(/)를 포함하지 않는다.
4) 밑줄( _ )을 사용하지 않고, 하이픈(-)을 사용한다.
5) URI는 소문자로만 구성된다.</p>
<h2 id="call-by-value와-call-by-reference의-차이">Call By Value와 Call By Reference의 차이</h2>
<h4 id="call-by-value값에-의한-호출">Call By Value(값에 의한 호출)</h4>
<p>인자로 받은 값을 복사하여 처리하는 방식이다.
장점 - 값을 복사하여 처리하기 때문에 원래의 값이 보존된다.
단점 - 복사하기 때문에 메모리 사용량이 증가한다.</p>
<h4 id="call-by-reference참조에-의한-호출">Call By Reference(참조에 의한 호출)</h4>
<p>인자로 받은 값의 주소를 참조하여 직접 저장해 값에 영향을 주는 방식이다.
장점 - 복사하지 않고 직접 참조하기에 빠르다.
단점 - 직접 참조를 하기에 원래의 값이 영향을 받는다.</p>
<h4 id="자바는-기본적으로-모든-전달-방식이-call-by-value이다">자바는 기본적으로 모든 전달 방식이 Call By Value이다.</h4>
<p>나도 Call By Reference라고 생각했다.
근데 &quot;주소값&quot;이 아닌 &quot;주소를 가리키는 참조값&quot;이다.
주소값 자체를 &quot;복사 없이&quot; 인자로 전달하는게 아니라 자기 자신이 갖고 잇는 값을 복사해서 전달한다.</p>
<h2 id="cors">CORS</h2>
<p>CORS란 도메인이 서로 다른 2개의 사이트가 데이터를 주고 받을 때 발생하는 문제이다.
예를 들어 domainA.com과 domainB.com이 데이터를 주고받을 시 따로 설정하지 않으면 CORS 에러를 마나게 된다.
<strong>브라우저는 보안 상의 이유로, 스크립트에서 시작한 교차 출처 HTTP 요청을 제한한다.</strong></p>
<p>다른 서버의 리소스를 불러오기 위해서는, 그 출처에서 CORS에 대한 내용을 Response의 헤더에 추가해줘야 된다.</p>
<p>1) Access-Control-Allow-Origin - 요청을 보내는 페이지의 출처(도메인) Defaul: null
2) Access-Control-Allow-Methods - 요청을 허용하는 메서드 Default: GET, POST
3) Access-Control-Allow-Headers - 요청을 허용하는 헤더</p>
<h2 id="절차지향-vs-객체지향">절차지향 VS 객체지향</h2>
<h4 id="절차지향-프로그래밍">절차지향 프로그래밍</h4>
<p>기능중심으로 바라보는 방식으로 &quot;무엇을 어떤 절차로 할 것인가?&quot;가 핵심이 되며, 어떤 기능을 어떤 순서로 처리하는가에 대해 초점을 맞춘다. 대표적으로 C언어가 있다.</p>
<h4 id="객체지향-프로그래밍">객체지향 프로그래밍</h4>
<p>기능이 아닌 객체 중심으로 바라보는 방식으로 &quot;누가 어떤 일을 할 것인가?&quot;가 핵심이며, 객체를 도출하고 각각의 역할을 정의해 나가는 것에 초점을 둔다. 대표적으로 Java가 있다.</p>
<p>절차지향 언어가 컴퓨터의 처리구조와 유사해 실행속도가 더 빠르고, 객체지향 언어는 절차지향 언어보다 실행속도가 느리다.</p>
<h2 id="동적쿼리">동적쿼리</h2>
<p>동적 쿼리란 실행시에 특정 조건이나 실행에 따라 쿼리 문장이 변경되어 실행되는 쿼리문을 말한다.</p>
<p>컴파일시에 SQL문장을 확장할 수 없는 경우에 사용된다. 실행 시점에 따라 where절에 조건이 달라질 때 사용된다.</p>
<h2 id="csrf">CSRF</h2>
<p>사이트 간 요청 위조의 약자로 웹 애플리케이션 취약점 중 하나로 공격자가 의도한대로 사용자가 행동하게 하여 특정 웹페이지를 보안에 취약하게 한다거나 수정, 삭제 등의 작업을 하게 만드는 공격 방법을 의미한다.</p>
<p>1) 사용자의 요청에 referrer를 확인하여 도메인이 일치하는지 확인하는 방법으로 공격을 방어
요청 헤더에서 referrer 정보를 확인할 수 있다. (같은 도메인에서 들어오는 접속은 허용하나 다른 도메인에서 호출할 때는 차단하는 개념이다)</p>
<p>2) 상탤글 변화시키는 POST, PUT등의 요청에 대해 csrf 토큰이 포함되어야만 요청을 처리하여 공격을 방어</p>
<pre><code>&lt;img src=&quot;http://auction.com/changeUserAcoount?id=admin&amp;password=admin&quot; width=&quot;0&quot; height=&quot;0&quot;&gt;</code></pre><p>이미지를 누르면 저 src안에 있는 주소로 요청이 날라가게됨</p>
<p>REST API를 이용한 서버라면, session 기반 인증과는 다르게 stateless하기 때문에 서버에 인증정보를 보관하지 않는다. REST API에서 Client는 권한이 필요한 요청을 하기 위해서는 요청에 필요한 인증 정보(JWT)를 포함시켜야 된다. 
따라서 서버에 인증정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다.</p>
<h2 id="tddtest-driven-development">TDD(Test-Driven-Development)</h2>
<p>TDD란 작은 단위의 테스트 케이스를 작성하고 그에 맞는 코드를 작성하여 테스트를 통과한 후에 상황에 맞게 리팩토링 하는 테스트 주도 개발 방식을 말한다.</p>
<h4 id="tdd-사이클">TDD 사이클</h4>
<p>1) Red - 어떠한 기능을 검증하는 테스트가 실패하는 코드를 작성하고, 실제로 실패하는지 확인한다.
2) Green - 어떠한 기능을 검증하는 테스트가 통과하는 코드를 작성하고, 실제로 성공하는지 확인한다.
3) Refactor - 앞에 실패하는 테스트와 성공하는 테스트를 모두 검증했다면, 작성한 코드를 꺠끗하고 가독성 좋게 고친다.
4) Repeat - 이 세가지 과정을 반복하여 프로그램을 완성한다.</p>
<h4 id="tdd를-하는-이유">TDD를 하는 이유</h4>
<p>1) 기능의 추가, 변경, 삭제로 인한 영향도를 쉽게 파악 가능
2) 예상하지 못한 오류에 대한 피드백을 위해
3) 좋은 설계로 작성되게끔 코드를 유도</p>
<h2 id="ddddomain-driven-design">DDD(Domain-Driven-Design)</h2>
<p>Domain이란 영역 또는 집합이다.</p>
<h4 id="객체와-도메인의-차이">객체와 도메인의 차이</h4>
<p>&quot;고양이는 사과를 먹는다.&quot;
객체의 관점에서는 &quot;고양이&quot;와 &quot;사과&quot;를 표현할 수 있고, &quot;먹는다.&quot;는 객체가 하는 행위이다.
도메인의 관점에서는 &quot;고양이&quot;,&quot;사과&quot;,&quot;먹는다&quot;,&quot;고양이는 사과를 먹는다.&quot; 모두 각각 도메인이라고 할 수 있다.
객체는 현실 그대로를 표현하고 있고, 도메인은 사용자가 바라보는 관점에 따라 각각을 구분하거나 전체라고 할 수 있다.</p>
<p>1) 표현 계층(Presentation Layer) - 사용자의 요청에 대해 해석하고 응답하는 일을 책임지는 계층(Controller)
Client로부터 Request를 받고 Response를 return 하는 API 정의</p>
<p>2) 응용 계층(Application Layer) - 비즈니스 로직을 정의하고 정상적으로 수행될 수 있도록 도메인 계층과 인프라스트럭처 계층을 연결해주는 역할을 하는 계층(Service)
Transaction 관리, DTO 변환, 모듈간의 연계를 진행</p>
<p>3) 도메인 계층(Domain Layer) - 비즈니스 규칙, 정보에 대한 실질적인 도메인에 대한 정보를 가지고 있으며 이 모든것을 책임지는 계층 (Entity)
Entity를 활용하여 도메인 로직이 진행된다.</p>
<p>4) 인프라스트럭처 계층(Infrastructure Layer) - 외부와의 통신(ORM, DB, NoSQL)을 담당하는 계층 (Repository)
해당 계층에서 얻어온 정보를 응용 계층 또는 도메인 계층에 전달하는 것을 주 역할로 담당</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] DB]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-DB</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-DB</guid>
            <pubDate>Thu, 19 Dec 2024 06:34:07 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터베이스">데이터베이스</h2>
<p>데이터베이스를 한 마디로 정의하면 &quot;데이터의 집합&quot;이라고 할 수 있다.</p>
<h4 id="데이터베이스의-특징">데이터베이스의 특징</h4>
<p>1) 실시간 접근성 - 실시간 처리에 의한 응답이 가능해야 된다.
2) 지속적인 변화 - 데이터베이스의 상태는 동적이다. 즉, 새로운 데이터의 삽입, 삭제, 갱신으로 항상 최신의 데이터를 유지해야 된다.
3) 동시 공용 - 다수의 사용자가 동시에 같은 내용의 데이터를 이용할 수 있어야 된다.
4) 내용에 의한 참조 - 데이터를 참조할 때 데이터 레코드의 주소나 위치에 의해서가 아닌 사용자가 요구하는 데이터 내용으로 찾는다.</p>
<h4 id="데이터베이스-언어">데이터베이스 언어</h4>
<p>1) DDL (Data Definition Language) - 데이터베이스 구조를 정의, 수정, 삭제하는 언어 (alter, create, drop)
2) DML (Data Manipulation Language) - 데이터베이스 내의 자료 검색, 삽입, 갱신, 삭제를 위한 언어 (select, insert, update, delete)
3) DCL (Data Control Language) - 데이터에 대해 무결성 유지, 병행 수행 제어, 보호와 관리를 위한 언어 (commit, rollback, grant, revoke)</p>
<h3 id="dbms">DBMS</h3>
<p>데이터베이스를 데이터의 집합이라고 정의한다면, 데이터베이스를 관리하고 운영하는 소프트웨어를 DBMS라고 한다. 다양한 데이터가 저장되어 있는 데이터베이스는 여러 명의 사용자나 응용 프로그램과 공유하고 동시에 접근이 가능해야 된다.
응용 프로그램들이 데이터베이스에 접근할 수 있는 인터페이스를 제공하고 복구기능과 보안성 기능을 제공한다.</p>
<h4 id="dbms의-기능">DBMS의 기능</h4>
<p>1) 정의 - 데이터에 대한 형식, 구조, 조건들을 정의하는 기능이다. 정의 및 설명은 카탈로그나 사전형태로 저장된다.
2) 저장 - 기억장치에 데이터를 저장하는 기능이다.
3) 보안 - 하드웨어나 소프트웨어의 오류 또는 권한이 없는 접근으로부터 시스템을 보호한다.
4) 공유 - 여러 사용자와 프로그램이 데이터베이스에 접근할 수 있도록 공유한다.
5) 기능 - 데이터의 검색을 위한 질의나 데이터베이스의 갱신, 생성기능을 포함한다.
6) 유지 - 요구사항의 변화에 따라 반영할 수 있도록 하는 기능이다.</p>
<h4 id="dbms의-종류">DBMS의 종류</h4>
<p>Oracle - MySQL, MSSQL보다 대량의 데이터를 처리하기 용이하다, 대기업에서 주로 사용하며, DB 시장 점유율 1위이다.
MySQL - 오픈소스로 무료 프로그램이다(상업적 사용시 비용 발생), 5천만건 이하의 데이터를 다루는데 적합하다.
MariaDB - MySQL과 동일한 소스 코드 기반이다. MySQL과 비교하여 애플리케이션 부분속도가 약 4~5천배 빠르다.
PostgreSQL - 다른 DBMS에 비해 복잡한 쿼리에 탁월하다. 대용량 데이터 관리에 적합하다. NoSQL 및 다양한 데이터 형식을 지원한다.</p>
<h4 id="rdbms의-특징">RDBMS의 특징</h4>
<p>1) 데이터의 일관성(Consistency) 보장
RDB는 트랜잭션 ACID원칙을 준수하여 데이터의 일관성을 보장할 수 있다. 이러한 주요 특징은 데이터의 무결성과 안정성을 보장해 준다는 특징을 가지고 있다.</p>
<p>2) 정형화된 데이터 구조를 갖는다.
데이터들이 테이블의 형태로 구조화되어 있기 때문에 데이터의 형식과 구조가 명확하게 정의된다. 이러한 특징을 통해서도 데이터의 일관성을 보장받을 수 있다.</p>
<p>3) 데이터의 보안, 권한 제어
관계형 데이터베이스는 사용자 인증, 엑세스 제어 등 다양한 보안 기능을 제공하고 있으며 이를 통해 관리되고 있는 데이터들의 보안을 유지할 수 있다.</p>
<h3 id="옵티마이저optimizer">옵티마이저(Optimizer)</h3>
<p>SQL을 가장 빠르고 효율적으로 수행할 최적의 처리 경로를 생성해주는 DBMS 내부의 핵심 엔진이다.
컴퓨터의 두뇌가 CPU인 것처럼 DBMS의 두뇌는 옵티마이저라고 할 수 있다.
개발자가 SQL을 실행하면 바로 실행되는 것이 아닌 옵티마이저라는 곳에서 &quot;이 쿼리문을 어떻게 실행시키겠다&quot; 라는 여러가지 실행 계획을 세우고, 최고 효율을 갖는 실행계획을 판별할 후 그 실행계획에 따라 쿼리를 수행하게 되는 것이다.</p>
<h4 id="무결성과-일관성의-차이">무결성과 일관성의 차이</h4>
<p>무결성은 데이터가 정확하고 유효한 상태를 유지해야 하는 것이고 이에 따라 데이터의 형식, 제약조건(Constraint) 등이 준수되어야 한다.
일관성은 데이터베이스에서 관련된 데이터 간의 상호 연관성을 의미하고 데이터들을 대상으로 이루어지는 모든 작업들에 대해 항상 일관된 상태를 유지해야 된다. 따라서 여러 테이블 간의 관계가 정확해야 된다.</p>
<h3 id="nosql">NoSQL</h3>
<p>Not Only SQL의 약자로 비관계형 데이ㅐ터베이스를 지칭한다.
기존의 RDBMS와 같은 관계형 데이터 모델을 지양하며 대량의 분산된 비정형 데이터를 저장하고 조회하는데 특화된 데이터베이스로 스키마 없이 사용하거나 느슨한 스키마를 제공하는 저장소이다.</p>
<p>주로 빅데이터, 분산 시스템 환경에서 대용량의 데이터를 처리하는데 적합하다.
즉, 기존의 RDBMS는 일관성(Consistency)와 가용성(Availability)에 중점을 두었다면 NoSQL은 확장성(Scalability)과 가용성(Availability)에 중점을 두고 있는 것이다.</p>
<h4 id="nosql의-특징">NoSQL의 특징</h4>
<p>1) RDBMS와 달리 데이터 간의 관계를 정의하지 않는다.
RDBMS는 데이터 간의 관계를 Foreign Key로 정의하고 Join 연산을 수행할 수 있지만 NoSQL은 Key-Value 형태로 저장되기 때문에 Join 연산이 불가능하다.</p>
<p>2) RDBMS에 비해 대용량의 데이터를 저장할 수 있다.</p>
<p>3) 분산형 구조로 설계되어 있다.
여러 곳의 서버에 데이터를 분산 저장하여 특정 서버에 장애가 발생했을 때도 데이터 유실 혹은 서비스 중지가 발생하지 않도록 한다.</p>
<p>4) 데이터 중복이 발생할 수 있다.
중복된 데이터가 변경될 경우 수정을 모든 컬렉션에서 수행해야된다는 단점이 있다.</p>
<h4 id="rdbms-nosql-선택">RDBMS, NoSQL 선택</h4>
<p>RDBMS는 데이터 구조가 명확하고, 변경 될 여지가 없으며 스키마가 중요한 경우 사용하는 것이 좋다. 또한, 중복된 데이터가 없어 변경이 용이하기 때문에 관계를 맺고 있는 데이터가 자주 변경이 이루어지는 시스템에 적합하다.
NoSQL은 정확한 데이터 구조를 알 수 없고 데이터가 변경/확장 될 수 있는 경우 사용하는 것이 좋다. 중복이 허용되어 모든 컬렉션에서 수정해야 되기 때문에 Update가 많이 발생하지 않는 시스템에 좋다.
대용량의 데이터를 저장해야 돼서 DB를 Scale-out 해야 되는 시스템에 적합하다.</p>
<h2 id="mysql">MySQL</h2>
<p>MySQL은 데이터를 저장하고 관리하는 데 널리 사용되는 오픈 소스 관계형 데이터베이스 관리 시스템(RDBMS)이다.</p>
<p>MySQL은 SQL을 사용하여 데이터를 관리하고 조작한다. 트랜잭션, 보기, 저장 프로시저 및 트리거를 비롯한 다양한 기능을 지원한다.</p>
<h3 id="장단점">장단점</h3>
<h4 id="장점">장점</h4>
<p>1) 무료로 사용할 수 있고 널리 지원되는 오픈 소스 데이터베이스이다.
2) 다른 데이터베이스보다 빠르고 저렴하며 안정적인 고유한 스토리지 엔진 아키텍처가 있다.
3) 뷰, 트리거 및 저장 프로시저를 사용하여 개발자에게 더 높은 생산성을 제공한다.</p>
<h4 id="단점">단점</h4>
<p>1) MySQL은 복잡하고 강력할 수 있으므로 소규모 애플리케이션에 적합하지 않을 수 있다.
2) Oracle이 MySQL을 인수한 이후 MySQL의 운명에 대한 우려가 있다.
3) PostgreSQL처럼 기능이 풍부하지 않다.</p>
<h3 id="트리거-trigger">트리거 (Trigger)</h3>
<p>트리거는 특정 테이블에 대한 이벤트에 반응해 Insert, Delete, Update 같은 DML 문이 수행되었을 때, 실행시키고자 하는 추가 쿼리 작업들을 자동으로 수행할 수 있게끔 트리거를 미리 설정해 두는 것이다.</p>
<p>사용자가 직접 호출하는 것이 아닌, 데이터베이스에서 자동적으로 호출한다는 것이 가장 큰 특징이다.</p>
<p>데이터베이스 트리거는 테이블에 대한 이벤트에 반응해 자동으로 실행되는 작업을 의미한다.</p>
<h2 id="join">Join</h2>
<h4 id="food-테이블">Food 테이블</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/40863f96-453f-4623-b18e-fe4dc21dddc8/image.png" alt=""></p>
<h4 id="buy-테이블">Buy 테이블</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/088440a4-811d-4194-bf9a-184b35500b74/image.png" alt=""></p>
<h3 id="inner-join-내부-조인">Inner Join (내부 조인)</h3>
<p>MySQL에서는 Join, Inner Join, Cross Join이 모두 같은 의미로 사용된다.</p>
<p>Inner Join을 하면 조인 관계에 부합되는 레코드를 모두 반환한다.</p>
<pre><code>select * from food f join buy b on f.name = b.name;</code></pre><p><img src="https://velog.velcdn.com/images/diense_kk/post/77160019-b8a8-4e1f-8c62-7b8f89fbe8b7/image.png" alt="">
위 결과와 같이 두 테이블에서 이름이 같은 2개의 결과만이 조회된다.</p>
<h3 id="outer-join-외부-조인">Outer Join (외부 조인)</h3>
<h4 id="left-outer-join">Left Outer Join</h4>
<p>A LEFT OUTER JOIN B일 경우, LEFT OUTER JOIN은 왼쪽 테이블(이 경우 Food 테이블)의 모든 데이터를 반환하며, 오른쪽 테이블(이 경우 Buy 테이블)과 일치하는 데이터를 반환합니다. 만약 오른쪽 테이블에 일치하는 데이터가 없다면, 그 오른쪽 테이블의 필드는 NULL로 채워집니다.</p>
<pre><code>select * from food f left outer join buy b on f.name = b.name;</code></pre><p><img src="https://velog.velcdn.com/images/diense_kk/post/93df7849-a819-435f-950c-5c5e605c59af/image.png" alt=""></p>
<p>Food의 데이터는 모두 출력이 되었지만 Join 조건을 만족하지 못한 Food 데이터들은 Buy와 조인되지 않은채로 null이 입력된 모습을 볼 수 있다.</p>
<h4 id="right-outer-join">Right Outer Join</h4>
<p>Left Outer Join과 실행 방식은 비슷하다. 다만 Left가 아닌 Right가 기준이 된다.
<img src="https://velog.velcdn.com/images/diense_kk/post/0ce5a770-cc12-4616-9e4f-01187495b0b3/image.png" alt=""></p>
<h4 id="full-outer-join">Full Outer Join</h4>
<p>MySQL에서는 Full Outer Join을 사용하기 위해 Left Join과 Right Join 을 Union하는 형태로 사용한다.</p>
<h2 id="클러스터링과-리플리케이션">클러스터링과 리플리케이션</h2>
<h3 id="클러스터링">클러스터링</h3>
<p>여러 개의 DB를 수평적인 구조로 구축하는 방식이다. 동기 방식으로 사용된다.</p>
<h4 id="클러스터링-장점">클러스터링 장점</h4>
<p>1) DB간의 데이터를 동기화하여 항상 일관성 있는 데이터를 얻을 수 있다.
2) 1개의 DB가 죽어도 다른 DB가 살아있어 시스템을 장애없이 운영할 수 있다.(높은 가용성)
3) 기존에 하나의 DB서버에 몰리던 부하를 여러 곳으로 분산시킬 수 있다.(로드밸런싱)</p>
<h4 id="클러스터링-단점">클러스터링 단점</h4>
<p>1) 저장소 하나를 공유하면 병목현상이 발생할 수 있다.
2) 서버를 동시에 운영하기 위한 비용이 많이 든다.</p>
<h3 id="리플리케이션">리플리케이션</h3>
<p>두 개 이상의 DBMS를 이용하여 Master/Slave (수직적) 구조를 활용하여 DB의 부하를 분산시키는 기술이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/aeeeb911-c321-4c5f-9143-265374e3a570/image.png" alt=""></p>
<p>리플리케이션은 Master DB에는 Insert, Update, Delete 작업을 수행하도록 하고 Select 작업을 Slave DB에서 하도록 구성을 한다. Master에서 발생한 데이터 변경 작업이 자동으로 Slave로 동기화된다.
Select 작업을 따로 뺴는 이유는 Select 작업이 시간이 많이 걸리기 때문이다.</p>
<h4 id="리플리케이션-장점">리플리케이션 장점</h4>
<p>1) DB 요청의 60%~80% 정도가 읽기 작업이기 때문에 리플리케이션만으로도 충분히 성능을 높일 수 있다.
2) 비동기 방식으로 운영되어 지연시간이 거의 없다.</p>
<h4 id="리플리케이션-단점">리플리케이션 단점</h4>
<p>1) 노드들 간 데이터 동기화가 보장되지 않아 일관성 있는 데이터를 얻지 못할 수 있다.
2) Master DB가 다운되면 복구 및 대처가 까다롭다.</p>
<h2 id="데이터베이스-트랜잭션">데이터베이스 트랜잭션</h2>
<h3 id="트랜잭션의-정의">트랜잭션의 정의</h3>
<p>트랜잭션이란 데이터베이스 내부에서 수행되는 일련의 작업들을 하나의 논리적인 단위로 묶은 것을 의미한다.</p>
<p>돈을 송금하는 상황이라고 가정해보자. 이때 송금이라는 작업에 대한 트랜잭션이 시작된다. A 사용자의 계좌에서 100원을 뺴서 B 사용자 계좌에 100원을 송금해야 하는 상황이다. 이러한 전체적인 작업의 논리적 단위를 트랜잭션이라고 한다.</p>
<p>이 과정에서 내부 작업이 하나라도 실패하게 되면 롤백이 발생되어 트랜잭션 작업 전으로 돌아갈 수 있어야 되고, 트랜잭션이 성공하면 커밋이 수행되어야 할 것이다.
트랜잭션은 데이터베이스의 일관성을 유지하는 데 큰 역할을 한다.</p>
<h3 id="트랜잭션의-4대-원칙-acid">트랜잭션의 4대 원칙 ACID</h3>
<p>RDBMS는 시스템이 안정적이고 신뢰할 수 있는 트랜잭션 처리를 보장해 주어 데이터의 무결성과 일관성을 유지할 수 있게 도와준다.</p>
<p>1) 트랜잭션의 원자성 (Atomicity of Transaction)
트랜잭션 내부에서 수행된 모든 연산은 성공적으로 완료되거나 중간에 문제가 있어서 실패한다면 어떠한 연산도 수행되지 않은 상태로 되돌아 갈 수 있어야 된다.</p>
<p>2) 트랜잭션의 일관성 (Consistency of Transaction)
트랜잭션이 수행된 이후에도 데이터베이스는 항상 일관된 상태를 유지해야 하는 것. 즉, 트랜잭션이 시작하기 전과 끝난 이후에도 데이터베이스는 유효한 규칙을 따라야 된다.</p>
<p>3) 트랜잭션의 격리성 (Isolation of Transaction)
여러 트랜잭션이 동시에 수행될 때 각각의 트랜잭션이 서로에게 영향을 주지 않고 독립적으로 실행되어야 함을 의미한다. 트랜잭션은 다른 트랜잭션의 수행에 있어 간섭하지 않아야 된다.</p>
<p>4) 트랜잭션의 지속성(Durability of Transaction)
트랜잭션이 성공적으로 수행된 후 결과값이 영구적으로 저장되는 것을 보장하는 성질. 시스템에 문제가 발생하더라도 트랜잭션의 결과는 손실되지 않아야 된다.</p>
<h2 id="정규화">정규화</h2>
<p>정규화의 기본 목표는 테이블 간에 중복된 데이터를 허용하지 않는 것이다.
중복된 데이터를 허용하지 않음으로써 무결성을 유지할 수 있으며, DB의 저장 용량 역시 줄일 수 있다.</p>
<h3 id="이상-현상">이상 현상</h3>
<p>이상 현상은 테이블을 설계할 때 잘못 설계하여 데이터를 삽입, 삭제, 수정할 때 생기는 논리적 오류를 말한다.</p>
<p>1) 삽입 이상 - 자료를 삽입할 때 특정 속성에 해당하는 값이 없어 Null을 입력해야 하는 현상
2) 갱신 이상 - 중복된 데이터 중 일부만 수정되어 데이터 모순이 일어나는 현상
3) 삭제 이상 - 어떤 정보를 삭제하면, 의도하지 않은 다른 정보까지 삭제되어버리는 현상</p>
<p>이러한 이상 현상을 예방하고 효과적인 연산을 하기 위해 데이터 정규화를 한다.</p>
<h3 id="제1-정규화">제1 정규화</h3>
<p>제1 정규화란 테이블의 컬럼이 원자값(하나의 값)을 갖도록 테이블을 분해하는 것이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/3d3abb56-3d68-4b8f-906d-d246d31dce4a/image.png" alt=""></p>
<p>위 테이블에서는 추신수와 박세리가 여러 개의 취미를 가지고 있기 때문에 1정규형을 만족하지 못하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/d49c3786-29f7-4f35-9d80-8612f86e0f6b/image.png" alt="">
제1 정규화를 진행한 테이블은 위와 같다</p>
<h3 id="제2-정규화">제2 정규화</h3>
<p>제1 정규화를 진행한 테이블에 대해 완전 함수 종속을 만족하도록 테이블을 분해하는 것이다.
완전 함수 종속이란 기본키의 부분집합이 결정자가 되어선 안된다는 것을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/1158ee43-d1ab-408a-ae9e-8632844b9b41/image.png" alt="">
위 테이블에서 기본기는 학생번호, 강좌이름으로 복합키이다. 그리고 학생번호, 강좌이름인 기본키는 성적을 결정하고 있다. (학생번호, 강좌이름) -&gt; (성적)
하지만 여기서 강의실이라는 컬럼은 기본키의 부분집합인 강좌이름에 의해 결정될 수 있다. (강좌이름) -&gt; (강의실)</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/5a1b1daa-cc69-49e4-90e7-f5a1786db263/image.png" alt="">
제2 정규화를 진행하여 위와 같이 기존의 테이블에서 강의실을 분해하여 별도의 테이블로 관리한다.</p>
<h3 id="제3-정규화">제3 정규화</h3>
<p>제2 정규화를 진행한 테이블에 대해 이행적 종속을 없애도록 테이블을 분해하는 것이다.
이행적 종속이란 A -&gt; B, B -&gt; C가 성립될 때 A -&gt; C가 성립되는 것을 의미한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/31562f8d-56bb-42dc-9a7a-f1475b7281ac/image.png" alt=""></p>
<p>위 테이블에서 학생번호는 강좌이름을 결정하고 있고, 강좌이름은 수강료를 결정하고 있다. 그렇기 때문에 위 테이블을 (학생번호, 강좌이름) 테이블과 (강좌이름, 수강료) 테이블로 분해해야 된다.</p>
<p>만약 위 테이블에서 501번 학생이 수강하는 강좌이름을 스포츠경영학으로 수정할 경우 수강료 또한 같이 변경해줘야 하는 번거로움이 생긴다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/93250115-b5f1-4cdd-afff-c26939d286ab/image.png" alt="">
위와 같이 테이블을 분해하면 강좌이름만 수정하면 된다.</p>
<h3 id="bcnf-정규화">BCNF 정규화</h3>
<p>제3 정규화를 진행한 테이블에 대해 모든 결정자가 후보키가 되도록 테이블을 분해하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/b1c32176-2546-41fd-b162-ce3f09f5def1/image.png" alt=""></p>
<p>위 테이블에서 기본키는 (학생번호, 특강이름)이다. 그리고 기본키는 교수를 결정하고 있다. 또한 여기서 교수는 특강이름을 결정하고 있다.
여기에서 문제는 교수가 특강이름을 결정하는 결정자이지만, 후보키가 아니라는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/9c686314-9890-4e11-b617-8cd835134324/image.png" alt="">
위와 같이 테이블을 분해하면 된다.</p>
<h3 id="정규화의-장점과-단점">정규화의 장점과 단점</h3>
<h4 id="장점-1">장점</h4>
<p>1) 데이터베이스 변경 시 이상현상이 발생하는 문제점을 해결할 수 있다.
2) 데이터베이스 구조 확장 시 정규화된 데이터베이스는 그 구조를 변경하지 않아도 되거나 일부만 변경해도 된다.</p>
<h4 id="단점-1">단점</h4>
<p>1) 릴레이션의 분해로 인해 릴레이션 간의 연산(Join)이 많아진다. 이로인해 질의에 대한 응답 시간이 느려질 수 있다.</p>
<h3 id="역정규화란">역정규화란?</h3>
<p>정규화를 거치면 릴레이션 간의 연산(Join)이 많아지는데, 이로인해 성능이 저하될 우려가 있다.
역정규화를 하는 가장 큰 이유는 성능 문제가 있는(읽기작업이 많이 필요한) DB의 전반적인 성능을 향상시키기 위함이다.</p>
<h2 id="인덱스">인덱스</h2>
<p>인덱스란 추가적인 쓰기 작업과 저장 공간을 활용하여 데이터베이스 테이블의 검색 속도를 향상시키기 위한 자료구조이다.
인덱스는 특정 조건을 만족하는 데이터를 빠르게 조회하기 위해, 빠르게 정렬하거나 그룹핑하기 위해 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/baa0b86e-1de5-466c-ad84-44ab7fb9f2ef/image.png" alt=""></p>
<p>데이터베이스에서 테이블의 모든 데이터를 검색하면 시간이 오래 걸리기 때문에 데이터와 데이터의 위치를 포함한 자료구조를 생성하여 빠르게 조회할 수 있도록 돕고있다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/ec65253b-f1fd-4cfb-bf70-4e8e865af9b1/image.png" alt=""></p>
<p>인덱스를 활용함변, 데이터를 조회하는 SELECT 외에도 UPDATE, DELETE의 성능이 함께 향산된다.
이유는 UPDATE, DELETE를 실행하기 위해서는 그 대상을 조회해야만 작업을 할 수 있기 때문이다.</p>
<p>만약 인덱스를 사용하지 않은 컬럼을 조회해야 하는 상황이라면 전체를 탐색하는 Full Scan을 수행해야 된다. Full Scan은 전체를 비교하여 탐색하기 때문에 처리 속도가 떨어진다.</p>
<h3 id="인덱스-관리">인덱스 관리</h3>
<p>DBMS는 인덱스를 항상 최신의 정렬된 상태로 유지해야 원하는 값을 빠르게 탐색할 수 있다.
그렇기 때문에 SELECT를 제외한 INSERT, UPDATE, DELETE가 수행된다면 각각 다음과 같은 연산을 추가적으로 해주어야 하며 그에 따른 오버헤드가 발생한다.</p>
<p>1) INSERT - 새로운 데이터에 대한 인덱스를 추가함
2) DELETE - 삭제하는 데이터의 인덱스를 사용하지 않는다는 작업을 진행함
3) UPDATE - 기존의 인덱스를 사용하지 않음 처리하고, 갱신된 데이터에 대한 인덱스를 추가함</p>
<h4 id="btree-인덱스-자료구조">B+Tree 인덱스 자료구조</h4>
<p>자식 노드가 2개 이상인 B-Tree를 개선시킨 자료구조이다.
해시 테이블보다 나쁜 O(log2N)의 시간복잡도를 갖지만 일반적으로 사용되는 자료구조이다.</p>
<h4 id="해시-테이블">해시 테이블</h4>
<p>컬럼의 값으로 생성된 해시를 기반으로 인덱스를 구현한다.
시간 복잡도가 O(1)이라 검색이 매우 빠르다.
부등호와 같은 연속적인 데이터를 위한 순차 검색이 불가능하기 때문에 사용에 적합하지 않다.</p>
<h3 id="장점과-단점">장점과 단점</h3>
<h4 id="장점-2">장점</h4>
<p>1) 테이블을 조회하는 속도와 그에 따른 성능을 향상시킬 수 있다.
2) 전반적인 시스템의 부하를 줄일 수 있다.</p>
<h4 id="단점-2">단점</h4>
<p>1) 인덱스를 관리하기 위해 DB의 약 10%에 해당하는 저장공간이 필요하다.
2) 인덱스를 관리하기 위해 추가 작업이 필요하다.
3) 인덱스를 잘못 사용할 경우 오히려 성능이 저하되는 역효과가 발생할 수 있다.</p>
<p>만약 INSERT, DELETE, UPDATE가 빈번한 속성에 인덱스를 걸게 되면 인덱스의 크기가 비대해져서 성능이 오히려 저하되는 역효과가 발생할 수 있다. 그 이유는 UPDATE, DELETE 연산은 기존의 인덱스를 삭제하는 것이 아닌, 사용하지 않음 처리를 하는 것이기 때문이다.</p>
<h3 id="인덱스를-사용하면-좋은-경우">인덱스를 사용하면 좋은 경우</h3>
<p>1) 규모가 작지 않은 테이블
2) INSERT, UPDATE, DELETE가 자주 발생하지 않는 컬럼
3) JOIN이나 WHERE 또는 ORDER BY에 자주 사용되는 컬럼
4) 데이터의 중복도가 낮은 컬럼 (인덱스는 내부적으로 Key Value의 트리 형태로 데이터를 저장하는데, 데이터(Key)가 중복되어 여러개 존재하면 검색되는 대상이 증가하기 떄문이다.)</p>
<h3 id="인덱스를-사용할-떄-주의할-점">인덱스를 사용할 떄 주의할 점</h3>
<p>1) 데이터 변경 작업이 얼마나 자주 일어나는지 고려
2) 단일 테이블에 인덱스가 많으면 속도가 느려질 수 있다.(테이블당 4~5개 권장)
3) 검색할 데이터가 전체 데이터의 20% 이상이라면, MySQL에서 인덱스를 사용하지 않음. (강제로 사용할 시 성능 저하를 초래할 수 있음)
전체 페이지의 대부분을 읽어야 하고, 인덱스 관련 페이지도 읽어야 해서 작업량이 크기 때문이다.
검색할 데이터가 전체 데이터의 20% 이상이라는 말은 테이블에 100개의 레코드가 있고, 특정 조건으로 쿼리를 실행했을 때 결과가 20개가 넘는 것을 말하는 것이다.
4) 사용하지 않는 인덱스는 제거하는 것이 바람직함</p>
<h2 id="동시성">동시성</h2>
<p>동시성 이슈는 멀티슬레드 환경에서 발생하는 문제로, 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 말한다. 이는 데이터의 일관성을 해칠 수 있다.</p>
<p>특정 테이블의 속성 값에 +1 하는 로직을 멀티 스레드를 사용하여 100번 실행시키면 값이 몇이 될까?</p>
<pre><code>    @Transactional
    public void plusOne() {
        Stock stock = stockRepository.findById(1L).get();
        stock.plusOne();
    }</code></pre><p><img src="https://velog.velcdn.com/images/diense_kk/post/c69659b8-5b4d-43fc-a8ab-88a6229f996e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/b7ce3833-7804-4c13-b2f0-34856b54cfce/image.png" alt=""></p>
<p>JMeter를 사용하여 5000번의 요청을 보내보았다. 이 결과로 stock의 num은 5000이 되었을까?</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/8bb7fa0c-5baa-49fb-9702-7208b378b96c/image.png" alt=""></p>
<p>예상과는 다르게 num 값은 1758로 5000이 나오지 않았다.</p>
<h3 id="해결-방법">해결 방법</h3>
<h4 id="1-synchronized">1) synchronized</h4>
<pre><code>    public synchronized void plusOne(){
        this.num++;
    }</code></pre><p>자바에서의 동시성 해결 방법이다.
결론부터 말하자면** synchronized는 정답이 아니다**.</p>
<p>plusOne 메서드에 synchronized를 붙여도 결과는 이전과 비슷하다.
그 이유는 synchronized 키워드는 메서드 수준에서만 동기화를 보장하며, 데이터베이스 레벨에서의 동시성 제어를 하지 않는다.
데이터에 동시에 하나의 스레드만 접근이 가능하다는 조건은 하나의 프로세스에서만 보장된다.</p>
<h4 id="2-비관적-락pessimistic-lock">2) 비관적 락(Pessimistic Lock)</h4>
<pre><code>    @Lock(LockModeType.PESSIMISTIC_WRITE) // 읽기 쓰기 잠금
    @Query(&quot;SELECT s FROM Stock s WHERE s.id = :stockId&quot;)
    Optional&lt;Stock&gt; findByIdForUpdate(@Param(&quot;stockId&quot;) Long stockId);</code></pre><p>Lock 어노테이션을 사용하여 비관적 락을 설정했다.
위와 같이 설정하면, Select ... FOR UPDATE 쿼리가 실행되어 다른 트랜잭션이 해당 데이터를 수정하거나 읽을 수 없도록 읽기 쓰기 잠금을 설정한다.</p>
<p>FOR UPDATE 키워드가 사용되면, 트랜잭션이 해당 데이터를 읽는 순간, 데이터에 잠금이 걸려서 읽거나 수정할 수 없게 된다.
다른 트랜잭션이 동일한 데이터를 읽으려고 시도하면 잠금을 해제할 때까지 대기한다.
잠금은 트랜잭션 범위 내에서만 유효하며, 트랜잭션이 커밋되거나 롤백되면 해제된다.</p>
<p>단점으로는, 트랜잭션을 완전히 기다리기 때문에 대기 시간이 길어지고 높은 트래픽 환경에서는 성능 저하를 초래할 가능성이 높은 편이다.</p>
<p>비관적 락은 데이터 정합성(여러 데이터 간에 일관성이 유지되는 상태)이 매우 중요하거나, 충돌 가능성이 높은 경우에 적합하다.</p>
<h4 id="3-낙관적-락optimistic-lock">3) 낙관적 락(Optimistic Lock)</h4>
<p>낙관적 락은 동시성 충돌을 허용하지만, 충돌이 발생하면 이를 감지하고 처리하는 방식이다.
일반적으로 @Version 어노테이션을 사용해 구현하며, 데이터를 수정할 때 버전 정보를 기반으로 변경 충돌을 감지한다.</p>
<pre><code>    @Version
    private Long version;</code></pre><p>낙관적 락을 위한 버전 필드를 추가한다.</p>
<pre><code>    @Lock(LockModeType.OPTIMISTIC)
    @Query(&quot;SELECT s FROM Stock s WHERE s.id = :stockId&quot;)
    Optional&lt;Stock&gt; findByIdForUpdate(@Param(&quot;stockId&quot;) Long stockId);</code></pre><p>이후 Lock 어노테이션의 LockModeType을 OPTIMISTIC으로 수정한다.</p>
<pre><code>Hibernate: 
    /* SELECT s FROM Stock s WHERE s.id = :stockId */ 
    select s1_0.id,s1_0.num,s1_0.version from stock s1_0 where s1_0.id=?
Hibernate: 
    /* update for com.example.testserver.Domain.Stock */
    update stock set num=?,version=? where id=? and version=?</code></pre><p>낙관적 락은 트랜잭션이 시작될 때 잠금을 걸지 않고, 트랜잭션이 커밋될 때 버전 정보를 비교하여 충돌 여부를 확인한다. 만약 버정 정보가 일치한다면 버전 값 증가 후 커밋하고, 그렇지 않은 경우 충돌이 발생한 것으로 간주하고 예외가 발생한다.</p>
<p>@Version 어노테이션을 추가한 필드인 version은 트랜잭션이 커밋될 때 자동으로 검증되고, 충돌이 발생하면 ObjectOptimisticLockingFailureException이 발생한다.</p>
<pre><code>    @Transactional
    public void plusOne() {
        int retry = 0;
        while (retry &lt; 3) {
            try {
                Stock stock = stockRepository.findByIdForUpdate(1L).orElseThrow(() -&gt; new RuntimeException(&quot;Stock not found&quot;));
                stock.plusOne();
                return; // 성공 시 종료
            } catch (ObjectOptimisticLockingFailureException e) {
                retry++;
                if (retry &gt;= 3) {
                    log.warn(&quot;실패&quot;);
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }</code></pre><p>ObjectOptimisticLockingFailureException가 발생할 경우, 그냥 예외를 던지지 않고 재시도 횟수를 정해두고 재시도 해보는 로직을 구현할 수 있다.
여기에서 재시도 횟수를 과도하게 설정하면 좋지 않을 것같다.</p>
<p>낙관적 락이 비관적 락보다 잠금 시간이 짧기 떄문에 성능이 더 나을 수 있다. 하지만, 재시도 로직으로 인해 더 많은 시간이 걸릴 수 도 있을 것이다.</p>
<p>낙관적 락은 데이터 충돌이 자주 일어나지 않을 것으로 예상되고, 조회 성능이 중요한 경우에는 괜찮은 방법일 것이라 생각한다.</p>
<h4 id="4-수정쿼리-작성">4) 수정쿼리 작성</h4>
<pre><code>    //Repository
    @Modifying
    @Query(&quot;UPDATE Stock s SET s.num = s.num + 1 WHERE s.id = :stockId&quot;)
    void incrementNum(@Param(&quot;stockId&quot;) Long stockId);

    //Service
    @Transactional
    public void plusForQuery(){
        stockRepository.findById(1L).orElseThrow(() -&gt; new RuntimeException(&quot;Stock not found&quot;));

        stockRepository.incrementNum(1L);
    }</code></pre><p>기존 코드는 애플리케이션 수준에서 값을 읽고 증가시키는 것이였다. 사용자 두명이 동시에 ID가 1인 Stock을 조회하여 +1을 하는 메서드를 실행했다면 그 값은 1이였을 것이다.
하지만, 이 방법은 데이터베이스 수준에서 값을 증가시킨다. 두명의 사용자가 동시에 ID가 1인 Stock에 +1을 하면 값이 2가 된다. 그 이유는 데이터베이스는 트랜잭션 직렬화 매커니즘을 활용하여 동시 접근 시 순차적 처리를 보장하기 때문이다.</p>
<ul>
<li>트랜잭션 직렬화 매커니즘 - 여러 트랜잭션이 동시에 실행될 때 서로 간섭이나 충돌을 방지하는 방법이다.</li>
</ul>
<h4 id="sql-injection이란">SQL Injection이란</h4>
<p>공격자가 악의적인 의도를 갖는 SQL 구문을 삽입하여 데이터베이스를 비정상적으로 조작하는 코드 인젝션 공격 기법이다.</p>
<p>&#39; OR &#39;1&#39; = &#39;1 같은 형태로 값을 넣는 것이다.</p>
<pre><code>SELECT user FROM user_table WHERE id=&#39;admin&#39; AND password=&#39; &#39; OR &#39;1&#39; = &#39;1&#39;;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security + OAuth2]]></title>
            <link>https://velog.io/@diense_kk/Spring-Security-OAuth2</link>
            <guid>https://velog.io/@diense_kk/Spring-Security-OAuth2</guid>
            <pubDate>Thu, 12 Dec 2024 10:59:23 GMT</pubDate>
            <description><![CDATA[<h2 id="oauth2란">OAuth2란?</h2>
<p>인증을 위한 개방형 표준 프로토콜이다. 
리소스 소유자(사용자)가 특정 Third-Party 프로그램(서비스)에게 자신을 대신해 리소스 서버(구글, 카카오)에 있는 자원에 대한 접근 권한을 위임 할 수 있도록 설계되었다.</p>
<h4 id="리소스-사유자---자원의-실제-소유자-사용자이다">리소스 사유자 - 자원의 실제 소유자, 사용자이다.</h4>
<h4 id="클라이언트---리소스-소유자의-권한을-위임받아-자원에-접근하려는-third-party-애플리케이션">클라이언트 - 리소스 소유자의 권한을 위임받아 자원에 접근하려는 Third-Party 애플리케이션</h4>
<h4 id="인증-서버---사용자-승인을-받은-클라이언트에게-accesstoken을-발급한다-이-토큰은-리소스-서버에-접근할-때-사용된다">인증 서버 - 사용자 승인을 받은 클라이언트에게 AccessToken을 발급한다. 이 토큰은 리소스 서버에 접근할 때 사용된다.</h4>
<h4 id="리소스-서버---사용자-데이터를-저장하고-이를-제공하는-서버구글-카카오-등">리소스 서버 - 사용자 데이터를 저장하고 이를 제공하는 서버(구글, 카카오 등)</h4>
<p>인증 서버와 리소스 서버는 보통 같은 서비스 제공자(구글, 카카오 등)에서 운영되지만, 역할이 다르다.
인증 서버는 사용자의 인증(로그인)을 처리하고, 클라이언트에게 AccessToken과 같은 권한 부여 토큰을 발급한다.
리소스 서버는 사용자의 데이터(이메일, 프로필 이미지 등)를 저장하고, 클라이언트가 제공한 AccessToken을 검증한 후 요청한 데이터를 반환한다.</p>
<h3 id="oauth2-프로토콜을-사용하는-이유">OAuth2 프로토콜을 사용하는 이유</h3>
<p>OAuth2 프로토콜이 소셜 로그인 시스템에서 널리 사용되는 이유는 제 3자 애플리케이션을 사용자 이름과 비밀번호 없이 인증하는 매커니즘이 포함되어 있기 때문이다.
즉, OAuth2를 사용하면 사용자는 구글이나 카카오 같은 신뢰할 수 있는 서비스 제공자를 통해 제 3자 애플리케이션에 로그인할 수 있다.
신뢰할 수 있는 구글이나 카카오 같은 서비스 제공자는 사용자의 신원을 확인(로그인) 하고, 이 정보를 제 3자 애플리케이션에 제공한다.
OAuth2 는 로그인 과정을 직접 다루는 것이 아닌, 인증과 밀접하게 연관되어 있다.</p>
<h2 id="oauth2-로그인-흐름">OAuth2 로그인 흐름</h2>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/37fa2b98-6e5f-4161-80f8-5a1c4a1ed5ba/image.jpg" alt=""></p>
<p>1) 사용자가 소셜 로그인을 클릭</p>
<p>2) 프론트가 /oauth2/authorization/naver로 요청을 보낸다.</p>
<p>3) 백엔드는 인증 서버로 리다이렉트 처리한다.</p>
<p>4) 인증 서버에서 프론트에 로그인 페이지를 표시하고, 사용자는 ID와 비밀번호를 입력하여 로그인한다.</p>
<p>5) 로그인에 성공 시, 인증 서버는 등록된 리다이렉트 URI(백엔드 API 엔드포인트)로 리다이렉트 시킨다. 이 때, 쿼리 파라미터로 Authorization Code를 전달한다.</p>
<p>6) 백엔드는 이 Code를 이용해 인증 서버에 AccessToken을 요청한다.</p>
<p>7) 인증 서버는 이 Code를 확인 후, 백엔드는 인증 서버로부터 AccessToken을 받는다.</p>
<p>8) 받은 AccessToken으로 리소스 서버에 사용자 정보를 요청한다.</p>
<p>9) 리소스 서버는 AccessToken를 확인 후, 사용자의 정보를 백엔드에 발급 해준다.</p>
<p>10) 백엔드에서 사용자 정보를 확인 후, 회원가입이 되어 있지 않은 상태라면 DB에 저장 후, JWT를 생성하고, 이미 회원인 경우에는 DB에 저장하지 않고 JWT를 생성한다. (사용자의 닉네임이나 프로필 이미지와 같은 정보가 변경된 경우에는 DB에 반영)</p>
<p>11) 프론트는 로그인 요청을 하이퍼링크로 보냈기 때문에 JWT를 받을 로직이 존재하지 않는다.
그렇기 때문에 쿠키에 JWT를 발행하고, 로그인 완료된 화면으로 리다이렉트 한다.</p>
<h3 id="구현">구현</h3>
<p>위 flow를 보면 백엔드가 처리해줘야 될 부분이 굉장히 많아 보인다.
하지만 Spring Security가 이미 다 구현해두었기 때문에 직접 다 만들지 않고, 이를 잘 활용하는 방법으로 구현할 수 있다.</p>
<p>소셜 로그인을 하기 위한 OAuth2 클라이언트 부분(OAuth2Service, OAuth2User, LoginSuccessHandler)과 토큰을 발급하고 검증할 JWT부분(JWTFilter, JWTUtil)만 구현하면 된다.</p>
<p>OAuth2Service는 AccessToken으로 OAuth2 제공자 리소스 서버에서 사용자 정보를 얻어오고 정보를 가지고 OAuth2 인터페이스 구현체인 인증 객체를 생성하여 리턴해야된다.
이때 사용자 정보를 리소스 서버로부터 가져오는 과정은 이미 구현되어 있다. 상위 클래스인 DefaultOAuth2UserService객체에게 위임만 하면 된다.
<img src="https://velog.velcdn.com/images/diense_kk/post/db492ee6-ec0f-4e8b-8bee-6dcdf575c50f/image.png" alt=""></p>
<h3 id="applicationproperties">application.properties</h3>
<pre><code>#registration
#registration은 외부 서비스에서 우리 서비스를 특정하기 위해 등록하는 정보로, 등록이 필수이다.
spring.security.oauth2.client.registration.서비스명.client-name=서비스명
spring.security.oauth2.client.registration.서비스명.client-id=서비스에서 발급 받은 아이디
spring.security.oauth2.client.registration.서비스명.client-secret=서비스에서 발급 받은 비밀번호
spring.security.oauth2.client.registration.서비스명.redirect-uri=서비스에 등록한 우리쪽 로그인 성공 URI(스프링 IP:PORT/login/oauth2/서비스명)
spring.security.oauth2.client.registration.서비스명.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.서비스명.scope=리소스 서버에서 가져올 데이터 범위(name,email)

#provider
#구글, 페이스북, 깃허브 같은 경우는 provider를 등록하지 않아도 서비스별로 정해진 값이 존재한다.
spring.security.oauth2.client.provider.서비스명.authorization-uri=서비스 로그인 창 주소(네이버를 예로 https://nid.naver.com/oauth2.0/authorize)
spring.security.oauth2.client.provider.서비스명.token-uri=토큰 발급 서버 주소(https://nid.naver.com/oauth2.0/token)
spring.security.oauth2.client.provider.서비스명.user-info-uri=사용자 정보 획득 주소(https://openapi.naver.com/v1/nid/me)
spring.security.oauth2.client.provider.서비스명.user-name-attribute=응답 데이터 변수(response)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Entity]]></title>
            <link>https://velog.io/@diense_kk/Spring-Entity</link>
            <guid>https://velog.io/@diense_kk/Spring-Entity</guid>
            <pubDate>Tue, 10 Dec 2024 07:02:51 GMT</pubDate>
            <description><![CDATA[<h3 id="엔티티란">엔티티란</h3>
<p>JPA에서 엔티티는 데이터베이스 테이블을 자바의 클래스로 매핑한 객체이다.
각 엔티티 인스턴스는 DB 테이블의 한 행(row)에 해당한다.
@Entity를 붙인 클래스는 JPA가 관리할 수 있는 객체로 등록된다.
JPA는 엔티티를 영속성 컨텍스트에서 관리하면서 데이터베이스와 동기화하고, 개발자는 SQL을 직접 작성하지 않아도 메서드 호출만으로 CRUD 작업을 수행할 수 있다.</p>
<p>엔티티는 반드시 식별자를 가져야 된다. JPA에서는 @Id를 사용해 식별자를 지정한다.
@Id 어노테이션이 붙은 필드는 JPA에서 영속성 컨텍스트에서 엔티티를 관리하는 기준이 된다.</p>
<h4 id="연관관계">연관관계</h4>
<table>
<thead>
<tr>
<th align="left"><center>연관관계</center></th>
<th align="right"><center>JPA Annotation</center></th>
</tr>
</thead>
<tbody><tr>
<td align="left"><center>1:1</center></td>
<td align="right"><center>@OneToOne</center></td>
</tr>
<tr>
<td align="left"><center>1:N</td>
<td align="right"><center>@OneToMany</td>
</tr>
<tr>
<td align="left"><center>N:1</center></td>
<td align="right"><center>@ManyToOne</center></td>
</tr>
<tr>
<td align="left"><center>N:M</td>
<td align="right"><center>@ManyToMany</td>
</tr>
</tbody></table>
<h3 id="연관관계-정의-규칙">연관관계 정의 규칙</h3>
<p>크게 3가지를 생각해야된다.</p>
<p>1) 방향 : 단방향, 양방향 (객체 참조)
2) 연관 관계의 주인 : 양방향일 때, 연관관계에서 관리 주체
3) 다중성 : 다대일, 일대다, 일대일, 다대다</p>
<h2 id="onetomany-manytoone-사용법">@OneToMany, @ManyToOne 사용법</h2>
<p>사용자(User)가 글(Post)을 작성하고 댓글(Comment)를 달 수 있다.</p>
<pre><code>@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;user_id&quot;)
    private Long userId;

    @OneToMany
    private List&lt;Post&gt; posts;

    @OneToMany
    private List&lt;Comment&gt; comments;
}

@Entity
@Getter @Setter
public class Post {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private User user;

    @OneToMany
    private List&lt;Comment&gt; comments;
}

@Entity
@Getter @Setter
public class Comment {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Post post;

    @ManyToOne
    private User user;
}</code></pre><h3 id="양방향과-단방향">양방향과 단방향</h3>
<p>DB 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다.
따라서 DB는 단방향이나 양방향으로 나눌 필요가 없다.
하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
선택은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해보면 된다.
user.getPosts처럼 참조가 필요하면 User -&gt; Posts 참조
posts.getUser()처럼 참조가 필요하면 Posts -&gt; User 참조</p>
<p>만약 참조가 필요없다면 하지 않으면 된다.</p>
<h4 id="그냥-무조건-양방향-관계를-맺으면-쉽지-않나">그냥 무조건 양방향 관계를 맺으면 쉽지 않나?</h4>
<p>객체 입장에서는 양방향 매핑을 했을 때 오히려 복잡해질 수 있다.
User 엔티티는 일반적인 비즈니스 애플리케이션에서 굉장히 많은 엔티티와 연관 관계를 갖는다.
이러면 User 엔티티는 엄청나게 많은 테이블과 연관관계를 맺게 되어 복잡성이 증가한다.</p>
<p>기본적으로 단방향 매핑으로 하고 나중에 양방향 객체 탐색이 꼭 필요한 경우에 추가하면 된다.</p>
<h3 id="양방향과-단방향-연관관계">양방향과 단방향 연관관계</h3>
<p>단방향 - 한쪽 엔티티에서만 연관관계를 설정한다.
양방향 - 양쪽 엔티티 모두 연관관계를 설정한다.</p>
<p>@OneToMany 기준
단방향은 상대 엔티티에 @ManyToOne이 없는 경우이다.
양방향은 상대 엔티티에 @ManyToOne이 있는 경우이다.</p>
<p>@ManyToOne 기준
단방향은 상대 엔티티에 @OneToMany가 없는 경우이다.
양방향은 상대 엔티티에 @OneToMany가 있는 경우이다.</p>
<p>둘의 양방향은 기준만 다를 뿐 차이는 없다.</p>
<p>단방향과 양방향 상관 없이 @OneToMany가 붙어있는 엔티티가 부모 엔티티이다.
쉽게 생각하면 FK를 가진 쪽이 자식 엔티티이다.</p>
<p>연관관계의 주인을 지정하는 것은 양방향 관계 중, 제어의 권한을 갖는 실질적인 관계가 어떤 것이닞 JPA에게 알려주는 것이다.
관계의 주인은 연관관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제할 수 있지만, 주인이 아니면 조회만 가능하다.</p>
<pre><code>post.setUser(user); -&gt; // 관계 설정 가능
user.getPosts().add(post) -&gt; DB 반영 안됨</code></pre><p>양방향 연관관계에서는 mappedBy 속성을 사용해 어느 쪽이 주인이며(FK를 관리) 어느 쪽이 연관관계의 주인을 따라가는지 지정해야 된다.
mappedBy는 주인이 아닌 쪽에 붙인다.
User 엔티티와  Posts 엔티티를 예로 들면</p>
<pre><code>@Entity
public class Post {
    @ManyToOne
    @JoinColumn(name = &quot;user_id&quot;) // DB의 컬럼명과 같아야된다.
    private User user;
}

@Entity
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;user_id&quot;)
    private Long userId;
    @OneToMany(mappedBy = &quot;user&quot;) // 주인 엔티티의 필드명과 같아야된다.
    private List&lt;Post&gt; posts;
}</code></pre><blockquote>
<p>쉽게 말해 FK를 가진 쪽이 자식 엔티티이며, 관계를 주도한다(연관관계의 주인이다).
  자식 엔티티는 외래 키의 값을 업데이트하거나 관리할 수 있는 권한이 있다.</p>
</blockquote>
<p>물론 단방향의 경우, 주인은 자연스럽게 해당 어노테이션이 존재하는 엔티티가 된다.
이 경우에는 그렇게 주인 개념이 강하지는 않다.
하지만 FK를 들고있는 Many쪽의 자식 엔티티가 주인이 되는 것이 더 자연스럽다.</p>
<p>따라서, @OneToMany 단방향을 사용하여 부모 엔티티가 주인이 되기 보다는 양방향 연관관계를 이용해서 자식 엔티티가 FK를 관리하는 것이 권장된다.</p>
<h3 id="manytoone-단방향">@ManyToOne 단방향</h3>
<p>@JoinColumn 어노테이션과 함께 쓰이며, 이때 @JoinColumn은 엔티티 테이블에 FK 컬럼을 정의해준다.</p>
<pre><code>/* Post.java */
@ManyToOne
@JoinColumn
private User user</code></pre><h3 id="onetomany-단방향">@OneToMany 단방향</h3>
<p>엔티티를 참조할 수 있는 매핑이 부모 엔티티에 존재하지만, FK는 자식 엔티티 테이블에 존재하는 연관관계이다.</p>
<p>@JoinColumn 없이 사용할 경우 Hibernate에서 자체적으로 중간 테이블을 생성하여 연관관계를 관리하게 된다.</p>
<h2 id="옵션">옵션</h2>
<h3 id="fetch">fetch</h3>
<p>해당 객체를 DB에서 조회할 때, 연관관계에 있는 엔티티의 정보를 언제 끌어올지에 대한 옵션이다.</p>
<p>1) Lazy Fetch
연관관계에 있는 엔티티에 접근할 때 DB에 쿼리를 날려 엔티티를 조회한다.
접근하지 않는 경우에는 쿼리가 발생하지 않는다.</p>
<p>2) Eager Fetch
조회 여부에 상관없이 쿼리가 발생한다.</p>
<p>@OneToMany의 기본값은 Lazy Fetch이며, @ManyToOne의 기본값은 Eager Fetch이다.
Eager, Lazy 값에 상관없이 단건 조회가 아닌 경우에는 N+1 문제가 발생할 수 있다.</p>
<h3 id="영속성-전이-cascade">영속성 전이 cascade</h3>
<p>특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티들에 대해 영속성을 전파시키는 옵션이다.
CascadeType은 6가지가 있다.</p>
<p>1) PERSIST - 저장 메서드 호출 시 연관된 엔티티도 저장 (user 만들고 posts 만들어서 add해둔 경우)
2) REMOVE - 삭제 메서드 호출 시 연관된 엔티티도 삭제 (user 삭제시 posts도 삭제 -&gt; Comment도 삭제되는거임)
3) MERGE - 병합 메서드 호출 시 연관된 엔티티도 병합 ()
4) REFRESH - 새로고침 메서드 호출 시 인스턴스의 값을 다시 읽어옴 (user 값 변경 후 시도하면 변경된 값이 무효화 되고, DB에서 최신 상태를 다시 읽어온다.)
5) DETACH - detach 메서드 호출 시 연관된 엔티티들까지 준영속 상태로 변환 (user를 준영속 상태로 만들면 posts도 준영속 상태가 되어 값을 변경하더라도 DB에 적용 X)
6) ALL - 위에 항목 전부 포함됨  </p>
<p>영속성 전파를 설정하게 되면, 객체에 해당 작업이 이루어질 때, 자식 엔티티에도 작업이 전파된다.</p>
<h3 id="영속성-컨텍스트란">영속성 컨텍스트란?</h3>
<p>영속성 컨텍스트는 &quot;엔티티를 영구 저장하는 환경&quot;이다.
엔티티를 저장하거나 조회할 때 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
JPA는 트랜잭션을 커밋할 때 영속성 컨텍스트에 새로 저장된 Entity를 데이터베이스 자동으로 반영해준다. (영속 상태에서 데이터를 변경하고 따로 저장하지 않아도 자동 반영)</p>
<h3 id="jpa에서의-영속성">JPA에서의 영속성</h3>
<p>영속 상태란 JPA에서 엔티티가 영속성 컨텍스트에 의해 관리되고 있는 상태로, DB와의 동기화를 JPA가 보장하는 상태이다.
JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있느냐이다.
영속성 컨텍스트가 유지된 상태에서 엔티티의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경 내용을 반영하게 된다.
이러한 개념을 더티 체킹이라고 한다.</p>
<ul>
<li>더티 체킹 -&gt; 상태 변경 검사</li>
</ul>
<h3 id="동작">동작</h3>
<p>EntityManagerFactory를 빈으로 등록 -&gt; EntityManager를 생성 -&gt; 트랜잭션 시작 -&gt; 엔티티를 영속 상태로 변경 -&gt; 필요한 작업 수행 -&gt; 트랜잭션 종료</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/8b4fb95a-0c95-4d22-8c91-0cac63ed067c/image.png" alt=""></p>
<h3 id="설계시-주의점">설계시 주의점</h3>
<p>1) 가급적 Setter를 사용하지 말자
Setter가 모두 열려있는 경우, 변경 포인트가 너무 많아서 유지보수가 어렵다.</p>
<p>2) 지연로딩으로 설정하자
즉시 로딩은 연관 테이블까지 모두 조회하기 때문에 예측이 어렵고 어떤 SQL이 실행될지 추적하기 어렵다.</p>
<p>3) 컬렉션은 필드에서 초기화하자
컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
NPE 문제에서 안전하다.</p>
<h2 id="id-uuid">ID (UUID?)</h2>
<p>클라이언트와 서버 사이에서 데이터를 확인하기 위해 PK를 주고받는 것은 보안적인 측면에서 위험하다고 한다.</p>
<p><a href="http://www.domain.com/user/info?userid=1">http://www.domain.com/user/info?userid=1</a>
이러한 URL이 있을때 파라미터로 들어가는 userid 값만 바꿔도, 다른 사람의 정보를 확인할 수 있는 것을 예측할 수 있다.
따라서 PK값을 그대로 넘겨주는 것은 바람직하지 않다.</p>
<p>서버 내에서 Token의 userId와 파라미터로 들어온 userid값을 비교하는 방식으로 해결 가능하지만, 트래픽이 많아져 서버를 늘리게 된다면 글로벌한 환경에서 고유한 값을 유지할 수 있도록 관리해야 된다.</p>
<h3 id="uuid">UUID</h3>
<p>UUID는 고유성이 보장되는 표준 규약이다.</p>
<p>Java.util 에서 제공하는 UUID 클래스는 UUIDv4인데 해당 방식은 단순 랜덤으로 값을 생성한다.
하지만, MySQL에선 기본적으로 B-Tree로 데이터를 관리하기 때문에 항상 정렬된 상태를 유지한다.
기본적으로 삽입되는 데이터의 기본키를 기준으로 구조를 재배치하는데 auto_increment와 같은 순차적인 값을 넣을때는 재배치를 하지 않지만 UUID와 같은 순서가 보장되지 않는 경우에는 재배치를 하게 된다.</p>
<p>UUID는 기본적으로 16진수 32개로 이루어진 16바이트의 크기를 가지지만 이를 DB에 그대로 문자열로 저장한다면 32자리이기 때문에 32바이트가 된다.
이는 bigint auto_increment보다 큰 용량을 차지하게 된다.</p>
<h4 id="최적화-방법">최적화 방법</h4>
<p>UUIDv4가 아닌 UUIDv1을 사용한다면 Timestamp를 기반으로 값을 생성하여, 순차적인 값을 생성하기 때문에 재배치를 하지 않아도 된다.</p>
<p>총 128bits로 구성되어 32개의 문자가 5 묶음으로 구분되어 있는 형태이다.
V1, V2 형태)
Timestamp - Timestamp - Timestamp &amp; Version - Variant &amp; Clock sequence - Node id
(버전이 높다고 좋은 건 아님)</p>
<p>위에서 언급 했듯이 문자열 형식으로 저장하게 되면 32바이트가 된다.
MySQL에서는 binary라는 타입을 제공하기 때문에 binary(16) 타입으로 UUID를 저장해야 된다.
JPA Entity에서 UUID 또는 byte[] 타입으로 사용하면 된다.
하지만 JPA는 binary(16)을 UUID로 변환해주긴 하지만 UUID v4로 생성되기 때문에 UUID v1을 사용하는 경우에는 @Converter를 정의하거나 @Id에 @Convert를 적용해야 된다.</p>
<p>UUIDv4를 사용하기를 원하는 경우에는 DB에서는 auto_increment를 사용하고, UUID를 사용하는 엔티티 id 컬럼에 보조 인덱스를 거는 방식으로 성능 저하를 완화할 수 있다.</p>
<p>하지만 트래픽이 정말 많지 않고, 데이터가 아무리 생각해도 수백, 수천만 건이 생기지 않은 서비스가 아니라면 단순히 auto_increment PK를 엔티티의 ID로 사용하거나, UUID를 PK와 엔티티의 ID로써 사용하여도 큰 문제는 없을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 캐싱을 포함한 성능 최적화]]></title>
            <link>https://velog.io/@diense_kk/Spring-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@diense_kk/Spring-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 09 Dec 2024 08:44:58 GMT</pubDate>
            <description><![CDATA[<p>스프링 부트 애플리케이션의 성능을 극대화하는 방법을 알아보자</p>
<h2 id="1-캐싱">1. 캐싱</h2>
<blockquote>
<p>캐시 사용은 자주 요청되지만 변경은 적은 데이터 또는 계산 비용이 높은 데이터 (통계 데이터)에 적합하다.</p>
</blockquote>
<p>캐싱은 반복적인 데이터 조회나 연산 결과를 메모리에 저장해 나중에 동일한 요청이 들어올 때 빠르게 응답할 수 있도록 하는 기법이다.
캐싱을 적절히 활용하면 DB 요청 수를 줄이고 애플리케이션의 응답 속도를 크게 향상시킬 수 있다.</p>
<p>스프링 부트에서 캐싱을 사용하려면 애플리케이션 클래스에 @EnableCaching 애너테이션을 추가하고, 재시를 적용할 메서드에 @Cacheable 애너테이션을 사용한다.
@CacheEvict 애너테이션을 사용하여 캐시를 명시적으로 제거할 수도 있디.</p>
<p>스프링 부트는 캐시 추상화를 통해 다양한 캐시 제공자(EhCache, Redis, Caffeine 등)를 지원하며, 애플리케이션 요구에 맞게 캐시 전략을 선택할 수 있다.</p>
<p>Cache Hit - Redis에 데이터가 있을 경우 바로 가져옴 (빠르다)
Cache Miss - Redis에 데이터가 없을 경우 DB에서 가져옴 (느리다)</p>
<h3 id="캐시-전략">캐시 전략</h3>
<h4 id="캐시-읽기-전략">캐시 읽기 전략</h4>
<p><strong>1) Look Aside 패턴</strong>
데이터를 찾을 때 우선 캐시에 저장된 데이터가 있는지 확인 후 없다면 DB에서 조회한다.
반복적인 읽기가 많은 호출에 적합하다.
만약 Redis가 다운 되더라도 DB에서 데이터를 가져올 수 있어서 서비스 자체는 문제가 없다.
<img src="https://velog.velcdn.com/images/diense_kk/post/8e2f711c-8cf1-4f8d-83f2-f9b5ed0368a2/image.png" alt=""></p>
<p><strong>2) Read Through 패턴</strong>
캐시에서만 데이터를 읽어오는 전략
캐시에 데이터가 없을 경우 캐시가 직접 DB에서 데이터를 조회하여 자체 업데이트한다.
따라서 데이터를 조회하는데 있어 전체적으로 속도가 느리다.
Redis가 다운될 경우 서비스 이용에 차질이 생긴다.
대신 캐시와 DB 간의 데이터 동기화가 항상 이루어져 데이터 정합성 문제에서 벗어날 수 있다.
<img src="https://velog.velcdn.com/images/diense_kk/post/d3135cb8-b6c1-47ff-b531-d9b07cd7f5dd/image.png" alt=""></p>
<h4 id="캐시-쓰기-전략">캐시 쓰기 전략</h4>
<p><strong>1) Write Back 패턴</strong>
캐시와 DB 동기화를 비동기하기 때문에 동기화 과정이 생략된다.
데이터를 저장할 때 DB에 바로 쿼리하지 않고, 캐시에 모아서 일정 주기 배치 작업을 통해 DB에 반영
모아뒀다가 DB에 쓰기 떄문에 쓰기 쿼리 회수 비용과 부하를 줄일 수 있다.
Write가 빈번하면서 Read를 하는데 많은 양의 리소스가 소모되는 서비스에 적합하다.
다만 캐시에서 오류가 발생하면 데이터를 영구 소실한다.
<img src="https://velog.velcdn.com/images/diense_kk/post/30ecba62-b207-41de-afd7-7bd5a3bb13bb/image.png" alt=""></p>
<p><strong>2) Write Through 패턴</strong>
DB와 캐시에 동시에 데이터를 저장한다.
캐시에 먼저 저장하고 바로 DB에 저장한다. 캐시에 먼저 저장하는 이유는 데이터를 읽는 요청은 캐시를 먼저 읽기 떄문이다.
데이터 유실이 발생함녀 안되는 상황에 적합하다.
다만 매 요청마다 2번의 Write가 발생하여 성능 이슈가 발생한다.
<img src="https://velog.velcdn.com/images/diense_kk/post/f20e7165-0fd9-4c61-b7f0-cf1b8f8e15e6/image.png" alt=""></p>
<p><strong>3) Write Around 패턴</strong>
Write Through 보다 훨씬 빠르다.
모든 데이터는 캐시를 저장하지 않고 DB에 저장한다.
Cache Miss가 발생하는 경우에만 캐시에도 저장
캐시와 DB의 데이터가 불일치 할 가능성이 높다.
DB 데이터가 수정되는 경우에 캐시에 있는 데이터를 수정 또는 삭제하는 방법으로 해결 해야된다.
<img src="https://velog.velcdn.com/images/diense_kk/post/dda1de64-c79a-498e-8160-45b31f6f1433/image.png" alt=""></p>
<blockquote>
<p>Write Around 패턴은 Look Aside + Read Through로 사용된다.</p>
</blockquote>
<h4 id="캐시-읽기--쓰기-전략-조합">캐시 읽기 + 쓰기 전략 조합</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/d4ea9ba6-5dd5-43bf-bcde-d5235ee55683/image.png" alt=""></p>
<p>1) Look Aside + Write Around 조합
가장 일반적으로 자주 쓰이는 조합이다.</p>
<p>2) Read Through + Write Around 조합
항상 DB에 쓰고, 캐시에서 읽을 때 항상 DB에서 먼저 읽어오기 때문에 데이터 정합성 이슈에 대한 완벽한 안전 장치를 구성할 수 있다.</p>
<p>3) Read Through + Write Through 조합
데이터를 쓸 때 항상 캐시에 먼저 쓰기 때문에 읽어올 때 최신 캐시 데이터를 보장한다.
데이터를 쓸 때 항상 캐시에서 DB로 보내기 때문에, 데이터 정합성을 보장한다. </p>
<h2 id="2-데이터베이스-최적화">2. 데이터베이스 최적화</h2>
<p>데이터베이스는 애플리케이션 성능에 큰 영향을 미친다.</p>
<p>1) 쿼리 최적화
불필요한 쿼리를 줄이고, 필요한 데이터만 조회하도록 쿼리를 최적화 해야된다.
반환 자체를 Entity가 아닌 DTO로 받는 방법이 있다. 이러면 내가 필요한 필드만 받아올 수 있다.</p>
<p>2) 지연로딩을 사용
JPA의 Lazy Loading을 활용하면, 실제로 필요한 시점에만 데이터를 로딩하도록 설정한다.
실제 사용되는(필요한) 시점에 쿼리가 나가도록 할 수 있다는 것이다. 사용하지 않으면 쿼리가 나가지 않음
이는 불필요한 데이터 로딩을 방지하고 성능을 최적화하는 데 도움이 된다.</p>
<p>즉시로딩 - 데이터를 조회할 때, 연관된 모든 객체의 데이터까지 한 번에 불러오는 것이다.</p>
<p>3) 인덱스 사용
테이블에 적절한 인덱스를 설정하여 데이터 조회 속도를 높일 수 있다. 
인덱스는 자주 조회되는 필드에 적용하고, 복합 인덱스도 고려해야 된다.</p>
<h2 id="3-비동기-처리와-멀티쓰레딩">3. 비동기 처리와 멀티쓰레딩</h2>
<p>1) 비동기 처리
비동기 처리는 멀티스레드를 사용하여 작업을 분리하고, 작업이 끝날 때까지 대기하지 않고 다른 작업을 처리할 수 있다.
애플리케이션 클래스에 @EnableAsync를 붙이고 비동기로 실행할 메서드에 @Async를 붙이면 된다.
@Async 메서드가 붙은 메서드는 별도의 스레드에서 실행되므로 메인 스레드에서 캐치를 할 수 없기 때문에 예외가 발생해도 호출자에게 전파가 되지 않는다.</p>
<p>2) 멀티쓰레딩
하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것이다.
멀티쓰레딩을 활용하면 CPU 자원을 최대한 활용할 수 있다.
스프링의 ThreadPoolTaskExecutor를 사용해 쓰레드 풀을 구성하고 효율적으로 작업을 분배할 수 있다.
ThreadPoolTaskExecutor를 사용해야 매 비동기 작업마다 새로운 스레드를 생성하지 않고, 제한된 리소스를 사용하는 스레드풀을 사용하여 리소스를 낭비하지 않을 수 있다.</p>
<p>요청이 동시에 굉장히 많이 들어오면 서버는 쓰레드 풀에서 처리 할 수 있는 만큼만 동시에 스레드를 생성하여 작업을 처리하고 나머지 요청들은 대기큐에 쌓여서 처리가 가능한 시점까지 기다린다.</p>
<h2 id="프로파일링-및-모니터링">프로파일링 및 모니터링</h2>
<p>1) 스프링 부트 액추에이터
애플리케이션의 상태를 모니터링하고, 메트릭스를 제공하여 성능을 최적화할 수 있는 유용한 도구이다.
이를 통해 애플리케이션의 상태, 메모리 사용량, HTTP 요청 처리 속도 등을 실시간을 확인할 수 있다.</p>
<p>액추에이터가 제공하는 기능은 우리 애플리케이션 내부 정보를 너무 많이 노출하기 때문에, 외부 인터넷망이 공개된 곳에 액추에이터의 엔드포인트를 공개하는 것은 보안상 좋지 않다. 
(액추에이터를 다른 포트에서 실행하거나 엔드포인트 경로 변경 등으로 해결)</p>
<p>2) APM(Application Performance Monitoring) 도구 사용
DataDog와 같은 APM 도구를 사용하여 애플리케이션 성능을 모니터링 하고, 병목 현상을 발견할 수 있다.
트랜잭션 추적, 메모리 및 CPU 사용량 모니터링, 오류 보고 등 다양한 기능을 제공한다.</p>
<h2 id="의존성-관리-및-애플리케이션-경량화">의존성 관리 및 애플리케이션 경량화</h2>
<p>1) 필요하지 않은 의존성 제거
필요하지 않은 의존성 을 제거하여 애플리케이션을 경량화 한다면 애플리케이션의 성능을 높일 수 있다.
불필요한 라이브러리는 애플리케이션 시작 시간과 메모리 사용량에 악영향을 미칠 수 있다.</p>
<p>2) JVM 튜닝
애플리케이션 성능을 높이기 위해 JVM의 가비지 컬렉션 정책이나 힙 메모리 크기 등을 튜닝할 수 있다.</p>
<h2 id="스프링-프로파일-사용">스프링 프로파일 사용</h2>
<p>1) 프로파일 정의
스프링 부트는 개발, 테스트, 프로덕션 환경에 맞게 설정을 분리할 수 있도록 프로파일 기능을 제공한다.
각 환경에 최적화된 설정을 사용하면 성능을 크게 개선할 수 있다.</p>
<p>2) 프로파일 활성화
application.properties 또는 환경 변수에서 spring.profiles.active 값을 설정하여 활성화할 프로파일을 지정한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] Kafka]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Kafka</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Kafka</guid>
            <pubDate>Sat, 30 Nov 2024 07:44:21 GMT</pubDate>
            <description><![CDATA[<h2 id="kafka">Kafka</h2>
<p>Kafka는 분산형 스트리밍 플랫폼으로, 대량의 데이터를 안정적이고 실시간으로 처리할 수 있도록 설계되었다.
카프카는 주로 대량의 이벤트 스트림 데이터를 처리하고 여러 시스템 간에 데이터를 신속하게 전송하는데 사용된다.</p>
<p>카프카는 기업에서 대규모 데이터 처리 및 이벤트 기반 시스템을 구축하는데 사용되며, 대용량의 로그 데이터를 수집하고 분석하는데 유용하다.</p>
<p>Pub-Sub 모델의 메시지 큐 형태로 동작하며 분산환경에 특화되어 있다.</p>
<p>Kafka는 큐처럼 메시지가 생산(요청)된 순서대로 소비(소비자에게 응답)되는 특성이 있다.</p>
<h3 id="kafka-등장-전">Kafka 등장 전</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/3614d0d8-d211-485b-b057-dd36f3ced9b6/image.png" alt=""></p>
<p>Kafka 등장 전에는 각 애플리케이션과 DB가 End-To-End로 연결되어 있고, 요구사항이 늘어남에 따라 데이터 시스템 복잡도가 높아지면서 몇가지 문제가 있었다.</p>
<h4 id="문제점">문제점</h4>
<p>1) 시스템 복잡도 증가
특정 부분에서 장애 발생 시 조치 시간이 증가한다. 연결 되어있는 애플리케이션을 모두 확인해야 하기 떄문이다.
2) 데이터 파이프라인 관리의 어려움
새로운 파이프라인 확장이 어려워지면서, 확장성 및 유연성이 떨어짐</p>
<h3 id="pub-sub-모델">Pub-Sub 모델</h3>
<p>Pub-Sub 모델은 Publish/Subscribe의 줄임말로 메시지 기반의 미들웨어 시스템을 말한다. 
일반적으로 Server-Client 구조에서는 메시지를 전송할 떄는 Publisher가 Subscriber(Receiver)에게 직접 메시지를 전송한다.
하지만 Pub-Sub 모델에서는 Publisher는 어떤 Subscriber가 있는지 모르는 상태에서 메시지를 전송하고 Subscriber는 Publisher에 대한 정보 없이 자신의 Interest에 맞는 메시지만을 전송 받는 것을 말한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/21af49d1-d8d4-407e-904e-3b45be88655e/image.png" alt=""></p>
<p>Publisher와 Subscriber가 직접적으로 연결되지 않는 것이 핵심이다.</p>
<p>카프카는 발행-구독(Pub-Sub)모델에서 브로커의 역할을 하고있다. 
발행자(Publisher)와 구독자(Consumer) 사이에서 이벤트라 불리는 메시지를 받고, 전달한다.</p>
<h3 id="message-queue">Message Queue</h3>
<p>메시지 큐는 메시지 지향 미들웨어를 구현한 시스템으로 프로그램 간의 데이터를 교환할 때 사용하는 기술이다.
<img src="https://velog.velcdn.com/images/diense_kk/post/40f85750-e6ea-4f03-975a-b51c484659f7/image.png" alt=""></p>
<p><strong>Producer</strong> - 정보를 제공
<strong>Consumer</strong> - 정보를 제공 받아서 사용
<strong>Queue</strong> - Producer의 데이터를 임시 저장 및 Consumer에 제공</p>
<p>Message Queue에서 메시지는 Endpoint간에 직접적으로 통신하지 않고, 중간에 Queue를 통해 중개된다.</p>
<h4 id="mq의-장점">MQ의 장점</h4>
<p>1) 비동기
Queue라는 임시 저장소가 있기 때문에 나중에 처리 가능
2) 낮은 결합도
애플리케이션과 분리
3) 확장성
Producer/Consumer 서비스를 원하는대로 확장할 수 있음 (서버 인스턴스를 늘릴 수 있음)
4) 탄력성
Consumer가 다운되더라도 애플리케이션이 중단되는 것은 아니기 때문에 메시지는 지속하여 MQ에 남아있는다.
5) 보장성
MQ에 메시지가 들어가면 모든 메시지가 Consumer 서비스에게 전달되는 것을 보장한다.</p>
<h3 id="message-broker-vs-event-broker">Message Broker VS Event Broker</h3>
<p>둘은 공통적으로 Publisher가 메시지를 보내면 메시지를 저장했다가 Consumer가 가져갈 수 있도록 중간 다리 역할을 해주는 브로커이다.</p>
<p>둘의 가장 큰 차이점은 메시지를 소비하고 그 메시지를 바로 삭제하냐? 이다.</p>
<p>Message Broker는 Consumer가 큐에서 데이터를 가져가게 되면 즉시 짧은 시간 내에 큐에서 데이터를 삭제하는 특징이 있다.</p>
<p>하지만 Event Broker는 이벤트를 처리한 후에 바로 삭제하지 않고 저장하여, 이벤트 시점이 저장되어 있어서 Consumer가 특정 시점부터 이벤트를 다시 소비할 수 있는 장점이 있다.
예를들어, 장애가 발행한 시점부터 그 이후의 이벤트를 다시 처리할 수 있음</p>
<p>Message Broker - Redis, RabbitMQ
Event Broker - Kafka</p>
<h3 id="kafka-구성요소">Kafka 구성요소</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/103b2ef0-d16c-4ecd-a394-f3ef082cdc49/image.png" alt=""></p>
<h4 id="kafka-cluster">Kafka Cluster</h4>
<p>브로커들의 모임으로 확장성과 고가용성을 위해 Broker들이 클러스터로 구성되어 있다.</p>
<h4 id="broker">Broker</h4>
<p>각각의 Kafka 서버를 말한다.
Producer에게 메시지를 전달받아 토픽에 저장하고 컨슈머에 저장한다.
하나의 브로커는 여러 개의 토픽을 가질 수 있다.</p>
<h4 id="zookeeper">Zookeeper</h4>
<p>Kafka의 분산처리를 위한 관리 도구이다.
Kafka 클러스터 상태와 정보 등을 관리하는 역할을 한다.
Zookeeper는 어떤 브로커가 특정 파티션 및 토픽의 리더인지 결정하고 리더 선택을 수행하는데 사용 된다.</p>
<p><strong>한계</strong> - Kafka 자체가 아닌 외부에서 메타데이터를 관리하여, Kafka 확장성에 제한이 된다.</p>
<h4 id="kraft-모드">KRaft 모드</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/59809438-8ec6-4cc9-a3ab-6093ebee0cfe/image.webp" alt=""></p>
<p>KRaft 모드는 Kafka가 Zookeeper 없이 독립적으로 클러스터 메타데이터를 관리할 수 있게 해준다.
KRaft 모드를 통해 Kafka가 단순화 되어 확장성, 안정성, 일관성 등에 도움이 된다. Kafka 자체 관리 시스템이기 때문에 메타데이터의 일관성과 안정성을 보장한다.</p>
<p>Kafka 메타데이터 로그를 관리하는 Raft 쿼럼으로 클러스터 메타데이터의 각 변경사항에 대한 정보가 포함이 되어 Zookeeper에 저장되어 있는 모든 것을 대신 저장하고 있다.</p>
<h4 id="producer">Producer</h4>
<p>메시지를 발행하는 주체이다. 메시지 발행 시 특정 토픽을 정하여 발행한다.</p>
<h4 id="consumer">Consumer</h4>
<p>메시지를 소비, 수신하는 주체이다. 특정 토픽을 구독하여 메시지를 전달 받는다.</p>
<h3 id="partition">Partition</h3>
<p>분산 처리를 위해 사용 하는 것으로, Topic 생성 시 Partition 개수를 지정할 수 있다.(개수 변경이 가능하지만 추가만 가능하다. 줄이는건 불가능)
카프카의 토픽에 메시지가 쓰여지는 것도 어느정도 시간이 소비된다. 몇 천건의 메시지가 동시에 카프카에 write 되면 병목현상이 발생할 수 있다.</p>
<p>파티션이 1개라면 모든 메시지에 대핸 순서가 보장된다.
파티션이 여러개면 Kafka 클러스터가 라운드 로빈 방식으로 분배해서 분산처리 되기 때문에 순서를 보장하지 않는다.
파티션이 많으면 처리량은 좋지만 장애 복구 시간이 늘어난다.
파티션 내부에서 각 메시지는 Offset(고유번호)로 구분된다.
<img src="https://velog.velcdn.com/images/diense_kk/post/6c2c76fb-081a-4b88-b35a-7d487eaaf598/image.png" alt="">
<img src="https://velog.velcdn.com/images/diense_kk/post/1fa7aa8f-9fd2-4ff2-9c15-284487b057f5/image.png" alt=""></p>
<h4 id="offset">Offset</h4>
<p>파티션 내에서 메시지의 위치(식별자)를 나타낸다. 책의 페이지 번호를 알면 해당 페이지로 바로 이동할 수 있듯이, 오프셋 값을 알면 해당 메시지로 바로 접근할 수 있다.</p>
<h4 id="consumer-group">Consumer Group</h4>
<p>컨슈머 그룹은 하나의 이상의 컨슈머가 모여 구성된 그룹이다.</p>
<p>컨슈머 그룹은 하나의 Topic에 대한 책임을 갖고 있다.
즉, 어떤 Consumer가 Down된다면, 파티션 재조정을 통해 다른 컨슈머가 해당 파티션의 sub을 맡아서 한다. Offset 정보를 그룹간에 공유하고 있기 때문에 down되기 전 마지막으로 읽었던 메시지 위치부터 시작한다.
한 그룹 안에 있는 여러 컨슈머들이 서로 다른 파티션에서 동일한 토픽을 동시에 소비하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/6f77e78e-233d-4c51-9eb9-177682c4edb3/image.png" alt=""></p>
<p>Kafka에서 파티션당 하나의 컨슈머만 메시지를 처리할 수 있다. 따라서 파티션 개수와 컨슈머 개수를 동일하게 하면 모든 파티션을 병렬로 처리할 수 있어 성능을 극대화한다.</p>
<h3 id="kafka-broker-3개-추천">Kafka Broker 3개 추천</h3>
<p>Kafka Broker를 최소 3개를 사용하는 것을 추천한다는 글을 봤다.</p>
<h4 id="producer-acks">Producer acks</h4>
<p>acks는 프로듀서가 보낸 데이터를 카프카가 정상적으로 수신했는지 확인하는 옵션이다.</p>
<p>1) acks = 0
acks 값이 0이라면, 프로듀서는 카프카에게 메시지를 전송하고 Leader 파티션이 메시지를 잘 받았는지 확인하지 않는다.
프로듀서가 메시지를 보내는 동안 Leader 파티션이 Down 되면 메시지 손실이 발생하고, 확인하는 과정이 없기 때문에 가장 빠르다.
메시지 손실을 감안하고 빠르게 보내야 하는 경우 사용할 수 있다.
<img src="https://velog.velcdn.com/images/diense_kk/post/2b098ef9-340c-4ccd-8861-389cb028b597/image.webp" alt=""></p>
<p>2) acks = 1
acks 값이 1이라면, 프로듀서는 메시지를 전송하고 Leader 파티션이 메시지를 잘 받았는지 기다린다.
Leader 파티션이 메시지를 받았기 때문에 메시지 손실률은 acks 값이 0일때 보다 상대적으로 적으며 속도는 조금 더 느리다.</p>
<p>acks 값이 1이라도 메시지가 손실될 수 있는 경우가 있다
2-1) Leader 파티션이 메시지를 받은 뒤 프로듀서에게 정상 응답을 한다.
2-2) 그 후 Follower 파티션이 메시지를 복제하기 전에 Leader 파티션이 Down 된다면 메시지를 손실하게 된다.
<img src="https://velog.velcdn.com/images/diense_kk/post/89f25a64-8642-4ae0-92b6-842e83b92ca5/image.webp" alt=""></p>
<p>3) acks = -1 or all
acks 값이 -1 or all 이라면, Leader 파티션이 정상적으로 수신했고 Follower 파티션도 복제가 안료됨을 보장할 수 있다. 데이터 손실률은 없지만 기다리는 시간이 길어지기 때문에 가장 느리다. </p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/beeef475-f43f-4f85-bf4d-e120e71ad86b/image.webp" alt=""></p>
<p>만약 브로커가 3개(1 Leader Broker + 2 Follower Broker)인 경우 Follower 1대만 복제에 실패한다면 프로듀서가 보낸 메시지는 실패할 수도 있고 아닐 수도 있다.
이 결과는 min.insync.replicas 값에 의해 좌우된다.</p>
<h4 id="mininsyncreplicas-옵션">min.insync.replicas 옵션</h4>
<p>min.insync.replicas 옵션은 프로듀서가 acks=all로 설정하여 메시지를 보낼 때 필요한 최소 복제본의 수를 의미한다.
이 옵션은 프로듀서가 아닌 브로커의 옵션이다.</p>
<p>1) min.insync.replicas 1
acks 값이 all이기 때문에 Leader Broker + Follower Broker 쓰기를 기다린다. 이때 Follower Broker의 복제가 실패하더라도 min.insync.replicas 값이 1이기 때문에 프로듀서에게 정상적으로 응답을 한다. 따라서 복제에 실패할 수 있다.
<img src="https://velog.velcdn.com/images/diense_kk/post/ef877aa0-a429-4717-a3fc-a56d3eb971b4/image.webp" alt=""></p>
<p>2) min.insync.replicas 2
값이 2이기 때문에 Leader + Follower 쓰기가 성공하면 정상적으로 응답한다. 만약 Follower Broker 복제에 실패하게 되면 에러가 발생한다.
<img src="https://velog.velcdn.com/images/diense_kk/post/1692c37b-92ac-4add-ab6e-1f1287091bc3/image.webp" alt=""></p>
<p>min.insync.replicas 값과 관련하여 가장 중요한 것은 하나의 브로커의 개수가 min.insync.replicas 옵션 값보다 같거나 많아야 된다.
만약 값이 2인 상태에서 브로커 하나에 장애가 발생하면 애초에 브로커의 개수가 min.insync.replicas 개수 보다 작기 때문에 프로듀서는 데이터 전송에 실패한다.
아래 그림이 그 예시이다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/6bdd89b7-aa19-4d16-ba57-85703053d437/image.webp" alt=""></p>
<h4 id="결론">결론</h4>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/8b0b9e69-17a0-4deb-b978-efc215766d14/image.webp" alt=""></p>
<p>위 그림을 보면 브로커 3대에 min.insync.replicas 값이 2이다. 1대의 Leader Broker와 2대의 Follower Broker 파티션이 있는데 최소 복제 수가 2이기 때문에 Follower 복제가 하나 실패하더라도 문제 없이 동작한다.
지금까지 본 예제중에 가장 안정적이다.</p>
<p>따라서 실무에서는 3대의 브로커를 사용하고 min.insync.replicas 값은 2로 설정하는 것이 가장 안정적이라고 한다. 다만 서비스에 맞게 메시지가 조금 손실되더라도 빠른 속도를 제공하고 싶다면 실무에서 프로듀서의 acks 값을 1로 설정하여 사용하는 경우도 많다고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] Docker]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Docker</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Docker</guid>
            <pubDate>Fri, 29 Nov 2024 10:26:49 GMT</pubDate>
            <description><![CDATA[<h2 id="docker">Docker</h2>
<p>Docker는 Go언어로 작성된 리눅스 컨테이너 기반으로 하는 오픈소스 가상화 플랫폼이다.</p>
<p>Docker란 애플리케이션을 컨테이너화하여 실행할 수 있도록 돕는 플랫폼이다. Docker는 개발자가 애플리케이션과 그 환경을 독립적이고 일관성 있게 실행할 수 있게 해주며, 이를 위해 컨테이너라는 가상화된 환경을 사용한다. 컨테이너는 호스트 OS 위에서 실행되며, 애플리케이션과 그에 필요한 모든 라이브러리 및 종속성을 함께 패키징하여 배포한다.
이를 통해 개발, 테스트, 배포 과정에서 발생할 수 있는 환경 차이로 인한 문제를 해결하고, 애플리케이션을 다양한 환경에서 효율적이고 안정적으로 실행할 수 있게 한다. Docker는 Docker Engine을 통해 컨테이너를 관리하고, Docker Hub와 같은 저장소를 통해 이미지 공유가 가능하다.</p>
<p>Docker를 사용하면 저 사람 컴퓨터에서는 되는데 왜 내 컴퓨터에서는 안돼? 같은 문제가 해결된다.</p>
<h3 id="1-가상화를-하는-이유는-무엇인가">1. 가상화를 하는 이유는 무엇인가?</h3>
<p>가상화란 하나의 물리적 시스템(서버, 네트워크 장비 등)을 여러 개의 독립된 가상 시스템으로 분할하여 자원을 효율적으로 사용하는 기술이다.
가상 머신(Virtual Machine)이란 물리적 하드웨어 시스템에 구축되어 자체 CPU, 메모리, 네트워크 인터페이스 및 스토리지를 갖추고 가상 컴퓨터 시스템으로 작동하는 가상 환경이다.</p>
<p>요즘은 향상된 컴퓨터의 성능을 더욱 효율적으로 사용하기 위해 가상화 기술이 많이 등장했다.</p>
<p>서버가 CPU 사용률이 10%도 되지 않는다면 활용도가 낮은 서버들의 리소스 낭비일 것이다.
그렇다고 모든 서비스를 한 서버 안에 올린다면 안정성에 문제가 생길 수도 있다.
그래서 안정성을 높이며 리소스도 최대한 활용할 수 있는 방법으로 나타난게 서버 가상화이다.</p>
<p><strong>가상화 전</strong>
<img src="https://velog.velcdn.com/images/diense_kk/post/da9d7f22-7ae3-46a3-9c53-682e74a04ba2/image.png" alt="">
<strong>가상화 후</strong>
![]
(<a href="https://velog.velcdn.com/images/diense_kk/post/8b117f86-ac29-444c-b395-8a821138c9b7/image.png">https://velog.velcdn.com/images/diense_kk/post/8b117f86-ac29-444c-b395-8a821138c9b7/image.png</a>)</p>
<p>가상화 기술 등장 이후, 한 개의 물리 서버를 두 개 이상의 가상 서버로 동작시킬 수 있게 되었다.
덕분에 더 이상 서버 리소스를 낭비하지 않고 효율적으로 사용할 수 있게 됐다.</p>
<p>대표적인 가상화 플랫폼으로는 VM이 있다.</p>
<h3 id="2-docker의-사용-이유">2. Docker의 사용 이유</h3>
<p>Docker를 사용하면 팀원 및 서버와 개발 환경을 쉽게 동기화 할 수 있다.</p>
<h4 id="2-1-팀워크에서의-이점">2-1. 팀워크에서의 이점</h4>
<p>개발을 하다보면 팀원들과의 언어나 프레임워크의 버전이 달라 오류가 나는 경우가 있다.</p>
<p>도커를 사용하면 이런 문제를 쉽게 해결할 수 있다. 도커 이미지에 언어나 프레임워크 버전을 미리 정해두었기 때문에 해당 이미지를 컨테이너화 시키면 그 컨테이너는 로컬 환경의 간섭 없이 독립적으로 구동하여 위와 같은 문제를 해결할 수 있다.</p>
<p>예를 들면 프론트엔드 개발자는 백엔드 개발자가 만든 웹 애플리케이션 서버를 Docker를 사용해 실행할 수 있다. 이때, 로컬에 설치된 자바 버전이나 프레임워크 버전과 관계없이 Docker 컨테이너 내에서 실행되는 서버는 백엔드 개발자가 설정한 환경 그대로 동작하게 된다.</p>
<h4 id="2-2-서버에서의-이점">2-2. 서버에서의 이점</h4>
<p>가장 큰 장점은 서버를 옮기거나 늘릴 때 환경설정을 따로 할 필요가 없는 것이다.</p>
<p>만약 서버를 늘리거나 더 좋은 사양의 서버로 옮긴다면, 새로운 서버에 전 서버에서 사용하던 언어나 프레임워크를 설치해야 될 것이다.</p>
<p>이때 도커를 사용하면 이미지만을 가져와 서버에 컨테이너를 만들어 쉽게 동일한 환경을 구축할 수 있다.</p>
<p>또한, 하나의 물리 서버에서 여러 도커 컨테이너를 돌려 여러 서비스를 배포하는 것도 가능하다.</p>
<p>이때 각 서비스마다 같은 언어와 프레임워크를 사용해도 필요한 버전이 다를 수 있는데, 도커 컨테이너는 각각 독립적으로 구동되기 때문에 버전 차이에서 오는 이슈를 걱정할 필요가 없다.
물리적 서버에 설치된 버전이 아닌 각 컨테이너의 이미지에 정의된 버전의 언어나 프레임워크를 사용하기 때문이다.</p>
<h3 id="3-container">3. Container</h3>
<p>컨테이너는 가상화 기술 중 하나로 대표적으로 Linux Container가 있다. 
기존 OS를 가상화 시키던 것과 달리 컨테이너는 OS레벨의 가상화로 프로세스를 격리시켜 동작하는 방식으로 이루어진다.</p>
<p><strong>Linux Container</strong> - 리눅스 컨테이너는 운영체제 수준의 가상화 기술로 리눅스 커널을 공유하면서 프로세스를 격리된 환경에서 실행하는 기술이다.</p>
<h3 id="4-vm-가상화-vs-docker-가상화">4. VM 가상화 VS Docker 가상화</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/f5f720b6-c044-46c3-8145-013ac6a24624/image.png" alt=""></p>
<p><strong>요약</strong> - VM 가상화는 독립된 OS를 가지며 이 위에서 동작한다. 반면 Docker 가상화는 Host OS의 커널을 공유한다.</p>
<p><strong>인프라</strong> - 개발이나 서비스를 하기 위해 물리적으로 구성된 Network, DB, Server, Cloud 등을 의미한다.</p>
<p><strong>Hypervisor</strong> - 호스트 컴퓨터로 다수의 운영체제를 동시에 실행하기 위한 논리적 플랫폼으로서 Guest OS와 Guest OS에서 구동되는 프로그램을 실제 물리적 장치에서 분리하는 프로세스이다.
하이퍼바이저를 통해 새로운 가상 서버를 생성하고, 물리 서버가 가진 컴퓨팅 리소스를 각 가상 서버에 할당해준다.</p>
<p><strong>Container Engine</strong> - 유저가 컨테이너를 쉽게 사용할 수 있게 해주는 주체로써 이미지, 볼륨, 네트워크 관리와 컨테이너의 라이프 사이클 관리를 해준다.
Container Engine에서 가장 유명한 것이 Docker Engine이다.</p>
<h4 id="4-1-vm-가상화">4-1. VM 가상화</h4>
<p>하드웨어를 가상화하여 각 VM이 독립된 OS를 실행하도록 한다. 이를 위해 Hypervisor(VMware)가 사용되며, 각 VM은 자체 커널과 사용자 공간을 갖는다.</p>
<h4 id="4-2-docker-가상화">4-2. Docker 가상화</h4>
<p>OS 수준에서 가상화를 수행한다. 모든 컨테이너는 호스트 OS의 커널을 공유하며, 애플리케이션과 필요한 라이브러리만 격리된다. VM보다 가볍고 실행 속도가 빠르며 리소스 효율적이다.</p>
<p><strong>예시)</strong>
큰 건물에 여러 사무실이 입주해 있다고 생각하자.
이때 각 사무실은 전기와 물을 사용해야 한다. 이를 위해 각 사무실마다 발전소와 물탱크를 설치해야 된다면 비용이 엄청날 것이다. 이것이 VM 가상화이다.
이 방식이 아닌 건물에 있는 커다란 발전소와 물탱크를 각 사무실이 유동적으로 나눠 쓰는 방식이 Docker 가상화이다.
이것이 Docker 가상화가 효율적인 이유이다.</p>
<h4 id="4-3-vm-가상화의-장점">4-3. VM 가상화의 장점</h4>
<p>1) Host OS 위에 가상화를 시기키 위한 Hypervisor 엔진 그리고 그 위에 Guest OS를 올려 사용하기 때문에 거의 완벽하게 Host와 분리된다고 봐도 무방하다.
2) 높은 격리 레벨을 지원하여 보안적인 측면에서 더욱 유리하다.
3) 커널을 공유하지 않는 만큼 멀티 OS가 가능하다.</p>
<h4 id="4-4-vm-가상화의-단점">4-4. VM 가상화의 단점</h4>
<p>1) OS 위에 Guest OS를 올리기 때문에 무겁고 느리다.
2) 각 환경마다 사용할 수 있는 자원이 고정으로 정해져있기 때문에 컴퓨터의 성능과 환경이 제한된다.</p>
<h4 id="4-5-docker-가상화-장점">4-5. Docker 가상화 장점</h4>
<p>1) Host OS, Docker 엔진 위에서 바로 동작하며 Host의 커널을 공유하기 때문에 IO 처리가 쉽게 되어 성능의 효율을 높일 수 있다.
2) 각 환경마다 사용할 수 있는 자원이 고정되어 있지 않다.
3) 성능향상, 뛰어난 이식성, 쉽게 Scale Out을 할 수 있는 유연성이 있다.</p>
<h3 id="5-docker에-도커-컨터이너를-띄우는-과정">5. Docker에 도커 컨터이너를 띄우는 과정</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/4464498e-4e35-4b45-b6b8-4f70c526e658/image.png" alt=""></p>
<h4 id="과정">과정</h4>
<p>1) 우선 내 로컬 PC와 서버에 Docker를 설치한다.
2) 로컬 PC에서 Docker Image로 만들 Application이 있는 디렉토리에 Docker File을 생성하고, Docker build 명령어를 Docker File을 기반으로 Docker Image를 생성한다. 
3) 생성된 Docker Image를 Docker push로 Docker Hub와 같은 Public 또는 Private 저장소에 업로드한다.
4) 서버에서 Docker pull 명령어로 해당 이미지 저장소에서 이미지를 받아서 docker run을 사용하여 Docker image를 기반으로 Docker Container를 실행시킨다.</p>
<h3 id="6-도커의-구성요소">6. 도커의 구성요소</h3>
<h4 id="6-1-docker-file">6-1. Docker File</h4>
<p>Docker File은 Docker Image를 생성하기 위한 명령어와 설정을 정의한 파일로, 보통 이미지를 만들고자 하는 Application의 디렉토리 안에 생성한다.
이 파일에는 이미지 생성 과정에서 필요한 환경변수, 의존성, 실행 명령등을 포함하여 이미지의 구성을 지정한다.
이 Docker File을 빌드하면 Docker Image를 만들 수 있다.</p>
<h4 id="6-2-docker-image">6-2. Docker Image</h4>
<p>Docker Image란 컨테이너를 실행할 수 있는 실행파일, 설정 값들을 가지고 있는 것으로, 더 이상의 의존성 파일을 컴파일하거나 이것저것 설치 할 필요 없는 상태의 파일을 의미한다.</p>
<h4 id="6-3-docker-container">6-3. Docker Container</h4>
<p>Docker Image를 실행한 상태이다.
응용프로그램의 종속성과 함께 응용프로그램 자체를 패키징/캡슐화 하여 격리된 공간에서 프로세스를 동작시키는 기술이다.</p>
<h3 id="7-docker-architecture">7. Docker Architecture</h3>
<p>Docker는 기본적으로 Server-Client 아키텍처를 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/e4f3d2ad-8ed8-46e6-bea8-f39b6773e5eb/image.png" alt=""></p>
<p>Docker의 Client는 사용자의 입력을 받아서 Docker Daemon과 통신한다.
Docker Client와 Docker Daemon은 같은 시스템에서 실행되거나 UNIX 소켓, REST API 등으로 원격으로 통시도 가능하다.</p>
<h4 id="7-1-docker의-전체적인-실행-흐름">7-1. Docker의 전체적인 실행 흐름</h4>
<p>사용자가 명령어를 입력하면 Docker Client가 Docker Daemon에게 전달하고, Docker Daemon이 Images에 해당 이미지가 있는지 확인하여 있다면 실행하고, 없다면 Registry에서 이미지를 가져와 실행한다.</p>
<h4 id="7-2-docker-client">7-2. Docker Client</h4>
<p>Docker Client는 사용자와 상호작용 하는 곳이다. 
명령어를 입력하면 Docker Daemon에게 Docker API를 통해 전달한다.
한 개의 Client는 두 개 이상의 Docker Daemon과 통신이 가능하다.</p>
<h4 id="7-3-docker-daemon">7-3. Docker Daemon</h4>
<p>Docker Daemon은 Docker Client 측에서 보낸 명령어롤 Docker API를 통해 전달받고 Docker의 이미지, 컨테이너, 네트워크, 볼륨 등 Docker 객체를 관리한다.</p>
<h4 id="7-4-docker-registry">7-4. Docker Registry</h4>
<p>Docker Registry는 Docker 이미지를 저장하는 공간이다.
개인 레지스트리를 구성할 수도 있으며, 공용 레지스트리인 Docker Hub도 존재한다.</p>
<h3 id="8-docker-compose">8. Docker Compose</h3>
<p>Docker Compose는 여러 개의 Docker 컨테이너들을 하나의 서비스로 정의하고 구성해 하나의 묶음으로 관리할 수 있는 하나의 애플리케이션을 만드는 것이다.</p>
<p>Docker Compose를 사용하지 않을 경우에는 각각의 서비스를 따로 실행해야 하기 때문에 번거롭다.</p>
<p>Docker Compose는 여러 개의 컨테이너의 옵션과 환경을 정의한 파일을 읽어 컨테이너를 순차적으로 생성하는 방식으로 동작한다. 
Docker Compose의 설정 파일은 도커 엔진의 run 명령어의 옵션을 그대로 사용할 수 있으며, 각 컨테이너의 의존성, 네트워크, 볼륨 등을 함께 정의할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] MSA & Spring Cloud]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-MSA-Spring-Cloud</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-MSA-Spring-Cloud</guid>
            <pubDate>Thu, 28 Nov 2024 12:02:40 GMT</pubDate>
            <description><![CDATA[<h2 id="모놀리식-아키텍처">모놀리식 아키텍처</h2>
<p>모놀리식 아키텍처는 전통적인 개발 방식으로 하나의 프로젝트에 모든 기능을 포함한다. 이렇게 하면 코드 베이스가 커질수록 개발 및 배포의 복잡성이 증가한다.
모놀리식 아키텍처는 모듈 단위로 나누지 않고 하나의 프로젝트로 전체 애플리케이션을 묶어서 개발하는 방식이다. 이러한 경우 회원, 상품, 주문뿐만 아니라 여러 개의 비즈니스 로직이 추가 되면 코드 베이스가 커지게 되는 구조이다.</p>
<h3 id="모놀리식-아키텍처의-장점">모놀리식 아키텍처의 장점</h3>
<p>1) 초기 개발에 유리하며 빠르게 프로토타입을 개발할 수 있다.
2) 필요한 모든 기능을 한 번만 호출하기 때문에 복잡한 통신 없이 직접 사용할 수 있다.</p>
<h3 id="모놀리식-아키텍처의-단점">모놀리식 아키텍처의 단점</h3>
<p>1) 코드 베이스가 커질수록 복잡해지고 유지관리 및 확장이 어려워진다.
2) 일부 기능을 수정하거나 업데이트를 하려면 전체 애플리케이션을 재배포 해야된다.</p>
<h3 id="모놀리식-아키텍처를-사용하기에-적합한-상황">모놀리식 아키텍처를 사용하기에 적합한 상황</h3>
<p>1) 간단한 소규모 프로젝트 (사이드 프로젝트)
2) 프로토타입 제작 및 단기 프로젝트</p>
<h2 id="msa-마이크로서비스-아키텍처">MSA 마이크로서비스 아키텍처</h2>
<p>MSA는 여러 개의 작은 서비스로 구성되어 각 서비스가 독립적으로 개발되고 배포되는 구조이다.
MSA로 구성되어 있는 애플리케이션의 경우 전체 시스템이 분산되어 있어 개발, 배포가 독립적으로 가능하며 확장성과 유지관리가 용이해진다.
MSA의 경우 애플리케이션을 작은 독립적인 서비스로 분리하고, 각 서비스는 모듈 또는 서비스를 기준으로 나눠서 개발 및 관리를 진행한다. 이렇게 진행할 경우 독립적으로 개발 및 배포가 가능하여 개별적인 배포 주기를 가질 수 있다.</p>
<h3 id="msa의-장점">MSA의 장점</h3>
<p>1) 서비스 간 독립성으로 인해 확장성과 유연성이 높아진다.
2) 기능 고립성이라는 특징 때문에 일부 서비스가 실패하더라도 전체 시스템에 큰 영향을 미치지 않는다.</p>
<ul>
<li>고립성 - 어떠한 작업을 수행하는 것이 다른 작업에 영향을 주어서는 안된다는 것이다.
3) 독립된 기술 스택과 데이터 스토리지</li>
<li>각각의 서비스를 다른 프레임워크를 통해 개발할 수 있고, 이에 따른 데이터 스토리지도 공유하지 않고 각각 운영하여 의존성을 낮추고 낮은 결합도를 가질 수 있다.
4) 서비스의 부하에 따라 개별적으로 확장하거나 축소할 수 있다.</li>
</ul>
<h3 id="msa의-단점">MSA의 단점</h3>
<p>1) 서비스 간 통신이 필요하며, 서로 간 연결 구축 및 관리의 복잡성이 증가한다.
2) 초기 개발 및 통신 등에 시간이 많이 소요된다.</p>
<h3 id="msa를-사용하기에-적합한-상황">MSA를 사용하기에 적합한 상황</h3>
<p>1) 대규모 및 복잡한 프로젝트
2) 시스템을 독립적으로 개발하고 확장해야 하는 경우</p>
<h2 id="eureka-server">Eureka Server</h2>
<p>Eureka 라이브러리를 사용하면 Service Discovery를 구현할 수 있다.
Service Discovery 패턴은 MSA를 도입하면서 불편한 점을 해결 해주는 패턴이다.</p>
<h3 id="service-discovery가-필요한-이유">Service Discovery가 필요한 이유</h3>
<p>MSA에서는 외부에서 들어오는 요청은 API Gateway를 통해서 전달되고, 서비스 간 통신도 이루어진다.
이때 API Gateway는 어떻게 모든 서비스의 정보를 알아서 요청을 전달하고, 각각의 서비스들은 어떻게 다른 서비스의 정보를 알고 통신을 하는가?
수동으로 각 서비스들의 정보를 등록해주는 방식을 생각할 수 있을 것이다. 그럼 서비스가 확장/축소 될 떄마다 이렇게 수동으로 Gateway에 등록을 한다면 각각의 마이크로 서비스가 확장/축소 될 때마다 해당 마이크로 서비스의 정보(IP, Port)를 수동으로 업데이트 해야 하는 불편함이 있다.</p>
<p>이러한 불편한 점을 해결하는 것이 바로 <strong>Service Discovery</strong> 패턴이다.
Service Discovery는 서비스의 위치(IP, Port)를 저장 및 관리하는 서비스의 <strong>주소록</strong> 역할을 한다.
여기에서 <strong>Service Registry</strong>가 서비스의 위치를 저장 및 관리하는 서비스의 주소록이다.</p>
<p>이렇게 Service Discovery 패턴을 구현함으로써 특정 서비스에 요청을 보내고자 하는 서비스(클라이언트)에서는 Service Discovery를 구현한 구현체에게 서비스의 위치를 질의(쿼리)함으로써 요청을 전달할 수 있다.</p>
<h3 id="service-discovery-패턴-종류">Service Discovery 패턴 종류</h3>
<p>이러한 Service Discovery 패턴에는 2가지 종류가 있다.</p>
<h4 id="server-side-discovery">Server-Side Discovery</h4>
<p>Server-Side Discovery는 클라이언트가 다른 서비스를 호출할 때 Service Registry에 직접 요청을 보내지 않고, 앞단의 Gateway로 요청을 전달한다. Gateway는 Service Registry를 조회해 적절한 서비스 인스턴스를 선택하고, 로드밸런싱을 통해 요청을 분배한다.</p>
<h4 id="장점">장점</h4>
<p>1) 각 서비스들이 다른 서비스를 호출할 때 Gateway에만 요청을 보내기 때문에 Service Registry의 구체적인 구현은 몰라도 된다. Gateway가 Service Registry와의 통신을 처리한다. 즉, 캡슐화 되어있다.
2) Gateway를 통해 라우팅이 자동으로 이루어지기 때문에, 각 서비스에서 직접 다른 서비스를 검색하거나 연결 로직을 구현할 필요가 없다.</p>
<h4 id="단점">단점</h4>
<p>1) 배포 환경에서 Gateway를 사용해야 하며, Gateway에 내장된 로드 밸런서 또는 별도의 로드 밸런서를 반드시 구성해야 한다.
2) 요청이 Gateway를 통해 전달되기 때문에 네트워크 홉이 추가로 발생하여 처리 지연이 상대적으로 증가할 수 있다.</p>
<h4 id="client-side-discovery">Client-Side Discovery</h4>
<p>Client-Side Discovery는 Gateway로 요청을 보내지 않고 각 서비스들이 직접 Service Registry에게 질의하여 요청을 보내려는 서비스의 정보를 받아와서 직접 요청을 보낸다.
<strong>Spring Cloud Netflix Eureka</strong> - Service Registry의 서버 역할, 서비스들의 정보를 등록하는 역할</p>
<h4 id="장점-1">장점</h4>
<p>1) 각 서비스들이 호출하려는 서비스를 알기 때문에 서비스의 특성에 맞게 로드밸런싱 방식을 구현할 수 있다.</p>
<h4 id="단점-1">단점</h4>
<p>1) 각 서비스별로 다른 서비스를 검색하는 로직을 언어 및 프레임워크 별로 구현해야된다.
2) 따라서 각 서비스가 Service Registry에 의존적이다.</p>
<h2 id="gateway">Gateway</h2>
<p>Gateway 패턴은 MSA 관리/운영을 위한 플랫폼 패턴이며 해당 패턴에 필요한 기능들을 제공하는 서버를 말한다.</p>
<p>API Gateway는 개별 서비스의 앞 단에서 모든 서비스들의 엔드포인트를 단일화하고 다음과 같은 필수 기능 요소를 제공한다.</p>
<p>1) 인증과 인가 - 모든 서비스들에 대한 접근에 있어서 단일 집임점에서 인증과 인가 처리를 진행
2) API 요청 로드밸런싱 및 라우팅 - API 요청을 식별하여 적절한 마이크로서비스로 전달
3) QoS - 안정적인 서비스 제공 및 네트워크 품질을 관리하며 사용자, 클라이언트, API 단위로 접속 제어
4) 로깅 및 모니터링 - API 요청에 대한 로깅/모니터링 기능 지원
5) 입력 유효성 검사 - API 요청의 적절한 형식과 필수 데이터 포함 여부를 식별 및 관리</p>
<h3 id="gateway의-장점">Gateway의 장점</h3>
<p>1) 애플리케이션의 내부 구조를 캡슐화 - 클라이언트는 특정 마이크로 서비스를 호출하지 않고 단순히 게이트웨이와 통신하며, API 게이트웨이는 각 종류의 클라이언트에 특정 API를 제공
2) 클라이언트와 애플리케이션 간의 왕복 횟수가 감소하며, 클라이언트 코드 단순화</p>
<h3 id="gateway-단점">Gateway 단점</h3>
<p>1) 개발, 배포 및 관리해야 하는 지점이 증가
2) 각 마이크로 서비스의 EndPoint를 노출하기 위해 API게이트웨이를 업데이트해야 하는데 이로 인해 개발 병목 현상이 발생할 수 있음</p>
<ul>
<li>병목 현상 - 프로젝트의 실현을 지연시키는 기술적 문제로 인해 발생하는 모든 상황</li>
</ul>
<h2 id="spring-cloud-gateway">Spring Cloud Gateway</h2>
<p>Spring Cloud gateway는 Spring Framework에서 제공하는 오픈 소스 기반의 Gateway 서비스이다.
Spring Cloud gateway는 클라이언트의 단일 진입점 역할을 하는 서버이다.
Spring Cloud Zuul이 있었으나 패치가 중단됨에 따라 Spring Cloud Gateway로의 이전이 이루어졌다.</p>
<p>가장 큰 차이점은
Zuul은 Tomcat이였지만, Spring Cloud Gateway는 Netty로 비동기 방식이며, Spring WebFlux 기반이다.</p>
<h3 id="webflux란">WebFlux란?</h3>
<p>반응형 및 비동기적인 웹 애플리케이션 개발을 지원하는 모듈이다. 이 모듈은 Reactive Streams 사양을 기반으로 하여, 비동기적인 이벤트 지향 프로그래밍을 통해 높은 확장성과 성능을 제공한다.
Spring과 완벽한 통합을 이루고 netty를 지원하며, 비동기 논 블로킹 메시지 처리를 도와준다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/a03b4fc9-6921-42eb-9cdf-7690f6e5cf3e/image.png" alt=""></p>
<p>Spring Cloud Gateway는 비동기 방식을 통해 수많은 요청을 빠르게 처리할 수 있으며 다른 Spring Cloud 기반 기술과 통합이 잘 되어 있어 다양한 기술적 연계가 가능하다.</p>
<h3 id="아키텍처와-특징">아키텍처와 특징</h3>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/8a037637-defe-4943-9880-644297732ae8/image.png" alt=""></p>
<p>Spring Cloud Gateway를 구성하는 주요 3요소는 Route, Predicate, Filters가 있다.</p>
<h4 id="route">Route</h4>
<p>Route는 고유 ID + 목적지 URI + Predicate + Filter로 구성되며, Predicate + Filter의 묶음이자 라우팅이 될 규칙이라고 할 수 있다.
Route를 통해 Spring Cloud Gateway로 요청된 URI의 조건이 Predicate를 통과하여 참인 경우 매핑된 해당 경로로 매칭된다.</p>
<h4 id="predicate">Predicate</h4>
<p>Predicate는 요청이 주어진 조건을 충족하는지 테스트하는 구성요소이며, 하나 이상의 조건자를 정할 수 있습니다.
만약 Predicate에 매칭되지 않을 경우 Spring Cloud Gateway에서 자체적으로 404 NotFound로 응답한다.
여기에서 말하는 매칭은 Spring Cloud Gateway의 라우팅 규칙. 즉, 이 요청을 처리할 서비스가 없다.
시간, URI 패턴, 요청, 네트워크 관련으로 분류할 수 있다.</p>
<h4 id="filter-filter-chain">Filter, Filter Chain</h4>
<p>Filter, Filter Chain은 Spring Cloud Gateway를 통해 들어오는 요청이나 반환되는 응답에 대해 전처리/후처리를 담당한다.
Proxy Filter는 프록시 요청이 처리될 때 수행되는 필터이다.</p>
<h3 id="load-balancer">Load Balancer</h3>
<p>로드밸런서는 각 마이크로 서비스가 N개의 인스턴스로 돌아가고 있다면, 서버의 부하를 줄여주기 위해 사용자 접근을 여러 서버로 고르게 분산시켜주는 역할을 한다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/00eb8999-87ac-414b-b31d-c52795f47e55/image.png" alt=""></p>
<h2 id="eureka-server와-gateway">Eureka Server와 Gateway</h2>
<p>클라이언트에서 특정 MicroService로 요청시에 API Gateway Server와 Eureka Server 그리고 MicroService의 동작 순서는 다음과 같다.</p>
<p>1) Client에서 Service A로 요청을 보낸다.
2) Gateway에서 Eureka Server에 등록된 Service A에 대한 정보(IP, Port)를 요청한다.
3) Eureka Server는 Service A의 위치를 검색 후 Gateway에게 전달해준다.
4) Gateway는 Eureka Server에게 응답 받은 Service A의 정보를 통해 Service A로 포워딩 후 응답한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백엔드 개발자 면접] Spring Security & JWT]]></title>
            <link>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Spring-Security-JWT</link>
            <guid>https://velog.io/@diense_kk/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A9%B4%EC%A0%91-Spring-Security-JWT</guid>
            <pubDate>Mon, 25 Nov 2024 10:41:43 GMT</pubDate>
            <description><![CDATA[<h2 id="1-spring-security">1. Spring Security</h2>
<p>Spring Security는 Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다.</p>
<p>Spring Security는 &quot;인증&quot;과 &quot;권한&quot;에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
Filter는 요청이 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 요청을 받는다.
Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.</p>
<p>🤔 Servlet Filter에서 Spring Security의 Filter Chain으로 어떻게 넘어가는가?
그 역할을 하는 것이 바로 DelegationFilterProxy와 FilterChainProxy이다.</p>
<p>DelegationFilterProxy는 IoC Container에서 관리하는 빈이 아닌 표준 Servlet Filter를 구현하고 있으며, Servlet Container와 IoC Container를 연결하는 역할을 한다.</p>
<p>1) DelegatingFilterProxy가 Servlet filter chain을 통해 온 요청(Request)를 받는다.</p>
<p>2) Application context에서 SpringSecurityFilterChain 이름으로 생성된 Bean을 찾는다. (이 Bean이 바로 FilterChainProxy이다.)</p>
<p>3) Bean을 찾으면 SpringSecurityFilterChain으로 요청을 위임한다.</p>
<p>4) 각각의 Filter들에게 순서대로 요청을 chain 형식으로 넘기며 처리한다.</p>
<p>🤔 Security Filter에 직접 커스텀 한 Filter를 어떻게 등록하는가?
SecurityConfig클래스를 만들고, filterChain() 메서드를 통해 Filter Chain에 대한 전반적인 관리가 가능하다.
매개변수의 HttpSecurity가 해당 메서드에서 정의한 설정을 기반으로 Filter Chain을 생성하며, Application Context 초기화 시 진행된다.</p>
<h2 id="2-인증-인가">2. 인증, 인가</h2>
<p>인증(Authentication) - 해당 사용자가 본인이 맞는지를 확인하는 절차
인가(Authorization) - 인증된 사용자가 요청한 자원에 접근 가능한지를 결정하는 절차. 즉, 권한이 있는가</p>
<p>Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인을 한다. Spring Security에서는 이러한 인증과 인가를 위해 Principal(아이디), Credential(비밀번호)를 사용한다. 
-&gt; 로그인 요청뿐만 아니라 모든 보호된 리소스에 대한 접근 요청도 인증 절차를 먼저 거친 후 인가 절차를 진행한다.</p>
<p>인증이 되지 않은 사용자는 보호된 리소스에 접근할 수 없으며, 일반적으로 로그인 페이지로 리다이렉트 된다.
만약 인증은 되었지만 인가 과정에서 권한이 없는 리소스에 접근하는 경우에는 401 Unauthorized 오류를 반환받게 된다.</p>
<h2 id="3-spring-security-인증-과정">3. Spring Security 인증 과정</h2>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/220f6f1f-e633-4838-ba65-df4cac12bca6/image.png" alt=""></p>
<ol>
<li><p>사용자가 로그인 정보와 함께 인증 요청을 보낸다.</p>
</li>
<li><p>AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성한다.</p>
</li>
<li><p>AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.</p>
</li>
<li><p>AuthenticationManager는 등록된 AuthenticationProvider를 조회하여 인증을 요구한다.</p>
</li>
<li><p>DB에서 사용자 인증정보를 가져와서 UserDetailsService에 사용자 정보를 넘겨준다.</p>
</li>
<li><p>넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.</p>
</li>
<li><p>AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교한다.</p>
</li>
<li><p>인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.</p>
</li>
<li><p>다시 최초의 AuthenticationFitler에 Authentication 객체가 반환된다.</p>
</li>
<li><p>Authentication 객체를 SecurityContext에 저장한다.</p>
</li>
</ol>
<p>최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다.
사용자 정보를 저장한다는 것은 Spring Security가 전통적인 세션/쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.</p>
<h4 id="authentication">Authentication</h4>
<p>Authentication 객체는 SecurityContext에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근 할 수 있다.</p>
<p>이렇게 직접 접근하는 방법과 Controller 메서드의 파라미터 부분에 @AuthenticationPrincipal 애노테이션을 이용해서 로그인 세션 정보를 받아오는 방법도 있다.</p>
<h4 id="usernamepasswordauthenticationtoken">UsernamePasswordAuthenticationToken</h4>
<p>UsernamePasswordAuthenticationToken은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다.
UsernamePasswordAuthenticationToken의 첫 번째 생성자는 인증 전의 객체를 생성하고, 두번째는 인증이 완료된 객체를 생성한다.</p>
<pre><code>// 인증 완료 전의 객체 생성    
public UsernamePasswordAuthenticationToken(
Object principal, 
Object credentials
) {        
    super(null);        
    this.principal = principal;        
    this.credentials = credentials;        
    setAuthenticated(false);    
}     

// 인증 완료 후의 객체 생성    
public UsernamePasswordAuthenticationToken(
Object principal, 
Object credentials,
Collection&lt;? extends GrantedAuthority&gt; authorities
) {        
     super(authorities);        
    this.principal = principal;        
    this.credentials = credentials;        
    super.setAuthenticated(true);
}</code></pre><h4 id="authenticationmanager">AuthenticationManager</h4>
<p>인증에 대한 부분은 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다.
인증에 성공하면 객체를 생성하여 SecurityContext에 저장된다.</p>
<h4 id="providermanager">ProviderManager</h4>
<p>AuthenticationManager를 implements한 ProviderManager는 AuthenticationProvider를 구성하는 목록을 갖는다.</p>
<h4 id="authenticationprovider">AuthenticationProvider</h4>
<p>AuthenticationProvider에서는 실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication 객체를 AuthenticationManager에게 받아서 UserDetailsService에 넘기고 UserDetailsService에서 반환받은 UserDetails 객체와 비교하여 사용자의 정보가 모두 일치한다면 인증이 완료된 Authentication 객체를 반환하는 역할을 한다.</p>
<h4 id="userdetailsservice">UserDetailsService</h4>
<p>UserDetailsService는 UserDetails 객체를 반환하는 하나의 메서드만을 가지고 있는데, 일반적으로 이를 implements한 클래스에 UserRepository를 주입받아 DB와 연결하여 처리한다.</p>
<h4 id="userdetails">UserDetails</h4>
<p>인증에 성공하여 생성된 UserDetails 객체는 Authentication객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용한다. UserDetails를 implements하여 처리할 수 있다.</p>
<h4 id="securitycontextholder">SecurityContextHolder</h4>
<p>SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.</p>
<h4 id="securitycontext">SecurityContext</h4>
<p>Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication을 저장하거나 꺼내올 수 있다.</p>
<pre><code>SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder.getContext().getAuthentication(authentication);</code></pre><h4 id="grantedauthority">GrantedAuthority</h4>
<p>GrantedAuthority는 현재 사용자가 가지고 있는 권한을 의미하며, ROLE_ADMIN이나 ROLE_USER와 같이 ROLE_~ 형태로 사용된다. GrantedAuthority 객체는 UserDetailsService를 통해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 걸정한다.</p>
<h4 id="usernamepasswordauthenticationfilter">UsernamePasswordAuthenticationFilter</h4>
<p>Spring Security에서 제공하는 보안 필터 중 하나로, 나는 LoginFilter라는 커스텀 필터를 생성해 UsernamePasswordAuthenticationFilter를 상속받아 구현하였다.
폼 기반의 로그인을 처리하며,HTTP POST요청을 통해 전송된 사용자의 아이디와 비밀번호를 기반으로 인증을 수행한다.</p>
<p>POST /login 요청으로 username과 password를 보내면, Spring Security에서 기본적으로 제공하는 UsernamePasswordAuthenticationFilter가 이 요청을 처리한다.</p>
<p>내부에서는 attemptAuthentication 메서드에서 요청으로 받은 username과 password를 확인 하고, UsernamePasswordAuthenticationToken을 생성 후, AuthenticationManager를 통해 인증 시도한다.</p>
<p>로그인 성공 시, successfulAuthentication 메서드에서 따로 JWT를 생성해서 응답으로 보내주었다.</p>
<h4 id="genericfilterbean">GenericFilterBean</h4>
<p>GenericFilterBean은 서블릿 필터를 구현하기 위한 편의 클래스로, 로그아웃 커스텀 필터를 구현하기 위해 상속하였다.
GenericFilterBean은 로그아웃 필터와 같이 요청 전/후에 처리 로직을 삽입하고 싶을 때 유용하게 사용된다.</p>
<p>doFilter 내부에서 POST /logout으로 요청이 들어왔을 때, 쿠키에 있는 RefreshToken을 만료시키고 Redis에 있는 RefreshToken을 삭제하여 응답을 보내주었다. 이때 쿠키에 있는 RefreshToken의 값을 null로 바꾸어 보내주었다.</p>
<h4 id="onceperrequestfilter와-genericfilterbean의-차이">OncePerRequestFilter와 GenericFilterBean의 차이</h4>
<p>GenericFilterBean은 요청이 들어올 때마다 실행되며, 필터 체인에 연결된 모든 필터가 실행된다.
요청 처리 과정에서 특정 작업을 모든 요청에 대해 수행해야 할 때 적합하다.
단점은, 필터를 거쳐 들어온 요청을 다른 API에 리다이렉트 시켰을 경우, 이 요청은 필터 체인을 한 번 더 거치게 된다. 클라이언트는 한 번의 요청을 한 것 뿐이지만 흐름상 두 번의 요청을 보낸 것과 같다.</p>
<p>이러한 문제를 해결하기 위해 나온 것이 OncePerRequestFilter이다.</p>
<p>OncePerRequestFilter는 리다이렉트나 포워딩으로 인해 필터가 중복 실행되지 않도록 필터 체인에 플래그를 설정하여 이를 방지한다. 나는 JWTFilter를 구현할 때 상속 받아 구현하였다. 이렇게 하면, 요청당 JWT 검증 로직이 한 번만 실행되도록 보장할 수 있다.</p>
<h2 id="4-cookiesessiontoken">4. Cookie/Session/Token</h2>
<p>JWT를 알아보기 전에 Cookie/Session/Token 인증 방식에 대해 알아보자.</p>
<p>쿠키는 브라우저가 종료되도, 만료시점이 지나지 않으면 삭제되지 않는다.
용량 제한이 있으며, 하나의 도메인 당 20개 하나의 쿠키 당 4KB이다.
세션은 브라우저 종료시 삭제된다. (기간 지정 가능)
세션은 서버가 허용하는 한 용량제한이 없다.</p>
<p>세션 기반 인증은 클라이언트로부터 요청을 받으면 클라이언트의 상태 정보를 저장하므로 Stateful한 구조를 가지고, 토큰 기반 인증은 상태 정보를 서버에 저장하지 않으므로 Stateless한 구조를 가진다.</p>
<h4 id="stateful한-인증-방식의-단점은">Stateful한 인증 방식의 단점은?</h4>
<p>1) 서버에 세션을 저장하기 때문에 사용자가 증가하면 서버에 과부하를 줄 수 있어 확장성이 낮다.
2) 해커가 훔친 쿠키를 이용해 요청을 보내면 서버는 올바른 사용자가 보낸 요청인지 알 수 없다.</p>
<h3 id="4-1-cookie-인증">4-1. Cookie 인증</h3>
<p>쿠키는 Key-Valu 형식의 문자열 덩어리이다.
클라이언트가 어떠한 웹사이트를 방문할 경우, 그 사이트가 사용하고 있는 서버를 통해 클라이언트의 브라우저에 설치되는 작은 기록 정보 파일이다.
각 사용자마다 브라우저에 정보를 저장하니 고유 정보 식별이 가능하다.</p>
<h4 id="cookie-인증-방식">Cookie 인증 방식</h4>
<p>1) 브라우저가 서버에 요청을 보낸다.</p>
<p>2) 서버는 클라이언트의 요청에 대한 응답을 작성할 때, 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 Set-Cooki에 담는다.</p>
<p>3) 이후 클라이언트는 요청을 보낼 때마다 매번 저장된 쿠키를 요청 헤더의 Cookie에 담아 보낸다.</p>
<p>4) 서버는 쿠키에 담긴 정보를 바탕으로 해당 요청의 클라이언트가 누군지 식별한다.</p>
<h4 id="cookie-단점">Cookie 단점</h4>
<p>가장 큰 단점은 보안에 취약하다.
요청시 쿠키의 값을 그대로 보내기 때문에 유출 및 조작 당할 위험이 존재한다.
또한, 쿠키에는 용량 제한이 있어 많은 정보를 담을 수 없다.</p>
<h3 id="4-2-session-인증-방식">4-2. Session 인증 방식</h3>
<p>이러한 쿠키의 보안적인 이슈 때문에, 세션은 비밀번호 등 클라이언트의 민감한 인증 정보를 브라우저가 아닌 서버 측에 저장하고 관리한다. 서버의 메모리에 저장하기도 하고, 서버의 로컬 파일이나 DB에 저장하기도 한다.
핵심은 사용자의 정보를 브라우저가 아닌 서버에서 모두 관리한다는 것이다.</p>
<h4 id="session-인증-방식">Session 인증 방식</h4>
<p>1) 유저가 웹사이트에서 로그인하면 세션이 서버 메모리 혹은 DB에 사용자 정보를 저장한다.
이떄, 세션을 식별하기 위한 Session ID를 기준으로 정보를 저장한다.(Session ID를 Key로 가지는 Value형태로 저장)</p>
<p>2) 서버는 클라이언트의 요청에 대한 응답을 작성할 때, 클라이언트 측에 Session ID를 응답 헤더에 담는다.</p>
<p>3) 쿠키에 정보가 담겨있기 때문에 브라우저는 해당 사이트에 대한 모든 요청에 Session ID를 쿠키에 담아 전송한다.</p>
<p>4) 서버는 클라이언트가 보낸 Session ID와 서버에서 관리하는 Session ID를 비교하여 인증을 수행한다.</p>
<h4 id="session-단점">Session 단점</h4>
<p>브라우저 상에 사용자의 정보가 아닌 Session ID만을 저장하지만 해커가 세션 ID 자체를 탈취한다면 클라이언트로 위장할 수 있다는 한계가 있다.
이는 서버에서 IP특정을 통해 해결 할 수는 있다.</p>
<h3 id="4-3-token-인증-방식">4-3. Token 인증 방식</h3>
<p>토큰 기반 인증 시스템은 클라이언트가 서버에 접속을 하면 서버에서 해당 클라이언트에게 인증되었다는 의미로 &quot;토큰&quot;을 부여한다. 
서버에 요청을 보낼 때 요청 헤더에 토큰을 담아 보낸다. 그러면 서버에서는 클라이언트로부터 받은 토큰을 서버에서 제공한 토큰과의 일치 여부를 체크하여 인증 과정을 처리하게 된다.</p>
<p>세션 인증 방식은 서버가 파일이나 DB에 세션 정보를 가지고 있어야 하고 이를 조회하는 과정이 필요하기 때문에 많은 오버헤드가 발생한다.
하지만 토큰은 세견과 달리 서버가 아닌 클라이언트에 저장되기 떄문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있다.
토큰 자체에 데이터가 들어있기 떄문에 클라이언트에서 받아 위조되었는지를 판별만 하면 되기 때문이다.</p>
<h4 id="token-인증-방식">Token 인증 방식</h4>
<p>1) 사용자가 로그인을 시도한다.</p>
<p>2) 로그인에 성공시 서버에서 클라이언트에게 토큰을 발급한다.</p>
<p>3) 클라이언트는 서버 측에서 전달받은 토큰을 쿠키나 스토리지에 저장해두고, 서버에 요청을 할 때마다 해당 토큰을 HTTP요청 헤더에 포함시켜 전달한다.</p>
<p>4) 서버는 전달받은 토큰을 검증하고 요청을 응답한다. 이때 토큰에 사용자의 정보가 담겨있기 때문에 서버에서 따로 조회하는 과정이 필요없다.</p>
<h4 id="token-단점">Token 단점</h4>
<p>쿠키/세션과 다르게 토큰 자체의 데이터 길이가 길어 인증 요청이 많아질수록 네트워크 부하가 심해질 수 있다.
토큰을 탈취당하면 대처하기 어렵다. 따라서 사용 시간을 제한해서 설정하는 식으로 극복한다.</p>
<h2 id="5-jwt">5. JWT</h2>
<p>JWT란 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다.
JWT는 보통 AccessToken과 RefreshToken가 있다.
서버로부터 발급 받은 이후, 클라이언트에서 LocalStorage나 Cookie등에 보관한다.</p>
<p>JWT는 JSON 데이터를 인코딩하여 직렬화한 것이며, 토큰 내부에는 위변조 방지를 위해 개인키를 통한 전자서명이 들어있다. 따라서 사용자가 JWT를 서버로 전송하면 서버는 서명을 검증하는 과정을 거치게 되며 검증이 완료되면 요청한 응답을 돌려준다.</p>
<p>AccessToken은 로컬 스토리지나 세션 스토리지에 저장하고, RefreshToken은 쿠키에 저장하고 보안 옵션을 최대로 걸어 접근을 막아서 탈취됐을 경우를 대비한다.</p>
<p>Redis에 AccessToken과 RefreshToken이 매핑되어 저장되어 있기 때문에 이전에 발급했던 AccessToken과 RefreshToken이 아닌 경우에는 AccessToken 재발급이 되지 않도록 추가 로직을 구현해두어야 된다.</p>
<h3 id="5-1-jwt의-구조">5-1. JWT의 구조</h3>
<p>JWT는 &quot;.&quot;을 구분자로 나누어지는 세 가지 문자열의 조합이다.
구분자를 기준으로 Header, Payload, Signature를 의미한다.</p>
<h4 id="header">Header</h4>
<p>JWT에서 사용할 타입과 해시 알고리즘의 종류가 담겨있다.</p>
<h4 id="payload">Payload</h4>
<p>내용이라고도 하며 토큰에 담을 정보들이 존재하고, 보통은 유저를 구분하고자 하는 유저의 정보를 담는다.
여기서 담는 정보의 한 조각을 Claim이라고 한다.</p>
<p>1) Registered Calim - 등록된 클레임들은 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기 위해 이미 지정된 클레임이다. 사용하는 것은 모두 선택적이다.
issuer(토큰 발급자), subject(토큰 제목), audience(토큰 대상자), expiration(토큰의 만료시간), issued at(토큰이 발급된 시간)등이 있다.</p>
<p>2) Public Claim - 특정 커뮤니티나 사용자 그룹에서 공통으로 사용할 수 있도록 정의된 클레임이다. 충돌이 방지된 이름이 있어야하기 때문에 클레임 이름을 url형식으로 짓는다.</p>
<pre><code>{&quot;https://example.com/claims/role&quot;: &quot;admin&quot;}</code></pre><p>3) Private Claim - 양측간의 합의 하에 사용되는 클레임이다. 합의하에 설정하는 것이기 때문에 중복충돌을 주의해야 된다.</p>
<pre><code>{ &quot;username&quot; : &quot;KK&quot; &quot;email&quot; ; &quot;asdasd@gmail.com&quot; }</code></pre><h4 id="signature">Signature</h4>
<p>Header와 Payload는 암호화를 한 것이 아닌 단순히 JSON 문자열을 base64로 인코딩한 것이다. 누구나 이 값을 디코딩하여 JSON에 어떤 내용이 들어있는지 확인 가능하다.
토큰을 사용하는 경우 이 토큰을 다른 사람이 위변조할 수 없어야 하므로, 헤더와 페이로드의 위변조 여부를 검증하기 위한 부분이 Signature이다.
헤더와 페이로드를 base64로 인코딩해서 만든 두 값을 마침표(.)로 이어 붙이고 헤더에서 alg로 지정된 알고리즘 HMAC SHA-256으로 인코딩하면 JWT 토큰의 세 번째 부분인 Signature가 완성된다.</p>
<p><img src="https://velog.velcdn.com/images/diense_kk/post/bbf31e50-b500-4cdc-832a-31aa72e8d542/image.png" alt=""></p>
<p>토큰을 탈취해서 만료 시간을 늘리거나 토큰의 정보를 수정해도 Server의 SecretKey를 알지 못하면 유효한 JWT를 생성할 수 없다. (요청마다 JWT를 검증하는데 이때도 SecretKey를 사용해 서명을 확인하고 내부 값을 검증함)</p>
<p>JWT 또한 해커에게 토큰 탈취의 위험성이 있기 때문에 그대로 사용하는 것이 아닌 AccessToken과 RefreshToken으로 이중으로 나누어 인증을 하는 방식을 사용한다.
AccessToken과 RefreshToken은 둘 다 JWT이지만, 토큰이 어디에 저장되고 관리되느냐에 따른 사용 차이가 있다.</p>
<h3 id="5-2-accesstoken">5-2. AccessToken</h3>
<p>로그인에 성공시 서버는 AccessToken을 클라이언트에게 전달해준다. 실제로 유저의 정보가 담겨있다.
유효시간은 RefreshToken에 비해 짧게 설정된다.</p>
<h3 id="5-3-refreshtoken">5-3. RefreshToken</h3>
<p>새로운 AccessToken을 발급해주기 위해 사용되는 토큰으로 짧은 유효시간을 가지는 AccessToken에게 새로운 토큰을 발급해주기 위해 사용된다. 해당 토큰은 보통 DB에 유저 정보와 같이 저장된다.
접근 속도가 RDBMS보다 상대적으로 빠른 Redis에 주로 저장한다.</p>
<h2 id="6-세션-vs-토큰">6. 세션 VS 토큰</h2>
<h3 id="인증이-필요한-이유는-무엇인가">인증이 필요한 이유는 무엇인가?</h3>
<p>HTTP는 Stateless 특성을 가진다. 그렇기 때문에 사용자를 특정할 수 있는 어떠한 수단이 필요하다.
이를 위해서 세션 OR 토큰을 사용해 서버와 클라이언트 사이에서 값을 확인하고 사용자를 특정할 수 있다.
가장 큰 차이점은 서버까지 값을 저장하는가 클라이언트에만 저장 하는가이다.
세션은 SessionID와 함께 값을 서버에 저장해두고 요청마다 확인하지만 토큰은 전달받은 토큰을 검증하기만 하면 된다.</p>
<h3 id="jwt의-장점">JWT의 장점</h3>
<p>1) HTTP 헤더에 넣어 쉽게 전달 가능
2) 확장성 용이
토큰을 해석하는 알고리즘만 서버에 두면 되기에 MSA와 같은 분산 시스템에 적합하다.
3) JWT토큰 사용시 서버는 클라이언트의 상태를 유지할 필요가 없기 때문에 stateless하게 할 수 있다.
4) 인증에 필요한 정보가 토큰에 있기 때문에 별도의 저장소가 필요 없다.
보안성을 높이기 위해 RefreshToken을 사용하는 경우 별도의 저장소에 저장하면서 사용하는 경우도 있다.</p>
<h3 id="jwt의-단점">JWT의 단점</h3>
<p>1) 토큰의 정보가 클수록 네트워크에 부하를 줄 수 있다.
2) 페이로드는 암호화된 것이 아니기에 Base64 디코딩을 하면 내용을 볼 수 있다.</p>
<h3 id="세션-방식의-문제점">세션 방식의 문제점</h3>
<p>무엇보다 HTTP의 Stateless 특성을 위배한다. 서버에서는 클라이언트의 상태를 저장하지 않아야 되지만 세션 저장소라는 곳에 클라이언트의 상태를 저장하게 되므로 Stateful한 상태가 된다.</p>
<p>이 문제는 결국 확장성의 문제로 이어진다. 1번 서버에서 로그인한 사용자가 2번 서버로 요청하게 되면 2번 서버에서는 세션이 저장되어 있찌 않기 때문이다.</p>
<h3 id="안정성과-보안성">안정성과 보안성</h3>
<p>세션은 모든 인증 접오를 서버에서 관리하기 때문에 서버의 의존성이 높아 보안적 측면에서는 유리하다.
세션이 탈취되면 서버 측에서 해당 세션을 무효처리 하면 되기 때문이다.</p>
<p>토큰은 stateless한 특성으로 서버에서는 검증 알고리즘만 존재하기 때문에 토큰이 한 번 탈취 당하면 세션보다 복잡한 방식으로 해킹을 막아야 된다.</p>
<h3 id="확장성">확장성</h3>
<p>토큰의 가장 큰 장점은 확장성이다. 서버가 직접 인증 방식을 저장하지 않고 토큰 복호화 로직을 통해 인증처리를 하기 때문에 세션 불일치 문제로부터 자유롭다. 토큰 기반 인증 방식은 HTTP Stateless를 활용할 수 있고, 높은 확장성을 갖는다.</p>
<p>세션 방식은 서버의 요청을 처리하는데 별도의 작업을 해주지 않으면 세션 불일치가 발생한다.
스티키 서버, 세션 스토리지 등의 방식으로 외부에서 분리 작업을 해주어야 되는데 단일 책임 원칙을 벗어나는 문제가 있다.</p>
<h2 id="7-csrf--cors">7. CSRF &amp; CORS</h2>
<h3 id="7-1-csrf">7-1. CSRF</h3>
<p>CSRF는 Cross site Request forgery로 사이트간 위조 요청이다. 즉, 정상적인 사용자가 의도치 않은 위조요청을 보내는 것을 의미한다.</p>
<p>사용자가 NN이라는 은행 서비스에 로그인 된 상태로 공격자가 보낸 메일의 링크에 들어가면 NN서비스에 사용자로 위조된 의도하지 않은 요청이 날라가는 것입니다.</p>
<pre><code>&lt;img src = &quot;http://NN.com/send/money?to=attacker_account&amp;money=1000000000000000&quot;/&gt;
대충 뭐 이런식으로 이미지 누르면 저게 날아감</code></pre><p>Spring Security에서는 csrf를 disable 하여도 좋다고 한다.
그 이유는 RestAPI를 이용한 서버라면, Session 기반 인증과는 다르게 stateless 하기 떄문에 서버에 인증 정보를 보관하지 않는다.
RestAPI에서는 권한이 필요한 요청을 위해서는 인증정보(JWT 등)를 포함시켜야 된다.
따라서 서버에 인증정보를 저장하지 않기 때문에 굳이 불필요한 csrf 코드들을 작성할 필요가 없다.</p>
<h3 id="7-2-cors">7-2. CORS</h3>
<p>CORS란 HTTP 헤더를 이용하여 웹 애플리케이션에 대한 리소스를 다른 도메인에서 접근할 수 있도록 권한을 부여하는 정책이다.</p>
<p>쉽게 설명하면, 서버가 다른 도메인(주소, 프로토콜, 포트)에서 리소스에 접근하려는 요청을 허용할지를 결정하는 정책이다.</p>
<p>CORS를 통해 허용할 URL, HTTP 메서드, 요청 헤더, 응답 헤더(노출 헤더) 등을 설정할 수 있다.</p>
]]></description>
        </item>
    </channel>
</rss>