<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://devwithpug.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://devwithpug.github.io/" rel="alternate" type="text/html" /><updated>2024-12-31T23:02:36+09:00</updated><id>https://devwithpug.github.io/feed.xml</id><title type="html">공부하며 끄적이는 장소 🐶</title><subtitle>공부하며 끄적이는 장소</subtitle><author><name>최준규</name></author><entry><title type="html">2024년 회고</title><link href="https://devwithpug.github.io/blog/close-2024/" rel="alternate" type="text/html" title="2024년 회고" /><published>2024-12-31T00:00:00+09:00</published><updated>2024-12-31T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/close-2024</id><content type="html" xml:base="https://devwithpug.github.io/blog/close-2024/"><![CDATA[<p class="align-center"><img src="/assets/teasers/close2024.png" alt="image" /></p>

<h1 id="2024-todo-review">2024 TODO REVIEW</h1>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>개인 프로젝트 시작하기</strong> ❌
    <ul>
      <li>시작이 반이라고 하는데 시작조차 못했다(아니 안했다)</li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>열일해서 좋은 성과 내기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><code class="language-plaintext highlighter-rouge">여행</code>: <strong>인생 첫 렌트해서 여행 다녀오기</strong>
    <ul>
      <li>강원도 속초를 다녀왔다. 혼자 운전한 건 아니라 아쉽지만 ㅎㅎ 계속 연습할 예정</li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><code class="language-plaintext highlighter-rouge">건강</code>: <strong>운동 꾸준히 열심히 하기</strong></li>
</ul>

<h1 id="in-conclusion">IN CONCLUSION</h1>

<ol>
  <li>
    <p>올해는 도전적인 과제들을 몇 가지 진행했는데, 아쉬운 점도 있지만 만족한다. 상세 내용을 블로그에 정리해서 올려야겠다는 생각만 하고 실천을 못한다ㅜㅜ 연말이 된 지금은 살짝 붕 뜬 느낌이 들긴 하지만 2025년도 빡세게 달려보려고 한다.</p>
  </li>
  <li>업무와 별개로 개인 프로젝트에는 손도 못 댔는데, 몇 가지 핑계를 대보자면..
    <ol>
      <li>요새 생활 루틴은 운동 -&gt; 일 -&gt; 잠으로 반복하고있다. 잠이 많다 보니 새벽 운동을 가지는 못하고 아침에 운동을 하고 출근하게 되는데, 그만큼 퇴근도 늦어지다 보니 집에 오면 온전히 개인 프로젝트에 쏟을 시간을 만들지 못했다.</li>
      <li>흥미로운 주제를 찾고 있는데, 프로덕션 정도로 만들어볼 만한 재밌는 주제를 찾기가 쉽지 않다.</li>
      <li>요즘 일이 재미있어서 개발은 온전히 업무 중에만 하게 되더라..</li>
    </ol>
  </li>
  <li>
    <p>운동을 꾸준하게 하다 보니 하루 일과에 빠질 수 없게 되었다. 아직 많이 부족하지만 꾸준함이 있다면 늘어날 것이라 생각하기 때문에 앞으로가 더 기대된다.</p>
  </li>
  <li>
    <p>자취에 대한 준비를 하면서 여러 가지 고민과 선택의 연속이 되고 있는데.. 별 탈 없이 마무리되었으면 좋겠다. 내년 회고를 쓰면서 돌아보면 재미있을 것 같다. ㅎㅎ</p>
  </li>
  <li>올해는 블로그 글을 단 한 번도 쓰지 않았는데, 업무 중에 생긴 이슈들을 올리기에는 조금 애매한 것들이 많았고, 개인 공부를 안 하다 보니 자연스럽게 블로그에도 관심이 떨어진 것 같다. 블로그를 막 시작하던 때에는 하나라도 올려야지라고 생각하는 사소한 목표를 꼭 가지고 있었는데 요즘은 그것보다는 양질의 글을 써야 한다는 생각이 매우 커져서.. 글을 쓰기 전에 고민이 더 앞서게 되는 것 같다. 내년에는 어떻게 될지 모르겠지만, 업무 외적으로 개발을 조금씩 하다 보면 다시 블로그가 활성화될지도 모르겠다.</li>
</ol>

<h1 id="2025-todo-review">2025 TODO REVIEW</h1>

<p class="align-center"><img src="https://github.com/devwithpug/devwithpug.github.io/assets/69145799/c900b5fb-7569-4872-8971-ee8d399a9aba" alt="image" /></p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>개인 프로젝트 시작하기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>오픈소스 컨트리뷰션 도전</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>쌓여있는 책 마저 읽기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">건강</code>: <strong>운동 꾸준히 열심히 하기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">자취</code>: <strong>독립하기 &amp; 집 꾸미기</strong></li>
</ul>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="retrospect" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">2023년 회고</title><link href="https://devwithpug.github.io/blog/takecare-2023/" rel="alternate" type="text/html" title="2023년 회고" /><published>2024-01-26T00:00:00+09:00</published><updated>2024-01-26T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/takecare-2023</id><content type="html" xml:base="https://devwithpug.github.io/blog/takecare-2023/"><![CDATA[<p class="align-center"><img src="/assets/teasers/takecare2023.png" alt="image" /></p>

<h1 id="2023-todo-review">2023 TODO REVIEW</h1>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>미루고 미루고 미루던 블로그 글 작성하기</strong>
    <ul>
      <li>하나밖에 쓰지 못했다 ❌</li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>규칙적으로 살기 / 루틴 설정하기</strong>
    <ul>
      <li>일 -&gt; 운동 -&gt; 공부</li>
      <li>운동 -&gt; 일 -&gt; 공부</li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>먼지가 쌓여가는 밀린 책들 조금씩 읽기</strong>
    <ul>
      <li>완독 성공
        <ul>
          <li><code class="language-plaintext highlighter-rouge">클린 아키텍처 (로버트 C. 마틴)</code></li>
          <li><code class="language-plaintext highlighter-rouge">만들면서 배우는 클린 아키텍처 (톰 홈버그)</code></li>
          <li><code class="language-plaintext highlighter-rouge">자바 ORM 표준 JPA 프로그래밍 (김영한)</code></li>
          <li><code class="language-plaintext highlighter-rouge">리팩터링 2판 (마틴 파울러)</code></li>
          <li><code class="language-plaintext highlighter-rouge">파이브 라인스 오브 코드 (크리스찬 클라우젠)</code></li>
          <li><code class="language-plaintext highlighter-rouge">데이터 중심 애플리케이션 설계 (마틴 클레프만)</code></li>
          <li><code class="language-plaintext highlighter-rouge">도메인 주도 개발 시작하기 (최범균)</code></li>
        </ul>
      </li>
      <li>완독 실패 (언젠간 꼭 완독..)
        <ul>
          <li><code class="language-plaintext highlighter-rouge">도메인 주도 설계 (에릭 에반스)</code></li>
          <li><code class="language-plaintext highlighter-rouge">오브젝트 (조영호)</code></li>
        </ul>
      </li>
    </ul>
  </li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>학업 잘 마무리하기</strong>
    <ul>
      <li>드디어 2월에 졸업 ㅎㅎ</li>
    </ul>
  </li>
</ul>

<h1 id="in-conclusion">IN CONCLUSION</h1>

<ol>
  <li>
    <p>몸과 마음이 건강해지는 한 해였다. 노력한 만큼 뿌듯한 결과를 얻었다고 생각한다.</p>
  </li>
  <li>
    <p>개발 공부도 개인적으로 짬 내서 조금씩은 했다. (조금씩..)</p>
  </li>
  <li>
    <p>일이 재밌었다. 아직 부족하지만 경험하면서 성장하고 있다는 이 느낌이 좋다.</p>
  </li>
</ol>

<h1 id="2024-todo">2024 TODO</h1>

<p class="align-center"><img src="https://github.com/devwithpug/devwithpug.github.io/assets/69145799/c900b5fb-7569-4872-8971-ee8d399a9aba" alt="image" /></p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>개인 프로젝트 시작하기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">개발</code>: <strong>열일해서 좋은 성과 내기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">여행</code>: <strong>인생 첫 렌트해서 여행 다녀오기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">건강</code>: <strong>운동 꾸준히 열심히 하기</strong></li>
</ul>

<blockquote>
  <p>2024년도 화이팅야 ಠ◡ಠ</p>
</blockquote>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="retrospect" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Debezium connector for MySQL: schema history에 대해</title><link href="https://devwithpug.github.io/cdc/debezium-file-schema-history/" rel="alternate" type="text/html" title="Debezium connector for MySQL: schema history에 대해" /><published>2023-09-16T00:00:00+09:00</published><updated>2023-09-16T00:00:00+09:00</updated><id>https://devwithpug.github.io/cdc/debezium-file-schema-history</id><content type="html" xml:base="https://devwithpug.github.io/cdc/debezium-file-schema-history/"><![CDATA[<p><img src="/assets/teasers/debezium-schema-history.png" alt="image" class="align-center" /></p>

<h1 id="개요">개요</h1>

<p>트랜잭셔널 아웃박스 패턴을 위한 CDC 플랫폼 중 <code class="language-plaintext highlighter-rouge">kafka connect</code>의 debezium에 대해 리서치를 해보면서 debezium을 적용하기 위해 고려할 점이 생각보다 많았고, 운영 환경에서 간단하게 사용할 만한 기술이 아님을 알게 되었다.</p>

<p>이번 글에서는 debezium에서 관리하는 스키마 히스토리에 대해 알게 된 점들을 정리해 보고자 한다.</p>

<h2 id="세팅">세팅</h2>

<p>스키마 히스토리를 설명하기에 앞서서 이번 글에서는 <code class="language-plaintext highlighter-rouge">debezium-embedded</code>를 기준으로 스키마 히스토리를 설정하는 방법들을 정리했다. 설정하는 방법은 <code class="language-plaintext highlighter-rouge">kafka connect</code>의 debezium 커넥터(Debezium connector for MySql)과 큰 차이가 없다.</p>

<blockquote>
  <p>ℹ️ 이 글에서는 <code class="language-plaintext highlighter-rouge">kafka connect</code> 에서 source connector 플러그인으로 사용되는 <code class="language-plaintext highlighter-rouge">Debezium connector for MySql</code>과 <code class="language-plaintext highlighter-rouge">debezium-embedded</code> 에서 사용되는 커넥터를 모두 <strong>debezium 커넥터</strong> 라고 부릅니다.</p>
</blockquote>

<blockquote>
  <p>ℹ️ 이 글에서 다루는 <code class="language-plaintext highlighter-rouge">debezium-embedded</code> 환경은 <a href="https://github.com/devwithpug/debezium-mysql-connector-embedded-study" target="_blank">github repo</a>에서 확인 가능합니다.</p>
</blockquote>

<h1 id="스키마-히스토리">스키마 히스토리</h1>

<p>클라이언트가 db에 쿼리를 날릴 때, 클라이언트는 db의 현재 스키마를 사용한다. <br />
그러나 db 스키마는 언제든지 변경될 수 있으므로 debezium 커넥터는 <code class="language-plaintext highlighter-rouge">insert</code>, <code class="language-plaintext highlighter-rouge">update</code>, <code class="language-plaintext highlighter-rouge">delete</code> 작업이 <strong>기록될 당시의 스키마</strong> 를 식별할 수 있어야 한다.</p>

<p>마찬가지로 debezium 커넥터가 모든 이벤트에 반드시 현재 스키마를 적용할 수는 없다. 이벤트가 비교적 오래된 경우, 현재 스키마가 적용되기 전의 이벤트일 수 있기 때문이다.</p>

<h2 id="스키마-히스토리-생성-예제">스키마 히스토리 생성 예제</h2>

<p>스키마 히스토리가 어떻게 생성되는지 확인해보기 위해 아래와 같은 테이블을 생성했다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">table</span> <span class="n">if</span> <span class="k">not</span> <span class="k">exists</span> <span class="nv">`debezium`</span><span class="p">.</span><span class="nv">`test_table`</span><span class="p">(</span>
    <span class="nv">`id`</span> <span class="nb">bigint</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
    <span class="nv">`msg`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span>
<span class="p">);</span>
</code></pre></div></div>

<p>그리고, debezium mysql connector 가 설정된 <code class="language-plaintext highlighter-rouge">kafka connect</code>를 실행하면 아래와 같은 스냅샷 단계를 거치면서 스키마 히스토리가 생성된다.</p>
<blockquote>
  <p>ℹ️ 스냅샷 단계의 자세한 설명은 <a href="https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-snapshots" target="_blank">debezium 공식 문서</a>를 참고 바랍니다.</p>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2023-09-13T22:38:38.494+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 1 - Preparing
2023-09-13T22:38:38.496+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 2 - Determining captured tables
2023-09-13T22:38:38.496+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Read list of available databases
2023-09-13T22:38:38.520+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : 	 list of available databases is: [debezium, information_schema, mysql, performance_schema, sys]
2023-09-13T22:38:38.520+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Read list of available tables in each database
2023-09-13T22:38:38.555+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : 	snapshot continuing with database(s): [information_schema, performance_schema, debezium, mysql, sys]
2023-09-13T22:38:38.556+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Adding table debezium.test_table to the list of capture schema tables
2023-09-13T22:38:38.557+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 3 - Locking captured tables [debezium.test_table]
2023-09-13T22:38:38.564+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Flush and obtain global read lock to prevent writes to database
2023-09-13T22:38:38.570+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 4 - Determining snapshot offset
2023-09-13T22:38:38.575+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Read binlog position of MySQL primary server
2023-09-13T22:38:38.578+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : 	 using binlog 'binlog.000002' at position '3803' and gtid ''
2023-09-13T22:38:38.579+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 5 - Reading structure of captured tables
2023-09-13T22:38:38.579+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : All eligible tables schema should be captured, capturing: [debezium.test_table]
2023-09-13T22:38:39.168+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Reading structure of database 'debezium'
2023-09-13T22:38:39.231+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 5.a - Creating connection pool
2023-09-13T22:38:39.231+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Created connection pool with 1 threads
2023-09-13T22:38:39.231+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 6 - Persisting schema history
2023-09-13T22:38:39.265+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Releasing global read lock to enable MySQL writes
2023-09-13T22:38:39.268+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Writes to MySQL tables prevented for a total of 00:00:00.7
2023-09-13T22:38:39.268+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Snapshot step 7 - Snapshotting data
2023-09-13T22:38:39.269+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : Creating snapshot worker pool with 1 worker thread(s)
2023-09-13T22:38:39.270+09:00  INFO 5878 --- [rce-coordinator] .d.r.RelationalSnapshotChangeEventSource : For table 'debezium.test_table' using select statement: 'SELECT `id`, `msg` FROM `debezium`.`test_table`'
2023-09-13T22:38:39.283+09:00  INFO 5878 --- [rce-coordinator] i.d.c.m.MySqlSnapshotChangeEventSource   : Estimated row count for table debezium.test_table is OptionalLong[0]
2023-09-13T22:38:39.285+09:00  INFO 5878 --- [pool-5-thread-1] .d.r.RelationalSnapshotChangeEventSource : Exporting data from table 'debezium.test_table' (1 of 1 tables)
2023-09-13T22:38:39.301+09:00  INFO 5878 --- [pool-5-thread-1] .d.r.RelationalSnapshotChangeEventSource : 	 Finished exporting 1 records for table 'debezium.test_table' (1 of 1 tables); total duration '00:00:00.016'
2023-09-13T22:38:39.306+09:00  INFO 5878 --- [rce-coordinator] .d.p.s.AbstractSnapshotChangeEventSource : Snapshot - Final stage
2023-09-13T22:38:39.306+09:00  INFO 5878 --- [rce-coordinator] .d.p.s.AbstractSnapshotChangeEventSource : Snapshot completed
</code></pre></div></div>

<p>스냅샷이 완료된 후, 생성된 스키마 히스토리를 보면 아래와 같다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"source":{"server":"local-test"},"position":{"ts_sec":1694612318,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319142,"databaseName":"","ddl":"SET character_set_server=utf8mb4, collation_server=utf8mb4_bin","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319160,"databaseName":"debezium","ddl":"DROP TABLE IF EXISTS `debezium`.`test_table`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319172,"databaseName":"debezium","ddl":"DROP DATABASE IF EXISTS `debezium`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319177,"databaseName":"debezium","ddl":"CREATE DATABASE `debezium` CHARSET utf8mb4 COLLATE utf8mb4_bin","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319179,"databaseName":"debezium","ddl":"USE `debezium`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319230,"databaseName":"debezium","ddl":"CREATE TABLE `test_table` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `msg` varchar(255) COLLATE utf8mb4_bin NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin","tableChanges":[{"type":"CREATE","id":"\"debezium\".\"test_table\"","table":{"defaultCharsetName":"utf8mb4","primaryKeyColumnNames":["id"],"columns":[{"name":"id","jdbcType":-5,"typeName":"BIGINT","typeExpression":"BIGINT","charsetName":null,"position":1,"optional":false,"autoIncremented":true,"generated":true,"comment":null,"hasDefaultValue":false,"enumValues":[]},{"name":"msg","jdbcType":12,"typeName":"VARCHAR","typeExpression":"VARCHAR","charsetName":"utf8mb4","length":255,"position":2,"optional":false,"autoIncremented":false,"generated":false,"comment":null,"hasDefaultValue":false,"enumValues":[]}],"attributes":[]},"comment":null}]}
</code></pre></div></div>

<p>각각의 스키마 히스토리가 json 포맷으로 한 라인씩 저장되어 있는데, 가장 마지막 히스토리를 자세히 보면 아래와 같다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"server"</span><span class="p">:</span><span class="w"> </span><span class="s2">"local-test"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ts_sec"</span><span class="p">:</span><span class="w"> </span><span class="mi">1694612319</span><span class="p">,</span><span class="w">
    </span><span class="nl">"file"</span><span class="p">:</span><span class="w"> </span><span class="s2">"binlog.000002"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pos"</span><span class="p">:</span><span class="w"> </span><span class="mi">3803</span><span class="p">,</span><span class="w">
    </span><span class="nl">"snapshot"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ts_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">1694612319230</span><span class="p">,</span><span class="w">
  </span><span class="nl">"databaseName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"debezium"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"ddl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CREATE TABLE `test_table` (</span><span class="se">\n</span><span class="s2">  `id` bigint NOT NULL AUTO_INCREMENT,</span><span class="se">\n</span><span class="s2">  `msg` varchar(255) COLLATE utf8mb4_bin NOT NULL,</span><span class="se">\n</span><span class="s2">  PRIMARY KEY (`id`)</span><span class="se">\n</span><span class="s2">) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tableChanges"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CREATE"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">debezium</span><span class="se">\"</span><span class="s2">.</span><span class="se">\"</span><span class="s2">test_table</span><span class="se">\"</span><span class="s2">"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"table"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"defaultCharsetName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"utf8mb4"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"primaryKeyColumnNames"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="s2">"id"</span><span class="w">
        </span><span class="p">],</span><span class="w">
        </span><span class="nl">"columns"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"id"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"jdbcType"</span><span class="p">:</span><span class="w"> </span><span class="mi">-5</span><span class="p">,</span><span class="w">
            </span><span class="nl">"typeName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIGINT"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"typeExpression"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIGINT"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"charsetName"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
            </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
            </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"autoIncremented"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
            </span><span class="nl">"generated"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
            </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
            </span><span class="nl">"hasDefaultValue"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"enumValues"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
          </span><span class="p">},</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"msg"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"jdbcType"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span><span class="w">
            </span><span class="nl">"typeName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"VARCHAR"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"typeExpression"</span><span class="p">:</span><span class="w"> </span><span class="s2">"VARCHAR"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"charsetName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"utf8mb4"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"length"</span><span class="p">:</span><span class="w"> </span><span class="mi">255</span><span class="p">,</span><span class="w">
            </span><span class="nl">"position"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
            </span><span class="nl">"optional"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"autoIncremented"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"generated"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
            </span><span class="nl">"hasDefaultValue"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
            </span><span class="nl">"enumValues"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">],</span><span class="w">
        </span><span class="nl">"attributes"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>위 히스토리는 스키마 변경 이벤트 메시지이고, 다음과 같은 필드를 가지고 있다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ddl</code>: 스키마 변경을 초래하는 ddl (create, alter, drop)</li>
  <li><code class="language-plaintext highlighter-rouge">databaseName</code>: ddl 문이 적용되는 데이터베이스의 이름이다. 이때 데이터베이스 이름은 메시지 key 역할을 한다.</li>
  <li><code class="language-plaintext highlighter-rouge">pos</code> binlog에서 해당 ddl의 위치</li>
  <li><code class="language-plaintext highlighter-rouge">tableChanges</code>: 스키마 적용 후 전체 테이블 스키마의 구조화된 표현이다. <code class="language-plaintext highlighter-rouge">tableChanges</code> 필드에는 테이블의 각 column에 대한 항목의 배열이 포함된다. 구조화된 표현은 json 또는 avro 포맷으로 데이터를 제공하기 때문에, 컨슈머는 ddl parser를 통해 먼저 처리하지 않고도 데이터를 쉽게 읽을 수 있다.</li>
</ul>

<blockquote>
  <p>ℹ️ debezium 커넥터의 스키마 변경 이벤트 포맷은 인큐베이팅 중이며 예고 없이 변경될 수 있다고 한다. <br />
이 글은 <strong>debezium-embedded, debeizum-conenctor-mysql 2.3.2.Final</strong> 버전을 기준으로 작성하였습니다.</p>
</blockquote>

<p>스키마 변경 후 발생하는 이벤트를 올바르게 처리하기 위해, MySQL은 데이터에 영향을 미치는 row-level 변경뿐만 아니라, 데이터베이스에 적용되는 ddl 문도 binlog에 포함한다. debezium 커넥터는 binlog에서 이러한 ddl 문을 발견하면 이를 파싱 하여 각 테이블의 스키마의 <strong>인메모리 데이터</strong> 를 업데이트한다. debezium 커넥터는 이 스키마 표현을 사용하여 각 <code class="language-plaintext highlighter-rouge">insert</code>, <code class="language-plaintext highlighter-rouge">update</code>, <code class="language-plaintext highlighter-rouge">delete</code> 작업 시점에 테이블의 구조를 식별하고 적절한 cdc 이벤트를 생성한다.</p>

<p>만약 debezium 커넥터가 크래시 또는 정상 종료(graceful stop) 후 재시작 하면, debezium 커넥터는 스키마 히스토리 데이터를 읽고 debezium 커넥터가 시작되는 binlog의 시점까지 모든 ddl 문의 구문을 분석하여 해당 시점에 존재했던 테이블 구조를 다시 빌드한다.(인 메모리)</p>

