<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Developer_314</title>
        <link>https://velog.io/</link>
        <description>블로그 이전했습니다 https://dev314.tistory.com/</description>
        <lastBuildDate>Thu, 20 Jul 2023 12:35:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Developer_314</title>
            <url>https://images.velog.io/images/314_dev/profile/2946deec-48c3-4523-ad43-1ba2a54bafe9/Cherry-Pie-1-6.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Developer_314. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/314_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[카프카 핵심 가이드 3장. 프로듀서]]></title>
            <link>https://velog.io/@314_dev/%EC%B9%B4%ED%94%84%EC%B9%B4-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C-3%EC%9E%A5.-%ED%94%84%EB%A1%9C%EB%93%80%EC%84%9C</link>
            <guid>https://velog.io/@314_dev/%EC%B9%B4%ED%94%84%EC%B9%B4-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C-3%EC%9E%A5.-%ED%94%84%EB%A1%9C%EB%93%80%EC%84%9C</guid>
            <pubDate>Thu, 20 Jul 2023 12:35:48 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://search.shopping.naver.com/book/catalog/39317862619?cat_id=50010920&amp;frm=PBOKPRO&amp;query=%EC%B9%B4%ED%94%84%EC%B9%B4+%ED%95%B5%EC%8B%AC+%EA%B0%80%EC%9D%B4%EB%93%9C&amp;NaPm=ct%3Dlk83gl88%7Cci%3D00a22ec1a0e4513788ebfeda99f94432287b6a0c%7Ctr%3Dboknx%7Csn%3D95694%7Chk%3D2b1703941b9c801f401032a61e0c7709023d1b6c">카프카 핵심 가이드</a>를 읽고 정리한 내용입니다.</p>
</blockquote>
<h1 id="31-프로듀서-개요">3.1 프로듀서 개요</h1>
<ul>
<li>프로듀서로 카프카 메시지를 쓴다 == 프로듀서의 <code>ProducerRecord</code> 전송 API를 호출한다.</li>
</ul>
<ol>
<li>프로듀서가 <code>ProducerRecord</code> 객체를 생성<ul>
<li>레코드가 저장될 토픽, 벨류 지정은 필수</li>
<li>키, 파티션 지정은 선택</li>
</ul>
</li>
<li>프로듀서가 <code>ProducerRecord</code>를 직렬화<ul>
<li>바이트 배열로 전환</li>
</ul>
</li>
<li>(파티션을 지정하지 않았다면) 데이터를 <code>Partitioner</code>에게 전달<ul>
<li>파티셔너는 (보통) ProducerRecord의 키값을 기준으로 파티션을 결정</li>
</ul>
</li>
<li>토픽 파티션이 결정되면, 프로듀서가 데이터를 같은 토픽 파티션으로 전송할 데이터를 모아 놓은 <code>Record Batch</code>에 추가</li>
<li>별도의 스레드가 레코드 베치를 적절한 카프카 브로커에게 전송</li>
<li>브로커가 데이터를 정상적으로 파티션에 저장하면, 브로커는 프로듀서에게 응답을 보냄<ul>
<li>응답 == 토픽, 파티션, 데이터의 파티션 내의 오프셋으로 구성된 <code>RecordMetadata</code> 객체</li>
<li>메시지 저장에 실패하면 응답으로 에러를 전송<ul>
<li>프로듀서가 에러를 수신하면, 사용자에게 에러를 리턴하기 전에 몇 번 더 재전송을 시도할 수 있다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<h1 id="32-프로듀서-생성하기">3.2. 프로듀서 생성하기</h1>
<ul>
<li>프로듀서를 생성하기 위해선 세 가지 필수값을 제공해야 한다.<h2 id="1-bootstrapservers">1. bootstrap.servers</h2>
</li>
<li>카프카 클러스터와 첫 연결을 생성하기 위해 제공해야하는 브로커의 <code>host:port</code> 목록</li>
<li>프로듀서가 첫 연결을 생성한 후, 추가 정보를 받아오기 때문에 모든 브로커의 주소를 제공할 필요는 없음<ul>
<li>다만 브로커에 장애가 발생하더라도 계속해서 클러스터에 연결될 수 있도록, 최소 2개 이상의 브로커를 지정할 것을 권장 <h2 id="2-keyserializer">2. key.serializer</h2>
</li>
</ul>
</li>
<li>레코드의 키를 직렬화하기 위해 사용하는 Serializer </li>
<li>org.apache.kafka.common.Serialization.Serializer 인터페이스를 구현하는 클래스의 이름을 제공해야 한다. (카프카는 자바 App) <ul>
<li>기본적으로 여러 구현체를 제공함 (ByteArray, String, Integer, ...)</li>
</ul>
</li>
<li>레코드에 키를 사용하지 않더라도 key.serializer는 반드시 설정해야 함<h2 id="3-valueserialier">3. value.serialier</h2>
</li>
<li>레코드의 벨류를 직렬화하기 위해 사용</li>
<li>마찬가지로 직렬화 클래스 이름을 제공해야 함<h1 id="33-카프카로-메시지-전달하기">3.3 카프카로 메시지 전달하기</h1>
</li>
<li>카프카는 여러 방법으로 메시지를 보낼 수 있다.<h4 id="1-fire-and-forget">1. Fire and Forget</h4>
</li>
<li>클러스터(브로커)에 메시지를 전송하고, 성공·실패 여부는 신경 쓰지 않음</li>
<li>todo: 이 파트 다시 읽어보기<h4 id="2-synchronous-send">2. Synchronous send</h4>
</li>
<li>다음 메시지를 보내기 위해선, 이전 메시지 전송의 성공·실패 여부를 확인해야 함<h4 id="3-asynchronous-send">3. Asynchronous send</h4>
</li>
<li>기본적으로 프로듀서는 항상 비동기적으로 작동<ul>
<li>응답을 받는 시점에 콜백 메서드를 자동으로 호출</li>
</ul>
</li>
</ul>
<h2 id="330-golang으로-간단히-다뤄보기">3.3.0 GoLang으로 간단히 다뤄보기</h2>
<ul>
<li>책은 Java기반인데 나는 Golang으로 해본다.</li>
</ul>
<pre><code class="language-go"></code></pre>
<h2 id="331-synchronous">3.3.1 Synchronous</h2>
<h2 id="332-asynchronous">3.3.2 Asynchronous</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[카프카 핵심 가이드 1장]]></title>
            <link>https://velog.io/@314_dev/Kafka</link>
            <guid>https://velog.io/@314_dev/Kafka</guid>
            <pubDate>Tue, 18 Jul 2023 09:31:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://search.shopping.naver.com/book/catalog/39317862619?cat_id=50010920&amp;frm=PBOKPRO&amp;query=%EC%B9%B4%ED%94%84%EC%B9%B4+%ED%95%B5%EC%8B%AC+%EA%B0%80%EC%9D%B4%EB%93%9C&amp;NaPm=ct%3Dlk83gl88%7Cci%3D00a22ec1a0e4513788ebfeda99f94432287b6a0c%7Ctr%3Dboknx%7Csn%3D95694%7Chk%3D2b1703941b9c801f401032a61e0c7709023d1b6c">카프카 핵심 가이드</a>를 읽고 정리한 내용입니다.</p>
</blockquote>
<h1 id="pubsub-pattern">Pub/Sub Pattern</h1>
<ul>
<li><code>Publisher</code>가 생성한 데이터(메시지)를 직접 <code>Subscriber</code>에게 전달하지 않는다.</li>
<li>대신 어떤 형태로든 메시지를 분류해서 보내고, 수신자는 메시지를 구독한다.</li>
<li>대부분의 펍섭 시스템은 중간에 <code>Broker</code>가 존재한다.<h1 id="kafka">Kafka</h1>
</li>
<li>PubSub 기반 <code>분산 커밋 로그 (분산 스트리밍 플랫폼)</code> <ul>
<li>마치 RDBMS가 <code>commit log</code>를 사용해서 트랜잭션을 관리하는 것 처럼 작동</li>
</ul>
<ol>
<li>트랜잭션 기록을 보존 (durable)</li>
<li>시스템 상태를 일관성 있게 복구 가능 (consistency)</li>
<li>데이터 순서 보장 (deterministic)</li>
</ol>
</li>
</ul>
<hr>
<ul>
<li>카프카의 기본 데이터 단위는 <code>Message</code><ul>
<li>단순한 byte 배열 -&gt; 특정한 형식, 의미가 없다</li>
<li>메시지는 <code>key</code>라 불리는 메타 데이터를 포함할 수 있다.<ul>
<li>메시지를 저장할 <code>Partition</code>을 결정하기 위해 사용</li>
<li><code>키</code> 역시 단순히 바이트 배열</li>
</ul>
</li>
</ul>
</li>
<li>카프카는 효율성을 위해 메시지를 <code>Batch</code> 단위로 저장<ul>
<li>단순히 같은 <code>Topic</code>의 파티션에 쓰여지는 메시지의 집합을 의미함</li>
<li>메시지 쓸 때 마다 생기는 네트워크 비용을 줄이기 위해, 그냥 모아서 전달하는 것<ul>
<li>latency, throughput 사이의 tradeoff발생<h2 id="topic-partition">Topic, Partition</h2>
</li>
</ul>
</li>
</ul>
</li>
<li>메시지는 <code>topic</code> 단위로 분류된다.</li>
<li>토픽은 여러 <code>partition</code>으로 나뉘어진다.<ul>
<li>토픽에 여러 파티션이 있는 경우, 토픽 안의 메시지 전체에 대해서는 순서 보장이 안 됨<ul>
<li>단일 파티션의 경우에만 (구조적으로) 가능</li>
</ul>
</li>
</ul>
</li>
<li>파티션 개념을 통해 카프카는 데이터 중복, 확장성을 제공한다.<ul>
<li>각 파티션이 서로 다른 서버에 저장될 수 있다.<ul>
<li>하나의 토픽이, 하나의 서버의 용량을 넘어가는 성능을 제공할 수 있다.</li>
</ul>
</li>
<li>파티션은 복제 가능하다.<ul>
<li>서로 다른 서버들이 동일한 파티션의 복제본을 저장하고 있기 때문에, 한 서버에 장애가 발생해도 다른 서버에서 계속해서 읽고, 쓸 수 있다.<h2 id="producer-consumer-consumer-group">Producer, Consumer, Consumer Group</h2>
</li>
</ul>
</li>
</ul>
</li>
<li>카프카 클라이언트에 해당<ul>
<li>프로듀서, 컨슈머로 구분됨</li>
</ul>
</li>
<li>프로듀서<ul>
<li>aka Publisher, Writer</li>
<li>메시지 생성 주체<ul>
<li>메시지는 특정 토픽에 쓰여짐</li>
</ul>
</li>
<li>기본적으로 프로듀서는 토픽에 속한 파티션들 사이에 고르게 나눠서 메시지를 쓴다.<ul>
<li>특정 경우에는 <code>partitioner</code>를 통해 특정 파티션을 지정해서 메시지를 쓸 수도 있다.</li>
</ul>
</li>
</ul>
</li>
<li>컨슈머<ul>
<li>aka Subscriber, Reader</li>
<li>메시지 소비 주체<ul>
<li>1개 이상의 토픽을 구독해서, 저장된 메시지를 각 파티션에 쓰여진 순서대로 읽음</li>
</ul>
</li>
<li><code>offset</code>을 기록함으로써, 어느 메시지까지 읽었는지 유지<ul>
<li>카프카가 메시지를 저장할 때, 각 메시지에 부여하는 메타데이터</li>
<li>지속적으로 증가하는 정수값</li>
<li>파티션의 각 메시지는 고유한 오프셋을 가지며, 앞서 저장된 메시지는 뒤에 오는 메시지보다 반드시 작은 값을 갖는다.</li>
</ul>
</li>
</ul>
</li>
<li>컨슈머 그룹<ul>
<li>컨슈머는 <code>Consumer Group</code>의 일원으로서 작동</li>
<li>하나 이상의 컨슈머로 구성</li>
<li><strong>컨슈머 그룹은 각 파티션이 하나의 컨슈머에 의해서만 읽히도록 한다</strong><ul>
<li>컨슈머와 파티션의 대응 관계를 <code>onwership</code>이라고 한다.</li>
</ul>
</li>
<li>이를 통해 <ul>
<li>컨슈머를 수평 확장 할 수 있다.</li>
<li>한 컨슈머에 장애가 발생하더라도, 다른 컨슈머가 장애가 난 컨슈머가 읽고 있던 파티션을 재할당받은 뒤 이어서 데이터를 읽을 수 있다.<h2 id="broker-cluster">Broker, Cluster</h2>
</li>
</ul>
</li>
</ul>
</li>
<li>브로커<ul>
<li>하나의 카프카 서버를 <code>Broker</code>라고 부른다.</li>
<li>프로듀셔로부터 메시지를 전달받아 오프셋을 할당한 뒤 디스크 저장소에 쓴다.</li>
<li>컨슈머의 파티션 <code>fetch</code> 요청에 응답하여 메시지를 전달한다.</li>
</ul>
</li>
<li>클러스터<ul>
<li>브로커는 <code>Cluster</code>의 일부로 작동하도록 설계되었다.</li>
<li>하나의 클러스터 안에 여러 개의 브로커가 포함될 수 있다.<ul>
<li>브로커 중 하나가 <code>Cluster Controller</code> 역할을 한다.<ul>
<li>작동중인 브로커 중 하나로 자동 선정된다.</li>
<li>파티션을 브로커에 할당해주거나, 장애가 발생한 브로커를 모니터링하는 등의 작업을 수행</li>
</ul>
</li>
</ul>
</li>
<li>파티션은 클러스터 안의 브로커 중 하나가 담당한다.<ul>
<li>그 브로커를 <code>Partition Leader</code>라고 부른다.</li>
</ul>
</li>
<li>복제된 파티션이 여러 브로커에 할당될 수도 있다.<ul>
<li>복제된 파티션을 가지고 있는 브로커들을 <code>Follower</code>라고 한다.<h3 id="카프카의-replication-retention">카프카의 Replication, Retention</h3>
</li>
</ul>
</li>
</ul>
</li>
<li>Replication<ul>
<li>파티션의 메시지를 중복 저장함으로써, 리더 브로커에 장애가 발생할 경우 팔로워들 중 하나가 리더 역할을 이어 수행한다.</li>
<li>모든 프로듀서는 리더 브로커에 메시지를 발행해야 하지만, 컨슈머는 리더, 팔로워 상관 없이 한 곳에서 데이터를 읽어올 수 있다.</li>
</ul>
</li>
<li>Durability Retention<ul>
<li>브로커는 토픽에 대해 기본적인 보존 설정이 되어 있다.<ul>
<li>특정 기간 동안 메시지를 보존</li>
<li>파티션이 특정 크기에 도달할 때 까지 데이터를 보존<h1 id="kafka의-특징">Kafka의 특징</h1>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<ol>
<li>다중 프로듀서<ul>
<li>한 토픽에 대해 여러 프로듀서가 메시지를 생성할 수 있다.</li>
</ul>
</li>
<li>다중 컨슈머<ul>
<li>여러 컨슈머가 상호 간섭 없이 어떠한 메시지도 읽을 수 있다.</li>
<li>다른 메시지큐 서비스와 달리, 컨슈머 단위가 아닌 컨슈머 그룹 단위로 메시지가 소비된다.</li>
</ul>
</li>
<li>디스크 기반 보존<ul>
<li>메시지를 지속성 있게 저장할 수 있다 -&gt; 즉시 메시지를 읽지 않아도 보존된다.</li>
<li>토픽별로 보존 정책을 지정할 수 있다.</li>
</ul>
</li>
<li>확장성<ul>
<li>유연한 확장성 덕분에 어떠한 크기의 데이터도 처리할 수 있다.</li>
<li>클러스터 작동 중에도 시스템 중단 없이 확장 가능하다 (availability)</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Json Schema Validation]]></title>
            <link>https://velog.io/@314_dev/Json-Schema-Validation</link>
            <guid>https://velog.io/@314_dev/Json-Schema-Validation</guid>
            <pubDate>Wed, 05 Jul 2023 23:38:15 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[PostgreSQL Index]]></title>
            <link>https://velog.io/@314_dev/Index</link>
            <guid>https://velog.io/@314_dev/Index</guid>
            <pubDate>Fri, 16 Jun 2023 02:21:05 GMT</pubDate>
            <description><![CDATA[<h1 id="postgresql과-index">PostgreSQL과 Index</h1>
<h2 id="조회">조회</h2>
<p><code>PostgreSQL</code>은 스키마의 인덱스를 <code>pg_indexes</code> 테이블로 관리한다.</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Collation</th>
<th>Nullable</th>
<th>Default</th>
<th>Storage</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>schemaname</td>
<td>name</td>
<td></td>
<td></td>
<td></td>
<td>plain</td>
<td></td>
</tr>
<tr>
<td>tablename</td>
<td>name</td>
<td></td>
<td></td>
<td></td>
<td>plain</td>
<td></td>
</tr>
<tr>
<td>indexname</td>
<td>name</td>
<td></td>
<td></td>
<td></td>
<td>plain</td>
<td></td>
</tr>
<tr>
<td>tablespace</td>
<td>name</td>
<td></td>
<td></td>
<td></td>
<td>plain</td>
<td></td>
</tr>
<tr>
<td>indexdef</td>
<td>text</td>
<td></td>
<td></td>
<td></td>
<td>extended</td>
<td></td>
</tr>
<tr>
<td>``` sql</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>SELECT * FROM pg_indexes;</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>```</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>## 생성</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>``` sql</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>CREATE INDEX users_name_age ON Users(name, age);</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>```</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>Users</code> 테이블의 <code>name</code>, <code>age</code> 컬럼에 대한 <code>users_name_age</code>라는 인덱스를 생성한다.</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>별도로 <code>인덱스 타입</code>을 지정하지 않으면, 기본적으로 <code>B-Tree</code>기반 인덱스를 생성한다.</p>
<pre><code>hash, GIN, GiST, SP-GiST등의 Index가 존재함 (하단에서 이어서 설명)</code></pre><h2 id="삭제">삭제</h2>
<pre><code class="language-sql">DROP INDEX SCHEMA_NAME.INDEX_NAME;</code></pre>
<h2 id="업데이트">업데이트</h2>
<p>데이터가 삭제, 추가되면 인덱스의 정확도가 점점 떨어질 것이다. </p>
<p>변경된 데이터 상황에 맞게 인덱스를 업데이트 해줘야 한다.</p>
<pre><code class="language-sql">// Postgresql 방식
REINDEX INDEX index_name;
// 인덱스의 statistic 정보만 업데이트 -&gt; 인덱스 구조가 변경되는게 아님
ANALYZE table_name(index_name);</code></pre>
<h2 id="참고">참고</h2>
<p>데이터 개수가 늘어날 수록 인덱스 생성, 삭제, 업데이트는 비용이 많이 소모된다는 점을 항상 염두해야 한다.</p>
<h1 id="index-종류">Index 종류</h1>
<h1 id="성능-비교">성능 비교</h1>
<p><code>PostgreSQL</code>에서는 <code>\timing</code>과 <code>EXPLAIN ANALYZE</code>를 통해 간단히 실행 시간을 확인할 수 있다.</p>
<h2 id="index-적용-이전">INDEX 적용 이전</h2>
<pre><code>\timing
SELECT * FROM Users WHERE name =</code></pre><h2 id="index-적용-후">INDEX 적용 후</h2>
<h1 id="index가-사용이-안-돼요">Index가 사용이 안 돼요</h1>
<p>문제없이 인덱스를 만들었는데도 Index를 타지 않는다.</p>
<p><code>Query Optimizer</code>가 Index를 타는게 오히려 성능이 안 좋다고 판단했기 때문이다.</p>
<p>그럼에도 인덱스를 사용하고 싶으면 <code>PostgreSQL</code>의 경우, <code>Query Optimizer</code>가 Index를 타도록 다음과 같이 힌트를 제공할 수 있다.</p>
<pre><code class="language-sql">// 
SELECT /*+ INDEX(user_name_age_idx) */ * FROM Users WHERE name = &#39;hi&#39; AND age = 25;</code></pre>
<p>SELECT 절에 <code>/*+ INDEX(인덱스_이름) */</code>형식으로 힌트를 줄 수 있다.</p>
<p>그런데 힌트를 제공한다고 해서 무조건 인덱스를 사용하는건 아니다. </p>
<p>내가 아무리 인덱스 사용하라고 해도, 쿼리 옵티마이저가 <code>성능상 이건 아닌데...</code>하면 인덱스를 사용하지 않는다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[gqlgen으로 GraphQL 서버 만들기]]></title>
            <link>https://velog.io/@314_dev/gplgen%EC%9C%BC%EB%A1%9C-GraphQL-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@314_dev/gplgen%EC%9C%BC%EB%A1%9C-GraphQL-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Mon, 29 May 2023 06:41:15 GMT</pubDate>
            <description><![CDATA[<h1 id="다짜고짜-환경-설정">다짜고짜 환경 설정</h1>
<pre><code class="language-bash"># 프로젝트 init 
go mod init github.com/wonju-dev

# gqlgen 의존성 설정
# gqlgen 이후에 버전을 명시할 수 있음 (생략하면 최신 버전)
go get -d github.com/99designs/gqlgen

# gqlgen 프로젝트 구조 초기화
go run github.com/99designs/gqlgen init</code></pre>
<p>위 명령을 수행하면 다음과 같은 프로젝트가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/314_dev/post/b896e2c9-26e1-4e0b-97ea-22884c7e96b2/image.png" alt=""></p>
<h2 id="graphql-schema-정의하기">GraphQL Schema 정의하기</h2>
<p><code>graph/sechema.graphqls</code>에 <code>회원 CRUD</code>와 관련한 GraphQL 스키마를 정의한다.</p>
<p>GrahpQL 문법은 <a href="">다음</a>에서 설명</p>
<pre><code class="language-graphql">type Query {
  getUser(
    input: QgetUserInput
  ): [QgetUserOutput!]!
}

input QgetUserInput {
  id: [ID!]
  name: [String!]
  age: [Int !]
}

type QgetUserOutput {
  status: Boolean!
  result : [User!]
}

type Mutation {
  addUser(
    input: MaddUserInput
  ): MaddUserOutput

  updateUser(
    input: MupdateUserInput
  ): MupdateUserOutput

  deleteUser(
    input: MdeleteUserInput
  ): MdeleteUserOutput
}

input MupdateUserInput {
  id: ID!
  name: String
  age: Int
}
input MdeleteUserInput {
  ids: [ID!]
  names: [String!]
  ages: [Int!]
}

type MupdateUserOutput {
  state: Boolean
  results: User
}

type MdeleteUserOutput {
  state: Boolean
  results: [User]
}

input MaddUserInput {
  name: String!
  age: Int!
}

type MaddUserOutput{
  status: Boolean!
  result: User
}

type User {
  id: ID
  name: String!
  age: Int!
}</code></pre>
<p>그런다음 <code>go run github.com/99designs/gqlgen generate</code> 명령을 수행하면 자동으로 GraphQL 서버를 위한 코드, 파일이 생성된다.</p>
<p><a href="https://gqlgen.com/">공식문서</a>에 따르면 </p>
<ol>
<li><code>gqlgen</code>은 <code>Schema first approach</code>를 따르기 때문에, GraphQL 쿼리로 API명세를 정의할 수 있다.</li>
<li>위와 같은 접근법을 지원하기 위해, <code>schema.graphql</code>에 정의된 스키마를 토대로 <code>golang의 구조체</code>와 <code>resolver template</code>와 같은 boilerplate를 자동 생성한다. </li>
</ol>
<p>그래서 우리는 <code>schema.graphqls</code>에 스키마를 정의했고, <code>gqlgen</code>의 도움을 받아 요청을 처리할 로직을 <code>schem.resolver.go</code>에 구현할 것이다.</p>
<h1 id="유저-create-예제">유저 Create 예제</h1>
<p>일반적인 <code>Controller(resolver) - Service</code> 구조를 만들기 위해 의존성을 주입한다.</p>
<pre><code class="language-go">// graph/resolver.go
package graph

import &quot;github.com/wonju-dev/service/user&quot;

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct {
    userService user.UserService
}</code></pre>
<h2 id="create">Create</h2>
<pre><code class="language-go">// graph/schema.resolvers.go
// AddUser is the resolver for the addUser field.
func (r *mutationResolver) AddUser(ctx context.Context, input *model.MaddUserInput) (*model.MaddUserOutput, error) {
    return r.userService.CreateUser(ctx, input)
}

// userService.go
package user

import (
    &quot;context&quot;
    &quot;github.com/wonju-dev/graph/model&quot;
    &quot;github.com/wonju-dev/repository/User&quot;
)

type UserService struct {
    userRepository User.UserRepository
}

func (userService *UserService) CreateUser(ctx context.Context, input *model.MaddUserInput) (*model.MaddUserOutput, error) {
    err := userService.userRepository.AddUser(input)
    if err != nil {
        return nil, err
    }

    return &amp;model.MaddUserOutput{
        Status: true,
        Result: &amp;model.User{
            Name: input.Name,
            Age:  input.Age,
        },
    }, nil
}</code></pre>
<h2 id="의존성-주입-실패">의존성 주입 실패</h2>
<p>위 처럼 만든 다음 로컬에서 테스트를 했는데, <code>nil pointer error</code>가 발생한다.</p>
<p>생각해 보니 의존성 관계를 설정만 했지, 실제로 의존성을 주입한 부분이 없다.</p>
<p>당최 gqlgen은 어떻게 자동하는지 모르겠다. gqlgen 내부를 한 번 뜯어보자...</p>
<h1 id="gqlgen-구성-작동-방식">gqlgen 구성, 작동 방식</h1>
<p>튜토리얼 강의를 보면 GraphQL 서버를 만든 다음, golang의 표준<code>http</code>라이브러리를 통해 서버를 띄운다.</p>
<pre><code class="language-go">    ...
    srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &amp;graph.Resolver{}}))

    http.Handle(&quot;/query&quot;, srv)
    ...</code></pre>
