<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hindsight.log</title>
        <link>https://velog.io/</link>
        <description>IT관련 된 것들은 가리지 않고 먹어요.</description>
        <lastBuildDate>Wed, 29 Dec 2021 12:50:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hindsight.log</title>
            <url>https://images.velog.io/images/hind_sight/profile/f98aee87-7520-402c-a6d9-9c2db3094d44/self.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hindsight.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hind_sight" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[도서리뷰] 모던 자바스크립트 핵심 가이드]]></title>
            <link>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Wed, 29 Dec 2021 12:50:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/hind_sight/post/644c402e-6afe-4636-8f5f-6da3d968eb35/image.png" alt=""></p>
<blockquote>
<p>본 글은 한빛미디어 &lt;나는 리뷰어다&gt; 활동을 위해서 책을 제공받아 작성된 서평입니다.</p>
</blockquote>
<h1 id="서론">서론</h1>
<p>그동안 자바스크립트, 타입스크립트를 활용하면서 책으로 공부해본적이 없다는 것을 깨닫고 그래도 책으로 문법이 어떻게 정의되어있는지 정도는 보자는 취지로 이 책을 골랐습니다.</p>
<h1 id="책-소개">책 소개</h1>
<p>간결한 설명 덕분인지, 대부분은 아는 내용이었지만 책으로써 읽기에 부담없이 술술 읽을 수 있었습니다.</p>
<p>책 전반적으로 자바스크립트의 아주 기초적인 문법내용을 주로 다뤄 초보자분들에게 적합하다고 생각됩니다. 사실 처음 배울때부터 너무 딥하게 들어가면 쉽게 질릴 수 있는 것이 사실이니까요 ㅎㅎ;</p>
<p>자바스크립트 외에도 JS 개발자라면 반드시 봐야할 타입스크립트 또한 간략하게 소개해주어 맛보기용으로 보기 좋습니다.</p>
<h1 id="결론">결론</h1>
<p>저도 앞으로 간이사전처럼두고 간간히 기억이 나지 않을 때 들여다볼 수 있을 만큼 가벼운 책인 것 같습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[도서리뷰] Fundamentals of Software Architecture, 소프트웨어 아키텍쳐 101]]></title>
            <link>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-Fundamentals-of-Software-Architecture-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-101</link>
            <guid>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-Fundamentals-of-Software-Architecture-%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90-101</guid>
            <pubDate>Wed, 24 Nov 2021 13:43:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://images.velog.io/images/hind_sight/post/dffce5d3-8fc8-4fcc-b9ef-8c29ed0f5083/image.png" alt="도서 이미지"></p>
<p>본 글은 한빛미디어 &lt;나는 리뷰어다&gt; 활동을 위해서 책을 제공받아 작성된 서평입니다.</p>
<blockquote>
<p>소프트웨어 아키텍처의 기본 소양부터 전체적인 흐름까지!</p>
</blockquote>
<h1 id="서론">서론</h1>
<p>소프트웨어 아키텍쳐, 평소 시스템 전체에 대한 아키텍쳐에 관심이 꽤나 있었다.</p>
<p>그래서 각종 컨퍼런스들에서 MSA, 이벤트 버스 등 여러 구조들을 접하려고 노력했다. 구조 자체는 이해하는데 크게 어렵지 않지만, 내부적인 요소들이 왜 필요한지, 어떤 케이스에서 효율적일지 등 완전한 이해를 하기가 어려웠습니다.</p>
<p>이런 부분들을 긁어주려고 하는 책인데, 제가 가려운 부분을 잘 파악하지 몰라 시원하지 못하다는 생각이 듭니다 ㅎ</p>
<h1 id="책-소개">책 소개</h1>
<p>책은 <code>소프트웨어 아키텍처</code>가 무엇인가? 부터 시작해 아키텍처의 특성, 스타일 그리고 그 외에 아키텍처가 갖춰야할 소양들(협상과 리더십, 커리어 패스 등)에 대해 다루고있습니다.</p>
<p>제 목표는 <code>아키텍처 관련 키워드들을 몸으로 익히는 것</code>이었습니다. 추후 이런 키워드가 나오면 언제든지 찾아볼수 있게끔.. 이 모든 것을 이해하기에는 역시나 약간의 무리가 있었습니다 ㅎ</p>
<p>하지만 책 자체에서 소개해주는 백엔드 내부의 스타일부터 전체적인 스스템의 스타일까지 어떻게 생겼는지 정도를 이해하는데에 설명이 굉장히 상세하게 되어있었습니다. 설명에서 아키텍처영역을 벗어난 부분도 주석으로 잘 풀어주는 부분이 이해하는데에 도움을 많이 받았습니다.</p>
<h1 id="결론">결론</h1>
<p>추천! 소프트웨어 아키텍처라는 직업에 대해서도, 아키텍처 그 자체에 대해서도 많은 도움을 받을 수 있습니다!</p>
<p><a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B1494466807">책 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[devNine] 어디까지 캐싱할 수 있을까? - DB(MySQL) 편]]></title>
            <link>https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-DBMySQL-%ED%8E%B8</link>
            <guid>https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-DBMySQL-%ED%8E%B8</guid>
            <pubDate>Wed, 03 Nov 2021 11:57:43 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! <a href="https://devnine.kr">devNine</a>입니다.</p>