<h1 id="스키마-히스토리-데이터-관리">스키마 히스토리 데이터 관리</h1>

<p>스키마 히스토리는 해당 시점의 테이블 구조를 위해 반드시 필요한 데이터임을 알게 되었다. 그런데, 이러한 스키마 히스토리를 debezium 커넥터에서는 따로 저장하지 않고, 인 메모리로만 관리한다.</p>

<p>즉, <strong>debezium 커넥터의 상태와 관계없이 스키마 히스토리는 완전하게 보존되어야 함을 의미한다.</strong> 이번에는 스키마 히스토리 데이터를 관리하는 방법에 대해 알아보려 한다.</p>

<p>debezium 에서는 두 가지 방법을 제공한다.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">KafkaSchemaHistory</code>: kafka 토픽으로 관리</li>
  <li><code class="language-plaintext highlighter-rouge">FileSchemaHistory</code>: 파일로 관리</li>
</ol>

<h2 id="kafkaschemahistory">KafkaSchemaHistory</h2>

<p>debezium 커넥터에서는 기본으로 kafka 토픽을 통해 스키마 히스토리 데이터를 관리하는 방법을 제시한다.</p>

<p>데이터베이스의 테이블에 적용되는 스키마 변경 이벤트를 생성하도록 debezium 커넥터를 구성할 수 있는데, 이때 debezium 커넥터는 <code class="language-plaintext highlighter-rouge">topic.prefix</code> 설정 프로퍼티의 값으로 kafka 토픽에 스키마 히스토리를 기록한다.</p>

<p>기본적으로는 <code class="language-plaintext highlighter-rouge">payload</code> 필드를 보내고, 선택적으로 <code class="language-plaintext highlighter-rouge">schema</code> 필드도 히스토리 데이터에 포함할 수 있다.</p>

<p>kafka 토픽을 통해 스키마 히스토리를 관리하는 경우 몇 가지 고려할 점이 있다.</p>

<h3 id="설정">설정</h3>

<h4 id="토픽-생성">토픽 생성</h4>

<p>먼저, 데이터베이스의 스키마 히스토리 데이터를 관리할 kafka 토픽을 생성해야 한다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bin/kafka-topics.sh <span class="nt">--create</span> <span class="nt">--topic</span> &lt;topic_name&gt; <span class="nt">--partitions</span> 1 <span class="nt">--replication-factor</span> &lt;number-of-replication-factor&gt; <span class="nt">--bootstrap-server</span> &lt;broker host:port&gt; <span class="nt">--config</span> retention.ms<span class="o">=</span><span class="nt">-1</span>
</code></pre></div></div>

<p>토픽을 생성할 때 주의할 점은 파티션을 1개로 설정해야 하고, 토픽의 retention을 가능하다면 무제한(-1)으로 설정하는 것이 좋은데, 그 이유는 아래에서 자세히 설명한다.</p>

<h4 id="debezium-커넥터-설정">debezium 커넥터 설정</h4>

<ul>
  <li>(1) <code class="language-plaintext highlighter-rouge">debezium-embedded</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">debezium-embedded</code>의 경우 debezium engine을 구성할 때, 아래와 같이 설정 값을 넘겨주면 된다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@Bean</span>
    <span class="k">fun</span> <span class="nf">engine</span><span class="p">():</span> <span class="nc">EmbeddedEngine</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nc">EmbeddedEngine</span><span class="p">.</span><span class="nc">BuilderImpl</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">using</span><span class="p">(</span><span class="nf">embeddedEngineConfig</span><span class="p">())</span>
            <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">embeddedEngineConfig</span><span class="p">()</span> <span class="p">=</span> <span class="n">io</span><span class="p">.</span><span class="n">debezium</span><span class="p">.</span><span class="n">config</span><span class="p">.</span><span class="nc">Configuration</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span>
        <span class="nf">mapOf</span><span class="p">(</span>
            <span class="s">"schema.history.internal"</span> <span class="n">to</span> <span class="nc">KafkaSchemaHistory</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="s">"schema.history.internal.kafka.topic"</span> <span class="n">to</span> <span class="s">"{스키마 히스토리를 저장할 kafka 토픽 명}"</span>
            <span class="s">"schema.history.internal.kafka.bootstrap.servers"</span> <span class="n">to</span> <span class="s">"{kafka 서버 주소}"</span>
            <span class="c1">// 다른 설정 값 생략</span>
        <span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div>

<ul>
  <li>(2) <code class="language-plaintext highlighter-rouge">kafka conenct</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">kafka connect</code>를 통해 서버를 구성하는 경우, source connector를 생성할 때, 아래와 같이 <code class="language-plaintext highlighter-rouge">json</code> 포맷으로 설정값을 넘겨주면 된다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">#</span><span class="w"> </span><span class="err">POST</span><span class="w"> </span><span class="err">(kafka-conenct-host):(port)/connectors</span><span class="w">

</span><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-test-connector"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"schema.history.internal.kafka.bootstrap.servers"</span><span class="p">:</span><span class="w"> </span><span class="s2">"localhost:9092"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"schema.history.internal.kafka.topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"debezium-history-events"</span><span class="w">
    </span><span class="err">#</span><span class="w"> </span><span class="err">다른</span><span class="w"> </span><span class="err">설정</span><span class="w"> </span><span class="err">값</span><span class="w"> </span><span class="err">생략</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="토픽의-파티션-개수">토픽의 파티션 개수</h3>

<p>데이터베이스의 스키마 히스토리 토픽은 <strong>파티션을 반드시 1개로 구성해야 한다.</strong></p>

<p>그 이유는 kafka가 동작하는 방식 때문이다. debezium 커넥터가 내보내는 이벤트 레코드의 순서 보장을 위해서는 파티션을 여러 개로 나누면 안 되기 때문이다. 토픽의 파티션을 여러개로 나누어 이벤트 레코드를 파티션 별로 분할하는 것은 일관된 전역 순서를 유지하는 것과 맞지 않다.</p>

<p>따라서, 직접 스키마 히스토리 토픽을 생성할 때 파티션 수를 1로 지정하거나, 스키마 히스토리 토픽을 자동 생성하는 경우에는 kafka의 <code class="language-plaintext highlighter-rouge">num.partitions</code> 값을 1로 설정해야 한다.</p>

<h3 id="토픽의-retention-설정">토픽의 retention 설정</h3>

<p>토픽의 retention 에 대해 설명하기 전에 debezium 에서 제공하는 여러 스냅샷 모드에 대해 알 필요가 있다.</p>

<p>debezium은 스냅샷을 실행할 때 다양한 모드를 사용할 수 있다. 스냅샷 모드는 <code class="language-plaintext highlighter-rouge">snapshot.mode</code> 설정값에 의해 결정되며, 기본 값은 <code class="language-plaintext highlighter-rouge">initial</code> 이다.</p>

<ul>
  <li>스냅샷 모드</li>
</ul>

<blockquote>
  <p>ℹ️ <code class="language-plaintext highlighter-rouge">snapshot.mode</code></p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">initial</code> - 서버의 오프셋이 기록되지 않은 경우에만 스냅샷을 실행한다.</li>
    <li><code class="language-plaintext highlighter-rouge">initial_only</code> - 서버의 오프셋이 기록되지 않은 경우에 초기 스냅샷까지만 실행한다.(binlog에서 변경 이벤트는 읽지 않는다)</li>
    <li><code class="language-plaintext highlighter-rouge">when_needed</code> - debezium 커넥터가 스냅샷이 필요하다고 판단할 때 실행한다.(아래 경우 참고)
      <ul>
        <li>서버의 오프셋이 기록되지 않은 경우</li>
        <li>기록된 오프셋이 사용할 수 없는 binlog 위치 또는 GTID를 지정하는 경우</li>
      </ul>
    </li>
    <li><code class="language-plaintext highlighter-rouge">never</code> - 스냅샷을 사용하지 않는다. 처음 시작될 때 서버의 binlog 시작 부분부터 읽는다. binlog가 데이터베이스의 전체 기록을 포함하도록 보장되는 경우에만 유효하다.(binlog의 보관 기간이 무제한이어야 한다)</li>
    <li><code class="language-plaintext highlighter-rouge">schema_only</code> - 스냅샷 시점의 테이블 구조만 스냅샷으로 찍는다. 지금부터 발생하는 변경 사항만 토픽으로 보내는 경우에 유용하다.</li>
    <li><code class="language-plaintext highlighter-rouge">schema_only_recovery</code> - 손상되거나 손실된 데이터베이스 복구하거나 예기치 못하게 스키마 히스토리 데이터가 크게 증가하는 경우 이를 정리하는 작업이 필요하다. 
이때, <code class="language-plaintext highlighter-rouge">schema_only_recovery</code> 모드를 통해 복구가 가능한데, 이를 위해선 히스토리 스키마의 데이터가 완전히 보존되어야 한다. 즉, 토픽의 retention을 infinite로 설정해 주어야 한다.</li>
  </ul>
</blockquote>

<p>여기서 주의 깊게 보아야 하는 스냅샷 모드는 <code class="language-plaintext highlighter-rouge">never</code>, <code class="language-plaintext highlighter-rouge">schema_only_recovery</code> 라고 생각한다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">schema_only_recovery</code></li>
</ul>

<p>먼저 <code class="language-plaintext highlighter-rouge">schema_only_recovery</code> 의 경우, 특정 상황에서 복구가 필요한 경우에 사용할 수 있는 모드임을 알 수 있다. 하지만 복구를 하기 위해선 히스토리 스키마의 데이터가 완전히 보존되어야 한다.</p>

<p>스키마 히스토리를 kafka 토픽으로 관리하고, 비상 상황에도 안전하게 복구를 진행하기 위해서는 <strong>토픽의 retention을 무제한으로 설정해야 한다.</strong></p>

<p>retention 은 kafka broker를 통해 손쉽게 설정을 변경할 수 있지만, kafka의 메시지 retention을 무제한으로 설정한다는 것은 고민이 필요할 수 있다. kafka는 데이터를 영구적으로 보관하는 데이터베이스의 용도로 설계한 것이 아니다. 물론 특별한 케이스들을 위해서 retention을 무제한으로 가져갈 수 있겠지만, 프로덕션 환경에서 토픽의 retention을 무제한으로 설정하는 것에 대해서는 더 깊은 고민이 필요하다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">never</code></li>
</ul>

<p>두 번째는 <code class="language-plaintext highlighter-rouge">never</code>의 경우이다. 앞서 말한 <code class="language-plaintext highlighter-rouge">schema_only_recovery</code>가 토픽의 retention을 무제한으로 설정해야 하는데, 그게 어렵다면 스냅샷을 사용하지 않는 <code class="language-plaintext highlighter-rouge">never</code> 모드에 대해 고민해볼 수 있다.</p>

<p>하지만, 스냅샷 모드를 <code class="language-plaintext highlighter-rouge">never</code> 로 설정하는 경우 스냅샷 자체를 사용하지 않으므로 <strong>debezium 커넥터로 실행될 때마다 서버의 binlog를 시작 지점부터 읽게 된다.(binlog를 영구 보관해야한다)</strong></p>

<p>binlog를 영구 보관하는 케이스가 실제 있는지는 모르지만, binlog를 영구적으로 보관하려면 binlog를 보관하는 저장소에 대한 고민이 필요하다.</p>

<p>또한, 매우 오래된 데이터베이스의 binlog가 어마어마하게 많이 쌓여있다고 가정했을 때, debezium 커넥터가 실행되면서 binlog의 시작 부분부터 전부 읽어야 한다면, 이와 연관된 문제가 발생할 것 같다는 생각이 든다.</p>

<ul>
  <li>정리</li>
</ul>

<p>데이터베이스의 스키마 히스토리를 kafka 토픽을 통해 관리하는 KafkaSchemaHistory 방법은 <code class="language-plaintext highlighter-rouge">kafka connect</code>를 사용하는 인프라 구조에서 간단하게 구성할 수 있는 방법이지만, 개인적인 생각으로는 이를 실제 운영에서 사용하기 위해서 더 깊은 고민이 필요하다고 느낀다.</p>

<h2 id="fileschemahistory">FileSchemaHistory</h2>

<blockquote>
  <p>🛑 <strong>주의!</strong> <br />
<code class="language-plaintext highlighter-rouge">kafka connect</code>의 <code class="language-plaintext highlighter-rouge">Debezium connector for MySql</code> 플러그인에서는 FileSchemaHistory 설정이 불가능하다.(현재 공식 문서 기준으로 제공되고 있지 않다)</p>
</blockquote>

<p>debezium 에서는 파일을 통해 스키마 히스토리를 관리할 수 있는 방법을 제공한다.</p>

<p>KafkaSchemaHistory 와 다르게, 스키마 히스토리 데이터를 파일로 직접 관리하기 때문에, kafka 토픽으로 관리할 때 고려할 점들을 모두 신경 쓰지 않고 오로지 파일을 직접 관리할 수 있다는 것이 장점이라고 생각한다.</p>

<p>파일을 위해 복잡한 설정을 할 필요 없이, 스키마 히스토리 데이터 파일을 저장할 경로만 지정해 주면 설정이 끝난다.</p>

<h3 id="설정-1">설정</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">private</span> <span class="k">fun</span> <span class="nf">embeddedEngineConfig</span><span class="p">()</span> <span class="p">=</span> <span class="n">io</span><span class="p">.</span><span class="n">debezium</span><span class="p">.</span><span class="n">config</span><span class="p">.</span><span class="nc">Configuration</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span>
        <span class="nf">mapOf</span><span class="p">(</span>
            <span class="s">"schema.history.internal"</span> <span class="n">to</span> <span class="nc">FileSchemaHistory</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="s">"schema.history.internal.file.filename"</span> <span class="n">to</span> <span class="s">"schemahistory.dat"</span><span class="p">,</span> <span class="c1">// 스키마 히스토리 데이터 파일 경로</span>
            <span class="c1">// 다른 설정 값 생략</span>
        <span class="p">)</span>
    <span class="p">)</span>
</code></pre></div></div>

<p>FileSchemaHistory를 설정한 뒤 <code class="language-plaintext highlighter-rouge">debezium-embedded</code> 서버를 실행하면 설정한 경로에 아래와 같이 스키마 히스토리 데이터가 저장된 것을 확인할 수 있다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"source":{"server":"local-test"},"position":{"ts_sec":1694612318,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319142,"databaseName":"","ddl":"SET character_set_server=utf8mb4, collation_server=utf8mb4_bin","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319160,"databaseName":"debezium","ddl":"DROP TABLE IF EXISTS `debezium`.`test_table`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319172,"databaseName":"debezium","ddl":"DROP DATABASE IF EXISTS `debezium`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319177,"databaseName":"debezium","ddl":"CREATE DATABASE `debezium` CHARSET utf8mb4 COLLATE utf8mb4_bin","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319179,"databaseName":"debezium","ddl":"USE `debezium`","tableChanges":[]}
{"source":{"server":"local-test"},"position":{"ts_sec":1694612319,"file":"binlog.000002","pos":3803,"snapshot":true},"ts_ms":1694612319230,"databaseName":"debezium","ddl":"CREATE TABLE `test_table` (\n  `id` bigint NOT NULL AUTO_INCREMENT,\n  `msg` varchar(255) COLLATE utf8mb4_bin NOT NULL,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin","tableChanges":[{"type":"CREATE","id":"\"debezium\".\"test_table\"","table":{"defaultCharsetName":"utf8mb4","primaryKeyColumnNames":["id"],"columns":[{"name":"id","jdbcType":-5,"typeName":"BIGINT","typeExpression":"BIGINT","charsetName":null,"position":1,"optional":false,"autoIncremented":true,"generated":true,"comment":null,"hasDefaultValue":false,"enumValues":[]},{"name":"msg","jdbcType":12,"typeName":"VARCHAR","typeExpression":"VARCHAR","charsetName":"utf8mb4","length":255,"position":2,"optional":false,"autoIncremented":false,"generated":false,"comment":null,"hasDefaultValue":false,"enumValues":[]}],"attributes":[]},"comment":null}]}
</code></pre></div></div>

<h1 id="wrap-up">Wrap-up</h1>

<p>이 글에서는 아래와 같은 내용을 다루었다.</p>

<ol>
  <li>스키마 히스토리의 생성</li>
  <li>스키마 히스토리 데이터의 포맷</li>
  <li>스키마 히스토리의 역할</li>
  <li>스키마 히스토리 데이터를 관리하는 두 가지 방법</li>
  <li>debezium 커넥터의 스냅샷 모드</li>
  <li>스키마 히스토리를 kafka 토픽으로 관리할 때 고려할 점</li>
</ol>

<p>kafka connect를 통해 파이프라인을 구축하는 경우 MySQL을 사용하는 많은 사람들이 debezium 사용을 고려해 볼 것 같은데, 이러한 고민에 도움이 되길 바라면서 글을 정리해 보았다. 좋은 기술도 적절하게 사용해야 좋은 결과를 얻을 수 있기도 하고.. debezium 을 프로덕션 레벨에서 사용하기 위해 고려할 점이 많은 것 같다는 생각이 들었다.</p>

<blockquote>
  <p>이 글은 debezium 을 사용하기 전에 고민했던 부분들을 정리했던 글입니다. <br />
잘못된 정보 또는 더 나은 방법이 있을 수 있습니다 😊 <br />
글에 수정이 필요하거나 의견이 있는 경우 자유롭게 코멘트를 남겨주세요.</p>
</blockquote>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://debezium.io/documentation/reference/stable/connectors/mysql.html" target="_blank">Debezium connector for MySQL</a></li>
  <li><a href="https://debezium.io/documentation/reference/stable/operations/embedded.html" target="_blank">Embedding Debezium Connectors</a></li>
</ul>]]></content><author><name>최준규</name></author><category term="cdc" /><category term="messaging" /><category term="kafka" /><category term="debezium" /><category term="cdc" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">주절주절 정리 안된 2022년 회고</title><link href="https://devwithpug.github.io/blog/farewell-2022/" rel="alternate" type="text/html" title="주절주절 정리 안된 2022년 회고" /><published>2023-01-28T00:00:00+09:00</published><updated>2023-01-28T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/farewell-2022</id><content type="html" xml:base="https://devwithpug.github.io/blog/farewell-2022/"><![CDATA[<p><img src="/assets/teasers/farewell2022.png" alt="image" class="align-center" /></p>

<p>조금 많이 늦었지만… 2022년 13월에 회고를 작성하게 되었다..!</p>

<h1 id="2022">2022</h1>

<h2 id="good-point">GOOD POINT</h2>

<p>2022년 회고는 처음 직장에서 업무를 하면서 느낀 점들을 마구잡이로 적어보려고 한다.</p>

<p>2021년 11월, 공채에 합격하여 장장 3개월이라는 긴 시간 동안 온보딩을 진행하고.. 2022년 2월, 드디어 팀에 합류하게 되었다. 정말 감사하게도 비교적 최신 기술들로 이루어진 서비스를 개발하는 팀이었는데, 이곳에서 첫발을 디딜 수 있다는 생각에 너무너무 기분이 좋았다. 프로젝트와 함께 성장해나가야겠다고 생각했다. 대부분 모르고 새로운 것들이었지만, 누구나 처음은 있지 않은가? 돌아보면, 차곡차곡 지식을 쌓아가려고 노력했던 것 같다.</p>

<p>개발 및 업무 환경, 업무 프로세스, 모두 하나 빠짐없이 좋지만, 그 무엇보다 뛰어난 팀원들과 함께 일하는 점이 가장 좋았다. 다들 나에 비해 경험이 많기에, 팀원들의 코드 스타일, 아키텍처 설계와 고민 포인트, 코드 리뷰, 업무 진행, 협업 방식들과 같이, 개발자로서의 배울 점뿐만 아니라, 직장인으로서도 많은 것을 배웠다. 또한, 문제에 대해 진심으로 함께 고민해 주고, 내가 업무를 진행하면서 놓치는 부분들에 조언해 주는 점이 매우 감사했다. 이렇게 좋은 동료들을 만났기에 나는 더 잘해야 하고, 더 성장해야 한다.</p>

<p>올해는 새로운 경험의 연속이었다. 내부 코드를 개선하기도 하고, 프로젝트를 맡아 일정에 맞추어 개발도 해보고, 서비스 장애를 경험 해보고, 대량 트래픽에 대비하여 성능 테스트도 경험했다. 재밌는 점은, 이렇게 많은 새로운 경험과 업무를 진행할 땐 모르는데, 항상 업무가 마무리되어 갈 시점 또는, 마무리되고 나서는 아쉬움이 남는다는 것이다. <code class="language-plaintext highlighter-rouge">'왜 처음부터 이렇게 하지 않았을까? 불필요한 시간 낭비를 줄일 수 있었는데!'</code> 라는 생각이 들 수도 있다. 하지만 지금 글을 쓰면서 느끼는 점은, 업무를 진행하는 것도 코드를 작성하는 것과 같다는 점이다. 처음에는 문제 해결을 위해 생각한 것보다 훨씬 많은 시간을 쏟게 되는데, 그 시간은 절대 낭비가 아니다. 그 모든 시간이 경험으로 남게 되고, 아쉬웠던 부분 하나하나가 성장 포인트가 될 수 있다고 생각한다.</p>

<h2 id="pain-point">PAIN POINT</h2>
<p>몇 가지 아쉬웠던 점을 적어보자면..</p>

<p>팀에 합류하고 초반에는 팀원들의 신뢰를 얻기 위해 주도적인 사람이 되려 했었는데, 되려 항상 조급한 마음이 컸다. 개발을 하다 보면 언제나 놓치는 부분이 있을 수 있기 때문에, 그 빈도를 줄여 나가는 것이 성장하는 것이라는 느낌이 들었다. 처음부터 완벽할 수는 없다고 생각한다. 앞으로는 조급해하지 말고 조금씩 실수를 줄여나가려 한다.</p>

<p>자잘한 실수를 몇 번 하다 보니, 내가 작성한 코드에 자신감이 부족했었다. 완벽하다고 생각했는데도,  놓쳤던 부분이 있었고 그걸 뒤늦게 알게 되었을 때, 마음속으로 너무 아쉬웠던 기억이 있다. 개발자는 자신이 작성하는 코드에 자신감이 있어야 한다고 생각한다. 이 부분도 차차 개선해나가야겠다.</p>