<p>기본적으로 GraphQL은 HTTP Protocol을 기반으로 작동한다.
그렇기 때문에, GraphQL 요청을 처리하는 녀석(?)은 단순히 Request Handler와 다르지 않다. 다만 Requset Body 파싱하고, 요청 처리에 사용할 변수를 만들기 위해 조금 복잡할 뿐이다.</p>
<h2 id="gqlgen-작동-방식">gqlgen 작동 방식</h2>
<ol>
<li><code>NewDefaultServer</code>은 <code>http</code>표준인 <code>Handler</code> 인터페이스를 구현한 <code>Server</code>라는 구현체를 반환한다.<ul>
<li><code>Server</code> 객체는 다음과 같다.<pre><code class="language-go">Server struct {
  transports []graphql.Transport
  exec       *executor.Executor
}</code></pre>
</li>
<li><code>exec</code>: 실재로 요청을 처리하는 객체</li>
<li>참고: 사실 GraphQL은 POST말고, GET, WebSocket 으로도 요청을 보낼 수 있다. 서로 다른 요청 방식을 표준화 하기 위해 <code>transports</code>가 존재한다.</li>
</ul>
</li>
<li>HTTP 요청이 발생하면 <code>Server</code>의 <code>ServeHTTP</code>가 요청을 처리한다.</li>
<li><code>ServeHTTP</code>는 <ol>
<li><code>Server</code>의 멤버 변수 배열에서, 요청을 처리할 수 있는 적절한<code>Transport</code>를 찾은 뒤, </li>
<li>선택된 <code>Transport.Do</code>메서드에 멤버 변수<code>exec</code>을 파라미터로 전달하여 호출한다.</li>
<li>아래에서 설명될 하위 메서드에서 만든 Response를 전송</li>
</ol>
</li>
</ol>
<pre><code>    참고
    1. `Transport`는 `Server`객체 생성 시점에 자동으로 등록된다. (`NewDefaultServer`를 사용하는 경우)
    2. `New`를 통해 직접 서버를 만들 수도 있는데, 이 경우에는 직접 `Transport`를 등록해야 한다.
    3. 이번 분석에서는 NewDefaultServer를 통해 자동으로 등록된 `Transport`구현체인 POST만 다룬다. (일반적으로 GraphQL은 POST method로 이용하므로)</code></pre><pre><code class="language-go">    // Transport는 단순히 인터페이스고
    // NewDefaultServer을 사용하는 경우 gqlgen은 자동으로 
    // Transport 구현체(GET, POST, WebSocket, Option, ...)를 등록한다.
    Transport interface {
        Supports(r *http.Request) bool // Transport가 Http Method를 구분할 때 사용
        Do(w http.ResponseWriter, r *http.Request, exec GraphExecutor)
    }</code></pre>
<ul>
<li><p><code>exec</code>의 타입인 <code>Executor</code>은 다음과 같다.</p>
<pre><code class="language-go">type Executor struct {
  es         graphql.ExecutableSchema
  extensions []graphql.HandlerExtension
  ext        extensions

  errorPresenter graphql.ErrorPresenterFunc
  recoverFunc    graphql.RecoverFunc
  queryCache     graphql.Cache
}</code></pre>
<p>다 필요 없고, 몇 가지 포인트만 짚자면</p>
<ol>
<li><code>Executor</code>은 <code>ExecutableSchema</code>을 멤버 변수로 갖는다.</li>
<li><code>Executor</code>는 <code>GraphExecutor</code> 인터페이스를 구현체이다.</li>
<li><code>Executor</code>는 <code>Server</code> 객체 생성시에 같이 생성되고, <code>Server</code>에 멤버 변수로 할당된다.<pre><code class="language-go">GraphExecutor interface {
CreateOperationContext(ctx context.Context, params *RawParams) (*OperationContext, gqlerror.List)
DispatchOperation(ctx context.Context, rc *OperationContext) (ResponseHandler, context.Context)
DispatchError(ctx context.Context, list gqlerror.List) *Response
}</code></pre>
</li>
</ol>
</li>
</ul>
<ol start="4">
<li>(다시 원래 흐름으로 돌아와서)<code>Transport</code> 구현체인 <code>POST</code>의 <code>Do</code>메서드는 대략 다음과 같다.<pre><code class="language-go">func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
 1. 기본적인 Response Message Header 구성
 2. Request Message Body 검증
     2.1 유효하지 않은 포맷이면 Error를 Dispatch
 3. OperationContext 생성
 4. 요청 처리
 var responses graphql.ResponseHandler
 responses, ctx = exec.DispatchOperation(ctx, rc)
 5. Response Message 생성
 writeJson(w, responses(ctx))
}</code></pre>
</li>
<li><code>Executeor</code>의 <code>DispatchOperation</code>의 메서드는, 자신의 멤버 변수인 <code>ExecutableSchema</code>의 <code>Exec</code>메서드를 통해 최종적으로 요청을 처리한다. </li>
<li><code>ExecutableSchema</code>는 객체는 <code>generated.go</code>의 내용을 토대로 생성되었고, <code>generated.go</code>의 내용에 따라 <code>schema.resolvers.go</code>의 메서드들이 호출된다.<h2 id="gqlgen-사용-방법-의존성-주입">gqlgen 사용 방법 (의존성 주입)</h2>
앞 선 주제를 통해, gqlgen이 어떻게 작동하는지 파악했다. 
이제 적절한 파라미터를 전달해서, 우리가 <code>schema.graphqls</code>에 정의한 내용을 토대로 서버가 작동하도록 해보자.</li>
</ol>
<p>다시 맨 처음 코드로 돌아와서 GraphQL 서버를 설정하는 코드를 보면</p>
<pre><code class="language-go">config := graph.Config{Resolvers: &amp;graph.Resolver{}}
executableSchema := graph.NewExecutableSchema(config)
srv := handler.NewDefaultServer(executableSchema)</code></pre>
<p>다음 처럼 분석할 수 있다.</p>
<ol>
<li><code>&amp;graph.Resolver</code>는 우리가 의존 관계를 설정한 구조체의 객체<ul>
<li><strong>여기에 의존성 주입을 하면 된다.</strong></li>
</ul>
</li>
<li><code>config</code>와 <code>executableSchema</code>는 <code>generated.go</code>를 토대로 생성되었고, gqlgen은 <code>executableSchema</code>을 통해 <code>schema.resolvers</code>의 메서드를 사용하게 된다.</li>
</ol>
<p>따라서 다음과 같이 의존성을 주입하면 된다.</p>
<pre><code class="language-go">config := graph.Config{
    Resolvers: &amp;graph.Resolver{
        user.UserService{ // Resolver가 사용할 Service
            User.UserRepositoryImpl{ // Service가 사용할 Repository
                // 대충 db config 관련 내용
            },
        },
    },
}
executableSchema := graph.NewExecutableSchema(config)
srv := handler.NewDefaultServer(executableSchema)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Go에서 RDBMS 다루기]]></title>
            <link>https://velog.io/@314_dev/Go%EC%97%90%EC%84%9C-RDBMS-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@314_dev/Go%EC%97%90%EC%84%9C-RDBMS-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Sun, 28 May 2023 04:30:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://go.dev/doc/tutorial/database-access">Tutorial: Accessing a relational database</a>
<a href="https://github.com/jackc/pgx">pgx - PostgreSQL Driver and Toolkit</a></p>
</blockquote>
<h1 id="connection-생성">Connection 생성</h1>
<h2 id="기본-개념">기본 개념</h2>
<p>Go는 표준화된 방법으로 DB를 사용할 수 있도록 <code>database/sql</code>이라는 표준 라이브러리를 제공한다.
이는 단순히 인터페이스이므로, 실제 구현체를 import해서 사용해야 한다.</p>
<p><a href="https://github.com/golang/go/wiki/SQLDrivers">공식문서</a>를 살펴보면 다양한 PostgreSQL 드라이버가 있는 것 같은데, 그 중 <a href="https://github.com/jackc/pgx">jackc/pgx</a>을 사용하기로 한다.</p>
<h3 id="드라이버-특화-방식으로-연결하기">드라이버 특화 방식으로 연결하기</h3>
<p>드라이버마다 조금씩 연결 방식이 다른데, <code>jackc/pgx</code>는 다음과 같다.</p>
<pre><code class="language-go">package main

import (
    &quot;context&quot;
    &quot;github.com/jackc/pgx/v4&quot;
    &quot;log&quot;
)

func main() {
    connectionInfo := &quot;postgres://USER_NAME:PASSWORD@RDBMS_URL:PORT_NUM/DATABASE_NAME&quot;
    connection, err := pgx.Connect(context.Background(), connectionInfo)
    if err != nil {
        log.Panic(err)
    }
    defer closeDBConnection(connection, context.Background())

    log.Println(&quot;success to establish DB Connection&quot;)
}

func closeDBConnection(connection *pgx.Conn, context context.Context) {
    connection.Close(context)
}</code></pre>
<h3 id="표준-방식으로-연결하기">표준 방식으로 연결하기</h3>
<p>당연히 <code>database/sql</code>을 사용해서 Connection을 만들 수 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;database/sql&quot;
    _ &quot;github.com/jackc/pgx/v4/stdlib&quot; // 생략하면 안 됨
    &quot;log&quot;
)

func main() {
    connectionInfo := &quot;postgres://USER_NAME:PASSWORD@RDBMS_URL:PORT_NUM/DATABASE_NAME&quot;
    connection, err := sql.Open(&quot;pgx&quot;, connectionInfo) // takes String
    // sql.OpenDB(driver.Connector) // takes driver.Connector
    if err != nil {
        log.Panic(err)
    }

    defer connection.Close()
    log.Print(&quot;success to establish connection&quot;)
}</code></pre>
<p>표준 인터페이스에 <code>어떤 RDBMS를 사용할 것인가</code>를 초기화 단계에 알려줘야 한다.</p>
<pre><code>github.com/jackc/pgx의 경우 &#39;postgress&#39;가 아니고, &#39;pgx&#39;이다.
github.com/lib/pg의 경우에는 &#39;postgress;가 맞다.</code></pre><p>특별한 이유가 없으면 표준 인터페이스를 사용하는게 좋을 듯 하다.</p>
<pre><code>참고
1. Connection 정보로 String이 아닌 객체를 넘겨줄 수도 있는데, 정확한 사용법은 RDBMS 드라이버마다 다르다.
2. `os.Getenv`를 사용하여 Credentials들을 은닉화 할 수 있다. (https://go.dev/doc/database/open-handle#:~:text=log.Fatal(err)%0A%7D-,Storing%20database%20credentials,-Avoid%20storing%20database)</code></pre><h2 id="connection-pool">Connection Pool</h2>
<p>앞서 살펴본 Connection은 사실 <code>Connection Pool</code>이다.</p>
<pre><code class="language-go">conn, err := sql.Open(&quot;pgx&quot;, connectionInfo)
stats := conn.Stats(); // sql.DBStats

// DBStats contains database statistics.
type DBStats struct {
    MaxOpenConnections int // Maximum number of open connections to the database.

    // Pool Status
    OpenConnections int // The number of established connections both in use and idle.
    InUse           int // The number of connections currently in use.
    Idle            int // The number of idle connections.

    // Counters
    WaitCount         int64         // The total number of connections waited for.
    WaitDuration      time.Duration // The total time blocked waiting for a new connection.
    MaxIdleClosed     int64         // The total number of connections closed due to SetMaxIdleConns.
    MaxIdleTimeClosed int64         // The total number of connections closed due to SetConnMaxIdleTime.
    MaxLifetimeClosed int64         // The total number of connections closed due to SetConnMaxLifetime.
}</code></pre>
<p><code>sql.DB</code> 구조체의 메서드를 통해 Conncection (Pool) 관련 설정을 할 수 있다. <a href="https://go.dev/doc/database/manage-connections#connection_pool_properties:~:text=for%20database%20access.-,Setting%20connection%20pool%20properties,-You%20can%20set">참고</a></p>
<h1 id="query">Query</h1>
<h2 id="insert-update-delete">INSERT, UPDATE, DELETE</h2>
<p>반환값이 없는 작업을 수행할 때는 기본적으로 <code>Exec</code>을 사용한다.</p>
<pre><code class="language-go">func AddAlbum(alb Album) (int64, error) {
    result, err := db.Exec(&quot;INSERT INTO album (title, artist) VALUES (?, ?)&quot;, alb.Title, alb.Artist)
    if err != nil {
        return 0, fmt.Errorf(&quot;AddAlbum: %v&quot;, err)
    }

    // Get the new album&#39;s generated ID for the client.
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf(&quot;AddAlbum: %v&quot;, err)
    }
    // Return the new album&#39;s ID.
    return id, nil
}</code></pre>
<p>RDBMS에 따라 Parameter Placeholde가 <code>?</code>일 수도 <code>$1</code>일 수도 있다.</p>
<pre><code>PostgreSQL은 $1 형식을 사용한다.</code></pre><p>용도에 따라 사용할 수 있는 다양한 <code>Exec</code> 메서드가 존재한다.
<img src="https://velog.velcdn.com/images/314_dev/post/56da34ad-ee5a-41bb-9a00-b5cdd2413cf3/image.png" alt=""></p>
<h2 id="select">SELECT</h2>
<h3 id="single-row">Single Row</h3>
<p><code>PK로 조회</code>같이 결과 값이 단 건이 경우에는 <code>QueryRow</code>를 사용한다.</p>
<pre><code class="language-go">func canPurchase(id int, quantity int) (bool, error) {
    var enough bool // 쿼리 결과값을 받을 변수를 선언

    // 쿼리 수행
    // sql.Row Type
    row := db.QueryRow(&quot;SELECT (quantity &gt;= ?) from album where id = ?&quot;, quantity, id)
    // 결과값을 parsing해서 변수에 할당, 파라미터로 Ref를 전달해야 함
    err := row.Scan(&amp;enough)
    if err != Nil {
        return false, fmt.Errorf(&quot;canPurchase %d: unknown album&quot;, id)
    }
    return enough, nil
}</code></pre>
<p><code>QueryRow</code>로 실행시킨 쿼리의 결과값이 여러 건인 경우, <code>Scan</code>을 수행하면 맨 처음 찾은 값만 반환한다.
<img src="https://velog.velcdn.com/images/314_dev/post/221f2b96-b715-466e-a474-26aafe512dc2/image.png" alt=""></p>
<h3 id="multi-row">Multi Row</h3>
<pre><code class="language-go">func albumsByArtist(artist string) ([]Album, error) {
    // sql.Rows Type
    rows, err := db.Query(&quot;SELECT * FROM album WHERE artist = ?&quot;, artist)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    // An album slice to hold data from returned rows.
    var albums []Album

    // Loop through rows, using Scan to assign column data to struct fields.
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&amp;alb.ID, &amp;alb.Title, &amp;alb.Artist,
            &amp;alb.Price, &amp;alb.Quantity); err != nil {
            return albums, err
        }
        albums = append(albums, album)
    }
    if err = rows.Err(); err != nil {
        return albums, err
    }
    return albums, nil
}</code></pre>
<p>Single Row랑 크게 다른게 없다.</p>
<p>다만 <code>rows.Close</code>를 호출해서 자원을 반납함을 확인할 수 있는데,
기본적으로 <code>sql.Rows</code>는 <code>rows.Next</code>를 통해 모든 loop을 돌면 묵시적으로 자원을 반납하는데, 
에러가 발생하는 경우를 고려해서 <code>defer</code>를 통해 명시적으로 반환하는 것을 권장한다.</p>
<h3 id="null">Null</h3>
<p>특정 컬럼이 Null인 경우를 고려할 수 있다.</p>
<pre><code class="language-go">var s sql.NullString // NullBool, NullFloat64, NullInt32, NullInt64, NullString, NullTime
err := db.QueryRow(&quot;SELECT name FROM customer WHERE id = ?&quot;, id).Scan(&amp;s)
if err != nil {
    log.Fatal(err)
}

name := &quot;Valued Customer&quot; // 마치 기본값 처럼 사용할 수 있다.
// Valid(not null)이면
if s.Valid {
    // 조회된 값을 사용
    name = s.String
}</code></pre>
<h3 id="scan">Scan</h3>
<p>위에서 살펴본 것처럼, <code>Scan</code>을 사용하면 RDBMS의 데이터 타입을 Golang의 비슷한 데이터타입으로 Convert 해준다.</p>
<p>구체적으로 <code>어떤 값으로 변환되는지, 어떻게 변환하는지</code>는 RDBMS 드라이버에 따라 다르다. (<a href="https://pkg.go.dev/database/sql#Rows.Scan">다음</a>을 참고)</p>
<h1 id="transaction">Transaction</h1>
<p>앞서 다룬 예제에서는 따로 트랜잭션을 설정하지 않았기 때문에, RDBMS의 기본 설정에 따라 Rollback, Commit 될 것이다.</p>
<h2 id="commit-rollback">Commit, Rollback</h2>
<h2 id="isolation-level">Isolation Level</h2>
<h1 id="trial-and-error">Trial and Error</h1>
<h2 id="postgresql의-array-다루기">PostgreSQL의 array 다루기</h2>
<p>PostgreSQL은 <code>array</code> 타입을 지원하는데, Golang에서 이를 다룰 때 단순히 Array(또는 Slice)를 사용하면 된다.</p>
<pre><code class="language-sql">CREATE TABLE Person (
    ...
    hobbies varchar[64] notnull, // {&quot;soccer&quot;, &quot;bascketball&quot;} 형식으로 저장한다.
    ...
)</code></pre>
<pre><code class="language-go">var hobbies []string

row := conn.QueryRow(&quot;SELECT hobbies from Person&quot;,...)
err := row.Scan(&amp;hobbies) // 알아서 PostgreSQL Array를 Golang Slice로 변환한다.</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[gRPC, Protocol buffers]]></title>
            <link>https://velog.io/@314_dev/gRPC-Protocol-buffers</link>
            <guid>https://velog.io/@314_dev/gRPC-Protocol-buffers</guid>
            <pubDate>Tue, 23 May 2023 00:50:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://grpc.io/docs/what-is-grpc/introduction/">Introduction to gRPC</a></p>
</blockquote>
<h1 id="grpc">gRPC</h1>
<p>gRPC는 <code>protocol buffers</code>을 두 가지 용도로 사용한다.</p>
<ol>
<li>Interface Definition Language (IDL) </li>
<li>underlying message interchange format.</li>
</ol>
<p>gRPC에서는, client app이 직접적으로 다른 머신에서 작동 중인 server application의 메서드를 마치 &#39;local object&#39;처럼 호출할 수 있다. 이로써 분산 app, service를 더 쉽게 만들 수 있다.</p>
<p>다른 RPC 시스템 처럼, gRPC도 &#39;서비스를 정의하고&#39;, &#39;파라미터와 리턴 타입을 가지고 원격으로 호출할 수 있는 메서드를 식별하자&#39;는 아이디어를 기본으로 한다.</p>
<p>서버는 클라이언트의 요청을 처리하기 위해 이를 위한 인터페이스를 구현하고, gRPC 서버를 가동시킨다.</p>
<p>클라이언트는 stub를 가진다.</p>
<pre><code>stub: 서버의 메서드와 동일한 메서드를 제공하는 것</code></pre><p><img src="https://grpc.io/img/landing-2.svg" alt="gRPC 개념도"></p>
<p>gRPC 클라이언트와 서버는 다양한 환경에서 작동할 수 있고, gRPC를 지원하는 어떠한 언어로도 작성될 수 있다.</p>
<h1 id="protocol-buffers">Protocol Buffers</h1>
<p>기본적으로 gRPC는 <code>Protocol Buffers</code>를 사용한다. </p>
<pre><code>Protocol Buffers
    - Google’s mature open source mechanism for serializing structured data </code></pre><p><code>protocol buffers</code>를 사용하려면 우선, <code>proto file</code>로 serialize하고 싶은 데이터의 구조를 정의해야 한다.</p>
<pre><code>proto file
    - 단순히 &#39;.proto&#39; 확장자의 text file을 의미함</code></pre><p>Protocol buffer data는 <code>message</code>라는 단위로 조직된다.</p>
<pre><code>message
    - a small logical record of information
    - containing a series of name-value pairs called `fields`.</code></pre><p><code>message</code>는 다음과 같은 구조를 가진다.</p>
<pre><code class="language-go">// message는 &#39;type key = value&#39; 형태의 field를 가진다
message Person {
  string name = 1; 
  int32 id = 2;
  bool has_ponycopter = 3;
}</code></pre>
<p><code>message</code>를 통해 데이터 구조를 한 다음, protocol buffer 컴파일러인 <code>protoc</code>을 사용해서 <code>data access classes</code>를 만든다.</p>
<pre><code>원하는 언어로 만들면 된다.</code></pre><p><code>data access class</code>는 각 필드에 접근할 수 있는 <code>accessors</code>를 제공한다.</p>
<pre><code>like name() and set_name(), as well as methods to serialize/parse the whole structure to/from raw bytes. So, for instance, if your chosen language is C++, running the compiler on the example above will generate a class called Person. You can then use this class in your application to populate, serialize, and retrieve Person protocol buffer messages.</code></pre><p>You define gRPC services in ordinary proto files, with RPC method parameters and return types specified as protocol buffer messages:</p>
<h1 id="실습">실습</h1>
<blockquote>
<p>참고
<a href="https://grpc.io/docs/languages/go/basics/">Basics tutorial</a></p>
</blockquote>
<ol>
<li><p><code>.proto</code> 확장자 파일을 만들어서 gRPC 통신을 위한 인터페이스를 정의한다. (마치 <code>gqlgen</code>에서 <code>schema.graphql</code>을 정의하는 것과 비슷)</p>
</li>
<li><p>다음 명령을 통해 <code>protoc</code>를 가지고 컴파일 (파일 generate)</p>
<pre><code class="language-bash">protoc --go_out=. --go_opt=paths=source_relative \
 --go-grpc_out=. --go-grpc_opt=paths=source_relative \
 helloworld/helloworld.proto</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[GraphQL 찍먹]]></title>
            <link>https://velog.io/@314_dev/GraphQL-%EC%B0%8D%EB%A8%B9</link>
            <guid>https://velog.io/@314_dev/GraphQL-%EC%B0%8D%EB%A8%B9</guid>
            <pubDate>Mon, 08 May 2023 05:51:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://tech.kakao.com/2019/08/01/graphql-basic/#%EC%BF%BC%EB%A6%AC%EB%AE%A4%ED%85%8C%EC%9D%B4%EC%85%98querymutation">GraphQL 개념잡기</a></p>