<h2 id="devnine">devNine</h2>
<p>devNine에서 <em>메일링 서비스를 오픈한지 2주 가량이 지났습니다!</em>
극초기 몇 번의 오류를 겪고 현재는 안정적인 상태로 메일이 잘 전송되고 있습니다!</p>
<p>하지만, 여러 블로그 또는 유튜브에서 컨텐츠들을 가져오다보니 역시 여러 이슈들을 겪기 마련이었습니다.
이번 DB편이 마무리되면 이런 이슈들 또한 공유할 예정입니다!</p>
<h2 id="시작">시작</h2>
<p>사실, 이 글을 쓰게된 계기였던 <code>select count(*) from table</code>은 해결했습니다.</p>
<p>기존 Page를 활용해 페이지네이션을 진행했을 때는 전체 컨텐츠 즉, total을 제공하기 위해 <code>전체 카운트 쿼리</code>가 필요했는데, <a href="https://devnine.kr">devNine</a>처럼 무한스크롤로 컨텐츠를 제공할 경우 <strong>Slice</strong>를 사용해 총 개수를 알지 않아도 <code>first, last, empty 등</code>의 signal을 통해 마지막 페이지인지, 비어있는지를 체크해 기능을 구현할 수 있습니다.</p>
<p>그래도 혹시나 후에 게시판 기능이 추가되어 활발한 커뮤니티사이트처럼 <strong>아주 많은 컨텐츠</strong>를 가지면서도, <strong>CRUD가 활발</strong>하고, <strong>페이지네이션</strong>을 제공해야할 때를 대비해 알아봤습니다.</p>
<h2 id="query-caching">Query Caching</h2>
<blockquote>
<p>SELECT 명령문 텍스트를 클라이언트에 보내는 결과와 함께 저장한다.</p>
</blockquote>
<p><a href="https://dataonair.or.kr/db-tech-reference/d-guide/dbms-2/?mod=document&amp;uid=62466">https://dataonair.or.kr/db-tech-reference/d-guide/dbms-2/?mod=document&amp;uid=62466</a></p>
<p>거두절미 하고 쿼리 캐싱이 켜져있고, SELECT를 요청했다면 해당 결과는 SELECT 명령문과 함께 저장됩니다. 정확히 <strong><em>동일한(대소문자도 동일, 바이트 비교) SELECT 명령문</em></strong>이 요청된다면, 저장되었던 결과가 반환됩니다.</p>
<p>그런데, 해당 SELECT 명령문과 관련된 테이블에 단 하나라도 변경이 발생한다면 <strong><em>모든 캐시가 지워집니다.</em></strong></p>
<h3 id="사라져가는사라진-query-caching">사라져가는(사라진) Query Caching</h3>
<p><img src="https://images.velog.io/images/hind_sight/post/5ef7dddf-17ab-4abf-b9a3-2e11a565e9e4/image.png" alt=""></p>
<p><a href="https://dev.mysql.com/doc/refman/5.7/en/query-cache.html">https://dev.mysql.com/doc/refman/5.7/en/query-cache.html</a></p>
<p>쿼리 캐싱을 사용하지 않았던 가장 큰 이유가 이것이다. 5.7부터 deprecated되었고, 8.0에서는 아예 제거되었다니.. 사실상 쓰지 말라는 의미로 이해했습니다.</p>
<h3 id="왜">왜?</h3>
<p><a href="http://www.tocker.ca/2013/09/27/how-do-you-use-the-query-cache.html">http://www.tocker.ca/2013/09/27/how-do-you-use-the-query-cache.html</a>
<a href="https://dba.stackexchange.com/questions/23699/is-the-overhead-of-frequent-query-cache-invalidation-ever-worth-it/23727#23727">https://dba.stackexchange.com/questions/23699/is-the-overhead-of-frequent-query-cache-invalidation-ever-worth-it/23727#23727</a>
<a href="https://dba.stackexchange.com/questions/217577/why-did-mysql-remove-the-query-cache-feature-after-version-8-0">https://dba.stackexchange.com/questions/217577/why-did-mysql-remove-the-query-cache-feature-after-version-8-0</a>
<a href="https://mysqldba.tistory.com/356">https://mysqldba.tistory.com/356</a></p>
<p>이 외 관련 글들을 읽어보고 정리해보자면,</p>
<ol>
<li><p><em>MySQL 엔진인 InnoDB와 쿼리 캐시 무효화 사이의 오버헤드가 너무 크다.</em>
결정적으로 테이블에 <strong>단 한번의 DML에도 참조된 모든 영역에 무효화</strong>가 일어나는데, 이 때 <em>모든 캐시에서 참조</em>를 찾는다. 이 부분에서 Redis와 같은 Key-Value형태는 어플리케이션에서 Key로 데이터가 관리되면서 비교적 적은 영역의 참조가 일어날 것이라고 합니다.</p>
</li>
<li><p><em>여러 ORM에서 준수한 성능의 쿼리를 제공한다.</em>
물론 각각 차이는 있겠지만, 사용되고 있는 ORM들에서 쿼리에 대한 캐싱을 제공하고 캐싱에 대한 관리 또한 DB보단 부담이 덜하다는 의견입니다. 또한 별도의 캐싱을 위한 DB들(Redis 등) 또한 제공되고 있기 때문에 <strong><em>쿼리 캐시를 개선하기보다 MySQL 자체의 성능 향상에 집중하겠다는 MySQL 서버 팀의 글이 있었습니다.</em></strong></p>
</li>
</ol>
<h2 id="결론">결론</h2>
<p>결론적으로 DB에서의 쿼리 캐싱은 다른 DB는 더 알아봐야겠지만, 최소한 MySQL에서는 지양하고 있으며 어플리케이션 레이어 이상에서의 캐싱을 권하는 것으로 보여집니다.</p>
<p><a href="https://devnine.kr">devNine</a>에서도 현재 WAS(Spring Boot)와 Client(React의 Recoil)에서 캐싱을 사용하고 있습니다.</p>
<p>지난 <a href="https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-WASSpring-Boot-%ED%8E%B8">WAS편</a>을 보지 못하신분들은 <a href="https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-WASSpring-Boot-%ED%8E%B8">요기</a>로!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[도서리뷰] 제대로 배우는 수학적 최적화]]></title>
            <link>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%88%98%ED%95%99%EC%A0%81-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@hind_sight/%EB%8F%84%EC%84%9C%EB%A6%AC%EB%B7%B0-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%88%98%ED%95%99%EC%A0%81-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Mon, 25 Oct 2021 17:00:42 GMT</pubDate>
            <description><![CDATA[<p>본 글은 한빛미디어 &lt;나는 리뷰어다&gt; 활동을 위해서 책을 제공받아 작성된 서평입니다.</p>
<blockquote>
<p>알고리즘을 수식과 함께 생각해보고 싶은 분들께 추천합니다!</p>
</blockquote>
<p>코딩 테스트를 위한 알고리즘들을 공부하다보니, 결국엔 수학이 가장 중요하다는 것을 여러번 느낍니다.
기업 코딩테스트를 위해 반드시 깊은 수학적 지식이 필요한건 아니지만, 지식이 있다면 분명 도움이 될것이라는 생각에 <code>제대로 배우는 수학적 최적화</code>를 선택해 읽어봤습니다.</p>
<p><img src="https://images.velog.io/images/hind_sight/post/bd67b0a3-5224-4614-938f-3539e4d44730/KakaoTalk_Photo_2021-10-26-01-47-22.jpeg" alt=""></p>
<p>먼저, 전체적인 느낌은 <strong>충분히 실생활 또는 업무에서 필요할만한 문제들을 다룬다</strong>는 것입니다. 가끔 알고리즘 문제들을 보면, 마치 찢은 달력을 보고 특정 날짜를 추론하는 어릴적 수학문제 처럼, 썩 와닿지 않는 경우를 가져와 문제들을 가져오는 일들이 종종 있었습니다. 이 책도 여러 수학문제들을 다루다보면 그럴것 같았지만, 생각보다 와닿는 문제들로 알고리즘을 다뤄 좀더 _몰입감(?)_을 갖고 읽을 수 있었습니다.</p>
<p>주의하셔야할 부분은, <em><strong>알고리즘을 위한 코드는 단 한줄도 나오지 않습니다.</strong></em>
코딩이 아닌, 수식을 통한 알고리즘 최적화를 다루는 책입니다.
<img src="https://images.velog.io/images/hind_sight/post/a82768a4-d375-4a51-964b-6a257b0860a9/KakaoTalk_Photo_2021-10-26-01-55-54.jpeg" alt=""></p>
<p>보시는 것처럼, 특정 알고리즘의 수행 예시와 최적화하기 위한 추가 개념들을 상세하게 알려줍니다.</p>
<p>전 필요하다고 생각되는 알고리즘이 있을 때, 해당하는 챕터를 읽는 식으로 활용하고 있습니다.
아무래도 책이 수식과 풀이로 이루어져있다보니 소설처럼 읽기는 무리가 조금 있습니다.</p>
<p>책 링크를 마지막으로 글을 마칩니다!
<a href="https://www.hanbit.co.kr/store/books/look.php?p_code=B3558796278">https://www.hanbit.co.kr/store/books/look.php?p_code=B3558796278</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[devNine] 어디까지 캐싱할 수 있을까? - WAS(Spring Boot) 편]]></title>
            <link>https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-WASSpring-Boot-%ED%8E%B8</link>
            <guid>https://velog.io/@hind_sight/devNine-%EC%96%B4%EB%94%94%EA%B9%8C%EC%A7%80-%EC%BA%90%EC%8B%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C-WASSpring-Boot-%ED%8E%B8</guid>
            <pubDate>Thu, 21 Oct 2021 03:39:14 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! <strong><a href="https://devnine.kr">devNine</a></strong> 입니다.</p>
<h2 id="시작">시작</h2>
<p>최근 <strong><em>메일링 서비스</em></strong> 를 오픈했어요! 둘러보시고 매일 올라오는 IT 기업 블로그, 유튜브 컨텐츠를 받아보세요! 이 외에도 매주 스택오버플로우의 Q&amp;A들의 키워드를 분석해 분석 보고서를 제공하고있습니다.</p>
<h2 id="동기">동기</h2>
<p>블로그와 유튜브, StackOverFlow 컨텐츠가 <em><strong>수십만개</strong></em> 가량 쌓였습니다. 그리고 유튜브와 블로그 컨텐츠 제공시 적용되는 페이징을 처리하는 쿼리에서</p>
<pre><code>Select Count(*) from Table</code></pre><p>를 보고야 말았습니다. Spring Boot 내장 캐시를 적용해놓긴 했지만, 파라미터가 조금만 달라지면 <code>Select Count(*)</code>가 포함되어 쿼리가 수행되므로 이를 피하기 위해 MySQL에서의 <strong>Query Caching</strong>을 적용하려 했습니다.</p>
<p>하지만 MySQL 8.0부터는 deprecated되었다는 것과 생각보다 부정적인 시각이 많아 좀 더 근거를 갖고 실용적인 솔루션을 적용하고자 각 레이어에서 사용할 수 있는 캐싱들을 생각해보는 계기가 되었습니다.</p>
<p>서비스 특성상 자정 무렵 약 30분동안 크롤링, Insert되는 데이터 외에는 모두 Read라고 생각하셔도 무방할정도로 캐싱에 유리한 성격이라고 생각합니다. 따라서 <code>WAS -&gt; DB -&gt; Browser</code>순으로 할 수 있는 캐싱을 고려해보고 도입해볼 생각입니다.</p>
<p>먼저 오늘은 <strong><em>백엔드 어플리케이션(Spring Boot)</em></strong> 에서 수행할 수 있는 캐싱 방법들을을 살펴보고, 그 중에서 현재 서비스에 가장 적절한 방법을 선택할 예정입니다.
각 방법들에 대한 상세한 구현 방법들은 아주 자세하게 쓰여진 글들이 매우 많으므로 생략합니다.</p>
<p>많은 캐싱 관련 자료들 중 저는 토비님과 Redis 컨트리뷰터 강대명님의 <a href="https://www.youtube.com/watch?v=zkbvFOwJFgA&amp;t=1746s">토비의 봄 TV 스페셜 - 강대명 - 캐시의 모든 것</a> 재밌게 봤습니다!</p>
<p><del>캐시 자체가 무엇인지에 대한 글또한 이미 수많은 고수분들이 써주셨습니다. 검색해보세요!</del></p>
<h2 id="spring-boot에서-할-수-있는-캐싱">Spring Boot에서 할 수 있는 캐싱</h2>
<h3 id="1-redis">1. Redis</h3>
<p>Redis는 spring-data-redis로 활용할 수 있으며, 대표적인 InMemory DB입니다.</p>
<p>devNine이 프로젝트 단계일때도 활용했었고, 이미 구현이 완료되어있는 상태였음에도 Redis를 사용하지 않는 이유는 <strong>서비스 규모에 비해 과도한 리소스</strong>라고 생각했기 때문입니다.</p>
<p>레디스의 장점만 나열하자면 <strong>Key-Value 저장소, 다양한 데이터 타입, 마스터-슬레이브, 샤딩, 동기화 ** 등.. 많은 장점이 있는만큼 알아야할 그리고 관리해야할</strong> 리소스가 굉장히 컸습니다**.</p>
<p>리스크를 감당하기 위해 마스터-슬레이브 또는 샤딩을 구성했다면 그만큼의 인프라 비용을 감당해야하므로.. 아직까진 이렇게까지 절실할 만큼의 트래픽은 없으니 조금 더 간단한 캐싱을 구성하고 나서, <strong>좀 더 단단한 근거와 지식 그리고 서버를 Scale Out할 정도의 트래픽을 가졌을 때, 도입하기로 결정</strong>합니다.</p>
<h3 id="2-spring-cache-abstraction추상화">2. Spring Cache Abstraction(추상화)</h3>
<p>캐싱에서 사용되는 기능들이 추상화되어있어 <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache">Spring Cache Abstraction</a>이 지원하는 스토리지 내에서는 특정 스토리지에 종속되어 구현할 필요가 없습니다. 그렇지만 각 스토리지 라이브러리에서 구현된 구현체를 사용하는 경우도 있으니, 자세한 내용은 Docs를 확인하세요!</p>
<pre><code>org.springframework.boot:spring-boot-starter-cache</code></pre><p>사실 속도 자체는 <em><strong>로컬 캐시가 가장 빠릅니다</strong></em>. Redis도 결국엔 <em><strong>네트워크를 거쳐야하기 때문</strong></em> 니다. 하지만 트래픽 규모가 큰 즉, 동일한 기능을 하는 _<strong>다수의 서버가 구동</strong>_될 때는 로컬 캐시가 _<strong>동기화 문제</strong>_로 까다로울 수 있습니다. 그 말은 단일 서버인 현재 상태에서 최적이라는 뜻이죠!</p>
<h4 id="2-1-concurrenthashmap">2-1. ConcurrentHashMap</h4>
<p>자바 내 <strong>Multi-Thread 환경에서 사용할 수 있는 Map</strong>입니다. 이름부터 Concurrent해 안전한 느낌이 듭니다.
Spring-boot-starter-cache를 사용했을 때, 아무 설정을 하지 않으면(Default) ConcurrentHashMap을 통해 캐싱이됩니다.
사실 <strong>가장 쉽고 빠른 캐싱</strong>은 어플리케이션 내에서 <strong>변수로 저장하는 것</strong>이 가장 빠르니까요!</p>
<p>실제로 Multi-Thread 기반인 Spring-boot 내에서도 Map이 필요할 때 ConcurrentHashMap이 많이 사용됩니다.
<img src="https://images.velog.io/images/hind_sight/post/fe5b856c-1612-480f-9335-d1a7aacf1eeb/image.png" alt=""></p>
<p>하지만, Map의 구현체이다보니 캐시 관리에서 필요한 다양한 기능들이 부족합니다..
대표적으로 TTL, TTI 등 쓰이지 않는 데이터들에 대한 관리 기법들이 구현된 Ehcache를 찾게됩니다.
(ConcurrentHashMap의 캐시 정리 자체가 불가능한 것은 아닙니다. 직접 호출하거나 구현해야 할 뿐 모두 가능합니다! )</p>
<h4 id="2-2-ehcache-httpswwwehcacheorg">2-2. Ehcache (<a href="https://www.ehcache.org/">https://www.ehcache.org/</a>)</h4>
<p>Java 기반 캐시입니다. ConcurrentHashMap과 차이점은 <em><strong>off-heap</strong></em> 을 설정할 수 있다는 것입니다.
위의 ConcurrentHashMap를 사용하면 on-heap 즉, Java 힙에 올라갑니다. 그렇게되면 ConcurrentHashMap은 힙에 올라가긴 하지만, 스스로 정리되지 않는다는 단점이 있습니다. 즉, 사용자가 직접 사용되지 않는 부분을 직접 삭제시켜주지 않는다면 메모리가 낭비될 수 있습니다.</p>
<p>Ehcahe는 이러한 부분을 위해 off-heap을 지원해 인메모리 처럼 <em><strong>RAM에 데이터를 저장</strong></em> 할 수 있도록 지원하고, TTL, expiry를 통해 만료기한을 설정할 수 있습니다. 하지만, RAM은 비싸고 보통 적기 때문에.. 할당을 신중하게 해주어야 합니다.</p>
<p>또한 Terracota라는 분산 캐시 서버를 활용하면, 여러 서버간의 동기화, Replication을 할 수 있습니다.
(참고 : <a href="https://www.nextree.co.kr/p3151/">https://www.nextree.co.kr/p3151/</a>)</p>
<p>로컬 캐시로 사용가능하고, 이후 확장 시 코드 변경 없이 약간의 코드 추가만을 거쳐 분산 캐시도 구현해낼 수 있다는 것이 큰 장점이라고 생각했습니다. 또한 캐시마다 독립적인 세팅을 해줄 수 있고, 동일한 세팅의 경우 코드를 줄일 수 있기도 합니다.
(참고 : <a href="https://jaehun2841.github.io/2018/11/07/2018-10-03-spring-ehcache/#ehcache-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95">https://jaehun2841.github.io/2018/11/07/2018-10-03-spring-ehcache/#ehcache-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95</a>)</p>
<h4 id="3-jpa-2차캐시">3. JPA 2차캐시</h4>
<p>스레드가 종료되면 사라지는 1차캐시와 달리 2차캐시는 상시 유지된다. 대부분의 요청은 데이터베이스를 거치기 때문에 2차 캐시를 사용하면 JPA가 더 빨라지지 않을까? 라는 생각을 했었지만, 김영한님께서 다음과 같이 말씀하신다.
<img src="https://images.velog.io/images/hind_sight/post/ce9bd25b-1de7-4ef1-b167-f9f9929d4c8d/image.png" alt="김영한님 2차캐시"></p>
<blockquote>
<p>결론 : 위에서 언급된 스프링 캐시를 사용하자!</p>
</blockquote>
<h3 id="마무리">마무리</h3>
<p>저희 서비스는 결론적으로 Ehcache를 활용했습니다. 아직 off-HeapSize 설정 등에 대한 근거가 부족하긴 하지만, 여러 테스트를 통해 세부적인 설정을 적용해볼 예정입니다.</p>
<pre><code>&lt;config
        xmlns:xsi=&#39;http://www.w3.org/2001/XMLSchema-instance&#39;
        xmlns=&#39;http://www.ehcache.org/v3&#39;
        xmlns:jsr107=&quot;http://www.ehcache.org/v3/jsr107&quot;
        xsi:schemaLocation=&quot;http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd&quot;&gt;
    &lt;service&gt;
        &lt;jsr107:defaults enable-management=&quot;true&quot; enable-statistics=&quot;true&quot;/&gt;
    &lt;/service&gt;
    &lt;cache-template name=&quot;myDefaultTemplate&quot;&gt;
        &lt;expiry&gt;
            &lt;ttl unit=&quot;hours&quot;&gt;4&lt;/ttl&gt;
        &lt;/expiry&gt;
        &lt;resources&gt;
            &lt;heap unit=&quot;entries&quot;&gt;100&lt;/heap&gt;
            &lt;offheap unit=&quot;MB&quot;&gt;1&lt;/offheap&gt;
        &lt;/resources&gt;
    &lt;/cache-template&gt;
    &lt;!--  블로그 --&gt;
    &lt;cache alias=&quot;blogContent&quot; uses-template=&quot;myDefaultTemplate&quot;&gt;
    &lt;/cache&gt;
&lt;/config&gt;</code></pre><p>그리고 서비스 특성상 자정 경 데이터가 변경되고, 이것이 반드시 적용되어야 했는데 설정에 ttl, tti는 존재하지만 cron으로 특정 시간대에 캐시 삭제를 설정할수는 없었습니다. 따라서 저희는 @Secheduled를 활용해 정해진 시간에 모든 캐시를 클리어하도록 설정했습니다.</p>
<pre><code class="language-java">@Slf4j
@RequiredArgsConstructor
public class CachingConfig {

    private final CacheManager cacheManager;

    @Scheduled(cron = &quot;0 0 2 * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public void evictAllCachesAtIntervals() {
        for(String cacheNames : cacheManager.getCacheNames()){
            cacheManager.getCache(cacheNames).clear();
        }
        log.info(&quot;[+] 모든 캐시 제거&quot;);
    }
}</code></pre>
<p>모든 캐시를 제거하고 다시 쌓아나가는 것 자체가 많은 리소스가 필요로 되지만, 새로운 컨텐츠가 반드시 리프레시되어야 했으므로 현재는 필요한 작업이라고 생각하고 있습니다.</p>
<p>마지막으로 <code>Select Count(*)</code>은 아직 해결되지 않았습니다. 각각의 쿼리는 캐싱되지만, 캐싱 전 모든 페이지 그리고 기업 별, 유튜브 채널 별 쿼리가 달라 매번 <code>COUNT(*)</code>을 수행합니다. Count(*)를 분리해서 구현하고 캐싱할지 아니면 가장 처음 생각했던 해결책인 DB Query Caching를 적용할지는 좀 더 테스트해본 후 다음 편에서 소개드리겠습니다.
감사합니다.</p>
<h2 id="번외">번외</h2>
<p><img src="https://images.velog.io/images/hind_sight/post/b8187052-4c61-460d-ac4c-04188b6e3f3a/image.png" alt="">
<a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/build.gradle">https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-starters/spring-boot-starter-cache/build.gradle</a></p>
<p>spring-boot-starter-cache는 spring-context-support에 구현된 내용들이 그대로 따라온겁니다!
<img src="https://images.velog.io/images/hind_sight/post/b627bd89-1cb4-4d84-81b3-4e5558f6ae89/image.png" alt="">
<a href="https://github.com/spring-projects/spring-framework/tree/5.2.x/spring-context-support/src/main/java/org/springframework">https://github.com/spring-projects/spring-framework/tree/5.2.x/spring-context-support/src/main/java/org/springframework</a></p>
<p>spring-context-support에는 위와 같이 cache 뿐만 아니라 mail, scheduling, freemarker가 함께 포함되어있습니다.&#39;
그런데 사실, cache에 보면 ConcurrentHashMap은 찾아볼 수 없었습니다.
<img src="https://images.velog.io/images/hind_sight/post/51a20b44-764b-42d6-b77e-4d371d6f94ff/image.png" alt="">
위처럼 concurrent외의 것들은 있는데.. 그래서 Intellij에서 클래스를 추적해본 결과
<img src="https://images.velog.io/images/hind_sight/post/9aafad3c-0885-401f-9bed-8c3cf4dc5ff6/image.png" alt="">
concurrent는 Spring-context에 구현되어있었습니다! 위 spring-starter-cache의 build.gradle을 보시면, spring-boot-starter도 사용하고 있습니다. 여기에 포함된 Spring-context의 ConcurrentHashMap을 사용하는 것으로 보여집니다.</p>
<p><img src="https://images.velog.io/images/hind_sight/post/ce60e1ac-763e-4916-a699-7cbfb8add67c/image.png" alt="">
<a href="https://github.com/spring-projects/spring-framework/blob/main/spring-context-support/spring-context-support.gradle">https://github.com/spring-projects/spring-framework/blob/main/spring-context-support/spring-context-support.gradle</a>
그리고 위 spring-context-support의 gradle을 보시면, <strong>javax.cache:cache-api</strong>를 보실 수 있는데요!
이는 아까 <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache">Spring Cache Abstraction</a>를 보신분들이라면 슬쩍 보셨을 <a href="https://www.javadoc.io/doc/javax.cache/cache-api/1.0.0/javax/cache/CacheManager.html">JSR-107</a>입니다. Spring Cache Abstraction 내에서 추상화된 기반이 JSR-107이 되었던 것을 코드로도 확인하실 수  있습니다. ( 참고 : <a href="https://github.com/jsr107/jsr107spec">JSR107 Git</a> )</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[서비스 소개] dev-Nine]]></title>
            <link>https://velog.io/@hind_sight/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%86%8C%EA%B0%9C-dev-Nine</link>
            <guid>https://velog.io/@hind_sight/%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%86%8C%EA%B0%9C-dev-Nine</guid>
            <pubDate>Sun, 10 Oct 2021 10:30:09 GMT</pubDate>
            <description><![CDATA[<h1 id="go-to-dev-nine-👈">Go to <a href="https://devnine.kr">dev-Nine</a> 👈</h1>
<h2 id="dev-nine을-소개합니다">dev-Nine을 소개합니다!</h2>
<p>매일 오전 9시, 하루가 시작할 때 받아보는 개발 컨텐츠!</p>
<p>IT기업 기술 블로그와 IT관련 유튜브의 글들을 모아 매일 오전 9시에 메일로 쏴드립니다.</p>
<h3 id="메인-기능-1-블로그-유튜브-메일-구독-기능">메인 기능 #1. 블로그, 유튜브 메일 구독 기능</h3>
<p>수많은 국내외 IT기업의 <strong>기술 블로그</strong>와 <strong>IT관련 유튜브</strong>의 컨텐츠들을 모아 매일 오전 9시에 메일함으로 쏴드립니다.
(현재는 컨텐츠가 부족할 때가 종종 있어 각 블로그, 유튜브 별 가장 최근 게시글 3개씩 보내고있습니다. 추후 기업 및 유튜버 추가 시 전날 컨텐츠만 골라서 보내드릴 예정이에요.)</p>
<p>이 기능을 만든 계기를 소개하자면,</p>
<p>첫번째로, 매번 모든 <strong>기업 블로그들을 방문하며 새 글들을 찾기 번거로웠어요.</strong></p>
<p>두번째는, <strong>봐야 할 개발 유튜브가 뒤로 밀려나거나 눈에 띄지 않아 못보는 경우가 종종 생겼어요.</strong>
유튜브는 다 좋지만, <em>구독을 카테고리나, 디렉토리로 분류할 수 없어요</em>..
개발 유튜브도 좋지만, 영화 리뷰, 플레이리스트, 게임, vlog 등등.. 모두 챙겨봐야하잖아요?</p>
<p>이제는 <a href="https://devnine.kr">dev-Nine</a>을 통해, 전날 올라온 컨텐츠들은 다음날 오전 9시에 <strong>메일</strong>로 받아볼 수 있고, 전체 컨텐츠는 <strong><a href="https://devnine.kr/contents">웹사이트</a></strong>에서 볼 수 있어요!</p>
<p><img src="https://images.velog.io/images/hind_sight/post/7c5d2a3f-1bca-45b4-b920-0068e1285fa7/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/hind_sight/post/7e3c5374-444f-4d52-9429-63883de2fa2b/image.png" alt=""></p>
<h3 id="메인-기능-2-stackoverflow-분석-보고서">메인 기능 #2. StackOverFlow 분석 보고서</h3>
<p><img src="https://images.velog.io/images/hind_sight/post/99140c36-2696-4215-afb6-05215662b672/image.png" alt=""></p>
<p><strong><em>매주 월요일</em></strong>, 한 주간의 <strong>StackOverFlow의 질문</strong> 글들을 파싱하고, <strong>키워드 기반으로 카테고리화해 분석한 보고서</strong>를 제공합니다!</p>
<p>주간 가장 많은 Vote를 받은 글, 각 카테고리별 가장 많이 언급된 키워드 등을 통해 지난 주엔 <strong>어떤 개발 키워드가 핫🔥했는지 확인</strong>할 수 있습니다!</p>
<h3 id="개발-진행중-💪">개발 진행중 💪</h3>
<ol>
<li>유저분들의 피드백을 받을 수 있는 게시판 또는 채팅창을 고민중에 있습니다!</li>
<li>유저분들이 기술 블로그 또는 IT 유튜브를 제안할 수 있는 창구를 마련중에 있습니다!</li>
</ol>
<h3 id="팀원">팀원</h3>
<p>Swanious <a href="https://github.com/swanious">github</a>
Young <a href="https://github.com/juneyoung-jo">github</a>
HanJaehee <a href="https://github.com/HanJaehee">github</a></p>
<p><em>많관부!</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java에서 RSS 포맷의 시간 다루기]]></title>
            <link>https://velog.io/@hind_sight/Java%EC%97%90%EC%84%9C-RSS-%ED%8F%AC%EB%A7%B7%EC%9D%98-%EC%8B%9C%EA%B0%84-%EB%8B%A4%EB%A3%A8%EA%B8%B0</link>
            <guid>https://velog.io/@hind_sight/Java%EC%97%90%EC%84%9C-RSS-%ED%8F%AC%EB%A7%B7%EC%9D%98-%EC%8B%9C%EA%B0%84-%EB%8B%A4%EB%A3%A8%EA%B8%B0</guid>
            <pubDate>Sun, 03 Oct 2021 19:06:15 GMT</pubDate>
            <description><![CDATA[<h1 id="결론">결론</h1>
<p>약간의 서사가 있어 결론부터 말씀드리자면,</p>
<pre><code class="language-java">// Fri, 01 Oct 2021 07:24:36 +0000
SimpleDateFormat RFC_822 = new SimpleDateFormat(&quot;EEE, dd MMM yyyy HH:mm:ss z&quot;, Locale.ENGLISH);
// 2021-09-10T00:23:16Z
SimpleDateFormat ISO_INSTANT = new SimpleDateFormat(&quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&#39;Z&#39;&quot;);
SimpleDateFormat target = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);

if(rawDate.charAt(0) &gt;= &#39;A&#39; &amp;&amp; rawDate.charAt(0) &lt;= &#39;Z&#39;){
      return target.format(RFC_822.parse(rawDate));
}else {
      return target.format(ISO_INSTANT.parse(rawDate));
}</code></pre>
<p>위와 같은 형식으로 해결했습니다!</p>
<h1 id="발단">발단</h1>
<p><a href="https://post-it.site">POST-IT</a>에서는 블로그와 유튜브를 파싱해 데일리 컨텐츠를 제공하고있습니다.</p>
<p>이 블로그와 유튜브가 작성된 시간을 다루는데 기존에는 </p>
<pre><code class="language-java">String rawTime = &quot;Fri, 01 Oct 2021 07:24:36 +0000&quot;
int year = Integer.parseInt(rawTime.substring[12,16]);
int month = monthMap.get(rawTime.substring[8,11]);
int day = Integer.parseInt(rawTime.substring[5,7]);</code></pre>
<p>와 같은 형태로 문자열을 substring으로 다뤘습니다.
그런데 이제 메일 구독 서비스를 준비하면서 yyyy-MM-mm HH:mm:ss 형태로 시간을 제공하기로 결정하면서, 시간을 좀 더 제대로 다뤄보며 겪은 이슈들입니다.</p>
<h1 id="시작">시작</h1>
<p>시간을 다루기 전 자주 언급될 클래스는 <em>DateTimeFormatter*과 *Instant</em> 입니다.</p>
<p>Instant는 Java 8부터 지원되는 TIME API입니다.
기본적으로 시간을 생성하면 밑에서 언급될 <strong><em>ISO_INSTANT</em></strong> 형태로 생성되며, 타임존을 변경하기 용이하며, <em>DateTimeFormatter</em>와 연동해시간을 원하는 형태로 변경하기 쉽습니다.</p>
<p>이제 본격적으로 시간을 다루기 전, RSS에서 자주 사용되는 시간의 포맷을 알아야합니다.
먼저, <strong><em>ISO_INSTANT</em></strong> 으로, UTC기준 시간형태입니다.</p>
<pre><code class="language-java">2011-12-03T10:15:30Z</code></pre>
<p>이 시간은 문자열 split을 하기도 좋게 생기기도 했지만, 자바에서는 Instant 클래스를 통해 가장 활용하기 무난한 형태입니다.</p>
<pre><code class="language-java">Instant.parse(&quot;2011-12-03T10:15:30Z&quot;).atZone(ZondId.of(&quot;Asia/Seoul&quot;));
// 2011-12-03 19:15:30</code></pre>
<p>그리고 RSS 표준에서 사용되는 <strong><em>RFC_1123_DATE_TIME</em></strong>입니다.</p>
<pre><code class="language-java">Fri, 01 Oct 2021 07:24:36 +0000
Fri, 01 Oct 2021 07:24:36 GMT</code></pre>
<p><img src="https://images.velog.io/images/hind_sight/post/38d9da47-0454-4940-8653-85d13a205351/image.png" alt="">
<a href="https://validator.w3.org/feed/docs/rss2.html">https://validator.w3.org/feed/docs/rss2.html</a>
위에서 빨간 표시된 PDT는 이후에 언급됨으로 기억해두세요!</p>
<p><img src="https://images.velog.io/images/hind_sight/post/ce4d3593-d238-4e10-8618-2235449412b7/image.png" alt="">
<a href="https://datatracker.ietf.org/doc/html/rfc822#page-26">https://datatracker.ietf.org/doc/html/rfc822#page-26</a>
위에서 빨간 표시된 <strong><em>PDT</em></strong>는 이후에 언급됨으로 기억해두세요!</p>
<p>RSS 2.0 표준과 RFC 822에서의 DATE와 TIME에 대한 부분입니다.
RFC 1123에서도 DATE, TIME 부분은 RFC 822를 따라가기 때문에 정의는 RFC 1123으로 되어있습니다.</p>
<p>위의 타입은 다음과 같이 처리할 수 있습니다.</p>
<pre><code class="language-java">OffsetDateTime
              .parse(rawDate, DateTimeFormatter.RFC_1123_DATE_TIME)
              .toInstant();</code></pre>
<p><em>OffsetDateTime</em>이 <strong>+HHMM</strong>과 <strong>GMT, EDT</strong>등을 모두 처리해줍니다. 다음은 실제 클래스 구현부입니다.
<img src="https://images.velog.io/images/hind_sight/post/cbfad081-4ebf-4165-9f73-bc9958882215/image.png" alt=""></p>
<p>미리 정의된 형태 외의 커스텀이 필요하다면, <em>DateTimeFormatterbuilder</em>로 커스텀할 수 있습니다.
<em>DateTimeFormatter</em>로 받을 수 있는 시간 형태는 다음과 같습니다.
<img src="https://images.velog.io/images/hind_sight/post/5227326d-0172-4ad3-a5c5-bb0c62c6b63d/image.png" alt=""></p>
<p>이제 RSS에서 사용되는 시간 형태는 모두 관리할 수 있을 줄 알았는데..</p>
<h1 id="문제">문제</h1>
<p><img src="https://images.velog.io/images/hind_sight/post/a6072caf-d8ed-48e0-8dbe-10cb7ca99522/image.png" alt="">
Microsoft에서는 PDT를 사용하는데, JAVA의 <strong><em>RFC_1123_DATE_TIME</em></strong>에서는 PDT를 지원하지 않습니다..</p>
<p>그렇다면 PDT는 무엇인가?</p>
<blockquote>
<p>Pacific Daylight Time, 태평양 일광 절약 시간, UTC -0700</p>
</blockquote>
<p>위에서 사용했던 <em>DateTimeFormatter</em>에서는
<img src="https://images.velog.io/images/hind_sight/post/d7dd958f-54af-4538-b421-5be9140cfc57/image.png" alt="">
위에서 빨간 표시됐던 PDT만 없습니다.</p>
<p>PDT를 왜 지원하지 않는지는 찾지 못했고.. 대체법을 찾을 수 있었습니다.
<img src="https://images.velog.io/images/hind_sight/post/edff3ed3-be09-43aa-b70b-3b714565ff97/image.png" alt="">
<a href="https://stackoverflow.com/questions/18493528/java-date-format-gmt-0700-pdt">https://stackoverflow.com/questions/18493528/java-date-format-gmt-0700-pdt</a></p>
<p>각각이 왜 지원하고, 하지 않는지까지는 잘 모르겠지만..
<em>SimpleDateFormat</em>으로 날짜형식을 커스텀해 정의하는 것으로 해결했습니다.</p>
<p>사실 기존의 <strong><em>RFC_1123</em></strong>과 <strong><em>ISO_INSTANT</em></strong>를 그대로 따라가는 것이라 크게 어렵지 않아 <a href="#%EA%B2%B0%EB%A1%A0">결론</a>과 같이 해결했습니다.</p>
<pre><code class="language-java">// Fri, 01 Oct 2021 07:24:36 +0000
SimpleDateFormat RFC_822 = new SimpleDateFormat(&quot;EEE, dd MMM yyyy HH:mm:ss z&quot;, Locale.ENGLISH);
// 2021-09-10T00:23:16Z
SimpleDateFormat ISO_INSTANT = new SimpleDateFormat(&quot;yyyy-MM-dd&#39;T&#39;HH:mm:ss&#39;Z&#39;&quot;);
SimpleDateFormat target = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);

if(rawDate.charAt(0) &gt;= &#39;A&#39; &amp;&amp; rawDate.charAt(0) &lt;= &#39;Z&#39;){
      return target.format(RFC_822.parse(rawDate));
}else {
      return target.format(ISO_INSTANT.parse(rawDate));
}</code></pre>
<h1 id="마무리">마무리</h1>
<p>PDT와 같은 특이 케이스가 아니라면 <em>DateTimeFormatter</em>로 간단히 해결할 수 있을 것이고, 특별한 형태로 받아야 한다면 <em>SimpleDateFormat</em>으로 해결할 수 있습니다!
현재까지는 RSS에 표준이 있긴 해도 사이트별로 시간 형태가 제각각이라.. 틈틈히 케이스들을 추가하고 있습니다.
이런 형태로 본다면, SimpleDateFormat를 더 자주 쓸 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Elastic Beanstalk에 Spring Boot를 Github Actions로 배포하고, SSL 적용 및 CORS 설정]]></title>
            <link>https://velog.io/@hind_sight/Elastic-Beanstalk%EC%97%90-Spring-Boot%EB%A5%BC-Github-Actions%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B3%A0-SSL-%EC%84%A4%EC%A0%95-%EC%A4%91-CORS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@hind_sight/Elastic-Beanstalk%EC%97%90-Spring-Boot%EB%A5%BC-Github-Actions%EB%A1%9C-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B3%A0-SSL-%EC%84%A4%EC%A0%95-%EC%A4%91-CORS-%EC%84%A4%EC%A0%95-%EB%B0%8F-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Thu, 30 Sep 2021 17:35:32 GMT</pubDate>
            <description><![CDATA[<h1 id="발단">발단</h1>
<p>현재 운영중인 <a href="https://post-it.site">POST-IT(포스트 아이티)</a>의 서버를 AWS Elastic Beanstalk로 옮겼습니다. CI/CD는 기존의 Github Actions를 그대로 활용하고 싶었고, 단일 인스턴스로 활용하다가도 필요시 적절하게 로드밸런싱도 가능했으면 했습니다. 그렇게 후보를 LightSail과 Beanstalk 둘로 좁혔습니다.</p>
<h2 id="lightsail-vs-beanstalk">LightSail vs Beanstalk</h2>
<p>각각을 한줄로 표현하자면 다음과 같습니다.</p>
<p><strong><em>LightSail</em></strong> : 좀 더 사용하기 쉽게 세팅해주는 EC2
<strong><em>Beanstalk</em></strong> : 서버관리 없이, 코드만 업로드하면 어플리케이션이 실행되는 PaaS</p>
<p>이 외에도 서로 많은 장단점이 있겠지만, 어쨌든 우리의 상황에 더 적합한 것이 <strong><em>Beanstalk</em></strong>인 이유는</p>
<ol>
<li>좀 더 어플리케이션에 집중하기 위해 <strong>서버 관리를 최소화</strong> 하고 싶다.</li>
<li>필요 시 <strong>Auto Scailing</strong>을 해야한다. (LightSail은 불가)</li>
</ol>
<p>물론 단점도 존재합니다.</p>
<ol>
<li>서버 구성에 커스텀을 많이 적용하려는 경우, 오히려 더 어려워질 수 있다.(.ebextions 구성 변경해야함)</li>
<li>지원하는 플랫폼의 한계</li>
</ol>
<p>하지만, 소규모 프로젝트 단계에서는 위의 단점들을 겪을 일이 <strong>아직은</strong> 없을 것으로 예상되어 선택했습니다.</p>
<h1 id="시작">시작</h1>
<p>기본적으로 Github Actions, Spring Boot와 Beanstalk세팅은 <a href="https://jojoldu.tistory.com/549">https://jojoldu.tistory.com/549</a>를 따라 진행했습니다.</p>
<h2 id="route53에-loadbalancer-연결">Route53에 LoadBalancer 연결</h2>
<p>Elastic Beanstalk(이하 &quot;eb&quot;)에는 <strong>EC2와 LoadBalancer</strong>가 포함되어있습니다. eb를 배포하고 나면 환경 URL이 생성되는데 이를 그대로 사용해도 되지만 프론트엔드에 SSL이 적용되어 있기도하고, AWS내의 인스턴스에 ACM을 적용하는 것이 매우 쉽기 때문에 적용합니다.</p>
<p>과정을 진행하기 전 도메인이 <strong>Route53에 등록</strong>되어있어야 합니다.
등록 하는 과정은 현재 UI로 잘 소개되어있는 <a href="https://sovovy.tistory.com/37">https://sovovy.tistory.com/37</a> 를 추천드립니다. 도메인 구매는 AWS에서 하셔도 되지만, 쪼오끔 비쌉니다..</p>
<p>위의 글로 도메인을 등록하셨다면, 등록한 도메인의 원하는 <strong>하위 주소를 선택해 eb에 연결</strong>할 수 있습니다.
호스팅 영역에서 도메인을 클릭해 레코드 생성에 접근하면,
<img src="https://images.velog.io/images/hind_sight/post/875756d4-4121-4d0b-bb1f-e90d79ba9bff/image.png" alt="">
위와 같은 여러 라우팅 정책을 만날 수 있지만, 현재 프로젝트는 특별한 라우팅 정책이 필요할만한 사이즈가 아니기 때문에 단순 라우팅을 선택했습니다.</p>
<p>선택 후 단순 레코드 정의에서 하위 도메인을 입력하시고, <strong>엔드포인트 선택에서 Elastic Beanstalk과 리전을 설정</strong>합니다.
<img src="https://images.velog.io/images/hind_sight/post/5514628c-7d30-45fe-b59c-57a80c0dd56b/image.png" alt="">
이후 환경을 클릭하시면, 아까 만든 환경 URL을 선택해 레코드를 생성하면 끝입니다. 이제 eb는 우리의 도메인에서 접근할 수 있게 되었습니다.</p>
<h2 id="loadbalancer에-acm연결-ssl-적용">LoadBalancer에 ACM연결, SSL 적용</h2>
<p>도메인이 등록되어있다면, 해당 도메인의 인증서를 발급받을 수 있습니다.
인증서를 생성하는 과정은 <a href="https://jojoldu.tistory.com/434">https://jojoldu.tistory.com/434</a>를.. 잘 써져있는 글이 너무 많습니다..</p>
<p>인증서를 발급 받았다면, <strong>Beanstalk -&gt; 환경 -&gt; 구성 -&gt; 로드 밸런서</strong> 편집으로 넘어옵니다. 가장 상단의 리스너에서 리스터 추가를 선택합니다.
포트 443과 프로토콜 HTTPS를 선택하면 SSL인증서를 선택할 수 있습니다.
위에서 등록한 인증서를 선택하고, <strong>SSL 정책</strong>을 선택해야하는데
<img src="https://images.velog.io/images/hind_sight/post/59aeea38-5fa2-4388-96ec-c4e98038c024/image.png" alt="">
<a href="https://docs.aws.amazon.com/ko_kr/elasticloadbalancing/latest/application/create-https-listener.html">AWS ALB HTTPS용 Listener 생성 DOCS</a>에서 추천하는 2016-08 정책을 선택했습니다.</p>
<p>이제 리스너를 추가하면 우리의 LB는 https로 안전하게 요청을 받을 수 있습니다. 이제 80포트를 특별하게 사용해야할 것이 아니라면, 비활성해주는 것이 좋습니다.</p>
<p>여기서, 팀원에게 받았던 질문을 하나 소개해드고자 합니다.</p>
<h3 id="q-nginx에서-443포트를-열어놓지-않았는데-어떻게-ssl이-적용되나요">Q. Nginx에서 443포트를 열어놓지 않았는데 어떻게 SSL이 적용되나요?</h3>
<p>먼저, 요청이 어플리케이션까지 닿는 간단한 경로를 보면 어떻게 적용되는지 알 수 있습니다.</p>
<p><img src="https://images.velog.io/images/hind_sight/post/35582a19-3b73-492d-8d67-cc24af58a964/image.png" alt="">
추가로 Nginx에 직접적으로 80포트로는 보안 그룹으로 제한되어 접근할 수 없습니다. 따라서 ALB로 요청이 Https로 접근하면 뒷단의 트래픽은 외부에서 직접 접근할 수 없기 때문에 SSL로만 요청을 받는 것을 보장받을 수 있습니다.</p>
<h2 id="cors-설정">CORS 설정</h2>
<pre><code>add_header  &#39;Access-Control-Allow-Origin&#39;  &#39;frontend address&#39;;
add_header  &#39;Access-Control-Allow-Credentials&#39;  &#39;true&#39;;</code></pre><p>위는 Nginx에 추가한 CORS 설정입니다.
사실 CORS 정책은 브라우저에서 동작하는 것이기 때문에 Nginx 또는 Spring Boot 아무곳에서나 Response를 보낼 때 Access-Control-Allow-Origin을 붙이면 됩니다. 프론트엔드에서 요청한 백엔드의 도메인 내의 어떤 요소에서든 CORS정책에 대한 헤더를 추가해주면 됩니다.
Nginx에 추가하는 이유 중 하나는 <strong>추가로 연결될 백엔드단의 서버</strong>를 위함입니다. 모든 서버에 CORS 설정을 각각 하는 것보다 Nginx 하나에서 관리하기 위함입니다.</p>
<h1 id="끝">끝</h1>
<p>최근 블로그를 다시 시작하며 남들과 똑같은 글을 쓰지 말자는 생각을 하게 되었습니다. 정의를 그대로 카피하는 것이 아닌 그것을 응용하고 그에 대한 경험을 전달하는 것을 첫번째 목표로 정하고 있습니다. 읽어주셔서 감사합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA에서의 Lock과 Isolation 테스트 및 회고]]></title>
            <link>https://velog.io/@hind_sight/JPA%EC%97%90%EC%84%9C%EC%9D%98-Lock%EA%B3%BC-Isolation-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%8F-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hind_sight/JPA%EC%97%90%EC%84%9C%EC%9D%98-Lock%EA%B3%BC-Isolation-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%8F-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 29 Sep 2021 01:00:01 GMT</pubDate>
            <description><![CDATA[<h1 id="준비">준비</h1>
<h2 id="발단">발단</h2>
<p>지인에게 데이터베이스에서의 DeadLock을 경험해본적이 있냐는 질문을 받았다. 없었다. 지금까지 만들어왔던 모든 기능들에서 데드락에 대한 처리를 한 적이 없었다..<br>그래서 가상의 숫자 카운팅 어플리케이션을 통해 데드락이 일어날 상황을 시뮬레이션해보고 테스트해봤다.</p>
<h2 id="환경">환경</h2>
<p>M1 MAC, JAVA 11, MySQL</p>
<h1 id="시작">시작</h1>
<h2 id="요구사항">요구사항</h2>
<p>들어온 요청만큼 숫자가 반드시 1이 증가해야하는 어플리케이션이 있다. 요청은 많은 사람들이 동시에 지속적으로 요청을 보낸다고 가정한다.</p>
<h2 id="이론">이론</h2>
<h4 id="1-무방비">1. 무방비</h4>
<p>아무 처리를 하지 않았을 땐, 동시 요청이 들어올 경우 MySQL의 기본 Isolation인 <em>REPEATABLE READ</em>로 적용된다.<br>따라서, 트랜잭션이 시작될때 읽은 값이 끝날때 까지 유지되므로, 동시에 N개의 요청이 들어왔을 때의 숫자가 0이라면, 모두 1로 업데이트 될 것이다.</p>
<h4 id="2-lock---pessimistic-write-비관적-락-x-lock">2. Lock - PESSIMISTIC WRITE (비관적 락, X Lock)</h4>
<p>요구사항에 맞추기 위해 데드락이 자주 발생할 것이라고 생각하고, <strong>Select 시 배타적 락(X Lock)을 걸어 블락</strong>을 걸고, <strong>순서를 보장</strong>한다.<br>=&gt; 공유 락(S Lock)이 안되는 이유 : 한 트랜잭션이 공유 락을 걸었을 경우, 다른 트랜잭션 또한 공유 락을 걸어 둘 다 읽을 수는 있다.<br>하지만, 요구사항처럼 데이터를 수정해야하는 경우, 공유 락이 걸린 데이터는 수정할 수 없기 때문에 배타적 락을 걸어야 하는데 공유 락과 배타적 락은 함께 사용할 수 없어 데드락이 발생한다.</p>
<h4 id="3-isolation---serializable">3. Isolation - SERIALIZABLE</h4>
<p>Serializable로 데드락을 유도하고, 발생 시 리트라이 한다.</p>
<h2 id="어플리케이션-구성">어플리케이션 구성</h2>
<ul>
<li>NumberEntity<br>엔티티는 구성되어 있지만, 하나의 데이터를 생성해 숫자를 증가시킨다. (ID가 1로 고정)</li>
</ul>
<pre><code class="language-java">@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class NumberEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private Long count;

    @Builder
    private NumberEntity(int id, Long count) {
        this.id = id;
        this.count = count;
    }

    public static NumberEntity of(int id, Long count){
        return NumberEntity.builder()
                .id(id)
                .count(count)
                .build();
    }

    public Long increment(){
        return ++this.count;
    }

    public Long decrement(){
        return --this.count;
    }
}</code></pre>
<ul>
<li>NumberRepository<br>각각 일반, Lock의 PESSIMISTIC_WRITE, Isolation의 Serializable을 적용한 같은 동작을 하는 find함수들
(Isolation은 서비스 단에서 적용 예정)</li>
</ul>
<pre><code class="language-java">@Repository
public interface NumberRepository extends JpaRepository&lt;NumberEntity, Integer&gt; {

    Optional&lt;NumberEntity&gt; findById(int id);

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select n from NumberEntity n where n.id = :id&quot;)
    Optional&lt;NumberEntity&gt; findByNumberIdLock(@Param(&quot;id&quot;) int id);

    @Query(&quot;select n from NumberEntity n where n.id = :id&quot;)
    Optional&lt;NumberEntity&gt; findByNumberIdIsolation(@Param(&quot;id&quot;) int id);
}</code></pre>
<ul>
<li>NumberService<br>각각 이론 1,2,3에 해당하는 경우를 구현</li>
</ul>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
@Slf4j
public class NumberService {

    private final NumberRepository numberRepository;

    private final int numberId = 1;

    @Transactional
    public void incrementNumberNormal(){
        NumberEntity numberEntity = numberRepository.findById(numberId).orElseThrow(NoSuchElementException::new);
        System.out.println(Thread.currentThread().getName() + &quot; : &quot; + numberEntity.increment() + &quot; &quot; + numberEntity);
    }

    @Transactional
    public void incrementNumberLock() {
        NumberEntity numberEntity = numberRepository.findByNumberIdLock(numberId).orElseThrow(NoSuchElementException::new);
        System.out.println(Thread.currentThread().getName() + &quot; : &quot; + numberEntity.increment() + &quot; &quot; + numberEntity);
    }

    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void incrementNumberSerializable(){
        NumberEntity numberEntity = numberRepository.findByNumberIdIsolation(numberId).orElseThrow(NoSuchElementException::new);
        System.out.println(Thread.currentThread().getName() + &quot; : &quot; + numberEntity.increment());
    }
}</code></pre>
<h2 id="이론-별-테스트">이론 별 테스트</h2>
<p>테스트는 @SpringBootTest를 활용해 어플리케이션 구동환경과 맞춘상태로 진행한다. 또한 동시 요청 테스트를 위해 쓰레드 100개를 활용해 각각 진행하며, 모든 테스트의 숫자는 0에서 시작한다.</p>
<pre><code class="language-java">@SpringBootTest
public class NumberServiceTest {

    private static final int COUNT = 100;
    private static final ExecutorService service = Executors.newFixedThreadPool(COUNT);

    @Autowired
    private NumberService numberService;
}</code></pre>
<h4 id="1-무방비-1">1. 무방비</h4>
<pre><code class="language-java">@Test
@DisplayName(&quot;요청 수 만큼 숫자 증가 (Normal)&quot;)
void incrementNumber_normal() throws InterruptedException {
    // given
    CountDownLatch latch = new CountDownLatch(COUNT);
    Long before = numberService.getNumber();

    // when
    for (int i = 0; i &lt; COUNT; ++i) {
        service.execute(() -&gt; {
            numberService.incrementNumberNormal();
            latch.countDown();
        });
    }
    // then
    latch.await();
    assertEquals(before + COUNT, numberService.getNumber());
}</code></pre>
<p>결과</p>
<p><img src="https://images.velog.io/images/hind_sight/post/a3c41540-046d-4b14-ae50-3a3c02e5901a/theory1_test_result.png" alt="theory1_test_result"></p>
<p>이론과 같이 요청은 100번 들어왔지만, 실제 업데이트된 숫자는 14임을 볼 수 있다. 모든 요청이 숫자가 증가됨을 보장할 수 없다는 것을 알 수 있다.</p>
<h4 id="2-lock---pessimistic-write-비관적-락-x-lock-1">2. Lock - PESSIMISTIC WRITE (비관적 락, X Lock)</h4>
<pre><code class="language-java">@Test
@DisplayName(&quot;요청 수 만큼 숫자 증가 (Lock - PESSIMISTIC_WRITE)&quot;)
void incrementNumber_concurrency() throws InterruptedException {
    // given
    CountDownLatch latch = new CountDownLatch(COUNT);
    Long before = numberService.getNumber();
    // when
    for (int i = 0; i &lt; COUNT; ++i) {
        service.execute(() -&gt; {
            numberService.incrementNumberLock();
            latch.countDown();
        });
    }
    // then
    latch.await();
    assertEquals(before + COUNT, numberService.getNumber());
}</code></pre>
<p><img src="https://images.velog.io/images/hind_sight/post/c71175c0-9f46-4f68-82f3-ba435a2dfd2c/theory2_test_result.png" alt="theory2_test_result">
이론과 같이 모든 트랜잭션이 블로킹에 의해 순서대로 처리되어 정상적으로 100 증가되었다.</p>
<h4 id="3-isolation---serializable-1">3. Isolation - SERIALIZABLE</h4>
<pre><code class="language-java">@Test
@DisplayName(&quot;요청 수만 큼 증가 (Isolation - SERIALIZABLE)&quot;)
void incrementNumber_Isolation() throws InterruptedException {
    // given
    CountDownLatch latch = new CountDownLatch(COUNT);
    Long before = numberService.getNumber();
    // when
    for (int i = 0; i &lt; COUNT; ++i) {
        service.execute(() -&gt; {
            numberService.incrementNumberSerializable();
            latch.countDown();
        });
    }
    // then
    latch.await();
    assertEquals(before + COUNT, numberService.getNumber());
}</code></pre>
<p><img src="https://images.velog.io/images/hind_sight/post/92873c76-610f-450c-9a90-69d53355b40c/theory3_test_result.png" alt="theory3_test_result">
<strong>데드락</strong>이 발생했다. 그런데, 데드락 이후에 리트라이를 해주더라도 요청이 지속되는 어플리케이션이라면 리트라이에도 성공한다는 보장이 없다. 상황에 따라 다르겠지만, 이 어플리케이션의 요구사항에서는 <em>데드락자체를 발생시키는 것이 적합하지 않다</em>는 생각이 든다.</p>
<h1 id="끝">끝</h1>
<p>이론 중 최종선택을 한다면, <strong>Lock을 선택</strong>할 것이다.<br>하지만, 자료들을 찾아보면서 Lock과 Isolation 모두 신중하게 적용해야한다는 내용들을 공통적으로 볼 수 있었다. 확실히 Lock에 의한 순서 보장이 장점일 수도 있지만, 요청이 많아진다면 언제까지 기다릴수도 없다고 생각된다. 특히나 서버 여러 개가 물려있어 요청에 요청을 무는 구조에서는 더더욱 그럴 것이다.
앞으로의 과제는 이 동시성과 일관성의 균형을 상황에 맞게 잘 결정하는 것이 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker & Jenkins] 도커와 젠킨스를 활용한 Spring Boot CI/CD🥸]]></title>
            <link>https://velog.io/@hind_sight/Docker-Jenkins-%EB%8F%84%EC%BB%A4%EC%99%80-%EC%A0%A0%ED%82%A8%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Spring-Boot-CICD</link>
            <guid>https://velog.io/@hind_sight/Docker-Jenkins-%EB%8F%84%EC%BB%A4%EC%99%80-%EC%A0%A0%ED%82%A8%EC%8A%A4%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-Spring-Boot-CICD</guid>
            <pubDate>Mon, 08 Mar 2021 16:36:32 GMT</pubDate>
            <description><![CDATA[<h4>요약 : 도커에 Jenkins 띄운 후 Build Now를 클릭해 Github에서 소스코드를 가져와 Spring Boot 프로젝트 빌드, Docker이미지로 생성해 배포</h4>

<h2 id="작성-동기">작성 동기</h2>
<p>이전 프로젝트에서는 빌드,배포를 쉘 스크립트로 진행했습니다.</p>
<p>현재 프로젝트에서는 브랜치 전략으로 Git-Flow를 선택하며 테스트를 위한 develop과 release에서는 자동 배포를, master에서는 수동 배포를 하기로 했습니다.</p>
<p>사용자에게 직접 영향이 가는 master가 WebHook으로 인해 자동으로 배포되며 혹시 모를 서비스 중단되는 것을 최대한 피하기 위해, 수동 배포를 선택하면서 적용한 것들을 공유하기 위해 테스트 글을 작성했습니다.</p>
<p><em><strong>진행하면서 발생하는 모든 오류와 피드백 감사히 받겠습니다 !!</strong></em></p>
<h2 id="시작">시작</h2>
<p>모든 테스트는<br/>
<strong>Intel Mac<br/>
Intellj Ultimate<br/>
Java 8<br/>
Spring Boot 2.4.3<br/>
Gradle<br/>
Jenkins 2.282<br/></strong>
로 했습니다.</p>
<p>본 테스트는 WebHook을 포함하지 않습니다.
<br/></p>
<p>테스트는 아래와 같은 순서로 진행됩니다.</p>
<ol>
<li><p>Spring Boot Project 생성</p>
</li>
<li><p>Github에 프로젝트 푸쉬</p>
</li>
<li><p>Github 토큰 새성</p>
</li>
<li><p>Docker Jenkins Image 생성 및 실행</p>
</li>
<li><p>Jenkins 초기 세팅</p>
</li>
<li><p>CI/CD (빌드 및 배포) 세팅</p>
<br/>

</li>
</ol>
<h3 id="1-spring-boot-project-생성">1. Spring Boot Project 생성</h3>
<p>( 생성 과정이 귀찮으신 분들은 <a href="https://github.com/HanJaehee/JenkinsTutorial.git">https://github.com/HanJaehee/JenkinsTutorial.git</a> clone 하셔도 좋습니다 )</p>
<p><img src="https://images.velog.io/images/hind_sight/post/f2c5ac36-9380-4eb1-8a59-d5ad2d5420da/image.png" alt="">
스프링 부트 프로젝트를 생성해줍니다.
<img src="https://images.velog.io/images/hind_sight/post/97a490de-77fb-4b59-80e6-ecfb568968ef/image.png" alt="">
편의상 Gradle로 사용했습니다
본 테스트에서 Maven을 선택하신 분들은 후반부의 젠킨스 빌드 과정에서 커맨드가 달라 테스트가 정상적으로 진행되지 않을 수 있습니다!</p>
<p><img src="https://images.velog.io/images/hind_sight/post/86998ecd-1e15-4a28-91e0-acdfcbb8e0c6/image.png" alt="">
<img src="https://images.velog.io/images/hind_sight/post/7629498b-3733-433a-9b62-12de723bd5b6/image.png" alt="">
프로젝트에는 controller 패키지에 HelloController를 생성합니다.</p>
<pre><code>@RestController
public class HelloController {

    @GetMapping(&quot;/hello&quot;)
    public String hello(){
        return &quot;Hello #1&quot;;
    }
}</code></pre><p>로컬에 테스트를 해보시고, Hello #1이 정상적으로 리턴된다면, 다음으로 Dockerfile을 추가해줍니다. 경로는 프로젝트 최상단에 추가합니다.
<img src="https://images.velog.io/images/hind_sight/post/ae9e9077-b8d6-4954-bd6d-1ba0a8a25cd8/image.png" style="height:700px"/></p>
<pre><code>FROM java:8
EXPOSE 8081
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]</code></pre><p>java:8 이미지를 기반으로, 8081 포트로 매핑하며, build/libs에 위치한 jar파일을 실행시켜주는 도커 빌드파일입니다. 후반부의 도커 이미지 빌드과정에서 쓰일 예정입니다.</p>
<h3 id="2-github에-프로젝트-푸시">2. Github에 프로젝트 푸시</h3>
<p>GitHub에 프로젝트를 생성하고 위에서 생성한 프로젝트를 푸시합니다.
<img src="https://images.velog.io/images/hind_sight/post/bf61fb08-5382-4f6a-82aa-625093256d9b/image.png" />
푸시된것을 확인 했다면, 액세스 토큰을 생성하러 다음으로!👏</p>
<h3 id="3-github-액세스-토큰-생성">3. Github 액세스 토큰 생성</h3>
<p>settings -&gt; Develop settings -&gt; personal access tokens 에서 토큰을 생성합니다.
<img src="https://images.velog.io/images/hind_sight/post/75b7fed1-30cc-4f38-9a7d-f3fdbd1cfd47/image.png" style="width:800px text-align:center;"/>
노트는 되도록 띄어쓰기없는 이름으로, 그리고 퍼블릭 레포지토리의 접근을 허용하는 public_repo로 허용합니다.</p>
<hr>
<p>만약 토이 프로젝트를 진행하실 서버에 테스트하실 예정이라면, Private 레포지토리에 프로젝트를 생성하시는 것을 추천합니다.</p>
<p>이것은 그저 연습용 테스트 시나리오니 양해해주세요!</p>
<hr>
<br/>
### 4. Docker Jenkins Image 생성 및 실행
이제 본격적으로 젠킨스를 이용합니다.

<p>저는 Docker Desktop for Mac에서 진행했습니다! 제가 사용한 jenkins 이미지는 <strong>jenkins/jenkins:jdk11</strong> 입니다</p>
<p>먼저, 도커파일을 작성해줍시다. ( 제 레포지토리의 ForJenkins 폴더내에 작성되어있습니다. )</p>
<pre><code>FROM jenkins/jenkins:jdk11

#도커를 실행하기 위한 root 계정으로 전환
USER root

#도커 설치
COPY docker_install.sh /docker_install.sh
RUN chmod +x /docker_install.sh
RUN /docker_install.sh

#설치 후 도커그룹의 jenkins 계정 생성 후 해당 계정으로 변경
RUN groupadd -f docker
RUN usermod -aG docker jenkins
USER jenkins</code></pre><p>위 코드는 젠킨스 이미지를 빌드하기 위한 <strong>Dockerfile</strong> 입니다.</p>
<pre><code>#!/bin/sh
apt-get update &amp;&amp; \
apt-get -y install apt-transport-https \
  ca-certificates \
  curl \
  gnupg2 \
  zip \
  unzip \
  software-properties-common &amp;&amp; \
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo &quot;$ID&quot;)/gpg &gt; /tmp/dkey; apt-key add /tmp/dkey &amp;&amp; \
add-apt-repository \
&quot;deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo &quot;$ID&quot;) \
$(lsb_release -cs) \
stable&quot; &amp;&amp; \
apt-get update &amp;&amp; \
apt-get -y install docker-ce</code></pre><p>위 코드는 도커파일 내에 있는 도커를 설치하기 위한 <strong>docker_install.sh</strong> 파일입니다.</p>
<p>이 두 파일을 동일 경로에 위치시켜 주시고, 도커 이미지를 빌드합니다.</p>
<pre><code>docker build -t jenkins/myjenkins .</code></pre><img src="https://images.velog.io/images/hind_sight/post/64b0e3b9-61b7-494a-a809-d7a0a5f6774f/image.png" width="700" text-align="center"/>

<p>빌드가 완료되면, 빌드된 이미지를 확인해봅시다. <b>jenkins/myjenkins</b> 이미지가 생성되었다면 성공!</p>
<pre><code>docker imgages</code></pre><img src="https://images.velog.io/images/hind_sight/post/c8a83d32-b8cb-4d29-bde3-630ea1d09544/image.png" width="800" />

<p>이제, 이미지를 젠킨스 이미지를 실행시킵니다. 아래의 명령어에 테스트 내용의 절반이 들어있습니다.
docker run --help를 통해 하나씩 뜯어봅시다.</p>
<pre><code>docker run -d -p 9090:8080 --name=jenkinscicd \
-v /사용자경로/jenkinsDir:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/myjenkins</code></pre><pre><code>docker run -d -p 9090:8080 --name=jenkinscicd</code></pre><img src="https://images.velog.io/images/hind_sight/post/ca21f840-429b-4b14-ab28-dbd5d18db57f/image.png" />

<p><code>-d</code> 는 백그라운드에서 실행을 의미하고
<img src="https://images.velog.io/images/hind_sight/post/590ff1d3-d625-426e-b2ea-ae4a50cd8783/image.png" style="margin: 15px 0 0 0"/></p>
<p><code>-p</code> 는 매핑할 포트를 의미합니다. ( p가 port의 단축어가 아니었음 .. )</p>
<p><code>:</code> 기준으로 왼쪽은 로컬포트, 오른쪽은 도커 이미지의 포트를 의미합니다. 도커 이미지에서의 8080 포트를 로컬 포트 9090으로 매핑한다는 뜻입니다.</p>
<p>젠킨스(Spring 기반) 디폴트 포트인 8080을 따라가지 않고 9090으로 매핑한 이유는 스프링 부트 디폴트 포트가 8080때문이기 때문에 혹시 모를 충돌을 피하기 위해서 9090으로 매핑했습니다.</p>
<p><strong>--name</strong>은 컨테이너의 이름을 지정해줍니다.</p>
<pre><code>-v /사용자경로/jenkinsDir:/var/jenkins_home</code></pre><img src="https://images.velog.io/images/hind_sight/post/685faf4e-37c7-4406-825e-0354b79d665d/image.png" />

<p><code>-v</code> 옵션은 &quot;:&quot;를 기준으로 왼쪽의 로컬 경로를 오른쪽의 컨테이너 경로로 마운트 해줍니다.</p>
<p>즉, 제 컴퓨터의 <strong><code>사용자경로/jenkinsDir</code></strong> 을 컨테이너의 <strong><code>/var/jenkins_home</code></strong>과 바인드 시켜준다는 것입니다. 물론, 양방향으로 연결됩니다.</p>
<p>컨테이너가 종료되거나 알 수없는 오류로 정지되어도, jenkins_home에 남아있는 소중한 설정 파일들은 로컬 경로에 남아있게 됩니다.</p>
<p><code>단, 잘못된 설정으로 오류가 발생 했을 경우 로컬 경로를 꼭 비워 줍시다. 위와 같은 옵션으로 100번 젠킨스 컨테이너를 삭제하고 생성해도 동일한 설정파일로 실행되기 때문에 오류가 발생했을 때는 꼭! 비워 줍니다.. ~~제가 그래서 그런거 아님~~</code></p>
<pre><code>-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/myjenkins</code></pre><p>이 옵션은 로컬의 도커와 젠킨스 내에서 사용할 도커 엔진을 동일한 것으로 사용하겠다는 의미입니다.</p>
<p><code>Docker in Docker(DinD)</code>와 <code>Docker out of Docker(DooD)</code>  두 개의 개념이 존재하는데,
<strong>DinD는 도커에서도 권장하지 않습니다.</strong></p>
<p>DinD와 DooD에 대한 자세한 개념은 <a href="https://aidanbae.github.io/code/docker/dinddood">https://aidanbae.github.io/code/docker/dinddood</a> 에 아주 잘 소개되어있어서 링크를 걸어둡니다.</p>
<p>jenkins/myjenkins 는 위에서 빌드한 도커이미지의 이름으로, 해당 이미지로 컨테이너를 생성한다는 의미입니다.</p>
<img src="https://images.velog.io/images/hind_sight/post/2d9199f2-d653-421d-8a9e-e784ed4f2919/image.png" />

<p>이제 정상적으로 실행되었다면, 다음과 같이 컨테이너 ID가 출력됩니다. 가장 하단의 <strong>컨테이너 해시값</strong> 를 잘 확인해주세요.</p>
<h3 id="5-jenkins-초기-세팅">5. Jenkins 초기 세팅</h3>
<p>젠킨스에 접속하기 전에 <code>/var/run/docker.sock</code> 에 대한 권한을 설정해주어야 합니다.</p>
<p>일반적인 도커를 이용한 젠킨스 튜토리얼에서는 도커파일에 작성했던</p>
<pre><code>RUN groupadd -f docker
RUN usermod -aG docker jenkins</code></pre><p>이 두 명령으로 jenkins 계정을 docker group에 추가하며 설정되는 것 같지만.. 저는 여전히 <strong>Permission denied</strong>가 떴습니다.</p>
<p>확인해보니, 초기 <code>/var/run/docker.sock</code>의 권한이 <strong>소유자와 그룹 모두 root</strong>였기 때문에 이제 그룹을 root에서 <code>docker</code>로 변경해줄겁니다.</p>
<p>먼저, jenkins로 실행됐던 컨테이너의 bash를 root 계정으로 로그인 하기전에, 현재 실행되고 있는 컨테이너의 정보들을 확인할 수 있는 명령어를 입력해 아이디를 확인하겠습니다.</p>
<pre><code>docker ps -a</code></pre><img src="https://images.velog.io/images/hind_sight/post/7897203d-6b06-458c-a7ff-db706d2c38e9/image.png" />

<p>우리가 방금 생성한 컨테이너의 ID는 <strong>80bcdb8~</strong> 입니다. 도커는 다른 컨테이너 ID와 겹치지 않는 부분까지 입력하면 해당 컨테이너로 알아서 매핑해줍니다.</p>
<pre><code>docker exec -it -u root 컨테이너ID /bin/bash</code></pre><p><code>exec</code>는 컨테이너에 명령어를 실행시키는 명령어인데, /bin/bash와 옵션 -it를 줌으로써 컨테이너의 쉘에 접속할 수 있습니다.</p>
<p>이제 정말로 root 계정으로 컨테이너에 접속하기 위해 컨테이너ID에 0bc를 입력해 실행합니다.
<img src="https://images.velog.io/images/hind_sight/post/dc9722a9-1959-47ac-947e-167f134686a2/image.png" width="500"/>
root 계정으로 로그인이 잘 되었습니다. 이제 그룹을 바꾸기 위해 다음 명령어를 실행해줍니다.</p>
<pre><code>chown root:docker /var/run/docker.sock</code></pre><p>그리고 이제 쉘을 exit 명령어로 빠져나온 후 다음 명령어를 실행해 컨테이너를 재실행해줍니다.</p>
<pre><code>docker restart 0bc</code></pre><p>드디어 젠킨스 재부팅이 끝나면,
<img src="https://images.velog.io/images/hind_sight/post/e6a3714b-e9e5-41cb-b862-33e18603429a/image.png" width="800"/></p>
<p>관리자 패스워드를 입력하라는 창이 뜹니다. 로컬 쉘로 돌아가 다음 명령어를 입력해줍니다.</p>
<pre><code>docker logs bd2</code></pre><p>docker logs 컨테이너 id를 입력해 로그를 출력하면 initialAdminPassword가 출력됩니다. 이 패스워드를 입력해주면 됩니다.</p>
<img src="https://images.velog.io/images/hind_sight/post/92531a06-5df6-4983-82ae-aa5be65f560c/image.png" width="800"/>

<img src="https://images.velog.io/images/hind_sight/post/56e653e2-20b7-4c6a-a50e-dd998d61dd37/image.png" width="800"/>
정상적으로 입력했다면 플러그인 설치가 나오는데, 우리는 Install suggested plugins를 선택합니다.

<img src="https://images.velog.io/images/hind_sight/post/058f2ff5-86ab-4b79-b2d2-6ce9819ce3ba/image.png" width="800"/>

<p>자주 사용되는 플러그인들을 많이 설치해줍니다. 우리가 사용할 gradle, git 등등 도 보이네요.</p>
<p><em>여기서 설치실패가 절반이상 나시는 분들은 젠킨스 버전을 확인해주세요. 저도 구버전으로 할땐 젠킨스와 플러그인의 버전이 안맞아서 설치 실패하는 경우가 많았습니다..</em></p>
<img src="https://images.velog.io/images/hind_sight/post/fcad8792-0bbb-483b-a864-981cf828f2b7/image.png" width="800"/>
설치가 완료되면, 어드민 계정 생성창이 나오고, 본인이 사용하실 정보들을 입력해줍시다.

<img src="https://images.velog.io/images/hind_sight/post/c43f4bfb-1dd4-458c-84d7-a507fc4b5bd8/image.png" width="800"/>
앞으로 이 url로 젠킨스에 접속하시면 됩니다.

<img src="https://images.velog.io/images/hind_sight/post/bf13c3c3-5e3d-402c-859b-98405bae8ab0/image.png" width="800" />
여기까지 오셨다면, 젠킨스 설치 및 초기 세팅 완료!

<h3 id="6-cicd-빌드-및-배포-세팅">6. CI/CD (빌드 및 배포) 세팅</h3>
<img src="https://images.velog.io/images/hind_sight/post/3d7c26ca-0732-44f1-8009-e0849f4d5cb9/image.png" width="600" />
먼저, 대쉬보드의 새로운 아이템을 클릭합니다.


<img src="https://images.velog.io/images/hind_sight/post/864c5097-2966-4ffb-890d-7503177fb3ba/image.png" width="600" />

<p>아이템이름을 자유롭게 입력해주시고, Freestyle project를 선택하고, OK로 생성합니다.</p>
<p>이제 빌드 설정창이 뜰텐데, 소스 코드 관리쪽에서 Git을 선택하고, Repository URL에 다음과 같이 입력해줍니다.</p>
<img src="https://images.velog.io/images/hind_sight/post/df2ebf50-975c-4863-b687-ec0093993599/image.png" width="600" />


<p><code>https://토큰이름:토큰값@레포지토리경로</code></p>
<p>이제 Build에서 Execute shell을 선택해줍니다.
<img src="https://images.velog.io/images/hind_sight/post/1ad941c4-e597-417e-95f2-bd680021bd8c/image.png" width="500"/></p>
<p><code>./gradlew clean build</code>
<img src="https://images.velog.io/images/hind_sight/post/f1e304ea-e01a-476b-99b9-fde41cccb485/image.png" width="500" />
커맨드에 위와 같이 입력해주세요! 동일한 방법으로 하단의 커맨드도 모두 각각 입력해주세요.</p>
<pre><code>docker build -t jenkins/testapp .</code></pre><p>현재 경로에 있는 Dockerfile을 이용해 빌드하고 <code>jenkins/testapp</code>이라는 이름의 도커 이미지를 생성합니다.</p>
<pre><code>docker ps -q --filter &quot;name=jenkins-testapp&quot; | grep -q . &amp;&amp; docker stop jenkins-testapp &amp;&amp; docker rm jenkins-testapp | true</code></pre><p>실행되고있는 컨테이너의 이름이 jenkins-testapp 을 필터링하고, jenkins-testapp 이름의 실행되고 있는 컨테이너를 stop, 그리고 삭제합니다. 초기 빌드에선 실행되고 있는 컨테이너가 없겠지만, 빌드를 두번 이상 할 때는 기존에 실행되고 컨테이너와의 충돌을 막아줍니다.</p>
<pre><code>docker run -p 8081:8080 -d --name=jenkins-testapp jenkins/testapp</code></pre><p>위에서 빌드한 이미지 <code>jenkins/testapp</code>를 <code>jenkins-testapp</code> 이름의 컨테이너로 실행합니다. 여기서 jenkins-testapp을 바꾼다면, 위의 이름들도 모두 변경해주어야합니다.</p>
<pre><code>docker rmi -f $(docker images -f &quot;dangling=true&quot; -q) || true</code></pre><p>도커 이미지중 <code>dangling=true</code>옵션을 이용해 사용되지 않는 불필요한 이미지들을 지워줍니다.</p>
<p>결과적으로 다음과 같이 세팅하면 됩니다.
<img src="https://images.velog.io/images/hind_sight/post/3a8d3354-79fe-4d11-aa95-387f99a1ba3b/image.png" width="800" /></p>
<h3 id="7-빌드-확인">7. 빌드 확인</h3>
<p>이제 드디어 세팅한 값들을 확인해볼 차례입니다. 위의 내용들을 저장하고, Build Now를 눌러봅니다.
<img src="https://images.velog.io/images/hind_sight/post/4aad5d65-3939-48db-86cb-16666141ab9b/image.png" width="500" />
그럼 첫 빌드가 실행됩니다. #1을 누르고, console output로 진행사항을 확인해봅시다.</p>
<img src="https://images.velog.io/images/hind_sight/post/e04e7363-b91e-48dd-95c6-8fb87ad6194a/image.png" width="700" />

<p>빌드를 기다리고 Finished: SUCCESS가 보이면, 로컬에서 docker ps -a를 확인해봅시다.
<img src="https://images.velog.io/images/hind_sight/post/3a15c035-36e8-4923-bca4-827c65912b53/image.png" width="1000" />
로컬의 docker.sock과 젠킨스의 docker.sock이 공유되어 로컬에서도 jenkins/testapp이 실행중인것을 볼 수 있습니다!</p>
<p>이제 <a href="http://localhost:8081/hello%EC%97%90">http://localhost:8081/hello에</a> 접속해보면,
<img src="https://images.velog.io/images/hind_sight/post/a8066298-f1cc-4d93-b449-4886f0e7e716/image.png" width="300" />
Hello #1을 확인하실 수 있습니다.</p>
<p>이제, Hello #1을 Hello #2로 변경해 푸시 후 젠킨스에서 다시 빌드를 해봅시다.
<img src="https://images.velog.io/images/hind_sight/post/62e473af-36ba-4b98-bf24-44de3a1b3654/image.png" />
두번째 빌드의 Console Output 중 커밋메세지에 마지막으로 푸시한 커밋 메세지가 보이면서, 빌드 완료 후 다시 접속해 Hello #2를 보셨다면, 드디어 끝 🔥!</p>
<p><del>다음 글에서는 develop 브랜치를 위한 Web Hook 자동 배포를 공유해볼 예정입니다.</del> 
현재 프로젝트에서는 Jenkins를 활용하지 않아, 당분간은 Jenkins Webhook 관련 글 작성이 어려울 것 같습니다.
<a href="https://pooney.tistory.com/86">https://pooney.tistory.com/86</a>
대신 위의 글을 소개 드리면서, 마치겠습니다! ( 구글 검색을 통해 다양한 케이스들을 살펴보시는 것 추천드립니다! )</p>
<p>모두 화이팅!</p>
<h2 id="참고"><strong>참고</strong></h2>
<p><a href="coding-start.tistory.com/329">coding-start.tistory.com/329</a></p>
<p>도커관련 글을 써주신 모든 분들.. 감사합니다</p>
]]></description>
        </item>
    </channel>
</rss>