<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[직방 기술 블로그 - Medium]]></title>
        <description><![CDATA[사람들이 공간을 경험하는 방식을 바꾸고자 합니다. - Medium]]></description>
        <link>https://medium.com/zigbang?source=rss----2f055286701b---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>직방 기술 블로그 - Medium</title>
            <link>https://medium.com/zigbang?source=rss----2f055286701b---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 11 Jun 2026 01:10:38 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/zigbang" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Bit(Binary digit) Byte 의 고찰]]></title>
            <link>https://medium.com/zigbang/bit-binary-digit-byte-%EC%9D%98-%EA%B3%A0%EC%B0%B0-ebefe5afb8d3?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/ebefe5afb8d3</guid>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[dba]]></category>
            <dc:creator><![CDATA[Chase]]></dc:creator>
            <pubDate>Mon, 18 Dec 2023 02:54:52 GMT</pubDate>
            <atom:updated>2023-12-18T02:54:52.390Z</atom:updated>
            <content:encoded><![CDATA[<p>최근 우리가 다루는 용량이 매우 커져서 작은 용량은 무시해도 될 수준이 되었지만, 우리가 누군가.? 우리는 데이터를 다루는 사람이고, 작은 용량의 데이터도 무시할수 없는 전문가다.</p><p>그렇다면, 최근 우리의 기억에서 잊혀진 데이터 용량에 대해 다시 한번 고찰해 보고자 한다.</p><p>사전적 정의부터 알아보자!</p><blockquote><strong><em>비트</em></strong><em>(</em><strong><em>Bit</em></strong><em>, Binary digit) 는 하나의 비트는 0 이나 1 의 값을 가질수 있고, 각각은 참, 거짓 혹은 서로 배타적인 상태를 나타낸다.</em></blockquote><blockquote><strong><em>바이트</em></strong><em>(</em><strong><em>Byte</em></strong><em>) 는 일정량의 비트 모음을 정의할때 사용하며, 컴퓨터의 기억장치의 크기를 나타내는 단위로 자주 쓰인다. 실질적인 의미는 ASCII 문자 하나를 나타내는 단위로 최근에는 사실상 1바이트를 8비트로 간주하는 경우가 흔하다.</em></blockquote><ul><li><a href="https://ko.wikipedia.org/wiki/%EC%98%A5%ED%85%9F_(%EC%BB%B4%ED%93%A8%ED%8C%85)"><strong>옥텟</strong></a>(octet): 한데 묶여 쓰이는 2진 이진부호모음. 바이트가 8비트를 가리킬 때 서로 같은 뜻을 갖는다. 전기통신 분야에서는 바이트란 용어 대신 옥텟이란 표현을 사용한다. <strong>팔중수</strong>라고도 부른다.</li><li><a href="https://ko.wikipedia.org/wiki/%EB%8B%88%EB%B8%94"><strong>니블</strong></a>(nibble) = 1바이트의 절반으로 보통 4비트를 가리킨다. 이때 바이트 하나는 상위 니블(상위 4비트)과 하위 니블(하위 4비트)로 나눌 수 있다.</li><li><a href="https://ko.wikipedia.org/wiki/%EC%9B%8C%EB%93%9C_(%EC%BB%B4%ED%93%A8%ED%8C%85)"><strong>워드</strong></a>(word): 전자통신 기기에 따라 2바이트 또는 4바이트를 묶어서 부르는 단위이다.</li><li><a href="https://ko.wikipedia.org/wiki/%ED%82%AC%EB%A1%9C%EB%B0%94%EC%9D%B4%ED%8A%B8"><strong>킬로바이트</strong></a>(kbyte): 1024 바이트는 1 킬로바이트이다.</li></ul><p>해묵은 이슈이긴 한데, 시간이 지날수록 컴퓨터 저변 확대와 데이터 용량 단위 증가로 인한 이슈가 발생하게 되었다.</p><p>앞서 정의된 바와 같이 용량 표현의 최소 단위인 바이트(byte)는 비트(bit)를 기반으로 한 이진법을 사용하고 있기 때문에 1KB 는 1,024 Byte 이다. <br>하지만, 우리가 표준으로 사용하는 국제단위계(SI)는 모두 10진법을 기초로 하고, 이를 기반으로 제조사는 표기를 하기 시작하며 문제가 발생하기 시작했다.</p><p>1,024 Byte 나 1,000 Byte 나 큰 차이가 없어, 같은 크기로 받아들여졌지만, 용량이 커질 경우 그 차이는 매우 커지게 된다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/651/1*jx0Hg4ZG9XA6Aw6A0JuDhw.png" /><figcaption>십진법과 이진법 표기시의 용량 차이 (출처 : <a href="https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8">https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8</a><a href="https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A7%84_%EC%A0%91%EB%91%90%EC%96%B4)">)</a></figcaption></figure><p>이러한 문제를 해결하기 위하여 1998년 IEC(International Electrotechnical Commission)에서 아래와 같은 새로운 표기법을 만들었다.</p><p>이제 KB 는 1,000 Byte 이고 새롭게 추가된 표기법인 KiB 는 1,024 Byte 이다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/481/1*GVVH55Sgm4z3K4zIIqI5Mg.png" /><figcaption>십진법과 이진법 표기시의 용량 차이 (출처 : <a href="https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8">https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8</a><a href="https://ko.wikipedia.org/wiki/%EC%9D%B4%EC%A7%84_%EC%A0%91%EB%91%90%EC%96%B4)">)</a></figcaption></figure><p>지금 이 이슈를 다시 고찰할 필요성을 느낀 이유는 AWS 단위 표기의 구분을 명확히 하고 있기 때문이다.<br>아래와 같이 표기 단위에 따라 다르게 구분하여 사용해야 한다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/795/1*uSGjCrxnC3_orKjArVDQ2g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/831/1*qgS89BpuBce0W_vwAN8QRQ.png" /></figure><ul><li>참조링크<br><a href="https://ko.wikipedia.org/wiki/%EB%B9%84%ED%8A%B8_(%EB%8B%A8%EC%9C%84)">https://ko.wikipedia.org/wiki/비트_(단위)</a> <br><a href="https://ko.wikipedia.org/wiki/%EB%B0%94%EC%9D%B4%ED%8A%B8">https://ko.wikipedia.org/wiki/바이트 </a><br><a href="https://ko.wikipedia.org/wiki/SI_%EC%A0%91%EB%91%90%EC%96%B4">https://ko.wikipedia.org/wiki/SI_접두어 </a><br><a href="https://ko.wikipedia.org/wiki/%EA%B5%AD%EC%A0%9C%EB%8B%A8%EC%9C%84%EA%B3%84">https://ko.wikipedia.org/wiki/국제단위계</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ebefe5afb8d3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/bit-binary-digit-byte-%EC%9D%98-%EA%B3%A0%EC%B0%B0-ebefe5afb8d3">Bit(Binary digit) Byte 의 고찰</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TypeORM QueryBuilder 활용 사례: 재사용성을 높이는 방법과 테스트 작성하기]]></title>
            <link>https://medium.com/zigbang/typeorm-querybuilder-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80-%EC%9E%AC%EC%82%AC%EC%9A%A9%EC%84%B1%EC%9D%84-%EB%86%92%EC%9D%B4%EB%8A%94-%EB%B0%A9%EB%B2%95%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-c604f1913920?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/c604f1913920</guid>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Many(김희만) Kim]]></dc:creator>
            <pubDate>Mon, 18 Dec 2023 02:54:19 GMT</pubDate>
            <atom:updated>2023-12-18T02:54:19.638Z</atom:updated>
            <content:encoded><![CDATA[<p>안녕하세요, 백엔드 하우스팀에서 근무하고 있는 Many입니다.</p><p>저희 팀에서는 TypeORM을 활용하여 쿼리 조회를 수행하고 있습니다. 이 글에서는 특히, TypeORM의 QueryBuilder를 활용하여 쿼리를 더 재사용 가능하고 테스트 작성이 용이한 형태로 리팩토링한 내용을 소개하려 합니다.</p><h3>왜 QueryBuilder의 재사용성을 높여야 하는가?</h3><p>ORM을 통한 데이터 조회는 대체로 편리하고 가독성이 높은 코드를 작성할 수 있어 개발자들 사이에서 선호되는 방식입니다. 객체를 통한 조회는 테스트 코드를 작성하고 검증하기가 비교적 수월하며, 쿼리 조건을 명시적으로 나열하여 가독성을 높일 수 있어 코드로서의 명료성을 지킬 수 있습니다.</p><p>그러나 ORM이 제공하지 않는 특별한 기능이나 일부 동작을 원할 때, QueryBuilder를 통한 데이터 조회를 고려해야 하는 상황이 발생합니다. 이때 일부 쿼리문이 코드로 들어오면서 QueryBuilder를 사용하는 코드는 가독성을 낮추고, 테스트 작성이 어려워지며 코드의 재사용성이 떨어질 수 있습니다.</p><h3>개선 사례: 매물 조회를 위한 QueryBuilder 활용</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/357/1*z8eZhNFHdMyeEDqXGlT6BA.png" /><figcaption>직방의 핵심인 매물은 여러 유형의 필터를 제공합니다.</figcaption></figure><p>직방에서는 사용자들이 전국 각지에 등록된 다양한 매물을 지도와 필터를 통해 쉽게 찾을 수 있는 매물 찾기 기능을 제공하고 있습니다. 사용자들은 매물 검색 시 다양한 정보와 조건을 입력할 수 있어, 이에 대응하는 복잡한 코드 로직이 매물 정보 조회 부분에서 수행되고 있습니다.</p><p>이에 기존 코드의 복잡한 부분을 알고있기에 이번에 진행한 지킴중개를 위한 별도의 페이지에서는 조금 더 유지보수가 용이한 형태로 진행한 QueryBuilder 사용 사례를 소개할까합니다.</p><blockquote>QueryBuilder 조건 적용을 위한 모델과 클래스 작성</blockquote><p>매물 조회를 위한 매물 조회 옵션 클래스를 정의해줍니다.</p><p>필터 기능의 경우 매번 요구되는 옵션이 아니기에 모두 optional 처리를 해줍니다.</p><pre>export class ItemFindOptions {<br> public floorTypes?: Array&lt;&#39;지상&#39; | &#39;반지하&#39; | &#39;옥탑&#39;&gt;<br> public depositMax?: number<br> public depositMin?: number<br> public type?: &#39;전세&#39; | &#39;월세&#39; | &#39;매매&#39;<br> public rentMin?: number<br> public rentMax?: number<br> public bjdCodes?: string[]<br>}</pre><p>필터 옵션에 입력된 값을 이용해서 QueryBuilder에 쿼리를 적용시켜주는 ItemFindQueryBuilder 클래스를 작성합니다.</p><pre>export class ItemFindQueryBuilder {<br> private alias = &#39;item&#39;<br><br> constructor(private qb: SelectQueryBuilder&lt;Item&gt;, private options: ItemFindOptions) {<br>  this.alias = this.qb.alias<br> }<br><br> public build(): SelectQueryBuilder&lt;Item&gt; {<br>   this.qb.andWhere(<br>    new Brackets((innerQb) =&gt; {<br>     this.setFloorTypes(innerQb, options)<br>         .setDepositMax(innerQb, options)<br>         .setDepositMin(innerQb, options)<br>    }),<br>   )<br><br>  return this.qb<br> }<br><br> private setFloorTypes(qb: WhereExpressionBuilder, options: ItemFindOptions) {<br>  if (options.floorTypes &amp;&amp; options.floorTypes.length &gt; 0) {<br>   qb.andWhere(<br>    new Brackets((innerQb) =&gt; {<br>     options.floorTypes?.forEach((floorType) =&gt; {<br>      switch (floorType) {<br>       case &#39;지상&#39;:<br>        innerQb.orWhere(`${this.alias}.floorType = &#39;지상&#39;`)<br>        break<br>       case &#39;반지하&#39;:<br>        innerQb.orWhere(`${this.alias}.floorType = &#39;반지하&#39;`)<br>        break<br>       case &#39;옥탑&#39;:<br>        innerQb.orWhere(`${this.alias}.floorType = &#39;옥탑&#39;`)<br>        break<br>      }<br>     })<br>    }),<br>   )<br>  }<br><br>  return this<br> }<br><br> private setDepositMin(qb: WhereExpressionBuilder, options: ItemFindOptions) {<br>  if (options.depositMin) {<br>   qb.andWhere(`${this.alias}.deposit &gt;= :depositMin`, { depositMin: options.depositMin })<br>  }<br><br>  return this<br> }<br><br> private setDepositMax(qb: WhereExpressionBuilder, options: ItemFindOptions) {<br>  if (options.depositMax) {<br>   qb.andWhere(`${this.alias}.deposit &lt;= :depositMax`, { depositMax: options.depositMax })<br>  }<br><br>  return this<br> }<br>}</pre><p>Repository에 매물 조회 옵션을 이용해서 조회할 수 있는 메소드를 작성합니다.</p><pre> public async findByOptions(options: ItemFindOptions, offset = 0, limit = 20) {<br>  const qb = await this.createQueryBuilder(&#39;item&#39;)<br>  const itemFindBuilder = new ItemFindQueryBuilder(qb, options).build()<br>  itemFindBuilder.skip(offset).take(limit)<br><br>  return itemFindBuilder.getMany()<br> }</pre><p>블로그 작성을 위해서 일부 필터만 구현했지만 실제 관리자 페이지에서 사용할 수 있는 필터의 수는 19개입니다. 필터 항목이 총 19개이며, 일부 필터를 여러 화면에서 사용한다면 각각의 조회를 위한 메소드를 구현한다면 findByOOO, findByOOOAndOOO, findByOOOAndOOOAndOOO와 같은 무수히 많은 메소드가 생성될 수 있습니다.</p><h3>실제 사용 후 느낀 편리함</h3><p>실제 수많은 코드 리뷰 과정을 통해 팀원들과 유사한 형태의 QueryBuilder 재사용을 위한 코드가 구성되었습니다. 해당 코드를 사용하면서 몇 달간의 서비스 운영, 신규 개발, 유지보수를 진행하며 겪은 장점은 다음과 같습니다.</p><blockquote>데이터를 몰라도 필터 사용에 문제가 없다.</blockquote><p>필터를 구현하기 위해서는 현재 데이터가 어떻게 구성되어 있고, 어떤 조건들을 사용해야 하는지 등 기존 데이터에 대한 업무 지식이 필요합니다. 하지만 구성한 빌더와 연동하는 ItemFindOptions 객체를 통해서 실제 구현부와 데이터를 알지 못하더라도 현재 어떤 조회가 구현되어 있는지 빠르게 확인할 수 있습니다. 또한, 구현된 필터는 언제라도, 누구라도 바로 사용할 수 있습니다.</p><blockquote>이곳저곳 매물 조회를 위한 만능 메소드</blockquote><p><strong>테스트 코드 열심히 작성했는데 잘못 조회한 케이스 없으신가요?</strong></p><p>가끔 테스트 코드까지 열심히 작성했는데 실제 구동 시 일부 조건에서 의도치 않게 조회되는 케이스를 가끔씩 겪게 됩니다. 특히나 조건이 복잡해지는 경우 AND, OR 조건의 괄호에 의해서 많이 발생합니다. 이미 많은 코드에서 검증되었고 또한 테스트 코드를 통한 빌더에 대한 검증이 이뤄졌기 때문에 많은 코드들에서 걱정 없이 해당 메소드를 통해 손쉽게 매물에 대한 조회를 수행합니다.</p><blockquote>ORM 만큼이나 수월한 테스트 코드 작성</blockquote><p>ORM과 QueryBuilder를 통한 조회 중 가장 큰 차이점이자, 저는 ORM을 통한 조회를 더 선호하는 이유는 테스트 코드 작성의 난이도가 낮아진다는 점입니다.</p><p>ORM은 객체 지향적인 접근 방식으로 데이터를 다루기 때문에 테스트 코드 작성이 자연스럽고 직관적입니다. 객체와 데이터베이스 간의 매핑이 이미 정의되어 있기 때문에 별도의 쿼리 빌딩 로직을 작성하지 않아도 됩니다. 따라서, ORM을 사용하면 쿼리 빌딩과 관련된 테스트 코드를 작성할 필요가 줄어들어 전반적인 테스트 작성이 간편해집니다.</p><p>반면에 QueryBuilder는 쿼리를 직접 작성해야 하기 때문에 쿼리 로직의 테스트 코드 작성이 필수적입니다. 특히 복잡한 쿼리에서는 AND, OR 조건 및 괄호 처리 등이 추가로 필요하므로 테스트 코드 작성이 더욱 번거로울 수 있습니다.</p><p>다음은 실제 검증하는 테스트 코드 일부입니다.</p><pre> test(&#39;[depositMin] 최소보증금이 주어지면 최소보증금 조건을 추가한다.&#39;, () =&gt; {<br>  const itemFindBuilder = new ItemFindQueryBuilder(mockQb, {<br>    depositMin: 5000,<br>  }).build()<br><br>  expect(mockQb.andWhere).toHaveBeenCalledWith(<br>   expect.stringContaining(`deposit &gt;= :depositMin`),<br>   expect.objectContaining({ depositMin: 5000 }),<br>  )<br> })</pre><p>위의 내용처럼 제공하는 각 필터 항목에 해당하는 쿼리들이 잘 추가되었는지 유닛테스트를 통해서 검증하고 있습니다.</p><p>이미 제공하는 필터의 모든 기능을 해당 빌더에서 테스트하기에 각 서비스 로직에서는 조건에 맞는 ItemFindOptions 객체가 잘 생성되었는지만 확인하면 되기에 기존 결합된 코드에 비해서 테스트 코드 작성의 난이도가 낮아졌습니다.</p><h3>결론</h3><p>QueryBuilder를 사용해야하는 케이스에서 재사용성을 높이고 테스트 코드 작성의 편의성을 높이기 위해 다양한 빌더 클래스를 도입하였습니다. 이러한 빌더 패턴을 적용함으로써 새로운 필터의 추가나 변경이 쉬워지고, 유지보수도 간편해집니다. 또한, 각 빌더 클래스에 대한 테스트 코드 작성으로 코드 품질을 높이는데 기여하였습니다.</p><h4>마지막으로</h4><p>지금까지의 경험을 토대로 우리는 코드의 유연성과 확장성을 높이는 방법에 대해 고민해보았습니다. 적절한 도구와 디자인 패턴의 적용은 개발 생산성을 향상시키며, 팀 전체의 코드 품질을 향상시키는데 중요한 역할을 합니다. 저희 팀의 끊임없는 학습과 개선을 통해 더 나은 코드와 효율적인 테스트 방법을 찾아 나가는 여정은 계속될 것입니다. 함께 성장하며 더 나은 소프트웨어를 만들어 나가기를 기대합니다.</p><p>감사합니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c604f1913920" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/typeorm-querybuilder-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80-%EC%9E%AC%EC%82%AC%EC%9A%A9%EC%84%B1%EC%9D%84-%EB%86%92%EC%9D%B4%EB%8A%94-%EB%B0%A9%EB%B2%95%EA%B3%BC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0-c604f1913920">TypeORM QueryBuilder 활용 사례: 재사용성을 높이는 방법과 테스트 작성하기</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MYSQL 인덱스 튜닝]]></title>
            <link>https://medium.com/zigbang/mysql-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%ED%8A%9C%EB%8B%9D-18e183e9246d?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/18e183e9246d</guid>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Paul An]]></dc:creator>
            <pubDate>Mon, 18 Dec 2023 02:53:58 GMT</pubDate>
            <atom:updated>2023-12-18T02:53:58.331Z</atom:updated>
            <content:encoded><![CDATA[<p>안녕하세요. 호갱노노 BE팀 폴 입니다.</p><p>백엔드 개발 업무를 하다보면 데이터베이스에 쿼리하는 일이 잦은데요. 그러다보면 종종 성능을 고려한 SQL을 작성해야하는 고민에 마주하게 됩니다.</p><p>필요한 데이터를 어떻게 테이블에서 조회하고 갱신할지에 대해 최적의 방법을 생각하게 되는데요.</p><p>이번 글에서는 인덱스 성능이 안 나오는 몇몇 사례를 중심으로 SQL의 비효율을 제거하는 과정을 (특히 인덱스 중심으로) 정리해 보도록 하겠습니다.</p><h3>TOC</h3><ol><li>SQL 튜닝 개요</li><li>인덱스 튜닝 세부 분류</li><li>인덱스가 있는데도 인덱스를 안타는 케이스</li><li>인덱스를 타긴 타는데 성능이 안나오는 케이스</li><li>인덱스 스캔 비효율은 없지만 성능이 안나오는 케이스</li><li>인덱스가 너무 많이 있는 케이스</li><li>마무리</li></ol><h3>SQL 튜닝 개요</h3><p>SQL 튜닝은 동일한 쿼리 결과를 더 빠르고 더 가볍게 개선하는 모든 행위를 말합니다.</p><p>SQL 튜닝의 성능지표는 cpu 시간이나 block IO 등을 통해 평가합니다. 동일한 결과를 조회하는데 DBMS가 얼마나 더 적은 자원을 사용했느냐가 성능 좋은 쿼리의 기준이 됩니다.</p><p>SQL의 튜닝 포인트들은 인덱스 튜닝, 조인 튜닝, 소트 튜닝, DML 튜닝 등이 있는데요. 이번 글에서는 인덱스 튜닝에 대해서 다뤄보도록 하겠습니다.</p><p><strong>※인덱스 튜닝 관련 배경설명들이 매우 방대한 관계로 아래 항목들에 대한 자세한 설명은 각 항목에 연결해드린 외부 자료 참고 부탁드리겠습니다.</strong></p><ul><li>explain, explain analyze<br>- <a href="https://dev.mysql.com/doc/refman/8.0/en/explain-output.html">https://dev.mysql.com/doc/refman/8.0/en/explain-output.html</a><br>- <a href="https://dev.mysql.com/blog-archive/mysql-explain-analyze/">https://dev.mysql.com/blog-archive/mysql-explain-analyze/</a></li><li>storage architecture<br>- <a href="https://blog.ex-em.com/1699">https://blog.ex-em.com/1699</a></li><li>block io<br>- <a href="http://www.gurubee.net/lecture/3117">http://www.gurubee.net/lecture/3117</a></li><li>clustered index / nonclustered index</li><li>single block io / multi block io</li><li>table random access / table sequential access</li><li>buffer pool access, latch</li></ul><h3>인덱스 튜닝 세부 분류</h3><p>인덱스 튜닝은 아래와 같이 세부적으로 나누어 볼 수 있습니다.</p><ol><li><strong>인덱스를 타지 않는 경우<br></strong>1.1. 인덱스가 없을때<br> — 인덱스 생성<br>1.2. 인덱스가 있을때<br> — 인덱스 컬럼 가공으로 인한 성능저하 여부 확인<br> <a href="#"><strong><em>— 인덱스 손익분기점 확인 (=테이블 풀스캔이 더 효율적인지 확인)</em></strong></a></li><li><strong>인덱스를 타는 경우<br></strong>2.1. 인덱스 스캔 비효율 (인덱스 스캔량이 최종rows 보다 훨씬 많을때)<br><strong> </strong><a href="#"><strong><em>— 인덱스 필터링으로 동작하는 인덱스 스캔 개선</em></strong></a><strong><br></strong>2.2. 인덱스 스캔 비효율은 없지만, 테이블 엑세스가 많을때<br> <a href="#"><strong><em>— 인덱스만 읽고 멈추도록 개선</em></strong></a></li><li><strong>불필요한/중복인 인덱스 제거or통합<br> </strong><a href="#"><strong><em>— DML 성능 향상</em></strong></a><br> — 인덱스 다이브 성능 개선</li></ol><p>이 중에서 아래 4가지 항목에 대해 사례와 함께 살펴보도록 하겠습니다.</p><ul><li><strong><em>인덱스가 있는데도 인덱스를 안타는 상황 → 인덱스 손익 분기점</em></strong></li><li><strong><em>인덱스를 타긴 타는데 성능이 안나오는 상황 → 인덱스 스캔 비효율</em></strong></li><li><strong><em>인덱스 스캔 비효율은 없지만 성능이 안나오는 상황 → 테이블 랜덤엑세스 최소화</em></strong></li><li><strong><em>인덱스가 너무 많이 있는 상황 → 중복 인덱스 최적화</em></strong></li></ul><p>※ 테스트 환경은 아래와 같이 구성했습니다.</p><ul><li>mysql 8.0.34 (<a href="https://registry.hub.docker.com/_/mysql/">https://registry.hub.docker.com/_/mysql/</a>)</li><li>default my.cnf (zero config)</li></ul><h3>인덱스가 있는데도 인덱스를 안타는 상황 → 인덱스 손익 분기점</h3><p>인덱스 스캔으로 잘 동작하던 SQL이 어느날 갑자기 테이블 풀스캔으로 동작하는 상황이 있습니다.</p><p>옵티마이저가 인덱스 레인지 스캔보다 테이블 풀스캔을 선택했기 때문인데요. 실제로 io 가 어떻게 발생하길래 옵티마이저가 그런 선택을 했는지 살펴보도록 하겠습니다.</p><p>실험을 위한 데이터는 국토부 실거래 데이터(<a href="http://rtdown.molit.go.kr">http://rtdown.molit.go.kr</a>)를 사용했습니다.</p><pre>&lt;거래금액&gt;      82,500<br>&lt;/거래금액&gt;&lt;거래유형&gt;중개거래<br>&lt;/거래유형&gt;&lt;건축년도&gt;2002<br>&lt;/건축년도&gt;&lt;년&gt;2023<br>&lt;/년&gt;&lt;등기일자&gt;23.04.11<br>&lt;/등기일자&gt;&lt;법정동&gt; 영등포동<br>&lt;/법정동&gt;&lt;아파트&gt;영등포푸르지오<br>&lt;/아파트&gt;&lt;월&gt;3<br>&lt;/월&gt;&lt;일&gt;11<br>&lt;/일&gt;&lt;전용면적&gt;59.912<br>&lt;/전용면적&gt;&lt;중개사소재지&gt;서울 영등포구<br>&lt;/중개사소재지&gt;&lt;지번&gt;647<br>&lt;/지번&gt;&lt;지역코드&gt;11560<br>&lt;/지역코드&gt;&lt;층&gt;10<br>&lt;/층&gt;&lt;해제사유발생일&gt; <br>&lt;/해제사유발생일&gt;&lt;해제여부&gt; <br>&lt;/해제여부&gt;</pre><p>데이터의 스키마는 대략 아래와 같습니다.</p><ul><li>지역코드</li><li>매물구분 (아파트,오피스텔,연립다세대)</li><li>거래구분 (매매,전월세)</li><li>거래일자</li><li>매매금액</li><li>보증금</li><li>월세</li><li>기타<br>- 갱신요구권<br>- 해제사유발생일<br>- 등기일자<br>- …</li></ul><p>위 데이터를 기반으로 2020년도 이후 분량만 뽑아서 아래와 같이 아파트 실거래 테이블을 구축했습니다.</p><pre>create table T<br>(<br>	id int unsigned auto_increment<br>		primary key,<br>	지역코드 varchar(5) not null,<br>	거래년월 int unsigned not null,<br>  	...<br>);<br><br>create index IDX_01 on T (거래년월);<br>...</pre><p>이 테이블을 특정 거래일자 전후로 실행계획이 다르게 동작하는데요. 아래와 같이 인덱스가 있는데도 인덱스를 타지않고 풀스캔으로 동작하는 경우가 등장했습니다.</p><pre>select count(*) from T<br># 2034128건<br><br>select * from T where 거래년월 &gt;= 202307<br># 96977건 (range scan)<br><br>select * from T where 거래년월 &gt;= 202306<br># 134176건 (range scan)<br><br>select * from T where 거래년월 &gt;= 202305<br># 173069건 (full scan) &lt;&lt;&lt; 갑자기 인덱스를 안탄다..?</pre><p>위 상황과 관련하여 mysql 공식문서(<a href="https://dev.mysql.com/doc/refman/8.0/en/table-scan-avoidance.html">table-scan-avoidance</a>) 에서는 아래와 같이 4가지 상황을 가이드하고 있습니다.</p><ul><li>The table is so small that it is faster to perform a table scan than to bother with a key lookup. This is common for tables with fewer than 10 rows and a short row length.</li><li>There are no usable restrictions in the ON or WHERE clause for indexed columns.</li><li>You are comparing indexed columns with constant values and MySQL has calculated (based on the index tree) that the constants cover too large a part of the table and that a table scan would be faster. See <a href="https://dev.mysql.com/doc/refman/8.0/en/where-optimization.html">Section 8.2.1.1, “WHERE Clause Optimization”</a>.</li><li>You are using a key with low cardinality (many rows match the key value) through another column. In this case, MySQL assumes that by using the key probably requires many key lookups and that a table scan would be faster.</li></ul><p>3,4 항목을 보시면 옵티마이저가 <a href="#"><strong>“인덱스 키 조회 비용이 테이블 스캔 비용보다 더 크다고 판단”</strong></a> 하면, 인덱스가 있는데도 불구하고 테이블 풀 스캔을 사용한다고 설명합니다.</p><p>인덱스 스캔보다 테이블 풀스캔 비용이 더 낮아지는 지점을 <a href="#"><strong>“인덱스 손익 분기점”</strong></a> 이라고 하는데요.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/513/0*TYx6Xo4SwxrEXmTZ.png" /><figcaption>(이미지 출처 : <a href="https://richardfoote.wordpress.com/2012/06/08/indexes-vs-full-table-scan-picture-vs-1000-words-pictures-of-lily/">https://richardfoote.wordpress.com/2012/06/08/indexes-vs-full-table-scan-picture-vs-1000-words-pictures-of-lily/</a>)</figcaption></figure><p>위 그래프처럼 테이블 풀스캔의 비용은 일정한데 비해, 인덱스 스캔은 조회 건수에 비례하고 있습니다.</p><p>또한 쿼리 구조나 시스템 상황 등에 따라 기울기가 조금씩 달라지게 되는데요. 인덱스 스캔의 비용 직선이 테이블 풀스캔과 만나는 지점 어딘가에서 옵티마이저는 인덱스를 사용하지 않는 실행계획을 세우게 됩니다.</p><p>실제로 그렇게 동작하는지 확인해보겠습니다.</p><p><strong>옵티마이저 테이플 풀스캔 vs 힌트 인덱스 레인지 스캔 : io 비교 (상세)</strong></p><ul><li><a href="https://gist.github.com/paulan-zb/51a7c8af8e9161faf129fa3154b93ffb">https://gist.github.com/paulan-zb/51a7c8af8e9161faf129fa3154b93ffb</a></li></ul><p><strong>옵티마이저 테이플 풀스캔 vs 힌트 인덱스 레인지 스캔 : io 비교 (요약)</strong></p><pre>/**<br> * 옵티마이저 풀스캔 vs 힌트 인덱스스캔 성능비교 정리<br> */<br>## 옵티마이저 풀스캔<br># =&gt; 논리IO: 145788-83717 = 62071<br># =&gt; 물리IO: 36651-21307 = 15344<br><br>## 힌트 인덱스스캔<br># =&gt; 논리IO: 275930-16207 = 259723<br># =&gt; 물리IO: 3483-1205 = 2278<br><br>## 논리IO 비교 : <br># 259723 / 62071 = 4.18428896<br># 259723 - 62071 = 197652<br># =&gt; 인덱스스캔이 테이블풀스캔보다 논리IO가 훨씬 많이 발생했음 (풀스캔 대비 400% 수준)<br># =&gt; 인덱스스캔에서 인덱스트리의 root-branch-leaf노드(block)를 각각 탐색하는 비용<br>#    + 최종 leaf노드에 담겨있는 clustered index key(여기에서는 pk id) 로 테이블엑세스를 single block io 로 접근함 (1요청 1블록)<br># =&gt; 테이블스캔에서는 데이터block 을 곧바로 접근 + multi block io 방식으로 접근함 (1요청 N블록 → 서버/os 설정에 따라 달라질 수 있음)<br><br>## 물리IO 비교 : <br># 2278 / 15344 = 0.14846194<br># 15344 - 2278 = 13066<br># =&gt; 힌트인덱스스캔이 옵티마이저풀스캔보다 더 적은 물리IO가 발생함 (풀스캔 대비 14% 수준)<br># =&gt; 인덱스스캔에서는 많은 block io 비용을 사용해서 최종row의 key를 선정한 다음에 테이블엑세스를 진행한 반면,<br># =&gt; 테이블스캔에서는 일단 필요한 블록을 다 읽은 다음에 최종row 를 필터링했기 때문.</pre><p>위 비교 자료를 통해 아래와 같이 결론을 정리할 수 있습니다.</p><ul><li><strong>스캔량이 인덱스 손익 분기점을 지나면, 테이블 풀스캔보다 인덱스 스캔에서 논리IO 가 더 많이 발생함</strong></li><li><strong>옵티마이저는 이를 알고 풀스캔을 실행계획으로 선택함</strong></li></ul><p>인덱스 스캔은 결국 소량의 데이터를 찾는 목적에서 잘 동작합니다. 많은 양의 데이터를 조회하는 쿼리에서는 적합하지 않으므로 다른 조회 전략을 찾는게 유리할 수 있습니다.</p><p>즉, 인덱스 스캔 에서는</p><ol><li><strong>인덱스트리를 탐색하는 비용과</strong><br>버퍼캐시키체인 -&gt; 래치 -&gt; 버퍼풀에서 인덱스엑세스(없으면 버퍼풀 로딩) -&gt; 인덱스필터링 -&gt; 최종 대상 리프노드에 저장되어있는 클러스터드인덱스키(PK)로 테이블엑세스(이 역시 버퍼풀에서 먼저 찾고 없으면 로딩)</li><li><strong>최종 인덱스 리프노드에서 뽑은 clustered index key 로 테이블 로우를 읽는 비용(random access) 을 합한 비용이 발생하고,</strong></li></ol><p>테이블 풀스캔 에서는</p><ol><li><strong>전체 테이블 로우를 다 읽는 비용(sequential access) 만 발생합니다.</strong></li></ol><p>너무 많은 대상을 인덱스로 찾으면, 전체 테이블을 읽는 비용보다 인덱스를 찾는 비용이 훨씬 더 많은 비용이 발생할 수도 있습니다. 인덱스를 안탄다고 강제로 힌트를 지정하더라도 사실은 그게 DBMS에 더 많은 io 비용을 만들어버리게 되는 것입니다.</p><p>다시 말해서, 테이블 풀스캔이 항상 나쁜 것은 아니고, 바꿔말해 인덱스 스캔이 항상 좋은 것도 아닙니다. 각 상황에 맞는 최적의 스캔방법을 선정하는 것이 중요합니다.</p><h3>인덱스를 타긴 타는데 성능이 안나오는 상황 → 인덱스 스캔 비효율</h3><blockquote>인덱스필터링조건 vs 인덱스엑세스조건 : block io 비교 (인덱스 100개 읽어서 타겟팅완료 vs 인덱스 10개 읽어서 타겟팅 완료)</blockquote><blockquote>튜닝포인트 : 인덱스 필터링조건 → 엑세스조건 으로 변경</blockquote><p>인덱스를 잘 탔는데도 성능이 떨어지는 상황이 있을 수 있습니다.</p><p>인덱스 스캔 자체에서 비효율이 발생하는 경우인데요. 복합인덱스에서는 인덱스 구성 컬럼에서 범위조건이 사용되면, 해당 컬럼 이후의 컬럼들은 인덱스 스캔이 “엑세스” 가 아니라 “필터링” 으로 동작합니다.</p><p>예를 들면, 동일테이블 동일인덱스 에서</p><ul><li>상황1 : 100건 스캔 → 5건 필터링 → 5건 테이블엑세스</li><li>상황2 : 20건 스캔 → 5건 필터링 → 5건 테이블엑세스</li></ul><p>로 동작할수도 있다는 의미 입니다.</p><p>위 상황을 만들어서 한번 살펴보도록 하겠습니다.</p><p>아래와 같은 테이블을 준비했습니다.</p><pre>create table T<br>(<br>	id int unsigned not null<br>		primary key,<br>	지역코드 varchar(5) not null,<br>	거래년월 int unsigned not null,<br>  ...<br>);<br><br>create index IDX_02 on T (지역코드, 거래년월);<br>...</pre><p>위 테이블에서 서울지역의 2023년도 매매건수를 확인해보겠습니다.</p><pre>select count(*)<br>from T<br>where 지역코드 like &#39;11%&#39;<br>and 거래년월 between 202301 and 202309<br>-- 27498건<br><br>explain<br>select count(*)<br>from T<br>where 지역코드 like &#39;11%&#39;<br>and 거래년월 between 202301 and 202309<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+--------------------------------+<br># |id|select_type|table|partitions|type |possible_keys|key   |key_len|ref |rows  |filtered|Extra                           |<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+--------------------------------+<br># |1 |SIMPLE     |T    |null      |range|IDX_02       |IDX_02|206    |null|360246|11.11   |Using index condition; Using MRR|<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+--------------------------------+</pre><p>위에서 만든 IDX_02 인덱스를 레인지 스캔으로 잘 타는 것처럼 보입니다.</p><p>하지만 실제로는 어떻게 스캔이 동작하는지 좀 더 상세하게 확인해보겠습니다.</p><pre>explain analyze<br>select count(*)<br>from T<br>where 지역코드 like &#39;11%&#39;<br>and 거래년월 between 202301 and 202309<br># -&gt; Aggregate: count(0)  (cost=84718 rows=1) (actual time=132..132 rows=1 loops=1)<br>#     -&gt; Filter: ((T.`지역코드` like &#39;11%&#39;) and (T.`거래년월` between 202301 and 202308))  (cost=80715 rows=40023) (actual time=1.19..130 rows=25620 loops=1)<br>#         -&gt; Covering index range scan on t using IDX_02 over (&#39;11￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿&#39; &lt;= 지역코드 &lt;= &#39;11￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿￿&#39; AND 202301 &lt;= 거래년월 &lt;= 202308)  (cost=80715 rows=360246) (actual time=0.0297..96.3 rows=167787 loops=1)</pre><p>explain analyze 에서 분석한 내용을 확인해보면, <br>1) 인덱스 레인지 스캔을 통해 167787건 읽은 다음<br>2) 140289건이 필터링되어 25620건이 출력으로 나와<br>3) 최종적으로 count함수로 1건이 Aggregate되었다고 설명하고 있습니다.<br>27498/167787 = 0.16388636 (약 16%) 즉 16% 를 얻기위해 84% 를 읽고 그냥 버려버리는 매우 비효율적인 처리로 동작했습니다.</p><p>사실 이 167787건은 지역코드 11(서울지역) 에 해당되는 데이터 건수이고, 25620건은 지역코드 11(서울지역) + 거래년월 2023–01 ~ 2023–09에 해당되는 데이터 건수인데요.</p><p>위에서 만든 IDX_02 인덱스는 (지역코드, 거래년월) 컬럼으로 만들어졌고, 위 쿼리는 지역코드 거래년월 만 사용했는데, 왜 두 컬럼중 앞쪽 지역코드 컬럼만 사용되었을까요?</p><p>그 이유는 복합인덱스의 경우, 인덱스 선행컬럼이 조건절에 없거나 =조건이 아니면, 인덱스 스캔 과정에 비효율이 발생하기 때문입니다.</p><p>인덱스 스캔은 액세스 조건과 필터 조건으로 구분되어 동작하는데요.</p><ul><li><strong>인덱스 엑세스 조건</strong> : 인덱스 스캔 범위 결정</li><li><strong>인덱스 필터 조건</strong> : 테이블 엑세스 여부 결정</li><li><strong>테이블 필터 조건</strong> : 최종 결과집합 포함 여부 결정</li></ul><p>위 문제의 쿼리에서는 인덱스 선행컬럼인 지역코드를 지역코드 like &#39;11%&#39; 와 같이 범위검색으로 접근했기 때문에, 인덱스 후행컬럼인 거래년월 컬럼조건 거래년월 between 202301 and 202309 이 스캔범위를 결정하는데 사용되지 못했기 때문입니다.</p><p>이런 유형의 쿼리는 선행컬럼의 범위검색 조건을 IN-List 조건으로 변경하는 것으로 개선할 수 있습니다.</p><pre>explain<br>select count(*)<br>from T<br>where 지역코드 in (<br>    &#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;<br>)<br>and 거래년월 between 202301 and 202309<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+-----+--------+------------------------+<br># |id|select_type|table|partitions|type |possible_keys|key   |key_len|ref |rows |filtered|Extra                   |<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+-----+--------+------------------------+<br># |1 |SIMPLE     |T    |null      |range|IDX_02       |IDX_02|206    |null|27499|100     |Using where; Using index|<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+-----+--------+------------------------+<br># =&gt; 인덱스 레인지스캔 탐<br><br>explain analyze<br>select count(*)<br>from T<br>where 지역코드 in (<br>    &#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;<br>)<br>and 거래년월 between 202301 and 202309<br># -&gt; Aggregate: count(0)  (cost=8912 rows=1) (actual time=34.6..34.6 rows=1 loops=1)<br>#     -&gt; Filter: ((T.`지역코드` in (&#39;11000&#39;,&#39;11110&#39;,&#39;11140&#39;,&#39;11170&#39;,&#39;11200&#39;,&#39;11215&#39;,&#39;11230&#39;,&#39;11260&#39;,&#39;11290&#39;,&#39;11305&#39;,&#39;11320&#39;,&#39;11350&#39;,&#39;11380&#39;,&#39;11410&#39;,&#39;11440&#39;,&#39;11470&#39;,&#39;11500&#39;,&#39;11530&#39;,&#39;11545&#39;,&#39;11560&#39;,&#39;11590&#39;,&#39;11620&#39;,&#39;11650&#39;,&#39;11680&#39;,&#39;11710&#39;,&#39;11740&#39;)) and (T.거래년월 between 202301 and 202309))  (cost=6162 rows=27499) (actual time=0.0413..32.8 rows=27498 loops=1)<br>#         -&gt; Covering index range scan on t using IDX_02 over (지역코드 = &#39;11000&#39; AND 202301 &lt;= 거래년월 &lt;= 202309) OR (지역코드 = &#39;11110&#39; AND 202301 &lt;= 거래년월 &lt;= 202309) OR (24 more)  (cost=6162 rows=27499) (actual time=0.0368..16.8 rows=27498 loops=1)</pre><p>27498건 읽어서 최종 27498건을 사용했고, 27498/27498 = 1.0 (100%) 스캔 비효율이 없는 쿼리로 개선되었습니다.</p><p>IN-List 조건은 내부적으로 IN절을 N번 반복하는 형태로 동작하는데요. 이는 explain analyze 에서 레인지스캔 분석 부분을 살펴보면 (지역코드 = &#39;11000&#39; AND 202301 &lt;= 거래년월 &lt;= 202309) OR (지역코드 = &#39;11110&#39; AND 202301 &lt;= 거래년월 &lt;= 202309) OR (24 more) 로 표현되어 있는 것으로 확인할 수 있습니다.</p><p>즉, 인덱스 선행컬럼인 지역코드 조건이 = 조건으로 총 26회 펼쳐져서 동작하게 되어, 지역코드 + 거래년월 조건으로 정확하게 필요한 영역(27498건) 만 스캔하도록 동작하게 되었습니다.</p><p>※ 인덱스 탐색 동작에 대한 내용은 다음 기회에 좀 더 자세히 설명해보도록 하겠습니다.</p><p>※ IN-LIST 변환에 너무 많은 값이 들어가게 되면 오히려 성능이 나빠질 수도 있습니다. (ex. eq_range_index_dive_limit)</p><p>이처럼 explain 결과에서 인덱스 스캔 사용여부만 확인하고 넘어간다면, 비효율적인 인덱스 스캔을 놓치게 될 수도 있습니다. 인덱스 스캔으로 동작하지만 실제 비효율적인 동작이 있는지 explain analyze 를 통해 한번 더 확인해본다면 예상하지 못한 성능저하를 예방할 수 있습니다.</p><h3>인덱스 스캔 비효율은 없지만 성능이 안나오는 상황 → 테이블 랜덤 엑세스 최소화</h3><blockquote>테이블엑세스 vs 인덱스만읽기 : block io 비교 (인덱스 10개 테이블 5개 읽어서 타겟팅완료 vs 인덱스 10개-5개필터링 해서 타겟팅완료)</blockquote><blockquote>튜닝포인트 : 인덱스 컬럼 추가</blockquote><p>인덱스 스캔 비효율은 없지만 그래도 성능이 안나오는 경우도 있습니다.</p><p>이런 케이스는 실제 데이터를 스캔하는 일량 자체가 많은 상황이기 때문에, 조회 조건이나 순서를 조정하는 등의 SQL 외적인 방법을 고려하는게 바람직하지만,</p><p>그럼에도 불구하고 특정 상황에서는 SQL 레벨에서 성능을 개선하는 방법도 있습니다.</p><p>아래 데이터를 통해 확인해보겠습니다.</p><pre>create table T<br>(<br>	id int unsigned not null<br>		primary key,<br>	지역코드 varchar(5) not null,<br>	거래년월 int unsigned not null,<br>	거래금액 int unsigned not null,<br>  ...<br>);<br><br>create index IDX_02 on T (지역코드, 거래년월);<br>...</pre><pre>## 서울지역 2020~2022년 사이 시군구별 최고매매가 계산<br>select 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드;<br><br>explain<br>select 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드;<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+---------------------+<br># |id|select_type|table|partitions|type |possible_keys|key   |key_len|ref |rows  |filtered|Extra                |<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+---------------------+<br># |1 |SIMPLE     |t    |null      |range|IDX_02       |IDX_02|206    |null|195035|100     |Using index condition|<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+---------------------+<br># =&gt; 레인지스캔 잘 탐<br><br>explain analyze<br>select 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드<br># -&gt; Group aggregate: max(T.`거래금액`)  (cost=212457 rows=238) (actual time=10.8..892 rows=25 loops=1)<br>#     -&gt; Index range scan on t using IDX_02 over (지역코드 = &#39;11000&#39; AND 202001 &lt;= DEAL_YM &lt;= 202212) OR (지역코드 = &#39;11110&#39; AND 202001 &lt;= DEAL_YM &lt;= 202212) OR (24 more), with index condition: ((T.`지역코드` in (&#39;11000&#39;,&#39;11110&#39;,&#39;11140&#39;,&#39;11170&#39;,&#39;11200&#39;,&#39;11215&#39;,&#39;11230&#39;,&#39;11260&#39;,&#39;11290&#39;,&#39;11305&#39;,&#39;11320&#39;,&#39;11350&#39;,&#39;11380&#39;,&#39;11410&#39;,&#39;11440&#39;,&#39;11470&#39;,&#39;11500&#39;,&#39;11530&#39;,&#39;11545&#39;,&#39;11560&#39;,&#39;11590&#39;,&#39;11620&#39;,&#39;11650&#39;,&#39;11680&#39;,&#39;11710&#39;,&#39;11740&#39;)) and (T.거래년월 between 202001 and 202212))  (cost=192953 rows=195035) (actual time=0.061..857 rows=140289 loops=1)<br># =&gt; 인덱스스캔 140289건 → 테이블스캔 140289건 (100%, 비효율없음)<br># =&gt; 140289건 → group by 25건</pre><p>IDX_02 인덱스를 레인지 스캔으로 비효율없이 잘 쿼리하는 것 같습니다.</p><pre><br>show status like &#39;Innodb_buffer_pool_read%&#39;<br># +-------------------------------------+-----+<br># |Variable_name                        |Value|<br># +-------------------------------------+-----+<br># |Innodb_buffer_pool_read_ahead_rnd    |0    |<br># |Innodb_buffer_pool_read_ahead        |0    |<br># |Innodb_buffer_pool_read_ahead_evicted|0    |<br># |Innodb_buffer_pool_read_requests     |16453|<br># |Innodb_buffer_pool_reads             |1432 |<br># +-------------------------------------+-----+<br><br>select 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드;<br><br>show status like &#39;Innodb_buffer_pool_read%&#39;<br># +-------------------------------------+------+<br># |Variable_name                        |Value |<br># +-------------------------------------+------+<br># |Innodb_buffer_pool_read_ahead_rnd    |0     |<br># |Innodb_buffer_pool_read_ahead        |0     |<br># |Innodb_buffer_pool_read_ahead_evicted|0     |<br># |Innodb_buffer_pool_read_requests     |474380|<br># |Innodb_buffer_pool_reads             |4003  |<br># +-------------------------------------+------+<br># =&gt; 474380-16453 = 457927 (논리read io)</pre><p>위 쿼리는 스캔 비효율은 없지만 read io 일량(인덱스 스캔 + 테이블 랜덤 엑세스) 자체가 많은 케이스 입니다. 140289건을 읽기위해 457927만큼의 read io 가 발생했습니다.</p><p>이런 경우에는 아래와 같이 인덱스에 컬럼을 추가하는 것으로 인덱스까지만 읽고 테이블 랜덤 엑세스가 발생하지 않도록 유도할 수 있습니다.</p><pre>-- create index IDX_02 on T (지역코드, 거래년월);<br>create index IDX_03 on T (지역코드, 거래년월, 거래금액);</pre><pre>## 서울지역 2020~2022년 사이 시군구별 최고매매가 계산<br>select /*+ index(T IDX_03) */ 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드;<br><br>explain<br>select /*+ index(T IDX_03) */ 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+------------------------+<br># |id|select_type|table|partitions|type |possible_keys|key   |key_len|ref |rows  |filtered|Extra                   |<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+------------------------+<br># |1 |SIMPLE     |t    |null      |range|IDX_03       |IDX_03|206    |null|217038|100     |Using where; Using index|<br># +--+-----------+-----+----------+-----+-------------+------+-------+----+------+--------+------------------------+<br># =&gt; 레인지스캔 잘 탐<br><br>explain analyze<br>select /*+ index(T IDX_03) */ 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드<br># -&gt; Group aggregate: max(T.`거래금액`)  (cost=70364 rows=238) (actual time=2.25..218 rows=25 loops=1)<br>#     -&gt; Filter: ((T.`지역코드` in (&#39;11000&#39;,&#39;11110&#39;,&#39;11140&#39;,&#39;11170&#39;,&#39;11200&#39;,&#39;11215&#39;,&#39;11230&#39;,&#39;11260&#39;,&#39;11290&#39;,&#39;11305&#39;,&#39;11320&#39;,&#39;11350&#39;,&#39;11380&#39;,&#39;11410&#39;,&#39;11440&#39;,&#39;11470&#39;,&#39;11500&#39;,&#39;11530&#39;,&#39;11545&#39;,&#39;11560&#39;,&#39;11590&#39;,&#39;11620&#39;,&#39;11650&#39;,&#39;11680&#39;,&#39;11710&#39;,&#39;11740&#39;)) and (T.거래년월 between 202001 and 202212))  (cost=48660 rows=217038) (actual time=0.0412..187 rows=140289 loops=1)<br>#         -&gt; Covering index range scan on t using IDX_03 over (지역코드 = &#39;11000&#39; AND 202001 &lt;= 거래년월 &lt;= 202212) OR (지역코드 = &#39;11110&#39; AND 202001 &lt;= 거래년월 &lt;= 202212) OR (24 more)  (cost=48660 rows=217038) (actual time=0.0369..104 rows=140289 loops=1)<br># =&gt; 140289건 읽어서 140289건 필터링 (100%, 비효율없음)<br># =&gt; 인덱스스캔 140289건 → group by 25건 (커버링인덱스에서 필터링 동작, 테이블스캔이 사라짐)<br><br>show status like &#39;Innodb_buffer_pool_read%&#39;;<br># +-------------------------------------+-----+<br># |Variable_name                        |Value|<br># +-------------------------------------+-----+<br># |Innodb_buffer_pool_read_ahead_rnd    |0    |<br># |Innodb_buffer_pool_read_ahead        |0    |<br># |Innodb_buffer_pool_read_ahead_evicted|0    |<br># |Innodb_buffer_pool_read_requests     |16320|<br># |Innodb_buffer_pool_reads             |1289 |<br># +-------------------------------------+-----+<br><br>select /*+ index(T IDX_03) */ 지역코드, max(거래금액)<br>from T<br>where 지역코드 in (&#39;11000&#39;, &#39;11110&#39;, &#39;11140&#39;, &#39;11170&#39;, &#39;11200&#39;, &#39;11215&#39;, &#39;11230&#39;, &#39;11260&#39;, &#39;11290&#39;, &#39;11305&#39;, &#39;11320&#39;, &#39;11350&#39;, &#39;11380&#39;, &#39;11410&#39;, &#39;11440&#39;, &#39;11470&#39;, &#39;11500&#39;, &#39;11530&#39;, &#39;11545&#39;, &#39;11560&#39;, &#39;11590&#39;, &#39;11620&#39;, &#39;11650&#39;, &#39;11680&#39;, &#39;11710&#39;, &#39;11740&#39;)<br>and 거래년월 between 202001 and 202212<br>group by 지역코드;<br><br>show status like &#39;Innodb_buffer_pool_read%&#39;;<br># +-------------------------------------+-----+<br># |Variable_name                        |Value|<br># +-------------------------------------+-----+<br># |Innodb_buffer_pool_read_ahead_rnd    |0    |<br># |Innodb_buffer_pool_read_ahead        |0    |<br># |Innodb_buffer_pool_read_ahead_evicted|0    |<br># |Innodb_buffer_pool_read_requests     |33105|<br># |Innodb_buffer_pool_reads             |1490 |<br># +-------------------------------------+-----+<br># =&gt; 33105-16320 = 16785 (read io)</pre><p>IDX_02 에 비해 IDX_03 에서 read io 가 큰 폭으로(asis: 457927 → tobe: 16785) 줄어들었습니다.</p><pre>/**<br> * 결론<br> */<br>## 비효율은 없지만 테이블랜덤엑세스(read io 실제 일량)가 많은 상황<br># =&gt; 474380-16453 = 457927 (read io)<br># =&gt; 인덱스스캔 140289건 → 테이블스캔 140289건 (100%, 비효율없음) → group by 25건<br><br>## 인덱스 컬럼추가로 테이블랜덤엑세스 최소화<br># =&gt; 33105-16320 = 16785 (read io)<br># =&gt; 인덱스스캔 140289건 → group by 25건 (테이블랜덤엑세스 사라짐)</pre><p>기존 대비 3% (16785 / 457927 = 0.03665431) 수준의 io 만 사용하는 쿼리로 개선되었습니다. 쿼리가 인덱스만 읽고 처리를 완료할 수 있도록 변경되어서 테이블 랜덤 엑세스가 사라졌고, 이에 관련 비용들이 줄어들게 된 것입니다.</p><p>커버링인덱스의 실제 io 프로파일링을 확인해보니, 생각했던 것 보다 더 많은 io 를 줄일수 있다는 것을 알 수 있었습니다. (테이블 랜덤 엑세스 비용이 그 만큼 비싸다는 반증으로 볼 수 있겠습니다.)</p><p>커버링인덱스는 유용한 인덱스 튜닝 방법이긴 하지만, 복잡한 인덱스를 여기저기 과도하게 생성하게 되면 또 다른 문제가 발생할 수 있습니다.</p><p>이어지는 주제에서 과도한 인덱스가 어떤 문제를 발생하는 지에 대해 자세히 다뤄보도록 하겠습니다.</p><h3>인덱스가 너무 많이 있는 상황 → 중복 인덱스 최적화</h3><blockquote>중복인덱스 테이블 vs 인덱스없는 테이블 vs 인덱스 최적화 테이블 : query profile 비교</blockquote><blockquote>튜닝포인트 : 인덱스 통폐합</blockquote><p>인덱스를 줄이는 것도 인덱스 튜닝이 될 수 있습니다.</p><p>너무 많은 인덱스, 불필요한 인덱스는 DML 성능을 떨어뜨립니다. 또한 저장공간의 낭비, 의도하지 않은 인덱스 선정 등과 같은 부작용도 함께 발생할 수 있습니다.</p><p>중복인덱스 테이블과 인덱스를 최적화한 테이블 간의 query profile 을 비교해보겠습니다.</p><p>실험을 위한 테이블을 아래와 같이 준비했습니다.</p><pre>create table 아파트_실거래<br>(<br>	id int unsigned auto_increment<br>		primary key,<br>	거래일자 datetime not null,<br>	아파트코드 int unsigned not null,<br>	전용면적 decimal unsigned default 0 not null,<br>	...<br>);<br>create index IDX_01 on 아파트_실거래 (전용면적);<br>create index IDX_02 on 아파트_실거래 (거래일자);<br>create index IDX_03 on 아파트_실거래 (아파트코드, 전용면적, 거래일자);<br>create index IDX_04 on 아파트_실거래 (아파트코드, 전용면적);<br>create index IDX_05 on 아파트_실거래 (아파트코드);<br><br>## 인덱스 없는 테이블<br>create table 아파트_실거래_noindex like 아파트_실거래;<br>insert into 아파트_실거래_noindex select * from 아파트_실거래;<br><br>## 인덱스 통폐합 테이블<br>create table 아파트_실거래_compact_index like 아파트_실거래;<br>insert into 아파트_실거래_compact_index select * from 아파트_실거래;<br>create index IDX_02 on 아파트_실거래_compact_index (거래일자);<br>create index IDX_03 on 아파트_실거래_compact_index (아파트코드, 전용면적, 거래일자);</pre><p><strong>중복인덱스 테이블 vs 인덱스없는 테이블 vs 인덱스 최적화 테이블 : query profile 비교 (상세)</strong></p><ul><li><a href="https://gist.github.com/paulan-zb/6245874265333577523746084d2de9f5">https://gist.github.com/paulan-zb/6245874265333577523746084d2de9f5</a></li></ul><p><strong>중복인덱스 테이블 vs 인덱스없는 테이블 vs 인덱스 최적화 테이블 : query profile 비교 (요약)</strong></p><pre>/**<br> * 중복인덱스 vs 인덱스없음 vs 인덱스통폐합 DML 성능비교 정리<br> */<br>## 중복인덱스 테이블<br># block io :<br># =&gt; read io : 15876 → 450088 =&gt; 434212<br># =&gt; write io :  1857 → 201534 =&gt; 199677<br><br>## 인덱스 없는 테이블<br># block io :<br># =&gt; read io : 41120 → 82548 =&gt; 41428<br># =&gt; write io :  3412 → 23887 =&gt; 20475<br><br>## 인덱스통폐합 테이블<br># block io :<br># =&gt; read io : 16932 → 176005 =&gt; 159073<br># =&gt; write io :  1856 → 82184 =&gt; 80328<br><br><br>## 결론1<br># 동일 스키마 테이블에 10000건 insert 에 대해서<br># write io 는 중복인덱스(199677) &gt; 인덱스통폐합(80328) &gt; 인덱스없음(20475) 의 형상을 띔<br># =&gt; 중복인덱스(199677) / 인덱스통폐합(80328) = 2.48577084 (약 250%)<br># =&gt; 중복인덱스(199677) / 인덱스없음(20475) = 9.75223443 (약 970%)<br><br><br>## 결론2<br># DML 쿼리에서는 테이블 CUD 뿐만아니라, 인덱스 갱신/정렬 도 함께 발생하기때문에<br># read io 역시 중복인덱스에서 훨씬 많이 발생함<br># =&gt; 중복인덱스(434212) / 인덱스통폐합(159073) = 2.72963985 (약 270%)<br># =&gt; 중복인덱스(434212) / 인덱스없음(41428) = 10.48112388 (약 1050%)<br><br><br>## 결론3<br>select table_schema, table_name,<br>       round(sum(data_length)/1024/1024,2) as &#39;table Size(Mb)&#39;,<br>       round(sum(index_length)/1024/1024,2) as &#39;index Size(Mb)&#39;,<br>       max(table_rows) as rows_number,<br>       max(auto_increment) as auto_increment_value<br>from information_schema.tables<br>where table_schema = &#39;test&#39; and table_name like &#39;아파트_실거래%&#39;<br>group by table_schema, table_name;<br># +------------+---------------------+--------------+--------------+-----------+--------------------+<br># |TABLE_SCHEMA|TABLE_NAME           |table Size(Mb)|index Size(Mb)|rows_number|auto_increment_value|<br># +------------+---------------------+--------------+--------------+-----------+--------------------+<br># |test        |아파트_실거래              |82.61         |176.34        |1495158    |31963385            |<br># |test        |아파트_실거래_noindex      |82.61         |0.00          |1495158    |31963385            |<br># |test        |아파트_실거래_compact_index|82.61         |30.56         |1495158    |31963385            |<br># +------------+---------------------+--------------+--------------+-----------+--------------------+<br># =&gt; 불필요한 인덱스 최적화를 통해, 테이블보다 인덱스 사이즈가 더 큰 비효율을 개선할 수 있음<br># =&gt; 중복인덱스 : 82.61(테이블) / 176.34(인덱스)<br># =&gt; 인덱스통폐합 : 82.61(테이블) / 30.56(인덱스)<br>#     =&gt; 176.34 → 30.56 으로 asis 대비 17% 수준으로 인덱스 용량 최적화<br>#     =&gt; 176.34/82.61(약 210%) → 30.56/82.61(약 36%) 으로 테이블 대비 인덱스 용량 최적화<br># =&gt; 인덱스없음 : 82.61(테이블) / 0(인덱스)</pre><p>위 비교 자료를 통해 아래와 같이 결론을 정리할 수 있습니다.</p><ul><li><strong>불필요한 인덱스로 인해 DML 쿼리에서 상당한 비효율이 발생할 수 있음</strong></li><li><strong>DML 성능 뿐만아니라, 저장공간 낭비도 발생할 수 있음</strong></li></ul><p>인덱스는 꼭 필요한 조회경로에 대해서만 생성하는 것이 좋습니다.</p><p>특히 중복 인덱스의 경우에는 조회/갱신 양쪽 모두에서 불필요한 성능저하가 발생할 수 있는데요. 가능하다면 중복 인덱스가 보인다면 인덱스 정리를 고려해볼 수 있겠습니다.</p><p>※중복 인덱스 정리는 해당 컬럼을 사용하는 모든 쿼리의 실행계획에 영향을 줄 수 있으므로 신중한 진행이 필요합니다.</p><h3>마무리</h3><p>지금까지 인덱스를 타더라도 성능이 안나올 수 있는 상황들을 살펴봤습니다.</p><ul><li><strong>인덱스가 있는데도 인덱스를 안타는 상황 → 인덱스 손익 분기점<br> </strong>— 옵티마이저 테이플풀스캔 vs 힌트 인덱스레인지스캔 : block io 비교</li><li><strong>인덱스를 타긴 타는데 성능이 안나오는 상황 → 인덱스 스캔 비효율<br> </strong>— 인덱스필터링조건 vs 인덱스엑세스조건 : block io 비교 (인덱스 100개 읽어서 타겟팅완료 vs 인덱스 10개 읽어서 타겟팅 완료)<br> — <a href="#"><strong><em>튜닝포인트 : 인덱스 필터링조건 → 엑세스조건 으로 변경</em></strong></a></li><li><strong>인덱스 스캔 비효율은 없지만 성능이 안나오는 상황 → 테이블 랜덤엑세스 최소화<br> — </strong>테이블엑세스 vs 인덱스만읽기 : block io 비교 (인덱스 10개 테이블 5개 읽어서 타겟팅완료 vs 인덱스 10개-5개필터링 해서 타겟팅완료)<br> — <a href="#"><strong><em>튜닝포인트 : 인덱스 컬럼 추가</em></strong></a></li><li><strong>인덱스가 너무 많이 있는 상황 → 중복 인덱스 최적화<br> — </strong>중복인덱스 테이블 vs 인덱스없는 테이블 vs 인덱스 최적화 테이블 : query profile 비교<br> — <a href="#"><strong><em>튜닝포인트 : 인덱스 통폐합</em></strong></a></li></ul><p>이 밖에도 인덱스 튜닝 포인트는 많이 있을 수 있습니다.</p><ul><li>인덱스 스킵 스캔</li><li>인덱스 다이브 최적화</li><li>인덱스를 이용한 sort 연산 생략</li><li>…</li></ul><p>튜닝의 핵심은 이슈 상황에 따라 어떤 지점에서 어떤 비용이 많이 발생하는지를 파악하는게 가장 중요합니다.</p><p>1) explain, explain analyze 등을 통해 쿼리가 어떻게 동작하는지 확인하고, <br>2) 필요하다면 쿼리 프로파일링을 통해 실제 시스템에서 cpu, block io 등이 얼마나 발생하고 있는지 검토한 다음, <br>3) 비효율을 제거할 수 있는 포인트를 찾고, <br>4) 해당 지점을 효율적으로 동작하도록 개선해 나가는게 중요하겠습니다.</p><p>감사합니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=18e183e9246d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/mysql-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%ED%8A%9C%EB%8B%9D-18e183e9246d">MYSQL 인덱스 튜닝</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[react-native 에 모듈 페더레이션 더하기 (with. RePack)]]></title>
            <link>https://medium.com/zigbang/react-native-%EC%97%90-%EB%AA%A8%EB%93%88-%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98-%EB%8D%94%ED%95%98%EA%B8%B0-with-repack-12288f15e617?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/12288f15e617</guid>
            <category><![CDATA[repack]]></category>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[module-federation]]></category>
            <category><![CDATA[react-native]]></category>
            <dc:creator><![CDATA[Jerrynim]]></dc:creator>
            <pubDate>Thu, 26 Oct 2023 08:42:48 GMT</pubDate>
            <atom:updated>2023-10-26T08:42:48.604Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MFL3z2ye4EYkRknr6LUtdg.png" /></figure><p>모듈 페더레이션(<a href="https://webpack.js.org/concepts/module-federation/#motivation">Module Federation</a>) 은 애플리케이션을 여러 개의 작은 애플리케이션으로 분할하는 것을 말합니다. 마이크로 서비스와 유사하여 Micro-frontends 라고도 불립니다. 직방 프론트엔드 팀에서는 react-native를 사용하여 앱을 개발하면서 다음과 같은 미션을 수행하기 위해 모듈 페더레이션을 고려하고 있습니다.</p><ul><li><strong>업데이트 과정 최적화</strong><br>OTA(Over-the-air programming)를 활용하여 자바스크립트 코드를 업데이트하여, 사용자는 앱을 다시 다운로드하거나 재설치하지 않고도 업데이트된 서비스를 사용할 수 있습니다.</li><li><strong>유연한 배포 관리</strong><br>각 모듈의 코드를 서버에서 제어하므로 A/B 테스팅, 사용자 그룹 또는 각 모듈별로 업데이트를 제어할 수 있습니다.</li><li><strong>심사과정 없는 앱 업데이트</strong><br>remote 모듈을 업데이트하는 것으로 앱을 업데이트를 수행할 수 있습니다. 다만 Store의 정책을 위반하지 않도록 코드에 대한 수정 사항이나 조정을 제공하는 데 사용하여야 하고, 새 기능을 추가해서는 안 됩니다.</li><li><strong>서비스 별 개발 및 배포</strong><br>전체 서비스를 빌드하는 것에 비해 작은 앱으로 빌드하게 되어 디버깅 및 테스트를 빠르게 할 수 있습니다.</li></ul><h4>모듈 페더레이션의 기능</h4><p>모듈 페더레이션을 통해 다음과 같은 것을 이룰 수 있습니다.</p><ul><li>애플리케이션을 여러 개의 격리된 컨테이너로 분할할 수 있습니다.</li><li>각 컨테이너에 대한 빌드 및 프로세스를 구성할 수 있습니다.</li><li>필요에 따라 컨테이너를 동적으로 로드할 수 있습니다.</li><li>각기 다른 버전의 컨테이너를 로드할 수 있습니다.</li><li>외부 Micro-frontend를 사용가능하게 됩니다.</li></ul><h4>모듈 페더레이션의 용어</h4><p>모듈 페더레이션을 이해하기 위해서는 몇가지 용어를 익혀두어야 합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xAFrQNjQftBaZq8rXmOlcA.png" /><figcaption>[사진-1] 모듈 페더레이션 예시</figcaption></figure><ul><li><strong>Host Application(host)</strong>: 처음으로 실행되는 컨테이너, 스토어에 배포된 앱</li><li><strong>Local module(local): </strong>App bundle(.ipa, .apk)에 포함되는 모듈</li><li><strong>Remote module(remote): </strong>App bundle 로 포함되지 않으며 요청시 원격 위치에서 다운로드됩니다.</li><li><strong>Expose</strong> : 컨테이너가 외부에 노출하려는 모듈의 목록을 나타냅니다.</li><li><strong>Shared</strong>: 별도의 청크로 분리하여 앱의 런타임에 로드해 사용하는 의존성 모듈입니다.</li><li><strong>Shell</strong>: 껍데기라는 의미로 여러 마이크로 프론트엔드 프로젝트를 모아서 보여주는 하나의 프론트엔드 프로젝트입니다. 마이크로 앱 간의 라우팅, 공유 데이터 저장소 초기화 및 각 앱의 lazy loading 을 처리합니다.</li></ul><p>react-native 에 모듈 페더레이션을 적용하기 위해서는 Re.Pack 라이브러리를 사용해야 합니다.</p><h3>Re.Pack</h3><p><a href="https://github.com/callstack/repack">Re.Pack</a>은 callstack 에서 만든 React Native 어플리케이션을 위한 bundler 입니다. react-native cli 를 대체하고, 개발 서버로 작동하거나 React Native 앱을 번들링 하는 역할을 합니다. 또한, Re.Pack 은 Webpack 으로 제작된 번들을 react-native 애플리케이션에서 사용할 수 있게 만들어, <a href="https://webpack.kr/guides/code-splitting/">코드 스플리팅</a>(Code splitting) 을 가능하게할 수 있습니다.</p><p>Re.Pack을 코드레벨에서 살펴보면서 간단하게 모듈 페더레이션을 살펴보도록 하겠습니다. 앱의 구조는 다음 그림과 같이 host앱이 app1, app2의 remote 앱을 가지는 구성을 해보도록 하겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*ELDq2ERXVxaPLLyHzORYCg.png" /></figure><p>프로젝트의 구조는 다음과 같습니다.(monorepo 구조를 사용합니다)<br>각각의 모듈에서 독립적으로 구성 및 실행이 가능하도록 구성합니다.</p><pre>/packages<br>  ㄴ host<br>    ㄴ android<br>    ㄴ ios<br>    ㄴ webpack.config.js<br>  ㄴ app1<br>    ㄴ android<br>    ㄴ ios<br>    ㄴ webpack.config.js<br>  ㄴ app2<br>    ㄴ android<br>    ㄴ ios<br>    ㄴ webpack.config.js</pre><h4>host 설정</h4><p>host 앱은 app1, app2 컨테이너로부터 “./App” 모듈을 import 합니다.<br>원격으로 모듈을 불러오기때문에 로딩이 발생하게됩니다. 로딩을 처리하기 위하여 Suspense 와 ErrorBoundary를 사용하도록 합니다.</p><pre>//App.tsx<br>import { Federated } from &quot;@callstack/repack/client&quot;<br><br>const App1 = React.lazy(() =&gt; Federated.importModule(&quot;app1&quot;, &quot;./App&quot;))<br>const App2 = React.lazy(() =&gt; Federated.importModule(&quot;app2&quot;, &quot;./App&quot;))<br><br>return (<br>  &lt;ErrorBoundary&gt;<br>   &lt;React.Suspense<br>    fallback={<br>      &lt;Loading /&gt;<br>    }&gt;<br>    &lt;App1 /&gt;<br>   &lt;/React.Suspense&gt;<br>  &lt;/ErrorBoundary&gt;<br>  &lt;ErrorBoundary&gt;<br>   &lt;React.Suspense<br>    fallback={<br>      &lt;Loading /&gt;<br>    }&gt;<br>    &lt;App2 /&gt;<br>   &lt;/React.Suspense&gt;<br>  &lt;/ErrorBoundary&gt;</pre><p>host는 각 remote 모듈을 요청할 주소를 설정해주어야합니다. Re.Pack의 <a href="https://re-pack.netlify.app/docs/api/repack/client/classes/ScriptManager#addresolver">ScriptManager.shared.addResolver </a>를 사용하여 주소를 설정하도록합니다.</p><pre>// index.js<br>import { ScriptManager, Federated } from &quot;@callstack/repack/client&quot;<br><br>const resolveURL = Federated.createURLResolver({<br> containers: {<br>  app1: __DEV__<br>   ? &quot;http://localhost:9001/[name][ext]&quot;<br>   : `https://CF.zigbang.in/chunks/app1/${Platform.OS}/[name][ext]`,<br>  app2: __DEV__<br>   ? &quot;http://localhost:9002/[name][ext]&quot;<br>   : `https://CF.zigbang.in/chunks/app2/${Platform.OS}/[name][ext]`,<br> },<br>})<br><br>ScriptManager.shared.addResolver(async (scriptId, caller) =&gt; {<br> const url = resolveURL(scriptId, caller)<br> return {<br>  url,<br>  cache: false,<br>  query: {<br>   platform: Platform.OS,<br>  },<br> }<br>})</pre><p>개발시에는 localhost:9001, localhost:9002 주소를 사용하도록 하였기 때문에 host의 dev 서버 이외에도 각각의 모듈의 서버를 실행해주어야합니다. (host 앱은 기본적으로 localhost:8081 주소를 사용합니다.)</p><pre>//package.json<br>&quot;start&quot;: &quot;react-native webpack-start --port 9001&quot;,</pre><pre>$app1 % yarn start<br>&gt;Server listening at http://[::1]:9001<br>$app2 % yarn start<br>&gt;Server listening at http://[::1]:9002</pre><h4>모듈 expose 설정</h4><p>각 컨테이너에서는 host 에서 요청할 모듈을 expose 해주어야합니다.</p><pre>//app1/webpack.config.js<br>plugins: [<br>      new Repack.plugins.ModuleFederationPlugin({<br>        name: &#39;app1&#39;,<br>        exposes: {<br>          &#39;./App&#39;: &#39;./src/App.tsx&#39;,<br>        },<br><br>//app2/webpack.config.js<br>plugins: [<br>      new Repack.plugins.ModuleFederationPlugin({<br>        name: &#39;app2&#39;,<br>        exposes: {<br>          &#39;./App&#39;: &#39;./src/App.tsx&#39;,<br>        },</pre><p>보신 코드를 통해 코드 스플리팅과 모듈을 원격으로 불러오도록 구성이 가능하게 됩니다.</p><h4>캐싱</h4><p>remote 모듈은 요청 시 네트워크를 통해 다운로드를 하기 때문에 최적화를 위해 캐싱을 활용할 수 있습니다. 한번 다운로드된 모듈은 ScriptManager에 의해 캐싱되어 이후에 다시 요청을 할 때에는 다운로드를 하지 않습니다. 그렇다면 업데이트된 모듈을 다운로드하기 위해서는 어떻게 해야 할까요?<br>캐싱된 모듈을 사용하지 않고 모듈을 다시 다운로드하기 위해서 다음과 같은 방법을 사용할 수 있습니다.</p><ul><li>url 변경 : 기본적으로 ScriptManager 는 method/url/query/header or body를 비교하여 다운로드가 필요한지 결정합니다.</li><li>서버 캐싱 Invalidation: Cloudfront 등 을사용하여 서버에서 캐싱을 하고 있다면 Invalidation을 통해 캐시를 새로 다운로드하도록할 수 있습니다.</li><li>invalidateScripts 메서드 사용: <a href="https://re-pack.netlify.app/docs/api/repack/client/classes/ScriptManager#invalidatescripts">ScriptManager.shared.invalidateScripts</a> 메서드를 활용하여 캐시를 제거할 수 있습니다.</li></ul><h4>Re.Pack 앱 배포</h4><p>Re.Pack 앱을 배포할때에는 host 앱과 remote 모듈을 배포하는 방식이 다릅니다. host 앱을 배포하기 위하여 번들링 된 .ipa, .apk 파일을 만들어야 합니다.<br>이때 ios에서는 main.jsbundle 파일이 필요로 하는데 <strong>webpack-bundle</strong> 커맨드를 통해 번들을 생성하여 main.jsbundle 로 지정해주어야 합니다.</p><pre>//기존 react-native main.jsbundle 생성<br>&quot;build:ios&quot;: &quot;react-native bundle --entry-file=&#39;index.js&#39; --bundle-output=&#39;./ios/main.jsbundle&#39; --reset-cache --dev=false --platform=&#39;ios&#39;&quot;,<br>//repack에서 bundle 생성<br>&quot;bundle:ios&quot;: &quot;react-native webpack-bundle --platform ios --entry-file index.js  --bundle-output ./ios/main.jsbundle --dev=false&quot;,</pre><p>remote 모듈을 배포를 할 때에도 <strong>webpack-bundle</strong> 커맨드를 통해 청크를 생성하여야 합니다. webpack-bundle을 통해 생성된 build는 다음과 같은 구조를 가지게 됩니다.</p><pre>/build<br>  ㄴ ios<br>    ㄴ assets<br>    ㄴ app.container.bundle<br>    ㄴ app.container.bundle.map<br>    ㄴ index.bundle<br>    ㄴ index.bundle.map<br>    ㄴ src_App_tsx.chunk.bundle<br>    ㄴ src_App_tsx.chunk.bundle.map   </pre><p>생성된 청크를 서버에 업로드하여 다운로드할 수 있도록 합니다. 저의 경우 S3에 업로드하였습니다.</p><h4>Re.Pack 사용시 고려해야할 사항</h4><p>Re.Pack을 적용하고 사용해 보면서 겪은 것들이 많았습니다.</p><ul><li>모듈 페더레이션에 대한 이해도가 필요하였고, 기존 앱을 어떻게 분할하고 구성할 것인지 많은 고민이 필요하였습니다.</li><li>초기 설정을 하는 과정에서 react-native와 native 라이브러리와의 호환성으로 인해 버전을 수시로 변경하여야 했습니다.</li><li>기존의 metro 번들러에 비하여 속도적으로 느렸지만 리소스를 분리하여 빌드하기에 더 빠르게 빌드할 수 있었습니다.</li><li>remote 모듈을 사용하면서 네트워크 비용이 발생하게 되었습니다.</li><li>앱의 개수가 늘어나게 되면서 테스트 앱의 관리비용이 증가하였습니다.</li><li>에러 발생 시 생태계의 도움을 받기 힘들어 많은 시간이 필요했습니다.</li></ul><h4>마치며</h4><p>react-native에 모듈 페더레이션을 도입하기 위해 Re.Pack을 사용한 경험을 소개해 드렸습니다.<br>모듈 페더레이션은 react-native를 사용하고 계시는 많은 개발자들에게 성숙한 서비스의 개발환경 개선을 위한 좋은 방안이라고 생각합니다. 이 글에서 제시하는 Re.Pack을 통해 모듈 페더레이션을 사용해 보시면 좋을 것 같습니다.</p><p>출처:<br>사진-1: <a href="https://rangle.io/blog/module-federation-federated-application-architectures">https://rangle.io/blog/module-federation-federated-application-architectures</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=12288f15e617" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/react-native-%EC%97%90-%EB%AA%A8%EB%93%88-%ED%8E%98%EB%8D%94%EB%A0%88%EC%9D%B4%EC%85%98-%EB%8D%94%ED%95%98%EA%B8%B0-with-repack-12288f15e617">react-native 에 모듈 페더레이션 더하기 (with. RePack)</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[쉽고 빠른 NodeJS 부하테스트 툴, autocannon]]></title>
            <link>https://medium.com/zigbang/%EC%89%BD%EA%B3%A0-%EB%B9%A0%EB%A5%B8-nodejs-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%88%B4-autocannon-c860886d6f15?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/c860886d6f15</guid>
            <category><![CDATA[부하-테스트]]></category>
            <category><![CDATA[test]]></category>
            <category><![CDATA[stress-test]]></category>
            <category><![CDATA[autocannon]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Kay Hwang]]></dc:creator>
            <pubDate>Thu, 12 Oct 2023 02:15:40 GMT</pubDate>
            <atom:updated>2023-10-12T02:15:40.580Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ff-3GWSIZ2lRdQLj7LymBw.png" /></figure><p>개발단계에서 부하를 발생시켜 성능테스트를 해야 할 경우가 간혹 있습니다. 이 경우 NodeJS 부하테스트 툴인 autocannon을 주로 사용하는 편인데요. 오늘은 부하테스트가 필요한 실제와 가까운 사례를 한 가지 들어 보면서 autocannon을 사용하는 법을 소개해 드리도록 하겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WwMZSdvMIUQECU7V" /></figure><p>비즈니스 로직을 만드는 과정에서 만약 두 위치 거리를 계산하는 것이 필요하다고 가정해 보겠습니다. 이 문제를 해결하기 위한 방법은 크게 두 가지로 직접 구현하거나, 검증된 오픈소스를 사용하거나 일 것 같습니다.</p><p>직접 구현하는 것도 좋지만 구면에서 <a href="https://en.wikipedia.org/wiki/Haversine_formula">두 지점간거리를 구하는 공식</a>이 생각보다 복잡하고 이 복잡한 수식을 코드로 옮기다가 실수가 발생할 수도 있을 것 같아 검증된 오픈소스 라이브러리를 사용하기로 했습니다.</p><p>두 지점간 거리를 구하는 오픈소스 라이브리리를 검색해보니 다양하게 나옵니다 .여러 오픈소스 라이브러리 중 <a href="https://github.com/manuelbieh/geolib">geolib</a>, <a href="https://github.com/njj/haversine">haversine</a> 그리고 <a href="https://github.com/mapbox/cheap-ruler">cheap-ruler</a> 이 셋이 괜찮아 보였습니다. 셋 중 계산결과의 정확도 보다는 좋은 성능을 가진 라이브러리를 선택하기로 결정했습니다.</p><p><a href="https://nodejs.org/en/docs/guides/dont-block-the-event-loop">NodeJS는 다수 클라이언트의 요청을 제한된 쓰레드로 처리하기 때문에 Event Loop를 블록시킨다면 다른 클라이언트 요청도 블록</a>되기 때문에 연산을 할 때 리소스를 많이 사용할 것 같은 라이브러리를 사용할 때 성능을 검증할 필요가 있습니다.</p><p>먼저 두 지점간거리를 구하는 NestJS HTTP API 3개를 만들어 보겠습니다. /distance1, /distance2, /distance3 3가지 path를 만들었고 /distance1은 haversine, /distance2는 geoLib 리고 /distance3은 CheapRuler를 사용합니다.</p><h4>controller</h4><pre><br>import { Controller, Get, Query } from &#39;@nestjs/common&#39;;<br>import { AppService } from &#39;./app.service&#39;;<br><br>class GetDistanceRequest {<br>  point1Lat: number;<br>  point1Lng: number;<br>  point2Lat: number;<br>  point2Lng: number;<br>}<br><br>@Controller()<br>export class AppController {<br>  constructor(private readonly appService: AppService) {}<br><br>  @Get(&#39;/distance1&#39;)<br>  getDistance1(@Query() query: GetDistanceRequest) {<br>    return this.appService.getDistanceByHaversine(<br>      { lat: query.point1Lat, lng: query.point1Lng },<br>      { lat: query.point2Lat, lng: query.point2Lng },<br>    );<br>  }<br><br>  @Get(&#39;/distance2&#39;)<br>  getDistance2(@Query() query: GetDistanceRequest) {<br>    return this.appService.getDistanceByGeolib(<br>      { lat: query.point1Lat, lng: query.point1Lng },<br>      { lat: query.point2Lat, lng: query.point2Lng },<br>    );<br>  }<br><br>  @Get(&#39;/distance3&#39;)<br>  getDistance3(@Query() query: GetDistanceRequest) {<br>    return this.appService.getDistanceByCheapRuler(<br>      { lat: query.point1Lat, lng: query.point1Lng },<br>      { lat: query.point2Lat, lng: query.point2Lng },<br>    );<br>  }<br>}</pre><h4>service</h4><pre>import { Injectable } from &#39;@nestjs/common&#39;;<br>import * as geolib from &#39;geolib&#39;;<br>import * as haversine from &#39;haversine&#39;;<br>import * as CheapRuler from &#39;cheap-ruler&#39;;<br><br>export class Point {<br>  lat: number;<br>  lng: number;<br>}<br><br>@Injectable()<br>export class AppService {<br>  getDistanceByHaversine(point1: Point, point2: Point) {<br>    const distance = haversine(<br>      { latitude: point1.lat, longitude: point1.lng },<br>      { latitude: point2.lat, longitude: point2.lng },<br>      { unit: &#39;meter&#39; },<br>    );<br><br>    return distance;<br>  }<br><br>  getDistanceByGeolib(point1: Point, point2: Point): number {<br>    const distance = geolib.getDistance(point1, point2);<br><br>    return distance;<br>  }<br><br>  getDistanceByCheapRuler(point1: Point, point2: Point): number {<br>    // eslint-disable-next-line @typescript-eslint/ban-ts-comment<br>    // @ts-expect-error<br>    const ruler = new CheapRuler(point1.lat, &#39;meters&#39;);<br>    const distance = ruler.distance(<br>      [point1.lng, point1.lat],<br>      [point2.lng, point2.lat],<br>    );<br><br>    return distance;<br>  }<br>}</pre><p>앱을 실행시킨 후 3가지 경우를 테스트해 보니 각 결과는 아래와 같았습니다.</p><pre>$ curl &#39;localhost:3000/distance1?point1Lat=37.497952&amp;point1Lng=127.027619&amp;point2Lat=37.508872&amp;point2Lng=127.063186&#39;<br>3364.237180878499<br><br>$ curl &#39;localhost:3000/distance2?point1Lat=37.497952&amp;point1Lng=127.027619&amp;point2Lat=37.508872&amp;point2Lng=127.063186&#39;                                       <br>3368<br><br>curl &#39;localhost:3000/distance3?point1Lat=37.497952&amp;point1Lng=127.027619&amp;point2Lat=37.508872&amp;point2Lng=127.063186&#39;<br>3370.5534698980437</pre><p>두 지점은 사실 지하철 2호선 강남역과 삼성역이었습니다. 네이버 지도에서 둘 간 거리는 3.4km 인것으로 확인돼 계산 결과에서 셋의 라이브러리 모두 가까운 거리를 계산하는데 있어서 크게 오차를 가지지는 않을 것 같습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aBNwmT2kdJy2i05la8FSYA.png" /></figure><h3>autocannon</h3><p><a href="https://github.com/mcollina/autocannon#readme">autocannon</a>은 JavaScript로 만들어진 http API 부하테스트 툴입니다. 설치 후 cli 또는 Node.JS 라이브러리 형태로 사용가능합니다.</p><p>/distance1을 테스트 하기 위한 라이브러리로 형태의 코드는 다음과 같습니다.</p><pre>import autocannon from &quot;autocannon&quot;;<br><br>async function main() {<br>  const instance = autocannon({<br>    url: &quot;http://localhost:3000/distance1?point1Lat=37.497952&amp;point1Lng=127.027619&amp;point2Lat=37.508872&amp;point2Lng=127.063186&quot;,<br>  }, finishedBench);<br><br>  autocannon.track(instance, {<br>    renderProgressBar: true,<br>    renderLatencyTable: true,<br>    renderResultsTable: true,<br>  });<br><br>  function finishedBench (err: any, res: any) {<br>    console.log(&#39;finished bench&#39;, err, res)<br>  }<br>}<br><br>main();</pre><p>결과는 아래와 같이 출력되는데요. 첫 번째 표는 request latency, 두 번째 표는 request volume 입니다.</p><p>request latency 표는 요청에 대한 응답속도라고 볼 수 있습니다. 2.5%는 빠른 상위 latency 2.5%를, 50%는 latency의 중앙값을, 97.5%는 느린 하위 latency 그리고 99%는 가장 느린 백분위의 latency 를 나타냅니다.</p><p>request volumn 표는 초당 전송된 요청의 수, 다운로드된 byte 수를 보여줍니다. 그리고 매 초당 한 번씩 샘플링 됩니다. 숫자가 높을 수록 더 많이 처리가능한, 높은 성능을 가진다고 볼 수 있습니다. request latency와 달리 1%는 가장 느린 경우를 나타내며 %가 올라갈수록 상위로 빨라지는 경우라고 볼 수 있습니다.</p><pre>Running 10s test @ http://localhost:3000/distance1?point1Lat=37.497952&amp;point1Lng=127.027619&amp;point2Lat=37.508872&amp;point2Lng=127.063186<br>10 connections<br><br><br>┌─────────┬──────┬──────┬───────┬──────┬────────┬─────────┬───────┐<br>│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg    │ Stdev   │ Max   │<br>├─────────┼──────┼──────┼───────┼──────┼────────┼─────────┼───────┤<br>│ Latency │ 0 ms │ 0 ms │ 2 ms  │ 3 ms │ 0.2 ms │ 0.62 ms │ 30 ms │<br>└─────────┴──────┴──────┴───────┴──────┴────────┴─────────┴───────┘<br>┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬────────┬─────────┐<br>│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%  │ Avg     │ Stdev  │ Min     │<br>├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤<br>│ Req/Sec   │ 11527   │ 11527   │ 14495   │ 15087  │ 14170.4 │ 1131.5 │ 11526   │<br>├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼────────┼─────────┤<br>│ Bytes/Sec │ 2.82 MB │ 2.82 MB │ 3.55 MB │ 3.7 MB │ 3.47 MB │ 277 kB │ 2.82 MB │<br>└───────────┴─────────┴─────────┴─────────┴────────┴─────────┴────────┴─────────┘<br><br>Req/Bytes counts sampled once per second.<br># of samples: 10<br><br>┌────────────┬──────────────┐<br>│ Percentile │ Latency (ms) │<br>├────────────┼──────────────┤<br>│ 0.001      │ 0            │<br>├────────────┼──────────────┤<br>│ 0.01       │ 0            │<br>├────────────┼──────────────┤<br>│ 0.1        │ 0            │<br>├────────────┼──────────────┤<br>│ 1          │ 0            │<br>├────────────┼──────────────┤<br>│ 2.5        │ 0            │<br>├────────────┼──────────────┤<br>│ 10         │ 0            │<br>├────────────┼──────────────┤<br>│ 25         │ 0            │<br>├────────────┼──────────────┤<br>│ 50         │ 0            │<br>├────────────┼──────────────┤<br>│ 75         │ 0            │<br>├────────────┼──────────────┤<br>│ 90         │ 1            │<br>├────────────┼──────────────┤<br>│ 97.5       │ 2            │<br>├────────────┼──────────────┤<br>│ 99         │ 3            │<br>├────────────┼──────────────┤<br>│ 99.9       │ 4            │<br>├────────────┼──────────────┤<br>│ 99.99      │ 12           │<br>├────────────┼──────────────┤<br>│ 99.999     │ 29           │<br>└────────────┴──────────────┘<br><br>142k requests in 10.01s, 34.7 MB read</pre><p>위의 예에서는 요청파라미터를 쿼리스트링에 고정하여서 매번 요청하였는데, 매 번 요청 마다 값을 바꿔서 테스트 해 볼 수도 있습니다. 그렇게 하기 위해서는 아래 코드에서 볼 수 있듯이 requests에서 요청 전 쿼리스트링을 세팅하도록 설정하면 됩니다. 개인적으로 요청 파라미터를 요청마다 랜덤하게 바꿀 수 있다는 점 때문에 다른 툴에 비해 autocannon을 주로 사용합니다.</p><pre>import autocannon from &quot;autocannon&quot;;<br>import qs from &quot;node:querystring&quot;;<br>import * as _ from &quot;lodash&quot;;<br><br>function randomLat() {<br>    return _.random(36.0, 37.5)<br>}<br><br>function randonLng() {<br>    return _.random(125.5, 128.5)<br>}<br><br>async function main() {<br>  const instance = autocannon({<br>    url: &quot;http://localhost:3000/distance1&quot;,<br>    requests: [<br>        {<br>            path: &quot;&quot;,<br>            method: &quot;GET&quot;,<br>            // @ts-ignore<br>            setupRequest: (request, context) =&gt; {<br>              const params = {<br>                point1Lat: randomLat(),<br>                point1Lng: randonLng(),<br>                point2Lat: randomLat(),<br>                point2Lng: randonLng(),<br>              };<br>              const queryString = qs.encode(params);<br>              <br>              request.path = &quot;http://localhost:3000/distance1?&quot; + queryString;<br>              return request;<br>            },<br>          },<br>    ]<br>}, finishedBench);<br><br>  autocannon.track(instance, {<br>    renderProgressBar: true,<br>    renderLatencyTable: true,<br>    renderResultsTable: true,<br>  });<br><br>  function finishedBench (err: any, res: any) {<br>    console.log(&#39;finished bench&#39;, err, res)<br>  }<br>}<br><br>main();</pre><p>그 외에 얼마나 테스트를 지속할 것인지(duration), 몇 회 테스트할 것인지(amount), 동시 연결을 몇 개 할 것인지(connections), 초당 요청 수를 몇 회로 제한할 것인지(connectionRate) 등 세부 설정도 가능합니다.</p><p>예를 들어, 60초 동안 테스트를 하고 싶다면 아래와 같이 duration: 60을 추가해 주면 됩니다.</p><pre>  const instance = autocannon({<br>    url: &quot;http://localhost:3000/distance1&quot;,<br>    duration: 60, // 60초 동안 테스트<br>    requests: [<br>    ...</pre><h3>테스트 결과</h3><p>/distance1, /distance2 그리고 /distance3을 60초 동안 위경도를 무작위로 바꿔가면서 테스트한 결과입니다.</p><h4>1. /distance1</h4><pre><br>Running 60s test @ http://localhost:3000/distance1<br>10 connections<br><br><br>┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐<br>│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%   │ Avg     │ Stdev   │ Max    │<br>├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤<br>│ Latency │ 0 ms │ 0 ms │ 5 ms  │ 10 ms │ 0.56 ms │ 4.14 ms │ 358 ms │<br>└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘<br>┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐<br>│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │<br>├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤<br>│ Req/Sec   │ 243     │ 307     │ 10775   │ 14207   │ 9183.5  │ 4386.86 │ 243     │<br>├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤<br>│ Bytes/Sec │ 59.7 kB │ 75.4 kB │ 2.64 MB │ 3.49 MB │ 2.25 MB │ 1.08 MB │ 59.7 kB │<br>└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘<br><br>Req/Bytes counts sampled once per second.<br># of samples: 60<br><br>┌────────────┬──────────────┐<br>│ Percentile │ Latency (ms) │<br>├────────────┼──────────────┤<br>│ 0.001      │ 0            │<br>├────────────┼──────────────┤<br>│ 0.01       │ 0            │<br>├────────────┼──────────────┤<br>│ 0.1        │ 0            │<br>├────────────┼──────────────┤<br>│ 1          │ 0            │<br>├────────────┼──────────────┤<br>│ 2.5        │ 0            │<br>├────────────┼──────────────┤<br>│ 10         │ 0            │<br>├────────────┼──────────────┤<br>│ 25         │ 0            │<br>├────────────┼──────────────┤<br>│ 50         │ 0            │<br>├────────────┼──────────────┤<br>│ 75         │ 0            │<br>├────────────┼──────────────┤<br>│ 90         │ 1            │<br>├────────────┼──────────────┤<br>│ 97.5       │ 5            │<br>├────────────┼──────────────┤<br>│ 99         │ 10           │<br>├────────────┼──────────────┤<br>│ 99.9       │ 44           │<br>├────────────┼──────────────┤<br>│ 99.99      │ 181          │<br>├────────────┼──────────────┤<br>│ 99.999     │ 330          │<br>└────────────┴──────────────┘<br><br>551k requests in 60.03s, 135 MB read</pre><h4>2. /distance2</h4><pre>Running 60s test @ http://localhost:3000/distance2<br>10 connections<br><br><br>┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐<br>│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%   │ Avg     │ Stdev   │ Max    │<br>├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤<br>│ Latency │ 0 ms │ 0 ms │ 5 ms  │ 10 ms │ 0.54 ms │ 3.81 ms │ 289 ms │<br>└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘<br>┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐<br>│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │<br>├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤<br>│ Req/Sec   │ 252     │ 337     │ 11311   │ 14191   │ 9314.02 │ 4482.55 │ 252     │<br>├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤<br>│ Bytes/Sec │ 61.9 kB │ 82.8 kB │ 2.78 MB │ 3.48 MB │ 2.29 MB │ 1.1 MB  │ 61.9 kB │<br>└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘<br><br>Req/Bytes counts sampled once per second.<br># of samples: 60<br><br>┌────────────┬──────────────┐<br>│ Percentile │ Latency (ms) │<br>├────────────┼──────────────┤<br>│ 0.001      │ 0            │<br>├────────────┼──────────────┤<br>│ 0.01       │ 0            │<br>├────────────┼──────────────┤<br>│ 0.1        │ 0            │<br>├────────────┼──────────────┤<br>│ 1          │ 0            │<br>├────────────┼──────────────┤<br>│ 2.5        │ 0            │<br>├────────────┼──────────────┤<br>│ 10         │ 0            │<br>├────────────┼──────────────┤<br>│ 25         │ 0            │<br>├────────────┼──────────────┤<br>│ 50         │ 0            │<br>├────────────┼──────────────┤<br>│ 75         │ 0            │<br>├────────────┼──────────────┤<br>│ 90         │ 1            │<br>├────────────┼──────────────┤<br>│ 97.5       │ 5            │<br>├────────────┼──────────────┤<br>│ 99         │ 10           │<br>├────────────┼──────────────┤<br>│ 99.9       │ 45           │<br>├────────────┼──────────────┤<br>│ 99.99      │ 164          │<br>├────────────┼──────────────┤<br>│ 99.999     │ 276          │<br>└────────────┴──────────────┘<br><br>559k requests in 60.03s, 137 MB read</pre><h4>3. /distance3</h4><pre>Running 60s test @ http://localhost:3000/distance3<br>10 connections<br><br><br>┌─────────┬──────┬──────┬───────┬───────┬─────────┬─────────┬────────┐<br>│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%   │ Avg     │ Stdev   │ Max    │<br>├─────────┼──────┼──────┼───────┼───────┼─────────┼─────────┼────────┤<br>│ Latency │ 0 ms │ 0 ms │ 5 ms  │ 10 ms │ 0.55 ms │ 3.67 ms │ 290 ms │<br>└─────────┴──────┴──────┴───────┴───────┴─────────┴─────────┴────────┘<br>┌───────────┬───────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐<br>│ Stat      │ 1%    │ 2.5%    │ 50%     │ 97.5%  │ Avg     │ Stdev   │ Min     │<br>├───────────┼───────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤<br>│ Req/Sec   │ 289   │ 293     │ 11543   │ 14255  │ 9292.22 │ 4581.79 │ 289     │<br>├───────────┼───────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤<br>│ Bytes/Sec │ 71 kB │ 71.9 kB │ 2.83 MB │ 3.5 MB │ 2.28 MB │ 1.12 MB │ 70.9 kB │<br>└───────────┴───────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘<br><br>Req/Bytes counts sampled once per second.<br># of samples: 60<br><br>┌────────────┬──────────────┐<br>│ Percentile │ Latency (ms) │<br>├────────────┼──────────────┤<br>│ 0.001      │ 0            │<br>├────────────┼──────────────┤<br>│ 0.01       │ 0            │<br>├────────────┼──────────────┤<br>│ 0.1        │ 0            │<br>├────────────┼──────────────┤<br>│ 1          │ 0            │<br>├────────────┼──────────────┤<br>│ 2.5        │ 0            │<br>├────────────┼──────────────┤<br>│ 10         │ 0            │<br>├────────────┼──────────────┤<br>│ 25         │ 0            │<br>├────────────┼──────────────┤<br>│ 50         │ 0            │<br>├────────────┼──────────────┤<br>│ 75         │ 0            │<br>├────────────┼──────────────┤<br>│ 90         │ 1            │<br>├────────────┼──────────────┤<br>│ 97.5       │ 5            │<br>├────────────┼──────────────┤<br>│ 99         │ 10           │<br>├────────────┼──────────────┤<br>│ 99.9       │ 45           │<br>├────────────┼──────────────┤<br>│ 99.99      │ 142          │<br>├────────────┼──────────────┤<br>│ 99.999     │ 254          │<br>└────────────┴──────────────┘<br><br>558k requests in 60.03s, 137 MB read</pre><p>60초 동안 1, 2, 3 경우 각 551k, 559k, 558k가 처리되었고 request latency는 평균 0.56ms, 0.54ms, 0.55ms로 큰 차이가 없습니다. 결론적으로 3 개의 라이브러리 모두 크게 성능의 차이는 없는 것 같습니다.</p><p>그렇다면 오픈소스 커뮤니티의 크기와 활동정도, 정확도, 사용성 등에 따라 선호하는 것을 선택하면 될 것 같습니다.</p><h3>결론</h3><p>3가지 오픈소스 라이브러리의 처리 속도를 비교하기 위해 NodeJS 부하테스트 툴인 autocannon을 사용해보았고 각 요청마다 파라미터를 세팅하는 방법, 세부적인 테스트 옵션, 결과 테이블에 대한 내용을 소개해 드렸습니다.</p><p>기회가 된다면 autocannon을 한 번 쯤은 사용해보시길 추천드립니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c860886d6f15" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/%EC%89%BD%EA%B3%A0-%EB%B9%A0%EB%A5%B8-nodejs-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%88%B4-autocannon-c860886d6f15">쉽고 빠른 NodeJS 부하테스트 툴, autocannon</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[RDS 모니터링을 위한 나만의 CloudWatch Dashboard 만들기]]></title>
            <link>https://medium.com/zigbang/rds-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%82%98%EB%A7%8C%EC%9D%98-cloudwatch-dashboard-%EB%A7%8C%EB%93%A4%EA%B8%B0-f1960e10f7b3?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/f1960e10f7b3</guid>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[James Lim]]></dc:creator>
            <pubDate>Mon, 18 Sep 2023 02:35:59 GMT</pubDate>
            <atom:updated>2023-09-18T02:35:59.494Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GtXAeZtKf0gEFGScZkef9g.png" /></figure><p>안녕하세요 직방 DataManagement팀 DBA James 입니다.</p><p>AWS RDS 모니터링을 위한 다양한 솔루션들이 존재하지만 그중 가장 기본이라고 할 수 있는 CloudWatch를 활용한 모니터링 이야기를 해보려 합니다.<br>(aurora RDS의 경우로 이야기하겠습니다)</p><p>AWS RDS Console에서 RDS의 Status 모니터링을 해오셨던 분이라면 Monitoring 탭에서 제공하는 CloudWatch의 지표들을 많이 접해오셨을 것이라고 생각합니다.</p><p>일반적으로 RDS Monitoring에서 기본적으로 제공되는 CloudWatch 지표는 다음과 같습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z3k38Hi8Iw3l2RV6P5UrJA.png" /><figcaption><strong><em>AbortedClients</em></strong>부터 <strong><em>WriteThroughput</em></strong>까지 알파벳 순으로 정렬된 100개의 지표</figcaption></figure><p>다양한 지표들을 제공해 주지만 모니터링을 하면서 우리는 이렇게 많은 지표를 모두 볼 수도 없고, 항상 필요하지는 않습니다.</p><p>다양한 지표의 홍수 속에서 <strong><em>필요한 정보를 선별</em></strong>하고 <strong><em>선택과 집중</em></strong>이 필요한 부분이죠.</p><blockquote>ChatGPT에게 Aurora RDS를 모니터링하는데 중요한 지표를 물어보았습니다.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/850/1*NfUkXSX_nkWZAVHAFRCB5g.png" /><figcaption>10여 가지의 지표를 추천.ai</figcaption></figure><p>100가지 지표 중 내가 원하는 중요한 지표를 보기 위해서는 열심히 마우스 휠을 굴리거나 지표명을 검색하여 원하는 정보의 확인이 가능합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MP-EwAKBlMu85bmfEpBlXQ.png" /><figcaption>CPUUtilization 지표를 확인! CPUUtilization 지표만 확인..</figcaption></figure><p>DB 모니터링 시, 대부분의 문제는 복합적인 원인으로 인해 발생하는 경우가 많기 때문에 하나의 지표로는 상태 파악이 쉽지 않습니다. <br><br>하지만 안타깝게도 AWS RDS Console에서 제공하는 CloudWatch에서는 내가 원하는 지표들을 한눈에 확인이 불가능하고 레이아웃 편집이 불가능하기 때문에 모니터링으로서의 기능은 떨어진다고 할 수 있습니다.</p><p>그렇다면 내가 필요한 지표의 정보만 한눈에 볼 수 있는 조금 더 간편하게 모니터링을 할 수 있게 만들어주는 나만의 Dashboard를 만들어 보겠습니다.</p><h4>나만의 Dashboard 만들기</h4><p>CloudWatch Dashboard 자체를 만드는 법은 어렵지 않습니다.</p><blockquote>다시 한번 ChatGPT에게 물어보았습니다.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/816/1*FN2TEvmnXjrMOWeAUxf8_g.png" /><figcaption>참 쉽죠..?</figcaption></figure><p>위 방법을 따라서 Dashboard를 생성하고 ChatGPT가 추천해 준 중요 지표를 추가해 보겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YrUslbOwAYcfv6t77WRZgg.png" /><figcaption>다수의 DB 모니터링이 가능합니다.</figcaption></figure><p>다수의 DB를 한눈에 모니터링이 가능한 나만의 Dashboard 가 완성되었습니다.</p><p><strong>끝! 이라고 하기에는 지표의 그래프만 봐서 현재 상태가 정상인지 혹은 주의 깊게 지켜봐야 하는 상태인지 파악이 어렵지 않으신가요?</strong></p><p>이제는 좀 더 멀리서 봐도 상태를 파악할 수 있는 Dashboard를 만들기 위한 추가 설정을 진행해 보겠습니다.</p><h4><strong><em>첫 번째, Horizontal thresholds(가로 주석/임계값)</em> 추가입니다.</strong></h4><p>Horizontal thresholds 추가는 가로주석을 통해 멀리서 Dashboard를 확인할 때 에도 경고 또는 위험 수준에 근접한 지 확인을 용이하게 하기 위한 기능입니다.</p><p>설정을 위한 방법은 대상 지표의 <strong>[Edit graph]</strong> 화면으로 이동 후<strong> [Options] </strong>탭에서 <strong>[Add Horizontal annotations] </strong>버튼을 클릭하여 항목을 추가/설정해줍니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*riueZVS9pIlpwyEyqit-ng.png" /><figcaption>Label과 색상, Value를 지표에 맞게 입력해 줍니다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LlIvUQoJHwS4LXwou-99Ag.png" /><figcaption>Warning과 Error 주석을 추가한 후의 Dashboard</figcaption></figure><p>지표의 변화가 현재 정상적인지, 가로 주석에 근접한 주의가 필요한 상황인지 파악이 용이해진 것을 확인할 수 있습니다.</p><h4>두 번째, Anomaly detection(이상 탐지) 추가입니다.</h4><p>CloudWatch Anomaly detection(이상 탐지)이란?</p><blockquote>지표에 대해 ‘이상 탐지’를 사용 설정하면 CloudWatch는 통계 및 기계 학습 알고리즘을 적용합니다. 이러한 알고리즘은 시스템 및 애플리케이션의 지표를 지속적으로 분석하고, 정상 기준을 결정하며, 최소한의 사용자 개입으로 이상을 나타냅니다.</blockquote><blockquote>알고리즘은 이상 탐지 모델을 생성합니다. 모델은 정상 지표 동작을 나타내는 예상 값의 범위를 생성합니다.</blockquote><blockquote><em>AWS Document 내용 발췌</em></blockquote><p>간단하게 얘기하면, 지표의 CloudWatch 지표를 분석하여 예상 값의 범위를 밴드로 표시하고, 지표의 실제 값이 이 밴드를 초과하면 빨간색으로 표시해주는 기능입니다.</p><p>적용을 통해 확인해 보겠습니다. 적용 순서를 이미지로 나열해 보겠습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*erNymkdFQ5Zjnyp8EljmGw.png" /><figcaption>1. 적용 지표의 View in metrics를 선택합니다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a5gvwxFlYFlpDOp5L74MOQ.png" /><figcaption>2. 대상 지표의 anomaly detection을 활성화 합니다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q2r8Jt7MmHN_YeiDP3iWjA.png" /><figcaption>3. 2개 DB의 DatabaseConnection 지표에 anomaly detection 적용이 완료되었습니다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kijPLmoZEBSCzeOxEnJcqw.png" /><figcaption>3–2. anomaly detection이 적용된 상세 지표화면 입니다.\</figcaption></figure><p>여기까지가 지표에 anomaly detection을 적용 완료한 상태로 이어서 Dashboard에도 적용 완료된 지표를 등록하면 작업이 완료됩니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OkDU_gPyzhxAO8x4ddiXlw.png" /><figcaption>1. [Actions]-[Add to dashboard] 기능을 사용하여 등록</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BMjqqDWS7prnnXDxs7WATg.png" /><figcaption>2. Dashboard를 선택 후 Add to dashboard를 클릭</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oQdQKL_N1gO3aoLL1QuLLg.png" /><figcaption>3. Dashboard에 적용 완료!</figcaption></figure><h4>마치며</h4><p>지금까지 나만의 Dashboard의 생성과 조금 더 편안한 모니터링을 위한 가로 주석과 이상탐지 두 가지 모니터링 보조 설정을 적용해 보았습니다.</p><p>물론 다양한 모니터링 솔루션이 많이 나와있지만, 자주 이용하는 AWS Console에서 제공하는 기능을 활용한 모니터링이 필요하신 분들에게 조금이라도 도움이 되었기를 바라며 글을 마치도록 하겠습니다.<br>긴 글을 읽어주셔서 감사합니다.</p><h4>참고</h4><ul><li><a href="https://chat.openai.com/">https://chat.openai.com/</a></li><li><a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html">https://docs.aws.amazon.com/ko_kr/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f1960e10f7b3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/rds-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%82%98%EB%A7%8C%EC%9D%98-cloudwatch-dashboard-%EB%A7%8C%EB%93%A4%EA%B8%B0-f1960e10f7b3">RDS 모니터링을 위한 나만의 CloudWatch Dashboard 만들기</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Tracing NodeJs Applications with OpenTelemetry]]></title>
            <link>https://medium.com/zigbang/tracing-nodejs-applications-with-opentelemetry-624958d38d4d?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/624958d38d4d</guid>
            <category><![CDATA[grafana]]></category>
            <category><![CDATA[backend]]></category>
            <category><![CDATA[opentelemetry]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[typescript]]></category>
            <dc:creator><![CDATA[Fabio Reis]]></dc:creator>
            <pubDate>Fri, 15 Sep 2023 00:28:17 GMT</pubDate>
            <atom:updated>2023-09-15T00:28:17.685Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-aPX6UHEgLfqawwTxSVBKg.png" /></figure><p>When we think about monitoring, the first thing that comes to our minds are logs and metrics. We oftentimes underestimate traces because of the extra work we need to set up traces, whereas metrics and logs on the other hand are often automatically provided when using any cloud platform.</p><p>The combination of metrics, logs, traces, and alarms is known as observability. Different from monitoring, which only tells us that there is something wrong within our system, for example with metrics, the goal of observability is to correlate the data collected trying to help us identify what, where, and why something is wrong.</p><p>In this blog post, we will show you how to collect traces by setting up a simple observability stack with node js and typescript. Although this is not the focus of this post, Grafana also works perfectly integrating with external metrics and logs you must already have, and we encourage you to take a look at their other integrations.</p><h3>Prerequisites</h3><p>We must install Grafana and Loki so we can add telemetry to our test application. For that reason, you should have the following tools installed. Please follow their respective installation guide.</p><ul><li><a href="https://nodejs.org/en">NodeJs</a> ≥ 18</li><li><a href="https://www.docker.com/products/docker-desktop/">Docker for Desktop</a> <br>(Kubernetes is included but you must enable it)</li><li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/">kubectl</a></li><li><a href="https://helm.sh/docs/intro/install/">Helm</a></li></ul><h3>Setting Helm Repositories and Containers</h3><p>Helm is a package manager that will help us set up our Kubernetes environment with minimal effort. After installing Helm, you can add Grafana and Tempo repositories with the following commands.</p><pre>#adding Grafana charts<br>helm repo add grafana https://grafana.github.io/helm-charts<br>helm repo update</pre><h4>Installing Tempo</h4><p>For this example, we will use the standard configuration of tempo, but if you wish to customize any values please check tempo helm <a href="https://github.com/grafana/helm-charts/blob/main/charts/tempo-distributed/values.yaml">documentation</a> and <a href="https://github.com/grafana/tempo/tree/main/example/helm">examples</a>. You can easily customize your distribution by creating a YAML file and applying it with Helm.</p><pre>helm upgrade --install tempo grafana/tempo</pre><p>Tempo supports many receivers and protocols and for the purpose of this tutorial, we will use OTLP receiver with HTTP protocol (port 4318). If you are interested in other receivers take a look o <a href="https://grafana.com/docs/tempo/latest/configuration/#distributor">Tempo&#39;s documentation</a>.</p><h4>Installing Grafana</h4><p>In order to install Grafana we need to configure our Helm template to add our tempo container as a data source, so we can visualize our traces on Grafana. For more information on Grafana customization please check Helm <a href="https://github.com/grafana/helm-charts/blob/main/charts/grafana/values.yaml">documentation</a>.</p><ul><li>Add the following content to a file called <strong>grafana-helm.yaml</strong></li></ul><pre>env:<br>  GF_AUTH_ANONYMOUS_ENABLED: true<br>  GF_AUTH_ANONYMOUS_ORG_ROLE: &#39;Admin&#39;<br>  GF_AUTH_DISABLE_LOGIN_FORM: true<br><br>datasources:<br>  datasources.yaml:<br>    apiVersion: 1<br><br>    datasources:<br>      - name: Tempo<br>        type: tempo<br>        access: proxy<br>        orgId: 1<br>        url: http://tempo:3100<br>        basicAuth: false<br>        isDefault: true<br>        version: 1<br>        editable: false<br>        apiVersion: 1<br>        uid: tempo</pre><ul><li>Install Grafana with the YAML file we just created</li></ul><pre>helm upgrade -f grafana-helm.yaml --install grafana grafana/grafana</pre><h4>Configure Nginx-controller</h4><p>We will need to access both our Grafana and our tempo services so therefore we need to expose the service with Nginx-controller and k8 ingress, so we can use it in our test application.</p><ol><li>Install Nginx-controller</li></ol><pre>helm upgrade --install ingress-nginx ingress-nginx \<br>  --repo https://kubernetes.github.io/ingress-nginx \<br>  --namespace ingress-nginx --create-namespace</pre><p>2. Creating Ingress for the services</p><ul><li>Save the following content to a file called <strong>ingress.yaml</strong></li></ul><pre>apiVersion: networking.k8s.io/v1<br>kind: Ingress<br>metadata:<br>  name: grafana<br>  namespace: default<br>  annotations:<br>    ingress.kubernetes.io/ssl-redirect: &quot;false&quot;<br>spec:<br>  ingressClassName: nginx<br>  rules:<br>    - http:<br>        paths:<br>          - pathType: Prefix<br>            backend:<br>              service:<br>                name: grafana<br>                port:<br>                  number: 80<br>            path: /<br>    - http:<br>        paths:<br>          - pathType: Prefix<br>            backend:<br>              service:<br>                name: tempo<br>                port:<br>                  number: 4318<br>            path: /v1/traces</pre><ul><li>Create ingress with kubectl</li></ul><pre>kubectl create -f ingress.yaml</pre><h4>Testing Grafana</h4><p>After completing the setup you will be able to access both services in the following addresses. If you wish to use a host instead you can change your ingress file as needed. Check K8 <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">documentation</a> for more details.</p><p><strong>Grafana</strong>: <a href="http://localhost">http://localhost</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VOTmDkPbA-xCFIprYuz_kw.png" /><figcaption>Sample of Grafana dashboard</figcaption></figure><p>You can also check all the Kubernetes resources we created with the following command:</p><pre>#services and pods<br>kubectl get all -A<br><br>#for our custom ingress<br>kubectl get ingress</pre><h3>Other Observaibility Integrations</h3><p>This is not the main focus of this tutorial, but if you wish you can also use metrics, logs, and alerts with Grafana. In order to do that you can use Prometheus for metrics, and Loki for logs for example. The installation process can be done in the same way we did with Helm. For more information check <a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus">Prometheus Helm</a> documentation and <a href="https://github.com/grafana/helm-charts/tree/main/charts/loki-stack">Loki Helm</a> documentation.</p><h3>Setting Up OpenTelemetry with NodsJs</h3><p>We want to add traces to any NodeJs API, so you can actually use your own project if you wish. If you don&#39;t already have a project you can use any open-source sample available on GitHub, like for example the cats API on NestJs repository:</p><p><a href="https://github.com/nestjs/nest/tree/master/sample/10-fastify">https://github.com/nestjs/nest/tree/master/sample/10-fastify</a></p><h4>Adding OpenTelemetry Dependencies</h4><p>Add the following dependencies to your project:</p><pre>npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-node @opentelemetry/sdk-trace-node</pre><h4>Adding Tracer and Auto-instrumentation</h4><p>On your src folder add a tracer.ts file with the following content:</p><pre>import { getNodeAutoInstrumentations } from &#39;@opentelemetry/auto-instrumentations-node&#39;;<br>import { OTLPTraceExporter } from &#39;@opentelemetry/exporter-trace-otlp-http&#39;;<br>import { NodeSDK } from &#39;@opentelemetry/sdk-node&#39;;<br>import {<br>  BasicTracerProvider,<br>  ConsoleSpanExporter,<br>  SimpleSpanProcessor,<br>} from &#39;@opentelemetry/sdk-trace-node&#39;;<br>import { Resource } from &#39;@opentelemetry/resources&#39;;<br>import { SemanticResourceAttributes } from &#39;@opentelemetry/semantic-conventions&#39;;<br><br>class Tracer {<br>  private sdk: NodeSDK | null = null;<br><br>  // url is optional and can be omitted - default is http://localhost:4318/v1/traces<br>  private exporter = new OTLPTraceExporter({ url: &quot;http://localhost/v1/traces&quot;});<br><br>  private provider = new BasicTracerProvider({<br>    resource: new Resource({<br>      [SemanticResourceAttributes.SERVICE_NAME]: &#39;DEMO-APP&#39;,<br>    }),<br>  });<br><br>  public init() {<br>    try {<br>      // export spans to console (useful for debugging)<br>      this.provider.addSpanProcessor(<br>        new SimpleSpanProcessor(new ConsoleSpanExporter()),<br>      );<br><br>      // export spans to opentelemetry collector<br>      this.provider.addSpanProcessor(new SimpleSpanProcessor(this.exporter));<br>      this.provider.register();<br><br>      this.sdk = new NodeSDK({<br>        traceExporter: this.exporter,<br>        instrumentations: [<br>          getNodeAutoInstrumentations({<br>            // Lets disable fs for now, otherwise we cannot see the traces we want,<br>            // You can disable or enable instrumentation as needed<br>            &#39;@opentelemetry/instrumentation-fs&#39;: { enabled: false },<br>          }),<br>        ],<br>      });<br><br>      this.sdk.start();<br><br>      console.info(&#39;The tracer has been initialized&#39;);<br>    } catch (e) {<br>      console.error(&#39;Failed to initialize the tracer&#39;, e);<br>    }<br>  }<br>}<br><br>export default new Tracer();</pre><h4>Initialize our Tracer</h4><p>Make sure you initialize your tracer before your server starts, otherwise, auto-instrumentation won&#39;t work properly. On your main.ts file (or your entry point class) add the following code.</p><pre>import tracer from &#39;./tracer&#39;<br>tracer.init()<br><br>//Other imports<br>...<br><br>async function bootstrap() {<br>...<br>}<br><br>bootstrap()</pre><h3>Testing our Telemetry</h3><p>After running your application let&#39;s use some of the APIs so we can generate some traces. For the cat API example, you can run one of the commands below.</p><pre># POST cats<br>curl -X POST http://localhost:3000/cats -H &quot;Content-Type: application/json&quot; -d &#39;{<br>    &quot;name&quot;:&quot;test&quot;,<br>    &quot;age&quot;: 18,<br>    &quot;breed&quot;: &quot;test&quot;<br>}&#39;<br><br># GET cats<br>curl  http://localhost:3000/cats</pre><h4>Check Telemetry on Grafana</h4><p>After collecting some data, let&#39;s check our traces on Grafana. On the <strong>Explore</strong> menu, Tempo will be selected automatically, but if not, select Tempo in the data source drop-down list.</p><p>On the query options, select search and input the name of the service we configured in our tracer.ts file (DEMO-APP). You will be able to see all the instrumentation collected with auto instrumentation.</p><p>Note that auto instrumentation collects different information for each library. For example, if you check our HTTP spans you will be able to see many of our request parameters. Now, if we were using MySQL, we would be able to see up to the query executed for that specific trace. For each instrumentation, check their respective documentation specifically.</p><p>If you are curious about which libraries are instrumented automatically, check the documentation on the <a href="https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node">@opentelemetry/auto-instrumentations-node npm</a> page.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zMscsl9_DcwL21DUiXPdBw.png" /><figcaption>Instrumentation sample</figcaption></figure><h4>Adding Manual Span</h4><p>For this tutorial, we&#39;ve only touched the auto-instrumentation setup, and even though this alone helps us to see the path our users take inside of our system, it does not always provide us with all the information we need.</p><p>OpenTelemetry also allows us to define our own custom spans and add them to our traces. This is useful for example when you need to add business information to the telemetry.</p><p>You can play around with your own custom spans by adding them to your API code. Follow the <a href="https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/tracing.md#acquiring-a-tracer">documentation</a> for more information.</p><h4>Extra (Telemetry Propagation)</h4><p>Trace opens a whole lot of possibilities for us. However, we only saw a small part of it in this tutorial. OpenTelemetry also works really well with distributed systems and provides us with tools to integrate APIs together in the same trace. This is extremely useful when working with microservice architecture, where multiple APIs are triggered for a single user action.</p><p>In order to connect microservices together, you should configure OpenTelemetry to send and receive traces to and from other APIs. You can easily achieve that using custom headers and openTelemetry node SDK.</p><p>OpenTelemetry provides us with some examples of how to set up our API to communicate with other services. Check the <a href="https://github.com/open-telemetry/opentelemetry-js/blob/6d174e2a0689d9386e1aaf69a3c774ce2a9cbca0/doc/propagation.md">documentation</a> on propagation for more details.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=624958d38d4d" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/tracing-nodejs-applications-with-opentelemetry-624958d38d4d">Tracing NodeJs Applications with OpenTelemetry</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[lambda@edge를 활용한 이미지 워터마킹]]></title>
            <link>https://medium.com/zigbang/lambda-edge%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9B%8C%ED%84%B0%EB%A7%88%ED%82%B9-41f1cb282682?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/41f1cb282682</guid>
            <category><![CDATA[watermarking]]></category>
            <category><![CDATA[aws-cloudfront]]></category>
            <category><![CDATA[lambda-edge]]></category>
            <category><![CDATA[image]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Austin]]></dc:creator>
            <pubDate>Mon, 10 Jul 2023 08:36:27 GMT</pubDate>
            <atom:updated>2023-07-10T08:36:26.908Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jsfnbUqgoCcZbt09uLPRzQ.png" /></figure><p>안녕하세요. 호갱노노 BE 팀의 오스틴입니다.</p><p>이번 포스팅에서는 AWS lambda@edge를 활용하여 이미지에 워터마킹 하는방법에 대해서 이야기해보겠습니다.</p><p>이미지 워터마킹을 다루기에 앞서 일반적으로 서비스에서는 이미지를 어떤식으로 저장하고 사용하는지를 먼저 다루어 보겠습니다.</p><h3>이미지 다루기</h3><p>우리가 접하는 많은 서비스에서 이미지는 모두 온라인상 어딘가에 저장되어 있고, 클라이언트가 요청할 때마다 이미지를 전송합니다.</p><p>아주 단순한 시스템에서는 서버내 폴더 어딘가에 저장해 놓았다가 내려주기도 하고, Web Server 와 WAS 가 분리된 구조에서는 정적인 파일(html, js, image)들을 Web Server 에 저장하는 방식으로 서버의 부하를 분산시키고 캐시를 사용하기도 합니다.</p><p>AWS 를 사용하는 경우에는 S3 를 image 서버처럼 활용하기도 합니다. 이와 같은 경우에는 image 를 서버로 업로드하는게 아니라, presigned URL 을 활용하여 클라이언트에서 S3로 직접 이미지를 upload 하는 방법을 주로 사용합니다. 다음 그림은 presigned URL 을 생성하는 람다를 활용한 s3 upload 방법입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/861/0*R-UI1adD3uYgsgFh.png" /><figcaption>그림1. <a href="https://aws.amazon.com/ko/blogs/korea/uploading-to-amazon-s3-directly-from-a-web-or-mobile-application/">람다를 활용한 presigned URL 취득 및 UPLOAD</a></figcaption></figure><h3>CloudFront 와 Lambda@Edge</h3><p>저희 호갱노노에서는 AWS 를 기본 서버 환경으로 사용하고있고 S3 에 이미지를 저장하고 있습니다.</p><p>그리고, 저장된 이미지는 AWS <a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/Introduction.html">CloudFront</a> 를 통해 사용자에게 전달됩니다. 다음은 CloudFront (이후 CF) 에 대한 AWS 의 설명입니다.</p><blockquote>Amazon CloudFront는 .html, .css, .js 및 이미지 파일과 같은 정적 및 동적 웹 콘텐츠를 사용자에게 더 빨리 배포하도록 지원하는 웹 서비스입니다.</blockquote><p>그럼 이제 S3 에 있는 이미지를 워터마킹해서 내려주는 방법을 알아보겠습니다.</p><p>저희는 CloudFront 를 통해 내려주는 이미지에 워터마킹을 표시하기 위해서 lambda@edge 를 사용하였습니다.</p><p>다음은 <a href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/lambda-edge.html">lambda@edge</a> 에 대한 AWS 에 설명입니다.</p><blockquote>Lambda@Edge를 사용하면 Node.js 및 Python Lambda 함수를 실행하여 CloudFront가 제공하는 콘텐츠를 사용자 지정하여 AWS 위치의 함수를 최종 사용자와 더 가깝게 실행할 수 있습니다. 이 함수는 서버 프로비저닝 또는 관리 없이 CloudFront 이벤트에 응답하여 실행됩니다. Lambda 함수를 사용하여 CloudFront 요청 및 응답을 다음과 같이 변경할 수 있습니다.</blockquote><p>그러니까 CF 에 들어오는 요청 전후로 람다함수(에지함수)를 이용하여 req/res 를 변경할 수 있습니다. 그리고, 워터마킹은 바로 이 람다함수를 작성하여 실행합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/545/0*bXEhIaOKnZv1lSZl.png" /><figcaption>그림2. CF 처리 FLOW</figcaption></figure><h3>워터 마킹을 위한 Lambda@Edge 설정</h3><ul><li>lambda 페이지로 이동 후 Region을 Virginia로 바꿉니다. 그리고 Create function을 클릭합니다. (lambda edge는 Virginia에서만 사용 가능)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DfR3joUOvPNHYqV5VeMKOg.png" /></figure><ul><li>필요한 내용을 채우고 <strong>Create Function</strong> 을 클릭합니다. 내용은 이름만 적어도 됩니다. (기존에 사용하던 role을 그대로 사용하고 싶다면 Execution role에서 Use an existing role을 선택합니다.)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*T8nKkZlcNfQezIcv_DAcSg.png" /></figure><ul><li>Configuration &gt; Permissions에서 해당 Role을 클릭합니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DQwUvMt2fI0cnFQKtzc3IQ.png" /></figure><ul><li>아래 화면에서 Policy 왼쪽 +를 누르면 json이 나오고 Edit을 눌러 아래와 같이 적어줍니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BEtl1jSXgV_GGVWcd-ZVhg.png" /></figure><pre>{<br>    &quot;Version&quot;: &quot;2012-10-17&quot;,<br>    &quot;Statement&quot;: [<br>        {<br>            &quot;Effect&quot;: &quot;Allow&quot;,<br>            &quot;Action&quot;: &quot;logs:CreateLogGroup&quot;,<br>            &quot;Resource&quot;: &quot;{resource arn을 적어주세요}&quot;<br>        },<br>        {<br>            &quot;Effect&quot;: &quot;Allow&quot;,<br>            &quot;Action&quot;: [<br>                &quot;iam:CreateServiceLinkedRole&quot;,<br>                &quot;lambda:GetFunction&quot;,<br>                &quot;lambda:EnableReplication*&quot;,<br>                &quot;cloudfront:UpdateDistribution&quot;,<br>                &quot;logs:CreateLogGroup&quot;,<br>                &quot;logs:CreateLogStream&quot;,<br>                &quot;logs:PutLogEvents&quot;,<br>                &quot;logs:DescribeLogStreams&quot;<br>            ],<br>            &quot;Resource&quot;: &quot;*&quot;<br>        }<br>    ]<br>}</pre><ul><li>Trust relationships 탭으로 이동해서 아래와 같이 수정합니다.<br>Service 목록에 “edgelambda.amazonaws.com”이 꼭 있어야 합니다.</li></ul><pre>{<br>    &quot;Version&quot;: &quot;2012-10-17&quot;,<br>    &quot;Statement&quot;: [<br>        {<br>            &quot;Effect&quot;: &quot;Allow&quot;,<br>            &quot;Principal&quot;: {<br>                &quot;Service&quot;: [<br>                    &quot;lambda.amazonaws.com&quot;,<br>                    &quot;edgelambda.amazonaws.com&quot;<br>                ]<br>            },<br>            &quot;Action&quot;: &quot;sts:AssumeRole&quot;<br>        }<br>    ]<br>}</pre><ul><li>코드를 작성합니다.코드가 너무 크다면 업로드 할 수 있습니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JX-NNTVx6dmpOqSKQxemmQ.png" /></figure><p><a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html">예제 함수들</a> 및 <a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-generating-http-responses.html">response 생성 및 업데이트</a> 을 확인할 수 있습니다.</p><p>다음은 실제 코드중 워터마킹하는 부분만 발췌하였습니다.</p><pre>/**<br> * watermark 생성 로직<br>**/<br>let resizedWaterMarkImage = sharp(&#39;watermark.png&#39;).rotate();<br>const width = imageMetaData.width;<br>const heigth = imageMetaData.height;<br><br>// ....<br>// 원본이미지에 맞게 워터마크의 크기를 조절한 후,<br>// ....<br>resizedWaterMarkImage = await resizedWaterMarkImage.resize(waterWidth, waterHeight, { fit: &#39;inside&#39; }).toBuffer();<br>await zigbangImage.composite([{ input: resizedWaterMarkImage, gravity: &#39;center&#39; }]);</pre><ul><li>그리고, 테스트를 해봅니다. (<a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html">이벤트 함수 예제</a>)<br>Test할 때 생기는 log는 확인이 가능합니다. CloudWatch Log group에 없다면 생성하면 됩니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mrYUwmcPqoxiYNp4YHuCgw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yaDX1rAe21TasOM_EV-Wsw.png" /></figure><ul><li>새 버전을 publish 합니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tTRvPV8EKSI-DJ-lohMoWg.png" /></figure><ul><li>publish 한 람다의 ARN을 복사 후 CF 에 붙입니다.<br>behavior 까지 새롭게 만드려면 Create behavior 를 하고 그게 아니라면 해당 Behavior를 수정합니다.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8RROi3DjKGmc725K5qmDTA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HN8WrOP-G0H_Of0__Ztp1g.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*A-OL3HL0UX4P-r1QuulUUQ.png" /></figure><ul><li>적용시 캐시 때문에 시간이 다소 소요될 수 있습니다. 그럼, 다음과 같이 호갱노노 이미지가 워터마크로 이미지에 표시됩니다.</li></ul><h3>결과물</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/690/1*Gkq0hgHHcJea9kieVCmHvw.png" /></figure><p>이상으로 S3 에 저장된 이미지를 CloudFront 의 lambda@edge 를 통해서 워터마킹 하는 방법에 대해서 알아봤습니다.</p><p>확실히 AWS 를 활용하면 많은 노력 없이도 쉽고 편리하게 서버에서 이미지를 다룰 수 있다는 점이 참 매력적인거 같습니다.</p><p>감사합니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=41f1cb282682" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/lambda-edge%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%9B%8C%ED%84%B0%EB%A7%88%ED%82%B9-41f1cb282682">lambda@edge를 활용한 이미지 워터마킹</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[코드 가독성을 높이는 젯브레인 계열 IDE 에디터 세팅과 플러그인 추천]]></title>
            <link>https://medium.com/zigbang/%EC%BD%94%EB%93%9C-%EA%B0%80%EB%8F%85%EC%84%B1%EC%9D%84-%EB%86%92%EC%9D%B4%EB%8A%94-%EC%A0%AF%EB%B8%8C%EB%A0%88%EC%9D%B8-%EA%B3%84%EC%97%B4-ide-%EC%97%90%EB%94%94%ED%84%B0-%EC%84%B8%ED%8C%85%EA%B3%BC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%B6%94%EC%B2%9C-b51b9e87f221?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/b51b9e87f221</guid>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Bernard]]></dc:creator>
            <pubDate>Wed, 14 Jun 2023 06:45:45 GMT</pubDate>
            <atom:updated>2023-07-10T08:56:52.629Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ongyUh_0YrRPaEd5v7ZmoQ.png" /></figure><p>코드의 가독성은 매우 중요한 문제입니다.</p><p>클린코드로 대표되는 코드 작성 지침에는 다양한 제안이 있습니다. 변수 이름을 어떻게 짓는 것이 좋은가부터 시작해서, 전체적인 코드의 구성과 분리등의 컨벤션까지 커버합니다. 이 모든것이 <strong>읽기좋은 </strong>코드를 작성하기 위한 제안이고, 그것만으로도 책 한권은 우스울정도로 얘기를 쏟아낼 수 있습니다</p><p>하지만 이러한 클린코드는 어디까지나 텍스트 그 자체에만 집중합니다. IDE가 지원하는 여러 강력한 개인화는 다루지 않죠. 때문에 클린코드 지침과 별개로, 각자의 개발환경에서 코드의 가독성을 높이기 위한 다양한 개인화 시도는 해 볼 가치가 있습니다.</p><p>사람의 눈은 패턴인식기입니다. 때문에 텍스트 그 자체보다, 색과 모양등의 패턴 등의 다양한 시각 효과를 코드에 적용하면 전체적인 가독성이 훨씬 좋아집니다.</p><p>코드 작성과 코드 읽기의 비중은 1:10정도입니다. 개발자들은 대부분의 시간을 코드를 읽는데 보냅니다. 가독성에 신경쓰는건 당연한 일이죠.</p><h3>에디터 설정</h3><p>ide가 지원하는 여러가지 에디터 설정을 바꾸어서, 가독성을 끌어올리는 방법을 알아보겠습니다.</p><h4>찾기 결과 하이라이트</h4><p>특정 단어 찾기시 눈에 띄게 좀 더 밝기를 올리거나, 사각박스를 치거나 하는 것을 저는 좀 더 선호합니다. 별다른 설정 없이도 부분의 컬러 스킴들이 이러한 텍스트 찾기 결과를 눈에 띄게 하이라이트 하지만, 코드를 읽는게 아니라 빠르게 눈으로 훑어가면서 단번에 눈에 뜨이게 하는편이 저는 더 좋더라구요.</p><p>코드 읽기의 대부분은 텍스트 또는 변수를 찾아가면서 뇌내 디버깅 내지는 변수 변화 흐름 추적이니, 이런식으로 밝게 시각적 꼬리표를 달아두는 것이 여러가지로 편리합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CGq4XVnqBgqrlnsVs6JKiA.png" /><figcaption>const 를 찾아보았습니다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7EIaI2Bm7VrOOMj_9-GBZg.png" /><figcaption>설정 경로</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*n_-klSX6eXGKq8szDuaeOQ.png" /><figcaption>기본 Dracula 컬러 스킴의 경우 저채도의 녹색</figcaption></figure><h4>커서가 위치한 변수 하이라이트</h4><p>매번 텍스트 찾기를 할 수만은 없는 노릇입니다. 요즈음의 ide는 커서가 위치한 변수를 하이라이트해주는 기능이 있죠. 그 하이라이트를 좀 더 하이라이트답게 바꾸어 봅시다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zt52ImKwb_aU9NNry6O-Kw.png" /><figcaption>employee에 커서를 올렸을때</figcaption></figure><p>위와 같이 설정하면, 커서를 올린것만으로도 변수의 흐름이 확연이 눈에 보이게 됩니다. 특히 쓰기가 일어나는 경우는 다른 색으로 표현가능해서 어떤 시점에 읽기쓰기가 일어나는지 단번에 알 수 있죠. 스크롤이 넘어가도 커서는 해당 변수 밑에 계속 있기에, 드륵드륵 코드를 내려가면서 전체 흐름을 보기에도 편합니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/607/1*iBToQmgklLMJDNdEMR0-hw.png" /><figcaption>기본 dracula의 경우</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SEGzoPaz_tx9ENJ4ekDHvg.png" /><figcaption>설정 경로. 변수 읽기의 경우</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dNCZAeeNwIg3u8uqQpR8cA.png" /><figcaption>설정 경로. 변수 쓰기의 경우</figcaption></figure><p>저같은 경우에는 위와같이 파스텔톤 형광 계열 빨강과 노랑으로 해두었습니다. 각자의 컬러 스킴에 맞추어 눈에 잘 띄게 해두면 코드 읽기가 한결 쉬워질겁니다.</p><h4>커서가 위치한 라인 하이라이트</h4><p>가끔은 변수가 아니라 라인 바이 라인으로 모든 코드를 읽어내려나가야 할 때가 있습니다.</p><p>기본적으로도 커서가 위치한 라인이 하이라이트가 되긴하지만…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*EZ3bIBUV5319OdnsTzjhYg.png" /><figcaption>기본 커서가 위치한 라인 하이라이트</figcaption></figure><p>조금 더 눈에 띄면 좋겠단 생각을 지울수가 없죠.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*e_Jd0gG2l0IjF-bppHYTwQ.png" /><figcaption>커서가 위치한 라인 하이라이트 설정 후</figcaption></figure><p>위와같이 테마에서 자주 쓰이는 색에서 좀 벗어난 색으로 포인트를 주면, 내가 지금 어디를 읽고 있었는지 한번에 화면내에서 볼 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CheLAEZDh3dcmmKcpBALZw.png" /><figcaption>커서가 위치한 라인의 색 설정 경로</figcaption></figure><h4>함수 분리선</h4><p>이상적으로는, 한 파일안에 손으로 셀 수 있을정도의 갯수의 함수가 있고, 많아야 1000 라인정도의 코드만 있으면 좋겠지만, 실제로는 그러지 않을때가 많습니다.</p><p>인덴트가 들쭉날쭉하면서 어디서부터 어디까지가 이전 함수의 범위였는지 한번에 알기도 어려울때가 많죠.</p><p>ide가 감지하는 범위 내에서긴하지만, 함수 사이마다 분리선을 그어줄 수 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*_QQm13Zv1jlOQOFhbnq8jg.png" /><figcaption>함수 분리선(흰색 줄)이 그어진 모습</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f-qHK1CZscwsIWljnLFoTQ.png" /><figcaption>show method separators 옵션을 킨다.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jlA7CTYxovY_yhRXXs7VTA.png" /><figcaption>해당 구분선의 색상은 methd separator color 옵션에서 설정 가능</figcaption></figure><p>수없이 많은 함수들 사이를 공백으로 다다다다 구별하는것도 좋지만, 그 전에 ide가 지원하는 구분선을 먼저 시도해보는것도 좋을거 같습니다.</p><h4>시맨틱 하이라이팅</h4><p>이 기능은 제가 제일 좋아하는 기능중 하나입니다. 변수마다 서로 다른 임의의 색상을 부여하는 기능이죠.</p><p>기본적으로는 이 기능이 off 상태일텐데… 그렇게되면 아래처럼 보입니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*rMyXHf26khs-cMh7W7YJsg.png" /><figcaption>semantic highlighting 이 꺼져 있을때</figcaption></figure><p>코드들이 다소 플랫해보이죠. 여기서… 해당 옵션을 키게 되면 각 변수별로 색상이 부여됩니다. 이제 여기서 취향에 따라서 색상을 더 부여주면 아래처럼 좀 더 구별이 쉬워집니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*haYJPbjAKfukWhdV_uwiCA.png" /><figcaption>semantic highlighing 설정</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/966/1*kTsK93xPVqHCcjrOD4hzkg.png" /><figcaption>semantic highlighting 이 적용된 코드</figcaption></figure><p>위와 같이 ide의 설정만으로도 코드의 시각적 패턴화가 상당히 이루어집니다. 하지만 아래의 플러그인까지 설치하면 훨씬 더 코드 읽기가 쾌적해지는걸 경험했습니다.</p><h3>플러그인</h3><h4>인덴트 레인보우</h4><p>코드 작업을 하다보면, 그러지 않으려고 해도 들여쓰기 단계가 깊어지는 경우가 있습니다.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2D7FxXGO6hmH2sjpFOKqvg.png" /><figcaption>typeorm 코드의 15단계 들여쓰기</figcaption></figure><p>ide 의 기본 지원 기능으로, 들여쓰기 단계마다 수직선을 그어주지만, 아무래도 역부족인점이 있습니다. 특히 동일 들여쓰기 단계가 길어지는 코드의 경우, 까딱하다가는 눈으로 쫓던 코드 스코프를 놓치기 십상이죠.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZAamkv1SFHqh8UdBfGgLmA.png" /><figcaption>인덴트 레인보우 플러그인 활성화시</figcaption></figure><p>하지만 이처럼 시각적으로 강조를 하게되면, 훨씬 보기 편해지는걸 느낍니다.</p><p><a href="https://plugins.jetbrains.com/plugin/13308-indent-rainbow">Indent Rainbow - IntelliJ IDEs Plugin | Marketplace</a></p><p>여러가지 방법을 동원해서 코드의 가독성뿐만 아니라, 시각적 강조와 효과로 코드를 패턴처럼 눈에 보이게끔 하는 방법들을 알아보았습니다. 익숙해지면 코드를 변수와 선언의 연속인 글자들이 아니라, 색과 도형으로, 읽는게 아니라 흘깃 보고 바로 알아챌 수 있게 되어 훨씬 생산성이 높아지는걸 경험할 수 있습니다.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b51b9e87f221" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/%EC%BD%94%EB%93%9C-%EA%B0%80%EB%8F%85%EC%84%B1%EC%9D%84-%EB%86%92%EC%9D%B4%EB%8A%94-%EC%A0%AF%EB%B8%8C%EB%A0%88%EC%9D%B8-%EA%B3%84%EC%97%B4-ide-%EC%97%90%EB%94%94%ED%84%B0-%EC%84%B8%ED%8C%85%EA%B3%BC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EC%B6%94%EC%B2%9C-b51b9e87f221">코드 가독성을 높이는 젯브레인 계열 IDE 에디터 세팅과 플러그인 추천</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[코드가 클린 하면 왜 좋아?(feat. 뇌 인지 관점)]]></title>
            <link>https://medium.com/zigbang/%EC%BD%94%EB%93%9C%EA%B0%80-%ED%81%B4%EB%A6%B0-%ED%95%98%EB%A9%B4-%EC%99%9C-%EC%A2%8B%EC%95%84-feat-%EB%87%8C-%EC%9D%B8%EC%A7%80-%EA%B4%80%EC%A0%90-4ff53268a8b8?source=rss----2f055286701b---4</link>
            <guid isPermaLink="false">https://medium.com/p/4ff53268a8b8</guid>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Pjj]]></dc:creator>
            <pubDate>Mon, 22 May 2023 10:58:04 GMT</pubDate>
            <atom:updated>2023-05-22T11:25:46.941Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wa5vQ6pM_z4qbtjo-ibljA.png" /></figure><p>안녕하세요 BE APT 팀의 Jin 입니다.</p><p>개발자라면 누구나 한번 쯤 클린코드에 대해 들어본 적 있지 않으신가요?우리 모두 클린코드의 지침을 따르다 보면 읽기 쉬운 코드가 작성된다고 잘 알고 있습니다.</p><p><strong>그렇다면 클린코드의 가독성이 왜 좋은지 생각해보신적이 있나요?</strong>저는 이번 포스팅에서 클린코드를 다른 시각으로 바라보고 개발자가 클린 코드를 왜 쉽게 이해하는지에 대해 얘기하려 합니다.</p><p>(참고로 제 얘기는 대부분 “프로그래머의 뇌” 라는 책을 바탕으로 합니다.)</p><p>먼저 본격적으로 시작하기에 앞서 우리는 언제 코드가 읽기 쉽다, 이해하기 쉽다고 느낄까요? 우리는 코드를 읽을 때 머릿속에 혼란(코드가 하는 일, 작동방식, 의미의 혼란)이 없을 때 코드를 이해하기 쉽다고 느낍니다.</p><p>그러면 우리는 코드를 읽을 때 도대체 어떤 혼란이 우리 뇌 속에서는 일어날까요?</p><h4><strong>코드를 읽을 때 일어나는 3가지 혼란</strong></h4><ol><li>지식의 부족</li></ol><p>아래 코드는 APL이라는 언어로 된 코드로 숫자 n을 이진수 표현으로 변경하는 코드입니다.</p><pre>2 2 2 2 2 T N</pre><p>코드를 읽으시면서 어떠셨나요? 혼란스럽지 않으셨나요? 여기서 우리가 혼란스러웠던 이유는 <strong>T</strong>에 대한 <strong>지식</strong>이 없기 때문입니다.</p><p>2. 정보의 부족</p><p>아래의 코드는 자바로 작성된 코드입니다.</p><pre>public class BinaryCalculator {</pre><pre>  public static void main(Integer n) {<br>    System.out.println(Integer.toBinaryString(n));<br>  }<br>  <br>}</pre><p>이번엔 코드를 읽으시면서 어떠셨나요? 이번에도 혼란스러웠나요? 이번 코드는 자바 언어 전문가가 아니어도 어떤 일을 하는지 메서드 이름으로 유추할 수는 있었을 거라 예상합니다. 하지만 toBinaryString() 메서드 내부적으로 어떻게 작동하는지 모른다면 이코드도 혼란스러울 수 있습니다. 그 혼란의 원인은 toBinaryString() 메서드에 관한 <strong>정보</strong>가 부족하다는 점입니다.</p><p>3. 처리 능력의 부족</p><p>아래 코드는 베이직으로 작성된 코드입니다.</p><pre>LET N2 = ABS (INT(N))<br>LET B$ = &quot;&quot;<br>FOR N1 = N2 TO 0 step 0<br>  LET N2 = INT(N1/2)<br>  LET B$ = STR$(N1- N2 * 2) + B$<br>  LET N1 = N2<br>NEXT N1<br>PRINT B$</pre><p>이번엔 코드를 읽으시면서 어떠셨나요? 이번에도 혼란스러웠나요? 이번 코드에서는 변수 이름이나 연산자를 통해 무슨 일하는지 유추가 가능합니다. 하지만 코드의 각각의 단계가 실행되는 것을 한눈에 파악하기 어려워 혼란스러울 수 있습니다. 이번 혼란은 <strong>처리 능력</strong>이 부족하기 때문입니다. 이런 코드는 이해하기 위해서는 변수들의 중간값을 따로 적거나 하는 등의 노력이 필요할 수 있기 때문입니다.</p><p>이제까지 우리 머릿속에서 일어나는 혼란들에 대해 알아봤습니다. <strong>그럼 이번엔 우리가 프로그래밍을 할 때 우리 두뇌에 어떤 일이 일어나는지 알아보겠습니다.</strong></p><p>코딩을 할 때 우리 두뇌는 크게 3가지 <strong>기억</strong>과 관련된 영역이 사용됩니다.</p><p><strong>1. 장기 기억 공간(long-term memory, LTM)</strong></p><ul><li>오랜 시간에 걸쳐 얻은 정보를 저장하는 장소입니다.</li><li>컴퓨터의 하드디스크 드라이브와 비슷합니다.</li><li>여기에 저장된 기억은 아주 오랫동안 보관됩니다.</li><li>이진 검색과 같은 추상적 알고리즘, 프로그래밍 언어 문법, 특정 언어의 키워드, 자전거 타는 법, 신발 끈을 묶는 법 등이 여기에 저장되어 있습니다.</li><li>앞서 얘기한 혼란 중 <strong>지식</strong>이 없다는 것은 LTM에 해당 내용이 없다는 뜻입니다.</li></ul><p><strong>2. 단기 기억 공간(short-term memory, STM)</strong></p><ul><li>들어오는 정보를 잠시 보관하는 곳입니다.</li><li>최대 공간이 12개를 넘지 않습니다. (2–6 사이로 추정)</li><li>우리의 두뇌는 정보들을 <em>청크(chunk)</em> 라는 묶음으로 나누려고하고 LTM과 협업을 통해 STM의 공간 제약을 극복합니다. (아래에서 좀 더 자세히 설명하겠습니다.)</li><li>특정 코드에서의 키워드 , 변수명, 자료구조 등이 여기에 저장 됩니다.</li><li>컴퓨터의 캐시나 메인 메모리와 비슷합니다.</li><li>앞서 얘기한 혼란 중 <strong>정보</strong>가 부족하다 것은 STM에 해당 내용이 없다는 뜻입니다.</li></ul><p>우리의 뇌가 청킹과 LTM과의 협업을 통해 공간의 제약을 극복하는 방법에 대해 좀 더 자세히 설명해 보겠습니다.</p><p>다음 문장을 5초간 본 후 어떤 문장인지 기억해 보세요</p><pre>abk mrtpi gbar</pre><p>어떠셨나요? 기억하기 쉬웠나요?</p><p>그럼 다음 문장을 똑같이 5초간 보고 기억해 보세요</p><pre>cat loves cake</pre><p>어떠셨나요? 훨씬 쉽지 않았나요? 그 이유는 LTM에 세 단어의 지식이 있어서 한 단위로 묶을 수 있기 때문입니다. 하지만 abk mrtpi gbar와 같은 문장은 각 단어를 기억해야 하고 STM 한도를 넘어섰기 때문에 어렵습니다</p><p><strong>3.</strong> <strong>작업 기억 공간(working memory)</strong></p><ul><li>실제 LTM, STM 의 정보를 가지고 와서 정보를 처리하는 곳 입니다. (생각, 아이디어, 해결책 등이 여기서 만들어집니다.)</li><li>컴퓨터의 프로세서와 비슷합니다.</li><li>STM과 동일하게 2–6까지 항목만 저장할 수 있고 청킹을 사용해서 더 많은 정보를 처리할 수 있습니다.</li><li>앞서 얘기한 혼란 중 <strong>처리 능력</strong>이 부족하다는 것은 작업 기억 공간이 과부하 상태라는 뜻입니다.</li></ul><p>그럼 어떻게 이런 공간들이 서로 상호 작용해 우리는 코드를 이해하게 되는 걸까요? 예를 하나 들어보겠습니다. 제가 몇 달 전 만든 코드에 버그가 생겨 코드를 분석해야 하는 상황입니다. 버그는 삽입정렬을 구현하는 부분에서 발생했다고 가정하겠습니다.</p><p>이때 코드를 분석하면 제 머릿속에서는</p><ul><li>코드를 읽으면서 분석하는 내용, 버그 리포트 내용 등이 STM에 저장됩니다.</li><li>몇 개월 전에 구현한 내용, 삽입정렬에 관한 내용, 삽입정렬 구현 시 흔히 발생한 오류나 겪었던 경험, 잘 알려진 해결법 등을 LTM에서 가져오려고 합니다.</li><li>관련된 LTM과 STM에 있는 내용들이 working memory에 들어오게 되고 버그에 대한 해결법을 도출하게 됩니다.</li></ul><h4>그럼 이제 까지 배운 내용을 드디어 클린코드에 적용 해봅시다.</h4><ol><li>함수의 인수를 최대한 작게 유지하라!</li></ol><p>함수의 인수는 작을수록 좋다는 클린코드의 지침입니다.</p><p>앞에 말씀드렸다시피 우리의 작업 기억 공간의 용량은 6개 정도로 작아서 많은 매개변수 리스트를 기억하기에는 무리가 있습니다. 그래서 함수의 인수가 많을수록 우리는 인지부하를 느낄 게 될 가능성이 높습니다.</p><p>클린코드에서는 인수가 많을 때 줄이는 방법으로 인수 객체를 사용해서 개념을 표현하라고 합니다. 예를 들어 다음과 같은 메서드가 있습니다.</p><pre>public void line(int xOrigin, yOrigin, xDestination, yDestination)</pre><p>우리의 두뇌는 4개의 청크(xOrigin, yOrigin, xDestination, yDestination)로 인식하게 될 가능성이 큽니다.</p><p>이번엔 클린코드 지침대로 인수 객체를 도입해 보겠습니다.</p><pre>public void line(Point origin, Point destination)</pre><p>이렇게 되면 우리 두뇌는 2개의 청크(origin좌표, destination좌표)로 해당 메서드를 인식하게 될 가능성이 커지고 이로써 작업 기억공간의 부하를(4-&gt;2) 줄일 수 있게 됩니다. (물론 코드에 사전 지식을 얼마나 가지고 있냐 또는 개인에 역량에 따라 첫 번째 코드를 보고 머릿속에서 바로 2개의 청크로 인지할 수도 있습니다. )</p><p>2. 클래스/메서드를 작게 유지하라.</p><p>많은 기능을 가지는 “만능 클래스” 또는 “만능 메서드”를 의미 있는 단위로 분리해서 작게 만들라는 클린 코드의 원칙입니다.</p><p>긴 클래스, 긴 메서드를 읽을 때는 해당 코드가 무슨 일을 하는지 코드를 한줄 한줄 읽어야 하고 이는 우리 두뇌에서 청킹 작업을 하는데 많은 시간을 쏟고 어렵게 만듭니다.</p><p>반면 긴 클래스, 메서드를 작은 단위로 분리하면 분리된 클래스, 메서드의 이름이 문서 역할을 합니다.그래서 코드를 읽을 때 그 이름을 가지고 효율적으로 청킹을 할 수 있게 되고 이는 우리의 작업 기억 공간과 STM의 용량 부하를 줄이는 효과로 이어지게 됩니다.</p><p>3. 반복하지 마라!</p><p>코드의 중복을 없애라는 클린코드의 지침입니다. 유지보수를 위해서도 있지만 가독성 측면에서도 중복은 혼란을 야기 하기 쉽습니다. 예를 들어 다음과 같은 두 메서드가 있습니다.</p><pre>int foo(int j) {<br>  if (j &lt; 0)<br>    return j;<br>  else<br>    return ++j;<br>}</pre><pre>int goo(int j) {<br>  if (j &lt; 0)<br>    return j;<br>  else<br>    return j+2;<br>}</pre><p>foo() 함수를 보고 난 뒤 goo() 함수를 보게 되면 우리 작업 기억 공간은 LTM에서 foo()에 대한 정보를 수집하게 될 것입니다. 그리고 나서는 goo() 함수를 foo() 함수와 동일하다고 <strong>잘못 </strong>인지하게 될 가능성이 높습니다.</p><p>4. 의미 있는 이름을 지어라!</p><p>이름에 의도를 분명히 밝히라는 클린코드의 지침입니다. 구체적으로는 아래와 같은 여러 지침이 있습니다.</p><ul><li>해법 영역에서 가져온 이름을 사용하라.</li><li>문제의 영역에서 가져온 이름을 사용하라.</li><li>의미 있는 맥락을 추가하라.</li></ul><p>이렇게 많은 클린 코드 지침들은 LTM과 관련이 있습니다. 올바른 개념을 이름에 단어로 적절히 사용하면 코드를 읽을 때 관련 정보를 LTM에서 찾는 데 도움을 주기 때문입니다.</p><p>예를 들면 Visitor 디자인 패턴이 적용된 코드에 xxxVisitor라는 단어를 넣어주면 해당 코드가 Visitor 패턴이 적용되어있다는 걸 인지하고 관련 정보를 가지고 와서 이해하게 됩니다. 또는 지뢰 찾기 게임에서 <strong>게임판</strong>을 <strong>theList</strong>와 같은 함축적인 이름으로 명명 하는것보다 <strong>gameBord</strong>와 같은 이름을 지어주면 우리는 해당 코드를 읽을 때 도움이 되는 정보들을(지뢰 찾기 게임판은 여러 칸으로 구성되어 있다 등) LTM에서 가져올수 있습니다.</p><p>추가로 클린코드에는 없지만, 우리가 보편적으로 준수하는 네이밍(camelCase, snake_case) 관례 또한 인지과정과 연관이 있습니다. 아시다시피 STM의 크기는 제한적이기 때문에 효율적으로 저장하기 위해 우리 뇌는 이름을 청킹 하려고 하고 합니다. 이름이 체계적일수록 이름의 각 부분을 식별하고 청킹 하기 쉽습니다. 예를 들어 nmcntrlst 보다 name_counter_list 같은 이름이 관련된 내용(이름, 카운터, 리스트)을 훨씬 쉽게 우리는 인지 할 수 있습니다.</p><h3>마치며</h3><p>우리 모두 타인, 자신의 코드를 읽으며 좌절하고 괴로워한 경험이 있을것입니다. 저는 우리 모두 뇌의 동작방식을 이해하고 효과적으로 코드를 읽어나가길 기대하며 이 글을 작성했습니다.</p><p>사실 이 글이 그동안 모르셨던 엄청난 사실을 알려주지는 않았을 겁니다. 하지만 우리가 그동안 “<strong>그래 이런 코드가 읽기 쉽지</strong>” 라고 당연히 생각했던 부분에 대해서 과학적인 근거를 제시했을 거라 생각합니다.</p><p>제가 소개하지 않았지만, 책에서는 신속하게 코드를 읽는 법, 다른 언어를 빨리 배우는 법, 복잡한 문제를 더 잘 해결하는 법, 코드베이스를 평가하는 법, 설계 개선법 등 재밌고 유익한 내용이 많이 들어 있습니다. 궁금하시다면 직접 책을 읽어보면 좋을 것 같습니다.</p><h3>Ref</h3><p><a href="http://www.yes24.com/Product/Goods/105911017">http://www.yes24.com/Product/Goods/105911017</a> (프로그램의 뇌)</p><p><a href="http://www.yes24.com/Product/Goods/11681152">http://www.yes24.com/Product/Goods/11681152</a> (클린코드)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ff53268a8b8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/zigbang/%EC%BD%94%EB%93%9C%EA%B0%80-%ED%81%B4%EB%A6%B0-%ED%95%98%EB%A9%B4-%EC%99%9C-%EC%A2%8B%EC%95%84-feat-%EB%87%8C-%EC%9D%B8%EC%A7%80-%EA%B4%80%EC%A0%90-4ff53268a8b8">코드가 클린 하면 왜 좋아?(feat. 뇌 인지 관점)</a> was originally published in <a href="https://medium.com/zigbang">직방 기술 블로그</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>