<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[jupiny의 개발일지]]></title><description><![CDATA[나를 지탱하고 있는 나뭇가지는 중요하지 않다.
"날개"를 키우자.]]></description><link>https://jupiny.com/</link><generator>Ghost 0.11</generator><lastBuildDate>Thu, 15 Aug 2024 15:31:32 GMT</lastBuildDate><atom:link href="https://jupiny.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[git 커밋 메시지 앞에 이슈 넘버 입력 자동화하기]]></title><description><![CDATA[<p>git을 사용하는 대부분의 개발 팀에서는 약속된 git 브랜치 전략이나 git 커밋 메시지에 대한 컨벤션을 가지고 있다. <br>
내가 속한 개발 팀 역시 이러한 컨벤션을 가지고 있고, 간단하게는 아래와 같다.</p>

<ul>
<li>이슈 추적 시스템(issue tracking system)으로 JIRA를 사용하고 있고, 이슈에 해당하는 feature 브랜치를 따서 PR을 올린다.
<ul><li>ex) <code>feature/XXX-123</code></li></ul></li>
<li>PR에 속한</li></ul>]]></description><link>https://jupiny.com/2024/08/12/automate-issue-number-input-in-git-commit-message/</link><guid isPermaLink="false">47fd9340-d7d2-4b53-89f7-1bf9ba3f6b44</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sun, 11 Aug 2024 18:05:00 GMT</pubDate><content:encoded><![CDATA[<p>git을 사용하는 대부분의 개발 팀에서는 약속된 git 브랜치 전략이나 git 커밋 메시지에 대한 컨벤션을 가지고 있다. <br>
내가 속한 개발 팀 역시 이러한 컨벤션을 가지고 있고, 간단하게는 아래와 같다.</p>

<ul>
<li>이슈 추적 시스템(issue tracking system)으로 JIRA를 사용하고 있고, 이슈에 해당하는 feature 브랜치를 따서 PR을 올린다.
<ul><li>ex) <code>feature/XXX-123</code></li></ul></li>
<li>PR에 속한 커밋에는 이슈 넘버를 메세지 앞에 붙인다.
<ul><li>ex) <code>[XXX-123] message...</code></li></ul></li>
</ul>

<p>이를 바탕으로 내가 자동화하고 싶은 요구사항을 커밋 메시지의 관점에서 정리하면,</p>

<ul>
<li><code>$ git commit -v</code> 으로 커밋을 생성할 때 적용된다.</li>
<li><code>feat/XXX-123</code> 또는 <code>feature/XXX-123</code> 형식의 이름을 가진 브랜치에서 커밋을 생성할 시에는 커밋 메시지 앞에 <code>[XXX-123]</code> 를 붙인다.
<ul><li>ex) commit message: <code>[XXX-123] message...</code> </li></ul></li>
<li><code>feat/XXX-123</code>, <code>feature/XXX-123</code> 형식의 이름이 아닌 브랜치에서 커밋을 생성할 시에는 커밋 메시지 앞에 <code>[NO-ISSUE]</code> 를 붙인다.
<ul><li>ex) commit message: <code>[NO-ISSUE] message...</code> </li></ul></li>
<li>Merge, Revert 커밋 메시지 앞에는 아무것도 붙이지 않는다.
<ul><li>ex) commit message: <code>Merge branch '...'</code></li></ul></li>
</ul>

<p>이 요구사항을 어떻게 해결하였는지에 대해 한번 정리해보았다. (시간이 없으신 분들은 맨 밑의 최종 스크립트만 확인해주셔도 될것 같다.) <br>
물론, 각 팀마다 컨벤션은 다르기 때문에 어디까지나 참고용이고 비슷한 작업이 필요할 때 제 글이 도움이 되길 빈다.</p>

<hr>

<h3 id="">자동화하기</h3>

<h4 id="preparecommitmsg">prepare-commit-msg</h4>

<p>git에서는 git의 각 액션별로 hook을 걸어 실행할 수 있는 스크립트 sample들을 <code>.git/hooks</code> 디렉토리 내에 제공해주고 있다. <code>*.sample</code> 로 끝나는 파일을 입맞에 맞게 편집한 후, <code>.sample</code>를 파일 이름에서 제거하면 스크립트는 해당하는 hook에 걸려 실행된다. <br>
많은 hook들 중에 우리가 건드려야할 건 <code>prepare-commit-msg</code> hook이다. 이 hook은 git이 커밋 메시지를 생성하고 나서 편집기를 실행하기 전에 실행된다.</p>

<p>먼저, <code>prepare-commit-msg</code> 파일을 생성해서 (또는 <code>prepare-commit-msg.sample</code>의 이름을 수정해서) 한번 테스트해보자.</p>

<pre><code class="language-bash">#!/bin/sh
# .git/hooks/prepare-commit-msg

COMMIT_MSG_FILE=$1  
DEFAULT_COMMIT_MSG=$(cat $COMMIT_MSG_FILE)  
echo "$DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE  
</code></pre>

<p><code>prepare-commit-msg</code> 이 실행될 때 첫번째 인자로 커밋 메시지 파일 이름을 받게 된다. 이 파일의 이름은 <code>.git/COMMIT_EDITMSG</code> 으로 커밋이 생성될 시 여기 파일에 커밋 메시지가 기록된다. <br>
위의 예제에서는 이 파일 전체를 읽어서 다시 이 파일에 그대로 쓰도록 스크립트를 짰기 때문에, 결국 우리가 지금처럼 커밋을 생성할 때와 아무런 차이가 없다.  </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-1-2.png" alt=""></p>

<h4 id="">브랜치 이름에서 이슈 넘버 추출</h4>

<p>먼저, 현재 내가 checkout한 브랜치 이름을 가져와야 한다.
이는 아래 git 명령어로 간단하게 가져올 수 있다.</p>

<pre><code class="language-bash">$ git rev-parse --abbrev-ref HEAD
master  
</code></pre>

<p><code>feat/XXX-123</code> 또는 <code>feature/XXX-123</code> 패턴에 해당하는 브랜치 이름에서 <code>XXX-123</code> 을 추출하기 위해 아래와 같이 스크립트를 작성해보았다.</p>

<pre><code class="language-bash">CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)  
ISSUE_TICKET=""  
if [[ $CURRENT_BRANCH_NAME =~ (feature|feat)/([A-Z]+-[0-9]+) ]]; then  
  ISSUE_TICKET="${BASH_REMATCH[2]}"
fi  
echo $ISSUE_TICKET  
</code></pre>

<p>여기서 위에서 선언하지도 않은 <code>BASH_REMATCH</code> 변수가 나와서 이건 뭐야 하는 분들도 계실텐데, 그런 분들은 <strong><a id="jupiny-blog-link" href="https://www.inflearn.com/community/questions/1109196/bash-rematch-%EB%B3%80%EC%88%98%EA%B0%80-%EC%96%B4%EB%94%94%EC%84%9C-%EB%82%98%EC%98%A8%EA%B1%B0%EC%A3%A0" target="_blank">여기</a></strong> 를 참고하시면 좋을 것 같다.</p>

<h4 id="">최종</h4>

<p>여기까지 종합해서, 요구사항에 맞춰 <code>prepare-commit-msg</code> 를 완성해보면,</p>

<pre><code class="language-bash">#!/bin/sh
# .git/hooks/prepare-commit-msg

COMMIT_MSG_FILE=$1  
DEFAULT_COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)  
ISSUE_TICKET="NO-ISSUE"  # 추출된 이슈 없는 경우 커밋 메시지는 "[NO_ISSUE] ..."  
if [[ $CURRENT_BRANCH_NAME =~ (feature|feat)/([A-Z]+-[0-9]+) ]]; then  
    ISSUE_TICKET="${BASH_REMATCH[2]}"
fi

echo "[$ISSUE_TICKET] $DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE  
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-2-2.png" alt=""></p>

<h4 id="_">최종_최종</h4>

<p>아, 여기서 한가지 요구사항 반영하지 않은 것이 있다.</p>

<blockquote>
  <p>Merge, Revert 커밋 메시지 앞에는 아무것도 붙이지 않는다.</p>
</blockquote>

<p>현재 위의 스크립트에서는 Merge, Revert 커밋이 생성될 시에도 아래처럼 이슈 넘버가 앞에 붙게 된다. </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-3-1.png" alt=""></p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-4-1.png" alt=""></p>

<p>커밋 메시지가 "Merge" 또는 "Revert" 로 시작할 시에는 이슈 넘버를 붙이지 않도록 수정하였다.</p>

<pre><code class="language-bash">#!/bin/sh
# .git/hooks/prepare-commit-msg

COMMIT_MSG_FILE=$1  
DEFAULT_COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)  
ISSUE_TICKET="NO-ISSUE"  # 추출된 이슈 없는 경우 커밋 메시지는 "[NO_ISSUE] ..."  
if [[ $CURRENT_BRANCH_NAME =~ (feature|feat)/([A-Z]+-[0-9]+) ]]; then  
  ISSUE_TICKET="${BASH_REMATCH[2]}"
fi

if grep -q -E "^Merge|^Revert" $COMMIT_MSG_FILE; then  
    # Merge, Revert 커밋은 이슈 넘버 붙이지 않음
    echo "$DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE
else  
    echo "[$ISSUE_TICKET] $DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE
fi
</code></pre>

<p><br>  </p>

<h4 id="_">최종_진짜최종</h4>

<p>사실 여기까지만 해놓아도, git을 일반적으로 사용하기에는 충분한 스크립트이다. <br>
하지만 git의 다양한 고급(?) 명령어들을 사용할 때 중복으로 이슈 넘버가 붙는 문제점이 생겨서 이를 위한 추가적인 예외 처리가 더 필요하다. (블로그에 이 글을 쓴 이유기도 하다.)</p>

<p>예를 들어,
나는 마지막 내 커밋을 수정할 때 <code>$ git commit --amend</code>, <br>
특정 커밋을 가져올 때 <code>$ git cherry-pick [commit sha]</code> 을 자주 사용한다. <br>
둘다 이미 존재하는 커밋을 이용한다는 공통점이 있는데, 이 경우에 이슈 넘버가 한번 더 붙게 되어 아래처럼 중복되게 된다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-5-1.png" alt=""></p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/08/automate-issue-number-input-in-git-commit-message-6.png" alt=""></p>

<p>사실 위의 Merge, Revert 커밋 예외 처리도 어떻게 보면 커밋 메시지 텍스트에 종속되는 workaround 였는데, 이제 commit ammend와 cherry-pick까지 대응하려면 결국 근본적으로 내가 생성할 커밋의 출처를 알아내야만 한다.</p>

<p>여기서 아까 위에서 언급된, <code>prepare-commit-msg</code> 의 sample 파일 (<code>prepare-commit-msg.sample</code>)을 다시 열어보자.</p>

<pre><code class="language-bash"># .git/hooks/prepare-commit-msg.sample
# ...
# ...
# The third example adds a Signed-off-by line to the message, that can
# still be edited.  This is rarely a good idea.

COMMIT_MSG_FILE=$1  
COMMIT_SOURCE=$2  
SHA1=$3  
</code></pre>

<p>사실 우리는 첫번째 인자, <code>COMMIT_MSG_FILE</code> 만 사용하였지만, 두번째 인자로 커밋의 출처를 의미하는 <code>COMMIT_SOURCE</code> 변수가 하나 더 들어온다.</p>

<p><strong><a id="jupiny-blog-link" href="https://git-scm.com/docs/githooks#_prepare_commit_msg" target="_blank">Git 공식 문서</a></strong> 에도 이 파라미터에 대한 설명을 볼 수 있다.</p>

<blockquote>
  <p>The second is the source of the commit message, and can be: <code>message</code> (if a <code>-m</code> or <code>-F</code> option was given); <code>template</code> (if a <code>-t</code> option was given or the configuration option <code>commit.template</code> is set); <code>merge</code> (if the commit is a merge or a <code>.git/MERGE_MSG</code> file exists); <code>squash</code> (if a <code>.git/SQUASH_MSG</code> file exists); or <code>commit</code>, followed by a commit object name (if a <code>-c</code>, <code>-C</code> or <code>--amend</code> option was given).</p>
</blockquote>

<p>하지만 이것만으로는 명확하게 이해가 안 되는 부분이 있어, 직접 테스트해보았고 각 케이스별 <code>COMMIT_SOURCE</code> 의 값을 정리해보았다.</p>

<ul>
<li><code>git commit -v</code>: 없음</li>
<li><code>git commit -m</code> (-m 옵션으로 커밋 생성): <code>message</code> </li>
<li><code>git revert</code> (Revert 커밋 생성): <code>merge</code></li>
<li><code>git merge</code> (Merge 커밋 생성): <code>merge</code></li>
<li><code>git commit --amend</code> (마지막 커밋 수정): <code>commit</code></li>
<li><code>git cherry-pick</code> (Cherry pick하여 생성): <code>message</code></li>
</ul>

<p>아까 Merge, Revert 커밋의 케이스까지 포함해서 결국 한마디로 </p>

<blockquote>
  <p><code>git commit -v</code>: 없음</p>
</blockquote>

<p>이 경우에만 이슈 넘버를 붙이고, 그 외의 경우는 모두 지금과 동일하게 커밋 메시지를 생성하면 된다는 결론이 나온다. <br>
아까의 if 조건을 <code>COMMIT_SOURCE</code> 값을 체크하는 조건으로 수정하면,</p>

<pre><code class="language-bash">#!/bin/sh
# .git/hooks/prepare-commit-msg

COMMIT_MSG_FILE=$1  
COMMIT_SOURCE=$2  
DEFAULT_COMMIT_MSG=$(cat $COMMIT_MSG_FILE)

CURRENT_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)  
ISSUE_TICKET="NO-ISSUE"  # 추출된 이슈 없는 경우 커밋 메시지는 "[NO_ISSUE] ..."  
if [[ $CURRENT_BRANCH_NAME =~ (feature|feat)/([A-Z]+-[0-9]+) ]]; then  
  ISSUE_TICKET="${BASH_REMATCH[2]}"
fi

if [ -n "$COMMIT_SOURCE" ]; then  
    echo "$DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE
else  
    echo "[$ISSUE_TICKET] $DEFAULT_COMMIT_MSG" &gt; $COMMIT_MSG_FILE
fi  
</code></pre>

<hr>

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

<ul>
<li><strong><a id="jupiny-blog-link" href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" target="_blank">8.3 Customizing Git - Git Hooks</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://www.inflearn.com/questions/1109196/bash-rematch-%EB%B3%80%EC%88%98%EA%B0%80-%EC%96%B4%EB%94%94%EC%84%9C-%EB%82%98%EC%98%A8%EA%B1%B0%EC%A3%A0" target="_blank">BASH_REMATCH 변수가 어디서 나온거죠?</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://git-scm.com/docs/githooks#_prepare_commit_msg" target="_blank">prepare-commit-msg</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[git stash 사용하기]]></title><description><![CDATA[<p>git으로 코드 형상을 관리하면서 로컬에서 열심히 작업하는 도중에, 아래와 같은 상황들이 종종 생긴다.</p>

<ul>
<li>현재 하고 있는 일을 잠깐 내려두고 다른 커밋부터 빠르게 만들어야하는 경우</li>
<li>최신 코드를 pull로 땡겨와야 하는 경우</li>
</ul>

<p>이 때 결국 working directory와 index(또는 staging area)를 깨끗하게 비우고, 새로운 작업을 시작해야 한다. working directory 또는 index에</p>]]></description><link>https://jupiny.com/2024/07/22/use-git-stash/</link><guid isPermaLink="false">c42866b4-84be-4f98-b046-2a296a875c61</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sun, 21 Jul 2024 17:11:00 GMT</pubDate><content:encoded><![CDATA[<p>git으로 코드 형상을 관리하면서 로컬에서 열심히 작업하는 도중에, 아래와 같은 상황들이 종종 생긴다.</p>

<ul>
<li>현재 하고 있는 일을 잠깐 내려두고 다른 커밋부터 빠르게 만들어야하는 경우</li>
<li>최신 코드를 pull로 땡겨와야 하는 경우</li>
</ul>

<p>이 때 결국 working directory와 index(또는 staging area)를 깨끗하게 비우고, 새로운 작업을 시작해야 한다. working directory 또는 index에 아직 내가 커밋하지 않은 작업이 올라가 있는 경우, pull을 하려고 했을 때 아래와 같은 에러 메시지가 발생하는 것을 자주 보았을 것이다.</p>

<pre><code class="language-bash">$ git pull origin new-branch
error: cannot pull with rebase: Your index contains uncommitted changes.  
error: please commit or stash them.  
</code></pre>

<p>이를 해결하기 위한 가장 단순한 방법으로는 아래 방법 중 하나를 선택하면 된다.</p>

<ul>
<li>내가 작업한 코드 되돌리기(즉, working directory와 index 비우기)</li>
<li>내가 작업한 코드 커밋하기</li>
</ul>

<p>하지만 내가 작업한 코드량이 많고 아직 미완성인 경우에는, 이를 모두 지우기에는 작업한 게 너무 아깝고 그렇다고 아직 완성된 작업은 아니라 커밋하기에는 애매하다. <br>
이 때 편리하게 사용할 수 있는 것이 바로 <code>$ git stash</code> 이다. stash는 영어로 "숨기는 장소"란 의미로 git에서 내 작업사항을 잠시 어딘가에 숨겨놓는다의 의미로 풀이할 수 있다.</p>

<hr>

<h3 id="stashgitstashgitstashpush">stash 저장: git stash (git stash push)</h3>

<p>내가 현재 작업한 사항이 아래처럼 working directory에 있다고 하자. </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-1.png" alt=""></p>

<p>이 때 다른 branch를 pull로 땡겨 오려고 하면, 에러가 발생할 것이다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-2.png" alt=""></p>

<p>이 때 이 변경사항을 stash로 로컬에 임시 저장해놓을 수 있다.</p>

<pre><code class="language-bash">$ git stash # 또는 git stash push
</code></pre>

<p>정식 명령어는 <code>$ git stash push</code> 이지만, 그냥 <code>$ git stash</code> 만 입력해도 된다. (<code>$ git stash save</code> 도 가능하지만 <code>save</code>는 이제 deprecate되었다고 한다.)</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-3.png" alt=""></p>

<p>stash로 저장한 후에는 이제 working directory와 index가 깨끗하게 비워졌음을 확인할 수 있다. (이젠 pull로 다른 branch를 땡겨올 수도 있다!!)</p>

<p>참고로 untracked file(즉, 기존에 없었던 새로 생성한 파일)들에 대해서는 기본적으로 git stash로는 저장되지 않는데,</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-4.png" alt=""></p>

<p>이 때 두 가지 방법을 사용해 stash로 저장할 수 있다.</p>

<ol>
<li><code>$ git add</code> 를 통해 tracked file로 만들어 index에 올린 후, <code>$ git stash</code>  </li>
<li><code>-u</code> 옵션을 이용해 모든 untracked file들도 함께 stash 저장, <code>$ git stash -u</code></li>
</ol>

<hr>

<h3 id="stashgitstashlist">stash 목록 조회: git stash list</h3>

<p>stash로 저장해놓은 내용들은 <code>list</code> 명령어로 조회할 수 있다.</p>

<pre><code class="language-bash">$ git stash list
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-5.png" alt=""></p>

<p>내가 방금 저장한 stash가 하나 출력된다. 아까 위에서처럼 어떠한 옵션도 없이 그냥 stash로 저장하면 기본적으로 아래 포맷으로 출력된다.</p>

<blockquote>
  <p><code>stash@{[stash의 index]} WIP on [현재 checkout한 branch]: [현재 HEAD commit의 hash] [현재 HEAD commit의 message]</code></p>
</blockquote>

<p>여기서 stash의 index란 0부터 증가하는 값이고, 가장 마지막에 push된 stash의 index가 0이 되고 그전에 push된 것의 index들은 1씩 늘어난다. <br>
물론 저장한 stash가 몇개 없고 거의 바로 꺼내쓰는 상황이라면 index만 볼수 있어도 딱히 큰 불편함은 없겠지만, 만약 저장한 stash가 많다면</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-6.png" alt=""></p>

<p>나의 기억력이 슈퍼컴퓨터가 아닌 이상 stash의 순서와 commit, branch 만으로는 각 stash가 어떤 작업을 의미하는지 알기 힘들 것이다.</p>

<p>그래서 가급적이면 <code>$ git stash push</code> 를 할 때, <code>--message</code>(또는 <code>-m</code>) 옵션을 이용해 각 stash에 대한 짧은 설명을 남겨놓는 습관을 들여놓는 것도 좋을 것 같다.</p>

<pre><code class="language-bash">$ git stash -m "[stash message]" # 또는 git stash push -m "[stash message]"
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-7.png" alt=""></p>

<hr>

<h3 id="stashgitstashpop">stash 가져오기: git stash pop</h3>

<p>push, pop 명령어로 알수 있듯이, stash는 기본적으로는 stack과 상당히 닮아있다. <br>
그래서 별다른 index를 지정하지 않는다면 0번째(<code>stash@{0}</code>), 즉 가장 마지막에 push된 것이 pop이 된다. (쉽게 stack의 LIFO를 생각하면 된다.) <br>
하지만 그렇다고 가장 마지막 push한 것만 가져올 수 있는 건 아니다. 특정 index 옵션을 주면 특정 위치의 stash를 가져올 수 있다.</p>

<pre><code class="language-bash">$ git stash pop [stash의 index]
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-8-1.png" alt=""></p>

<p>참고로 <code>apply</code> 라는 명령어도 있다.</p>

<pre><code class="language-bash">$ git stash apply [stash의 index]
</code></pre>

<p>위의 pop 명령어 결과 뒤에 출력된 <code>Dropped refs/stash@…</code> 메시지에서 알 수 있듯이, pop은 stash를 꺼냄과 동시에 삭제(drop)시킨다. 이와 다르게 apply는 stash의 작업 사항을 꺼내기만 하고, 저장된 건 그대로 유지시킨다. (하지만 나는 stash를 가져올 때는 보통 drop해도 되는 경우가 대부분이라 apply를 사용한 적은 거의 없었던 듯 하다.)</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-9-1.png" alt=""></p>

<p>아 모르겠고, 그냥 stash에서 삭제만 하고 싶다면? <code>$ git stash drop</code>를 사용하면 된다.</p>

<pre><code class="language-bash">$ git stash drop [stash의 index]
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-10-1.png" alt=""></p>

<p>참고로 <code>$ git stash pop</code> 을 한 작업이 현재 HEAD의 작업과 충돌이 발생할 수도 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-11.png" alt=""></p>

