<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Trade-Off with Kevin</title>
        <link>https://velog.io/</link>
        <description>Hello, World! \n</description>
        <lastBuildDate>Sat, 25 May 2024 05:53:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Trade-Off with Kevin</title>
            <url>https://velog.velcdn.com/images/kevin_/profile/124c53fc-d8cd-47a9-af59-00dda161bacd/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Trade-Off with Kevin. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kevin_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Oracle] Error : The Network Adapter could not establish the connection]]></title>
            <link>https://velog.io/@kevin_/Oracle-Error-The-Network-Adapter-could-not-establish-the-connection</link>
            <guid>https://velog.io/@kevin_/Oracle-Error-The-Network-Adapter-could-not-establish-the-connection</guid>
            <pubDate>Sat, 25 May 2024 05:53:45 GMT</pubDate>
            <description><![CDATA[<h2 id="😕-서론">😕 서론</h2>
<p><img src="https://velog.velcdn.com/images/kevin_/post/ea66f632-b0c2-41ae-823f-2474c608f85f/image.png" alt=""></p>
<p>리눅스 서버 자체를 재 기동 시켜야 할 경우가 있어 재 기동을 시켰다.</p>
<p>서버에 있는 프로세스들도 마찬가지로 재 기동을 시켜준 후 DB Tool인 DBeaver을 통해서 원격 접속을 하려고 할 때 위 사진과 같이 “<em>The Network Adapter could not establish the connection”</em> 오류가 떴다.</p>
<p>이 때 서버의 인터넷 문제인가 싶어 Ping 명령을 수행했을 때 정상적으로 통신이 이루어졌다.</p>
<p>그래서 아래와 같은 가설들을 세우며 문제를 해결 하고자 하였다.</p>
<br />

<h2 id="😏-본론">😏 본론</h2>
<h3 id="가설1-오라클-리스너의-문제">가설#1, 오라클 리스너의 문제?</h3>
<p><code>오라클 리스너</code>는 네트워크를 이용하여 클라이언트에서 오라클 서버로 연결하기 위한 오라클 네트워크 관리자이다.</p>
<p>오라클 서버에서 리스너를 시작 시켜줘야만 클라이언트들이 접속할 수 있다.</p>
<p>이 때 오라클 리스너가 꺼져있다면 지금 접속이 안되고 있는 것이 설명이 되기 때문에 오라클 시스너에 대해서 먼저 살펴보았다.
<br /></p>
<p>오라클 리스너 상태를 확인 하기 위해서는 아래의 과정을 따라서 확인할 수 있다.</p>
<ol>
<li><code>su - oracle</code></li>
<li><code>lsnrctl status</code></li>
</ol>
<p>이 때 리스너가 꺼져있다면 리스너가 없다는 문구가 뜨거나 Service의 상태가 READY가 아닐 것이다.</p>
<p>그러나 나는 이 때 리스너가 정상적으로 동작 중이며, Service의 상태가 READY로 표시 되었다.</p>
<p>혹시 오라클 리스너 설정 파일에서 IP가 재부팅시 변경 되었는지를 확인 하기 위해서 listener.ora와 tnsnames.ora 파일을 살펴 보았는데 정상적으로 IP가 기입 되어있었다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/c3c3a41a-2c08-48cb-af70-72534b00472d/image.png" alt=""></p>
<p>listener.ora와 tnsnames.ora 파일의 경로는 lsnrctl status 명령어 실행시에 Listener Parameter File에 대한 경로를 확인하면 찾을 수 있다.</p>
<p>그래서 나는 서버가 재부팅 되었을 때 Oracle의 기본 Port인 1521에 대해 방화벽 설정이 막히게 되어서 통신이 현재 불가한지를 살펴보고자 하였다.
<br /></p>
<h3 id="가설2-방화벽의-문제">가설#2, 방화벽의 문제?</h3>
<p>결과부터 확인하면 방화벽의 문제가 맞았다.</p>
<p>서버가 재 기동 되면서 기존 1521번의 방화벽 설정이 리셋이 되면서 포트가 닫힌걸로 확인된다.</p>
<p>아래의 과정을 따라가며 나는 문제를 해결하였다.
<br /></p>
<ol>
<li><code>firewall-cmd --query-port=1521/tcp</code></li>
</ol>
<p>1521번 포트가 현재 열려있는지의 여부를 확인한다.</p>
<p>만약 열려있으면 yes를 반환하고, 닫혀있으면 no를 반환한다.</p>
<br />


<ol start="2">
<li><code>firewall-cmd --add-port=1521/tcp --permanent</code></li>
</ol>
<p>1521번 포트를 영구적(--permanent)으로 열어놓겠다는 뜻이다.</p>
<p>이 때 영구적이라는 뜻은 변경이 불가하다는 뜻이 아니라, 서버가 재기동 될 시에도 해당 설정은 초기화 되지 않고 지속 된다는 뜻이다.</p>
<br />


<ol start="3">
<li><code>firewall-cmd --reload</code></li>
</ol>
<p>reload 옵션을 통한 업데이트를 통해 해당 설정을 적용 시켜야 한다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/40c4ce6d-4df7-4b62-a5c6-eb5884ae2412/image.png" alt=""></p>
<p>1521번 포트에 대한 방화벽 설정을 열어주고 나니 정상적으로 Conenction이 되었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB VIEW]]></title>
            <link>https://velog.io/@kevin_/DB-VIEW</link>
            <guid>https://velog.io/@kevin_/DB-VIEW</guid>
            <pubDate>Sat, 25 May 2024 05:48:55 GMT</pubDate>
            <description><![CDATA[<h2 id="😘-서론">😘 서론</h2>
<p>기능 수정을 하면서 최근에 DB의 뷰를 다뤄야 하는 일이 있었다.</p>
<p>기존 기능을 유지보수 하면서 DB의 쿼리를 변경 해서 조회 속도를 올리고자 하였는데, 기존 기능에서는 뷰를 통해 조회를 하고 있었다.</p>
<p>정보 처리 기사를 공부할 당시에  이론적으로만 뷰에 대해서 공부를 하였고, 내가 실제로 뷰를 사용 해본적은 없었는데 이번 기회를 통해 뷰에 대해서 알아보고자 한다.</p>
<br />

<h2 id="😳-본론">😳 본론</h2>
<blockquote>
<p><em>뷰는 쿼리를 하나의 테이블처럼 사용할 수 있게 하는 기술이다.</em></p>
</blockquote>
<p>위는 뷰에 대해서 정말 간단하게 요약한 문장이지만, 이게 사실 정답이다.</p>
<p>아래 사용법을 보면 볼수록 정말 간단하고 명료하게 정리가 된 문장이라는 생각이 들 것이다.</p>
<pre><code class="language-sql">SELECT *
FROM EXAMPLE_TABLE et
WHERE 매우 복잡한 쿼리
LEFT JOIN ANOTHER_TABLE at1
LEFT JOIN ANOTHER_TABLE at2
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at4
LEFT JOIN ANOTHER_TABLE at5
LEFT JOIN ANOTHER_TABLE at6;</code></pre>
<p>만약 위 복잡한 쿼리를 미리 뷰로 정의 해두지 않았다면 매번 위 쿼리를 아래와 같이 서브 쿼리 등으로 매번 사용해야 할 것이다.</p>
<pre><code class="language-sql">첫번째 쿼리...
SELECT * 
FROM EX_TABLE1 
WHERE val1 = (
SELECT *
FROM EXAMPLE_TABLE et
WHERE 매우 복잡한 쿼리
LEFT JOIN ANOTHER_TABLE at1
LEFT JOIN ANOTHER_TABLE at2
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at4
LEFT JOIN ANOTHER_TABLE at5
LEFT JOIN ANOTHER_TABLE at6);</code></pre>
<pre><code class="language-sql">두번째 쿼리...
SELECT * 
FROM EX_TABLE2
WHERE val1 = (
SELECT *
FROM EXAMPLE_TABLE et
WHERE 매우 복잡한 쿼리
LEFT JOIN ANOTHER_TABLE at1
LEFT JOIN ANOTHER_TABLE at2
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at4
LEFT JOIN ANOTHER_TABLE at5
LEFT JOIN ANOTHER_TABLE at6);</code></pre>
<pre><code class="language-sql">세번째 쿼리...
SELECT * 
FROM EX_TABLE3 
WHERE val1 = (
SELECT *
FROM EXAMPLE_TABLE et
WHERE 매우 복잡한 쿼리
LEFT JOIN ANOTHER_TABLE at1
LEFT JOIN ANOTHER_TABLE at2
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at3
LEFT JOIN ANOTHER_TABLE at4
LEFT JOIN ANOTHER_TABLE at5
LEFT JOIN ANOTHER_TABLE at6);</code></pre>
<p>이러한 문제를 해결하는데 뷰가 사용될 수 있는데, 이러한 뷰의 특징에는 무엇이 있는지에 대해서 먼저 알아본 후 실제 뷰 문법에 대해서 알아보자.
<br /></p>
<h3 id="뷰의-특징">뷰의 특징</h3>
<p>뷰란 다른 테이블에서 파생된 테이블을 의미하며, 물리적으로 데이터가 저장되는 것이 아니라 논리적으로만 존재한다.</p>
<p>뷰를 사용한 질의 시에는 DBMS의 뷰의 정의를 잘 파악하고 필요시에 재 작성하여 수행해야 한다.</p>
<p>뷰의 장점과 특징에 대해서 그렇다면 정리를 해보자. 
<br /></p>
<p><strong>뷰의 장점은 아래와 같다.</strong></p>
<ol>
<li><strong>독립성</strong> : 테이블 구조가 변경되어도 뷰에서 조회하는 컬럼이나 컬럼명, 테이블 명이 그대로라면, 뷰를 사용하고 있는 응용 프로그램을 변경하지 않아도 된다.</li>
<li><strong>편리성</strong> : 자주 사용되는 복잡한 쿼리를 미리 뷰로 정의시에는 추후 쿼리는 간단한 형태로 표현, 조회가 가능하다.</li>
<li><strong>보안성</strong> : 사용자의 권한에 따라 열람 가능한 데이터를 다르게 할 수 있다.</li>
</ol>
<br />


<p><strong>뷰의 특징은 아래와 같다.</strong></p>
<ol>
<li>생성된 뷰는 또 다른 뷰를 생성하는데 사용될 수 있다.</li>
<li>뷰의 정의는 변경할 수 없고, 수정을 원할시 삭제 후 재생성을 해야 한다.</li>
<li>뷰를 통한 갱신에는 제약이 따른다.<ul>
<li>갱신을 하고자 할 시에 기본적으로 원천 테이블의 기본키가 포함되어야 한다.</li>
</ul>
</li>
<li>원천이 되는 테이블이나 뷰가 삭제되면 이를 기반으로 하는 뷰도 함께 삭제된다.</li>
</ol>
<br />


<p>그렇다면 뷰를 실질적으로 어떻게 생성, 수정,  조회, 삭제 할 수 있을지에 대해서 알아보자.</p>
<br />


<h3 id="뷰-문법">뷰 문법</h3>
<p><em>이 때 문법은 Oracle 문법입니다.</em></p>
<br />


<h3 id="생성-및-수정">생성 및 수정</h3>
<p>뷰는 아래의 문법 통해서 생성, 수정 할 수 있다.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW [뷰 이름] AS [쿼리]</code></pre>
<p>이를 위 예시에 대입해보면 아래와 같다.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW EXAMPLE_VIEW
AS 
SELECT *
FROM EXAMPLE_TABLE et
WHERE 매우 복잡한 쿼리
LEFT JOIN ANOTHER_TABLE at;
LEFT JOIN ANOTHER_TABLE at1;
LEFT JOIN ANOTHER_TABLE at2;
LEFT JOIN ANOTHER_TABLE at3;
LEFT JOIN ANOTHER_TABLE at3;
LEFT JOIN ANOTHER_TABLE at4;
LEFT JOIN ANOTHER_TABLE at5;
LEFT JOIN ANOTHER_TABLE at6;</code></pre>
<p>참고로 뷰는 create or replace를 통해 동일한 이름의 뷰가 이미 있다면 재생성하고, 없다면 생성할 수 있다.</p>
<p>뷰가 별도의 수정이 없는 이유는 뷰의 정의는 변경할 수 없기에 수정을 원한다면 삭제 후 재 생성을 해야 한다. </p>
<blockquote>
<p><em>즉 create or replace를 통해 생성 또는 수정을 할 수 있다.</em></p>
</blockquote>
<br />


<h3 id="조회">조회</h3>
<p>뷰는 아래의 문법 통해서 사용(=조회) 할 수 있다.</p>
<pre><code class="language-sql">SELECT * FROM EXAMPLE_VIEW;</code></pre>
<p>이를 통해서 위 예시에서 봤던 것 처럼 번거로이 복잡한 쿼리를 서브 쿼리등으로 사용 하는 일을 줄일 수 있다.</p>
<p>만약 뷰의 쿼리를 보고 싶다고 한다면 아래와 같이 조회를 하면 된다.</p>
<pre><code class="language-sql">SELECT text FROM USER_VIEWS 
WHERE view_name = &#39;찾고자 하는 뷰 이름&#39;;</code></pre>
<br />


<h3 id="삭제">삭제</h3>
<p>뷰는 아래의 문법 통해서 삭제 할 수 있다.</p>
<pre><code class="language-sql">CREATE OR REPLACE VIEW [뷰 이름] AS [쿼리]</code></pre>
<p>이 때 뷰 삭제 시에는 해당 쿼리에 대한 원천 데이터가 사라지는게 아니라 뷰만 삭제 처리 된다.</p>
<br />


<h3 id="레퍼런스">레퍼런스</h3>
<p><a href="https://as-j.tistory.com/113">[SQL] 뷰(VIEW) 간단 정리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Cursor 기반 페이지네이션이란?]]></title>
            <link>https://velog.io/@kevin_/Cursor-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@kevin_/Cursor-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Wed, 14 Feb 2024 05:36:32 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<p>이번에 회사 코드를 작성하면서, 페이지네이션을 구현해볼 기회가 있었다. </p>
<blockquote>
<p>페이지네이션이란 콘텐츠를 여러 페이지로 나누고, 이전 혹은 다음 페이지로 넘어가거나 특정 페이지로 넘어갈 수 있는 링크를 페이지 상단이나 하단에 배치하는 방법을 의미한다.</p>
</blockquote>
<br />

<p>이러한 페이지네이션은 <code>LIMIT</code>와 <code>OFFSET</code>을 사용하여서, 간단한 방식으로 구현될 수 있다. </p>
<blockquote>
<p><code>LIMIT</code> ← 결과 중에서 적힌 갯수만큼만 가져온다.
<code>OFFSET</code> ← 어디서부터 가져올지</p>
</blockquote>
<pre><code class="language-sql">SELECT *   
FROM EXAMPLE
WHERE model_name LIKE &quot;%modelName%&quot;
LIMIT 0, 10;</code></pre>
<p>위의 쿼리는 내가 페이지네이션을 이용해 조회를 할 때 작성했던 쿼리이다.</p>
<p>위의 방식이 어떤 방식인지에 대해서 먼저 이야기를 해보고, 더 나은 방식은 무엇이 있는지에 대해서 알아보자.
<br /></p>
<h3 id="1-오프셋-기반-페이지네이션">1. 오프셋 기반 페이지네이션</h3>
<pre><code class="language-sql">SELECT *   
FROM EXAMPLE
WHERE model_name LIKE &quot;%modelName%&quot;
LIMIT 0, 10;</code></pre>
<p>오프셋 기반 페이지네이션은 MYSQL 기준으로 <code>OFFSET</code>, <code>LIMIT</code>를 사용한 쿼리를 이용한다. </p>
<blockquote>
<p>첫번째 인자 = <code>OFFSET</code> <em>// 기본값은 0이다.</em>
두번째 인자 =  <code>LIMIT</code></p>
</blockquote>
<p><em>무한 스크롤을 구현할 때는 보통 <code>OFFSET</code>을 변수로 두고 페이지가 넘어갈때마다 정해진 만큼씩 더해주는 방법을 사용한다.</em>
<br /></p>
<p>이 방식을 사용하면 조건절을 거친 <strong>전체 결과를 먼저 가져오고</strong>,  결과 데이터에서 <code>OFFSET</code> 에 명시된 순서부터 LIMIT까지의 데이터만을 가져온다.</p>
<blockquote>
<p>OFFSET은 단순히 레코드를 조회하기 전에 데이터베이스가 건너뛰는 레코드의 수다.</p>
</blockquote>
<p>즉, 요청한 데이터를 바로 조회하는 것이 아니라, 이전에 데이터를 모두 조회하고 그 결과값에서 OFFSET을 조건으로 잘라내는 것이다.
<br /></p>
<p>이 때 큰 문제점이 있는데 바로 <code>OFFSET</code>의 순서를 가진 데이터를 찾기 위해 그 전에 데이터들을 읽어야 하기 때문에 <code>OFFSET</code>이 1억이 되어버리면 <strong>1억개의 데이터를 읽은 후</strong>에 찾아서 가져올 수 있다는 것이다.
<br /></p>
<h3 id="2-커서-기반-페이지네이션">2. 커서 기반 페이지네이션</h3>
<ol>
<li>첫번째 페이지</li>
</ol>
<pre><code class="language-sql">SELECT *   
FROM EXAMPLE
WHERE model_name LIKE &quot;%modelName%&quot;  
LIMIT 10;</code></pre>
<ol start="2">
<li>두번째 페이지</li>
</ol>
<pre><code class="language-sql">SELECT *   
FROM EXAMPLE
WHERE model_name LIKE &quot;%modelName%&quot;  
AND id &gt; 10 
LIMIT 10;</code></pre>
<p>먼저 커서 기반 페이지는 <strong>Cursor</strong> 개념을 사용하고, <strong>사용자에게 응답해준 마지막 데이터의 식별값</strong>을 Cursor로 사용한다.</p>
<p>커서 기반 페이지네이션은 위 오프셋 기반 페이지네이션의 문제점을 해결해준다.
<br />
위 코드를 보면, 첫번째 페이지의 요청으로 처음 데이터부터 10개의 데이터를 사용자에게 응답해주었다. </p>
<p>그리고 이 때 마지막으로 응답해준 데이터의 식별값(id)은 10이며, 해당 10을 Cursor로 사용해서, 2번째 페이지에서는 10의 다음 식별값인 11부터 조건절에 사용해 사용자에게 응답해준다.</p>
<p>이 때 커서 기반 페이지네이션의 장점은 마지막으로 읽은 데이터의 다음 데이터부터 10개를 조회하기 때문에 매번 원하는 데이터 개수만큼만 조회한다는 이점이 있다.
<br />
더 쉽게 이야기하면, 굳이 커서 뒤의 데이터들을 읽을 필요없이, LIMIT 절 만큼 자른다는 것이다.</p>
<p>이는 대용량 데이터일수록 더 효율적인 속도를 보여준다.
<br /></p>
<h3 id="결론">결론</h3>
<p>가장 큰 차이점은 오프셋 기반 페이지네이션은 오프셋에 해당 하는 순서의 데이터를 찾기 위해서 오프셋 이전 데이터들을 읽을 필요가 있고, 커서 기반 페이지네이션은 커서 이후의 데이터들만 찾으면 되기에 이전 데이터들은 읽을 필요가 없다는 것이다. 
<br />
즉 Offset 기반 페이지네이션은 우리가 원하는 데이터가 <strong>‘몇 번째’</strong>에 있다는 데에 집중하고 있다면, 커서 기반 페이지네이션은 우리가 원하는 데이터가 <strong>&#39;어떤 데이터의 다음&#39;</strong>에 있다는데에 집중한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우당탕탕 JDBC Bulk Insert 도입기]]></title>
            <link>https://velog.io/@kevin_/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-JDBC-Bulk-Insert-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@kevin_/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-JDBC-Bulk-Insert-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Tue, 13 Feb 2024 11:45:56 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<p>과거 카카오 테크 캠퍼스에서 활동할 때 Bulk Insert라는 것을 배웠었다. </p>
<p>그동안(학생으로서 프로젝트를 진행할 때)은 대용량으로 Insert를 쿼리를 작성할 일이 없었기 때문에 그다지 와닿지 않는 개념이었다.</p>
<p>많아봐야 한번에 10건 정도의 동시 Insert를 진행했는데 이는 단일 Insert 쿼리로도 충분했었기 때문이다.</p>
<p>이러한 이유로 단순히 개념만 알고있었는데, 우연히 이를 적용하기 좋은 예시를 발견해서 적용시켜보았다.</p>
<p>나는 엑셀에 있는 컬럼들을 DB에 Insert 시키는 기능을 구현하고 있었다.</p>
<p>그러던 중 기존에 작성되어있는 코드를 보게 되었고, 이 코드를 보면서 문제점은 무엇이었고 나는 어떤식으로 리팩토링 했는지에 대해서 말해보겠다.</p>
<br />