<p>팀원들이 보는 나는 어떤 개발자일까? 내가 믿을 수 있고 정말 필요한 개발자일까? 팀에 합류한지 1년이 다 되어가는 시점에서 갑자기 궁금했다. ㅎㅎ 조금 더 자신감을 가지고 팀에 더욱더 열심히 기여할 필요가 있다고 생각한다.</p>

<h1 id="2023">2023</h1>

<p><img src="https://user-images.githubusercontent.com/69145799/211039468-8131c3d3-57e1-49fe-b533-919f78a9eec8.png" alt="image" class="align-center" /></p>

<blockquote>
  <p><strong>(작년 회고) 2021 회고 - 앞으로의 목표</strong> <br />
[✅] 주니어 개발자라고 소개할 수 있는 자격 갖추기 <br />
[❌] 다음 회고를 쓸 때 과거에 대한 후회가 없도록 하기 <br />
[❌] 블로그 꾸준히 쓰는 습관을 잃지 않기</p>
</blockquote>

<p>작년 회고에서 위와 같이 목표를 설정했었는데, 모두 만족하진 못했다. 많이 부족했고, 목표를 설정할 때는 간단한 목표라고 생각했는데, 그렇지 못했던 것 같다. 100점으로 1년을 마무리하진 못했지만, 어떻게 1년을 마무리해버렸다.</p>

<p>2023년 목표도 얼마나 잘 이룰지 모르겠지만, 간단하게나마 생각해 보았다.</p>

<p>1년 뒤에 나야, 잘 부탁해~</p>

<h2 id="todo">TODO</h2>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>미루고 미루고 미루던 블로그 글 작성하기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>규칙적으로 살기 / 루틴 설정하기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>먼지가 쌓여가는 밀린 책들 조금씩 읽기</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>학업 잘 마무리하기</strong></li>
</ul>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="retrospect" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">다중 수신 객체(Multiple Receivers) 에 대하여</title><link href="https://devwithpug.github.io/kotlin/kotlin-multiple-receivers/" rel="alternate" type="text/html" title="다중 수신 객체(Multiple Receivers) 에 대하여" /><published>2022-03-13T00:00:00+09:00</published><updated>2022-03-13T00:00:00+09:00</updated><id>https://devwithpug.github.io/kotlin/kotlin-multiple-receivers</id><content type="html" xml:base="https://devwithpug.github.io/kotlin/kotlin-multiple-receivers/"><![CDATA[<blockquote>
  <p><strong>모든 코드는 실제 작성하였으며, 직접 실행한 결과들을 글에 담았습니다.</strong> <br />
<strong>코틀린 버전에 따라 변경사항이 생길 수 있습니다. (사용한 Kotlin 버전 1.6)</strong> <br />
<strong>모든 의견은 언제나 환영합니다 😊</strong></p>
</blockquote>

<h1 id="개요">개요</h1>

<p>최근 코틀린을 공부하고 있다. 이전에도 가볍게 다뤄본 적은 있었지만 제대로 공부할 기회가 생겨서 재밌게 공부하고 있다.(새로운 언어를 배울 때가 가장 재밌다 ㅎㅎ)</p>

<p>코틀린 표준 라이브러리의 <code class="language-plaintext highlighter-rouge">with</code>, <code class="language-plaintext highlighter-rouge">apply</code> 에 대해 공부하던 중, 간단한 이슈를 겪게 되어서 이를 정리해 보았다.</p>

<h1 id="수신-객체">수신 객체</h1>

<p>자바의 람다에는 없는 코틀린 람다의 독특한 기능이 있다. 그 기능은 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출할 수 있게 하는 것이다. 그런 람다를 <strong>수신 객체 지정 람다(lambda with receiver)</strong> 라고 부른다.</p>

<p><code class="language-plaintext highlighter-rouge">with</code>, <code class="language-plaintext highlighter-rouge">apply</code> 그리고 수신 객체 지정 람다에 대한 설명은 생략하고 간단히 정리해 보았다.</p>

<h2 id="filter-확장-메서드">filter 확장 메서드</h2>

<ul>
  <li>코틀린의 filter() 메서드를 구현한 <code class="language-plaintext highlighter-rouge">String.myFilter</code> 확장 메서드</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span> <span class="c1">// 필터링할 조건을 파라미터로 받는다</span>
    <span class="kd">val</span> <span class="py">sb</span> <span class="p">=</span> <span class="nc">StringBuilder</span><span class="p">()</span> <span class="c1">// 결과를 리턴할 StringBuilder 객체 생성</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">indices</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 문자열의 문자들을 순환한다</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span> <span class="c1">// 문자가 필터링 술어 연산을 통과한 경우에만 결과에 포함한다</span>
            <span class="n">sb</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">sb</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span> <span class="c1">// 필터링 결과 리턴</span>
<span class="p">}</span>

<span class="s">"FooAndBar"</span><span class="p">.</span><span class="nf">myFilter</span> <span class="p">{</span> <span class="n">it</span> <span class="k">in</span> <span class="sc">'A'</span><span class="o">..</span><span class="sc">'Z'</span> <span class="p">}</span> <span class="c1">// A ~ Z 까지의 문자만 필터링</span>
<span class="c1">// 실행 결과 : FAB</span>
</code></pre></div></div>

<p>매우 간단한 코드이며 단순히 필터링 한 문자열만 리턴하는 메서드이다. <br />
하지만, 수신 객체에 대해 알지 못한 채 코드를 유심히 보면 궁금증이 생길 수 있다.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">):</span> <span class="nc">String</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">sb</span> <span class="p">=</span> <span class="nc">StringBuilder</span><span class="p">()</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">indices</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// this.indices 에서 this 생략</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span> <span class="c1">// this.get(index) 에서 this 생략</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">sb</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">sb</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>바로 <code class="language-plaintext highlighter-rouge">indices</code> 와 <code class="language-plaintext highlighter-rouge">get()</code> 에 대한 참조(String 객체)가 없는데도 코드가 정상적으로 동작한다는 것이다.<br />
코틀린에서는 이런 것을 수신 객체(Receiver) 라고 정의하고 있다.</p>

<blockquote>
  <p><strong>[참고] 수신 객체 지정 람다와 확장 메서드 비교</strong></p>

  <p>위 코드에서는 확장 메서드를 통해 수신 객체에 접근하는 것을 예시로 들었다.<br />
확장 메서드는 with, apply 를 사용하지 않으며, 람다도 아니기 때문에 ‘수신 객체 지정 람다와 다른 건가?’ 하는 궁금증이 생길 수 있다.</p>

  <p>어떤 의미에서는 확장 메서드를 수신 객체 지정 람다라고 할 수도 있다. 두 가지 모두 동일하게 수신 객체가 내부 블럭으로 넘어오고, 동일하게 동작하기 때문이다.</p>

  <p>정확하게 구분을 짓는다면, 다음과 같은 관계로 표현할 수 있다.</p>

  <p><strong>(1) 일반 메서드 &lt;-&gt; 일반 람다</strong><br />
<strong>(2) 확장 메서드 &lt;-&gt; 수신 객체 지정 람다</strong></p>

  <p>일반 람다는 일반 메서드와 비슷한 동작을 정의하는 한 방법이다.<br />
수신 객체 지정 람다는 확장 메서드와 비슷한 동작을 정의하는 한 방법이다.</p>
</blockquote>

<h2 id="filter-확장-메서드-개선">filter 확장 메서드 개선</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">buildString</code> 을 사용해서 <code class="language-plaintext highlighter-rouge">String.myFilter()</code> 메서드를 개선</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">=</span> <span class="nf">buildString</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
            <span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kotlin.text.StringBuilder.buildString()</code>
<img src="https://user-images.githubusercontent.com/69145799/158021167-04f29da4-1421-4f79-9564-72cbe1aef723.png" alt="image" class="align-center" /></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">buildString()</code> 메서드는 코틀린 표준 라이브러리에서 제공하는 <code class="language-plaintext highlighter-rouge">StringBuilder</code> 클래스 내부 메서드이다. <code class="language-plaintext highlighter-rouge">apply</code> 를 감싸고 있어 간단히 <code class="language-plaintext highlighter-rouge">apply</code> 를 적용하고 원하는 문자열을 리턴할 수 있다.</p>

<ul>
  <li>개선한 <code class="language-plaintext highlighter-rouge">String.myFilter()</code> 실행 결과</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">result</span> <span class="p">=</span> <span class="s">"FooAndBar"</span><span class="p">.</span><span class="nf">myFilterTest</span> <span class="p">{</span> <span class="n">it</span> <span class="k">in</span> <span class="sc">'A'</span><span class="o">..</span><span class="sc">'Z'</span> <span class="p">}</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"result: $result"</span><span class="p">)</span>
<span class="c1">// 실행 결과</span>
<span class="c1">// result:</span>
</code></pre></div></div>

<p>하지만 예상과 다르게 <strong>문자열이 전혀 출력되지 않았다!</strong></p>

<p><img src="https://user-images.githubusercontent.com/69145799/156915925-aa41702c-f242-4aeb-8ef7-8ba06e87bbe4.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ 파라미터로 넘어온 수신 객체를 사용하지 않고 있다는 IDEA 경고</p>
</blockquote>

<h2 id="문제-디버깅-하기">문제 디버깅 하기</h2>

<p>문제의 원인을 파악하기 위해 디버깅을 했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">"FooAndBar"</code> 문자열을 수신 객체로 받았을 텐데 <code class="language-plaintext highlighter-rouge">indices</code>의 범위가 0..-1 이다.</li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/158021598-7c11c737-5d02-4422-a0a7-fa84183a0afd.png" alt="image" class="align-center" /></p>

<ul>
  <li>수신 객체 <code class="language-plaintext highlighter-rouge">this</code> 를 확인해보니 StringBuilder의 객체로 나오고 있다.</li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/158021639-f01549b0-de62-4eff-9911-a3ce31e48399.png" alt="image" class="align-center" /></p>

<h1 id="다중-수신-객체">다중 수신 객체</h1>

<p>문제의 원인은 String의 확장 메서드인 <code class="language-plaintext highlighter-rouge">myFilter()</code> 의 내부 블록에서 String 뿐만이 아닌, 다중 수신 객체를 받고 있었기 때문이었다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">this</code> 에 대한 값이 두 가지로 나온다.(this, this@myFilter)</li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/158021801-8a97293c-fd47-477b-a2f5-8361cb60d1d0.png" alt="image" class="align-center" /></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">this@myFilter.indices</code> 는 범위가 제대로 나온다 (“FooAndBar”)</li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/158021933-1d3aacd3-216e-4981-877b-262292df9048.png" alt="image" class="align-center" /></p>

<h2 id="레이블을-명시하여-해결">레이블을 명시하여 해결</h2>

<p>위와 같이 블록 내부에서 다중 수신 객체가 넘어오는 경우 <code class="language-plaintext highlighter-rouge">this</code> 에 대한 객체가 우리가 예상한 값이 아닐 수 있기 때문에 조심해야 한다.</p>

<p>다양한 방법으로 문제를 해결할 수 있지만 모두 같은 개념을 이용한 것이라 간단히만 정리했다.</p>

<ul>
  <li>(1) String 수신 객체를 레이블로 직접 명시하기</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">=</span> <span class="nf">buildString</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="k">this</span><span class="nd">@myFilter</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">this</span><span class="nd">@myFilter</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
            <span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>레이블을 직접 명시해서 문제를 해결했다. 하지만 다른 사람이 이 메서드 코드를 처음 볼 때 혼동이 있을 것 같다.<br />
코드를 통해 다중 수신 객체가 넘어오는 것을 알 수는 있지만 수신 객체들에 대한 정보가 모두 코드에 표현되어 있지 않기 때문이다.</p>

<ul>
  <li>(2) <code class="language-plaintext highlighter-rouge">with</code> 을 통해 수신 객체를 한번 더 감싸기</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">=</span> <span class="nf">buildString</span> <span class="p">{</span>
    <span class="nf">with</span><span class="p">(</span><span class="k">this</span><span class="nd">@myFilter</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
            <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
                <span class="k">this</span><span class="nd">@buildString</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">this@myFilter</code> 를 생략하고 싶은 경우 위와 같이 <code class="language-plaintext highlighter-rouge">this@myFilter</code> 수신 객체를 <code class="language-plaintext highlighter-rouge">with</code> 으로 감싸서 사용하는 방법도 있다. 하지만 반대로 <code class="language-plaintext highlighter-rouge">buildString</code> 의 수신 객체를 사용할 때 레이블을 명시해야 하므로 좋은 구조가 아니고 이전 코드보다 복잡해졌다.</p>

<ul>
  <li>(3) <code class="language-plaintext highlighter-rouge">buildString</code> 대신 <code class="language-plaintext highlighter-rouge">apply</code> 를 직접 사용하기</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">=</span> <span class="nf">apply</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">sb</span> <span class="p">=</span> <span class="nc">StringBuilder</span><span class="p">()</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="k">get</span><span class="p">(</span><span class="n">index</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">sb</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">sb</span><span class="p">.</span><span class="nf">toString</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">apply</code> 를 사용하면 String의 수신 객체만 넘어오기 때문에 레이블을 사용하지 않아도 되지만.. <code class="language-plaintext highlighter-rouge">StringBuilder</code> 객체를 생성하고 연산이 끝난 후 문자열을 리턴해야 하기 때문에 <code class="language-plaintext highlighter-rouge">apply</code> 를 제대로 사용한다고 할 수 없으며, 코드가 그렇게 깔끔하지도 않다.</p>

<ul>
  <li>(4) 내부 로직을 확장 메서드로 추출하기</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nc">String</span><span class="p">.</span><span class="nf">myFilter</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">=</span> 
    <span class="nf">buildString</span> <span class="p">{</span> <span class="nf">filterString</span><span class="p">(</span><span class="n">predicate</span><span class="p">,</span> <span class="k">this</span><span class="nd">@myFilter</span><span class="p">)</span> <span class="p">}</span>

<span class="k">private</span> <span class="k">fun</span> <span class="nc">StringBuilder</span><span class="p">.</span><span class="nf">filterString</span><span class="p">(</span><span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="nc">Char</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nc">Boolean</span><span class="p">,</span> <span class="n">string</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">index</span> <span class="k">in</span> <span class="n">string</span><span class="p">.</span><span class="n">indices</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">element</span> <span class="p">=</span> <span class="n">string</span><span class="p">[</span><span class="n">index</span><span class="p">]</span>
        <span class="k">if</span> <span class="p">(</span><span class="nf">predicate</span><span class="p">(</span><span class="n">element</span><span class="p">))</span> <span class="p">{</span>
            <span class="nf">append</span><span class="p">(</span><span class="n">element</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>내부 필터링 로직을 <code class="language-plaintext highlighter-rouge">StringBuilder</code> 의 확장 메서드로 추출했다. 레이블을 최소한으로 사용할 수 있고, 코드 가독성 면에서도 좋은 코드라고 생각된다. 하지만 이러한 메서드는 종속성을 가지게 되고 다른 곳에서 재사용될 일이 없을 것 같아 조금 아쉽다.</p>

<blockquote>
  <p>개인적으로는 (1), (4) 예제가 가장 좋은 방법인 것 같다. <br />
<code class="language-plaintext highlighter-rouge">buildString</code> &amp; <code class="language-plaintext highlighter-rouge">apply</code> 의 장점을 그대로 가져가면서 메서드 내부 로직을 간결하게 표현할 수 있기 때문이다.</p>
</blockquote>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://www.youtube.com/watch?v=0FF19HJDqMo&amp;t=795s" target="_blank">A Look Into the Future by Roman Elizarov - Multiple receivers</a></li>
  <li><a href="https://kotlinlang.org/docs/scope-functions.html#with" target="_blank">Scope functions(with, apply)</a></li>
  <li><a href="https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver" target="_blank">Function literals with receiver</a></li>
</ul>]]></content><author><name>최준규</name></author><category term="kotlin" /><category term="kotlin" /><summary type="html"><![CDATA[모든 코드는 실제 작성하였으며, 직접 실행한 결과들을 글에 담았습니다. 코틀린 버전에 따라 변경사항이 생길 수 있습니다. (사용한 Kotlin 버전 1.6) 모든 의견은 언제나 환영합니다 😊]]></summary></entry><entry><title type="html">Karabiner-Elements로 키보드 입력 딜레이 추가하기</title><link href="https://devwithpug.github.io/blog/add-delay-to-keys-with-karabiner/" rel="alternate" type="text/html" title="Karabiner-Elements로 키보드 입력 딜레이 추가하기" /><published>2022-01-28T00:00:00+09:00</published><updated>2022-01-28T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/add-delay-to-keys-with-karabiner</id><content type="html" xml:base="https://devwithpug.github.io/blog/add-delay-to-keys-with-karabiner/"><![CDATA[<p><img src="/assets/teasers/karabiner-elements.png" alt="image" class="align-center" /></p>

<h1 id="개요">개요</h1>

<p>나는 요즘 애플의 매직 키보드를 사용하고 있다. <br />
예전부터 꼭 한번 써보고 싶었지만, 가성비를 따졌을 때는 사치라고 생각해서 차마 구매할 용기를 내지 못했었다ㅠ
그런데! 최근에 애플 매직 키보드를 받게 되어서 매직 키보드의 매력에 빠지고 있다. ㅎㅎ</p>

<p>아무튼 기대한 것보다 더 만족하며 사용 중이고, 혹시나 나와 비슷하게 구매를 고민했던 사람이 있다면 추천하고 싶다(키감이 맥북과 매우 비슷하다)</p>

<p>하지만 한 가지 마음에 안 드는 점이 있는데, 그것은 바로 잠금(lock) 키다.</p>

<p><img src="https://user-images.githubusercontent.com/69145799/151392119-c7219b60-0021-4a0e-b5de-9e8000fbe0ff.jpeg" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ Apple Magic Keyboard</p>
</blockquote>

<p>매직 키보드의 키 배열을 보면 백스페이스(delete) 키 상단에 잠금(lock) 키가 있다. 간단히 PC를 잠글 수 있어서 매우 간편하지만, 몰입해서 키보드를 두드리다 보면 나도 모르게 백스페이스 대신 잠금 키를 누르게 돼서 의도치 않게 PC를 잠근 적이 많았다..! (아직 완벽하게 적응을 못해서 그런가?)</p>

<p>한 번쯤이야 실수로 잠금이 걸려도 다시 로그인을 했었지만.. 하루에 3~4번 정도 반복해서 실수로 PC를 잠그게 되니 편의를 위해 만든 키가 나에게는 불편함으로 다가왔다.</p>

<p>그래서 문제를 해결할 방법을 찾아보았다. 하지만 MacOS에서 간단하게 잠금 키를 비활성화하는 기능은 제공하지 않았고, 기존에 사용하고 있던 <code class="language-plaintext highlighter-rouge">Karabiner-Elements</code> 를 통해 문제를 해결할 수 있었다! 글을 통해 관련 내용들을 정리해 보았다.</p>

<h1 id="karabiner-elements-설정">Karabiner-Elements 설정</h1>

<p>가장 먼저 <code class="language-plaintext highlighter-rouge">Karabiner-Elements</code> 를 실행해서 설정화면으로 들어간다.</p>

<h2 id="1-잠금-키를-비활성화">1. 잠금 키를 비활성화</h2>

<p>가장 간단한 방법은 잠금 키를 사용하지 않는 키로 바꿔버리는 것이다. 아래와 같이 간단하게 설정할 수 있다.</p>

<p><img src="https://user-images.githubusercontent.com/69145799/151488983-aeecf6e1-6463-4ed2-b1b8-3fb1de5cfce4.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ <code class="language-plaintext highlighter-rouge">Simple modifications</code> 탭의 <code class="language-plaintext highlighter-rouge">Add item</code> 을 클릭</p>
</blockquote>

<p><img src="https://user-images.githubusercontent.com/69145799/151488635-fd365a6e-ba41-4575-91aa-f182274b8347.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ 새롭게 추가된 설정에서 <code class="language-plaintext highlighter-rouge">From key</code> 값에 <code class="language-plaintext highlighter-rouge">Lock key on Magic Keyboard without Touch ID</code> 를 선택</p>
</blockquote>

<p><img src="https://user-images.githubusercontent.com/69145799/151488867-da478c78-2680-4bd8-8c5e-8e57c502ce05.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ <code class="language-plaintext highlighter-rouge">To key</code> 값에 사용하지 않는 키 (f13 ~ f20) 선택</p>
</blockquote>

<p>이렇게만 설정해 주면 간단하게 잠금 키를 비활성화할 수 있다.</p>

<h2 id="2-잠금-키에-딜레이를-추가">2. 잠금 키에 딜레이를 추가</h2>

<p>잠금 키를 실수로 누를 때는 잠그지 않고, 실제 잠그고 싶을 때만 잠글 수 있도록 하는 방법이다.
잠금 키에 딜레이를 추가해서 키를 길게 누른 경우에만 잠금 키가 작동하도록 설정할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">Karabiner-Elements Preferences</code> 에서 간편하게 설정할 수는 없고, <code class="language-plaintext highlighter-rouge">Karabiner-Elements</code> 설정 파일을 직접 수정해 주어야 한다.</p>

<p><img src="https://user-images.githubusercontent.com/69145799/151489868-34c343dc-cd37-48dd-9b78-134b11ca41ce.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ <code class="language-plaintext highlighter-rouge">Misc</code> 탭의 <code class="language-plaintext highlighter-rouge">Open config folder</code> 를 클릭하면 설정 파일의 경로로 이동할 수 있다.</p>
</blockquote>

<p>그 다음, <code class="language-plaintext highlighter-rouge">karabiner.json</code> 파일을 텍스트 편집기로 아래와 같이 수정하자.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"global"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"check_for_updates_on_startup"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"show_in_menu_bar"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
        </span><span class="nl">"show_profile_name_in_menu_bar"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"profiles"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"complex_modifications"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="err">..생략..</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="nl">"rules"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                    </span><span class="p">{</span><span class="w">
                        </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"잠금 키에 1000ms 딜레이 추가"</span><span class="p">,</span><span class="w">
                        </span><span class="nl">"manipulators"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                            </span><span class="p">{</span><span class="w">
                                </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                    </span><span class="nl">"consumer_key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"al_terminal_lock_or_screensaver"</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">잠금</span><span class="w"> </span><span class="err">키</span><span class="w">
                                </span><span class="p">},</span><span class="w">
                                </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                    </span><span class="nl">"basic.to_if_held_down_threshold_milliseconds"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="mi">1000</span><span class="err">ms</span><span class="w"> </span><span class="err">딜레이</span><span class="w">
                                </span><span class="p">},</span><span class="w">
                                </span><span class="nl">"to_if_held_down"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">위</span><span class="w"> </span><span class="err">설정</span><span class="w"> </span><span class="err">값만큼</span><span class="w"> </span><span class="err">키를</span><span class="w"> </span><span class="err">누르고</span><span class="w"> </span><span class="err">있으면</span><span class="w">
                                    </span><span class="p">{</span><span class="w">
                                        </span><span class="nl">"consumer_key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"al_terminal_lock_or_screensaver"</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">잠금</span><span class="w"> </span><span class="err">키</span><span class="w"> </span><span class="err">입력</span><span class="w">
                                    </span><span class="p">}</span><span class="w">
                                </span><span class="p">],</span><span class="w">
                                </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="w">
                            </span><span class="p">}</span><span class="w">
                        </span><span class="p">]</span><span class="w">
                    </span><span class="p">}</span><span class="w">
                </span><span class="p">]</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="err">..생략..</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><img src="https://user-images.githubusercontent.com/69145799/151490268-aba17e07-d48c-4239-b97b-282f36761bfe.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆️ <code class="language-plaintext highlighter-rouge">Complex modifications</code> 탭을 보면 설정값이 적용된 것을 확인할 수 있다.</p>