</blockquote>
<h1 id="graphql">GraphQL</h1>
<pre><code>SQL은 관계형데이터베이스를 다루는 문법
GraphQL (이하 gql)은 gql을 지원하는 서버에서 데이터를 요청하는 &#39;문법, 형식&#39;</code></pre><p><code>gql</code>은 HTTP Protocol의 POST Method위에서 작동한다. 
gql문법을 만족하는 데이터를 POST Method의 Body에 담아 서버로 전송하는 것이다.</p>
<p>주로 비교되는 <code>REST API</code>를 지원하는 서버는 여러 Endpoint를 가지고, 각 Endpoint는 한 가지 성격의 작업을 수행하는 반면,
gql은 1개의 Endpoint만을 가진다. gql 요청을 받은 서버측에서 요청을 분석한 뒤, 적합한 작업을 선택하여 수행하는 것이다. </p>
<pre><code>REST API 서버는 1(EndPoint):1(Operation) 쌍이 여러개 존재
GraphQL 서버는 1(EndPoint):M(Operation)이 1개 존재</code></pre><h1 id="gql-구조">gql 구조</h1>
<pre><code class="language-graphql"># 기본 형태 (일반 쿼리)
{
    hero {
        name
    }
}</code></pre>
<p><code>gql</code>은 크게 <code>Query</code>와 <code>Mutation</code>으로 구분된다.</p>
<pre><code>Query(질의)는 Read
Mutation(변조)은 Create, Update, Delete </code></pre><p>단순히 의미를 구체화하기 위해 개념적으로 구분한 것이다.</p>
<pre><code class="language-graphql">[query | mutation] {
    ...
}</code></pre>
<h2 id="query">Query</h2>
<pre><code class="language-graphql"># 일반쿼리
{
  human(id: &quot;1000&quot;) {
    name
    height
  }
}

# 오퍼레이션 네임 쿼리
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}</code></pre>
<p>쿼리는 <code>일반 쿼리</code>와 <code>오퍼레이션 네임 쿼리</code>로 구분할 수 있다.</p>
<p><code>일반 쿼리</code>는 다짜고짜 <code>중괄호</code>로 시작하고, <code>오퍼레이션 네임 쿼리</code>는 앞에 query가 붙어있다.</p>
<p><code>일반 쿼리</code>는 Specific한 내용을 담고있는 반면, <code>오퍼레이션 네임 쿼리</code>는 마치 함수처럼, <code>변수</code> 개념을 통해 재사용할 수 있다. <code>react apollo client</code>같은 gql을 구현한 클라이언트를 통해 변수에 값을 할당하여 사용한다.</p>
<p>여기서 일반적인 REST API와 프로그래밍 페러다임(?)적으로 차이점이 있는데, 
REST API는 데이터를 반환하는 쪽에서 어떤 데이터를 조회할지 SQL문을 통해 결정하는데,
GQL은 데이터를 요청하는 쪽에서 어떤 데이터를 조회할지 GQL을 통해 결정한다. </p>
<pre><code>SQL은 백엔드 개발자가 작성, 관리
GQL은 프론트엔드 개발자가 작성, 관리</code></pre><p>이러한 관계를 통해 프론트엔드가 백엔드 API의 Request, Response에 덜 의존적일 수 있다.</p>
<pre><code>그러나 데이터 스키마에는 여전히 의존적임을 지적함 -&gt; 애초에 극복할 수 있는 문제인가...?</code></pre><h1 id="schematype">schema/type</h1>
<p>의미는 데이터베이스의 스키마와 비슷하나, 형태는 C언어의 헤더 파일과 비슷하다.</p>
<pre><code class="language-graphql">type Character {
  name: String!
  appearsIn: [Episode!]!
}</code></pre>
<pre><code>오브젝트 타입 : Character
필드 : name, appearsIn
스칼라 타입 : String, ID, Int 등
느낌표(!) : 필수 값을 의미(non-nullable)
대괄호([, ]) : 배열을 의미(array)</code></pre><h1 id="resolver">Resolver</h1>
<p>SQL만 작성하면 DBMS가 알아서 데이터를 가져오는 것과 달리, GQL은 <code>resolver(이하 리졸버)</code>를 직접 구현함으로써 데이터를 가져오는 구체적인 과정을 명시해야 한다.</p>
<pre><code>gql 쿼리문 파싱은 보통 gql 라이브러리를 사용해서 처리</code></pre><p>리졸버를 직접 구현해야하는 부담은 있지만, 데이터 source에 상관 없이 구현 가능하다는 장점이 있다. </p>
<pre><code>리졸버를 통해 데이터를 데이터베이스에서 가져 올 수 있고, 일반 파일에서 가져올 수 있다.
심지어 http, SOAP와 같은 네트워크 프로토콜을 활용해서 원격 데이터를 가져올 수 있다.</code></pre><h2 id="graph의-의미"><code>Graph</code>의 의미</h2>
<p>gql 쿼리에서는 각각의 필드마다 <code>다음 타입을 반환하는 함수</code>가 하나씩 존재 한다. 그리고 그 함수가 바로 <code>리졸버</code>인 것이다.</p>
<p>리졸버는 다음과 같은 특징을 갖는다.</p>
<ol>
<li>필드가 스칼라 값(문자열이나 숫자와 같은 primitive 타입)인 경우에는 실행 종료</li>
<li>필드의 타입이 스칼라 타입이 아닌 우리가 정의한 타입이라면 해당 타입의 리졸버를 호출</li>
</ol>
<p>이처럼 리졸버는 필드에 따라 연쇄적으로 호출될 수 있고, 그 과정이 마치 그래프 탐색과 유사하다.</p>
<h1 id="graphql-서버-개발하기">GraphQL 서버 개발하기</h1>
<p><a href="https://velog.io/@314_dev/gplgen%EC%9C%BC%EB%A1%9C-GraphQL-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0">gplgen으로-GraphQL-서버-만들기</a>를 참고</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes]]></title>
            <link>https://velog.io/@314_dev/Kubernetes</link>
            <guid>https://velog.io/@314_dev/Kubernetes</guid>
            <pubDate>Sat, 06 May 2023 06:26:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://www.udemy.com/course/learn-kubernetes/?utm_source=adwords&amp;utm_medium=udemyads&amp;utm_campaign=LongTail_la.EN_cc.ROW&amp;utm_content=deal4584&amp;utm_term=_._ag_77879423894_._ad_535397245857_._kw__._de_c_._dm__._pl__._ti_dsa-1007766171032_._li_1009871_._pd__._&amp;matchtype=&amp;gclid=Cj0KCQjw0tKiBhC6ARIsAAOXutl5my1grpIx2PkmsTssA2RkVu_9slVlK8mzFWYeItQbMwNc3z3oMBMaAopUEALw_wcB">Kubernetes for the Absolute Beginners</a></p>
</blockquote>
<p>Docker Container로 띄운 서비스를 어떻게 관리해야 할까?</p>
<pre><code>Traffic에 따라 Scaling up, down을 어떻게 자동화할까?
Container의 상태를 어떻게 추적, 관리할까?
...</code></pre><p>Docker Container의 상태를 관리하는 도구가 여러가지가 있다.</p>
<pre><code>Docker swarm: 간단한 설정, 다양한 기능 제공 X
MESOS: 어려운 설정, 다양한 기능 제공 O
Kubernetes: 어려운 설정, 다양한 기능 제공 O</code></pre><p>그 중 <code>Kubernetes</code>에 대해 알아보도록 하겠다.</p>
<h1 id="kubernetes-basics">Kubernetes Basics</h1>
<p><code>Kubernetes(이하 k8s)</code>를 사용함으로써 다양한 이점을 얻을 수 있다.</p>
<ol>
<li>HW Failure가 SW Failure에 영향을 주지 않도록 할 수 있다.</li>
<li>Load Balancing</li>
<li>Container들을 체계적으로 관리할 수 있다.</li>
<li>...</li>
</ol>
<h2 id="k8s-architecture">k8s Architecture</h2>
<p><code>k8s</code>의 주요 컨셉을 살펴보자</p>
<h3 id="node">Node</h3>
<ul>
<li>k8s가 설치된 물리적(또는 가상) 머신</li>
<li>k8s에 의해 launch된 container들이 위치하는 곳 == <code>Worker Node</code></li>
<li>worker node들을 관리하는 node == <code>master node (아래에서 다룸)</code></li>
</ul>
<p>만약 한 개의 Node만 운영하면 어떻게 될까? Node에 장애가 발생하면 서비스가 죽을것이다. 
그렇기 때문에 Node는 2개 이상 유지해야 한다.</p>
<h3 id="cluster">Cluster</h3>
<ul>
<li>여러 Node들로 구성된 일종의 Group</li>
</ul>
<p>Cluster에 속한 Node들 중 하나가 죽더라도, 다른 Node들은 여전히 살아있으므로 서비스는 계속 작동할 것이다.
그리고 Cluster에 여러 Node를 유지함으로써 Load balancing을 할 수 있을 것이다.</p>
<h3 id="master-node">Master Node</h3>
<ul>
<li>Cluster와, Cluster에 속한 Node들을 관리, 모니터링하는 Node</li>
<li>Master Node도 Cluster에 속한다.</li>
<li>Cluster에 속한 다른 Worker Node의 상태를 모니터링하고, Failure가 발생한 Node의 workload를 다른 node로 옮긴다 == <code>Orchestration</code></li>
</ul>
<h2 id="k8s-components">k8s Components</h2>
<ul>
<li>node에 k8s를 설치한다 == k8s를 구성하는 여러 service들을 설치한다.<h3 id="api-server">API Server</h3>
</li>
<li>k8s 기능을 사용하기위한 인터페이스 역할<ul>
<li>k8s 기능을 사용 == 클러스터와의 interaction</li>
</ul>
</li>
</ul>
<h3 id="etcd">etcd</h3>
<ul>
<li>클러스터를 관리하기 위해 필요한 모든 데이터를 저장하기 위한 distributed reliable key value store<ul>
<li>클러스터에 존재하는 node 데이터를 ectd에 분산 방식으로 저장한다.</li>
</ul>
</li>
<li>etcd는 Master node들 사이에서 발생하느 conflict를 예방하기 위한 Lock을 지원한다.</li>
</ul>
<h3 id="kubelet">kubelet</h3>
<ul>
<li>클러스터에 속한 각 node에서 작동하는 agent</li>
<li>agent<ul>
<li>컨테이너가 node에서 정상적으로 작동하도록 함<h3 id="container-runtime">container runtime</h3>
</li>
</ul>
</li>
<li>container를 구동시키는 underlying SW<ul>
<li>Docker, rkt, cri-o 등이 SW에 해당</li>
</ul>
</li>
</ul>
<h3 id="controller">controller</h3>
<ul>
<li>k8s의 orchestration의 핵심<ol>
<li>node, container, end point의 정상 작동 여부를 확인 -&gt; 새로운 컨테이너 생성 여부를 결정</li>
</ol>
</li>
</ul>
<h3 id="scheduler">scheduler</h3>
<ul>
<li>여러 node들 사이의 work, container를 분산하는 역할<ul>
<li>새로 생성된 컨테이너를 찾으면 특정 node에 할당함</li>
</ul>
</li>
</ul>
<h2 id="기본-동작-원리">기본 동작 원리</h2>
<ol>
<li>Worker Node에는 <ol>
<li>컨테이너를 작동시키기 위한 <code>Container Runtime</code>(Ex. Docker)을 가지고 있음</li>
<li>Master Node와 통신하기 위한 <code>Kubelet Agent</code>를 가지고 있음</li>
</ol>
</li>
<li>Master Node에는<ol>
<li>Master Node가 Master로서의 기능을 지원하기 위한 <code>Kube API Server</code>를 가지고 있음<ul>
<li>Worker Node의 kubelet Agent와, Master Node의 kube API Server가 서로 통신하는 것<ul>
<li>Worker Node의 상태를 Master Node로 전송<ul>
<li>Master Node로 부터 명령을 받아 Worker Node를 조작</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Node들의 정보를 저장, 관리하기 위한 <code>etcd</code>를 가지고 있음</li>
<li>Node를 관리하기 위한 <code>Controller</code>, <code>Scheduler</code>를 가지고 있음<h1 id="pods">Pods</h1>
k8s는 컨테이너를 workder node에 직접적으로 배포하지 않는다.
대신, container를 <code>pod</code>라 불리는 k8s object 단위로 encapsulate한뒤 배포한다.<h2 id="pod란">Pod란</h2>
</li>
</ol>
</li>
</ol>
<ul>
<li>single instance of an application</li>
<li>smallest object that can be created in k8s</li>
</ul>
<p><img src="https://velog.velcdn.com/images/314_dev/post/3e463743-1849-49f7-8437-75292f15de4a/image.png" alt=""></p>
<ol>
<li>파이썬으로 만든 서버를 Container로 만든 뒤, 이를 <code>Pod</code>로 캡슐화한뒤 Worker Node에 배포한다.</li>
<li>서버를 Scale up해야 하는 상황이라면, <strong>한 Pod에 동일한 Container를 추가하지 않는다.</strong><ul>
<li>대신, <strong>동일한 구성의 Pod를 배포하는 단위로 Scaling을 진행한다.</strong></li>
<li>Pod를 배포하려는 Node의 capacity를 초과한다면, 새로운 Node를 생성한 뒤 Pod를 배포한다.</li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/314_dev/post/709fc57f-ce93-4209-8f0d-fd01fc601f63/image.png" alt="">
3. 파이썬 서버가 아닌, 다른 서비스 Container는 동일한 Pod에 배포할 수 있다.</p>
<ul>
<li>같은 Pod에 배포된 Container들은 저장공간, 네트워크 등의 자원을 공유한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/314_dev/post/fc63e8a2-b390-4150-8e1c-5f907de0b986/image.png" alt="">
파이썬 서버 컨테이너를 일일이 배포하는 방식으로 서비스를 운영해도 문제는 없다.
그런데, 서버 작동을 위한 helper container가 추가되거나, Scaling을 위해 서버 Container가 추가되는 경우에는 관리가 복잡해진다.</p>
<ul>
<li>서버 container - helper container 간 자원 매핑 등</li>
<li>서버 container 제거할 때, 연관된 helper container도 같이 제거</li>
<li>...</li>
</ul>
<p>k8s는 <code>Pod</code> Object 단위로 Container들을 관리하도록하여, 위의 불편함을 개선한다.</p>
<h2 id="kubectl로-pod-다루기">kubectl로 pod 다루기</h2>
<pre><code class="language-bash"># 단순히 Docker container run 하기
docker run container_name

# pod를 생성하여 Docker Container run 하기
kubectl run pod-name --image IMAGE_NAME

# Pods 목록 확인
kubectl get pods

# Pods 목록 확인 + wide
kubectl get pods -o wide

# Pods 디테일 확인
kubectl describe pod pod-name

# Pods 제거
kubectl delete pod pod-name

# cluster 목록 확인
kubectl config get-clusters

# Pod 접속
kubectl -it exec POD_NAME -n NAMESPACE_NAME -- /bin/bash</code></pre>
<p>기본적으로 Docker Image는 Docker hub (public image repository)에서 가져온다.</p>
<p>설정을 통해 이미지를 불러올 주소를 지정할 수 있다.</p>
<h2 id="pods-with-yaml-based-configuration">Pods with YAML based Configuration</h2>
<p>k8s은 Pod를 비롯한 Object를 생성하기 위해 YAML을 사용할 수 있다.</p>
<p>기본적으로 4개의 Required Top level Property를 가진다.</p>
<pre><code class="language-yaml"># pod-definition.yaml
apiVersion: v1
# Object를 생성하는 k8s API 버전을 지정하는 부분
# 생성하는 Object에 따라 적합한 API 버전을 사용해야 함
kind: Pod
# 생성하려는 Object의 종류를 지정하는 부분
metadata: 
# 생성하는 Object에 대한 정보를 명시하는 부분
# k8s이 제공하는 child만 사용 가능
    name: myapp-pod
    # Object의 이름을 명시
    labels:
        # dictionary 형태로, 원하는 형태의 key, value label을 명시 가능
        # label을 가지고 pod를 필터링, 검색할 때 사용할 수 있음
        app: myapp
        type: front-end

spec: 
# 생성하는 Object에 따라, k8s에 추가로 제공해야 하는 정보를 명시하는 부분
    containers:
        # containers 속성은 list 형식
        - name: nginx-container
          image: nginx</code></pre>
<p>위처럼 <code>yaml</code>형식으로 정의한 내용를 가지고 pod를 생성할 수 있다.</p>
<pre><code class="language-bash">kubectl craete -f pod-definition.yaml
# kubectl apply -f pod-definition.yaml</code></pre>
<h1 id="replication-controller">Replication Controller</h1>
<p>여러 개의 Pod을 운영함으로써 High Availability, Load Balancing, Scaling의 이점을 얻을 수 있다.</p>
<p><img src="blob:https://velog.io/1d59f717-1dfd-4dbf-9b77-92b95de41c9c" alt="업로드중.."></p>
<p><code>Replication Controller</code>는 Definition file에 명시한 내용에 따라 Pod을 관리한다.</p>
<pre><code>Pod 개수를 몇 개 유지하라
...</code></pre><p><code>Replication Controller</code>는 같은 Cluster에 위치한 여러 (Worker) Node에 걸쳐 사용된다.    </p>
<pre><code class="language-yaml"># Replication Controller Definition file
# rc-definition.yaml
apiVersion: v1
kind: ReplicationController
metadata:
    name: myapp-rc
    labels:
        app: myapp
        type: front-end
spec:
    # Replication Controller에서 생성할 Pod들을 정의하기 위한 Template 부분
    template:
        # 위에서 작성한 pod-definition.yaml과 동일한 형식, 내용을 사용한다
        metadata: 
            name: myapp-pod
        labels:
            app: myapp
            type: front-end

        spec: 
            containers:
                - name: nginx-container
                  image: nginx
    # 몇 개의 pod replica를 만들 것인가
    replicas: 3</code></pre>
<pre><code class="language-bash"># Replication Controller, Pod를 생성
kubectl create -f rc-definition.yaml

# Replication Controller 목록 확인
kubectl get replicationcontroller</code></pre>
<h1 id="replicaset">ReplicaSet</h1>
<p><code>ReplicaSet</code>은 <code>Replication Controller</code>와 동일한 목적을 가지나 차이점이 있다.</p>
<pre><code class="language-yaml"># ReplicaSet Definition file
# replicaset-definition.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myrs
  labels:
    app: rs

spec:
  template:
    metadata: 
      name: myapp-pod
      labels:
        app: myapp
        type: front-end

    spec: 
      containers:
        - name: nginx-container
          image: nginx
  replicas: 3
  selector:
    matchLabels:
      type: front-end</code></pre>
<pre><code class="language-bash"># ReplicaSet을 생성
kubectl create -f rs-definition.yaml

# ReplicaSet 목록 확인
kubectl get replicaset</code></pre>
<h2 id="selector">selector</h2>
<blockquote>
<p>ReplicaSet에 Pod definition이 들어있는데, 굳이 <code>selector</code>써서 자기가 만든 Pod들을 구분하려고 하는건가?</p>
</blockquote>
<p><code>Replication Controller</code>는 기본적으로, 자기에 정의된 Pod만을 관리(개수, 상태 등)하는 반면, <strong><code>ReplicaSet</code>은 자기 밖에서 만들어진 Pod도 관리할 수 있기 때문이다.</strong></p>
<pre><code>`Replication Controller`도 `selector`필드를 사용할 수는 있지만 필수는 아님</code></pre><p><code>ReplicaSet</code>은 자기가 만들지는 않았지만, 관리해야할 Pod를 식별할 수 있어야하는데, 이때 <code>selector</code>와 <code>label</code> 필드가 사용되는 것이다.</p>
<pre><code class="language-yaml"># ReplicaSet의 selector
spec:
    ...
    # 나는 pod가 어디에서 생성되었는지 상관 없이, label이 &#39;type: front-end&#39;인 pod를 관리할거야
    selector:
        matchLabels:
            type: front-end

# Pod의 label 정보
metadata:
    name: mypod-fe
    labels:
        type: front-end</code></pre>
<p>미리 만들어진 Pod가 3개가 있는데, ReplicaSet의 desired replica가 5라면, ReplicaSet은 2개의 Pod만 생성한다.</p>
<h2 id="scaling">Scaling</h2>
<blockquote>
<p>ReplicaSet을 통해 3개의 Pod를 운영하는 상황에서, Pod를 6개로 늘리고 싶으면 어떻게 할까</p>
</blockquote>
<p>여러 방법이 있다.</p>
<h3 id="definitionyaml-수정하고-replace하기">definition.yaml 수정하고 replace하기</h3>
<p><code>ReplicaSet</code>의 definition file의 <code>replicas</code>필드를 수정하고 replace한다.</p>
<p><code>kubectl replace -f replicaset-definition.yaml</code></p>
<h3 id="scale-명령-사용하기">scale 명령 사용하기</h3>
<h4 id="definition-file을-전달하기">definition file을 전달하기</h4>
<p><code>kubectl scale --replicas=6 -f replicaset-definition.yaml</code></p>
<h4 id="type-name을-전달하기">type, name을 전달하기</h4>
<p><code>kubectl scale --replicas=6 -f replicaset my-replicaset-name</code></p>
<h1 id="deployments">Deployments</h1>
<p><img src="blob:https://velog.io/204a7a8c-b8e6-4a77-a1d9-97fc68b0f8d5" alt="업로드중..">
<code>Deployment</code>는 말 그대로 <code>배포와 관련한 편의성</code>을 제공하기 위한 k8s Object이다.</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: myrs
  labels:
    app: rs

spec:
  template:
    metadata: 
      name: myapp-pod
      labels:
        app: myapp
        type: front-end

    spec: 
      containers:
        - name: nginx-container
          image: nginx
  replicas: 3
  selector:
    matchLabels:
      type: front-end</code></pre>
<p><code>Deployment</code>는 <code>ReplicaSet</code>과 동일한 definition file구조를 가진다.</p>
<p><code>kubectl create 0f dp-definition.yaml</code>을 통해 <code>Deployment</code>를 생성하면 자동으로 <code>ReplicaSet</code>이 생성되고, <code>ReplicaSet</code>이 생성됨에 따라 자동으로 <code>Pod</code>가 생성된다.</p>
<h2 id="배포와-관련한-편의성">배포와 관련한 편의성</h2>
<blockquote>
<p><code>Deployment</code>가 <code>ReplicaSet</code>보다 더 큰 개념인건 알겠는데, 무슨 장점이 있는것일까</p>
</blockquote>
<h3 id="rollout-and-versionning">Rollout and Versionning</h3>
<p><code>kubectl create dp.yaml</code>을 수행하면 <code>rollout</code>이 트리거된다. 그리고 <code>rollout</code>은 새로운 <code>deployment revision</code>을 생성한다.</p>
<p>새로운 App 버전이 나와서 Container가 변경되면, <code>rollout</code>이 또 트리거되어 새로운 <code>revision</code>이 생성된다.</p>
<p><code>revision</code> 기록은 계속 누적되고, 이를 통한 버저닝이 가능하다.</p>
<pre><code class="language-bash"># 특정 deployment의 rollout 상태 확인하기
kubectl rollout status deployment/my-deployment-name

# 특정 deployment의 rollout history 확인하기
kubeclt rollout history deployment/my-deployment-name
kube</code></pre>
<h3 id="deployment-strategy">Deployment Strategy</h3>
<p>Deployment에 속한 Pod들을 Update하려고 한다.</p>
<ol>
<li>기존 Pod delete</li>
<li>신규 Pod create</li>
</ol>
<p>위와 같은 <code>Recreate</code>전략은 &#39;삭제-생성&#39; 간격동안 서비스를 제공할 수 없다.</p>
<p>Deployment는 대신에 기본값으로 <code>RollingUpdate</code>전략을 사용한다.</p>
<p><img src="blob:https://velog.io/965935f2-7d2b-424c-83b5-d347562c64f2" alt="업로드중.."></p>
<h3 id="rollingupdate">RollingUpdate</h3>
<p><img src="blob:https://velog.io/6783f175-8666-4904-b462-83a19a4c02f0" alt="업로드중.."></p>
<p><code>Recreate</code>전략을 수행할 경우 replicaset은 다음의 흐름으로 작동한다.</p>
<ol>
<li>replicaset의 <code>replicas</code> 값을 0으로 설정 -&gt; pod all delete</li>
<li>replicaset의 <code>replicas</code> 값을 5으로 설정 -&gt; pod all create</li>
</ol>
<p><code>RollingUpdate</code>전략을 수행할 경우 replicaset은 다음의 흐름으로 작동한다.</p>
<ol>
<li>replicaset-2의 Pod + 1</li>
<li>replicaset-1의 Pod -1</li>
<li>replicaset-2가 desired state가 될 때 까지 1,2 반복</li>
</ol>
<h3 id="rollback">Rollback</h3>
<p><code>kubectl rollout undo deployment/myapp-deployment</code></p>
<p>롤백 역시 RollingUpdate기반으로 작동한다.</p>
<ol>
<li>replicaset-1의 Pod + 1 (이전 버전 Pod 생성)</li>
<li>replicaset-2의 Pod -1 (현재 버전 Pod 제거)</li>
<li>replicaset-1이 desired state가 될 때 까지 1,2 반복</li>
</ol>
<h3 id="deployment-update">Deployment update</h3>
<p>deployment의 definition file을 변경하고 이를 반영하자</p>
<h4 id="apply-명령-사용하기">apply 명령 사용하기</h4>
<ol>
<li>definition file 내용 변경하기 (Ex. replica 개수, pod image 등)</li>
<li><code>kubectl apply -f dp-definition.yaml</code> 수행</li>
</ol>
<p>그러면 deployment는 기본적으로 rolling update 방식으로 업데이트를 수행할 것이다.</p>
<h4 id="set-명령-사용하기">set 명령 사용하기</h4>
<p>특정 필드의 값만 지정하여 업데이트 할 수 있다.
<code>kubectl set image deployment/my-dp-name nginx-continaer=nginx:1.9.1</code></p>
<p>변경사항이 definition file에 영구적으로 반영되지 않는 다는 점을 주의해야 한다 (ReplicaSet도 마찬가지)</p>
<h1 id="services">Services</h1>
<p>네트워킹과 관련한 기능을 지원하는 k8s object
세 종류의 타입이 있다.</p>
<h2 id="nodeport">NodePort</h2>
<p>Node에 속한 Pod와 Node의 port를 연결하여 Pod가 accessible하게 만드는 기능
<img src="blob:https://velog.io/18d4e1d7-3b3a-40c0-a1b0-ea2e6c2e0499" alt="업로드중.."></p>
<ol>
<li>TargetPort<ul>
<li><code>Service</code>가 요청을 전달하려는 Pod의 포트</li>
</ul>
</li>
<li>Port<ul>
<li><code>Service</code>의 포트</li>
</ul>
</li>
<li>NodePort<ul>
<li>외부에서 Pod에 접근하기 위한 Node의 포트</li>
</ul>
</li>
</ol>
<p><code>Service</code>는 마치 Node의 가상 서버이다. 클러스터 내부에서 서비스는 고유한 IP(ClusterIP)를 갖는다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
    name: myapp-service