<h3 id="기존-코드">기존 코드</h3>
<p><em>아래 코드는 실제 코드가 아니며 편의를 위해 수정된 코드임을 말씀드립니다.</em></p>
<pre><code class="language-java">                for(int cn=0 ; cn&lt;sheetCn ; cn++) {
                    // 해당하는 시트, 행, 셀을 가져온다.
                    XSSFSheet sheet = workbook.getSheetAt(cn);

                    int rows=sheet.getPhysicalNumberOfRows();//행
                    int cells = sheet.getRow(cn).getPhysicalNumberOfCells();//셀                                 

                    for (int r = 1; r &lt; rows; r++) { //0번째 행은 목록이기에 제외
                        row = sheet.getRow(r); // row 가져오기                                  
                        String sql=&quot;INSERT INTO &quot;+TableName+&quot; (NO,PHONE_NO,SN,IMEI,USIM,LOCATION,USIM_NO,MODEL,OPEN_DT,ORDER_NO) VALUES(&#39;&quot;
                                    +NO+&quot;&#39;,&#39;&quot;+PHONE_NO+&quot;&#39;,&#39;&quot;+SN+&quot;&#39;,&#39;&quot;+IMEI+&quot;&#39;,&#39;&quot;+USIM+&quot;&#39;,&#39;&quot;+LOCATION+&quot;&#39;,&#39;&quot;+USIM_NO+&quot;&#39;,&#39;&quot;+MODEL+&quot;&#39;,&#39;&quot;+OPEN_DT+&quot;&#39;,&#39;&quot;+ORDER_NO+&quot;&#39;)&quot;;

                        // 명령어 실행
                        stmt.execute(sql);                            
                        }
                    }
</code></pre>
<p>위 코드를 이야기 해보기전 Bulk Insert에 대해서 모르는 분들이 있을 수도 있기에, 해당 개념을 이야기 해보고 가자.</p>
<p>아래의 단일 쿼리와 달리</p>
<pre><code class="language-sql">INSERT INTO NameTable (col1, col2) VALUES (val11, val12);
INSERT INTO NameTable (col1, col2) VALUES (val21, val22);
INSERT INTO NameTable (col1, col2) VALUES (val31, val32);</code></pre>
<p>Bulk Insert는</p>
<pre><code class="language-sql">INSERT INTO NameTable (col1, col2) VALUES
(val11, val12),
(val21, val22),
(val31, val32);</code></pre>
<p>이런식으로 3개의 단일 쿼리를 작성했던 것과 달리 하나의 쿼리로 묶어서 처리하는 방식이다.
<br /></p>
<p>이제 기본적인 개념을 알았으니 위의 코드를 이야기해보자.</p>
<pre><code class="language-java">for (int r = 1; r &lt; rows; r++) { //0번째 행은 목록이기에 제외

        String sql=&quot;INSERT INTO &quot;+TableName+&quot; (NO,PHONE_NO,SN,IMEI,USIM,LOCATION,USIM_NO,MODEL,OPEN_DT,ORDER_NO) VALUES(&#39;&quot;
                                    +NO+&quot;&#39;,&#39;&quot;+PHONE_NO+&quot;&#39;,&#39;&quot;+SN+&quot;&#39;,&#39;&quot;+IMEI+&quot;&#39;,&#39;&quot;+USIM+&quot;&#39;,&#39;&quot;+LOCATION+&quot;&#39;,&#39;&quot;+USIM_NO+&quot;&#39;,&#39;&quot;+MODEL+&quot;&#39;,&#39;&quot;+OPEN_DT+&quot;&#39;,&#39;&quot;+ORDER_NO+&quot;&#39;)&quot;;

       // 명령어 실행
       stmt.execute(sql);                            
}</code></pre>
<p>코드를 살펴보면 단순히 For문을 돌면서 단일 Insert 쿼리를 날리고 있다. </p>
<p>이러한 코드도 사실 엑셀의 row가 10개 정도된다고 하면, 기존의 내 프로젝트들처럼 단일 쿼리로 작성해도 아무 문제 없을 것이다.
<br /></p>
<p>그러나</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/0d79627b-c608-4438-b15d-a6c6182e2ad4/image.png" alt=""></p>
<p>내가 구현하고자 하는 기능은 한번의 요청에 약 10,000개에 가까운 row들이 Insert 된다. </p>
<p>만약 단일 쿼리로 이러한 엑셀 데이터를 DB에 Insert 하고자 한다면, </p>
<p>데이터 하나 Insert하고, 커밋하고, Connection 닫고 이러한 과정을 불필요하게 반복하게 될 것이고 이에 따라서 이 기능을 사용하는 Client는 오랜 시간을 기다려야하는 불쾌한 경험을 하게 될 것이다.</p>
<p>그래서 나는 이 코드를 Batch Insert 방식으로 리팩토링 하고자 했다.
<br /></p>
<h3 id="현재-코드">현재 코드</h3>
<p><em>아래 코드는 실제 코드가 아니며 편의를 위해 수정된 코드임을 말씀드립니다.</em></p>
<pre><code class="language-java">public class BulkInsertUtil {

    public static void bulkInsert(List&lt;ExcelBulkInsert&gt; dataList) throws Exception {      

        // JDBC 커넥션 얻기
            Connection con = DriverManager.getConnection(GlobalProperties.getProperty(&quot;Globals.Url&quot;), GlobalProperties.getProperty(&quot;Globals.UserName&quot;), GlobalProperties.getProperty(&quot;Globals.Password&quot;)); 

            // Bulk Insert를 위한 쿼리문 작성
      String sql = &quot;INSERT INTO EX_TABLE(a, b, c, d) &quot;
                + &quot;VALUES (?, ?, ?, ?)&quot;;

        // PreparedStatement 객체 미리 생성
        PreparedStatement pstmt = con.prepareStatement(sql);

        // 롤백을 위한 자동 커밋 방지
        con.setAutoCommit(false);

        // 100 단위로 끊기 위한 카운터 변수 선언
        int cnt = 0;

        try {
            for (ExcelBulkInsert data : dataList) {
                // ? 파라미터에 값 주입
                pstmt.setString(1, data.getModelName());
                pstmt.setTimestamp(2, data.getOpeningDate());
                pstmt.setTimestamp(3, data.getDeliveryDate());
                pstmt.setString(4, data.getDestinationCompany());

                // 배치에 추가
                pstmt.addBatch();

                // batch 메모리에 넣은 후 파라미터 클리어
                pstmt.clearParameters();

                // 100개 단위로 배치 실행
                if (cnt % 100 == 0 &amp;&amp; cnt != 0) {
                    pstmt.executeBatch();
                    pstmt.clearBatch();
                }
            }

            // 마지막으로 남은 배치 실행 및 커밋
            pstmt.executeBatch();
            con.commit();

        } catch (Exception e) {
            // 롤백
            con.rollback();
            throw e;
        } finally {
            // 자동 커밋 다시 설정
            con.setAutoCommit(true);
            con.close();
            // PreparedStatement 닫기
            pstmt.close();
        }
    }
}</code></pre>
<p>위 코드가 Batch Insert를 적용한 코드이다.</p>
<p>각 코드들이 어떤 역할을 하는지 주석보다 좀 더 자세하게 이야기해보자.
<br /></p>
<br />


<hr>
<pre><code class="language-java">Connection con = DriverManager.getConnection(GlobalProperties.getProperty(&quot;Globals.Url&quot;), GlobalProperties.getProperty(&quot;Globals.UserName&quot;), GlobalProperties.getProperty(&quot;Globals.Password&quot;)); </code></pre>
<p>먼저 나는 해당 Bulk Insert를 사용하기 위해 JDBC를 사용하였다.</p>
<p>JPA의 .saveAll()을 통해서 Insert 하면 되는거 아닌가라는 생각이 들 수 있겠지만,</p>
<p>JPA에서는 테이블 ID 규칙이 <code>@GeneratedValue(strategy = GenerationType.IDENTITY)</code> 로 되어 있을 경우 Bulk Insert가 아니라 단일 쿼리로 각각 insert를 하기 때문이다.</p>
<p>이러한 이유로 JDBC를 사용하기 위해 DB 정보를 통해서 <code>Connection</code> 객체를 생성하였다.
<br /></p>
<br />


<hr>
<pre><code class="language-java">             // Bulk Insert를 위한 쿼리문 작성
      String sql = &quot;INSERT INTO EX_TABLE(a, b, c, d) &quot;
                + &quot;VALUES (?, ?, ?, ?)&quot;;

        // PreparedStatement 객체 미리 생성
        PreparedStatement pstmt = con.prepareStatement(sql);</code></pre>
<p>또한 JDBC 쿼리를 String 문자열로 작성을 하였고, <code>PreparedStatement</code>를 통해서 <strong>동적인 쿼리</strong>를 작성하고자 했다.</p>
<p><code>PreparedStatement</code>에 대해서 간단하게 개념을 말해보겠다.</p>
<p>기존 String 문자열 만을 사용해 쿼리를 작성할 때는 문자열에 ?를 작성하지 못하고, 직접 변수를 선언해주어야 했다.</p>
<p>이게 무슨 말이냐면 </p>
<pre><code class="language-java">String sql = &quot;INSERT INTO EX_TABLE(a, b, c, d) VALUES (&quot; + a, &quot;, &quot;, + b, &quot;, &quot; + c + &quot;, &quot; + d)&quot;;</code></pre>
<p>원래는 위와같이 직접 문자열에 변수를 지정했어야 했다…</p>
<p>그러나<code>PreparedStatement</code> 의 도입으로 위 코드와 같이 동적 쿼리에서 변수가 들어갈 곳을 ?로 작성하고, 추후 set메서드를 통해서 주입하는 방식으로 작성이 가능해졌다.</p>
<p>이는 동적 쿼리를 작성하는데 있어서 굉장히 편리하고, 유지보수가 뛰어나게 작성을 하는데 도움을 준다.
<br /></p>
<br />


<hr>
<pre><code class="language-java">// 롤백을 위한 자동 커밋 방지
con.setAutoCommit(false);</code></pre>
<p>기본적으로 JDBC API의 Connection 객체는 setAutoCommit이라는 메서드가 true가 기본값으로 되어있다.</p>
<p>이 때 setAutoCommit이 true가 의미하는 바는 하나의 쿼리당 자동 커밋 시작 밑 자동 커밋이 일어난다는 의미이다.</p>
<p>그러나 우리는 Bulk Insert를 통해 한번에 쿼리를 날리려고 하는 것이기에 해당 설정을 통해서 false로 지정을 해 직접 <code>Commit</code> 및 <code>Rollback</code>을 적는다.
<br /></p>
<br />


<hr>
<pre><code class="language-java"> try {
            for (ExcelBulkInsert data : dataList) {
                // ? 파라미터에 값 주입
                pstmt.setString(1, data.getModelName());
                pstmt.setTimestamp(2, data.getOpeningDate());
                pstmt.setTimestamp(3, data.getDeliveryDate());
                pstmt.setString(4, data.getDestinationCompany());

                // 배치에 추가
                pstmt.addBatch();

                // batch 메모리에 넣은 후 파라미터 클리어
                pstmt.clearParameters();

                                // 100개 단위로 배치 실행
                if (cnt % 100 == 0 &amp;&amp; cnt != 0) {
                    pstmt.executeBatch();
                    pstmt.clearBatch();
                }
            }

                        // 마지막으로 남은 배치 실행 및 커밋
            pstmt.executeBatch();
            con.commit();
</code></pre>
<p>먼저 SQL 관련해서 트랜잭션 및 여러 예외가 생길 수 있기에 try 문으로 감싸두었다.</p>
<p><em>현재는 Exception으로 예외를 묶어두었는데, 이는 좋은 방향은 아니기에 더 세밀하게 리팩토링 해 나갈 계획이다.</em></p>
<p>그리고 위에서 이야기한 것처럼 <code>?</code> 인자에 변수를 설정해주는데 이 때 set Method의 첫인자는 해당 변수가 들어갈 ?의 순서이고, 두번째 인자는 첫번째 인자 순서의 <code>?</code>에 들어갈 변수이다. </p>
<p><code>?</code> 인자들에 대한 설정을 마치면, 해당 SQL문을 배치에 추가해주어 Batch 메모리에 넣어준다.</p>
<p>그 후 <code>?</code> 파라미터들에 대해서 초기화를 해서 다음 for문에서 인자가 겹치는 문제를 방지한다.</p>
<p>그리고 한번에 모든 Batch들을 Insert 하는 것 또한 많은 리소스가 소모되므로, 100개 단위로 끊어서 Batch를 실행하고, Batch 메모리를 초기화 해준다.</p>
<p>이 때 100개 미만의 Batch가 남아있는 경우를 대비해서도 남은 Batch를 실행하고, 커밋해준다.
<br /></p>
<br />


<hr>
<pre><code class="language-java">       } catch (Exception e) {
          // 롤백
            con.rollback();
          throw e;
        } finally {
          // 자동 커밋 다시 설정
            con.setAutoCommit(true);
            con.close();
          // PreparedStatement 닫기
          pstmt.close();
        }
    }
}</code></pre>
<p>만약 예외가 발생할 경우에 rollback을 진행한다.</p>
<p>그리고 finally 문을 통해서 최종적으로 setAutoCommit을 다시 true 기본값으로 변경해주고, Connection과 PreparedStatement 를 닫아준다.</p>
<p><em>이 때 열었던 순서대로 닫아주어야 한다.</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@DateTimeFormat을 이용한 날짜 포매팅]]></title>
            <link>https://velog.io/@kevin_/DateTimeFormat%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%82%A0%EC%A7%9C-%ED%8F%AC%EB%A7%A4%ED%8C%85</link>
            <guid>https://velog.io/@kevin_/DateTimeFormat%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%82%A0%EC%A7%9C-%ED%8F%AC%EB%A7%A4%ED%8C%85</guid>
            <pubDate>Mon, 12 Feb 2024 11:14:48 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<pre><code class="language-java">@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InsertExcelVO {

    private String modelName;

    @DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)    
    private Date openingDate; 

    @DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)    
    private Date deliveryDate;</code></pre>
<p>위 코드는 원본의 코드를 각색한 코드이다.</p>
<p>나는 현재 Excel의 행들을 DB 스키마에 맞춰서 매핑시켜 넣어주는 기능을 구현 중에 있다.</p>
<p>이 때 나는 엑셀에 있는 날짜 데이터들을 DB Date 타입에 맞춰서 넣어주려고 하는데, 이 때 중간에서 타입을 변환 시켜주기 위해서 <code>@DateTimeFormat</code>이라는 어노테이션을 사용하였다.</p>
<p><code>@DateTimeFormat</code>이 어떤 역할을 하길래 나는 사용했을까?</p>
<br />

<h3 id="datetimeformat이란">@DateTimeFormat이란?</h3>
<pre><code class="language-java">/**
 * Declares that a field should be formatted as a date time.
 *
 * &lt;p&gt;Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
 * Can be applied to {@code java.util.Date}, {@code java.util.Calendar}, {@code java.long.Long},
 * Joda-Time value types; and as of Spring 4 and JDK 8, to JSR-310 &lt;code&gt;java.time&lt;/code&gt; types too.
 *
 * &lt;p&gt;For style-based formatting, set the {@link #style()} attribute to be the style pattern code.
 * The first character of the code is the date style, and the second character is the time style.
 * Specify a character of &#39;S&#39; for short style, &#39;M&#39; for medium, &#39;L&#39; for long, and &#39;F&#39; for full.
 * A date or time may be omitted by specifying the style character &#39;-&#39;.
 *
 * &lt;p&gt;For ISO-based formatting, set the {@link #iso()} attribute to be the desired {@link ISO} format,
 * such as {@link ISO#DATE}. For custom formatting, set the {@link #pattern()} attribute to be the
 * DateTime pattern, such as {@code yyyy/MM/dd hh:mm:ss a}.
 *
 * &lt;p&gt;Each attribute is mutually exclusive, so only set one attribute per annotation instance
 * (the one most convenient one for your formatting needs).
 * When the pattern attribute is specified, it takes precedence over both the style and ISO attribute.
 * When the iso attribute is specified, if takes precedence over the style attribute.
 * When no annotation attributes are specified, the default format applied is style-based
 * with a style code of &#39;SS&#39; (short date, short time).
 *
 * @author Keith Donald
 * @author Juergen Hoeller
 * @since 3.0
 * @see org.joda.time.format.DateTimeFormat
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DateTimeFormat {

    /**
     * The style pattern to use to format the field.
     * &lt;p&gt;Defaults to &#39;SS&#39; for short date time. Set this attribute when you wish to format
     * your field in accordance with a common style other than the default style.
     */
    String style() default &quot;SS&quot;;

    /**
     * The ISO pattern to use to format the field.
     * The possible ISO patterns are defined in the {@link ISO} enum.
     * &lt;p&gt;Defaults to {@link ISO#NONE}, indicating this attribute should be ignored.
     * Set this attribute when you wish to format your field in accordance with an ISO format.
     */
    ISO iso() default ISO.NONE;

    /**
     * The custom pattern to use to format the field.
     * &lt;p&gt;Defaults to empty String, indicating no custom pattern String has been specified.
     * Set this attribute when you wish to format your field in accordance with a custom
     * date time pattern not represented by a style or ISO format.
     */
    String pattern() default &quot;&quot;;

    /**
     * Common ISO date time format patterns.
     */
    public enum ISO {

        /**
         * The most common ISO Date Format {@code yyyy-MM-dd},
         * e.g. 2000-10-31.
         */
        DATE,

        /**
         * The most common ISO Time Format {@code HH:mm:ss.SSSZ},
         * e.g. 01:30:00.000-05:00.
         */
        TIME,

        /**
         * The most common ISO DateTime Format {@code yyyy-MM-dd&#39;T&#39;HH:mm:ss.SSSZ},
         * e.g. 2000-10-31 01:30:00.000-05:00.
         * &lt;p&gt;This is the default if no annotation value is specified.
         */
        DATE_TIME,

        /**
         * Indicates that no ISO-based format pattern should be applied.
         */
        NONE
    }

}</code></pre>
<p>위 코드는 <code>@DateTimeFormat</code> 어노테이션의 코드이다.</p>
<br />

<p>위 설명을 조금 간략화 시켜보면 다음과 같다.</p>
<ul>
<li>Spring에서 지원하는 어노테이션이다.</li>
<li>이 어노테이션은 필드의 형식이 날짜 시간으로 지정되어야 함을 선언할 수 있게 해준다.</li>
<li>날짜 시간 패턴이나 사용자 정의 패턴을 통한 형식을 지원한다.</li>
</ul>
<br />

<p>위 설명이 무슨 말인지 더 쉽게 풀어가보면 아래와 같다.</p>
<br />

<p><code>@DateTimeFormat</code> 어노테이션은 스프링 프레임워크에서 날짜와 시간을 나타내는 문자열을 자바의 <code>java.util.Date</code>, <code>java.util.Calendar</code>, <code>java.time.LocalDate</code>, <code>java.time.LocalTime</code>, <code>java.time.LocalDateTime</code> 등과 같은 날짜 및 시간 객체로 변환할 <del>~</del>때 사용된다.</p>
<br />

<p>내부적으로 <code>@DateTimeFormat</code> 어노테이션은 문자열을 해당 날짜 또는 시간 객체로 변환하는 데 사용되는 포맷을 지정한다. </p>
<p>즉, 이 어노테이션을 사용하여 속성에 적용된 문자열을 해당하는 자바 날짜 또는 시간 객체로 변환할 때 사용할 포맷을 지정할 수 있다.</p>
<br />

<p>예를 들어, 다음과 같이 <code>@DateTimeFormat</code> 어노테이션을 사용하여 문자열을 날짜 객체로 변환하는 방법을 지정할 수 있다.</p>
<pre><code class="language-java">@DateTimeFormat(pattern = &quot;yyyy-MM-dd&quot;)
private Date startDate;</code></pre>
<p>위 예제에서는 <code>startDate</code> 필드의 값이 <code>&quot;yyyy-MM-dd&quot;</code> 형식의 문자열로 제공되어야 하며, 이 문자열은 <code>java.util.Date</code> 객체로 자동 변환된다.</p>
<br />

<h3 id="주의점">주의점</h3>
<p>그러나 <code>@DateTimeFormat</code> 을 사용할 때 중요한 점이 있다.</p>
<p><code>@DateTimeFormat</code> 은 @RequestBody나 @ResponseBody에서는 동작하지 않는다.</p>
<p>그 이유는 @RequestBody나 @ResponseBody 어노테이션은 Jackson 라이브러리가 사용된다.</p>
<p>Jackson 라이브러리는 Java에서 제공하는 JSON 파싱 라이브러리이다.</p>
<br />

<p>여기서 눈치가 빠른 사람들은 알았겠지만, 위 @DateTimeFormat은 Spring 어노테이션이다.</p>
<p>Spring에서 Jackson을 의존하기에 Spring은 Jackson을 알고 있지만, Jackson은 Spring을 알지 못한다.</p>
<p>그렇기에 Jackson 라이브러리가 사용되는 @RequestBody나 @ResponseBody를 이용한 역/직렬화에서는 작동하지 않는 것이다.</p>
<p>그러나 내 코드에서 <code>@DateTimeFormat</code> 이 잘 작동했던 이유는 <code>@ModelAttribute</code>나 <code>@RequestParam</code>에서는 Jackson 라이브러리가 동작하지 않기 때문이다.</p>
<br />

<p>그렇다면 @RequestBody나 @ResponseBody에서는 어떻게 날짜를 역/직렬화 하면 좋을까??</p>
<pre><code class="language-java">@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InsertExcelVO {

    private String modelName;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = &quot;dd-MMM-yyyy&quot;, timezone = &quot;Asia/Seoul&quot;)
    private Date openingDate; </code></pre>
<ul>
<li>이 때는 위와 같이 코드를 작성하면 된다.</li>
</ul>
<p><em>추가적으로 Spring에서는 기본으로 사용하는 날짜 패턴(yyyy-MM-ddTHH:mm:ss)의 경우 별도의 어노테이션을 달지 않아도 자동으로 바인딩(==역직렬화)된다.</em></p>
<br />

<h3 id="결론">결론</h3>
<ol>
<li>@RequestBody, @ResponseBody : @JsonFormat 사용하자</li>
<li>@RequestParam, @ModelAttribute : @DateTimeFormat 사용하자</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 @Autowired 대신에 @Resource를 쓸까?]]></title>
            <link>https://velog.io/@kevin_/Autowired-%EB%8C%80%EC%8B%A0%EC%97%90-Resource%EB%A5%BC-%EC%99%9C-%EC%93%B8%EA%B9%8C</link>
            <guid>https://velog.io/@kevin_/Autowired-%EB%8C%80%EC%8B%A0%EC%97%90-Resource%EB%A5%BC-%EC%99%9C-%EC%93%B8%EA%B9%8C</guid>
            <pubDate>Wed, 07 Feb 2024 07:23:33 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">@Controller
public class DeviceController {