<p>보통 충돌 해결할 때와 마찬가지로 파일에서 직접 충돌을 해결한 이후에 커밋하면 된다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2024/07/use-git-stash-12.png" alt=""></p>

<p>이 때, 아까 위의 pop 명령어 출력결과에서도 알 수 있듯이</p>

<blockquote>
  <p>The stash entry is kept in case you need it again.</p>
</blockquote>

<p>커밋을 한 뒤에도, pop을 한 작업은 stash에 그대로 유지된다. <br>
아무래도 충돌이 발생하였다는 것 자체가 찝찝한 부분이고, 혹시 충돌 해결을 잘못하였거나 아까 pop한 작업을 다시 확인이 필요한 경우도 있기 때문에 stash에는 그대로 유지해주는 git의 작은 배려가 아닐까 싶다.</p>]]></content:encoded></item><item><title><![CDATA[vim에서 여러 줄 동시에 수정하기]]></title><description><![CDATA[<p>개발 업무를 진행하다보면, 여러 줄로 추출해낸 문자열들을 간단한 데이터 전처리를 통해 일괄적으로 수정해야 되는 경우가 빈번하게 생긴다. <br>
예를 들어,</p>

<ol>
<li>문자열 뒤에 콤마(,)를 붙여야 하는 경우  </li>
<li>전화번호 사이에 하이픈(-)을 중간에 넣어야 하는 경우  </li>
<li>원하는 날짜/시각 형식으로 바꿔야 하는 경우</li>
</ol>

<p>간단한 script를 짜서 일괄적으로 수정할 수도 있지만, 이것</p>]]></description><link>https://jupiny.com/2024/07/16/vim-edit-multiple-lines-simultaneously/</link><guid isPermaLink="false">a4084b5a-fbb4-497b-b7a3-33c464146af8</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Mon, 15 Jul 2024 16:45:04 GMT</pubDate><content:encoded><![CDATA[<p>개발 업무를 진행하다보면, 여러 줄로 추출해낸 문자열들을 간단한 데이터 전처리를 통해 일괄적으로 수정해야 되는 경우가 빈번하게 생긴다. <br>
예를 들어,</p>

<ol>
<li>문자열 뒤에 콤마(,)를 붙여야 하는 경우  </li>
<li>전화번호 사이에 하이픈(-)을 중간에 넣어야 하는 경우  </li>
<li>원하는 날짜/시각 형식으로 바꿔야 하는 경우</li>
</ol>

<p>간단한 script를 짜서 일괄적으로 수정할 수도 있지만, 이것 때문에 또 script를 짤려니 여간 귀찮은 게 아니다. <br>
만약 각 행의 문자열들이 동일한 패턴인 경우, 이때 vim을 이용하면 아주 빠르고 간편하게 일괄적으로 수정할 수 있다. <br>
이 글에서는, 개인적으로 자주 사용된다고 생각되는 몇가지 케이스에 대해서 각 케이스별로 한번 정리해보았다.</p>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2024/07/16/vim-edit-multiple-lines-simultaneously/#1">여러 줄에서 특정 문자 삽입하기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2024/07/16/vim-edit-multiple-lines-simultaneously/#2">여러 줄에서 특정 위치의 문자 제거하기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2024/07/16/vim-edit-multiple-lines-simultaneously/#3">여러 줄에서 맨 끝에 문자 추가하기</a></strong></li>
</ul>

<hr>

<h3 id="1">여러 줄에서 특정 문자 삽입하기</h3>

<p>다음과 같은 파일이 있다고 가정하자.</p>

<pre><code>01012345678  
01022229999  
01038481928  
01029472929  
01047439204  
01029572483  
01078739732  
01093274739  
01043849293  
01048294850  
</code></pre>

<p>이 문자열을 전화번호 형식, <code>XXX-XXXX-XXXX</code>, 에 맞게 일괄적으로 수정하고자 한다.</p>

<ol>
<li>맨 첫 줄에서 내가 입력할 위치에 커서를 위치시킨 후 <code>Ctrl</code> + <code>v</code> 를 입력한다. <br>
<ul><li>이 커서의 위치가 늘 헷갈리는데, 지금 커서에 위치한 문자가 뒤로 밀려난다고 생각하면 이해하기 쉽다. 위의 예에서는 <code>01012345678</code> 에서 <code>1</code>과 <code>5</code>에 위치시키면 된다.</li></ul></li>
<li><code>Shift</code> + <code>g</code>를 눌러 맨 아래까지 모두 블록을 씌운다. 그리고 내가 1번에서 위치시킨 커서로 다시 옮겨서, 모든 행에서 내가 입력할 위치의 문자만 블록을 씌운다. <br>
<ul><li>사실 행이 몇개 없는 경우는 이렇게까지 안해도 되고, 그냥 화살표 아래 방향(<code>j</code>) 으로 커서를 하나씩 직접 옮겨서 블록을 씌워도 된다. 하지만 만약 몇 백, 몇 천개의 행이라면.. 그렇게 할 수 없을 것이다.</li></ul></li>
<li><code>Shift</code> + <code>i</code> 를 입력한다.  </li>
<li>중간에 삽입할 문자를 입력한다. <br>
<ul><li>여기서는 <code>-</code></li></ul></li>
<li><code>Esc</code> 를 두번 입력한다.</li>
</ol>

<script src="https://asciinema.org/a/tVCFcifsddm3mau3r24hPwqco.js" id="asciicast-tVCFcifsddm3mau3r24hPwqco" async="true"></script>

<hr>

<h3 id="2">여러 줄에서 특정 위치의 문자 제거하기</h3>

<pre><code>010-1234-5678  
010-2222-9999  
010-3848-1928  
010-2947-2929  
010-4743-9204  
010-2957-2483  
010-7873-9732  
010-9327-4739  
010-4384-9293  
010-4829-4850  
</code></pre>

<p>이번엔 아까와 반대로 하이픈(-)을 모두 제거하여 <code>XXX-XXXX-XXXX</code> 형태로 일괄 수정하고자 한다.</p>

<ol>
<li>맨 첫 줄에서 내가 제거할 위치의 문자에 커서를 위치시킨 후 <code>Ctrl</code> + <code>v</code> 를 입력한다.  </li>
<li><code>Shift</code> + <code>g</code>를 눌러 맨 아래까지 모두 블록을 씌운다. 마찬가지로 내가 1번에서 위치시킨 커서로 다시 옮겨서, 모든 행에서 내가 제거할 위치의 문자만 블록을 씌운다.  </li>
<li><code>x</code> 를 입력하여 제거한다.</li>
</ol>

<script src="https://asciinema.org/a/jCUCWCHP3wFTHRKmThnCbgcTz.js" id="asciicast-jCUCWCHP3wFTHRKmThnCbgcTz" async="true"></script>

<hr>

<h3 id="3">여러 줄에서 맨 끝에 문자 추가하기</h3>

<pre><code>01012345678  
01022229999  
01038481928  
01029472929  
01047439204  
01029572483  
01078739732  
01093274739  
01043849293  
01048294850  
</code></pre>

<p>이번엔 각 문자열의 맨 끝에 콤마(,)를 붙여보자.</p>

<ol>
<li>앞선 방법들과 동일한 방법을 이용하여 각 행의 맨 마지막 문자만 블록을 씌운다.  </li>
<li><code>Shift</code> + <code>i</code> 를 입력한다.  </li>
<li>화살표 오른쪽(<code>→</code>)을 입력하여 맨끝의 문자 오른쪽에 커서를 위치시킨다.  </li>
<li>추가할 문자를 입력한다. <br>
<ul><li>여기서는 <code>,</code></li></ul></li>
<li><code>Esc</code> 를 두번 입력한다.</li>
</ol>

<script src="https://asciinema.org/a/4do1vcLZTuY3tvyh4wvU0xp5n.js" id="asciicast-4do1vcLZTuY3tvyh4wvU0xp5n" async="true"></script>

<hr>

<h3 id="">복합 케이스</h3>

<p>마지막으로 이 3가지가 모두 사용된 복합적인 케이스를 보여주며 이 글을 마친다.
(설명은 생략)</p>

<pre><code>2024-07-16 21:02:47  
2024-07-16 21:02:52  
2024-07-16 21:03:57  
2024-07-16 21:04:27  
2024-07-16 21:05:11  
2024-07-16 21:05:44  
2024-07-16 21:06:18  
2024-07-16 21:07:35  
2024-07-16 21:08:04  
2024-07-16 21:08:19  
</code></pre>

<p>이 문자열들을 <code>yyyy-MM-ddTHH:mm:ss+09:00</code> 형식으로 모두 일괄 변경한 예제이다.</p>

<script src="https://asciinema.org/a/FxiPD7ABvENO7S4yLkF0ccrfx.js" id="asciicast-FxiPD7ABvENO7S4yLkF0ccrfx" async="true"></script>]]></content:encoded></item><item><title><![CDATA[MySQL AUTO_INCREMENT counter의 진실]]></title><description><![CDATA[<p>현재 서비스에서 MySQL 5.7버전으로 데이터를 운용하고 있고, Storage Engine은 InnoDB를 사용하고 있다. 그리고 테이블들의 Primary Key(PK)는 모두 AUTO_INCREMENT로 설정되어 있다. 여기까지는 특별한 점이라곤 전혀 찾아볼 수 없는 아주 진부한 구성이다. <br>
하지만 여기서 이 끝없이 올라갈 것이라고 믿었던 이 PK가 예전에 생성되었던 값부터 다시 반복되는(?) 충격적인 모습을</p>]]></description><link>https://jupiny.com/2022/04/20/mysql-auto-increment-counter/</link><guid isPermaLink="false">0b2236a9-e4c3-4528-b8cf-243c02fd5a0f</guid><category><![CDATA[MySQL]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Tue, 19 Apr 2022 18:28:17 GMT</pubDate><content:encoded><![CDATA[<p>현재 서비스에서 MySQL 5.7버전으로 데이터를 운용하고 있고, Storage Engine은 InnoDB를 사용하고 있다. 그리고 테이블들의 Primary Key(PK)는 모두 AUTO_INCREMENT로 설정되어 있다. 여기까지는 특별한 점이라곤 전혀 찾아볼 수 없는 아주 진부한 구성이다. <br>
하지만 여기서 이 끝없이 올라갈 것이라고 믿었던 이 PK가 예전에 생성되었던 값부터 다시 반복되는(?) 충격적인 모습을 최근 관측하게 되었다.</p>

<p>그리고 DBA 분에 의해 최근 MySQL 서버 재기동이 있었음을 듣게 되었고, 그 시점이 이 현상이 발생하기 시작한 시점과 동일하다는 것을 알게되었다.</p>

<blockquote>
  <p>음.. 그런데 MySQL 서버 재기동이 왜?</p>
</blockquote>

<p>사실 이 때만해도 재기동이 AUTO_INCREMENT에 영향을 줄 것이라 전혀 생각하지 못했었다. 당연히 MySQL에서는 재기동 뒤에도 이전 AUTO_INCREMENT된 값 이후로 잘 증가시켜줄 것이라는 막연한 믿음이 있었다. <br>
DBA 분의 친절한 설명을 통해 AUTO_INCREMENT counter의 동작이 MySQL 8.0 이전과 이후 버전에 따라 다르다는 것을 알게 되었다. <br>
(개인적으로 MySQL 5.7 바로 다음 공식 버전이 8.0이라는 것이 의아해서 좀 찾아봤는데 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/8.0/en/faqs-general.html#faq-mysql-why-8.0" target="_blank">MySQL 공식문서 FAQ</a></strong> 에 아래와 같이 이유가 정리되어 있었다.)</p>

<blockquote>
  <p>Due to the many new and important features we were introducing in this MySQL version, we decided to start a fresh new series. As the series numbers 6 and 7 had actually been used before by MySQL, we went to 8.0.</p>
</blockquote>

<hr>

<h3 id="">실습</h3>

<blockquote>
  <p>macOS Big Sur, M1 환경에서 실습하였고 Docker 20.10.7 버전을 사용하였습니다.</p>
</blockquote>

<p>편리한 실습을 위해 각 버전별 <strong><a id="jupiny-blog-link" href="https://hub.docker.com/_/mysql" target="_blank">MySQL Docker 컨테이너</a></strong> 를 띄워 아래 과정을 테스트해보았다.</p>

<ol>
<li>테이블 생성  </li>
<li>Insert item1(pk: 1), item2(pk: 2), item3(pk: 3)  </li>
<li>Delete item3(pk: 3)  </li>
<li>MySQL 재기동  </li>
<li>AUTO_INCREMENT 확인</li>
</ol>

<hr>

<h4 id="mysql57">MySQL 5.7</h4>

<p>우선 재기동 이후에도 MySQL 데이터가 남아있어야하므로 docker volume을 생성하였다.</p>

<pre><code class="language-bash">$ docker volume create mysql-5.7-volume
</code></pre>

<p>그리고 이 volume을 mount한 MySQL 5.7 버전 컨테이너를 띄운 후 mysql에 접속하였다. </p>

<pre><code class="language-bash">$ docker pull --platform linux/amd64 mysql:5.7
$ docker run \
--platform linux/amd64 \
—-name mysql-5.7-server \
-e MYSQL_ROOT_PASSWORD=1234 \
-v mysql-5.7-volume:/var/lib/mysql \
-d -p 3306:3306 mysql:5.7
</code></pre>

<p>컨테이너의 mysql에 접속하여 1 ~ 3까지의 과정을 SQL로 실행하였다.</p>

<pre><code class="language-bash">$ docker exec -it mysql-5.7-server bash
root@4c9bcddfbb64:/# mysql -u root -p  
mysql&gt;  
</code></pre>

<pre><code class="language-sql">mysql&gt; -- 1. 테이블 생성  
mysql&gt; USE mysql;  
mysql&gt; CREATE TABLE jupiny_table (  
    -&gt; id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    -&gt; name varchar(10) NOT NULL
    -&gt; )
    -&gt; ENGINE=InnoDB;
mysql&gt; DESC jupiny_table;  
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(10)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(10) | NO   |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
mysql&gt; -- 2. Insert item1(pk: 1), item2(pk: 2), item3(pk: 3)  
mysql&gt; INSERT INTO jupiny_table(name) VALUES('item1');  
mysql&gt; INSERT INTO jupiny_table(name) VALUES('item2');  
mysql&gt; INSERT INTO jupiny_table(name) VALUES('item3');  
mysql&gt; SELECT * FROM jupiny_table;  
+----+-------+
| id | name  |
+----+-------+
|  1 | item1 |
|  2 | item2 |
|  3 | item3 |
+----+-------+
mysql&gt; -- 3. Delete item3(pk: 3)  
mysql&gt; DELETE FROM jupiny_table WHERE id=3;  
mysql&gt; SELECT * FROM jupiny_table;  
+----+-------+
| id | name  |
+----+-------+
|  1 | item1 |
|  2 | item2 |
+----+-------+
</code></pre>

<p>아래와 같은 쿼리 다음 생성될 AUTO_INCREMENT 값을 미리 확인해볼 수 있다.</p>

<pre><code class="language-sql">mysql&gt; SELECT AUTO_INCREMENT  
    -&gt; FROM information_schema.TABLES
    -&gt; WHERE TABLE_SCHEMA="mysql"
    -&gt; AND TABLE_NAME="jupiny_table";
+----------------+
| AUTO_INCREMENT |
+----------------+
|              4 |
+----------------+
</code></pre>

<p>컨테이너를 빠져나온 후, <code>docker restart</code> 로 mysql 서버를 재시작하였다.</p>

<pre><code class="language-bash">$ docker restart mysql-5.7-server
</code></pre>

<p>다시 mysql에 접속하여 다음 생성될 AUTO_INCREMENT 값을 동일하게 확인해보았다.</p>

<pre><code class="language-sql">mysql&gt; SELECT AUTO_INCREMENT  
    -&gt; FROM information_schema.TABLES
    -&gt; WHERE TABLE_SCHEMA="mysql"
    -&gt; AND TABLE_NAME="jupiny_table";
+----------------+
| AUTO_INCREMENT |
+----------------+
|              3 |
+----------------+
</code></pre>

<p><code>4</code>가 아닌 <code>3</code>으로 변경되어 있음을 확인할 수 있다. 여기서 데이터를 추가로 insert해보면,</p>

<pre><code class="language-sql">mysql&gt; INSERT INTO jupiny_table(name) VALUES('item4');  
mysql&gt; SELECT * FROM jupiny_table;  
+----+-------+
| id | name  |
+----+-------+
|  1 | item1 |
|  2 | item2 |
|  3 | item4 |
+----+-------+
</code></pre>

<p><code>3</code> 부터 다시 AUTO_INCREMENT 값이 증가함을 확인할 수 있다.</p>

<hr>

<h4 id="mysql80">MySQL 8.0</h4>

<p>이번엔 MySQL 8.0 버전 컨테이너를 띄워보았다.</p>

<pre><code class="language-bash">$ docker volume create mysql-8.0-volume
$ docker run \
--platform linux/amd64 \
--name mysql-8.0-server \
-e MYSQL_ROOT_PASSWORD=1234 \
-v mysql-8.0-volume:/var/lib/mysql \
-d -p 3306:3306 mysql:8.0
</code></pre>

<p>컨테이너의 mysql에 접속하여 1 ~ 3까지의 과정을 동일하게 실행해보았다. <br>
item3을 delete한 후, 다음 생성될 AUTO_INCREMENT 값을 확인해보았다.</p>

<pre><code class="language-sql">mysql&gt; SELECT AUTO_INCREMENT  
    -&gt; FROM information_schema.TABLES
    -&gt; WHERE TABLE_SCHEMA="mysql"
    -&gt; AND TABLE_NAME="jupiny_table";
+----------------+
| AUTO_INCREMENT |
+----------------+
|              4 |
+----------------+
</code></pre>

<p>그 후 8.0 컨테이너를 재시작한 후에</p>

<pre><code>$ docker restart mysql-8.0-server
</code></pre>

<p>한번더 확인해보았지만, 재시작 전과 동일하게 그대로 <code>4</code>임을 확인할 수 있다.</p>

<pre><code class="language-sql">mysql&gt; SELECT AUTO_INCREMENT  
    -&gt; FROM information_schema.TABLES
    -&gt; WHERE TABLE_SCHEMA="mysql"
    -&gt; AND TABLE_NAME="jupiny_table";
+----------------+
| AUTO_INCREMENT |
+----------------+
|              4 |
+----------------+
</code></pre>

<p>새로 insert한 데이터 역시 AUTO_INCREMENT 값이 4부터 계속 증가함을 확인할 수 있다.</p>

<pre><code class="language-sql">mysql&gt; INSERT INTO jupiny_table(name) VALUES('item4');  
mysql&gt; SELECT * FROM jupiny_table;  
+----+-------+
| id | name  |
+----+-------+
|  1 | item1 |
|  2 | item2 |
|  4 | item4 |
+----+-------+
</code></pre>

<hr>

<h3 id="">원인</h3>

<p>위 현상의 원인은 두 버전에서의 AUTO_INCREMENT counter값을 저장하는 방식의 차이에 있다. <br>
5.7 이하 버전에서 MySQL은 AUTO_INCREMENT counter값을 메모리에 저장하였다. 따라서 재기동 후에는 이 값이 소멸되기 때문에, 어쩔 수 없이 현재 테이블의 데이터 상태를 기반으로 새로 AUTO_INCREMENT counter 값을 다시 계산해야만 했다.</p>

<pre><code class="language-sql">SELECT MAX(ai_col) FROM table_name FOR UPDATE;  
</code></pre>

<p>이렇게 현재 존재하는 AUTO_INCREMENT 컬럼의 최대값을 기준으로 counter가 초기화되기 때문에, 이후 삭제된 AUTO_INCREMENT 값으로 다시 증가했던 것이다.</p>

<p>8.0 이후부터는 AUTO_INCREMENT counter 값을 디스크에 저장하는 방식으로 변경되었다. (정확히는 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/8.0/en/innodb-redo-log.html" target="_blank">redo log</a></strong> 에 쓰인다.) 그래서 재기동이 되더라도 counter값이 남아있기 때문에 한번 AUTO_INCREMENT된 값은 다시 생성될 수 없고 영원히 계속 증가하게 된다.</p>

<hr>

<h3 id="">해결책</h3>

<p>당연한 소리지만, 우선 이러한 문제가 해결된 최신버전인 MySQL 8.0 이후 버전을 사용하는 것이 가장 좋을것 같다. (기승전업그레이드 아니던가.) <br>
하지만 어떠한 이유로 만약 아직 MySQL을 8.0 이상 버전으로 업그레이드를 못하고 이전 버전을 사용하고 있는 상태라면, 가급적이면 DELETE 쿼리를 사용해서 실제 record를 날려버리지 말고, 일종의 삭제 상태로 UPDATE하는 방식이 안전할 것 같다. <br>
DB 서버 재기동이 흔한 일은 아니지만, 언제든 서버는 모종의 이슈(장애, Migration, 설정 변경 등)로 재기동이 발생할 수 있다는 것을 가정해야한다. <br>
이때 만약 이전에 삭제되었던 PK들이 다시 등장하고 또 운나쁘게 이전에 이 PK들을 참조하고 있었던 데이터들이 있다면.. 디버깅하기도 힘든 생각만해도 아찔한 카오스가 실제 서비스에서 펼쳐질 수 있을 것이다.</p>

<hr>

<h3 id="">출처</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/8.0/en/innodb-auto-increment-handling.html#innodb-auto-increment-initialization" target="_blank">InnoDB AUTO_INCREMENT Counter Initialization</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://www.tutorialspoint.com/how-to-get-the-next-auto-increment-id-in-mysql" target="_blank">How to get the next auto-increment id in MySQL?</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[개발 블로그를 5년간 운영하며]]></title><description><![CDATA[<p>2016년 9월에 첫 글을 기재하였으니, 어느덧 이 블로그를 운영한지도 거의 5년이라는 시간이 다되간다. (이 블로그로 옮기기 전, <strong><a id="jupiny-blog-link" href="https://jupiny.tistory.com/" target="_blank">Tistory</a></strong> 에 최초로 글을 올리기 시작한 시점을 기준으로 하면 사실 이미 5년은 넘었다.) <br>
안수찬 강사님의 개발 블로그를 보고 감명받아 처음 개발 블로그를 시작했고, 하나 둘 글을 올리다보니 현재 총 60개정도의 글이 블로그에 올라가있다.</p>]]></description><link>https://jupiny.com/2021/07/21/developer-blog-retrospect/</link><guid isPermaLink="false">f8e62e9a-8b75-41c3-93e1-7ca945f2f5f0</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Tue, 20 Jul 2021 15:07:01 GMT</pubDate><content:encoded><![CDATA[<p>2016년 9월에 첫 글을 기재하였으니, 어느덧 이 블로그를 운영한지도 거의 5년이라는 시간이 다되간다. (이 블로그로 옮기기 전, <strong><a id="jupiny-blog-link" href="https://jupiny.tistory.com/" target="_blank">Tistory</a></strong> 에 최초로 글을 올리기 시작한 시점을 기준으로 하면 사실 이미 5년은 넘었다.) <br>
안수찬 강사님의 개발 블로그를 보고 감명받아 처음 개발 블로그를 시작했고, 하나 둘 글을 올리다보니 현재 총 60개정도의 글이 블로그에 올라가있다. <br>
회사를 다니기 시작한 후로는, 회사 업무로 시간이 부족하기도 하고 또 연차가 올라감에 따라 조금더 깊이있는 글을 써야된다는 스스로의 압박감(?)으로 최근에는 글을 잘 못 쓰고 있다. 아니 사실 다 변명이고, 안 쓰고 있다로 보는게 맞는 것 같다. 최근 1년동안 쓴 글이 10개도 채 안되는것 같다.(반성) <br>
5주년을 맞아 그동안 블로그를 운영하며 느낀 여러가지 생각과 느낀점들을 한번 정리해보고 싶다는 생각이 들어, 이 글을 쓰게 되었다.</p>

