<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>holymoly.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 24 Jul 2024 15:30:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>holymoly.log</title>
            <url>https://velog.velcdn.com/images/sujin-create/profile/39c568a2-b79d-4e67-a1bd-bf53290e161f/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. holymoly.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sujin-create" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[ CS ] IPv4  주소 클래스]]></title>
            <link>https://velog.io/@sujin-create/CS-IPv4-%EC%A3%BC%EC%86%8C-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@sujin-create/CS-IPv4-%EC%A3%BC%EC%86%8C-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Wed, 24 Jul 2024 15:30:15 GMT</pubDate>
            <description><![CDATA[<h3 id="intro-ip">Intro. IP</h3>
<p>인터넷에서 컴퓨터나 다른 네트워크 장치를 식별하는 고유한 번호이다.
IPv4는 총 32 비트로 구성되어있다. 그러나, 공간이 점차 부족해지고 보다 긴 주소 형식인 IPv6가 고안되었다.
이때, 네트워크 부분과 호스트 부분을 구분해야한다.
학교로 따지면 네트워크는 반을 의미하고 그 안에 학생이 여러명일 때, 호스트는 각 학생을 의미한다.
하나의 네트워크 안에 여러개의 IP를 가질 수 있고, 호스트로 구분한다.</p>
<p>이때, 어떻게 네트워크 안에서 IP를 쪼갤 수 있을까?</p>
<p>이름에서도 힌트가 있다. subnet을 하는 방식! subnetmask를 사용할 수 있다.
그렇다면, 네트워크에서 호스트를 나눠서 사용할 수 있다.</p>
<h3 id="잠깐-왜-호스트를-굳이-나눠야하나요">잠깐! 왜 호스트를 굳이 나눠야하나요?</h3>
<p>네트워크의 자원이 클 때, 즉 반에 에어컨이 들어져있습니다. 전기세가 나가고 있는데 반에 1명만 존재해요 그렇다면 매우 비효율적일 것입니다. 그렇다면 반에 많은 사람들이 들어가면 좋겠죠? 이렇게 자원을 효율적으로 사용하기 위해서 Internet에서 IP를 통해 network를 나눠서 사용하게 됩니다.
또한, 보안의 이유가 있을 수 있고 네트워크를 분산시켜 과부하를 방지할 수 있기 때문입니다.</p>
<h3 id="ipv4-표시형식">IPv4 표시형식</h3>
<blockquote>
<p>총 32비트로 구성되어있고 8비트씩 1개의 옥텟으로 총 4 옥텟으로 ip address가 구성됩니다.</p>
</blockquote>
<p>192.168.43.100 이라는 ip address가 있다고 합시다.
이때, <code>11000000, 10101000, 00101011, 01100100</code> 으로 이진수로 변환 가능합니다 </p>
<p>여기서 네트워크 부분은 뭐고 호스트 부분은 뭘까요?
192로 시작하는 것을 통해 네트워크 주소 범위와 호스트 영역을 알 수 있습니다.</p>
<p>어떻게 알 수 있는 걸까요?
이를 이해하기 위해서는 우선 IP ADDRESS의 클래스를 알아야합니다.</p>
<p>그렇다면 ip 주소 클래스에 대해서 먼저 이해합시다!</p>
<p><strong>ip 주소 클래스</strong></p>
<p>ip주소의 클래스는 a,b,c,d,e 총 5개의 클래스로 나눠져있습니다. 즉, 반이 총 5개라는 겁니다!
이때, d와 e는 보통 멀티캐스트와 실험용으로 사용되어 주로 다루지 않습니다.</p>
<p>클래스의 네트워크를 결정하는 요소는 가장 첫번째 옥텟입니다. 
가장 첫번째 옥텟이 0으로 시작하는지 10로 시작하는지 혹은 110으로 시작하는지에 따라서 class가 결정되고 각 class마다 네트워크 수가 존재합니다. </p>
<ol>
<li><p>a 클래스
 가장 처음의 옥텟 IP가 무조건 0으로 시작해야합니다. +) 1번째 옥텟에 의해 네트워크가 결정됩니다. 나머지 3개의 옥텟에 의해 호스트가 결정됩니다.
 그렇다면 네트워크 주소로 가능한 경우의 수는 8자리 중에서 1자리가 정해졌기에 2의 7승인 128가지겠죠? </p>
<ul>
<li>최상위 비트 : 0 ( 1로 시작 안 됨)</li>
<li>1번째 옥텟에 의해 네트워크가 결정된다  -&gt; 네트워크 수 : 128</li>
<li>뒤의 3개의 옥텟에 의해 호스트가 결정된다 -&gt; 호스트 수 : 2의 24승</li>
</ul>
</li>
<li><p>b 클래스
 가장 처음의 옥텟 IP가 무조건 10으로 시작해야합니다. +) 처음 2개의 옥텟에 의해 네트워크가 결정됩니다. 나머지 2개의 옥텟에 의해 호스트가 결정됩니다.
 10으로 시작하고 11보다는 작아야하기에 128~191까지 첫번재 옥텟에 의해 64개의 경우가 나오고 다음 2번재 옥텟에 의해 256가지의 경우를 곱하면? 16,384라는 값이 나옵니다.</p>
<ul>
<li>최상위 비트 : 10 (0으로 시작 안 됨, 11안됨)</li>
<li>2개의 옥텟에 의해 네트워크가 결정된다 -&gt; 네트워크 수 : 16,384</li>
<li>뒤의 2개의 옥텟에 의해 호스트가 결정된다 -&gt; 호스트 수 : 2의 16승</li>
</ul>
</li>
<li><p>C 클래스
 가장 처음이 옥텟 IP가 무조건 11으로 시작해야합니다. +) 처음 3개의 옥텟에 의해 네트워크가 결정됩니다. 나머지 1개의 옥텟에 의해 호스트가 결정됩니다.
 11으로 시작하고 111보다는 작아야하기에 첫번재 옥텟은 192~223까지의 수가 가능합니다. 그렇기에 223-192+1인 32개와 256개와 256개의 조합 32×256×256=2,097,152 개가 네트워크 주소로 사용됩니다.</p>
