<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>MyeongDev</title>
    <link>https://myeongdev.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 27 Jun 2026 16:35:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>MyeongDev</managingEditor>
    <item>
      <title>업무에 바로 쓰는 SQL 튜닝 4장 악성 SQL 튜닝으로 초보자 탈출하기</title>
      <link>https://myeongdev.tistory.com/139</link>
      <description>&lt;h1&gt;4장 악성 SQL 튜닝으로 초보자 탈출하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️&amp;nbsp;&lt;b&gt;각 테이블과 인덱스는 책을 참조⚠️&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 사항 (3장 내용)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(4장의 내용을 이해하기 위해서는 최소 3장에서 이정도는 알아야 된다고 생각해서 공부하고 작성함.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리 튜닝을 고려&lt;/b&gt;할 때 현재 &lt;b&gt;해당 쿼리&lt;/b&gt;가 &lt;b&gt;어떤 실행 계획&lt;/b&gt;을 통해 실행되는지를 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;EXPLAIN, DESCRIBE , DESC&lt;/b&gt; 3가지 키워드를 사용해서 쿼리의 실행 계획을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN SQL문;
DESCRIBE SQL문;
DESC SQL문;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;-- EXPLAIN 실행문
EXPLAIN
SELECT 사원.사원번호, 사원.이름. 사원.성, 급여.연봉,
	(SELECT MAX(부서번호)
		FROM 부서사원_매핑 as 매핑 WHERE 매핑.사원번호 = 사원.사원번호) 카운트
FROM 사원, 급여
WHERE 사원.사원번호 = 10001
	AND 사원.사원번호 = 급여.사원번호;

-- EXPLAIN 실행 결과

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id select_type table partitions type possible_keys&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;td&gt;사원&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;td&gt;급여&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;ref&lt;/td&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;DEPENDENT SUBQUERY&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key key_len ref rows filtered Extra&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100.00&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PRIMARY&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;const&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;100.00&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;Select tables optimized away&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;id&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 순서를 표시.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ID의 숫자가 작을수록 먼저 수행&lt;/b&gt;. &lt;b&gt;ID가 같은 값&lt;/b&gt;이라면 &lt;b&gt;두 개 테이블의 조인&lt;/b&gt;이 이루어짐.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;select_type&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SQL 문을 구성하는 SELECT 문의 유형을 표시.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브 쿼리가 없는 단순 SIMPLE, 서브 쿼리가 존재한는 쿼리의 주 쿼리, UNION 쿼리 등을 표시.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;table&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테이블 명을 표시&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;partitions&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터가 저장된 논리적인 영역을 표시.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전에 정의한 특정 파티션에 선택적으로 접근하는 것이 성능이 좋음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 너무 많은 영역의 파티션에 접근하면 파티션 튜닝 고려.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;type&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 &lt;b&gt;데이터를 어떻게 찾을지에 관한 정보를 제공하는 항목.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;system
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블에 데이터가 없거나 하나만 존재하는 경우&lt;/li&gt;
&lt;li&gt;성능상 최고&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;const
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조회되는 데이터가 단 1건일때&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 매우 우수, 지향해야 할 타입&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;eq_ref
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조인&lt;/b&gt;이 수행될 때 &lt;b&gt;드리븐 테이블의 데이터에 접근&lt;/b&gt;하여 &lt;b&gt;고유 인덱스&lt;/b&gt; 또는 &lt;b&gt;기본 키&lt;/b&gt;로 &lt;b&gt;단 1건의 데이터를 조회&lt;/b&gt;할 때의 타입.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조인 수행시 가장 성능 좋음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ref
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;조인&lt;/b&gt;이 수행될 때 &lt;b&gt;드리븐 테이블 데이터 접근 범위&lt;/b&gt;가 &lt;b&gt;2개 이상&lt;/b&gt;일 경우&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;1 : N 관계&lt;/b&gt;에서 많이 나타남. 또는 인덱스 열을 통한 비교 연산자(&amp;lt; &amp;gt; =)에서도 나타남.&lt;/li&gt;
&lt;li&gt;드리븐 테이블의 데이터가 많을 경우 성능 저하 고려 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ref_or_null
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;IS NULL 구문&lt;/b&gt;에 대해 &lt;b&gt;인덱스를 활용&lt;/b&gt;하는 &lt;b&gt;최적화 방식&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NULL 데이터 양이 적다면 효율적, 데이터 양이 많다면 튜닝 대상.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;range
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비교연산자, BETWEEN, IN 연산을 통한 &lt;b&gt;범위 스캔을 수행&lt;/b&gt;하는 방식.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스캔 범위&lt;/b&gt;가 &lt;b&gt;넓으면&lt;/b&gt; &lt;b&gt;성능 저하의 요인&lt;/b&gt; &lt;b&gt;튜닝 대상.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fulltext
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트 검색 처리를 위해 &lt;b&gt;Full Text Index&lt;/b&gt; 를 사용하는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;index_merge
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 테이블에 생성된 &lt;b&gt;두 개 이상의 인덱스&lt;/b&gt;가 &lt;b&gt;병합&lt;/b&gt;되어 &lt;b&gt;동시에 적용&lt;/b&gt;되는 경우.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;index
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스 풀 스캔.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ALL
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;테이블 풀 스캔&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;사용할 수 있는 인덱스가 없거나, 옵티마이저 판단에 인덱스가 비효율일 경우.&lt;/li&gt;
&lt;li&gt;인덱스 변경 &amp;amp; 추가로 튜닝 가능&lt;/li&gt;
&lt;li&gt;전체 테이블에서 10 ~ 20% 이상 분량의 데이터를 조회할 때는 오히려 성능상 유리할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;possible_keys&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저가 &lt;b&gt;최적화에 사용할 수 있는 인덱스 목록&lt;/b&gt;을 출력.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, &lt;b&gt;실제 해당 쿼리문 실행에 사용된 인덱스가 아니다&lt;/b&gt;. 그러므로 튜닝할 때 별로 도움은 안된다고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;key&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵타마이저가 최적화에 사용한 기본 키 또는 인덱스명&lt;/b&gt;을 의미.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;비효율적인 인덱스 사용&lt;/b&gt; 혹은 &lt;b&gt;인덱스 자체를 사용하지 않을 경우&lt;/b&gt; &lt;b&gt;튜닝 대상.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;key_len&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용된 키본 키 또는 인덱스의 byte 수.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 사원 번호(INT) + 날짜(VARCHAR(50) = 4 + (50 + 1) * 3 = 159&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조인의 조건문&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;rows&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL문 수행에서 &lt;b&gt;해당 테이블에 접근한 모든 데이터 수&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경우에 따라 데이터는 달라지기 때문에 완전한 신뢰는 불가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 해당 결과가 너무 크게 차이가 난다면 불필요한 데이터에 너무 많이 접근하고 가져온다는 뜻. 튜닝 대상.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;filtered&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 엔진에 넘어온 데이터가 얼마나 &lt;b&gt;필터링&lt;/b&gt; 됐는지에 대한 &lt;b&gt;퍼센트 수치.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 100개의 데이터 넘어옴, 결과 10개 &amp;rArr; filtered = 10.00&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;extra&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 문을 어떻게 수행할 것인지에 관한 추가 정보들을 표시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종류가 개많음. 여러가지 중복 출력 가능. 난 내가 많이 접할것 같은것만 작성할 예정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Distinct
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DISTINCT&lt;/b&gt; , &lt;b&gt;UNION&lt;/b&gt; 구문이 포함되어 &lt;b&gt;중복이 제거&lt;/b&gt;되어 &lt;b&gt;유일한 값&lt;/b&gt;을 찾을 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using where
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;WHERE 절의 필터 조건을 사용&lt;/b&gt;할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using temporary
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터의 중간 결과를 저장하고자 &lt;b&gt;임시 테이블을 생성&lt;/b&gt;하겠다는 의미.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GROUP BY&lt;/b&gt; , &lt;b&gt;ORDER BY&lt;/b&gt; &lt;b&gt;정렬&lt;/b&gt; 작업 혹은 &lt;b&gt;중복 제거&lt;/b&gt; 작업에 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Using index
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물리적인 데이터를 읽지 않고 &lt;b&gt;인덱스만 읽어서 요청을 처리&lt;/b&gt;하는 경우. (&lt;b&gt;Covering Index)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적은 양의 데이터에 접근&lt;/b&gt;할 경우 &lt;b&gt;성능 우수.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.2 SQL 문 단순 수정으로 착한 쿼리 만들기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜닝 이전의 쿼리와 튜닝 이후의 쿼리에 EXPLAIN 문을 사용해서 어떤점이 변경되었는지 확인하면서 하면 도움이 많이 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 키를 변경하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 키본 키가 변경되어 기본 키 인덱스 안타는 경우
SELECT *
FROM 사원
WHERE SUBSTRING(사원번호, 1, 4) = 1100
	AND LENGTH(사원번호) = 5;

-- 30만건 데이터 0.23초

-- 튜닝 결과
SELECT *
FROM 사원
WHERE 사원번호 BETWEEN 11000 AND 11009

-- 30만건 데이터 0.00초
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용하지 않는 함수를 포함하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- NOT NULL 컬럼에 IFNULL을 사용해서 NULL Check 하는 경우
-- IFNULL () 함수를 처리하기위해 Using Temporary로 DB내에 임시 테이블을 만들어 연산하는 과정 추가됨
SELECT IFNULL(성별, 'NO DATA') AS 성별, COUNT(1) 건수
FROM 사원
GROUP BY IFNULL(성별, 'NO DATA')

-- 30만건 데이터 0.77초

-- 튜닝 결과
SELECT 성별, COUNT(1) 건수
FROM 사원
GROUP BY 성별

-- 30만건 데이터 0.10초
-- 즉, 무의미한 함수와 데이터 처리는 지양하자.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;형변환으로 인덱스를 활용하지 못하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 여기서 사용여부 라는 컬럼의 타입은 VARCHAR(1) 이다.
-- DB에서 묵시적 형변환이 발생하여 사용여부 인덱스를 활용하지 못함.
SELECT COUNT(1)
FROM 급여
WHERE 사용여부 = 1;

-- 약 5만건 데이터 0.15초

-- 튜닝 결과
SELECT COUNT(1)
FROM 급여
WHERE 사용여부 = '1'

-- 약 5만건 데이터 0.01초
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;열을 결합하여 사용하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 성별 과 성을 묶어 놓은 인덱스가 이미 존재.
-- 그러나 CONCAT 연산을 통해 새로운 문자열이 되었으므로 해당 인덱스 사용 불가능.
SELECT *
FROM 사원
WHERE CONCAT(성별, ' ', 성) = 'M Radwan'
--약 100건의 데이터 추출 0.25초

-- 튜닝 결과
SELECT *
FROM 사원
WHERE 성별 = 'M'
	AND 성 = 'Radwan'
-- 동일한 결과 0.01초
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;습괍적으로 중복을 제거하는 나쁜 SQL문&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 중복된 데이터가 없는데 DISTINCT를 사용하여 중복제거를 하려고 한 경우.
SELECT DISTINCT 사원.사원번호, 사원.이름, 사원.성, 부서관리자.부서번호
FROM 사원
	JOIN 부서관리자
		ON(사원.사원번호 = 부서관리자.사원번호)

-- EXPLINE 실행 계획에서 type = eq_ref 로 사원번호라는 기본 키를 사용해서 
-- 단 1건의 데이터 조회가 일어난다고 확인할 수 있음
-- DISTINCT 키워드는 내부적으로 결과를 정렬한 뒤 중복 제거하는 과정이 추가됨.
-- 인덱스를 이용할 경우 이미 정렬되어 있으며, 1건의 데이터 조회이기 때문에 중복이 존재하지 않음.

-- 쿼리 튜닝
SELECT 사원.사원번호, 이름, 성, 부서번호
FROM 사원
	JOIN 부서관리자
		ON (사원.사원번호 = 부서관리자.사원번호)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다수 쿼리를 UNION 연산자로만 합치는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- UNION 연산의 경우 두개의 쿼리의 결과를 합쳐서 중복을 제거하는 과정을 거친다.
-- 그 과정은 메모리 내에서 임시 테이블을 만들어서 연산이 진행된다.
-- 그러나, 해당 쿼리에서는 중복에 대한 제거가 불필요하다.
-- 따라서, 단순히 결과를 합치는 연산을 하는 UNION ALL을 사용하면 된다.
SELECT 'M' AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'M'
	AND 성 = 'Baba'

UNION

SELECT 'F', AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'F'
	AND 성 = 'Baba'

-- 튜닝 결과
SELECT 'M' AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'M'
	AND 성 = 'Baba'

UNION ALL

SELECT 'F', AS 성별, 사원번호
FROM 사원
WHERE 성별 = 'F'
	AND 성 = 'Baba'
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 고려 없이 열을 사용하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 성별 + 성으로 인덱스가 잡혀있다.
-- 즉, 성별을 통해 정렬이 된 후 성으로 정렬이 되어있다.
-- 이 경우는 인덱스를 통해서만 조회가 가능한 커버링 인덱스로 수행된다.
-- 그러나, 성 + 성별의 경우 정렬이 다른 방식으로 진행되기 때문에 임시 테이블을 생성하여 그루핑되게 된다.
SELECT 성, 성별, COUNT(1) as 카운트
FROM 사원
GROUP BY 성, 성별

-- 튜닝 결과
SELECT 성, 성별, COUNT(1) AS 카운트
FROM 사원
GROUP BY 성별, 성
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엉뚱한 인덱스를 사용하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 처음 해당 쿼리를 보았을때 이게 뭐가 문제인거지? 
-- 기본 키 인덱스를 사용하고있고 괜찮은거 아닌가??라는 생각이 들었다.
-- 총 30만명의 사원에서 입사일자가 1989년인 경우는 총 2만건, 사원번호가 10만보다 큰 경우는 20만건이 존재한다.
-- 이 경우는 스토리지 엔진에 접근할때 입사일자를 통해 먼저 접근하는게 더 낫다.
SELECT 사원번호
FROM 사원
WHERE 입사일자 LIKE '1989%'
	AND 사원번호 &amp;gt; 100000;

-- 튜닝 결과
SELECT 사원번호
FROM 사원
WHERE 입사일자 &amp;gt;= '1989-01-01' AND 입사일자 &amp;lt; '1990-01-01' USE INDEX(I_입사일자)
	AND 사원번호 &amp;gt; 100000;

-- 0.13초의 결과가 0.02

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동등 조건으로 인덱스를 사용하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 가정: 총 60만건 데이터, 결과는 총 30만건 
-- 실행 결과 출입문 컬럼의 인덱스 스캔으로 약 3.7초
-- 전체 데이터의 약 50%에 달하는 데이터 조회에 인덱스를 활용하는게 더 안 좋은 상황이 있다.
-- 전체 테이블의 10 ~ 20% 가 넘어가는 데이터를 조회할 경우 테이블 풀 스캔이 성능이 좋을 수 있다.
SELECT *
FROM 사원출입기록
WHERE 출입문 = 'B'

-- 튜닝 결과
SELECT *
FROM 사원출입기록 IGNORE INDEX(I_출입문)
WHERE 출입문 = 'B'

-- 튜닝 실행 결과 0.85초
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;범위 조건으로 인덱스를 사용하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 인덱스를 이용한 Index Range Scan 이다. 굳.
-- 그러나, 총 30만건의 사원 데이터에서 해당 범위의 데이터는 약 5만건이다. 실행 시간 1.21초
-- 총 데이터의 17퍼센트에 해당됨.
-- 이 경우 인덱스 스캔으로 랜덤 액세스의 부하가 발생하도록 하기보다는 
-- 테이블 풀 스캔 방식을 고정적으로 설정하는 것을 고려
SELECT 이름, 성
FROM 사원
WHERE 입사일자 BETWEEN STR_TO_DATE('1994-01-01', '%Y-%m-%d')
	AND STR_TO_DATE('2000-12-31', '%Y-%m-%d')

-- 튜닝 결과
SELECT 이름, 성
FROM 사원
WHERE YEAR(입사일자) BETWEEN '1994' AND '2000'
-- 인덱스를 버리고 테이블 풀 스캔. 실행 시간 0.2초
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 테이블 풀 스캔이 더 효율적일까?&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가정: 전체 테이블의 데이터 중 상당량의 데이터(10 ~ 20%가 넘는 데이터)를 찾아오는 경우.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스를 이용하는 경우.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 스캔으로 랜덤 액세스의 부하가 발생.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 오버헤드 증가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 데이터 중 많은 양의 데이터에 필요한 인덱스에 접근 후 디스크에 접근하는 비용.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인덱스를 사용하지 않고 테이블 풀 스캔을 하는 경우.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 통한 랜덤 액세스가 발생하지 않아 디스크 오버헤드가 적음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스에 접근한뒤 테이블에 접근하는 과정 없이, 바로 테이블에 접근.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀 스캔의 경우 인덱스 없이 테이블에 직접 접근하며 한 번에 다수의 페이지에 접근하므로 더 효율적인 SQL 문이 실행될 수 있음.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.3 테이블 조인 설정 변경으로 착한 쿼리 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작은 테이블이 먼저 조인에 참여하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 가정: 부서 = 9, 부서사원_매핑 = 약 30만, 매핑.시작일자 &amp;gt;= '2002-03-01' = 약 1천 3백 = 0.4%, 
-- 약 1천3백의 실행 결과 13.2초 실행시간 소요.
-- 드라이빙 테이블 = 부서, 드리븐 테이블 = 부서사원_매핑. 중첩 루프 조인 실행됨.
-- 즉, 부서 9개 가지고 30만개 랜덤 액세스 해서 데이터 가져오고 거기서 필터링 진행.
-- 인덱스를 이용하더라도 드리븐 테이블의 데이터가 이렇게 많은 경우 랜덤 액세스 비용이 비싸다.
-- 그렇다면 조건절을 먼저 이용해서 드리븐 테이블 데이터 30만 -&amp;gt; 1천 3백으로 줄인다면?
SELECT 매핑.사원번호, 부서.부서번호
FROM 부서사원_매핑 매핑, 부서
WHERE 매핑.부서번호 = 부서.부서번호
	AND 매핑.시작일자 &amp;gt;= '2002-03-01';

-- 튜닝 결과
SELECT STRAIGHT_JOIN
	매핑.사원번호,
	부서.부서번호
FROM 부서사원_매핑 매핑, 부서
WHERE 매핑.부서번호 = 부서.부서번호
	AND 매핑.시작일자 &amp;gt;= '2002-03-01'

-- 실행결과 0.17초
-- 튜닝 후 실행 계획
-- 상대적으로 대용량인 부서사원_매핑 테이블 -&amp;gt; 테이블 풀 스캔으로 처리. Type: index -&amp;gt; ALL
-- 부서 테이블 -&amp;gt; 기본 키로 반복 접근 Type: ref -&amp;gt; eq_ref
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메인 테이블에 계속 의존하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 메인인 사원 테이블로부터 조건을 전달받아 수행해야 하는 의존성을 가진 서브쿼리이다.
-- select_type: DEPENDENTY SUBQUERY
-- 실행 계획의 select_type 항목에 DEPENDENT 라는 키워드가 있으면,
-- 외부 테이블에서 조건절을 받은 뒤 처리되어야 하므로 튜닝 대상으로 고려할 수 있다.
SELECT 사원.사원번호, 사원.이름, 사원.성
FROM 사원
WHERE 사원번호 &amp;gt; 450000
	AND ( SELECT MAX(연봉)
				FROM 급여
				WHERE 사원번호 = 사원.사원번호
			) &amp;gt; 100000;

-- 튜닝 결과
SELECT 사원.사원번호, 사원.이름, 사원.성
FROM 사원
WHERE 사원번호 &amp;gt; 450000
	AND 사원.사워넌호 = 급여.사원번호
GROUP BY 사원.사원번호
HAVING MAX(급여.연봉) &amp;gt; 1000000

-- 0.57 -&amp;gt; 0.11초 
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자의 팁 &lt;b&gt;서브쿼리 vs 조인&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자는 10년 경력의 DBA 라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저자의 그간 경험상 서브쿼리보다는 &lt;b&gt;조인으로 수행하는 편이 성능 측면에서 유리&lt;/b&gt;할 가능성이 높다고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;불필요한 조인을 수행하는 나쁜 SQL 문&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 실행 시간: 22.5초
-- 이 경우 사원입출잉기록에서 출입문 A에 관한 기록이 있는 사원이 몇명인지만 알면 된다.
-- 따라서, 사원출입기록과 조인하여 EXIST 구문을 통해 집계하면 된다.
SELECT COUNT(DISTINCT 사원.사원번호) AS 데이터건수
FROM 사원,
	( SELECT 사원번호
		FROM 사원출입기록 AS 기록
		WHERE 출입문 = 'A'
	)
WHERE 사원.사원번호 = 기록.사원번호

-- 튜닝 결과
SELECT COUNT(1) AS 데이터건수
FROM 사원
WHERE EXISTS ( 
				SELECT 1
				FROM 사원출입기록 AS 기록
				WHERE 출입문 = 'A'
					AND 기록.사원번호 = 사원.사원번호
			)
-- 실행 시간: 0.5초
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절 서브쿼리(인라인 뷰)는 옵티마이저에서 어떻게 처리되나?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 절의 인라인 뷰는 옵티마이저에 의해 조인 방식이 뷰 병험(View Merging)으로 최적화 되어 아래와 같이 실행된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT COUNT(DISTINCT 기록.사원번호) AS 데이터건수
FROM 사원, 사원입출입기록 AS 기록
WHERE 사원.사원번호 = 기록.사원번호
	AND 출입문 = 'A'
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;</description>
      <category>  책/업무에 바로 쓰는 SQL 튜닝</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/139</guid>
      <comments>https://myeongdev.tistory.com/139#entry139comment</comments>
      <pubDate>Mon, 10 Mar 2025 15:43:12 +0900</pubDate>
    </item>
    <item>
      <title>업무에 바로 쓰는 SQL 튜닝 2장 SQL 튜닝 용어를 직관적으로 이해하기</title>
      <link>https://myeongdev.tistory.com/138</link>
      <description>&lt;h1&gt;2장 SQL 튜닝 용어를 직관적으로 이해하기&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1.1 물리 엔진과 오브젝트 용어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL이라는 DBMS는 데이터를 저장하고, 저장된 데이터를 가공하는 연산을 수행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QckZP/btsL3xp8MkN/nylMVOiSel4K8gkxj8xC70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QckZP/btsL3xp8MkN/nylMVOiSel4K8gkxj8xC70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QckZP/btsL3xp8MkN/nylMVOiSel4K8gkxj8xC70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQckZP%2FbtsL3xp8MkN%2FnylMVOiSel4K8gkxj8xC70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;538.5014&quot; height=&quot;388.3594&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;923&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행된 SQL 문을 다양한 문법 및 구문으로 검사(Parser)&lt;/li&gt;
&lt;li&gt;사용자가 요청한 데이터를 빠르고 효율적으로 찾아가는 전략적 계획 수립(Optimizer)&lt;/li&gt;
&lt;li&gt;스토리지 엔진에 위한 데이터까지 찾아간 뒤 해당 데이터를 MySQL 엔진으로 전달.&lt;/li&gt;
&lt;li&gt;MySQL 엔진은 전달된 데이터에서 불필요한 부분을 필터링하고 필요한 연산을 수행한 뒤 최종 결과 전달.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스토리지 엔진&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(InnoDB, MyISAM, Memory 등) &lt;b&gt;스토리지 엔진&lt;/b&gt;은 &lt;b&gt;사용자가 요청한 SQL 문을 토대로&lt;/b&gt; DB에 저장된 디스크나 메모리에 &lt;b&gt;필요한 데이터를 가져오는 역할을 수행&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 온라인상의 트랜잭션 발생으로 데이터를 처리하는 &lt;b&gt;OLTP(Online Transaction Processing)&lt;/b&gt; 환경이 대다수인 만큼 &lt;b&gt;주로 InnoDB 엔진을 사용&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대용량 쓰기 트랜잭션이 발생&lt;/b&gt;하면 &lt;b&gt;MyISAM 엔진&lt;/b&gt;, &lt;b&gt;메모리 데이터를 로드&lt;/b&gt;하여 빠르게 읽기 위하면 &lt;b&gt;Memory 엔진&lt;/b&gt;을 사용하는 식으로 응용이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- MySQL DBMS 사용중인 엔진 확인 법
SELECT ENGINE, TRANSACTIONS, COMMENT
FROM information_Schema.engines;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1.2 SQL 프로세스 용어&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SQL 문을 실행하면, **파서(Parser)**는 MySQL이 &lt;b&gt;이해할 수 있는 최소 단위로 구성요소를 분리&lt;/b&gt;하여 구성요소 &lt;b&gt;트리로 만든다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;트리를 만드는 과정에서 문법 오류 검토, &lt;b&gt;트리의 최소 단위&lt;/b&gt;는 &amp;gt;, &amp;lt;, = 의 &lt;b&gt;기호&lt;/b&gt; 혹은 &lt;b&gt;SQL 키워드&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;**전처리기(PreProcessor)**는 생성된 &lt;b&gt;트리 결과를 토대&lt;/b&gt;로, 이미 만들어진 테이블이나 뷰 등으로 구성되지 않는지, 존재하지 않은 열을 포함하지는 않는지, 조회 권한이 없는 테이블을 조회하는지 등의 &lt;b&gt;유효성 검증.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;**옵티마이저(Optimizer)**는 트리를 구성하는 오브젝트의 &lt;b&gt;데이터를 효율적으로 가져오기 위한&lt;/b&gt; &lt;b&gt;실행 계획 수립&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;**엔진 실행기(Engin Executor)**는 &lt;b&gt;수립된 실행 계획&lt;/b&gt;으로 &lt;b&gt;스토리지 엔진을 호출&lt;/b&gt;해 &lt;b&gt;데이터 가져오기.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;가져온 데이터 &lt;b&gt;엔진 실행기&lt;/b&gt;가 &lt;b&gt;불필요한 데이터 필터링&lt;/b&gt;하여 &lt;b&gt;사용자에게 전달.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;옵티마이저&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DBMS의두뇌&lt;/b&gt;라고 불리는 가장 핵심적인 역할.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파서 트리를 토대&lt;/b&gt;로 &lt;b&gt;필요하지 않은 조건 제거&lt;/b&gt;, &lt;b&gt;연산 과정 단순화&lt;/b&gt;, &lt;b&gt;테이블 접근 순서 제어&lt;/b&gt;, &lt;b&gt;인덱스 사용 여부 결정&lt;/b&gt;, &lt;b&gt;임시 테이블 사용 여부 결정&lt;/b&gt; 등의 &lt;b&gt;실행 계획 수립.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 실행 계획으로 &lt;b&gt;도출할 수 있는 경우의 수가 지나치게 많을 경우&lt;/b&gt; &lt;b&gt;모든 실행 계획을 판단하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 &lt;b&gt;옵티마이저가 선택한 결과&lt;/b&gt;가 &lt;b&gt;최상의 실행 결과는 아닐 수 있다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엔진 실행기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵티마이저의 실행 계획&lt;/b&gt;을 통해 &lt;b&gt;스토리지 엔진&lt;/b&gt;에서 &lt;b&gt;데이터를 가져오는 역할&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;읽어온 데이터&lt;/b&gt;를 &lt;b&gt;정렬&lt;/b&gt;, &lt;b&gt;조인&lt;/b&gt;, &lt;b&gt;불필요한 데이터 필터링&lt;/b&gt; 처리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL &lt;b&gt;엔진의 부하를 줄이려면&lt;/b&gt; 스토리지 엔진에서 &lt;b&gt;가져오는 데이터양을 줄이는게 매우 중요!&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1.3 DB 오브젝트 용어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 2차원 배열 형태로 테이블을 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 로우(행)과 컬럼(열)을 통해서 데이터를 관리하고 표현한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본키 Primary Key (주 키)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 행을 대표하는 열을 가르키는&lt;/b&gt; 용어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 &lt;b&gt;기본 키&lt;/b&gt;는 **클러스터형 인덱스(Clustered Index)**로 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;기본 키의 구성 열 순서를 기준&lt;/b&gt;으로 &lt;b&gt;물리적인 스토리지에 데이터가 쌓인다&lt;/b&gt;는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;비슷한 기본 키 값들&lt;/b&gt;이 &lt;b&gt;근거리에 적재&lt;/b&gt;되므로 &lt;b&gt;기본 키를 활용&lt;/b&gt;하여 &lt;b&gt;인덱스 스캔을 수행&lt;/b&gt;하며 &lt;b&gt;더 빠르게 데이터 접근&lt;/b&gt;이 가능하다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스의 주의사항 &lt;b&gt;기본 키와 똑같은 인덱스를 생성&lt;/b&gt;하면 인덱스가 저장되는 &lt;b&gt;물리적 공간이 낭비&lt;/b&gt;되는 한편 &lt;b&gt;데이터의 삽입, 삭제, 수정에 따른&lt;/b&gt; &lt;b&gt;인덱스 정렬의 오버헤드가 발생&lt;/b&gt;한다. 예시 CREATE TABLE 학생 ( 학번 INT(11) NOT NULL, 이름 VARCHAR(14) NOT NULL, &amp;hellip; PRIMARY KEY (학번), INDEX I_학번 (학번) ) 이러한 인덱스의 경우 공간 낭비 &amp;amp; 오버헤드 발생으로 비용증가 &amp;rarr; 삭제하자.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외래 키 Foreign Key&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외래 키는 &lt;b&gt;외부에 있는 테이블을 항상 참조&lt;/b&gt;하면서, &lt;b&gt;외부 테이블의 데이터가 변경&lt;/b&gt;되면 &lt;b&gt;함께 영향을 받는 관계를 설정&lt;/b&gt;하는 키.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연관관계가 필요한 &lt;b&gt;테이블에서의 변경사항이 발생할 때마다&lt;/b&gt; &lt;b&gt;외래 키 설정조건을 항상 검증&lt;/b&gt;하므로 &lt;b&gt;데이터 정합성 향상을 위해&lt;/b&gt;서라도 &lt;b&gt;외래 키를 설정&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 Index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 데이터베이스에서 키 값으로 실제 &lt;b&gt;데이터 위치를 식별하고 데이터 접근속도를 높이고자 생성되는&lt;/b&gt;, &lt;b&gt;키 기준으로 정렬된 오브젝트이다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고유 인덱스 Unique Index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스를 구성하는 열들의 데이터가 유일&lt;/b&gt;하다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차례로 정렬되는 인덱스 열의 &lt;b&gt;데이터는 서로 중복되지 않고 유일성을 유지&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE 학생
ADD UNIQUE INDEX 연락처_인덱스(연락처);
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 키와 고유 인덱스의 차이점 &lt;b&gt;기본 키와 고유 인덱스&lt;/b&gt; 모두 &lt;b&gt;데이터의 유일성을 보장해야 하는 특성&lt;/b&gt;과 &lt;b&gt;효율적인 데이터 접근을 위한 인덱스의 수단으로 사용&lt;/b&gt; 다만, &lt;b&gt;기본 키에는 NULL을 입력할 수 없지만&lt;/b&gt; &lt;b&gt;고유 인덱스&lt;/b&gt;에는 &lt;b&gt;얼마든지 입력할 수 있다&lt;/b&gt;는 차이점이 존재.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비고유 인덱스 Non-unique Index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비고유 인덱스는 고유 인덱스에서 데이터의 유일한 속성만 제외한 키이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 재정렬되더라도 &lt;b&gt;인덱스 열의 중복 체크를 거치지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ALTER TABLE 학생
ADD INDEX 이름_인덱스 (이름);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;뷰 View&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 == 가상 테이블&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한된 정보만을 제공&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템을 안전하게 운영&lt;/b&gt;하고 &lt;b&gt;개발&lt;/b&gt;할 수 있는 환경을 제공하는 만큼 &lt;b&gt;보안성 측면&lt;/b&gt;에서 뷰의 &lt;b&gt;가치가 부각&lt;/b&gt;될 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰를 사용하는 이유 &lt;b&gt;일부 데이터&lt;/b&gt;에 대해서만 데이터를 &lt;b&gt;공개&lt;/b&gt;하고, 노출에 민감한 &lt;b&gt;데이터&lt;/b&gt;에 대해서는 &lt;b&gt;제약을 설정&lt;/b&gt;할 수 있는 &lt;b&gt;보안성&lt;/b&gt; 때문. 또한, 여러 개의 테이블을 병합해서 활용할 대는 &lt;b&gt;성능&lt;/b&gt;을 고려한 &lt;b&gt;최적화된 뷰를 생성함&lt;/b&gt;으로써 &lt;b&gt;일관된 성능 제공&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2.1 서브쿼리 위치에 따른 SQL 용어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브쿼리(subquery)란 쿼리 안의 보조쿼리를 가리키는 용어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 바깥쪽의 SELECT 문인 메인 쿼리(main query)를 기준으로 안쪽에 작성된 SELECT 에 따라 불리는 이름이 다르다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;SELECT (SELECT ... FROM ...) -- SELECT 절: 스칼라 서브쿼리 (scalar subquery)
	FROM (SELECT ... FROM ...) -- FROM 절: 인라인 뷰(inline view)
WHERE COLUMN_NAME IN (SELECT ... FROM ...) -- WHERE 절: 중첩 서브쿼리(nested subquery)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스칼라 서브쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SELECT 절에서 사용되는 서브쿼리&lt;/b&gt;를 &lt;b&gt;스칼라 서브쿼리&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스칼라 서브쿼리는 SELECT 절 뿐만 아니라 FROM , WHERE 절 안에서도 사용이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스칼라 서브쿼리의 결괏값은 1행 1열의 구조로 출력되어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 스칼라 서브쿼리의 결과값이 2개 이상 나온다면 에러가 발생할 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인라인 뷰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FROM 절에서 사용되는 서브쿼리&lt;/b&gt;를 &lt;b&gt;인라인 뷰&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FROM 절 내부&lt;/b&gt;에서 &lt;b&gt;일시적으로 뷰를 생성하는 방식&lt;/b&gt;이므로 인라인 뷰라고 불린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인라인 뷰의 결과&lt;/b&gt;는 내부적으로 &lt;b&gt;메모리 또는 디스크에&lt;/b&gt; &lt;b&gt;임시 테이블을 생성하여 활용&lt;/b&gt;한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중첩 서브쿼리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WHERE 절에서 사용되는 서브쿼리를 중첩 서브쿼리&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중첩 서브쿼리는 보통 비교연산자(=, &amp;lt;, &amp;gt;, &amp;le;, &amp;ge;, &amp;lt;&amp;gt;, &amp;ne;)를 비롯해 IN, EXISTS, NOT EXIST 문에 많이 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2.2 메인쿼리와의 관계성에 따른 SQL 용어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비상관 서브쿼리 Non Correlated Subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비상관 서브쿼리는 &lt;b&gt;메인쿼리와 서브쿼리 간에 관계성이 없음&lt;/b&gt;을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;서브쿼리가 독자적으로 실행된 뒤 메인쿼리에게 그 결과를 던져주는 형태&lt;/b&gt;인 것이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT ...
	FROM 학생
WHERE ... IN (
	SELECT ... FROM 지도교수 -- 비상관 쿼리 -&amp;gt; 메인 쿼리와 연관성 없음
)
-- 실행순서: 서브쿼리 -&amp;gt; 메인쿼리
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상관 서브 쿼리 Correlated Subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상관 서브쿼리는 &lt;b&gt;메인쿼리와 서브쿼리 간에 관계성이 있음&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브쿼리가 실행되려면 메인쿼리의 값을 받아야 함&lt;/b&gt;으로 서로 관계가 깊게 유지된다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;SELECT ...
	FROM 학생
WHERE ... IN (
	SELECT ... FROM 지도교수 WHERE 학생.학번 = ... -- 상관 쿼리 -&amp;gt; 메인 쿼리에 영향을 받음 학생.학번
)
-- 실행 순서: 메인쿼리 -&amp;gt; 서브쿼리 -&amp;gt; 메인쿼리
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두둥 옵티마이저 등장!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 의도하는 결과를 얻기위해 위와 같이 서브쿼리들을 사용해서 쿼리를 작성할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 우리가 작성한 쿼리는 결국 옵티마이저에 의해 실행계획이 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 &lt;b&gt;DB 버전 혹은 옵티마이저에 따라&lt;/b&gt; &lt;b&gt;서브쿼리가 제거&lt;/b&gt;되고 &lt;b&gt;하나의 메인쿼리로 통합&lt;/b&gt;되는 &lt;b&gt;뷰 병합(View Merging), 즉 SQL 재작성(rewrite)이 작동할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2.3 반환 결과에 따른 SQL 용어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단일행 서브쿼리 Single-Row Subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브쿼리 결과&lt;/b&gt;가 &lt;b&gt;1건의 행으로 반환&lt;/b&gt;되는 쿼리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주로 스칼라 서브쿼리&lt;/b&gt;라고 생각하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중행 서브쿼리 Multiple-Row Subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브쿼리 결과가 여러 건의 행으로 반환&lt;/b&gt;되는 쿼리이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT ...
	FROM ...
WHERE 학번 IN (
	SELECT MAX(학번) FROM 학생 GROUP BY 전공코드	
) -- 다중행 서브쿼리
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중열 서브쿼리 Multiple-Column Subquery&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;서브쿼리 결과가 여러 개의 열과 행&lt;/b&gt;으로 반환되는 쿼리이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT ...
	FROM ...
WHERE (이름, 전공코드) IN (
	SELECT 이름, 전공코드 FROM 학생 WHERE 이름 LIKE '김%'
) -- 다중열 서브쿼리
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2.4 조인 연산방식 용어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다수의 테이블에 &lt;b&gt;흩어져 있는 데이터를 결합할 때&lt;/b&gt; &lt;b&gt;조인(Join) 방식 사용&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 조인 Inner Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;양쪽에 모두 존재하는 데이터&lt;/b&gt;만 반환 (교집합)&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 명시적 조인
SELECT 학생.학번, 학생.이름, 지도교수.교수명
FROM 학생
	INNER JOIN 지도교수
		ON 학생.학번 = 지도교수.학번

-- 암시적 조인
SELECT 학생.학번, 학생.이름, 지도교수.교수명
FROM 학생, 지도교수
WHERE 학생.학번 = 지도교수.학번
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왼쪽 외부 조인 Left Outer Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 테이블을 기준으로 &lt;b&gt;조건과 일치하지 않더라도&lt;/b&gt; 왼&lt;b&gt;쪽 테이블의 결과는 최종 결과에 포함&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT 학생.학번, 학생.이름, 지도교수.교수명
FROM 학생
LEFT OUTER JOIN 또는 LEFT JOIN 지도교수
	ON 학생.학번 = 지도교수.학번
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오른쪽 외부 조인 Right Outer Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조인 조건과 일치하지 않더라도&lt;/b&gt; &lt;b&gt;오른쪽 테이블의 결과는 최종 결과에 포함&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT 지도교수.학번, 학생.이름, 지도교수.교수명
FROM 학생
RIGHT OUTER JOIN 또는 RIGHT JOIN 지도교수
	ON 학생.학번 = 지도교수.학번
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LEFT JOIN vs RIGTH JOIN 사람의 인지적 특성상 보통 왼쪽 &amp;rarr; 오른쪽을 정방향으로 인식. 따라서, LEFT JOIN을 주로 사용하게 됨. 일관된 조인문을 사용함으로써 유지보수나 관리편의성 측면에서 이점을 얻자&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FULL OUTER JOIN은? FULL OUTER JOIN의 경우 LEFT JOIN 과 RIGTH JOIN이 통합된 방식으로 MySQL과 MariaDB에서는 지원하지 않는다고 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;교차 조인 Cross Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;교차 조인&lt;/b&gt;은 &lt;b&gt;데카르트 곱(cartesian product) 곱집합&lt;/b&gt; 개념, &lt;b&gt;조인에 참여&lt;/b&gt;하는 테이블에서 &lt;b&gt;발생할 수 있는 모든 조합을 찾아내어 반환.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 명시적 조인
SELECT 학생.학번, 학생.이름, 지도교수.학번, 지도교수.교수명
FROM 학생
	CROSS JOIN 지도교수

-- 암시적 조인
SELECT 학생.학번, 학생.이름, 지도교수.학번, 지도교수.교수명
FROM 학생, 지도교수

-- 암시적 조인의 경우 WHERE 절의 조인 조건문이나 
-- JOIN 키워드를 명시하지 않고 작성하면 CROSS JOIN이 된다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자연 조인 Natural Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2개 테이블에 &lt;b&gt;동일한 Column 이름이 있을 때 조건을 따로 작성하지 않아도 조인&lt;/b&gt;이 되는 방식.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT 학생.*, 지도교수.*
FROM 학생
	NATURAL JOIN 지도교수
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 동일한 Column 이름이 존재하지 않을경우 Cross Join발생.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 무슨 조인 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적 &lt;b&gt;INNER JOIN&lt;/b&gt; 혹은 &lt;b&gt;LEFT OUTER JOIN&lt;/b&gt; 무조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가독성과 유지보수성이 넘사.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2.5 조인 알고리즘 용어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;드라이빙 테이블(Driving table)과 드리븐 테이블(Driven table)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블 == 먼저 접근하는 테이블&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드리븐 테이블 == 뒤 늦게 접근하는 테이블&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT 학생.학번, 학생.이름, 비상연락망.관계, 비상연락망.연락처
FROM 학생
	JOIN 비상연락망
		ON 학생.학번 = 비상연락망. 학번
WHERE 학생.학번 IN(1, 100)
-- 드라이빙 테이블 == 학생
-- 드리븐 테이블 == 비상연락망
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가능하면 &lt;b&gt;적은 결과가 반환될 것&lt;/b&gt;으로 &lt;b&gt;예상되는 테이블&lt;/b&gt;을 &lt;b&gt;드라이빙 테이블&lt;/b&gt;로 선정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조인 조건절의&lt;/b&gt; &lt;b&gt;열&lt;/b&gt;이 &lt;b&gt;인덱스로 설정&lt;/b&gt;되도록 구성.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중첩 루프 조인 Nested Loop Join, NL Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중첩 루프 조인&lt;/b&gt;은 &lt;b&gt;드라이빙 테이블&lt;/b&gt;의 &lt;b&gt;데이터 1건당&lt;/b&gt; &lt;b&gt;드리븐 테이블&lt;/b&gt;을 &lt;b&gt;반복&lt;/b&gt;해 검색하며 최종적으로 양쪽 테이블에 공통된 데이터를 출력한다. (드라이빙 테이블 N * 드리븐 테이블 M)&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;SELECT ...
FROM 학생
	JOIN 비상연락망
		ON 학생.학번 = 비상연락망.학번
WHERE 학생.학번 IN(1, 100)
--
-- 드라이빙 테이블 결과는 1과 100 2개
-- 드리븐 테이블 비상연락망의 테이블이 만약 1000건이라고 하면
-- 조건에 맞는 데이터를 가져오기 위해 최악 2000건의 데이터 접근이 일어나게 됨.
-- 만약, 조인 조건절이 모두 인덱스가 걸려있다면 == 학생 테이블 학번 컬럼 인덱스, 비상연락망 학번 컬럼 인덱스
-- 드리븐 테이블 비상연락망 테이블에서 조건에 맞는 인덱스만 접근.
-- 데이터 접근 횟수 감소.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스는 &lt;b&gt;인덱스로 정의된 열 기준으로 순차 정렬&lt;/b&gt;되지만, 해당 인덱스가 &lt;b&gt;비고유 인덱스일 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 이용해 테이블의 데이터를 찾아가는 과정에서 &lt;b&gt;임의 접근 방식인 랜덤 액세스(Random Access)가 발생&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 랜덤 액세스를 줄일 수 있도록 &lt;b&gt;데이터의 액세스 범위를 좁히는 방향&lt;/b&gt;으로 &lt;b&gt;인덱스를 설계&lt;/b&gt;하고 &lt;b&gt;조건절을 작성&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본 키&lt;/b&gt;는 &lt;b&gt;클러스터형 인덱스&lt;/b&gt;이므로 기본 키의 순서대로 테이블의 데이터가 적재되어 있어 &lt;b&gt;조회 효율이 매우 높다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블록 중첩 루프 조인 Block Nested Loop Join, BNL Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인덱스가 걸려있지 않는 상황&lt;/b&gt;의 &lt;b&gt;중첩 루프 조인의 성능을 개선하기 위해&lt;/b&gt; 등장.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;**조인 버퍼(Join Buffer)**에 &lt;b&gt;드라이빙 테이블의 결과&lt;/b&gt;를 조인 버퍼가 가득 찰때까지 &lt;b&gt;적재.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조인 버퍼와 드리븐 테이블 데이터를 조인&lt;/b&gt;하는 식으로 &lt;b&gt;반복&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 과정은 &lt;b&gt;드리븐 테이블의 풀 스캔을 줄이는 게 목적&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라이빙 테이블 Full Scan &amp;rarr; 드리븐 테이블 Full Scan 반복을 최소화 시킬 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치 키 엑세스 조인 Batched Key Access Join(BKA 조인)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중첩 루프 조인 방식의 인덱스의 랜덤 엑세스의 단점을 해결하고자,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;접근할 데이터를 미리 예상하고 가져오는&lt;/b&gt;데 착안된 조인 알고리즘.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 중첩 루프 조인 방식의 &lt;b&gt;조인 버퍼 개념을 사용&lt;/b&gt;하고, &lt;b&gt;드리븐 테이블&lt;/b&gt;의 &lt;b&gt;데이터를 예측&lt;/b&gt;하고 &lt;b&gt;정렬된 상태&lt;/b&gt;로 담는 &lt;b&gt;랜덤 버퍼의 개념이 추가&lt;/b&gt;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, &lt;b&gt;드리븐 테이블의 데이터를 예측하고 정렬된 상태로 버퍼에 적재하는 기능&lt;/b&gt;을 **다중 범위 읽기(Multi Range Read, MRR)**라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미리 예측, 정렬된 데이터를 액세스&lt;/b&gt; 하기 때문에 랜덤 액세스가 아닌 &lt;b&gt;시퀀셜 액세스가 일어나게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 조인 Hash Join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0.18 버전 부터 지원되는 방식.(상용 DMBS 오라클 같은 애들은 이미 지원하고 있었다고 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 중첩 루프 조인과 배치 키 액세스 조인의 한계를 탈피하기 위함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조인에 참여하는 각 테이블의 데이터&lt;/b&gt;를 내부적으로 &lt;b&gt;해시값으로 만들어 내부 조인을 수행&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내부 조인 수행 결과&lt;/b&gt;는 &lt;b&gt;조인 버퍼에 저장&lt;/b&gt;되므로 &lt;b&gt;조인열의 인덱스를 필수로 요구하지 않아도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3.1 기초 용어 - 오브젝트 스캔 유형&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 풀 스캔 Table Full Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀 스캔은 &lt;b&gt;인덱스를 거치지 않고&lt;/b&gt; &lt;b&gt;테이블로 바로 직행&lt;/b&gt;하여 &lt;b&gt;처음부터 끝까지 데이터를 훑어보는 방식.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀 스캔은 &lt;b&gt;인덱스 없이 사용하는 유일한 방식&lt;/b&gt;. &lt;b&gt;성능 측면에서는 부정적&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 범위 스캔 Index Range Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 범위 스캔은 &lt;b&gt;인덱스를 범위 기준으로 스캔&lt;/b&gt;한 뒤 &lt;b&gt;스캔 결과를 토대로&lt;/b&gt; &lt;b&gt;테이블의 데이터를 찾아가는 방식.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좁&lt;b&gt;은 범위를 스캔&lt;/b&gt;할 때 &lt;b&gt;매우 효율적&lt;/b&gt;, 넓은 범위의 경우 비효율적인 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL의 BETWEEN ~ AND, &amp;lt;, &amp;gt;, LIKE 구문 등에 사용됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 풀 스캔 Index Full Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀 스캔은 &lt;b&gt;인덱스를 처음부터 끝까지 스캔&lt;/b&gt;하는 방식.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;테이블에 접근하지 않고&lt;/b&gt; &lt;b&gt;인덱스로 구성된 열 정보만 요구하는 SQL 문에서 인덱스 풀 스캔이 수행됨.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀 스캔보다는 성능 좋음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 최대한 검색 범위를 줄이는 방향으로 SQL 튜닝 권장.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 고유 스캔 Index Unique Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 고유 스캔은 &lt;b&gt;기본 키나 고유 인덱스로 테이블에 접근하는 방식.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스를 사용하는 스캔 방식 중 &lt;b&gt;가장 효율적인 스캔 방법.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WHERE 기본 키 혹은 고유 인덱스 컬럼 = 조건&lt;/b&gt; 으로 되어 있을 경우 활용됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 루스 스캔 Index Loose Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 루스 스캔은 &lt;b&gt;인덱스의 필요한 부분들만 골라 스캔하는 방식.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요&lt;/b&gt;한 데이터와 불필요한 &lt;b&gt;데이터를 구분&lt;/b&gt;한 뒤 불필요한 인덱스 키는 무시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GROUP BY , MAX(), MIN() 함수의 경우 작동.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 병합 스캔 Index Merge Scan&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 병합 스캔은 &lt;b&gt;테이블 내에 생성된 인덱스들을 통합해서 스캔하는 방식.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵티마이저&lt;/b&gt;가 &lt;b&gt;서로 다른 인덱스&lt;/b&gt;를 가져와 &lt;b&gt;결합(Union)과 교차(Intersection) 방식&lt;/b&gt;을 통해 서로 다른 인덱스를 &lt;b&gt;각각 실행&lt;/b&gt;하며 원하는 데이터를 찾음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 인덱스를 각각 수행하므로 &lt;b&gt;인덱스에 접근하는 시간이 몇 배로 걸림&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 별개의 인덱스를 &lt;b&gt;하나의 인덱스로 통합&lt;/b&gt; 혹은 SQL문 자체를 &lt;b&gt;독립된 하나의 인덱스만 사용&lt;/b&gt;하도록 &lt;b&gt;변경&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3.1 기초 용어 - 디스크 접근 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시퀀셜 액세스 Sequential Access&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접한 페이지를 차례대로 읽는 &lt;b&gt;순차 접근&lt;/b&gt; 방식, 테이블 풀 스캔에서 활용됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;랜덤 액세스 Random Access&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적으로 떨어진 페이지들에 &lt;b&gt;임의 접근&lt;/b&gt; 방식.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리적으로 떨어진 페이지에 접근하기 때문에 &lt;b&gt;디스크 헤더가 움직이는 비용이 보다 큼.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;접근 범위를 줄이고&lt;/b&gt; &lt;b&gt;인덱스를 활용할 수 있도록 튜닝&lt;/b&gt;해야 함.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3.1 기초 용어 - 조건 유형&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엑세스 조건 Access Condition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;디스크에 있는 데이터에 어떻게 접근할 것인지&lt;/b&gt;를 다루는 &lt;b&gt;SQL 튜닝의 핵심&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;옵티마이저&lt;/b&gt;는 &lt;b&gt;WHERE 절의 특정 조건문을 이용&lt;/b&gt;해 &lt;b&gt;소량의 데이터를 가져&lt;/b&gt;오고, &lt;b&gt;인덱스&lt;/b&gt;를 통해 &lt;b&gt;시간 낭비를 줄이는 조건절을 선택&lt;/b&gt;하여, 스토리지 엔진에 접근하고 MySQL 엔진으로 데이터를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM TAB
WHERE ID = 1
	AND CODE ='A';
-- ID 컬럼 인덱스 O, CODE 컬럼 인덱스 X
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옵티마이저는 위와 같은 경우 인덱스가 존재하는 ID 컬럼을 통해 디스크에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 엑세스 조건이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필터 조건 Filter Condition&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엑세스 조건을 이용해 MySQL 엔진으로 &lt;b&gt;가져온 데이터&lt;/b&gt;를 기준으로 추가로 &lt;b&gt;불필요한 데이터를 제거&lt;/b&gt;하거나 가공하는 &lt;b&gt;조건.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터 조건으로 &lt;b&gt;필터링되어 제거된 데이터가 다수 존재 할수록 효율이 좋지 않다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스토리지에서 MySQL 엔진&lt;/b&gt;으로 넘기는 데이터의 &lt;b&gt;오버헤드가 발생&lt;/b&gt;하기 때문.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제거될 데이터&lt;/b&gt;라면 &lt;b&gt;스토리지 엔진에 접근하는 과정에서 같이 제외&lt;/b&gt;되는 편이 &lt;b&gt;효율적&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM TAB
WHERE ID = 1
	AND CODE ='A';  -- CODE = 'A' 이게 필터 조건임.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3.2 응용 용어&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선택도 Selectivity&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 &lt;b&gt;특정 열을 기준&lt;/b&gt;으로 해당 열의 &lt;b&gt;조건절에 따라&lt;/b&gt; &lt;b&gt;선택되는 데이터 비율&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;낮은 선택도를 가지는 열&lt;/b&gt;은 &lt;b&gt;인덱스 생성&lt;/b&gt;할 때 &lt;b&gt;주요 고려 대상&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;선택도 = 선택한 데이터 건수 / 전체 데이터 건수

변형된 선택도 = 1 / DISTINCT(COUNT COLUMN_NAME) -- 보통 중복이 제거된 데이터의 건수를 활용함.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-- 예시 학번
-- 총 100명의 학생 중 학번을 통해 학생을 찾는 경우
학번 열의 선택도 = 1 / 100 = 0.01 -- 낮은 선택도

-- 예시 성별
-- 총 100명의 학생 중 성별을 통해 학생을 찾는 경우
성별 열의 선택도 =  50 / 100 = 0.5 -- 높은 선택도
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카디널리티 Cardinality&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전적 정의는 하나의 데이터 유형으로 정의되는 데이터 행의 개수.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업에서는 &lt;b&gt;전체 행에 대한&lt;/b&gt; &lt;b&gt;특정 열&lt;/b&gt;의 &lt;b&gt;중복 수치&lt;/b&gt;를 나타내는 &lt;b&gt;지표&lt;/b&gt;로 사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복&lt;/b&gt;되는 값이 &lt;b&gt;적&lt;/b&gt;다면 &lt;b&gt;카디널리티 높음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복&lt;/b&gt;되는 값이 &lt;b&gt;많&lt;/b&gt;으면 &lt;b&gt;카디널리티 낮음&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;카디널리티 = 전체 데이터 건수 * 선택도
-- 예시 
-- 주민등록번호: 카디널리티 높음
-- 이름: 카디널리티 중간
-- 성별: 카디너리티 낮음
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;힌트 Hint&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 데이터베이스에 &lt;b&gt;데이터를 빨리 찾을 수 있도록&lt;/b&gt; &lt;b&gt;추가 정보를 전달하는 것&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 주석처럼 힌트 명시하는 법
SELECT 학번, 전공코드
FROM 학생 /*! USE INDEX (학생_IDX01) */
WHERE 이름 = '유재석';

-- 쿼리의 일부로 작성
SELECT 학번, 전공코드
FROM 학생 USE INDEX (학생_IDX01)
WHERE 이름 = '유재석';

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;옵티마이저가 우리의 힌트를 무조건 참고하는건 아니다.&lt;/b&gt; 비효율적이라고 생각되면 얄짤 없다. (과연 내가 옵티마이저보다 뛰어날까?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;힌트를 사용할 경우 별도의 관리가 필요하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 &lt;b&gt;특정 인덱스&lt;/b&gt;를 이용해 &lt;b&gt;힌트를 관리&lt;/b&gt;하다가 그 &lt;b&gt;인덱스를 삭제&lt;/b&gt;하게 된다면 &lt;b&gt;에러를 발생&lt;/b&gt;시키고 &lt;b&gt;서비스 장애&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜레이션 Collation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 문자셋&lt;/b&gt;으로 데이터베이스에 저장된 값을 &lt;b&gt;비교&lt;/b&gt;하거나 &lt;b&gt;정렬&lt;/b&gt;하는 &lt;b&gt;규칙.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;-- 예시
utf8_bin: A &amp;gt; B &amp;gt; a &amp;gt; b
utf8_general_ci: A &amp;gt; a &amp;gt; B &amp;gt; b
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 캐릭터 셋(Character Set)이랑 뭐가 다름? 캐릭터 셋 == 데이터를 어떻게 저장 할 것인지에 대한 규칙 콜레이션 == 데이터를 어떻게 비교할 것인지에 대한 규칙&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>  책/업무에 바로 쓰는 SQL 튜닝</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/138</guid>
      <comments>https://myeongdev.tistory.com/138#entry138comment</comments>
      <pubDate>Thu, 30 Jan 2025 16:53:17 +0900</pubDate>
    </item>
    <item>
      <title>MDC(Mapped Diagnostic Context) 적용하기</title>
      <link>https://myeongdev.tistory.com/137</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java MDC (Mapped Diagnostic Context) 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MDC (Mapped Diagnostic Context) 는 Java 의 로깅 프레임워크 (Slft4j, Logback, Log4j) 에서 제공하는 기능으로, Thread-local 컨텍스트에 데이터를 저장하고 로깅할 때 이를 로그 메시지에 포함할 수 있도록 하는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 로그의 컨텍스트 정보를 추가하고, 이를 통해 로그 메시지를 더 유용하고 분서하기 쉽게 만들기 위해 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MDC 의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thread-local Storage
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MDC 는 내부 구현적으로 ThreadLocal 을 사용한다.&lt;/li&gt;
&lt;li&gt;즉, 데이터를 현재 쓰레드에만 저장하고, 다른 쓰레드에 영향을 미치지 않는다.&lt;/li&gt;
&lt;li&gt;따라서, 멀티 쓰레드 환경에서 안전하게 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동화된 로그 컨텍스트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그에 일관된 정보를 자동으로 포함할 수 있어, 디버깅과 분석이 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;의존성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Slf4j, Logback , log4j 와 같은 로깅 프레임쿼와 같이 사용하며 , 내부에 구현되어있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MDC 내부 구현 파헤치기&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class MDC {

    static MDCAdapter mdcAdapter;

    /**
     * An adapter to remove the key when done.
     */
    public static class MDCCloseable implements Closeable {
        private final String key;

        private MDCCloseable(String key) {
            this.key = key;
        }

        public void close() {
            MDC.remove(this.key);
        }
    }

    private MDC() {
    }
    
    static {
        try {
            mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
        } catch (NoClassDefFoundError ncde) {
            mdcAdapter = new NOPMDCAdapter();
            String msg = ncde.getMessage();
            if (msg != null &amp;amp;&amp;amp; msg.contains(&quot;StaticMDCBinder&quot;)) {
                Util.report(&quot;Failed to load class \\&quot;org.slf4j.impl.StaticMDCBinder\\&quot;.&quot;);
                Util.report(&quot;Defaulting to no-operation MDCAdapter implementation.&quot;);
                Util.report(&quot;See &quot; + NO_STATIC_MDC_BINDER_URL + &quot; for further details.&quot;);
            } else {
                throw ncde;
            }
        } catch (Exception e) {
            // we should never get here
            Util.report(&quot;MDC binding unsuccessful.&quot;, e);
        }
    }
    
    /**
     * As of SLF4J version 1.7.14, StaticMDCBinder classes shipping in various bindings
     * come with a getSingleton() method. Previously only a public field called SINGLETON 
     * was available.
     * 
     * @return MDCAdapter
     * @throws NoClassDefFoundError in case no binding is available
     * @since 1.7.14
     */
    private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
        try {
            return StaticMDCBinder.getSingleton().getMDCA();
        } catch (NoSuchMethodError nsme) {
            // binding is probably a version of SLF4J older than 1.7.14
            return StaticMDCBinder.SINGLETON.getMDCA();
        }
    }
    
   ... 
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MDC 클래스의 경우 static 블럭을 통해 MDCAdapter 를 초기화 시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;public class StaticMDCBinder {

    /**
     * The unique instance of this class.
     */
    public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();

    private StaticMDCBinder() {
    }

    /**
     * Currently this method always returns an instance of 
     * {@link StaticMDCBinder}.
     */
    public MDCAdapter getMDCA() {
        return new LogbackMDCAdapter();
    }

    public String getMDCAdapterClassStr() {
        return LogbackMDCAdapter.class.getName();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StaticMDCBinder 클래스의 경우 singletone 으로 관리도고 있으며, MDCAdapter 인터페이스의 구현체인LogbackMDCAdapter 클래스를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class LogbackMDCAdapter implements MDCAdapter {

    final ThreadLocal&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; copyOnThreadLocal = new ThreadLocal&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt;();
    
    final ThreadLocal&amp;lt;Integer&amp;gt; lastOperation = new ThreadLocal&amp;lt;Integer&amp;gt;();

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException(&quot;key cannot be null&quot;);
        }

        Map&amp;lt;String, String&amp;gt; oldMap = copyOnThreadLocal.get();
        Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map&amp;lt;String, String&amp;gt; newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LogbackMDCAdapter 의 경우 final 인자로 내부에 ThreadLocal 을 두개 가지고 있다.&lt;/li&gt;
&lt;li&gt;copyOnThreadLocal 의 경우 현재 스레드의 MDC 데이터를 저장하기 위해 사용된다.&lt;/li&gt;
&lt;li&gt;필요할 경우에는 기존 맵의 복사본을 만들어서 사용하는 모습을 볼 수 있다.&lt;/li&gt;
&lt;li&gt;lastOperation 의 경우 변경사항에 대한 기록을 하는 역할로 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;현재 작업 상태를 설정하고, 이전 작업 상태를 반환하는 역할로 데이터 일관성을 보장하고, 불필요한 맵 객체 복사를 방지하는 역할로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MDC 적용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MDCFilter&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String traceId = UUID.randomUUID().toString().replaceAll(&quot;-&quot;, &quot;&quot;);
        MDC.put(&quot;traceId&quot;, traceId);
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;logback.xml&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;property name=&quot;LOG_PATTERN&quot;
              value=&quot;%-5level %d{yyyy-MM-dd HH:mm:ss} [%X{traceId}] [%logger{0}:%line] - %msg%n&quot;/&amp;gt;
              
	&amp;lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt; 
	    &amp;lt;file&amp;gt;${LOG_PATH}/${LOG_FILE_NAME}.log&amp;lt;/file&amp;gt;
	    &amp;lt;encoder class=&quot;ch.qos.logback.classic.encoder.PatternLayoutEncoder&quot;&amp;gt;
	        &amp;lt;pattern&amp;gt;${LOG_PATTERN}&amp;lt;/pattern&amp;gt;
	    &amp;lt;/encoder&amp;gt; 
	    &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&amp;gt;
	        &amp;lt;fileNamePattern&amp;gt;${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log&amp;lt;/fileNamePattern&amp;gt;
	        &amp;lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&amp;gt; 
	            &amp;lt;maxFileSize&amp;gt;1GB&amp;lt;/maxFileSize&amp;gt;
	        &amp;lt;/timeBasedFileNamingAndTriggeringPolicy&amp;gt; 
	        &amp;lt;maxHistory&amp;gt;3&amp;lt;/maxHistory&amp;gt;
	    &amp;lt;/rollingPolicy&amp;gt;
	&amp;lt;/appender&amp;gt;
	
	
    &amp;lt;root level=&quot;${LOG_LEVEL}&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;FILE&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;INFO  2025-01-21 17:09:03 [6a064952adaf47629f4fb57e3e9b8771] [LoggingFilter:71]
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 여러 요청이 들어올 경우 로그를 파악하는데 힘이 든다.&lt;/li&gt;
&lt;li&gt;최근 관심있는 OpneSource 에서도 MDC 를 사용하는 모습을 보고 현재 프로젝트에 찾아보고 적용하게 되었다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>1.프로그래밍/Java</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/137</guid>
      <comments>https://myeongdev.tistory.com/137#entry137comment</comments>
      <pubDate>Thu, 30 Jan 2025 16:17:47 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 후기</title>
      <link>https://myeongdev.tistory.com/136</link>
      <description>&lt;h1&gt;도커 교과서 책 후기&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MhgTO/btsL0Yv6Psc/kkiRP7XeCjCOktAyILvdAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MhgTO/btsL0Yv6Psc/kkiRP7XeCjCOktAyILvdAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MhgTO/btsL0Yv6Psc/kkiRP7XeCjCOktAyILvdAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMhgTO%2FbtsL0Yv6Psc%2FkkiRP7XeCjCOktAyILvdAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;198&quot; height=&quot;255&quot; data-origin-width=&quot;198&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker 개념에 대해 알고있고, 더 심화 학습을 진행하기에는 더 할 나위 없는 책이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지 레이어에 대한 개념과 골든 이미지, 도커 컴포즈, 도커 스웜을 이용한 분산 애플리케이션, 도커 애플리케이션 모니터링 방법 등 도커를 실제 프로젝트에 적용할 수 있을만큼의 지식을 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 경우 입문서 한권을 읽고 해당 책을 읽으면서 실무에서 사용되고 있는 Dockerfile, docker-compose 파일을 볼 때 시선이 달라짐을 확실히 느끼게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부족하지 않게 작성되어 있는 실습 내용들과 개념의 내용들이 해당 책을 공부하는데 지루하지 않게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 실습에서 사용되는 이미지들이 부족한 부분들이 가끔가다 존재했던걸로 기억한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 부분은 책의 출판부터 시간이 지나면서 어쩔수 없는 부분이라고 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 아쉬운 점으로 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 그외 부분의 내용들은 짜임새 있게 잘 짜여있고, 지식의 양도 풍부하다고 생각된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커에 대해서 실무에서 적용할 만큼의 지식을 얻고 싶다면 해당 책 한권으로 충분한 지식을 얻을 수 있을것이라고 생각한다.&lt;/p&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/136</guid>
      <comments>https://myeongdev.tistory.com/136#entry136comment</comments>
      <pubDate>Sun, 26 Jan 2025 16:46:58 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 22장 끝없는 정진</title>
      <link>https://myeongdev.tistory.com/135</link>
      <description>&lt;h1&gt;&lt;b&gt;22장 끝없는 정진&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;22.1 도커를 이용한 개념 검증&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커를 사용하면 개발중인 애플리케이션을 검증하기 쉬워진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러개의 컴포넌트를 컨테이너화
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 컴포즈의 강력한 기능 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Best Practice 적용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티 스테이지 Dockerfile, 스크립트 최적화, 골든 이미지 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;중화된 로그 수집
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그라파나 대시보드 혹은 키바나를 통한 시각화 도구 함꼐 사용하기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인 구축
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 빌드 자동화를 이용한 CI/CD 파이프라인 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;22.2 소속 조직에서 도커의 유용함을 입증하라&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자가 전체 애플리케이션 스택을 운영환경과 완전히 동일하게 개발환경을 구축할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의존 모듈 누락 및 버전 차이로 인한 문제 발생 차단.&lt;/li&gt;
&lt;li&gt;개발팀과 운영팀의 애플리케이션 주체의식 동등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;운영팀의 애플리케이션 운영에 대한 표준 도구 사용.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너화된 컴포넌트들의 설정을 위한 표준 API 사용 가능.&lt;/li&gt;
&lt;li&gt;이미지를 통한 배포 시 롤백 처리 단순화를 통한 더 잦은 배포 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;데이터베이스 컨테이너화를 통한 관리.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 관리자 관점에서는 바람직하지 않음.&lt;/li&gt;
&lt;li&gt;그러나, 개발팀과 운영팀이 직접 관리할 수 있게 되며, 이미지를 이용해서 스키마 형상관리 및 CI/CD 도입 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;도커를 사용한 컨테이너 보안
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;골든 이미지, 보안 스캐닝, 이미지 샤이닝 등 공급체인 전체를 안전하게 유지 가능.&lt;/li&gt;
&lt;li&gt;아쿠아 트위스트록 등의 도구를 통해 컨테이너를 모니터링하여 잠재적인 공격 방지 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;소프트웨어 품질 향상.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자가 수보 애플리케이션, 헬스 대시보드 등을 통한 더 쉬운 CI/CD 구축.&lt;/li&gt;
&lt;li&gt;소프트웨어 품질 향상 및 잦은 배포 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비용 절감
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너의 가장 큰 장점. 경량화&lt;/li&gt;
&lt;li&gt;줄어드는 서버 수 만큼 비용 절감 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최신 기술
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커는 최신 기술이 맞다.&lt;/li&gt;
&lt;li&gt;최신 기술으 사용함으로써 기술 부채를 줄이고 최신 커뮤니티에서 최신 정보를 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/135</guid>
      <comments>https://myeongdev.tistory.com/135#entry135comment</comments>
      <pubDate>Sun, 26 Jan 2025 15:57:49 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 21장 메시지 큐를 이용한 비동기 통신</title>
      <link>https://myeongdev.tistory.com/134</link>
      <description>&lt;h1&gt;&lt;b&gt;21장 메시지 큐를 이용한 비동기 통신&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;21.1 비동기 메시징이란?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동기 통신&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어의 컴포넌트는 대개 동기적으로 통신한다.&lt;/li&gt;
&lt;li&gt;클라이언트가 서버에 접속 요청을 보내고, 서버의 응답을 기다린 다음 접속을 종료하는 전체 과정이 동기적으로 이뤄진다.&lt;/li&gt;
&lt;li&gt;동기적 통신을 할 때 서버가 다운되거나, 응답 시간이 오래 걸리는 경우, 네트워크 수준에서 실패한 경우 등이 문제가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 통신&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 통신을 적용하려면 클라이언트와 서버 사이에 계층이 하나 추가된다.&lt;/li&gt;
&lt;li&gt;클라이언트가 큐에 요청을 보내고, 서버는 큐를 주시하다가 메시지를 수신하고 처리한다.&lt;/li&gt;
&lt;li&gt;그렇다면 비동기 통신이 무조건 좋은가? 에 대한 답은 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;큐&lt;/b&gt;를 제공하는 &lt;b&gt;기술의 신뢰성&lt;/b&gt;이 뛰어나야 하며, &lt;b&gt;큐&lt;/b&gt; 기술의 사용량이 &lt;b&gt;비싸다&lt;/b&gt;는 큰 단점이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;도커&lt;/b&gt;를 사용하면 엔터프라이즈급 안정성을 갖춘 &lt;b&gt;오픈 소스 큐 시스템을 도입&lt;/b&gt;해 문제를 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;또한, &lt;b&gt;경량 컨테이너&lt;/b&gt;에서 메시지 큐를 실행하면 &lt;b&gt;애플리케이션 마다 별도의 전용 큐를 사용&lt;/b&gt;할 수 있고, &lt;b&gt;모든 환경에서 동일하게 도입&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;li&gt;메시지 큐를 이용한 비동기 통신의 경우 전달자와 수신자의 결합을 느슨하게 하는 방법으로 애플리케이션의 성능과 확장성을 개선할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Message Queue&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis 를 활용하여 비동기 메시지 큐를 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;메시지 큐에는 크게 &lt;b&gt;Publisher&lt;/b&gt; 와 &lt;b&gt;Subscriber&lt;/b&gt; 가 존재한다.&lt;/li&gt;
&lt;li&gt;발행자(Publisher) 는 메시지를 보내는 주체이며, 구독자(Subscriber) 는 메시지르 받는 주체이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ docker network create ch21

$ docker container run -d --name redis --network ch21 diamol/redis

$ docker logs redis --tail 1

# Publisher 컨테이너
$ docker run -d --name publisher --network ch21 diamol/redis-cli -r 50 -i 5 PUBLISH channel21 ping

$ docker logs publisher

# Subscriber 컨테이너
$ docker run -it --name subscriber --network ch21 diamol/redis-cli SUBSCRIBE channel21

Reading messages... (press Ctrl-C to quit)
1) &quot;subscribe&quot;
2) &quot;channel21&quot;
3) (integer) 1
1) &quot;message&quot;
2) &quot;channel21&quot;
3) &quot;ping&quot;
1) &quot;message&quot;
2) &quot;channel21&quot;
3) &quot;ping&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;21.2 클라우드 네이티브 메시지 큐 사용하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NATS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NATS 는 CNCF 재단에서 관리하는 프로젝트로, 높은 완성도와 신뢰도를 바탕으로 많이 사용되고 있다.&lt;/li&gt;
&lt;li&gt;NATS 는 메시지를 메모리에 저장하며, 속도가 매우 빠르고 컨테이너 간의 통신에 적합하다.&lt;/li&gt;
&lt;li&gt;NATS 에는 채널 개념이 존재하지 않고, 모든 메시지에 Subject 를 부여한다.&lt;/li&gt;
&lt;li&gt;subject 를 통해서 메시지 유형을 구분하며, 원하는 명명 규칙을 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;이를 통해 Subscriber 는 해당 Subject 를 구독하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ vi message-queue