<hr>

<h3 id="">나만의 개발 블로그 작성 원칙</h3>

<p>글을 하나둘씩 쓰다보니 이후에는 자연스럽게 나만의 개발 블로그 글 작성 원칙이 생겼던 것 같다.</p>

<ul>
<li>어떤 주제로 글을 쓰려할  때 그 주제에 대해 다른 사람이 이미 작성한 글이 있고, 더 추가되는 내용이 없거나 그것보다 더 잘 작성할 자신이 없다면 쓰지 않는다.</li>
<li>개념 설명만 주구장창 하지 않는다. 실제 동작을 확인할 수 있는 실습 예제를 함께 넣는다.</li>
<li>텍스트만 많으면 지루해보이고 안 읽고 싶어진다. 이해를 돕기 위한 이미지나 코드를 최대한 중간중간 함께 넣는다.</li>
</ul>

<p>최근에는 이 나만의 원칙들을 대부분 따라서 계속 글을 쓰고 있는 듯하다. 이러한 원칙들이 하나둘 모여 결국 내 개발 블로그의 identity를 형성해주기 때문에, 이렇게 나만의 원칙을 만들어보는것도 좋은 것 같다. </p>

<hr>

<h3 id="">개발 블로그의 장점</h3>

<p>사실 개발 블로그를 쓰면 좋은점이 많을 것이라는 것은 누구나 다 아는 사실이다. 이 글에서는 직접 5년동안 블로그를 운영하면서 내가 느꼈던 블로그의 장점들을 주관적으로 써내려보았다.</p>

<h4 id="">블로그 작성을 위한 공부</h4>

<p>이는 내가 개발 블로그을 시작한 최초 목적 중 하나이기도 하지만, 블로그는 내 개발 공부에 큰 도움이 되었다. 대충 알고 있던 내용들도 막상 글로 정리해서 모든 사람들에게 공개하려다보니, 더욱 꼼꼼하게 알아보게 되었고 글을 쓰다보니 혼란했던 개념들이 머리속으로 잘 정리되었다. <br>
예전에는 주로 내가 공부한 내용들을 어딘가에 정리하고싶었고, 그 내용을 블로그에 올렸었다. 하지만 요즘은 반대로 아예 내가 공부하고 싶은 주제를 미리 정한 다음, 그 주제로 글부터 써내려가면서 공부하는 경우도 종종 있는것 같다. (블로그 driven 개발 공부?)</p>

<h4 id="stackoverflow">나만의 Stack Overflow</h4>

<p>사실 나는 이 블로그의 작성자이기도 하지만, 또한 가장 우수 고객이 아닐까 싶다. 내가 해결했던 문제들을 다시 만났을 때, 그 당시 어떻게 해결했었는지 기억이 안나는 경우가 태반이다. 그때 내가 정리해놓은 글이 있다면, 그때 내가 실행했었던 커맨드나 작성했던 코드들을 쉽게 가져와서 재사용할 수 있다. 블로그는 때로는 나만의 Stack Overflow다.</p>

<h4 id="">글쓰기 능력</h4>

<p>사실 현업을 해보신 분들이라면, 개발자가 단순히 코딩만 하는게 아니라, "문서화"에 들이는 시간이 생각보다 많다는 것을 알 것이다. 나 또한 업무를 하며 문서화에 시간을 많이 할애하는 편인데, 개발자에게 있어 이 문서화도 중요한 능력이라고 생각한다. 글을 조리있게 작성하여 주변 개발자들이 내가 말하고자 하는바를 쉽게 이해하도록 만들어야 한다. <br>
블로그에 글을 쓰면서, 이러한 능력이 나도 모르게 많이 키워졌던 것 같다. 특히 항상 블로그 글을 쓰기전에 글의 구조를 먼저 정해놓고, 세부적인 내용을 채우는 습관들은 현업에서 문서화를 할 때에도 큰 도움이 되었다.</p>

<h4 id="">댓글</h4>

<p>블로그 글에 댓글이 가능해지는 순간, 독자와 쌍방향으로 커뮤니케이션할 수 있는 채널이 생기는 것이다. <br>
대부분의 댓글들은 글이 도움이 되었다는 감사한 마음을 표현해주시는 내용이고,</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-14.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-2.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-15.png" alt=""></p>

<p>그 외에 글과 관련된 질문이나 피드백들도 많이 달아주신다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-4.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-5.png" alt=""></p>

<p>사실 블로그를 운영하면서, 감사함을 표현해주시는 댓글을 볼때 가장 큰 보람을 느끼는 듯 하다. 내가 이 맛으로 블로그하지!라는 생각이 절로 든다. 그러한 댓글이 없었다면, 내가 과연 여태까지 블로그를 운영할 수 있었을까 싶다. <br>
또한 댓글로 질문이나 지적을 해주시면, 내가 미처 생각지 못했던 부분에 대해서 다시 한번 생각해보게 되고 덕분에 글의 잘못된 내용을 수정할 수 있었다. 댓글을 남겨주신 모든 분들께 이 글을 빌어 다시 한번 감사한 마음을 전하고 싶다. <br>
나도 이러한 작성자의 마음을 너무나도 잘 알기 때문에, 내가 인상깊게 본 블로그는 감사한 마음을 댓글로 최대한 남길려고 노력하고 있다. 특히 이때 댓글을 달 수 없는 글이라면, 개인적으로 안타까운 마음이 든다.    혹시 아직 본인의 블로그에 댓글 시스템을 달아 놓지 않았다면, 지금이라도 당장 달아볼 것을 적극 추천하고 싶다. (많이 쓰이는 댓글 플러그인은 <strong><a id="jupiny-blog-link" href="https://disqus.com/" target="_blank">Disqus</a></strong>, <strong><a id="jupiny-blog-link" href="https://developers.facebook.com/docs/plugins/comments?locale=ko_KR" target="_blank">Facebook</a></strong> 정도가 아닐까 싶다.)</p>

<p>블로그를 운영하며 가장 인상깊었던 댓글은,</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-6.png" alt=""></p>

<p>이 댓글을 본 후, 매달 내고 있는 서버 비용을 아까워하지말고 평생 블로그를 운영해야겠다고 다짐하게 되었던 것 같다.</p>

<h4 id="">광고 수익</h4>

<p>블로그로도 돈을 벌 수 있다. 바로 블로그의 유일한 수입원이라 볼 수 있는 "광고" 덕분이다. <br>
블로그를 시작한 시간에 비해 나는 비교적 광고를 늦게 달긴했지만, 최근에 드디어  <strong><a id="jupiny-blog-link" href="https://www.google.co.kr/adsense/start/" target="_blank">Google Adsense</a></strong> 광고비 지급 가능금액을 달성하였다. (!)</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-7.png" alt=""></p>

<p>그동안 내가 여태까지 낸 서버 비용에 비하면 적은 금액이고 앞으로도 매달 계속 적자일테지만, 수익이 생긴다는것 자체가 소소한 재미가 아닐까 싶다. 만약 개발 블로그 광고 수익으로 큰 돈을 벌 수 있을 정도의 능력을 가진 분이라면, 이미 높은 연봉을 받고 있는 슈퍼 개발자이지 않을까...</p>

<h4 id="">트래픽</h4>

<p>개발 블로그도 결국 하나의 웹 서비스이다. 블로그에 글이 늘어나고, 운영 기간이 길어질수록 트래픽은 자연스럽게 늘어나게 된다. 내가 만든 웹 서비스의 트래픽이 늘어난다는건 개발자에게는 누구나 즐거운 일이 아닐까 싶다. <br>
<strong><a id="jupiny-blog-link" href="https://search.google.com/search-console/about" target="_blank">Google Search Console</a></strong> 을 이용하면, 단순 검색 트래픽 뿐만 아니라 어떤 글이 인기가 많은지, 어떤 검색어로 유입이 됬는지 등 다양한 통계들을 확인할 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-8.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-9.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-10.png" alt=""></p>

<p>git에서 push한 commit을 되돌리는 <strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/03/19/revert-commits-in-remote-repository/" target="_blank">글</a></strong> 을 예전에 하나 작성한 적이 있었는데, 이 글이 가장 인기가 많은 것이 흥미로웠다. 그만큼 git에 잘못 push해서 되돌리고 싶어하는 분들의 절박한 마음(?)들도 느낄 수 있었다. <br>
이러한 통계들을 확인해보는 것도 블로그를 운영하는데 있어서 하나의 재미지 않을까 싶다.</p>

<h4 id="prpublicrelation">자기 PR(Public Relation)</h4>

<p>개발 블로그는 개발자에게 있어, 본인을 잘 알릴 수 있는 홍보 수단이 되기도 한다. <strong><a id="jupiny-blog-link" href="https://github.com/" target="_blank">GitHub</a></strong> 이나 <strong><a id="jupiny-blog-link" href="https://www.linkedin.com/" target="_blank">LinkedIn</a></strong> 처럼, 개발 블로그 역시 개발자 스스로의 identity를 표현할 수 있는 중요한 수단이 될 수 있다. <br>
특히 나는 내가 블로그에 작성했던 글들을 통해 우연히 좋은 기회들을 많이 얻었다. 예를 들어, 개발자로서 취업을 준비했던 경험을 공유했던 <strong><a id="jupiny-blog-link" href="https://jupiny.com/2018/01/31/prepare-for-employment-as-developer/" target="_blank">글</a></strong> 을 올린 적이 있었는데, <strong><a id="jupiny-blog-link" href="https://fastcampus.co.kr/" target="_blank">패스트캠퍼스</a></strong> 관계자 분께서 그 글을 보시고 먼저 연락을 해주셨다. </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-11.jpeg" alt=""></p>

<p>그리고 이후에 <strong><a id="jupiny-blog-link" href="https://www.facebook.com/fastcampusschool/" target="_blank">패스트캠퍼스 School 페이스북</a></strong> 에 글이 공유가 되면서, 덕분에 글의 조회수가 꽤 올라갔었던 기억이 난다. (현재도 가장 댓글을 많이 받은 글 중 하나이다.)  </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/07/developer-blog-retrospect-13.png" alt=""></p>

<p>또 사내 오픈 소스인 Armeria를 공부하면서 개인적으로 정리해서 올린 글들이 있었는데, 우연히 사내 오픈 소스 개발팀에서 제 글을 보시고 사내 블로그에 올려보는게 어떻냐고 제안해주셨다. 나 또한 너무 영광이었기 때문에 흔쾌히 수락했고, 덕분에 사내 블로그에 글을 올리는 영예를 얻을 수 있었다.</p>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://engineering.linecorp.com/ko/blog/try-armeria-circuit-breaker/" target="_blank">Armeria의 서킷 브레이커 사용해 보기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://engineering.linecorp.com/ko/blog/armeria-prometheus-monitoring/" target="_blank">Armeria에서 Prometheus 지표 모니터링하기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://engineering.linecorp.com/ko/blog/armeria-metrics-customizing/" target="_blank">Armeria 지표 커스터마이징하기</a></strong></li>
</ul>

<p>실력에 비해 본인을 알리고 싶어하지 않는 은둔형 개발자(?)들도 꽤 많이 존재하는 듯하지만, 개인적으로는 이렇게 이름을 알리는게 커리어 면에서 좋다고 생각하는 편이라서 이런 저같은 부류의 개발자 분들께 개발 블로그를 적극 추천드리고 싶다 :)</p>

<hr>

<h3 id="">글을 마치며</h3>

<p>사실 이렇게까지 글을 길게 쓸 생각은 없었는데, 생각나는 개발 블로그의 장점을 하나둘씩 추가하다보니 어느새 글이 꽤 길어진 것 같다.
또 이렇게 장점들을 하나씩 나열하다보니, 블로그를 앞으로도 계속 운영해야겠다는 마음이 더욱더 불타오른다. <br>
혹시나 아직 개발 블로그를 시작할지 고민하는 분이라면, 이 글이 결정을 내리는데 큰 도움이 되길 바란다.</p>]]></content:encoded></item><item><title><![CDATA[Armeria에서 Prometheus 메트릭 커스터마이징하기]]></title><description><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://jupiny.com/2021/01/03/armeria-metric-monitoring-by-prometheus/" target="_blank">이전 글</a></strong> 에서 Spring Actuator, Grafana를 활용하여 Prometheus로 Armeria 서버 metric을 모니터링하는 방법을 살펴봤다.
이번 글에서는 내 입맛에 맞게 Armeria에서의 Prometheus 메트릭 설정을 커스터마이징하는 방법을 다루어 보려고 한다.</p>

<hr>

<h3 id="springactuator">Spring Actuator 이용하지 않기</h3>

<p>이전 글에서는 Spring Actuator를 이용하여 Prometheus 메트릭들을 <code>/actuator/prometheus</code> 경로로 노출하였다. <br>
하지만 사실 <code>PrometheusExpositionService</code> 를 이용하면 Spring Actuator를</p>]]></description><link>https://jupiny.com/2021/04/14/how-to-customize-armeria-prometheus-metric/</link><guid isPermaLink="false">aaf9d9fb-25cb-4136-88c4-45e2e3f990dd</guid><category><![CDATA[Java]]></category><category><![CDATA[Spring]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Tue, 13 Apr 2021 16:39:41 GMT</pubDate><content:encoded><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://jupiny.com/2021/01/03/armeria-metric-monitoring-by-prometheus/" target="_blank">이전 글</a></strong> 에서 Spring Actuator, Grafana를 활용하여 Prometheus로 Armeria 서버 metric을 모니터링하는 방법을 살펴봤다.
이번 글에서는 내 입맛에 맞게 Armeria에서의 Prometheus 메트릭 설정을 커스터마이징하는 방법을 다루어 보려고 한다.</p>

<hr>

<h3 id="springactuator">Spring Actuator 이용하지 않기</h3>

<p>이전 글에서는 Spring Actuator를 이용하여 Prometheus 메트릭들을 <code>/actuator/prometheus</code> 경로로 노출하였다. <br>
하지만 사실 <code>PrometheusExpositionService</code> 를 이용하면 Spring Actuator를 사용하지 않고도 내가 설정한 path로 메트릭들을 노출시킬 수 있다.</p>

<pre><code class="language-java">@Configuration
public class ArmeriaServerConfiguration {

    // 추가
    @Bean
    public ArmeriaServerConfigurator prometheusConfigurator(PrometheusMeterRegistry registry) {
        return server -&gt; server
                .service("/metrics",
                         new PrometheusExpositionService(registry.getPrometheusRegistry()));
    }

    @Bean
    public ArmeriaServerConfigurator serverConfigurator(MyAnnotatedService myService) {
        return server -&gt; server
                .annotatedService("/", myService);
    }
}
</code></pre>

<p><code>/metrics</code> 라는 path로 <code>PrometheusExpositionService</code>를 띄운 후, 해당 경로로 접근하면 이전과 동일하게 Prometheus 메트릭들을 확인할 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/04/how-to-customize-armeria-prometheus-metrics-1.png" alt=""></p>

<hr>

<h3 id="meterid">Meter Id 이름 변경하기</h3>

<p>어떤 설정도 하지 않았다면, Armeria에서는 기본적으로 서버 메트릭들에 <code>armeria.server</code>라는 prefix를 붙인다.  </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/04/how-to-customize-armeria-prometheus-metrics-2.png" alt=""></p>

<p>이 부분은 나만의 <code>MeterIdPrefixFunction</code> Bean을 만들어  inject하는 방법으로 내가 원하는 이름으로 변경할 수 있다. </p>

<pre><code class="language-java">@Configuration
public class ArmeriaServerConfiguration {

    @Bean
    public ArmeriaServerConfigurator prometheusConfigurator(PrometheusMeterRegistry registry) {
        return server -&gt; server
                .service("/metrics",
                         new PrometheusExpositionService(registry.getPrometheusRegistry()));
    }

    @Bean
    public ArmeriaServerConfigurator serverConfigurator(MyAnnotatedService myService) {
        return server -&gt; server
                .annotatedService("/", myService);
    }

    // 추가
    @Bean
    public MeterIdPrefixFunction meterIdPrefixFunction() {
        return MeterIdPrefixFunction.ofDefault("my.server");
    }
}
</code></pre>

<p>Metric들의 prefix가 <code>my.server</code>로 모두 변경되었음을 확인할 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/04/how-to-customize-armeria-prometheus-metrics-3.png" alt=""></p>

<hr>

<h3 id="metric">Metric 필터링하기</h3>

<p>많은 metric 중에서 내가 원하는 metric만 필터링해서 노출시키고 싶을 수 있다.
이 때 <code>MeterFilter</code> Bean을 inject하여 요구사항에 맞게 필터링 설정을 할수 있다. <br>
예를 들어 위에서 이름을 변경했었던 <code>my.server</code> prefix가 붙은 메트릭들만 노출시키고 싶다고 한다면,</p>

<pre><code class="language-java">@Configuration
public class ArmeriaServerConfiguration {

    @Bean
    public ArmeriaServerConfigurator prometheusConfigurator(PrometheusMeterRegistry registry) {
        return server -&gt; server
                .service("/metrics",
                         new PrometheusExpositionService(registry.getPrometheusRegistry()));
    }

    @Bean
    public ArmeriaServerConfigurator serverConfigurator(MyAnnotatedService myService) {
        return server -&gt; server
                .annotatedService("/", myService);
    }

    @Bean
    public MeterIdPrefixFunction meterIdPrefixFunction() {
        return MeterIdPrefixFunction.ofDefault("my.server");
    }

    // 추가
    @Bean
    public MeterFilter meterFilter() {
        return new MeterFilter() {
            @Override
            public MeterFilterReply accept(Id id) {
                return id.getName().startsWith("my.server") ?
                       MeterFilterReply.ACCEPT : MeterFilterReply.DENY;
            }
        };
    }
}
</code></pre>

<p>이전에 함께 노출됬었던 jvm, cpu 등의 메트릭은 이제 보이지 않고, <code>my.server.*</code> 메트릭들만 노출되는 것을 확인할 수 있다. </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/04/how-to-customize-armeria-prometheus-metrics-4.png" alt=""></p>]]></content:encoded></item><item><title><![CDATA[MySQL Query Cache은 무조건 좋을까? (Feat. query cache lock)]]></title><description><![CDATA[<p>MySQL에서는 한 SELECT 쿼리의 결과를 캐싱해주는 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/5.7/en/query-cache.html" target="_blank">Query Cache</a></strong> 라는 최적화 기능을 제공해주고 있다. <br>
(하지만, 5.7.20 버전부터 deprecate되었고, 8.0 버전에서부터는 제거되었다고 한다. - <strong><a id="jupiny-blog-link" href="https://mysqlserverteam.com/mysql-8-0-retiring-support-for-the-query-cache/" target="_blank">참고 글</a></strong>) <br>
그렇다면, Query Cache은 항상 사용하면 좋은 것일까? (모든 캐싱이 그러하듯, 당연히 아닐 듯 하다.) 실습과 함께 한번 확인해보자.</p>

<hr>

<h3 id="">실습 준비</h3>

<blockquote>
  <p><strong><a id="jupiny-blog-link" href="https://hub.docker.com/_/mysql" target="_blank">MySQL 5.7.</a></strong></p></blockquote>]]></description><link>https://jupiny.com/2021/01/11/mysql-query-cache-disadvantage/</link><guid isPermaLink="false">185e7d4d-ac19-4b02-8053-43dafc0415ac</guid><category><![CDATA[MySQL]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sun, 10 Jan 2021 16:29:10 GMT</pubDate><content:encoded><![CDATA[<p>MySQL에서는 한 SELECT 쿼리의 결과를 캐싱해주는 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/5.7/en/query-cache.html" target="_blank">Query Cache</a></strong> 라는 최적화 기능을 제공해주고 있다. <br>
(하지만, 5.7.20 버전부터 deprecate되었고, 8.0 버전에서부터는 제거되었다고 한다. - <strong><a id="jupiny-blog-link" href="https://mysqlserverteam.com/mysql-8-0-retiring-support-for-the-query-cache/" target="_blank">참고 글</a></strong>) <br>
그렇다면, Query Cache은 항상 사용하면 좋은 것일까? (모든 캐싱이 그러하듯, 당연히 아닐 듯 하다.) 실습과 함께 한번 확인해보자.</p>

<hr>

<h3 id="">실습 준비</h3>

<blockquote>
  <p><strong><a id="jupiny-blog-link" href="https://hub.docker.com/_/mysql" target="_blank">MySQL 5.7.20 Docker</a></strong> Container 환경에서 실습하였습니다. </p>
</blockquote>

<p>참고로 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_query_cache_type" target="_blank"><code>query_cache_type</code></a></strong> 의 default 값은 <code>OFF</code>이다. 이 값은 MySQL을 구동한 이후에는 변경할 수 없으므로, 아래와 같이 설정파일을 변경한 후, 한번 재기동해주어야 한다.</p>

<pre><code class="language-apacheconf">[mysqld]
query_cache_type = 2 # or DEMAND  
</code></pre>

<p>MySQL 서버에 접속해서 Query Cache가 잘 설정되어 있는지 확인해보자.</p>