spec:ㄹ
    type: NodePort
    ports: # 여러 port를 지정할 수 있다
        - targetPort: 80 # optional: 따로 설정하지 않으면 port과 같은 번호를 사용
          port: 80 # mandatory
          nodePort: 30008 # optional: 따로 설정하지 않으면 30000 ~ 32767값으로 자동 설정
    # 어떤 Pod와 포트를 매핑시킬지 selector를 통해 지정
    selector:
        app: myapp
        type: front-end

## pod-definition.yaml
...
metadat:
    labels:
        app: myapp
        type: front-end
...</code></pre>
<pre><code class="language-bash"># 생성
kubectl create -f service-definition.yaml
# 조회
kubectl get services
# Node의 IP, Port를 통해 Pod에 접근 가능함
curl http://NODE_IP_ADD:NODE_PORT</code></pre>
<h3 id="한-node에-여러-pod">한 Node에 여러 Pod</h3>
<blockquote>
<p>한 Node에 여러 Pod이 있는 경우는 어떻게 할까</p>
</blockquote>
<p><img src="blob:https://velog.io/f984af86-4511-47e7-b604-29f6f10c86cf" alt="업로드중.."></p>
<p>Pod가 여러 개인 경우에도 별도로 설정 없이, Pod가 1개인 경우처럼 Service의 <code>selector</code>로 Pod의 Label과 매핑하면 알아서 포트 작업을 진행한다.</p>
<p>별도의 설정 없이 <code>Service</code>가 LoadBalancer 기능을 수행한다. </p>
<pre><code>기본 LoadBalance 알고리즘은 &#39;random&#39;
Service definition의 &#39;Algorithm&#39;필드를 통해 방식을 변경할 수 있다.</code></pre><h3 id="여러-node에-여러-pod">여러 Node에 여러 Pod</h3>
<p>이 경우에도 별다른 설정 없이 없어도, 클러스터 내부의 여러 Node에 걸쳐 작동하는 서비스가 생성되고, 동일한 <code>NodePort</code>를 통해 접근할 수 있게 된다.</p>
<p><img src="blob:https://velog.io/512d38b3-ea20-4040-9510-6a7a1df5e105" alt="업로드중.."></p>
<h2 id="clusterip">ClusterIP</h2>
<p><img src="blob:https://velog.io/8e31b810-051a-4c41-b7b0-86922d92babf" alt="업로드중.."><em>각 계층의 서비스를 여러 Pod들로 지원하는 상황</em></p>
<p>Pod들은 상태에 따라 계속 생성, 제거를 반복한다. 그런 상황에서 계층간의 연결을 Pod의 IP로 하면 문제가 발생할 것이다.</p>
<p>즉, static한 IP를 가진 클러스터 역할이 필요한데, <code>clusterIP</code> 타입의 서비스로 해결할 수 있다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
    name: back-end
spec:
    type: ClusterIP # Service의 기본 type은 ClusterIP
    ports:
        - targetPort: 80 # Pod들의 포트
          port: 80 # Service의 포트
    selector: # Back-end 서버 Pod들의 Label로 식별
        app: myapp
        type: back-end</code></pre>
<h2 id="loadbalancer">LoadBalancer</h2>
<pre><code>1. 여러 Node에 분산된 Pod들로 구성된 서비스가 존재한다.
2. Node마다 IP 주소가 존재한다.
Q: 사용자는 어떤 IP(Node)로 요청을 보내야 하는가?</code></pre><pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
    name: back-end
spec:
    type: LoadBalancer
    ports:
        - targetPort: 80 # Pod들의 포트
          port: 80 # Service의 포트
          nodePort: 30008 # Node의 포트</code></pre>
<p>클라우드 플랫폼 (Ex. AWS, GCP, ...)에서는 단순히 Service type을 <code>LoadBalancer</code>로 설정하면 알아서 로드밸런싱을 해준다.</p>
<pre><code>on-premise 환경을 직접 구축해야 함</code></pre><h1 id="namespace">Namespace</h1>
<blockquote>
<p>참고</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Docker]]></title>
            <link>https://velog.io/@314_dev/K8s</link>
            <guid>https://velog.io/@314_dev/K8s</guid>
            <pubDate>Sat, 06 May 2023 04:54:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://www.udemy.com/course/learn-kubernetes/?utm_source=adwords&amp;utm_medium=udemyads&amp;utm_campaign=LongTail_la.EN_cc.ROW&amp;utm_content=deal4584&amp;utm_term=_._ag_77879423894_._ad_535397245857_._kw__._de_c_._dm__._pl__._ti_dsa-1007766171032_._li_1009871_._pd__._&amp;matchtype=&amp;gclid=Cj0KCQjw0tKiBhC6ARIsAAOXutl5my1grpIx2PkmsTssA2RkVu_9slVlK8mzFWYeItQbMwNc3z3oMBMaAopUEALw_wcB">Kubernetes for the Absolute Beginners</a></p>
</blockquote>
<h1 id="container">Container</h1>
<blockquote>
<p>구체적인 구분이 없다면 Container와 Docker는 동일한 기술을 의미함</p>
</blockquote>
<ol>
<li>사용하려는 컴포넌트가 구동 환경(Ex. OS)과 호환이 되어야 한다.</li>
</ol>
<p>다음과 같은 경우가 발생할 수 있다.</p>
<ol>
<li>컴포넌트에 변경이 생기면, 이와 관련된 다른 컴포넌트와의 호환성을 고려해야 한다.</li>
<li>새로운 개발자를 위해 환경을 설정하는 과정이 반복적, 복잡하다.</li>
<li>개발자마다 사용하는 개발 환경이 다를 수 있다.</li>
</ol>
<p><code>컴포넌트를 다른 컴포넌트, OS와 독립적으로 관리</code>하기 위해 컨테이너 기술을 사용한다. </p>
<p>컨테이너 기술을 사용함으로써, 컴포넌트들은 각자의 환경, 필요로 하는 라이브러리, 의존성을 사용할 수 있게 된다.</p>
<p><img src="https://velog.velcdn.com/images/314_dev/post/43a2e532-4153-4404-811a-387f85df7135/image.png" alt=""></p>
<h2 id="container란">Container란</h2>
<ul>
<li>완벽히 독립된 환경을 의미<ul>
<li>고유한 프로세스, 서비스를 실행할 수 있음</li>
<li>네트워킹 인터페이스, 저장공간을 가질 수 있음</li>
<li>LXC, LXD, LXCFS와 같은 다양한 Container 기술이 존재하나 <code>Docker</code>가 대표적인 기술로 주로 사용된다.</li>
<li><strong>컨테이너는 동일한 OS의 커널을 공유한다</strong>    </li>
</ul>
</li>
</ul>
<pre><code>    Docker는 LXC를 기반으로 만들어진 컨테이너 기술
    사용자가 편하게 컨테이너 기술을 사용할 수 있도록 도와주는 것 = Docker</code></pre><h2 id="docker는-os-kernel을-공유한다">&#39;Docker는 OS kernel을 공유한다&#39;</h2>
<p><code>Docker는 OS kernel을 공유한다</code>의 의미는 크게 두 가지로 풀이될 수 있다.</p>
<h3 id="os-호환성">OS 호환성</h3>
<p>Linux환경을 예로 들면,</p>
<ol>
<li>Common Linux Kernel은 하드웨어를 다룰 수 있도록 도와준다.</li>
<li>OS(Ex. Ubuntu, Fedora, ...)은 각자만의 <code>유저 인터페이스, 드라이버, 컴파일러, 파일 시스템 등(Set of SW)</code>을 통해 Common Linux Kernel을 Customize한다.</li>
</ol>
<p>만약 Ubuntu에서 Container를 정의했다면, 그 Container는 Fedora에서도 사용할 수 있다. 
즉, 같은 OS Kernel을 사용하는 어떠한 OS 환경에서도 공유할 수 있는 것이다.</p>
<pre><code>그러므로, Ubuntu에서 정의한 컨테이너를 Fedora에서는 실행시킬 수 있는데, Windows에서는 실행시킬 수 없다.</code></pre><p>Docker는 OS에 맞게, 필요한 <code>Set of SW</code>만 변경해서 OS 호환성을 지원한다.</p>
<h3 id="vm에-구분되는-container의-특성">VM에 구분되는 Container의 특성</h3>
<p><img src="https://velog.velcdn.com/images/314_dev/post/790f2e17-83ff-423c-b584-212fec7b0fdd/image.png" alt="">
Virtual Machine은 Host OS와는 별도로 각자마다 OS를 가진다. 그렇기 때문에 Container에 비해 상대적으로 Utilazation, Isolation이 높다.
그러나 더 큰 Size를 차지하고, Boot up 시간을 더 요구한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Docker</th>
<th>Virtual Machine</th>
</tr>
</thead>
<tbody><tr>
<td>OS</td>
<td>Host OS위에서 작동</td>
<td>각자의 OS를 가짐</td>
</tr>
<tr>
<td>Utilzation</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>Isolation</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>Size</td>
<td>작음</td>
<td>큼</td>
</tr>
<tr>
<td>Boot up</td>
<td>빠름</td>
<td>느림</td>
</tr>
</tbody></table>
<h2 id="public-docker-registry">Public Docker Registry</h2>
<p>Public Docker Registry (Docker hub)를 통해 미리 만들어진 Docker Image를 공유할 수 있다.
단순히 다음 명령을 통해 Docker Container를 구동시킬 수 있다.</p>
<pre><code class="language-bash">docker run IMAGE_NAME</code></pre>
<h2 id="image-container">Image, Container</h2>
<p><code>Image</code>는 일종의 Template, 
<code>Container</code>는 Image에 대한 Instance</p>
<p>직접 Image를 만들어서 public repository에 공유할 수 있다.</p>
<h1 id="docker-명령">Docker 명령</h1>
<pre><code class="language-bash"># Repository에서 Image 불러오기
docker pull IMAGE_NAME

# 컨테이너 목록 확인
docker ps

# 컨테이너 접속
docker exec -u root -it COTAINER_ID(or CONTAINER_NAME) bash

# Docker-compose 실행
docker-compose up -d

# Docker-composer 종료</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Postgresql: psql]]></title>
            <link>https://velog.io/@314_dev/Postgresql-psql</link>
            <guid>https://velog.io/@314_dev/Postgresql-psql</guid>
            <pubDate>Thu, 27 Apr 2023 00:08:20 GMT</pubDate>
            <description><![CDATA[<h1 id="psql이란">psql이란</h1>
<h2 id="database-schema-table">Database, Schema, Table</h2>
<blockquote>
<p>참고: <a href="https://www.devkuma.com/docs/postgresql/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%8A%A4%ED%82%A4%EB%A7%88-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%98-%EA%B4%80%EA%B3%84/">PostgreSQL | 스키마(Schema) | 데이터베이스, 스키마, 테이블의 관계</a></p>
</blockquote>
<h1 id="psql-명령어-정리">psql 명령어 정리</h1>
<blockquote>
<p>참고: <a href="https://browndwarf.tistory.com/51">알아두면 유용한 psql 명령어 정리</a></p>
</blockquote>
<pre><code class="language-sql"># psql 접속
psql
# 특정 유저가, 특정 DB에 접속
psql -U USER_NAME -d DATABASE_NAME
# 외부 스크립트 실행
psql -f SCRIPT_PATH

-U : 유저
-h : 호스트
-p : 포트
-d : Traget DB
# 데이터베이스 목록 조회
\list (\l)
# 데이터베이스 전환
\c DATABASE_NAME
# 유저 목록 조회
\du
# 유저 변경
\c USER_NAME DATABASE_NAME
# 외부 Query Script 실행
\i QUERY_SCRIPT_PATH

# 테이블 describe
\d+ TABLE_NAME</code></pre>
<h1 id="postgresql-특화-쿼리">PostgreSQL 특화 쿼리</h1>
<h2 id="on-conflict-do-update">ON CONFLICT DO UPDATE</h2>
<h1 id="enum">ENUM</h1>
<blockquote>
<p>참고
<a href="https://www.postgresql.org/docs/current/sql-createtype.html">psql 공식문서</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Go: Package, Module]]></title>
            <link>https://velog.io/@314_dev/Go-Package</link>
            <guid>https://velog.io/@314_dev/Go-Package</guid>
            <pubDate>Tue, 25 Apr 2023 13:19:13 GMT</pubDate>
            <description><![CDATA[<h1 id="package">Package</h1>
<blockquote>
<p>참고
<a href="https://dev-yakuza.posstree.com/ko/golang/package/">[Golang] Package</a>
<a href="http://golang.site/go/article/15-Go-%ED%8C%A8%ED%82%A4%EC%A7%80">1. Go 패키지</a></p>
</blockquote>
<p><code>패키지</code></p>
<ul>
<li>코드를 묶는 기본 단위 -&gt; 모든 코드는 반드시 패키지로 묶어야 함.</li>
</ul>
<p><code>표준 라이브러리</code></p>
<ul>
<li>Go가 미리 제공하는 라이브러리</li>
<li><code>GOROOT/pkg</code> 안에 존재한다. <h2 id="goroot-gopath">GOROOT, GOPATH</h2>
터미널에 <code>go env</code>명령을 치면 여러 정보들이 출력된다.<pre><code class="language-text">GOPATH=&quot;/Users/wonju/go&quot;
...
GOROOT=&quot;/usr/local/go&quot; // `GOROOT`는 Go 설치 디렉토리를 가리킨다.
...</code></pre>
</li>
</ul>
<p>패키지를 import 할 때, Go 컴파일러는 <code>GOROOT</code>, <code>GOPATH</code> 환경변수를 사용하여, 다음과 같은 위치에서 패키지를 찾는다.</p>
<ol>
<li>표준 패키지는 <code>GOROOT/pkg</code></li>
<li>사용자 패키지, 3rd Party 패키지는 <code>GOPATH/pkg</code></li>
</ol>
<p><code>GOROOT</code>는 Golang설치시에 자동으로 시스템에 설정되지만, <code>GOPATH</code>는 사용자가 지정해야 한다.</p>
<h2 id="main-package-main-func">main package, main func</h2>
<p><code>main</code>패키지를 제외한 다른 패키지들은 <code>main</code>함수를 포함하지 않고, <code>main</code>패키지의 의 보조 패키지로 취급된다.</p>
<p>일반적인 패키지는 라이브러리로 사용되는 반면, <code>main</code>패키지는 Go Compiler에 의해 특별하게 인식된다. 
컴파일러는 <code>main</code>패키지를 공유 라이브러리가 아닌 실행(executable) 프로그램으로 만든다. </p>
<p><code>main</code>패키지의 <code>main</code>함수는 프로그램의 Entry Point로 사용된다. 따라서 패키지를 공유 라이브러리로 만들 때에는, main 패키지나 main 함수를 사용해서는 안된다.</p>
<h1 id="module">Module</h1>
<blockquote>
<p>참고: <a href="https://dev-yakuza.posstree.com/ko/golang/module/">[Golang] Module</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Go: Channel]]></title>
            <link>https://velog.io/@314_dev/Go-Channel</link>
            <guid>https://velog.io/@314_dev/Go-Channel</guid>
            <pubDate>Sat, 22 Apr 2023 14:15:21 GMT</pubDate>
            <description><![CDATA[<h1 id="채널">채널</h1>
<blockquote>
<p>참고
<a href="https://edu.goorm.io/learn/lecture/2010/%ED%95%9C-%EB%88%88%EC%97%90-%EB%81%9D%EB%82%B4%EB%8A%94-%EA%B3%A0%EB%9E%AD-%EA%B8%B0%EC%B4%88/lesson/372559/%EA%B3%A0%EB%A3%A8%ED%8B%B4%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%86%B5%EB%A1%9C-%EC%B1%84%EB%84%90">한 눈에 끝내는 고랭 기초 - 채널</a></p>
</blockquote>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    a := 1
    b := 2
    result := 0

    func() {
        result = a + b
    }()

    fmt.Println(result) // 3
}</code></pre>
<p>클로저를 통해 <code>result == 3</code>임을 알 수 있다.
클로저(익명 함수)도 Goroutine으로 호출할 수 있다.</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    a := 1
    b := 2
    result := 0

    go func() {
        result = a + b
    }()

    fmt.Println(result) // 0
}</code></pre>
<p>그런데 결과값이 3이 아닌 0이 나온다. 
앞서 살펴봤듯, Goroutine이 종료되기를 기다리지 않기 때문이다.</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    a := 1
    b := 2
    result := 0

    go func() {
        result = a + b
    }()

    // s1. 억지로 Goroutine 끝날때 까지 기다리기
       // time.Sleep(time.Duration(1000))

    // s2. fmt.Scanln으로 강제로 block
    // s3. sync.WaitGroup 사용하기

    fmt.Println(result) // 0
}</code></pre>
<p>크게 3가지 방법으로 해결할 수 있을 것 같은데, 생각해 보면 세 방법 모두 Goroutine이 끝나기를 기다리는 것이지, Goroutine의 흐름을 제어하는건 아니다.</p>
<p><code>Channel</code>은 두 용도로 사용된다.</p>
<ol>
<li>고루틴 사이에서 값을 주고받는 통로 역할을 하고, 송/수신자가 서로를 기다리는 속성때문에 고루틴의 흐름을 제어함</li>
<li>채널의 데이터를 주고 받을때까지 해당 고루틴을 종료하지 않아 별도의 lock을 하지 않고도 데이터를 동기화함</li>
</ol>
<p>Channel은 다음과 같이 사용한다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
)

func main() {
    a := 1
    b := 2
    result := 0

    channel := make(chan int)

    go func() {
        channel &lt;- a + b
    }()

    result = &lt;-channel

    fmt.Println(result)
}</code></pre>
<p>Goroutine에서 채널에 데이터를 <code>&lt;-</code> 키워드로 송신한다.
Goroutine에서 받은 데이터를 <code>&lt;-</code> 키워드로 변수에 할당할 수 있다. (꼭 변수에 할당해야 하는건 아니다)</p>
<p>중요한 점은 Goroutine의 종료 시점이다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
)

func main() {
    defer fmt.Println(&quot;main function finished&quot;)
    a := 1
    b := 2
    result := 0

    channel := make(chan int)

    go func() {
        defer fmt.Println(&quot;Goroutine finished&quot;)
        channel &lt;- a + b
    }()

    fmt.Println(result)
}
</code></pre>
<p>Goroutine A (main)에서 Goroutine B (익명 함수)에서 송신한 데이터를 사용하지 않고 있다.</p>
<pre><code>// 실행 결과
0
main function finished</code></pre><p><strong>Goroutine B는 Goroutine A에서 데이터를 수신할 때 까지 기다린다.</strong> 
그런데 Goroutine A에서 데이터를 수신하지 않았으므로, Goroutine A가 종료되었음에도 Goroutine B는 종료되지 않는다. 
따라서 <code>Goroutine finished</code>는 출력되지 않는 것이다.</p>
<h1 id="deadlock">Deadlock</h1>
<p>다음과 같은 상황이 있다.</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    c := make(chan string)

    c &lt;- &quot;Hello goorm!&quot;

    fmt.Println(&lt;-c)
}</code></pre>
<p><code>메인 함수</code>에서 채널을 통해, 다른 고루틴으로 데이터를 보내려고한다.
그런데 채널을 통해 데이터를 수신하는 고루틴이 없다.
즉, 다른 고루틴에서 데이터를 수신하지 않으므로 <code>메인 함수</code> 고루틴은 종료되지 않고 Deadlock에 빠지게 된다.</p>
<p>이 예제를 통해, <strong>송/수신을 위한 고루틴을 만들고 수신자와 송신자가 1:1 대응하지 않으면 DeadLock에 빠질 수도 있다</strong>는 사실을 알 수 있다.</p>
<h1 id="채널-버퍼">채널 버퍼</h1>
<p>그런데 고루틴에서 채널을 통해 송신하는 데이터를, 다른 고루틴에서 항상 수신하도록 하는건 불편하다.</p>
<p>이를 송신자, 수신자가 <code>채널 버퍼</code>를 통해 데이터를 전송하도록 함으로써 해결할 수 있다.
<img src="https://velog.velcdn.com/images/314_dev/post/9e4ade59-d632-470b-9c6f-f6f4507bef98/image.png" alt=""><em>이미지 출처: <a href="https://edu.goorm.io/learn/lecture/2010/%ED%95%9C-%EB%88%88%EC%97%90-%EB%81%9D%EB%82%B4%EB%8A%94-%EA%B3%A0%EB%9E%AD-%EA%B8%B0%EC%B4%88/lesson/373284/%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B1%84%EB%84%90%EA%B3%BC-%EB%B2%84%ED%8D%BC">한 눈에 끝내는 고랭 기초 - 비동기 채널과 버퍼</a></em></p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    channel := make(chan string) // 채널 생성
    buffer := make(chan string, 1) // 크기가 1인 버퍼 생성

    buffer &lt;- &quot;Hello goorm!&quot;

    fmt.Println(&lt;-buffer) // 정상적으로 출력 후 종료된다.
}</code></pre>
<p><code>채널 버퍼</code>는 다음의 규칙을 따른다.</p>
<ol>
<li>송신 루틴은 버퍼가 가득차면 대기한다.<ul>
<li>보내고 할 일을 함. </li>
<li>보낸 순간 버퍼가 가득찼으면 대기, 버퍼에 빈 공간이 생기면 하던 일 마저 끝냄.</li>
</ul>
</li>
<li>수신 루틴은 버퍼에 값이 없으면, 버퍼에 값이 들어올 때까지 대기한다.</li>
</ol>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;sync&quot;
)

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)
    buffer := make(chan int, 5)

    go produce(buffer)
    go consume(buffer, wg)
    wg.Wait()
}

func produce(buffer chan int) {
    for i := 0; i &lt; 10; i++ {
        buffer &lt;- i
        fmt.Println(&quot;produce: &quot;, i)
    }
}

func consume(buffer chan int, wg *sync.WaitGroup) {
    for i := 0; i &lt; 10; i++ {
        num := &lt;-buffer
        fmt.Println(&quot;consume: &quot;, num)
    }
    wg.Done()
}</code></pre>
<ol>
<li><code>produce</code>와 <code>consume</code>은 각각 별도의 고루틴으로 실행된다.</li>
<li>produce는, consume 여부와 상관없이 루프를 돌면서 값을 넣는다.<ul>
<li>그 과정에서 버퍼가 꽉차면 block</li>
</ul>
</li>
<li>consume은 produce 여부와 상관없이 루프를 돌면서 값을 꺼낸다.<ul>
<li>그 과정에서 버퍼가 비어있으면 block</li>
</ul>
</li>
</ol>
<p>그러면 동일한 상황에서, consume하는 데이터의 개수가 더 많으면 어떻게 될까?</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;sync&quot;
)

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)
    buffer := make(chan int, 5)

    go produce(buffer)
    go consume(buffer, wg)
    wg.Wait()
}