  @Resource(name = &quot;XXXService&quot;)
    protected XxxService xxxService;</code></pre>
<br />
이번 글도 회사 코드를 보다가 생긴 궁금증을 풀어가기 위한 글이다.
<br />

<p>의존성을 주입하는 코드들 중 기존 <code>@Autowired</code>나 생성자 주입이 아닌 <code>@Resource</code> 라는 어노테이션을 사용해서 의존성을 주입하는 코드가 있었다.</p>
<p>의존성을 주입하는 코드라는 것은 웹 서치를 통해 알게되었는데, 여기서 궁금한 것은 왜 굳이 <code>@Autowired</code>를 두고, <code>@Resource</code>를 사용했는지이다.</p>
<p>그러면 먼저 둘의 가장 핵심적인 차이점에 대해서 간략하게 알아보자.
<br /></p>
<h3 id="autowired">@Autowired</h3>
<p>먼저 <code>@Autowired</code>는 의존 객체의 타입을 기준으로 Bean 객체를 선택한다. </p>
<p>또한 <strong>Spring</strong> 전용 어노테이션이다. 즉 <code>@Autowired</code> 을 사용하기 워해서는 Spring이 전제가 되어야 한다.</p>
<p><code>Autowired</code> 의 동작 순서는 아래와 같이 동작한다.</p>
<ol>
<li>Spring 컨테이너는 <strong>`@</strong>Autowired`가 적용된 필드, 생성자 또는 메서드 파라미터의 타입에 해당하는 <strong>빈(Bean)을 검색</strong>한다.</li>
<li>일치하는 빈이 하나만 있는 경우, 해당 빈을 <strong>주입한다.</strong> 하지만 여러 개의 빈이 있는 경우, 타입에 따라 자동으로 선택될 수 있다. 다음 조건에 따라 주입될 빈이 결정된다<ul>
<li>타입이 일치하는 빈이 하나만 존재하는 경우, 해당 빈이 주입된다.</li>
<li>타입이 일치하는 빈이 여러 개 존재하는 경우, Spring은 해당 타입의 빈 중에서 <code>@Primary</code> 어노테이션이 지정된 빈을 우선적으로 선택한다.<ul>
<li>이 때 만약 <code>@Primary</code> 어노테이션이 지정되지 않은 경우, 예외가 발생한다.</li>
</ul>
</li>
<li><code>@Qualifier</code> 어노테이션을 사용하여 명시적으로 주입할 빈을 지정할 수 있다.</li>
</ul>
</li>
<li>선택된 빈이 존재하고 주입될 <strong>빈이 필요한 객체의 생성 또는 초기화 시점에 해당 빈이 생성되고 초기화</strong>된다. 이 과정에서 해당 빈의 의존성도 재귀적으로 주입된다.</li>
<li>주입된 빈은 <code>@Autowired</code>가 적용된 필드, 생성자 또는 메서드 파라미터에 주입되어 <strong>사용</strong>된다.<br />

</li>
</ol>
<h3 id="resource">@Resource</h3>
<p> <code>@Resource</code>는 이름을 기준으로 Bean 객체를 선택한다. </p>
<p>또한 Java Specification Request 스펙의 일부로 <strong>Java</strong>에서 제공하는 어노테이션이다.</p>
<p>그렇기에 Spring이 아닌 다른 Java EE 컨테이너에서도 사용할 수 있다.</p>
<p><code>Resource</code>의 동작 순서는 아래와 같이 동작한다.</p>
<ol>
<li>name 속성에 지정된 빈 객체를 찾는다.</li>
<li>name 속성이 없을 경우 동일한 타입을 갖는 빈 객체를 찾는다.<ol>
<li>이 때 동일한 타입을 갖는 빈 객체가 두 개 이상이면, 같은 이름을 가진 빈 객체를 찾는다.</li>
<li>이 때 동일한 타입을 갖는 빈 객체가 두 개 이상이고, 같은 이름을 가진 빈 객체가 없는 경우 <code>@Qualifier</code>를 이용해서 주입할 빈 객체를 찾는다.<br />
<br />

</li>
</ol>
</li>
</ol>
<p>위 차이점에서부터 알 분들은 다 아셨겠지만, <code>@Resource</code>를 사용하는 이유는 Spring 프레임워크에서 벗어나, Java EE 컨테이너나 Spring과 연동되는 경우에는, <code>@Resource</code>를 사용하는 것이 더 적절할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eureka란?]]></title>
            <link>https://velog.io/@kevin_/Eureka%EB%9E%80</link>
            <guid>https://velog.io/@kevin_/Eureka%EB%9E%80</guid>
            <pubDate>Mon, 05 Feb 2024 00:13:37 GMT</pubDate>
            <description><![CDATA[<p>회사 코드를 살펴보던 중 SpringBootApplication 위에 <code>@Eureka</code> 라는 어노테이션을 발견하게 되었다.</p>
<p>처음 보는 어노테이션이자 용어였기에, 공부를 진행해보고자 한다.</p>
<p>Eureka가 무엇인지 알기 이전에 먼저 LB(Load Balancing)에 대해서 알아야 한다.</p>
<p>LB(Load Balancer)란 여러대의 서버에 트래픽을 골고루 분산하기 위해 사용하는 기술이다.
<br /></p>
<p>LB가 어떻게 트래픽이 들어왔을 때 특정 Server에 트래픽을 배정할 수 있을까?</p>
<p>→ LB는 MSA의 각 모듈에 대한 연결 정보(ip, port, hostname)을 알고있다. 그렇기에 우리는 각 모듈의 연결 정보를 LB에 등록해야한다.</p>
<p>→ 그런데 각 모듈이 업데이트됨에 따라 연결 정보 또한 업데이트 된다.</p>
<p>→ 만약 모듈이 100개가 넘어간다고 해보자. 그러면 각 모듈 업데이트시마다 LB의 연결 정보를 매번 업데이트 시켜주어야 할 것이다.
<br />
이를 해결하기 위해 Eureka가 등장했다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/bd2c1969-d259-41c7-b653-eef5191cb12c/image.png" alt=""></p>
<p>Eureka란 넷플릭스에서 제공한 MSA를 위한 클라우드 오픈 소스이다.
<br /></p>
<p>Eureka는 Eureka Server와 Eureka Client로 구성된다.</p>
<p>→ <strong>Eureka Client</strong>는 각 서비스 모듈이라고 생각하자.
<br /></p>
<p>Eureka의 실제 흐름은 아래와 같다.</p>
<ol>
<li><p>Eureka Client 서비스가 시작될 경우에 Eureka Server에 자신의 정보를 등록한다.</p>
</li>
<li><p>Eureka Client는 Eureka Server로부터 다른 Client의 연결 정보가 등록되어 있는 Registry를 받고 자신의 Local에 저장하게 된다.</p>
<p> → <strong>Registry</strong>는 서비스의 연결 정보를 등록하는 것이다.</p>
</li>
<li><p>30초마다 Eureka Server로부터 변경 사항을 갱신 받는다.</p>
</li>
<li><p>30초마다 Eureka Server로부터 변경 사항을 갱신 받는다.</p>
</li>
<li><p>30초마다 ping을 통하여 자신이 동작하고 있다는 신호를 보낸다. 만약 신호를 보내지 못하면 Eureka Server가 보내지 못한 Client를 Registry에서 제외시킨다.</p>
<br />

</li>
</ol>
<p>Spring에서는 <code>@EnableEurekaServer</code> Annotation을 달면 바로 Eureka Server로 사용할 수 있게 된다.</p>
<pre><code class="language-java">@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[복합키를 사용하는 이유]]></title>
            <link>https://velog.io/@kevin_/%EB%B3%B5%ED%95%A9%ED%82%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@kevin_/%EB%B3%B5%ED%95%A9%ED%82%A4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 02 Feb 2024 08:51:33 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-sql">CREATE table TMP_SAMPLE(
    phone_num varchar(255) not null,
    opening_date timestamp,
    insert_date timestamp DEFAULT CURRENT_TIMESTAMP,
    now_date timestamp DEFAULT CURRENT_TIMESTAMP,
    primary key(phone_num, insert_date, opening_date)
) charset = utf8;</code></pre>
<p>복합키란 위 SQL문의 primary key처럼 여러개의 컬럼으로 하나의 Primay Key를 만드는 방법이다. </p>
<br />

<p>이번에 회사에서 복합키를 통해서 SQL문을 작성하였었다.</p>
<br />

<p>중요한 것은 왜 복합키를 통해서 SQL문을 작성하였고, 이 때 장점과 단점은 무엇인지를 생각해내는 것이다.
<br /></p>
<p>먼저 복합키의 단점에 대해서 먼저 알아보자.
<br /></p>
<ol>
<li><p><strong>FK를 다른 테이블과 맺을 때 사이드 이펙트가 크다.</strong></p>
<p> → 위 TMP_SAMPLE과 외래 관계를 맺고 싶을 때 대다수 숫자인 ID에 비해서 복합키에 해당 하는 필드들 모두 해당 테이블에 속해야하는 문제가 있다.</p>
 <br />
</li>
<li><p>인덱스에 좋은 영향을 주지 못한다.</p>
<p> → PK 컬럼은 인덱스가 자동으로 걸리는데, 복합키를 사용하면 복합키 중 하나의 컬럼만을 조회에 사용한다면 이 때 인덱스가 적용되지 않는다. </p>
<p> 위 쿼리에서 예를 들어서 phone_num, insert_date, opening_date 중 insert_date만을 복합키로 사용할 경우에는 인덱스가 적용되지 않을 수 있다.</p>
<p> 정확히는 조건절에 복합키의 순서대로 조합하지 않는다면 PK 인덱스는 타지 않거나 일부만 탄다.</p>
<p> 아래는 조회시에 인덱스가 걸리는 조합들이다.</p>
</li>
</ol>
<pre><code>    phone_num, insert_date, opening_date

    phone_num, insert_date

    phone_num</code></pre><p>   일반적으로 복합키는 <strong>카디널리티</strong>가 높은 곳에서 낮은 순으로 구성하되(= 데이터의 중복이 높은), 조회 / 입력 방법에 따라서 적절히 순서를 바꿔서 구성하거나 새로 인덱스를 추가하면 된다.</p>
<p>   → 카디널리티가 높은 순에서 낮은 순으로 해야 하는 이유는 복합키는 적힌 순서에 따라서 인덱스가 결정되기 때문에, 많이 중복 되면 인덱스의 효과가 덜하기 때문이다.</p>
<p>   <strong>카디널리티</strong> → 특정 데이터 집합의 유니크한 값의 개수</p>
   <br />

<ol start="3">
<li><p>제약 조건 변경시 PK 전체 수정이 필요하다.</p>
<p> → 복합키에 필요한 필드들의 제약조건이 변경된다면 PK 전체 수정이 발생할 수도 있다. 또한 대란 테이블과 FK를 맺고 있다면 또한 해당 테이블의 내용도 모두 바꿔줘야 하는 문제가 생긴다.</p>
</li>
</ol>
<br />

<p>이러한 단점들에도 불구하고 왜 복합키를 사용할까?</p>
<p>→ 만약 테이블에 특정 데이터를 식별하는게 큰 필요가 없고, 단순히 수치와 같은 조회를 위한 통계성 데이터들에 가깝다면 복합키를 사용해봐도 좋다. ID라는 식별자를 별도로 둘 필요도 없고, 복합키를 통한 조회로 PK 인덱스 또한 잘 활용할 수 있다.
<br /></p>
<h3 id="jpa에서의-복합키는">JPA에서의 복합키는?</h3>
<p>JPA에서는 알파벳 순서대로 복합키 순서를 적절히 구성한다. </p>
<p>이러한 방법은 PK를 효율적으로 사용하기 부적합한 방식이다.</p>
<p>위에서 카디널리티를 고려하여서 복합키 순서를 구성해야한다고 이야기했었다. 그러나 알파벳 순서로 복합키 순서를 정해버리면, 이를 고려할 수 없게 되기 때문이다.</p>
<p>이러한 문제를 해결하기 위해 DB에서 직접 복합키를 구성하는 방식을 취할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CrudRepository와 JpaRepository의 차이]]></title>
            <link>https://velog.io/@kevin_/CrudRepository-%EC%99%80-JPARepository%EC%9D%98-%EC%B0%A8%EC%9D%B4</link>
            <guid>https://velog.io/@kevin_/CrudRepository-%EC%99%80-JPARepository%EC%9D%98-%EC%B0%A8%EC%9D%B4</guid>
            <pubDate>Mon, 29 Jan 2024 00:16:36 GMT</pubDate>
            <description><![CDATA[<p>회사 코드를 온보딩 기간 중 살펴보던 중 <code>CrudRepositry</code>를 구현한 <code>Repository</code>가 있어 왜 <code>JpaRepsotiory</code> 대신 사용하셨을까라는 궁금증으로 해당 차이점을 공부하기 시작했다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/kevin_/post/45eec62b-4cf2-45db-ad45-742395b0ee8d/image.png" alt=""></p>
<p>위 사진은 Repository간 상속 계층이다.</p>
<p>일단 <code>CrudRepository</code>와 <code>JpaRepository</code> 둘 다 <code>Repository</code>를 확장한 &#39;&#39;인터페이스&#39;&#39;라는 공통점이 있다.
<br /></p>
<p>대표적인 차이점이라면 <code>CrudRepository</code>는 <code>Repository</code>를 상속받으며 CRUD 기능이 명세되어있다는 것이고, <code>JpaRepository</code>는 이 <code>CrudRepository</code>를 상속한 <code>PagingAndSortingRepository</code>를 상속받았다는 것이다.
<br /></p>
<p>즉 JpaRepository는 기본적인 CRUD 기능과 더불어 페이징 및 정렬 관련 기능 명세도 추가되어있다는 것이다.</p>
<p>자세히 코드를 보면서 살펴보자.
<br /></p>
<h3 id="crudrepository">CrudRepository</h3>
<ul>
<li><p>코드 보기</p>
<pre><code class="language-java">  @NoRepositoryBean
  public interface CrudRepository&lt;T, ID&gt; extends Repository&lt;T, ID&gt; {

      /**
       * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the
       * entity instance completely.
       *
       * @param entity must not be {@literal null}.
       * @return the saved entity; will never be {@literal null}.
       * @throws IllegalArgumentException in case the given {@literal entity} is {@literal null}.
       */
      &lt;S extends T&gt; S save(S entity);

      /**
       * Saves all given entities.
       *
       * @param entities must not be {@literal null} nor must it contain {@literal null}.
       * @return the saved entities; will never be {@literal null}. The returned {@literal Iterable} will have the same size
       *         as the {@literal Iterable} passed as an argument.
       * @throws IllegalArgumentException in case the given {@link Iterable entities} or one of its entities is
       *           {@literal null}.
       */
      &lt;S extends T&gt; Iterable&lt;S&gt; saveAll(Iterable&lt;S&gt; entities);

      /**
       * Retrieves an entity by its id.
       *
       * @param id must not be {@literal null}.
       * @return the entity with the given id or {@literal Optional#empty()} if none found.
       * @throws IllegalArgumentException if {@literal id} is {@literal null}.
       */
      Optional&lt;T&gt; findById(ID id);

      /**
       * Returns whether an entity with the given id exists.
       *
       * @param id must not be {@literal null}.
       * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
       * @throws IllegalArgumentException if {@literal id} is {@literal null}.
       */
      boolean existsById(ID id);

      /**
       * Returns all instances of the type.
       *
       * @return all entities
       */
      Iterable&lt;T&gt; findAll();

      /**
       * Returns all instances of the type {@code T} with the given IDs.
       * &lt;p&gt;
       * If some or all ids are not found, no entities are returned for these IDs.
       * &lt;p&gt;
       * Note that the order of elements in the result is not guaranteed.
       *
       * @param ids must not be {@literal null} nor contain any {@literal null} values.
       * @return guaranteed to be not {@literal null}. The size can be equal or less than the number of given
       *         {@literal ids}.
       * @throws IllegalArgumentException in case the given {@link Iterable ids} or one of its items is {@literal null}.
       */
      Iterable&lt;T&gt; findAllById(Iterable&lt;ID&gt; ids);

      /**
       * Returns the number of entities available.
       *
       * @return the number of entities.
       */
      long count();

      /**
       * Deletes the entity with the given id.
       *
       * @param id must not be {@literal null}.
       * @throws IllegalArgumentException in case the given {@literal id} is {@literal null}
       */
      void deleteById(ID id);

      /**
       * Deletes a given entity.
       *
       * @param entity must not be {@literal null}.
       * @throws IllegalArgumentException in case the given entity is {@literal null}.
       */
      void delete(T entity);

      /**
       * Deletes the given entities.
       *
       * @param entities must not be {@literal null}. Must not contain {@literal null} elements.
       * @throws IllegalArgumentException in case the given {@literal entities} or one of its entities is {@literal null}.
       */
      void deleteAll(Iterable&lt;? extends T&gt; entities);

      /**
       * Deletes all entities managed by the repository.
       */
      void deleteAll();
  }</code></pre>
</li>
<li><p>코드에 나오는 <code>@NoRepositoryBean</code>은 Repository 인터페이스 받았기에, Spring Data JPA나 다른 Repository가 실제 빈을 만들지 않도록 지정하는 어노테이션이다.</p>
<br />

</li>
</ul>
<h3 id="jparepository">JpaRepository</h3>
<ul>
<li><p>코드 보기</p>
<pre><code class="language-java">  @NoRepositoryBean
  public interface JpaRepository&lt;T, ID&gt; extends PagingAndSortingRepository&lt;T, ID&gt;, QueryByExampleExecutor&lt;T&gt; {

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.CrudRepository#findAll()
       */
      @Override
      List&lt;T&gt; findAll();

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
       */
      @Override
      List&lt;T&gt; findAll(Sort sort);

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable)
       */
      @Override
      List&lt;T&gt; findAllById(Iterable&lt;ID&gt; ids);

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable)
       */
      @Override
      &lt;S extends T&gt; List&lt;S&gt; saveAll(Iterable&lt;S&gt; entities);

      /**
       * Flushes all pending changes to the database.
       */
      void flush();

      /**
       * Saves an entity and flushes changes instantly.
       *
       * @param entity entity to be saved. Must not be {@literal null}.
       * @return the saved entity
       */
      &lt;S extends T&gt; S saveAndFlush(S entity);

      /**
       * Saves all entities and flushes changes instantly.
       *
       * @param entities entities to be saved. Must not be {@literal null}.
       * @return the saved entities
       * @since 2.5
       */
      &lt;S extends T&gt; List&lt;S&gt; saveAllAndFlush(Iterable&lt;S&gt; entities);

      /**
       * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
       * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
       * method.
       *
       * @param entities entities to be deleted. Must not be {@literal null}.
       * @deprecated Use {@link #deleteAllInBatch(Iterable)} instead.
       */
      @Deprecated
      default void deleteInBatch(Iterable&lt;T&gt; entities) {
          deleteAllInBatch(entities);
      }

      /**
       * Deletes the given entities in a batch which means it will create a single query. This kind of operation leaves JPAs
       * first level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this
       * method.
       *
       * @param entities entities to be deleted. Must not be {@literal null}.
       * @since 2.5
       */
      void deleteAllInBatch(Iterable&lt;T&gt; entities);

      /**
       * Deletes the entities identified by the given ids using a single query. This kind of operation leaves JPAs first
       * level cache and the database out of sync. Consider flushing the {@link EntityManager} before calling this method.
       *
       * @param ids the ids of the entities to be deleted. Must not be {@literal null}.
       * @since 2.5
       */
      void deleteAllByIdInBatch(Iterable&lt;ID&gt; ids);

      /**
       * Deletes all entities in a batch call.
       */
      void deleteAllInBatch();

      /**
       * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
       * implemented this is very likely to always return an instance and throw an
       * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
       * immediately.
       *
       * @param id must not be {@literal null}.
       * @return a reference to the entity with the given identifier.
       * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
       * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
       */
      @Deprecated
      T getOne(ID id);

      /**
       * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
       * implemented this is very likely to always return an instance and throw an
       * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
       * immediately.
       *
       * @param id must not be {@literal null}.
       * @return a reference to the entity with the given identifier.
       * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
       * @deprecated use {@link JpaRepository#getReferenceById(ID)} instead.
       * @since 2.5
       */
      @Deprecated
      T getById(ID id);

      /**
       * Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is
       * implemented this is very likely to always return an instance and throw an
       * {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers
       * immediately.
       *
       * @param id must not be {@literal null}.
       * @return a reference to the entity with the given identifier.
       * @see EntityManager#getReference(Class, Object) for details on when an exception is thrown.
       * @since 2.7
       */
      T getReferenceById(ID id);

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example)
       */
      @Override
      &lt;S extends T&gt; List&lt;S&gt; findAll(Example&lt;S&gt; example);

      /*
       * (non-Javadoc)
       * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
       */
      @Override
      &lt;S extends T&gt; List&lt;S&gt; findAll(Example&lt;S&gt; example, Sort sort);
  }</code></pre>