version: &quot;3.7&quot;

services:
  todo-web:
    image: diamol/ch21-todo-list
    ports:
      - 8080:80
    networks:
      - app-net

  todo-db:
    image: diamol/postgres:11.5
    networks:
      - app-net

  message-queue:
    image: diamol/nats
    ports:
      - &quot;8222:8222&quot;
    networks:
      - app-net

  save-handler:
    image: diamol/ch21-save-handler
    networks:
      - app-net

networks:
  app-net:

$ docker compose up -d message-queue

$ docker logs todo-list-message-queue_1

[1] 2025/01/16 04:46:10.381523 [INF] Starting nats-server version 2.1.9
[1] 2025/01/16 04:46:10.381536 [INF] Git commit [7c76626]
[1] 2025/01/16 04:46:10.381640 [INF] Starting http monitor on 0.0.0.0:8222
[1] 2025/01/16 04:46:10.381661 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2025/01/16 04:46:10.381666 [INF] Server id is NBPKEDLHJMXTO3NDKAXR6OERYRJXK2N5XIS5Z7YFBQ3G3JOH544I5SKG
[1] 2025/01/16 04:46:10.381667 [INF] Server is ready
[1] 2025/01/16 04:46:10.381753 [INF] Listening for route connections on 0.0.0.0:6222