</blockquote>

<p>이렇게 설정해 주면 잠금 키를 1초간 누른 경우에만 PC가 잠기게 된다.</p>

<h2 id="3-키-조합-추가하기">3. 키 조합 추가하기</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"control + lock 입력 시 구글 크롬 실행"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"manipulators"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"from"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"key_code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"f2"</span><span class="p">,</span><span class="w">
                </span><span class="nl">"modifiers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"mandatory"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"control"</span><span class="p">]</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"to"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"shell_command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"open -a 'Google Chrome.app'"</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"basic"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>위와 같이 <code class="language-plaintext highlighter-rouge">from</code> 부분에 <code class="language-plaintext highlighter-rouge">modifiers</code>, <code class="language-plaintext highlighter-rouge">mandatory</code> 값을 추가해서 키 조합을 추가할 수 있다.
<code class="language-plaintext highlighter-rouge">mandatory</code>에 설정된 키가 반드시 눌러져야만 작동하게 된다.</p>

<p>하지만, mandatory에 모든 키 값을 설정할 수는 없고, 아래와 같은 키 들을 지원한다.<a href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/from/modifiers/" target="_blank">(출처)</a></p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Name</th>
      <th style="text-align: left">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">caps_lock</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">left_command</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">left_control</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">left_option</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">left_shift</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">right_command</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">right_control</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">right_option</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">right_shift</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">fn</td>
      <td style="text-align: left">—</td>
    </tr>
    <tr>
      <td style="text-align: left">command</td>
      <td style="text-align: left">Either left command or right command is pressed</td>
    </tr>
    <tr>
      <td style="text-align: left">control</td>
      <td style="text-align: left">Either left control or right control is pressed</td>
    </tr>
    <tr>
      <td style="text-align: left">option</td>
      <td style="text-align: left">Either left option or right option is pressed</td>
    </tr>
    <tr>
      <td style="text-align: left">shift</td>
      <td style="text-align: left">Either left shift or right shift is pressed</td>
    </tr>
    <tr>
      <td style="text-align: left">left_alt</td>
      <td style="text-align: left">Alias of left_option (available since Karabiner-Elements 12.3.0)</td>
    </tr>
    <tr>
      <td style="text-align: left">left_gui</td>
      <td style="text-align: left">Alias of left_command (available since Karabiner-Elements 12.3.0)</td>
    </tr>
    <tr>
      <td style="text-align: left">right_alt</td>
      <td style="text-align: left">Alias of right_option (available since Karabiner-Elements 12.3.0)</td>
    </tr>
    <tr>
      <td style="text-align: left">right_gui</td>
      <td style="text-align: left">Alias of right_command (available since Karabiner-Elements 12.3.0)</td>
    </tr>
    <tr>
      <td style="text-align: left">any</td>
      <td style="text-align: left">Any modifiers</td>
    </tr>
  </tbody>
</table>

<p>또한 위의 방법과 같이 <code class="language-plaintext highlighter-rouge">to</code> 옵션에 shell command가 실행되도록 설정해 줄 수 있는데, 이를 통해 원하는 작업을 무궁무진하게 커스터마이징 할 수 있다! 👍</p>

<p>이외에도 <code class="language-plaintext highlighter-rouge">Karabiner-Elements</code> 에서 지원하는 이벤트는 종류가 생각보다 많다.<a href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/to/" target="_blank">(참고)</a></p>

<blockquote>
  <p><strong>모든 코드는 실제 작성하였으며, 직접 실행한 결과들을 글에 담았습니다.</strong> <br />
<strong>유용하게 사용하고 계시는 설정 값이 있으면 코멘트로 공유해주세요!</strong> <br />
<strong>모든 의견은 언제나 환영합니다 😊</strong></p>
</blockquote>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/" target="_blank">Karabiner-Elements Docs: complex-modifications-manipulator-definition</a></li>
  <li><a href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/to-if-held-down/" target="_blank">Karabiner-Elements Docs: to-if-held-down</a></li>
  <li><a href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/from/modifiers/" target="_blank">Karabiner-Elements Docs: from/modifiers</a></li>
</ul>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="util" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">자바에서 동시성 문제를 해결하는 3가지 키워드</title><link href="https://devwithpug.github.io/java/java-thread-safe/" rel="alternate" type="text/html" title="자바에서 동시성 문제를 해결하는 3가지 키워드" /><published>2022-01-17T00:00:00+09:00</published><updated>2022-01-17T00:00:00+09:00</updated><id>https://devwithpug.github.io/java/java-thread-safe</id><content type="html" xml:base="https://devwithpug.github.io/java/java-thread-safe/"><![CDATA[<h1 id="개요">개요</h1>

<p>자바로 코드를 작성하다 보면 동시성 문제에 대해 한 번쯤은 생각을 해보게 된다. <br />
하지만 간단하게 해결할 수 있는 문제가 아니다.. (어렵다😂)</p>

<p>내가 생각하는 스레드와 동시성 관리가 어렵다고 느껴지는 이유는 다음과 같다.</p>

<ol>
  <li><strong>코드만 보고 동시성 문제의 발생 가능성을 파악하기 쉽지 않다.</strong></li>
  <li>일반적으로 동시성으로 인해 생기는 예외는 <strong>재현하기 어렵다.</strong></li>
  <li>최악의 경우, <strong>동시성 문제가 발생해도 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시될 수 있다.</strong></li>
</ol>

<p>코드를 작성할 때 대부분 스레드를 크게 신경쓰지는 않지만, 동시성 문제로 인한 결함이 치명적인 결과로 이어질 수 있기 때문에 동시성에 대해 자세히 아는 것이 중요하다. 따라서 관련 키워드를 정리해보았다.</p>

<h2 id="간단한-동시성-문제-테스트">간단한 동시성 문제 테스트</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">10</span><span class="o">;</span>

    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="n">count</span><span class="o">++;</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">count</span><span class="o">);</span>
        <span class="o">}).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">100</span><span class="o">);</span> <span class="c1">// 모든 스레드가 종료될 때까지 잠깐 대기</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count</span><span class="o">).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>간단한 코드로 테스트를 해보았다. 다수의 스레드들은 공유자원 <code class="language-plaintext highlighter-rouge">static long count</code> 을 참조하여 값을 증가시킨다.
위 테스트 케이스는 대부분의 경우 아무런 문제 없이 통과한다.
하지만 값을 증가하는 <code class="language-plaintext highlighter-rouge">count++</code> 연산이 <strong>원자성을 보장하지 않기 때문에</strong> 아래와 같이 테스트에 실패하는 경우가 생길 수 있다.</p>

<p><img src="https://user-images.githubusercontent.com/69145799/149611483-5dd5455c-376f-4b14-97a8-0ce6593083ce.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆ 테스트 실패 결과 // 1이 두 번 출력 되었다. (0 -&gt; 1 증가 연산에서 동시성 문제가 발생했다)</p>
</blockquote>

<h1 id="동시성-문제-해결-방법">동시성 문제 해결 방법</h1>

<h2 id="1-synchronized">1. synchronized</h2>

<p><code class="language-plaintext highlighter-rouge">synchronized</code> 키워드를 통해 해당 블럭의 액세스를 동기화할 수 있다. <br />
간단히 말해서 <code class="language-plaintext highlighter-rouge">synchronized</code> 가 선언된 블럭에는 동시에 하나의 스레드만 접근할 수 있다.</p>

<ul>
  <li>간단한 사용 예제</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">SomeClass</span> <span class="o">{</span>
    <span class="c1">// 메서드 전체에 동기화 적용</span>
    <span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">foo</span><span class="o">()</span> <span class="o">{</span> 
        <span class="cm">/* critical section */</span>
    <span class="o">}</span>

    <span class="c1">// 내부에 동기화 블럭 생성</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">bar</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">synchronized</span> <span class="o">(</span><span class="k">this</span><span class="o">)</span> <span class="o">{</span>
            <span class="cm">/* critical section */</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// 클래스 내부의 전역 메서드에서 동기화 블럭을 생성하는 방법</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SomeClass</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">syncMethod</span><span class="o">()</span> <span class="o">{</span>
        <span class="kd">synchronized</span> <span class="o">(</span><span class="nc">SomeClass</span><span class="o">.</span><span class="na">class</span><span class="o">)</span> <span class="o">{</span>
            <span class="cm">/* critical section */</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>다음은 앞에서 동시성 문제로 인해 실패한 테스트 케이스에 <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드를 사용해 보았다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">10</span><span class="o">;</span>

    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">plus</span><span class="o">).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">100</span><span class="o">);</span> <span class="c1">// 모든 스레드가 종료될 때까지 잠깐 대기</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count</span><span class="o">).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">plus</span><span class="o">()</span> <span class="o">{</span> <span class="c1">// synchronized 키워드 사용</span>
    <span class="n">count</span><span class="o">++;</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">synchronized</code> 를 추가함으로써 위 테스트는 100% 통과할 수 있다.</p>

<p>특정 스레드는 <code class="language-plaintext highlighter-rouge">synchronized</code> 메서드에 접근 시 블록 전체에 lock을 건다. 
따라서 해당 스레드가 블럭을 빠져나가기 전까지 다른 스레드들은 동기화 처리된 블록에 접근할 수 없다.
하지만, 다른 스레드들은 아무런 작업을 하지 못하고 기다릴 수밖에 없어 자원의 낭비가 발생할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">synchronized</code> 로 선언된 블록 내부에 복잡한 로직이 들어간다고 생각해 보자. 스레드의 개수가 많으면 많을수록 실행 시간에 엄청난 지연이 생길 것이다. 이를 확인해 보기 위해 간단한 테스트를 해보았다.</p>

<h3 id="딜레이-테스트-1">딜레이 테스트 (1)</h3>

<p>가장 먼저 <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드를 사용하지 않은 경우를 테스트해보았다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>

    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">plus</span><span class="o">).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1000</span><span class="o">);</span> <span class="c1">// 1000ms 후에 테스트 종료(결과 값을 확인)</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count</span><span class="o">).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">plus</span><span class="o">()</span> <span class="o">{</span> <span class="c1">// synchronized 를 사용하지 않은 경우 (동시성 제어를 하지 않는 경우)</span>
    <span class="n">count</span><span class="o">++;</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span> <span class="c1">// 추가적인 딜레이(복잡한 로직이 추가된다고 가정)</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">plus()</code> 함수 내부에 추가적인 딜레이를 발생시켜 보았다. 테스트는 대부분의 경우 통과하지만 <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드를 사용하지 않는 이상, 처음 테스트와 동일하게 동시성 문제가 발생할 수 있다.
하지만 <code class="language-plaintext highlighter-rouge">Thread.sleep(1)</code> 딜레이는 큰 영향을 주지 않는다. (각각의 스레드는 독립적으로 실행되며 critical section에 아무런 제약 없이 접근하기 때문)</p>

<h3 id="딜레이-테스트-2">딜레이 테스트 (2)</h3>

<p>그렇다면, <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드를 추가하면 어떨까?</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">plus</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> <span class="c1">// synchronized 사용 (해당 블록에 동시에 하나의 스레드만 접근 가능)</span>
</code></pre></div></div>

<p><img src="https://user-images.githubusercontent.com/69145799/149612719-0884a4c3-5a61-4fbf-aea3-a925f98c3bf3.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆ synchronized 사용 테스트 결과</p>
</blockquote>

<p>결과가 상당히 흥미로운데, 목표 값인 1000에 턱없이 부족한 결과가 나온 이유는 간단하다.</p>

<ol>
  <li>1000개의 스레드를 생성했다.</li>
  <li>count를 증가하는 <code class="language-plaintext highlighter-rouge">plus()</code> 메서드는 동시에 하나의 스레드만 접근 가능하다.</li>
  <li>특정 스레드가 <code class="language-plaintext highlighter-rouge">count++</code> 연산을 끝낸 후 1ms 동안 딜레이를 가지고 나서야 메서드를 빠져나오고 lock을 해제한다.</li>
  <li><strong>테스트는 1000ms 후에 종료되므로 1000개의 모든 스레드가 <code class="language-plaintext highlighter-rouge">plus()</code> 메서드를 실행하기에는 시간이 부족하다.</strong></li>
  <li>비록 시간이 모자라 테스트는 실패했지만, <strong>동시성 제어가 확실히 되고 있다는 점</strong> 을 확인할 수 있었다.</li>
</ol>

<p>따라서, 동시성 문제를 해결하기 위해 <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드는 매우 간단한 해결방법이 될 수 있지만, critical section의 크기및 실행시간에 따라 성능하락 및 자원낭비가 매우 심해지게 된다.</p>

<p><a href="http://www.yes24.com/Product/Goods/11681152" target="_blank">Uncle Bob(로버트 C. 마틴)의 클린 코드</a> 책에서도 동시성에 대해 다룬 부분이 있는데, 내용은 다음과 같다.</p>

<ul>
  <li><strong>공유 자료를 최대한 줄여라.</strong></li>
  <li><strong>동기화하는 부분을 최대한 작게 만들어라.</strong></li>
  <li><strong>프로세서 수보다 많은 스레드를 돌려보라.</strong></li>
  <li><strong>코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라.</strong></li>
</ul>

<h2 id="2-volatile">2. volatile</h2>

<p><code class="language-plaintext highlighter-rouge">volatile</code> 키워드에 대한 <a href="https://nesoy.github.io/articles/2018-06/Java-volatile" target="_blank">자세한 설명은 다른 블로그를 참고하기 바랍니다.</a></p>

<p><code class="language-plaintext highlighter-rouge">volatile</code> 에 대해 간단하게 정리해 보면..</p>

<p>JVM에서 스레드는 실행되고 있는 CPU 메모리 영역에 데이터를 캐싱 한다. (CPU Cache)
따라서 멀티 코어 프로세서에서 다수의 스레드가 변수 a를 공유하더라도 캐싱 된 시점에 따라 데이터가 다를 수 있으며,
서로 다른 코어의 스레드는 데이터 값이 불일치하는 문제가 생긴다.</p>

<p>임의로 데이터를 갱신해 주지 않는 이상 <strong>캐싱 된 데이터가 언제 갱신되는지 또한 정확히 알 수 없다.</strong></p>

<p>이런 경우 <code class="language-plaintext highlighter-rouge">volatile</code> 키워드를 사용하여 CPU 메모리 영역에 캐싱 된 값이 아니라 <strong>항상 최신의 값을 가지도록 메인 메모리 영역에서 값을 참조하도록 할 수 있다.</strong>
-&gt; 즉, 동일 시점에 모든 스레드가 동일한 값을 가지도록 동기화한다.</p>

<p>변수 앞에 키워드를 붙여서 선언이 가능하다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">volatile</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
</code></pre></div></div>

<p>하지만 <code class="language-plaintext highlighter-rouge">volatile</code> 을 통해 모든 동기화 문제가 해결되는 건 아니다.</p>

<p>앞에서 예로 들었던 <code class="language-plaintext highlighter-rouge">++</code> 연산과 같이 원자성이 보장되지 않는 경우 동시성 문제는 동일하게 발생한다.
(단지 멀티 코어에서의 모든 스레드가 캐시 없이 최신의 값을 보게 할 뿐이다!)</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">volatile</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="c1">// volatile 키워드 추가</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>

    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">count</span><span class="o">++).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">100</span><span class="o">);</span> <span class="c1">// 모든 스레드가 종료될때 까지 잠깐 대기</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count</span><span class="o">).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p><img src="https://user-images.githubusercontent.com/69145799/149614756-7055cbfb-b48a-4dd3-a757-9815effc4af5.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆ 인텔리제이는 친절하게도 volatile 필드에 비 원자적 연산이 있다는 경고를 해준다 ㅎㅎ</p>
</blockquote>

<p><img src="https://user-images.githubusercontent.com/69145799/149614599-42e40249-c115-4bb4-ba6b-14066fd364bc.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>⬆ 동시성 문제로 인해 실패한 케이스 (volatile 키워드도 원자성을 보장하지 않는 연산에서는 동일한 동시성 문제가 발생한다.)</p>
</blockquote>

<p>따라서, <code class="language-plaintext highlighter-rouge">volatile</code> 의 특징은 다음과 같다.</p>

<ol>
  <li>mutual exclusion(상호 배제)를 제공하지 않고도 데이터 변경의 가시성을 보장한다.</li>
  <li>원자적 연산에서만 동기화를 보장한다.</li>
</ol>

<h2 id="3-atomic-클래스">3. Atomic 클래스</h2>

<p>앞에서 설명한 두 가지 키워드 <code class="language-plaintext highlighter-rouge">synchronized</code>, <code class="language-plaintext highlighter-rouge">volatile</code> 만으로는 동시성 문제를 깔끔하게 해결할 수 없다.</p>

<p>자바에서는 위 문제들을 해결하기 위해, 비-원자적 연산에서도 동기화를 빠르고 쉽게 이용하기 위한 클래스 모음을 제공한다.</p>