</li>
</ul>
<p>위 코드들 중 눈 여겨보아야 할 메서드는 <code>findAll()</code> 메서드이다.</p>
<p><code>CrudRepository</code> 같은 경우에는 반환 타입이 Iterable<T>이며, 별도 정렬은 지원하지 않고 있다.</p>
<p>그러나 <code>PagingAndSortingRepository</code> 을 상속받은 JpaRepository 같은 경우에는 반환 타입이 List<T>이며, Sort 관련 인자를 받아 정렬을 지원하는 걸 볼 수 있다.</p>
<p>여기까지가 두 Repository의 대표적인 차이점 및 특징이며, 궁금증을 해결해보자.
<br /></p>
<h3 id="그러면-왜-crudrepository를-사용했을까">그러면 왜 CrudRepository를 사용했을까?</h3>
<p>나는 그 코드를 작성했던 사람이 아니기에 정확한 이유까지는 알지 못한다. 그러나 내가 해당 주제를 통해 알아보기 위해 찾아봤던 레퍼런스들에서 이야기한 것 중 공감하는 내용들을 유추해볼 수 있다.
<br /></p>
<p>첫번째 이유로는 <strong>시스템의 설계를 간단하게 만들기 위해서</strong>이다.</p>
<p>내가 만약 페이징 및 정렬 기능을 사용하지 않고, 단순한 CRUD 기능만을 필요로 한다고 하자. 그럴 경우에 JpaRepository를 사용할 경우, <strong>불필요한</strong> 페이징 및 정렬 관련 Interface 또한 상속을 받아야 한다.</p>
<p>또 어떤 이들은 이런 아직 사용하지 않는 기능들을 위한 불필요한 상속들을 제하기 위해서 기본 Repository 인터페이스를 상속받는 사람들도 많았다.
<br /></p>
<p>두번째 이유로는 다른 개발자들이 명세를 보고 헷갈리지 않게 하기 위해서이다.</p>
<p>우리 기능이 페이징 및 정렬같은 기능들을 사용하지 않는데, <code>JpaRepository</code>를 상속받아서 사용한다면 다른 개발자들이 페이징 및 정렬의 기능들을 사용한다고 인터페이스 명세를 보고 헷갈릴 수 있기 때문이다.</p>
<p>아래 사진은 CrudRepository를 상속받았을 때이다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/423ca2d9-ea13-447e-9075-d80d71d39e6c/image.png" alt=""></p>
<p>아래 사진은 JpaRepository를 상속받았을 때이다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/fe20280d-033e-4951-9568-c46e44bb635a/image.png" alt=""></p>
<p>위 사진을 보면 같은 메서드라 할지라도 인터페이스의 다른 인자 타입이나 반환 타입으로 인해서 개발자에게 충분히 혼동을 줄 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[빅 엔디안 리틀 엔디안]]></title>
            <link>https://velog.io/@kevin_/%EB%B9%85-%EC%97%94%EB%94%94%EC%95%88-%EB%A6%AC%ED%8B%80-%EC%97%94%EB%94%94%EC%95%88</link>
            <guid>https://velog.io/@kevin_/%EB%B9%85-%EC%97%94%EB%94%94%EC%95%88-%EB%A6%AC%ED%8B%80-%EC%97%94%EB%94%94%EC%95%88</guid>
            <pubDate>Fri, 26 Jan 2024 01:11:15 GMT</pubDate>
            <description><![CDATA[<p>Big Endian and Little Endian</p>
<p>빅 엔디안(Big Endian)과 리틀 엔디안(Little Endian)은 컴퓨터 메모리에서 다중 바이트 데이터를 저장하는 두 가지 주요 바이트 순서(Byte Order) 방식이다. </p>
<br />

<p>예를 들어, 32비트 정수 &quot;0x12345678&quot;을 저장한다는 가정을 들고 설명을 해보겠다.</p>
<br />


<p>먼저 <strong>빅 엔디안 (Big Endian)</strong>은 은 가장 높은(맨 앞의) 바이트부터 저장하는 방식이다.</p>
<p>즉 메모리에는 주소가 낮은 부분부터 차례로 &quot;12 34 56 78&quot;과 같이 저장되는 방식이다.
<br /></p>
<p><strong>리틀 엔디안 (Little Endian)</strong>은 가장 낮은(맨 뒤의) 바이트부터 저장하는 방식이고, 리틀 엔디안 방식으로 저장하면 메모리에는 주소가 낮은 부분부터 &quot;78 56 34 12&quot;와 같이 저장된다.
<br /></p>
<p>이러한 바이트 순서의 차이는 특히 네트워크 통신이나 데이터 교환에서 중요하다. 
<br /></p>
<p>서로 다른 엔디안 방식을 사용하는 시스템 간에는 데이터를 올바르게 해석하기 위해 변환 작업이 필요할 수 있다. 
<br /></p>
<p>예를 들어, 빅 엔디안 시스템에서 생성된 데이터를 리틀 엔디안 시스템으로 전송할 때 데이터를 바이트 순서에 맞게 변환해주어야 한다.
<br /></p>
<p>변환을 해주어야 하는 번거로움이 있기에 네트워크에서는 빅 엔디안으로 통일하도록 되어있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Batch 기본 Task 방식]]></title>
            <link>https://velog.io/@kevin_/Batch-%EA%B8%B0%EB%B3%B8-Task-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@kevin_/Batch-%EA%B8%B0%EB%B3%B8-Task-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Tue, 23 Jan 2024 11:51:17 GMT</pubDate>
            <description><![CDATA[<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-java">@Getter
@Setter
@ToString
@Entity
@Table(name = &quot;pass&quot;)
public class PassEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임합니다. (AUTO_INCREMENT)
    private Integer passSeq;
    private Integer packageSeq;
    private String userId;

    @Enumerated(EnumType.STRING)
    private PassStatus status;
    private Integer remainingCount;

    private LocalDateTime startedAt;
    private LocalDateTime endedAt;
    private LocalDateTime expiredAt;

}</code></pre>
<pre><code class="language-java">@Slf4j
@Component
public class AddPassesTasklet implements Tasklet {
    private final PassRepository passRepository;
    private final BulkPassRepository bulkPassRepository;
    private final UserGroupMappingRepository userGroupMappingRepository;

    public AddPassesTasklet(PassRepository passRepository, BulkPassRepository bulkPassRepository, UserGroupMappingRepository userGroupMappingRepository) {
        this.passRepository = passRepository;
        this.bulkPassRepository = bulkPassRepository;
        this.userGroupMappingRepository = userGroupMappingRepository;
    }

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
        // 이용권 시작 일시 1일 전 user group 내 각 사용자에게 이용권을 추가해줍니다.
        final LocalDateTime startedAt = LocalDateTime.now().minusDays(1);
        final List&lt;BulkPassEntity&gt; bulkPassEntities = bulkPassRepository.findByStatusAndStartedAtGreaterThan(BulkPassStatus.READY, startedAt);

        int count = 0;
        for (BulkPassEntity bulkPassEntity : bulkPassEntities) {
            // user group에 속한 userId들을 조회합니다.
            final List&lt;String&gt; userIds = userGroupMappingRepository.findByUserGroupId(bulkPassEntity.getUserGroupId())
                    .stream().map(UserGroupMappingEntity::getUserId).toList();

            // 각 userId로 이용권을 추가합니다.
            count += addPasses(bulkPassEntity, userIds);
            // pass 추가 이후 상태를 COMPLETED로 업데이트합니다.
            bulkPassEntity.setStatus(BulkPassStatus.COMPLETED);

        }
        log.info(&quot;AddPassesTasklet - execute: 이용권 {}건 추가 완료, startedAt={}&quot;, count, startedAt);
        return RepeatStatus.FINISHED;

    }

    // bulkPass의 정보로 pass 데이터를 생성합니다.
    private int addPasses(BulkPassEntity bulkPassEntity, List&lt;String&gt; userIds) {
        List&lt;PassEntity&gt; passEntities = new ArrayList&lt;&gt;();
        for (String userId : userIds) {
            PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);
            passEntities.add(passEntity);

        }
        return passRepository.saveAll(passEntities).size();

    }

}</code></pre>
<br />
Tasklet은  데이터 처리과정이 tasklet안에서 한번에 이뤄진다.

<br />

<p>배치 처리과정이 쉬운 경우 쉽게 사용되며, 대량처리 경우 더 복잡해질 수 있다.
<br /></p>
<blockquote>
<p>그르니까 소량 데이터일 때 Tasklet을 사용하자.</p>
</blockquote>
<p>현재 사용하고 있는 실무에서도 대용량은 Chunk를 적극 사용하고 있다.
<br /></p>
<p>Tasklet으로 처리하면 전체를 한번에 처리하거나, 수동으로 N개씩 분할할 수 있다.
<br /></p>
<pre><code class="language-java">public interface Tasklet {

    @Nullable
    RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception;

}</code></pre>
<p>tasklet 인터페이스를 사용해서 개발자는 execute 메서드가 repeat status finished를 반환할 때까지 트랜젝션 범위내에서 반복적으로 실행되게 할 수 있다.
<br /></p>
<h3 id="배치-작업-순서">배치 작업 순서</h3>
<ol>
<li><p>Job을 생성한다.</p>
<pre><code class="language-java"> @Bean
     public Job addPassesJob() {
         return this.jobBuilderFactory.get(&quot;addPassesJob&quot;)
                 .start(addPassesStep())
                 .build();
     }</code></pre>
</li>
</ol>
<br />

<ol start="2">
<li><p>Pass를 생성한다.</p>
<pre><code class="language-java"> @Bean
     public Step addPassesStep() {
         return this.stepBuilderFactory.get(&quot;addPassesStep&quot;)
                 .tasklet(addPassesTasklet)
                 .build();
     }</code></pre>
<p> 이 때 Tasklet 구현체를 주입받은후 인자로 넘겨준다.</p>
</li>
</ol>
<br />

<ol start="3">
<li><p>Tasklet 인터페이스를 구현하고, execute 메서드를 오버라이딩</p>
<pre><code class="language-java"> @Override
     public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
         // 이용권 시작 일시 1일 전 user group 내 각 사용자에게 이용권을 추가해줍니다.
         final LocalDateTime startedAt = LocalDateTime.now().minusDays(1);
         final List&lt;BulkPassEntity&gt; bulkPassEntities = bulkPassRepository.findByStatusAndStartedAtGreaterThan(BulkPassStatus.READY, startedAt);

         int count = 0;
         for (BulkPassEntity bulkPassEntity : bulkPassEntities) {
             // user group에 속한 userId들을 조회합니다.
             final List&lt;String&gt; userIds = userGroupMappingRepository.findByUserGroupId(bulkPassEntity.getUserGroupId())
                     .stream().map(UserGroupMappingEntity::getUserId).toList();

             // 각 userId로 이용권을 추가합니다.
             count += addPasses(bulkPassEntity, userIds);
             // pass 추가 이후 상태를 COMPLETED로 업데이트합니다.
             bulkPassEntity.setStatus(BulkPassStatus.COMPLETED);

         }
         log.info(&quot;AddPassesTasklet - execute: 이용권 {}건 추가 완료, startedAt={}&quot;, count, startedAt);
         return RepeatStatus.FINISHED;

     }

         // bulkPass의 정보로 pass 데이터를 생성합니다.
     private int addPasses(BulkPassEntity bulkPassEntity, List&lt;String&gt; userIds) {
         List&lt;PassEntity&gt; passEntities = new ArrayList&lt;&gt;();
         for (String userId : userIds) {
             PassEntity passEntity = PassModelMapper.INSTANCE.toPassEntity(bulkPassEntity, userId);
             passEntities.add(passEntity);

         }
         return passRepository.saveAll(passEntities).size();

     }</code></pre>
 <br />

<p> tasklet 인터페이스를 사용해서 개발자는 execute 메서드가 repeat status finished를 반환할 때까지 트랜젝션 범위내에서 반복적으로 실행되게 할 수 있다. </p>
<p> ← 이 말인 즉슨 메서드 시작부터 <code>return RepeatStatus.FINISHED;</code> 사이의 로직들을 for문등을 통해서 반복적으로 실행되게 할 수 있다는 것이다.</p>
 <br />

<h3 id="test-code">Test Code</h3>
<pre><code class="language-java"> package com.fastcampus.pass.job.pass;

 import com.fastcampus.pass.repository.pass.*;
 import com.fastcampus.pass.repository.user.UserGroupMappingEntity;
 import com.fastcampus.pass.repository.user.UserGroupMappingRepository;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.batch.core.StepContribution;
 import org.springframework.batch.core.scope.context.ChunkContext;
 import org.springframework.batch.repeat.RepeatStatus;

 import java.time.LocalDateTime;
 import java.util.List;

 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;

 @Slf4j
 @ExtendWith(MockitoExtension.class) // JUnit5
 public class AddPassesTaskletTest {
     @Mock
     private StepContribution stepContribution;

     @Mock
     private ChunkContext chunkContext;

     @Mock
     private PassRepository passRepository;

     @Mock
     private BulkPassRepository bulkPassRepository;

     @Mock
     private UserGroupMappingRepository userGroupMappingRepository;

     // @InjectMocks 클래스의 인스턴스를 생성하고 @Mock으로 생성된 객체를 주입합니다.
     @InjectMocks
     private AddPassesTasklet addPassesTasklet;

     @Test
     public void test_execute() {
         // given
         final String userGroupId = &quot;GROUP&quot;;
         final String userId = &quot;A1000000&quot;;
         final Integer packageSeq = 1;
         final Integer count = 10;

         final LocalDateTime now = LocalDateTime.now();

         final BulkPassEntity bulkPassEntity = new BulkPassEntity();
         bulkPassEntity.setPackageSeq(packageSeq);
         bulkPassEntity.setUserGroupId(userGroupId);
         bulkPassEntity.setStatus(BulkPassStatus.READY);
         bulkPassEntity.setCount(count);
         bulkPassEntity.setStartedAt(now);
         bulkPassEntity.setEndedAt(now.plusDays(60));

         final UserGroupMappingEntity userGroupMappingEntity = new UserGroupMappingEntity();
         userGroupMappingEntity.setUserGroupId(userGroupId);
         userGroupMappingEntity.setUserId(userId);

         // when
         when(bulkPassRepository.findByStatusAndStartedAtGreaterThan(eq(BulkPassStatus.READY), any())).thenReturn(List.of(bulkPassEntity));
         when(userGroupMappingRepository.findByUserGroupId(eq(&quot;GROUP&quot;))).thenReturn(List.of(userGroupMappingEntity));

         RepeatStatus repeatStatus = addPassesTasklet.execute(stepContribution, chunkContext);

         // then
         // execute의 return 값인 RepeatStatus 값을 확인합니다.
         assertEquals(RepeatStatus.FINISHED, repeatStatus);

         // 추가된 PassEntity 값을 확인합니다.
         ArgumentCaptor&lt;List&gt; passEntitiesCaptor = ArgumentCaptor.forClass(List.class);
         verify(passRepository, times(1)).saveAll(passEntitiesCaptor.capture());
         final List&lt;PassEntity&gt; passEntities = passEntitiesCaptor.getValue();

         assertEquals(1, passEntities.size());
         final PassEntity passEntity = passEntities.get(0);
         assertEquals(packageSeq, passEntity.getPackageSeq());
         assertEquals(userId, passEntity.getUserId());
         assertEquals(PassStatus.READY, passEntity.getStatus());
         assertEquals(count, passEntity.getRemainingCount());

     }

 }</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[의미 있는 삶이란]]></title>
            <link>https://velog.io/@kevin_/%EC%9D%98%EB%AF%B8-%EC%9E%88%EB%8A%94-%EC%82%B6%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@kevin_/%EC%9D%98%EB%AF%B8-%EC%9E%88%EB%8A%94-%EC%82%B6%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Thu, 18 Jan 2024 15:07:52 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<p>본격적인 글쓰기에 앞서서 나를 간단히 소개를 하자면, 나는 다음주 월요일 첫 출근을 앞둔 평범한 (진)백엔드 개발자이다.</p>
<br />

<p>글을 쓰고 있는 필자는 교회 오빠보다 단순히 과에 한명쯤 있을만한 기독교 신자이며, 갓 대학교를 졸업한 키도 얼굴도 성격도 평범한 그냥 대한민국 평균 남자이다. </p>
<br />


<p>개발도 무척 뛰어나지도, 그렇다고 무척 뒤떨어지지도 않는 실력을 보유 중인 것 같다.</p>
<br />

<p>이렇게 정규분포도의 중간쯤에 정확히 위치하고 있는 나는 직장생활을 앞서서 내가 무엇을 위해 살아야 하는지에 대해서 한번쯤 정리를 하고 싶었다.</p>
<br />

<p>나는 내 스스로 군대 전역 후 개발이라는 것을 접하게 되면서부터 대회, 동아리, 대외활동등 쉼없이 나름대로 열심히 살았다고 생각했고, 대학교 졸업 직전 나름 만족할만한 취업까지 성공하였다.</p>
<br />


<p>이러한 평탄한 인생은 분명히 행복해야만 하고, 축하받아야만 마땅한 나일 것인데 마음 한 곳이 계속 불편했다.</p>
<br />


<p>분명 다른 사람들처럼 잘 살고 있고, 모두가 잘 달리고 있는 도로에서 다른 길로 새지도 않고 쭉 평탄히 직진하고만 있는데 도대체 왜 내 스스로가 이렇게 불편한 것인지 감이 잡히지 않았다.
<br /></p>
<p><em>“내가 무엇을 실수한 걸까?”</em></p>
<p><em>“내가 남들이 다 했던 어떤 것을 놓쳐버린걸까?”</em></p>
<p><em>“모든 사람들이 걷고 있는 길을 내가 벗어난 걸까?”</em></p>
<br />

<p>이런 잡다한 생각들이 들던 중 내가 다니던 교회에서 태국 선교를 간다고 하는 소식을 들었다.
<br /></p>
<p>이러한 생각들을 잊고자, 또 출근 전 하나의 추억을 쌓고자 단순히 지원을 하고, 준비를 하였다.
<br /></p>
<p>우리 선교팀은 태국 선교를 위해서 여러가지 공연들을 준비를 했었는데, 이러한 공연 또한 그 동안 살아왔던대로 <strong>남들과 같이</strong> 열심히 준비를 하였고
<br /></p>
<p>그렇게 2024년 1월 9일 태국으로 떠나게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/d9fa4872-00f5-40b2-a6c9-72f6c2adf6c7/image.jpg" alt=""></p>
<br />

<h2 id="태국에-도착하다-😩">태국에 도착하다. 😩</h2>
<p><img src="https://velog.velcdn.com/images/kevin_/post/fd02628e-014e-4210-b315-c3890a615cf1/image.jpg" alt=""></p>
<p>광주에서 인천까지 버스 4시간, 인천에서 방콕까지 비행기 6시간 종합 10시간 걸려서 태국 방콕 공항에 도착했다.
<br /></p>
<p>태국은 이번이 두번째 방문이다. 
<br /></p>
<p>늘 올 때마다 느끼는 거지만 진짜 🐶덥다.
<br /></p>
<p>진짜로 </p>
<p>🐶</p>
<p>덥</p>
<p>다.
<br /></p>
<p>단순히 덥기만 하면 참을만한데, 진짜 너무 습하다.
<br /></p>
<p>그래도 <strong>남들도 참는데</strong> 나도 참아봐야겠다고 생각했다.
<br /></p>
<p>이대로 방콕에서 콕 하면 좋겠지만, 내가 가는 곳은 방콕에서 차로 2시간정도 더 들어가야하는 빡청이라는 지역이었다.
<br /></p>
<p>스타렉스 같이 생긴 일본차에 몸을 맡겨 오른쪽, 왼쪽 흔들거리면서 가다보니 얼마전 유행했던 노재팬 운동을 내 가슴 깊이 외치고 있었다.
<br /></p>
<p>그렇게 반일 독립 투사가 될 때쯤 어느새 목적지에 도착하여 짐을 풀고, 샤워하고 정신없이 잠에 들었다.
<br />
<br /></p>
<h2 id="태국에서-살아남기">태국에서 살아남기</h2>
<p><img src="https://velog.velcdn.com/images/kevin_/post/e6250cc2-84a6-4aba-abbf-8b64ebc14e7b/image.jpg" alt=""></p>
<p>한국에서 매번 아이폰의 싫증나는 잔소리에 일어났던 그동안과는 다르게 태국에서는 햇살과 치킨들의 소리로 인해서 자연스럽게 눈을 떴다.
<br /></p>
<p>되게 별거 아닌데도 이러한 싱그러움은 날 기분 좋게 만드는 데 충분했다.
<br /></p>
<p>눈을 뜬 후 여러 공연 준비 및 잡다한 준비들을 마치고 처음 준비를 하고, 짬 나는 시간에 틈틈히 사진도 찍었다.
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/920ae1c1-d535-4fc4-9a35-4ddca4b816ae/image.jpg" alt=""></p>
<p>같이 떠난 동생들을 귀찮게 굴며, 사진들을 찍다보니 어느새 출발할 시간이 되었다.
<br /></p>
<p>그렇게 학교에 가서 여러 공연들과 활동들을 성공리에 마쳤다.
<br /></p>
<p><del>사실 실수도 많이 했는데 그냥 아이들이 봐준 것 같다.</del>
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/1c17c33f-a54a-4206-9503-4d34711c2af8/image.JPG" alt=""></p>
<br />

<p>아이들과 사진도 찍고, 놀아도 주고 그렇게 나름대로 <strong>의미 있는 시간</strong>을 보냈다.
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/dc5350c4-a87d-4aef-9a6a-e107e22786a3/image.JPG" alt=""></p>
<br />

<p>그렇게 활동들을 마치고, 아이들과 즐거운 시간을 보내다보니 무언가 잃어버렸던 것이 마음에 일렁거리는 것을 느꼈다.
<br /></p>
<p>이 느낌을 단순히 뿌듯함이라 칭하고 나는 다음날, 다다음날 똑같이 아이들을 섬기고, 챙겨주었다.
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/a7583251-fe0b-4172-84b0-52f4a90137b0/image.jpg" alt=""></p>
<br />

<p>점점 챙겨주면 챙겨줄수록 이 느낌이 단순한 뿌듯함이 아닌 것 같다는 생각이 점차 들기 시작했다.
<br /></p>
<p>이 선교 활동을 통해서 내가 어렸을 때부터 꿈꾸어왔던 삶, 내가 놓쳐버린 아니 어찌보면 놓아버린 무언가를 되찾을 수도 있겠다는 생각이 들었다.
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/c1b5218e-54e3-4ddd-a8db-053dcf3214ea/image.JPG" alt=""></p>
<br />

<p>그렇게 알듯 말듯 시간은 꾸준히 흘러 우리는 어느새 마지막 활동인 수련회 활동만을 앞두고 있었다.
<br /></p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/f5ca2450-9ac8-4686-ab67-ab65c7ec0e69/image.jpg" alt=""></p>
<br />

<p>이 선교의 마지막 활동인 수련회 또한 무난하게 잘 마무리를 해나갔고, 어느덧 마무리하는 시간이 되었다.
<br /></p>
<p>그 때 나와 가장 많은 시간을 함께 보냈던 위 사진에 나온 작디 작은 태국 친구가 내 품에서 눈물을 훔치었다. 
<br /></p>
<p>그 때 처음 들었던 기분은 단순한 당황스러움이었지만, 점차 이 아이에게 나는 <strong>어떤 의미</strong>이기에 이렇게 눈물을 흘리는 걸까라는 생각을 하게 되었다.
<br /></p>
<p>그러면서 여러 복합적인 기분이 들어 나 또한 울음을 참지 못했고,
<br /></p>
<p>그 복잡하고, 얽힌 감정 가운데에서 내가 찾고자 했던 무언가를 찾아낸 기분이었다. 
<br /></p>
<p>이렇게 모든 선교 활동을 마치게 되었다. 
<br /></p>
<p>이번 선교 활동을 통해서 나는 처음 목표인 추억거리 뿐만 아니라 내가 어떻게 살아야 할지, 내가 무엇을 위해 살아야할지, 내가 살고자 하는 인생은 무슨 인생인지에 대해서 한번쯤 생각해보게 된 것 같다.
<br />
<br /></p>
<h2 id="선교를-마치며-😶">선교를 마치며 😶</h2>
<br />