$ curl &amp;lt;http://localhost:8222/connz&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만약, 메시지 큐에 전달했지만 큐를 구독하는 구독자가 없을 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통의 메시지 큐는 구독자가 없을 경우 DLQ(Dead Letter Queue) 에 메시지를 저장한다.&lt;/li&gt;
&lt;li&gt;그러나, Redis 와 NATS 의 경우 구도자가 없으면 해당 메시지를 그대로 버린다.&lt;/li&gt;
&lt;li&gt;즉, Redis 와 NATS 는 구독한 시점 이후부터 발행된 메시지만 수신할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;21.3 메시지 수신 및 처리&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Message Queue 를 구독하는 컴포넌트를 메시지 핸들러라고 한다.&lt;/li&gt;
&lt;li&gt;대게 메시지의 종류마다 이를 처리하는 메시지 핸들러가 하나씩 필요하다.&lt;/li&gt;
&lt;li&gt;예시에서 사용되는 to-do 애플리케이션에는 할 일 이벤트 메시지를 받아, DB 에 추가하는 역할을 하는 메시지 핸들러가 필요하다.&lt;/li&gt;
&lt;li&gt;이렇듯 메시지 핸들러(Worker) 를 따로 두게 되면 스케일 아웃에 용이하다.&lt;/li&gt;
&lt;li&gt;메시지 큐가 급증하는 부하의 버퍼와 같은 역할을 한다.&lt;/li&gt;
&lt;li&gt;사용자가 아무리 많아도 동시에 메시지 핸들러를 통해 메시지가 하나씩 소비되므로 들어오는 SQL 커넥션은 메시지 핸들러 개수로 제한된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과적 일관성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 메시지의 경우 작업이 완료되는데 약간의 시간이 발생한다.&lt;/li&gt;
&lt;li&gt;이러한 부수 효과를 결과적 일관성이라고 한다.&lt;/li&gt;
&lt;li&gt;모든 메시지의 처리가 끝나면 애플리케이션 데이터의 상태가 정확해지는데, 그 이전 시점에는 일관성이 깨질 수 있다.&lt;/li&gt;
&lt;li&gt;이를 해결하기 위해 메시지가 모두 처리가 완료되었다는 이벤트를 통해 데이터를 갱신함으로써 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;21.4 메시지 핸들러로 기능 추가하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이벤트 지향 아키택처&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트 지향 아키텍처는 애플리케이션이 모든 일을 즉각 동기적으로 처리하는 대신 이벤트를 통해 다른 구성 요소에 자신의 현재 상태를 알리는 방식이다.&lt;/li&gt;
&lt;li&gt;이벤트를 발행하는 로직을 변경하지 않고도 이벤트 처리 로직을 바꿀 수 있으므로 애플리케이션의 구성 요소 간 결합도를 느슨하게 하는 효과가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 핸들러를 이용하여 새 기능 추가하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지 핸들러를 이용하여 새로운 기능을 추가하는 가장 쉬운 방법은 새로운 메시지 핸들러의 그룹을 만들고, 모든 메시지를 수신하도록 하되 이벤트 처리를 다르게 하는 것이다.&lt;/li&gt;
&lt;li&gt;이렇게하면 기존 컨테이너에는 수정이 일어나지 않고, 신규 기능에 대한 컨테이너만 실행시키면 되기 때문에 배포의 부담이 크게 낮아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;21.5 비동기 메시징 패턴 이해하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Pub-Sub (Publish-Subscribe) 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;발행자(Publisher) 가 메시지를 특정 주제(Topic) 이나 채널 (Channel) 에 게시하면, 해당 주제를 구독(Subscribe) 한 모든 구독자(Subscriber) 가 메시지를 받는 방식이다.&lt;/li&gt;
&lt;li&gt;이 패턴에서는 퍼블리셔가 메시지를 사용한 것이 누구고, 어떻게 처리하며, 언제 처리가 끝나는지 알 수 없다.&lt;/li&gt;
&lt;li&gt;한 명의 발행자가 여러 구독자에게 메시지를 전달해야 하는 다대다 통신에 사용한다.&lt;/li&gt;
&lt;li&gt;상황에 따라 Pub/Sub 패턴이 적합하지 않을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Rquest-Response 패턴&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 메시지 큐에 메시지를 전달하고 응답을 기다린다.&lt;/li&gt;
&lt;li&gt;메시지 핸들러는 요청 메시지를 처리한 다음 메시지 큐에 응답한다.&lt;/li&gt;
&lt;li&gt;메시지 큐는 다시 응답을 클라이언트에게 전달한다.&lt;/li&gt;
&lt;li&gt;하나의 요청에 대해 하나의 응답이 매핑되는 일대일 통신이다.&lt;/li&gt;
&lt;li&gt;핸들러와 클라이언트가 기다리는 동안 다른 작업을 할 수 있다는 비동기 메시징의 장점을 그대로 유지하면서도 일반적인 동기 서비스 호출을 대체할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/134</guid>
      <comments>https://myeongdev.tistory.com/134#entry134comment</comments>
      <pubDate>Sun, 19 Jan 2025 22:34:39 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 20장 리버스 프록시를 이용해 컨테이너 HTTP 트래픽 제어하기</title>
      <link>https://myeongdev.tistory.com/133</link>
      <description>&lt;h1&gt;&lt;b&gt;20장 리버스 프록시를 이용해 컨테이너 HTTP 트래픽 제어하기&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;20.1 리버스 프록시란?&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 웹 애플리케이션으로 통하는 관문 역할을 수행.&lt;/li&gt;