<ul>
<li>최상위 비트 : 11 ( 10안됨, 111안됨)</li>
<li>3개의 옥텟에 의해 네트워크 결정 -&gt; 네트워크 수 : 2,097,152개</li>
<li>뒤의 1개의 옥텟에 의해 호스트가 결정된다. -&gt; 호스트 수 : 2의 8승(256)</li>
</ul>
</li>
</ol>
<p>그럼 다시 돌아가서 192.168.43.100 이라는 ip address를 봅시다.
192는 맨 앞의 두 비트가 11로 시작합니다. 그렇기 때문에 C클래스인 것을 알 수 있습니다. 그리고, 네트워크 영역은 C클래스이기 때무에 192.168.43까지가 네트워크 영역입니다. 그렇다면 호스트는 ? 100입니다. 즉, 256개의 경우 중에서 브로드캐스트 주소(가장 마지막 주소-&gt; 같은 네트워크의 모든 장비에게 보내는 통신을 위한 주소)와 네트워크 주소 (호스트 주소 중 가장 맨 처음 주소)를 제외하고 가능한 254개 중 1개의 경우를 갖고 있는 것입니다.</p>
<p>정리하자면,</p>
<ul>
<li>네트워크 부분 : 192.168.43</li>
<li>호스트 부분 : 100</li>
<li>이유 : 첫번재 옥텟이 11로 시작하기 때문에 C타입이고 그렇다면, 3개의 옥텟이 네트워크 주소를 의미한다. 나머지 1개의 옥텟만 호스트 주소를 의미한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[OSPF]]></title>
            <link>https://velog.io/@sujin-create/%EC%9D%B8%ED%84%B0%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-OSPF</link>
            <guid>https://velog.io/@sujin-create/%EC%9D%B8%ED%84%B0%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-OSPF</guid>
            <pubDate>Sat, 20 Jul 2024 09:53:02 GMT</pubDate>
            <description><![CDATA[<h3 id="intro">Intro.</h3>
<p>기억속에서 사라졌던 인터네트워킹 관련 기억을 끄집어 내기 위해 총정리를 해야겠다고 생각했다. </p>
<h1 id="인터네트워킹">인터네트워킹</h1>
<blockquote>
<p>인터네트워킹이란 네트워크와 네트워크 사이의 연결을 의미한다. 두개 이상의 네트워크를 연결하기 위해 필요한 기술을 의미한다.</p>
</blockquote>
<h2 id="1-ospf-개념">1. ospf 개념</h2>
<p>⇒ 가장 최적의 루트를 찾기 위한 네트워킹 프로토콜</p>
<blockquote>
<p>Open Shortest Path First </p>
</blockquote>
<p>소스에서 목적지까지 패킷에 대한 사용 가능한 가장 빠른 경로를 찾기 위해서 설계됨.</p>
<h3 id="11-introduntion-to-ospfopen-shortest-path-first">1.1 introduntion to ospf(open shortest path first)</h3>
<p><code>&lt;ospf 소개&gt;</code></p>
<p>RIP : Routing Information Protocol (거리<strong>벡터</strong> 프로토콜)</p>
<p><strong>홉(네트워크 경로 상의 라우터)의 수</strong>를 통해 거리벡터 계산.</p>
<p>⇒ 문제점 : 홉의 수에 의존성이 문제가 됨. 다양한 속도의 다중경로가 있는 대규모 네트워크에서 확장되지 않는다.</p>
<p>ospf : Open Shortest Path First( <strong>링크 상태</strong> 라우팅 [map]프로토콜)</p>
<p>⇒ 링크 : 라우터의 인터페이스</p>
<p>링크 상태: 각 라우터는 자신의 직접 연결된 링크의 상태를 <strong>인접 라우터들</strong>에게 전송합니다. 이를 통해 전체 네트워크의 링크 상태 데이터베이스를 유지한다.</p>
<p>링크는 두개의 라우터를 연결하는 네트워크 세그먼트이거나 단일 라우터에 연결된 이더넷 LAN과 같은 것을 연결하는 network segment.</p>
<h3 id="1-2-ospf-동작">1-2. OSPF 동작</h3>
<p><code>ospf component</code></p>
<ol>
<li><p>hello packet</p>
</li>
<li><p><strong>Database description</strong></p>
<ul>
<li><p>&lt;데이터 베이스 설명 패킷 : shortest path mapping 정보를 담은 테이블을 담은 패킷&gt;</p>
<ol>
<li><p>Adjancy database [Neighbor table]: 라우터가 각자가 통신하고 있는 모든 인접한 라우터 목록을 적은 database로 이웃테이블을 의미한다.</p>
<p> ⇒ show ip ospf Neighbor</p>
</li>
<li><p>Link-state database(LSDB)[Topology table] : topology database</p>
<ol>
<li><p>topology : 어떠한 설계나 방법론적인 특성 그 자체를 의미한다. (링크 혹은 노드 등을 물리적으로 연결해 놓은 것 혹은 연결방식)</p>
</li>
<li><p>영역 내의 <strong>모든 라우터</strong>는 당연히 동일한 토폴로지를 가지고 있으니, <strong>동일한 LSDB를 가짐.</strong></p>
</li>
<li><p>네트워크의 <strong>다른 모든 라우터에 대한 정보를 나타냄.</strong></p>
<p>⇒ show ip ospf database</p>
<p>⇒shortest path를 설정. </p>
</li>
</ol>
</li>
<li><p>Forwarding Database[Routing table]</p>
<ol>
<li><p>shortest path를 기반으로 routing Table을 생성. 인접LSDB에서 알고리즘이 실행될때 생성되는 경로 목록</p>
</li>
<li><p>각 라우터의 라우팅 테이블을 고유하며 다른 라우터에 패킷을 보내는 방법과 위치에 대한 정보를 포함.</p>
<p>⇒ show ip route</p>
<p>⇒ 알고리즘 : Dijkstra (다익스트라)알고리즘을 사용 ⇒ short path first algorithm( SPF ) [이전 LSDB를 구성하며 Topology table 을 생성할때 사용되었던 것]</p>
</li>
</ol>
</li>
</ol>
</li>
</ul>
</li>
<li><p>Link-state request packet</p>
</li>
<li><p>Link-state update packet</p>
</li>
<li><p>Link-state acknowledgement(확인) packet</p>
</li>
</ol>
<h3 id="1-3-link-state-5가지">1-3. Link-STATE 5가지</h3>
<p>Link-state 목록 5가지</p>
<ol>
<li><p>인접 항목 설정[1. Establish Neighbor Adjacencies]</p>
<ul>
<li>인접 항목 설정 : 정보를 공유하기 전에 네트워크에서 hello-packet을 보내서 인접한 노드가 있는지 확인한다. 만약 이웃이 존재한다면 neighbor adjacency를 구성한다.<ol>
<li>hello packet</li>
<li>establish neighbor adjanency</li>
</ol>
</li>
</ul>
</li>
<li><p>링크 상태 광고 교환[2. Exchange Link-State Advertisements]</p>
<ul>
<li><p>인접항목이 생성됐다면 라우터들은 해당 정보를 공유하는데 이것을 LSA(퍼즐조각 : link-state advertisement) 공유한다고 한다.</p>
<p>  ⇒ LSA에는 이웃(직접연결된 라우터)의 상태와 비용이 포함.</p>
<p>  ⇒ LSA를 수신하는 인접 이웃은 해당 영역의 모든 라우터가 모든 LSA를 가질때까지 (모든 퍼즐조각을 각 라우터들이 가질때까지) flooding(흘러보내기)을 진행한다.</p>
</li>
</ul>
</li>
<li><p>링크 상태 데이터베이스 구축[3. Build the Link State Database]</p>
<ul>
<li><p>R1은 토폴로지 테이블을 생성합니다.</p>
<p>  토폴로지 테이블 : LSDB를 생성하는 과정은 LSA를 모두 받은 뒤에 table로 각 라우터마다 만드는데, 이때, 모든 링크에 대한 정보들을 포함한다. </p>
</li>
</ul>
</li>
<li><p>SPF 알고리즘 실행[4. Execute the SPF Algorithm]</p>
<ul>
<li><p>R1은 SPF tree를 generate</p>
<p>  생성된 LSDB를 가지고 각 라우터마다 shortest path algorithm을 적용시키고 spf tree를 생성한다.</p>
</li>
</ul>
</li>
<li><p>최적의 경로 선택 [5. Choose the Best Route]</p>
<ul>
<li><p>R1의 SPF tree → Routing Table</p>
<p>  목적지와 최단 경로, 비용이 각 라우터마다 table로 만들어진다.</p>
</li>
</ul>
</li>
</ol>
<p><strong>ospf에서 cost ⇒ 목적지에 대한 최적의 경로를 결정하는데 사용.</strong></p>
<h3 id="1-4-network-연결">1-4. Network 연결</h3>
<p><code>Sing-Area and Multiarea OSPF</code></p>
<ul>
<li><p>단일영역</p>
<ul>
<li>All routers are in one area. Best practice is to use area 0.</li>
<li>&lt;identical (동일한) thing : LSDB&gt;</li>
</ul>
</li>
<li><p>다중영역</p>
<ul>
<li><p>All areas must connect to the backbone area (area 0).</p>
<p>  모든 영역은 “무조건&quot; backbone area (area 0 )에 연결이 되어서 서로간에 연결이 되어야한다. 이때 area 0으로 연결되는 라우터를 각 영역에서 ABR(Area Border Router)라고 하고 ABRs들이 있게된다.</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/6195ac66-572a-4c4b-9991-a0ea0aff160c/image.png" alt=""></p>
<p>⇒ multi area에서 area 0 에 직접 연결되는 라우터⇒ ABR(area border router)</p>
<h3 id="1-5-multiarea-ospf">1-5. multiarea ospf</h3>
<p>다중 영역 ospf를 사용하면 계층적으로 라우팅을 진행할 수 있음.</p>
<p>여전히, 라우팅은 해당되는 영역 안에 있는 라우터끼리만 연결되지만(영역간 라우팅)</p>
<p>데이터베이스 재계산과 같은 프로세서 집약적인 라우팅 작업의 경우 특정 영역내에서 재계산 되는 것의 이점을 볼 수 있다.</p>
<p> ⇒ 토폴로지가 만약에 다른 영역에서 변경될 경우, 그 영역에서 SPF 알고리즘을 사용해서 SPF tree를 만들고 routing table을 만드는 작업을 하고 <strong>다른 영역에서는 SPF 를 사용하지 않고 바로 routing table만 update 할 수 있게 됨.</strong></p>
<p>⇒ 라우터를 영역으로 배열하면 잠재적으로 큰 데이터베이스를 더 작고 관리하기 쉬운 데이터베이스로 효과적으로 분할할 수 있습니다.<strong>(CPU의 부하를 줄이며 데이터베이스를 분할하여 효과적으로 관리)</strong></p>
<p>&lt;효과&gt;</p>
<ol>
<li>더 작은 라우팅 테이블 관리 → cpu의 부하를 줄임[<strong>Smaller routing tables]</strong></li>
<li>링크 상태 업데이트 오버헤드 감소 → 링크 상태 업데이트시 필요한 메모리나 처리 요구사항이 감소[<strong>Reduced link-state update overhead]</strong></li>
<li>SPF 계산 빈도 감소 → LSA 플러딩이 영역 경계에서 중지되며 영역 내부에서만 실행됨[<strong>educed frequency of SPF calculations]</strong></li>
</ol>
<p><code>ospfv3</code></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/1b550fd0-21ca-4a23-b83f-25982938aa75/image.png" alt=""></p>
<p>⇒ OSPFv3은 라우팅 정보를 교환하여 IPv6 라우팅 테이블을 원격 접두사로 채웁니다.</p>
<p>⇒ 여기서 접두사란 ? 네트워크 주소를 의미. 접두사 길이는 subnetmask.</p>
<p><strong>OSPFv3은 IPv6 접두사 교환에 해당하는 OSPFv2입니다. IPv6에서 네트워크 주소는 접두사라고 하고 서브넷 마스크는 접두사 길이라고 합니다.</strong></p>
<h2 id="2-ospf-packet-동작과정">2. OSPF Packet 동작과정</h2>
<p><code>OSPF의 종류(types)</code> 5가지 종류존재</p>
<ol>
<li><p>hello packet</p>
<p> ⇒ 다른 ospf 라우터와의 인접성을 설정하고 유지하는데 사용.</p>
</li>
<li><p>DBD packet(Database Description packet)</p>
<p> ⇒ LSDB(link-state database)의 축약된 목록이 포함되며 LSDB를 확인하기 위해서 받는 라우터에서사용. 정확한 SPF(short path frist)트리를 구성하려면 영역 내의 모든 링크상태 라우터에서 LSDB가 동일해야함.</p>
</li>
<li><p>LSR(link-state request) 패킷</p>
<p> ⇒ 수신 라우터는 LSR를 전송해서 DBD항목에 대한 추가 정보를 요청</p>
</li>
<li><p>LSU(link-state update) packet</p>
<p> ⇒LSR(link-state request)에 응답하고 새 정보(update)를 알리는데 사용. LSU에는 여러 유형의 LSA가 포함되어있음.</p>
<p> U : 그릇이라고 보면 되고 A : advertisement(광고 : 근접한 정보들을 담고 있는 퍼즐조각)</p>
<p> LSU에 LSA를 담아서 update가 진행됨.</p>
</li>
<li><p>LSAck(Link-state Acknowledgement packet)</p>
<p> ⇒ LSU가 수신되면 라우터는 잘 받았다고 LSAck를 전송해서 LSU 수신을 확인할 수 있다.</p>
</li>
</ol>
<h3 id="2-1-퀴즈">2-1. 퀴즈</h3>
<ol>
<li><p>송신 라우터의 LSDB의 List가 축약정보를 포함하고 있는 ospf 패킷은 무엇?</p>
<p>Which of the following OSPF packets contains an abbreviated list of the LSDB of the sending router?</p>
<p>⇒ 답 : DBD(DateBase Description packet). type2</p>
</li>
</ol>
<ol start="2">
<li><p>new information을 알리기 위해서 사용되는 패킷은?</p>
<p> Which of the following OSPF packets is used by routers to announce new information?</p>
<p> ⇒ 답 ) LSU(link-state update) : lsa를 하나이상 포함. type4.</p>
</li>
<li><p>추가 정보를 요청하기 위해서 사용되는 패킷은?</p>
<p> Which of the following OSPF packets is used by routers to request more information?</p>
<p> ⇒ 답 ) LSR(Link-state request packet)</p>
</li>
<li><p>다른 라우터들과의 이웃관계를 설립하거나 유지하기 위해서 사용되는 패킷은?</p>
<p> Which of the following OSPF packets is responsible for establishing and maintaining adjacency with other OSPF routers?</p>
<p> ⇒ 답) hello packet</p>
</li>
<li><p>Lsa에 대한 받음을 확인하기 위해서 사용되는 것은?</p>
<p> Which of the following OSPF packets is used to confirm receipt of an LSA?</p>
<p> ⇒ 답) LSAck(link-state acknowledge packet)</p>
</li>
<li><p>원조 router를 구별하기 위해서 hello packet과 함께 사용되는 것은?</p>
<p> Which of the following is used with the Hello Packet to uniquely identify the originating router?</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[투 포인터] 알고리즘]]></title>
            <link>https://velog.io/@sujin-create/%ED%88%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@sujin-create/%ED%88%AC-%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Tue, 11 Jun 2024 12:55:51 GMT</pubDate>
            <description><![CDATA[<h3 id="투포인터">투포인터</h3>
<p>코딩테스트를 보며, 시간초과 나도록 빡구현을 했다. 후기를 찾아보면 내가 주로 틀린 것은 투포인터 혹은 이진트리였다.  투포인터 알고리즘에 대해서 알고는 있지만, 명확한 이해가 없어서 다시 알아봤고 이를 활용 가능할 정도로 연습해야겠다고 생각했다. </p>
<h2 id="what">what?</h2>
<blockquote>
<p>말 그대로 두개의 포인터를 사용하는 알고리즘이다.
두 점의 위치를 기록하며 처리하는 알고리즘으로, 정렬되어있는 두 리스트의 합집합에서도 사용된다.</p>
</blockquote>
<p>투포인터 알고리즘은 배열이나 문자열과 같은 연속적인 자료 구조에서 <strong>부분</strong> 배열, <strong>부분</strong> 문자열, 혹은 <strong>특정 조건을 만족하는 쌍</strong>을 찾는 문제에서 사용된다.</p>
<ul>
<li><p>장점
시간복잡도는 어떻게 될까?</p>
<h3 id="시간복잡도">시간복잡도</h3>
<p>포인터마다 결국 탐색하는 배열만큼 옮겨진다. 즉, 갔던 길은 되돌아가지 않는다는 것이고 하나의 포인터는 O(N)시간복잡도를 가진다.
그렇다면 2개일 때, O(2N)일텐데 2는 상수이기에 빅오표기법에 의해 무시한다. </p>
</li>
<li><blockquote>
<p>O(N)을 가지기에 시간복잡도에서 매우 효율적이다.</p>
</blockquote>
</li>
<li><p>단점
아무래도 그냥 빡구현 (이중포문) 보다 어렵다. 따라서 기본 문제들이 보통 백준기준 골드5정도다. 그래서 정확히 이해하고 연습을 많이 해야한다. </p>
</li>
</ul>
<h2 id="how">how?</h2>
<p>| 사용방법은? 두개의 포인터의 위치를 저장하며 옮긴다. 특정 조건을 걸어서 s와 e 포인터를 옮기기를 반복한다.</p>
<ol>
<li>동일한 index에서 pointer가 같이 시작 -&gt; 서서히 멀어지기</li>
<li>양 끝의 index에서 pointer가 따로 시작 -&gt; 서서히 모여지기</li>
</ol>
<p>그렇다면 서서히 멀어뜨리는 경우는? 문제에 따라서 조금 더 알맞은 것을 고르면 된다.</p>
<p>에를 들어, <a href="https://www.acmicpc.net/problem/1806">부분합</a> 해당 문제는 start와 end의 포인터를 옮기면? 부분 배열을 찾는 문제이기에 동시에 같은 위치에서 부분배열을 변경해가며 특정 조건을 만족하는지 확인해야한다.</p>
<ul>
<li>부분 배열 포인터 시작위치 </li>
</ul>
<p>s를 시작포인터, e를 끝포인터라고 하자.
둘다 마지막 index를 향해간다.</p>
<table>
<thead>
<tr>
<th>s, e</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>2</td>
<td>5</td>
<td>8</td>
<td>1</td>
</tr>
</tbody></table>
<p>예시를 들어, </p>
<p>부분합을 찾는다면?</p>
<pre><code class="language-python">import sys

input = sys.stdin.readline

n,s = map(int, input().split())

nums = list(map(int, input().split()))

start = end = 0

sum = 0
min_len = n
flag = False
while start &lt; n:

    if sum &lt; s: #부분합이 s이하라면 end+1
        if end&gt;=n:
            break
        sum += nums[end]
        end+=1
    else: # 부분합이 s이상이라면 s +1
        flag = True
        min_len = min(min_len, end-start)
        sum -= nums[start]
        start+=1


if flag ==False:
    print(0)
else:
    print(min_len)</code></pre>
<ul>
<li>특정 만족하는 쌍을 찾는 경우</li>
</ul>
<p>주로 정렬 후에 양 끝쪽에서 시작한다. 왜냐? 정렬을 하면 규칙이 있고 그 규칙 하에 양 끝쪽에서 시작하면 가치치기가 빠르게 되기 때문이다. 정렬의 특성을 활용한다.! 그리고 조건을 만족하는 쪽으로 포인터를 이동하며 찾는 것을 생각하면 양쪽 끝에서 시작하는게 올바른 선택이다.</p>
<p><a href="https://www.acmicpc.net/problem/3273">두 수의 합</a> 해당 문제를 풀어보면 이해할 것이다.</p>
<ul>
<li>부분 배열 포인터 시작위치 (정렬의 속성을 사용) </li>
</ul>
<table>
<thead>
<tr>
<th>s</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>e</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>8</td>
<td>10</td>
</tr>
</tbody></table>
<pre><code class="language-python">
n = int(input())

numbers = list(map(int, input().split()))

result = int(input())

s, e = 0,n-1
numbers.sort()

count=0

while s!=e:

    if numbers[s] + numbers[e] &lt;= result: # 더 작은 경우에는 더 값을 키워야하기에 s를 증가
        if numbers[s] + numbers[e] == result:
            count+=1
        s+=1
    else: # 값을 줄여야하기에 e를 -
        e-=1

print(count)

</code></pre>
<h3 id="문제-풀이-리스트">문제 풀이 리스트</h3>
<ol>
<li><a href="https://www.acmicpc.net/problem/2118">https://www.acmicpc.net/problem/2118</a></li>
</ol>
<ul>
<li>처음에 시간초과</li>
<li>나중 풀이 : 투포인터 -&gt; O(N)으로 해결</li>
</ul>
<p>핵심 ) start와 end 포인터를 언제, 어떤 경우에 바꿔줘야 할지 생각하는 포인트</p>
<p>시계, 반시계 중 작은 하나 -&gt; dist 라고 하자. ---(1)
<strong>result는 가능한 dist 중 가장 큰 값이 된다.</strong> </p>
<ol>
<li>result가 update</li>
</ol>
<p>-&gt; end 포인터를 +1, 증가한 쪽의 거리를 합에 누적
2. result가 update가 안 될 때 (result가 (1)에서 구한 dist가 아니라면 update되지 않은거다. )
-&gt; start 포인터를 +1, 누적합에 값을 빼준다.</p>
<p>update될때까지만 end를 증가시키는 이유는 update되지 않았다는건 dist1이 가장 큰 값이 아니라는거고 그렇다는건 end를 또 늘려도? 그다음은 무조건 dist1이 가장 큰 값이 아니게 되기에 더 탐색할 필요없음.</p>
<pre><code class="language-python">
n = int(input())

s_diff = [0]*(n)

for i in range(n):
    s_diff[i] = int(input())

total = sum(s_diff) 

start = end = 0
result = 0

min_now = 0

while start &lt;= end and end &lt; n:

    min_dist = min(min_now, total - min_now) # 둘 중 작은게 두 점사이의 길이
    result = max(min_dist, result)

    if min_now == min_dist: # 현재가 가장 작은 값이였다면
        min_now += s_diff[end]
        end+=1
    else:
        min_now -= s_diff[start]
        start +=1
print(result)

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[LocalDateTime 맞추기]]></title>
            <link>https://velog.io/@sujin-create/LocalDateTime-%EB%A7%9E%EC%B6%94%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/LocalDateTime-%EB%A7%9E%EC%B6%94%EA%B8%B0</guid>
            <pubDate>Fri, 31 May 2024 16:09:21 GMT</pubDate>
            <description><![CDATA[<p>DEV에서 localDateTime이 달랐다. 한국시간보다 9시간 이전이기에, UTC기준 시간으로 보였다. 
WEB Application이 배포된 서버의 TimeZone을 봐야한다.
NCP(TZ -&gt; Seoul)에 배포하였지만, 그 전에 Application을 Docker에 배포하였기에 웹을 배포한 실질적인 서버는 Docker Container이고 Docker Container의 TimeZone을 체크해야한다.</p>
<p>Spring Application을 Docker를 Container를 사용하고 이를 NCP에 배포하며 사용하고 있다. 그렇다면 Spring 웹의 배포된 서버는 사실상 Docker Container이고 이를 NCP에 호스팅한 것으로 봐야한다. </p>
<p>문제해결)</p>
<ul>
<li>따라서, Docker 컨테이너의 timezone을 변경해준다. </li>
</ul>
<p>이때, CI/CD로 docker image를 자동배포하고 있다.</p>
<p>CI/CD를 통해 이미지를 만들고 NCP에 접속해서 docker compose up을 시켜주고 있다.
따라서, 아래와 같이 docker-compose.yml을 수정해주면, docker conatiner가 실행될때 TZ가 Asia/Seoul이 된다.</p>
<p>그리고, Volumes는 volume을 마운트할 수 있다.
즉, 로컬 파일에 설정된 시간대로 docker에 연결하겠다는 것이다. :ro는 read only이다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/836502c8-b259-427e-92c7-a47eb5cea948/image.png" alt=""></p>
<p>이렇게, 9시간 차이나던 craetedAt, updatedAt이 현재 서울 시간으로 정상 반영되도록 변경되었다.</p>
<p>아래는 NCP 서버에 접속해서 mount된 파일을 cat으로 본 것이고 encoding때매 파일이 깨져보인다. 그러나 가장 아래 KST-9를 확인할 수 있고 한국 표준시간으로 적용된 것을 확인할 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/59662ffe-68e4-491f-a1b2-5730249ace62/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Cookie 세팅 안 되는 문제 해결]]></title>
            <link>https://velog.io/@sujin-create/Spring-Cookie%EB%A5%BC-Browser%EC%97%90-Backend%EC%97%90%EC%84%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/Spring-Cookie%EB%A5%BC-Browser%EC%97%90-Backend%EC%97%90%EC%84%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 05 Apr 2024 16:12:30 GMT</pubDate>
            <description><![CDATA[<h2 id="intro">Intro</h2>
<p>cookie를 frontend에서 저장하지 않고 backend에서 저장하는 이유부터 알아보자!
왤까?
가장 중요한건 보안때문이다. 보안때매 사용하는거면 별로 안 중요한거면 그냥 넘겨도 되겠네 싶겠는데 별로 안 중요한걸 기록하며 사용하는 일이 많을까 싶다. 일단 내가 저장하려는거는 &quot;토큰&quot;이였다. access token을 cookie에 저장하려고한다. 탈취되거나 조작되면 안 된다. 따라서 코드로 접근해서 저장하고 넣는 과정을 frontend에게 허용한다면 보안 취약점이 될 수 있다.
중요한 정보를 Cookie에 저장하려면 Backend 코드에 의해서 Browser에 저장해줘야한다.!
그렇기 때문에 나는 Spring에서 Cookie를 세팅해줬다.
이 과정에서,,,, 백엔드 개발자도 기본적인 브라우저 동작 과정과 정책을 알고 있어야한다는 점을 깨달았다... 돌고돌고 돌아서 결국 Cookie를 browse에 세팅해줬다. 그 과정을 공유한다!</p>
<h2 id="사전지식">사전지식</h2>
<h3 id="samesite">Samesite</h3>
<p>같은 사이트인가? 말 그대로 같은 출처를 공유하는지 체크하는거고 &#39;같은 출처&#39;의 영역의 정의는 아래의 3가지 옵션에 따라 달라진다.
CORS랑 같은건가??뭐가다르지? 생각해봤다. 사실 처음에 같은 줄 알았다. Site랑 Origin의 체크의 차이이다. Samesite는 도메인을 비교한다. 포트는 상관없다. Origin이랑 다르다. </p>
<p>Site랑 Origin이랑 다르다. </p>
<p>Site는 http.asdf.com일때 asdf.com이다. 
그러면 https.asdf.zcxv.com일때 사이트는? zcxv.com이다. com과 같은 public suffix 바로 앞에까지를 포함해서 site라고 한다.</p>
<ul>
<li>None
다른 출처여도 허용한다. 도메인이 다른(ex. asdf.com, localhost 두개의 도메인 서로 쿠키 설정 가능) Cross-site도 가능하다.</li>
<li><blockquote>
<p>SSL 설정이 적용된(Https)환경에서 Cookie 세팅이 가능하다.</p>
</blockquote>
</li>
<li>Lax
크롬 브라우저의 기본값이다. 다른 Site를 차단한다 그러나, <code>&lt;a&gt;</code> , GET은 되고 이러한 몇가지 예외가 존재하긴한다.</li>
<li>Strict
무조건 Site가 같아야한다. Cross-Site는 쿠키 세팅이 불가능하다.</li>
</ul>
<p><strong>CSRF</strong>
Cross-Site Request Fogery 공격은 악의적으로 쿠키탈취를 위한 링크를 심어놓고 클릭을 유도하여 인증정보를 빼서 사용하기 위한 공격이다. 이를 방지할 수 있는게 Samesite다. 왜냐? 다른 도메인을 클릭했을 때 쿠키를 아예 전송할 수 없기 때문이다.! 물론 None일땐 다른 도메인이여도 가능하다. 그래서, None일때는 Secure가 (SSL이 적용된 도메인) 무조건 적용이 되어있어야 애초에 세팅이 가능하다.</p>
<h3 id="origin이란">Origin이란?</h3>
<p>그렇다면 위에서 말한 같은 Origin이란? 자꾸 도메인이라는 표현을 써서 헷갈릴 수 있는데(나도 그랬다)</p>
<blockquote>
<p>Origin은 Protocol, Host, Port로 구성된다. 세게가 모두 동일해야 같은 Origin이다.</p>
</blockquote>
<p>예를들어 React, Spring은 localhost일때 같은 Origin일까? Port가 같지 않으니,다른 Origin이다. 그러나 Samesite는 만족한다.<br>따라서, React, Spring 개발 환경일때 Spring에서 React 환경의 웹에 쿠키를 넣고 싶다면? 
그러나, 다른 Origin이다. 따라서, Origin이 다르면 Cross Origin으로 인해, 브라우저는 쿠키세팅을 막는다.</p>
<h3 id="httponly">HttpOnly</h3>
<p>Protocol인 HTTP를 사용할때만 죽, 코드가 아닌 네트워크 상에서만 쿠키 세팅을 할 수 있도록 설정할 수 있는 옵션이다.
True로 해줘야 XSS(Cross Site Scripting)인 SQL injection같이 악의적으로 사이트에 이상한 스크립트(코드)를 넣어 공격하는거를 방지할 수 있다.
이 옵션을 통해 프론트엔드에서 javascript를 통한 쿠키 탈취를 예방할 수 있게 된다.</p>
<p>그렇다면,네트워크로 접속하는건 어떻게 처리하지? 이거는 CSRF와 관련있다. 위에서 말했듯, CSRF는 다른 사이트로 넘어가며 네트워크상에서 탈취당하는건데 Samesite로 막을 수 있다. 그러나, None일때는? Secure로 막을 수 있다.</p>
<h3 id="secure">Secure</h3>
<p>HTTPS에서의 통신만 허용하게끔 하는 옵션이다. HTTPS 프로토콜을 사용하는 것은 데이터 암호화하여 통신하는 방법으로, 쿠키 또한 암호화되어 전송돼서 탈취당하더라도 내용을 알 수 없다. 그렇기에, 네트워크 상으로 쿠키가 이동해도 안전하게끔 해주는 옵션이다.</p>
<blockquote>
<p>Cookie 하나에 브라우저 지식이 많이 필요했다... 항상 느끼지만 문제를 해결할땐 이론을 파면 되는 것 같다! 그냥 무작정 구글링하지 않고 Cookie 설정 속성값들을 천천히 읽고 이해하니까 문제가 풀렸다! 이래에서 확인해보자!</p>
</blockquote>
<h3 id="문제">문제</h3>
<p>네트워크상으로 전송은 되는데, 브라우저에 세팅이 안 된다</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/ea49fdc8-3d2c-42ff-8fd3-eb6c786c85fd/image.png" alt="쿠키세팅안되는 사진"></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/210b1e60-d219-43b7-af4e-afa447ab8fc9/image.png" alt="쿠키세팅안되는 사진2"></p>
<p>request cookie에는 잘 들어간다. Network탭에서 Headers에 가보면 Set-Cookie에도 잘 들어가있는걸 확인했다.</p>
<p>서로 다른 Origin이기에 안 되는 것이다. 포트가 다르기에 credential을 true로 바꿔줘야한다.</p>
<ul>
<li>withCredential 옵션을 Frontend와 Backend 모두 허용해줘야 Request, Response에서 쿠키를 설정하고 가져올 수 있다.</li>
</ul>
<h3 id="해결-withcredentials을-모두-true">해결) withCredentials을 모두 True</h3>
<p>결론은, withCredentials를 Frontend와 Backend에서 모두 true로 해주니 Cookie설정 지옥에서 빠져나올 수 있었다!</p>
<pre><code class="language-java">@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping(&quot;/**&quot;)
                .allowedOrigins(
                        &quot;http://localhost:3000&quot;, // local,
                        &quot;https://where-qr.com&quot; //dev
                )
                .allowedMethods(&quot;GET&quot;, &quot;POST&quot;, &quot;PATCH&quot;, &quot;DELETE&quot;)
                .allowedHeaders(&quot;*&quot;)
                .allowCredentials(true);
    }
}</code></pre>
<p>그리고 주의할 점은, allowedOrigins를 특정 도메인을 정해줘야지, * 하면 allwCredentials true가 먹히지 않는다는 점이다!</p>
<h2 id="느낀점">느낀점</h2>
<ul>
<li><p>문제해결할때 흐름이 끊기면 안 된다..미루게 된다....! localstorage에서 refresh token을 드디어 cookie에 넣었다.</p>
</li>
<li><p>브라우저 정책! 매우매우 중요하다! Backend에서 브라우저에 뭔가를 세팅하려고한다? 무조건 정책부터 보고시작하자! 그리고 쿠키 옵션들을 완전히 이해하고 시작했으면 문제해결이 빨랐을 것이다.</p>
</li>
<li><p>덕분에 쿠키랑 관련된 브라우저 보안관련 지식에서 마스터했다...^^ HttpOnly는 network 전송만 가능하도록하기에, js와 같은 코드로 발생하는 XSS 방지. 그러면 다른 사이트로 클릭을 유도해서 쿠키를 탈취할 수 있는 것을 막는 것은? CSRF를 막는것인데 -&gt; 이는 Secure를 통해 가능하고 ...등등에 대해서 이해했다. </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JAVA] JDK 도구를 사용하여 모니터링을 해보자!]]></title>
            <link>https://velog.io/@sujin-create/JAVA-JDK-%EB%8F%84%EA%B5%AC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@sujin-create/JAVA-JDK-%EB%8F%84%EA%B5%AC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 28 Nov 2023 03:21:30 GMT</pubDate>
            <description><![CDATA[<p> 본 내용은, JAVA 전체적인 구조에 대한 이해 중, JDK 도구의 역할과 그 중 모니터링 도구에 대해서 알게된 점을 공유하기 위해 작성하였습니다.</p>
<p> <a href="https://docs.oracle.com/en/java/javase/11/tools/tools-and-command-reference.html#GUID-55DE52DF-5774-4AAB-B334-E026FBAE6F34">JDK 도구 살펴보러가기</a></p>
<p>오늘은 JDK에서 제공하는 모니터링 도구들에 대해서 알아보려고한다!.
CPU, Memory, Classes 등에 대한 정보들을 모니터링하면서 메모리 Leak 혹은 GC가 최적화 등에 대한 전략을 잘 세워서 최적화를 해나갈 수 있다고 한다!. 그러기 위해서는 우선 resource를 확인해야하는데, 다양한 방법이 있다!.
termainl에서 단 한줄의 명령어로 아래와 같은 모니터링 툴을 사용할 수 있는데 너무 좋은 것 같다! 
 <img src="https://velog.velcdn.com/images/sujin-create/post/79893a8f-ae01-43ae-aeb8-72bd207d2554/image.png" alt=""></p>
<p>최근에 JVM Heap Memory Issue를 운영환경에서 겪으며, Code에서 Memory Leak이 되는 원인이 있는것인지 찾아보기 위해 Jstat와 Visual VM을 다운받아 모니터링하며 메모리 의 여부를 확인한 경험이 있다. 이때, jstat, Visual VM의 큰 도움을 받았는데 아니...JDK 문서를 살펴보는데 다른 것들이 많았다! 위에 있는 도구, application을 다운받아야할까? 아니다! mac에서 terminal을 열어서 jconsole이라고 입력하기만 하면 해당 서비스를 지원한다.</p>
<p>우선 위의 사진은 jconsole을 사용한 것이다.</p>
<h3 id="jconsole">jconsole</h3>
<blockquote>
<p>JVM monitoring이 가능하다.</p>
</blockquote>
<p>memory와 GC와의 관게를 파악하고 싶다면? Memory 탭을 들어가면 아래와 같이 YGC, OGC time이 나오며 오른쪽 위에서 PerfomGC를 분석할 수도 있습니다.
<img src="https://velog.velcdn.com/images/sujin-create/post/20882f7d-81ad-46ee-8d28-20e0f5c831b1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/08617d61-56a6-454c-ad54-58fae5f466ac/image.png" alt=""></p>
<p>위의 사진처럼 Memory의 양상을 추적할 수 있고, Chart는 원하는 것을 고를 수 있다.
이때, CodeCache라는 것이 존재하는데 JIT Compiler가 ByteCode를 기계어로 번역하여 저장해놓는 곳이 바로 CodeCache이다. Code Cache의 경우에는 non heap영역에 해당한다. 따라서, GC가 직접적으로 관여하지 않기에 모니터링이 더욱 필요할 것이다!</p>
<p>heap memory usage</p>
<ul>
<li><p>Eden Space: 새로 생성된 객체가 위치하는 초기 공간입니다. 대부분의 객체는 여기에서 생성되며, 초기에는 Eden Space에서 발생한 가비지 컬렉션을 피할 수 있습니다.</p>
</li>
<li><p>Survivor Spaces (S0, S1): Eden Space에서 가비지 컬렉션이 발생하면 살아남은 객체들은 이곳으로 이동합니다. Survivor Spaces는 두 부분(S0와 S1)으로 나뉘어 있으며, 한 곳에서 객체가 쌓이는 동안 다른 곳은 비어 있게 됩니다.</p>
</li>
<li><p>Old Generation (Old Space 또는 Tenured Space): 여러 차례의 가비지 컬렉션 후에 살아남은 객체들이 최종적으로 이곳으로 이동합니다. Old Generation은 일반적으로 더 오래 살아남은 객체들이 위치하는 곳입니다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/7993f2a1-41ef-4ba1-a41b-6f7b2b177554/image.png" alt=""></p>
<p>다음과 같이 VM 에 대한 정보를 파악할 수 있는 페이지도 제공한다.</p>
<h3 id="jps">JPS</h3>
<blockquote>
<p>jps 명령어를 사용해 실행중인 java process를 볼 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/1b01d615-976a-446c-8602-93940909a619/image.png" alt="jps"></p>
<p>아래와 같이 실행중인 프로세스 확인이 가능하고 앞에 있는 숫자는 process id이다.</p>
<p>분석하고 싶은 process id를 확인하는데 사용할 수 있다.</p>
<h3 id="jstat">jstat</h3>
<blockquote>
<p>jstat를 사용하여 terminal에서 JVM 통계 데이터를 모니터링할 수 있다.</p>
</blockquote>
<p>다양한 옵션을 제공하는데 그 옵션에 대한 hint를 얻으려면? <code>jstat -help</code>를 입력하면 된다.</p>
<p>그리고 <a href="https://docs.oracle.com/en/java/javase/11/tools/jstat.html#GUID-5F72A7F9-5D5A-4486-8201-E1D1BA8ACCB5">jstat oracle 문서</a>를 통해서 확인이 가능하다.</p>
<ul>
<li>class loader 통계</li>
</ul>
<p>JVM(Class Loader) 클래스 로더는 JVM 내에서 클래스 파일을 로딩하고 메모리에 적재하는 역할을 하는데 이때 로드된 클래스들에 대한 통계를 확인할 수 있다.</p>
<ul>
<li>compiler 통계</li>
</ul>
<p>Java HotSpot VM 컴파일러 통계를 확인할 수 있는데 수행된 컴파일 적업이나 실패한 컴파일 작업 수를 확인할 수 있다.</p>
<ul>
<li>gc 통계</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/1db13ec6-a403-4560-a055-0e8710feaf35/image.png" alt=""></p>
<p>현재 생존 공간, eden과 old 영역의 메모리와 YGC, OGC, 전체에 걸쳐 진행되는 FGC 등을 확인할 수 있다.</p>
<h3 id="마무리">마무리</h3>
<p>이렇게 다양한 모니터링 도구를 제공하고 이슈 트래킹이나 성능 개선을 위해서 문제점을 파악하고 최적화를 해나가는 과정을 대비해, 알아두면 좋을 것 같다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] spring security의 예외처리]]></title>
            <link>https://velog.io/@sujin-create/spring-security%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@sujin-create/spring-security%EC%9D%98-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Sun, 26 Nov 2023 09:43:10 GMT</pubDate>
            <description><![CDATA[<p>Exception 구조화를 하기 위해, 보통 ExceptionHandler Class를 사용해서 예외처리를 진행한다. ExceptionHandler Class는 @RestControllerAdvice와 @ExceptionHandler를 통해서 만들 수 있다. 그러나 spring security에서 예외처리를 위해서도 해당방법을 사용해도 될까? </p>
<h3 id="restcontrolleradvice와-exceptionhandler는-controller에서-동작">@RestControllerAdvice와 @ExceptionHandler는 Controller에서 동작</h3>
<p>두개의 annotation을 사용해서 Application 전역에서 발생하는 특정 예외에 대해 , 예외를 한번에 처리할 수 있다.</p>
<ul>
<li>@RestControllerAdvice</li>
</ul>
<pre><code class="language-text">A convenience annotation that is itself annotated with @ControllerAdvice and @ResponseBody.
Types that carry this annotation are treated as controller advice where @ExceptionHandler methods assume @ResponseBody semantics by default.</code></pre>
<p>해당 annotation은 @ControllerAdvice와 @ResponseBody로 구성되어있는데, @ControllerAdvice의 역할은 아래의 사진과 같다.
<img src="https://velog.velcdn.com/images/sujin-create/post/2cfb0f40-5201-440f-80b0-eb85d9533617/image.png" alt=""></p>
<p>@ControllerAdvice는 여러 @Controller 클래스에 걸쳐 코드를 중복하지 않고 예외 처리 및 기타 관련 작업을 효과적으로 공유하고자 할 때 사용되도록 구현된다.
여러 @Controller 클래스 간에 공유되는 @ExceptionHandler, @InitBinder, 또는 @ModelAttribute 메서드를 선언하는 클래스를 위한 @Component의 특수화된 어노테이션이다.</p>
<p>즉, @ControllerAdvice가 붙여진 @RestControllerAdvice는 @ExceptionHandler를 @ResponseBody 의미를  가진다. </p>
<p>따라서, Controller 클래스에 걸쳐서 코드 중복을 피하기 위해서 특정 예외들에 대해서 한번에 처리하고자, CustomExceptionHandler를 생성하기 위해 두 annotation을 사용한다.</p>
<p>어떻게 사용할 수 있나? </p>
<pre><code class="language-java">@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity&lt;String&gt; handleException(Exception e) {
        // 예외에 대한 처리 로직
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                             .body(&quot;Internal Server Error&quot;);
    }

    @ExceptionHandler(MyCustomException.class)
    public ResponseEntity&lt;String&gt; handleCustomException(MyCustomException e) {
        // 특정 예외에 대한 처리 로직
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                             .body(&quot;Bad Request: &quot; + e.getMessage());
    }
}
</code></pre>
<p>다음과 같이 ExceptionHandler를 만들 수 있게 되는 것이다. 
위의 코드는 단순히 예시를 보여줄 뿐입니다! 각자 원하는 MyCustomException Class를 만들면 됩니다~</p>
<ul>
<li>저의 경우, MyCustomException Class를 만들고 RuntimeException을 상속받아 super를 통해 생성자에 message를 함께 넣어 처리하였습니다. MyCustomException의 생성자에서 Trowable 생성자를 호출하기 위해, RuntimeException 부모 클래스의 기본 생성자를 사용해 message를 넘겨줬습니다. Java에서 모든 예외 클래스는 최소한 하나의 부모 클래스인 Throwable의 생성자를 호출해야합니다.!  해당 부분을 기억해서 만들면 될 것 입니다..! 만약 그게 아니라면 Compile Error가 날 것입니다.</li>
</ul>
<p>다시 돌아와서 전역에서 발생하는 예외를 Controller단에서 처리한다? 그러면 Controller에 들어오기 전에 발생하는 예외는 어떻게 해야할까?를 생각해야합니다.</p>
<h3 id="spring-security에서-controlleradvice를-사용하지-못하는-이유">Spring Security에서 @ControllerAdvice를 사용하지 못하는 이유</h3>
<p>결론부터 말하자면, 위의 방법인 &quot;@RestControllerAdvice와 @ExceptionHandler를 사용해서는 spring filter chain에서의 Exception을 처리할 수 없습니다.&quot;</p>
<p>따라서, try-catch로 처리되고 있는 예외가 아닌 예상하지 못한 error가 발생했다면? Exception이 발생할 것이다.</p>
<p>그 이유는 spring security filterchain의 동작에 대해서 알면 자연스럽게 알게될 것이다.!</p>
<ul>
<li>사실 저도, ㅎㅎㅎㅎ 깊게 생각 하지 않고 코드를 짰고 accessToken이 유효하지 않았을 때 spring security filterchain에서 error가 났고 이를 대비하지 않아서 배포서버에서 제가 정의해둔 Custom Exception Response와 다른 구조의 error를 볼 수 있었습니다.허허허,, 그래서 서비스 실사용을 미루게 됐습니다...</li>
</ul>
<p><a href="https://velog.io/@sujin-create/Spring-spring-security%EC%99%80-JWT-%EC%9D%B8%EC%A6%9D-%EC%9D%B8%EA%B0%80-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">spring security로 인증,인가 구현하기</a></p>
<p>해당 포스트에서 Spring Security를 공부하였습니다.</p>
<div align="center">
<img src="https://velog.velcdn.com/images/sujin-create/post/a33b1774-023b-4331-b03a-27079ed130b2/image.png" width="30%" height="10" >
</div>


<p>이 사이에 spring security를 사용한다면 filterchain이 연결되어 request를 가지고 dispatcher servlet으로 들어오고 -&gt;  다시, servlet에서 response를 가지고 출발하여 filterchain을 거쳐서 다시 client에 나가는 구조입니다.</p>
<h2 id="filter단에서-처리해줘야한다">filter단에서 처리해줘야한다.</h2>
<h3 id="1-인증-필터--exceptionhandling과-accessdeniedhandler">1. 인증 필터 : exceptionHandling과 accessDeniedHandler</h3>
<pre><code class="language-java">.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler()) // 권한이 없는 경우의 처리</code></pre>
<p>filterchain을 config를 작성할때, exceptionHandling을 사용해서 권한 에러에 대해서 처리를 진행해줬다!</p>
<ul>
<li>accessDeniedHandler
403 access Denied error를 핸들링 하기 위해서 만든 handler이다.
<img src="https://velog.velcdn.com/images/sujin-create/post/80645729-2c4f-4c9f-b000-03886210f18c/image.png" alt=""></li>
</ul>
<p>AccessDeniedHandler를 넘겨줘야한다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/9da78787-e6c0-4e8b-aa6d-21b6e96c0823/image.png" alt=""></p>
<p>따라서, 필요한 parameter을 넘겨 custom handler를 생성해줘서 AccessDeniedHandler를 반환하도록 해줬다.</p>
<pre><code class="language-java">@Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, accessDeniedException) -&gt; {

            CustomException customException = new ForbiddenException(&quot;forbidden&quot;,this.getClass().toString());
            ErrorResponse errorResponse =
                    new ErrorResponse(customException.getErrorType(), customException.getMessage(), customException.getPath());

            Map&lt;String, Object&gt; responseBody = new HashMap&lt;&gt;();
            responseBody.put(&quot;status&quot;, &quot;FAILED&quot;);
            responseBody.put(&quot;data&quot;, errorResponse);

            response.setStatus(200);
            response.setContentType(&quot;application/json;charset=UTF-8&quot;);
            response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody));
        };
    }
</code></pre>
<ul>
<li>결과 (현재 Token에 대한 결과)<div align = center>
<img src = "https://velog.velcdn.com/images/sujin-create/post/6221f295-08ea-403b-8738-0cbd7d7b19d8/image.png" width=50%>


</li>
</ul>
<ul>
<li><p>추가로 알아두면 좋은 것!</p>
</li>
<li><p>) 추가로, unauthorized에 대해서도 처리할 수 있다. (사용할게 아니라 간략히만 작성했다!)</p>
</li>
<li><p>AuthenticationEntryPoint
Commences an authentication scheme.</p>
<div align = center>
<img src = "https://velog.velcdn.com/images/sujin-create/post/e14aa061-71c5-487b-87a4-d7e7d4fec4f2/image.png" width=50%>
</div>

</li>
</ul>
<p>여기서 볼 수 있듯이, request, response, exception을 parameter로 넘겨줘야한다.lambda 표현식을 사용한다면 아래와 같이 만들 수 있을 것이다!</p>
<pre><code class="language-java"> @Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
        return (request, response, authException) -&gt;
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, &quot;Unauthorized&quot;);
 }</code></pre>
<p>-&gt; 하지만, 다양한 에러들을 처리해주기 위해서 FilterChain을 하나 만들기로 했다!</p>
<h3 id="2-인가-필터">2. 인가 필터</h3>
<p>spring security fitler chain으로 만들어줬다! role의 경우에는 GrantedAuthority를 확인하는거고 Authentication이 성공한 시점에 Authorization을 하는 것이다!</p>
<p>이때, 인증은 사실  Role에 의해서 Forbidden만 존재하기에 위의 방법으로 간편하게 처리했다! 반면 Authentication은 그 안에서 발생가능한 예외가 많다. </p>
<p>token이 없을 때, 토큰이 유효하지 않은 경우. 등등이 존재한다.</p>
<p><strong>로직</strong></p>
<ul>
<li>spring security는 doFilter()로 다음단계 Filter로 넘긴다.
Filter1 -&gt; Filter2( 인증필터 : 여기서 authentication 예외발생 가능) -&gt; Filter1(예외처리) 다음과 같이 동작해야한다.
따라서, 여기서 Filter1은 예외처리를 진행해줘야하고 처리해주는 로직은 Filter2가 될 것이기에 Filter1이 ExceptionFilterChain이 되어야하고 Filter2는 인증,인가를 위한 FilterChain이 될 것이다.</li>
</ul>
<p>나의 코드에서는 Filter2의 역할을 하는것은 jwtAuthenticationFilter이다. 
jwtAuthenticationFilter 이전에 Filter1의 역할을 하는 <code>jwtAuthenticationExceptionFilter</code>가 동작하도록 하기 위해서, <code>addFilterBefore</code>를 사용하면 된다.</p>
<pre><code class="language-java">.addFilterBefore(jwtAuthenticationFilter, RequestHeaderAuthenticationFilter.class)
.addFilterBefore(jwtAuthenticationExceptionFilter, JwtAuthenticationFilter.class); // 인가에 대한 필터</code></pre>
<h3 id="jwtauthenticationexceptionfilter-생성">jwtAuthenticationExceptionFilter 생성</h3>
<p>CustomException에 해당하는 에러가 &#39;jwtAuthenticationFilter&#39;에서 발생했다면, 그 error message를 전송할 것이다.</p>
<p>그 외의 예외에 대해서는 500 interval error message를 전송해줄 것이다.</p>
<p><strong>instanceOf</strong></p>
<p>instanceOf를 사용해서 Exception의 종류를 구분하여 처리하면 된다.</p>
<pre><code>@Component
@Slf4j
public class JwtAuthenticationExceptionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try{
            filterChain.doFilter(request, response); // jwtAuthentication 실행
        }catch(Exception e){
            if (e instanceof CustomException) {// 정의한 error에 속할 경우
                CustomException customException = (CustomException) e;
                handleAuthenticationException(response, customException.getMessage());
            }else{
                handleAuthenticationException(response, &quot;internal server error&quot;); // 전체 internal로 처리
            }
        }
    }

    private void handleAuthenticationException(HttpServletResponse response,String message ) throws IOException {
        response.setStatus(200);
        response.setContentType(&quot;application/json;charset=UTF-8&quot;);

        Map&lt;String, Object&gt; responseBody = new HashMap&lt;&gt;();
        Map&lt;String, Object&gt; responseBodyData = new HashMap&lt;&gt;();
        responseBodyData.put(&quot;message&quot;, message);
        responseBody.put(&quot;status&quot;, &quot;FAILED&quot;);
        responseBody.put(&quot;data&quot;, responseBodyData);

        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(responseBody));
        writer.flush();
        writer.close();
    }
}
</code></pre><p>나는 모든 Response를 200 status로 설정하고 status로 구분하는 틀을 잡아놨기에, Spring Security의 Excpetion의 Response도 그 구조에 맞춰서 제공해주기 위해서 handleAuthenticationException 내부에서 Response 위와같이 작성하였다. </p>
<div align = center>
<img src = "https://velog.velcdn.com/images/sujin-create/post/be55338b-2889-4318-9579-5a077dbbee61/image.png" width=50%>
</div>

<div align = center>
<img src = "https://velog.velcdn.com/images/sujin-create/post/afd62fa0-6092-4687-98e8-511bcf533734/image.png" width=50%>
</div>


<p> 이렇게 Filter Chain을 Custom해서 token이 없는 경우와 유효하지 않은 경우 등등에 대해서 처리해줄 수 구조화된 예외처리를 해줄 수 있었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring]  Response Entity 구조화하기]]></title>
            <link>https://velog.io/@sujin-create/spring-REST-Api-Response-Entity-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/spring-REST-Api-Response-Entity-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Nov 2023 16:33:41 GMT</pubDate>
            <description><![CDATA[<h3 id="rest-api-응답-반환-어떻게-해야할까">REST Api 응답 반환, 어떻게 해야할까?</h3>
<p>rest api의 반환 결과, 어떻게 하는게 좋을까?</p>
<p>spring 프로젝트를 하며 기능적으로 완성됐다고 생각하는 api들에 대해서 리팩토링을 진행하다 문득 생각이 들었다. </p>
<p>우선 나는 아래와 같이 구조화했다. </p>
<ul>
<li><p>GET, POST에 대한 정상 응답 Response -&gt; DTO마다 다르다.
<img src="https://velog.velcdn.com/images/sujin-create/post/2f3cf4e4-8fa9-4694-b1dc-ee6be5b9da7f/image.png" alt=""></p>
</li>
<li><p>예외처리를 위한 custom Error Response
<img src="https://velog.velcdn.com/images/sujin-create/post/6b91ad82-d57c-4e69-87d8-8ff3c36b955c/image.png" alt=""></p>
</li>
</ul>
<p>frontend 팀원에게 Api를 설명하다보니, error인지 아닌지 구분하기 위해서 &quot;errorType&quot;이라는 key값이 있는지 파악하는 코드를 사용해야한다고 설명했고 말하다보니,, 뭔가 이상하다. 타입안의 값이 아닌 errorType의 key값 자체가 있는지 없는지로 에러를 판단하는게 과연 구조화가 잘 된 Response일까? 아니다라는 생각이 들었다.
차라리 모든 응답에 &quot;status&quot;와 같은 key를 가지게 하고 key 안의 값으로 판단하는게 더 구조화되고 가독성 있는 코드를 만들지 않을까?생각이 들었다. 그러면서 response 타입들을 알아보기 시작했다! </p>
<h3 id="api-response-형태-벤치마킹을-진행하자">Api Response 형태 벤치마킹을 진행하자??</h3>
<p>찾아보니, 정답은 정해져있지 않은 것 같다. 그래서, 이럴때는 대기업의 api developers 문서를 슬~~쩍 참고하며 익숙해지면 좋을 것 같다고 생각했다.</p>
<p>우선 카카오의 response 형태는 Header와 Body의 조합이다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/8b258f90-84c7-491e-a902-28fc00772424/image.png" alt="kakao api"></p>
<p>다음으로 네이버의 response의 형태는? 역시 마찬가지였다. HTTP 상태 코드로 리턴하고 Body로 정보를 전달한다고 나와있다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/a982b28c-7d84-4cdb-8079-6a9e3b455a14/image.png" alt="naver api"></p>
<ul>
<li><p>해당 방법이 나의 프로젝트에서 과연 알맞을까?
현재 프로젝트에서는 규모가 그렇게 크지 않기에 모든 error에 대해서 handling하는 page가 동일하다. 따라서 status code를 특별히 구분해줄 필요까지는 없다고 생각했다.
그리고 다 나열하고 있기에는 오히려 정상 reponse일때의 code와 error일때의 response를 구분하는 로직을 넣는게 간단한 로직인데 더욱 복잡하게 만들 수 있다고 생각했다. </p>
<ul>
<li>따라서, 벤치마킹은 하지 않도록 했다. </li>
</ul>
</li>
</ul>
<p>그래서 그냥 error일때와 정상 response일때의 반환 구조를 맞춰주는 방법을 생각했다.</p>
<h3 id="구조만들기">구조만들기</h3>
<ul>
<li>error response<pre><code class="language-java">package whereQR.project.exception;
</code></pre>
</li>
</ul>
<p>import lombok.Getter;</p>
<p>@Getter
public class ErrorResponse {</p>
<pre><code>private ErrorType errorType;
private String message;
private String path;

public ErrorResponse(ErrorType errorType, String message, String path){
    this.errorType = errorType;
    this.message = message;
    this.path = path;
}

public ErrorResponse(ErrorType errorType, String message){
    this.errorType = errorType;
    this.message = message;
}</code></pre><p>}</p>
<pre><code>error response를 위한 class는 위와같이 작성했다. 그리고 그때의 결과 형식은 아래와 같다.

{
    &quot;errorType&quot;: &quot;BAD_REQUEST&quot;,
    &quot;message&quot;: &quot;kakao api 요청이 유효하지 않습니다.&quot;,
    &quot;path&quot;: &quot;class whereQR.project.service.KakaoAuthService&quot;
}


- DTO ex
```java 
  @GetMapping(&quot;/detail&quot;)
    public ResponseEntity&lt;MemberDetailDto&gt; detail() {
        Member currentMember = MemberUtil.getMember();
        return ResponseEntity.ok(memberService.getMemberById(currentMember.getId()).toMemberDetailDto());
    }</code></pre><pre><code class="language-java">@Data
public class MemberDetailDto {

    private String username;
    private String phoneNumber;
    private List&lt;Qrcode&gt; qrcodes;

    public MemberDetailDto(String username, String phoneNumber, List&lt;Qrcode&gt; qrcodes){
        this.username = username;
        this.phoneNumber = phoneNumber;
        this.qrcodes = qrcodes;
    }
}
</code></pre>
<p>DTO가 위와 같을 때, Response는 아래와 같다.</p>
<p>{
    &quot;username&quot;: &quot;이름&quot;,
    &quot;phoneNumber&quot;: &quot;01012345678&quot;,
    &quot;qrcodes&quot;: []
}</p>
<p>우선 error인지 success type인지를 구분하기 위한 status를 key로 추가해주자! 그리고 data에 나머지를 넣어주자!</p>
<p>그리고 DTO를 반환하는 controller에서 보이는 것처럼 ResponseEntity로 감싸서 반환하도록 할 것이다. 
상태코드, header값은 넣지 않을 것이다!</p>
<p>반환하면서 패턴도 코드를 읽을 때 가독성도 올라가도록 Builder pattern을 사용해서 구조화를 진행해줬다!</p>
<ul>
<li><p>ResponseEntity Class 만들기</p>
<pre><code class="language-java">@Data
@Builder
public class ResponseEntity {

  @Enumerated(EnumType.STRING)
  public Status status;

  public Object data;
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>- status를 enum으로 생성하기
```java
public enum Status {
    SUCCESS(&quot;SUCCESS&quot;),
    FAILED(&quot;FAILED&quot;);

    private final String type;

    Status(String type) {
        this.type = type;
    }

}</code></pre><h3 id="변경-후">변경 후</h3>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/8ff2890e-c9a9-4ef3-85c0-8402dfae5c7b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/0734779b-d79e-4055-a7f3-2d6e04657785/image.png" alt=""></p>
<ul>
<li>id값만 반환하는 api의 경우, 가장 보기가 좋아졌다..ㅎㅎ</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/e452e11c-17b5-46f2-8b6d-a022f20b5a7b/image.png" alt=""></p>
<p>status와 data가 없었다면?? &quot;3<del>~</del>700&quot;만 존재했을 것이다,,,ㅜㅜ</p>
<h3 id="마무리">마무리</h3>
<p>처음부터 response 타입까지 미리 정해두고 작성을 했었다면 더 빠르게 작업할 수 있었을텐데, 그러지 못했다. 다음에는, 처음부터 api 설계를 할 때 형태까지 제대로 잡고 들어가자! 그리고 덕분에 builder 패턴까지 알아보게 되었다! 제대로 각잡고 다 builder 패턴으로 바꿔버릴까? 생각중이다!! 
우선은 제 기준에서 controller에서 코드가 중구난방인게 싫어서 response를 우선으로 builder 패턴을 적용해봤습니다~</p>
<ul>
<li>) 더욱 좋은 방법이나 구조가 있다면 추천해주시면 감사하겠습니다~~ :)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[암호화 알고리즘] DES]]></title>
            <link>https://velog.io/@sujin-create/%EC%A0%95%EB%B3%B4%EB%B3%B4%EC%95%88-DES</link>
            <guid>https://velog.io/@sujin-create/%EC%A0%95%EB%B3%B4%EB%B3%B4%EC%95%88-DES</guid>
            <pubDate>Wed, 01 Nov 2023 07:45:20 GMT</pubDate>
            <description><![CDATA[<h3 id="des란">DES란?</h3>
<p>DES란 암호화 알고리즘 중 하나로, AES의 기반이 되는 <strong>Data Encryption Standard</strong> 보안 알고리즘이다.
특징은, substitution과 permutation을 반복하는 것이다. DES는 0과 1로 이루어진,비트 총 64비트를 16 round에 걸쳐서 변형시키는 방법을 사용한다.</p>
<h3 id="des-algorithm">DES Algorithm</h3>
<p>16번의 라운드를 걸쳐서 64 bit를 조작하게 되는데, 각 라운드에서 진행되는 과정을 소개한다.</p>
<h3 id="1-one-round">1. One round</h3>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/c15b56a5-f46d-4a1b-8a76-8e2c2cc536db/image.png" alt="one round"></p>
<ul>
<li>input key : 암호화하려는 키 -&gt; 64 bit가 들어온다.</li>
<li>left(L), right(R)로 32 bit씩 쪼개야한다. 아래의 그림은 0번 Round의 과정이다.</li>
<li>f function : right(R) key와 48 bit로 들어오는 각 라운드마다 정해진 key값인 K를 사용하여 연산한다. -&gt; 이때, R을 48 bit로 늘리는 과정이 필요한데, E-bit selection table을 사용해서 늘릴 수 있다. 아래의 과정이라고 이해하면 된다.
<img src="https://velog.velcdn.com/images/sujin-create/post/5b3fe8d4-bcb0-4431-89f3-a66755845ec0/image.png" alt="f function">
다음으로, substitution을 사용해서 변형하고 permutation을 사용해서 6비트씩 4비트로 변환하며 48비트를 32비트로 다시 변형한다. 그 과정이 아래의 그림이다.
<img src="https://velog.velcdn.com/images/sujin-create/post/d2804445-828e-4418-a06f-80ea98b311ea/image.png" alt="">
<img src="https://velog.velcdn.com/images/sujin-create/post/0c7602f1-f866-4dde-91b1-44006369556f/image.png" alt=""></li>
</ul>
<ul>
<li>f function 결과(f(R, K))인 32 bit와 left(L)을 사용해서 XOR 연산을 실행한다. </li>
<li>R(0) -&gt; L(1) 그리고 L(0)과 R(0)을 연산한 결과를 R(1)로 조합한다.</li>
</ul>
<h3 id="2-one-round-연산에-대한-code">2. One round 연산에 대한 code</h3>
<p>java를 사용해서 라운드 하나에 대한 코드를 작성해보자!</p>
<p>이때, E-bit selection table과 substitution table은 DES 표준 문서를 보며 사용했다!</p>
<pre><code>package 기타.des;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class DES {

    private static final String INPUT_FILEPATH = &quot;/Users/baeksujin/Desktop/JavaStudy/java_basic/src/기타/des/roundinput.txt&quot;;
    private static final String KEY_FILEPATH = &quot;/Users/baeksujin/Desktop/JavaStudy/java_basic/src/기타/des/roundkey.txt&quot;;

    private static final String OUT_FILEPATH = &quot;/Users/baeksujin/Desktop/JavaStudy/java_basic/src/기타/des/output.txt&quot;;
    private static final int[] E_BIT_TABLE = {32, 1, 2,3,4,5,4,5,
                                                6,7,8,9,8,9,10,11,
                                                12,13,12,13,14,15,16,17,
                                                16,17,18,19,20,21,20,21,
                                                22,23,24,25,24,25,26,27,
                                                28,29,28,29,30,31,32,1};

    private static final int[][] SubstitutionTable = {
            {
                14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7
            },
            {
                0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8
            },
            {
                4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0
            },
            {
                15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13
            }
    };

    private static final int[] PermutationTable = {
            16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25
    };

    private static List&lt;String&gt; getKeys(){
        List&lt;String&gt; result = new ArrayList&lt;&gt;();
        try {
            // 파일을 읽기 위한 FileReader 객체 생성
            FileReader inputFileReader = new FileReader(INPUT_FILEPATH);
            FileReader keyFileReader = new FileReader(KEY_FILEPATH);


            // BufferedReader를 사용하여 파일 내용을 읽어옴
            BufferedReader inputBufferedReader = new BufferedReader(inputFileReader);
            BufferedReader keyBufferedReader = new BufferedReader(keyFileReader);

            String input, roundKey;
            while ((input = inputBufferedReader.readLine()) != null) {
                // 파일에서 한 줄씩 읽어서 처리
                System.out.println(&quot;inputKey -&gt; &quot; + input);
                result.add(input);
            }
            while ((roundKey = keyBufferedReader.readLine()) != null) {
                // 파일에서 한 줄씩 읽어서 처리
                System.out.println(&quot;roundKey -&gt; &quot; + roundKey);
                result.add(roundKey);
            }

            // 파일과 스트림을 닫음
            inputBufferedReader.close();
            inputFileReader.close();
            keyBufferedReader.close();
            keyFileReader.close();

            return result;

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private static String xorBinaryStrings(String s1, String s2){

        String result = &quot;&quot;;
        for(int i=0; i&lt;s1.length(); i++){
            char s1Value = s1.charAt(i);
            char s2Value = s2.charAt(i);

            if(s1Value == s2Value){
                result = result.concat(&quot;0&quot;);
            }else{
                result = result.concat(&quot;1&quot;);
            }
        }
        System.out.println(&quot;xorBinaryStrings result -&gt; &quot; + result);
        return result;

    }

    private static String convertTo48BitByUsingEBitTable(String right){

        String result =&quot;&quot;;

        for(int i=0; i&lt;48; i++){
            int temp = E_BIT_TABLE[i];
            result = result.concat(String.valueOf(right.charAt(temp-1)));
        }

        System.out.println(&quot;convertTo48BitByUsingEBitTable result -&gt; &quot; + result);

        return result;
    }

    private static String substitution(String input){

        String result =&quot;&quot;;

        for(int i=0; i&lt;48; i= i+6){// 6개씩 8번 반복

            String subBinary = input.substring(i, i+6);
            String rowBinary = String.valueOf(subBinary.charAt(0)) + subBinary.charAt(5);
            String columnBinary = subBinary.substring(1,5);
            int row = Integer.parseInt(rowBinary, 2); // binary to decimal!
            int column = Integer.parseInt(columnBinary, 2); // binary to decimal!
            int substitutionTemp = SubstitutionTable[row][column];
            // 4비트로 변경
            String substitutionTempBinary = Integer.toBinaryString(substitutionTemp);
            if (substitutionTempBinary.length() &lt; 4) {
                int leadingZeros = 4 - substitutionTempBinary.length();
                for (int j = 0; j &lt; leadingZeros; j++) {
                    substitutionTempBinary = &quot;0&quot; + substitutionTempBinary;
                }
            }
            result = result.concat(substitutionTempBinary); // 4비트로 변경됨 (6비트가 4비트로 변경 -&gt; 총 32비트로)// 4비트로 변경됨 (6비트가 4비트로 변경 -&gt; 총 32비트로)
        }

        System.out.println(&quot;substitution result -&gt; &quot; + result);
        return result;
    }

    private static String permutation(String input){

        String result = &quot;&quot;;

        for(int i=0; i&lt;32; i++){
            result = result.concat(String.valueOf(input.charAt(PermutationTable[i]-1)));
        }

        System.out.println(&quot;permutation result -&gt; &quot; + result);
        return result;
    }
    public static void main(String[] args) {

        List&lt;String&gt; keys = DES.getKeys();

        String input = keys.get(0);
        String roundKey = keys.get(1);

        // step 1. 분리
        String left = input.substring(0,32);
        String right = input.substring(32);

        System.out.println(&quot;input key를 나눈 값 -&gt; 왼쪽 : &quot; + left + &quot; 오른쪽 : &quot; + right);


        // step2. R (32 -&gt; 48) by using E-bit-Table

        String convertRightTo48Bit = DES.convertTo48BitByUsingEBitTable(right);

        // step3. roundKey와 convertRightTo48Bit의 xor연산
        String xorResultBit = DES.xorBinaryStrings(convertRightTo48Bit, roundKey);

        // step4. substitution

        String substitutionResultBit = DES.substitution(xorResultBit);

        // step5. permutation

        String permutationResultBit = DES.permutation(substitutionResultBit);

        // step6. left와 permutationResultBit 의 xor연산
        String changeRightBit = DES.xorBinaryStrings(left, permutationResultBit);

        // final
        String roundResult = right.concat(changeRightBit);
        System.out.println(&quot; ----- 0번째 round result -------&gt;  &quot; + roundResult);

        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(OUT_FILEPATH));
            writer.write(roundResult);
            writer.close();
            System.out.println(&quot;데이터가 &quot; + OUT_FILEPATH + &quot; 파일에 성공적으로 쓰였습니다.&quot;);
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println(&quot;파일에 쓰기 실패: &quot; + e.getMessage());
        }


    }
}
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring-ELK 1] Spring 3.0.X 환경에서의 Docker를 활용한 Elastic Search , Kibana Setting]]></title>
            <link>https://velog.io/@sujin-create/Spring-ELK-1-Spring-3.0.X-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Elastic-Search-Kibana-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@sujin-create/Spring-ELK-1-Spring-3.0.X-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-Docker%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Elastic-Search-Kibana-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Sat, 19 Aug 2023 04:17:14 GMT</pubDate>
            <description><![CDATA[<p>한동안 블로그 작성을 안 했는데....못한거였던걸로~ 오늘은 spring에서 elastic search, kibana를 세팅하는 방법에 대해서 적어보도록하겠다!!</p>
<p>서비스에서 검색엔진을 적용시켜야했고, 따라서 Solr, Elastic Search를 비교한다음 Elastic Search를 사용하기로 했다!</p>
<h3 id="0-intro-먼저-elastic-search와-같은-검색엔진을-왜-사용하는지-알아보자">0. Intro 먼저! Elastic Search와 같은 검색엔진을 &quot;왜&quot; 사용하는지 알아보자!</h3>
<p>이부분은 검색엔진 원리를 알아보며 이해했는데 다른 포스팅을 통해서 자세히 적어서 다시 공유하도록 하겠다! 결론만 말하자면 아래와 같다.</p>
<ul>
<li>빠르다.</li>
<li>정확하다.</li>
<li>전문 검색에 효율적이다.</li>
<li>여러 조건을 걸었을 때에도 빠르다.</li>
</ul>
<p>한마디로 QueryDSL을 사용할 때보다 검색 성능 향상이 된다는 것이다.</p>
<pre><code>🧸 전문검색이란?

전문이란?Full Text Search를 의미하는데 Full Text란 블로그 글이나 뉴스 논문 등 긴 글부터
블로그 제목 논문 제목 등과 같은 짧은 글과 같은 글의 전체 내용을 의미한다.

- 전문검색 : 여러 문서에 전문이 존재할 때, 사용자가 검색 문장을 입력했을 경우 유사도가 높은 전문을 제공해주는 검색이다.

이때, Elastic Search는 빠르고 정확한 답변을 주기 위해서 모든 전문을 &quot;용어&quot;단위로 분리하고 
이를 &quot;역인덱싱&quot;하여 전문에서 나온 여러 용어들을 index로 만들고 전문을 value로 한다. 
사용자가 입력한 검색문장의 용어들이 있을 때 역인덱싱한 용어들과 비교하여 
해당하는 용어의 index를 가지는 value인 전문을 찾아내는 기법을 사용한다.
</code></pre><p>이번에는 세팅방법을 설명할 것이기에 자세한 내용은 추가 포스팅을 통해 알아보도록하자!</p>
<h3 id="1-spring에서-elasitc-search를-사용하는-방법">1. spring에서 Elasitc Search를 사용하는 방법</h3>
<p>Elastic Search는 Json을 형식으로 하고 HTTP 통신을 제공하기에 직접 Elastic Search Query 문법에 맞춰서 명령어를 입력하고 이를 HTTP 통신으로 주고 받아도 된다.
하지만 나는 빠른 도입과 유지보수를 고려하여 이미 잘 만들어져있는 Elastic Search Engine의 Sprign Client인 <code>spring-data-elasticsearch</code>를 사용했다.</p>
<h3 id="1-1-spring-data-elasticsearch">1-1. spring-data-elasticsearch</h3>
<p>결론) spring 3.0.6에서는 elastic-cleint 8.5.3이 downlaod되기에 elastic search, kibana version은 8.5.3이 되어야한다.</p>
<ul>
<li>gradle</li>
</ul>
<pre><code class="language-kotlin">implementation(&quot;org.springframework.boot:spring-boot-starter-data-elasticsearch&quot;)</code></pre>
<p>gradle에서 다음과 같이 implementation을 구성하고 의존성을 설정해주면 사용할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/58ec81b3-1ada-4903-9537-f35e420f6430/image.png" alt="version"></p>
<p>다음과 같이 library를 보면 8.5.3 version client가 설치된 것을 확인하면 되고 이 버전에 맞는 elasticsearch, kibana를 설치해줘야한다!</p>
<p>이부분을 먼저 체크하지 않아서 꽤나 헤맸는데 여러분들은 그러지 않기를 바랍니다,,,ㅜㅜ</p>
<p>그렇다면 이제는 client를 사용할 준비가 되었으니 elastic search를 다운받아서 사용하거나 docker container image를 사용해서 띄워줘야합니다. </p>
<h3 id="1-2-docker-image를-사용한-elastic-search-환경-구성하기">1-2. docker image를 사용한 elastic search 환경 구성하기</h3>
<pre><code>version: &#39;3.7&#39;

services:

    es:
      image: docker.elastic.co/elasticsearch/elasticsearch:8.5.3
      container_name: es
      environment:
        - node.name=es-node
        - cluster.name=search-cluster
        - discovery.type=single-node
        - xpack.security.enabled=false
        - xpack.security.http.ssl.enabled=false
        - xpack.security.transport.ssl.enabled=false
      ports:
        - 9200:9200 # https
        - 9300:9300 #tcp
      networks:
        - es-bridge


    kibana:
      image: docker.elastic.co/kibana/kibana:8.5.3
      container_name: kibana
      environment:
        SERVER_NAME: kibana
        ELASTICSEARCH_HOSTS: http://es:9200
      ports:
        - 5601:5601
      # Elasticsearch Start Dependency
      depends_on:
        - es
      networks:
        - es-bridge


networks:
  es-bridge:
    driver: bridge
</code></pre><p>docker compose를 사용해서 kibana와 elasticsearch를 모두 함께 관리해줍니다.
여기서 kibana란?</p>
<pre><code> 🧸 Kibana란?


 elastic search가 검색엔진이여서 검색 request, response를 처리해준다면 
 kibana는 log를 모니터링하거나 api 체크를 위해 사용하는 postman처럼 query를 날려볼 수 있는 UI
 를 제공하는 elastic stack입니다.

</code></pre><p>그렇다면 docker-compose.yml file에 대해서 알아보자!</p>
<ul>
<li><p>image : docker에서 제공하는 elastic stack의 image를 넣어주면 되는데 <code>:</code> 뒤에 version을 적어주고 우리가 사용하는 elastic search client와 맞춰준다.</p>
</li>
<li><p>container_name : 말그대로 image를 띄우면 container가 생성되는데 이때 container의 이름을 의미한다.</p>
</li>
<li><p>environment : environment는 환경변수를 전달해줘야할때 사용한다.</p>
<ol>
<li>elastic search :  local에서 돌릴 것이기에 security는 false로 설정해준다.
security설정을 따로 하지 않는이상 해당 환경변수 설정이 없다면 돌아가지 않는다.
그리고 돌리다보면 memory exception이 발생할 수 있는데 <code>- ES_JAVA_OPTS=-Xms512m -Xmx512m #ERROR: Elasticsearch exited unexpectedly 대안 -&gt; memory size up</code> 해당 줄을 environment에 추가하면 메모리를 늘릴 수 있다.</li>
<li>kibana : kibana는 어떤 elastic search와 연결할것인지 host를 정해줘야한다. 
이때 우리는 container가 서로 연결이 되어있기에 es container를 사용한다. 따라서 <code>http://es:9200</code>으로 해줘야한다. 만약에 elastic search의 image name을 es로 설정하지 않았고 elasticsearch라고 했다면? <code>http:elasticsearch:9200</code>이 될 것이다.</li>
</ol>
</li>
<li><p>ports</p>
<ol>
<li>elastic search 
docker container에서 허용할 port number를 적어줘야하는데  elasitc search는 node끼리의 통신을 하는데 이때 tcp(9300 port)를 사용하고 client와 node의 통신을 위해 http(9200 port)를 사용하게 된다.</li>
<li>kibana
kibana는 5601 port를 사용하게 된다.</li>
</ol>
</li>
</ul>
<ul>
<li>network
container들 사이의 network를 연결해줘야한다. 이때 bridge를 <code>es-bridge</code>라는 이름을 가지도록 만들어주고 두개의 container에 각각 등록하면 된다. 이때 kibana는 elastic search가 먼저 띄워져야 작동하게끔 의존관계를 명시해야해서  <code>depends_on:es</code>을 사용한다.</li>
</ul>
<h3 id="1-3-실행">1-3. 실행</h3>
<p><code>docker compose up -d</code></p>
<p>log를 보기 싫다면 background에서 돌리도록 deamon을 활용하도록 <code>-d</code>를 사용한다.</p>
<p><code>docker ps</code>를 입력하면 실행중인 container를 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/dd1475ea-6bc4-483f-9a1f-1787a8cca29f/image.png" alt=""></p>
<p>지금 logstash도 같이 보이는데 logstash는 data pipeline을 위해서 사용했는데 이거는 Spring-ELK2에서 함께 살펴보도록하겠다<del>! 따로 살펴보는 이유는 할얘기가 많다...시행착오를 조금 생각보다 많이했다...</del>!!</p>
<p>그리고, 여기서 아래와 같이 화면이 나온다면 성공이다!</p>
<ul>
<li><p>kibana
<img src="https://velog.velcdn.com/images/sujin-create/post/211a85bb-cc80-4f3a-b6cd-e0c241c65a20/image.png" alt=""></p>
</li>
<li><p>logstash</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/53d7aef2-af84-4efb-b9c9-5f03e1d55ffa/image.png" alt=""></p>
<h3 id="2-spring에서-검색-api-만들기">2. spring에서 검색 api 만들기</h3>
<p>api를 생성하려면? 기존에 Entity를 생성 -&gt; Repository 생성 -&gt; Service를 생성 -&gt; Fetcher를 등록한다.</p>
<p>Fetcher단은? graphql을 사용하기 때문에 controller가 아닌 fetcher로 적었을뿐, controller단이라고 생각하면 된다.</p>
<p>자, 이 과정이랑 똑같다! elasitc search client의 <code>ElasticsearchRepository</code>를 사용해서 repository를 queryDSL을 사용하듯이 쉽게 사용할 수 있다.</p>
<p>그런데 우리는 지금 검색엔진을 만든다. 따라서 검색에 사용할 api라는 것인데 findById와 같은 query보다는 동적쿼리에 집중해서 알아볼 것이다.</p>
<p>여기서는 Criteria를 사용할 것이다.</p>
<p>우선 그전에, configuration을 등록해주자!</p>
<h3 id="2-0-config">2-0. config</h3>
<pre><code>@Configuration
@EnableElasticsearchRepositories
@EnableConfigurationProperties(ELKProperties::class)
class ElasticSearchConfig(private val elkProperties: ELKProperties ) : ElasticsearchConfiguration() {


    override fun clientConfiguration(): ClientConfiguration {
        return ClientConfiguration.builder()
            .connectedTo(elkProperties.host)
            .build()
    }
</code></pre><p>다음과 같이 구성하면 된다.
properties는 정의해서 사용하면 되는데 나는 아래와 같이 구성했다.</p>
<pre><code>@ConfigurationProperties(prefix = &quot;elastic&quot;)
data class ELKProperties(
    val host: String?= null,
    val username: String?= null,
    val password: String?= null
)
</code></pre><p>username, password는 dev, prod환경을 위해서 넣어놨다! </p>
<h3 id="2-1-criteria를-사용한-전문-검색-준비">2-1. criteria를 사용한 전문 검색 준비</h3>
<p>지금부터 Item이라는 domain이 존재할때 Item 검색 api service를 만든다고 해보자!</p>
<p>지금 구현하려고 했던 것은, 검색어를 입력했을 때 완벽히 똑같은 구문을 포함할 때만 검색되는 것이 아닌 관련이 있다면 모두 검색하려는 것이다. 그때 Elasticsearch에서는 <code>match_phrase</code>를 사용하는데 spring-data-elastic client에서는 <code>matches</code>를 사용하면 된다.</p>
<ul>
<li>matches</li>
</ul>
<p>전문 검색을 위해서 Elastic search query를 공부해보면 match가 존재한다. 이것은 완벽하게 똑같지 않아도 된다.우리가 <code>핑크색 나이키 신발</code> 을 검색했다고 하자.</p>
<p>여러 아이템들이 존재할 수 있는데 이때 핑크색, 나이키, 신발 해당하는 용어를 하나라도 가지고 있는 아이템이 존재한다면 해당 리스트들을 보여줄 수 있도록 할 것이다.</p>
<ul>
<li>operations</li>
</ul>
<pre><code>private val operations: ElasticsearchOperations
</code></pre><p>Criteria를 만들 때 의존관계를 주입해줘야한다. 
ElaistcSearchOperation을 사용해서 우리는 elastic query를 사용할 수 있게 된다.</p>
<ul>
<li>Document</li>
</ul>
<p>criteria는 사실 spring data jpa + queryDSL에 익숙해져서 까먹었었다!ㅎㅎㅎ
그래서 다시 문법을 살펴보고 적용했다!</p>
<p>원래 같으면 builder가 필수이지만, elastic criteria는 문법이 약간 다르다!</p>
<p>criteria는 repository이기에 그 전에는 entity를 작성하듯이 ElasticSearch에서는 Document 를 정의해줘야한다. 아래와 같이 작성하면 된다.</p>
<pre><code>@Document(indexName = &quot;item&quot; )
@Setting(settingPath = &quot;elastic/item-setting.json&quot;)
@Mapping(mappingPath = &quot;elastic/item-mapping.json&quot;)
class ItemDocument(

    @Id
    val id: UUID,

    var price: String,

    var name: String,

    var memo: String? = null

)
</code></pre><p>다른 feild들이 존재할 수 있지만 일단 우선으로 예제를 보여주기 위해서 다음과 같이 간단하게 작성해봤다!</p>
<ol>
<li>Mapping
이때, elasticsearch에서는 mapping을 사용하여 index(DB에서의 table개념)의 Field(RDB에서의 Column개념)의 type과 매핑을 해줘야한다.
직접 <code>@Field</code> annotation을 사용해서 매핑해도 되지만, 만약에 많아진다고 해보자! 그럼 annotation을 필드마다 적어주면 된다. 그러나 보기싫다. ㅎㅎㅎㅎㅎㅎㅎ
따라서 관리하기 편하게 <code>@Mapping</code>을 사용해서 mapping annotation을 사용한다.</li>
</ol>
<pre><code>{
  &quot;properties&quot; : {
    &quot;id&quot; : {&quot;type&quot; :  &quot;keyword&quot;}, 
    &quot;price&quot; : {&quot;type&quot; : &quot;integer&quot;},
    &quot;name&quot;: {&quot;type&quot;:  &quot;text&quot;,  &quot;analyzer&quot;:  &quot;korean&quot;},
    &quot;memo&quot;: {&quot;type&quot;:  &quot;text&quot;}
  }
}</code></pre><p>keyword, text의 차이점은 text는 전문검색용이고 keyword는 해당 키워드를 완벽히 똑같이 포함해야 검색한다. ID값은 unique한 값이기에 keyword로 하고 title, content는 text여야 검색한 구문이 완벽히 똑같지 않아도 찾아낼 수 있는 것이다.</p>
<p>analyzer는 setting시에 등록해줘야한다. setting하는 방법은 mapping처럼 <code>@Setting</code>을 사용하면 된다.</p>
<ol start="2">
<li>Setting</li>
</ol>
<pre><code>{
  &quot;analysis&quot;: {
    &quot;tokenizer&quot;: {
      &quot;whitespace_tokenizer&quot;: {
        &quot;type&quot;: &quot;whitespace&quot;
      },
      &quot;nori_none&quot;: {
        &quot;type&quot;: &quot;nori_tokenizer&quot;
      },
      &quot;nori_discard&quot;: {
        &quot;type&quot;: &quot;nori_tokenizer&quot;
      },
      &quot;nori_mixed&quot;: {
        &quot;type&quot;: &quot;nori_tokenizer&quot;
      }
    },
    &quot;analyzer&quot;: {
      &quot;english&quot;: {
        &quot;type&quot;: &quot;whitespace&quot;
      },
      &quot;korean&quot;: {
        &quot;type&quot;: &quot;nori&quot;
      }
    }
  }
}</code></pre><p>나는 영어에서는 whitespace를 사용할 것이고 한국어 tokenizer는 nori를 사용할 것이다.
analyzer를 사용하려면 tokenizer가 등록되어야만 가능하다!
token화를 할 때 한 token으로 analyzer를 사용해야하기 때문이다.</p>
<ol start="3">
<li>구조</li>
</ol>
<p>구조는 아래의 사진과 같아야한다. 
resources 폴더 안에 elastic 폴더를 만들고 그 안에 <code>item-mapping.json</code>과 <code>item-setting.json</code>을 넣어놨다!</p>
<p>물론 디테일한 경로는 바꿔도 좋은데,resources에 넣어놔야 경로를 찾을 수 있으니 주의하자!</p>
<ol start="4">
<li>nori-tokenizer download</li>
</ol>
<p>nori tokenizer는 설치해야한다.
<code>docker compose up -d</code> 시에 elastic search container를 띄우는데 container에 접속해서 다운하면 된다.</p>
<p><a href="https://esbook.kimjmin.net/06-text-analysis/6.7-stemming/6.7.2-nori">https://esbook.kimjmin.net/06-text-analysis/6.7-stemming/6.7.2-nori</a></p>
<p>여기서 설치 방법을 잘 알려준다.</p>
<pre><code>docker exec -it pid /bin/bash</code></pre><p>을 통해서 접속한다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/f235dfbb-d069-4690-9d2b-f6e077c43363/image.png" alt=""></p>
<p>중간에 의식의 흐름대로 쓴 코드가 있는데 눈감아주면 좋겠다,,,ㅎㅎ 
결국 <code>elasticearch-plugin install analysis-nori</code> 명령어를 사용한다.</p>
<p>그리고 docker restart es 를 통해서 컨테이너를 재시작해주자!</p>
<h3 id="2-2-criteria-작성">2-2. Criteria 작성</h3>
<pre><code>@Repository
class ItemDocumentCriteria(
    private val operations: ElasticsearchOperations
) {

    fun findItemsBySearch(search: ItemSearch): MutableList&lt;ItemDocument&gt;? {


        val query: CriteriaQuery = createCriteriaQueryByCondition(search)

        val searchHits: SearchHits&lt;ItemDocument?&gt; = operations.search(query, ItemDocument::class.java)

        return searchHits.stream()
            .map{ it: SearchHit&lt;ItemDocument?&gt; -&gt; it.content }
            .collect(Collectors.toList())

    }

    private fun createCriteriaQueryByCondition(search: ItemSearch): CriteriaQuery{

        if (search.nameCondition == null){
            return CriteriaQuery(Criteria())
        }

       return CriteriaQuery(Criteria(&quot;name&quot;).matches(search.nameCondition!!))

    }


}</code></pre><p>name을 기반으로 했을 때 사용자가 입력한 값을 ItemSearch kolin의 dataClass에 format을 넣어두고 이를 사용했다.</p>
<p>현재는 Name만 사용해봤는데 dataClass에 값을 더 넣으면 여러 조건들을 기반으로 검색을 할 수 있을 것이다.</p>
<pre><code>data class ItemSearch(
    val nameCondition: String?= null
)
</code></pre><p>다음과 같이 작성했는데 사용자가 입력한 검색구문을 ItemSearch에 넣어준다.</p>
<p>이로써 사용할 repository는 작성완료했다!</p>
<p>이를 활용해서 나머지 Service, Fetcher를 작성하면 된다!</p>
<h3 id="의문점">의문점</h3>
<p>여기서 궁금한점이 생길 것이다. Item이 등록될 때마다 application에서 사용하는 DB에 넣어주듯이 Elastic Search에도 넣어줘야 query가 가능하다. 그러면 Item을 등록하거나 수정할 때 Elasitc Search와 DB에 둘다 넣어주려면 put Query와 update Mutation을 entity 그리고 document에 대해서 각각 api를 1번씩 총 2번 호출해야하는가?</p>
<p>자 이때 Logstash를 사용한다.
Logstash를 사용하면 Spring Application의 DB에 데이터가 들어갔을 때 특정 주기마다 읽어와서 Elastic Search의 DB에 저장하도록 Pipeline을 작성할 수 있게 된다.</p>
<h3 id="마무리">마무리</h3>
<p>spring에서 elasitc search 그리고 kibana를 사용하는 세팅하고 사용하는 방법에 대해서 알아봤다.</p>
<p>Elastic Search를 이해하고 왜 사용하는지 알려면 그 원리를 한번 제대로 이해해보면 쉽게 사용하게 될 것이라고 생각했다. 그래서 나도 그렇게 학습했는데 개념을 알고나니 사용하기 쉬워졌다. 따라서 다음 포스팅은 Elasitc Search 자체에 대해서 적어보려고한다. 그리고 Logstash를 연동하여 data pipeline을 작성하는 것을 해보자!</p>
<p><del>logstash와 동작 구현을 보여드리는 것은, 직접 project를 만들어서 예제를 보여드리기 위해서,,조금 시간이 걸릴 것 같습니당..</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] (2탄 : redis cache 적용)  원점으로 돌아가서 api를 갈아 엎어보자!]]></title>
            <link>https://velog.io/@sujin-create/spring-%EC%9B%90%EC%A0%90%EC%9C%BC%EB%A1%9C-%EB%8F%8C%EC%95%84%EA%B0%80%EC%84%9C-api%EB%A5%BC-%EA%B0%88%EC%95%84-%EC%97%8E%EC%96%B4%EB%B3%B4%EC%9E%902%ED%83%84</link>
            <guid>https://velog.io/@sujin-create/spring-%EC%9B%90%EC%A0%90%EC%9C%BC%EB%A1%9C-%EB%8F%8C%EC%95%84%EA%B0%80%EC%84%9C-api%EB%A5%BC-%EA%B0%88%EC%95%84-%EC%97%8E%EC%96%B4%EB%B3%B4%EC%9E%902%ED%83%84</guid>
            <pubDate>Sun, 23 Jul 2023 11:25:40 GMT</pubDate>
            <description><![CDATA[<h3 id="intro-이전-포스팅에-이어서">intro. 이전 포스팅에 이어서..</h3>
<p>이전 포스팅에서 api하나를 분리하고 2개의 api로 만든 것을 확인할 수 있었다.
그리고 마지막으로 cache 적용기를 메인 주제로 하여 2탄을 가져왔다.</p>
<p>cache를 도입하려고 하는 이유가 무엇일까?</p>
<p>6시에 한번 Update하고 그 이후로는 계속 동일한 정보에 대해서 굳이 database에서 계속 조회할 필요가 없다. cpu는 memory보다 cache를 더 빨리 읽기 때문이다. 그리고 home page에 접근할 때마다 계속 필요한 데이터이다. 하루에 사용자 1명당 발생할 수 있는 트래픽이 가장 많을 페이지이다. 그렇다는 것은 더더욱 데이터베이스에 가면 안 된다.</p>
<ul>
<li>그래서?
그래서 나는 cache를 도입해서 성능을 upgrade시켜보기로했다.
물론,,,,엄청나게 많은 데이터가 아니여서 드라마틱한 차이가 없을 것은 예상했다.(한번의 request에 대해서 말이다!)
그런데,,,동시에 많은 양의 요청이 있다면 main page이기 때문에 사용자가 답답함을 느낄 수 있다고 생각했다. </li>
</ul>
<h2 id="1-redis">1. redis</h2>
<h3 id="redis-image">redis image</h3>
<p>spring에서 사용이 가능한 cache에는 여러가지가 존재한다.
redis는 다양한 형태로 cache를 사용해 데이터를 저장할 수 있다는 장점이 있다.
그리고 가용성이 뛰어난 인 메모리 캐시 구현에 매우 적합하다고 한다.
그래서 redis를 사용해보기로했다.</p>
<p>docker image로 불러오자!</p>
<pre><code>version: &quot;3.8&quot;

networks:
  application:
    driver: bridge

services:
  redis:
    image: redis:latest
    container_name: redis
    ports:
      - 6379:6379
    volumes:
      - ./redis/data:/data
      - ./redis/conf/redis.conf:/usr/local/conf/redis.conf
    labels:
      - &quot;name=redis&quot;
      - &quot;mode=standalone&quot;
    restart: always
    command: redis-server /usr/local/conf/redis.conf</code></pre><p>바로 terminal에서 실행해도 되지만, application 실행할때마다 하면 귀찮다!! docker compose file로 관리하기로 했다.</p>
<pre><code>docker compose up</code></pre><p>해당 명령어로 간단하게 container를 띄울 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/6904f789-baf8-4134-be42-7615e4483613/image.png" alt=""></p>
<h3 id="dependency">dependency</h3>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre><p>의존성을 등록해준다.</p>
<h3 id="config">config</h3>
<p>config에 @EnablaeCaching을 사용해서 spring이 redis를 사용할 수 있도록해준다.</p>
<pre><code>@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory cf){
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .entryTtl(Duration.ofHours(24L));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf).cacheDefaults(redisCacheConfiguration).build();
    }
}
</code></pre><p>cache삭제를 24시간 주기로 해줬다. 외부 api를 24시간에 한번씩 call하고 저장할 것이기 때문이다.</p>
<h2 id="2-api에-적용하기">2. api에 적용하기</h2>
<h3 id="service">service</h3>
<h3 id="cookie-사용하기">cookie 사용하기</h3>
<pre><code>@Cacheable(value = &quot;weather&quot;, key=&quot;#informType&quot;)
public ApiInfoDto getApiDataByInformType(@PathVariable InformType informType, ApiInfoDto apiInfoDto) throws IOException, ParseException {
        log.info(&quot;no api info for {}&quot;, informType);
        switch (informType){
            case PARTICULATE:
                HashMap&lt;String, String&gt; particulate = updateApiDataParti();
                return apiInfoDto.updateParticulate(particulate.get(&quot;informCause&quot;), particulate.get(&quot;informOverall&quot;) );
            case WEATHER:
                HashMap&lt;String, String&gt; weather = updateApiDataWeather();
                return apiInfoDto.updateWeather(weather.get(&quot;informSky&quot;),weather.get(&quot;informPty&quot;) );
        }

        return apiInfoDto;
}

@CacheEvict(value=&quot;weather&quot;, allEntries = true)
public void deleteApiDataAll(){

}</code></pre><ul>
<li>Cacheable
weather라는 cache에서 informType값에 따른 (key) value를 초기화해주고 값이 반환된다.</li>
</ul>
<p>key, value쌍으로 구성된 weather라는 이름을 가지는 cache에 key(informType)에 대한 value값이 존재한다면 getApiDataByInformType 안의 method를 실행시키지 않고 바로 cache의 key안의 value를 반환해준다.</p>
<ul>
<li>cacheEvict</li>
</ul>
<p>allEntries를 true로 하여 cache를 삭제한다. key value를 모두 삭제해줄 것이다. </p>
<h3 id="api-call-service-그리고-scheduler">api call service 그리고 scheduler</h3>
<pre><code>@Transactional
public HashMap&lt;String, String&gt; updateApiDataParti() throws IOException, ParseException {

        HashMap&lt;String,String&gt; particulatePredictInfo = particulateMatter.extractParticulatePredictInfo();
        return particulatePredictInfo;
    }

@Transactional
public HashMap&lt;String, String&gt; updateApiDataWeather() throws IOException, ParseException {

        List&lt;City&gt; cityList = cityRepository.findAll();
        HashMap&lt;String, String&gt; weatherDataForAllCity = getWeatherDataForAllCity(weather, &quot;&quot;, &quot;&quot;, cityList);
        return weatherDataForAllCity;
    }</code></pre><p>특정 시간이 되면(6시로 설정) 외부 api값을 불러와야하는데 2개의 method를 사용해야한다.
미세먼지, 날씨 데이터를 각각 가져온다.</p>
<pre><code>@Scheduled(cron = &quot;0 0 6 * * *&quot;)
public void updateApiData() throws IOException, ParseException {

        log.info(&quot;update data Scheduled&quot;);

        //이전의 cache를 모두 삭제
        deleteApiDataAll();
        ApiInfoDto partiApiInfoDto = getApiDataByInformType(InformType.PARTICULATE, new ApiInfoDto());
        getApiDataByInformType(InformType.WEATHER, partiApiInfoDto);
    }</code></pre><p>cache에 6시가 됐을 때 api에서 data를 불러오고 초기화해주는 과정이전에, 기존에 존재하는 cache에서 저장중이였던 데이터들을 모두 삭제해준다.</p>
<p>이렇게 완성했다면 이제 controller에서 적용하면 된다!</p>
<h3 id="controller">controller</h3>
<pre><code>@GetMapping(&quot;/weather&quot;)
public ApiInfoDto getWeather() throws IOException, ParseException {
        //cache를 조회 -&gt; 존재하지 않을 때 아래를 실행. -&gt; cache를 생성
        log.info(&quot;getWeather controller&quot;);
        ApiInfoDto partiApiInfoDto = apiMapService.getApiDataByInformType(InformType.PARTICULATE, new ApiInfoDto());
        ApiInfoDto apiInfoDto = apiMapService.getApiDataByInformType(InformType.WEATHER, partiApiInfoDto);
        return apiInfoDto;
    }</code></pre><p>기존의 api에서 weather라는 api를 따로 빼서 하나의 역할만 가지도록 했다(이전 포스팅 참고).</p>
<h2 id="3-결과">3. 결과</h2>
<ul>
<li>database에 저장하지 않음</li>
</ul>
<p>아예 api data에 대해서는 database를 사용하지 않도록 변경했다. 따라서 기존에는 memory를 차지하고 있었던 ApiData entity에 대한 table이 없어졌고 관련된 코드 역시 모두 불필요해졌다.</p>
<ul>
<li>기존의 api에서 weather api관련 service만 적용한 api와 cache를 적용한 weather api service에 대한 api를 비교해보자</li>
</ul>
<p>100명이 100번 요청 -&gt; 3회 반복</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/d830d52f-d719-42b3-9e4d-c5875a4b0879/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/ac476653-802c-459f-b948-88146c104a41/image.png" alt=""></p>
<p>TPS 즉, Throughput의 경우 약 <strong>3배 정도 차이가 나는 것을 확인할 수 있었다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] 원점으로 돌아가서 api를 갈아 엎어보자!]]></title>
            <link>https://velog.io/@sujin-create/spring-%EC%99%9C-%EB%8A%90%EB%A6%B0%EA%B2%83%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@sujin-create/spring-%EC%99%9C-%EB%8A%90%EB%A6%B0%EA%B2%83%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sun, 16 Jul 2023 12:39:12 GMT</pubDate>
            <description><![CDATA[<h3 id="intro">intro.</h3>
<p>이전에 spring project를 했었는데 지금와서 생각해보니, api를 무지성으로 작성한것이 생각났다.</p>
<p>우선 메인 화면에 날씨 api를 통해서 스케줄링해서 매일 6시에 update되는 정보를 보면준다. 이때 미세먼지 공공 api 그리고 기상청의 날씨 api를 사용했다.
그리고 그 아래에 svg vector기반 지도를 보여주며 시/도를 기준으로 구역이 나뉘어져있고 그 색을 변경하려고한다.
색의 변경의 경우에는 지도에 className을 기준으로 frontend에서 가능하기 때문에 className은 애초에 svg vector가 id(시/도를 기준으로 다르겠지요!)로 나뉘어져있는데 이를 활용할 예정이고 backend에서도 이 svg vector의 id값을 기준으로 추천 grade를 제공한다.</p>
<p>이전에 만들어놓은 api는 위의 서비스를 고려해서 생성했고 아래와 같다.</p>
<pre><code>📗 citymap endpoint를 가지는 get api</code></pre><p>description : main map에서 사용할 api로, citymap에 대해서 지도생성을 위해 svg vector와 citycode를 우리의 사진스팟 추천 등급과 함께 보내주며 날씨 데이터를 불러올 수 있도록한다.</p>
<ul>
<li>svg vector란?
말그대로 이미지를 vector화 시켜놨다고 보면 된다.</li>
</ul>
<hr>
<h3 id="citymap-endpoint를-가지는-api의-문제점">citymap endpoint를 가지는 api의 문제점</h3>
<p>특징 : </p>
<ol>
<li><p>매일 6시에 기상청 api를 사용해서 날씨 데이터를 불러오고 이를 통해 등급을 매겨서 사용자에게 사진장소를 추천한다.</p>
</li>
<li><p>날씨 정보도 사진찍으러 갈 때 참고할 수 있도록 함께 제공한다.</p>
</li>
</ol>
<p> <img src="https://velog.velcdn.com/images/sujin-create/post/62f0f041-76fb-4096-bebc-b6aa57b64368/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/f6dde3ca-1127-4bef-8cb7-ab5bc12c5c29/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/a9bf98f2-d30a-4e5a-872d-610e1ef0bdeb/image.png" alt=""></p>
<p>지금 문제점)</p>
<ul>
<li><p>citymap을 불러올 때 하나의 api로 여러데이터를 관리한다.</p>
<ul>
<li>api로 여러데이터를 관리하기 때문에 문제는 6시에만 update되는 날씨 관련 데이터를 계속 db까지 가서 불러온다는 것.</li>
<li>→ 6시(30초정도뒤에) cache에 update를 진행하고 거기서 쓰고 읽으면 되지 않을까?라는 생각을 함!</li>
</ul>
</li>
<li><p>하나의 api는 하나의 기능을 가지도록해야하는데 2개의 service를 하나의 api를 통해서 보여준다.</p>
<p>  지금은 크게 기능을 2가지를 가지고 있는데 1개의 api로 처리한다</p>
<p>  → 분리해서 2개의 api를 만들고 하나는 날씨 데이터에 대한 api, 그리고 다른 하나는 svg에 대한 api로한다.</p>
</li>
<li><p>call할 때마다 6시에 한번 변경되는 데이터가 (심지어 svg vector는 행정구역이 바뀌지 않는이상 변경이 없음…) 계속 database에 접근해서 읽어온다.
  -&gt; backend traffic은 아무래도 DB를 얼마나 거치느냐에 따라서 달라질텐데 이건 뭐 너무 비효율적인 설계이다.!!!</p>
</li>
</ul>
<hr>
<h2 id="1-controller를-각-역할에-맞춰-2개로-분리">1. controller를 각 역할에 맞춰 2개로 분리</h2>
<h3 id="srp를-지키자">SRP를 지키자!</h3>
<p>single responsibility principle....!! 사실 이때도 이미 단일책임 원칙을 잘 지켜야 유지보수가 쉬우며 트래픽이 몰리지 않고 좋은 설계임을 알고있었다!그런데 구현에 너무 집중했던 것 같기도하고 사실 두개를 하나의 기능이라고 봤던게 더 컸다.
개발이랑 알고리즘이랑 다른게 ,,, 알고리즘 문제를 풀 때 얼마나 줄여서 재사용을 하지 않도록하며 등등을 고려했는데 개발은 줄이는 것보다 중요한게있다는 것을 요즘들어 깨닫고 있는데 지금보니 당장 이것부터 바꿔야겠다고 생각했다.</p>
<p>설계를 신경써야하는데, 여기서는 SRP를 제대로 지키지 않았다!</p>
<p>서비스는 분리를 해뒀었기에(당연한거겠지만 ,,ㅎㅎ) controller만 분리해주면 됐다!</p>
<pre><code>  private final CityServiceImpl cityService;
  private final ApiMapServiceImpl apiMapService;

    @GetMapping(&quot;/citymap&quot;)
    public HashMap&lt;String, List&gt; cityMap(){
        LocalDateTime startTime = LocalDateTime.now();

        List&lt;CityMapInfoDto&gt; cityMap = cityService.getCityMap();

        HashMap&lt;String , List&gt; cityMapResult = new HashMap&lt;&gt;();
        cityMapResult.put(&quot;data&quot;, cityMap);

        LocalDateTime endTime = LocalDateTime.now();
        log.info(&quot;cityMap api  duration : {} &quot;,Duration.between(startTime, endTime));

        return cityMapResult;
    }

    @GetMapping(&quot;/weather&quot;)
    public ApiInfoDto getWeather(){
        return apiMapService.getApiData();
    }</code></pre><p><img src="https://velog.velcdn.com/images/sujin-create/post/8ec06e2e-b028-48f9-9a1e-6fb05d85b191/image.png" alt=""></p>
<p>그래서 다음과 같이 두개의 endpoint로 분리했다!</p>
<hr>
<h2 id="2-city-map-api에-대한-고민">2. city map api에 대한 고민</h2>
<ul>
<li>다시 돌아가자~
다시 원점을 돌아가서, city map api가 필요한 이유에 대해서 먼저 생각해봤다!</li>
</ul>
<p>사진찍기 좋은 장소를 날씨와 미세먼지 등등을 고려해서 등급을 매기고 그것을 사용자가 한눈에 잘 보기 쉽게 지도에서 행정구역별 (시/도 기준) 색으로 구분해줄것이였다.</p>
<p>-&gt; 역할 : 시/도별 사진찍기 좋은 등급(grade값)을 제공하기 위해서 필요하다.</p>
<ul>
<li>api 구성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/26e738f5-de9b-4def-a650-685f071785b1/image.png" alt=""></p>
<ul>
<li>vector가 어디있어야 좋은 것일까?
원래는 vector가 react환경의 frontend에 있었다. 그러나, 너무 길기도하고 jsx에서 html부분에 너무 길게 들어가있어서 해당 부분의 코드를 개선한답시고 backend의 city entity의 vector column에 넣어서 관리하는 것으로 바꿨다.</li>
</ul>
<p>지금생각해보면 frontend에 두는것이 더 합리적이였던 것 같다.
왜냐하면, svg vector는 잘 바뀌지 않는다.(사용자에 의해서 바뀔 가능성이 없고 오로지 개발자에 의해서 변경되어야하는 값이다) static한 value인데 굳이 api가 call될 때마다(map page에 접근할 때마다 call) database에서 가벼운 데이터도 아니고 무거운 데이터를 가져와야했을까?</p>
<p>그래서 총 3가지를 생각해봤다.</p>
<ol>
<li>frontend에서 vector를 관리한다. city ID값에 따라서 vector를 읽어야하니까 json으로 저장해두면 되지 않을까 생각한다.</li>
<li>yaml에서 읽어가도록한다.
properties or yaml에서 관리해서 개발자가 행정구역 변화시에 변경하기도 쉬울 것 같고 database까지 가지 않는다. 다만, 이것은 결국 frontend에서 call되는 api에 vector를 담아서 전송하게된다.</li>
<li>cache를 사용한다.
vector값은 변경되지 않지만, 페이지 접근시 꼭 필요한 값이다. 그래서 cache에서 관리하는게 좋을까 싶었지만, 생각해보니 cache는 비싸다.
그런데 용량이 큰 데이터를 굳이 cache에 넣어서 관리해야할까?에 대한 의문이 들었다. 물론 cpu의 속도와 비슷하다. 그러나 cache는 비싸다.</li>
</ol>
<p>그래서 결론, 나는 1번 방법이 이 상황에서는 가장 적합하지 않을까 생각했다.</p>
<p>자! 그럼 frontend를 바꾸면 되겠다!
그것도 내가한다~ ㅎㅎㅎ</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/84219e33-4e2f-4910-9bd0-242f7e55b35e/image.png" alt=""></p>
<p>노가다를 한다!!!</p>
<pre><code>{
  &quot;city&quot;:{
    &quot;CD11&quot; : &quot; M 178 231 l -4 2 -4 3 -1 0 0 1 -3 1 -4 -5 -4 0 -6 1 -4 3 -2 -1 -1 -1 -3 2 -3 -3 -1 -3 -2 -3 0 0 -1 -2 -3 2 -3 1 -2 -4 0 -8 -3 -1 0 0 0 0 0 0 -2 -1 -3 -2 2 -4 3 -3 0 -1 0 0 0 0 0 -1 1 0 4 2 7 1 2 -2 0 1 2 0 1 -4 2 -8 7 -1 4 2 0 -3 1 -1 2 -4 3 -4 2 0 3 2 7 2 2 8 1 4 1 1 0 0 0 2 0 2 0 1 0 0 -1 6 3 0 8 -1 0 5 -4 3 -1 5 z &quot;,
    &quot;CD26&quot; : &quot; M 496 722 l 0 4 -1 0 -2 -5 -1 6 -1 -2 0 0 -1 0 -1 1 -1 2 0 1 -1 -1 -1 -6 -1 -6 -2 3 -2 -3 0 2 0 0 0 0 0 0 0 0 -1 0 1 0 -4 1 -2 -5 -1 5 -1 2 0 0 -2 0 -7 -2 0 -2 2 -2 -1 -3 -5 -4 -2 -2 4 -1 9 0 0 -5 1 -6 5 -1 6 -2 7 -1 4 -6 1 -4 2 0 3 1 1 -3 6 -3 4 -1 1 -5 3 -6 7 0 3 -2 0 -3 0 0 2 -3 1 3 0 0 1 0 2 0 4 -1 4 4 1 3 0 1 -1 1 5 1 -7 4 -1 6 -1 4 -1 0 0 0 0 -1 -1 2 -1 1 2 2 -1 4 -2 4 -3 6 -3 4 0 1 0 0 -5 0 -4 0 -2 -1 -1 4 -1 4 1 -1 0 4 -7 0 -2 -4 -1 1 -3 4 z &quot;,
    &quot;CD27&quot;: &quot; M 441 530 l 3 1 3 7 0 4 2 9 1 7 -4 4 -2 -1 0 4 -1 2 -3 6 -2 7 0 8 -4 2 -4 2 -3 1 -3 -5 -6 0 -5 3 -1 6 -3 7 -2 3 -5 0 -7 3 -7 1 0 -8 -4 -4 -1 -3 5 -2 6 0 0 -5 -5 -4 -1 -5 3 -5 3 -2 8 -1 0 -5 -8 -4 -6 0 0 -5 2 -5 3 -4 3 -3 5 -1 2 7 3 3 1 0 2 -4 3 -7 0 -7 2 2 1 1 3 -2 6 -4 3 -3 5 -1 z &quot;,
    &quot;CD28&quot;: &quot; M 91 229 l 1 -2 0 1 0 0 1 0 1 0 0 -1 1 0 -1 -1 0 0 0 0 2 0 -4 -5 0 -5 1 -3 0 0 1 0 -1 0 0 -1 0 0 1 -1 0 -1 4 -1 4 -1 -5 -1 -3 -1 -1 -1 -2 1 1 -3 5 -3 3 -4 4 1 3 3 5 4 4 3 5 2 -2 4 -1 3 -1 5 -2 2 2 5 2 3 -1 7 -1 2 -2 3 -1 -3 -1 6 -7 2 -1 6 -6 1 -1 0 -3 -5 -2 -4 -1 -5 5 -2 -4 -2 0 -3 -2 2 z M 82 219 l 3 1 0 0 1 2 2 3 -2 2 -4 1 -8 5 -9 7 -4 -2 -3 -3 -1 0 -2 1 -2 -4 4 -5 4 -2 7 -1 7 -3 1 -4 z M 64 156 l 3 2 4 4 7 4 0 4 2 3 -1 2 -2 4 2 3 -1 4 0 3 2 0 0 2 -1 1 1 1 -1 1 1 2 1 2 1 1 -1 0 -5 4 -8 1 -8 0 -5 -4 0 -4 4 -3 0 -5 -2 -4 1 0 0 -1 -5 -3 -1 -8 0 -7 4 -4 0 0 1 0 z &quot;,
    &quot;CD29&quot;: &quot; M 113 687 l 2 3 6 3 7 -3 4 -2 2 0 1 0 0 0 0 0 1 0 0 0 1 -1 4 0 4 3 2 3 -1 1 1 1 3 4 6 3 1 4 -1 7 -4 4 -3 4 -7 1 -7 0 -6 2 -1 1 -1 0 -1 1 -4 1 -8 -1 0 -4 -4 -5 -4 -1 -7 0 -4 -7 -1 -1 3 -3 2 -6 2 -4 2 0 1 1 4 -4 1 -4 0 0 0 0 z &quot;,
    &quot;CD30&quot;: &quot; M 223 432 l 1 6 4 1 0 0 3 0 4 -4 1 0 0 2 -1 2 3 2 2 6 5 1 0 5 -2 6 -2 3 -2 7 -1 8 1 7 -5 3 -2 4 -7 -1 -3 -4 -2 -1 1 -2 -1 -2 1 -1 0 0 -3 -2 -2 6 -1 6 -3 4 -1 1 -1 -4 -3 -4 -1 1 -1 0 -2 -3 -1 -7 -5 -4 1 -2 1 -3 2 -8 1 -7 0 -6 9 -3 2 -3 2 -3 2 -5 z &quot;,
    &quot;CD31&quot;: &quot; M 550 641 l 2 6 -1 7 -1 8 -3 3 0 0 -1 0 0 1 0 0 -2 1 -5 -1 1 -1 0 -1 -1 -3 -4 -4 -4 1 -2 0 -1 0 0 0 -1 -3 -1 -7 -4 -1 -6 -2 -4 -5 -2 -3 -4 -3 -5 -3 -1 -3 -3 1 -1 0 0 0 -2 0 -3 -2 -4 -4 5 -5 0 -8 3 -4 1 0 1 1 4 -3 3 0 -2 -3 0 -2 1 -1 0 -1 2 -4 4 -2 4 -1 9 -1 5 0 4 3 3 3 4 7 6 -1 3 -2 4 -2 7 2 1 0 7 3 2 8 0 7 -1 8 -2 4 2 -1 -2 5 -1 4 -1 -1 0 1 -2 1 -2 -6 -3 -6 -3 -1 2 2 -2 1 2 1 1 3 -3 1 2 0 0 0 1 1 0 4 z &quot;,
    &quot;CD36&quot;: &quot; M 184 410 l -2 -2 0 -5 1 -9 -1 -4 -1 1 0 0 -1 0 -1 -4 3 -4 3 0 5 2 5 3 6 3 2 0 0 0 0 0 0 0 2 1 2 2 -2 4 -1 4 2 4 0 3 0 3 2 1 3 3 6 2 3 4 3 2 0 8 -6 2 -2 5 -2 3 -2 3 -9 3 -6 -1 -6 -4 -1 -3 -3 -9 0 -7 2 -7 1 -4 0 0 0 0 -1 1 z &quot;,
    &quot;CD41&quot;: &quot; M 81 192 l 1 -2 -1 -1 -1 -7 -1 -3 1 -3 1 -4 -2 -3 0 -5 4 2 4 2 4 1 3 -1 4 -2 4 0 3 5 -1 -4 -1 -12 -1 -1 0 -1 0 0 0 0 0 0 0 0 3 -2 -1 -5 0 0 1 -3 0 -5 -1 1 -1 -4 3 -6 7 -2 7 -2 3 -2 5 -2 1 -4 1 -1 1 -1 3 -3 1 -6 1 -7 4 -4 3 -2 3 2 1 0 3 -5 1 -3 0 0 1 -2 2 -3 3 -7 8 2 3 -5 1 0 1 0 3 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 3 1 3 -1 1 0 1 3 4 9 1 5 6 1 4 -2 3 -3 4 0 -1 1 -1 5 0 6 4 2 3 3 2 -1 5 -3 5 0 6 2 1 5 1 8 2 6 6 2 6 1 2 5 6 3 4 3 1 3 -1 1 1 2 0 1 -1 4 -4 3 -5 4 -4 6 0 5 -1 2 1 2 1 6 -3 3 -1 4 7 1 -1 7 0 8 8 0 5 1 4 3 5 3 3 4 4 0 4 0 1 -1 3 2 7 3 4 3 1 2 -5 2 -4 2 1 1 -1 0 -3 4 2 5 2 3 0 6 -1 3 -1 5 -4 7 0 8 -1 7 0 6 -1 4 -1 6 -4 6 -2 5 -4 3 -8 0 0 7 -3 4 -1 1 -1 -1 -1 1 0 -1 0 3 -5 3 0 2 0 0 -2 1 -4 -2 -2 0 -3 1 -2 -1 -3 0 -2 5 -3 4 -5 3 -4 3 1 1 1 1 -5 4 -4 2 -4 2 -4 1 1 3 0 1 0 0 0 0 -2 3 -4 -4 -6 -3 -3 -3 -5 -3 -5 -2 -8 2 -1 3 -1 0 -2 0 -4 1 -7 1 -8 2 -6 2 -3 4 0 -4 -8 1 -8 -1 0 -7 3 -8 -9 -1 2 -7 -6 -3 -1 -5 -10 -11 0 -5 0 -4 -2 1 -1 -1 -1 1 -1 0 0 0 0 0 0 0 1 -4 2 -3 -3 0 -1 -4 -3 -5 -2 0 -2 1 -2 -1 2 3 -7 1 0 -7 0 -7 3 -2 -1 -1 1 0 6 -4 2 0 -3 1 -4 4 -2 3 2 1 4 3 3 3 -1 1 2 1 3 3 0 1 0 4 2 -1 3 -1 -2 -2 0 0 0 -1 1 -1 -1 -2 0 0 0 0 -1 -2 5 -3 6 1 5 0 0 0 1 2 -1 1 1 1 0 1 1 -4 1 0 -1 6 3 -6 1 -4 2 -1 -1 -1 3 0 4 3 1 1 1 -2 1 0 0 0 2 1 -1 -2 -7 -3 -7 -2 -4 -1 0 0 -1 0 -2 -2 -8 -5 -1 -4 9 -7 7 -2 -5 -2 0 -1 1 -1 0 -1 1 -2 1 -7 -2 -3 -2 -5 2 -2 1 -5 1 -3 3 2 2 1 0 0 0 0 0 0 3 1 0 8 2 4 3 -1 3 -2 1 2 0 0 2 3 1 3 3 3 3 -2 1 1 2 1 4 -3 6 -1 4 0 4 5 3 -1 0 -1 1 0 4 -3 4 -2 3 -8 1 -5 4 -3 0 -5 -8 1 -3 0 1 -6 0 0 0 -1 0 -2 0 -2 0 0 -1 -1 -1 -4 -2 -8 -7 -2 -3 -2 -2 0 -3 4 -2 4 -1 1 0 3 -4 -2 -7 1 -2 8 -1 4 -2 0 0 -1 -2 2 -7 -1 -4 -2 -1 0 0 1 0 0 0 0 0 1 -3 3 -5 -2 -4 -3 -5 -4 -3 -3 -4 -1 -3 4 -5 3 -7 -1 -2 -5 z M 120 292 l -5 1 -3 4 -5 4 -2 2 9 10 5 0 -1 -7 -1 -2 0 -1 3 -2 5 -4 -3 -2 1 -1 1 -1 -1 -2 1 -4 z &quot;,
    &quot;CD42&quot;: &quot; M 409 61 l 2 3 0 0 0 3 3 4 3 5 -2 4 2 -1 2 4 1 6 2 4 2 4 4 5 1 0 -1 0 2 3 4 6 5 5 1 3 4 5 0 0 1 0 0 3 4 6 4 4 3 5 2 4 3 3 3 5 0 -1 0 0 0 0 0 1 3 4 4 4 5 5 3 4 3 2 4 5 3 4 4 6 0 5 2 5 -1 0 1 0 4 4 4 3 0 0 0 0 0 0 0 0 0 1 0 -1 1 4 0 4 0 -1 1 4 2 7 4 4 4 4 2 4 1 1 -1 -1 0 0 0 2 2 4 4 3 4 3 2 7 1 4 0 -1 1 2 3 3 2 3 1 0 1 1 2 1 0 0 0 0 0 1 0 1 0 0 0 1 0 0 0 0 0 0 1 1 2 5 0 6 1 8 2 5 -1 0 0 0 0 0 0 0 -1 -1 -1 1 -1 -1 -1 1 -5 4 -5 2 -3 4 -3 4 0 3 0 3 0 1 1 0 -5 0 -6 -3 -5 -4 -3 -2 -7 -1 -3 5 -3 0 -4 -2 0 1 -1 -1 -5 0 -4 0 0 0 -3 -2 -4 1 -4 6 -7 1 -6 -5 -4 -2 -2 0 -3 4 -1 7 -6 -1 -4 -1 -5 -3 -5 -3 -2 -2 -2 1 -1 0 0 -1 -1 0 0 0 -2 1 -3 -1 -6 -2 -4 -4 -2 -2 -2 2 -5 1 -5 0 -4 -7 -2 -3 -1 1 0 0 -2 -1 -1 1 -2 -1 -1 1 -1 -1 0 1 -1 0 1 0 -4 2 -6 -1 -1 -3 5 -4 4 -5 -10 -2 -3 -2 0 0 -2 0 -3 -2 -5 -1 -3 5 -3 -1 -2 -1 -1 0 -1 -1 -2 4 -2 2 -4 2 -1 0 -1 0 -3 1 -2 -5 -2 -4 0 -1 0 0 -4 -3 -7 1 -5 4 0 1 1 5 0 4 -2 3 -4 2 -1 -1 -3 0 -2 1 -1 -1 -1 1 -4 1 -7 0 -4 -5 -2 -7 1 -6 1 -4 0 -6 1 -7 0 -8 4 -7 1 -5 1 -3 0 -6 -2 -3 -2 -5 3 -4 1 0 -1 -1 4 -2 5 -2 -1 -2 -4 -3 -7 -3 -3 -2 -1 1 -4 0 -4 0 -3 -4 -5 -3 -4 -3 -5 -1 -8 0 0 -8 1 -7 -7 -1 1 -4 3 -3 -1 -5 -1 -3 1 -2 0 -5 4 -6 5 -4 4 -3 1 -4 0 -1 -1 -2 1 -1 -1 -3 -4 -3 -6 -3 -2 -5 -6 -1 -6 -2 -2 -6 -1 -8 -1 -5 -6 -2 -5 0 -5 3 -2 1 -3 -3 -4 -2 0 -6 1 -5 1 -1 -4 0 -3 3 -4 2 -6 -1 -1 -5 -4 -9 -1 -3 -1 0 -3 1 -3 -1 0 -3 0 -3 -1 0 -1 -3 3 -1 -1 -1 2 -2 2 -2 0 0 0 0 1 4 6 -2 2 -2 -1 0 0 0 0 0 2 -1 4 -2 7 1 4 4 1 0 0 -1 0 0 -1 -1 0 0 1 0 0 0 0 0 0 -1 0 1 1 0 0 1 8 1 3 -6 6 0 8 3 3 1 4 2 4 -2 4 -2 6 -3 5 1 0 0 0 0 0 0 0 0 0 0 1 1 7 1 0 2 0 0 2 -2 1 2 6 0 3 0 3 -2 5 4 5 3 7 0 5 -5 0 0 4 1 8 3 2 3 1 1 1 -1 2 1 2 -3 5 0 6 -2 3 -5 6 -1 7 -1 2 -6 5 -3 6 -1 1 -5 2 -6 5 0 0 -5 0 1 0 -2 -3 0 3 -1 4 -8 1 -8 -1 -6 4 -2 6 3 1 4 2 8 3 5 2 4 2 6 2 6 3 4 3 5 3 7 1 3 z &quot;,
    &quot;CD43&quot;: &quot; M 321 275 l 4 3 0 0 0 1 2 4 2 5 3 -1 1 0 1 0 4 -2 2 -2 2 -4 1 1 1 0 2 1 3 1 3 -5 5 1 3 2 2 0 0 0 3 2 10 2 -4 5 -5 4 1 3 6 1 4 -2 -1 0 1 0 0 -1 1 1 1 -1 2 1 1 -1 1 1 1 0 1 -1 2 3 4 7 5 0 5 -1 2 -2 2 2 4 4 6 2 3 1 2 -1 0 0 1 0 0 1 1 0 2 -1 2 2 5 3 -4 1 -2 2 -3 -1 0 1 -5 3 -3 5 -4 3 -4 4 -4 2 -4 4 -2 5 -3 7 2 8 -2 6 -5 1 -7 1 -7 -3 -4 -3 -4 -4 -4 -2 0 0 -3 4 -2 6 -6 0 -5 -2 -1 -1 -3 2 -4 4 -1 2 -3 -3 -3 1 -1 5 -1 8 3 4 1 2 -5 0 -3 -3 -1 0 -4 2 -3 -2 -2 -1 -3 6 -6 2 -3 1 -1 1 0 0 0 1 4 1 4 3 1 5 -1 4 -1 2 1 1 -2 -1 -4 -5 -2 -4 0 -1 -4 4 0 4 0 0 -1 1 -6 2 1 5 7 3 2 5 4 3 0 5 -3 3 0 2 0 1 0 0 0 0 0 0 0 1 0 0 0 1 -1 4 0 2 2 2 -2 3 -1 3 3 3 -1 2 1 0 0 3 -3 4 -3 5 1 6 6 0 2 -3 2 3 4 3 2 2 1 0 1 1 2 0 3 -3 6 1 2 5 2 6 1 2 -4 -1 -7 0 0 8 -1 6 -3 4 0 1 0 2 -1 3 -3 6 -5 2 -4 3 -4 2 -9 -1 -8 2 -6 -2 -5 -2 -2 -3 -3 3 -2 -1 -5 -3 -1 -7 -4 -9 0 -6 0 -7 -2 -6 -6 -2 -8 0 -1 -7 1 -8 2 -7 2 -3 2 -6 0 -5 -5 -1 -2 -6 -3 -2 1 -2 0 -2 -1 0 -4 4 -3 0 0 0 -4 -1 -1 -6 0 -8 -3 -2 -3 -4 -6 -2 -3 -3 -2 -1 0 -3 0 -3 -2 -4 1 -4 2 -4 -2 -2 -1 -2 3 -4 5 -5 5 -4 7 1 -1 -5 -1 -5 -5 -4 -5 -2 -2 -5 -3 -5 -3 -3 2 -3 0 0 0 0 0 -1 -1 -3 4 -1 4 -2 4 -2 5 -4 -1 -1 -1 -1 4 -3 5 -3 3 -4 2 -5 3 0 2 1 3 -1 2 0 4 2 2 -1 0 0 0 -2 5 -3 0 -3 0 1 1 -1 1 1 1 -1 3 -4 0 -7 8 0 4 -3 2 -5 4 -6 2 7 4 5 7 0 4 -1 1 -1 1 1 2 -1 3 0 1 1 4 -2 2 -3 0 -4 -1 -5 0 -1 5 -4 z &quot;,
    &quot;CD44&quot;: &quot; M 53 418 l 1 8 2 5 2 2 0 4 -1 2 3 2 1 2 0 0 -1 5 -6 -1 -5 -2 0 -3 1 0 -3 -2 -1 -6 -2 2 1 -5 1 -3 -1 0 0 0 -1 0 0 -3 0 -8 -2 -3 0 0 0 0 0 -2 3 -3 6 1 0 3 -1 0 0 1 0 0 z M 30 354 l 1 1 2 -3 4 -3 2 -6 0 -8 2 -4 1 1 0 0 0 0 0 0 -1 2 1 1 0 0 1 0 -2 3 0 4 2 4 1 3 -2 2 1 0 -1 2 -2 4 3 3 -1 0 1 0 -1 1 -3 3 6 -1 2 -5 4 3 0 0 0 -1 -1 -3 1 -1 3 -5 6 -3 0 -5 0 -4 0 0 -1 0 -1 1 -2 0 -2 -1 0 0 -2 -2 -4 -3 5 -1 2 -1 -1 0 -1 -2 -6 -2 3 -3 0 -1 3 1 2 -1 6 0 2 1 3 1 2 -4 8 -7 3 3 5 2 3 1 0 1 0 0 6 3 3 2 10 1 6 2 7 -1 2 5 2 7 2 4 1 3 11 3 4 -2 3 -4 6 -2 8 -2 7 -1 4 -1 2 0 1 0 1 -3 8 -2 5 2 5 3 3 3 6 3 4 4 3 3 3 5 2 5 5 2 5 4 1 5 1 5 -7 -1 -5 4 -5 5 -3 4 -1 1 0 0 0 0 0 0 0 0 -2 0 -6 -3 -5 -3 -5 -2 -3 0 -3 4 1 4 1 0 0 0 1 -1 1 4 -1 9 0 5 4 3 2 3 1 -1 0 0 0 0 -1 4 -2 7 0 7 3 9 1 3 6 4 6 1 0 6 -1 7 -2 8 -1 3 -1 2 5 4 1 7 2 3 1 0 1 -1 3 4 1 4 1 -1 3 -4 1 -6 2 -6 3 2 0 0 -1 1 1 2 -1 2 2 1 3 4 7 1 2 -4 5 -3 8 0 6 2 2 6 0 7 0 6 4 9 1 7 -1 4 1 0 0 2 -2 1 1 3 -1 2 -1 0 -3 -4 -8 -2 -2 8 -5 2 -7 -1 -3 -5 -1 -3 0 0 -4 2 -5 -3 -3 -6 -1 -7 -1 -5 -3 -2 -6 2 -5 1 -1 0 -1 0 -3 3 -7 1 -8 2 -2 2 -6 0 -3 -1 -3 -3 -3 -8 -4 -2 -3 -2 -1 1 -2 0 -4 -2 -4 1 -2 2 -7 3 -2 7 0 5 -6 3 -7 4 -8 4 -2 2 -1 0 0 1 -7 -2 -4 1 1 -3 -3 -6 -3 -4 -1 -3 3 -2 -4 -2 -2 -2 0 0 -1 -2 -6 -3 -4 -2 -3 -1 -4 1 0 0 1 2 -2 -1 0 -3 1 -1 0 0 3 -1 3 -9 0 -7 1 -3 0 0 0 -1 1 -1 -1 -3 -5 -6 2 -5 6 -3 5 0 -4 -1 -4 -2 -4 -3 -5 -3 1 -5 1 -2 0 0 2 -1 2 -2 -3 1 -1 0 0 -1 0 0 0 0 1 -1 -2 -3 -1 -6 -1 -9 -2 -6 -1 -6 -7 -4 0 -1 -7 -2 -5 1 -3 3 -4 2 -1 -6 -2 -9 -3 -3 0 -1 0 0 0 0 0 0 0 -2 1 -3 1 -4 -1 0 0 -1 -1 2 -4 3 -5 4 -3 2 -5 0 1 -6 7 0 -1 -6 -4 -4 -2 0 -1 0 -1 0 -1 4 -2 4 -2 1 1 -3 1 -3 0 0 0 0 -1 -2 -1 -4 2 -1 0 -1 0 0 -1 0 1 -1 0 0 1 0 1 -4 2 -7 2 4 2 4 2 3 -1 -5 -1 -4 -1 -1 2 -3 0 -7 3 -4 0 1 0 -1 0 1 2 -1 4 -1 -2 3 -2 2 z &quot;,
    &quot;CD45&quot;: &quot; M 134 519 l 0 -5 2 -7 7 -3 2 -2 4 -1 4 2 2 0 1 -1 3 2 4 2 3 8 3 3 3 1 6 0 2 -2 8 -2 7 -1 3 -3 1 0 1 0 5 -1 6 -2 3 2 1 5 1 7 3 6 5 3 4 -2 0 0 1 3 3 5 7 1 5 -2 2 -8 8 2 3 4 1 0 1 -2 -1 -3 2 -1 0 -2 -1 0 1 -4 5 3 2 1 3 -3 2 3 5 2 6 2 8 -2 9 1 2 4 0 1 0 0 2 4 3 7 -4 7 -3 2 -3 5 -4 3 -3 2 -6 2 -5 2 -3 6 -3 4 -5 3 -2 8 0 5 -3 3 -2 7 -1 8 -2 4 -1 3 -2 8 6 6 2 8 0 5 4 3 -1 5 -3 3 -3 5 -2 3 0 1 0 0 0 0 1 4 -2 3 -4 4 -3 -3 -4 -2 -5 -4 -4 -2 -6 -1 -4 1 -2 4 -4 5 -4 1 -3 -2 -1 0 -3 2 -8 0 -4 -1 -5 -3 -2 2 -2 1 -4 -3 -3 1 -4 3 -5 2 -8 -3 -1 -4 0 -8 -4 -3 0 -2 3 -3 -1 -4 -1 -3 -1 -4 -3 1 -2 -1 -1 4 -1 2 -2 2 -1 0 -1 5 -7 -1 -4 -3 -2 -5 -5 -3 -1 -1 -2 1 -1 -4 -6 2 -5 1 -4 4 0 7 -3 3 0 1 0 2 -3 3 -3 3 -5 3 -4 1 1 2 -4 0 -6 0 -1 1 1 0 -2 2 -7 -1 -2 -5 -1 -4 -2 0 -1 1 -1 -4 -1 -8 -2 -3 -6 -2 4 -9 1 -3 4 -5 4 -3 1 0 0 0 0 0 0 0 2 -1 0 1 1 -1 0 1 1 0 0 1 1 -2 0 1 1 0 -1 0 0 -1 4 -1 3 2 1 1 0 -1 0 -1 2 -4 2 -3 0 0 0 0 0 0 2 0 2 0 4 4 2 5 -1 -5 -2 -5 -8 -2 -9 0 -6 0 -3 1 0 0 -3 -3 -4 -3 -1 -1 1 -2 -1 0 1 -1 -1 -2 1 0 3 -2 6 -5 1 -1 1 0 1 -2 3 -2 -1 -3 -4 -4 -3 -6 -4 -11 -2 0 -3 -1 0 -2 5 -2 6 -11 3 -8 -1 -2 -1 -7 5 1 0 -1 3 0 9 0 7 -2 7 -2 4 2 3 -2 0 -4 8 -4 7 -4 z M 70 573 l 1 6 2 3 3 6 6 7 2 0 3 -3 3 -5 2 -8 1 -4 4 0 7 -1 -1 -8 -1 -4 -3 -5 -7 -1 -2 -6 -1 -6 -5 0 -6 1 -4 12 -2 4 -1 5 z &quot;,
    &quot;CD46&quot;: &quot; M 118 837 l 1 -4 -1 -7 -2 -4 1 0 0 -1 -1 -3 1 -3 0 -1 0 0 -1 3 -2 8 0 9 -2 8 -3 2 0 0 2 3 -3 4 -3 2 -7 4 -4 0 0 0 0 1 -4 4 -2 6 -2 5 -2 4 -7 2 -3 3 -3 0 0 -1 0 0 0 0 0 0 0 -2 1 -2 0 -1 1 -1 -1 -1 0 -1 0 0 -3 -3 -4 1 0 1 -1 -1 1 -1 0 -1 0 0 -1 -2 1 0 -1 -1 0 0 1 -1 -1 0 3 -3 1 -7 -6 -2 0 -2 2 1 -1 -4 0 -4 0 0 0 0 0 0 -1 -4 -1 -4 0 1 0 -1 -2 1 0 -3 -2 -2 1 -1 -2 -1 -2 -1 -2 3 -3 0 -3 -4 -9 -3 -2 -3 -1 -1 -1 1 -1 -4 -2 -6 -3 -5 0 -1 1 -1 -1 -1 1 -2 2 -5 3 -8 5 1 3 5 2 3 2 2 3 -3 2 -3 0 -1 -2 -4 3 -4 6 -1 3 -1 -6 -1 -7 0 1 -7 5 -4 0 -5 -2 -4 -1 1 2 -5 -1 -6 0 -6 -2 -4 -1 -3 -1 5 0 8 -4 3 -2 0 -3 -3 -2 -5 -1 -2 3 -2 6 -1 0 -3 -2 0 4 -3 4 -3 1 -2 -5 -3 -4 -2 -1 -1 1 0 0 0 1 -2 -4 -1 -2 5 -2 1 0 -1 -1 -1 -1 -1 -2 3 -2 6 -2 0 -1 -2 -1 2 -2 -7 -3 -1 0 1 0 0 0 1 -1 -1 -1 -2 0 0 -1 0 0 0 -2 -1 -3 -1 1 -1 0 0 0 -1 -1 -2 0 -2 5 -1 6 1 4 1 0 0 0 0 2 0 0 -1 -2 -3 1 -3 0 -1 1 1 3 -2 4 1 2 1 3 -3 3 -1 -1 5 -1 4 0 3 2 0 1 -1 0 3 3 4 1 -1 1 -2 0 -1 2 2 1 6 0 4 2 -1 1 1 1 -1 0 0 0 -2 2 -3 2 -6 0 -4 -5 -3 -3 -4 -1 0 -1 -2 -3 -4 -1 -5 2 -4 -4 2 -3 1 1 -2 0 -2 -1 0 1 1 0 1 -2 0 -3 -4 1 -5 1 2 2 -1 0 -1 0 -1 1 0 -2 0 2 -4 -1 0 0 -2 4 -1 4 -2 -1 -3 1 -1 0 -1 0 -1 0 0 0 0 0 0 0 0 1 -2 3 -6 4 1 2 3 0 -3 -4 -4 -1 -4 2 -6 3 -1 6 2 2 3 1 8 1 4 1 -1 2 0 1 4 2 5 7 1 2 -2 -1 0 1 -1 6 0 4 0 -1 -2 4 -1 5 -3 3 -3 3 -3 0 -2 0 -1 3 -3 0 -7 4 -4 5 -1 6 -2 1 4 2 -1 1 1 5 3 2 5 4 3 7 1 1 -5 1 0 2 -2 1 -2 1 -4 2 1 3 -1 1 4 1 3 1 4 -3 3 0 2 4 3 0 8 1 4 8 3 5 -2 4 -3 3 -1 4 3 2 -1 2 -2 5 3 4 1 8 0 3 -2 1 0 3 2 4 -1 4 -5 2 -4 4 -1 6 1 4 2 5 4 4 2 3 3 3 8 2 3 1 4 0 8 4 6 5 3 4 6 0 4 4 4 3 4 4 3 3 5 1 2 0 3 -3 6 -5 3 -1 -2 -3 5 -3 3 0 2 -3 -2 -2 3 -1 1 -4 0 -5 -3 -1 -5 -3 8 5 3 0 1 -2 5 1 3 1 0 0 0 0 0 0 -1 0 0 3 3 9 0 3 -2 0 -1 -1 -1 5 0 3 2 0 0 3 -2 2 7 -2 6 -2 9 1 2 -1 1 -5 2 -3 1 0 0 -3 -3 -4 -2 -1 1 -2 3 0 4 -3 -1 0 5 2 9 0 3 -3 -2 -5 -1 -1 3 -2 -2 -2 -4 0 -2 0 0 0 -3 3 -3 -2 -1 -1 -1 -1 1 1 -4 3 -2 1 -6 -3 -3 -1 0 1 0 0 -1 0 1 0 0 1 -1 0 1 0 -2 -4 -4 -4 -1 0 0 2 -5 -2 -2 0 0 0 -1 0 0 0 -1 -1 -3 0 -4 1 0 -1 0 -4 3 -1 6 -6 1 -1 -3 -1 1 0 -1 -1 1 -4 4 -7 -1 -4 -1 4 2 4 3 0 0 1 0 1 0 1 0 1 1 1 -1 -3 3 -3 3 1 1 -1 0 0 1 1 3 -2 2 0 6 2 3 1 0 0 0 3 3 4 4 1 1 0 0 3 2 2 1 0 1 2 1 -3 0 1 2 1 0 0 0 1 1 1 -1 1 4 -1 7 -6 0 -7 1 -2 3 1 2 4 1 0 0 0 0 -1 1 1 1 -1 1 -2 4 -4 2 0 -1 -2 1 1 3 0 0 -2 1 -3 -1 -1 1 -1 1 0 0 -3 2 -1 5 -3 -1 0 0 0 0 0 -1 0 0 1 1 -1 -2 -3 -3 -2 -3 1 0 -1 -3 -6 -3 -3 -3 -1 1 -1 1 0 -1 -1 1 -3 -1 -3 0 0 1 1 0 -2 0 -2 -1 -1 1 -2 -1 0 1 -1 -1 0 0 0 0 -2 -3 0 -4 0 0 1 -1 1 -1 0 0 2 -2 1 -4 2 2 2 -4 1 -2 -1 -1 1 -1 3 -3 6 -3 2 -6 2 -4 3 -1 2 5 0 4 0 0 1 1 4 0 2 -5 2 -7 -3 -6 -3 1 -3 2 -4 0 -3 -4 -4 3 -2 4 -3 5 -6 1 -4 -2 0 0 0 0 0 -1 -3 4 -3 4 -2 0 1 1 -4 2 -3 4 -1 0 -1 0 -2 2 -8 0 -4 -1 4 2 4 3 -1 1 1 0 0 0 -2 4 3 3 -1 2 0 -1 -1 0 -3 4 -1 7 0 6 -5 2 -2 4 -2 -1 -6 0 -1 4 -4 -2 -3 -2 -1 1 -2 0 -1 -1 -1 1 -2 -4 0 -4 -1 0 z M 61 823 l 3 -2 2 -2 -4 -2 -2 -3 -2 1 0 -2 -1 0 0 0 -1 -3 -2 -3 0 0 -1 0 -1 2 -3 0 -1 -4 1 2 1 0 1 -3 -1 -3 2 0 -1 -2 -2 -1 -4 4 -3 0 1 4 2 5 3 6 5 3 4 2 z M 49 798 l 1 0 2 0 3 4 3 -2 2 -1 0 4 -3 4 0 0 4 3 4 2 0 0 0 0 0 0 4 1 3 1 0 0 0 1 0 0 0 0 1 0 0 -2 -4 -4 -3 -4 4 3 7 2 5 3 5 1 2 1 1 -1 -8 -2 -4 -4 1 -2 1 -2 -1 0 1 0 -3 1 -6 -2 2 -1 -3 -1 -2 -3 -4 -2 2 -3 -1 -1 0 0 0 0 0 0 0 0 -1 0 -2 1 -2 -2 -1 1 -2 0 0 0 0 0 0 0 0 -1 0 0 0 0 -2 0 -4 2 -1 2 0 1 0 0 0 0 z M 122 727 l 4 -1 1 -1 1 0 1 -1 6 -2 7 0 7 -1 3 -4 4 -4 1 -7 -1 -4 -6 -3 -3 -4 -1 -1 1 -1 -2 -3 -4 -3 -4 0 -1 1 0 0 -1 0 0 0 0 0 -1 0 -2 0 -4 2 -7 3 -6 -3 -2 -3 -4 1 0 0 0 0 -1 4 -4 4 -2 -1 -1 0 -2 4 -2 6 -3 3 1 1 4 7 7 0 4 1 4 5 0 4 z M 95 876 l -1 0 -1 -7 0 -5 6 -2 6 2 3 6 1 4 0 0 0 0 2 4 2 6 -4 -1 1 0 -3 0 -7 -1 z M 191 843 l 1 0 0 1 0 0 1 3 -2 6 -2 3 -10 1 -7 -4 0 -6 3 -3 0 0 0 0 0 1 4 2 5 -3 0 0 0 0 1 0 0 0 1 0 2 0 1 0 1 0 z M 27 823 l 5 2 4 3 -1 1 0 0 0 0 0 0 4 2 3 2 0 0 2 2 3 6 1 4 -1 -1 -2 3 0 6 -4 5 -2 2 -1 -2 -2 2 -1 -1 0 0 0 1 0 1 -3 1 -5 2 1 1 -3 1 -3 1 -5 1 -7 1 -5 0 1 -1 -1 0 1 -1 1 -2 -5 -2 -2 -8 3 -4 1 1 1 0 0 0 -1 -3 5 -4 4 -1 0 -1 4 -3 4 -3 0 -1 -1 0 2 -3 z M 281 794 l 2 1 5 3 0 2 -1 0 -1 -1 -1 1 0 -1 0 3 2 5 1 0 1 0 0 4 0 7 1 5 -6 0 -3 -1 -1 0 0 -1 0 0 0 0 -3 -3 0 -7 4 -3 1 -5 -1 -6 -1 -1 0 0 0 0 0 0 0 0 0 -1 z &quot;,
    &quot;CD47&quot;: &quot; M 560 528 l 7 2 5 -4 3 -4 3 -4 1 -1 0 0 3 -4 3 2 2 6 -1 8 -2 3 -1 0 0 0 -1 4 0 4 0 0 -1 0 0 -1 0 1 0 0 -2 2 -2 3 0 2 1 0 0 0 1 4 0 4 -1 -1 -1 1 0 0 1 4 -1 3 0 0 0 0 0 0 0 1 -1 3 0 3 0 0 0 1 -1 -1 -1 4 -1 8 -2 7 -1 6 -2 4 -2 4 -7 -3 -1 0 -7 -2 -4 2 -3 2 -6 1 -4 -7 -3 -3 -4 -3 -5 0 -9 1 -4 1 -4 2 -2 4 0 1 -1 1 0 2 2 3 -3 0 -4 3 -1 -1 -1 0 -3 4 -7 0 -5 -4 -3 0 -3 1 -4 3 -4 5 -5 1 -6 3 -8 0 -7 -2 -8 -1 -7 2 -6 -3 -3 -3 -3 -3 -1 -7 0 -5 1 -6 5 -3 6 0 3 5 3 -1 4 -2 4 -2 0 -8 2 -7 3 -6 1 -2 0 -4 2 1 4 -4 -1 -7 -2 -9 0 -4 -3 -7 -3 -1 -9 0 -5 1 -3 3 -7 4 -2 2 -1 -1 -2 -2 0 7 -3 7 -2 4 -1 0 -3 -3 -2 -7 -5 1 -3 3 -3 4 -2 5 0 5 6 0 8 4 0 5 -8 1 -3 2 -3 5 1 5 5 4 0 5 -6 0 -5 2 1 3 4 4 0 8 -5 -3 -3 -3 -7 -1 -8 1 -3 2 -2 0 -3 -2 -5 -3 2 -3 0 0 1 0 2 -4 -1 -7 -2 -4 -3 -3 -5 -4 -2 -4 -1 -4 -1 0 -1 0 -5 -2 -8 0 -7 -2 -5 -3 -1 0 -3 1 -1 -4 -4 -3 -4 -5 4 -7 -3 -7 -2 -4 0 0 0 -1 -2 -4 4 -2 4 -3 5 -2 3 -6 1 -3 0 -2 0 -1 3 -4 1 -6 0 -8 7 0 4 1 -1 -2 -2 -6 -2 -5 -6 -1 -3 3 -2 0 -1 -1 -1 0 -2 -2 -4 -3 -2 -3 -2 3 -6 0 -1 -6 3 -5 3 -4 0 -3 -1 0 1 -2 -3 -3 1 -3 2 -3 -2 -2 0 -2 1 -4 0 -1 0 0 0 -1 0 0 0 0 0 0 0 -1 0 -2 3 -3 0 -5 -4 -3 -2 -5 -7 -3 -1 -5 6 -2 1 -1 0 0 0 -4 4 -4 0 1 2 4 4 5 2 1 -1 -1 1 -2 1 -4 -1 -5 -4 -3 -4 -1 0 -1 0 0 1 -1 3 -1 6 -2 3 -6 2 1 3 2 4 -2 1 0 3 3 5 0 -1 -2 -3 -4 1 -8 1 -5 3 -1 3 3 1 -2 4 -4 3 -2 1 1 5 2 6 0 2 -6 3 -4 0 0 4 2 4 4 4 3 7 3 7 -1 5 -1 2 -6 -2 -8 3 -7 2 -5 4 -4 4 -2 4 -4 4 -3 3 -5 5 -3 0 -1 3 1 2 -2 4 -1 5 3 4 1 6 1 1 -7 3 -4 2 0 4 2 6 5 7 -1 4 -6 4 -1 3 2 4 0 5 0 1 1 1 -1 3 2 3 0 3 -5 7 1 3 2 5 4 6 3 5 0 -1 0 0 -1 0 -3 0 -3 3 -4 3 -4 5 -2 5 -4 1 -1 1 1 1 -1 1 1 0 0 0 0 0 0 1 0 0 -1 1 2 1 4 2 5 6 7 -1 3 0 1 0 1 -1 2 1 3 0 4 0 1 0 1 -1 1 1 1 0 0 0 1 1 5 0 6 1 5 -1 -1 0 4 4 7 2 3 -1 0 1 1 1 4 3 7 0 9 0 6 -2 5 -1 -2 -1 1 -3 5 -2 6 0 7 4 8 1 5 0 2 0 0 1 2 -1 2 0 1 0 0 0 0 0 0 -1 3 0 7 -2 8 -3 6 -2 3 -1 6 -1 7 0 7 1 8 -1 4 0 -1 1 4 2 3 1 5 1 5 6 3 0 0 0 3 -4 4 -4 3 -1 0 -2 4 6 2 z M 797 205 l 2 1 1 0 -1 3 0 6 1 2 -3 2 -3 3 -6 0 -4 -2 -3 -5 1 -5 6 -2 4 -2 0 0 2 0 z &quot;,
    &quot;CD48&quot;: &quot; M 307 751 l 4 2 0 5 -1 4 0 0 -1 0 -1 4 1 4 1 0 -1 1 3 3 3 4 3 0 -1 -1 0 0 4 -2 7 -1 4 1 0 0 1 1 0 5 0 7 -1 4 -1 4 1 2 0 0 0 0 0 0 1 0 -1 1 0 0 0 0 -1 1 -2 -1 -1 1 -2 -3 -4 1 -6 -3 -2 -8 -6 2 -2 6 -6 -1 -2 -6 1 -4 -1 1 -1 -4 -2 -3 -2 -4 0 -7 2 -3 0 -1 0 0 0 -2 0 0 0 -1 0 -1 3 -1 2 -5 z M 441 731 l 3 1 1 2 -2 3 0 1 0 0 0 1 -1 0 0 4 3 3 1 5 0 4 0 0 -1 2 -2 3 -1 3 2 0 -1 1 2 -1 3 -2 3 -1 -1 1 -2 5 -1 4 -2 -1 -1 1 -1 1 3 2 0 7 -2 -3 -1 0 0 0 0 0 -1 1 -3 -1 -2 3 -3 4 -1 3 4 4 3 0 -1 1 -1 1 0 -1 -1 0 -1 0 -1 0 -4 2 -6 1 -1 -3 0 0 -1 0 -1 0 2 -5 -1 -3 -2 1 -2 0 -1 -3 5 -3 0 -6 0 -4 0 1 -1 -2 -2 1 0 1 0 0 -3 2 -4 2 -1 3 0 0 -1 0 -2 -4 -3 -3 -1 -4 0 -4 1 -1 0 0 3 -3 4 -4 1 0 0 0 0 0 3 3 4 1 0 -1 4 1 4 0 -1 -2 1 -1 -1 0 0 0 0 0 -3 -4 2 -5 5 -1 3 -5 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 -1 -1 1 0 1 1 0 -4 z M 429 693 l -4 1 -3 2 -2 4 1 3 0 0 0 1 2 3 0 3 0 0 0 1 -1 -1 -1 0 -1 1 4 2 3 6 2 2 -2 1 3 2 -2 1 0 0 0 0 -3 1 -4 1 1 -2 1 -1 -4 -1 3 -3 -4 -1 -3 -4 -3 1 -1 2 0 0 -1 0 0 0 -1 0 -1 -1 -2 -1 -1 2 0 0 -1 0 -3 -1 0 5 -3 2 -3 2 -7 4 0 4 2 -3 5 -2 4 -2 0 -1 0 -1 1 -1 1 1 1 0 0 0 1 -1 2 1 0 0 -1 4 1 4 0 0 0 0 1 -1 0 1 0 0 0 1 2 1 -4 3 -2 2 -1 0 0 0 -1 0 0 0 0 0 0 0 0 0 -4 -1 -1 2 4 2 -2 3 -1 5 -1 7 1 6 1 -2 0 0 1 0 1 -1 0 0 0 0 -1 -1 1 0 0 0 0 -2 -1 -1 4 2 1 5 -1 -2 -1 4 -2 4 -4 2 -6 0 0 -4 5 -1 -5 -3 -1 -3 -1 2 0 -1 -1 2 -2 -1 -1 -1 -2 1 -2 -1 -1 1 -1 -1 -1 0 1 0 -1 -3 4 -1 4 -1 0 -1 -3 -5 -1 -4 -1 1 0 -1 -1 0 0 0 -1 2 -2 3 0 3 0 0 0 0 0 0 0 0 0 0 -4 2 -2 -1 0 0 0 -1 -1 -1 1 -1 0 -1 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 -4 0 -6 0 -2 7 -6 1 -4 2 0 1 -3 -4 -3 -2 3 -1 -2 0 -2 0 0 -1 -1 0 1 0 -1 1 0 0 -4 -1 -5 -4 -1 -3 0 1 2 -2 1 -2 -1 -2 0 -4 1 -2 0 0 0 0 -2 -2 0 -5 1 -4 2 -3 -3 2 -2 5 -1 4 0 0 0 0 0 0 0 0 -2 0 0 0 1 -3 -2 0 1 0 -3 1 2 2 2 4 0 3 -1 0 0 1 -1 1 1 0 0 0 0 -1 -1 0 -1 -1 -2 2 -3 -2 0 0 -1 0 0 0 0 1 0 0 -3 1 -1 -2 0 0 0 -2 -4 -3 1 3 -1 2 1 0 0 0 -1 1 1 0 0 1 0 0 0 0 0 0 0 0 0 1 -2 1 -4 5 -5 0 -2 -2 0 0 0 0 -1 1 -5 2 -4 1 0 0 -1 -1 -2 -5 3 -6 0 -3 -1 -2 -3 -5 -4 -3 -3 -4 -4 -4 0 -4 -4 -6 -5 -3 -4 -6 0 -8 -1 -4 -2 -3 -3 -8 4 -4 2 -3 -1 -4 0 0 0 0 0 -1 2 -3 3 -5 3 -3 1 -5 -4 -3 0 -5 -2 -8 -6 -6 2 -8 1 -3 2 -4 1 -8 2 -7 3 -3 0 -5 2 -8 5 -3 3 -4 3 -6 5 -2 6 -2 3 -2 4 -3 3 -5 3 -2 4 5 4 3 1 4 3 -1 1 0 5 3 7 2 8 0 5 2 1 0 1 0 1 4 2 4 5 4 3 3 2 4 1 7 -2 4 -1 0 0 0 -2 3 5 3 3 2 2 0 3 -2 8 -1 7 1 3 3 5 3 7 -1 7 -3 5 0 2 -3 3 -7 0 5 1 7 3 3 3 3 6 3 7 -2 8 1 7 2 8 0 6 -3 5 -1 4 -5 4 -3 3 -1 3 0 5 4 7 0 0 8 -5 5 4 4 3 2 2 0 0 0 1 0 3 -1 1 3 5 3 4 3 2 3 4 5 6 2 4 1 1 7 -2 3 0 0 0 3 -3 2 -7 0 -3 6 -1 5 -4 1 -6 3 -1 3 -3 -1 -2 0 -1 4 -4 6 -7 1 -6 2 -5 1 -1 6 0 5 -9 0 -4 1 2 2 5 4 1 3 -2 2 -3 2 -4 1 2 -2 0 -1 -4 -1 -2 6 -3 -2 -1 -3 -3 1 -2 -1 -2 0 -1 1 0 -3 -2 -6 -2 3 -1 1 0 -1 -1 -1 0 -1 -1 1 -3 -3 -3 2 1 0 -1 0 1 1 -1 0 -2 -5 -2 -6 4 -6 z &quot;,
    &quot;CD50&quot;: &quot; M 115 1034 l 5 2 3 2 5 3 6 2 3 5 -1 4 0 0 1 0 0 0 1 1 2 1 -2 1 3 0 -1 7 -2 1 -3 7 -4 4 -3 5 -3 5 -5 4 -5 0 -3 1 0 0 0 0 0 0 -1 1 -2 1 0 1 -3 1 -8 2 -4 0 0 0 -2 0 -3 2 -6 3 -7 0 -8 1 -7 1 -7 -1 -8 0 -7 0 -6 1 -3 5 -6 -1 -3 -4 -4 -3 -5 -2 -2 -4 -2 -8 2 -7 3 -4 1 0 1 0 3 -3 4 -3 3 -5 3 -4 4 -2 6 -4 4 -2 5 -1 6 -1 7 -3 4 -2 4 -1 5 -1 -1 1 4 -1 7 -1 6 -2 3 -3 1 1 0 0 1 0 0 1 0 0 1 0 2 -1 2 0 1 0 0 0 1 0 6 -1 z &quot;
  }

}</code></pre><p>다 했다! 사실 더있는데 지금은 city 로 구분된 것만 진행한다! (서울에 대한 구별 svg도 해야하고~~등등 남았다!)</p>
<p><code>import svgVector from &#39;../../svg.json&#39;</code>
import를 해서 사용하면 된다!!</p>
<p>그러면 database의 용량도 줄일 수 있으며 api에서 vector값에 대한 traffic을 고려하지 않아도 된다.</p>
<hr>
<h3 id="3-weather-api에-대한-고민">3. weather api에 대한 고민</h3>
<p>이거는 cache와 스케줄링을 같이 사용해서 프로세스를 분리시키고 기존의 프로세스와 thread경쟁을 하지 않도록 해보자!
cache를 사용하는 이유는, call 할 때마다 제공되어야하는 정보인데 database에서 계속 가져오는 보다는 현재 6시에 한번 database가 update되도록 설계 했는데 그러면서 cache에 값을 저장해놔서 Database접근시간보다 더 빠르게 처리하기 위해서이다. 
cache는 평균 database보다 1:7정도로 시간이 걸려서 실행시간은 빠르지만, 용량이 적고 비싸다.
그런데 지금 데이터가 용량이 엄청 크지 않다. 단지 <img src="https://velog.velcdn.com/images/sujin-create/post/a86c87af-b9dd-460f-a39b-105eb0cff3cd/image.png" alt="">
이것들만 저장하면 된다!</p>
<p>만약 cache가 삭제되더라도 database에도 6시에 한번은 저장되니까 캐시가 없으면 DB에서 가져오도록하면 된다!</p>
<p>이부분에 대해서는 다음 포스팅에서 적도록하겠다~~!</p>
<hr>
<h3 id="느낀점">느낀점</h3>
<p>프로젝트는 많이 해봐서 다양한 경험이있어서 새로운 api나 기술들을 적용하는데 있어서는 나름 매번 잘 되었기 때문에 개발을 그래도 못하지 않는다고 생각했다.
그런데, 사실 백엔드는 구현의 문제가 아니다,,,,
성능!까지 고려할 수 있는 사고를 할 수 있어야할텐데 요즘에는 개발블로그 커뮤니티가 잘 되어있어서 많이 읽어봐야겠다고 생각했고 왜?에 대한 집착을 해야겠다고 생각했다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] cache를 사용하여 불필요한 트래픽을 줄여보자!]]></title>
            <link>https://velog.io/@sujin-create/spring-cache%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%9D%84-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@sujin-create/spring-cache%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%9D%84-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 15 Jul 2023 03:54:05 GMT</pubDate>
            <description><![CDATA[<h2 id="어떤-경우에-cache를-사용해야할까">어떤 경우에 cache를 사용해야할까</h2>
<p>cache를 사용하는 이유는 데이터 베이스에서 읽기 성능 개선을 할 수 있기 때문이다. cache는 cpu가 database 메모리에 접근을 하지 않아도 되도록 중간 버퍼의 역할을 한다. cache에 값을 저장해놓고 &quot;읽는다&quot;면 database까지 가서 조회하지 않을 것이다! </p>
<p>그렇다는 것은 cache에 저장한다는 것은 database에는 저장하지 않는다는 것이다. (물론 시간을 정해두고 update를 하겠지만 말이다!) 그러면 database에 안전하게 보관하거나 데이터가 여기저기 연관관계가 얽혀있는 것이라면 cache를 사용해도 될까?</p>
<p>예를 들어서, 고객이 송금을 했다. 그러면 송금한 고객의 잔액을 차감하고 입금받은 사용자의 계좌에서 돈을 늘려놔야, 간편송금 시스템에서의 &#39;간편&#39;이 의미가 있을 것이다. 캐시에 저장해두고 1시간 뒤에 다른 고객들의 정보와 함께 update를 한다? 그것은 너무 느리고 돈이 걸린 중요한 정보를 날릴 수 있게 되는 불안요소가 있다. </p>
<p>결론은, 실시간적으로 중요한 데이터라면 데이터베이스에서 관리하는게 맞고, 데이터베이스에는 늦게 반영해도 되는 것!은 cache에 저장하고 조회를 하는 시스템을 가져가자!라고 생각했다.</p>
<h3 id="조회수">조회수</h3>
<p>사용자의 통계 분석을 하려고했다. 이때, 어떤 카테고리의 페이지가 가장 조회수가 많은 것인지 알아보려고한다. 그래서 page entity에 view라는 조회수 컬럼을 넣어서 관리를 하려고했다. api를 클릭할 때마다 조회수를 증가시키고 그것을 매번 database에 저장해뒀다가 update될때마다 database에서 가져와야하는가? cache를 사용하자!
왜냐? 우리 서비스에서는 조회수를 사용자에게 보여줄 필요는 없다. 그리고 사용자 통계 분석을 한다고 했듯이 그 전에만 반영되어있으면 되고 전체적인 흐름을 볼 것이기 때문이다. 
그렇기때문에 실시간성이 중요하지 않다고 생각했다. 그리고 사라지면 안되는 엄청나게 큰 영향이 있는지도 의문이였다.</p>
<hr>
<h2 id="spring-boot-starter-cache-를-소개한다">spring-boot-starter-cache 를 소개한다</h2>
<p>물론 redis처럼 인메모리를 사용할 때 cache데이터와 같은 것을 사용하기 좋으니,,,,고려를 해봤다. 하지만 아직은 사용자 통계 분석을 위한 view table을 생성하기 위해서 page view count데이터 적용을 고려하는 단계이기에 간단한 spring-boot-starter를 사용하도록한다.</p>
<h3 id="dependency">dependency</h3>
<pre><code> implementation(&quot;org.springframework.boot:spring-boot-starter-cache&quot;)</code></pre><p>spring에서 기본으로 제공하는 spring-boot-starter-cache를 사용하려면? dependency에 등록을 해줘야한다.</p>
<h3 id="bean-등록">bean 등록</h3>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/249006ce-edd5-4882-be04-4bda25d9ef47/image.png" alt=""></p>
<p>Cache Manager를 통해서 cache를 등록하고 사용할 수 있게 된다.
cache를 key, value를 통해서 등록할 수 있는데 이때 바구니를 만드는 과정이 필요할 것이다.
그 바구니는 여러개가 될 수 있다. 바구니의 이름을 지정하면서 cache를 생성하면 된다고 생각하면 좋을 것인데 그 이름을 등록할 수 있도록 <code>ConcurrentMapCacheManager</code>가 역할을 해준다.</p>
<p>cacheManager를 생성할 때
<img src="https://velog.velcdn.com/images/sujin-create/post/6eccf8af-3c72-47b5-ad74-606d00b4f029/image.png" alt=""></p>
<p>concurrentMap위의 method를 사용한다. 이는 CacheManager와 BeanClassLoaderAware를 상속받아서 만들어진 public class에서 정의된 method이다. 코드에서 볼 수 있듯이 이름을 넘기면 된다.</p>
<blockquote>
<p>Construct a static ConcurrentMapCacheManager, managing caches for the specified cache names only.</p>
</blockquote>
<p>라고 설명되어있는데, 아래의 이미지를 보면 더 이해가 쉬울것이다.</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/465b1199-83db-4c86-a4d5-0a66cd11d11a/image.png" alt=""></p>
<p>처음에는 16개의 공간으로 바구니 16개가 존재한다. 그렇지만, 이름이 부여되지 않았다. 여기에 이름을 부여하기 위해서 ConcurrentMapCacheManager의 생성자를 사용해서 정의를 해준다. 이때 setCacheNames를 사용하는데 </p>
<pre><code>public void setCacheNames(@Nullable Collection&lt;String&gt; cacheNames) {
        if (cacheNames != null) {
            for (String name : cacheNames) {
                this.cacheMap.put(name, createConcurrentMapCache(name));
            }
            this.dynamic = false;
        }
        else {
            this.dynamic = true;
        }
    }
</code></pre><p>다음과 같다. 이름과 cahce를 매핑시켜주는 작업이라고 생각하면 된다.
아까 key, value를 저장할 바구니를 만든다고 했는데 그 이름을 정해주면 된다고 했다.</p>
<pre><code>this.cacheMap.put(name, createConcurrentMapCache(name));</code></pre><p>이거를 보면 이해가 쉬울 것이다. createConcurrentMapCache를 생성하는데 결국 key, value를 저장가능한 ConcurrentHashMap을 만들어주는 것이다. 그래서 여기에 cache data를 채워나가면 된다.</p>
<p>돌고돌아 다시 bean!</p>
<pre><code>
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties::class)
class CacheConfig {

    @Bean
    fun cacheManager(properties: CacheProperties): CacheManager{
        return ConcurrentMapCacheManager(properties.cacheName)
    }

}</code></pre><p>이렇게 등록하면 된다. 나는 아직 cache 바구니 하나만 필요해서 다음과 같이 작성했지만, 여러개 필요하면 여러개 넘기면 된다.</p>
<h2 id="cache사용하기">cache사용하기</h2>
<h3 id="cacheput과-cacheable">CachePut과 Cacheable</h3>
<ul>
<li><code>@CachePut</code>
cache에 key에 대해서 value가 있는지 없는지 여부에 관계없이 key를 정해서 value를 update한다.</li>
</ul>
<ul>
<li><code>@Cacheable</code>
cache에서 key값이 이미 존재한다면 value가 존재할 때 그 값을 불러온다. 이미 존재한다면 그것을 읽어오고 존재하지 않는다면 넣어준다.</li>
</ul>
<p>처음 사용해봐서 cachePut, cacheable 중에서 무엇을 어떨때 사용해야하나?에 대해서 위주로 찾아봤다.
cachePut은 데이터를 update할 때 사용한다.
내가 cahce를 사용하는 것은 <strong>조회수</strong>를 위해서였다. 그러려면 페이지 id를 key로 설정하고 value를 view값으로 하려고 했다. 그렇다는 것은 value가 계속 바껴야한다는 것이다. 그러면 cachePut을 update하는 로직에서 사용하고, cache를 database에 저장했다면 모든 key,value가 제거 됐을 것이고 그때 cacheable을 사용해서 key의 value를 0으로 setting해주는 역할로 사용하여야겠다고 생각했다.</p>
<h3 id="delete">delete</h3>
<p>delete는 역시 key로 특정 view에 대해서 삭제가 가능한데, 모든 key에 대해서도 삭제가 가능하다.
나는 cacheManager를 사용해서 cache name 기반으로 모두 삭제를 해줬다.</p>
<h2 id="스케줄러를-사용한-db-업데이트">스케줄러를 사용한 DB 업데이트</h2>
<p>cron을 사용해서 특정 주기마다 스케줄러에 의해서 thread pool에서 cache의 값을 database에 update하도록 해줬다.</p>
<p>그래서 기존의 thread pool에는 이로인한 영향을 주지 않는 선에서 update를 시킬 수 있었고 cache의 data를 key를 통해서 모두 읽어와서 update시켰다.</p>
<p>그런데 이때 있을법한 method를 내가 찾지 못해서인지,,,직접 정의를 해줄 필요가 있었다. 언제였냐면 key를 넘기지 않아도 value를 가져온다거나 혹은 key만 다 가져오는 것이다. key, value값이 Cache 객체 안에서 저장되어 Cache를 출력할때 보이긴하는데 그안의 값을 가져오지 못했다.
그래서 string으로 바꿔서 직접 key, value 쌍을 map으로 반환해주는 utils를 만들어서 사용했다.</p>
<p>이때, CacheManager에서 <code>getCache</code> method를 사용했는데 name을 넘겨주면 된다.</p>
<h2 id="마무리">마무리</h2>
<p>이후, 레디스를 도입해서 cache관리를 하려고하는데 미리 공부를 해봐야겠다!!
그리고 이번에 조회수 카운팅 로직에 대해서 생각해보니 사실 cache에 어떤 고객이 어떤 페이지를 많이 클릭했는지 보려면 사용자 id도 같이 담아놓는것도 방법이다. 이것은 또 어떻게 처리해야할까..에 대해서 더 깊게 알아봐야겠다고 생각했다!
기술 블로그...를 뒤져보자 하하하,,,</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[postgreSQL의 index 그리고 쿼리 최적화의 관계]]></title>
            <link>https://velog.io/@sujin-create/postgreSQL%EC%9D%98-index-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%98-%EA%B4%80%EA%B3%84</link>
            <guid>https://velog.io/@sujin-create/postgreSQL%EC%9D%98-index-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94%EC%9D%98-%EA%B4%80%EA%B3%84</guid>
            <pubDate>Mon, 10 Jul 2023 10:34:30 GMT</pubDate>
            <description><![CDATA[<h3 id="0-intro">0. intro</h3>
<p>database를 공부하며 index를 사용하면 조회시에 성능을 높일 수 있다고만 알았었다. 그리고 이번에 postgreSQL을 사용하는데 query 최적화를 진행할 수 있는 방법에 대해서 알아보면서 index의 중요성에 대해서 깨닫게되었다.
알게된 사실에 대해서 공유를 하려고한다!</p>
<h3 id="1-query최적화란">1. query최적화란?</h3>
<p>query 성능 향상을 의미한다.
최적화 되지 않은 쿼리는 데이터베이스의 성능 저하로 영향을 미칠 것이다.</p>
<p>이때, 어떤 것을 최적화 해야할까?</p>
<ol>
<li>비효율적으로 설계된 스키마</li>
<li>비효율적으로 설계된 쿼리</li>
<li>large table에 걸쳐서 사용될 때의 index의 결함</li>
<li>사용되지 않는 index를 insert, update, delete 연산을 수행할 때</li>
<li>n+1 문제
등등이 있을 것이다.</li>
</ol>
<p>여기서 index라는 것을 많이 볼 수 있다.
postgreSQL은 firebase와 같은 platform인 supabase에서 활용도가 높다.
supabase에서의 문서를 참고하여 query최적화에 대해서 알아보았는데 이때 index를 많이 확인할 수 있었다.</p>
<p>공부를 하고 난 지금은 &quot;index를 활용해서 쿼리 튜닝이 가능하다&quot;라는 것을 알고있다.
물론 적절하게 사용할 때만 사용해야한다.
아니면,,,, 성능이 더 나빠질 수도있다.</p>
<h3 id="2-postgresql의-구조">2. postgreSQL의 구조</h3>
<p>postgreSQL의 기본 구조와 작동원리에 대해서 이해해야 쿼리 튜닝에 대해서 잘 이해할 수 있다.</p>
<ul>
<li>postgreSQL의 기본구조</li>
</ul>
<p>postgreSQL의 내부 구조에 대해서 보면, file system의 구조는 크게 3가지로 구성되고 있다. </p>
<p>하나의 member table이 존재한다고 해보자!
그렇다면 row들이 존재할 것이고 각 row마다 data가 존재할 것이다.
이때, row 하나 하나 각각을 tuple이라고 하는데 이들을 모아 하나의 테이블 속의 데이터들을 구성하는데, 특정 단위로 묶은것이 page이고 이들을 전체 모아 테이블을 구성하는데 heap file을 구성한다.</p>
<ol>
<li>item(tuple)
item은 곧 tuple이 된다. 즉, 특정 테이블의 row 하나를 의미한다. 이것들이 모여서 page를 구성한다.</li>
<li>데이터 페이지
데이터 페이지는 하나의 페이지는 확장 가능한 slotted page의 구조를 가진다. 이때 페이지는 테이블의 모든 row 데이터를 담고 있는 공간이다. block이라고도 한다.page 정해진 사이즈만큼의 tuple을 받을 수 있다.</li>
<li>heap
하나의 페이지인 row가 page를 구성하는데 이러한 page는 결국 연결되어 하나의 heap file을 생성하게 되는데 이는 결국 모든 데이터를 담고 있게 될 것이다.</li>
</ol>
<ul>
<li>postgreSQL의 join방법</li>
</ul>
<ol>
<li><p>nested join
중첩 루프조인이다. 두개의 table이 있을 때 외부 테이블의 각 행을 반복하고 내부 테이블의 각 행과 비교하면서 동작한다. 중첩 루프조인인만큼 루프를 돌면서 외부 테이블 하나에 대해서 내부 테이블 전체를 스캔한다.
full scan이 이루어지는 조인 방식이다.
따라서, 이 경우는 어떤 경우에 사용해야할까?
외부 테이블 하나의 행에서 스캔하면서 비교해야할 내부 테이블의 사이즈가 적을 때 유리할 것이다.</p>
</li>
<li><p>hash join
해시조인은 더 큰 테이블에 대해서 중첩 루프 조인보다 효율적이다.
말그대로 해시를 사용하겠다는 것인데 조인 조건에 따라서 더 작은 테이블에 대한 해시 테이블을 생성한다. 예를 들어서 주문과 고객이 있을 때 고객1개당 여러 주문이 가능할 것이다. 그렇다면 고객이 내부테이블인데! 내부 테이블인 고객 한명을 key로 가지도록하고 그 고객의 주문 전체를 해시 구조를 사용해서 고객에 대한 여러 주문을 관리하도록한다.
kotlin에서 map, java에서 hashmap 그리고 python에서 dictionary, c에서 structure,,.와 같이 만들겠다는 것이다.
그렇다면 찾을 때 고객 한명이 정해지면 주문을 scan해야하는게 줄어들게 될 것이다.</p>
</li>
<li><p>merge join
병합 조인은 두개의 정렬된 테이블을 조인하는 효율적인 알고리즘을 의미한다. 이때 중요한 것은 &quot;정렬&quot;이다. 두 테이블을 동시에 스캔하고 조인 조건에 따라서 행을 비교하면서 작동한다. 조인 조건이 충족되면 행이 결합되어 결과 집합에 추가된다.</p>
</li>
</ol>
<h3 id="3-postgresql과-index">3. postgreSQL과 index</h3>
<blockquote>
<p>추가적인 쓰기 작업과 저장 공간을 활용하여 데이터베이스 테이블의 검색 속도를 향상시키기 위해서 만들어진 자료구조를 의미한다. (라고 설명한다ㅎㅎ 공부할 때는 발견하지 못했는데,,, <a href="https://mangkyu.tistory.com/96">https://mangkyu.tistory.com/96</a> 해당 블로그에 잘 정리가 되어있더라!)</p>
</blockquote>
<p>그렇다면 본격적으로 index에 대해서 알아보자!
index는 데이터베이스 테이블의 검색 속도를 향상 시키기 위해서 만든다.
이때, b-tree구조를 사용하는데 데이터베이스와 파일시스템에서 주로 사용되는 트리자료구조의 일종이다.</p>
<p>이진트리가 익숙할텐데 이진트리는 자식노드가 left, right 총 2개가 존재한다. 그러나, B-Tree는 자식 노드의 개수가 2개 이상인 트리를 의미한다. B-Tree는 노드내에 데이터가 여러개 존재할 수 있는데 이는 B-Tree의 차수를 결정짓게 된다.</p>
<p>노드 안에서의 데이터별로는 정렬된 상태를 가지며 노드역시도 자식 노드들의 데이터들은 왼쪽에는 작은 값, 오른쪽에는 큰값을 가져야한다.
이때, 왼쪽 오른쪽의 기준은 노드자체라기보다 노드 안의 정렬된 상태로 있는 데이터를 의미하고 데이터 사이사이에 자식 노드들이 연결된다고 생각하면 된다.</p>
<p>탐색의 경우에는 탐색하고자하는 값을 root에서 시작해서 하향식으로 탐색해나간다. 정렬이 키값을 기준으로 되어있는데 이점이 중요하다!</p>
<p>테이블에서 값을 읽어올 때 페이지 단위로 읽어오게 되어있다. 이때, 페이지를 찾기 위해서는 pk를 알아야한다. 인덱스는 페이지 단위로 저장되게 되는ㄴ데 키를 바탕으로 항상 정렬된 상태를 유지한다.</p>
<p>데이터베이스에서는 pk를 먼저 찾고 pk를 레코드를 pk를 통해서 찾게된다. pk를 알기 위해서 순차적으로 탐색하는 방법을 사용한다면 시간이 오래걸릴 것이다. 반면에, 인덱스를 사용하면 pk를 찾을 때 수월하게 찾을 수 있다. 인덱스는 트리구조이고 정렬된 데이터를 가지고 있다. 찾으려는 데이터를 찾기 위해서 가지치기를 해나갈 것이다. 이것은 당연히 순차적으로 모든 데이터를 훑으며 pk를 찾는 과정보다 시간이 적게 걸려서 효율적일 것이다.</p>
<ul>
<li>어떤 index를 사용해야할까?</li>
</ul>
<p>구분을 높여주는 index를 사용할수록 좋을 것이다.
카디널리티가 높을수록 구분을 잘 해준다고 생각하면 되는데 그 속성을 index로 설정하면 좋다!</p>
<ul>
<li><p>index가 있다고 무조건 좋을까?
데이터도 별로 없는데 index를 넣었다. 그리고 조회보다 update 작업이 많이 이루어진다면? index도 역시 memory차지를 하게 될 것이다. 그리고 update를 할때마다 index가 불러와지게 된다. 그렇다면 데이터는 무거워지고 index는 잘 사용되지 않는다...이것은 가장 처음에 말했던 쿼리 튜닝이 필요한 이유중에 하나를 만들게 될 것이다. </p>
</li>
<li><p>postgreSQL과 index
postgreSQL에서는 pk를 생성하면 pk로 index를 생성해준다.
내가 임의로 test DB를 만들고 chapter라는 table에 데이터를 몇개 넣어봤다.
<img src="https://velog.velcdn.com/images/sujin-create/post/e0412319-8280-48f4-bdb4-846365fdc0f9/image.png" alt="">
pg_indexes를 통해서 index가 해당 테이블에 존재하는지 체크해보니, 존재하는 것을 확인할 수 있었다.
추가) 추가로 부분 index를 사용할 수 있다.
부분 index란, 원하는 부분에만 index를 사용하겠다는 것이다. 데이터가 테이블의 1%여도 엄청 많고 조회할일이 많다고 해보자. 1%가 과자이고 99%가 아이스크림일 때 사람들이 여름이라 아이스크림은 먹지 않는다고 해보자. 그렇다면 99%에는 인덱스를 걸 필요가 없을 것이다. 많은 데이터라고 무조건 인덱스가 좋은게 아니니까 그렇다. 반면에 1%라도 데이터가 많고 지속적인 조회가 일어난다면 index를 부분적으로 &#39;과자일때&#39;라는 조건을 where절에 함께 사용하면 될 것이다.</p>
</li>
<li><p>postgreSQL과 explain
explain &lt;&lt;쿼리&gt;&gt; 를 작성하면 해당 쿼리에 대한 query plan을 볼 수 있다.
쿼리플랜이란 postgreSQL에서 이 쿼리는 어떤 알고리즘을 사용해야 가장 효율적인 것인지 판단하고 그 것을 사용할 계획을 한다는 것이다.
<img src="https://velog.velcdn.com/images/sujin-create/post/152b87cc-3104-4dd6-80d5-4fec1fc0efb5/image.png" alt="">
seq scan을 사용한다는 것은 full scan을 하겠다는 것이다. 만약 나중에 규모가 커졌을 때도 seq scan을 사용한다면? index를 사용해서 쿼리 성능 튜닝을 해야겠다고 생각하였다!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] graphql+kotlin+spring에서 GCS를 활용한 multiple image uploader 생성기]]></title>
            <link>https://velog.io/@sujin-create/spring-graphqlkotlinspring%EC%97%90%EC%84%9C-GCS%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-multiple-image-uploader-%EC%83%9D%EC%84%B1%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/spring-graphqlkotlinspring%EC%97%90%EC%84%9C-GCS%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-multiple-image-uploader-%EC%83%9D%EC%84%B1%EA%B8%B0</guid>
            <pubDate>Sat, 01 Jul 2023 04:27:59 GMT</pubDate>
            <description><![CDATA[<p>이번주는 일주일동안 GCS를 spring에 연동하고 사용할 api를 만들어봤는데, 알고 시작했으면 좋았을 꿀팁들에 대해서 공유하려고 합니다.!!!
특히 graphql을 사용하며 postman으로 협업 혹은 test를 진행하고 있는 분들에게 도움이 될 거라고 믿습니다!! (사실 조회수는 0에 가깝지만,,,공유는 합니다^^)</p>
<h2 id="0-intro">0. Intro</h2>
<p>회사에서 CKeditor를 사용하는데 이때, CKeditor에서 image를 upload할 때 plugin은 file loader 객체를 생성한다.
upload adapter를 이용해서 파일을 읽어서 서버에 업로드를 해주는데 이때 이 서버!! 어떻게 해야할까?</p>
<p>CKeditor 유로버전을 사용하면 storage를 제공한다. 그러나 너<del>~</del>무 비싸다. out이다.</p>
<p>cloud storage service를 사용하기로한다.
회사에서는 google cloud platform을 사용하고 있기에 GCS를 사용하기로 했다!</p>
<p>그러면 나는 무엇을하면 될까?
GCS를 WAS와 연동하고 Ckeditor에서 필요한 요청인 &#39;다중 이미지 파일 업로드&#39; api와 delete 그리고 image를 에디터를 통해 작성한 페이지 entity에 연결 시켜주면 된다.</p>
<p>그 과정에서 겪었던 시행착오들을 공유하려고한다.</p>
<h2 id="1-gcs-iam-설정-및-spring-storage-bean-등록">1. GCS IAM 설정 및 spring storage bean 등록</h2>
<h3 id="iam-설정">IAM 설정</h3>
<p>우선 GCS에 들어가서 bucket을 생성하고 image를 upload하거나 download할 준비가 완료가 됐다고 
하자</p>
<p><a href="https://cloud.google.com/storage/docs/access-control/iam-permissions?hl=ko">GCS 권한 참고용</a> 여기를 들어가보면 권한이 많고 관리자에게는 업로드 다운로드 등등의 권한을 주고 사용자에게는 볼 수만 있도록 하려고 해서 IAM을 사용하면서 signed url을 생성해야겠다고 생각했다.</p>
<p><strong>signed url</strong> 이란 notion이나 google colab 등에서 링크공유를 했을 때 링크를 가진 사용자에게 권한을 부여하는데 권한을 사인해둔 링크라고 생각하면 된다.</p>
<p>그래서 IAM을 생성해서 project의 생성해놓은 storage의 bucket을 연결시켰다.</p>
<ul>
<li><p>key는 json으로 받아놓고 spring에서 bean을 등록할 때 사용해야한다.</p>
</li>
<li><p>IAM을 사용하여 bean을 등록하면? IAM의 role의 역할을 소유자만이 해당 Url에 접근, 생성 등의 권한을 가지고 있게 된다.</p>
</li>
<li><p>signed url을 사용하여 editor의 내용을 누구나 볼 수 있도로 해줘야한다.</p>
</li>
</ul>
<h3 id="spring-bean-등록">spring bean 등록</h3>
<p><strong>종속성 부여</strong></p>
<pre><code>implementation(&quot;com.google.cloud:spring-cloud-gcp-starter-storage:3.1.0&quot;)</code></pre><p>버전...안 넣어어주고 build하는 과정에서 30분 날렸다. (저처럼 마세요~ ㅎㅎ)</p>
<ul>
<li>위에서 받아놓은 json file을 활용해 등록</li>
</ul>
<p>나의 경우에는 properties를 만들어서 이후 계정이나 프로젝튿 등등 쉽게 바꾸게 하기 위해 환경변수를 yaml로 관리해줬다.</p>
<p>이부분은 편한대로 진행하면 되겠다!!</p>
<pre><code class="language-yqml">cloud:
  gcs:
    classPath: [~~.json]
    projectId: [~~~]
    bucketName: [~~~}</code></pre>
<p>이렇게 넣어주면 스스로 storage 객체를 사용할 때 injection된댔는데......안됐다!!
또 30분 날렸다~ 편하게 하려고 하지 말고 안 될 땐 그냥 직접 bean을 만들자!</p>
<p>그러면 그냥 직접 등록해주면 된다!</p>
<p><strong>bean등록</strong></p>
<p>나는 config folder에 properties를 <code>EnableConfigurationProperties</code>를 사용해서 읽어주고 등록해줬다.</p>
<p>코드는 google cloud storage guied를 보면 예제가 잘 나와있어서 <a href="https://cloud.google.com/storage/docs/uploading-objects#storage-upload-object-java">여기</a> 참고하면 된다. java code를 제공해서 참고해서 kotlin으로 바꿨다!!</p>
<p>같이 봐보자!</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/010b396e-c650-454f-80ad-5cdb3f21b1fd/image.png" alt=""></p>
<p>위의 GCS guied의 캡처본이다! 주석된 부분을 나는 properties에 정의해서 읽어줬고 이거는 upload 기능에 대한 것이기에 storage 등록 아래의 내용부터는 (BlobId등록) api마다 다르기에 service에서 bean injection할 대상을인 storage 등록까지만 참고하였다. 
(물론 아래의 내용은 api를 만들때 참고!)</p>
<pre><code>@Bean
fun storage(properties: GcsProperties): Storage {
        val classPathResource = ClassPathResource(properties.classPath);
        val googleCredentials = GoogleCredentials.fromStream(classPathResource.inputStream);
        val projectId = properties.projectId
        return StorageOptions.newBuilder()
            .setProjectId(projectId)
            .setCredentials(googleCredentials)
            .build()
            .service;
}
</code></pre><p>그래서 나는 이렇게 등록해줬다.</p>
<p>자 그럼 service를 만들 준비가 다 됐다!!
그렇다면 service를 만들고 fetcher까지 api를 만드는 과정을 함께 봐보자!</p>
<h2 id="2-service---upload-부분">2. Service - upload 부분</h2>
<p>delete는 <a href="https://cloud.google.com/storage/docs/deleting-objects">공식문서</a> 따라서 그냥 해주면 된다!</p>
<p>upload부분도 사실 따라서 하면 된다. 한가지 공유하고 싶은 부분이 있다면 signed url이다.</p>
<p>이것도 <a href="https://cloud.google.com/storage/docs/access-control/signed-urls">여기</a> 참고하였고 signed url을 생성하는 방법은 3가지가 존재하는데 HMAC은 AWS를 사용하지 않아서 pass하였고 V2,V4중에서 아래 예제가 있는 V4를 선택했다.</p>
<p>공식문서에서는</p>
<pre><code>// Define resource
    BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(bucketName, objectName)).build();

    URL url =
        storage.signUrl(blobInfo, 15, TimeUnit.MINUTES, Storage.SignUrlOption.withV4Signature());
</code></pre><p>이렇게 나와있다.</p>
<pre><code>fun generateV4GetObjectSignedUrl(name: String): String = storage.signUrl(
        BlobInfo.newBuilder(BlobId.of(properties.bucketName, name))
            .build(),
        properties.duration,
        TimeUnit.MINUTES,
        Storage.SignUrlOption.withV4Signature()
    ).toString()
</code></pre><p>이렇게 만들어줬다. 그리고 transactional이 걸린 upload method에서 signed url을 생성하는데 사용하였고 IAM으로 접근이 차단된 url역시 해당 upload method에서 만들어줘서 결과적으로 signed url과 iam 소유자가 사용할 수 있는 url을 제공해주도록 하였다.</p>
<p>이때, editor 페이지의 pk안에 여러개의 image가 들어갈 수 있기 때문에 Image entity의 경우에는 페이지 entity와 다대일 관계를 가지기 때문에 페이지의 pk값을 받아서 그 안에 multiple image를 모두 넣어주었다. 이러한 비즈니스 로직은 원하는 서비스가 있다면 거기에 맞춰서 작성하면 될 것이다!</p>
<p><strong>multiple uploader</strong></p>
<p>MultipartFile list를 받아서 foreach를 사용해서 하나씩 save해줬다!
물론 page Entity의 pk를 함께 넣어줬다!</p>
<h2 id="3-fetcher---multipartfile-parsing부분">3. Fetcher - Multipartfile parsing부분</h2>
<h3 id="dgsdata와-datafetchingenviroment을-사용">DgsData와 DataFetchingEnviroment을 사용</h3>
<ul>
<li>graphql을 사용할 때 multipartfile은 기존 restapi를 사용할 때처럼 multipartfile로 그냥 받을 수 없다.</li>
</ul>
<p>Jackson object mapper를 deserialize가 불가능하다. 그렇기때문에 그냥 받을수 없다. 따라서, 파일 인수를 이용해서 명시적으로 가져와야한다.</p>
<p>그렇다면, <strong>DgsData와 DataFetchingEnviroment</strong>를 사용해야할 것이다.
dfe를 사용해 getArgument로 우리가 넘긴 인수를 받으면 된다.</p>
<p>참고한 사이트를 기반으로 설명을 하겠다!! <a href="https://netflix.github.io/dgs/advanced/file-uploads/">여기</a>에 들어가면 더 자세히 알 수 있다.</p>
<p>아래코드는 netflix grapqhl의 공식문서를 가져온 것이다.</p>
<pre><code>@DgsData(parentType = DgsConstants.MUTATION.TYPE_NAME, field = &quot;uploadScriptWithMultipartPOST&quot;)
    public boolean uploadScript(DataFetchingEnvironment dfe) throws IOException {
        // NOTE: Cannot use @InputArgument  or Object Mapper to convert to class, because MultipartFile cannot be
        // deserialized
        MultipartFile file = dfe.getArgument(&quot;input&quot;);
        String content = new String(file.getBytes());
        return ! content.isEmpty();
    }
</code></pre><p>그렇다면 이때, 스키마 설계는 어떻게 해야할까?</p>
<p>File이라는 scheme는 존재하지 않는다. <code>Upload</code> 스키마를 사용해야한다.</p>
<p>아래의 코드 역시 공식문서를 가져온것이다.</p>
<pre><code>scalar Upload

extend type Mutation  {
    uploadScriptWithMultipartPOST(input: Upload!): Boolean
}</code></pre><h3 id="single이-아닌-multiple-생성">single이 아닌 multiple 생성</h3>
<p>그러면 위의 코드가 딱 보면 하나만 처리한다는 것을 알 수 있는데 여러개의 <code>Upload</code> 입력을 보내고 이를 받아서 처리하는 코드로만 변경하면 된다.</p>
<pre><code>
scalar Upload

extend type Mutation  {
    uploadScriptWithMultipartPOST(input: [Upload!]!): Boolean
}
</code></pre><p>이렇게 하면 될 것이다!
물론 Boolean이 나오지 않고 url이 나오도록 하려면 entity를 스키마에 타입을 설정해주고 그것을 반환해주면 될 것이다~</p>
<h3 id="argument-추가하기">Argument 추가하기</h3>
<p>역시 예제 기반으로 보여주겠다!
만약에 title을 같이 보내고 싶다면?</p>
<pre><code>scalar Upload

extend type Mutation  {
    uploadScriptWithMultipartPOST(title: String!, input: [Upload!]!): Boolean
}
</code></pre><p>이런식으로 변경하고 fetcher도 이에 맞게 수정해줘야하는데 위의 fetcher 코드에서 어떤 부분이 추가 되어야할까?</p>
<p><code>InputArgument</code> annotation을 사용해서 데이터를 읽어오는게 필요할 것이다! arg를 읽어오면 되고 이를 service에 함께 넘겨서 원하는 로직을 구현하면 된다.</p>
<h2 id="4-postman에서-사용하기">4. postman에서 사용하기</h2>
<h3 id="header">header</h3>
<p>content-type을 multipart-form data 는 simple request이기에 cors error(CSRF error)를 동반한다..
따라서</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/f5ff3605-cb93-4796-81d7-3371ba7e4a0d/image.png" alt=""></p>
<p>위의 사진처럼 header에 등록해주자!!</p>
<h3 id="body">body</h3>
<p>postma에서 file upload test를 진행하기 위해서는 form-data를 이용해야한다.</p>
<p>이거는 30분이 아니라 하루 이틀 날려먹었다~~</p>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/ea4e55c5-b88c-4d13-b022-4e4c65a0e3b6/image.png" alt="">
나는 mutation이름을 uploadLocalImage로 해서 mutation이름이 저렇다!</p>
<pre><code>
{ &quot;query&quot;: &quot;mutation uploadScriptWithMultipartPOST($title: String, $input: [Upload!]!) { 
    uploadScriptWithMultipartPOST(title: $title, input: $input){ 
    url\n signedUrl }\n}&quot;,
    &quot;variables&quot;: {&quot;title&quot;: &quot;title&quot; , &quot;input&quot;: [null, null] } }</code></pre><p>잘 보이게 띄어쓰기를 해놨는데 string을 operation의 value에 넣어줘야한다.</p>
<p><strong>inputArgument</strong> (이것도 map에 넣어주는걸로 당연히 생각했지만 그게 아니였다...)</p>
<p>이거는 map으로 넣지 않고 operation의 string값 안에 바로 직접 넣어준다.
따라서 variables안에 &quot;title&quot;안에 &quot;title&quot;이 들어있는 이유다. 만약 &quot;title2&quot;를 넣어주고 싶다면 &quot;title&quot;: &quot;title2&quot;를 하면 되겠죠??
<strong>File전송</strong></p>
<p>map을 사용해야한다. 위의 operation에서 &quot;image&quot; : [null, null]이다
2개를 넣을 수 있는데 map value를</p>
<p><strong>{&quot;1&quot;: [&quot;variables.input.0&quot;], &quot;2&quot;: [&quot;variables.input.1&quot;]}</strong>
다음과 같이 넣어준다.
그러면  null첫번째에 1번 key에 저장된 value인 image.png 파일을 매핑해주고 null 2번째에는 key 2에 해당하는 value에 저장된 file을 매핑해준다.</p>
<p><strong>참고</strong></p>
<p>그래도 했는데 잘 안 된다면 다른 request를 만들어서 다시 해봐라! 나도 중간중간에 뭐가 꼬였는지 안 된적이있었는데 그대로 다음날 다른 request에 복붙해보니 됐다...!!</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>많은 내용을 한번에 공유하기 위해서 간단한 예제로 작성을 해봤다!
이번에 느낀거는 공식 문서는 생각보다 이런 예제까지 있어?하는 것까지...많은 것을 알려준다!</p>
<p>뭔가 안 된다하면 구글링해서 스택오버플로우를 들어가는 것보다 공식 github 이라던가 page를 들어가서 먼저 조사를 해보자!!를 느꼈다 </p>
<p>그리고 postman은 진짜 잘 안 나오더라...특히 file이랑 title과 같은 다른 인자를 같이 전달하는 방법은 결국 찾지 못했고 내가 이것저것 차례대로 해보다가 유레카~했다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] You have an error in your SQL syntax; check the manual thatcorresponds to your MYSQL server version for the right syntax to use near 해결]]></title>
            <link>https://velog.io/@sujin-create/spring-You-have-an-error-in-your-SQL-syntax-check-the-manual-thatcorresponds-to-your-MYSQL-server-version-for-the-right-syntax-to-use-near-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@sujin-create/spring-You-have-an-error-in-your-SQL-syntax-check-the-manual-thatcorresponds-to-your-MYSQL-server-version-for-the-right-syntax-to-use-near-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 21 Jun 2023 14:31:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>You have an error in your SQL syntax; check the manual thatcorresponds to your MYSQL server version for the right syntax to use near</p>
</blockquote>
<p>아무리봐도 syntax 에러를 발생시킬 속성이 존재하지 않았다.
에러 로그 그대로 구글링을 진행해봤을 때 대부분 예약어때문에 발생한다고 나와있었다.</p>
<pre><code> @Id @GeneratedValue
    private Long id;
    private String title;
    private String memo;
    private String key;
    private QrStatus qrStatus;

    @Embedded
    Address address;
    @Embedded
    PhoneNumber phoneNumber;
    @Column(name=&quot;url&quot;, columnDefinition = &quot;MEDIUMBLOB&quot;)
    private String url;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;


    private LocalDateTime createDate;
    private LocalDateTime updateDate;


    ...
</code></pre><p>도대체 어디서 나는 에러란 말인가... 생각을 해봤는데 이전에는 잘 돌아갔는데 지금 안 되는거니까 새롭게 추가한 key, qrStatus 둘중 하나로 추려졌다!
ㅎㅎㅎㅎ 그렇다면 enum type인 qrStatus는 아닐테고 당연히 key!
근데 Key가 당연히 예약어가 아닌줄 알았다!</p>
<p>그래서 뻘짓이 시작되었다~
예약어 오류가 아니라 ddl을 create로 해놨는데 이것때문에 외래키에서 걸려서 alter table을 할 때 테이블을 찾지 못하는 에러인가 생각을 해봤다. 로그를 보니 alter table하고 create를 진행하는데 drop도 아니고 create라서 외래키 사이에서도 문제가 되어보이지 않았다.</p>
<p>그래서 혹시 key가 설마 예약어인가 찾아봤다.</p>
<h3 id="wow-key는-예약어">wow! key는 예약어</h3>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/5beba84d-6401-4df1-b83a-1c022c22139d/image.png" alt=""></p>
<pre><code> private String qrcodeKey;</code></pre><p>로 냅다 바꾸고 spring data jpa에서 key를 사용해서 findQrcode를 진행중이였기에 findQrcodeByQrcodeKey로 바꾸고 다시 돌리니가 정상적으로 돌아갔다</p>
<h3 id="느낀점">느낀점</h3>
<p>혹시나가 역시나...!
내가 생각한대로 지레짐작 하지 말자,,,,^^ 일단 코드로 돌려보자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[spring] soft delete와 hard delete에 대해서]]></title>
            <link>https://velog.io/@sujin-create/spring-soft-delete%EC%99%80-hard-delete%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@sujin-create/spring-soft-delete%EC%99%80-hard-delete%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Mon, 19 Jun 2023 10:09:55 GMT</pubDate>
            <description><![CDATA[<p>만약에 사용자가 삭제한 데이터를 다시 필요로한다면? 혹은 관리자가 될 수도 있을 것이다.</p>
<p>사실 이러한 것까지 프로젝트를 진행하면서 생각해보지 못했다. database를 공부할 때 데이터 삭제는 delete문을 사용해서 바로 삭제해버리는 hard delete에 익숙해졌기 때문이다.</p>
<p>그렇지만, 최근 soft delete에 대해서 알아보게 되었고 왜 사용하고 어떻게 적용할 수 있는지에 대해서 조사를 해봤다!
그래서 조사한 것들에 대해서 오늘은 soft delete와 hard delete에 대해서 공유하려고한다.
그리고 soft delete를 적용할 때 함께 사용할 수 있는 annotation을 알아보도록하자!</p>
<h2 id="0-soft-delete란-무엇일까">0. soft delete란 무엇일까</h2>
<p><img src="https://velog.velcdn.com/images/sujin-create/post/243f66f8-a9eb-4d7f-aaef-0c10fed7448f/image.png" alt="">
출처: <a href="https://www.orchidsinternationalschool.com/learning-key-concepts/grade-1/english/find-the-opposite-word/">이곳</a></p>
<p>귀여워서 가져와봤다 ㅎㅎ^^</p>
<p>앞서 말한것에서 힌트를 받았을 수 있지만, soft delete는 언제 사용하게 되는 것일까?
사용자가 삭제한 데이터는 실제로 남아있지만 삭제되었다고 느끼게끔 하는 로직이 필요할 때 soft delete를 사용한다.
그렇다면 어떻게 그게 가능할까?
우선 delete를 시켜버리면 안 된다는 것은 당연하다. 그리고 이게 삭제된 것인지 삭제되지 않은 것인지 구별해줄 필요가 있는데 구별해줄 무언가는 필요할 것이다. 따라서 삭제된 것인지 삭제되지 않은 것인지 구별하기 위해서 boolean값을 가지는 속성을 하나 만들어서 체크하면 될 것이다.</p>
<p>예를 들어서 삭제됐을 때 deleted라는 속성을 true로 하고 만약 삭제 되지 않았을 때 false로 하겠다고 했으면, 사용자에게 보여주는 정보는 deleted가 false인 데이터들을 보여주면 될 것이다.</p>
<p>그렇다면 soft delete를 왜하는 것일까?
사용자가 삭제를 하더라도 그 정보가 통계정보에 유용하다거나 이후에 사용가능성이 있다면 남겨두는 것이 좋을 것이다. 이외에도 데이터가 돈인 시대이기에 장점이 많을 것이다. </p>
<h2 id="1-soft-delete-적용-방법">1. soft delete 적용 방법</h2>
<ul>
<li><p><code>update</code>
soft delete를 적용하기 위해서는 위에서 말했듯이 하나의 속성을 지정하고 그 속성을 <code>update</code>하는 방식으로 진행하면 된다.</p>
</li>
<li><p>논리삭제</p>
<blockquote>
<p>실제로 DB에서 삭제된 것이 아니고 사용자는 deleted가 안 된 것들만 볼 수 있기에 삭제 되었다고 느끼게 한다.</p>
</blockquote>
</li>
</ul>
<p>논리삭제를 사용하기 때문에 이때 활용할 <code>column</code>을 하나 생성하면 된다.</p>
<h2 id="2-spring에서-soft-delete구현하기">2. Spring에서 soft delete구현하기</h2>
<h3 id="sqldelete"><code>@SQLDelete</code></h3>
<blockquote>
<p>Custom SQL statement for delete of an entity/collection.으로 소개되고 있다.</p>
</blockquote>
<p>해당 annotation을 사용하면 entity나 collection에 대해서 delete 상태를 custom할 수 있다.
sql 구문을 넣을 수 있는데 이때 sql에 update를 사용해서 entity의 특정 속성 값을 true로 update해주는 로직이 delete가 적용됐을 때 내부에서 동작하게 된다.
그렇다면 우리는 hard delete가 아니라 soft delete를 적용하게 될 것이다.</p>
<p>이렇게 적용했다면 우리는 속성값이 false인 것만 사용자에게 보여줘서 논리삭제 구현을 마무리해야한다.</p>
<h3 id="where"><code>@Where</code></h3>
<blockquote>
<p>Specifies a restriction written in native SQL to add to the generated SQL when querying an entity or collection.
For example, @Where(&quot;deleted = false&quot;) could be used to hide entity instances which have been soft-deleted. 라고 설명에 나와있다.</p>
</blockquote>
<p>@Where를 사용하면 일반적인 코드에서 where를 사용하듯이 조건을 걸 수 있는데 <code>clause</code>에 조건을 string으로 넣어줄 수 있다. 
위에서 예시를 든것을 가져와서 사용한다고 하면, deleted가 false인 조건을 넣어주면 될 것이다.
그렇다면, 우리가 jpa, querydsl을 사용할 때 조건을 걸어주지 않더라도 해당 annotation이 적용된 entity의 경우에는 무조건 deleted가 false인 것들만 사용하게 된다.</p>
<h3 id="class에-적용하기">class에 적용하기</h3>
<p>해당 두가지 annotation을 사용해서 만들고자하는 entity에 적용을 하면 soft delete를 구현할 수 있다. </p>
<h2 id="마무리">마무리</h2>
<p>한가지 의문이 들 수 있다. 왜 Filter가 아니라 where annotation을 굳이 내가 언급했을까?
사실 filter를 사용해도 될텐데 말이다!!</p>
<p>다음 포스트에 적어보도록 하겠다~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[graphql] @DgsRequestData를 사용하여 HTTP 요청 개체 가져오고 다루기]]></title>
            <link>https://velog.io/@sujin-create/graphql-DgsRequestData%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-HTTP-%EC%9A%94%EC%B2%AD-%EA%B0%9C%EC%B2%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B3%A0-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/graphql-DgsRequestData%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-HTTP-%EC%9A%94%EC%B2%AD-%EA%B0%9C%EC%B2%B4-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B3%A0-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Wed, 14 Jun 2023 10:26:52 GMT</pubDate>
            <description><![CDATA[<h3 id="0-intro">0. Intro</h3>
<p>이전 post에서 context를 중심으로 설명을 진행하였다. 잠깐 언급을 진행하였는데 header에 대한 값을 가져올 수 있다고 했다.</p>
<p>그렇게 header에 대한 값을 가져오려면 무엇을 사용해야한다고 했을까?
<code>DgsDataFetchingEnvironment</code> 를 사용해서 가져오면 된다.</p>
<p>이렇게 해서 requestData를 가져올 수 있다고 해보자. 해당 requestData는 WebMVC를 사용하는지 WebFlux를 사용하는지에 따라서 다르게 typeCasting을 적용해줘야한다.</p>
<p>그렇다면 어떻게 casting을 하여 우리가 원하는 header값을 불러올 수 있을지 알아보도록하자!!</p>
<blockquote>
<p>해당 포스트는 <a href="https://netflix.github.io/dgs/datafetching/">https://netflix.github.io/dgs/datafetching/</a> 를 기반으로 작성하였으며 자세한 내용 확인이 가능하다.</p>
</blockquote>
<h3 id="1-사전지식">1. 사전지식</h3>
<p>캐스팅을 해줘야하는데 WebMVC, WebFlux에 대해서 알아야 가능하다. 아래에서 간단히 설명을 하려고한다!</p>
<ul>
<li>spring WebMVC</li>
</ul>
<p>동기적으로 작동되는 블로킹 방식이다.</p>
<p>동기, 비동기 그리고 블로킹과 논블로킹에 대해서 궁금한 사람들은 이전의 OS Posting에서 찾아볼 수 있을 것이다.</p>
<p>thread pool을 생성해놓고 사용자의 요청이 들어오면 1개가 처리될 동안 1개의 thread가 할당되어 처리가 완료될 때까지 다른 요청을 처리하기 위해서 사용되지 않는다.
이때 중요한 것이 무엇일까? thread pool size를 적절히 조정하는 것이다.
하지만 많은 사용자의 요청을 대량으로 받아내는데에는 한계가 존재하는데 이를 WebFlux가 해결해줄 수 있다고 말한다.</p>
<ul>
<li>spring WebFlux</li>
</ul>
<p>Node.js와 같이 반응형 프로그래밍을 사용하며 모든 코드는 non-blocking되어있지 않아서 하나의 요청이 끝나지 않더라도 다른 요청을 처리할 수 있다는 것이다.</p>
<ul>
<li>추가
최근에는 kotlin으로 개발하기 위해서 kotlin을 공부하고 있는데 java에서는 제공하지 않는 Coroutine을 제공한다. 이는 thread를 전환하지 않고도 Non-Blocking이 되어있기에 엄청난 속도로 왔다갔다 하며 작업을 처리해주기에 동시에 진행이 되는 것처럼 보인다고 한다.</li>
</ul>
<h3 id="2-dgsrequestdata">2. DgsRequestData</h3>
<p><code>DgsRequestData</code>를 사용해서 request data를 가져올 수 있는데 이는 위에서 언급했듯이 어떤 환경에서 가져오는것이냐에 따라서 casting이 달라진게된다.</p>
<ol>
<li>DgsWebMvcRequestData
MVC 기반의 requestData를 가져올 경우에는 <code>DgsWebMvcRequestData</code>로 type casting을 진행해야한다.<pre><code class="language-kotlin"></code></pre>
</li>
</ol>
<p>val reqeustData = dfe.getDgsContext().requestData as DgsWebMvcRequestData</p>
<pre><code>위와 같이 사용한다면 `DgsWebMvcRequestData`로 타입캐스팅을 진행한 것이다.

2. DgsReactiveRequestData
Webflux 기반의 requestData로 type casting을 진행하는 경우에는 `DgsReactiveRequestData`로 type casting을 진행한다.
```kotlin

val reqeustData = dfe.getDgsContext().requestData as DgsReactiveRequestData
</code></pre><p>이렇게 requestData를 가져왔다고 하자.</p>
<p>이 다음으로 각각에 맞게 requestData를 처리해주면 된다.</p>
<h3 id="3-cookie넣기">3. cookie넣기</h3>
<p><code>DgsWebMvcRequestData</code>로 casting을 진행했다고 해보자!</p>
<p>webRequest를 ServletWebRequest로 형변환을 진행하고 webRequest의 response에 cookie를 넣을 수 있게된다!</p>
<pre><code>val webRequest = requestData.webRequest as ServletWebRequest
</code></pre><p>다음과 같이 ServletWebRequest로 변환을 진행한다.</p>
<p>그렇다면 <code>ServletRequestAttributes</code>에서 제공하는 <code>getResponse</code> method를 사용할 수 있을 것이고 response에 cookie를 addCookie 매서드를 활용해서 넣으면 될 것이다!!</p>
<h3 id="마무리">마무리</h3>
<p>spring, java, restapi의 조합으로 프로젝트를 진행했을 때는 HttpServeltRequest에서 바로 getRequest를 진행했는데 spring, kotlin, graphql의 조합으로 공부를 진행하고 있는데 로직에 있어서 차이점들이 점점 많이 보이는 것 같다!
그래도 결국 servlet 기반의 http response, request를 사용한다는 점에서는 같으니 이해하기는 좋은 것 같다 :) </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[graphql] api fetcherContext 를 활용해 사용한 불필요한 traffic 줄이기]]></title>
            <link>https://velog.io/@sujin-create/graphql-Graqphl%EC%9D%98-api-fetcher%EC%97%90%EC%84%9C-Context-%EC%A0%95%EB%B3%B4-%ED%99%9C%EC%9A%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-traffic-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
            <guid>https://velog.io/@sujin-create/graphql-Graqphl%EC%9D%98-api-fetcher%EC%97%90%EC%84%9C-Context-%EC%A0%95%EB%B3%B4-%ED%99%9C%EC%9A%A9%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-traffic-%EC%A4%84%EC%9D%B4%EA%B8%B0</guid>
            <pubDate>Wed, 14 Jun 2023 09:43:38 GMT</pubDate>
            <description><![CDATA[<p>netflix grapqhl에 대해서 알아보면서 알아둬야할 점인 context 정보 활용하는 방법에 대해서 공유하려고한다.
context를 잘 활용한다면 api에서 불필요한 데이터들에 대한 query traffic이 발생하지 않을 것이다.</p>
<h3 id="0-intro">0. Intro</h3>
<p>java spring으로 예시를 들자면, HttpServeltRequest를 사용하며 RequestContextHolder를 사용해서 지금까지의 api에 담긴 내용에서 필요한 정보들을 가져와서 사용할 것이다.</p>
<p>그렇다면 graphql에서는 특히 netflix graphql에서는 어떻게 사용할 수 있을까?</p>
<p>netflix graphql에서 지금까지의 context 정보를 활용하기 위해서 <code>DataFetchingEnvironment</code>를 상속받아서 만들어진 <code>DgsDataFetchingEnvironment</code>를 사용할 수 있다.</p>
<p>그렇다면 <code>DgsDataFetchingEnvironment</code>는 어떤 기능을 제공하고 어떨때 사용하는 것일까?</p>
<h3 id="1-graphql-context">1. GraphQL Context</h3>
<p>먼저 Graphql Context 자체에 대한 이해가 필요할 것이다.</p>
<p>GraphqlContext는 승이노딘 컨텍스트 메커니즘으로 key-value쌍으로 적절한 context를 전달하고 framework 및 사용자 공간 코드 서로 &quot;독립적&quot;으로 적절히 사용하여 문맥정보를 얻고, 공유 및 활용할 수 있도록 하기 위해서 제공된다.</p>
<blockquote>
<p><a href="https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/GraphQLContext.java">https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/GraphQLContext.java</a> 해당 링크를 통해서 GraphqlContext에 대한 source code를 확인할 수 있다.</p>
</blockquote>
<p>어떤 기능을 제공하는지는 알겠는데, 어떻게 제공이 가능한 것일까에 대한 의문이 생길 것이다.</p>
<p>application의 흐름이 담긴 context가 각 메서드에서마다 어떻게 사용될 수 있는 것일까일반 계측 클래스가 호출되기 전에 DguRequestData를 활용해서 제공된 모든 항목을 검사하고 제공된 GraphQLContext.Builder를 통해서 GrapqhlContext에 값을 설정하는 과정을 거치기 때문에 문맥 정보가 기록되는 것이다. 
또한 우리는 이러한 문맥정보를 활용함으로써 request에 대한 header값과 같은 필요한 정보들을 확보할 수 있는 것이다.</p>
<p>context를 사용하는 방법에 대해서 알아보자.</p>
<h3 id="2-data-fetching-context">2. Data Fetching Context</h3>
<p>그렇다면 문맥정보를 어떻게 활용할 수 있을까?</p>
<p><code>DataFetchingEnvironment.getContext()</code>
를 사용함으로써 Context 정보를 얻어올 수 있다.</p>
<p>가장 대표적인 예시로는, 해당 문맥정보는 로그인을 할 때 사용할 수 있을 것이다.
로그인을 하고 발생한 JWT token을 cookie에 저장하고 싶은 경우를 생각해보면, cookie에 데이터를 넣기 위해서는 request를 먼저 불러와야하고 해당 요청이 들어온 request를 불러와야한다.
이때, context를 사용할 수 있을 것이다.</p>
<p>위의 경우가 아니라, 아래의 경우도 생각해보자!! 이는 앞서 언급했듯이 불필요한 트래픽을 없애주는데 도움을 줄 것이기에 알아두면 좋겠다!!</p>
<p>예를 들어서 아래와 같은 스키마가 있다고 해보자.</p>
<pre><code class="language-graphql">
type Query {
   shows: [Show]
}

type Show {
  title: String
  actors: [Actor]
}</code></pre>
<p>이때, @DgsQuery를 사용해서 shows 데이터를 얻으려고 한다고 해보자.</p>
<pre><code class="language-graphql">
{
    shows{
        title
    }
}</code></pre>
<p>이때, client가 shows query를 사용해서 title, actors중에서 title만 query를 했다고 해보자. 이때 show 1개에 존재할 수 있는 모든 actors에 대해서 불러오는 traffic이 존재하게 될 것이다.
왜냐하면 사용자가 어떤 값을 쿼리 필드에 포함했는지는 중요하지 않기 때문이다.</p>
<p>그렇다면 title 하나만을 위해 쿼리를 했는데도 actors에 대한 &quot;불필요한&quot; 트래픽이 발생하게 되는 것이다.</p>
<p>이를 방지하기 위해서는 아래와 같이 datafetcher를 분리하면 된다.</p>
<p>우선, Query를 해야하는 api는 당연히 그대로 써줘야할 것이다(사용자가 query를 해야하니까).
그렇다면 그대로 놓는다. 다음으로 <code>DgsDataFetchingEnvironment</code>를 사용하면 된다.
DgsDataFetchingEnvironment에서 현재 context 정보를 가져올 수 있다.</p>
<pre><code class="language-java">@DgsQuery
public List&lt;Show&gt; shows() {

    //Load shows, which doesn&#39;t include &quot;actors&quot;
    return shows;
}

@DgsData(parentType = &quot;Show&quot;, field = &quot;actors&quot;)
public List&lt;Actor&gt; actors(DgsDataFetchingEnvironment dfe) {

   Show show = dfe.getSource();
   actorsService.forShow(show.getId());
   return actors;
}</code></pre>
<p>이렇게 작성을 한다면 dfe를 활용해서 actor가 필요한 경우에만 context에서의 show를 불러와서 제공할 수 있게 된다.
자세한 방법은 아래에서 알아보도록하자!
우선 간단히 context를 사용하는 흐름에 대해서 이해하는 것이 중요할 것이다.</p>
<p>actor의 경우에 schema를 보면, Show type에 존재하는 것을 알 수 있다.
그리고 이것은 show 하나가 있을 때 한번의 actors datafetcher가 실행되어 해당하는 show에 대해서 접근이 가능하다.
이때, n+1 문제가 발생가능한데 이를 위해서 data loader를 사용한다는 것은 이전 포스트를 참고바란다! 지금은 해당 문제 최적화를 고려하지 않는다.</p>
<p>해당 예제를 통해서 사용자가 query를 진행할 때, 요청하지 않는데도 DB에 traffic이 발생하는 경우가 있을 때(title을 쿼리 하지 않는 경우가 아니라 actors와 같은 list를 반환하는 큰 비용이 발생하는 경우)에는 Context를 사용해서 <strong>child Datafetcher</strong>에서 처리를 진행해야함을 알아보았다!</p>
<h3 id="3-dgsdatafetchingenvironment-와-dgsdata-사용해-쿼리-분리하기">3. <code>DgsDataFetchingEnvironment</code> 와 <code>@DgsData</code> 사용해 쿼리 분리하기</h3>
<p>그렇다면 netflix graphql에서는 query를 분리할 때 어떻게 작성해야하나?에 대해서 궁금할 것이다.</p>
<p>사실 위의 예제에서 큰 힌트를 얻을 수 있다.</p>
<h3 id="dgsdatafetchingenvironment">DgsDataFetchingEnvironment</h3>
<p><code>DgsDataFetchingEnvironment</code>를 사용하면 context 정보를 활용할 수 있다.</p>
<pre><code class="language-kotlin">
class DgsDataFetchingEnvironment(private val dfe: DataFetchingEnvironment) : DataFetchingEnvironment by dfe {

    fun getDgsContext(): DgsContext {
        return DgsContext.from(this)
    }

    fun &lt;K, V&gt; getDataLoader(loaderClass: Class&lt;*&gt;): DataLoader&lt;K, V&gt; {
        val annotation = loaderClass.getAnnotation(DgsDataLoader::class.java)
        return if (annotation != null) {
            dfe.getDataLoader(DataLoaderNameUtil.getDataLoaderName(loaderClass, annotation))
        } else {
            val loaders = loaderClass.fields.filter { it.isAnnotationPresent(DgsDataLoader::class.java) }
            if (loaders.size &gt; 1) throw MultipleDataLoadersDefinedException(loaderClass)
            val loaderField: java.lang.reflect.Field = loaders
                .firstOrNull() ?: throw NoDataLoaderFoundException(loaderClass)
            val theAnnotation = loaderField.getAnnotation(DgsDataLoader::class.java)
            val loaderName = theAnnotation.name
            dfe.getDataLoader(loaderName)
        }
    }
}
</code></pre>
<p>코드를 보면 다음과 같이 작성이 되어있다. 이것은 <code>DataFetchingEnvironment</code>를 상속받아서 생성됐음을 알 수 있는데, DataFetchingEnvironment의 경우에는 getSource, getArguments, getContext 등등의 메서드를 제공한다.</p>
<p><code>DgsDataFetchingEnvironment</code> 는 context에 접근을 하도록 해준다.</p>
<ol>
<li>query itself -&gt; query 그 자체에 대한 값</li>
<li>data loaders</li>
<li>source Object -&gt; 필드 정보를 포함하는 객체</li>
</ol>
<p>등등을 제공해준다.</p>
<p>그렇다면 이를 활용해서 문맥정보를 얻고 문맥 정보안의 필요한 데이터를 사용하면 될 것이다.</p>
<p>다시 그러면 위의 예제를 살펴보자!!</p>
<pre><code class="language-java">@DgsQuery
public List&lt;Show&gt; shows() {

    //Load shows, which doesn&#39;t include &quot;actors&quot;
    return shows;
}

@DgsData(parentType = &quot;Show&quot;, field = &quot;actors&quot;)
public List&lt;Actor&gt; actors(DgsDataFetchingEnvironment dfe) {

   Show show = dfe.getSource();
   actorsService.forShow(show.getId());
   return actors;
}</code></pre>
<ul>
<li><code>@DgsData</code></li>
</ul>
<p>query안의 chlid fetcher(데이터처리를 위한)로 분리를 진행하였다. 이때 fetcher의 경우에는 <code>DgsData</code>를 사용하면 된다.</p>
<p>필요한 값은 어떤 쿼리에서 나온 것인지 알려줘야할 것이다. 따라서 스키마의 이름을 parentType으로 넘겨줘야한다.
또한 그중에서 어떤 필드에 대한 값인지를 field에 알려줘야한다.</p>
<ul>
<li><code>dfe</code>
DgsDataFetchingEnvironment 를 사용해서 getSource()를 진행하면 Show에 대한 source Object를 가져올 수 있고 이를 활용해서 id를 가져올 수 있다.
그렇다면 우리가 미리 작성해놓은 service method에 id를 넘겨주면 되는 것이다.</li>
</ul>
<p>이처럼 트래픽을 많이 차지하는 쿼리인데 client가 요청을 하지 않을 가능성이 많다면?
해당 필드를 child로 분리해서 <code>DgsData annotation</code>을 사용한다.
그렇게 필요할 때 traffic을 처리할 수 있게 된다.
그말은 필요하지 않다면 traffic을 낭비하지 않는다는 말이된다.</p>
<h3 id="마무리">마무리</h3>
<p>지금까지 context를 활용해서 netflix grapqhl에서 어떤 경우에 traffic을 어떻게 줄일 수 있는지에 대해서 알아보았다!</p>
<p>netflix graphql뿐만 아니라 graphql 모두 적용이 가능할 것이다. 
restapi에서는 api를 분리하는게 client에 따라서 어떻게 다르게 해야할지 고민을 해본적이 없는데 graphql에서는 client가 많이 사용하지 않는데 traffic cost가 많이 드는 필드에 대해서는 sub datafetcher를 만드는 식으로 고려해야한다는 것을 느꼈다.</p>
<p>그리고 이러한 고려사항이 있다는 것은 성능개선의 여지(가능성?)이 더 존재한다는 것이니까 restapi보다 더욱 효율적인 api라고 생각이든다 :)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[kotlin] exception]]></title>
            <link>https://velog.io/@sujin-create/kotlin-exception</link>
            <guid>https://velog.io/@sujin-create/kotlin-exception</guid>
            <pubDate>Wed, 14 Jun 2023 05:14:23 GMT</pubDate>
            <description><![CDATA[<p>오늘은 kotlin이 java와 유사한데 예외처리 부분에서 차이가 존재하고 상호운용성을 위해서 어떤 방식으로 예외처리를 진행해야할지에 대해서 공유하려고한다!</p>
<h3 id="0-intro">0. Intro</h3>
<p>kotlin에는 자바처럼 compile 시점에 발생하는 checked 예외와 unchecked 예외로 구분되어있지 않다.
따라서 java처럼 throws를 사용해서 method에 지정하지 않아도 된다는 것을 의미하고 이는 곧, 다른 method에서도 메서드 시그니처에 지정하지 않고 쉽게 예외처리가 가능하다는 것이다.</p>
<p>예를 들어서, 
<code>IOException()</code>의 경우에는 unchecked exception에 포함된다. 이때, 해당 exception을 포함하는 메서드를 살펴보자.</p>
<pre><code>fun throwJavaChecked() {
    throw IOException()
}</code></pre><p>다음과 같은 method가 존재한다고 할 때, method에 java에서 사용하는 것처럼 throws가 존재하지 않는다. checked exception이 존재하지 않기 때문에 signature에 선언하지 않아도 다른 함수에서 예외를 쉽게 던질 수 있게 되는 것이다.</p>
<p>그렇다면, java를 사용해서 kotlin으로 만든 throwJavaChecked()를 호출하여 사용하면 어떻게 될 것인가? 여기부터가 개발할 때 문제가 될 것이다.</p>
<pre><code>public static void checked() {
    try {
        ThrowsKt.throwJavaChecked();
    } catch (IOException e) {
        System.out.println(&quot;Won&#39;t even compile&quot;);
    }
}</code></pre><p>Java compiler는 오류 메시지를 내보내면서 코드 컴파일에 실패하게 될 것이다. java에서는 checked exception <code>throws</code>를 사용하지 않고는 처리할 수 없다.</p>
<p>그렇다면 이때 어떻게 해야할까? kotlin에서는 이러한 문제점을 해결하기 위해서 throws annotation을 제공한다. </p>
<p>우선 상호 운용성을 위한 방법을 알아보기 전에 코틀린에 왜 checked exception이 존재하지 않는지에 대해서 먼저 알아보자!!</p>
<h3 id="1-kotlin에서의-checked-exception가-존재하지-않는-이유">1. kotlin에서의 checked exception가 존재하지 않는 이유</h3>
<p>공식 문서를 살펴보면, 왜 코틀린에서 확인된 예외를 제공하지 않는지에 대해서 나와있다. </p>
<pre><code>Appendable append(CharSequence csq) throws IOException;</code></pre><p>자바에서는 try-catch를 활용해서 예외처리를 매번 진행해줘야한다. 그렇다는 것은 throws를 활용해서 개발자가 try-catch로 처리하도록 넘긴다는 것과 동일한 말이 된다.</p>
<p>그렇다면, 이것은 과연 좋은 것인가?</p>
<p>소규모 프로젝트가 아니라 대규모 프로젝트라면 해당 사항은 매우 불편함으로 작용될 것이다.
개발자의 생산성이 감소하게 될 것이고 (예외처리를 개발자가 매번 해줘야하기 때문에) 코드 품질(코드도 그에 따라서 추가적으로 생겨나게 될 것임) 에도 도움이 되지 않을 것이다.
따라서 이러한 문제점으로 인해서 JAVA에서의 checked exception 개념이 사라지게 됐다.</p>
<p>이제는 그러면 checked exception이 사라졌으니, 상호 운용성을 어떻게 가져야할지에 대해서 알아보자!</p>
<h3 id="2-throws-annotation">2. @Throws annotation</h3>
<p>코틀린은 checked error의 개념이 존재하지 않기 때문에 예상되는 예외 클래스 목록을 throws를 활용해서 알려줘야한다.</p>
<p>코틀린의 method or function에 <code>@Throws</code>를 추가하면 시그니처에 throws 절을 사용해서 해당 메서드나 함수를 컴파일한다. 따라서 컴파일시 발생가능한 checked exception을 처리할 수 있게된다.
따라서, Java에서 kotlin function을 사용하고 checked exception이 존재하는 경우에 compile시에 exception을 처리할 수 있게된다.</p>
<h3 id="마무리">마무리</h3>
<p>어디까지나 @Throws annotation은 java와의 상호운용성을 위해서 사용할 뿐이지 코틀린 자체를 위한 주석은 아니라는 것을 기억해야 할 것으로 보인다.</p>
]]></description>
        </item>
    </channel>
</rss>