<p><code class="language-plaintext highlighter-rouge">java.util.concurrent.*</code> (대표적으로 컬렉션, Wrapper 클래스 등이 있다.)</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">java.util.concurrent.atomic.AtomicLong</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">AtomicLong</span> <span class="kd">extends</span> <span class="nc">Number</span> <span class="kd">implements</span> <span class="n">java</span><span class="o">.</span><span class="na">io</span><span class="o">.</span><span class="na">Serializable</span> <span class="o">{</span>
	
    <span class="kd">private</span> <span class="kd">volatile</span> <span class="kt">long</span> <span class="n">value</span><span class="o">;</span> <span class="c1">// volatile 키워드가 적용되어 있다.</span>
	
    <span class="kd">public</span> <span class="kd">final</span> <span class="kt">long</span> <span class="nf">incrementAndGet</span><span class="o">()</span> <span class="o">{</span> <span class="c1">// value 값을 실제로 증가시키는 메서드</span>
        <span class="k">return</span> <span class="no">U</span><span class="o">.</span><span class="na">getAndAddLong</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="no">VALUE</span><span class="o">,</span> <span class="mi">1L</span><span class="o">)</span> <span class="o">+</span> <span class="mi">1L</span><span class="o">;</span>
    <span class="o">}</span>
	
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">jdk.internal.misc.Unsafe</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">Unsafe</span> <span class="o">{</span>
    <span class="c1">// 메모리에 저장된 값과 CPU에 캐시된 값을 비교해 동일한 경우에만 update 수행</span>
    <span class="kd">public</span> <span class="kd">final</span> <span class="kt">long</span> <span class="nf">getAndAddLong</span><span class="o">(</span><span class="nc">Object</span> <span class="n">o</span><span class="o">,</span> <span class="kt">long</span> <span class="n">offset</span><span class="o">,</span> <span class="kt">long</span> <span class="n">delta</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">v</span><span class="o">;</span>
        <span class="k">do</span> <span class="o">{</span>
            <span class="n">v</span> <span class="o">=</span> <span class="n">getLongVolatile</span><span class="o">(</span><span class="n">o</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">while</span> <span class="o">(!</span><span class="n">weakCompareAndSetLong</span><span class="o">(</span><span class="n">o</span><span class="o">,</span> <span class="n">offset</span><span class="o">,</span> <span class="n">v</span><span class="o">,</span> <span class="n">v</span> <span class="o">+</span> <span class="n">delta</span><span class="o">));</span> <span class="c1">// CAS 알고리즘 (JNI 코드로 이루어져 있다.)</span>
        <span class="k">return</span> <span class="n">v</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Non-Blocking 임에도 동시성을 보장하는 이유는 <code class="language-plaintext highlighter-rouge">CAS(Compare-and-swap)</code> 알고리즘을 이용하기 때문이다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">volatile</code> 키워드를 이용하면서 현재 스레드에 저장된 값과 메인 메모리에 저장된 값을 비교한다.
    <ul>
      <li>일치하는 경우 새로운 값으로 교체(thread-safe 한 상태이므로 로직 수행)</li>
      <li>일치하지 않는 경우 실패 후 재시도(thread-safe 하지 않은 상태였으므로 재시도)</li>
    </ul>
  </li>
</ul>

<h3 id="성능-비교-테스트">성능 비교 테스트</h3>

<p>Blocking vs Non-Blocking 에 대해 속도 비교를 간단히 해보았다.</p>

<ul>
  <li>Blocking (synchronized)</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">startTime</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">plus</span><span class="o">).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">2000</span><span class="o">);</span> <span class="c1">// 모든 스레드가 종료될때 까지 잠깐 대기</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count</span><span class="o">).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kd">synchronized</span> <span class="kt">void</span> <span class="nf">plus</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(++</span><span class="n">count</span> <span class="o">==</span> <span class="n">maxCnt</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">startTime</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><img src="https://user-images.githubusercontent.com/69145799/149615263-d1c059a2-467d-4ee1-a152-eae70a12356c.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>평균 1300ms 정도 소요되었다. (Blocking 연산에서 1000개의 스레드가 각각 1ms의 추가 딜레이를 가지기 때문)</p>
</blockquote>

<ul>
  <li>Non-Blocking (AtomicLong)</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">long</span> <span class="n">startTime</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">int</span> <span class="n">maxCnt</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">;</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="nc">AtomicLong</span> <span class="n">count2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">AtomicLong</span><span class="o">();</span>

<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">threadNotSafe2</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">maxCnt</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="k">new</span> <span class="nf">Thread</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">plus2</span><span class="o">).</span><span class="na">start</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">2000</span><span class="o">);</span> <span class="c1">// 모든 스레드가 종료될때 까지 잠깐 대기</span>
    <span class="nc">Assertions</span><span class="o">.</span><span class="na">assertThat</span><span class="o">(</span><span class="n">count2</span><span class="o">.</span><span class="na">get</span><span class="o">()).</span><span class="na">isEqualTo</span><span class="o">(</span><span class="n">maxCnt</span><span class="o">);</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">plus2</span><span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">count2</span><span class="o">.</span><span class="na">incrementAndGet</span><span class="o">()</span> <span class="o">==</span> <span class="n">maxCnt</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">startTime</span><span class="o">);</span>
    <span class="o">}</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><img src="https://user-images.githubusercontent.com/69145799/149615309-0dc44641-89f1-40da-a868-21ccca99fb53.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>평균 140ms 정도 소요되었다. (Non-Blocking 연산에서 1ms의 추가 딜레이는 큰 의미가 없다.)</p>
</blockquote>

<p>당연한 결과이지만, <code class="language-plaintext highlighter-rouge">synchronized</code> 키워드는 효과적인 성능과 함께 사용하기에는 큰 어려움이 있다.</p>

<h1 id="마무리">마무리</h1>

<p>테스트에서는 임의로 딜레이를 1ms로 주었기 때문에 결과가 드라마틱 하게 차이 난다고 말할 수도 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">plus</span><span class="o">()</span> <span class="o">{</span>
    <span class="kd">synchronized</span> <span class="o">(</span><span class="k">this</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(++</span><span class="n">count</span> <span class="o">==</span> <span class="n">maxCnt</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">startTime</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
    <span class="k">try</span> <span class="o">{</span>
        <span class="nc">Thread</span><span class="o">.</span><span class="na">sleep</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InterruptedException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>위와 같이 <code class="language-plaintext highlighter-rouge">plus()</code> 메서드에서 동기화가 필요한 부분만 <code class="language-plaintext highlighter-rouge">synchronized</code> 블록으로 감싸주면 성능하락이 생기지 않는다.</p>

<p>하지만, 실제 프로그래밍에서 위 테스트 코드와 같이 동시성 문제가 예상되는 곳들을 모두 파악해서 <code class="language-plaintext highlighter-rouge">synchronzied</code> 를 통해 동기화 설정을 할 수 있을까?</p>

<p>복잡한 비즈니스 로직 사이사이에 들어가 있는 비-원자적 연산을 여러개의 <code class="language-plaintext highlighter-rouge">synchronized</code> 블록으로 설정하는건 가능하겠지만 코드가 복잡해질 것이고, 개발자가 모든 코드에 동시성 문제를 하나하나 검토해야만 완벽하게 적용할 수 있을 것이다.</p>

<p>따라서 기본으로 제공하는 concurrent 패키지의 클래스들을 이용하는 것이 자바에서 동시성 문제를 해결하는 적절한 방법이라고 생각한다.</p>

<blockquote>
  <p><strong>모든 코드는 실제 작성하였으며, 직접 실행한 결과들을 글에 담았습니다.</strong> <br />
<strong>내용에 오류가 있을 수 있습니다. 관련 코멘트를 주시면 감사히 받겠습니다.</strong> <br />
<strong>모든 의견은 언제나 환영합니다 😊</strong></p>
</blockquote>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://n1tjrgns.tistory.com/244" target="_blank">Atomic Type vs Synchronized</a></li>
  <li><a href="https://brunch.co.kr/@kd4/156" target="_blank">자바 고유락과 Synchronization</a></li>
  <li><a href="https://nesoy.github.io/articles/2018-06/Java-volatile" target="_blank">Java volatile이란?</a></li>
  <li><a href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicLong.html" target="_blank">Oracle Docs - AtomicLong</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Compare-and-swap" target="_blank">Compare-and-swap</a></li>
</ul>]]></content><author><name>최준규</name></author><category term="java" /><category term="java" /><category term="synchronization" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">2021 연말 회고</title><link href="https://devwithpug.github.io/blog/goodbye-2021/" rel="alternate" type="text/html" title="2021 연말 회고" /><published>2021-12-31T00:00:00+09:00</published><updated>2021-12-31T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/goodbye-2021</id><content type="html" xml:base="https://devwithpug.github.io/blog/goodbye-2021/"><![CDATA[<p><img src="/assets/teasers/close2021.png" alt="image" class="align-center" /></p>

<h1 id="public-string-close2021-">public String close2021() {</h1>

<p>2021년! 정말 빠르게 지나갔다.</p>

<p>바쁘게 달려오다가 정신을 차려보니, 벌써 한 해를 마무리하게 되었고 이제 곧 나도 반오십이다..😂</p>

<p>처음 작성하는 연말 회고이지만 내가 무엇들을 했는지 마음대로 풀어가보려고 한다.</p>

<p><strong>컴퓨터공학을 전공하는 대학생의 입장에서 쓴 글이므로 편하게 읽어줬으면 좋겠습니다. :)</strong></p>

<h1 id="2020-연말">2020 연말</h1>

<h2 id="첫-개인-프로젝트">첫 개인 프로젝트</h2>

<p>2021년 회고를 하는 글이지만 이 내용을 뺄 순 없었다.</p>

<p><strong>‘내가 언제부터 자기주도적으로 개발을 했었지?’</strong> 라고 생각해 보면 2020년 겨울로 돌아가게 된다. 난 게임을 좋아하는 평범한 복학생이었고 이듬해에 3학년이 된다는 생각에 개인적으로 큰 위기감을 느끼기 시작하고 있었다.</p>

<p>다른 사람들도 같은 기분을 느낀 적이 있을 것 같은데 나는 이렇게만 살면 아무것도 되지 못할 것이라는 생각이 들었다. 표현하기 힘든 무서운 감정. <strong>‘뭐라도 해야 하는데..’</strong> <strong>‘내가 뭘 잘할 수 있을까?’</strong> 와 같은 생각들이 꼬리에 꼬리를 물고 있었다.</p>

<p>객관적인 시선에서 나 자신을 돌아볼 필요가 있었다.</p>

<p><strong>‘게임과 컴퓨터를 좋아하는 흔하디흔한 컴공생’</strong> 나를 한 문장으로 표현해 본다면 그렇다. 하지만 보통 사람들보다 컴퓨터에 관심이 많다고 자신 있게 말할 수 있다. 그리고 코딩하는 것을, 개발하는 것을 좋아한다고 자신 있게 말할 수 있다. 이게 내 강점이고 내가 나아가야 할 길이라고 생각했다.</p>

<p><img src="https://user-images.githubusercontent.com/69145799/147571840-c9f98fff-1761-4b6c-8b39-0c2c446542d7.png" alt="image" class="align-center" /></p>

<p>그래서 난 첫 프로젝트를 혼자 해보기로 마음먹었다.</p>

<p>평소 관심이 있었던 파이썬, 그리고 디스코드 봇 제작에 흥미를 느끼게 되었고, 직접 나만의 봇을 만들어보게 되었다. 끊임없이 발생하는 예상치 못한 런타임 에러들, 비효율적인 로직, 스파게티 코드, 보여주기 부끄러운 프로젝트. 그래도 처음으로 주도적으로 만든 <a href="https://github.com/devwithpug/SABot" target="_blank">첫 번째 프로젝트</a>였다.</p>

<p><strong>엉망인 프로젝트라는 평가를 받을 수도 있겠지만 나는 생각이 조금 다르다.</strong> 스스로 설계한 프로젝트라는 점. 중간에 포기하지 않고 내가 생각한 프로그램을 완성한 점. 깃헙에서 스타를 처음 받아본 리포지토리인 점. 학교 전공 강의가 전부인 줄 알던 우물 안의 개구리에게 실제 프로그래밍이라는 것이 무엇인지 깨닫게 해준 점. 무엇보다도 나를 자기주도적인, 스스로 개발을 하는 사람으로 만들어 준 프로젝트라고 생각한다.</p>

<h1 id="2021-1">2021-1</h1>

<h2 id="기술-블로그-시작">기술 블로그 시작</h2>

<p>첫 번째 프로젝트로 디스코드 봇을 완성한 후, 친구들과 사용하는 디스코드 채널에서 내가 만든 봇을 유용하게 사용하고 있었다. 하지만, 내가 만든 봇을 퍼블릭에 배포하지는 못했고 아쉬움이 너무 컸다.</p>

<p>첫 프로젝트라서 애착이 있기도 했지만 프로젝트를 진행하면서 겪은 문제들이 많았기 때문에 이를 공유하는 것이 좋겠다고 생각했다.</p>

<p>이전부터 직접 기술 블로그를 만들어보고 싶었지만 어떤 글을 써야 할지에 대한 막막함이 있었고, 언젠가는 블로그를 해야겠다는 생각만 하고 있었다. 첫 프로젝트의 아쉬움을 달래기 위해 디스코드 봇에 대한 주제로 블로그를 시작하는 것이 좋을 것 같았다.</p>

<p>기술 블로그 관련 자료들을 찾아보면서 Jekyll 블로그에 대해 알게 되었다. JS 관련 지식이 전혀 없었지만 나만의 블로그를 하나하나 커스터마이징 하는 것이 너무 재미있었다. 특히, 내 블로그를 구성하고 있는 코드 하나하나를 직접 마음대로 수정할 수 있다는 점이 너무 좋았다.</p>

<p>기술 블로그를 시작한 지도 벌써 10개월 정도 되었다. Google 검색 결과 상단에 내 블로그가 있을 때 뿌듯함을 느끼게 된다. ㅎㅎ</p>

<p>게시글 클릭 수가 점점 증가하는 것을 보면서 아무리 적어도 한 달에 한 번은 포스팅을 하겠다고 다짐했다. 나태해지지 않기 위해 글을 쓴다는 생각도 들지만(?) <strong>내가 공부한 것들을 노트에 정리하는 것과 다를 바 없다고 생각한다.</strong></p>

<h2 id="백엔드-개발자">백엔드 개발자</h2>

<p>개강 전, 겨울방학 동안 ‘나는 무슨 개발자가 될 것인가?’에 대한 고민이 많았다. 분야가 매우 다양하기 때문에 내 목표를 구체적으로 설정할 필요를 느꼈던 것 같다. 관련 자료들을 찾아보면서 자연스럽게 취업에 대해 구글링을 끊임없이 했다.</p>

<p>네카라쿠배.. 스타트업.. SI.. 많은 기업들과 그들이 원하는 자격 조건 및 우대사항에 대해 알게 되었고 <strong>취업을 위해 전략적으로 행동해야겠다고 느꼈다.</strong> ‘내가 대기업에 맞는 인재가 될 수 있을까? 스타트업에서는 경험 많고 능력 많은 신입(?)을 요구하는 것 같네. 신입 공채의 경쟁률은 극악인가 보다..’ 와 같은 생각들을 했지만 결국 <strong>경험 없는 주니어 개발자는 신입 공채에 부딪혀 봐야 한다고 생각했고</strong> 경쟁은 어디에나 필요하기 때문에 어떤 방향으로도 지원이 가능한 직군을 선택해야겠다고 생각했다.</p>

<p>프론트엔드, 백엔드, 데브옵스, 인공지능, 블록체인 등의 여러 직군중에서 가장 먼저 내 눈에 띈 것은 <strong>사실 없었다.</strong>(😂😂)</p>

<p>HTML, CSS, JS 웹 개발 강의를 수강하면서 프론트엔드는 나와 맞지 않다고 생각했고 인공지능, 블록체인은 매우 유망한 직군이지만 학사만으로 기업에서 원하는 인재상이 되기에는 어려움이 있다고 생각했다.</p>

<p>이때는 백엔드와 데브옵스가 어떻게 다른지, 데브옵스가 그래서 정확히 뭘 하는 건지(?)도 잘 모르고 있었다. 그런데 신입 공채는 대부분 프론트,백엔드 만을 모집하고 있는 것을 알게 되었고, 결국 백엔드 개발자가 되기로 마음먹었다. ㅎㅎ;;</p>

<h1 id="2021-2">2021-2</h1>

<h2 id="스프링">스프링</h2>

<p>여러 개발 직군들 중에서 소거법(?)을 통해 백엔드 개발자가 되어야겠다고 생각했기 때문에, <strong>처음에는 백엔드 개발자가 나와 잘 맞을지 확신이 없었다.</strong></p>

<p>평소 파이썬을 사용했기 때문에 자연스럽게 파이썬 백엔드에 대해 찾아보았다. 파이썬 백엔드에서 사용하는 장고(Django) 프레임워크는 소수의 스타트업에서 사용하고 있는 것 같았고, 한국의 많은 기업들은 자바 스프링 프레임워크를 사용하고 있다는 것을 알게 되었다.<strong>(전자정부 표준프레임워크 ^^)</strong></p>

<p>파이썬과 자바 모두 하나의 언어일 뿐, 큰 문제가 없을 것이라 생각했기 때문에 자바 진영의 백엔드인 스프링에 대해 공부를 하기로 마음먹었다. 스프링 공부는 김영한 님의 인프런 무료 강의를 통해 시작하게 되었는데 내 진로를 정확하게 결정해 준, 그리고 스프링에 첫 시작을 보다 어렵지 않게 시작할 수 있게 해준 정말 고마운 강의였던 것 같다.</p>

<p>학교 수업과 병행하면서 공부를 하는 것이라 몰입에 어려움이 있었지만 <strong>지금 생각해 보면 최고의 결정이었다.</strong></p>

<p>무엇보다도 누가 정해주지 않은, 내 관심으로 시작한 공부라는 것이 너무 좋았다.</p>

<h2 id="topcit-정기평가">TOPCIT 정기평가</h2>

<p>5월에는 TOPCIT 정기평가 시험을 준비했었다. <a href="https://devwithpug.github.io/blog/topcit/" target="_blank">이전에 작성한 글 바로 가기</a></p>

<p>몇몇 대학에서 졸업 요건으로 도입하는 추세라고 해서 관심을 가지게 되었고 내가 지금까지 CS 공부를 제대로 해왔는지를 확인해 보고 싶어 신청하게 되었다.</p>

<p>선착순으로 TOPCIT 에센스 책을 받았는데, 이때 대학 중간고사와 TOPCIT 평가 일정이 겹치게 되어 공부를 거의 못했던 기억이 난다.(😂😂) 하루 이틀 벼락치기밖에 할 수 없어 매우 아쉬웠지만, 그래도 목표했던 등급을 받게 되어서 만족했다.</p>

<p>특히, 이전에 개인 프로젝트를 하면서 파이썬의 라이브러리인 pymongo를 사용했던 적이 있었는데 시험 문제에 그대로 나와서 놀랐다! <strong>‘경험이 정말 무엇보다 중요하구나..’</strong> 라는 것을 느낄 수 있었다.</p>

<h2 id="첫-팀-프로젝트">첫 팀 프로젝트</h2>

<p>TOPCIT 정기평가를 잘 마무리하고 강의를 듣던 중, 레스토랑 예약 웹서비스를 만드는 팀 프로젝트를 진행하게 되었다.</p>

<p>개인적으로 공부하던 스프링을 직접 프로젝트에 적용해 볼 수 있는 좋은 기회라고 생각했다. 팀원들 모두 웹 프로그래밍에 대한 경험이 많지 않아서 어쩌다 보니(?) 내가 팀장을 맡게 되었다.</p>

<p>먼저, 스프링 공부를 하면서 만들었던 예제 프로젝트와 다르게 이곳저곳에서 작은 문제들이 생겼었다. 숙련도가 부족하다 보니, 간단한 문제인데도 적절한 해결 방법을 찾지 못해 많은 시간을 낭비했던 적도 있었다.</p>

<p>역시 공부는 배우는 것에서 끝내지 않고 직접 응용을 해봐야 진짜 나의 지식으로 만들 수 있는 것 같다. 시간을 투자한 만큼 많은 공부가 되었다고 생각한다.(👏👏)</p>

<p>팀 프로젝트는 성공적으로 끝냈고 성적도 잘 받았지만 돌이켜보면 아쉬운 점이 몇 가지 있었다.</p>

<ol>
  <li><strong>백엔드를 혼자 맡게 되어 아쉬웠다. 내 코드에 부족한 점들이 많았다.</strong></li>
  <li><strong>팀장으로서 팀원들의 커뮤니케이션을 이끌어내지 못했다.</strong></li>
  <li><strong>내 생각을 온전히 상대방에게 전달하는 것이 정말 어려웠다.</strong></li>
</ol>

<p>아쉬웠던 점들을 통해 앞으로 내가 어떤 부분을 더 노력해야 할지를 결정할 수 있었던 것 같다. <strong>협업에서 커뮤니케이션 능력이 무엇보다 중요하다는 것</strong> 을 뼈저리게 깨닫게 되었고, 지금도 많이 부족한 능력이지만 많이 노력하게 되는 것 같다.</p>

<p>그래도 이번 팀 프로젝트는 성공적으로 마무리했고, 데드라인에 쫓기지 않으면서 프로젝트를 마무리한 것에 만족했다.</p>

<h1 id="2021-3">2021-3</h1>

<h2 id="ausg--saa">AUSG &amp; SAA</h2>

<p><img src="https://user-images.githubusercontent.com/69145799/147572164-95c3732e-6592-49e3-b660-51c0ad7379e3.png" alt="image" class="align-center" /></p>

<p>나는 클라우스 웹 서비스에 대해 잘 알지 못했다. 그래서 관련된 서비스들을 미리 공부하는 것이 백엔드 개발자로써 큰 도움이 될 것이라 생각했다.</p>

<p>GCP(Google Cloud Platform)만 간단하게 사용해 봤었지만 AWS(Amazon Web Service)에 대해서 알게 되었고 AWS 공부를 하기로 결정했지만 어디서부터, 어떻게 공부해야 할지가 막막했다.</p>

<p>기술 블로그에는 튜토리얼 형식의 간단한 내용 또는 이해하기 매우 어려운 글들이 대부분이었다. 처음부터 차근차근 제대로 공부하고 싶었고, 내게 필요한 정보들을 얻을 수 있는 곳이 필요했다.</p>

<p>이때 <a href="https://ausg.me/" target="_blank">AUSG</a>를 알게 되었고, AUSG 커뮤니티에 참여하는 것이 많은 도움이 될 것이라 생각해서 5기 활동을 지원했다.</p>

<p>커뮤니티 분들이 좋게 봐주신 덕분에 <strong>AUSG 5기로 활동할 기회를 얻었다!</strong> 대단하신 분들이 많았고, 빅 챗이 열릴 때마다 좋은 정보들을 얻어 갈 수 있어서 좋았다.</p>

<p>내가 가장 관심 있었던 AWS 서비스에 대해 공부할 수 있는 SAA 자격증 스터디에 참여하게 되었다.</p>

<p>매주 한 번씩 모여서 공부한 내용들을 서로 공유하는 시간을 가졌고 스터디를 통해 여러 정보들을 공유할 수 있어서 너무 좋았다. 무엇보다 <strong>부담되지 않게 참여할 수 있는 스터디</strong> 를 꾸려주셔서 포기하지 않고 끝까지 스터디에 참여할 수 있었다.</p>

<p><img src="/assets/images/saa.png" alt="image" class="align-center" /></p>

<p>SAA 시험을 준비하면서 틈틈이 공부했고, 다행히도 11월에 <strong>SAA 자격증을 취득할 수 있었다!</strong> 🥳</p>

<h2 id="sqld">SQLD</h2>

<p>내가 재학 중인 학교에서는 학사 졸업 요건으로 정보처리기사 또는 SQLD 자격증을 요구했었다. 개인적으로 두 자격증 중에서는 SQLD를 준비하는 것이 좋을 것이라 생각했다.</p>

<p>그 이유는 첫 번째로 추가적인 데이터베이스 공부를 하면서 백엔드 개발자로써 큰 도움이 될 것이라 생각했고, 두 번째로 SQLD는 필기시험 한 번만 통과하면 되기 때문에 비교적 적은 시간을 투자하면 될 것이라 생각했다.</p>

<p>약 한 달가량 많은 사람들이 추천하던 <a href="http://www.yes24.com/Product/Goods/33524934" target="_blank">노랭이 책</a>의 실전 문제와 기출문제들을 풀면서 공부했고, 다행히 한 번에 합격할 수 있었다! 🥳🥳</p>

<p>자격증을 준비하면서 들었던 생각은 <strong>‘데이터베이스 너란 녀석.. 역시 쉽지 않구나..!’</strong> 인 것 같다. ㅎㅎ</p>

<h2 id="공개sw-개발자대회">공개SW 개발자대회</h2>

<p>약 4개월 동안 공개SW 개발자대회에 참여했었다. 대학 동기들과 함께 팀으로 참가했고 은상을 수상했다! 🥳🥳🥳</p>

<p>바로 이전에 작성한 글이 이에 대한 회고의 내용이므로 생략하겠다. <a href="https://devwithpug.github.io/blog/opensw-retrospect/" target="_blank">공개SW 개발자대회 회고 바로 가기</a></p>

<h1 id="2021-4">2021-4</h1>

<h2 id="주니어-개발자">주니어 개발자</h2>

<p><img src="https://user-images.githubusercontent.com/69145799/147574982-31f75295-afd0-48c6-80cc-f8b104f186a3.png" alt="image" class="align-center" /></p>

<p>취준에 대한 글을 따로 쓸 생각은 없어서.. 간단하게 회고해 보았다.</p>

<p>올해에는 많은 일들이 있었지만 지금 쓰는 것이 가장 큰일이 아닐까 싶다. <strong>내 스스로 경쟁력을 가지기 위해 열심히 달려왔고, 알고리즘 및 코딩 테스트에 대한 공부는 절대 포기하지 않았다.</strong> 처음에는 간단한 문제에도 많은 시간을 투자했지만 점점 스스로 해답을 찾아가는 것에 재미를 느꼈고 솔루션을 찾을 때마다 느끼는 뿌듯함이 매우 컸던 것 같다.</p>

<p>온라인에서 응시가 가능한 코딩 테스트는 하나도 빼먹지 않고 참가했었다. 코딩 테스트에 대한 연습이자, 실전에 대한 감을 익히고자 항상 부딪혔었다. 난이도에 따라 결과는 천차만별이었지만 많은 경험을 쌓았다고 생각한다.(올솔 해보고싶다🥲)</p>

<p>올해 9월에 운 좋게도, 신입 공채의 1차, 2차 코딩 테스트를 합격하게 되었다. 2차까지 통과한 건 처음이라 너무 기뻤고 기대를 크게 하지 않아서인지 더욱더 놀랐다. 코딩 테스트에 합격했다는 것 자체가 너무 좋았고, 기업에서 정의한 커트라인을 통과했다는 것에 보람을 느꼈던 것 같다.</p>

<p>그리고 면접을 볼 수 있는 기회를 얻게 되었는데, 처음에는 걱정이 앞섰다. 왜냐하면 면접에 대한 경험은 AUSG 커뮤니티 그룹에서 본 간단한 면접을 제외하고는 처음이었고, 면접 준비라는 것을 해본 적이 없었기 때문이다. 하지만 이러한 걱정은 얼마 가지 않아 자연스럽게 사라졌다.</p>

<p>나는 면접에 대한 마음가짐을 완전히 바꿨었다. ‘면접에 꼭 합격하자’ 라기보다는 ‘면접을 후회 없이 마무리하자’ 라는 생각이었고 떨어져도 면접에 대한 경험을 얻을 수 있기 때문에 <strong>면접을 준비하면서도 걱정보다는 기대감이 높았던 것 같다.</strong></p>

<p>1차 인터뷰는 기술 면접으로 진행되었는데, 대답을 적절히 못한 부분도 많았지만 개인적으로 후회 없는 면접을 보고 왔다고 생각해서 마음이 매우 편안했었던 기억이 있다.</p>

<p>그런데, 1차 인터뷰 합격 메일을 받게 되었고 매우 당황했었다. 신입의 성장 가능성을 좋게 봐주신 것 같았다. 따라서 2차 인터뷰인 임원 면접을 준비하게 되었다. 이 기회를 절대 놓칠 수 없다 생각해서 2차 인터뷰는 정말 제대로 준비했다. (어떻게 면접이 진행될지 모르기 때문에 기술, 인성 면접을 모두 준비했다!)</p>

<p>결과는 1주일 뒤에 나왔는데 ‘나’라는 사람을 좋게 봐주셔서 최종 합격하게되었다. <strong>취뽀!!</strong> 🥳🥳🥳🥳🥳</p>

<p>입사 전날까지도 실감이 나지 않아서 얼떨떨했다.. 사실 지금도 이게 진짜 현실이 맞나 싶은 생각이 들 때가 있다. ㅋㅋ</p>

<h2 id="return-앞으로의-목표">return 앞으로의 목표;</h2>

<p>지금은 온보딩 기간이라서 열심히 배우고 있다. 항상 해왔던 것처럼 나태해지지 않으려고 노력하고 있다.</p>

<p>현재 내 목표는 남들에게 나 자신을 주니어 개발자라고 소개할 수 있는 자격이 되는 것이다. 빨리 대학생에서 벗어나고 개발자의 자질을 어느 정도 갖춘, 파릇파릇한 주니어 개발자가 될 것이다. ㅎㅎ</p>

<h1 id="-hello-2022">} “Hello, 2022”;</h1>

<p>회고를 작성하고 나니 생각보다 소제목이 많아서 놀랐다.</p>

<p>‘1년 동안 열심히 살았구나!’라는 것을 느낄 수 있어 정말 뿌듯하다. (이게 회고의 숨겨진 순기능인가? ㅎㅎ)</p>

<p>2022년도 마찬가지로 연말 회고를 쓸 때 과거에 대한 후회가 없도록 열심히 살아야겠다!</p>

<p>그리고, 블로그 글도 꾸준히 쓰는 습관을 잃지 않을 것이다. 😁</p>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="retrospect" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">2021 공개SW 개발자대회 회고</title><link href="https://devwithpug.github.io/blog/opensw-retrospect/" rel="alternate" type="text/html" title="2021 공개SW 개발자대회 회고" /><published>2021-11-15T00:00:00+09:00</published><updated>2021-11-15T00:00:00+09:00</updated><id>https://devwithpug.github.io/blog/opensw-retrospect</id><content type="html" xml:base="https://devwithpug.github.io/blog/opensw-retrospect/"><![CDATA[<h1 id="개요">개요</h1>

<p>7월 초부터 11월 초까지 4개월 동안 진행한 공개SW 개발자대회가 드디어 마무리되었다.</p>

<p>생각보다 프로젝트가 길기도 했고, 없는 시간들을 쪼개가며 열심히 달려왔던 것 같아서 돌아보며 회고를 해보려고 한다.</p>

<p>(너무 값진 경험이었고 다른 분들께 공유해 보고 싶은 글이라 편하게 읽어주셨으면 합니다 ^^)</p>

<h1 id="대회-참여">대회 참여</h1>

<ul>
  <li><a href="https://www.oss.kr/dev_competition" target="_blank">2021 공개SW 개발자대회</a></li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/141672238-9dede240-8abf-452e-94ad-93eed69ad9b6.png" alt="image" class="align-center" /></p>

<p><img src="https://user-images.githubusercontent.com/69145799/141672478-24b5b8ae-37d7-4548-987f-cccb93bc9e22.png" alt="image" class="align-center" /></p>

<blockquote>
  <p>공개SW에 관심있는 국민 누구나?? 나 말하는 건가?</p>
</blockquote>

<p>2021, 6월이 마무리되던 때, 과학기술정보통신부에서 주최하는 공개SW 개발자대회의 참가 신청을 받는다는 것을 학교 동기를 통해 알게 되었다. 이때는 여름방학 기간이라서 시간도 많았고 다음 학기가 시작하기 전에 출품을 할 수 있을 것 같았다. 그리고 무엇보다 새로운 프로젝트를 너무 개발하고 싶었다..ㅎㅎ (넘쳐흐르는 개발 욕구)</p>

<p>따라서 평소 프로젝트를 함께 진행해 보고 싶었던 두 사람과 함께 팀을 이루게 되었고 자유 주제를 목표로 프로젝트를 시작하게 되었다.</p>

<h1 id="대회-진행">대회 진행</h1>

<p>7월 초 대회 준비를 시작하게 되었고 출품 마감까지 두 달 가량의 시간이 있었다.</p>

<h2 id="프로젝트-주제">프로젝트 주제</h2>

<blockquote>
  <p>공개 SW.. 근데 뭘 만들지?</p>
</blockquote>

<p>먼저, 3인으로 구성된 우리 팀은 각자 공부하는 분야가 완전히 달랐다. (각각 백엔드, 안드로이드, 인공지능)</p>

<p>따라서 프로젝트의 주제를 정하기 전에 아래와 같은 필요 요건들을 추려보았다.</p>

<ol>
  <li>안드로이드, 백엔드, 인공지능으로 파트를 나누어 각각 개발 -&gt; 하나의 서비스로 통합</li>
  <li>공개SW 대회 목적에 맞는 오픈 소스 프로젝트를 개발</li>
  <li>창의적인, 혁신적인 프로젝트이지만 반드시 구현이 가능해야 함</li>
</ol>

<p>팀원 모두의 역량을 최대로 이끌어내기 위해 각자 자신 있는 분야를 하는 것이 중요하다고 생각했고 이들을 연결하는 것은 하나의 도전일 뿐 절대 불가능하다고 생각하지 않았다. 그다음에는 팀원들과 머리를 맞대고 주제를 생각해 보았다. 힘든 고민을 한 결과 미디어 파이프를 이용해 손 제스처를 인식한 뒤 동일한 제스처를 매칭 시키는 알고리즘을 개발하고 그 알고리즘을 이용하는 채팅 애플리케이션을 제작하는 것으로 결정을 내리게 되었다.</p>

<ul>
  <li>대략적인 애플리케이션 흐름</li>
</ul>

<blockquote>
  <ol>
    <li>안드로이드 애플리케이션을 통해 손 제스처를 캡처 후 서버로 전송</li>
    <li>제스처 매칭 알고리즘을 통해 동일한 손 제스처들을 군집화하여 매칭을 수행</li>
    <li>매칭에 성공한 클라이언트들에게 채팅방을 생성(웹소켓으로 구현)</li>
  </ol>
</blockquote>

<h2 id="백엔드-개발">백엔드 개발</h2>

<ul>
  <li>백엔드 개발은 생각보다 쉽지 않았다!</li>
</ul>

<p>구현에 대한 고민이 정말 많았다. 채팅 서버는 어떻게 구현할 것인지, 채팅 데이터는 어떻게 관리할 것인지, 동시성에 대한 고민, 디비에 대한 고민, 매칭 알고리즘을 어떻게 연계할 것인지, 어떤 인프라를 사용하고 장애 처리는 어떻게 할지, HA 구현 방법, 협업이 용이하도록 서버를 구성하는 방법 등등.. 평소 이론 및 실습을 통한 공부에서는 절대로 경험해 볼 수 없는 부분이었고 지금 와서 돌이켜보면 내 스스로 성장할 수 있는 정말 좋은 기회였던 것 같다.</p>

<h3 id="채팅-서버">채팅 서버</h3>

<p>기본적으로 애플리케이션과는 REST API로 호출할 생각을 가지고 있었고 큰 어려움이 없었다.</p>

<p>하지만 가장 먼저 고민한 것은 백엔드의 채팅 서버를 구현하는 것이었는데 롱 폴링을 사용할지, 웹소켓을 사용할지 이었다. 롱 폴링의 경우 HTTP Request/Response 만 생각하면 되겠다고 생각했지만 비교적 무겁고 효율성을 놓고 보았을 때 공개SW 대회의 성격과 맞지 않다고 판단했다. 웹소켓의 경우 매우 가벼웠고 관련 기술에 관심이 있었기 때문에 웹소켓을 통해 구현하기로 결정하게 되었다.</p>

<h3 id="msa">MSA</h3>

<p>이번 대회를 진행하기 전에 나는 Spring Cloud에 대해 공부하고 있었다. MSA에 대해 알게 된 지도 얼마 안 되었고 간단한 토이 프로젝트 정도의 경험밖에 없었기 때문에 이번에 MSA를 통해 프로젝트를 구현해 보고자 하는 욕심이 있었다.</p>

<p>하지만 프로젝트에서는 AWS EC2 프리 티어 인스턴스를 사용하고 있었고 컴퓨팅 파워가 부족하기 때문에 리소스에 대한 걱정이 항상 있었다. 때문에 모놀리식 구조로도 충분히 구현할 수 있지 않을까? 라고 고민했었지만 매칭 알고리즘이 Mediapipe, Tensorflow와 같은 인공지능 라이브러리들을 필요로 했기 때문에 모든 서버를 자바 스프링으로 구현할 수 없었다. 따라서 Spring eureka server를 토대로 하여 파이썬에서 유레카 클라이언트로 접속할 수 있도록 MSA 아키텍처를 선택하게 되었다. 하지만 이로 인해서 프로젝트를 개발하는 동안 여러 번의 리소스 문제를 겪게 된다..ㅠㅠㅠ</p>

<p>이와 연계하여 메시지 브로커는 RabbitMQ를 사용했다. 먼저 각각의 서비스들의 분리된 configuration을 외부에서 한 번에 모아서 관리할 필요가 있었고 여기서 RabbitMQ, Kafka, AWS SQS와 같은 서드파티 및 클라우드 서비스들을 이용할 수 있었다. 하지만 Kafka의 경우 초당 10만 건의 데이터를 다루는 대용량 이벤트에 적합한 솔루션이었고 SQS의 경우 프리 티어로 이용할 수 있었지만 클라우드 서비스에 종속적이고 싶지 않았다. 마찬가지로 RabbitMQ는 비교적 적은 양의 서비스 내부 통신에 적합했고 메시지 브로커이지만 pub/sub 또한 지원하고 있었기 때문에 선택하게 되었다.</p>

<h3 id="trouble-shooting">Trouble-Shooting</h3>

<p>그 외에도 개발 중에 여러 가지 문제들을 겪고, 해결하면서 짧은 글들을 작성했었다.</p>

<ul>
  <li>
    <p><a href="https://github.com/devwithpug/RandHand-Chat/tree/main/backend/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80" target="_blank">https://github.com/devwithpug/RandHand-Chat/tree/main/backend/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80</a></p>
  </li>
  <li>
    <p><a href="https://devwithpug.github.io/project/randhand-1-spring-security-with-jwt/" target="_blank">스프링 시큐리티 - JWT 인증 방식 도입하기</a></p>
  </li>
  <li>
    <p><a href="https://devwithpug.github.io/project/randhand-2-application-properties-encryption/" target="_blank">스프링부트 application.yml 설정 파일 암호화 및 외부 관리</a></p>
  </li>
  <li>
    <p><a href="https://devwithpug.github.io/project/randhand-3-ci-cd-with-github-actions-and-aws-codedeploy/" target="_blank">Github Actions, AWS CodeDeploy</a></p>
  </li>
  <li>
    <p><a href="https://devwithpug.github.io/project/randhand-4-jvm-warmup-on-spring-cloud-msa/" target="_blank">JVM WarmUp으로 스프링 클라우드의 첫번째 요청 딜레이 없애기</a></p>
  </li>
</ul>

<p>두 달동안 팀을 이끌어 가면서 체계적으로 개발하는 것에 큰 초점을 두었고, 계획한 출품 일정에 맞추어 프로젝트를 완성할 수 있었다.</p>

<blockquote>
  <p><a href="https://github.com/devwithpug/RandHand-Chat" target="_blank">프로젝트 리포지토리 바로가기</a></p>
</blockquote>

<ul>
  <li>백엔드 서버 구성</li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/141271929-b9b246d8-8313-4647-99d4-85c8c2457f79.png" alt="backend-1" class="align-center" /></p>

<p><img src="https://user-images.githubusercontent.com/69145799/141271953-553adcf9-bb25-4445-8f73-f3e93246c42f.png" alt="backend-2" class="align-center" /></p>

<h2 id="본선-진출">본선 진출</h2>

<p><img src="https://user-images.githubusercontent.com/69145799/141674374-9d65ab63-843f-4d38-8c34-e00c545acba7.png" alt="image" class="align-center" /></p>

<p>1차 평가에 통과하여 본선에 진출하게 되었다!</p>

<p>1차 평가 이후 대회 프로세스는 멘토링, 기능 테스트, 라이선스 검증, 최종 발표로 진행되며 대회가 마무리된다.</p>

<h3 id="멘토링">멘토링</h3>

<p>본선에 진출한 이후 각 팀마다 실무자가 직접 맞춤형 멘토링을 진행하면서 출품작을 보완하는 시간이 주어졌다. 대회의 특성상 프로젝트의 분야가 매우 다양하기 때문에 각 팀마다 맞춤형 멘토링을 위해 설문을 받았었다. 우리 팀의 경우 상의를 해본 결과 인공지능과 모바일 관련 멘토링을 신청하게 되었는데 실제로는 백엔드 실무자분께서 멘토로 매칭이 되어서 조금 놀랐었다.</p>

<p>하지만 내 입장에서는 백엔드 개발자분께서 직접 멘토링을 받을 수 있는 정말 좋은 기회가 생기게 된 것이라 너무 기쁘기도 했고 코드에 부족한 점들이 많을 것 같아 걱정이 되기도 했었다.</p>

<p>멘토님과는 주 1~2회 정도 비대면으로 멘토링을 진행했으며 멘토님께서 모바일, 인공지능 분야에 대해서는 많은 도움을 주지 못할 것 같다고 하셨지만 모바일, 인공지능 관련 멘토링에서도 엄청난 도움이 되었었다.(전문 분야가 아닌 지식에도 실무자분들은 이 정도로 알고 계시구나.. 라는 것에 또 놀랐다)</p>

<p>백엔드 멘토링은 멘토님께서 제가 구현한 백엔드 서버에 대해 들어보신 후에 함께 인프라에 대해 고민해 보는 시간을 가지기도 했고 실제 백엔드 및 채팅 서버가 실무에서 어떻게 구현되고, 어떻게 서비스되고 있는지 실무자의 경험들을 들을 수 있어 너무 값진 경험이었다. 추가적으로 백엔드에만 치중하지 않고 개발자의 측면에서도 여러 조언들을 주셔서 너무 고마웠다. 멘토님 덕분에 고민해 보지 못했던 부분에 대해 생각해 볼 수 있었고 문제에 대해 함께 고민해 보는 시간이 너무너무 좋았던 것 같다.(만약 이런 기회를 얻고 싶다면 공개SW 개발자대회에 꼭 참가해 보라고 말해주고 싶다!)</p>

<h3 id="라이선스-검증-및-기능-테스트">라이선스 검증 및 기능 테스트</h3>

<p>멘토링이 종료된 후 출품작에 라이선스 문제는 없는지, 프로젝트의 기능은 모두 정상적으로 동작하는지 검사를 하게 된다.</p>

<ul>
  <li>라이선스 검증</li>
</ul>

<p>라이선스 검증의 경우 오픈소스의 특성상 사용한 모든 라이브러리에 대해 라이선스 충돌이 있지는 않은지, 내가 정상적인 라이선스를 명시하였는지 등등 라이선스 규정을 위반하지 않았는지를 검증했다. 먼저 라이선스에 대해 간단하게만 알고 있었지만 이번에 교육을 듣게 되면서 오픈소스 라이브러리가 생각보다는 복잡한 것을 알게 되었고 내가 프로젝트에서 사용한 라이브러리의 라이선스를 하나하나 찾아보게 되었다. 큰 문제가 있지는 않아서 프로젝트에 영향을 주지는 않았지만 오픈소스가 말 그대로 오픈소스이지는 않으니 조심해야 겠다고 생각했다 ㅎㅎ; (MariaDB 커넥터의 경우 LGPL 2.1이다 ..!)</p>

<ul>
  <li>기능 테스트</li>
</ul>

<p>기능 테스트를 진행하기로 한 날 당일에 백엔드 서버에서 에러가 난 것을 확인하였고 이를 해결하기 위해 실례를 무릅쓰고 테스트 일정을 연기하게 되었다.. 문제점을 테스트 당일에 알게 되어 너무 당황했고 빠르게 해결할 것이라 예상했지만 그렇게 하지 못했었다. 내가 부족해서 발생한 문제였기 때문에 팀원들에게 너무 미안했고 여기서 페널티를 받게 될까 봐 정말 걱정이 많았다. 그리고 평소 백엔드 개발을 할 때 worst case에 대해 항상 고민한다고 생각했는데, 정말 사소한 실수를 키우게 된 것 같아서 나를 다시 돌아보게 되었다.. 다행히도 기능 테스트를 마무리할 수 있었지만 절대 잊지 못할 것 같다.</p>

<h3 id="최종-발표">최종 발표</h3>

<p>최종 발표는 15분 동안 진행했으며 비대면으로 진행되었다. 심사위원분들이 여러 질문을 주셨는데 우리 팀의 프로젝트가 오픈소스로 어떻게 발전할 수 있을지? 어떤 곳에 기여할 수 있을지 와 같은 것에 초점을 둔 것 같았다. 7월부터 시작해서 4개월 동안의 대장정이 드디어 막을 내리게 되었다.</p>

<h1 id="결과">결과</h1>

<p><img src="https://user-images.githubusercontent.com/69145799/141676173-4f4d25e5-740c-401d-b84f-ed79c0d3116d.png" alt="result" class="align-center" /></p>

<p>이번 대회를 참여할 때도 수상 여부와 관계없이 경험을 해보자는 취지로 참가한 대회였었는데, 은상을 받게 되어서 팀원들 모두 놀랐다!</p>

<p>좋은 결과를 얻어서 4개월 동안 시간을 투자한 보람을 느낄수 있었다. 팀원들 모두 고생했다~</p>

<h1 id="마치며">마치며</h1>

<p>역시 이론과 실습은 다르다. 나는 나 자신이 야생형 개발자라고 생각하는데 이번 기회로 부딪혀보면서 정말 많이 성장한 것 같다. 하지만 지금 생각해 보면 아쉬웠던 점들이 있기도 하다.</p>

<p>가장 먼저, 백엔드 서버에 부하 테스트를 적용하지 못했다. 출품작을 보완할 때 서버의 고가용성을 위해 HA 까지 구현을 했지만 이에 연관하여 부하 테스트를 해보고 테스트 결과를 토대로 더욱더 개선할 필요가 있었지만 그렇게 하지 못한 것이 너무 아쉬웠다. 이번 프로젝트에서 무엇보다 기능에 대해 중점을 두다 보니, 서비스의 안전성에 대해 놓친 게 아닌가 하는 생각이 든다.</p>

<p>두 번째로, 어떤 기술, 인프라, 라이브러리 및 모듈을 붙이는 경우 납득할 수 있는 이유가 있어야 한다. 프로젝트 개발 시간에 많은 부분을 차지한 것이 리소스 최적화 부분이었다. AWS EC2 프리 티어를 사용하다 보니 MSA 에서 대부분 리소스가 부족했다. 실제 CI/CD는 각각의 도커 이미지를 docker-compose를 통해 배포하게 되는데 한 번에 모든 서비스를 연속으로 배포하는 경우 EC2 인스턴스가 100% 다운되었다. 따라서 사용하는 라이브러리가 꼭 필요한지? 필요 이상의 리소스를 낭비하지는 않는지? 대안은 없는지? 등 고민이 필수라는 것을 느꼈다. MSA 를 위해 유레카 서버를 붙였지만 이를 완벽하게 사용하지 못했다고 생각한다. 라이브러리를 적용하기 전에 야생형처럼 부딪히는 것도 좋지만 때로는 기술에 대해 선행되는 지식이 필요한 경우가 많다는 것을 느꼈다.</p>

<p>마지막으로, 아직 많이 부족한 개발자라는 것을 느꼈다. 프로젝트의 팀장으로서, 백엔드 파트 담당으로서, 대회가 잘 마무리되었지만 내 스스로 만족하지 못하는 부분들이 많았다. 더욱더 성장하려면 더욱더 시간을 투자해야 한다고 생각한다. 아직 관련 인프라에도 관심이 많고 백엔드 공부도 하고 있는 상황이기 때문에 앞으로도 열심히 달려볼 예정이다.</p>]]></content><author><name>최준규</name></author><category term="blog" /><category term="blog" /><category term="retrospect" /><summary type="html"><![CDATA[개요]]></summary></entry><entry><title type="html">Querydsl으로 안전한 쿼리 작성하기 + DataJPA</title><link href="https://devwithpug.github.io/java/querydsl-with-datajpa/" rel="alternate" type="text/html" title="Querydsl으로 안전한 쿼리 작성하기 + DataJPA" /><published>2021-09-19T00:00:00+09:00</published><updated>2021-09-19T00:00:00+09:00</updated><id>https://devwithpug.github.io/java/querydsl-with-datajpa</id><content type="html" xml:base="https://devwithpug.github.io/java/querydsl-with-datajpa/"><![CDATA[<h1 id="개요">개요</h1>