<pre><code class="language-sql">mysql&gt; SHOW VARIABLES LIKE 'have_query_cache';  
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| have_query_cache | YES   |
+------------------+-------+
1 row in set (0.00 sec)

mysql&gt; SHOW VARIABLES LIKE 'query_cache_size';  
+------------------+---------+
| Variable_name    | Value   |
+------------------+---------+
| query_cache_size | 1048576 |
+------------------+---------+
1 row in set (0.01 sec)

mysql&gt; SHOW VARIABLES LIKE 'query_cache_type';  
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| query_cache_type | DEMAND |
+------------------+--------+
1 row in set (0.00 sec)  
</code></pre>

<p>Query Cache의 효과를 더 눈에 띄게 확인하기 위해서, <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/employee/en/employees-installation.html" target="_blank">Employees Sample Database</a></strong> 를 이용해 다량의 데이터들을 생성하였다.</p>

<pre><code class="language-sql">mysql&gt; use employees;  
mysql&gt; show tables;  
+----------------------+
| Tables_in_employees  |
+----------------------+
| current_dept_emp     |
| departments          |
| dept_emp             |
| dept_emp_latest_date |
| dept_manager         |
| employees            |
| salaries             |
| titles               |
+----------------------+
</code></pre>

<hr>

<h3 id="">장점</h3>

<p>MySQL Query Cache 설정이 켜져있고 Query Cache를 사용하는 쿼리라면, MySQL는 요청으로 들어온 쿼리에 대해서 먼저 Query Cache를 조회해본다. 해당 쿼리가 Query Cache에 존재하는 경우라면, 기존처럼 아래 3단계를 거치지않고,</p>

<ol>
<li>Parsing  </li>
<li>Optimizing  </li>
<li>Executing</li>
</ol>

<p>바로 캐시에 담겨있는 데이터를 반환하게 된다.</p>

<p>따라서 쿼리의 비용이 크고, 반복적으로 호출되는 쿼리일수록 리소스 면에서 큰 이점을 얻을 수 있다. <br>
이 때 캐시의 키가 되는 <code>SELECT</code> 쿼리는 byte 값까지 동일해야 동일한 키로 간주된다. 즉 대소문자까지 완벽히 동일해야한다.</p>

<pre><code class="language-sql">-- Query Cache 미사용
mysql&gt; SELECT count(*) FROM salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588322 |
+----------+
1 row in set (0.45 sec)

mysql&gt; SELECT count(*) FROM salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588322 |
+----------+
1 row in set (0.43 sec)


-- Query Cache 사용
mysql&gt; SELECT SQL_CACHE count(*) FROM salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588322 |
+----------+
1 row in set, 1 warning (0.42 sec)

mysql&gt; SELECT SQL_CACHE count(*) FROM salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588322 |
+----------+
1 row in set, 1 warning (0.01 sec)

-- SELECT의 's' 소문자
mysql&gt; sELECT SQL_CACHE count(*) FROM salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588322 |
+----------+
1 row in set, 1 warning (0.43 sec)  
</code></pre>

<hr>

<h3 id="">단점</h3>

<p>Query Cache를 사용할 때의 가장 주의할 점은, 만약 대상 테이블에 대한 변경(ex. <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>)이 있었다면 묻고 따지지도 않고 기존의 캐시를 제거한다는 점이다. (처음에는 테이블 단위인 점이 의아했지만, 조금더 생각해보니 이렇게 말고는 딱히 좋은 방법은 없었을 듯하다.)  </p>

<pre><code class="language-sql">mysql&gt; SELECT SQL_CACHE count(*) from salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588270 |
+----------+
1 row in set, 1 warning (00 sec)

-- 업데이트
mysql&gt; UPDATE salaries SET salary = salary + 1 WHERE salary BETWEEN 70000 AND 75000;  
Query OK, 230785 rows affected (2.07 sec)  
Rows matched: 230785  Changed: 230785  Warnings: 0

mysql&gt; SELECT SQL_CACHE count(*) from salaries WHERE salary BETWEEN 60000 AND 70000;  
+----------+
| count(*) |
+----------+
|   588270 |
+----------+
1 row in set, 1 warning (0.49 sec)  
</code></pre>

<p>Query Cache는 여러 세션(쓰레드)들이 공유하는 자원이기 때문에, 동기화를 위한 lock이 필요하다. (이를 <strong>"query cache lock"</strong> 라고 부른다.) 즉, 테이블의 변경으로 인해 캐시를 제거하는 시점에, 다른 쓰레드에서는 이제 더이상 유효하지 않은 데이터를 가져가지 못하도록 lock을 걸게 된다. 이 lock이 풀릴때까지 Query Cache에 접근하는 쓰레드들은 <em>"Waiting for query cache lock"</em> 상태에서 대기하게 된다. <br>
따라서 테이블의 변경이 잦을수록, Query Cache를 사용하는 SELECT 쿼리가 많을수록 이 lock을 waiting하는 시간은 많은 비중을 차지하게 된다. </p>

<p>query cache lock이 미치는 영향을 살펴보기 위해, MySQL에서 제공하는 <strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/8.0/en/mysqlslap.html" target="_blank">mysqlslap</a></strong> 툴을 이용하여 다량의 쿼리를 동시에 요청해보았다.</p>

<h4 id="select"><code>SELECT</code> 쿼리만 있는 경우</h4>

<p>먼저 <code>SELECT</code> 쿼리만 다량으로 호출되는 상황을 가정하였다. (50개 클라이언트에서 동시에 실행, 10번씩 테스트)</p>

<pre><code class="language-bash"># Query Cache 미사용
$ mysqlslap -u jupiny -p --concurrency=50 --iterations=10 --delimiter=";" --create-schema="employees" --query="SELECT count(*) from salaries WHERE salary BETWEEN 60000 AND 70000;" --verbose
Enter password:  
Benchmark  
    Average number of seconds to run all queries: 6.887 seconds
    Minimum number of seconds to run all queries: 6.364 seconds
    Maximum number of seconds to run all queries: 7.263 seconds
    Number of clients running queries: 50
    Average number of queries per client: 1

# Query Cache 사용 (처음에 Query Cache를 한번 flush한 후 실행)
$ mysqlslap -u jupiny -p --concurrency=50 --iterations=10 --delimiter=";" --create-schema="employees" --query="SELECT SQL_CACHE count(*) from salaries WHERE salary BETWEEN 60000 AND 70000;" --verbose
Benchmark  
    Average number of seconds to run all queries: 0.722 seconds
    Minimum number of seconds to run all queries: 0.012 seconds
    Maximum number of seconds to run all queries: 7.089 seconds
    Number of clients running queries: 50
    Average number of queries per client: 1
</code></pre>

<p>평균 실행시간을 보면 6.887s -> 0.722s, 약 90% 줄어들었엄을 볼 수 있다. (사실 처음에 캐싱이 안된 요청을 제외하면, 실행시간은 더욱 크게 줄어들었다.)</p>

<h4 id="selectupdate"><code>SELECT</code> 쿼리 중간에 <code>UPDATE</code>가 있는 경우</h4>

<p>극단적이긴하지만, <code>SELECT</code>, <code>UPDATE</code>가 1번씩 번갈아가며 호출되는 상황을 가정하였다. (50개 클라이언트에서 동시에 실행, 10번씩 테스트)</p>

<pre><code class="language-bash"># Query Cache 미사용
$ mysqlslap -u jupiny -p --concurrency=50 --iterations=10 --delimiter=";" --create-schema="employees" --query="SELECT count(*) from salaries WHERE salary BETWEEN 60000 AND 70000; UPDATE salaries SET salary = salary + 1 WHERE emp_no=10001 AND from_date='1986-06-26';" --verbose
Enter password:  
Benchmark  
    Average number of seconds to run all queries: 6.579 seconds
    Minimum number of seconds to run all queries: 5.573 seconds
    Maximum number of seconds to run all queries: 7.441 seconds
    Number of clients running queries: 50
    Average number of queries per client: 2

# Query Cache 사용 (처음에 Query Cache를 한번 flush한 후 실행)
$ mysqlslap -u jupiny -p --concurrency=50 --iterations=10 --delimiter=";" --create-schema="employees" --query="SELECT SQL_CACHE count(*) from salaries WHERE salary BETWEEN 60000 AND 70000; UPDATE salaries SET salary = salary + 1 WHERE emp_no=10001 AND from_date='1986-06-26';" --verbose
Enter password:  
Benchmark  
    Average number of seconds to run all queries: 6.807 seconds
    Minimum number of seconds to run all queries: 6.151 seconds
    Maximum number of seconds to run all queries: 7.910 seconds
    Number of clients running queries: 50
    Average number of queries per client: 2
</code></pre>

<p>많은 SELECT 쿼리들이 query cache lock을 대기하는 상황으로 인해 실행시간이 크게 늘어나는 상황을 연출하고 싶었지만, 정확하게 연출하기는 힘들었다.(더 좋은 방법을 고민해봐야겠다.) 어쨌든, Query Cache를 사용했을 때 평균 실행시간이 6.579s -> 6.807s, 약 3.5% 더 증가하였다.</p>

<hr>

<h3 id="">결론</h3>

<blockquote>
  <p>The query cache can be useful in an environment where you have tables that do not change very often and for which the server receives many identical queries. </p>
</blockquote>

<p>사실 공식문서에 나와있는 이 한마디가 언제 Query Cache를 사용해야하는지를 모두 설명하고 있다고 생각한다. </p>

<ul>
<li>테이블의 읽기 대비 변경 횟수가 얼마나 많은가?</li>
<li>동일한 쿼리가 얼마나 자주 호출되는가?</li>
</ul>

<p>서비스 환경에서 이 두 가지를 고려해서 Query Cache의 적용여부를 신중하게 결정해야할 것이다.</p>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/5.7/en/query-cache.html" target="_blank">The MySQL Query Cache</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://dev.mysql.com/doc/refman/5.7/en/query-cache-thread-states.html" target="_blank">Query Cache Thread States</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://web.archive.org/web/20160129162137/http://www.psce.com/blog/kb/how-query-cache-can-cause-performance-problems/" target="_blank">How query cache can cause performance problems?</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://m.blog.naver.com/PostView.nhn?blogId=bomyzzang&amp;logNo=220797362103&amp;proxyReferer=https:%2F%2Fwww.google.com%2F" target="_blank">MySQL에서의 Query Cache. 잘써도 독일 수 있다!!</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://medium.com/@shashwat12june/mysqls-logical-architecture-1-eaaa1f63ec2f" target="_blank">MySQL Logical Architecture</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[Prometheus로 Armeria 메트릭 모니터링하기]]></title><description><![CDATA[<blockquote>
  <p>macOS Catalina 환경에서 실습하였습니다.</p>
</blockquote>

<h3 id="springbootarmeria">Spring Boot, Armeria 설정</h3>

<p><strong><a id="jupiny-blog-link" href="https://armeria.dev/" target="_blank">Armeria</a></strong> 로 띄운 서버의 메트릭을 <strong><a id="jupiny-blog-link" href="https://github.com/spring-projects/spring-boot/tree/v2.4.1/spring-boot-project/spring-boot-actuator" target="_blank">Spring Actuator</a></strong> 로 노출하여 <strong><a id="jupiny-blog-link" href="https://prometheus.io/" target="_blank">Prometheus</a></strong> 서버에서 수집을 할 수 있게 하기 위해, 아래의 dependency들이 <code>build.gradle</code>에 정의되어야 한다.</p>

<pre><code class="language-groovy">dependencies {

    // Armeria
    implementation "com.linecorp.armeria:armeria:1.2.0" // Core
    implementation "com.linecorp.armeria:armeria-spring-boot2-autoconfigure:1.2.</code></pre>]]></description><link>https://jupiny.com/2021/01/03/armeria-metric-monitoring-by-prometheus/</link><guid isPermaLink="false">33797768-1843-4b0f-918d-3b936ddc15c8</guid><category><![CDATA[Java]]></category><category><![CDATA[Spring]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sun, 03 Jan 2021 06:56:54 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <p>macOS Catalina 환경에서 실습하였습니다.</p>
</blockquote>

<h3 id="springbootarmeria">Spring Boot, Armeria 설정</h3>

<p><strong><a id="jupiny-blog-link" href="https://armeria.dev/" target="_blank">Armeria</a></strong> 로 띄운 서버의 메트릭을 <strong><a id="jupiny-blog-link" href="https://github.com/spring-projects/spring-boot/tree/v2.4.1/spring-boot-project/spring-boot-actuator" target="_blank">Spring Actuator</a></strong> 로 노출하여 <strong><a id="jupiny-blog-link" href="https://prometheus.io/" target="_blank">Prometheus</a></strong> 서버에서 수집을 할 수 있게 하기 위해, 아래의 dependency들이 <code>build.gradle</code>에 정의되어야 한다.</p>

<pre><code class="language-groovy">dependencies {

    // Armeria
    implementation "com.linecorp.armeria:armeria:1.2.0" // Core
    implementation "com.linecorp.armeria:armeria-spring-boot2-autoconfigure:1.2.0" // For Spring Boot 2 integration

    // Micrometer
    implementation "io.micrometer:micrometer-core" // Core
    implementation "io.micrometer:micrometer-registry-prometheus" // For Prometheus

    // Spring Boot
    implementation 'org.springframework.boot:spring-boot-starter-actuator' // Actuator
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // ...
}
</code></pre>

<p><code>application.yml</code>에 Acuator과 Armeria 관련 설정들을 추가하였다.
Actuator는 8082 포트, Armeria 서버는 8083 포트를 사용하도록 설정하였다. <br>
Prometheus 메트릭들을 Actuator로 노출시키기 위해, <code>management.endpoints.web.exposure</code>에 <code>prometheus</code>가 포함되어 있는 점을 주목하자.</p>

<pre><code class="language-yaml">management:  
  server:
    port: 8082
  endpoints:
    web:
      exposure:
        include: health, info, prometheus

armeria:  
  ports:
    - port: 8083
      protocol: HTTP
</code></pre>

<p>간단한 모니터링 테스트를 위해 <code>/hello</code>로 GET 요청을 하면 임의의 확률로 성공 또는 실패를 응답하는 REST API를 하나 준비하였다.</p>

<pre><code class="language-java">@Named
@NoArgsConstructor
public class MyAnnotatedService {

    private static Random RAND = new Random();

    @Get("/hello")
    public HttpResponse hello() {
        int rand = RAND.nextInt(2);
        if (rand % 2 == 0) {
            return HttpResponse.of("world");
        }
        return HttpResponse.ofFailure(new RuntimeException("error"));
    }
}
</code></pre>

<pre><code class="language-java">@Configuration
public class ArmeriaServerConfiguration {

    @Bean
    public ArmeriaServerConfigurator serverConfigurator(MyAnnotatedService myService) {
        return server -&gt; server.annotatedService("/", myService);
    }
}
</code></pre>

<p>Application을 실행한 후에, 브라우저를 띄워보자. <br>
<a id="jupiny-blog-link" href="http://localhost:8082/actuator/prometheus" target="_blank">http://localhost:8082/actuator/prometheus</a> 에 접속했을때,</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-1-1.png" alt=""></p>

<p>위의 그림에서처럼 Prometheus 메트릭들이 잘 보이는지 확인해보자.</p>

<p>다음으로 <a id="jupiny-blog-link" href="http://localhost:8083/hello" target="_blank">http://localhost:8083/hello</a> 에 접속했을때, 응답이 성공 또는 실패로 잘 오는지 확인해보자.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-2.png" alt="">
<img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-3.png" alt=""></p>

<p>모두 잘 동작한다면, 의도한대로 잘 설정된 것이다.</p>

<hr>

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

<p><strong><a id="jupiny-blog-link" href="https://brew.sh/index_ko" target="_blank">Homebrew</a></strong> 패키지 매니저로 Prometheus를 설치한다.</p>

<pre><code class="language-bash">$ brew install prometheus
</code></pre>

<p>Homebrew로 설치하면 기본적으로 <code>prometheus.yml</code> 파일은 <code>/usr/local/etc/</code> 아래에 위치하게 된다. <br>
이 YAML파일을 이용해 Prometheus의 설정들을 변경할 수 있다. 설정한 후에는 다시 Prometheus 서비스를 재시작해야한다.</p>

<pre><code class="language-yaml">global:  
  scrape_interval: 10s

scrape_configs:  
  - job_name: "armeria-prometheus"
    metrics_path: '/actuator/prometheus'
    static_configs:
    - targets: ["localhost:8082"]
</code></pre>

<p>여기서 위에서 확인했던, Spring Boot Actuator로 Prometheus 메트릭들을 수집할 수 있었던 path(<code>/actuator/prometheus</code>)와 port(<code>8082</code>)를 정확하게 설정하여야 한다.</p>

<pre><code class="language-bash">$ brew services start prometheus
</code></pre>

<p>Prometheus가 사용하는 default 포트는 9090이므로, 브라우저에서 <a id="jupiny-blog-link" href="http://localhost:9090" target="_blank">http://localhost:9090</a> 으로 접속하면 아래 화면을 볼 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-4.png" alt=""></p>

<p>Select box를 눌렀을 때, Armeria 관련 메트릭들이 잘 보인다면, 잘 설정된 것이다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-5.png" alt=""></p>

<p><a id="jupiny-blog-link" href="http://localhost:8083/hello" target="_blank">http://localhost:8083/hello</a> 를 몇번 요청하여, <code>armeria_server_requests_total</code> 메트릭이 잘 올라가는지 그래프에서 확인해보자.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-6.png" alt=""></p>

<hr>

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

<p>마찬가지로 <strong><a id="jupiny-blog-link" href="https://grafana.com/" target="_blank">Grafana</a></strong> 도 Homebrew로 설치한 후 서비스를 시작한다.</p>

<pre><code class="language-bash">$ brew install grafana
$ brew services start grafana
</code></pre>

<p>Grafana의 기본 포트는 3000이므로, 브라우저에서 <code>http://localhost:3000</code>으로 접속하면 아래 로그인 페이지를 만나게된다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-7.png" alt=""></p>

<p>계정을 만든 적이 없는데..? 라고 당황하지 말고 침착하게 <code>admin</code>을 username, password에 입력하면 로그인된다. 이는 단순 기본 계정이므로 실제 서비스에서 사용할 때는 보안을 위해 반드시 새로운 계정을 하나 만들도록 하자.</p>

<p>Grafana에서 Prometheus 메트릭들을 그래프로 그릴 수 있도록, 앞에서 설정한 Prometheus 경로를 Data Source로 등록해야 한다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-8.png" alt=""></p>

<p>자 이제 Dashboard를 하나 생성한 후, <code>/hello</code> API의 성공/실패 횟수를 모니터링할 수 있는 Panel를 하나 생성해보자.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-9.png" alt=""></p>

<ul>
<li>success</li>
</ul>

<pre><code class="language-promql">increase(armeria_server_requests_total{hostname_pattern="*", method="hello", http_status="200", result="success", service="com.example.armeria_prometheus.MyAnnotatedService"}[1m])  
</code></pre>

<ul>
<li>failure</li>
</ul>

<pre><code class="language-promql">increase(armeria_server_requests_total{hostname_pattern="*", method="hello", http_status="500", result="failure", service="com.example.armeria_prometheus.MyAnnotatedService"}[1m])  
</code></pre>

<p>여기서 <code>armeria_server_requests_total</code> metric은 <strong><a id="jupiny-blog-link" href="https://prometheus.io/docs/concepts/metric_types/#counter" target="_blank">Counter</a></strong> 라는 metric type으로 단순히 누적되서 계속 증가하는 값이다. 실제 API 모니터링에서는 이 API가 현재까지 얼마나 많은 성공/실패가 있었나보다는, 시간별 성공/실패의 횟수가 더 의미있을 것이다. 이를 위해 <strong><a id="jupiny-blog-link" href="https://prometheus.io/docs/prometheus/latest/querying/functions/#increase" target="_blank"><code>increase()</code></a></strong> 함수를 사용해서 매 1분동안의 성공/실패 횟수를 그래프로 나타내었다.  </p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-11.png" alt=""></p>

<p>하지만 실제 그래프에 찍힌 수치를 보면 소수점 값이다. 1씩 증가하는 횟수인데, 어떻게 소수점이 나올 수 있을까? <br>
이 이유는 <strong><a id="jupiny-blog-link" href="https://www.innoq.com/en/blog/prometheus-counters/#orderscreatedwithinthelast5minutes" target="_blank">이 글</a></strong> 에 자세히 정리되어 있다.</p>

<p>(위의 블로그에는 아래와 같은 문구도 있다.)</p>

<blockquote>
  <p>Don't expect exact values from Prometheus. It's not designed to give you the exact number of anything, but rather an overview of what's happening.</p>
</blockquote>

<hr>

<h3 id="">모니터링</h3>

<p>이제 <strong><a id="jupiny-blog-link" href="https://www.npmjs.com/package/loadtest" target="_blank">loadtest</a></strong> 라이브러리를 이용하여 임의로 많은 요청을 보내보자.</p>

<pre><code>$ loadtest http://127.0.0.1:8083/hello --rps 1
</code></pre>

<p><code>/hello</code> API의 성공/실패 횟수가 Grafana에 그래프로 이쁘게 나옴을 볼 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2021/01/armeria-metric-monitoring-by-prometheus-10.png" alt=""></p>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://jongmin92.github.io/2019/12/04/Spring/prometheus/" target="_blank">SpringBoot Application의 monitoring 시스템 구축하기
</a></strong>  </li>
<li><strong><a id="jupiny-blog-link" href="https://badcandy.github.io/2018/12/29/prometheus-practice/" target="_blank">Prometheus #2 - Prometheus와 Grafana를 이용한 Spring Boot 2.0 어플리케이션 모니터링
</a></strong>  </li>
<li><strong><a id="jupiny-blog-link" href="https://www.innoq.com/en/blog/prometheus-counters/" target="_blank">Prometheus Counters and how to deal with them
</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[AtomicReferenceFieldUpdater 사용하기]]></title><description><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://armeria.dev/" target="_blank">Armeria</a></strong> 내부 구현을 보면 아래와 같은 코드를 심심찮게 볼 수 있다.</p>