func produce(buffer chan int) {
    for i := 0; i &lt; 10; i++ {
        buffer &lt;- i
        fmt.Println(&quot;produce: &quot;, i)
    }
}

func consume(buffer chan int, wg *sync.WaitGroup) {
    // 프로듀서는 10개만 생성하는데, 컨슈머는 100개의 데이터를 소비함
    for i := 0; i &lt; 100; i++ {
        num := &lt;-buffer
        fmt.Println(&quot;consume: &quot;, num)
    }
    wg.Done()
}</code></pre>
<p>컨슈머는 버퍼가 비어있으면 데이터가 생길 때 까지 기다린다. 
그런데 데이터는 10개밖에 생기지 않으므로 결국 consumer는 deadlock에 빠지게 된다.</p>
<p><strong>따라서 송/수신 채널의 개수를 잘 맞춰야한다.</strong></p>
<h1 id="채널-닫기">채널 닫기</h1>
<p>Deadlock이 발생하는 상황을 정리하자면 다음과 같다.</p>
<pre><code>if Produce 개수 &lt; Consume 개수 {
    Consumer goroutine이 무한 대기
}</code></pre><pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    channel := make(chan bool, 2)
    channel &lt;- true
    channel &lt;- true

    fmt.Println(&lt;-channel)
    fmt.Println(&lt;-channel)
    fmt.Println(&lt;-channel) // 비어있는 채널을 읽으면 deadlock 발생
}</code></pre>
<p>위와 같은 상황에서 채널을 닫으면 어떻게 될까</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    channel := make(chan string, 2)
    channel &lt;- &quot;hello&quot;
    channel &lt;- &quot;world&quot;

    value, isOpen := &lt;-channel
    fmt.Println(value, isOpen) // hello true
    value, isOpen = &lt;-channel
    fmt.Println(value, isOpen) // world true
    close(channel)
    value, isOpen = &lt;-channel
    fmt.Println(value, isOpen) // &quot;&quot; false
}</code></pre>
<ol>
<li><p>채널에 값이 없는 경우, 닫힌 채널에서 값을 읽으면 데드락이 발생하지 않고 빈 값을 반환한다.</p>
<pre><code>   primitive type이면 기본값
 pointer 등이면 nil</code></pre><ul>
<li>닫힌 채널에 값을 읽는 시도는 할 수 있다.</li>
</ul>
</li>
<li><p>닫힌 채널에 값을 넣으면 panic이 발생한다. (send on closed channel)</p>
<h1 id="채널-iterate">채널 iterate</h1>
<h2 id="무한-루프">무한 루프</h2>
<pre><code class="language-go">package main
</code></pre>
</li>
</ol>
<p>import &quot;fmt&quot;</p>
<p>func main() {
    channel := make(chan string, 2)
    channel &lt;- &quot;hello&quot;
    channel &lt;- &quot;world&quot;</p>
<pre><code>for {
    if value, isOpen := &lt;-channel; isOpen {
        fmt.Println(value, isOpen)
    } else {
        break
    }
}</code></pre><p>}</p>
<pre><code>isOpen이 false가 되려면, 채널이 close되어야만 한다.
그런데 위 코드는 채널이 닫히지 않았으므로 무한 루프, 즉 Deadlock에 빠지게 된다.

그러므로 채널을 for loop돌려면 close 해야 한다.
``` go
package main

import &quot;fmt&quot;

func main() {
    channel := make(chan string, 2)
    channel &lt;- &quot;hello&quot;
    channel &lt;- &quot;world&quot;

    close(channel)

    for {
        if value, isOpen := &lt;-channel; isOpen {
            fmt.Println(value, isOpen)
        } else {
            break
        }
    }
}</code></pre><h2 id="채널--for-range">채널 + for range</h2>
<p>채널에 <code>for ragne</code>를 도입해서 개선할 수 있다</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    channel := make(chan string, 2)
    channel &lt;- &quot;hello&quot;
    channel &lt;- &quot;world&quot;

    close(channel)

    for value := range channel {
        fmt.Println(value)
    }
}</code></pre>
<p>마찬가지로 채널은 close되어야 한다. 
채널에 <code>for range</code>를 사용할 경우, <code>isOpen</code>은 사용할 수 없다.</p>
<h1 id="채널-방향">채널 방향</h1>
<p>사실 채널은 방향을 정할 수 있다.</p>
<pre><code>방향 = 송신, 수신 여부</code></pre><p>기본적으로 채널은 송신, 수신이 가능한데, 목적에 따라 송신만 가능한 채널, 수신만 가능한 채널로 제한할 수 있다.</p>
<pre><code class="language-go">package main

func main() {
    bidirectionChannel := make(chan int, 2)
    bidirectionChannel &lt;- 100 // 송신 가능
    &lt;- bidirectionChannel // 수신 가능

    transChan := makeTransChan(make(chan int, 2))
    transChan &lt;- 1 // 송신만 가능
    // result := &lt;- transChan: Invalid operation: &lt;- transChan (receive from the send-only type chan&lt;- int)

    receiveChan := makeReceiveChan(make(chan int, 2))
    // receiveChan &lt;- 1: Invalid operation: receiveChan &lt;- 1 (send to the receive-only type &lt;-chan int)
    &lt;- receiveChan // 수신만 가능
}


func TransOnlyChParam(ch &lt;-chan int) {
    fmt.Println(&lt;-ch)
    // ch &lt;- 100: 수신 전용이므로 송신 불가능
}

func makeTransChan(ch chan int) chan&lt;- int {
    return ch
}

func makeReceiveChan(ch chan int) &lt;-chan int {
    ch &lt;- 2
    return ch
}</code></pre>
<p>방향이 생기더라도 채널, 버퍼가 꽉 차거나, 말랐을 때 발생하는 Deadlock문제는 여전히 발생한다.</p>
<h1 id="select">Select</h1>
<p>아래와 같은 코드가 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    ch1 := make(chan bool)
    ch2 := make(chan bool)

    go func() {
        for {
            time.Sleep(1000 * time.Millisecond)
            ch1 &lt;- true
        }
    }()

    go func() {
        for {
            time.Sleep(500 * time.Millisecond)
            ch2 &lt;- true    
        }
    }()

    go func() {
        for {
            &lt;-ch1
            fmt.Println(&quot;ch1 수신&quot;)
            &lt;-ch2
            fmt.Println(&quot;ch2 수신&quot;)
        }
    }()

    time.Sleep(5 * time.Second)
}</code></pre>
<p>두 고루틴(a, b)에서 각각 1초, 0.5초 주기로, 채널에 데이터를 전송한다.
그리고 두 채널에서 전송한 데이터를 고루틴(c)에서 수신한다. 
그런데 다음과 같은 조건이 있다.</p>
<pre><code>무조건 ch1를 먼저 수신한 다음에 ch2에서 데이터 수신</code></pre><p>ch1에 데이터가 없으면 고루틴(c)이 block되어, ch2에 데이터가 더 빨리 많이 쌓이더라도ch2의 데이터를 사용하지 못하는 상황이 발생한다. (ch2도 결국 1초 주기로 데이터가 순환(?)하는 상황)</p>
<p>이를 <code>Select</code>로 개선할 수 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    ch1 := make(chan bool)
    ch2 := make(chan bool)

    go func() {
        for {
            time.Sleep(1000 * time.Millisecond)
            ch1 &lt;- true
        }
    }()

    go func() {
        for {
            time.Sleep(500 * time.Millisecond)
            ch2 &lt;- true    
        }
    }()

    go func() {
        for {
            select {
            case dataFromCh1 := &lt;-ch1: // case문에서 변수 선언이 가능하다.
                fmt.Println(&quot;ch1 수신&quot;, dataFromCh1)
            case &lt;-ch2: // 변수 선언 안 해도 상관 없다.
                fmt.Println(&quot;ch2 수신&quot;)
            case ch1 &lt;- bool: // case문에서 송신도 가능하다.
            }
        }
    }()

    time.Sleep(5 * time.Second)
}</code></pre>
<ol>
<li><code>select</code>문을 사용함으로써, ch2는 ch1의 수신 여부와 상관없이 데이터를 사용할 수 있게 된다.</li>
<li>ch1, ch2에 데이터가 없다면, 마지막 case문에서 데이터를 송신한다.<h1 id="오답-노트">오답 노트</h1>
아래 코드는 Deadlock에 빠진다. 왜 그럴까<pre><code class="language-go">package main
</code></pre>
</li>
</ol>
<p>import (
    &quot;fmt&quot;
    &quot;sync&quot;
)</p>
<p>func main() {
    channel := make(chan bool)
    channel &lt;- true</p>
<pre><code>wg := new(sync.WaitGroup)
wg.Add(1)

go func() {
    result := &lt;-channel
    fmt.Println(result)
    wg.Done()
}()

wg.Wait()</code></pre><p>}</p>
<pre><code>1. 채널에 데이터를 넣는다. 
2. 채널이 꽉 찼으므로, 메인 고루틴은 block
3. 그런데 WaitGroup, 익명 함수(고루틴)을 실행하지 않은 상태로 block되었으므로 Deadlock에 빠진다.

다음 처럼 해결할 수 있다.
``` go
package main

import (
    &quot;fmt&quot;
    &quot;sync&quot;
)

func main() {
    channel := make(chan bool)
    wg := new(sync.WaitGroup)
    wg.Add(1)
    go func() {
        result := &lt;-channel
        fmt.Println(result)
        wg.Done()
    }()

    channel &lt;- true
    wg.Wait()
}</code></pre><p>또는 버퍼를 가득 채우지 않는 방식으로도 해결할 수 있을 것 같다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;sync&quot;
)