<p><strong><a href="https://querydsl.com/" target="_blank">Querydsl 홈페이지 바로가기</a></strong></p>

<p>나는 평소 <code class="language-plaintext highlighter-rouge">DataJPA</code> 에만 의존하여 코드를 작성하고 있었다. <code class="language-plaintext highlighter-rouge">Querydsl</code> 이라는 라이브러리가 있다는 것만 알았고 사용해 보지는 않았는데 궁금증이 생겼고 <code class="language-plaintext highlighter-rouge">Querydsl</code> 을 사용해 보면서 느낀 여러 유즈케이스들을 공유해 보고 싶어서 글을 쓰게 되었다.</p>

<h2 id="엔티티-구성">엔티티 구성</h2>

<p>예제 코드에 사용될 엔티티들은 다음과 같다.</p>

<p><code class="language-plaintext highlighter-rouge">Room</code>, <code class="language-plaintext highlighter-rouge">User</code> 엔티티가 각각 1:N 으로 결합되어 있으며 매우 간단한 구조를 가지고 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="nd">@Entity</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Room</span> <span class="o">{</span>

    <span class="nd">@Id</span> <span class="nd">@GeneratedValue</span>
    <span class="nd">@Column</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"room_id"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="nd">@Column</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">roomName</span><span class="o">;</span>

    <span class="nd">@OneToMany</span><span class="o">(</span><span class="n">mappedBy</span> <span class="o">=</span> <span class="s">"room"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">users</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>

    <span class="c1">// 생성자 생략</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="nd">@Entity</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="o">{</span>

    <span class="nd">@Id</span> <span class="nd">@GeneratedValue</span>
    <span class="nd">@Column</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"user_id"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>

    <span class="nd">@Column</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>

    <span class="nd">@Column</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">age</span><span class="o">;</span>

    <span class="nd">@Column</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">type</span><span class="o">;</span>

    <span class="nd">@Column</span>
    <span class="kd">private</span> <span class="kt">int</span> <span class="n">score</span><span class="o">;</span>

    <span class="nd">@ManyToOne</span><span class="o">(</span><span class="n">fetch</span> <span class="o">=</span> <span class="nc">FetchType</span><span class="o">.</span><span class="na">LAZY</span><span class="o">)</span>
    <span class="nd">@JoinColumn</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"room_id"</span><span class="o">)</span>
    <span class="kd">private</span> <span class="nc">Room</span> <span class="n">room</span><span class="o">;</span>

    <span class="c1">// 생성자 생략</span>
<span class="o">}</span>
</code></pre></div></div>