<p>이러한 일들을 겪으며, 문득 아래와 같은 생각을 하게 되었다.
<br /></p>
<p>나는 이 아이들에게 어떤 의미로 다가왔을까?
<br /></p>
<p>돈 주는 사람? 그냥 놀아주는 사람? 외국인?
<br /></p>
<p>뭐 사실 위에 나열한 것들이 사실일 수도 있다. 
<br /></p>
<p>그러나 나는 내 스스로에 대해서 한가지 깨닫게 되었다. 
<br /></p>
<p>나는 이들에게 <strong>의미있는 사람</strong>이 되고 싶었다.
<br /></p>
<p>아니 더 정확히는 내 스스로가 <strong>의미가 있는 삶을 사는 사람</strong>이 되고 싶었던 것 같다.
<br /></p>
<p>그저 고속도로에서 옆에 차들을 의식하며 어떻게든 같은 속도를 유지하기 위해서 직진만 하다 그대로 끝나는 인생이 아니라, 내가 갈 수 있는 속도대로 천천히 가며 호두과자도 팔고, 장난감도 팔고, 노래 부르고 춤추면서 가는 그런 인생을 살고 싶다.
<br /></p>
<p>나는 그토록 <strong>왜 남들과 같은 삶을 살고 싶었을까?</strong>
<br /></p>
<p>무서워서 그랬을지도 모르겠다. 
<br /></p>
<p>고속도로에서 직진이 아니라 다른 길로 돌아가면 낭떠러지일지도 모른다는 무서움에 휩싸였기 때문일지도 모른다. 그렇기에 남들이 가고 있고, 갔었던 안전한 길로 가고 싶었던 것일지도 모른다.
<br /></p>
<p>그렇다면 <strong>나에게 의미있는 삶이란 어떤 삶일까?</strong> 라는 생각을 하게 되었다.
<br /></p>
<p>내가 나름 정의하기에는 나에게 의미있는 삶이란 <strong>하루 하루 의미있게 살아가는 삶</strong>이다.
<br /></p>
<p>재귀도 아니고, 그러면 하루 하루 의미있게 살아간다는 것은 무엇일까?
<br /></p>
<p><strong>내게 주어진 하루를 감사하고, 버텨내고, 맞서서 싸울줄 알고, 사랑하며 살아간다는 것!</strong> 이라고 정의하기로 했다.
<br /></p>
<blockquote>
<p>오늘 하루 이겨내자</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring은 어떻게 구현체를 빈으로 등록할까?]]></title>
            <link>https://velog.io/@kevin_/Spring%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%EC%B2%B4%EB%A5%BC-%EB%B9%88%EC%9C%BC%EB%A1%9C-%EB%93%B1%EB%A1%9D%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@kevin_/Spring%EC%9D%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%EC%B2%B4%EB%A5%BC-%EB%B9%88%EC%9C%BC%EB%A1%9C-%EB%93%B1%EB%A1%9D%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Wed, 03 Jan 2024 03:35:33 GMT</pubDate>
            <description><![CDATA[<p>아래는 해당 글의 설명을 돕기 위한 내가 작성한 코드이다.
<br /></p>
<p><em>SaveMemberUseCase 인터페이스</em></p>
<pre><code class="language-java">public interface SaveMemberUseCase {
    void saveMember(SaveMemberRequestDTO saveMemberRequestDTO);
}</code></pre>
<br />
*SaveMemberUseCase 인터페이스를 구현한 MemberService*

<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<br />

<p><em>SaveMemberUseCase 인터페이스를 구현한 MemberSaveService</em></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberSaveService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<br />

<p><em>SaveMemberUseCase를 의존하는 MemberController</em></p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
@RequiredArgsConstructor
public class MemberController {

    private final SaveMemberUseCase saveMemberUseCase;

    // 회원가입
    @PostMapping(&quot;&quot;)
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.saveMember(requestDTO);
    }
}</code></pre>
<br />

<p>만약 하나의 인퍼페이스를 구현한 2개 이상의 구현체들이 존재하고, 이 구현체들이 모두 빈으로 등록이 되어있다면, 스프링은 어떤 구현체를 DI(주입)해줄 까?
<br /></p>
<p>정답은 아무 설정 없이는 아래와 같은 에러가 발생한다.</p>
<blockquote>
<p><strong>NoUniqueBeanDefinitionException: No qualifying bean of type &#39;test.a&#39; available: expected single matching bean but found 2</strong></p>
</blockquote>
<br />

<p>위 에러의 뜻은 1개의 빈만 매칭이 되어야 하지만 현재 2개 or n개의 빈이 매칭이 되었다는 것을 의미한다. </p>
<p>Spring boot가 어떤 빈을 DI(주입)해야하는지 이 경우에는 반드시 알려주어야 한다.</p>
<p>그러면 어떻게 알려주느냐?</p>
<p>아래와 같은 3가지 방법이 있다.
<br /></p>
<hr>
<h3 id="1-autowired된-field의-이름을-빈-이름으로-설정하는-방법">1. Autowired된 field의 이름을 빈 이름으로 설정하는 방법</h3>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
public class MemberController {

    private final SaveMemberUseCase ***MemberService***;

    // 회원가입
    @PostMapping(&quot;&quot;)
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.saveMember(requestDTO);
    }

        @Autowired
        public MemberController(SaveMemberUsecase saveMemberUsecase) {
            this.memberService = saveMemberUsecase;
        }
}</code></pre>
<p>→ Autowired로 생성자에 주입되는 SaveMemberUseCase 객체의 변수명을 구현체 클래스의 이름인 MemberService로 만들면, SaveMemberUseCase 클래스의 빈이 두 개인 것을 확인한 Spring Boot가 MemberService 이름과 같은 빈이 있는지 찾아서 주입해준다.</p>
<hr>
<br />

<h3 id="2-qualifier-어노테이션을-이용하는-방법">2. @Qualifier 어노테이션을 이용하는 방법</h3>
<pre><code class="language-java">@Qualifier(&quot;member&quot;)
@Service
@RequiredArgsConstructor
public class MemberService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<pre><code class="language-java">@Qualifier(&quot;memberSave&quot;)
@Service
@RequiredArgsConstructor
public class MemberSaveService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
public class MemberController {

    private final SaveMemberUseCase saveMemberUseCase;

    // 회원가입
    @PostMapping(&quot;&quot;)
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.saveMember(requestDTO);
    }

        @Autowired
        public MemberController(@Qualifier(&quot;member&quot;) SaveMemberUsecase saveMemberUsecase) {
            this.saveMemberUseCase = saveMemberUsecase;
        }
}</code></pre>
<p>Qualifier 어노테이션은 빈에 추가 구분자를 붙여주는 방법으로 생성자에서 해당 구분자를 명시하면 그 구분자를 가진 빈을 주입해준다. </p>
<p>→ 이는 빈 이름을 변경하는게 아니라 추가적인 구분자를 두는 것이다.</p>
<p>→ 그렇기에 만약 member라는 구분자를 가진 스프링 빈이 없으면, 해당 이름을 가진 member라는 빈을 찾아다닌다. 그러다가 해당 이름을 가진 빈도 없으면 NoSuchBeanDefinitionException 예외를 반환한다.
<br /></p>
<hr>
<h3 id="3-primary-어노테이션을-이용하는-방법">3. @Primary 어노테이션을 이용하는 방법</h3>
<pre><code class="language-java">@Primary
@Service
@RequiredArgsConstructor
public class MemberService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberSaveService implements SaveMemberUseCase {

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
public class MemberController {

    private final SaveMemberUseCase saveMemberUseCase;

    // 회원가입
    @PostMapping(&quot;&quot;)
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.saveMember(requestDTO);
    }

        @Autowired
        public MemberController(SaveMemberUsecase saveMemberUsecase) {
            this.saveMemberUseCase = saveMemberUsecase;
        }
}</code></pre>
<p>Qualifier 어노테이션은 모든 구현체에 붙여줘야 하기에 상대적으로 불편하다. </p>
<p>그러나 Qualifier 어노테이션과 Primary 어노테이션을 같이 사용하면 시너지를 낼 수 있다.</p>
<p>메인 기능의 빈에는 Primary를 적용하고, 서브 기능으로 사용하는 빈에는 Qualifier를 적용하면 두 장점을 각각 사용할 수 있다.</p>
<p>→ 이 때 우선순위는 Primary보다 명시적으로 지정하는 Qualifier 어노테이션이 우선 순위를 가진다.</p>
<p>→ 스프링은 기본적으로 자동으로 수동으로 지정하는 것이 높은 우선 순위를 갖는다.
<br /></p>
<h3 id="레퍼런스">레퍼런스</h3>
<p> <a href="https://bestinu.tistory.com/58">@Qualifier와 @Primary 어노테이션 사용법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내 입맛대로 헥사고날 아키텍쳐]]></title>
            <link>https://velog.io/@kevin_/%EB%82%B4-%EC%9E%85%EB%A7%9B%EB%8C%80%EB%A1%9C-%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90</link>
            <guid>https://velog.io/@kevin_/%EB%82%B4-%EC%9E%85%EB%A7%9B%EB%8C%80%EB%A1%9C-%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90</guid>
            <pubDate>Tue, 26 Dec 2023 03:01:46 GMT</pubDate>
            <description><![CDATA[<h3 id="요구-사항">요구 사항</h3>
<ol>
<li>유저는 닉네임, 성별, 이메일들을 등록할 수 있다.</li>
<li>유저는 게시글을 작성할 수 있다.</li>
<li>유저는 게시글의 본문을 수정할 수 있다.</li>
<li>유저는 여러 게시글을 가질 수 있다.</li>
<li>게시글은 작성자인 유저 하나를 가질 수 있다.</li>
<li>게시글을 제목, 본문을 가질 수 있다. </li>
</ol>
<br />

<h3 id="참고-레퍼런스">참고 레퍼런스</h3>
<p><img src="https://velog.velcdn.com/images/kevin_/post/8c2c1f65-9df9-4491-a48d-42ec79428831/image.png" alt=""></p>
<p>무의식적으로 코드를 작성하다보니 Entity부터 작성하려는 나를 발견하게 되었다.</p>
<p>DB 중심적인 개발로부터 벗어나기 위해서 <strong>도메인</strong>부터 먼저 작성을 해보자.</p>
<p><em>Member</em></p>
<pre><code class="language-java">@Getter
@Builder
public class Member {

    private Long id;

    private String nickName;

    private String sex;

    private String email;

    public void updateNickname(String nickName){
        this.nickName = nickName;
    }
}</code></pre>
<br />


<p><em>Post</em></p>
<pre><code class="language-java">@Getter
@Builder
public class Post {

    private Long id;

    private String title;

    private String content;

    private Member writer;

    public void updatePost(String content){
        this.content = content;
    }
}</code></pre>
<br />


<p>뭔가 어색할정도로 깔끔하고, 편안해지는 느낌이 든다. </p>
<p>DB, JPA와 관련된 어노테이션이나 로직이 없으니 비즈니스 로직과 도메인에 더 집중이된다.</p>
<p><em>이 참에 어노테이션 선언 순서도 내 나름대로 정했는데, DB에 가까워지거나 더 어노테이션의 개념이 무거워질수록(=객체에 더 큰 영향을 끼치는) 아래에 작성하도록 정하였다.</em></p>
<p>그 다음으로는 엔티티를 작성해보자.
<br /></p>
<p><strong><em>MemberEntity</em></strong></p>
<pre><code class="language-java">@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = &quot;MEMBER&quot;)
@Entity
public class MemberEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String nickName;

    private String sex;

    private String email;
}</code></pre>
<br />


<p><strong><em>PostEntity</em></strong></p>
<pre><code class="language-java">@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Table(name = &quot;POST&quot;)
@Entity
public class PostEntity {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    private String content;

    @ManyToOne
    private MemberEntity writer;

}</code></pre>
<p>엔티티도 마찬가지로 비즈니스 로직 없이, DB, JPA에 관련된 어노테이션, 로직만 있어서 깔끔해보인다.</p>
<p>내가 생각한 도메인과 엔티티를 분리했을 때의 대표적인 장, 단점은 다음과 같다.
<br /></p>
<p><strong>장점</strong></p>
<p>→ 명확한 역할 분리를 할 수 있다.</p>
<ul>
<li>DB 테이블을 생성하고, 영속 계층에서의 엔티티와 비즈니스 로직을 수행하는 도메인의 명확한 역할을 분리를 할 수 있다.</li>
<li>이전에 엔티티가 위 두 책임을 모두 가지고 있었을 때는 객체가 너무 비대해지거나 영속 계층에 비즈니스 로직이 있어서, 비즈니스 계층과 영속 계층이 불필요하게 서로 교류했었다.</li>
<li>이제는 객체의 코드를 줄일 수 있고, 영속 계층은 엔티티가 비즈니스 계층은 도메인이 책임질 수 있다.<br />


</li>
</ul>
<p><strong>단점</strong></p>
<p>→ 생산성이 떨어진다.</p>
<ul>
<li>현재는 정말 작디 작은 프로젝트이지만, 할 일이 2배 가까이 늘어난 느낌이다. 이는  프로젝트의 규모가 커질수록 분리되었을 때의 장점도 돋보이겠지만, 각기 달리 유지보수해야하는 단점도 부각될 것 같다.</li>
</ul>
<p>그 다음으로는 Entity와 도메인, 도메인과 Entity간을 매핑 시켜줄 Mapper를 작성하자. 
<br /></p>
<p><strong><em>MemberMapper</em></strong></p>
<pre><code class="language-java">@Component
public class MemberMapper {

    public MemberEntity toEntity(Member member){
        return MemberEntity.builder()
                .nickName(member.getNickName())
                .email(member.getEmail())
                .sex(member.getSex())
                .build();
    }

    public Member toDomain(MemberEntity memberEntity){
        return Member.builder()
                .id(memberEntity.getId())
                .nickName(memberEntity.getNickName())
                .email(memberEntity.getEmail())
                .sex(memberEntity.getSex())
                .build();
    }
}</code></pre>
<br />


<p><strong><em>PostMapper</em></strong></p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class PostMapper {

    private final MemberMapper memberMapper;

    public Post toDomain(PostEntity postEntity){
        return Post.builder()
                .id(postEntity.getId())
                .title(postEntity.getTitle())
                .content(postEntity.getContent())
                .writer(memberMapper.toDomain(postEntity.getWriter()))
                .build();
    }

    public PostEntity toEntity(Post post){
        return PostEntity.builder()
                .title(post.getTitle())
                .content(post.getContent())
                .writer(memberMapper.toEntity(post.getWriter()))
                .build();
    }
}</code></pre>
<br />


<p>추후 비즈니스 계층에서 DI 받아 사용하기 위해서 <code>@Component</code>를 명시해두었다.
<br /></p>
<p>이제 기본적인 도메인과 엔티티 구현이 끝났으니 Adapter를 구현해야한다.
<br /></p>
<p>Adapter 중 Web으로부터 요청을 받는 in Adapter, 즉 Controller를 먼저 구현해보자.
<br /></p>
<p>Controller를 구현하기 위해서는 먼저 Port(or Usecase)를 먼저 작성해야한다.</p>
<p>여기서는 보통 2가지 방법을 선택하는 것 같다.</p>
<ol>
<li><p>Port 단위에 맞는 크기의 Usecase를 각각 구현하기</p>
</li>
<li><p>Port대신 Usecase들을 인터페이스로 추상화하고, 하나의 Service에서 구현하기</p>
<ol>
<li>단 너무 광범위하게는 말고, ex:Post에 관련된 Usecase들만<br />


</li>
</ol>
</li>
</ol>
<p>내가 찾아본 코드들은 2번 방식이 많은 것 같다.</p>
<p>→ 2번 방식이 1번 방식에 비해서 더 빠르게 개발을 할 수 있어서, 생산성을 더 키울 수 있기에 다들 이런 방식으로 사용하지 않나 싶다.
<br /></p>
<p>구조도를 그려보면 현재 프로젝트는 아래와 같은 구조를 가지고 있다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/2da55014-5b19-4e16-907e-18ce28762754/image.png" alt=""></p>
<p><em>SaveMemberUsecase</em></p>
<pre><code class="language-java">public interface SaveMemberUseCase {

    Member saveMember(SaveMemberRequestDTO saveMemberRequestDTO);

}</code></pre>
<br />


<p>만약 위에서 1번 방식을 택했으면, Usecase는 클래스 네이밍을 통해서 본인의 역할을 충분히 설명하기에 메서드 이름은 단순하게 execute로 지정해도 된다.</p>
<p><em>DTO는 Usecase에 의존적이라고 생각해서, Usecase와 같은 경로에 두었다.</em>
<br /></p>
<p>이제 Adapter를 이어서 작성해보자.</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/api/member&quot;)
@RequiredArgsConstructor
public class MemberController {

    private final SaveMemberUseCase saveMemberUseCase;

    // 회원가입
    @PostMapping(&quot;&quot;)
    void saveMember(@RequestBody SaveMemberRequestDTO requestDTO){
        saveMemberUseCase.execute(requestDTO);
    }
}</code></pre>
<p>이제 useCase를 구현한 MemberService를 구현해보자.
<br /></p>
<p><em>MemberService</em></p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class MemberService implements SaveMemberUseCase {

    private final MemberMapper memberMapper;

    private final SaveMemberPort saveMemberPort;

    @Override
    public void saveMember(SaveMemberRequestDTO saveMemberRequestDTO) {
        saveMemberPort.saveMember(memberMapper.toEntity(saveMemberRequestDTO));
    }
}</code></pre>
<br />


<p>이제 out Port를 구현해보자.
<br /></p>
<p><em>saveMemberPort</em></p>
<pre><code class="language-java">public interface SaveMemberPort {

    void saveMember(Member member);

}</code></pre>
<p>이제 이 Port를 구현한 영속 Adapter를 작성하면 된다.</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class MemberPersistetenceAdapter implements SaveMemberPort {

    private final MemberMapper memberMapper;

    private final MemberSpringDataRepository repository;

    @Override
    public void saveMember(Member member) {
        repository.save(memberMapper.toEntity(member));
    }
}</code></pre>
<br />