<pre><code class="language-java">public class DefaultStreamMessage&lt;T&gt; extends AbstractStreamMessageAndWriter&lt;T&gt; {

    private static final AtomicReferenceFieldUpdater&lt;DefaultStreamMessage, SubscriptionImpl&gt;
            subscriptionUpdater = AtomicReferenceFieldUpdater.newUpdater(
            DefaultStreamMessage.class, SubscriptionImpl.class, "subscription");

    private static final AtomicReferenceFieldUpdater&lt;DefaultStreamMessage, State&gt; stateUpdater =
            AtomicReferenceFieldUpdater.newUpdater(DefaultStreamMessage.</code></pre>]]></description><link>https://jupiny.com/2020/06/24/use-atomicreferencefieldupdater/</link><guid isPermaLink="false">bd17bc87-83f0-4339-85d4-7f2b3d075941</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Tue, 23 Jun 2020 18:09:41 GMT</pubDate><content:encoded><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://armeria.dev/" target="_blank">Armeria</a></strong> 내부 구현을 보면 아래와 같은 코드를 심심찮게 볼 수 있다.</p>

<pre><code class="language-java">public class DefaultStreamMessage&lt;T&gt; extends AbstractStreamMessageAndWriter&lt;T&gt; {

    private static final AtomicReferenceFieldUpdater&lt;DefaultStreamMessage, SubscriptionImpl&gt;
            subscriptionUpdater = AtomicReferenceFieldUpdater.newUpdater(
            DefaultStreamMessage.class, SubscriptionImpl.class, "subscription");

    private static final AtomicReferenceFieldUpdater&lt;DefaultStreamMessage, State&gt; stateUpdater =
            AtomicReferenceFieldUpdater.newUpdater(DefaultStreamMessage.class, State.class, "state");

    private volatile SubscriptionImpl subscription;

    private volatile State state = State.OPEN;
</code></pre>

<p><code>AtomicReferenceFieldUpdater</code> 이름을 비추어 보았을 때, 멀티 쓰레드 환경에서 발생할 수 있는 race condition 상황에서 특정 필드를 atomic하게 업데이트하기 위한 목적으로 보인다. <br>
그러한 목적이라면 <code>AtomicReference</code> 만을 사용해도 충분할텐데, 왜 굳이 복잡하게 <code>volatile</code> 변수와 함께 <code>AtomicReferenceFieldUpdater</code> 을 사용하는 걸까?  </p>

<p>아래 예제를 보며 한번 그 이유를 살펴보도록 하자.</p>

<hr>

<h3 id="">예제</h3>

<p>아래는 race condition이 발생하는 간단한 예제이다. <br>
총 10개의 쓰레드에서 동시에 <code>boyFriend.makeGirlFriend(new GirlFriend())</code>을 실행한다. 만약 <code>BoyFriend</code> 객체의 <code>girFriend</code> 객체 필드가 이미 세팅된 적이 있다면, 함수가 호출되더라도 최초 생성된 그 <code>GirlFriend</code> 객체를 그대로 사용하기를 기대하고 있다.</p>

<pre><code class="language-java">public class AtomicOperationTest {

    public static void main(String[] args) throws Exception {
        int n = 10;
        ExecutorService executorService = Executors.newFixedThreadPool(n);
        BoyFriend boyFriend = new BoyFriend();

        Runnable runnable = () -&gt; {
            GirlFriend girlFriend = boyFriend.makeGirlFriend(new GirlFriend());
            System.out.println(girlFriend);
        };

        for (int i = 0; i &lt; n; i++) {
            executorService.submit(runnable);
        }

        executorService.shutdown();
    }
}
</code></pre>

<pre><code class="language-java">public class BoyFriend {

    private volatile GirlFriend girlFriend;

    public BoyFriend() { }

    public GirlFriend makeGirlFriend(GirlFriend girlFriend) {
        if (this.girlFriend == null) {
            this.girlFriend = girlFriend;
            return girlFriend;
        }
        return this.girlFriend;
    }

    static class GirlFriend { }
}
</code></pre>

<p>main 함수를 실행해보면, </p>

<pre><code class="language-shell">&gt; Task :AtomicOperationTest.main()
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@610add8a  
atomic.BoyFriend$GirlFriend@21f189d9  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@6c68f530  
atomic.BoyFriend$GirlFriend@4552d520  
</code></pre>

<p>아래와 같이 각 쓰레드 실행 후의 <code>GirlFriend</code> 객체가 모두 일치하지 않음을 확인할 수 있다. 쓰레드 간의 동기화 처리가 전혀 안 되어있기 때문에 각 쓰레드들에서 <code>this.girlFriend == null</code> 조건문을 동시에 <code>true</code> 로 통과하게 된다면, 이러한 결과는 충분히 발생할 수 있다.</p>

<hr>

<h3 id="volatile">volatile 사용하기</h3>

<p>먼저 <code>girlFriend</code> 변수를 <code>volatile</code> 타입으로 선언해보았다.</p>

<pre><code class="language-java">public class BoyFriend {

    private volatile GirlFriend girlFriend;

    // ...
}
</code></pre>

<p>다시 main 함수를 돌려보면,</p>

<pre><code class="language-shell">&gt; Task :AtomicOperationTest.main()
atomic.BoyFriend$GirlFriend@610add8a  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@50bd2adc  
atomic.BoyFriend$GirlFriend@4552d520  
atomic.BoyFriend$GirlFriend@133eafb9  
atomic.BoyFriend$GirlFriend@50bd2adc  
</code></pre>

<p>결과는 크게 다르지 않음을 볼 수 있다.  </p>

<p><code>volatile</code> 의 역할을 이해하면 어쩌면 당연한 결과인데, <code>volatile</code>이 해주는 것은 단지 CPU 캐시가 아닌, 메인 메모리에 저장된 변수에 직접 접근하여 읽거나 업데이트하는 것 뿐이다. 이를 이용하면 멀티 쓰레드 환경에서 하나의 쓰레드에서 변경한 변수를 다른 쓰레드들에서도 바로 최신 값을 읽을 수 있다는 이점을 얻을 수 잇다. <br>
하지만 위의 예제에서처럼, 하나의 쓰레드에서 변경한 변수(여기서는 <code>girlFriend</code>) 가 아직 메인 메모리에 업데이트되기 전이라면, 앞에서와 마찬가지로 다른 쓰레드에서는 <code>girlFriend</code>을 <code>null</code> 로 판단해 저마다의 객체를 세팅하게 된다는 한계점이 여전히 존재한다.</p>

<hr>

<h3 id="atomicreference">AtomicReference 사용하기</h3>

<p>위에서 발생한 문제들의 근본적인 원인은 변수의 값을 읽고(<code>null</code>인지 체크), 업데이트(필드를 새로 세팅) 하는 연산들이 atomic하지 않았기 때문에, 이 두 연산 사이에 변수의 값이 변경되는 문제가 발생할 수 있다는 것이었다. <br>
이러한 문제를 해결하기 위하여 <code>AtomicReference</code> 을 사용할 수 있다. <code>Atomic*</code> 클래스의 연산들은 CPU low level에서 atomic하게 수행되기 때문에, 따로 lock을 걸지 않고도 연산들의 원자성(atomicity)을 보장할 수 있다. <br>
<code>compareAndSet(null, girlFriend)</code> 함수를 이용하여 <code>null</code> 체크와 새로운 필드를 세팅하는 연산 2개를 atomic하게 실행하고 있음에 주목하자.</p>

<pre><code class="language-java">public class BoyFriend {

    private AtomicReference&lt;GirlFriend&gt; girlFriendRef;

    public BoyFriend() {
        girlFriendRef = new AtomicReference&lt;&gt;();
    }

    public GirlFriend makeGirlFriend(GirlFriend girlFriend) {
        if (!girlFriendRef.compareAndSet(null, girlFriend)) {
            return girlFriendRef.get();
        }
        return girlFriend;
    }

    static class GirlFriend { }
}
</code></pre>

<p>main 함수를 돌려보면, 각 쓰레드 실행 후의 <code>GirlFriend</code> 객체가 모두 동일함을 확인할 수 있다.</p>

<pre><code class="language-shell">&gt; Task :AtomicOperationTest.main()
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
atomic.BoyFriend$GirlFriend@29f29261  
</code></pre>

<hr>

<h3 id="atomicreferencefieldupdater">AtomicReferenceFieldUpdater 사용하기</h3>

<p>사실 위의 <code>AtomicReference</code> 을 사용한 방식으로 구현을 끝내더라도 문제없이 잘 돌아가는 코드이다. 다만 최적화 관점에서, 여기서 <code>AtomicReferenceFieldUpdater</code>을 사용하면 조금 더 메모리 사용률을 줄일 수 있다는 장점이 존재한다.  </p>

<p>만약 예제의 <code>BoyFriend</code> 객체가 <strong>N</strong>개의 <code>GirlFriend</code>를 필드를 가진다고 가정해보자. <code>BoyFriend</code> 객체는 각 <code>GirlFriend</code> 필드를 atomic하게 관리하게 위해 N개의 <code>AtomicReference</code> 필드를 선언하여야 한다.</p>

<pre><code class="language-java">public class BoyFriend {

    private AtomicReference&lt;GirlFriend&gt; girlFriendRef1;
    private AtomicReference&lt;GirlFriend&gt; girlFriendRef2;
    private AtomicReference&lt;GirlFriend&gt; girlFriendRef3;
    // ...
    private AtomicReference&lt;GirlFriend&gt; girlFriendRefN;
}
</code></pre>

<p>또 위의 예제의 main 함수에서는 <code>BoyFriend</code> 객체가 1개 생성되었었지만, 만약 <code>BoyFriend</code> 객체가 <strong>M</strong>개씩 생성되는 상황이라고 가정해보자.  </p>

<p>그렇게 되면</p>

<blockquote>
  <p>(<code>GirlFriend</code>을 <code>AtomicReference</code> 객체로 <strong>N</strong>개 생성하는 메모리 오버헤드) * <strong>M</strong></p>
</blockquote>

<p>만큼의 메모리가 추가적으로 요구된다.</p>

<p>이 때 <code>volatile</code>과 <code>AtomicReferenceFieldUpdate</code> 을 함께 사용하면 이러한 추가적인 비용을 막을 수 있다.</p>

<blockquote>
  <p>A reflection-based utility that enables atomic updates to designated {@code volatile} reference fields of designated classes. </p>
</blockquote>

<p><code>AtomicReferenceFieldUpdater</code> 의 <strong><a id="jupiny-blog-link" href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReferenceFieldUpdater.html" target="_blank">정의</a></strong>를 보면 알 수 있듯, <code>volatile</code> 필드를 atomic하게 업데이트하기 위해 만들어졌기 때문에 항상 <code>volatile</code> 필드와 짝을 이루어 사용하여야 한다.  </p>

<p>아래는 <code>AtomicReferenceFieldUpdater</code>를 적용한 코드이다.</p>

<pre><code class="language-java">public class BoyFriend {

    private volatile GirlFriend girlFriend;

    private static final AtomicReferenceFieldUpdater&lt;BoyFriend, GirlFriend&gt;
            girlFriendUpdater = AtomicReferenceFieldUpdater.newUpdater(
            BoyFriend.class, GirlFriend.class, "girlFriend");

    public BoyFriend() { }

    public GirlFriend makeGirlFriend(GirlFriend girlFriend) {
        if (!girlFriendUpdater.compareAndSet(this, null, girlFriend)) {
            return this.girlFriend;
        }
        return girlFriend;
    }

    static class GirlFriend { }
}
</code></pre>

<p><code>AtomicReference&lt;GirlFriend&gt;</code>가 아닌 <code>GirlFriend</code> 타입의 변수로 그대로 선언할 수 있기 때문에 <code>AtomicReference</code>로 인해 생성되는 추가적인 메모리 오버헤드를 줄일 수 있다. <br>
또 추가된 <code>AtomicReferenceFieldUpdater</code> 필드는 static 변수이기 때문에 인스턴스 생성시의 메모리 사용량에도 영향을 미치지 않는다.  </p>

<hr>

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

<p>정리하면, </p>

<ul>
<li>하나의 클래스가 <code>AtomicReferenceField</code> 필드를 많이 가질수록</li>
<li>그 클래스의 인스턴스가 많이 생성될수록 </li>
</ul>

<p><code>AtomicReferenceFieldUpdater</code>을 사용하였을 때 메모리 사용 측면에서 이점을 얻을 수 있다.</p>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="http://normanmaurer.me/blog/2013/10/28/Lesser-known-concurrent-classes-Part-1/" target="_blank">Lesser known concurrent classes - Atomic*FieldUpdater</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://stackoverflow.com/questions/9749746/what-is-the-difference-between-atomic-volatile-synchronized" target="_blank">What is the difference between atomic / volatile / synchronized?</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://nesoy.github.io/articles/2018-06/Java-volatile" target="_blank">Java volatile이란?</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[RxJava의 subscribeOn와 observeOn (2)]]></title><description><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://jupiny.com/2020/04/11/rxjava-subscribeon-observeon-1/" target="_blank">이전 글</a></strong> 에서 RxJava의 <code>subscribeOn()</code>, <code>observeOn()</code>의 내부 구현을 한번 살펴보며 쓰레드의 동작에 대해 정리해보았다. 이번 글에서는 로그를 남겨보며 이 실제 동작을 한번 살펴 볼것이다.</p>

<blockquote>
  <p>Rxjava2 2.2.19 버젼을 사용하였습니다.</p>
</blockquote>

<h3 id="">준비하기</h3>

<p>먼저 편리한 테스트를 위해, 아래와 같이 JUnit Test 클래스 안에 private method, class를 준비했다.</p>

<pre><code class="language-java">private Scheduler newScheduler(int</code></pre>]]></description><link>https://jupiny.com/2020/04/23/rxjava-subscribeon-observeon-2/</link><guid isPermaLink="false">98405ff5-fd70-4d78-8574-03c2f707b3b8</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Wed, 22 Apr 2020 16:08:37 GMT</pubDate><content:encoded><![CDATA[<p><strong><a id="jupiny-blog-link" href="https://jupiny.com/2020/04/11/rxjava-subscribeon-observeon-1/" target="_blank">이전 글</a></strong> 에서 RxJava의 <code>subscribeOn()</code>, <code>observeOn()</code>의 내부 구현을 한번 살펴보며 쓰레드의 동작에 대해 정리해보았다. 이번 글에서는 로그를 남겨보며 이 실제 동작을 한번 살펴 볼것이다.</p>

<blockquote>
  <p>Rxjava2 2.2.19 버젼을 사용하였습니다.</p>
</blockquote>

<h3 id="">준비하기</h3>

<p>먼저 편리한 테스트를 위해, 아래와 같이 JUnit Test 클래스 안에 private method, class를 준비했다.</p>

<pre><code class="language-java">private Scheduler newScheduler(int nThreads, String pattern) {  
    ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(pattern).build();
    ExecutorService pool = Executors.newFixedThreadPool(nThreads, threadFactory);
    return Schedulers.from(pool);
}

private static class LogManager {  
    private long start;

    public LogManager(long start) {
        this.start = start;
    }

    private void log(String message) {
        System.out.println(System.currentTimeMillis() - start + "\t| " +
                           Thread.currentThread().getName() + "\t| " + message);
    }
}
</code></pre>

<hr>

<h3 id="test1suscribeonobserveon">TEST1: suscribeOn, observeOn 미사용</h3>

<p>일단 하나의 <code>Flowable</code>를 구독하는 간단한 테스트 method를 작성해보았다.</p>