<h1 id="jpql의-문제점">JPQL의 문제점</h1>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 에 대해 알아보기 전에 기존 <code class="language-plaintext highlighter-rouge">JPQL</code> 의 문제점들을 말해보자면 다음과 같다.</p>

<p><strong>문자열로 쿼리를 하나하나 작성해야 한다는 점!</strong></p>

<p><code class="language-plaintext highlighter-rouge">JPQL</code> 에 익숙해지다 보면 큰 문제가 되지는 않지만 문자열의 최대 단점은 안정성이 매우 떨어진다는 것이다.</p>

<p>오타 또는 공백 하나의 프로퍼티를 잘못 참조하여 잘못된 결과를 가져오거나 올바르지 않은 쿼리문으로 바뀔 수 있다. 하지만 이런 <strong>오류가 생겨도 자바 프로젝트는 정상적으로 빌드가 된다는 점.</strong> 컴파일 단계에서 에러를 잡지 못하고 <strong>테스트 단계에서 에러를 잡아야 한다는 점.</strong> <strong>최악의 경우 테스트 코드가 작성되지 않았거나 테스트를 통과하게 되어 잘못된 JPQL 쿼리가 서비스에 배포된다면 크리티컬한 문제들이 생길 수 있다.</strong></p>

<p>아래의 코드는 공백 하나의 부재로 인해 런타임 에러가 발생하는 케이스이다. <code class="language-plaintext highlighter-rouge">DataJPA</code> 또한 동일한 문제가 발생한다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* JPA Repository */</span>
<span class="nd">@Repository</span>
<span class="nd">@Transactional</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserJpaRepository</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">EntityManager</span> <span class="n">em</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findAllWithJoinByAgeBetween</span><span class="o">(</span><span class="kt">int</span> <span class="n">goe</span><span class="o">,</span> <span class="kt">int</span> <span class="n">loe</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">em</span><span class="o">.</span><span class="na">createQuery</span><span class="o">(</span>
                        <span class="s">"select u "</span> <span class="o">+</span>
                        <span class="s">"from User u "</span> <span class="o">+</span>
                        <span class="s">"left join u.room r"</span> <span class="o">+</span> <span class="c1">// &lt;- 공백이 없지만 빌드는 정상적으로 수행되며 런타임 에러가 발생</span>
                        <span class="s">"where u.age between :goe and :loe"</span><span class="o">,</span> <span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setParameter</span><span class="o">(</span><span class="s">"goe"</span><span class="o">,</span> <span class="n">goe</span><span class="o">)</span>
                <span class="o">.</span><span class="na">setParameter</span><span class="o">(</span><span class="s">"loe"</span><span class="o">,</span> <span class="n">loe</span><span class="o">)</span>
                <span class="o">.</span><span class="na">getResultList</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="cm">/* DataJPA Repository */</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserDataJpaRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="c1">// DataJpa의 경우 동일한 문제가 발생할 수 있다.</span>
    <span class="nd">@Query</span><span class="o">(</span><span class="s">"select u from User u left join u.room r where u.age between ?1 and ?2"</span><span class="o">)</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findAllWithJoinByAgeBetween</span><span class="o">(</span><span class="kt">int</span> <span class="n">goe</span><span class="o">,</span> <span class="kt">int</span> <span class="n">loe</span><span class="o">);</span>

<span class="o">}</span>
</code></pre></div></div>

<h1 id="querydsl">Querydsl</h1>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 이란 <code class="language-plaintext highlighter-rouge">Querydsl</code> 을 통하여 생성되는 정적 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스를 이용하여 <code class="language-plaintext highlighter-rouge">SQL</code> 과 같은 쿼리를 생성하도록 도와주는 프레임워크이다. <code class="language-plaintext highlighter-rouge">JPA</code> 뿐만 아니라 <code class="language-plaintext highlighter-rouge">MongoDB</code>, <code class="language-plaintext highlighter-rouge">JDO</code>, <code class="language-plaintext highlighter-rouge">Lucene</code> 과 같은 라이브러리도 제공하고 있다.</p>

<p>앞에서 말했던 <code class="language-plaintext highlighter-rouge">JPQL</code>의 단점을 완벽하게 커버할 수 있는 <code class="language-plaintext highlighter-rouge">Querydsl</code> 은 타입에 안전한 방식으로 쿼리를 실행할 수 있다. 타입을 통하여 쿼리를 작성하므로 도메인 모델의 프로퍼티 변경에 유연하게 대처가 가능하며 강력한 코드 자동완성 기능의 이점을 얻을 수 있고 무엇보다 쿼리를 빠르고 안전하게 만들 수 있다.</p>

<h2 id="의존성-추가">의존성 추가</h2>

<p>Gradle 프로젝트에서 의존성을 추가하는 방법은 다음과 같다. 현재 5.0.0 버전이 최신 버전이며 다음과 같은 라이브러리들이 필요하다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">querydsl-jpa</code> : <code class="language-plaintext highlighter-rouge">Querydsl</code> 사용을 위한 라이브러리</li>
  <li><code class="language-plaintext highlighter-rouge">querydsl-core</code> : 코어 라이브러리. <code class="language-plaintext highlighter-rouge">querydsl-jpa</code>을 통해 함께 가져올 수 있지만 4.4.0 버전의 core를 가져오는 문제가 있어서 현재는 추가적으로 버전 명시가 필요함.</li>
  <li><code class="language-plaintext highlighter-rouge">annotationProcessor</code> : <code class="language-plaintext highlighter-rouge">Querydsl</code> 에서는 기존에 작성된 JPA 엔티티 클래스들을 어노테이션을 통해 인식하여 <code class="language-plaintext highlighter-rouge">Querydsl</code> 에서 사용 가능한 전용 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스들을 자동으로 생성 해준다. 이때 <code class="language-plaintext highlighter-rouge">JPA</code> 엔티티 클래스들을 인식하기 위해 다음과 같은 라이브러리 의존성이 요구된다.</li>
</ul>

<p>그리고 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스들을 생성할 디렉터리를 명시해 주어야 한다. 이는 개발자마다 프로젝트의 구조가 다르기 때문에 자신이 원하는 디렉터리를 커스터마이징할 수 있다. 그리고 <code class="language-plaintext highlighter-rouge">compileJava</code> task에서 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스를 생성하도록 지정해 주었다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">build.gradle</code></li>
</ul>

<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencies</span> <span class="o">{</span>
	<span class="n">implementation</span> <span class="s1">'com.querydsl:querydsl-jpa:5.0.0'</span>
	<span class="n">implementation</span> <span class="s1">'com.querydsl:querydsl-core:5.0.0'</span>
	<span class="n">annotationProcessor</span><span class="o">(</span>
			<span class="s1">'javax.persistence:javax.persistence-api:2.2'</span><span class="o">,</span>
			<span class="s1">'javax.annotation:javax.annotation-api:1.3.2'</span><span class="o">,</span>
			<span class="s1">'com.querydsl:querydsl-apt:5.0.0:jpa'</span>
	<span class="o">)</span>
	<span class="n">testImplementation</span> <span class="s1">'com.querydsl:querydsl-jpa:5.0.0'</span>
	<span class="n">testAnnotationProcessor</span> <span class="s1">'com.querydsl:querydsl-apt:5.0.0'</span>
<span class="o">}</span>

<span class="kt">def</span> <span class="n">queryDslOutput</span> <span class="o">=</span> <span class="n">file</span><span class="o">(</span><span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">projectDir</span><span class="o">,</span> <span class="s1">'/src/main/generated/'</span><span class="o">))</span>
<span class="n">sourceSets</span> <span class="o">{</span>
	<span class="n">main</span> <span class="o">{</span>
		<span class="n">java</span> <span class="o">{</span>
			<span class="n">srcDir</span> <span class="n">queryDslOutput</span>
		<span class="o">}</span>
	<span class="o">}</span>
<span class="o">}</span>

<span class="n">compileJava</span> <span class="o">{</span>
	<span class="n">options</span><span class="o">.</span><span class="na">compilerArgs</span> <span class="o">&lt;&lt;</span> <span class="s1">'-s'</span>
	<span class="n">options</span><span class="o">.</span><span class="na">compilerArgs</span> <span class="o">&lt;&lt;</span> <span class="s2">"$projectDir/src/main/generated/"</span>

	<span class="n">doFirst</span> <span class="o">{</span>
		<span class="n">delete</span><span class="o">(</span><span class="n">files</span><span class="o">(</span><span class="s2">"${projectDir}/src/main/generated/"</span><span class="o">))</span>
		<span class="n">file</span><span class="o">(</span><span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">projectDir</span><span class="o">,</span> <span class="s1">'/src/main/generated/'</span><span class="o">)).</span><span class="na">mkdirs</span><span class="o">();</span>
	<span class="o">}</span>
<span class="o">}</span>

</code></pre></div></div>

<h2 id="compilejava-task-실행">compileJava task 실행</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./gradlew compileJava 
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">compileJava</code> Gradle task를 실행하면 <code class="language-plaintext highlighter-rouge">build.gradle</code>에서 지정한 디렉터리에 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스들이 생성된 것을 확인할 수 있다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">QUser.java</code></li>
</ul>

<p><img src="https://user-images.githubusercontent.com/69145799/133917090-26f6ff1d-546d-4a4c-9b54-b9999a9d8453.png" alt="image" class="align-center" /></p>

<h2 id="bean-등록">Bean 등록</h2>

<p>다음으로는 <code class="language-plaintext highlighter-rouge">Querydsl</code> 의 쿼리를 작성할 <code class="language-plaintext highlighter-rouge">JPAQueryFactory</code> 클래스를 빈으로 등록해 주었다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">AppConfig</span> <span class="o">{</span>
    
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">EntityManager</span> <span class="n">em</span><span class="o">;</span>

    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">JPAQueryFactory</span> <span class="nf">queryFactory</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">JPAQueryFactory</span><span class="o">(</span><span class="n">em</span><span class="o">);</span>
    <span class="o">}</span>
    
<span class="o">}</span>
</code></pre></div></div>

<h2 id="querydsl-사용-사례">Querydsl 사용 사례</h2>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 을 통해 생성한 예제 쿼리들을 몇 가지 작성해 보았다.</p>

<h3 id="join-쿼리">join 쿼리</h3>

<p><code class="language-plaintext highlighter-rouge">.join()</code>, <code class="language-plaintext highlighter-rouge">.leftJoin()</code> 과같이 사용 가능하며 <code class="language-plaintext highlighter-rouge">.on()</code> 절을 통하여 동적인 조건을 통해 연관관계없이 조인도 가능하다.</p>

<p>또한 쿼리 성능 최적화를 위한 fetch join도 아래와 같이 사용할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QRoom</span><span class="o">.</span><span class="na">room</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QUser</span><span class="o">.</span><span class="na">user</span><span class="o">;</span>

<span class="cm">/* with JPQL */</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">resultList</span> <span class="o">=</span> <span class="n">em</span><span class="o">.</span><span class="na">createQuery</span><span class="o">(</span>
                        <span class="s">"select u "</span> <span class="o">+</span>
                        <span class="s">"from User u "</span> <span class="o">+</span>
                        <span class="s">"join u.room r"</span><span class="o">,</span> <span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
        <span class="o">.</span><span class="na">getResultList</span><span class="o">();</span>

<span class="cm">/* with Querydsl */</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">resultQuerydsl</span> <span class="o">=</span> <span class="n">queryFactory</span>
                <span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
                <span class="o">.</span><span class="na">join</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">room</span><span class="o">,</span> <span class="n">room</span><span class="o">).</span><span class="na">fetchJoin</span><span class="o">()</span> <span class="c1">// fetch join 또한 가능하다</span>
                <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
</code></pre></div></div>

<h3 id="페이징-쿼리">페이징 쿼리</h3>

<p>페이징 쿼리 또한 <code class="language-plaintext highlighter-rouge">JPQL</code> 과 매우 유사하다.</p>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 의 이전 버전에서는 <code class="language-plaintext highlighter-rouge">fetchResults()</code>, <code class="language-plaintext highlighter-rouge">fetchCount()</code> 메소드를 이용하여 페이징 쿼리를 작성했지만 <strong>5.0.0 버전에서 deprecated 되었다.</strong> 이유는 다음과 같다.</p>

<blockquote>
  <p>fetchResults() : Get the projection in QueryResults form. Make sure to use fetch() instead if you do not rely on the QueryResults.getOffset() or QueryResults.getLimit(), because it will be more performant. Also, count queries cannot be properly generated for all dialects. For example: in JPA count queries can’t be generated for queries that have multiple group by expressions or a having clause. Get the projection in QueryResults form. Use fetch() instead if you do not need the total count of rows in the query result.</p>
</blockquote>

<blockquote>
  <p>fetchCount() : An implementation is allowed to fall back to fetch().size().</p>
</blockquote>

<p>count 쿼리가 모든 dialect에서 또는 다중 그룹 쿼리에서 완벽하게 지원되지 않기 때문에 deprecated 되었다. <code class="language-plaintext highlighter-rouge">fetchCount()</code> 대신 <code class="language-plaintext highlighter-rouge">fetch().size()</code> 로 동일한 결과를 얻을 수 있다고 설명한다. 서비스의 크기에 따라 적절한 방법으로 count 를 받아와야 하므로 이에 대한 고민이 필요할 것 같다.</p>

<blockquote>
  <p><strong>내용 추가 (22.01.20)</strong></p>

  <p>List 인터페이스의 구현체인 <code class="language-plaintext highlighter-rouge">ArrayList.size()</code> 의 시간 복잡도는 <code class="language-plaintext highlighter-rouge">O(1)</code> 이다. 그렇다면 DB는 어떨까?</p>

  <p>관련 자료를 찾아보니 DB의 count(*) 쿼리는 DB 엔진마다 시간 복잡도가 다르다고 한다.</p>
  <ul>
    <li>MyISAM의 경우: 전체 row 수가 각 테이블에 저장되므로 count(*) 는 <code class="language-plaintext highlighter-rouge">O(1)</code> 의 시간복잡도를 가진다.</li>
    <li>InnoDB의 경우: 전체 row 수가 저장되지 않으므로 full scan이 필요하다. 시간복잡도 <code class="language-plaintext highlighter-rouge">O(n)</code></li>
  </ul>

  <p>출처 : <a href="https://stackoverflow.com/questions/5257973/mysql-complexity-of-select-count-from-mytable" target="_blank">stackoverflow - MYSQL - Complexity of: SELECT COUNT(*) FROM MyTable;</a></p>
</blockquote>

<p>따라서 아래와 같이 페이징 쿼리문을 작성하면 된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QUser</span><span class="o">.</span><span class="na">user</span><span class="o">;</span>

<span class="kd">public</span> <span class="nc">Page</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">findUserWithPaging</span><span class="o">(</span><span class="nc">Pageable</span> <span class="n">pageable</span><span class="o">)</span> <span class="o">{</span>

	<span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">content</span> <span class="o">=</span> <span class="n">queryFactory</span>
			<span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
			<span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">.</span><span class="na">like</span><span class="o">(</span><span class="s">"user_"</span><span class="o">))</span>
			<span class="o">.</span><span class="na">offset</span><span class="o">(</span><span class="n">pageable</span><span class="o">.</span><span class="na">getOffset</span><span class="o">())</span> <span class="c1">// offset</span>
			<span class="o">.</span><span class="na">limit</span><span class="o">(</span><span class="n">pageable</span><span class="o">.</span><span class="na">getPageSize</span><span class="o">())</span> <span class="c1">// limit</span>
			<span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

        <span class="kt">int</span> <span class="n">totalSize</span> <span class="o">=</span> <span class="n">queryFactory</span> <span class="c1">// count 쿼리</span>
                        <span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">.</span><span class="na">like</span><span class="o">(</span><span class="s">"user_"</span><span class="o">))</span>
                        <span class="o">.</span><span class="na">fetch</span><span class="o">().</span><span class="na">size</span><span class="o">();</span>

	<span class="k">return</span> <span class="k">new</span> <span class="nc">PageImpl</span><span class="o">&lt;&gt;(</span><span class="n">content</span><span class="o">,</span> <span class="n">pageable</span><span class="o">,</span> <span class="n">totalSize</span><span class="o">);</span> <span class="c1">// 쿼리 결과로 페이징 객체 리턴</span>
<span class="o">}</span>

</code></pre></div></div>

<h3 id="서브-쿼리">서브 쿼리</h3>

<p>서브 쿼리의 경우는 <code class="language-plaintext highlighter-rouge">Querydsl</code> 에서 제공하는 <code class="language-plaintext highlighter-rouge">JPAExpressions</code> 클래스를 사용하여 가독성 있는 코드 구현이 가능하다.</p>

<p>서브 쿼리에서 주의할 점은 <strong>메인 쿼리와 서브 쿼리의 Q-type 객체를 분리하여 사용해야 한다는 점이다.</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.querydsl.jpa.JPAExpressions</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QUser</span><span class="o">.</span><span class="na">user</span><span class="o">;</span>

<span class="cm">/* with JPQL */</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">em</span><span class="o">.</span><span class="na">createQuery</span><span class="o">(</span>
                <span class="s">"select u "</span> <span class="o">+</span>
                <span class="s">"from User u "</span> <span class="o">+</span>
                <span class="s">"where u.score in ("</span> <span class="o">+</span>
                        <span class="s">"select max(u.score) "</span> <span class="o">+</span>
                        <span class="s">"from User u "</span> <span class="o">+</span>
                        <span class="s">"group by u.type) "</span> <span class="o">+</span>
                <span class="s">"group by u.type"</span><span class="o">,</span> <span class="nc">User</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
        <span class="o">.</span><span class="na">getResultList</span><span class="o">();</span>

<span class="cm">/* with Querydsl */</span>
<span class="c1">// 1. where절 서브쿼리(중첩 서브쿼리)</span>
<span class="nc">QUser</span> <span class="n">userSub</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">QUser</span><span class="o">(</span><span class="s">"userSub"</span><span class="o">);</span> <span class="c1">// 서브 쿼리를 위한 Q타입 객체가 하나 더 필요함</span>

<span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="n">resultNestedSubQuery</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">.</span><span class="na">in</span><span class="o">(</span><span class="nc">JPAExpressions</span>
                <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="n">userSub</span><span class="o">.</span><span class="na">score</span><span class="o">.</span><span class="na">max</span><span class="o">())</span>
                <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">userSub</span><span class="o">)</span>
                <span class="o">.</span><span class="na">groupBy</span><span class="o">(</span><span class="n">userSub</span><span class="o">.</span><span class="na">type</span><span class="o">)))</span>
        <span class="o">.</span><span class="na">groupBy</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">type</span><span class="o">).</span><span class="na">fetch</span><span class="o">();</span>

<span class="c1">// 2. select절 서브쿼리(스칼라 서브쿼리)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Double</span><span class="o">&gt;</span> <span class="n">resultScalarSubQuery</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">JPAExpressions</span>
                <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="n">userSub</span><span class="o">.</span><span class="na">score</span><span class="o">.</span><span class="na">avg</span><span class="o">())</span>
                <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">userSub</span><span class="o">)</span>
                <span class="o">.</span><span class="na">groupBy</span><span class="o">(</span><span class="n">userSub</span><span class="o">.</span><span class="na">type</span><span class="o">)</span>
        <span class="o">)</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
</code></pre></div></div>

<h3 id="projection">Projection</h3>

<p>쿼리의 결과로 <code class="language-plaintext highlighter-rouge">Dto</code> 를 사용하지 않고 간단히 몇 가지 프로퍼티들만 조회하고 싶은 경우 아래와 같이 <code class="language-plaintext highlighter-rouge">Projection</code> 쿼리를 사용하면 된다. 이때 조회 결과가 <code class="language-plaintext highlighter-rouge">Tuple</code> 로 넘어오며 값을 꺼낼 때 <code class="language-plaintext highlighter-rouge">Q-type</code> 객체의 프로퍼티를 넘겨주면 된다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QRoom</span><span class="o">.</span><span class="na">room</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QUser</span><span class="o">.</span><span class="na">user</span><span class="o">;</span>