<h3 id="내-궁금증들">내 궁금증들</h3>
<ol>
<li><p>Mapper를 별도로 이용한 이유</p>
<ol>
<li>도메인에서 변환을 진행하게 되면 외부에서 도메인까지 접근할 수 있고, 의존을 하게 하기 때문에 영속성 계층에서 별도로 Mapper를 두어서 외부의 일은 외부에서 의존해서 해결하기로 하였다.</li>
</ol>
</li>
<li><p>out 패키지에서 만약 엔티티가 더 많이 생기게 된다면 클래스가 너무 많아지지 않을까? 이 때 다른 개발자들은 어떻게 해결하나?</p>
<ol>
<li>내가 참고했던 레퍼런스들에서는 별도의 레이아웃 정리를 하지 않았다.</li>
</ol>
</li>
</ol>
<pre><code>2. 엔티티, 즉 도메인 별로 패키지를 나누는 것이 아닌 각 객체에 맞게 패키지를 구성한 레퍼런스를 찾았다.
    1. 이 방식으로 구현하면, 계층형 아키텍쳐의 단점을 그대로 또 따라가지 않을까?
    -&gt; Nope, 계층형 아키텍쳐의 주 단점은 도메인 레이어가 영속 계층, 특히 DB를 의존하게 된다면, 도메인 레이어와 애플리케이션 레이어가 변경에 쉽게 영향을 받을 수밖에 없다는 것이다.
    -&gt; 그러나 헥사고날에서는 도메인 레이어가 영속 계층을 의존하지 않기에 이는 성립하지 않는다.

    ![](https://velog.velcdn.com/images/kevin_/post/34074441-d238-43f4-b4b8-467f87c671c6/image.png)</code></pre><p><a href="https://github.com/DEVdongbaek/study_hexagonal/tree/main/hexagonal/src/main/java/com/example/hexagonal">내 예시 코드</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[면접을 준비하면서 뽀짝뽀짝]]></title>
            <link>https://velog.io/@kevin_/%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EB%A9%B4%EC%84%9C-%EB%BD%80%EC%A7%9D%EB%BD%80%EC%A7%9D</link>
            <guid>https://velog.io/@kevin_/%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%A4%80%EB%B9%84%ED%95%98%EB%A9%B4%EC%84%9C-%EB%BD%80%EC%A7%9D%EB%BD%80%EC%A7%9D</guid>
            <pubDate>Sun, 17 Dec 2023 05:32:45 GMT</pubDate>
            <description><![CDATA[<p>현재 나는 면접을 준비하고 있는 대한민국의 흔하디 흔한 신입 취업 준비생이다.</p>
<p>현재 여러곳의 면접을 보았는데, 이러한 면접을 보면서 내가 어떻게 면접을 준비하고, 면접을 본 후에 어떻게 정리하고있는지 넉두리마냥 공유를 해보고자 한다.</p>
<br />

<h3 id="회사-도메인-파악하기">회사 도메인 파악하기</h3>
<p>나는 면접을 준비하면서 해당 회사가 어떠한 도메인을 다루고 있는지, 해당 회사의 웹사이트나 잡플래닛등 다양한 곳에서 인사이트를 찾기 위한 작업을 제일 먼저 하였다.</p>
<p>나는 그 후 해당 회사의 도메인을 파악하고, 내가 이 회사에 취업을 하게 되었을 때 단순히 개발 뿐만 아니라 내가 인생을 사는데 있어서 도움이 될만한 도메인인지를 판단하는 편이다.</p>
<p>회사의 도메인을 파악하였다면, 회사가 해당 도메인을 통해서 주로 어떤 방식과 사업 아이디어로 주로 수입을 내는지도 파악하는게 좋아보인다.</p>
<h3 id="회사에서의-개발-스킬이나--문화-파악하기">회사에서의 개발 스킬이나  문화 파악하기</h3>
<p>이 부분은 조금 어려운 부분이었다. 유명 IT 기업 같은 경우에는 독자적인 개발 블로그를 운영하기도 하고, 자체적인 부트캠프같은 것들을 진행하기도 한다. 이를 통해서 회사의 개발 스킬이나 문화를 파악할 수 있지만, 내가 느끼기로는 대체적으로는 해당 회사의 개발 문화를 알 방법은 없었던 것 같다. 그래서 나는 아래와 같은 방법들을 사용했다. </p>
<br />


<p>*<em>1. 잡코리아등의 취업 공고를 보고 파악하기
*</em>-&gt; 이 방법이 제일 기본적인 방법인 것 같다. 주로 채용 공고를 낼 때 예를 들어서 Spring boot, Django RestFramework 등과 같은 직접적인 개발 프레임워크등을 이야기하거나, TDD, 애자일등의 개발 방법이나 데일리 스크럼, 코드 리뷰와 같은 개발 문화를 주로 이야기하는 것 같다. 이를 토대로 해당 회사에서의 개발 스킬이나 문화를 직간접적으로 파악할 수 있다.</p>
<br />

<p>*<em>2. 회사 채용 이메일로 직접 메일 보내보기
*</em>-&gt; 회사 사이트나 구인 구직 게시글을 보게 되면 인사 담당자나 채용 담당자, 개발 부서 팀장분들의 메일이나 연락처가 있는 경우가 있다. 이 경우 메일등을 통해서 직접적으로 물어보는 것 또한 좋은 방법인 것 같다.(단 개인 연락처가 아닌 공연히 공고된 연락처에 한해)
<img src="https://velog.velcdn.com/images/kevin_/post/ba00a28f-4e9f-4987-ae31-148f6103a2f4/image.png" alt=""></p>
<br />

<p>*<em>3. 로켓펀치등을 통한 현직자와의 1:1 문의
*</em>-&gt; 1, 2번 방법보다 더 자세하고, 확실하게 인사이트를 얻을 수 있는 방법이지만 이 방법은 대단히 조심스럽다고 생각은 든다. 로켓펀치라는 서비스 자체가 일반 카카오톡 같은 서비스와 다르게 이직 시장에서 사용하는 서비스이기에 해당 방식이 통한 것이지 다른 서비스에서 어떻게 연락처 알아내서 질문 드리는 것은 현직자분께 상당히 폐를 끼치는 행위라고 개인적으로 생각한다...
<img src="https://velog.velcdn.com/images/kevin_/post/5cb58216-82f3-45dc-b37d-8493069a304f/image.jpeg" alt=""></p>
<br />

<h3 id="기술-면접-준비하기">기술 면접 준비하기</h3>
<p>기술 면접은 아래와 같은 레퍼런스들을 통해서 준비했다.
<br /></p>
<p><a href="https://github.com/ksundong/backend-interview-question">Github : ksundong님의 백엔드 인터뷰 질문 
</a>
<a href="https://mangkyu.tistory.com/88">Tistory : 망나니개발자님의 백엔드 인터뷰 질문
</a></p>
<br />
내가 실제로 회사 면접으로 받았던 기술 관련 질문들 중 일부 질문들은 아래와 같다.

<ol>
<li>프로세스와 쓰레드 차이</li>
<li>SSL에 대해서 설명하시오.</li>
<li>OOP의 SOLID에 대해서 설명하시오</li>
<li>스택과 큐의 차이</li>
<li>스택과 큐를 실제로 적용시키면 어떤 기능들을 구현할 수 있겠는가</li>
<li>Spring boot의 AOP에 대해서 설명하라</li>
<li>@Tranasctional(read_only=true)시 어떤 작업들이 일어나는지 </li>
<li>Transaction에 대해서 설명하라.</li>
<li>Spring JPA N+1이 왜 발생하는지와 해결 방법을 이야기하라.</li>
<li>가비지 컬렉터의 역할과 가비지 컬렉터의 종류에 대해서 말하라.</li>
<li>자바 버젼 몇 사용했는가?</li>
<li>해당 자바 버젼을 사용한 이유와 버젼 별로 업데이트 된 것을 이야기하라.</li>
<li>프로그래밍과 SQL 쿼리를 통해 해결하는 2가지 방법 중에서 더 선호하는 방법은 무엇인가?</li>
</ol>
<p>내가 기술 면접간 중요하다고 느꼈던 점들은 모르면 모른다고 확실히 말하고, 애매하게 아는거면 공부를 했었던 걸 어필하고 모른다고 하는 것이다.</p>
<p>예를 들어서 1번 질문인 프로세스와 쓰레드의 질문을 받았을 때 내가 아예 해당 개념을 처음들어보거나, 공부를 하지 않은 경우에는 
_&quot;해당 개념에 대해서 아직 숙지 되지 않은 것 같습니다.&quot; 
_같은 답변을 바로 하는 것이 좋은 것 같다. </p>
<p>대충 얼버무려봤자 상대는 나보다 개발을 훠어어얼씬 잘하는 사람들이다. 당연히 내가 모르는데 막 씨부리는지 분명히 아실 것이다.</p>
<p>단 내가 공부를 분명히 했었던 개념인데 애매하게 기억이 안나는 경우에는 나 같은 경우에는 
<em>&quot;해당 개념은 제가 학교 수업이나 진행했던 <del>프로젝트를 통해서 접했던 부분인데 ~</del>로 기억합니다.&quot;</em> 라는 답변을 통해서 그래도 내가 공부했었다라는 걸 어필했었다.</p>
<p>이 방식은 면접관님들마다 다르게 받아들일 수 있는 부분이기에 다르게 받아들일 수 있을 것 같다. 
<del>애초에 그냥 공부를 확실히 해놓는게 베스트라고 생각한다.</del></p>
<p>그리고 공부할 때 되도록 깔끔하게 개념을 설명할 수 있는게 좋을 것 같다.
그래서 나는 한 문장에서 길어도 두 문장정도로 예상 질문의 답변을 준비했었다.</p>
<p>아래는 내가 준비했던 예상 질문의 답변이다.<br />  </p>
<ol>
<li><code>OOP란?</code>
 -&gt; OOP는 현실 세계를 프로그래밍으로 옮겨와 현실 세계의 사물들을 객체로 보고, 그 객체로부터 개발하고자 하는 특징과 기능을 뽑아와 프로그래밍하는 기법입니다. <br />   </li>
<li><code>REST API란?</code>
 -&gt; 사람이 읽기 쉬운 API이며, 자원, 요청 방식, 자원의 형태등으로 이를 표현 <br />      </li>
<li><code>동기와 비동기란?</code>
 -&gt; 동기는 한 작업이 끝난 후에 다른 작업을 실행하는 것, 비동기는 여러 작업이 병렬적으로 수행되어서 동시에 수행되는 것처럼 작업할 수 있는 것 <br />  </li>
<li><code>SQL Injection?</code>
 -&gt; 공격자가 악의적인 의도를 갖는 구문을 삽입해서 공격자가 원하는 SQL을 실행하도록 하는 웹 해킹 기법<br />  </li>
<li><code>프레임워크와 라이브러리의 차이?</code>
 -&gt; 프레임워크 → 전체적인 흐름을 프레임워크가 주도
 -&gt; 라이브러리 → 사용자가 흐름을 제어하며, 가져다 쓰는 용도</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바의 멀티 쓰레드에 대해서]]></title>
            <link>https://velog.io/@kevin_/%EC%9E%90%EB%B0%94%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%93%B0%EB%A0%88%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@kevin_/%EC%9E%90%EB%B0%94%EC%9D%98-%EB%A9%80%ED%8B%B0-%EC%93%B0%EB%A0%88%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Sat, 09 Dec 2023 11:12:00 GMT</pubDate>
            <description><![CDATA[<h3 id="1️⃣-쓰레드-개념">1️⃣ 쓰레드 개념</h3>
<p>프로세스란 간단히 말해서 ‘실행중인 프로그램’이다.
<br /></p>
<p>프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.</p>
<p>프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어있다.</p>
<ul>
<li>모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 ‘멀티쓰레드 프로세스’라고 칭한다.</li>
<li>프로세스 → 공장</li>
<li>쓰레드 → 공장에서 일하는 일꾼</li>
</ul>
<br />
Java에서는 그래서 Main 메서드가 Main Thread이다.

<ul>
<li>해당 Main Thread만 이용하는 거면 싱글 쓰레드로 프로그램이 돌아가는 것이다.</li>
</ul>
<br />

<p>멀티 쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.</p>
<ul>
<li>CPU 코어가 한번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.<br />

</li>
</ul>
<p>프로세스의 성능이 단순히 쓰레드의 개수에 비례하는 것은 아니며, 하나의 쓰레드를 가진 프로세스보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수도 있다.
<br /></p>
<p>멀티 쓰레딩의 장점은 아래와 같다.</p>
<ol>
<li>CPU 사용률 향상</li>
<li>자원을 보다 효율적으로 사용</li>
<li>사용자에 대한 응답성이 향상</li>
<li>작업이 분리되어서 코드 간결화<br />

</li>
</ol>
<p>여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티 쓰레드로 작성하는 것은 필수적이어서, 하나의 프로세스가 여러 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍 해야한다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/b18f2831-5578-468e-9396-48d800220c7f/image.jpeg" alt=""></p>
<br />

<h3 id="2️⃣-프로세스-개념">2️⃣ 프로세스 개념</h3>
<p>쓰레드를 구현하는 방법은 일반적으로 Thread 클래스 상속, Runnable 인터페이스 구현 2가지 방식이 있다.</p>
<p>일반적으로는 Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기에, Runnable 인터페이스를 구현하는 방법을 사용한다.
<br /></p>
<p><em>Main method</em></p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {

        Runnable t1 = new ThreadEx();

        Thread thread = new Thread(t1);

        thread.start();

        }
    }</code></pre>
<br />

<p> <em>Runnable 인터페이스를 구현한 Thread</em></p>
<pre><code class="language-java">public class ThreadEx implements Runnable{

    @Override
    public void run() {
        System.out.println(&quot;쓰레드가 동작 중입니다.&quot;);
    }
}</code></pre>
<br />

<p> <em>Runnable 인터페이스</em></p>
<pre><code class="language-java">@FunctionalInterface
public interface Runnable {

    public abstract void run();
}</code></pre>
<p>쓰레드를 구현한다는 것은 그저 쓰레드를 통해 작업하고자 하는 내용으로 run() 메서드의 몸통을 채우는 것 뿐이다.
<br /></p>
<p>Runnable 인터페이스를 구현한 후, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.</p>
<p>왜냐면 Thread 클래스의 생성자에서는 Runnable 인터페이스를 구현한 클래스의 인스턴스를 인스턴스를 참조해서, 상속을 통해서 run()을 오버라이딩 하지 않아도, 외부로부터 run()을 제공받을 수 있게 된다.
<br /></p>
<p><em>Thread 클래스의 생성자</em></p>
<pre><code class="language-java">public Thread(Runnable target) {
        this(null, target, &quot;Thread-&quot; + nextThreadNum(), 0);
    }</code></pre>
<br />

<h3 id="3️⃣-thread의-실행---start">3️⃣ Thread의 실행 - start()</h3>
<p>쓰레드가 생성되었다고 해서 자동으로 실행되는 것은 아니다. start()를 호출해야만 쓰레드가 실행된다.</p>
<ul>
<li>정확히는 start()가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행대기 상태에 있다가 자신의 차례가 되어야 실행된다.<br />

</li>
</ul>
<p>한번 실행이 종료된 쓰레드는 다시 실행할 수 없다.
<br /></p>
<p><em>Thread 클래스의 start() method</em></p>
<pre><code class="language-java">public synchronized void start() {

        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {

            }
        }
    }</code></pre>
<ul>
<li>내부적으로 started 변수를 두어서 이를 구현한다.<br />
<br />

</li>
</ul>
<h3 id="4️⃣-start와-run">4️⃣ start()와 run()</h3>
<p>main() 메서드에서 run()을 호출한다는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.</p>
<ul>
<li>즉 새로운 쓰레드를 생성하는 것이 아니다.<br />

</li>
</ul>
<p>모든 쓰레드는 독립적인 작업을 수행하기 위해서 자신만의 호출 스택을 필요로 한다. 즉 새로운 쓰레드를 만들 때도 자신만의 호출 스택을 만들어주어야 한다.
<br /></p>
<p>start() 메서드는 새로운 쓰레드가 작업을 실행하는데 필요한 호출 스택을 생성한 다음에 run()을 호출해서, 생성된 호출 스택에 run()이 첫번째로 올라가게 한다.</p>
<ul>
<li>즉 새로운 쓰레드를 생성한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kevin_/post/a1fc2c2d-bb41-4685-8a7c-09e8eeb012c2/image.jpeg" alt=""></p>
<h3 id="5️⃣main-thread">5️⃣main Thread</h3>
<p>main 메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다.
<br /></p>
<p>그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)을 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업이 수행되도록 한다.</p>
<ul>
<li>Spring boot Application도 서버 실행시 main Thread를 생성하고 이 쓰레드가 main Method를 호출해서 작업이 수행되도록한다.<br />

</li>
</ul>
<p>프로그램은 Main 메서드의 종료 여부와 관계 없이, 실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
<br /></p>
<h3 id="6️⃣-싱글-쓰레드와-멀티-쓰레드">6️⃣ 싱글 쓰레드와 멀티 쓰레드</h3>
<p>자바가 OS(플랫폼) 독립적이라고 하지만 실제로는 OS 종속적인 부분이 몇가지 있는데 쓰레드도 그 중 하나이다.</p>
<p>두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글 쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적이다.</p>
<br />


<h3 id="7️⃣-쓰레드의-생명-주기-및-상태">7️⃣ 쓰레드의 생명 주기 및 상태</h3>
<p>쓰레드의 상태
<img src="https://velog.velcdn.com/images/kevin_/post/0cf0d4dc-7aa0-4aa5-b33e-c3d63f7659c1/image.png" alt=""></p>
<p>쓰레드의 생명 주기
<img src="https://velog.velcdn.com/images/kevin_/post/614c0024-5353-4889-b7a0-d5422aa59853/image.png" alt=""></p>
<h3 id="8️⃣-쓰레드의-동기화">8️⃣ 쓰레드의 동기화</h3>
<p>멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.</p>
<p>위와 같은 일을 방지 하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 임계영역과 락을 도입할 수 있다.</p>
<p>공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 <strong><code>단 하나의 쓰레드</code></strong>만 이 영역 내의 코드를 수행할 수 있게 한다. </p>
<ul>
<li><strong><code>해당 쓰레드</code></strong>가 임계 영역 내의 모든 코드를 수행하고, 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다.<br />

</li>
</ul>
<p>이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 ‘쓰레드의 동기화’라고 한다.
<br />
<br /></p>
<h3 id="9️⃣-syncronized를-이용한-동기화">9️⃣ Syncronized를 이용한 동기화</h3>
<p>먼저 가장 간단한 동기화 방법인 Syncronized 키워드를 이용한 동기화는 임계영역을 설정하는데 사용된다.</p>
<pre><code class="language-java">public class Sync {

    // 메서드 전체를 임계 영역으로 지정
    public synchronized void countPoints() {

    }

}</code></pre>
<p>쓰레드는 synchronized 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환한다.
<br /></p>
<p><code>synchronized</code> 를 통해서 메서드 내의 코드 일부를 임계영역으로 지정하는 방법도 존재한다.</p>
<pre><code class="language-java">public class Sync {

    public void countPoints() {

        // 특정한 영역을 임계 영역으로 지정
        synchronized (this){

        }
    }
}</code></pre>
<p>이 때 synchronized 인자에는 락을 걸고자 하는 객체를 참조해야 한다.</p>
<p>이 블럭의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납한다.</p>
<p>모든 객체는 lock을 하나씩 가지고 있으며, 해당 객체의 lock을 가지고 있는 쓰레드만 임계 영역의 코드를 수행할 수 있다.</p>
<ul>
<li>다른 쓰레드들은 lock을 얻을 때까지 기다리게 된다.<br />

</li>
</ul>
<p>임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기에 가능하면 메서드 전체에 락을 거는 것보다 synchronized 블록으로 임계 영역을 최소화해서 보다 효율적인 프로그램이 되도록 노력해야한다.</p>
<p>synchronized로 동기화해서 공유 데이터를 보호하는 것까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것이 중요하다.
<br /></p>
<h2 id="🤣-직접-테스팅-해보자">🤣 직접 테스팅 해보자.</h2>
<p><em>Main.java</em></p>
<pre><code class="language-java">public class Main {
    public static void main(String[] args) {

        SharedResource sharedResource = new SharedResource();

        // 여러 쓰레드가 같은 SharedResource에 접근
        Thread thread1 = new Thread(new ThreadEx(sharedResource), &quot;Thread-1&quot;);
        Thread thread2 = new Thread(new ThreadEx(sharedResource), &quot;Thread-2&quot;);

        thread1.start();
        thread2.start();

        try {
            // 모든 쓰레드의 실행이 끝날 때까지 대기
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 최종 결과 출력
        System.out.println(&quot;Final Counter: &quot; + sharedResource.getCounter());
    }
}</code></pre>
<p><em>ThreadEx.java</em></p>
<pre><code class="language-java">public class ThreadEx implements Runnable{

    private SharedResource sharedResource;

    public ThreadEx(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }

    @Override
    public void run() {
        for (int i = 0; i &lt; 5; i++) {
            sharedResource.increment();
        }
    }
}</code></pre>
<p><em>SharedResource.java</em></p>
<pre><code class="language-java">class SharedResource {
    private int counter = 0;

    // synchronized 키워드를 사용하여 메서드를 동기화
    public synchronized void increment() {
        counter++;
        System.out.println(Thread.currentThread().getName() + &quot; - Incremented counter: &quot; + counter);
    }

    public int getCounter() {
        return counter;
    }
}</code></pre>
<p>위 코드를 통해서 SharedResoure의 인스턴스 변수를 증가 시키기 위해서 Thread1과 Thread2가 접근을 하고, 경쟁을 한다. 그러나  <code>public synchronized void increment()</code> 를 통해서 먼저 들어온 쓰레드가 작업을 마칠 때까지 다른 쓰레드는 접근을 하지 못하게 락을 걸어준다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/7adc1684-e34c-4b1c-a24b-0eccbbe0790a/image.png" alt=""></p>
<p>Thread1이 먼저 start() 했음에도 Thread2가 먼저 실행된 이유는 바로 OS의 스케쥴러에 의해서 이 순서가 변경되기 때문이다.
<br /></p>
<p>만약 Lock을 풀게 된다면(<code>synchronized</code> 키워드를 삭제했다면) 아래와 같은 결과가 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/26351650-fa26-42db-b323-46050d918117/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Batch 기본 Chunk 방식]]></title>
            <link>https://velog.io/@kevin_/Batch-%EA%B8%B0%EB%B3%B8-Chunk-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@kevin_/Batch-%EA%B8%B0%EB%B3%B8-Chunk-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Mon, 04 Dec 2023 06:28:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kevin_/post/eeb6aae4-fd8e-4a84-ac36-ed384459ae22/image.png" alt=""></p>
<p>Chunk란 여러 개의 아이템을 묶은 하나의 덩어리, 블록을 의미합니다. <strong>한번에 하나씩 아이템을 입력 받아 Chunk 단위의 덩어리로 만든 후 Chunk 단위로 트랜잭션을 처리</strong>합니다.</p>
<ol>
<li>이용권 만료</li>
</ol>
<p>job : 이용권 만료</p>
<p>step : 이용권 만료, Chunk 방식 채택</p>
<ul>
<li>이용권 만료 대상을 읽어서 그 대상들을 만료 상태로 업데이트 시키면 된다.</li>
<li>step 내부<ul>
<li>ExpirePassesReader</li>
<li>ExpirePassesWriter</li>
</ul>
</li>
</ul>
<br />

<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-java">@Getter
@Setter
@ToString
@Entity
@Table(name = &quot;pass&quot;)
public class PassEntity extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본 키 생성을 DB에 위임합니다. (AUTO_INCREMENT)
    private Integer passSeq;
    private Integer packageSeq;
    private String userId;

    @Enumerated(EnumType.STRING)
    private PassStatus status;
    private Integer remainingCount;

    private LocalDateTime startedAt;
    private LocalDateTime endedAt;
    private LocalDateTime expiredAt;

}</code></pre>
<pre><code class="language-java">package com.fastcampus.pass.job.pass;

import com.fastcampus.pass.repository.pass.PassEntity;
import com.fastcampus.pass.repository.pass.PassStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.database.JpaCursorItemReader;
import org.springframework.batch.item.database.JpaItemWriter;
import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder;
import org.springframework.batch.item.database.builder.JpaItemWriterBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManagerFactory;
import java.time.LocalDateTime;
import java.util.Map;

@Configuration
public class ExpirePassesJobConfig {
    private final int CHUNK_SIZE = 1;

    // @EnableBatchProcessing로 인해 Bean으로 제공된 JobBuilderFactory, StepBuilderFactory
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;
    private final EntityManagerFactory entityManagerFactory;

    public ExpirePassesJobConfig(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, EntityManagerFactory entityManagerFactory) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.entityManagerFactory = entityManagerFactory;
    }

    @Bean
    public Job expirePassesJob() {
        return this.jobBuilderFactory.get(&quot;expirePassesJob&quot;)
                .start(expirePassesStep())
                .build();
    }

    @Bean
    public Step expirePassesStep() {
        return this.stepBuilderFactory.get(&quot;expirePassesStep&quot;)
                .&lt;PassEntity, PassEntity&gt;chunk(CHUNK_SIZE)
                .reader(expirePassesItemReader())
                .processor(expirePassesItemProcessor())
                .writer(expirePassesItemWriter())
                .build();

    }

    /**
     * JpaCursorItemReader: JpaPagingItemReader만 지원하다가 Spring 4.3에서 추가되었습니다.
     * 페이징 기법보다 보다 높은 성능으로, 데이터 변경에 무관한 무결성 조회가 가능합니다.
     */
    @Bean
    @StepScope
    public JpaCursorItemReader&lt;PassEntity&gt; expirePassesItemReader() {
        return new JpaCursorItemReaderBuilder&lt;PassEntity&gt;()
                .name(&quot;expirePassesItemReader&quot;)
                .entityManagerFactory(entityManagerFactory)
                // 상태(status)가 진행중이며, 종료일시(endedAt)이 현재 시점보다 과거일 경우 만료 대상이 됩니다.
                .queryString(&quot;select p from PassEntity p where p.status = :status and p.endedAt &lt;= :endedAt&quot;)
                .parameterValues(Map.of(&quot;status&quot;, PassStatus.PROGRESSED, &quot;endedAt&quot;, LocalDateTime.now()))
                .build();
    }

    @Bean
    public ItemProcessor&lt;PassEntity, PassEntity&gt; expirePassesItemProcessor() {
        return passEntity -&gt; {
            passEntity.setStatus(PassStatus.EXPIRED);
            passEntity.setExpiredAt(LocalDateTime.now());
            return passEntity;
        };
    }

    /**
     * JpaItemWriter: JPA의 영속성 관리를 위해 EntityManager를 필수로 설정해줘야 합니다.
     */
    @Bean
    public JpaItemWriter&lt;PassEntity&gt; expirePassesItemWriter() {
        return new JpaItemWriterBuilder&lt;PassEntity&gt;()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }

}</code></pre>
<br />

<h3 id="배치-작업-순서">배치 작업 순서</h3>
<ol>
<li><p>BatchConfig 작성으로 전역으로 배치 설정</p>
<pre><code class="language-java"> /*
  * @EnableBatchProcessing
  * Spring Batch 기능을 활성화하고 배치 작업을 설정하기 위한 기본 구성을 제공합니다.
  * JobRepository, JobLauncher, JobRegistry, PlatformTransactionManager, JobBuilderFactory, StepBuilderFactory 빈으로 제공됩니다.
  * https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.html
  */

 @EnableBatchProcessing
 @Configuration
 public class BatchConfig {

     /**
      * JobRegistry는 context에서 Job을 추적할 때 유용합니다.
      * JobRegistryBeanPostProcessor는 Application Context가 올라가면서 bean 등록 시, 자동으로 JobRegistry에 Job을 등록 시켜줍니다.
      */
     @Bean
     public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
         JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
         jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry);
         return jobRegistryBeanPostProcessor;

     }
 }</code></pre>
<br />
</li>
<li><p>배치를 작업할 Feature의 배치 설정 클래스를 생성</p>
<ol>
<li>이 때 클래스 내부에 <code>JobBuilderFactory</code> , <code>StepBuilderFactory</code> 를 주입받는다.<ul>
<li>위 전역으로 @EnableBatchProcessing을 통해서 Spring에서 자동으로 주입해준다.</li>
</ul>
</li>
</ol>
</li>
</ol>
<br />

<ol start="3">
<li><p>Feature에 맞는 Job을 먼저 작성한다.</p>
<pre><code class="language-java"> @Bean
     public Job expirePassesJob() {
         return this.jobBuilderFactory.get(&quot;expirePassesJob&quot;)
                 .start(expirePassesStep())
                 .build();
     }</code></pre>
<p> <code>.start(expirePassesStep())</code> ← job을 이루는 pass를 설정한다. </p>
 <br />
</li>
<li><p>step을 작성한다.</p>
<pre><code class="language-java"> @Bean
     public Step expirePassesStep() {
         return this.stepBuilderFactory.get(&quot;expirePassesStep&quot;)
                 .&lt;PassEntity, PassEntity&gt;chunk(CHUNK_SIZE)
                 .reader(expirePassesItemReader())
                 .processor(expirePassesItemProcessor())
                 .writer(expirePassesItemWriter())
                 .build();
     }</code></pre>
<p> <code>.&lt;PassEntity, PassEntity&gt;chunk(CHUNK_SIZE)</code> ← Chunk 방식의 Step이기에 Chunk를 설정해주어야 한다.</p>
 <br />

<p> 이 때 <code>PassEntity</code>, <code>PassEntity</code> 은 각각 Input, Output 타입이다.</p>
<pre><code class="language-java"> public &lt;I, O&gt; SimpleStepBuilder&lt;I, O&gt; chunk(int chunkSize) {
         return new SimpleStepBuilder&lt;I, O&gt;(this).chunk(chunkSize);
     }</code></pre>
<p> <code>private final int CHUNK_SIZE = 1;</code> ← ChunkSize는 1로 설정해두었는데, 이러면 1개의 DB row마다 트랜잭션이 발생한다.</p>
<p> <em>물론 실 개발시에는 이렇게 작게 설정 안한다.</em></p>
<p> <code>.reader(expirePassesItemReader())
 .processor(expirePassesItemProcessor())
 .writer(expirePassesItemWriter())</code></p>