func main() {
    channel := make(chan bool, 2)
    channel &lt;- true // 버퍼가 꽉 차지 않았으므로 블록되지 않는다.

    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        result := &lt;-channel
        fmt.Println(result)
        wg.Done()
    }()

    wg.Wait()
}</code></pre>
<p>이런 상황에서 waitGroup이 없다면, 메인 함수가 익명함수를 기다리지 않고 먼저 종료될 수도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Go: Goroutine, 비동기, 병렬 처리]]></title>
            <link>https://velog.io/@314_dev/Go-%EB%B9%84%EB%8F%99%EA%B8%B0</link>
            <guid>https://velog.io/@314_dev/Go-%EB%B9%84%EB%8F%99%EA%B8%B0</guid>
            <pubDate>Sat, 22 Apr 2023 06:00:32 GMT</pubDate>
            <description><![CDATA[<h1 id="goroutine">Goroutine</h1>
<blockquote>
<p><strong>논리적 가상 스레드</strong></p>
</blockquote>
<p>Golang은 <code>Goroutine</code>을 통해 스레드보다 훨씬 가벼운 비동기 동시 처리를 지원한다. 각각의 일에 대해 스레드와 1대 1로 대응하지 않고, 훨씬 적은 스레드를 사용한다.</p>
<p>메모리 측면에서, 스레드가 1MB의 스택을 갖을 때, 고루틴은 훨씬 작은 KB 단위의 스택을 갖고 필요시에 동적으로 증가한다. 
또한 <code>Gochannel</code>을 이용해 Goroutine간의 통신도 용이하게 할 수 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;
)

func main() {
    for i := 0; i &lt; 10; i++ {
        go printRandom(i)
    }

    fmt.Scanln()
}

func printRandom(idx int) {
    randomSleepTime := rand.Intn(5)
    time.Sleep(time.Duration(randomSleepTime * 1000))
    fmt.Println(idx)
}</code></pre>
<p>10개의 Goroutine을 띄워서 비동기적으로 index를 출력하는 모습을 확인해보자.</p>
<p>그런데 실행하면 아무것도 출력하지 않고 프로그램이 종료된다.
<code>main</code>에서 단순히 <code>Goroutine</code>을 호출시키기만 하고 기다리지 않고 종료되기 때문이다. (마치 JavaScript에서 async를 await하지 않는 것과 동일?)</p>
<p>그래서 일단은 억지로 main이 종료되지 않도록 해보자</p>
<pre><code class="language-go">``` go
package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;time&quot;
)

func main() {
    for i := 0; i &lt; 10; i++ {
        go printRandom(i)
    }

    fmt.Scanln() // 사용자 입력이 들어올 때 까지 block
}

func printRandom(idx int) {
    randomSleepTime := rand.Intn(5)
    time.Sleep(time.Duration(randomSleepTime * 1000))
    fmt.Println(idx)
}</code></pre>
<p>그러면 다음과 같이 비동기적으로 출력됨을 확인할 수 있다.</p>
<pre><code>0
5
7
3
...</code></pre><p>근데 딱봐도 <code>fmt.Scanln</code>을 사용해서 억지로 block하는건 아닌거 같다.
다른 방법으로 해결하자.</p>
<h2 id="syncwaitgroup">sync.WaitGroup</h2>
<p><code>sync</code>패키지의 <code>WaitGroup</code>을 사용해서 Goroutine이 완료될 때 까지 기다릴 수 있다.</p>
<pre><code class="language-go">package sync

import (
    &quot;internal/race&quot;
    &quot;sync/atomic&quot;
    &quot;unsafe&quot;
)

type WaitGroup struct {
    noCopy noCopy

    state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
    sema  uint32
}

func (wg *WaitGroup) Add(delta int) {...}
func (wg *WaitGroup) Done() {...}
func (wg *WaitGroup) Wait() {...}</code></pre>
<h3 id="add">Add</h3>
<p>Goroutine 추가</p>
<h3 id="done">Done</h3>
<p>Goroutine 제거</p>
<h3 id="wait">Wait</h3>
<p>모든 Goroutine이 제거될 때 까지 기다리기</p>
<p>사용법을 보면 바로 이해가 된다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;math/rand&quot;
    &quot;sync&quot;
    &quot;time&quot;
)

func main() {
    wait := new(sync.WaitGroup)
    for i := 0; i &lt; 10; i++ {
        wait.Add(1) // 기다릴 Goroutine 개수++
        go printRandom(i, wait) // waitGroup을 전달
    }
    wait.Wait() // 기다릴 Goroutine 개수가 0이 될 때 까지 block
}

func printRandom(idx int, wait *sync.WaitGroup) {
    defer wait.Done() // 함수가 종료되면 == Goroutine이 작업을 완료하면 기다릴 Goroutine 개수--
    randomSleepTime := rand.Intn(5)
    time.Sleep(time.Duration(randomSleepTime) * time.Second)
    fmt.Println(idx)
}</code></pre>
<p>WaitGroup, Goroutine과 클로저의 관계는 <code>Channel</code>에서 다루겠다.</p>
<h2 id="다중-cpu-환경-runtime-package">다중 CPU 환경: <code>runtime</code> Package</h2>
<p>Goroutine을 사용해서 간단하게 함수를 비동기로 호출할 수 있다.
그리고 <code>runtime</code> 패키지를 사용하면 다중 CPU를 이용한 병렬처리를 할 수 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;runtime&quot;
    &quot;sync&quot;
)

func main() {
    sum := 0
    wait := new(sync.WaitGroup)

    fmt.Println(runtime.NumCPU()) // 실행 환경의 CPU 개수
    runtime.GOMAXPROCS(200)       // 사용할 CPU 개수를 명시적으로 지정 가능
    fmt.Println(runtime.NumCPU()) // 실행 환경의 물리적 CPU 최대 개수로 설정된다.

    for i := 1; i &lt;= 100; i++ {
        wait.Add(1)
        go add(&amp;sum, i, wait)
    }
    wait.Wait()
    fmt.Print(&quot;sum: &quot;, sum)
}

func add(sum *int, i int, wait *sync.WaitGroup) {
    defer wait.Done()
    *sum += i
}
</code></pre>
<p>Goroutine을 사용해서 1부터 100까지의 합을 구하면 항상 5050이 나오지 않는다. 
멀티 코어 CPU가 Goroutine들을 효과적으로 처리하기 때문에, sum에 대해 race condition이 존재하기 때문이다.</p>
<pre><code class="language-go">runtime.GOMAXPROCS(1) // 단일 CPU 환경에서는 예상대로 race condition이 사라진다.</code></pre>
<p>단일 CPU 환경에서는 예상대로 5050을 확인할 수 있다.</p>
<h4 id="참고">참고</h4>
<p>1.5 이전 버전에는, 따로 <code>runtime.GOMAXPROCS</code>를 설정하지 않으면 기본적으로 1개의 CPU 코어만 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Go: 예외처리]]></title>
            <link>https://velog.io/@314_dev/Go-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@314_dev/Go-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Fri, 21 Apr 2023 14:48:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>참고
<a href="https://edu.goorm.io/learn/lecture/2010/%ED%95%9C-%EB%88%88%EC%97%90-%EB%81%9D%EB%82%B4%EB%8A%94-%EA%B3%A0%EB%9E%AD-%EA%B8%B0%EC%B4%88/lesson/322393/%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC%EC%9D%98-%EA%B8%B0%EB%B3%B8">한 눈에 끝내는 고랭 기초 - 에러 처리의 기본</a></p>
</blockquote>
<h1 id="error-interface--errors-package"><code>error</code> interface &amp; <code>errors</code> package</h1>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    input := 0
    count, err = fmt.Scanln(&amp;input)
    1
    if err != nil {
        return
    }
    ...
}

// builtin.go

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}</code></pre>
<p><code>fmt.Scanln</code> 반환하는 에러 객체는 <code>error 인터페이스이다</code>.
Go는 error 인터페이스 구현체를 제공하는데, 이것이 <code>erros 패키지의 errorString</code>이다.</p>
<pre><code class="language-go">// because the former will succeed if err wraps an *fs.PathError.
package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &amp;errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}</code></pre>
<p>그래서 다음의 방법으로 에러를 반환할 수 있다.</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;errors&quot;
)

func main() {
    var a, b int
    nm err := fmt.Scanln(&amp;a, &amp;b)

    if err != nil {
        panic(err)
    }

    result, err2 := getResult(a, b)

    if err2 != nil {
        panic(err2)
    }
}

func getResult(a int, b int) (int, error) {
    if a == 0 {
        return -1, errors.New(&quot;a should not be zero&quot;)
    }
    return a + b
}</code></pre>
<h1 id="log-package"><code>log</code> package</h1>
<p>위 코드처럼 <code>panic</code>을 사용하면 프로그램이 종료된다.</p>
<p>의도에 따라 틀린 방법은 아닌데, 보통은 예외처리를 위해 <code>log</code> 패키지 기능을 사용한다.</p>
<pre><code class="language-go">package mina

import &quot;log&quot;

func main() {

    log.Print(err) // log.Println(err)
    log.Fatal(err)
    log.Panic(err)
}</code></pre>
<p>크게 3가지 종류의 함수를 제공한다.</p>
<h2 id="print">Print</h2>
<p>시간, 에러 정보를 출력하고 계속 프로그램이 진행된다.</p>
<h2 id="fatal">Fatal</h2>
<p>시간, 에러 정보를 출력하고 <strong>정상적으로</strong> 프로그램이 종료된다.</p>
<h2 id="panic">Panic</h2>
<p>시간, 에러 정보를 출력하고 Panic을 발생시킨다. <code>defer</code> 구문이 없다면 런타임 패닉이 발생하고 <strong>비정상적으로</strong> 프로그램이 종료된다.</p>
<p>즉, <code>Panic()</code>을 호출한 것과 동일하다.</p>
<pre><code class="language-go">package main

import (
    &quot;errors&quot;
    &quot;fmt&quot;
    &quot;log&quot;
)

func main() {
    var input1, input2 int
    _, err1 := fmt.Scanln(&amp;input1, &amp;input2)

    if err1 != nil {
        log.Fatal(err1) // 에러가 있으면 그냥 종료
    }

    defer panicHandler()

    result, err2 := getResult(input1, input2)

    if err2 != nil {
        log.Panic(err2) // 에러가 있으면 Panic호출
    }

    fmt.Println(&quot;result is : &quot;, result)
}

func getResult(input1 int, input2 int) (int, error) {
    if input1 == 0 {
        return -1, errors.New(&quot;input1 should not be zero&quot;)
    }
    return input1 + input2, nil
}

func panicHandler() {
    fmt.Println(&quot;handle panic situation&quot;)
}</code></pre>
<p>실행 결과는 다음과 같다.</p>
<pre><code>// log.Panic(err2)으로 출력된 에러 메시지
2023/04/22 14:56:21 input1 should not be zero
// defer 구문을 사용해서 main 메서드 종료 전에 호출된 panicHanlder
handle panic situation
// panic을 호출했으므로 에러 메시지가 출력되고 비정상 종료
panic: input1 should not be zero</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL Part.3]]></title>
            <link>https://velog.io/@314_dev/QueryDSL-Part.3</link>
            <guid>https://velog.io/@314_dev/QueryDSL-Part.3</guid>
            <pubDate>Tue, 04 Apr 2023 08:39:29 GMT</pubDate>
            <description><![CDATA[<h1 id="순수-jpa--querydsl">순수 JPA + QueryDSL</h1>
<pre><code class="language-java">package study.querydsl.repository;


import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.dto.QMemberTeamDto;
import study.querydsl.entity.Member;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {

    private EntityManager em;
    private JPAQueryFactory qf;

    public Long save(Member member) {
        em.persist(member);
        return member.getId();
    }

    public Optional&lt;Member&gt; findById(Long id) {
        return Optional.ofNullable(em.find(Member.class, id));
    }

    public Optional&lt;Member&gt; findById_query(Long id) {
        return Optional.ofNullable(qf.selectFrom(member).where(member.id.eq(id)).fetchOne());
    }

    public List&lt;Member&gt; findByUsername(String username) {
        return em.createQuery(&quot;SELECT m FROM Member m WHERE m.username = :username&quot;, Member.class).setParameter(&quot;username&quot;, username).getResultList();
    }

    public List&lt;Member&gt; findByUsername_query(String username) {
        return qf.selectFrom(member).where(member.username.eq(username)).fetch();
    }

    public List&lt;Member&gt; findAll() {
        return em.createQuery(&quot;SELECT m FROM Member m&quot;, Member.class).getResultList();
    }

    public List&lt;Member&gt; findAll_query() {
        return qf.selectFrom(member).fetch();
    }

    public List&lt;MemberTeamDto&gt; searchByBuilder(MemberSearchCondition condition) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        if (StringUtils.hasText(condition.getUsername())) {
            booleanBuilder.and(member.username.eq(condition.getUsername()));
        }
        if (StringUtils.hasText(condition.getTeamName())) {
            booleanBuilder.and(team.name.eq(condition.getTeamName()));
        }
        if (condition.getAgeGeo() != null) {
            booleanBuilder.and(member.age.goe(condition.getAgeGeo()));
        }
        if (condition.getAgeLoe() != null) {
            booleanBuilder.and(member.age.loe(condition.getAgeLoe()));
        }
        return qf
                .select(new QMemberTeamDto(
                        member.id.as(&quot;memberId&quot;),
                        member.username,
                        member.age,
                        team.id.as(&quot;teamId&quot;),
                        team.name.as(&quot;teamName&quot;)
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(booleanBuilder)
                .fetch();
    }

    public List&lt;MemberTeamDto&gt; searchMember(MemberSearchCondition condition) {
        return qf
                .select(new QMemberTeamDto(
                        member.id.as(&quot;memberId&quot;),
                        member.username,
                        member.age,
                        team.id.as(&quot;teamId&quot;),
                        team.name.as(&quot;teamName&quot;)
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGeo()),
                        ageLoe(condition.getAgeLoe())
                )
                .fetch();
    }

    private Predicate ageLoe(Integer ageLoe) {
        return member.age.loe(ageLoe);
    }

    private Predicate ageGoe(Integer ageGeo) {
        return member.age.goe(ageGeo);
    }

    private Predicate teamNameEq(String teamName) {
        return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
    }

    private Predicate usernameEq(String username) {
        return StringUtils.hasText(username) ? member.username.eq(username) : null;
    }
}</code></pre>
<h2 id="참고-실행-환경-분리하기">참고: 실행 환경 분리하기</h2>
<pre><code class="language-yml"># src/main/resources/application.yml
# 로컬 실행 환경
spring:
    profiles:
        active: local

# src/main/test/resources/application.yml
# 테스트 환경
spring:
    profiles:
        active: test</code></pre>
<p>App을 실행시키면 <code>profile: local</code>로 설정된다.
Test 코드를 실행시키면 <code>profile: test</code>로 설정된다.</p>
<p>로컬 환경에서만 작동하는 코드 만들기</p>
<pre><code class="language-java">@Profile(&quot;local&quot;)
@Component
@RequiredArgsConstructor
public class InitMember {

    private final InitMemberService initMemberService;

    @PostConstruct
    public void init() {
        initMemberService.init();
    }

    @Component
    static class InitMemberSerivce {
        @PersistenceContext
        private EntityManager em;

        @Transactional
        public void init() {
            // DB에 데이터 넣는 코드
        }
    }
}</code></pre>
<h1 id="spring-data-jpa--querydsl">Spring Data JPA + QueryDSL</h1>
<p>간단한 기능은 <code>JpaRepository</code>로 해결할 수 있는데, 동적 쿼리는 QueryDSL로 해결하는게 좋다.</p>
<p><code>사용자 정의 레포지토리</code>를 사용해서 JpaRepository와 QueryDSL을 합칠 수 있다.</p>
<pre><code class="language-java">// MemberRepositoryCustom (interface)
public interface MemberRepositoryCustom {
    List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition);
}

// MemberRepositoryImpl
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory qf;

    public MemberRepositoryImpl(EntityManager em) {
        qf = new JPAQueryFactory(em);
    }

    @Override
    public List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition) {
        return qf
            ...
    }
}

// MemberRepository (JpaRepository)
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberRepositoryCustom {

    List&lt;Member&gt; findByUsername(String username);
}</code></pre>
<p>만약 <code>MemberRepositoryCustom</code>이 제공하는 기능이 범용적이지 않고 너무 특화(?)된 기능이라면, <code>사용자 정의 레포지토리</code>가 아닌 별도의 Repository Class로 구현한 뒤, Bean으로 등록해서 DI하는게 좋다.</p>
<h2 id="페이징">페이징</h2>
<p>Spring Data가 제공하는 <code>Page</code>, <code>Pageable</code>와 QueryDSL을 연동할 수 있다.</p>
<pre><code class="language-java">// MemberRepositoryCustom
public interface MemberRepositoryCustom {
    List&lt;MemberTeamDto&gt; search(MemberSearchCondition condition);

    Page&lt;MemberTeamDto&gt; searchPage(MemberSearchCondition condition, Pageable pageable);
}
// MemberRepositoryImpl
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    @Override
    public Page&lt;MemberTeamDto&gt; searchPage(MemberSearchCondition condition, Pageable pageable) {
        QueryResults&lt;MemberTeamDto&gt; results = qf
                .select(new QMemberTeamDto(
                        member.id.as(&quot;memberId&quot;),
                        member.username,
                        member.age,
                        team.id.as(&quot;teamId&quot;),
                        team.name.as(&quot;teamName&quot;)
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGeo()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset()) // 시작 페이지 번호
                .limit(pageable.getPageSize()) // 데이터 개수
                .fetchResults();
        List&lt;MemberTeamDto&gt; content = results.getResults();
        long total = results.getTotal();
        return new PageImpl&lt;&gt;(content, pageable, total);
    }</code></pre>
<p>앞서 살펴봤듯이, </p>
<ol>
<li><code>getResults</code>는 <code>페이징 데이터</code>, <code>총 개수</code>를 구하기 위해 쿼리가 두 번 발생한다.</li>
<li><code>getResults</code>를 통해 발생한 <code>총 개수</code> 구하는 쿼리는 <code>메인 쿼리</code>의 join, where문을 따라간다. <ul>
<li>즉, 단순히 총 개수만 구하는 쿼리인데 불필요한 join, where문이 발생할 수도 있다.</li>
</ul>
</li>
<li><code>getResults</code>는 deprecated</li>
</ol>
<h3 id="개선">개선</h3>
<p><code>페이징 데이터</code>와 <code>총 개수</code>를 별도로 구하도록 하자.</p>
<pre><code class="language-java">
    @Override
    public Page&lt;MemberTeamDto&gt; searchPage_simple(MemberSearchCondition condition, Pageable pageable) {
        // 첫 번째 쿼리로 데이터만 불러온다.
        List&lt;MemberTeamDto&gt; content = qf
                .select(new QMemberTeamDto(
                        member.id.as(&quot;memberId&quot;),
                        member.username,
                        member.age,
                        team.id.as(&quot;teamId&quot;),
                        team.name.as(&quot;teamName&quot;)
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGeo()),
                        ageLoe(condition.getAgeLoe())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        // 카운트는 명시적으로 별도의 쿼리를 발생시킨다.
        long total = qf
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGeo()),
                        ageLoe(condition.getAgeLoe())
                ).fetchCount();
        return new PageImpl&lt;&gt;(content, pageable, total);
    }</code></pre>
<h3 id="개선2">개선2</h3>
<pre><code class="language-java">    ...
    JPAQuery&lt;Member&gt; countQuery = qf
            .select(member)
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGeo()),
                    ageLoe(condition.getAgeLoe())
            );
    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);</code></pre>
<p>Spring Data의 최적화를 사용해서, 상황에 따라 CountQuery를 발생시키지 않도록 할 수도 있다.</p>
<pre><code>한 페이지에 보여줄 데이터가 20건인데, 검색 결과가 3건 밖에 없음. 
어차피 1 페이지 밖에 없으니까, 총 페이지 개수를 개산하기 위한 
&#39;총 개수&#39; 쿼리를 날릴 필요 없다. </code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL Part.2]]></title>
            <link>https://velog.io/@314_dev/QueryDSL-Part.2</link>
            <guid>https://velog.io/@314_dev/QueryDSL-Part.2</guid>
            <pubDate>Tue, 04 Apr 2023 05:28:07 GMT</pubDate>
            <description><![CDATA[<h1 id="프로젝션과-결과-반환">프로젝션과 결과 반환</h1>
<h2 id="기본">기본</h2>
<pre><code class="language-java">List&lt;String&gt; fetch = qf.select(member.username).from(member).fetch();
List&lt;Member&gt; fetch = qf.selectFrom(member).fetch();</code></pre>
<p>프로젝션 대상이 하나인 경우에는 타입을 명확히 지정할 수 있다.</p>
<pre><code class="language-java">List&lt;Tuple&gt; fetch = qf.select(member.username, member.age).from(member).fetch();

for (Tuple tuple : fetch) {
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);
}</code></pre>
<p>프로젝션 대상이 둘 이상인 경우에는 <code>Tuple</code>을 사용해야 한다.</p>
<p>다른 계층에서는 특정 기술(QueryDSL)에 의존적이면 안 된다. 
따라서 <code>Tuple</code>을 다른 계층에 넘기지 말고, DTO로 변환해서 전달해야 한다.</p>
<h2 id="dto">DTO</h2>
<p><code>순수 JPA</code>로 DTO를 다루려면 다음과 같을 것이다.</p>
<pre><code class="language-java">List&lt;MemberDto&gt; memberDtos = em.createQuery(
    &quot;SELECT new 패키지명.MemberDto(m.username, m.age) FROM Member m&quot;,
    MemberDto.class
).getResultList();</code></pre>
<p><code>순수 JPA</code>로 DTO를 다룰 때의 단점</p>
<pre><code>1. DTO의 패키지 명을 반드시 명시해야 한다.
2. 생성자 방식으로만 객체를 만들 수 있다.</code></pre><p><code>QueryDSL</code>에서는 세 가지 방법으로 DTO를 다룰 수 있다.</p>
<h3 id="projectionsbean">Projections.bean</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; fetch = qf
        .select(Projections.bean(MemberDto.class, 
                member.username, 
                member.age))
        .from(member)
        .fetch();</code></pre>
<ol>
<li><code>기본 생성자</code>로 객체 생성 후, <code>Setter</code>를 통해 값을 할당한다.</li>
<li>따라서 <code>기본 생성자</code>와 <code>Setter</code>가 반드시 있어야 한다.<h3 id="projectionsfield">Projections.field</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; fetch = qf
     .select(Projections.fields(MemberDto.class,
             member.username,
             member.age))
     .from(member)
     .fetch();</code></pre>
</li>
<li><code>Setter</code>를 사용하지 않고, <strong>필드명을 인식하여</strong> 값을 할당한다.</li>
<li>따라서 <code>Member</code> Entity의 필드명과, DTO의 필드명이 일치해야 한다.<h4 id="member-entity의-필드명과-dto의-필드명이-일치해야-한다"><code>Member</code> Entity의 필드명과, DTO의 필드명이 일치해야 한다.</h4>
만약 <code>Member</code> Entity는 <code>username</code>이라는 필드명을 사용하는데, <code>MemberDto</code>는 <code>name</code>이라는 필드명을 사용하면 <code>name</code>필드에 <code>null</code>이 할당된다.</li>
</ol>
<p>이렇게 <strong>필드명 불일치 문제</strong>를 다음과 같이 해결할 수 있다.</p>
<pre><code class="language-java">List&lt;MemberDto&gt; fetch = qf
        .select(Projections.fields(MemberDto.class,
                member.username.as(&quot;name&quot;),
                member.age))
        .from(member)
        .fetch();</code></pre>
<h4 id="select-절-subquery에-별칭-부여하기">SELECT 절 Subquery에 별칭 부여하기</h4>
<p><code>DTO + Projections.field</code>를 사용하려면 <strong>DTO 필드명 일치 여부</strong>를 주의해야 한다.</p>
<p>따라서 Subquery도 필드명을 일치시켜야 하는데, 다음의 방법으로 할 수 있다.</p>
<pre><code class="language-java">QMember subMember = new QMember(&quot;subMember&quot;)

List&lt;MemberDto&gt; fetch = qf
        .select(Projections.fields(MemberDto.class,
                // 위와 동일한 효과
                ExpressionUtils.as(member.username, &quot;username&quot;),
                ExpressionUtils.as( // subquery 결과를 필드로 사용
                    JPAExpressions // subquery 생성 구문
                        .select(subMember.age.max())
                        .from(subMember),
                    &quot;age&quot; // alias
                ))
        .from(member)
        .fetch();</code></pre>
<h3 id="projectionsconstructor">Projections.constructor</h3>
<pre><code class="language-java">List&lt;MemberDto&gt; fetch = qf
        .select(Projections.constructor(MemberDto.class,
                member.username,
                member.age))
        .from(member)
        .fetch();</code></pre>
<ol>
<li><p>시그니처가 일치하는 생성자를 통해 객체를 생성한다.</p>
</li>
<li><p>따라서 <code>fields</code>와 달리 필드명은 중요하지 않다.</p>
<h2 id="queryprojection">@QueryProjection</h2>
<p><code>DTO</code>를 직접 다루는 방식은 조금 복잡하다.
<code>@QueryProjection</code> Annotation으로 개선할 수 있다.</p>
<pre><code class="language-java">@Data
@NoArgsConstructor
public class MemberDto {

 private String username;
 private int age;

 @QueryProjection
 public MemberDto(String username, int age) {
     this.username = username;
     this.age = age;
 }
}</code></pre>
</li>
<li><p>생성자에 <code>@QueryProjection</code>을 붙인다</p>
</li>
<li><p><code>compileQuerydsl</code>로 컴파일 하면 DTO에도 Q-Type이 생성된다.</p>
</li>
</ol>
<p>그런 뒤, 다음과 같이 사용하면 된다.</p>
<pre><code class="language-java">List&lt;MemberDto&gt; fetch = qf
        .select(new QMemberDto(member.username, member.age))
        .from(member)
        .fetch();</code></pre>
<h3 id="장단점">장단점</h3>
<h4 id="장점">장점</h4>
<p><code>Projections.consturctor</code>는 컴파일 타임에 문제를 발견할 수 없다.</p>
<pre><code class="language-java">qf
.select(Projections.constructor(MemberDto.class,
        member.username,
        member.age,
        member.id
))</code></pre>
<p>DTO에 존재하지 않는 <code>member.id</code>를 사용했음에도, 컴파일 타임에 문제를 검출하지 못하고 런타임에 문제가 발생할 수도 있다.</p>
<p>그런데 <code>QueryProjection</code>은 실제 생성자를 사용하는 것이므로, 컴파일 타임에 문제를 발견할 수 있다.</p>
<h4 id="단점">단점</h4>
<ol>
<li>Q-Type을 생성, 관리해야 한다.</li>
<li><code>QueryProjection</code>은 QueryDSL의 기술이다.<ul>
<li>즉, DTO가 특정 기술에 종속적이게 되는 문제가 발생한다.</li>
<li>DTO는 여러 Layer에서 사용하는데, 이는 여러 Layer에서 QueryDSL에 종속적이게 되는 결과로 이어진다.</li>
</ul>
</li>
</ol>
<h4 id="결론">결론</h4>
<p>의존성 문제를 우선한다면 <code>Projection</code>을 사용하자
편의성을 우선한다면 그냥 <code>QueryProjection</code>을 사용하자. 
알아서 선택하라는 뜻</p>
<h1 id="동적-쿼리">동적 쿼리</h1>
<p><code>QueryDSL</code>은 두 가지 방법으로 동적 쿼리를 작성한다.</p>
<h2 id="booleanbuilder">BooleanBuilder</h2>
<pre><code class="language-java">// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = &quot;username1&quot;;
Integer age = 10;

BooleanBuilder booleanBuilder = new BooleanBuilder();
if (username != null) {
    booleanBuilder.and(member.username.eq(username));
}
if (age != null) {
    booleanBuilder.and(member.age.eq(age));
}

List&lt;Member&gt; fetch = qf
        .selectFrom(member)
        .where(booleanBuilder)
        .fetch();</code></pre>
<h2 id="where-다중-파라미터">Where 다중 파라미터</h2>
<pre><code class="language-java">// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = &quot;username1&quot;;
Integer age = 10;

List&lt;Member&gt; fetch = qf
        .selectFrom(member)
        .where(usernameEq(username), ageEq(age))
        .fetch();

private Predicate ageEq(Integer age) {
    if (age == null) {
        return null;
    }
    return member.age.eq(age);
}

private Predicate usernameEq(String username) {
    if (username == null) {
        return null;
    }
    return member.username.eq(username);
}</code></pre>
<ol>
<li><code>WHERE</code>의 파라미터가 <code>NULL</code>이면 무시된다는 특징을 활용한 방식</li>
<li>쿼리 재활용, 조립 가능</li>
<li>가독성</li>
</ol>
<p><code>조건 조합</code>을 위해, <code>Predicate</code>보다 <code>BooleanExpression</code>을 반환 타입으로 사용하는 것이 좋다.</p>
<pre><code class="language-java">private BooleanExpression usernameEq(String username) {...}</code></pre>
<p><code>Where 다중 파라미터</code> 사용 권장</p>
<h1 id="bulk-operation">Bulk Operation</h1>
<p><code>Dirty Checking</code>을 사용하지 않고 DB에 직접 쿼리를 날리자.
Persistence Context 불일치에 주의해야 한다.</p>
<ul>
<li>Bulk Operation 수행 후에 Persistence Context flush, clear 필요<pre><code class="language-java">long count = qf
      .update(member)
      .set(member.username, &quot;older than 30&quot;)
      .where(member.age.gt(30))
      .execute();</code></pre>
<h1 id="sql-function">SQL Function</h1>
나중에 다루도록 하겠다,,,</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[QueryDSL Part.1]]></title>
            <link>https://velog.io/@314_dev/QueryDSL-12bf26kp</link>
            <guid>https://velog.io/@314_dev/QueryDSL-12bf26kp</guid>
            <pubDate>Mon, 03 Apr 2023 05:45:11 GMT</pubDate>
            <description><![CDATA[<h1 id="querydsl-설정">QueryDSL 설정</h1>
<p>스프링 부트 2.6부터 Querydsl 5.0을 사용한다.</p>
<pre><code class="language-text">buildscript {
    ext {
        queryDslVersion = &quot;5.0.0&quot;
    }
}

plugins {
    ...
    //querydsl 추가
    id &quot;com.ewerk.gradle.plugins.querydsl&quot; version &quot;1.0.10&quot;
}
...
dependencies {
    //querydsl 추가
    implementation &quot;com.querydsl:querydsl-jpa:${queryDslVersion}&quot;
    annotationProcessor &quot;com.querydsl:querydsl-apt:${queryDslVersion}&quot;
    ...
}
...
def querydslDir = &quot;$buildDir/generated/querydsl&quot;
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}</code></pre>
<p><img src="https://velog.velcdn.com/images/314_dev/post/5483aa7a-3c54-437b-b3ca-d0c17b372d29/image.png" alt="">
<code>compileQuerydsl</code>을 클릭하면 <code>@Entity</code> Annotation이 붙은 Class를 확인하면서 <code>Qxxx</code>라는 이름의 클래스를 생성한다.
즉, <code>Member</code> Entity가 존재하면 <code>QMember</code>라는 클래스가 생기는 것이다.
QueryDSL은 Entity를 한 번 감싼(?) <code>Q클래스</code>를 통해 데이터에 접근하는 것이다.
<img src="https://velog.velcdn.com/images/314_dev/post/7a1531a3-af62-4502-9ec6-9eef3930c91a/image.png" alt="">
<img src="https://velog.velcdn.com/images/314_dev/post/5344c298-1285-4ce6-81ae-e2636b9b081c/image.png" alt="">
<code>querydsl-apt</code>: QClass를 만드는 역할
<code>querydsl-jpa</code>: 쿼리 작성 역할</p>
<h1 id="jpql-vs-querydsl">JPQL vs QueryDSL</h1>
<pre><code>유저 이름으로 조회하기</code></pre><pre><code class="language-java">
    @Test
    void jqplTest() {
        String query = &quot;SELECT m FROM Member m WHERE m.username = :username&quot;;
        Member findMember = em.createQuery(query, Member.class)
                .setParameter(&quot;username&quot;, &quot;username#1&quot;)
                .getSingleResult();
        assertThat(findMember.getUsername()).isEqualTo(&quot;username#1&quot;);
    }

    @Test
    void queryDslTest() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember(&quot;m&quot;);
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq(&quot;username#1&quot;))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo(&quot;username#1&quot;);
    }</code></pre>
<ol>
<li>JQPL은 문자열을 다루는 것 과 달리, QueryDSL은 쿼리를 자바 코드를 사용하는 것 처럼 작성한다.<ul>
<li>쿼리를 잘못 작성할 걱정이 없다.</li>
<li>파라미터 바인딩을 알아서 해준다.</li>
</ul>
</li>
<li>QueryDSL은 컴파일 타임에 쿼리 문법을 검사한다.<ul>
<li>런타임에 쿼리 문법 예외가 발생할 일이 없다.</li>
</ul>
</li>
</ol>
<h2 id="권장">권장</h2>
<pre><code class="language-java">    @Test
    void queryDslTest() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember(&quot;m&quot;);
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq(&quot;username#1&quot;))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo(&quot;username#1&quot;);
    }</code></pre>
<p>Spring이 주입하는 <code>EntityManager</code>는 Thread Safe하기 때문에, <code>JPAQueryFactory</code>를 필드로 추출해서 사용해도 된다.</p>
<pre><code class="language-java">    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    @Test
    void queryDslTest() {
        QMember m = new QMember(&quot;m&quot;);
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq(&quot;username#1&quot;))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo(&quot;username#1&quot;);
    }</code></pre>
<h1 id="기본-문법">기본 문법</h1>
<h2 id="q-type">Q-Type</h2>
<p>QueryDSL은 <code>Q클래스</code> 인스턴스를 통해 데이터에 접근한다.</p>
<p><code>Q클래스</code>인스턴스는 두 가지 방법으로 사용할 수 있다.</p>
<pre><code class="language-java">// 1. 별칭 직접 입력
// 입력한 별칭이 SQL문 Table의 별칭으로 사용된다.
// SELECT m1.username FROM Member AS m1
QMember qMember = new QMember(&quot;m1&quot;);
// 2. 기본 인스턴스 사용
// `Q클래스` 내부적으로 미리 초기화 해놓은 인스턴스를 가지고 있다.
QMember qMember = QMember.member;</code></pre>
<p><code>2. 기본 인스턴스 사용</code> 사용을 권장한다.</p>
<pre><code class="language-java">// 1. 기본 사용 
QMember qMember = QMember.member;
// 2. Static Import로 간소화 하기
import static 경로.entity.QMember.*;
QMember member = member; // QMember.member를 간소화</code></pre>
<p>같은 Table을 다룰 때는 Table을 구분해야 하므로, 그때 Alias 방식을 사용한다.</p>
<h2 id="검색-조건-쿼리">검색 조건 쿼리</h2>
<pre><code class="language-java">qf // queryFactory
.select(member)
.from(member)
.where(
    member.username.eq(&quot;a&quot;) // username = &quot;a&quot;
    member.username.ne(&quot;a&quot;) // username != &quot;a&quot;
    member.username.eq(&quot;a&quot;).not // username != &quot;a&quot;
    member.username.isNotNull() // username is not null

    member.age.in(10, 20) // age IN (10, 20)
    member.age.notIn(10, 20) age NOT IN (10, 20)
    member.age.between(10, 20) age BETWEEN 10 AND 20;

    member.age.goe(30) // age &gt;= 30 (greater or equal)
    member.age.gt(30) // age &gt; 30
    member.age.loe(30) // age &lt;= 30 (less or equal)
    member.age.lt(30) // age &lt; 30

    member.username.like(&quot;member%&quot;) // like member%
    member.username.contains(&quot;member&quot;) // like %member%
    member.username.startsWith(&quot;member&quot;) // like member%
).fetchOne();</code></pre>
<h2 id="검색-조건-쿼리---and">검색 조건 쿼리 - And</h2>
<p>두 방식으로 <code>And</code> 조건을 설정할 수 있다.</p>
<pre><code class="language-java">// 1. and() 사용
qf
.select(member)
.from(member)
.where(member.username.eq(&quot;a&quot;).and(member.age.eq(10)))
// 2. 파라미터로 사용
qf.select(member)
.from(member)
.where(
    member.username.eq(&quot;a&quot;),
    member.age.eq(10)
)</code></pre>
<p><code>2. 파라미터로 사용</code>을 권장함</p>
<ul>
<li>가독성</li>
<li>파라미터에 <code>null</code>이 들어가면 알아서 무시함<ul>
<li>동적 쿼리 작성할 때 편하다.</li>
</ul>
</li>
</ul>
<h2 id="결과-조회">결과 조회</h2>
<pre><code class="language-java">// 1. List로 조회
// selectFrom : Entity가 같은 경우 축약 가능 (select + from)
List&lt;Member&gt; members = qf.selectFrom(member).fetch();
// 2. 단건 조회
Member member = qf.selectFrom(member).fetchOne();
// 3. 첫번째 결과만 조회
Member member = qf.selectFrom(member).fetchFirst();
// 4. 복합 결과 (페이징)
// 페이징을 위해 count 개수를 검색하는 쿼리가 발생함 (총 쿼리 2번)
// 최신 버전에서는 deprecated
QueryResults&lt;Member&gt; results = qf.selectFrom(member).fetchResults();
// 5. 데이터 개수 조회 (count)
long total = qf.selectFrom(member).fetchCount();</code></pre>
<h2 id="orderby">OrderBy</h2>
<pre><code>나이 기준 오름차순, 이름 기준 내림차순, 이름이 null인 경우 뒤에 위치하도록 정렬</code></pre><pre><code class="language-java">List&lt;Member&gt; fetch = qf
    .selectFrom(member)
    .orderBy(member.age.asc(), member.username.desc().nullsLast())
.fetch();</code></pre>
<p><code>nullsLast</code>: 값이 null인 경우 마지막에 위치
<code>nullsFirst</code>: 값이 null인 경우 앞에 위치</p>
<h2 id="페이징">페이징</h2>
<pre><code class="language-java">// 1페이지에서부터 3개 불러오기
List&lt;Member&gt; results = qf
    .selectFrom(member)
    .offset(1).limit(3).fetch();</code></pre>
<p>QueryDSL도 <code>0</code>부터 페이지가 시작한다.</p>
<h2 id="집합">집합</h2>
<h3 id="aggregate-function">Aggregate Function</h3>
<pre><code class="language-java">List&lt;Tuple&gt; result = qf.select(
        member.count(),
        member.age.sum(),
        member.age.avg(),
        member.age.max(),
        member.age.min()
        )
        .from(member)
        .fetch();
Tuple tuple = result.get(0);
Long count = tuple.get(member.count());
Integer sum = tuple.get(member.age.sum());
Double avg = tuple.get(member.age.avg());
Integer max = tuple.get(member.age.max());
Integer min = tuple.get(member.age.min());</code></pre>
<p><code>Tuple</code>은 QueryDSL이 제공하는 자료구조이다.</p>
<h3 id="group-by-having">Group By, Having</h3>
<pre><code>팀 인원이 10명 이상인 팀의 이름과, 평균 나이</code></pre><pre><code class="language-java">List&lt;Tuple&gt; result = qf
        .select(team.name, member.age.avg())
        .from(member)
        .join(member.team, team)
        .groupBy(team.name)
        .having(team.count().goe(10))
        .fetch();
Tuple team1 = result.get(0);
Tuple team2 = result.get(2);</code></pre>
<h2 id="join">Join</h2>
<pre><code class="language-java">List&lt;Member&gt; result = qf
        .select(member)
        .from(member)
        // join, innerJoin, leftJoin, rightJoin
        // join을 수행하면 JPA를 따라 Inner Join 발생
        .join(member.team, team) // QTeam.team
        .fetch();</code></pre>
<p>첫 번째 파라미터에 <code>조인 대상</code>을 지정하고, 두 번째 파라미터 alias로 사용할 Q타입을 지정하면 된다.</p>
<h3 id="join---on">Join - On</h3>
<ul>
<li><p>JPA 2.1부터 <code>On</code>을 사용할 수 있다.</p>
<p>  회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인, 회원은 모두 조회
  (JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = &#39;A&#39;)</p>
<pre><code class="language-java">List&lt;Tuple&gt; result = qf
      .selectFrom(member, team)
      .from(member)
      .leftJoin(member.team, team).on(team.name.eq(&quot;A&quot;))
      .fetch();</code></pre>
<p><code>Outer Join</code>이므로, 팀 A에 속하지 않는 멤버 결과가 포함된다. <code>null</code>에 주의해야 한다.</p>
</li>
</ul>
<p><code>Inner Join</code>을 사용할 때 필터링 조건을 <code>On</code>에 걸든, <code>Where</code>절에 걸든 결과는 동일하다. 
따라서 일관된 스타일을 위해서, <code>Inner Join</code>에서는 익숙한 <code>Where</code>절을 사용하고, <code>Outer Join</code>에 경우에만 <code>On</code>절을 사용하도록 하자.</p>
<h3 id="join의-대상">Join의 대상</h3>
<p><code>join</code>을 사용할 때 두 문법이 있다.</p>
<pre><code class="language-java">// 1. 일반 조인
qf
.select(member)
.from(member)
.join(member.team, team)
.fetch();
// 2. 조인대상만 사용 + On 절
qf
.select(member)
.from(member)
.join(team).on(member.team.id.eq(team.id))
.fetch()</code></pre>
<p>첫 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성한다.
즉, SQL ON절에 <code>member.team_id = team.id</code>가 추가된다.</p>
<p>두 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성하지 않는다.
그러므로 명시적으로 ON절을 설정해 줘야 한다.</p>
<h3 id="연관-관계가-없는-경우">연관 관계가 없는 경우</h3>
<p><code>Cross Join (Cartesian Product)</code>: N * M</p>
<pre><code class="language-java">List&lt;Tuple&gt; tuples = qf
        .select(memeber, team)
        .from(member, team)
        .fetch();
// JPQL
SELECT member1, team FROM Member member1, Team team</code></pre>
<p><code>Theta Join</code>: Caretsian Product에서 특정 조건 (세타 조건)을 만족하는 경우만 반환</p>
<pre><code>유저 이름과 팀 이름이 같은 경우</code></pre><pre><code class="language-java">List&lt;Tuple&gt; result = qf
        .select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();
// JQPL
SELECT member1, team FROM Member member1, Team team WHERE member1.username = team.name

// 그냥 연습용으로 비교
List&lt;Member&gt; members = qf
        .select(member)
        .from(member)
        .where(member.username.eq(member.team.name))
        .fetch();
// JPQL
// 묵시적 JOIN 발생
SELECT member1 FROM Member member1 WHERE member1.username = member1.team.name</code></pre>
<h3 id="fetch-join">Fetch Join</h3>
<pre><code class="language-java">// QueryDSL
List&lt;Member&gt; result = qf
        .select(member)
        .from(member)
        .join(member.team, team).fetchJoin()
        .where(team.name.eq(&quot;team1&quot;))
        .fetch();
// JPQL
SELECT m FROM Member m JOIN FETCH m.team t WHERE t.name = &quot;team1&quot;</code></pre>
<p>어떤 Join이든지 <code>fetchJoin()</code>을 같이 사용하면 된다.</p>
<p>fetchJoin을 사용하지 않아도 같은 효과를 낼 수 있다.</p>
<pre><code class="language-java">// QueryDSL
List&lt;Tuple&gt; team1 = qf
        .select(member, member.team)
        .from(member)
        .join(member.team, team)
        .where(team.name.eq(&quot;team1&quot;))
        .fetch();</code></pre>
<p>다만 반환 타입이 <code>Tuple</code>이다.</p>
<h2 id="subquery">Subquery</h2>
<p><code>com.querydsl.jpa.JPAExpressions</code>를 사용하여 서브쿼리를 구성할 수 있다.</p>
<pre><code class="language-java">// 가장 나이가 많은 회원 조회
QMember subQMember = new QMember(&quot;sub_member&quot;);
List&lt;Member&gt; members = qf
        .selectFrom(member)
        .where(member.age.eq(
            JPAExpressions
                    .select(subQMember.age.max())
                    .from(subQMember)
        ))
        .fetch();</code></pre>
<p>초반부에 다뤘던 것 처럼, 같은 Table(Entity)를 다루므로 Alias를 다르게 해서 구분해야 한다.    </p>
<pre><code class="language-java">// SELECT절 Subquery
QMember subQMember = new QMember(&quot;sub_member&quot;);
List&lt;Member&gt; members = qf
        .select(
            member,username,
            JPAExpressions
                .select(subQMember.age.avg())
                .from(subQMember))
        .from(member)
        .fetch();</code></pre>
<p>역시나 <code>JPAExpressions</code>도 static import로 분리하여 가독성을 높일 수 있다.</p>
<p>JPA, JPQL이 From절 Subquery를 지원하지 않으므로, QueryDSL도 불가능하다.
다음의 방법으로 해결 할 수 있을 것이다.</p>
<pre><code>1. (가능한 경우) JOIN으로 변경하기
2. 쿼리를 두 번 날려서 변경하기
3. NativeSQL 사용하기</code></pre><p>SELECT절 Subquery는 JPA는 불가능한데, Hibernate는 가능해서 QueryDSL도 가능한 것이다.</p>
<h2 id="case">Case</h2>
<pre><code class="language-java">//Simple CASE Statement
List&lt;String&gt; ages = qf
        .select(member.age
                .when(10).then(&quot;ten&quot;)
                .when(20).then(&quot;twenty&quot;)
                .otherwise(&quot;??&quot;))
        .from(member)
        .fetch()
// Searched CASE Statement
List&lt;String&gt; ages = qf
        .select(new CaseBuilder()
                .when(member.age.between(0,10)).then(&quot;ten&quot;)
                .when(member.age.between(11,20)).then(&quot;twenty&quot;)
                .otherwize(&quot;??&quot;))
        .from(member)
        .fetch();</code></pre>
<p>DB에서는 단순히 데이터만 조회하기를 권장한다.</p>
<h2 id="상수-문자-더하기">상수, 문자 더하기</h2>
<pre><code class="language-java">// 상수 (Expressions)
List&lt;Tuple&gt; result = qf
                .select(member.username, Expressions.constant(&quot;A&quot;))
                .from(member)
                .fetch();
// 문자 더하기 (concat)
List&lt;String&gt; result = qf
                .select(member.username.concat(&quot;_&quot;).concat(member.age.stringValue()))
                .from(member)
                .fetch();</code></pre>
<p>상수는 JPQL, SQL에는 반영되지 않고, 최종 결과에서만 반영된다.
문자 더하기는 JPQL, SQL에도 반영되서 전달된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Data JPA Part.2]]></title>
            <link>https://velog.io/@314_dev/Spring-Data-JPA-Part.2</link>
            <guid>https://velog.io/@314_dev/Spring-Data-JPA-Part.2</guid>
            <pubDate>Sun, 02 Apr 2023 06:20:37 GMT</pubDate>
            <description><![CDATA[<h1 id="사용자-정의-repository">사용자 정의 Repository</h1>
<p><code>JpaRepository</code>를 사용하는 상황에서, 다른 종류의 Repository도 같이 사용하고 싶을 수 있다.</p>
<p>다음과 같이 사용할 수 있다.</p>
<p><strong>1. Repository 인터페이스(A) 정의하기</strong></p>
<pre><code class="language-java">// 순수 JPA로 구현한 Repository도 같이 사용하고 싶은 상황
public interface MemberRepositoryCustom {

    List&lt;Member&gt; findMemberCustom();
}</code></pre>
<p><strong>2. Repository 구현체 만들기 (A의 구현체)</strong></p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private EntityManager em;

    @Override
    public List&lt;Member&gt; findMemberCustom() {
        return em.createQuery(
                &quot;SELECT m FROM Member m&quot;,
                Member.class
        ).getResultList();
    }
}</code></pre>
<p><strong>3. 기존 Repository가 상속받도록 하기 (A 상속 받기)</strong></p>
<pre><code class="language-java">package doodlin.greeting.test;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository&lt;Member, Long&gt;, MemberRepositoryCustom {
    ...
}</code></pre>
<p>순수 자바 문법 관점에서는 불가능하지만, Spring Data의 도움을 받아서 <code>MemberRepository</code>에서 <code>MemberRepositoryCustom</code>의 구현체<code>MemberRepositoryImpl</code>에 구현된 내용을 사용할 수 있다.</p>
<h2 id="주의사항">주의사항</h2>
<ol>
<li><p>QueryDSL, JDBC Template을 주로 이러한 방식을 통해 사용한다.</p>
</li>
<li><p><strong>구현체의 이름은 반드시 접미사 <code>Impl</code>로 끝나야 한다.</strong></p>
<ul>
<li>옵션으로 변경 가능하긴 함</li>
<li>인터페이스 이름은 상관 X</li>
</ul>
</li>
<li><p>항상 <code>사용자 정의 레포지토리</code>가 필요한 건 아니다.</p>
<ul>
<li><p>그냥 별도의 Repository를 만들어서 Bean으로 등록한 뒤 DI해서 사용해도 된다.</p>
</li>
<li><p>순수 Entity만 다루는 Repository와, API 맞춤 Repository는 분리하여 관리하는게 좋다 (유지보수 측면).</p>
<h1 id="auditing">Auditing</h1>
<p>Entity 생성 변경 날짜, 생성 변경 인물을 기록하고 싶다.</p>
</li>
</ul>
</li>
</ol>
<h2 id="순수-jpa">순수 JPA</h2>
<pre><code class="language-java">@MappedSuperclass
public class BaseEntity {

    @Column(updatable = false)
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    @PrePersist // @PostPersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdDate = now;
        updatedDate = now;
    }

    @PreUpdate // @PostUpdate
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}

// Entity
@Entity
public class Member extends BaseEntity {
    ...
}</code></pre>
<h2 id="spring-data">Spring Data</h2>
<h3 id="설정">설정</h3>
<pre><code class="language-java">@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplicaton {

    public static void main(String[] args) {...}

    @Bean
    public AuditorAware&lt;String&gt; auditorProvider() {
        return () -&gt; Optional.of(UUID.randomUUID().toString());
    }
}</code></pre>
<p><code>auditorProvider</code>를 통해 <code>생성자, 변경자</code> 필드에 어떤 값이 들어갈 지 설정할 수 있다.
보통은 Session ID를 넣는다.</p>
<p>Spring 기본 정책상, 생성 당시에 <code>변경 날짜, 변경자</code>도 같이 초기화 하는데, 옵션을 통해 null이 들어가도록 할 수 있다.</p>
<pre><code class="language-java">  @EnableJpaAuditing(modifyOnCreate = false) // (권장 X).</code></pre>
<h3 id="baseentity">BaseEntity</h3>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}</code></pre>
<p><code>@EntityListeners(AuditingEntityListener.class)</code>을 별도의 XML로 분리해서 글로벌 설정으로 만들 수 있다.</p>
<h2 id="권장-tip">권장 Tip</h2>
<p><code>생성 날짜</code>는 보통 모든 Entity에 사용되는데, <code>생성자</code>는 특정 Entity에만 사용된다. 그러므로 보통 다음과 같이 사용한다.</p>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}</code></pre>
<p>Entity에 따라 원하는 baseEntity를 상속받도록 한다.</p>
<h1 id="web--도메인-클래스-컨버터">Web+ : 도메인 클래스 컨버터</h1>
<blockquote>
<p>참고: <a href="https://www.youtube.com/watch?v=_QBe2ZiXV-0">Spring Boot REST API Domain Class Converter</a></p>
</blockquote>
<pre><code>ID로 유저 찾기 API</code></pre><p>위 요구사항은 아마 아래처럼 구현할 것이다.</p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {

    private MemberRepository memberRepositry;

    @GetMapping(&quot;/api/members/{id}&quot;)
    public MemberDto findMember(@PathVariable(&quot;id&quot;) int id) {
        return new MemberDto(memberRepositry.findById(id));
    }
}</code></pre>
<p>Spring Data JPA를 사용하면 다음 처럼 구현할 수 있다.</p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {

    private MemberRepository memberRepositry;

    @GetMapping(&quot;/api/members/{id}&quot;)
    public MemberDto findMember(@PathVariable(&quot;id&quot;) Member member) {
        return new MemberDto(member);
    }
}</code></pre>
<p>Spring Data JPA가 메서드 시그니처를 보고, <code>DomainNameConverter</code>가 작동해서 Entity를 조회한 뒤 반환하는 것이다.
<code>DomainNameConverter</code>는 내부적으로 Repository를 사용한다.</p>
<h2 id="주의사항-1">주의사항</h2>
<p>트랜잭션 범위 밖에서 조회가 발생했으므로, 조회한 값을 변경해서는 안 된다.
즉, 조회 용도로만 사용해야 한다.</p>
<h1 id="web--페이징과-정렬">Web+ : 페이징과 정렬</h1>
<p>Spring Data가 제공하는 페이징, 정렬 기능을 Spirng MVC에서 편리하게 사용할 수 있다.</p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {

    // findAll(Pageable): JpaRepository의 부모 인터페이스에 정의된 메서드
    @GetMapping(&quot;/api/members&quot;)
    public Page&lt;MemberDto&gt; list(Pageable pageable) {
        Page&lt;Member&gt; page = memberRepository.findAll(pageable);
        return page.map((m)-&gt;new MemberDto(m));
    }
}</code></pre>
<p>페이징 조건을 알아서 Pageable의 구현체 <code>PageRequest</code>로 만들어준다.</p>
<p>다음과 같이 사용 가능하다.</p>
<pre><code>.../api/members -&gt; 모든 멤버 조회
.../api/members?page=0 -&gt; &#39;page = 0&#39;에 해당하는 멤버 조회 (기본 20, 최대 2000)
.../api/members?page=1&amp;size=3 -&gt; &#39;page = 1&#39;에 해당하는 멤버 3명 조회
.../api/members?page=1&amp;size=3&amp;sort=id,desc -&gt; 정렬 기준, 방향 지정 가능</code></pre><h2 id="커스터마이징">커스터마이징</h2>
<p>글로벌 설정으로 페이지 사이즈를 지정할 수 있다.</p>
<pre><code class="language-yml">spring:
    data:
        web:
            pageable:
                default-page-size: 10
                max-page-size: 100
                one-indexed-parameter: true 
                # page 시작 번호를 1로 만들기 (한계 존재)</code></pre>
<p>또는 <code>@PageableDefault</code> Annotation으로 지정 가능하다. (글로벌 설정에 앞선다.)</p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {

    // findAll(Pageable): JpaRepository의 부모 인터페이스에 정의된 메서드
    @GetMapping(&quot;/api/members&quot;)
    public Page&lt;MemberDto&gt; list(@PageableDefault(size = 5, sort = &quot;id&quot;) Pageable pageable) {
        Page&lt;Member&gt; page = memberRepository.findAll(pageable);
        return page.map((m)-&gt;new MemberDto(m));
    }
}</code></pre>
<h2 id="qualifier"><code>@Qualifier</code></h2>
<p>페이징 정보가 여러 건인 경우, <code>@Qualifier</code>를 사용해서 페이징 정보를 추가할 수 있다.</p>
<pre><code>.../api/members?member_page=0&amp;order_page=1</code></pre><pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class MemberController {

    // findAll(Pageable): JpaRepository의 부모 인터페이스에 정의된 메서드
    @GetMapping(&quot;/api/members&quot;)
    public Page&lt;MemberDto&gt; list(
        @Qualifier(&quot;member&quot;) Pageable memberPageable,
        @Qualifier(&quot;order&quot;) Pageable orderPageable
    ) {
        Page&lt;Member&gt; page = memberRepository.findAll(pageable);
        return page.map((m)-&gt;new MemberDto(m));
    }
}</code></pre>
<h1 id="jparepository-구현체">JpaRepository 구현체</h1>
<p><code>JpaRepository</code>를 상속받은 인터페이스를 정의하면 Spring Data JPA가 알아서 구현체를 만들어준다.</p>
<p>그 구현체는 바로 <code>SimpleJpaRepository</code>이다.</p>
<pre><code class="language-java">@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository&lt;T, ID&gt; implements JpaRepositoryImplementation&lt;T, ID&gt; {

    ...
}</code></pre>
<ol>
<li><p><code>@Repository</code>가 붙이있으므로 Spring Bean으로 등록될 뿐만 아니라, Jpa Exception을 Spring의 Excepiton으로 변환한다.</p>
</li>
<li><p><code>@Transactional(readOnly = true)</code>이므로, 메서드에 따로 <code>@Transactional</code>이 붙어있지 않는 이상, 트랜잭션이 종료되어도 <code>flush</code>를 수행하지 않는다(약간의 성능 이점).</p>
</li>
<li><p><strong><code>save</code>메서드는 새로운 Entity이면 persist, 아니면 merge를 수행한다.</strong> </p>
<ul>
<li>데이터 변경 목적으로 merge를 사용하면 안된다. </li>
<li>Dirty Checking을 통해 데이터를 변경해야 한다.</li>
</ul>
</li>
</ol>
<h2 id="새로운-entity-구분">새로운 Entity 구분</h2>
<blockquote>
<ol start="3">
<li><code>save</code>메서드는 새로운 Entity이면 persist, 아니면 merge를 수행한다.</li>
</ol>
</blockquote>
<pre><code class="language-java">    @Transactional
    @Override
    public &lt;S extends T&gt; S save(S entity) {

        Assert.notNull(entity, &quot;Entity must not be null.&quot;);

        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }</code></pre>
<p>새로운 Entity인걸 어떻게 구분하는 것일까?</p>
<pre><code class="language-java">// EntityInformation인터페이스 구현체인 AbstractEntityInformation
public boolean isNew(T entity) {
        ID id = getId(entity);
        Class&lt;ID&gt; idType = getIdType();

        // 식별자 타입이 객체일 때, id가 null이면 새로운 Entity
        if (!idType.isPrimitive()) {
            return id == null; 
        }

        // 식별자 타입이 숫자형 Primitive type일 때, id가 0이면 새로운 Entity
        if (id instanceof Number) {
            return ((Number) id).longValue() == 0L;
        }

        throw new IllegalArgumentException(String.format(&quot;Unsupported primitive id type %s&quot;, idType));
    }</code></pre>
<h3 id="문제-상황">문제 상황</h3>
<p><code>@GeneratedValue</code>를 사용할 때, <code>persist</code>가 호출될 때 Id에 값이 자동 할당된다.</p>
<p>다음과 같은 <code>Item</code> Entity가 있다.</p>
<pre><code class="language-java">@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {

    @Id
    private String id

    public Item(String id) {
        this.id = id;
    }
}
</code></pre>
<p>Q: <strong><code>@GeneratedValue</code>을 사용하지 않고, 직접 Id를 할당한 뒤, save를 호출하면 persist될까 아니면 merge될까?</strong></p>
<pre><code class="language-java">@SpringBootTest
public class MyTest {

    @Autowired
    ItemRepository itemRepositry; // JpaRepository를 상속받은 Repositry

    @Test
    void test() {
        Item item = new Item(&quot;A&quot;)
        itemRepositry.save(item);
    }
}</code></pre>
<p>id를 em.persist를 통해서가 아닌, 그 전에 강제로 할당했다. 그러므로 <code>save()</code>의 로직에 따라 새로운 Entity로 인식하지 못하고 <code>merge</code>를 수행한다.</p>
<p><code>merge</code>를 실행했더니 다음과 같은 쿼리가 발생한다.</p>
<pre><code>1. SELECT 쿼리
2. INSERT 쿼리</code></pre><p>merge는 기본적으로 값을 업데이트 하는 작업을 수행한다 (정확히는 detached된 entity를 다시 persist). 이를 위해 값을 변경할 데이터를 SELECT쿼리로 불러오는 것이다. </p>
<p>값 변경을 위해서는 Dirty Checking을, 값 저장을 위해선 Persist를 사용해야 한다. Merge는 지양해야 한다. </p>
<p>그런데 위 상황처럼 어쩔 수 없이 ID를 직접 할당해야 하는 상황에서는 어떻게 할까?</p>
<h3 id="persistable">Persistable</h3>
<pre><code class="language-java">public interface Persistable&lt;ID&gt; {
    @Nullable
    ID getId();
    boolean isNew();
}
// Entity
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable&lt;String&gt; {

    @Id
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;

    public Item (String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    // Entity가 새로 만들어졌음을 증명하는 조건을 직접 정의
    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}</code></pre>
<p>Entity가 <code>Persistable</code>를 구현하도록 하면, Entity 마다 <code>isNew</code>조건을 직접 정의할 수 있다.</p>
<p><code>생성 날짜</code>는 JPA가 persist할 때 자동으로 설정하므로, 보통은 <code>생성 날짜</code>를 기준으로 <code>isNew</code>를 판단하도록 한다. </p>
<p>보통은 <code>BaseEntity</code>를 만들어서 Entity가 상속 받도록 한다. <a href="">참고</a></p>
<h1 id="projections">Projections</h1>
<p>Spring Data는 필요한 데이터만 뽑아서 조회할 수 있도록 한다.</p>
<h2 id="interface-based">Interface Based</h2>
<h3 id="close-projection">Close Projection</h3>
<pre><code class="language-java">// 뽑아올 데이터만 getter를 만들어 준다.
public interface UsernameOnly {
    String getUsername();
}

// 반환형으로 사용하면 된다.
// repository
List&lt;UsernameOnly&gt; members = findByUsername(&quot;username&quot;);</code></pre>
<p>실제로도 <code>username</code>만 조회하는 SELECT 쿼리가 발생한다.</p>
<p>Spring Data JPA가 인터페이스를 보고 Proxy 기술을 통해 구현체를 만드는 것이다.</p>
<h3 id="open-projection">Open Projection</h3>
<p><code>Close Projection</code>이 SQL 상으로 정확히 필요한 데이터만 불러오는 것과 달리, <code>@Value + SpEL</code>을 사용하면 더 확장성(?) 있는 조회가 가능하다.</p>
<pre><code class="language-java">public interface UsernameAndAge {

    @Value(&quot;#{target.username + &#39; &#39; + target.age}&quot;)
    String getUsername();
}

List&lt;UsernameAndAge&gt; members = findByUsername(&quot;username&quot;);</code></pre>
<p>위 방식을 사용하면 <strong>모든 필드를 다루는 SELECT 쿼리가 발생</strong>하고, Data JPA Level에서 <code>@Value</code>에 명시된 필드만 가지고 원하는 형태로 값을 반환한다.</p>
<h2 id="class-based">Class Based</h2>
<p>클래스 기반으로도 만들 수 있다.</p>
<pre><code class="language-java">public class UsernameOnlyDto {

    private final String username;

    public UsernameOnlyDto(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

List&lt;UsernameOnlyDto&gt; members = findByUsername(&quot;username&quot;);</code></pre>
<ol>
<li>*<em>필드명이 아닌 <code>생성자의 파라미터 이름</code>을 기준으로 SQL 쿼리가 발생한다. *</em></li>
<li>인터페이스 방식이 Proxy 객체를 생성하는 것과 달리, 클래스 방식은 클래스의 객체를 사용한다.</li>
<li><h2 id="generic">Generic</h2>
사용하는 쿼리는 같은데, 원하는 필드만 다를 경우 Generic을 이용할 수 있다.<pre><code class="language-java">// repository
&lt;T&gt; List&lt;T&gt; findByUsername(@Param(&quot;username&quot;) String username, Class&lt;T&gt; type)
</code></pre>
</li>
</ol>
<p>// Class
List<UsernameOnlyDto> members = findByUsername(&quot;username&quot;, UsernameOnlyDto.class);
// Interface
List<UsernameAndAge> members = findByUsername(&quot;username&quot;, UsernameAndAge.class);</p>
<pre><code>## Nested Close Projection
Entity가 중첩된 상황에서 Projection을 사용하면
``` java
public interface NestedClosedProj {

    String getUsername();
    TeamInfo getTeam();

    interface TeamInfo() {
        String getName();
    }
}

List&lt;NestedClosedProj&gt; members = findByUsername(&quot;username&quot;, NestedClosedProj.class)</code></pre><p>다음과 같은 SQL 쿼리가 발생한다.</p>
<pre><code class="language-sql">SELECT
    u.username
    t.teamId,
    t.teamName,
    t.created_date
    t.updated_date
FROM member AS m LEFT OUTER JOIN ...</code></pre>
<p>**
Root에 대해서만 최적화가 발생하고, 이하의 Entity에 대해선 전체를 조회한다는 한계가 있다.
**
조회 대상이 단순한 경우에는 사용해도 괜찮은데, 복잡한 경우에는 QueryDSL로 해결 권장</p>
<h1 id="native-query">Native Query</h1>
<p>가급적 Native Query는 피하는게 좋다. 
사용할 경우에는 Projection와 같이 사용하는게 좋다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Query(
        value = &quot;SELECT * from Member WHERE username = ?&quot;, 
        nativeQuery = true
    )
    Member findByNative(String username);
}