<span class="cm">/* with JPQL */</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Object</span><span class="o">[]&gt;</span> <span class="n">resultJPQL</span> <span class="o">=</span> <span class="n">em</span><span class="o">.</span><span class="na">createQuery</span><span class="o">(</span>
                <span class="s">"select r.roomName, u.username, u.age, u.score "</span> <span class="o">+</span>
                <span class="s">"from User u "</span> <span class="o">+</span>
                <span class="s">"left join u.room r"</span>
        <span class="o">).</span><span class="na">getResultList</span><span class="o">();</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">Object</span><span class="o">[]</span> <span class="n">row</span> <span class="o">:</span> <span class="n">resultJPQL</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">roomName</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">row</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
    <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">row</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span>
    <span class="nc">Integer</span> <span class="n">age</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Integer</span><span class="o">)</span> <span class="n">row</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span>
    <span class="nc">Integer</span> <span class="n">score</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Integer</span><span class="o">)</span> <span class="n">row</span><span class="o">[</span><span class="mi">3</span><span class="o">];</span>
<span class="o">}</span>

<span class="cm">/* with Querydsl */</span>

<span class="nc">List</span><span class="o">&lt;</span><span class="nc">Tuple</span><span class="o">&gt;</span> <span class="n">resultQuerydsl</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">)</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">leftJoin</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">room</span><span class="o">,</span> <span class="n">room</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

<span class="k">for</span> <span class="o">(</span><span class="nc">Tuple</span> <span class="n">tuple</span> <span class="o">:</span> <span class="n">resultQuerydsl</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">String</span> <span class="n">roomName</span> <span class="o">=</span> <span class="n">tuple</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">);</span>
    <span class="nc">String</span> <span class="n">username</span> <span class="o">=</span> <span class="n">tuple</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">);</span>
    <span class="nc">Integer</span> <span class="n">age</span> <span class="o">=</span> <span class="n">tuple</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">);</span>
    <span class="nc">Integer</span> <span class="n">score</span> <span class="o">=</span> <span class="n">tuple</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="dto로-projection-하기">Dto로 Projection 하기</h3>

<p>자주 사용되는 특정 쿼리의 결과는 <code class="language-plaintext highlighter-rouge">Dto</code> 로 따로 분리하여 가져올 때가 많은데 이때는 다양한 방법으로 조회가 가능하다.</p>

<p>먼저 예제를 위한 <code class="language-plaintext highlighter-rouge">Dto</code> 클래스를 생성해 주었다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">UserDto.java</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserDto</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">roomName</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">age</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">score</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">UserDto</span><span class="o">(</span><span class="nc">String</span> <span class="n">roomName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="kt">int</span> <span class="n">age</span><span class="o">,</span> <span class="kt">int</span> <span class="n">score</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">roomName</span> <span class="o">=</span> <span class="n">roomName</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">age</span> <span class="o">=</span> <span class="n">age</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">score</span> <span class="o">=</span> <span class="n">score</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li>Dto 쿼리 예제</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.querydsl.core.Tuple</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">com.querydsl.core.types.Projections</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QRoom</span><span class="o">.</span><span class="na">room</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">static</span> <span class="n">com</span><span class="o">.</span><span class="na">example</span><span class="o">.</span><span class="na">querydslexample</span><span class="o">.</span><span class="na">entity</span><span class="o">.</span><span class="na">QUser</span><span class="o">.</span><span class="na">user</span><span class="o">;</span>

<span class="cm">/* with JPQL */</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultJPQL</span> <span class="o">=</span> <span class="n">em</span><span class="o">.</span><span class="na">createQuery</span><span class="o">(</span>
                        <span class="s">"select new com.example.querydslexample.dto.UserDto(r.roomName, u.username, u.age, u.score) "</span> <span class="o">+</span>
                        <span class="s">"from User u "</span> <span class="o">+</span>
                        <span class="s">"left join u.room r"</span><span class="o">,</span> <span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span>
        <span class="o">).</span><span class="na">getResultList</span><span class="o">();</span>

<span class="cm">/* with Querydsl */</span>

<span class="c1">// 1. setter 메소드를 이용한 방법</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultBySetter</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">bean</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

<span class="c1">// 2-(1). class fields 참조를 이용한 방법</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultByFields</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">fields</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

<span class="c1">// 2-(2). class field의 변수 명이 다른 경우</span>
<span class="c1">// ex) score(X) userScore(O)</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultByFields2</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">fields</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">.</span><span class="na">as</span><span class="o">(</span><span class="s">"userScore"</span><span class="o">))</span>
        <span class="o">)</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

<span class="c1">// 2-(3). 스칼라 서브쿼리와 연계하여 사용</span>
<span class="c1">// ex) roomName(X) rName(O) &amp; 대문자로 가져와야 하는 경우</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">fields</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="nc">ExpressionUtils</span><span class="o">.</span><span class="na">as</span><span class="o">(</span><span class="nc">JPAExpressions</span>
                        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="n">subUser</span><span class="o">.</span><span class="na">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">.</span><span class="na">upper</span><span class="o">())</span>
                        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">subUser</span><span class="o">)</span>
                        <span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">subUser</span><span class="o">.</span><span class="na">username</span><span class="o">.</span><span class="na">eq</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">)),</span> <span class="s">"rName"</span><span class="o">),</span>
                <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span>
        <span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>

<span class="c1">// 3. 생성자를 이용한 방법</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultByConstructor</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Projections</span><span class="o">.</span><span class="na">constructor</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Dto Q-type</code> 클래스 생성하여 사용하기</li>
</ul>

<p>또 다른 방법은 <code class="language-plaintext highlighter-rouge">Dto</code> 클래스에 <code class="language-plaintext highlighter-rouge">@QueryProjection</code> 어노테이션을 추가하여 <code class="language-plaintext highlighter-rouge">Dto</code> 전용 <code class="language-plaintext highlighter-rouge">Q-type</code> 객체를 생성하는 방법도 있다. 더욱더 간편하게 쿼리를 작성할 수는 있지만 <code class="language-plaintext highlighter-rouge">Dto</code> 클래스 내부에도 <code class="language-plaintext highlighter-rouge">Querydsl</code> 라이브러리 의존성이 생기기 때문에 확장성과 유지보수성에서 단점이 될 수도 있다.</p>

<p>가장 먼저 <code class="language-plaintext highlighter-rouge">Dto</code> 클래스의 생성자에 <code class="language-plaintext highlighter-rouge">@QueryProjection</code> 어노테이션을 추가해 주었다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.querydsl.core.annotations.QueryProjection</span><span class="o">;</span>

<span class="nd">@Data</span>
<span class="nd">@NoArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserDto</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">roomName</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">age</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">score</span><span class="o">;</span>

    <span class="nd">@QueryProjection</span>
    <span class="kd">public</span> <span class="nf">UserDto</span><span class="o">(</span><span class="nc">String</span> <span class="n">roomName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="kt">int</span> <span class="n">age</span><span class="o">,</span> <span class="kt">int</span> <span class="n">score</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">roomName</span> <span class="o">=</span> <span class="n">roomName</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">age</span> <span class="o">=</span> <span class="n">age</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">score</span> <span class="o">=</span> <span class="n">score</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>그다음 기존 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스를 생성했던 것과 마찬가지로 <code class="language-plaintext highlighter-rouge">compileJava</code> task를 실행하면 자동으로 <code class="language-plaintext highlighter-rouge">Dto</code> 전용 <code class="language-plaintext highlighter-rouge">Q-type</code> 클래스가 생성된다.</p>

<ul>
  <li>자동으로 생성된 <code class="language-plaintext highlighter-rouge">QUserDto.java</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">com.example.querydslexample.dto</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.querydsl.core.types.dsl.*</span><span class="o">;</span>

<span class="kn">import</span> <span class="nn">com.querydsl.core.types.ConstructorExpression</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">javax.annotation.processing.Generated</span><span class="o">;</span>

<span class="cm">/**
 * com.example.querydslexample.dto.QUserDto is a Querydsl Projection type for UserDto
 */</span>
<span class="nd">@Generated</span><span class="o">(</span><span class="s">"com.querydsl.codegen.DefaultProjectionSerializer"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">QUserDto</span> <span class="kd">extends</span> <span class="nc">ConstructorExpression</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="n">serialVersionUID</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1133442085L</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">QUserDto</span><span class="o">(</span><span class="n">com</span><span class="o">.</span><span class="na">querydsl</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">types</span><span class="o">.</span><span class="na">Expression</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">roomName</span><span class="o">,</span> <span class="n">com</span><span class="o">.</span><span class="na">querydsl</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">types</span><span class="o">.</span><span class="na">Expression</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">username</span><span class="o">,</span> <span class="n">com</span><span class="o">.</span><span class="na">querydsl</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">types</span><span class="o">.</span><span class="na">Expression</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">age</span><span class="o">,</span> <span class="n">com</span><span class="o">.</span><span class="na">querydsl</span><span class="o">.</span><span class="na">core</span><span class="o">.</span><span class="na">types</span><span class="o">.</span><span class="na">Expression</span><span class="o">&lt;</span><span class="nc">Integer</span><span class="o">&gt;</span> <span class="n">score</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="nc">UserDto</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">&lt;?&gt;[]{</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="kt">int</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="kt">int</span><span class="o">.</span><span class="na">class</span><span class="o">},</span> <span class="n">roomName</span><span class="o">,</span> <span class="n">username</span><span class="o">,</span> <span class="n">age</span><span class="o">,</span> <span class="n">score</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Q-type Dto</code> 클래스를 이용한 쿼리</li>
</ul>

<p>생성된 <code class="language-plaintext highlighter-rouge">Q-type Dto</code> 클래스를 사용하여 다음과 같이 조회가 가능하다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="n">resultByProjectionAnnotation</span> <span class="o">=</span> <span class="n">queryFactory</span>
        <span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="k">new</span> <span class="nc">QUserDto</span><span class="o">(</span><span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">,</span> <span class="n">user</span><span class="o">.</span><span class="na">score</span><span class="o">))</span>
        <span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
        <span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
</code></pre></div></div>

<h3 id="동적-쿼리">동적 쿼리</h3>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 를 통해서 동적 쿼리를 작성하는 방법은 다음과 같다.</p>

<p><code class="language-plaintext highlighter-rouge">BooleanBuilder</code>를 이용하면 아래와 같이 동적 조건들을 추가할 수 있다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BooleanBuilder</code></li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">dynamicQueryWithBooleanBuilder</span><span class="o">(</span>
	<span class="nc">String</span> <span class="n">rName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">uName</span><span class="o">,</span> <span class="nc">Integer</span> <span class="n">ageGoe</span><span class="o">,</span> <span class="nc">Integer</span> <span class="n">ageLoe</span><span class="o">)</span> <span class="o">{</span>

	<span class="nc">BooleanBuilder</span> <span class="n">builder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BooleanBuilder</span><span class="o">();</span>

	<span class="k">if</span> <span class="o">(</span><span class="n">rName</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">builder</span><span class="o">.</span><span class="na">and</span><span class="o">(</span><span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">.</span><span class="na">eq</span><span class="o">(</span><span class="n">rName</span><span class="o">));</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">uName</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">builder</span><span class="o">.</span><span class="na">and</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">.</span><span class="na">eq</span><span class="o">(</span><span class="n">uName</span><span class="o">));</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">ageGoe</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">builder</span><span class="o">.</span><span class="na">and</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">goe</span><span class="o">(</span><span class="n">ageGoe</span><span class="o">));</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">ageLoe</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="n">builder</span><span class="o">.</span><span class="na">and</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">loe</span><span class="o">(</span><span class="n">ageLoe</span><span class="o">));</span>

	<span class="k">return</span> <span class="n">queryFactory</span>
			<span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
			<span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">builder</span><span class="o">)</span>
			<span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">BooleanExpression</code></li>
</ul>

<p>또 다른 방법은 <code class="language-plaintext highlighter-rouge">BooleanExpression</code> 을 리턴하도록 메소드를 뽑아서 사용하는 것인데 이렇게 하면 생성한 조건 메소드들을 다른 쿼리에서도 재활용이 가능하다는 장점이 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">&gt;</span> <span class="nf">dynamicQueryWithBooleanExpressions</span><span class="o">(</span><span class="nc">String</span> <span class="n">rName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">uName</span><span class="o">,</span> <span class="nc">Integer</span> <span class="n">ageGoe</span><span class="o">,</span> <span class="nc">Integer</span> <span class="n">ageLoe</span><span class="o">)</span> <span class="o">{</span>
	<span class="nc">JPAQueryFactory</span> <span class="n">queryFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JPAQueryFactory</span><span class="o">(</span><span class="n">em</span><span class="o">);</span>

	<span class="k">return</span> <span class="n">queryFactory</span>
			<span class="o">.</span><span class="na">selectFrom</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
			<span class="o">.</span><span class="na">where</span><span class="o">(</span><span class="n">roomNameEq</span><span class="o">(</span><span class="n">rName</span><span class="o">),</span> <span class="n">usernameEq</span><span class="o">(</span><span class="n">uName</span><span class="o">),</span> <span class="n">ageBetween</span><span class="o">(</span><span class="n">ageGoe</span><span class="o">,</span> <span class="n">ageLoe</span><span class="o">))</span>
			<span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">BooleanExpression</span> <span class="nf">roomNameEq</span><span class="o">(</span><span class="nc">String</span> <span class="n">roomName</span><span class="o">)</span> <span class="o">{</span>
	<span class="k">return</span> <span class="n">roomName</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">room</span><span class="o">.</span><span class="na">roomName</span><span class="o">.</span><span class="na">eq</span><span class="o">(</span><span class="n">roomName</span><span class="o">)</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">BooleanExpression</span> <span class="nf">usernameEq</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">)</span> <span class="o">{</span>
	<span class="k">return</span> <span class="n">username</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">.</span><span class="na">eq</span><span class="o">(</span><span class="n">username</span><span class="o">)</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>

<span class="kd">private</span> <span class="nc">BooleanExpression</span> <span class="nf">ageBetween</span><span class="o">(</span><span class="nc">Integer</span> <span class="n">goe</span><span class="o">,</span> <span class="nc">Integer</span> <span class="n">loe</span><span class="o">)</span> <span class="o">{</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">goe</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">loe</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">goe</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">loe</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">goe</span><span class="o">(</span><span class="n">goe</span><span class="o">);</span>
	<span class="k">if</span> <span class="o">(</span><span class="n">goe</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">loe</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">loe</span><span class="o">(</span><span class="n">loe</span><span class="o">);</span>
	<span class="k">return</span> <span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">goe</span><span class="o">(</span><span class="n">goe</span><span class="o">).</span><span class="na">and</span><span class="o">(</span><span class="n">user</span><span class="o">.</span><span class="na">age</span><span class="o">.</span><span class="na">loe</span><span class="o">(</span><span class="n">loe</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="sql-함수-사용하기">SQL 함수 사용하기</h3>

<p><code class="language-plaintext highlighter-rouge">concat</code>, <code class="language-plaintext highlighter-rouge">coalesce</code>, <code class="language-plaintext highlighter-rouge">upper</code> 와 같은 간단한 함수들은 <code class="language-plaintext highlighter-rouge">Querydsl</code> 에서 메소드로 지원하고 있다.</p>

<p>하지만 <code class="language-plaintext highlighter-rouge">Oracle</code>, <code class="language-plaintext highlighter-rouge">SQL Server</code> 에 문법 차이가 있는 것처럼 자신이 사용하고 싶은 함수가 <code class="language-plaintext highlighter-rouge">Querydsl</code> 에 없는 경우는 <code class="language-plaintext highlighter-rouge">stringTemplate()</code>을 이용하여 쿼리를 작성할 수 있다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">com.querydsl.core.types.dsl.Expressions</span><span class="o">;</span>

<span class="c1">// MariaDB102Dialect</span>
<span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">queryFactory</span>
		<span class="o">.</span><span class="na">select</span><span class="o">(</span><span class="nc">Expressions</span><span class="o">.</span><span class="na">stringTemplate</span><span class="o">(</span>
				<span class="s">"function('regexp_replace', {0}, {1}, {2})"</span><span class="o">,</span>
				<span class="n">user</span><span class="o">.</span><span class="na">username</span><span class="o">,</span> <span class="s">"user"</span><span class="o">,</span> <span class="s">"User_"</span><span class="o">))</span>
		<span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">user</span><span class="o">)</span>
		<span class="o">.</span><span class="na">fetch</span><span class="o">();</span>
</code></pre></div></div>

<h1 id="datajpa와-함께-사용하기">DataJPA와 함께 사용하기</h1>

<p><code class="language-plaintext highlighter-rouge">DataJPA</code> 의 경우에도 <code class="language-plaintext highlighter-rouge">@Query</code> 를 통한 조회에 한계성이 있기 때문에.. 이러한 단점들을 <code class="language-plaintext highlighter-rouge">Querydsl</code> 과 연계하여 해결할 수 있다.</p>

<p><code class="language-plaintext highlighter-rouge">DataJPA</code> 에 커스텀 쿼리 메소드를 작성할 수 있도록 커스텀 인터페이스를 만들어 직접 구현해 주면 된다.</p>

<h2 id="custom-인터페이스">Custom 인터페이스</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* UserRepository.java */</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserRepository</span> <span class="kd">extends</span>
        <span class="nc">JpaRepository</span><span class="o">&lt;</span><span class="nc">User</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;,</span>
        <span class="nc">UserRepositoryCustom</span> <span class="c1">// 커스텀 인터페이스도 함께 상속</span>
<span class="o">{</span>
	<span class="c1">// 간단한 쿼리는 DataJPA로 작성 가능</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Member</span><span class="o">&gt;</span> <span class="nf">findByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">);</span>
<span class="o">}</span>


<span class="cm">/* UserRepositoryCustom.java */</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserRepositoryCustom</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="nf">search</span><span class="o">(</span><span class="nc">UserSearchCondition</span> <span class="n">condition</span><span class="o">);</span> <span class="c1">// 동적 쿼리</span>
    <span class="nc">Page</span><span class="o">&lt;</span><span class="nc">UserDto</span><span class="o">&gt;</span> <span class="nf">searchPage</span><span class="o">(</span><span class="nc">UserSearchCondition</span> <span class="n">condition</span><span class="o">,</span> <span class="nc">Pageable</span> <span class="n">pageable</span><span class="o">);</span> <span class="c1">// 페이징 쿼리</span>
<span class="o">}</span>

<span class="cm">/* UserSearchCondition.java */</span>
<span class="nd">@Data</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserSearchCondition</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">String</span> <span class="n">roomName</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">ageGoe</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Integer</span> <span class="n">ageLoe</span><span class="o">;</span>

<span class="o">}</span>
</code></pre></div></div>

<p>위에서 예제로 설명한 <code class="language-plaintext highlighter-rouge">Querydsl</code> 의 다양한 쿼리들을 <code class="language-plaintext highlighter-rouge">DataJPA</code> 에 적용할 수 있게 되었다.</p>

<p>이때 주의할 점은 <code class="language-plaintext highlighter-rouge">DataJPA</code> 의 커스텀 클래스의 네이밍을 아래와 같이 맞추어줘야 인식이 가능하다는 점이다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 반드시 'DataJPA 인터페이스 클래스 이름' + Impl 로 네이밍을 해야함!</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserRepositoryImpl</span> <span class="kd">implements</span> <span class="nc">UserRepositoryCustom</span> <span class="o">{</span>

	<span class="cm">/* Querydsl을 이용하여 커스텀 쿼리 구현 */</span>

<span class="o">}</span>
</code></pre></div></div>

<p>그 다음부터는 스프링 컨테이너를 통해 주입받은 동일한 <code class="language-plaintext highlighter-rouge">DataJPA</code> 객체를 사용하여 <code class="language-plaintext highlighter-rouge">Querydsl</code> 의 커스텀 쿼리들을 사용할 수 있다.</p>

<h1 id="마치며">마치며</h1>

<p><code class="language-plaintext highlighter-rouge">DataJPA</code> 를 사용하면서 메소드 네이밍으로는 구현할 수 없는 복잡한 쿼리들을 <code class="language-plaintext highlighter-rouge">JPQL</code> 없이도 모두 자바 코드로 사용할 수 있는 점이 매우 편리한 것 같다. 물론 <code class="language-plaintext highlighter-rouge">JPA</code> 의 기능을 그대로 이어받았기 때문에 인라인 뷰를 사용할 수 없거나 복잡한 그룹에 대해 제약사항이 있기는 하다. 하지만 이런 문제들은 <code class="language-plaintext highlighter-rouge">Querydsl</code> 의 문제라고 할 수는 없으며 복잡한 쿼리를 여러 개의 쿼리로 분리하여 사용하거나 네이티브 쿼리를 작성하는 대체 방법들을 사용하면 된다.</p>

<p><code class="language-plaintext highlighter-rouge">Querydsl</code> 을 통해 메소드 체이닝으로 쿼리문을 하나하나 작성하다 보면 그동안 <code class="language-plaintext highlighter-rouge">JPQL</code> 의 문자열들을 다룰 때 얼마나 귀찮은 점이 많았는지를 생각하게 되며 자바로 쿼리문을 작성하는 진정한 재미를 느낄 수 있었다!</p>

<blockquote>
  <p><strong>모든 코드는 실제 작성하였으며, 직접 실행한 결과들을 글에 담았습니다.</strong> <br />
<strong>내용에 오류가 있을 수 있습니다. 관련 코멘트를 주시면 반영하겠습니다.</strong> <br />
<strong>모든 의견은 언제나 환영합니다 😊</strong></p>
</blockquote>

<h1 id="references">References</h1>

<ul>
  <li><a href="https://querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/" target="_blank">Querydsl reference guide 한국어 번역 (v4.0.0)</a></li>
  <li><a href="https://querydsl.com/static/querydsl/latest/reference/html/" target="_blank">Querydsl reference guide latest version</a></li>
  <li><a href="https://www.baeldung.com/querydsl-with-jpa-tutorial" target="_blank">Baeldung - A Guide to Querydsl with JPA</a></li>
  <li><a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl" target="_blank">docs.spring.io - Querydsl Extension</a></li>
</ul>]]></content><author><name>최준규</name></author><category term="java" /><category term="java" /><category term="jpa" /><category term="querydsl" /><summary type="html"><![CDATA[개요]]></summary></entry></feed>