<pre><code class="language-java">@Test
public void test1() throws Exception {  
    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request-2: " + r))
            .doOnNext(i -&gt; logManager.log("onNext-1: " + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap: " + i);
                Thread.sleep(300);
                return i * 5;
            }))
            .doOnRequest(r -&gt; logManager.log("request-1: " + r))
            .doOnNext(i -&gt; logManager.log("onNext-2: " + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");
}
</code></pre>

<p>하나의 <code>flatMap()</code> 연산만이 있는 간단한 코드이다. 중간중간 <code>request()</code>, <code>onNext()</code>가 동작하는 쓰레드를 확인하기 위해 <code>doOnRequest()</code>, <code>doOnNext()</code> 안에 로그를 남겼고, <code>flatMap()</code>이 시간이 오래 걸리는 상황이라고 가정하기 위해 임의로 <code>Thread.sleep(300)</code>을 넣었다.</p>

<p>테스트를 실행하면,</p>

<pre><code class="language-shell">0    | main  | Starting  
161    | main  | request-1: 9223372036854775807  
161    | main  | request-2: 128  
161    | main  | onNext-1: 1  
162    | main  | flatMap: 1  
466    | main  | onNext-2: 5  
466    | main  | Got 5  
466    | main  | onNext-1: 2  
466    | main  | flatMap: 2  
767    | main  | onNext-2: 10  
767    | main  | Got 10  
767    | main  | onNext-1: 3  
767    | main  | flatMap: 3  
1068    | main  | onNext-2: 15  
1069    | main  | Got 15  
1071    | main  | Completed  
1071    | main  | Exiting  
</code></pre>

<p>어떤 <code>Scheduler</code> 설정도 하지 않았기 때문에, 모든 연산이 <code>main</code> 쓰레드에서 돌아감을 확인할 수 있다. 하나의 쓰레드에서 모든 것을 하고 있기 때문에, 연산들이 모두 순서대로 동작하고, 약 1000ms 뒤에 구독이 완전히 완료된 후에야 "Exiting" 로그가 <code>main</code> 쓰레드에 의해 출력됨을 볼 수 있다. <br>
추가로, <code>doOnRequest()</code> 로그를 통해 <code>subscribe()</code>는 기본적으로 upstream에 9223372036854775807(= <code>Long.MAX_VALUE</code>) 개의 item을 <code>request()</code>로 요청하고, <code>flatMap()</code>은 upstream에 128개의 item을 <code>request()</code>로 요청한다는 것도 알수 있다.</p>

<hr>

<h3 id="test2subscribeon1">TEST2: subscribeOn 1개 사용</h3>

<p>위의 테스트에서 <code>subscribeOn()</code>을 <code>subscribe()</code> 위에 하나 넣어보았다.</p>

<pre><code class="language-java">@Test
public void test2() throws Exception {  
    Scheduler schedulerA = newScheduler(1, "Sched-A-%d"); // NEW!

    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request-2: " + r))
            .doOnNext(i -&gt; logManager.log("onNext-1: " + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap: " + i);
                Thread.sleep(300);
                return i * 5;
            }))
            .doOnRequest(r -&gt; logManager.log("request-1: " + r))
            .subscribeOn(schedulerA) // NEW!
            .doOnNext(i -&gt; logManager.log("onNext-2: " + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");

    Thread.sleep(2000); // main thread 종료 방지
}
</code></pre>

<pre><code class="language-shell">0    | main  | Starting  
144    | main  | Exiting  
148    | Sched-A-0 | request-1: 9223372036854775807  
148    | Sched-A-0 | request-2: 128  
148    | Sched-A-0 | onNext-1: 1  
149    | Sched-A-0 | flatMap: 1  
454    | Sched-A-0 | onNext-2: 5  
454    | Sched-A-0 | Got 5  
454    | Sched-A-0 | onNext-1: 2  
454    | Sched-A-0 | flatMap: 2  
759    | Sched-A-0 | onNext-2: 10  
759    | Sched-A-0 | Got 10  
759    | Sched-A-0 | onNext-1: 3  
759    | Sched-A-0 | flatMap: 3  
1064    | Sched-A-0 | onNext-2: 15  
1064    | Sched-A-0 | Got 15  
1066    | Sched-A-0 | Completed  
</code></pre>

<p><code>Flowable</code>를 생성한 후 구독완료와 상관없이 <code>main</code> 쓰레드에서 바로 "Exiting" 로그를 출력한다. 그리고 <code>request()</code>, <code>onNext()</code>가 모두 <code>Sched-A-0</code> 쓰레드에서 동작함을 확인할 수 있다.</p>

<p>이번엔 <code>subscribeOn()</code>의 위치를 위로 옮겨보자.</p>

<pre><code class="language-java">@Test
public void test2() throws Exception {  
    Scheduler schedulerA = newScheduler(1, "Sched-A-%d"); // NEW!

    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request:" + r))
            .subscribeOn(schedulerA) // NEW!
            .doOnNext(i -&gt; logManager.log("onNext:" + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap:" + i);
                Thread.sleep(300);
                return i * 5;
            }))
            .doOnRequest(r -&gt; logManager.log("request:" + r))
            .doOnNext(i -&gt; logManager.log("onNext:" + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");

    Thread.sleep(2000); // main thread 종료 방지
}
</code></pre>

<pre><code class="language-shell">0    | main  | Starting  
141    | main  | request:9223372036854775807  
142    | main  | Exiting  
143    | Sched-A-0 | request:128  
143    | Sched-A-0 | onNext:1  
144    | Sched-A-0 | flatMap:1  
446    | Sched-A-0 | onNext:5  
446    | Sched-A-0 | Got 5  
446    | Sched-A-0 | onNext:2  
447    | Sched-A-0 | flatMap:2  
749    | Sched-A-0 | onNext:10  
749    | Sched-A-0 | Got 10  
749    | Sched-A-0 | onNext:3  
750    | Sched-A-0 | flatMap:3  
1052    | Sched-A-0 | onNext:15  
1052    | Sched-A-0 | Got 15  
1054    | Sched-A-0 | Completed  
</code></pre>

<p>첫번째 <code>request()</code> 요청까지는 <code>main</code> 쓰레드에서 실행되고 이후에는 모두 <code>Sched-A-0</code> 쓰레드에서 실행된다.</p>

<hr>

<h3 id="test3subscribeon2">TEST3: subscribeOn 2개 사용</h3>

<pre><code class="language-java">@Test
public void test3() throws Exception {  
    Scheduler schedulerA = newScheduler(1, "Sched-A-%d");
    Scheduler schedulerB = newScheduler(1, "Sched-B-%d"); // NEW!

    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request-2: " + r))
            .subscribeOn(schedulerB) // NEW!
            .doOnNext(i -&gt; logManager.log("onNext-1: " + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap: " + i);
                Thread.sleep(300);
                return i * 5;
            }))
            .doOnRequest(r -&gt; logManager.log("request-1: " + r))
            .subscribeOn(schedulerA)
            .doOnNext(i -&gt; logManager.log("onNext-2: " + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");

    Thread.sleep(2000); // main thread 종료 방지
}
</code></pre>

<pre><code class="language-shell">0    | main  | Starting  
150    | main  | Exiting  
154    | Sched-A-0 | request-1: 9223372036854775807  
155    | Sched-B-0 | request-2: 128  
155    | Sched-B-0 | onNext-1: 1  
156    | Sched-B-0 | flatMap: 1  
460    | Sched-B-0 | onNext-2: 5  
460    | Sched-B-0 | Got 5  
460    | Sched-B-0 | onNext-1: 2  
460    | Sched-B-0 | flatMap: 2  
761    | Sched-B-0 | onNext-2: 10  
761    | Sched-B-0 | Got 10  
761    | Sched-B-0 | onNext-1: 3  
761    | Sched-B-0 | flatMap: 3  
1063    | Sched-B-0 | onNext-2: 15  
1063    | Sched-B-0 | Got 15  
1065    | Sched-B-0 | Completed  
</code></pre>

<p>downstream에서 위로 올라가며 <code>subscribeOn()</code>를 만날때마다 <code>request()</code>가 실행되는 쓰레드가 변경됨을 확인할 수 있다. 최상단의 upstream에 도달해서 downstream으로 <code>onNext()</code>가 실행되기 시작할때는 결국 마지막으로 <code>request()</code>가 실행된 쓰레드, <code>Sched-B-0</code>에서 모두 동작한다. <br>
이러한 이유로 최상단의 <code>subscribeOn()</code> 외에 아래에 추가적으로 <code>subscribeOn()</code>을 넣는 것은 큰 의미가 없다.</p>

<hr>

<h3 id="test4subscribeonobserveon">TEST4: subscribeOn, observeOn 모두 사용</h3>

<p>이번에는 중간에 <code>observeOn()</code>도 넣어보았다.</p>

<pre><code class="language-java">@Test
public void test4() throws Exception {  
    Scheduler schedulerA = newScheduler(1, "Sched-A-%d");
    Scheduler schedulerB = newScheduler(1, "Sched-B-%d");
    Scheduler schedulerC = newScheduler(1, "Sched-C-%d");
    Scheduler schedulerD = newScheduler(1, "Sched-D-%d");

    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request-2: " + r))
            .subscribeOn(schedulerB)
            .observeOn(schedulerC)
            .doOnNext(i -&gt; logManager.log("onNext-1: " + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap: " + i);
                Thread.sleep(300);
                return i * 5;
            }))
            .doOnRequest(r -&gt; logManager.log("request-1: " + r))
            .subscribeOn(schedulerA)
            .observeOn(schedulerD)
            .doOnNext(i -&gt; logManager.log("onNext-2: " + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");

    Thread.sleep(5000); // main thread 종료 방지
}
</code></pre>

<pre><code class="language-shell">1    | main  | Starting  
142    | main  | Exiting  
144    | Sched-A-0 | request-1: 128  
145    | Sched-B-0 | request-2: 128  
145    | Sched-C-0 | onNext-1: 1  
146    | Sched-C-0 | flatMap: 1  
447    | Sched-C-0 | onNext-1: 2  
447    | Sched-D-0 | onNext-2: 5  
447    | Sched-C-0 | flatMap: 2  
447    | Sched-D-0 | Got 5  
747    | Sched-C-0 | onNext-1: 3  
747    | Sched-D-0 | onNext-2: 10  
747    | Sched-C-0 | flatMap: 3  
747    | Sched-D-0 | Got 10  
1047    | Sched-D-0 | onNext-2: 15  
1047    | Sched-D-0 | Got 15  
1049    | Sched-D-0 | Completed  
</code></pre>

<p>upstream에서 아래로 내려가며 <code>observeOn()</code>를 만날때마다 <code>onNext()</code>가 실행되는 쓰레드가 변경됨을 확인할 수 있다. <code>flatMap()</code>까지는 <code>Sched-C-0</code> 쓰레드에서 실행되고, 그 아래 downstream의 <code>onNext()</code>부터는 <code>Sched-D-0</code> 쓰레드에서 독립적으로 실행된다. <br>
추가로 <code>observeOn()</code>은 기본적으로 upstream에 128개의 item을 request()로 요청한다는 것도 알수 있다.</p>

<hr>

<h3 id="test5flatmapsubscribeon">TEST5: flatMap 안에 subscribeOn 사용</h3>

<p>전체 실행시간이 약 1000ms가 나오는 이유는 <code>flatMap()</code>을 실행하는 쓰레드가 오직 하나(TEST4에서는 <code>Sched-C-0</code>)이기 때문에 sleep 300ms에 의해 다음 item에 대한 <code>onNext()</code>가 바로 실행되지 못하고 block되기 때문이다. <br>
이러한 문제를 개선하기 위해선, <code>flatMap()</code> 안의 <code>Flowable</code> 에도 <code>subscribeOn()</code> 을 적용해 별도의 쓰레드에서 돌아가게 하면 된다.</p>

<pre><code class="language-java">@Test
public void test5() throws Exception {  
    Scheduler schedulerA = newScheduler(1, "Sched-A-%d");
    Scheduler schedulerB = newScheduler(1, "Sched-B-%d");
    Scheduler schedulerC = newScheduler(1, "Sched-C-%d");
    Scheduler schedulerD = newScheduler(1, "Sched-D-%d");
    Scheduler schedulerE = newScheduler(1, "Sched-E-%d"); // NEW!

    LogManager logManager = new LogManager(System.currentTimeMillis());
    logManager.log("Starting");
    Flowable.just(1, 2, 3)
            .doOnRequest(r -&gt; logManager.log("request-2: " + r))
            .subscribeOn(schedulerB)
            .observeOn(schedulerC)
            .doOnNext(i -&gt; logManager.log("onNext-1: " + i))
            .flatMap(i -&gt; Flowable.fromCallable(() -&gt; {
                logManager.log("flatMap: " + i);
                Thread.sleep(300);
                return i * 5;
            })
                                  .doOnRequest(r -&gt; logManager.log("flatMap request: " + r)) // NEW!
                                  .doOnNext(x -&gt; logManager.log("flatMap onNext: " + x)) // NEW!
                                  .subscribeOn(schedulerE))  // NEW!
            .doOnRequest(r -&gt; logManager.log("request-1: " + r))
            .subscribeOn(schedulerA)
            .observeOn(schedulerD)
            .doOnNext(i -&gt; logManager.log("onNext-2: " + i))
            .subscribe(i -&gt; logManager.log("Got " + i),
                       t -&gt; logManager.log("Error"),
                       () -&gt; logManager.log("Completed"));
    logManager.log("Exiting");

    Thread.sleep(2000); // main thread 종료 방지
}
</code></pre>

<pre><code class="language-shell">0    | main  | Starting  
139    | main  | Exiting  
141    | Sched-A-0 | request-1: 128  
143    | Sched-B-0 | request-2: 128  
143    | Sched-C-0 | onNext-1: 1  
145    | Sched-C-0 | onNext-1: 2  
145    | Sched-C-0 | onNext-1: 3  
145    | Sched-E-0 | flatMap request: 128  
146    | Sched-E-0 | flatMap: 1  
447    | Sched-E-0 | flatMap onNext: 5  
447    | Sched-E-0 | flatMap request: 128  
447    | Sched-D-0 | onNext-2: 5  
447    | Sched-E-0 | flatMap: 2  
447    | Sched-D-0 | Got 5  
747    | Sched-E-0 | flatMap onNext: 10  
748    | Sched-D-0 | onNext-2: 10  
748    | Sched-D-0 | Got 10  
748    | Sched-E-0 | flatMap request: 128  
748    | Sched-E-0 | flatMap: 3  
1051    | Sched-E-0 | flatMap onNext: 15  
1051    | Sched-D-0 | onNext-2: 15  
1051    | Sched-D-0 | Got 15  
1052    | Sched-D-0 | Completed  
</code></pre>

<p>이렇게 돌리면 당연히 실행시간은 변화가 없다. 왜냐하면 <code>schedulerE</code>의 쓰레드 수가 1개이기 때문에 결국 sleep을 만났을 때 block 될수 밖에 없다. <br>
아래처럼 <code>schedulerE</code>의 쓰레드 수를 item 수에 맞게 3으로 늘려서 다시 돌려보자.</p>

<pre><code class="language-java">Scheduler schedulerE = newScheduler(3, "Sched-E-%d");  
</code></pre>

<pre><code class="language-shell">0    | main  | Starting  
149    | main  | Exiting  
152    | Sched-A-0 | request-1: 128  
153    | Sched-B-0 | request-2: 128  
153    | Sched-C-0 | onNext-1: 1  
155    | Sched-C-0 | onNext-1: 2  
155    | Sched-C-0 | onNext-1: 3  
155    | Sched-E-0 | flatMap request: 128  
155    | Sched-E-1 | flatMap request: 128  
155    | Sched-E-1 | flatMap: 2  
155    | Sched-E-0 | flatMap: 1  
155    | Sched-E-2 | flatMap request: 128  
155    | Sched-E-2 | flatMap: 3  
459    | Sched-E-1 | flatMap onNext: 10  
459    | Sched-D-0 | onNext-2: 10  
459    | Sched-D-0 | Got 10  
460    | Sched-E-0 | flatMap onNext: 5  
460    | Sched-E-2 | flatMap onNext: 15  
460    | Sched-D-0 | onNext-2: 5  
460    | Sched-D-0 | Got 5  
460    | Sched-D-0 | onNext-2: 15  
460    | Sched-D-0 | Got 15  
461    | Sched-D-0 | Completed  
</code></pre>

<p><code>Sched-E-0</code>, <code>Sched-E-1</code>, <code>Sched-E-2</code> 3개 쓰레드에서 병렬로 <code>flatMap()</code>이 돌아가게 되며 실행시간이 단축됨을 볼 수 있다.</p>]]></content:encoded></item><item><title><![CDATA[RxJava의 subscribeOn와 observeOn (1)]]></title><description><![CDATA[<p>현재 약 2년동안 실무에 RxJava를 사용하고 있지만, 아직까지(부끄럽게도) 항상 동작이 헷갈리는 함수 2개가 있다. <br>
바로 <code>subscribeOn()</code> 과 <code>observeOn()</code>이다. 이미 많은 블로그들의 글을 읽어봤지만, 여전히 헷갈리는 듯하여 RxJava 내부적인 구현의 관점에서 나름대로 한번 정리를 해볼려고 한다.</p>

<hr>

<h3 id="reactivestreams">Reactive Streams</h3>

<p>먼저 RxJava의 <code>subscribeOn()</code> 과 <code>observeOn()</code>의 동작을 이해하기 위해서는, <strong><a id="jupiny-blog-link" href="https://github.com/reactive-streams/reactive-streams-jvm" target="_blank">Reactive Streams</a></strong></p>]]></description><link>https://jupiny.com/2020/04/11/rxjava-subscribeon-observeon-1/</link><guid isPermaLink="false">ee0d6c77-bb95-4bf4-b7c6-0d8ebeea6bd0</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sat, 11 Apr 2020 13:33:00 GMT</pubDate><content:encoded><![CDATA[<p>현재 약 2년동안 실무에 RxJava를 사용하고 있지만, 아직까지(부끄럽게도) 항상 동작이 헷갈리는 함수 2개가 있다. <br>
바로 <code>subscribeOn()</code> 과 <code>observeOn()</code>이다. 이미 많은 블로그들의 글을 읽어봤지만, 여전히 헷갈리는 듯하여 RxJava 내부적인 구현의 관점에서 나름대로 한번 정리를 해볼려고 한다.</p>

<hr>

<h3 id="reactivestreams">Reactive Streams</h3>

<p>먼저 RxJava의 <code>subscribeOn()</code> 과 <code>observeOn()</code>의 동작을 이해하기 위해서는, <strong><a id="jupiny-blog-link" href="https://github.com/reactive-streams/reactive-streams-jvm" target="_blank">Reactive Streams</a></strong> 의 기본 메커니즘을 이해하여야 한다. RxJava도 결국 이 Reactive Streams의 규격을 따르는 구현체의 하나일 뿐이기 때문이다.</p>

<h4 id="apispecification">API Specification</h4>

<p>Reactive Streams API 명세를 살펴보면, 아래와 같이 3개의 interface <code>Publisher</code>, <code>Subscription</code>, <code>Subscriber</code> 가 있다. (사실 <code>Processor</code>도 있지만, 여기서는 무시)</p>

<pre><code class="language-java">public interface Publisher&lt;T&gt; {  
    // 새로운 Subscription을 하나 만들어 Subscribe의 onSubscribe() 호출 
    public void subscribe(Subscriber&lt;? super T&gt; s);
}

public interface Subscription {  
    // Publisher에게 n개의 데이터 발행 요청
    public void request(long n);
    // Publisher에게 데이터 발행 중단 요청
    public void cancel();
}

public interface Subscriber&lt;T&gt; {  
    // Subscription의 request() 호출
    // Publisher는 그에 대한 응답을 내려줌
    public void onSubscribe(Subscription s);
    // Subscription의 request() 요청에 대한 Publisher의 데이터 응답
    public void onNext(T t)
    // Publisher의 데이터 발행 중의 에러 공지
    public void onError(Throwable t);
    // Publisher의 데이터 발행 완료 공지
    public void onComplete();
}
</code></pre>

<p><br></p>

<h4 id="flow">Flow</h4>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/04/rxjava-subscribeon-observeon-1-1.png" alt=""></p>

<ol>
<li><code>Subscriber</code> 생성하여 <code>Publisher</code>의 <code>subscribe(Subscriber s)</code> 실행  </li>
<li><code>Publisher</code>의 <code>subscribe(Subscriber s)</code>: <code>Subscription</code> 생성하여 <code>Subscriber</code>의 <code>onSubscribe(Subscription s)</code> 실행  </li>
<li><code>Subscriber</code>의 <code>onSubscribe(Subscription s)</code>: <code>Subscription</code>의 <code>request()</code> 실행  </li>
<li><code>Subscription</code>의 <code>request()</code>: <code>Publisher</code>에게 데이터 요청  </li>
<li><code>Publisher</code>은 <code>Subscription</code>을 통해 <code>Subscriber</code>의 <code>onNext()</code>/<code>onError()</code>/<code>onComplete()</code> 실행</li>
</ol>

<hr>

<h3 id="rxjava2flowable">RxJava2의 Flowable 구현</h3>

<p>코드를 통해 직접 확인해보기 위해 Reactive Streams의 규격을 잘 따르고 있는 RxJava2의 <code>Flowable</code>의 구현을 한번 살펴보자. <br>
(<code>Single</code>, <code>Mabye</code> 의 경우는 Reactive Streams의 규격을 따르지 않고 있음.)</p>

<blockquote>
  <p>Rxjava2 2.2.19 버젼을 사용하였습니다.</p>
</blockquote>

<h4 id="subscribeon">subscribeOn</h4>

<pre><code class="language-java">Flowable.just(1)  
        .subscribeOn(Schedulers.io()) // HERE
        .subcribe()
</code></pre>

<p><code>subscribeOn(Schedulers.io())</code> 코드의 구현을 따라가다 보면, <code>SubscribeOnSubscriber</code>이라는 <strong><a id="jupiny-blog-link" href="https://github.com/ReactiveX/RxJava/blob/v2.2.19/src/main/java/io/reactivex/internal/operators/flowable/FlowableSubscribeOn.java" target="_blank"><code>FlowableSubscribeOn</code></a></strong> 클래스의 내부 클래스를 만나게 된다. <br>
내부 코드 일부분을 살펴보면,</p>

<pre><code class="language-java">@Override
public void onNext(T t) {  
    downstream.onNext(t); // NOTE
}

@Override
public void onError(Throwable t) {  
    downstream.onError(t); // NOTE
    worker.dispose();
}

@Override
public void onComplete() {  
    downstream.onComplete(); // NOTE
    worker.dispose();
}
</code></pre>

<p>일단 downstream의 <code>Subscriber</code>의 동작을 실행하는 부분에는 설정한 <code>Scheduler</code>로 별도로 스케줄링하지 않고 있다.</p>

<pre><code class="language-java">@Override
public void request(final long n) {  
    if (SubscriptionHelper.validate(n)) {
        Subscription s = this.upstream.get();
        if (s != null) {
            requestUpstream(n, s); // NOTE
        } else {
            // ... 생략 ...
        }
    }
}

void requestUpstream(final long n, final Subscription s) {  
    if (nonScheduledRequests || Thread.currentThread() == get()) {
        s.request(n);
    } else {
        worker.schedule(new Request(s, n)); // NOTE
    }
}

static final class Request implements Runnable {  
    final Subscription upstream;
    final long n;

    Request(Subscription s, long n) {
        this.upstream = s;
        this.n = n;
    }

    @Override
    public void run() {
        upstream.request(n); // NOTE
    }
}
</code></pre>

<p><code>request()</code> 함수를 따라가보면, <code>scheduler.createWorker()</code>(위에 코드는 넣지 않았지만)로 생성한 <code>Worker</code>에 의해 upstream <code>Subscription</code>의 <code>request()</code>이 스케쥴링됨을 확인할 수 있다. </p>

<h4 id="observeon">observeOn</h4>

<pre><code class="language-java">Flowable.just(1)  
        .observeOn(Schedulers.io()) // HERE
        .subcribe()
</code></pre>

<p>마찬가지로 <code>observeOn(Schedulers.io())</code> 코드의 구현을 따라가다 보면, <code>ObserveOnSubscriber</code>이라는 <strong><a id="jupiny-blog-link" href="https://github.com/ReactiveX/RxJava/blob/v2.2.19/src/main/java/io/reactivex/internal/operators/flowable/FlowableObserveOn.java" target="_blank"><code>FlowableObserveOn</code></a></strong> 클래스의 내부 클래스를 만나게 된다.  그리고 이 <code>ObserveOnSubscriber</code> 상속하고 있는 추상 클래스 <code>BaseObserveOnSubscriber</code> 를 살펴보면,</p>

<pre><code class="language-java">@Override public final void onNext(T t) {
    // ... 생략 ...
    trySchedule(); // NOTE
}

@Override
public final void onError(Throwable t) {  
    // ... 생략 ...
    trySchedule(); // NOTE
}

@Override
public final void onComplete() {  
    // ... 생략 ...
    trySchedule(); // NOTE
}
</code></pre>

<p><code>Subscriber</code> 모든 함수의 마지막에 <code>trySchedule()</code>가 실행됨을 볼 수 있다. <code>trySchedule()</code> 의 구현을 보면,</p>

<pre><code class="language-java">final void trySchedule() {  
    if (getAndIncrement() != 0) {
        return;
    }
    worker.schedule(this); // NOTE
}
</code></pre>

<p>역시 <code>scheduler.createWorker()</code>를 통해 생성된 <code>Worker</code>를 통해 실행되는데, 여기서 <code>this</code>는 <code>Runnable</code> 를 구현한 자신이다. <br>
그럼 <code>Runnable</code>를 구현한 부분을 보면,</p>

<pre><code class="language-java">@Override
public final void run()  
    // ... 생략 ...
    runAsync(); // NOTE
}
</code></pre>

<p><code>runAsync()</code>이라는 추상 메쏘드를 실행한다. 처음으로 돌아와서 이 추상 클래스를 상속한 <code>ObserveOnSubscriber</code>에서 구현한 <code>runAsync()</code> 을 확인해보면,</p>

<pre><code class="language-java">@Override
void runAsync() {  
    // ... 생략 ...
    final Subscriber&lt;? super T&gt; a = downstream;
    final SimpleQueue&lt;T&gt; q = queue;
    // ... 생략 ...
    for (;;) {
        try {
            v = q.poll();
        } catch (Throwable ex) {
            // ... 생략 ...
            a.onError(ex); // NOTE
            worker.dispose();
            return;
        }

        boolean empty = v == null;

        // NOTE: checkTerminated() 반환값이 true인 경우, 반환하기 전에 함수 내에서 a.onError() 또는 a.onComplete()실행됨
        if (checkTerminated(d, empty, a)) {
            return;
        }
        // ... 생략 ...
        a.onNext(v); // NOTE
    }
}
</code></pre>

<p>무한 반복문을 돌면서, <code>queue</code>에 있는 value들을 하나씩 처리하면서 적절한 downstream <code>Subscriber</code>의 함수를 실행하게 된다. <br>
즉, downstream <code>Subscriber</code>의 함수들은 설정한 <code>Schedule</code>의 <code>Worker</code>에 의해 스케쥴링된다.</p>

<hr>

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

<p>구현에서 중요한 부분만 최대한 요약할려고 했음에도, 설명이 다소 길어졌던 것 같다. 구현 부분에 대한 설명이 길었지만, 확인한 결과를 간단히 요약해서 정리하면(<code>Flowable</code> 기준),</p>

<ul>
<li><code>subscribeOn(Scheduler scheduler)</code>: upstream <code>Subscription</code> 의 <code>reqeust()</code>를 실행할 <code>Scheduler</code> 를 설정</li>
<li><code>observeOn(Scheduler scheduler)</code>: downstream <code>Subscriber</code> 의 동작(<code>onNext()</code>, <code>onError()</code>, <code>onComplete()</code>) 을 실행할 <code>Scheduler</code>를 설정</li>
</ul>

<p>이 2가지만 기억한다면, 하나 이상의 <code>subscribeOn()</code> 또는 <code>observeOn()</code>을 붙였을 때 일어나는 스케쥴링 동작들을 이해하는데 큰 도움이 될 것이다.   </p>

<p>아래는 추가로 Reactive Streams의 규격을 따르고 있지는 않지만, Rxjava2의 <code>Single</code>, <code>Maybe</code>에서의 동작에 대해서도 적어보았다. <br>
Reactive Streams의 <code>Publiser</code>, <code>Subscriber</code>가 각각 <code>SingleSource</code>(또는 <code>MaybeSource</code>), <code>SingleObserver</code>(또는 <code>MaybeObserver</code>)에 대응된다고 생각하면 <code>Flowable</code>과 거의 유사하다.</p>

<ul>
<li><code>subscribeOn(Scheduler scheduler)</code>: upstream <code>SingleSource</code>(또는 <code>MaybeSource</code>)의 <code>subscribe()</code>를 실행할 <code>Scheduler</code> 를 설정</li>
<li><code>observeOn(Scheduler scheduler)</code>: downstream <code>SingleObserver</code>(또는 <code>MaybeObserver</code>)의 동작(<code>onSuccess()</code>, <code>onError()</code>, <code>onComplete()</code>) 을 실행할 <code>Scheduler</code>를 설정</li>
</ul>

<p><strong><a id="jupiny-blog-link" href="https://jupiny.com/2020/04/22/rxjava-subscribeon-observeon-2/" target="_blank">다음 글</a></strong> 에서는 실제 로그를 통해 이 동작을 확인해보도록 할 것이다.</p>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://engineering.linecorp.com/ko/blog/reactive-streams-with-armeria-1/" target="_blank">Armeria로 Reactive Streams와 놀자! – 1</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[Redis Sorted Set]]></title><description><![CDATA[<p>나는 과일 장수이다. 그리고 나는 판매하는 과일의 가격을 아래와 같이 손님들에게 보여주고 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/03/redis-sorted-set-1.png" alt=""></p>

<p>이 때, 아래와 같은 조건들을 만족해야 한다.</p>

<ol>
<li>각 과일의 가격은 자주 업데이트된다.  </li>
<li>각 과일은 유일(unique)하다.  </li>
<li>가격의 오름차순/내림차순으로 정렬된 과일 목록을 매번 고객에게 보여주어야 한다.</li>
</ol>

<p>이 모든 조건 안에서 가장 효율적인 자료구조를 이용하여 과일을 데이터로</p>]]></description><link>https://jupiny.com/2020/03/29/redis-sorted-set/</link><guid isPermaLink="false">150ad104-ffae-41d8-8a41-2eb844acbcab</guid><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Sat, 28 Mar 2020 19:13:56 GMT</pubDate><content:encoded><![CDATA[<p>나는 과일 장수이다. 그리고 나는 판매하는 과일의 가격을 아래와 같이 손님들에게 보여주고 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/03/redis-sorted-set-1.png" alt=""></p>