// 사용
Member member = findByNatvie(&quot;name#1&quot;);</code></pre>
<h2 id="주의사항-2">주의사항</h2>
<ol>
<li><p>반환 타입</p>
<ul>
<li>Object[]</li>
<li>Tuple</li>
<li><strong>DTO (Spring Data의 Projection)</strong></li>
</ul>
</li>
<li><p>Sort가 정상적으로 작동 안 할 수 있음</p>
<ul>
<li>코드 레벨에서 직접 정렬 권장</li>
</ul>
</li>
<li><p>컴파일 타임에 SQL 문법 확인 불가능</p>
</li>
<li><p>동적 쿼리 불가능</p>
<ul>
<li>Hibernate에서 가능하긴 함</li>
<li>MyBatis 또는 Spring JDBC Template 사용</li>
</ul>
</li>
</ol>
<h2 id="native-query--interface-projection">Native Query + Interface Projection</h2>
<pre><code class="language-java">public interface MemberProjection {
    Long getId();
    String getUsername();
    String getTeamName();
}

// Native Query에 Paging Projection을 사용할 수 있다.
@Query(
    value = &quot;SELECT m.member_id AS id, m.username, t.name AS teamName FROM Member AS m LEFT JOIN Team AS t&quot;,
    countQuery = &quot;SELECT COUNT(*) FROM Member&quot;,
    nativeQuery = true
)
Page&lt;MemberProjection&gt; findByNavtiveProjection(Pageable pageable);

// 사용
Page&lt;MemberProjection&gt; result = findByNavtiveProjection(PageResult.of(0, 10));</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Data JPA Part.1]]></title>
            <link>https://velog.io/@314_dev/Spring-Data-JPA</link>
            <guid>https://velog.io/@314_dev/Spring-Data-JPA</guid>
            <pubDate>Thu, 30 Mar 2023 06:42:13 GMT</pubDate>
            <description><![CDATA[<h1 id="spring-data-jpa-설정">Spring Data JPA 설정</h1>
<p>Spring Boot를 사용하지 않는 상황이면 다음과 같이 설정해야 한다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableJpaRepositories(basePackages = &quot;Data jpa인터페이스가 위치한 패키지 경로명&quot;)
public class Appliction {
    ...
}</code></pre>
<p>스프링 부트를 사용하면 따로 설정하지 않아도 된다.</p>
<h2 id="맛보기">맛보기</h2>
<pre><code class="language-java">import com.example.demo.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
}</code></pre>
<p>인터페이스를 정의해 놓고, 이를 사용하면 마치 구현체를 사용하는 것 처럼 사용할 수 있다.</p>
<p>Spring Data JPA가 Proxy 기술을 사용해서 자동으로 구현체를 만든뒤 주입하는 것이다.</p>
<p><code>@Repository</code>를 붙이지 않아도 Spring Data JPA가 알아서 처리해준다.</p>
<h3 id="참고">참고</h3>
<p><code>@Repository</code>는 단순히 Component Scan의 대상으로 등록하는 작업 뿐만 아니라, JPA 예외를 Spring의 공통 예외로 변환하는 작업도 수행한다.</p>
<h1 id="jparepository">JpaRepository</h1>
<p><img src="https://velog.velcdn.com/images/314_dev/post/5e7d07ca-9e64-469f-a182-ddd49ac7a86c/image.png" alt=""></p>
<pre><code>Spring Data의 인터페이스
    PagingAndSortingRepository
    CrudRepository
    Repository

Spring Data JPA의 인터페이스
    JpaRepository</code></pre><p><code>JpaRepository</code>는 Spring Data가 제공하는 인터페이스의 JPA 특화 버전이다.</p>
<ol>
<li>기본적인 메서드를 제공한다.</li>
<li><code>쿼리 메서드</code> 기능을 통해 도메인 특화된 메서드를 손쉽게 사용할 수 있다.<h1 id="쿼리-메서드">쿼리 메서드</h1>
도메인 특화된 메서드를 손쉽게 사용하게 하는 기능</li>
</ol>
<h2 id="1-메서드-이름으로-쿼리-생성">1. 메서드 이름으로 쿼리 생성</h2>
<pre><code>이름이 일치하고, 특정 나이 이상인 멤버 조회</code></pre><p>순수 JPA (JQPL)은 다음과 같을 것이다.</p>
<pre><code class="language-java">public List&lt;Member&gt; findByUsernameAndAgeGreaterThan(String username, int age) {
    return em.createQuery(
        &quot;SELECT m FROM Member m WHERE m.username = :username AND m.age &gt; :age&quot;,
        Member.class
    ).setParameter(&quot;username&quot;, username)
    .setParameter(&quot;age&quot;, age)
    .getResultList();
}</code></pre>
<p>Spring Data JPA는 다음처럼 해결 할 수 있다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    public List&lt;Member&gt; findByUsernameAndAgeGreaterThan(String username, int age);
}</code></pre>
<p>어떠한 구현체도 만들지 않고, 단순히 메서드를 선언했는데 실제로 작동한다.</p>
<p>자세한 사용법은 <a href="findByUsernameAndAgeGreaterThan">다음</a>을 참고</p>
<p>쿼리를 직접 작성하지 않아도 되는 장점이 있으나, 다음과 같은 단점도 존재한다.</p>
<ol>
<li>파라미터 개수에 따라 메서드 이름이 너무 길어진다.</li>
<li>모든 종류의 쿼리를 만들지는 못한다.</li>
<li>Entity 필드명이 변경되면 Repository 메서드 이름도 변경해야 한다.</li>
</ol>
<h2 id="2-메서드-이름으로-namedquery-호출">2. 메서드 이름으로 NamedQuery 호출</h2>
<blockquote>
<p>참고: <a href="https://velog.io/@314_dev/JPQL-%EC%A4%91%EA%B8%89#named-query">Named Query</a></p>
</blockquote>
<p><code>NamedQuery</code>를 EntityManager의 <code>createNamedQuery</code>를 사용하여 사용할 수도 있으나, Spring Data JPA를 사용하면 더 편리하게 사용할 수 있다.</p>
<pre><code class="language-java">// Member Entity
@Entity
@NamedQuery(
    name = &quot;Member.findByUsername&quot;
    query = &quot;SELECT M From Member AS m WHERE m.username = :username&quot;
)
public class Member {...}