<p> → 해당 Step에 맞는 각 Item 구성 요소들을 설정해준다.</p>
 <br />


</li>
</ol>
<ol start="5">
<li><p>ItemReader를 작성한다.</p>
<pre><code class="language-java"> /**
      * JpaCursorItemReader: JpaPagingItemReader만 지원하다가 Spring 4.3에서 추가되었습니다.
      * 페이징 기법보다 보다 높은 성능으로, 데이터 변경에 무관한 무결성 조회가 가능합니다.
      */
     @Bean
     @StepScope
     public JpaCursorItemReader&lt;PassEntity&gt; expirePassesItemReader() {
         return new JpaCursorItemReaderBuilder&lt;PassEntity&gt;()
                 .name(&quot;expirePassesItemReader&quot;)
                 .entityManagerFactory(entityManagerFactory)
                 // 상태(status)가 진행중이며, 종료일시(endedAt)이 현재 시점보다 과거일 경우 만료 대상이 됩니다.
                 .queryString(&quot;select p from PassEntity p where p.status = :status and p.endedAt &lt;= :endedAt&quot;)
                 .parameterValues(Map.of(&quot;status&quot;, PassStatus.PROGRESSED, &quot;endedAt&quot;, LocalDateTime.now()))
                 .build();
     }</code></pre>
<p> 여기서 커서 방식으로 조회를 한다.</p>
<p> <code>.entityManagerFactory(entityManagerFactory)</code> ← JPAItemReader, Writer 모두 EntityManagerFactory를 선언을 해주어야 한다. 왜냐하면 DB로부터 CRUD를 해야하기 때문에</p>
<p> <code>.queryString(&quot;select p from PassEntity p where p.status = :status and p.endedAt &lt;= :endedAt&quot;)</code> ← 바로 JPQL을 작성해주면 된다.</p>
<p> <code>.parameterValues(Map.of(&quot;status&quot;, PassStatus.PROGRESSED, &quot;endedAt&quot;, LocalDateTime.now()))</code> ← JPQL 인자에 대한 값을 Map 형태로 Key, Value 맞춰서 넣어주면 된다. </p>
</li>
</ol>
<br />

<p>  <code>@StepScope</code>  ← Bean의 생성 시점이 스프링 애플리케이션이 실행되는 시점이 아닌 @JobScope, @StepScope가 명시된 메서드가 실행될 때까지 <strong>지연시키는 것을 의미</strong>
<br /></p>
<p><strong>지연 시켰을 때 얻는 이점</strong></p>
<ol>
<li>JobParameter를 특정 메서드가 실행하는 시점까지 지연시켜 할당시킬 수 있습니다.</li>
</ol>
<p>즉, 애플리케이션이 구동되는 시점이 아니라 비즈니스 로직이 구현되는 어디든 JobParameter를 할당함으로 유연한 설계를 가능
<br /></p>
<ol start="2">
<li>병렬처리에 안전합니다. </li>
</ol>
<p>Step의 구성요소인 ItemReader, ItemProcessor, ItemWriter이 있고, ItemReader에서 데이터를 읽어 오는 메서드를 서로 다른 Step으로 부터 동시에 병렬 실행이 된다면 서로 상태를 간섭받게 될 수 있습니다. 하지만 @StepScope를 적용하면 각각의 Step에서 실행될 때 서로의 상태를 침범하지 않고 처리를 완료할 수 있습니다
<br /></p>
<aside>
💡 @JobScope는 Step 선언문에서만 사용이 가능하고, @StepScope는 Step을 구성하는 ItemReader, ItemProcessor, ItemWriter에서 사용 가능합니다.

</aside>

<p><a href="https://velog.io/@sa1341/JobScope%EC%99%80-StepScope">@JobScope와 @StepScope</a>
<br /></p>
<p>페이징이 아니라 커서를 쓴 이유 ← 현재 비즈니스 로직상 한 페이지에서 값을 불러온 후 값을 변경하면 다음 페이지에서 값이 변경되어서 누락이 되는 경우가 발생할 수 있기에</p>
<p>커서는 데이터 변경에 무결성하다.
<br /></p>
<ol start="6">
<li><p>ItemProcessor를 작성한다.</p>
<pre><code class="language-java"> @Bean
     public ItemProcessor&lt;PassEntity, PassEntity&gt; expirePassesItemProcessor() {
         return passEntity -&gt; {
             passEntity.setStatus(PassStatus.EXPIRED);
             passEntity.setExpiredAt(LocalDateTime.now());
             return passEntity;
         };
     }</code></pre>
<p> ItemReader를 통해서 읽어온 PassEntity의 데이터 값을 람다식으로 변경시켜주고 반환한다.</p>
 <br />
</li>
<li><p>ItemWriter를 작성한다.</p>
<pre><code class="language-java"> /**
      * JpaItemWriter: JPA의 영속성 관리를 위해 EntityManager를 필수로 설정해줘야 합니다.
      */
     @Bean
     public JpaItemWriter&lt;PassEntity&gt; expirePassesItemWriter() {
         return new JpaItemWriterBuilder&lt;PassEntity&gt;()
                 .entityManagerFactory(entityManagerFactory)
                 .build();
     }</code></pre>
<p> ItemWriter는 단순히 entityManager를 감싸고 있는 래퍼라고 생각해도 된다.</p>
</li>
</ol>
<br />

<hr>
<br />

<h3 id="test-코드">Test 코드</h3>
<pre><code class="language-java">@Slf4j
@SpringBatchTest
@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@ContextConfiguration(classes = {ExpirePassesJobConfig.class, TestBatchConfig.class})
public class ExpirePassesJobConfigTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private PassRepository passRepository;

    @Test
    public void test_expirePassesStep() throws Exception {
        // given
        addPassEntities(10);

        // when
        JobExecution jobExecution = jobLauncherTestUtils.launchJob();
        JobInstance jobInstance = jobExecution.getJobInstance();

        // then
        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
        assertEquals(&quot;expirePassesJob&quot;, jobInstance.getJobName());

    }

    private void addPassEntities(int size) {
        final LocalDateTime now = LocalDateTime.now();
        final Random random = new Random();

        List&lt;PassEntity&gt; passEntities = new ArrayList&lt;&gt;();
        for (int i = 0; i &lt; size; ++i) {
            PassEntity passEntity = new PassEntity();
            passEntity.setPackageSeq(1);
            passEntity.setUserId(&quot;A&quot; + 1000000 + i);
            passEntity.setStatus(PassStatus.PROGRESSED);
            passEntity.setRemainingCount(random.nextInt(11));
            passEntity.setStartedAt(now.minusDays(60));
            passEntity.setEndedAt(now.minusDays(1));
            passEntities.add(passEntity);

        }
        passRepository.saveAll(passEntities);

    }

}</code></pre>
<p><code>@ContextConfiguration(classes = {ExpirePassesJobConfig.class, TestBatchConfig.class})</code> ← 테스트간 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정, ExpirePassesJobConfig와 TestBatchConfig를 테스트간 설정 파일로 지정</p>
<p><code>JobLauncherTestUtils</code> ← 스프링 배치 테스트에 필요한 유틸 기능</p>
<p><code>JobRepositoryTestUtils</code> ← 데이터베이스에 저장된 JobExcution을 생성/삭제 지원</p>
<pre><code class="language-java">/**
     * Launch the entire job, including all steps.
     * 
     * @return JobExecution, so that the test can validate the exit status
     * @throws Exception thrown if error occurs launching the job.
     */
    public JobExecution launchJob() throws Exception {
        return this.launchJob(this.getUniqueJobParameters());
    }</code></pre>
<p><code>jobLauncherTestUtils.launchJob();</code> ← 아무 인자도 안넘겨주면, 모든 Job과 step들을 시작시킨다.</p>
<p><a href="https://cheese10yun.github.io/spring-batch-test-2/">Spring Batch Test 작성 방법 및 고찰 - Yun Blog | 기술 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Batch Batch Batch]]></title>
            <link>https://velog.io/@kevin_/Batch-Batch-Batch</link>
            <guid>https://velog.io/@kevin_/Batch-Batch-Batch</guid>
            <pubDate>Mon, 04 Dec 2023 06:20:22 GMT</pubDate>
            <description><![CDATA[<p>배치 작업 ← 데이터를 실시간으로 처리하는 게 아닌 일괄적으로 모아서 처리하는 작업</p>
<blockquote>
<p>Spring Batch는 엔터프라이즈 시스템의 운영에 있어 대용량 일괄처리의 편의를 위해 설계된 가볍고 포괄적인 배치 프레임워크다. Spring의 특성을 그대로 가져왔기 때문에 DI, AOP, 서비스 추상화 등 Spring 프레임워크의 3대 요소를 모두 사용할 수 있다.</p>
</blockquote>
<br />

<p>보통 아래와 같은 경우 많이 사용한다.</p>
<ol>
<li>대용량의 비즈니스 데이터를 복잡한 작업으로 처리해야하는 경우</li>
<li>특정한 시점에 스케쥴러를 통해 자동화된 작업이 필요한 경우 (ex. 푸시알림, 월 별 리포트)</li>
<li>대용량 데이터의 포맷을 변경, 유효성 검사 등의 작업을 트랜잭션 안에서 처리 후 기록해야하는 경우</li>
</ol>
<br />

<p><strong>Spring Batch</strong>는 로깅/추적, 트랜잭션 관리, 작업 처리 통계, 작업 재시작, 건너뛰기, 리소스 관리 등 대용량 레코드 처리 에필 수적인 재사용 가능한 기능을 제공한다. 또한 최적화 및 파티셔닝 기술을 통해 대용량 및 고성능 일괄 작업을 가능하게 하는 고급 기술 서비스 및 기능을 제공한다.</p>
<p><a href="https://devbksheen.tistory.com/284">스프링 배치(Spring Batch)란? 소개 및 예제</a></p>
<br />

<p><strong>배치는 언제 사용하는가?</strong></p>
<ul>
<li>예약 시간에 광고성 메시지 발송</li>
</ul>
<ul>
<li>결제한 내역을 정산</li>
<li>운영을 위해서 필요한 통계 데이터 구축</li>
<li>대량 데이터를 필요로 하는 모델 학습 작업</li>
</ul>
<br />

<p><strong>왜 위 기능들은 배치를 통해서 일괄 처리를 해야하는가?</strong></p>
<ul>
<li>메시지 예약같이 기능 스펙상 실시간으로 처리할 수 없는 경우</li>
<li>리소스를 효율적으로 사용할 수 있다.<ul>
<li>자동적으로 일괄 처리하기에 리소스 낭비를 줄일 수 있다.</li>
</ul>
</li>
</ul>
<br />

<p><strong>왜 스프링 배치이어야 하는가?</strong></p>
<ul>
<li>스프링에서 제공하는 특성 그대로 사용가능하기에<ul>
<li>유지보수가 좋기에<ul>
<li>AOP, DI, Test 등의 기능을 그대로 사용가능하기에</li>
</ul>
</li>
</ul>
</li>
</ul>
<br />

<p><strong>스프링 배치에는 일정 시간에 자동으로 잡이 실행되는 스케쥴링 기능이 없다.</strong></p>
<p>→ 스프링 배치 ≠ 스케쥴링</p>
<p>→ 스프링 스케쥴러나 Quartz등을 통해서 함께 사용할 수 있긴함.</p>
<p>→ 스프링 배치는 Job을 관리하나 실행시키는 주체는 아니다.</p>
<br />


<p><strong>Job</strong> ← Feature 하나</p>
<p>ex) 수업전 알람을 주는 job ← 하나의 Job
<br /></p>
<p>Job은 여러 step으로 이루어져 있다.</p>
<p>ex) 수업전 알람을 주는 job ← 알람 대상인 사용자를 가져오는 step, 알람을 전송하는 step
<br /></p>
<p><strong>step</strong> ← 순차적으로 이루어진 작업을 캡슐화한 도메인 객체 </p>
<p>step은 테스트 클릭 기반 step, chunk 기반 step으로 나눌 수 있다.</p>
<ol>
<li><strong>테스트 클릭 기반 step</strong><ul>
<li>단일 작업을 처리할 때 사용 = 간단히 정의한 하나의 작업 처리<ol>
<li>오래된 데이터를 삭제한다.</li>
<li>이미 정의된 알람을 전송한다.
<img src="https://velog.velcdn.com/images/kevin_/post/2bb0f154-edb2-4976-bd69-719aecfde5f9/image.png" alt=""></li>
</ol>
</li>
</ul>
</li>
</ol>
<br />

<ol start="2">
<li><p>chunk 기반 step</p>
<ol>
<li><p>아이템 기반으로 처리를 하게 돼서 아이템 리더, 아이템 프로세서, 아이템 라이터라는 부분으로 구성</p>
<ul>
<li>아이템 프로세서는 필수 요소가 아니기에 필요한 경우에만 사용</li>
</ul>
</li>
<li><p>Roop 1 : chunk 단위(=가져올 데이터 수)만큼 ItemReader로 조회하고, 아이템을 가져온다.</p>
</li>
<li><p>Roop 2 : 그 후에 동일하게 아이템 갯수만큼 ItemProccesor로 가서 데이터를 처리한다.</p>
</li>
<li><p>위 2번의 루프가 끝나면 아이템들을 한번에 ItemWriter로 쏴서 모두 전달한다.</p>
</li>
<li><p>Roop 3 : 전체 과정(1~ 2)을 아이템을 다 읽을 때까지 루프하는 것 
<img src="https://velog.velcdn.com/images/kevin_/post/88c5cdd8-8993-4953-a400-c40d67ea7a78/image.png" alt=""></p>
</li>
</ol>
</li>
</ol>
<p><strong>chunk 기반 step</strong>은 한번에 하나씩 데이터를 읽고 chunk를 만든 후 chunk 단위로 트랜잭션을 처리한다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/kevin_/post/7867ce94-0732-4967-9c0f-bdb48ba46f93/image.png" alt=""></p>
<p>Job은 유일하고, 고유하고 순서를 가진 여러 스텝들의 목록이며 외부 의존성에 영향을 받지않고 실행이 가능해야 하는 독립적인 작업이다.</p>
<p>Job은 정의된 step의 순서대로 작업을 진행한다.</p>
<br />

<p><img src="https://velog.velcdn.com/images/kevin_/post/9a66ff63-b6fd-47d1-974b-8b6dc385b043/image.png" alt=""></p>
<p><strong>jobRepository</strong> </p>
<p>→ 배치 수행과 관련된 데이터를 가지고 있다. ex) 시작하는 시간, 종료하는 시간, job의 상태, 읽는 건수, 쓰는 건수 등등</p>
<p>→ 일반적으로 rdb를 사용</p>
<p>→ 스프링 배치 내에 대부분의 컴포넌트들이 해당 데이터를 공유한다.</p>
<p>→ 스프링 설정을 통해서 애플리케이션 실행시 없으면 자동적으로 생성하게끔 할 수 있다.</p>
<p>→ 인메모리 데이터 방식도 지원함.
<br /></p>
<p><strong>jobLauncher</strong></p>
<p>→ job을 실행하는 역할, job을 현재 스레드에서 수행할지, 스레드 풀을 이용할지, job을 실행할 때 필요한 파라미터는 유효한지 검증</p>
<p>→ jobLauncher를 통해서 job을 실행하면, job은 정의된 step을 실행하게 된다. step은 위에서 이야기한대로 3단계의 루프를 거친다. 루프를 거치면서, jobRepository의 읽고, 쓰기 건수등을 update 한다.
<br /></p>
<h3 id="feature-하나-하나-정의해보기">Feature 하나 하나 정의해보기</h3>
<ol>
<li>이용권 만료</li>
</ol>
<p>job : 이용권 만료</p>
<p>step : 이용권 만료, Chunk 방식 채택</p>
<ul>
<li>이용권 만료 대상을 읽어서 그 대상들을 만료 상태로 업데이트 시키면 된다.</li>
<li>step 내부<ul>
<li>ExpirePassesReader</li>
<li>ExpirePassesWriter<br />
</li>
</ul>
</li>
</ul>
<ol start="2">
<li>이용권 일괄 지급</li>
</ol>
<p>job : 이용권 일괄 지급</p>
<p>step : 이용권 일괄 지급, 테스크 기반 방식 채택</p>
<ul>
<li>step 내부<ul>
<li>AddPassesTasklet</li>
</ul>
</li>
</ul>
<p>~2번까지의 배치 구조는 job 내에서 step을 정의된 순서대로 단일 Thread로 실행하는 것이었다.</p>
<p>그러나 대량의 데이터를 다룰수록 병렬처리는 핵심이된다.</p>
<p>그렇기에 3번은 대량의 데이터를 배치할 때 병렬 처리 방식을 도입한다.
<br /></p>
<ol start="3">
<li>예약 수업 전 알람</li>
</ol>
<p>job : 예약 수업 전 알람</p>
<p>step : 알람 대상 가져오기, 알람 전송하기, Chunk 방식 채택</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/8d459169-055d-46ad-aab2-f9d4b6988e18/image.png" alt=""></p>
<p>왼쪽 step이 알람 대상을 가져오는 step이고, 오른쪽 step이 알람을 병렬적으로 전송하는 step이다.</p>
<p>chunk 기반의 step 에서는 chunk 단위로 처리되고 각자 독립적인 트랜잭션이 적용된다.</p>
<p>그렇기에 100개의 커밋이 있으면, 순차적으로 커밋을 완료해간다.
<br /></p>
<p>이는 대용량에서 극심한 속도 문제를 가져다준다.</p>
<p>그러나 병렬처리로 하면 Chunck 개수만큼 빠른 작업 속도를 보여준다.
<br /></p>
<ol start="4">
<li>이용권 차감</li>
</ol>
<p>job : 이용권 차감</p>
<p>step : 이용권 차감, chunk 기반 방식 채택</p>
<ul>
<li>step 내부<ul>
<li>UsePassesReader</li>
<li><strong>Async</strong>ItemProcessor</li>
<li><strong>Async</strong>ItemWriter</li>
</ul>
</li>
</ul>
<p>시간상 오래걸리는 로직이나 트랜잭션이 있는 경우 Async를 적용</p>
<br />

<ol start="5">
<li>시간당 통계 데이터</li>
</ol>
<p>job : 이용권 차감</p>
<p>step : 이용권 차감, chunk 기반 방식 채택</p>
<ul>
<li>step 내부<ul>
<li>StaticsReader</li>
<li>StatsticsWriter</li>
</ul>
</li>
</ul>
<p>이 경우에 step을 서로 병렬로 작업한다.</p>
<p>→ step끼리 연관이 없기에, 서로 기다릴 필요가 없다. 그렇기에 동시에 병렬로 작업</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 인증 객체를 어떻게 가져올래?]]></title>
            <link>https://velog.io/@kevin_/Spring-Security-%EC%9D%B8%EC%A6%9D-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%80%EC%A0%B8%EC%98%AC%EB%9E%98</link>
            <guid>https://velog.io/@kevin_/Spring-Security-%EC%9D%B8%EC%A6%9D-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%80%EC%A0%B8%EC%98%AC%EB%9E%98</guid>
            <pubDate>Mon, 27 Nov 2023 04:20:35 GMT</pubDate>
            <description><![CDATA[<p>이번 카카오 테크 캠퍼스 활동간에 나는 아래와 같이 인증된 유저 객체를 Presentation layer로 받아오기 위해서 아래와 같은 방식을 사용했다.</p>
<pre><code class="language-java">         /**
     * 산책 허락하기 메서드
     */
    @PostMapping(&quot;walk/{walkerId}/{matchingId}&quot;)
    public ApiResponse&lt;ApiResponse.CustomBody&lt;Void&gt;&gt; acceptWalk(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable(&quot;walkerId&quot;) Long userId, @PathVariable(&quot;matchingId&quot;) Long matchingId)
            throws MatchNotExistException, DuplicateNotificationWithWalkException, MemberNotExistException {
        walkService.saveWalk(customUserDetails, userId, matchingId);
        return ApiResponseGenerator.success(HttpStatus.OK);
    }</code></pre>
<br />

<p>그 때 카카오 테크 캠퍼스의 같은 팀원분이 아래와 같이 Bean을 통해서 Service Layer에서 사용자 정보를 가져오는 방법에 대해서 이야기를 해주었다.</p>
<pre><code class="language-java">private String getEmail(){
        Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
        return loggedInUser.getName();
    }</code></pre>
<p>이 때 어느 방식이 우리 프로젝트에 더 적합한 방식이었는지에 대해서 각 방식의 장, 단점을 알아보면서 비교해보고자 한다.
<br /></p>
<h3 id="1️⃣-authenticationprincipal-을-이용한-방식">1️⃣ <code>@AuthenticationPrincipal</code> 을 이용한 방식</h3>
<p><strong>내가 생각한 장점</strong>을 뽑는다면 아래와 같다.</p>
<ol>
<li><p><strong>코드의 간결화와 간편성이다.</strong></p>
<pre><code class="language-java">          /**
      * 산책 허락하기 메서드
      */
     @PostMapping(&quot;walk/{walkerId}/{matchingId}&quot;)
     public ApiResponse&lt;ApiResponse.CustomBody&lt;Void&gt;&gt; acceptWalk(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable(&quot;walkerId&quot;) Long userId, @PathVariable(&quot;matchingId&quot;) Long matchingId)
             throws MatchNotExistException, DuplicateNotificationWithWalkException, MemberNotExistException {
         walkService.saveWalk(customUserDetails, userId, matchingId);
         return ApiResponseGenerator.success(HttpStatus.OK);
     }</code></pre>
<p> 인증된 유저 객체를 가져오는 2번 방식과 비교해서, 직접 SecurityContextHolder에 접근해서 유저객체를 가져오지 않고 메서드 파라미터로 주입을 받으니 코드의 간결화 및 간편성 또한 증대된다.</p>
 <br />


</li>
</ol>
<ol start="2">
<li><p><strong>Presentation Layer에서의 직접적인 접근이 편리하다.</strong></p>
<p> Presentation Layer에서부터 메서드 파라미터로 인증된 유저 객체를 받을 수 있기 때문에 Presentation Layer에서 직접 사용하거나, Service 계층에 유저 객체를 내려줄 때 특정 값으로 연산 후 내려줄 수도 있다.</p>
 <br />


</li>
</ol>
<p>반면 <strong>내가 생각한 단점</strong>들을 이야기 해보자.</p>
<ol>
<li><p><code>@AuthenticationPrincipal</code>은 내부적으로 UserDetailsService를 이용해서 DB로부터 유저 객체를 조회 후에 가져오기 때문에 조회 쿼리가 발생한다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

        Member member = memberRepository.findByEmail(email).orElseThrow(
                () -&gt; new UsernameNotFoundException(&quot;사용자를 찾을 수 없습니다.&quot;)
        );
        return new CustomUserDetails(member);
    }

}</code></pre>
</li>
</ol>
<br />