<p>이 때, 아래와 같은 조건들을 만족해야 한다.</p>

<ol>
<li>각 과일의 가격은 자주 업데이트된다.  </li>
<li>각 과일은 유일(unique)하다.  </li>
<li>가격의 오름차순/내림차순으로 정렬된 과일 목록을 매번 고객에게 보여주어야 한다.</li>
</ol>

<p>이 모든 조건 안에서 가장 효율적인 자료구조를 이용하여 과일을 데이터로 저장할려고 한다. <br>
단순히 1, 2번을 고려했을 때는 <em>Map</em>, <em>Dictionary</em>와 같은 key/value 형태의 자료구조를 쉽게 떠올릴 수 있다. key를 과일, value를 가격으로 저장하면 1, 2번에 대해 효율적으로 처리할 수 있다. <br>
하지만 3번의 경우를 생각해보면, 매번 저장된 key, value들을 모두 꺼내서 value에 대해 정렬해야하고, 또 정렬된 각 value에 해당하는 key들을 함께 최종적으로 반환해야되므로 시간적, 공간적으로 비용이 많이 든다. <br>
이 때 Redis에서 제공해주는 <strong>Sorted Set</strong> 자료구조를 이용하면 효율적으로 처리할 수 있다.</p>

<hr>

<h3 id="sortedset">Sorted Set이란?</h3>

<p>일반적으로 Set 자료구조는 저장된 value들을 unique하게 관리하기 위해 사용된다. 이 때 저장된 value들 사이의 순서는 관리되지 않는다. <br>
하지만 Redis에서 제공해주는 자료구조 중 하나인 <strong>Sorted Set</strong>(또는 <strong>ZSET</strong>, 둘다 동일한 말이다)은, 이름 그대로 Set의 특성을 그대로 가지면서 추가적으로 저장된 value들의 순서도 관리해준다. 이 때 이 순서를 위해 각 value에 대해 score를 필요에 맞게 설정할 수 있으며, 이 score를 기반으로 정렬이 된다.  </p>

<hr>

<h3 id="sortedset">Sorted Set 구조</h3>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/03/redis-sorted-set-2.png" alt=""></p>

<p>구조를 보면 우리가 쉽게 접한 단순한 key/value 형태의 자료구조보다 복잡하다. <code>key</code>, <code>member</code>, <code>score</code> 라는 새로운 용어가 등장하기 때문에 더 복잡해 보이기도 하다. <br>
여기서 <code>key</code> 는 Redis의 다른 자료구조들과 마찬가지로 단순히 하나의 <code>ZSET</code> 에 부여되는 key 이름이다. <br>
 <code>ZSET</code>은 key/value 형태의 자료구조이고, 여기서 key는 <code>member</code>, value는 <code>score</code> 라고 부른다. 하나의 <code>ZSET</code>에서 <code>member</code>는 unique하고, <code>member</code> 값을 통해 시간복잡도 O(log(N))으로 해당하는 value에 접근할 수 있다. (<strong><a id="jupiny-blog-link" href="https://redis.io/commands/zrank/" target="_blank"><code>ZRANK</code></a></strong> 참고) <br>
<code>score</code> 은 부동 소수점 수만 허용되고, 이 <code>score</code> 값을 기준으로 <code>ZSET</code> 내의 각 원소들이 순서를 가지게 된다.</p>

<hr>

<h3 id="sortedset">Sorted Set 명령어</h3>

<p>Sorted Set은 다양한 명령어들을 제공해준다. 이 중에서 가장 많이 사용되는 기본 명령어들에 대해서만 한번 살펴보도록 하자. 앞의 과일 장수 예시를 이용하여 각 명령어를 실행해보자.</p>

<h4 id="zadd">ZADD</h4>

<blockquote>
  <p>ZADD <em>key</em> <em>score</em> <em>member</em></p>
</blockquote>

<p>먼저 <code>ZSET</code>에 각 과일의 이름과 가격 데이터를 추가해보자. 과일 데이터를 저장할 <code>ZSET</code>의 <code>key</code>는 <code>fruit</code> 라고 정했다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZADD fruit 2 apple  
(integer) 1
127.0.0.1:6379&gt; ZADD fruit 10 strawberry  
(integer) 1
</code></pre>

<p>복수 개의 데이터를 한번에 넣을 수도 있다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZADD fruit 8 melon 4 orange 15 pineapple 5 banana  
(integer) 4
</code></pre>

<p>이미 존재하는 <code>member</code> 값이라면 <code>score</code> 값을 변경한다. 만약 apple의 가격이 20$으로 올랐다면, 아래와 같이 변경하면 된다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZADD fruit 20 apple  
(integer) 0
</code></pre>

<p><br></p>

<h4 id="zscore">ZSCORE</h4>

<blockquote>
  <p>ZSCORE <em>key</em> <em>member</em></p>
</blockquote>

<p>banana의 가격은 아래와 같이 조회하면 된다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZSCORE fruit banana  
"5"
</code></pre>

<p><br></p>

<h4 id="zrank">ZRANK</h4>

<blockquote>
  <p>ZRANK <em>key</em> <em>member</em></p>
</blockquote>

<p>melon이 몇 번째로 싼 과일인지는 아래와 같이 조회하면 된다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANK fruit melon  
(integer) 2
</code></pre>

<p><br></p>

<h4 id="zrange">ZRANGE</h4>

<blockquote>
  <p>ZRANGE <em>key</em> <em>start</em> <em>stop</em></p>
</blockquote>

<p><code>ZRANGE</code>에서 <code>start</code>, <code>stop</code> 에는 정렬된 원소들 중에서 내가 출력하고 싶은 원소의 시작 위치와 끝 위치를 넣으면 된다. 한가지 유념해야 할점은 첫번째 원소를 0이라 했을 때의 상대적인 위치값이고, 양수/음수 모두 가능하다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/03/redis-sorted-set-3.png" alt=""></p>

<p><code>ZRANGE</code>를 이용하여 전체 과일을 가격이 낮은 순서대로 모두 출력하고자 한다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGE fruit 0 -1  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "pineapple"  
6) "apple"  
</code></pre>

<p>가격이 가장 낮은 과일, 가장 높은 과일만을 출력하고 싶다면,</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGE fruit 0 0  
1) "orange"  
127.0.0.1:6379&gt; ZRANGE fruit -1 -1  
1) "apple"  
</code></pre>

<p>가격이 낮은 순서대로 나열했을 때, 2번째부터 3번째까지의 과일을 출력하고 싶다면,</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGE fruit 2 3  
1) "melon"  
2) "strawberry"  
</code></pre>

<p>만약 과일의 가격도 함께 출력하고 싶다면, <code>WITHSCORES</code> 옵션을 추가하면 된다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGE fruit 0 -1 WITHSCORES  
 1) "orange"
 2) "4"
 3) "banana"
 4) "5"
 5) "melon"
 6) "8"
 7) "strawberry"
 8) "10"
 9) "pineapple"
10) "15"  
11) "apple"  
12) "20"  
</code></pre>

<p>가격이 높은 순서대로 출력하고 싶다면, <code>ZREVRANGE</code> 명령어를 이용하자.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZREVRANGE fruit 0 -1 WITHSCORES  
 1) "apple"
 2) "20"
 3) "pineapple"
 4) "15"
 5) "strawberry"
 6) "10"
 7) "melon"
 8) "8"
 9) "banana"
10) "5"  
11) "orange"  
12) "4"  
</code></pre>

<p><br></p>

<h4 id="zrangebyscore">ZRANGEBYSCORE</h4>

<blockquote>
  <p>ZRANGEBYSCORE <em>key</em> <em>min</em> <em>max</em></p>
</blockquote>

<p>6$ 이상 15$ 이하 가격의 과일들을 가격이 낮은 순서대로 출력하고자 한다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGEBYSCORE fruit 6 15 WITHSCORES  
1) "melon"  
2) "8"  
3) "strawberry"  
4) "10"  
5) "pineapple"  
6) "15"  
</code></pre>

<p>5$ 초과 15$ 미만 가격의 가격의 과일들을 가격이 낮은 순서대로 출력하고자 한다면,</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGEBYSCORE fruit (5 (15 WITHSCORES  
1) "melon"  
2) "8"  
3) "strawberry"  
4) "10"  
</code></pre>

<p>가격 상관없이 모든 과일들을 가격이 낮은 순서대로 출력하고자 한다면,</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZRANGEBYSCORE fruit -inf +inf  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "pineapple"  
6) "apple"  
</code></pre>

<p>10$ 보다 비싼 모든 과일들을 가격이 높은 순서대로 출력하고자 한다면, <br>
(<code>ZREVRANGEBYSCORE</code>에서는 <code>min</code>과 <code>max</code> 의 위치가 <code>ZRANGEBYSCORE</code>에서와 반대다.)</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZREVRANGEBYSCORE fruit +inf (10 WITHSCORES  
1) "apple"  
2) "20"  
3) "pineapple"  
4) "15"  
</code></pre>

<p><br></p>

<h4 id="zrem">ZREM</h4>

<blockquote>
  <p>ZREM <em>key</em> <em>member</em></p>
</blockquote>

<p>pineapple을 더이상 판매하지 않기로 하여 pineapple 데이터를 지울려고 한다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZREM fruit pineapple  
(integer) 1
127.0.0.1:6379&gt; ZRANGE fruit 0 -1  
1) "orange"  
2) "banana"  
3) "melon"  
4) "strawberry"  
5) "apple"  
</code></pre>

<p><code>ZADD</code> 와 마찬가지로 한번에 복수개의 삭제도 가능하다.</p>

<pre><code class="language-shell">127.0.0.1:6379&gt; ZREM fruit strawberry orange  
(integer) 2
127.0.0.1:6379&gt; ZRANGE fruit 0 -1  
1) "banana"  
2) "melon"  
3) "apple"  
</code></pre>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://www.tutorialspoint.com/redis/redis_sorted_sets.htm" target="_blank">Redis - Sorted Sets</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/1-2-5-sorted-sets-in-redis/" target="_blank">1.2.5 Sorted sets in Redis</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://redislabs.com/ebook/part-2-core-concepts/chapter-3-commands-in-redis/3-5-sorted-sets/" target="_blank">3.5 Sorted sets</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://redis.io/commands" target="_blank">Command reference – Redis</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[Armeria의 Circuit breaker 사용해보기]]></title><description><![CDATA[<h3 id="circuitbreaker">Circuit Breaker란?</h3>

<p>만약 예상치 못한 장애(ex. 네크워크 이슈, 서버가 내려감)가 발생하여, 어떤 한 원격 서버가 요청에 대한 응답을 내리지 못하는 상태라고 가정해보자. 이러한 상황에서 원격 서버로 요청을 한 클라이언트는 timeout이 발생할 때까지 응답을 기다리거나, 자원을 소모하며 결국 무의미한 요청을 계속 보낼 것이다. 그리고 MSA(Microservice Architecture)에서 이</p>]]></description><link>https://jupiny.com/2020/01/31/armeria-circuit-breaker/</link><guid isPermaLink="false">8b602727-f0f4-4315-83b5-95d1667eba48</guid><category><![CDATA[Java]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Thu, 30 Jan 2020 16:01:12 GMT</pubDate><content:encoded><![CDATA[<h3 id="circuitbreaker">Circuit Breaker란?</h3>

<p>만약 예상치 못한 장애(ex. 네크워크 이슈, 서버가 내려감)가 발생하여, 어떤 한 원격 서버가 요청에 대한 응답을 내리지 못하는 상태라고 가정해보자. 이러한 상황에서 원격 서버로 요청을 한 클라이언트는 timeout이 발생할 때까지 응답을 기다리거나, 자원을 소모하며 결국 무의미한 요청을 계속 보낼 것이다. 그리고 MSA(Microservice Architecture)에서 이 클라이언트는 또 누군가에게는 서버일 수도 있다. 결국 이 서버에 대한 클라이언트 역시 똑같은 문제를 겪게될 것이다. <br>
이렇게 계속 장애가 전파되며, 결과적으로 한 원격 서버의 장애가 모든 시스템에 큰 영향을 주게 된다. 이러한 문제를 해결하기 위해 등장한 개념이 바로 <strong>Circuit Breaker</strong> 이다. <br>
<strong>Circuit Breaker</strong> 란 쉽게 말해, 클라이언트에서 한 원격 서버로의 요청에 대한 실패율이 특정 threshold를 넘게 되면, 이 서버에 문제가 있다고 스스로 판단하여 더이상 무의미한 요청을 날리지 않고, 빠르게 에러를 발생시키는(fail fast) 방법이다. 이러한 방법으로 앞서 언급한 문제들을 방지하며 장애의 규모를 최소화할 수 있다. <br>
이에 대해선 이미 많은 블로그들에 Circuit Breaker의 개념이 잘 나와 있으므로, 더이상 추가적인 설명은 생략하도록 하겠다. (<strong><a id="jupiny-blog-link" href="https://engineering.linecorp.com/ko/blog/circuit-breakers-for-distributed-services/" target="_blank">분산 서비스 환경에 대한 Circuit Breaker 적용</a></strong> 참고)</p>

<hr>

<h3 id="circuitbreaker">Circuit Breaker의 상태</h3>

<p>하나의 Circuit Breaker는 총 3가지 상태를 가진다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-1.png" alt=""></p>

<ul>
<li><code>CLOSED</code>: 요청의 실패율이 정해놓은 threshold보다 낮은 상태. 정상적인 상태.</li>
<li><code>OPEN</code>: 요청의 실패율이 정해놓은 threshold를 넘어선 상태. 실제 요청을 날리지 않고 바로 에러를 발생시킴(fail fast)</li>
<li><code>HALF_OPEN</code>: <code>OPEN</code> 상태 중간에 한번씩 요청을 날려 응답이 성공인지를 확인하는 상태. 성공인 경우, 다시 <code>CLOSED</code> 상태로 전환. 실패인 경우는 그대로 <code>OPEN</code> 상태로 유지.</li>
</ul>

<hr>

<h3 id="armeriacircuitbreaker">Armeria의 Circuit breaker</h3>

<p><strong><a id="jupiny-blog-link" href="https://linepluscorp.com/" target="_blank">LINE</a></strong> 에서 오픈소스로 운영하고 있는 <strong><a id="jupiny-blog-link" href="https://netty.io/" target="_blank">Netty</a></strong> 기반의 비동기 마이크로서비스 프레임워크, <strong><a id="jupiny-blog-link" href="https://github.com/line/armeria" target="_blank">Armeria</a></strong> 에서는 이러한 Circuit breaker 기능을 직접 구현하여 잘 제공해주고 있다. 이를 이용하여 Circuit breaker의 동작을 직접 로그를 통해 확인해보자.</p>

<hr>

<h3 id="">준비하기</h3>

<p>먼저, 테스트를 위해 간단하게 요청과 응답을 주고 받을 서버 2대(Server1, Server2)를 띄어보자. <br>
Server1은 클라이언트로부터 <code>/hello</code> 요청이 들어오면, Server2로 <code>/world</code> 요청을 보낸다. 그리고 Server2로부터 응답을 받으면, 그 응답을 다시 클라이언트에게 반환한다. Server1은 서버이기도 하지만, Server2의 클라이언트이기도 하다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-2.png" alt=""></p>

<h4 id="server2">Server2</h4>

<p><strong>[Server2Application.java]</strong></p>

<pre><code class="language-java">@SpringBootApplication
public class Server2Application {

    private static final AtomicInteger REQ_CNT = new AtomicInteger();

    public static void main(String[] args) {
        ServerBuilder sb = Server.builder();
        sb.http(5008);
        sb.decorator(LoggingService.newDecorator());

        sb.service("/world", (ctx, res) -&gt; {
            if (REQ_CNT.addAndGet(1) % 2 == 0) {
                return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
            }
            return HttpResponse.of(HttpStatus.OK);
        });

        Server server = sb.build();
        CompletableFuture&lt;Void&gt; future = server.start();
        future.join();
    }
}
</code></pre>

<p>먼저, Server1의 응답을 받을 Server2의 구현이다. <code>/world</code> 로 들어오는 요청에 대한 응답으로 성공(200)과 실패(500)를 번갈아가면서 반환하도록 간단하게 구현하였다.</p>

<h4 id="server1">Server1</h4>

<p><strong>[Server1Application.java]</strong></p>

<pre><code class="language-java">@SpringBootApplication
@Import(Server1Context.class)
public class Server1Application {

    public static void main(String[] args) {
        SpringApplication.run(Server1Application.class, args);
    }
}
</code></pre>

<p><br> <br>
<strong>[Server1Context.java]</strong></p>

<pre><code class="language-java">@Configuration
public class Server1Context {

    @Bean
    public HttpServiceRegistrationBean httpService(WebClient webClient) {
        return new HttpServiceRegistrationBean()
                .setService((ctx, req) -&gt; webClient.get("/world"))
                .setServiceName("httpService")
                .setRoute(Route.builder()
                               .path("/hello")
                               .methods(HttpMethod.GET)
                               .build());
    }

    @Bean
    public WebClient webClient() {
        CircuitBreakerStrategy strategy = CircuitBreakerStrategy.onServerErrorStatus();

        // CircuitBreaker 설정!!!
        CircuitBreaker circuitBreaker = CircuitBreaker
                .builder("test-circuit-breaker")
                .counterSlidingWindow(Duration.ofSeconds(10))
                .circuitOpenWindow(Duration.ofSeconds(5))
                .failureRateThreshold(0.3)
                .minimumRequestThreshold(5)
                .trialRequestInterval(Duration.ofSeconds(3))
                .build();

        return WebClient
                .builder("http://localhost:5008")
                .decorator(LoggingClient.newDecorator())
                .decorator(CircuitBreakerHttpClient.newDecorator(circuitBreaker, strategy))
                .build();
    }
}
</code></pre>

<p>이제 Server1의 구현이다. 서버2에서 <code>ServerBuilder</code>를 이용하여 간단하게 구현한 것과 다르게, 클라이언트로부터 요청을 받을 Service와 Server2로 요청을 보낼 Client를 모두 Bean으로 만들어 사용하고 있는 점이 눈에 띈다. <br>
Armeria에서 <code>CircuitBreaker</code>를 구현할 때 주의해야할 점은, 하나의 Client 객체가 자신만의 <code>CircuitBreaker</code>를 가진다는 것을 꼭 인지하고 있어야한다. 즉, 만약 Server2로의 매번 요청에 대해 Client 객체가 새로 생성되고, 또 요청이 끝난 후에 소멸이 되버리는 방식으로 구현했다면 결국 하나의 Client에 연결된 <code>CircuitBreaker</code>가 사실상 무의미하다. <br>
따라서 Server2로 요청을 보낼 <code>WebClient</code> 를 Bean으로 만들어, 모든 Server2로의  요청에 대해 동일한 <code>WebClient</code> 객체를 재사용하도록 구현하였다. 그리고 이 <code>WebClient</code> 객체에 Armeria에서 제공하는 <strong><a id="jupiny-blog-link" href="https://line.github.io/armeria/client-decorator.html" target="_blank">Decorator</a></strong>를 이용해 Circuit breaker를 적용하였다.</p>

<p>위의 <code>CircuitBreaker</code>에 설정한 각 필드를 하나씩 살펴보자.</p>

<ul>
<li><code>counterSlidingWindow</code>(10s): Circuit breaker에서 요청의 성공/실패의 수를 측정하는 시간 간격이다. 즉 10초 동안의 집계를 바탕으로 어떤 상태로 전환 또는 유지할지 판단한다.</li>
<li><code>circuitOpenWindow</code>(5s): Circuit breaker가 <code>OPEN</code> 상태로 유지되는 시간이다. 즉 한번 <code>OPEN</code> 상태가 되면, 5초 동안은 외부 서버로 요청하지 않고 바로 에러(<code>FailFastException</code>)를 던진다.</li>
<li><code>failureRateThreshold</code>(0.3): <code>OPEN</code> 상태로 전환되는데 필요한 요청의 실패율이다. <code>circuitOpenWindow</code> 시간 동안 0.3(30%) 이상의 요청이 실패하여야만 <code>OPEN</code> 상태가 된다.</li>
<li><code>minimumRequestThreshold</code>(5): 측정에 필요한 요청의 최소 갯수이다. 실패율이 <code>failureRateThreshold</code>보다 높다하더라도, 최소 5개 이상의 요청에 대한 결과값이여야만 <code>OPEN</code> 상태가 된다. </li>
<li><code>trialRequestInterval</code>(3s): <code>HALF_OPEN</code> 상태로 유지되는 시간이다. 3초 동안은 OPEN 상태일 때와 마찬가지로 <code>FailFastException</code> 에러를 던지는 한편 그동안 외부 서버에 요청을 날리며 서버가 정상으로 돌아왔는지를 검사한다. 만약 이 3초 동안에 날린 요청의 응답이 성공이라면 즉시 CLOSED 상태로 전환되고, 모두 실패라면 다시 OPEN 상태로 전환된다.</li>
</ul>

<hr>

<h3 id="">테스트</h3>

<p><strong><a id="jupiny-blog-link" href="https://www.npmjs.com/package/loadtest" target="_blank">loadtest</a></strong> 라이브러리를 이용하여 터미널에서 한번 Server1로 계속 요청을 날려보자.</p>

<pre><code class="language-shell">$ loadtest http://127.0.0.1:5007/hello --rps 1
</code></pre>

<p>Server1의 로그를 통해, Circuit breaker의 동작을 눈으로 확인할 수 있다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-3.png" alt=""></p>