// repository
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    @Query(name = &quot;Member.findByUsername&quot;)
    List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String username);
}</code></pre>
<p><code>createNamedQuery</code>을 사용하지 않고, Spring Data JPA에서도 <code>NamedQuery</code>를 사용할 수 있다.
<code>@Query</code>를 사용해서 어떤 <code>NamedQuery</code>를 사용할 지 명시하고,
<code>@Param</code>을 사용해서 파라미터를 바인딩 한다.</p>
<p><strong>Spring Data JPA의 NamedQuery는 다음의 관례를 따라 실행된다.</strong></p>
<ol>
<li>@Query의 name에 명시된 NamedQuery를 찾아서 실행</li>
<li>@Query가 없는 경우, <code>Repositry의 대상.메서드_이름</code>으로 된 NamedQuery를 찾아서 실행</li>
<li>NamedQuery가 없는 경우, 메서드 이름으로 쿼리를 생성</li>
</ol>
<h2 id="3-query을-사용해서-쿼리-직접-정의">3. @Query을 사용해서 쿼리 직접 정의</h2>
<p><code>@Query</code>에 JPQL를 직접 사용할 수 있다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Query(&quot;SELECT m FROM Member m WHERE m.username = :username&quot;)
    public List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String username);
}</code></pre>
<p><code>NamedQuery</code>와 <code>@Query + JPQL</code>은 크게 다르지 않다.</p>
<p>둘 다 App Loading 시점에 JPQL을 SQL로 미리 파싱하기 때문에, 그 과정에서 SQL 문법 오류를 확인할 수 있다는 장점이 있다.</p>
<p>실무에서는 NamedQuery보다 <code>@Query + JPQL</code>을 더 많이 사용한다고 한다.</p>
<h1 id="query로-값-dto-조회하기">@Query로 값, DTO 조회하기</h1>
<p><code>@Query</code>를 사용해서 Entity뿐만 아니라 단순 값도 조회할 수 있다.</p>
<pre><code class="language-java">@Query(&quot;SELECT m FROM Member m&quot;)
List&lt;Member&gt; findAll();

@Query(&quot;SELECT m.username FROM Member m&quot;)
List&lt;String&gt; findAllNames();</code></pre>
<p>DTO도 가능하다.</p>
<pre><code class="language-java">// DTO
@Data
@AllArgsConstructor
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;
}

// Repository
@Query(
    &quot;SELECT new 패키지명.MemberDto(m.id, m.username, t.teamName) &quot; + 
    &quot;FROM Member m JOIN FETCH m.team t&quot;
)
List&lt;MemberDto&gt; findMemberDto();</code></pre>
<h1 id="파라미터-바인딩">파라미터 바인딩</h1>
<p>JQPL은 <code>위치 기반</code>, <code>이름 기반</code> 파라미터 바인딩을 지원한다.</p>
<pre><code class="language-java">@Query(&quot;SELECT m FROM Member m WHER m.username = :name&quot;)
public List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String name);

// Collection기반 IN도 지원한다.
@Query(&quot;SELECT m FROM Member m WHER m.username IN :username&quot;)
public List&lt;Member&gt; findByUsernames(@Param(&quot;username&quot;) Collection&lt;String&gt; names);</code></pre>
<h1 id="반환-타입">반환 타입</h1>
<p>Spring Data JPA는 다양한 반환 타입을 지원한다.</p>
<pre><code class="language-java">// Collection
List&lt;Member&gt; findByUsername(String name);
// 단 건
Member findByUsername(String name);
// Optional
Optional&lt;Member&gt; findByUsername(String name);

그 외에도 primitive type, Interator, Stream, Page등 다양한 반환 타입을 지원한다.</code></pre>
<h2 id="주의사항">주의사항</h2>
<ol>
<li>Collection을 사용했는데 조회 결과가 0건이면, null이 아닌 size == 0인 Collection을 반환한다.</li>
<li>단 건을 사용했는데 조회 결과가 0건이면, null을 반환한다.<ul>
<li><strong>순수 JPA는 <code>NoResultException</code>이 발생한다.</strong></li>
<li>Spring Data JPA는 이를 try-catch로 감싼뒤 null을 반환한다.</li>
</ul>
</li>
<li>단 건을 사용했는데 조회 결과가 여러 건이면, <code>IncorrectResultSizeDataAccessException</code>이 발생한다.<ul>
<li>JPA의 <code>NonUniqueResultException</code>이 먼저 발생하고, Spring Data JPA가 이를 Spring Exception으로 변환해서 던진다.</li>
</ul>
</li>
</ol>
<h1 id="페이징-정렬">페이징, 정렬</h1>
<h2 id="순수-jpa">순수 JPA</h2>
<pre><code class="language-java">public List&lt;Member&gt; findByAge(int age, int offeset, int limit) {
    return em.createQuery(
        &quot;SELECT m FROM Member m WHERE m.age = :age ORDER BY m.username DESC&quot;,
        Member.class
    ).setParameter(&quot;age&quot;, age)
    .setFirstResult(offset)
    .setMaxResult(limit)
    .getResultList();
}</code></pre>
<p>요청 페이지 번호에 따라 offset을 계산해야 한다.</p>
<h2 id="spring-data">Spring Data</h2>
<p><code>Spring Data</code>는 정렬, 페이징을 추상화하여 제공한다.</p>
<pre><code>org.springframework.data.domian.Sort
org.springframework.data.domian.Pageable
org.springframework.data.domain.Page  
org.springframework.data.domain.Slice
...</code></pre><pre><code class="language-java">public interface MemberRepository extends JpaRepositry&lt;Member, Long&gt; {

    Page&lt;Member&gt; findByAge(int age, Pageable pageable);
}</code></pre>
<p>위 처럼 인터페이스를 정의하고, 아래 처럼 사용하면 된다.</p>
<pre><code class="language-java">// PageRequest: 페이징 조건
// Pageable 인터페이스 구현체
// 0번째 페이지 부터, 3개를 가져오는데, 다음의 Sort조건을 사용
PageRequest pageRequest = PageRequest.of(0, 3, Sort.of(Sort.Direction.DESC, &quot;username&quot;))

Page&lt;Member&gt; page = memberRepository.findByAge(10, pageRequest);

// 페이징 쿼리 결과
List&lt;Member&gt; content = page.getContent();
// 총 검색 개수
long totalElements = page.getTotalElements();
// 현재 페이지 번호
int pageNumber = page.getNumber()
// 총 페이지 개수
int totalPageNumber = page.getTotalPages();
// 첫 번째 페이지인가?
boolean isFirst = page.isFirst();
// 다음 페이지가 있는가?
boolean hasNext = page.hasNext();</code></pre>
<p><img src="https://velog.velcdn.com/images/314_dev/post/f22995cd-b2e6-48c5-a7e9-f50d0ee2612b/image.png" alt="">
<img src="https://velog.velcdn.com/images/314_dev/post/6ceb38e6-b377-4e1e-b81e-9ff02ab55140/image.png" alt=""></p>
<h2 id="page-slice">Page, Slice</h2>
<pre><code class="language-java">PageRequest pageRequest = PageRequest.of(0, 3, Sort.of(Sort.Direction.DESC, &quot;username&quot;))

Page&lt;Member&gt; page = memberRepository.findByAge(10, pageRequest);
// findByAge가 Page가 아닌 Slice를 반환해야 함
Slice&lt;Member&gt; slice = memberRepository.findByAge(10, pageRequest);</code></pre>
<p>Spring Data는 페이지 번호가 0번 부터 시작한다.</p>
<p><code>Page</code>는 <code>PageRequest</code>에 따라 페이징 쿼리를 날리고, <strong><code>총 개수</code>를 구하는 쿼리 또한 알아서 날린다.</strong></p>
<p><code>Page</code>는 Request에 따라 3개의 데이터를 가져오는 반면, <strong><code>Slice</code>는 추가로 한 개를 더 가져온다.</strong></p>
<p>현재 보여줄 데이터 개수 + 총 데이터 개수를 다루는 <code>Page</code>와 달리, 
<code>Slice</code>는 &#39;더보기&#39; 버튼을 눌렀을 때 동적으로 데이터를 Loading하는 기법에서 사용한다. 
그렇기 때문에 <code>Slice</code>는 <code>getTotalElements</code>와 같은 메서드를 지원하지 않는다.</p>
<pre><code class="language-java">PageRequest pageRequest = PageRequest.of(0, 3, Sort.of(Sort.Direction.DESC, &quot;username&quot;))

List&lt;Member&gt; members = memberRepository.findByAge(10, pageRequest);</code></pre>
<p>Page, Slice 기능이 필요 없으면 그냥 Collection으로 받으면 된다.</p>
<h2 id="countquery">countQuery</h2>
<p>반환 타입에 따라 <code>총 개수 쿼리</code>를 날릴지가 결정된다.</p>
<pre><code>Page는 자동 발생
Slice는 발생 X</code></pre><p><code>총 개수 쿼리</code> JOIN이 들어갈 경우, 데이터 개수가 많아질 수록 성능적인 부담이 생긴다.
<code>총 개수 쿼리</code>는 실제 데이터를 불러올 필요가 없다. 
그러므로 <code>데이터 조회 쿼리</code>와 <code>총 개수 쿼리</code>를 따로 관리할 필요가 있다.</p>
<p>다음과 같이 분리할 수 있다.</p>
<pre><code class="language-java">@Query(
    value = &quot;SELECT m FROM Member m LEFT JOIN m.team t&quot;,
    countQuery = &quot;SELECT COUNT(m.username) FROM Member m&quot;)
Page&lt;Member&gt; findByAge(int age, Pageable pageable);</code></pre>
<h2 id="dto로-감싸기">DTO로 감싸기</h2>
<p><code>Page</code>결과도 DTO로 변환하여 전달해야 한다.</p>
<pre><code class="language-java">Page&lt;Member&gt; page = memberRepository.findByAge(age, pageRequest);

Page&lt;MemberDto&gt; maped = page.map(p -&gt; new MemberDTO(...));</code></pre>
<p><code>Page</code>가 extends하는 <code>Streamable</code>을 통해 람다 비스무리한 방법을 사용할 수 있다.</p>
<h1 id="bulk-operation">Bulk Operation</h1>
<p>JPA는 명식적 Update를 사용하지 않고, Dirty Checking을 통해 Update하기를 권장한다.</p>
<pre><code>모든 직원의 나이를 n씩 올려라</code></pre><p>그런데 위와 같은 작업은 모든 Employee를 불러와서 Dirty Checking으로 하나씩 Update 쿼리를 날릴 필요 없이, Bulk 연산으로 처리하는게 더 효율적이다.</p>
<h2 id="순수-jpa-1">순수 JPA</h2>
<pre><code class="language-java">// 반환값: Update 영향 받은 row 개수
public int bulkAgePlus(int age) {
    return em.createQuery(
        &quot;Update Member m &quot; +
        &quot;SET m.age = m.age + :age&quot; 
    ).setParameter(&quot;age&quot;, age)
    .executeUpdate();
}</code></pre>
<h2 id="spring-data-1">Spring Data</h2>
<pre><code class="language-java">public interface memberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Modifying
    @Query(&quot;Update Member m SET m.age = m.age + :age&quot;)
    public int bulkAgePlus(@Param(&quot;age&quot;) int age);
}</code></pre>
<p><code>@Modifying</code>을 사용하지 않으면 <code>executeUpdate</code>대신에 <code>getResultList, getSingleList</code>가 호출되어서 예외가 발생한다.</p>
<h2 id="주의사항-pc-db-불일치">주의사항: PC-DB 불일치</h2>
<p>Bulk 연산은 Persistence Context를 무시하고 연산을 수행한다. 
즉, Persistence Context와 DB간 데이터 불일치가 발생할 수 있다.</p>
<pre><code class="language-java">memberRepository.save(new Member(&quot;name#1&quot;, 30));
// PC에는 30살로 저장

memberRepository.bulkAgePlus(20);
// Bulk 연산은 PC를 무시하고 바로 DB에 반영
// DB는 50살, PC는 30인 상황

// PC에서 Entity를 찾아옴
Member member = memberRepository.findByName(&quot;name#1&quot;).get(0);

sout(member.getAge()); // 30</code></pre>
<p><strong>따라서 관습적으로 Bulk 연산을 수행하고 나서 PC를 clear해줘야 한다.</strong></p>
<pre><code class="language-java">memberRepository.save(new Member(&quot;name#1&quot;, 30));
// PC에는 30살로 저장

memberRepository.bulkAgePlus(20);

// 혹시 모를 아직 DB에 반영 안된 변경사항 전달
em.flush();
// PC 비우기
em.clear();</code></pre>
<p>Spring Data JPA는 간편한 방법을 제공한다.</p>
<pre><code class="language-java">public interface memberRepository extends JpaRepository&lt;Member, Long&gt; {

    @Modifying(clearAutomatically = true)
    @Query(&quot;Update Member m SET m.age = m.age + :age&quot;)
    public int bulkAgePlus(@Param(&quot;age&quot;) int age);
}</code></pre>
<p><code>@Modifying(clearAutomatically = true)</code>로 설정하면 쿼리 발생 이후 자동으로 PC를 clear한다.</p>
<h1 id="entity-graph">Entity Graph</h1>
<p><code>Spring Data JPA</code>의 <code>findAll</code>을 사용하려고 하는데, 연관관계가 Lazy Loading으로 설정되어 있다. </p>
<p><code>@Query</code>를 사용해서 명시적으로 <code>FETCH JOIN</code>을 사용할 수 있으나, 이러면 <code>Spring Data JPA</code>의 장점을 살리지 못한다.</p>
<p>그러므로 <code>@EntityGraph</code>를 사용한다.</p>
<h2 id="entitygraph">@EntityGraph</h2>
<pre><code class="language-java">@Override
@EntityGraph(attributePaths = {&quot;team&quot;})
List&lt;Member&gt; findAll();</code></pre>
<p><code>@EntityGraph</code>는 이미 존재하는 JQPL에 <code>FETCH JOIN</code>을 추가하는 용도로 사용할 수 있다.</p>
<pre><code class="language-java">@Query(&quot;SELECT m FROM Member m&quot;)
@EntityGraph(attributePaths = {&quot;team&quot;})
List&lt;Member&gt; findMemberEntityGraph();</code></pre>
<p><code>쿼리 메서드 - 메서드 이름으로 쿼리 생성</code> 기능에도 사용할 수 있다.</p>
<pre><code class="language-java">@EntityGraph(attributePaths = {&quot;team&quot;})
List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String username);</code></pre>
<h2 id="namedentitygraph">@NamedEntityGraph</h2>
<p>JPA 표준 스펙에 정의된 <code>@NamedEntityGraph</code>를 사용하면, <code>FETCH JOIN</code>으로 불러올 Entity를 미리 정의해 놓을 수 있다. (마치 <code>@NamedQuery</code> 처럼)</p>
<pre><code class="language-java">// Entity
@Entitty
@NamedEntityGraph(
    name = &quot;Member.all&quot;,
    attributeNodes = @NamedAttributeNode(&quot;team&quot;)
)
public class Member {...}

// Repository
@EntityGraph(&quot;Member.all&quot;)
List&lt;Member&gt; findByUsername(@Param(&quot;username&quot;) String username);</code></pre>
<h2 id="참고-1">참고</h2>
<ol>
<li><code>FETCH JOIN</code>은 기본적으로 <code>LEFT OUTER JOIN</code>을 발생시킨다.
<code>RIGHT JOIN FETCH</code> 처럼 Join 방향을 지정할 수도 있다.</li>
<li>간단한 관계에서만 <code>@EntityGraph</code>를 사용하고, 복잡한 관계에서는 <code>@Query</code>에 명시적으로 <code>FETCH JOIN</code>을 사용한다.</li>
</ol>
<h1 id="jpa-hint">JPA Hint</h1>
<p>SQL이 아닌, JPA 구현체에게 제공하는 힌트를 의미함</p>
<h2 id="예시-readonly">예시: readOnly</h2>
<ol>
<li>JPA는 dirty checking을 위해, snapshot을 저장해 놓고 있음</li>
<li>어떤 비즈니스 로직에서 데이터를 조회하는데, 그 데이터에 변경이 일어나지 않을 것이 확실함</li>
<li>성능 최적화를 위해, 변경이 일어나지 않을 것이 확실한 경우에는 snapshot을 메모리에 저장하지 말도록 하고 싶음</li>
</ol>
<pre><code class="language-java">@QueryHints(value = @QueryHint(name = &quot;org.hibernate.readOnly&quot;, value = &quot;true&quot;))
Member findReadOnlyByUsername(String username);</code></pre>
<h3 id="주의사항-1">주의사항</h3>
<ol>
<li><code>readOnly</code>로 읽은 데이터를 변경하면 dirty checking이 발생하지 않음</li>
<li>모든 <code>ReadOnly Operation</code>에 위 내용을 전부 적용하는 건 생산성 측면에서 좋지 못함. 다음 경우에만 적용하길 권장<ul>
<li>엄청 성능이 중요한 API</li>
<li>성능 개선이 확실한 상황</li>
</ul>
</li>
</ol>
<h1 id="jpa-lock">JPA Lock</h1>
<p>Spring Data JPA에서 <code>Lock</code>을 제공한다.</p>
<pre><code class="language-java">@Lock(LockModeType.PESSIMISTIC_WRITE)
public List&lt;Member&gt; findLockByUsername(String username);</code></pre>
<p>다음과 같은 SQL이 발생한다.</p>
<pre><code class="language-sql">SELECT ...
FROM ...
WHERE member.username = ? FOR UPDATE</code></pre>
<p><code>Lock</code>에 대한 내용은 따로 다루겠다.</p>
]]></description>
        </item>
    </channel>
</rss>