<ol start="2">
<li><p>단위 테스트, 통합 테스트시 유저 인증 객체를 넣어주는 방식이 불편하다.</p>
<pre><code class="language-java">         @WithUserDetails(value = &quot;yardyard@likelion.org&quot;, userDetailsServiceBeanName = &quot;customUserDetailsService&quot;, setupBefore = TestExecutionEvent.TEST_EXECUTION
       @Test
      void get_payment_test() throws Exception {
          // given
          int matchingId = 1;

          // when
          ResultActions resultActions = mvc.perform(
                  get(String.format(&quot;/api/payment/%d&quot;, matchingId))
          );

          // console
          String responseBody = resultActions.andReturn().getResponse().getContentAsString();
          System.out.println(&quot;테스트 : &quot; + responseBody);

          // verify
          resultActions.andExpect(jsonPath(&quot;$.success&quot;).value(&quot;true&quot;));
      }</code></pre>
</li>
</ol>
<p><code>@WithUserDetails</code>을 이용해서 테스트시 <code>@AuthenticationPrinciple</code>에 인증 객체를 넣어줄 수 있다. 허나 이 때 반드시 value에 해당하는 유저가 존재해야하기에, BeforeEach등으로 유저를 미리 저장해야하는 불편함이 존재한다.
<br /></p>
<ol start="3">
<li><strong><code>@AuthenticationPrincipal</code>을 사용하면 해당 어노테이션에 의존성을 가지게 되어 특정 스프링 기능에 의존하게 된다.</strong></li>
</ol>
<p>해당 방식을 사용할 경우 추후 Spring에서 해당 어노테이션을 내부적으로 변경하거나, Deprecate할 경우에 높은 Spring 기능 의존으로 인해서 변경에 취약하게 된다.
<br />
<br /></p>
<h3 id="2️⃣-직접-securitycontextholder에서-가져오는-방법">2️⃣ <strong>직접 <code>SecurityContextHolder</code>에서 가져오는 방법</strong></h3>
<p>이 방식의 경우 <strong>내가 생각한 장점</strong>을 뽑는다면 아래와 같다.</p>
<ol>
<li><p>내가 User 객체가 필요한 로직에서 유연하게 가져올 수 있다.</p>
<pre><code class="language-java"> private String getEmail(){
         Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
         return loggedInUser.getName();
     }

 public List&lt;ChatListResDTO&gt; getChatList() {

         Member member = memberRepository.findByEmail(getEmail())
                 .orElseThrow(() -&gt; new InvalidMemberException(ChatRoomMessageCode.INVALID_MEMBER));

                 // ...

    }</code></pre>
</li>
</ol>
<p>위 코드와 같이 내가 사용하고 싶은 코드에서 직접 SecurityContextHolder에서 인증 유저 객체를 가져오는 메서드를 호출함으로써 특정 메소드에서 필요한 시점에 유연하게 사용자 정보에 접근할 수 있으며, 이를 통해 로직에 따라 다르게 동작하거나 특별한 처리를 할 수 있다.
<br /></p>
<ol start="2">
<li><p>테스트시 목 객체를 넣기 용이하다.</p>
<p> 사전에 SecurityContextHolder에 목 객체를 저장 시켜놓으면 되기 때문에 실제 DB를 거치지 않고, 테스팅을 할 수 있다.</p>
<pre><code class="language-java"> @Test
 public void testSomeMethod() {
     SecurityTestUtils.setAuthentication(&quot;testUser&quot;, &quot;ROLE_USER&quot;);

     // Test logic

     SecurityTestUtils.clearAuthentication();
 }</code></pre>
</li>
</ol>
<br />

<p>이 방식또한 <strong>내가 생각한 단점</strong>들을 이야기 해보겠다.
<br /></p>
<ol>
<li><p>1번 방식에 비해서 상대적으로 코드가 지저분해진다.</p>
<p> 매번 인증 객체가 필요할 때마다 메서드를 호출해야하며, 인증이 필요한 Service마다 해당 메서드를 구현해야 하기에 코드의 중복이 일어난다.</p>
<pre><code class="language-java"> // ChatService
 private String getEmail(){
         Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
         return loggedInUser.getName();
     }

 public List&lt;ChatListResDTO&gt; getChatList() {

         Member member = memberRepository.findByEmail(getEmail())
                 .orElseThrow(() -&gt; new InvalidMemberException(ChatRoomMessageCode.INVALID_MEMBER));

                 // ...

    }</code></pre>
</li>
</ol>
<p><em>그러면 static으로 선언하면 되는거 아니야??</em></p>
<p>Spring Security의 <code>SecurityContextHolder</code>는 <code>ThreadLocal</code>을 사용하여 현재 스레드에 대한 정보를 유지하기 때문에, 정적 메서드를 사용하면 여러 스레드 간에 공유되는 메서드가 되므로 위험하다.
<br /></p>
<ol start="2">
<li><p>Presentation layer와 Service Layer 간의 결합도가 증가한다.</p>
<p> 유저의 정보는 클라이언트와 맞닿아 있는 Presentation Layer에서 접근해 가져오는 것이 결합도를 줄이는 방법이다.</p>
<p> Service Layer에서 유저의 정보를 가져오면, 3 layers의 각 역할을 넘어선 것이라고도 생각한다.</p>
</li>
</ol>
<br />
<br />

<h3 id="내-생각">내 생각</h3>
<p>이번 프로젝트에서 내가 구현하고자 한 목표는 성능 최적화적인 부분보다 코드의 간결함이었기 때문에 1번 방식을 택하는게 맞을 것 같다는 생각을 하게 되었다. 반면 만약 극도의 성능 최적화를 하고자하면 직접 Bean에서 유저 객체를 가져오는 2번 방식을 택하는게 맞을 것 같다는 생각을 했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[헥사고날이 뭘깡]]></title>
            <link>https://velog.io/@kevin_/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0%EC%9D%B4-%EB%AD%98%EA%B9%A1</link>
            <guid>https://velog.io/@kevin_/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0%EC%9D%B4-%EB%AD%98%EA%B9%A1</guid>
            <pubDate>Mon, 27 Nov 2023 04:07:33 GMT</pubDate>
            <description><![CDATA[<p><strong>용어 정리</strong></p>
<ul>
<li>헥사고날에서의 용어<ul>
<li>Domain<ul>
<li>사용자가 이용하는 앱의 기능, 회사의 비즈니스 로직을 정의하고 있는 영역</li>
</ul>
</li>
<li>Entity<ul>
<li>비즈니스 로직을 캡슐화한 객체</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>아 그러면 비즈니스 로직을 JPA 개념의 엔티티에 작성하면, 엔티티가 도메인의 주체가 되는거고 비즈니스 로직을 Service 단에 작성하면, Service가 도메인의 주체가 되는거다.</p>
<p>핵심은 도메인 영역은 일반적으로 Service와 Entity를 의미하고, 영속 계층은 Repository를 의미한다.</p>
<br />

<h3 id="기존-계층형-아키텍쳐의-문제점">기존 계층형 아키텍쳐의 문제점</h3>
<p>전통적인 Layered Architecture의 경우에는 DB 주도 설계를 유도한다.</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/22afb4ee-7fe1-4e0c-a36a-5849e8bc6b5a/image.png" alt=""></p>
<p>→ 의존성이 영속성 레이어로 향하기 때문에</p>
<p>의존성이 아래로 흐르기 때문에, 도메인 레이어에서 필요로 하는 의존성(헬퍼, 유틸리티)이 영속성 레이어에 계속해서 추가가 되기 때문에 거대해질 수 있다.</p>
<p>이 때문에 내가 그동안 데이터베이스를 중심으로 애플리케이션을 개발한 것이다.</p>
<p>→ 먼저 쿼리를 작성하고, 이 쿼리에 맞춰서 도메인 로직을 작성했었다.</p>
<p>또한 다른 단점으로는 외부 시스템(ex: JPA)을 변경하기 힘들다는 단점이 있다.</p>
<p>→ Spring data가 제공하는 Repository에 지나치게 의존하여서, 추후 ORM을 변경할 경우에 취약하게 된다.</p>
<br />

<h3 id="클린-아키텍쳐-헥사고날">클린 아키텍쳐, 헥사고날</h3>
<p><img src="https://velog.velcdn.com/images/kevin_/post/b9329d0f-a853-4a77-9bd0-20d52bd7e37a/image.png" alt=""></p>
<p>클린 아키텍쳐는 위 계층형 구조의 단점들을 해결해준다.</p>
<p>각 레이어들은 동심원으로 둘러싸여 있고, 이는 도메인 로직을 담당하는 Use Case와 도메인 엔티티로 향하고 있다.</p>
<p>따라서 도메인 코드에서는 어떤 영속성, UI 프레임워크가 사용되는지 알 수 없기 때문에 비즈니스 규칙에 집중할 수 있다.</p>
<ul>
<li>어떻게 집중할 수 있는가?<ul>
<li>그러려면 먼저 도메인 코드에 대해서 알아야 한다.</li>
</ul>
</li>
</ul>
<p>또한 다른 서드파티 컴포넌트와 포트를 통해 협력하기에, 외부 시스템 변경에도 용이하다</p>
<blockquote>
<p>Use Case는 기존 Service보다 좁은, 기능 단위이기 때문에 넓어지는 Service 문제를 해결할 수 있다.</p>
<p>→ 프로젝트 하다보면 Service가 어마무시하게 뚱뚱해지지 않았는가</p>
</blockquote>
<p>헥사고날 아키텍처는 의존의 방향이 레이어드 아키텍처와 다르다.</p>
<p>클린 아키텍처와 마찬가지로 어디에도 의존하지 않는 도메인 객체들이 존재하고, 이들에 의존하는 서비스계층(또는 usecase 계층)이 존재한다. </p>
<p>서비스계층에서 수행되는 비즈니스 로직들은 외부와 연결된 포트를 통해 시스템 외부로 전달되며 인프라는 포트에 의존한다.</p>
<p>한 마디로, 외부와의 통신을 인터페이스로 추상화하여 비즈니스 로직 안에 외부 코드나 로직의 주입을 막는다는 것이 헥사고날 아키텍처의 핵심이다.</p>
<br />

<h3 id="실-코드와-함께-보는-헥사고날-주요-컴포넌트">실 코드와 함께 보는 헥사고날 주요 컴포넌트</h3>
<p><img src="https://velog.velcdn.com/images/kevin_/post/1ebf1481-ba57-490c-af90-effb3e2befeb/image.png" alt=""></p>
<br />

<h3 id="adapter"><strong>Adapter</strong></h3>
<ul>
<li><p>포트를 통해서 실제로 연결하는 부분을 담당하는 구현체</p>
</li>
<li><p>Adapter는 애플리케이션 코어에 포트를 통하지 않으면 접근할 수 있는 방법이 없다.</p>
<ul>
<li>오직 포트(Usecase)를 통해서 제공되는 메서드를 이용해야만 핵사고날 내부 코어에 접근할 수 있다.</li>
</ul>
</li>
<li><p>Adapter는 2 종류로 분류된다.</p>
<ul>
<li><p>사용자의 요청을 받아들일 때 사용되는 Adapter : Controller</p>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/account&quot;)
@RequiredArgsConstructor
public class BankAccountController {

  private final DepositUseCase depositUseCase;
  private final WithdrawUseCase withdrawUseCase;

  @PostMapping(value = &quot;/{id}/deposit/{amount}&quot;)
  void deposit(@PathVariable final Long id, 
               @PathVariable final BigDecimal amount) {
      depositUseCase.deposit(id, amount);
  }

  @PostMapping(value = &quot;/{id}/withdraw/{amount}&quot;)
  void withdraw(@PathVariable final Long id, 
                @PathVariable final BigDecimal amount) {
      withdrawUseCase.withdraw(id, amount);
  }
}</code></pre>
<p>→ 일반적으로 애플리케이션 코어에 들어오는 포트를 구현하는 Adapter이다.</p>
<p>→ Adapter라고 명칭을 하는게 맞으나 Controller를 관념상 사용하는 표현이기에 위와 같이 명칭하였다.</p>
<p>→ 입력 어댑터에서는 usecase만을 사용해서 내부에 접근 가능하다. </p>
<p>→ usecase를 둘지, input Port를 둘지 고민을 할 때 usecase가 port보다 추상도가 높기에 usecase를 사용</p>
</li>
<li><p>도메인 모델의 처리에 사용되는 Adapter : PersistenceAdapter</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class BankAccountPersistenceAdapter 
              implements LoadAccountPort, SaveAccountPort {

  private final BankAccountMapper bankAccountMapper;
  private final BankAccountSpringDataRepository repository;

  @Override
  public BankAccount load(Long id) {
      BankAccountEntity entity = repository.findById(id)
                  .orElseThrow(NoSuchElementException::new);

      return bankAccountMapper.toDomain(entity);
  }

  @Override
  public void save(BankAccount bankAccount) {
      BankAccountEntity entity = bankAccountMapper.toEntity(bankAccount);
      repository.save(entity);
  }
}</code></pre>
<p>→ repository, 즉 영속 계층을 호출해서 DB 작업을 한다.</p>
<p>→ 출력 어댑터쪽에서 추상화 계층인 출력 포트를 구현해주게 된다.</p>
<p>→ 해당 PersistenceAdapter을 누가 호출하는 걸까?</p>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kevin_/post/f2517b45-095a-4c6e-9dcc-e50bbf3373bc/image.png" alt="">
    → 외부 API 요청을 받은 Web Adapter(Controller)에서 요청을 받아서, usecase를 구현한 inputPort(현재 코드에서는 Port 없이 usecase 대신 바로 Service에서 구현함)에서 Out Port에 요청을 하면, Out Port는 출력 Adapter에 접근해 실 DB를 조회한다.</p>
<br />

<p>*<em><em>여담</em>
*</em>머리가 개 띵했음…</p>
<p><img src="https://velog.velcdn.com/images/kevin_/post/d5b59df4-a266-43f0-86a2-350819fc7ac0/image.png" alt=""></p>
<p>위 사진을 보면 Entity가 persistence 아래 있었다.</p>
<p>헥사고날에서 말하는 도메인은 Entity가 아니었다. 진짜 도메인 그 자체였던 것이었다.</p>
<p>→ <em>비즈니스 로직은 도메인에 작성되어있고, Service에서는 해당 도메인 로직을 호출하기만하는</em></p>
<p>영속 계층 자체를 아예 서드파티처럼 구분한다는게 너무 놀라웠다…</p>
<p><a href="https://github.com/KoEonYack/Tistory-Covenant-Code/tree/main/spring-hexagonal-architecture/src/main/java/covenant/hexagonal/bank/adapter/out/persistence"></a></p>
<br />

<h3 id="port">Port</h3>
<ul>
<li>서비스(또는 usecase)에 어댑터에 대한 명세(specification)만을 제공하는 계층을 의미한다.</li>
<li>단순히 인터페이스 정의만 존재하며, DI를 위해 사용된다.<br />

</li>
</ul>
<p><strong>Input Port</strong></p>
<pre><code class="language-java">@Service
public class FindMemberInputPort implements FindOneMemberUseCase {
    private final MemberFindOutputPort memberFindOutputPort;

    public FindMemberInputPort(MemberFindOutputPort memberFindOutputPort) {
        this.memberFindOutputPort = memberFindOutputPort;
    }

    @Override
    public Optional&lt;Member&gt; findOne(String userId) {
        return memberFindOutputPort.findOne(userId);
    }
}

---

@Service
public class RegisterMemberInputPort implements JoinMemberUseCase {

    private final MemberJoinOutputPort memberJoinOutputPort;

    public RegisterMemberInputPort(MemberJoinOutputPort memberJoinOutputPort) {
        this.memberJoinOutputPort = memberJoinOutputPort;
    }

    @Override
    public void join(String userid, String pw) {
        memberJoinOutputPort.join(userid, pw);
    }
}</code></pre>
<ul>
<li>현재 우리 프로젝트에서는 Port를 사용하지 않고, 하나의 Service에서 usecase를 바로 구현을 했기에 다른 프로젝트의 Port 코드를 가지고 왔다.</li>
<li>입력 포트의 역할은 유스케이스를 구현하는 것이다. 출력 포트(DB 접근)를 사용해 유스케이스를 구현해준다.<ul>
<li>우리는 해당 유스케이스 구현 역할을 Service에 넘겨주었다.</li>
</ul>
</li>
</ul>
<br />

<p><strong>Output Port</strong></p>
<pre><code class="language-java">public interface MemberFindOutputPort {

    Optional&lt;Member&gt; findOne(String userId);
}

---

public interface MemberJoinOutputPort {

    Long join(String userid, String pw);
}</code></pre>
<ul>
<li>외부 리소스에서 데이터를 가져오는 역할이다. 출력 포트는 인터페이스로 추상화하고, 실제 구현은 출력 어댑터에 할당하게 된다.<br />

</li>
</ul>
<h3 id="application-serviceusecase">Application Service(usecase)</h3>
<ul>
<li>어댑터를 주입 받아 도메인 모델과 어댑터를 적절히 오케스트레이션하는 계층을 의미합니다.</li>
<li>예를들어 게시글 작성이라는 usecase는 그에 필요한 Adapter를 주입받고 게시글 도메인 모델을 적절히 제어하는 로직을 지닙니다.</li>
</ul>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class BankAccountService implements DepositUseCase, WithdrawUseCase {

    private final LoadAccountPort loadAccountPort;
    private final SaveAccountPort saveAccountPort;

    @Override
    public void deposit(Long id, BigDecimal amount) {
        BankAccount account = loadAccountPort.load(id);

        account.deposit(amount);

        saveAccountPort.save(account);
    }

    @Override
    public boolean withdraw(Long id, BigDecimal amount) {
        BankAccount account = loadAccountPort.load(id);

        boolean hasWithdrawn = account.withdraw(amount);

        if(hasWithdrawn) {
            saveAccountPort.save(account);
        }

        return hasWithdrawn;
    }
}</code></pre>
<ul>
<li>내가 이해한 Service는 Input Port에 따른 요청과 Output Port에 요청해받은 응답값을 <code>도메인의 비즈니스 로직</code>을 잘 스까서 다시 InputPort로 전달하는 책임을 가지고 있다. ← 오케스트레이션이라는게 잘 스까는거라고 이해를 했다.<br />

</li>
</ul>
<h3 id="domain-model">Domain Model</h3>
<ul>
<li><p>DDD의 도메인 모델과 동일한 개념을 가지고 있다.</p>
</li>
<li><p>엔티티 변경에 대한 모든 로직은 해당 계층에서만 실행된다.</p>
</li>
<li><p>외부 Adapet에서 외부 Port로 엔티티를 리턴할 때 도메인 객체로 변환해서 넘겨준다.</p>
<pre><code class="language-java">  @Override
      public BankAccount load(Long id) {
          BankAccountEntity entity = repository.findById(id).orElseThrow(NoSuchElementException::new);
          return bankAccountMapper.toDomain(entity);
      }</code></pre>
</li>
</ul>
<pre><code class="language-java">public class BankAccount {

    private Long id;
    private BigDecimal balance;

    @Builder
    public BankAccount(Long id, BigDecimal balance) {
        this.id = id;
        this.balance = balance;
    }

    public boolean withdraw(BigDecimal amount) {
        if(balance.compareTo(amount) &lt; 0) {
            return false;
        }

        balance = balance.subtract(amount);
        return true;
    }

    public void deposit(BigDecimal amount) {
        balance = balance.add(amount);
    }

    public BigDecimal getBalance() {
        return balance;
    }
}</code></pre>
<ul>
<li><p>도메인 모델을 사용함으로써 얻는 이점</p>
<ul>
<li><p><code>명확한 관심사의 분리</code></p>
<ul>
<li><p>외부와의 연결에 문제가 생기면 <code>Adapter</code>를 확인하면 될 것이고, 인터페이스의 정의를 변경하고자 한다면 <code>Port</code>를 확인하면 됩니다.</p>
</li>
<li><p>처리 중간에 Custom Metric 측정을 위해 Event Bridge에 이벤트를 보내거나 트레이스를 로그를 심고 싶다면 <code>Service(usecase)</code>를 확인하면 됩니다.</p>
</li>
<li><p>마지막으로 비즈니스 로직이 제대로 동작하지 않는다면 <code>Domain Model</code>만 확인하면 되는 것이지요.</p>
<p>이러한 구조는 결국 쉬운 테스트를 가능하게 해주기도 합니다. 본인의 역할을 수행하기 위해 필요한 Port만 모킹하여 테스트를 쉽게 수행할 수 있습니다.</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<br />

<h3 id="핵사고날에서-발생하는-깨진-창문">핵사고날에서 발생하는 깨진 창문</h3>
<ul>
<li>만약 Port의 네이밍이 범용적이면 여러 usecase를 구현해야 하므로, port가 뚱뚱해지고 이에 따라서 외부 의존성이 늘어날 수록 코드의 로직이 깨지기 쉽고 유지보수 하기 어려워진다.</li>
<li>port의 이름이 너무 세부적이고, 지엽적이면 보일러 플레이트의 양이 과도하게 많아지게된다.<ul>
<li>의존성이 적다는 점에선 SRP를 잘 구현했다고 느낄 수 있지만, 현실적인 개발 세계에서는 개발 속도 또한 굉장히 중요한 요소 중 하나이기에 이를 간과해서는 안된다.</li>
</ul>
</li>
</ul>
<p>즉 MVC 코드가 레거시처럼 느껴질 수 있지만, 아키텍쳐 그 자체보다는 코드가 지니는 가치와 생산성에 집중을 하면 절대 나쁜 코드는 아니다.</p>
<p>반드시 trade-off를 계산하여서 프로젝트의 성격에 맞게 개발을 진행해야 한다.</p>
<h3 id="현재-내-flow">현재 내 Flow</h3>
<ol>
<li>입력 어댑터를 통해 외부에서 요청 들어옴</li>
<li>입력 어댑터에서는 추상화된 유스케이스를 사용하지만, 실제 주입되서 사용되는건 usecase를 구현한 BankAccoutService</li>
<li>유스케이스에서 추상화된 출력 포트를 사용하지만, 실제 주입되서 사용되는건 출력 어댑터</li>
<li>도메인을 사용하는건 출력 어댑터. (입력 어댑터쪽도 유스케이스를 통해 도메인을 리턴받을 수 있음)<br />

</li>
</ol>
<h3 id="참고-레퍼런스">참고 레퍼런스</h3>
<p><a href="https://nahwasa.com/entry/%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%ED%97%A5%EC%82%AC%EA%B3%A0%EB%82%A0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-%EC%BD%94%EB%93%9C-%EA%B5%AC%EC%A1%B0">스프링부트 헥사고날 아키텍쳐 코드 구조</a></p>
<p><a href="https://dataportal.kr/74#%EB%25-F%25--%EB%25A-%25--%EC%25-D%25B-%25--%EC%25A-%BC%EB%25-F%25--%25--%EC%25--%25A-%EA%25B-%25---Domain-Driven%25--Design-">Why DDD, Clean Architecture and Hexagonal ?</a></p>
]]></description>
        </item>
    </channel>
</rss>