<p>처음에 서버가 뜰 때, Circuit breaker의 초기 상태는 <code>CLOSED</code>이다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-4.png" alt=""></p>

<p><em>03:03:41.830</em> 초에 측정된 전체 5개의 요청 중 실패의 수는 2이므로, 실패율은 2/5 = 0.4이다. 따라서 실패율이 앞서 설정한 0.3(<code>failureRateThreshold</code>)보다 크고, 측정에 사용한 요청 수도 5(<code>minimumRequestThreshold</code>) 이상이므로 Circuit breaker는 <code>OPEN</code> 상태로 전환된다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-5.png" alt=""></p>

<p>이후 <em>03:03:41.830</em> ~ <em>03:03:47.824</em>, 약 5초(<code>circuitOpenWindow</code>)동안 Server2로 요청을 날리진 않고 <code>FailFastException</code> 에러가 계속 발생한다. (Server2의 로그를 확인해보면, 해당 시간에 요청이 들어오지 않았음을 확인해볼 수 있다.)</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-6.png" alt=""></p>

<p><em>03:03:47.824</em> 초에 <code>HALF_OPEN</code> 상태로 전환되고, 이 때 클라이언트로부터 들어온 요청은 Server2로 실제 전송된다. 그리고 이 때 전송된 요청이 운좋게 200 응답을 받으면서 <code>CLOSED</code> 상태로 전환된다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2020/01/armeria-circuit-breaker-7.png" alt=""></p>

<p>다시 <code>CLOSED</code>가 되었으므로 이 과정을 또 반복한다. <br>
<em>03:03:47.827</em> ~ <em>03:03:56.824</em>, 약 10초(<code>counterSlidingWindow</code>)동안의 실패율을 측정하게 되고, 이번에 측정한 실패율은 4/8 = 0.5이므로 Circuit breaker는 다시 <code>OPEN</code> 상태로 전환된다.</p>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://armeria.dev/docs/client-circuit-breaker/" target="_blank">Circuit breaker &mdash; Armeria documentation</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[로그를 볼 때 유용한 shell 명령어들]]></title><description><![CDATA[<p>서버 개발을 하다보면, 로그를 밥먹듯이 보게 된다.
로그는 디버깅을 하는데 이용되기도 하지만, 때로는 원하는 통계와 데이터를 얻기 위해 사용되기도 한다. <br>
이 글은 후자의 경우, 내가 자주 겪었던 문제와 이를 해결하기 위해 사용했던 방법들을 정리하기 위해 작성하였다.</p>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#1">특정 문자열이 포함된 줄 뽑아내기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#2">특정 위치의 문자열 뽑아내기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#3">특정 패턴에 해당하는 문자열 뽑아내기</a></strong></li></ul>]]></description><link>https://jupiny.com/2019/12/05/useful-shell-commands-when-viewing-logs/</link><guid isPermaLink="false">23d63e67-d474-4eb5-80e1-b3d2432bf0fb</guid><category><![CDATA[Linux]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Wed, 04 Dec 2019 19:12:58 GMT</pubDate><content:encoded><![CDATA[<p>서버 개발을 하다보면, 로그를 밥먹듯이 보게 된다.
로그는 디버깅을 하는데 이용되기도 하지만, 때로는 원하는 통계와 데이터를 얻기 위해 사용되기도 한다. <br>
이 글은 후자의 경우, 내가 자주 겪었던 문제와 이를 해결하기 위해 사용했던 방법들을 정리하기 위해 작성하였다.</p>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#1">특정 문자열이 포함된 줄 뽑아내기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#2">특정 위치의 문자열 뽑아내기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#3">특정 패턴에 해당하는 문자열 뽑아내기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#4">특정 위치의 문자열 기준으로 정렬하기</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="https://jupiny.com/2019/12/04/useful-shell-commands-when-viewing-logs/#5">특정 패턴에 해당하는 문자열 기준으로 정렬하기</a></strong></li>
</ul>

<blockquote>
  <p>예제에 사용된 <code>access_log</code>는, 운영하고 있는 블로그의 실제 <strong><a id="jupiny-blog-link" href="https://gist.github.com/jupiny/1acbc878d47cb497ac0dd0fbac2564af" target="_blank">access.log</a></strong> 파일을 사용하였습니다.</p>
</blockquote>

<h3 id="1">특정 문자열이 포함된 줄 뽑아내기</h3>

<p>Mac OS를 사용하고 있는 유저의 요청 로그만을 뽑고 싶다고 가정해보자. <br>
Mac OS를 사용하고 있는 유저의 로그에는 '<em>Macintosh; Intel Mac OS X</em>' 이라는 문자열이 존재한다는 공통점을 이용하면, <code>grep</code> 으로 쉽게 뽑아낼 수 있다.</p>

<pre><code class="language-shell">$ grep 'Macintosh; Intel Mac OS X' access.log
</code></pre>

<p>만약 총 몇 개의 요청이 있었는지 카운트하고 싶다면, <code>wc -l</code> 명령어를 파이프라인으로 연결하면 된다.</p>

<pre><code class="language-shell">$ grep 'Macintosh; Intel Mac OS X' access.log | wc -l

32  
</code></pre>

<hr>

<h3 id="2">특정 위치의 문자열 뽑아내기</h3>

<p>이번에는 로그에서 <code>GET</code> 으로 들어온 요청의 경로(PATH)만을 뽑아내고 싶다고 가정해보자. <br>
일단 <code>GET</code> 으로 들어온 요청은 앞에서와 동일하게 <code>grep</code> 만으로 쉽게 뽑아낼 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log
</code></pre>

<p>문제는 경로를 뽑아내는 것인데, 로그를 자세히 보면 각 줄을 공백으로 구분했을 때 7번째 구성요소가 항상 경로인 점을 발견할 수 있다. 이러한 규칙을 이용하면  <code>cut</code> 명령어로 뽑아낼 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log | cut -d " " -f 7

/
/2019/07/15/java-heap-dump-analysis/
/robots.txt
...
</code></pre>

<p>만약 각 경로별로 카운트를 하고 싶다면, <code>sort</code>와 <code>uniq</code> 명령어를 함께 이용하면 된다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log | cut -d " " -f 7 | sort | uniq -c

73 /  
 3 /2016/09/25/decorator-class/
 1 /2016/09/25/decorator-function/
 1 /2016/10/05/ci-github-slack
 1 /2016/10/05/ci-github-slack/
...
</code></pre>

<p>추가로, 가장 많이 카운트된 순서대로 위에서부터 출력하고 싶다면, <code>sort</code> 를 한번 더 사용하면 된다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log | cut -d " " -f 7 | sort | uniq -c | sort -nr

73 /  
40 /assets/js/jquery.fitvids.js?v=6ea3fea03b  
40 /assets/js/index.js?v=6ea3fea03b  
39 /assets/css/screen.css?v=6ea3fea03b  
35 /favicon.ico  
...
</code></pre>

<hr>

<h3 id="3">특정 패턴에 해당하는 문자열 뽑아내기</h3>

<p>이번에는 <code>GET /favicon.ico</code> 으로 들어온 요청에서 <code>HH(시):MM(분):SS(초) +0000</code> 부분만을 깔끔하게 뽑아내고 싶다고 가정해보자. <br>
일단 앞서 배운 것들을 이용해 <code>[05/Nov/2019:HH:MM:SS +0000]</code> 라고 적힌 부분만을 뽑아낼 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /favicon.ico' access.log | cut -d " " -f 4 -f 5

[05/Nov/2019:19:33:45 +0000]
[05/Nov/2019:19:34:40 +0000]
[05/Nov/2019:20:00:47 +0000]
...
</code></pre>

<p>먼저 앞의 <code>[05/Nov/2019:</code> 부분을 제거해보자. 이때 <code>awk</code> 명령어를 이용하면 이러한 처리를 쉽게 할 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /favicon.ico' access.log | cut -d " " -f 4 -f 5 | awk -F '0[5-6]/Nov/2019:' '{print $2}'

19:33:45 +0000]  
19:34:40 +0000]  
20:00:47 +0000]  
...
</code></pre>

<p>유사한 방법으로 끝에 붙어있는 <code>]</code> 도 제거할 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /favicon.ico' access.log | cut -d " " -f 4 -f 5 | awk -F '0[5-6]/Nov/2019:' '{print $2}' | awk -F ']' '{print $1}'

19:33:45 +0000  
19:34:40 +0000  
20:00:47 +0000  
...
</code></pre>

<p>또는 정규표현식과 <code>awk</code>의 내장함수 <code>match()</code>, <code>substr()</code> 함수를 이용하면 정규표현식에 해당하는 문자열만을 쉽게 뽑아낼 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /favicon.ico' access.log | awk 'match($0, "[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2} \\+0000")  {print substr($0, RSTART, RLENGTH)}'

19:33:45 +0000  
19:34:40 +0000  
20:00:47 +0000  
</code></pre>

<hr>

<h3 id="4">특정 위치의 문자열 기준으로 정렬하기</h3>

<p>이번에는 로그에서 GET 으로 들어온 요청의 경로(PATH)의 오름차순으로 로그를 정렬해보자. <br>
앞에서 배운 것을 활용하면 특정 위치의 문자열을 뽑은 후, 그 문자열들을 정렬한 결과를 쉽게 출력해볼 수 있다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log | cut -d " " -f 7 | sort

/
/
...
/tag/ruby-on-rails/
/tag/study/
/tag/study/
</code></pre>

<p>하지만 내가 정말 원하는 것은 뽑아낸 PATH만이 아닌, PATH를 기준으로 정렬된 전체 로그를 출력하고 싶다. <br>
이러한 경우를 위해 <code>sort</code> 는 특정 위치의 문자열에 대해 정렬할 수 있는 옵션을 제공해주고 있다. 각 줄을 공백으로 구분했을 때 7번째 구성요소를 기준으로 오름차순 정렬하고 싶은 것이므로, 아래와 같이 <code>sort</code> 명령어 옵션 <code>-t</code>와 <code>-k</code>에 적절한 값을 넣으면 된다.</p>

<pre><code class="language-shell">$ grep 'GET /' access.log | sort -t " " -k 7

67.212.187.106 - - [05/Nov/2019:19:31:24 +0000] "GET / HTTP/1.0" 200 14299 "-" "-"  
5.189.142.121 - - [05/Nov/2019:19:57:24 +0000] "GET / HTTP/1.0" 301 184 "-" "masscan/1.0 (https://github.com/robertdavidgraham/masscan)"  
138.197.64.71 - - [05/Nov/2019:21:25:51 +0000] "GET / HTTP/1.1" 200 14299 "-" "-"  
...
46.229.168.161 - - [05/Nov/2019:20:57:10 +0000] "GET /tag/study/ HTTP/1.1" 200 3144 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"  
138.197.64.71 - - [05/Nov/2019:21:26:14 +0000] "GET /tag/study/ HTTP/1.1" 200 3144 "https://jupiny.com" "Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"  
</code></pre>

<p>만약 7번째 문자열이 동일한 경우는 그 다음 8번째의 문자열을 기준으로 정렬되고, 또 동일하다면 그 다음 9번째, 10번째,... 이러한 순서로 정렬이 됨을 볼 수 있다.</p>

<hr>

<h3 id="5">특정 패턴에 해당하는 문자열 기준으로 정렬하기</h3>

<p>이번에는 더 난이도를 높여서 특정 위치의 문자열이 아닌, 특정 패턴에 해당하는 문자열을 뽑아서 그 문자열을 기준으로 전체 로그를 정렬해보자.</p>

<p>만약 <code>GET /yyyy/mm/dd/[URL 이름]/</code> 으로 들어온 요청 중에서, <code>URL 이름</code> 의 오름차순으로 전체 로그를 정렬하고 싶다고 하자. <br>
먼저 해당 패턴에 해당하는 문자열(여기선 URL 이름)을 뽑아내야 한다. 앞에서와 마찬가지로 <code>awk</code> 명령어와 <code>awk</code>의 내장함수 <code>match()</code>, <code>substr()</code> 함수를 이용하여 정규표현식에 해당하는 문자열을 뽑아내보자.</p>

<pre><code class="language-shell">$ awk 'match($0, "GET /[[:digit:]]{4}/[[:digit:]]{2}/[[:digit:]]{2}/[a-z0-9\-]+/")  {print substr($0, RSTART+16, RLENGTH-17)}' access.log

java-heap-dump-analysis  
test-in-multiple-environments-using-pyenv-and-tox  
linux-command-1-grep-less-head-tail  
sort-korean-in-postgresql  
ci-github-slack  
...
</code></pre>

<p>뒤에 <code>sort</code>를 파이프라인으로 연결하면 해당 문자열들은 오름차순으로 정렬되겠지만, 아까와 마찬가지로 이는 우리가 원하는 결과가 아니다. 우리가 원하는 것은 이 문자열을 기준으로 정렬된 전체 로그이다.  </p>

<p>이를 해결하기 위해 약간의 꼼수(?)를 써보자. ('꼼수'라고 언급한 이유는 사실 썩 마음에 드는 깔끔한 해결법이 아니었기 때문인데, 혹시라도 더 좋은 방법을 찾게 된다면 글을 업데이트하도록 하겠다.) <br>
이 해결 방법에는 크게 3단계가 필요하다.</p>

<ol>
<li><code>정규표현식으로 뽑아낸 문자열\t원본</code> 형태로 출력  </li>
<li>출력된 결과를 정렬 -> <code>정규표현식으로 뽑아낸 문자열</code> 기준으로 정렬됨  </li>
<li><code>\t</code> 뒤의 <code>원본</code>만을 출력</li>
</ol>

<p>한 단계식 순서대로 적용해보자. <br>
먼저 1단계, <code>정규표현식으로 뽑아낸 문자열\t원본</code> 형태로 출력해보자.</p>

<pre><code class="language-shell">$ awk 'match($0, "GET /[[:digit:]]{4}/[[:digit:]]{2}/[[:digit:]]{2}/[a-z0-9\-]+/")  {print substr($0, RSTART+16, RLENGTH-17)"\t"$0}' access.log

java-heap-dump-analysis 66.249.75.38 - - [05/Nov/2019:19:20:35 +0000] "GET /2019/07/15/java-heap-dump-analysis/ HTTP/1.1" 200 5556 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"  
test-in-multiple-environments-using-pyenv-and-tox       46.229.168.140 - - [05/Nov/2019:19:29:37 +0000] "GET /2018/07/31/test-in-multiple-environments-using-pyenv-and-tox/ HTTP/1.1" 200 6013 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl;  
+http://www.semrush.com/bot.html)"
linux-command-1-grep-less-head-tail     23.100.232.233 - - [05/Nov/2019:19:37:54 +0000] "GET /2017/07/09/linux-command-1-grep-less-head-tail/ HTTP/1.1" 200 4655 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0;  Trident  
/5.0)"
...
</code></pre>

<p>이제 이 로그들을 <code>sort</code>를 이용해 오름차순으로 정렬해보자.</p>

<pre><code class="language-shell">$ awk 'match($0, "GET /[[:digit:]]{4}/[[:digit:]]{2}/[[:digit:]]{2}/[a-z0-9\-]+/")  {print substr($0, RSTART+16, RLENGTH-17)"\t"$0}' access.log | sort

caching-using-redis-on-django   221.133.55.100 - - [06/Nov/2019:00:22:42 +0000] "GET /2018/02/27/caching-using-redis-on-django/ HTTP/1.1" 200 7060 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36"  
caching-using-redis-on-django   221.133.55.100 - - [06/Nov/2019:00:44:22 +0000] "GET /2018/02/27/caching-using-redis-on-django/ HTTP/1.1" 304 0 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36"  
...
test-in-multiple-environments-using-pyenv-and-tox       46.229.168.140 - - [05/Nov/2019:19:29:37 +0000] "GET /2018/07/31/test-in-multiple-environments-using-pyenv-and-tox/ HTTP/1.1" 200 6013 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"  
test-in-multiple-environments-using-pyenv-and-tox       66.249.75.42 - - [06/Nov/2019:00:50:03 +0000] "GET /2018/07/31/test-in-multiple-environments-using-pyenv-and-tox/amp/ HTTP/1.1" 200 7486 "-" "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"  
</code></pre>

<p>마지막으로, 정렬을 위해 임시로 앞에 붙였던 <code>정규표현식으로 뽑아낸 문자열</code> 부분을 <code>cut</code> 을 이용해 제거하면 된다.</p>

<pre><code class="language-shell">$ awk 'match($0, "GET /[[:digit:]]{4}/[[:digit:]]{2}/[[:digit:]]{2}/[a-z0-9\-]+/")  {print substr($0, RSTART+16, RLENGTH-17)"\t"$0}' access.log | sort | cut -d $'\t' -f 2

221.133.55.100 - - [06/Nov/2019:00:22:42 +0000] "GET /2018/02/27/caching-using-redis-on-django/ HTTP/1.1" 200 7060 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36"  
221.133.55.100 - - [06/Nov/2019:00:44:22 +0000] "GET /2018/02/27/caching-using-redis-on-django/ HTTP/1.1" 304 0 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36"  
...
46.229.168.140 - - [05/Nov/2019:19:29:37 +0000] "GET /2018/07/31/test-in-multiple-environments-using-pyenv-and-tox/ HTTP/1.1" 200 6013 "-" "Mozilla/5.0 (compatible; SemrushBot/6~bl; +http://www.semrush.com/bot.html)"  
66.249.75.42 - - [06/Nov/2019:00:50:03 +0000] "GET /2018/07/31/test-in-multiple-environments-using-pyenv-and-tox/amp/ HTTP/1.1" 200 7486 "-" "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"  
</code></pre>

<hr>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://recipes4dev.tistory.com/171" target="_blank">리눅스 awk 명령어 사용법. (Linux awk command) - 리눅스 파일 텍스트 데이터 검사, 조작, 출력.</a></strong></li>
<li><strong><a id="jupiny-blog-link" href="http://bahndal.egloos.com/621562" target="_blank">[bash: awk] 문자열을 검색해서 일치하는 부분만 출력하기(match, substr)</a></strong></li>
</ul>]]></content:encoded></item><item><title><![CDATA[ghost 로그인 시 SQLITE_FULL 에러]]></title><description><![CDATA[<p>최근 블로그에 글을 안 쓴지 오래되었다. 마지막 글이 4달 전에 작성된 걸 보면, 그동안 내가 얼마나 글쓰는 것을 미루었는가를 다시금 반성하게 된다. 그래서 큰 맘먹고 오랜만에 글을 쓰기 위해 로그인하였지만, 아래와 같이 예상치 못한 에러가 나타나며 로그인을 할 수 없었다. (만약 이 문제를 해결못했다면, 슬프게도 이 글조차 쓰지 못했을 것이다.</p>]]></description><link>https://jupiny.com/2019/11/07/ghost-signin-sqlite-full-error/</link><guid isPermaLink="false">32560acb-7a28-43b9-a555-6f3fd6e88942</guid><category><![CDATA[etc]]></category><dc:creator><![CDATA[jupiny]]></dc:creator><pubDate>Wed, 06 Nov 2019 17:03:32 GMT</pubDate><content:encoded><![CDATA[<p>최근 블로그에 글을 안 쓴지 오래되었다. 마지막 글이 4달 전에 작성된 걸 보면, 그동안 내가 얼마나 글쓰는 것을 미루었는가를 다시금 반성하게 된다. 그래서 큰 맘먹고 오랜만에 글을 쓰기 위해 로그인하였지만, 아래와 같이 예상치 못한 에러가 나타나며 로그인을 할 수 없었다. (만약 이 문제를 해결못했다면, 슬프게도 이 글조차 쓰지 못했을 것이다.)</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2019/11/ghost-signin-sqlite-full-error-1-1.png" alt=""></p>

<p>여태껏 로그인하며 한번도 본 적이 없었던 에러였기 때문에, 처음에는 믿지 못하고(?) 몇 번을 재시도했지만 결과는 마찬가지였다. <br>
이 에러 로그를 보고 처음 알았지만, ghost에서는 내부적으로 유저가 로그인할때마다 유저의 accessToken을 데이터베이스에 저장하고 있었고, 별도로 설정하지 않았다면 기본적으로 DBMS는 SQLite를 사용하고 있었다.
하지만 에러 로그에서 알 수 있듯이, 저장을 하려고 할 때 데이터베이스 또는 디스크의 용량이 부족하여 실패하였다.</p>

<p>이때까지만 해도, 에이 설마 디스크가 꽉 찼겠어?라고 생각했지만, 서버에 직접 접속해서 확인해보니</p>

<pre><code class="language-shell">$ df -h
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2019/11/ghost-signin-sqlite-full-error-2.png" alt=""></p>

<p>정말 디스크를 100% 모두 사용하고 있었다. <br>
그렇다면 어떤 디렉토리들이 용량을 많이 차지하고 있는지를 알고 싶어, 아래의 명령어로 용량이 가장 큰 20개의 디렉토리를 뽑아보았다.</p>

<pre><code class="language-shell">$ cd  /
$ sudo du -ckx | sort -n | tail -n 20
</code></pre>

<p><img src="https://d194zelh06zukz.cloudfront.net/2019/11/ghost-signin-sqlite-full-error-3.png" alt=""></p>

<p>눈에 띄는 점은, <code>logs</code> 디렉토리와 <code>tmp</code> 디렉토리가 대부분의 용량을 차지하고 있었다는 점이었다. 사실 블로그를 운영하며 여태껏 단 한번도 log 파일을 지운적도 없었고, <code>tmp</code> 디렉토리의 임시 파일들을 지운 적도 없었기 때문에 어찌 보면 당연한 결과이기도 했다. 결국 올게 오고야 말았구나하는 생각이 들었다. <br>
블로그를 운영하며 현재 log를 따로 보고 있지 않고, 또 임시 파일 역시 삭제해도 서비스에 영향없으므로 모두 삭제하기로 하였다. </p>

<p>해당 위치에 있는 많은 log 파일들과 임시 파일들을 모두 정리한 후, 다시 디스크 용량을 확인해보았다.</p>

<p><img src="https://d194zelh06zukz.cloudfront.net/2019/11/ghost-signin-sqlite-full-error-4.png" alt=""></p>

<p>디스크 전체의 절반정도의 여유공간이 생겨났음을 확인할 수 있다.</p>

<h3 id="">참고</h3>

<ul>
<li><strong><a id="jupiny-blog-link" href="https://wikibook.co.kr/article/when-the-disk-is-full/" target="_blank">디스크가 가득 찼을 때</a></strong></li>
</ul>]]></content:encoded></item></channel></rss>