&lt;li&gt;리버스 프로시는 포트를 외부로 공개한 유일한 컨테이너 이다.&lt;/li&gt;
&lt;li&gt;외부에서 들어오는 모든 트래픽은 먼저 리버스 프록시를 거치므로 애플리케이션 포트를 외부로 공개하지 않아도 된다.&lt;/li&gt;
&lt;li&gt;애플리케이션이 응답 내용을 캐시해 두었다가 적절하게 가공해서 클라이언트에게 전달.&lt;/li&gt;
&lt;li&gt;스케일링, 업데이트 보안 면에서 유리.&lt;/li&gt;
&lt;li&gt;리버스 프록시를 경량 컨테이너로 실행하게 되면서 모든 환경에서 동일한 프록시 설정을 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Nginx 프록시 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx 가 단순히 요청을 전달하는 매개자 역할을 한다.&lt;/li&gt;
&lt;li&gt;요청을 받을 때마다 이를 처리하는 컨테이너(upstream) 을 호출한다.&lt;/li&gt;
&lt;li&gt;응답을 다시 클라인어트 (downstream) 로 전달한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
	server_name whoami.local;  # 도메인
	
	location / {
		proxy_pass &amp;lt;http://whoami&amp;gt;;  # 콘텐츠 주소
		proxy_set_header Host $host; # 호스트 정보를 콘텐츠 위치로 설정
		add_header X-Host $hostname; # 읃답의 호스트 정보를 프록시 이름으로 변경
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리버스 프록시는 HTTP 로 제공되는 콘텐츠라면 무엇이든 사용 가능하다.&lt;/li&gt;
&lt;li&gt;Nginx 는 단순히 요청을 전달하는 매개자 역할이다.&lt;/li&gt;
&lt;li&gt;모든 애플리케이션 트래픽이 프록시를 경유하므로 설정의 중심 역할을 할 수 있다.&lt;/li&gt;
&lt;li&gt;또한, 인프라 스트럭쳐 수준의 사항을 애플리케이션 컨테이너와 분리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;20.2 리버스 프록시의 라우팅과 SSL 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx 를 이용하여 리버스 프록시 적용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx 는 업스트림 컨테이너가 여러개 존재한다면 이들 간의 로드밸런싱 처리가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;$ echo $'\\n127.0.0.1 image-gallery.local' | sudo tee -a /etc/hosts

$ docker compose -f ./image-gallery/docker-compose.yml up -d --scale image-gallery=3

$ ./nginx/sites-available/image-gallery.local ./nginx/sites-enabled/

$ docker compose -f ./nginx/docker-compose.yml restart nginx

$ curl -i --head &amp;lt;http://image-gallery.local&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;$ vi ./nginx/conf/conf.d/default.conf

server {
	server_name image-gallery.local;
	
	location / {
		proxy_pass &amp;lt;http://image-gallery&amp;gt;;
		proxy_set_header Host $host;
		add_header X-Proxy $hostname;
		add_header X-Upstream $upstream_addr;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X-Upstream 항목은 Nginx 가 응답을 받아 온 컨테이너의 IP 주소가 담긴 항목이다.&lt;/li&gt;
&lt;li&gt;curl 을 사용해 애플리케이션을 호출할 경우 로드밸런싱이 적용되어 매번 호출되는 IP 주소가 변경되는 것을 확인 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx SSL 적용하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리버스 프록시를 이용하면 HTTPS 를 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;Nginx 는 Let&amp;rsquo;s Encrypt 와 같은 실제 도메인 제고아나 서비스에서 발급한 실제 인증서를 설정에 포함할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# 자체 인증서 생성
$ docker container run -v &quot;$(pwd)/nginx/certs:/certs&quot; -e HOST_NAME=image-gallery.local diamol/cert-generator

# 기존 설정 파일 삭재
$ rm ./nginx/sites-enabled/image-gallery.local

# SSL 이 포함된 설정 파일 복사
$ cp ./nginx/sites-available/image-gallery-3.local ./nginx/sites-enabled/image-gallery.local

# 재시작
$ docker compose -f nginx/docker-compose.yml restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;$ vi ./nginx/conf/conf.d/default.conf

server {
	server_name image-gallery.local;
	listen 80;
		return 301 https://$server.name$request_uri;
}

server {
	server_name image-gallery.local;
	listen 443 ssl;
	
	ssl_certificate /etc/nginx/certs/server-cert.pem;
	ssl_certificate_key /etc/nginx/certs/server-key.pem;
	ssl_session_cache shared:SSL:10m;
	ssl_session_timeout 20m;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	
	ssl_prefer_server_ciphers on;
	ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
	
	add_header Strict-Transport-Security &quot;max-age=31536000&quot; always;
	
	location /api/image {
			proxy_pass &amp;lt;http://iotd/image&amp;gt;;
			proxy_set_header Host $host;
			add_header X-Proxy $hostname;
			add_header X-Upstream $upstream_addr;
	}
	
	location / {
		proxy_pass &amp;lt;http://image-gallery&amp;gt;;
		proxy_set_header Host $host;
		add_header X-Proxdy $hostname;
		add_header X-Upstream $upstream_addr;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증서와 키 파일 쌍은 하나의 도메인에서만 유효하므로, 애플리케이션 하나마다 인증서와 키 파일 세트가 필요하다.&lt;/li&gt;
&lt;li&gt;인증서와 키 파일은 민감 정보로 비밀값 형태로 클러스터에 저장된다.&lt;/li&gt;
&lt;li&gt;HTTPS 를 적용하지 않으면, 애플리케이션 컨테이너 설정과 인증서 관리 부담이 줄어들며, 개발자는 단순 HTTP 버전으로 테스트를 진행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;20.3 프록시를 이용한 성능 및 신뢰성 개선&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx 는 고성능 HTTP 서버로 정적 HTML 콘텐츠나 단일 페이지 애플리케이션을 제공하는데 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;컨테이너 하나만으로도 초당 수천건의 요청을 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;이를 활용하여 Nginx 를 &lt;b&gt;캐싱 프록시&lt;/b&gt;로 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션의 응답을 로컬 디스크나 메모리에 저장하고, 동일한 요청에 대해 업스트림 콘텐츠에 요청하지 않고, 저장된 것을 사용한다.&lt;/li&gt;
&lt;li&gt;따라서, 캐싱 프록시를 사용하게 되면 요청 시간을 줄이고, 같은 인프라 스트럭처로 더 많은 요청을 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;$ vi ./nginx/sites-available/image-gallery-4.local

server {
    server_name image-gallery.local;
    listen 80;
	return 301 https://$server_name$request_uri;
}

server {
	server_name  image-gallery.local;
	listen 443 ssl;
    
    gzip  on;    
    gzip_proxied any;

	ssl_certificate        /etc/nginx/certs/server-cert.pem;
	ssl_certificate_key    /etc/nginx/certs/server-key.pem;
	ssl_session_cache      shared:SSL:10m;
	ssl_session_timeout    20m;
	ssl_protocols          TLSv1 TLSv1.1 TLSv1.2;

	ssl_prefer_server_ciphers on;
	ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';

	add_header  Strict-Transport-Security &quot;max-age=31536000&quot; always;

    location = /api/image {
        proxy_pass             &amp;lt;http://iotd/image&amp;gt;;
        proxy_set_header       Host $host;
        proxy_cache            SHORT;
        proxy_cache_valid      200  1m;
        add_header             X-Cache $upstream_cache_status;
        add_header             X-Proxy $hostname;         
        add_header             X-Upstream $upstream_addr;
    }

    location / {
        proxy_pass             &amp;lt;http://image-gallery&amp;gt;;
        proxy_set_header       Host $host;
        proxy_cache            LONG;
        proxy_cache_valid      200  6h;
        proxy_cache_use_stale  error timeout invalid_header updating
                               http_500 http_502 http_503 http_504;
        add_header             X-Cache $upstream_cache_status;
        add_header             X-Proxy $hostname;         
        add_header             X-Upstream $upstream_addr;
    }        
}

$ cp ./nginx/sites-available/image-gallery-4.local ./nginx/sites-enabled/image-gallery.local

$ docker compose -f ./nginx/docker-compose.yml restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프록시 설정에 사용자 정의 응답 헤더인 X-Cache 가 포함된다.&lt;/li&gt;
&lt;li&gt;요청에 해당하는 캐시가 있는지 먼저 확인한 이후, 요청에 일치하는 캐시가 없을 경우 X-Cache: MISS 가 반환된다.&lt;/li&gt;
&lt;li&gt;추가로 X-Upstream 헤더에 콘텐츠를 제공한 IP 주소가 응답된다.&lt;/li&gt;
&lt;li&gt;같은 요청이 추가로 들어왔을 경우 X-Cache: HIT 과 함께 응답되며, X-Upstream 헤더는 들어오지 않는다.&lt;/li&gt;
&lt;li&gt;proxy_cache_use_stale 는 Upstream 을 사용하지 못할 경우 만료된 캐시를 사용하라는 의미이다.&lt;/li&gt;
&lt;li&gt;만료된 캐시라도 사용함으로써 컨테이너가 장애를 일으켜도 애플리케이션이 서비스를 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;20.4 클라우드 네이티브 리버스 프록시&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 엔진과 연결된 컨테이너는 도커 API를 통해 다른 컨테이너에 대한 정보를 얻을 수 있다.&lt;/li&gt;
&lt;li&gt;클라우드 네이티브 리버스 프록스 도구인 트래픽 (Traefik) 이 이런 방식으로 동작한다.&lt;/li&gt;
&lt;li&gt;트래픽을 사용할 경우 컨테이너에 레이블만 추가하면 스스로 설정과 라우팅 맵을 구성할 수 있다.&lt;/li&gt;
&lt;li&gt;트래픽을 사용하면 자동으로 리버스 프록시 기능을 사용할 수 있지만, Nginx 와 다르게 캐싱을 할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트래픽 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적 설정 구성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트래픽은 시작할 때 설정 파일을 읽지 않고, 런타임 중에 구성을 변경할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동 Service Discovery
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인프라스트럭처에 대한 네티이브 지원을 통해 새로운 서비스를 자동으로 감지하고, 트래픽을 해당 서비스로 라우팅한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최신 프로토콜 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP/2, WebSocket, GRPC 등의 최신 프로토콜을 지원함으로써, 원할한 통신이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt 를 사용한 HTTPS 지원
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt 를 사용하여 자동으로 SSL/TLS 인증서를 발급하고 관리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트래픽 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엔트리 포인트
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부에서 들어오는 트래픽을 주시하는 포트.&lt;/li&gt;
&lt;li&gt;해당 포트와 컨테이너의 공개 포트가 매핑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;라우터
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인입된 요청을 배정할 컨테이너를 결정하는 규칙.&lt;/li&gt;
&lt;li&gt;호스트 명, 경로 등으로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서비스
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 콘텐츠를 제공하는 업스트림 컴포넌트&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;미들웨어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 서비스 사이에서 서비스에 전달되는 요처을 변경하는 역할&lt;/li&gt;
&lt;li&gt;요청에 포함된 경로 또는 헤더를 변경하거나 인증을 강제할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;services:
	whoami:
		labels:
			- &quot;traefik.enable=true&quot;
			- &quot;traefik.http.routers.whoami.rule=Host('whoami.local')&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스티키 세션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션을 만들 때 최대한 많은 부분을 무상태로 만드는 것이 좋다.&lt;/li&gt;
&lt;li&gt;무상태로 만들면 아무 컨테이너에서나 요청을 처리할 수 있으므로 수평 확 장 및 로드 밸런싱 효과를 극대화 할 수 있다.&lt;/li&gt;
&lt;li&gt;그러나, 기존 애플리케이션의 경우 상태가 있는 구송 요소를 많이 포함하고있는 경우가 있다.&lt;/li&gt;
&lt;li&gt;이 경우 스티키 세션이 필요하다.&lt;/li&gt;
&lt;li&gt;스트키 세션을 활성화하면 클라이언트에 컨테이너를 식별할 수 있는 쿠기가 부여되므로 해당 사용자의 요청을 계속 같은 컨테이너로 라우팅 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;services:
	whoami:
		labels:
			- &quot;traefik.enable=true&quot;
			- &quot;traefik.http.routers.whoami.rule=Host('whoami.local')&quot;
			- &quot;traefik.http.services.whoami.loadBalancer.sticky=true&quot;
			- &quot;traefik.http.services.whoami.loadBalancer.sticky.cookie.name=whoami_cookie&quot;
			- &quot;traefik.http.services.whoami.loadBalancer.sticky.cookie.httpOnly=true&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;20.5 리버스 프록시를 활용한 패턴의 이해&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 클러스터에서 각 다른 도메인 이름을 갖는 여러 개의 애플리케이션을 호스팅하는 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리버스 프록시만 80번 포트와 443 포트를 통해 외부로 노출한다.&lt;/li&gt;
&lt;li&gt;그 외의 컨테이너는 모두 외부로 노출되지 않는다.&lt;/li&gt;
&lt;li&gt;라우팅 규칙을 통해 필요한 컨테이너에 라우팅한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마이크로서비스 아키택처의 일부 요소를 외부로 노출시키는 패턴&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리버스 프록시를 통해 웹 컨테이너와 마이크로서비스 중 일부가 외부로 노출된다.&lt;/li&gt;
&lt;li&gt;엔트리포인트는 같은 도메인을 사용하지만, HTTP 요청 경로에 따라 요청이 다른 컨테이너로 라우팅된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;모놀리식 설계를 가진 애플리케이션을 점진적으로 컨테이너로 이주시키기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리버스 프록시를 이용해 모놀리식 설계를 점진적으로 마이크로서비스로 분할할 수 있다.&lt;/li&gt;
&lt;li&gt;새로운 기능은 별도의 컨테이너 형태로 추가하며, 리버스 프록시를 통해 요청 경로에 따라 전달한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/133</guid>
      <comments>https://myeongdev.tistory.com/133#entry133comment</comments>
      <pubDate>Sat, 18 Jan 2025 00:45:19 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 19장 도커를 이용한 로그 생성 및 관리</title>
      <link>https://myeongdev.tistory.com/132</link>
      <description>&lt;h1&gt;&lt;b&gt;19장 도커를 이용한 로그 생성 및 관리&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;19.1 표준 에러 스트림과 표준 출력 스트림&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;도커 이미지&lt;/b&gt;는 애플리케이션 바이너리 및 의존성, 그리고 컨테이너를 시작할 때 &lt;b&gt;도커가 실행할 프로세스에 대한 정보&lt;/b&gt; 등을 담은 &lt;b&gt;파일 시스템의 스냅샷&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;컨테이너를 시작할 때 실행되는 프로세스는 포어그라운드로 동작한다.&lt;/li&gt;
&lt;li&gt;실행된 프로세스에서 생성한 로그 엔트리는 표준 출력 및 표준 오류 스트림으로 출력된다.&lt;/li&gt;
&lt;li&gt;도커는 각 컨테이너의 stdout 과 stderr 스트림을 주시하며 스트림을 통해 출력되는 내용을 수집한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포어그라운드(Foreground) 란? &lt;b&gt;터미널에서 직접 명령&lt;/b&gt;을 내리고, &lt;b&gt;실행 결과를 바로 확인&lt;/b&gt;할 수 있는 상태. 즉, &lt;b&gt;터미널과 프로세스가 긴밀하게 연결되어 있는 상태.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;$ docker container logs --tail 1 timecheck

Environment: DEV; version: 3.0; time check: 12:02.51

$ docker container inspect --format='{{.LogPath}}' timecheck

/var/lib/docker/containers/cffcd1c2efffae434bc7179bbfd55ab1de44792bff6c7476c7ad0188af540bb0/cffcd1c2efffae434bc7179bbfd55ab1de44792bff6c7476c7ad0188af540bb0-json.log
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;터미널 세션과 분리된 컨테이너와 종료된 컨테이너의 로그를 수집할 수 있도록 로그를 JSON 파일로 저장한다.&lt;/li&gt;
&lt;li&gt;해당 JSON 파일은 도커가 직접 컨테이너와 동일한 생애주길르 갖도록 관리한다.&lt;/li&gt;
&lt;li&gt;기본적으로 컨테이너마다 JSON 로그 파일 하나가 생성되며, 한 디스크 용량이 찰 때까지 이 파일의 크기가 증가한다.&lt;/li&gt;
&lt;li&gt;엔진 설정으로 전체 설정 혹은 컨테이너 단위의 설정도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;19.2 다른 곳으로 출력된 로그를 stdout 스트림에 전달하기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;표준 로그 모델을 적용하지 못하는 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표준 로그 모델을 적용하기 어려운 애플리케이션도 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) 출력 스트림으로 아무 내용을 출력하지 않는 애플리케이션&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또한, 별도의 로그 프레임워크를 경유해 다른 곳에 로그를 생하는 경우가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) 리눅스의 syslog, 윈도우의 이벤트 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이들 모두 컨테이너 시작 프로세스에서 출력되는 로그가 없으므로 도커가 로그를 수집하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 유틸리티&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;표준 로그모델을 적용하지 못하는 경우에는 &lt;b&gt;로그 전달용 유틸리티를 사용&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;로그 전달용 유틸리티는 &lt;b&gt;로그 파일&lt;/b&gt;의 내용을 읽어 &lt;b&gt;표준 출력&lt;/b&gt;으로 &lt;b&gt;내보내 주는 역할&lt;/b&gt;을 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨테이너 시작명령&lt;/b&gt;에서 별도의 로그 전달용 &lt;b&gt;유틸리티를 실행&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 유틸리티 단점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그 전달용 유티리티는 &lt;b&gt;포어그라운드로 동작&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;해당 프로세스가 종료되면 &lt;b&gt;애플리케이션과 함께 컨테이너까지 종료될 수 있다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;또한, &lt;b&gt;애플리케이션이 오류로 종료되어도&lt;/b&gt; 포어그라운드로 동작 중인 유틸리티가 계속 실행되므로 &lt;b&gt;컨테이너는 그대로 실행된다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;따라서, 헬스 체크를 적용하지 않는 컨테이너의 이상 상태를 감지할 수 없다.&lt;/li&gt;
&lt;li&gt;마지막으로, 유틸리티의 경우 &lt;b&gt;디스크 사용 효율이 떨어&lt;/b&gt;진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM diamol/dotnet-runtime AS base
...
WORKDIR /app
COPY --from=builder /out/ .
COPY --from=utility /out/ .

FROM base AS linux
CMD dotnet TimeCheck.dll &amp;amp; dotnet Tail.dll /logs timecheck.log
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마지막 CMD 인스트럭션을 확인하면, dotnet 애플리케이션을 백그라운드로 실행하고, Tail 을 실행한다.&lt;/li&gt;
&lt;li&gt;tail 은 로그 파일을 감시하다가, 새로운 내용이 추가되면 stdout 스트림으로 전달해 컨테이너 로그로 수집되도록 하는 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;19.3 컨테이너 로그 수집 및 포워딩하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통합 로깅 시스템으로는 오픈소스인 fluentd가 많이 사용된다.&lt;/li&gt;
&lt;li&gt;fluentd 는 &lt;b&gt;통합 로깅 계층&lt;/b&gt;으로, &lt;b&gt;다양한 곳&lt;/b&gt;에서 생산되는 &lt;b&gt;로그를 모으고&lt;/b&gt;, &lt;b&gt;필터링과 가공&lt;/b&gt;을 거쳐 다시 여러 대상으로 수집된 &lt;b&gt;로그를 포워딩 하는 역할&lt;/b&gt;을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ docker container run -d -p 24224:24224 --name fluentd -v &quot;$(pwd)/conf:/fluentd/etc&quot; -e FLUENTD_CONF=stdout.conf diamol/fluentd

$ docker container run -d --log-driver=fluentd --name timecheck5 diamol/ch19-timecheck:5.0

$ docker container logs --tail 1 fluentd
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fluentd 는 수집한 로그에 자체 메타데이터를 추가해서 저장한다.&lt;/li&gt;
&lt;li&gt;메타데이터에는 컨테이너 ID 와 이름 등이 포함된다.&lt;/li&gt;
&lt;li&gt;fluentd 는 수집된 로그의 컨텐스트를 파악하기 위해 이러한 메타데이터가 필요하다.&lt;/li&gt;
&lt;li&gt;수집된 로그는 중앙 데이터 스토어( elastic search ) 로 전송되고, 시각화를 제공하는 Kibana 와 함께 사용하는 것이 일반적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;EFK 스택을 사용할 때 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kibana
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 컨테이너의 로그 중에서 특정 키워드를 포함하는 로그를 검색하거나 시간 등을 기준으로 로그를 필터링 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Elastic Search
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수평 확장이 매우 용이하므로 운영 환경에서 생성되는 대량의 로그를 처리하는데 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Fluentd
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 컨테이너를 모아 한번에 볼 수 있기 때문에 로그를 편하게 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fluentd 로그에 태그 추가&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;services:
	accesslog:
		iamge: diamol/ch18-access-log
		logging:
			dirver: &quot;fluentd&quot;
			options:
				tag: &quot;gallery.access-log.{{.ImageName}}&quot;
		
	iotd:
		image: diamol/ch18-image-of-the-day
		logging:
			dirver: &quot;fluentd&quot;
			options:
				tag: &quot;gallery.iotd.{{.ImageName}}&quot;
				
	image-gallery:
		image: diamol/ch18-image-gallery
		logging:
			dirver: &quot;fluentd&quot;
			options:
				tag: &quot;gallery.image-gallery.{{.ImageName}}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Fluentd 로그에 태그를 추가하기 위해서는 로깅 드라이버에 설정을 추가해야 한단.&lt;/li&gt;
&lt;li&gt;고정된 이름을 사용하거나, 별도의 식별자를 외부에서 주입할 수 있다.&lt;/li&gt;
&lt;li&gt;위와 같이 컴포넌트 이름과 이미지 이름을 연결한 태그의 경우 애플리케이션과 컴포넌트를 한눈에 알 수 있는 좋은 태그 예시이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼, 중앙 집중식 로그 모델에 검색 가능한 데이터 스토어와 데이터 시각화 UI는 운영환경에서 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 로깅 드라이버는 도커 엔진 설정으로 지정하지만, 애플리케이션 매니페스트에서 로깅 시스템을 명시적을 지정하는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅 시스템에 대한 경험이 많지 않다면 Fluentd 를 사용한 EFK 스택을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 환경 확장이 어렵지 않으며, 적용이 간단하고, 메타데이터나 필터 기능을 통해 유형별 분리 및 저장이 가능하기 때문이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;19.4 로그 출력 및 로그 컬렉션 관리하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그는 대량의 불필요한 데이터 저장과 문제 진단에 필요한 정보 확보 사이에서 필요한 정보들만 확인할 수 있어야 된다.&lt;/li&gt;
&lt;li&gt;컨테이너에서 상세한 로그를 생산하면서 로그를 저장할 때는 필터링을 적용하여 균형을 잡을 수 있다.&lt;/li&gt;
&lt;li&gt;로그 필터링은 fluentd 설정 파일에서 정의할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;&amp;lt;match gallery.access-log.**&amp;gt;
	@type copy
	&amp;lt;store&amp;gt;
		@type stdout
	&amp;lt;/store&amp;gt;
&amp;lt;/match&amp;gt;

&amp;lt;match gallery.**&amp;gt;
	@type copy
	&amp;lt;store&amp;gt;
		@type elasticsearch
		...
	&amp;lt;/store&amp;gt;
&amp;lt;/match
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;match 블럭을 이용하여 필터 파라미터와 태그가 일치하는 로그를 어떻게 처리할지 정의할 수 있다.&lt;/li&gt;
&lt;li&gt;위의 설정은 access-log 는 컨테이너의 로그에서 확인하고, 나머지는 elasticsearch 에서 확인할 수 있도록 하는 설정이다.&lt;/li&gt;
&lt;li&gt;이를 통해 각 컴포넌트마다 로그 저장소를 분리해서 저장하거나, 중요한 로그를 따로 추출하는 행위가 가능하다.&lt;/li&gt;
&lt;li&gt;그러나, 로그는 유실될 수 있음을 항상 생각해야 한다.&lt;/li&gt;
&lt;li&gt;설정 내용을 바꾸기 위해 fluentd 만 재배포하는 과정에서 컨테이너가 생성한 로그가 수집되지 못할 경우가 발생하기 떄문에 주의해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;19.5 컨테이너의 로깅 모델&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커의 로깅 모델은 매우 유연성이 뛰어나다.&lt;/li&gt;
&lt;li&gt;이는 애플리케이션 로그를 컨테이너 로그로 보낼때만 적용된다.&lt;/li&gt;
&lt;li&gt;만약, 애플리케이션이 따로 로그를 출력하지 않느나면 별도의 로깅 유틸리티를 통해 stdout 으로 출력해줘야 한다.&lt;/li&gt;
&lt;li&gt;EFK 스택을 사용할 경우 컨테이너 로그를 중앙 집중식 데이터베이스로 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;단일 머신에서도 무리 없이 동작하며, 운영 환경에 맞춰 수평 확장에도 용이하다.&lt;/li&gt;
&lt;li&gt;어떤 조직은 fluentd 같은 도구를 사용하지 않고 스택을 구성하는것을 선호하기도 한다.&lt;/li&gt;
&lt;li&gt;하지만, 이는 로그 유연성을 잃게되고, 애플리케이션이 소스 코드 및 특정 로깅 기술에 크게 의존하게 되므로 추천하지 않는다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/132</guid>
      <comments>https://myeongdev.tistory.com/132#entry132comment</comments>
      <pubDate>Thu, 16 Jan 2025 23:57:06 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 18장 컨테이너의 애플리케이션 설정 관리</title>
      <link>https://myeongdev.tistory.com/131</link>
      <description>&lt;h1&gt;&lt;b&gt;18장 컨테이너의 애플리케이션 설정 관리&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;18.1 다단 애플리케이션 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 데이터 종류는 세 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전에 따라 달리지는 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 환경에서 동일하지만 버전별로 달라지는 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;환경에 따라 달라지는 설정&lt;/li&gt;
&lt;li&gt;기능 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전별로 애플리케이션의 동작을 달리하기 위한 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션에 설정 주입하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버전에 따라 달라지는 설정값은 이미지의 기본 설정에 포함시킨다.&lt;/li&gt;
&lt;li&gt;환경별로 달라지는 설정값은 컨테이너 파일 시스템에 주입되는 오버라이드 파일에 넣는다.&lt;/li&gt;
&lt;li&gt;기능 설정은 환경 변수 형태로 다룬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;$ docker container run -d -p 8080:80 diamol/ch18-access-log

$ docker container run -d -p 8081:80 -v &quot;$(pwd)/config/dev:/app/config-override&quot; diamol/ch18-access-log
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;미리 정의된 경로에서 오버라이드 설정 파일을 읽어 들이도록 해두면, 어떤 경로로든 컨테이너 파일 시스템에 설정 파일을 주입하기만 하면 해당 설정을 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;이런 설정은 개발자의 워크플로를 부드럽게 유지할 수 있는 좋은 사례다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;18.2 환경별 설정 패키징하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 프레임워크에서 환경별 설정 파일을 모두 배포에 포함시킬 수 있는 기능을 제공한다.&lt;/li&gt;
&lt;li&gt;해당 기능을 통해 환경 이름 선택만으로 해당 환경 설정을 적용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;appsettings.json: 모든 환경 공통
appsettings.{ENV}.json: 환경별 오버라이드 설정
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ docker container run -d -p 8083:80 diamol/ch18-todo-list

$ docker container run -d -p 8084:80 -e DOTNET_ENVIRONMENT=test diamol/ch18-todo-list
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만약, 설정 파일과 소스 코드를 별돌의 시스템으로 관리한다면&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CI/CD 파이프라인에서 설정 파일을 소스 코드로 가져와 이미지를 빌드하는 방법으로 개발과 설정 관리를 분리할 수 있다.&lt;/li&gt;
&lt;li&gt;그러나, 이미지에 포함시킬 수 없는 민감정보 때문에 외부에서 컨테이너에 주입해야 되는 데이터가 존재한다.&lt;/li&gt;
&lt;li&gt;레지스트리는 항상 외부에 노출될 위험이 있다고 생각하고 보안에 신경써야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;18.3 런타임에서 설정 읽어 들이기&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Go 언어의 Viper&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Go 언어에서는 Viper 라는 설정 모듈이 주로 사용된다.&lt;/li&gt;
&lt;li&gt;패키지 목록에 바이퍼를 추가하고, 코드에서 오버라이드 파일을 읽어 올 설정 디렉토리를 지정하기만 하면 된다.&lt;/li&gt;
&lt;li&gt;이미지에 포함된 config 디렉터리에서 기본 설정 파일을 읽고, 환경별 설정 파일은 config-override 디렉터리에서 읽어 들인다.&lt;/li&gt;
&lt;li&gt;해당 config-override 는 파일 시스템 마운트로 외부에서 주입된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TOML&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Viper 에서는 TOML 이라는 형식을 사용한다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구조가 간결하기 때문에 개발자가 설정을 이해하고 수정하기 쉽다.&lt;/li&gt;
&lt;li&gt;명확한 데이터 타입 시스템을 제공한다.&lt;/li&gt;
&lt;li&gt;계층형 구조로 복잡한 데이터를 표시하기 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;release = &quot;19.12&quot;
environment = &quot;UNKNOWN&quot;

[metrics]
enabled = true

[apis]

[api.image]
url = &quot;&amp;lt;http://iotd/inmage&amp;gt;&quot;

[api.access]
url = &quot;&amp;lt;http://accesslog/access-log&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정값을 반환하는 API 설계 시 주의사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 설정을 공개하지 않는다.&lt;/li&gt;
&lt;li&gt;민감한 정보는 절대 포함시키지 않는다.&lt;/li&gt;
&lt;li&gt;허가받은 사용자만이 접근할 수 있도록 엔드포인트 보안을 설정한다.&lt;/li&gt;
&lt;li&gt;설정 API의 사용 여부를 설정할 수 있도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;궁긍한점.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 API 는 단 한번도 만들어본적이 없다. 언제 어떤 이유로 만들어서 사용하는거지?&lt;/li&gt;
&lt;li&gt;애플리케이션 버전 ( 0.5.2 와 같은) 정보도 설정 API 인가?&lt;/li&gt;
&lt;li&gt;그렇다면, 해당 버전 정보는 사용자에게 공개되면 안되는지?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;18.4 레거시 애플리케이션에 설정 전략 적용하기&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레거시 애플리케이션은 환경 변수나 설정 파일 병합을 통한 설정 구성은 일반적으로 지원하지 않는다.&lt;/li&gt;
&lt;li&gt;레거시 애플리케이션은 특정 경로에 위치한 XML 파일을 설정 파일을 주로 사용한다.&lt;/li&gt;
&lt;li&gt;루트 디렉토리 외부에 위치한 설정 파일이나 환경 변수의 값을 설정에 도입할수는 없다.&lt;/li&gt;
&lt;li&gt;이를 컨테이너에 주입된 설정 파일을 애플리케이션의 설정 전력에 맞춰 변환하는 유티리티 또는 스크립트를 이미지에 포함시켜 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 컨테이너에 지정된 오버라이드 파일을 읽어 들이기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B. 환경 변수에서 오버라이드 설정을 읽어 들이기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C. 오버라이드 설정 파일과 환경 변수 설정을 병합하기. 이떄, 환경 변수 갓이 우선.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D. 병합된 오버라이드 설정을 컨테이너 내 대상 설정 파일에 추가.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ docker container run -d -p 8090:80 -v &quot;$(pwd)/config/dev:/config-override&quot; -e CONFIG_SOURCE_PATH=&quot;/config-override/application.properties&quot; diamol/ch18-image-of-the-day
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java 설정 변환 유틸리티의 빌드 및 실행&lt;/h3&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM diamol/maven AS builder
RUN mvn package

FROM diamol/maven as utility-builder
WORKDIR /usr/src/utilities
COPY ./src/utilities/ConfigLoader.java .
RUN javac ConfigLoader.java

FROM diamol/openjdk
ENV CONFIG_SOURCE_PATH=&quot;&quot; \\
	CONFIG_TARGET_PATH=&quot;7app/config/application.properties&quot;

CMD java ConfigLoader &amp;amp;&amp;amp; \\
	java -jar /app/iotd-service-0.1.0.jar

WORKDIR /app
COPY --from=utility-builder /usr/src/utilities/ConfigLoader.java
COPY -from=builder /usr/src/iotd/target/iotd-service-0.1.0.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 이미지를 확장해 기존 애플리케이션에 현대적 설정 모델을 도입할 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션 시작 로직을 수정해 실제 애플리케이션이 실행되기 전에 원하는 작업을 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;다만, 해당 작업으로 인해 컨테이너 시작과 애플리케이션 실행 사이에 시간 간격이 생기며, 컨테이너가 실패할 확률도 높아진다.&lt;/li&gt;
&lt;li&gt;따라서, 더욱 헬스 체크에 신경을 써야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;18.5 유연한 설정 모델의 이점&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계층별 설정 모델은 애플리케이션 동작 환경마다 조금씩 다르게 하면서도 단일 이미지 원칙을 유지할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;버전에 따라 달라지는 설정은 이미지 포함.&lt;/li&gt;
&lt;li&gt;환경별로 달라지는 설정은 컨테이너 플랫폼에서 제공하는 오버라이드 파일을 통해 적용.&lt;/li&gt;
&lt;li&gt;환경 변수를 통해 통제하는 기능별 설정 추가.&lt;/li&gt;
&lt;li&gt;이를 통해 운영 환경과 유사항 환경 재현이 가능하여 운영 환경 이슈 대응이 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/131</guid>
      <comments>https://myeongdev.tistory.com/131#entry131comment</comments>
      <pubDate>Fri, 3 Jan 2025 23:51:42 +0900</pubDate>
    </item>
    <item>
      <title>도커 교과서 17장 도커 이미지 최적화하기: 보안, 용량, 속도</title>
      <link>https://myeongdev.tistory.com/130</link>
      <description>&lt;h1&gt;&lt;b&gt;17장 도커 이미지 최적화하기: 보안, 용량, 속도&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;17.1 도커 이미지를 최적화하는 방법&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지가 최적화되어야 이미지 빌드와 배포가 빨라지고, 애플리케이션의 보안이 지켜진다.&lt;/li&gt;
&lt;li&gt;애플리케이션을 업데이트하기 위해 컨테이너를 교체하면 새로운 이미지를 내려받지만, 기존 이미지도 삭제되지 않고 남는다.&lt;/li&gt;
&lt;li&gt;이 경우가 지속되면 디스크 용량이 순식간에 부족해지는 경우가 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ docker system df
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주기적으로 docker system prune 명령어를 사용하여, 사용하지 않는 이미지 레이어나 빌드 캐시를 비워주는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;꼭 필요한 파일만 이미지에 포함시키기&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;꼭 필요한 파일만 이미지에 포함시키는 것이 디스크 용량 절약의 첫걸음 이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;$ vi Dockerfile.v1

FROM diamol/base
CMD echo app- &amp;amp;&amp;amp; Is app &amp;amp;&amp;amp; echo docs- &amp;amp;&amp;amp; Is docs
COPY . .

$ vi Dockerfile.v2

FROM diamol/base
CMD echo app= &amp;amp;&amp;amp; Is app &amp;amp;&amp;amp; echo docs- &amp;amp;&amp;amp; Is docs
COPY . .
RUN rm -rf docs
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;v1 Dockerfile , v2 Dockerfile 의 용량은 같다.&lt;/li&gt;
&lt;li&gt;Dockerfile 스크립트의 인스트럭션 하나마다 이미지 레이어가 하나씩 생성된다.&lt;/li&gt;
&lt;li&gt;이때, 한 번 &lt;b&gt;이미지에 복사한 파일&lt;/b&gt;은 &lt;b&gt;이미지에서 뺄 수 없다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그 다음 레이어에서 &lt;b&gt;파일을 지우더라도&lt;/b&gt; &lt;b&gt;파일 시스템에서 숨겨&lt;/b&gt;질 뿐 &lt;b&gt;실제로 파일이 삭제되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그렇기 때문에, 각 이미지 레이어를 최적화 해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;$ vi Dockerfile.v3

FROM diamol/base
CMD echo app- &amp;amp;&amp;amp; Is app &amp;amp;&amp;amp; echo docs- &amp;amp;&amp;amp; Is docs
COPY ./app ./app
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 처럼 필요한 파일만을 이미지에 복사함으로써, 이미지 용량을 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;$ mv rename.dockerignore .dockerignore

$ docker imge build -t diamol/ch17-build-context:v3 -f ./Dockerfile.v3 .
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커의 빌드 과정은 엔진에 빌드 컨텍스트(빌드를 실행한 디렉토리) 를 압축하고, Dockerfile 스크립트를 함께 보내면서 시작된다.&lt;/li&gt;
&lt;li&gt;이 빌드 컨텍스트에는 불필요한 파일이 포함된 경우가 많다.&lt;/li&gt;
&lt;li&gt;.dockerignore 파일을 이용해 불필요한 파일을 제외할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;17.2 좋은 기반 이미지를 고르는 법&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기반 이미의 크기는 디스크 용량이나 네트워크 전송 시간뿐만 아니라 애플리케이션 보안과도 관계가 깊다.&lt;/li&gt;
&lt;li&gt;작은 기반 이미미지로 동작하지 않는 애플리케이션도 있겠지만, 여러번 테스트하며 적합한 이미지를 찾아야 한다.&lt;/li&gt;
&lt;li&gt;자바 애플리케이션은 OpenJDK 공식 이미지를 기반 이미지로 많이 사용한다.&lt;/li&gt;
&lt;li&gt;설정값에 따라 다양한 태그가 부여된 자바 런타임(JRE), 개발자 키드 (JDK) 이미지가 있다.&lt;/li&gt;
&lt;li&gt;크기가 가장 큰 OpenJDK 이미지는 자바 SDK 전체가 들어 있는데, 이는 컨테이너 침입자의 공격 수단으로 활용될 수 있다.&lt;/li&gt;
&lt;li&gt;리눅스 컨테이너에는 알파인 리눅스, 데비안 슬림 이미지를 추천하고, 윈도우 컨테이너는 나노 서버를 추천한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앤코어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앤코어는 서드파티 도구로, 골든 이미지에 삽입해 빌드 중에 검사를 할 수 있다.&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인에 통합해서 사용하는 방식으로 사용한다.&lt;/li&gt;
&lt;li&gt;앤코어는 이미지를 분석하여, 오픈 소스 라이센스, 운영체제, 이미지에 포함된 바이너리 파일의 보안 문제까지 파악을 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;$ docker compose -f docker-compose.yml up -d

$ docker container exec anchore_engine-api_1 anchore-cli image add diamol/openjdk --dockerfile /Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;17.3 이미지 레이어 수와 이미지 크기는 최소한으로&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소프트웨어를 설치하면 패키지 목록을 캐싱하거나 추천 패키지 등을 함께 설치하기 때문에 대부분의 경우 불필요한 요소나 설치 후 잔재가 발생한다.&lt;/li&gt;
&lt;li&gt;이런 요소까지 확실하게 통제할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# apt-get install socat
$ docker image build -t diamol/ch17-socat:v1 .

# apt-get install socat --no-install-recommends
$ docker image build -t diamol/ch17-socat:v2 -f Dockerfile.v2 .

$ docker image Is -f reference=diamol/ch17-socat
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ vi Dockerfile

RUN apt-get install -y curl=7.52.1-5+deb9u16
RUN apt-get install -y socat=1.7.3.1-2+deb9u1

$ vi Dockerfile.v2

RUN apt-get update \\
 &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \\
    curl=7.52.1-5+deb9u16 \\
    socat=1.7.3.1-2+deb9u1 \\
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 두개의 차이는 단순 apt-get 명령어를 통해 socat 명령을 설치한 것과 --no-install-recommends 옵션을 사용한 것이다.&lt;/li&gt;
&lt;li&gt;위 두개의 이미지 용량은 약 20MB 가 차이가 난다.&lt;/li&gt;
&lt;li&gt;또한, RUN 인스트럭션을 하나로 합침으로써 아래와 같은 장점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대 이미지 레이어수는 정해져 있기때문에 여분의 레이어를 남겨 두는 것에도 의미가 있다.&lt;/li&gt;
&lt;li&gt;이미지 레이어 수가 적으면 컨테이너 파일 시스템의 내용을 추적하기 훨씬 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 압축 파일을 받은 후 해제, 그 이후 불필요한 파일 제거
$ vi Dockerfile

FROM diamol/base

ARG DATASET_URL=https://archive.ics.uci.edu/ml/machine-learning-databases/url/url_svmlight.tar.gz

WORKDIR /dataset

RUN wget -O dataset.tar.gz ${DATASET_URL} &amp;amp;&amp;amp; \\
    tar xvzf dataset.tar.gz

WORKDIR /dataset/url_svmlight
RUN cp Day1.svm Day1.bak &amp;amp;&amp;amp; \\
    rm -f *.svm &amp;amp;&amp;amp; \\
    mv Day1.bak Day1.svm

# 필요한 파일만 압축 해제
$ vi Dockerfile.v2

FROM diamol/base

ARG DATASET_URL=https://archive.ics.uci.edu/ml/machine-learning-databases/url/url_svmlight.tar.gz

WORKDIR /dataset

RUN wget -O dataset.tar.gz ${DATASET_URL} &amp;amp;&amp;amp; \\
    tar -xf dataset.tar.gz url_svmlight/Day1.svm &amp;amp;&amp;amp; \\
    rm -f dataset.tar.gz                                  
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 &lt;b&gt;필요한&lt;/b&gt; &lt;b&gt;파일만 압축 해제&lt;/b&gt; 과정을 &lt;b&gt;하나의 인스트럭션에 진행할&lt;/b&gt; 경우 약 2.4GB 의 용량이 차이난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 &lt;b&gt;이미지 빌드 과정&lt;/b&gt;에서 가장 신경 써야 할 것은 &lt;b&gt;애플리케이션에 필요한 것&lt;/b&gt;만 &lt;b&gt;이미지에 담아서 사용&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;17.4 멀티 스테이지 빌드를 한 단계 업그레이드하기&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;$ vi Dockerfile.v3

FROM diamol/base AS download
ARG DATASET_URL=https://archive.ics.uci.edu/ml/machine-learning-databases/url/url_svmlight.tar.gz
RUN wget -O dataset.tar.gz ${DATASET_URL}

FROM diamol/base AS expand
COPY --from=download dataset.tar.gz .
RUN tar xvzf dataset.tar.gz

FROM diamol/base
WORKDIR /dataset/url_svmlight
COPY --from=expand url_svmlight/Day1.svm . 
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스크립트 가도성과 이미지 최적화를 모두 고려한 Dockerfile 스크립트 이다.&lt;/li&gt;
&lt;li&gt;스테이지별로 어떤 단계를 밟는 중인지 이해하기 쉬우면서, 인스트럭션을 줄이느라 복잡하게 명령어를 합칠 필요가 없다.&lt;/li&gt;
&lt;li&gt;최종적으로 빌드되는 이미지에는 앞선 단계에서 명시적으로 복사해 온 파일만이 포함된다.&lt;/li&gt;
&lt;li&gt;원하는 지점까지만 이미지를 빌드할 수 있으므로 디버깅 편의성을 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# 이미지 전체 빌드
$ docker imiage build -t diamol/ch17-ml-dataset:v3 -f Dockerfile.v3 .

# download 스테이지까지 빌드
$ docker image build -t diamol/ch17-ml-dataset:v3-download -f Dockerfile.v3 --target download .

# expand 스테이지까지 빌드
$ docker image build -t diamol/ch17-ml-dataset:v3-expand -f Dockerfile.v3 --target expand .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;17.5 최적화가 중요한 이유&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 최적화 방법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기반 이미지를 잘 고르는 것이 중요하다.&lt;/li&gt;
&lt;li&gt;자신만의 골든 이미지를 갖출 수 있다면 매우 이상적이다.&lt;/li&gt;
&lt;li&gt;아주 간단한 애플리케이션 이나리면, 멀티 스테이지 빌드를 적용하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;불필요한 패키지나 파일을 포함하지 않음으로써, 레이어 크기를 최소한으로 유지한다.&lt;/li&gt;
&lt;li&gt;Dockerfile 스크립트는 자주 수정이 일어나는 순서대로 뒤에 오도록 배치하여 캐시를 최대한 활용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 최적화 효과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개발자는 최적화를 통해 시간과 디스크 공간을 더 많이 확복할 수 있다.&lt;/li&gt;
&lt;li&gt;최적화를 통해 빌드 시간, 검수, 푸시에 걸리는 시간을 절약에 CI/CD 파이프라인의 실행 횟수를 늘릴 수 있다.&lt;/li&gt;
&lt;li&gt;최적화된 이미지를 통해 컨테이너 실행 시간을 절약할 수 있다.&lt;/li&gt;
&lt;li&gt;이미지 크기를 줄임으로써, 불필요한 패키지나 파일을 통한 보안 문제를 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>  책/도커 교과서</category>
      <author>MyeongDev</author>
      <guid isPermaLink="true">https://myeongdev.tistory.com/130</guid>
      <comments>https://myeongdev.tistory.com/130#entry130comment</comments>
      <pubDate>Wed, 1 Jan 2025 21:44:16 +0900</pubDate>
    </item>
  </channel>
</rss>