<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>윤재의 개발 블로그</title>
    <link>https://promisingmoon.tistory.com/</link>
    <description>안녕하세요. PS풀이, 개발일지 및 일기, 소소한 이야기를 적어가는 윤재 입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 7 May 2026 02:22:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yunjae62</managingEditor>
    <image>
      <title>윤재의 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/5026916/attach/bacdf893b59648d994d8ef4b5a5c35a3</url>
      <link>https://promisingmoon.tistory.com</link>
    </image>
    <item>
      <title>TIL #133 : 맥에서 한글 입력 시 자모분리 및 입력지연 해결법 (구름 입력기 + 카라비너)</title>
      <link>https://promisingmoon.tistory.com/242</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥 사용 6년 차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 한글 자소분리/자모분리 및 입력 지연 문제를 해결했다!!!!!!!!!!!!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 단계대로 하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 10분 정도 걸리는데, 로그아웃/로그인 과정이 필요해서 약간 귀찮긴 하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 구름 입력기 설치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 로그아웃 후 다시 로그인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. Karabiner-Elements 설치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 룰 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 꼬우~&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 구름 입력기에 접속해서 pkg를 내려받아 설치를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소개 사이트 : &lt;a href=&quot;https://gureum.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gureum.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 사이트 : &lt;a href=&quot;https://github.com/gureum/gureum/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/gureum/gureum/releases&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ATzPm/dJMcahpUON5/Pp1WyNwGFNv9Rzhov5Sg5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ATzPm/dJMcahpUON5/Pp1WyNwGFNv9Rzhov5Sg5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ATzPm/dJMcahpUON5/Pp1WyNwGFNv9Rzhov5Sg5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FATzPm%2FdJMcahpUON5%2FPp1WyNwGFNv9Rzhov5Sg5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2174&quot; height=&quot;782&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2174&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비개발자라면 안 익숙할 수 있어서 스크린샷까지 첨부해 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;티스토리의 개구려 이미지 에디터에는 도형 추가 기능이 없어서 피치로 화살표를 대체한다.....&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최하단의 Assets -&amp;gt; Gureum-1.13.2.pkg 를 내려받으면 된다. (1.13.2는 버전이라서 더 최신 버전을 내려받으면 되겠다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;그리고 꼭 재시작이나 로그아웃/로그인을 해주어야 한다!!!!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안 하면 설정에서 입력 소스에 구름 입력기가 표기되지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CGa6H/dJMcajusIi5/b6cUTeoDz0QPnKD484onr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CGa6H/dJMcajusIi5/b6cUTeoDz0QPnKD484onr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CGa6H/dJMcajusIi5/b6cUTeoDz0QPnKD484onr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCGa6H%2FdJMcajusIi5%2Fb6cUTeoDz0QPnKD484onr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;592&quot; data-origin-width=&quot;1424&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재시작을 하고 설치를 한 후에는 설정에서 입력 소스 변경을 한글 기본 입력기에서 구름 입력기로 변경을 해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;키보드 -&amp;gt; 텍스트 입력 -&amp;gt; 입력 소스 편집 버튼&lt;/b&gt; 클릭하면 되겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.15.10.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;1036&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHC26t/dJMcag5ClSh/23kQBqKXAkbdhDse5tZSKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHC26t/dJMcag5ClSh/23kQBqKXAkbdhDse5tZSKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHC26t/dJMcag5ClSh/23kQBqKXAkbdhDse5tZSKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHC26t%2FdJMcag5ClSh%2F23kQBqKXAkbdhDse5tZSKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;1036&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.15.10.png&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;1036&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 ABC(영어)와 두벌식(한글)이 있었겠지만, 우선 두벌식을 클릭 후 하단의 - 버튼을 눌러 제거를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.16.47.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1034&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lQbhn/dJMcafZUtJn/TQxU9MD57jTm1Kh60dcPI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lQbhn/dJMcafZUtJn/TQxU9MD57jTm1Kh60dcPI0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lQbhn/dJMcafZUtJn/TQxU9MD57jTm1Kh60dcPI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlQbhn%2FdJMcafZUtJn%2FTQxU9MD57jTm1Kh60dcPI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;1034&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.16.47.png&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;1034&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 + 버튼을 눌러서 두벌식(옆의 구름 아이콘이 있어야 함)을 선택 후 추가 버튼을 누르면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 Caps Lock을 누르면 한/영 전환이 안 될 거다 ㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐면... 캡스락은 기본적으로 입력기가 2개 이상 있을 때만 전환이 되기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 더 충격적인 건, 맥에서 한영 전환 같은 입력 소스 전환 단축키는 캡스락이 아니라 컨트롤+스페이스 였다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥 6년 쓰면서 처음 알았다....&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 한글 자모분리만 안 되는 것에도 감사하며 열심히 컨+스를 썼지만... 이게 앵간히 불편해야지 ㅡㅡ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자꾸 습관적으로 캡스락 누르는데 한글 전환은 안 되고 대문자 영어로 써지니 안 되겠다 이거 해결 본다고 찾아봤다가 카라비너를 발견했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 사이트 : &lt;a href=&quot;https://karabiner-elements.pqrs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://karabiner-elements.pqrs.org/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Karabiner-Elements(이하 카라비너)는 맥에서 키보드 단축키를 자유롭게 재설정해주는 프로그램이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이걸 이용하면 캡스락으로 한/영 전환을 하면서도, 길게 누르면 영 대문자까지 사용 가능하도록 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위의 링크에 들어가서 설치를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 설치하면 허용해 달라고 물을 텐데 들어가서 설정해 주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 설치해 버려서 까먹었지만.. 기억을 되살려보자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 개인정보 보호 및 보안 -&amp;gt; 입력 모니터링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 맥 실행 시 자동 실행&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도였던 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.27.54.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdmYqC/dJMcaaK5mCZ/ZAqhp3gwoOqNC7hkPIz0o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdmYqC/dJMcaaK5mCZ/ZAqhp3gwoOqNC7hkPIz0o0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdmYqC/dJMcaaK5mCZ/ZAqhp3gwoOqNC7hkPIz0o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdmYqC%2FdJMcaaK5mCZ%2FZAqhp3gwoOqNC7hkPIz0o0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1502&quot; height=&quot;678&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.27.54.png&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;br /&gt;이제 진짜 마지막이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;네이버 블로그 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://blog.naver.com/hankboy/221200885234&quot;&gt;https://blog.naver.com/hankboy/221200885234&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 링크로 들어가서 덧 17에 써져 있는 다운로드 링크의 파일을 내려받는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종종 구글 드라이브에 접속 못 하는 사람들이 있어서 코드만 남겨두는데, 같은 블로거로서 위의 링크 1 클릭 한 번씩 하고 오면 좋겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래를 복사해서 파일명은 아무렇게 저장하면 되겠다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771940262524&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
   &quot;title&quot;:&quot;Caps Lock 을 한/영 전환으로 사용 (rev.1.0)&quot;,
   &quot;rules&quot;:[
      {
         &quot;description&quot;:&quot;Caps Lock to KO/EN Toggle on macOS&quot;,
         &quot;manipulators&quot;:[
            {
               &quot;type&quot;:&quot;basic&quot;,
               &quot;conditions&quot;:[
                  {
                     &quot;type&quot;:&quot;frontmost_application_unless&quot;,
                     &quot;bundle_identifiers&quot;:[
                        &quot;com.parallels.desktop&quot;,
                        &quot;com.parallels.vm&quot;,
                        &quot;com.parallels.desktop.console&quot;,
                        &quot;com.parallels.winapp.&quot;,
                        &quot;com.vmware.fusion&quot;,
                        &quot;com.vmware.horizon&quot;,
                        &quot;com.vmware.view&quot;
                     ]
                  }
               ],
               &quot;parameters&quot;:{
                  &quot;basic.to_if_alone_timeout_milliseconds&quot;:200,
                  &quot;basic.to_if_held_down_threshold_milliseconds&quot;:200
               },
               &quot;from&quot;:{
                  &quot;key_code&quot;:&quot;caps_lock&quot;,
                  &quot;modifiers&quot;:{
                     &quot;optional&quot;:[
                        &quot;any&quot;
                     ]
                  }
               },
               &quot;to_if_alone&quot;:[
                  {
                     &quot;key_code&quot;:&quot;spacebar&quot;,
                     &quot;modifiers&quot;:[
                        &quot;left_control&quot;,
                        &quot;left_option&quot;
                     ]
                  }
               ],
               &quot;to_if_held_down&quot;:[
                  {
                     &quot;key_code&quot;:&quot;caps_lock&quot;
                  }
               ]
            }
         ]
      },
      {
         &quot;description&quot;:&quot;Caps Lock to KO/EN Toggle on Parallels&quot;,
         &quot;manipulators&quot;:[
            {
               &quot;type&quot;:&quot;basic&quot;,
               &quot;conditions&quot;:[
                  {
                     &quot;type&quot;:&quot;frontmost_application_if&quot;,
                     &quot;bundle_identifiers&quot;:[
                        &quot;com.parallels.desktop&quot;,
                        &quot;com.parallels.vm&quot;,
                        &quot;com.parallels.desktop.console&quot;,
                        &quot;com.parallels.winapp.&quot;
                     ]
                  }
               ],
               &quot;parameters&quot;:{
                  &quot;basic.to_if_alone_timeout_milliseconds&quot;:200,
                  &quot;basic.to_if_held_down_threshold_milliseconds&quot;:200
               },
               &quot;from&quot;:{
                  &quot;key_code&quot;:&quot;caps_lock&quot;,
                  &quot;modifiers&quot;:{
                     &quot;optional&quot;:[
                        &quot;any&quot;
                     ]
                  }
               },
               &quot;to_if_alone&quot;:[
                  {
                     &quot;key_code&quot;:&quot;right_option&quot;
                  }
               ],
               &quot;to_if_held_down&quot;:[
                  {
                     &quot;key_code&quot;:&quot;caps_lock&quot;
                  }
               ]
            }
         ]
      },
      {
         &quot;description&quot;:&quot;Caps Lock to KO/EN Toggle on VMware&quot;,
         &quot;manipulators&quot;:[
            {
               &quot;type&quot;:&quot;basic&quot;,
               &quot;conditions&quot;:[
                  {
                     &quot;type&quot;:&quot;frontmost_application_if&quot;,
                     &quot;bundle_identifiers&quot;:[
                        &quot;com.vmware.fusion&quot;,
                        &quot;com.vmware.horizon&quot;,
                        &quot;com.vmware.view&quot;
                     ]
                  }
               ],
               &quot;parameters&quot;:{
                  &quot;basic.to_if_alone_timeout_milliseconds&quot;:200,
                  &quot;basic.to_if_held_down_threshold_milliseconds&quot;:200
               },
               &quot;from&quot;:{
                  &quot;key_code&quot;:&quot;caps_lock&quot;,
                  &quot;modifiers&quot;:{
                     &quot;optional&quot;:[
                        &quot;any&quot;
                     ]
                  }
               },
               &quot;to_if_alone&quot;:[
                  {
                     &quot;key_code&quot;:&quot;right_option&quot;
                  }
               ],
               &quot;to_if_held_down&quot;:[
                  {
                     &quot;key_code&quot;:&quot;caps_lock&quot;
                  }
               ]
            }
         ]
      }
   ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내려받으면 해당 파일을 ~/.config/karabiner/assets/complex_modifications 폴더 안에 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj3lrF/dJMcabDeSpW/zdT3g15JipIZzKrkh6WzVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj3lrF/dJMcabDeSpW/zdT3g15JipIZzKrkh6WzVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj3lrF/dJMcabDeSpW/zdT3g15JipIZzKrkh6WzVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj3lrF%2FdJMcabDeSpW%2FzdT3g15JipIZzKrkh6WzVk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1816&quot; height=&quot;796&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비개발자라면 여기서 조금 헤맬 수도 있어서 차근차근 설명을 붙여보자면... 우선 파인더에서 본인맥 홈폴더에 들어간다. 나 같은 경우는 yunjae62인데, 각 사용자마다 다르다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 command + shift + . 을 눌러준다. 숨김 파일을 볼 수 있는 단축키다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.33.44.png&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kZcui/dJMcafeA4bp/47Na3GQX50Rdz6Be2TEpJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kZcui/dJMcafeA4bp/47Na3GQX50Rdz6Be2TEpJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kZcui/dJMcafeA4bp/47Na3GQX50Rdz6Be2TEpJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkZcui%2FdJMcafeA4bp%2F47Na3GQX50Rdz6Be2TEpJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1814&quot; height=&quot;800&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.33.44.png&quot; data-origin-width=&quot;1814&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 .config 라고 하는 폴더가 보일 건데, 이후로 위의 경로까지 쭉쭉 들어가서 파일을 넣어주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.34.37.png&quot; data-origin-width=&quot;2178&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bswdhq/dJMcabQKtJI/aWfB5Nue77PbnVBgVaJ1JK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bswdhq/dJMcabQKtJI/aWfB5Nue77PbnVBgVaJ1JK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bswdhq/dJMcabQKtJI/aWfB5Nue77PbnVBgVaJ1JK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbswdhq%2FdJMcabQKtJI%2FaWfB5Nue77PbnVBgVaJ1JK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2178&quot; height=&quot;712&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.34.37.png&quot; data-origin-width=&quot;2178&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 다음 다시 카라비너로 돌아가서, 왼쪽의 Complex Modifications -&amp;gt; 상단의 Add predefined rule 버튼을 클릭한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 같은 경우는 이미 추가해서 아래 3개가 있으니 오해 금지.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.35.23.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZgMeT/dJMcah4wc0r/pzYmQ0BSECKHGdtdGeUIeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZgMeT/dJMcah4wc0r/pzYmQ0BSECKHGdtdGeUIeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZgMeT/dJMcah4wc0r/pzYmQ0BSECKHGdtdGeUIeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZgMeT%2FdJMcah4wc0r%2FpzYmQ0BSECKHGdtdGeUIeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1966&quot; height=&quot;638&quot; data-filename=&quot;스크린샷 2026-02-22 오후 6.35.23.png&quot; data-origin-width=&quot;1966&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 뜨는 창에서 파일을 정상적으로 넣었다면 이게 보일 건데, 오른쪽 아래의 Enable All 버튼을 클릭하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 끝!!!!!!!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하기 전에, 찐막 한 가지!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGagFF/dJMcajgWwhy/tHhGvwkYtkSgPRq2bTWsSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGagFF/dJMcajgWwhy/tHhGvwkYtkSgPRq2bTWsSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGagFF/dJMcajgWwhy/tHhGvwkYtkSgPRq2bTWsSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGagFF%2FdJMcajgWwhy%2FtHhGvwkYtkSgPRq2bTWsSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;396&quot; height=&quot;444&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상단바에 있는 구름 입력기를 클릭하면 환경설정란이 있는데 클릭.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-02-24 오후 10.34.14.png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dj7n0s/dJMcahcoIrq/Y1CooUxzydKmlZdg95ow10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dj7n0s/dJMcahcoIrq/Y1CooUxzydKmlZdg95ow10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dj7n0s/dJMcahcoIrq/Y1CooUxzydKmlZdg95ow10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdj7n0s%2FdJMcahcoIrq%2FY1CooUxzydKmlZdg95ow10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;416&quot; data-filename=&quot;스크린샷 2026-02-24 오후 10.34.14.png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번째는 이미 체크가 되어 있을 텐데 2번째 모아치기도 체크해 주면 좋다!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;느 ㄴ&lt;/b&gt; 같은 경우나 &lt;b&gt;ㅣㅇㅇ&lt;/b&gt; 같은 경우에 자동으로 &lt;b&gt;는&lt;/b&gt;, &lt;b&gt;잉&lt;/b&gt; 으로 완성해 준다 ㅋㅋㅋㅋ 진짜 편함!!!!!!!!!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 너무 편해서... 바로 블로그부터 켰다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스팅해야 하는데... 노션에다가 적어놓은 포스팅거리가 한가득인데... 퇴근하면 귀찮아서 글을 안 쓴다 ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노력해 보겠습니다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>karabiner</category>
      <category>구름입력기</category>
      <category>맥한글</category>
      <category>맥한글입력</category>
      <category>입력지연</category>
      <category>자모분리</category>
      <category>자소분리</category>
      <category>카라비너</category>
      <category>캡스락</category>
      <category>한글</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/242</guid>
      <comments>https://promisingmoon.tistory.com/242#entry242comment</comments>
      <pubDate>Sun, 22 Feb 2026 18:47:35 +0900</pubDate>
    </item>
    <item>
      <title>2025년 회고</title>
      <link>https://promisingmoon.tistory.com/240</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;벌써 2026년이다.&amp;nbsp;&lt;br /&gt;원래 회고를 잘 안 하기는 하는데 그래도 25년은 의미 깊은 날들이 많았어서 한번 정리를 해보려고 한다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;일단 잘한 점 3가지를 꼽아보았다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;1. 머리 변경&lt;br /&gt;2. 취업!&lt;br /&gt;3. 일기 꾸준히 쓰기&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;우선 머리를 가르마펌으로 바꾸었는데, 인생에서 제일 잘 한 결정 중 하나가 될 것 같다 ㅎㅎ&lt;br /&gt;내가 원래 머리를 덮고 투블럭으로만 했었는데... 머리를 바꾸는 계기가 된 사건이 하나 있었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;1월에 편의점에서 맥주를 사는데 사장님께서 나보고 신분증을 요구하면서.. 중학생인 줄 알았다고 하시는 거다.&lt;br /&gt;항상.... 듣는 말이어서 그냥 넘어갈 수도 있었지만, 그때 문득 나이 27살 먹고 중학생 소리를 듣는 게 맞나..?라는 생각이 들면서 주변에 나이 들어 보이는 법을 물어보고 다녔고, 머리를 까자는 결론을 내렸다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;처음에는 펌까지는 부담스러워서.. 어떻게든 도구들로 해보려 했는데 이눔의 자기주장 강한 쌩직모 머리는 들을 생각을 안 했다. 그때 부트캠프에서 막 친해지기 시작한 ㅁㅊ이가 울집 근처 회사에서 면접보고 울집까지 와서 스타일링을 해주었다... ㅋㅋㅋㅋㅋㅋㅋㅋ 슥슥 몇 번 하더니 완성되었다.. 대박&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOIL4k/dJMcaaRu55K/IMFJklhv9IryGWVpjVurXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOIL4k/dJMcaaRu55K/IMFJklhv9IryGWVpjVurXK/img.png&quot; data-alt=&quot;그 날의 일기 발췌 ㅋㅋ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOIL4k/dJMcaaRu55K/IMFJklhv9IryGWVpjVurXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOIL4k%2FdJMcaaRu55K%2FIMFJklhv9IryGWVpjVurXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;706&quot; height=&quot;128&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그 날의 일기 발췌 ㅋㅋ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 ㅁㅊ이는 그 회사에 합격하여 인턴을 하고 현재는 정직원으로 전환되어 재직중이고&lt;br /&gt;나는 머리를 깠음에도 여전히 중학생이냐는 소리를 듣고 다닌다. ㅠㅠ&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6qi12/dJMcadOcmQg/pBdCWzntjKA6zE2TdXHfYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6qi12/dJMcadOcmQg/pBdCWzntjKA6zE2TdXHfYK/img.png&quot; data-alt=&quot;그 날의 일기 발췌&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6qi12/dJMcadOcmQg/pBdCWzntjKA6zE2TdXHfYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6qi12%2FdJMcadOcmQg%2FpBdCWzntjKA6zE2TdXHfYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;530&quot; height=&quot;135&quot; data-origin-width=&quot;1188&quot; data-origin-height=&quot;302&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그 날의 일기 발췌&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 취업.&lt;br /&gt;큰 곳에 취업을 하지는 않았지만, 회사가 기대했던 것보다 더 좋았다.&amp;nbsp;&lt;br /&gt;우선 사람들이 너무 다 좋다. 다들 친절하니 열심히라서 일이 재밌다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crtp4Y/dJMcafyuRAE/KWmm0wq13D6LnuZhRSg0j1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crtp4Y/dJMcafyuRAE/KWmm0wq13D6LnuZhRSg0j1/img.png&quot; data-alt=&quot;참고로 제이지는 창업한 대표님이다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crtp4Y/dJMcafyuRAE/KWmm0wq13D6LnuZhRSg0j1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcrtp4Y%2FdJMcafyuRAE%2FKWmm0wq13D6LnuZhRSg0j1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;232&quot; data-origin-width=&quot;760&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;참고로 제이지는 창업한 대표님이다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 무엇보다 좋은 것은 내가 돈을 번다는 사실.&amp;nbsp;&lt;br /&gt;내가 소득세를 내다니 ㅋㅋㅋ&amp;nbsp;&lt;br /&gt;내가 사회에 기여하고 보수를 받는다는 사실이 너무 기뻤다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;반년 간 열심히 굴렀더니 기술이사님께 인정을 받아서 올해부터는 회사 핵심 업무를 담당하게 되었다.&amp;nbsp;&lt;br /&gt;최소 트래픽 200 TPS, 피크 때는 2000도 찍는 울 회사 캐시카우....&amp;nbsp;&lt;br /&gt;이제 장애내면 초단위 매출손실 감당해야 된다 ㅠㅠㅠ&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;마지막으로 일기 덕분에 이 회고가 존재했을 정도로 아주 가치가 크다.&amp;nbsp;&lt;br /&gt;사실 일기는 내 인생 전체에서 수많은 시도가 있었다. 다만 쓰다가 얼마 안 가 포기한 게 대부분이었다..&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;우선 공책이라는 물리적인 매체에 적다 보니 내 생각을 손이 못 따라가는 게 컸고, 또 나는 일기를 지금까지 조선왕조실록마냥 몇 시에 뭐 했다. 몇 시에 뭐 했다. 식의 사실중심 일기를 써와서 한 번 일기를 적는데 너무 오래 걸렸다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그래서 포기하고 있다가... 2년 전 한 지인의 추천으로 노션에 일기를 써보라고 권유받았는데, 사실 중심 말고 감정 중심의 일기를 써보라고 했다.&lt;br /&gt;그땐 일단 했었는데, 생각보다 효과가 굉장했고? 습관이 되어서 이제는 틈틈이 쓰게 되었다 ㅋㅋㅋㅋㅋㅋ&lt;br /&gt;&amp;nbsp;&lt;br /&gt;사실 아직도 내 감정을 구체적으로 서술하는 게 힘들기는 하지만, 그래도 의식적으로 훈련을 하니 많이 늘었다. 또 요즘은 적은 일기를 지피티 돌려서 내가 미쳐 놓치고 있던 감정들을 알려달라 하면서 나를 더 이해하려 한다.. ㅋㅋㅋ&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이렇게 하다 보니 뭔가 더 F에 가까워지고 있다. 정확히는 이성과 감성을 모두 적절하게 활용할 수 있게 되었다.&lt;br /&gt;일기를 쓰면서 내가 겪은 하나의 사건에 대해서 어떤 생각들을 했고, 그로 인해 어떤 감정들을 느꼈는지를 정리하다 보니&lt;br /&gt;나와 비슷한 경험을 했던 타인도 이런 생각을 하고 감정을 느꼈겠지? 하면서 공감이 되어 배려하고 이해할 수 있게 되었다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KRIE2/dJMcaiPtZsb/jMDKbhYaYuGIKt7UVIlDU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KRIE2/dJMcaiPtZsb/jMDKbhYaYuGIKt7UVIlDU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KRIE2/dJMcaiPtZsb/jMDKbhYaYuGIKt7UVIlDU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKRIE2%2FdJMcaiPtZsb%2FjMDKbhYaYuGIKt7UVIlDU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1220&quot; height=&quot;332&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;요즘은 매일 쓰지는 않고, 하루가 인상 깊을 때마다 적고 있고, 키워드로 그날 느낀 감정을 키워드로 해시태그 달아두고 있다 ㅋㅋ&amp;nbsp;&lt;br /&gt;하지만 올해 매일매일이 너무 스펙타클해서 하루도 빠짐없이 매일 쓰고 있다....&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또 일기를 쓰면서 나에 대해서 더 잘 알게 되는 것 같다.&amp;nbsp;&lt;br /&gt;그저 흘러가는 생각으로 휘발될 수 있었던 것이 활자로 남아서 두고두고 읽히니 과거의 나와 연결되는 느낌.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;일기 쓸 때 이미지를 최대한 많이 첨부하면 추억회상 때 감정이 더 배가 된다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;또 나는 유명한 기록쟁이다...&lt;br /&gt;나는 특히 시간을 소중하게 여겨서.. 수능이 끝난 후부터 지금까지 구글 캘린더에 내 일정을 전부 기록해오고 있다.&amp;nbsp;&lt;br /&gt;젊을 때는 자는 시간까지 기록했었는데, 요즘은 그것까지는 무의미한 데이터라 판단해서 특별히 알아두어야 할 일정만 기록하고 있다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이렇게 일기 + 사진 + 캘린더로 내 과거를 남겨놓으니 과거를 돌아볼 때 내가 몇 월 며칠에 무엇을 하고 있었는지 바로 알 수 있어서 좋은 점이 많은 것 같다. 습관이 잡혀서 기록하는게 귀찮지도 않고.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;지인 형과 함께 사업을 도운 적이 있다. 이후 외주를 물고 오면서 수익분배 관련해서 처음 내 가치에 대해서 생각해 보았다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;원래라면 나는 기획/설계/프론트/백엔드/인프라까지 전부 다 할 줄 아니 이 정도는 받아야 하지 않나?라고 추상적으로 생각했지만, 대표형은 내가 어떤 작업들을 하고, 각 작업에는 시간이 얼마나 걸리며, 내 시급은 어느 정도인 것 같으니 이 정도의 수익을 분배받아야겠다고 이야기를 해주었다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;이 말을 듣고 큰 충격을 받았다. 내 말에는 근거가 없구나. 나는 내 가치를 몰랐구나.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그 뒤로 내 연봉의 시급을 계산해보고, 내가 하는 일의 작업 시간을 측정해보며 내가 얼마 만큼의 리턴을 받아야 하는지를 의식적으로 생각해보는 훈련을 하고 있다. 회사에서도 내가 하는 업무가 어떤 임팩트가 있는지를 고려하면서...&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=nkc8tVCxZKM&amp;amp;t=757s&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span&gt;https://www.youtube.com/watch?v=nkc8tVCxZKM&amp;amp;t=757s&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=nkc8tVCxZKM&quot; data-video-thumbnail=&quot;https://blog.kakaocdn.net/dna/1Z7Ad/hyZRmanUlP/AAAAAAAAAAAAAAAAAAAAAD2PqmNH0aDEGv5OMo1Dn08ri1ZLf8vAz4YR2idtpkTK/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1769871599&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=oeEa7Yd4Dzupvi52dqpXhruZsTI%3D&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;다정은 체력이고 배려는 지능이라던데.. 필요한 다정 VS 피곤한 다정, &amp;lsquo;다정&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/nkc8tVCxZKM&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;링크 타면 시간 링크를 걸어두었는데, 한 1분 길이 정도 봐주면 좋겠다&amp;nbsp;&lt;br /&gt;내게 꽤 큰 울림이 있던 말이었다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;원소윤님이 말하는 다정함에는 발산하는 다정함과 수렴하는 다정함이 있다고 한다.&lt;br /&gt;일반적으로 앞에서 챙겨주는 발산하는 다정함도 있지만, 수렴하는 다정함도 있는 것 같다.&lt;br /&gt;곁에서 지켜주고 함구하며 기다리는 그런 다정함.&lt;br /&gt;듣고 머리가 띵 했다. 이런 류의 개념을 지금껏 떠올리지 못하고 있었어서.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;나는 수렴하는 다정함을 추구!하는 사람인 것 같다.&lt;br /&gt;지인의 비밀을 함구하고, 호불호를 기억해 챙겨주는 그런 사람이 되려고 의식적으로 노력해왔다.&lt;br /&gt;타인이 그렇게 안 느꼈다면 음.. 어쩔 수 없지만...&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;종종 만나면 삶의 낙이 무엇인지 물어보는 ㅇㄹ이라는 친구가 있다.&lt;br /&gt;덕분에 내 삶의 낙이 무엇을까? 계속 생각해보는 계기가 되었다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;그러다 한 7월쯤 어렴풋이 깨달은게 있다. 일상 속에서 여유가지고 행동하기.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;출근길에 사람들은 지금 오는 지하철 타려고 뛰어갈 때 나는 &amp;lsquo;먼저 가세요~ 전 나중에 가렵니다~&amp;rsquo; 생각하면서 옆으로 비켜주면서 천천히 걷기.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;퇴근길에 사람들은 몇 초 안 남은 횡단보도 건너려고 뛰어갈 때 나는 옆길로 비켜 걸으며 하늘에 둥둥 떠다니는 구름 보거나 비행기 구경하기.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;비가 오는 날에 어떻게든 안 맞으려고 우산으로 꽁꽁 싸매기 보다, 좀 맞더라도 우산을 조금 들어 올려서 내리는 비 구경하면서 우중충한 하늘 바라보기.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;꽤나 소소한 것들인데 이런 작은 것에서 은은히 재미를 느끼는 것 같다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2XxnT/dJMcacaKkwB/1PtAJq08tkldWZNdyTRgBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2XxnT/dJMcacaKkwB/1PtAJq08tkldWZNdyTRgBK/img.png&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;2048&quot; style=&quot;width: 39.4607%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2XxnT/dJMcacaKkwB/1PtAJq08tkldWZNdyTRgBK/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2XxnT%2FdJMcacaKkwB%2F1PtAJq08tkldWZNdyTRgBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;2048&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmpMfg/dJMcacaKkwI/OUMwnWPydbBY7jKivZxPDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmpMfg/dJMcacaKkwI/OUMwnWPydbBY7jKivZxPDk/img.png&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1276&quot; style=&quot;width: 59.3765%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmpMfg/dJMcacaKkwI/OUMwnWPydbBY7jKivZxPDk/img.png&quot; alt=&quot;&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmpMfg%2FdJMcacaKkwI%2FOUMwnWPydbBY7jKivZxPDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;월드 IT쇼에 ㅂㅈ이형이랑 다녀왔다가 화재로 대피를 했다. 생애 첫 화재사고 경험담...&lt;br /&gt;오전에 돌아다니고 있는데 갑자기 통로 반대편의 아주머니께서 불이야~하며 장난스런 말투로 지나가길래, 나는 사람들이 많아서 길 뚫으려고 장난치는 줄 알았는데, 갑자기 안내 방송으로 화재가 났다고 해서 대피했다. ㅋㅋ&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;나가서 점심 먹고 카페에 있다보니 재입장이 가능하다고 해서 다시 들어갔다. 3층 전시장에 가니 연기 냄새가 그윽했지만.. 일단 다녔다 ㅋ&lt;br /&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;359&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNzFWk/dJMcabXdlEw/KjtPmODQkf7B4NpioWmD6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNzFWk/dJMcabXdlEw/KjtPmODQkf7B4NpioWmD6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNzFWk/dJMcabXdlEw/KjtPmODQkf7B4NpioWmD6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNzFWk%2FdJMcabXdlEw%2FKjtPmODQkf7B4NpioWmD6k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;293&quot; height=&quot;517&quot; data-origin-width=&quot;359&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;삼성 부스가 제일 재미있었다. 갤럭시 AI가 문장으로 이모지를 만들어주었는데 코딩하는 다람쥐를 만들어 디스플레이에 슬쩍 공개해두고 도망쳤다 ㅋ&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/240</guid>
      <comments>https://promisingmoon.tistory.com/240#entry240comment</comments>
      <pubDate>Sun, 18 Jan 2026 14:59:26 +0900</pubDate>
    </item>
    <item>
      <title>[제주도 여행] 1일 차</title>
      <link>https://promisingmoon.tistory.com/238</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 쓰는 1일 차...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc2UoR/btsPAsGQ87T/euQ6Qz15ECNP5hlQeXUriK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc2UoR/btsPAsGQ87T/euQ6Qz15ECNP5hlQeXUriK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc2UoR/btsPAsGQ87T/euQ6Qz15ECNP5hlQeXUriK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc2UoR%2FbtsPAsGQ87T%2FeuQ6Qz15ECNP5hlQeXUriK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;469&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작은 이러했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 연락하는 군대 동기가 있는데 취업도 했겠다, 한번 만나야겠다 싶어서 연락하니 제주도란다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 연락할 때마다 장소가 서울, 부산, 진천, 인천이었고... 제주도까지는 생각을 못해서 많이 당황했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주공항 근처 게스트하우스에서 일하면서 한 달 살기를 하고 있다고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 친구도 볼 겸, 혼자 제주도 여행을 1박 2일로 다녀오기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 2박 3일로 잡았고 복귀날 다음날이 출근날이었다가 하루는 쉬려고 줄였는데 정말 잘한 결정이었다. 그 하루동안 하루 종일 누워있었다 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewQyMY/btsPzWaocC4/A7IEY33MPH7i8fLXkjk1k0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewQyMY/btsPzWaocC4/A7IEY33MPH7i8fLXkjk1k0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewQyMY/btsPzWaocC4/A7IEY33MPH7i8fLXkjk1k0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewQyMY%2FbtsPzWaocC4%2FA7IEY33MPH7i8fLXkjk1k0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;696&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6/30 (월)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주말동안 본가에 내려가서 가족들이랑 시간을 보내고, 근처에 있는 여수공항으로 아침 9시 반 비행기를 탔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평일 아침, 그것도 여수공항이라 사람이 거의 없었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탑승 수속을 하는데 사람이 아무도 없어서 들어가니 저 멀리서 직원분이 이리 오시면 됩니다!! 했다. 1분 컷 후 입장..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ePOykN/btsPAl15JTB/Qx7X9pbTL4sLavdZrWk9qK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ePOykN/btsPAl15JTB/Qx7X9pbTL4sLavdZrWk9qK/img.jpg&quot; data-alt=&quot;제주공항&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ePOykN/btsPAl15JTB/Qx7X9pbTL4sLavdZrWk9qK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FePOykN%2FbtsPAl15JTB%2FQx7X9pbTL4sLavdZrWk9qK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;583&quot; height=&quot;777&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;제주공항&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여수에서 출발해서인지 50분? 정도 하늘 구름 구경하니 도착했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도착하고 나서 공항까지 가는 버스를 타는데, 내가 제일 마지막으로 탑승해서 내릴 때 제일 먼저 내렸다 ㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짐도 배낭 하나만 챙겨서 1등으로 제주 땅을 밟았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;651&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wa3gA/btsPB0WuMQV/FkmZogEXkAuUycmyGMkufK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wa3gA/btsPB0WuMQV/FkmZogEXkAuUycmyGMkufK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wa3gA/btsPB0WuMQV/FkmZogEXkAuUycmyGMkufK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWa3gA%2FbtsPB0WuMQV%2FFkmZogEXkAuUycmyGMkufK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;480&quot; height=&quot;441&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;651&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 만나서 같이 밥먹고 카페에서 수다 떨었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dxzuf/btsPAk3cAAc/6bJN7YTQXI3ePi4D5sQDlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dxzuf/btsPAk3cAAc/6bJN7YTQXI3ePi4D5sQDlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dxzuf/btsPAk3cAAc/6bJN7YTQXI3ePi4D5sQDlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDxzuf%2FbtsPAk3cAAc%2F6bJN7YTQXI3ePi4D5sQDlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;459&quot; height=&quot;631&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 버거이름이 피즈버거 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주도는 버거도 다르구만 하면서 들어왔다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nKlWs/btsPAtFJyfu/KSAHiKu9hp51G18hGuGkr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nKlWs/btsPAtFJyfu/KSAHiKu9hp51G18hGuGkr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nKlWs/btsPAtFJyfu/KSAHiKu9hp51G18hGuGkr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnKlWs%2FbtsPAtFJyfu%2FKSAHiKu9hp51G18hGuGkr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;666&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니 제주도까지 와서 버거??? 라고 생각할 수도 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나랑 친구는 둘 다 먹는 것에 큰 관심이 없어서... 아무 곳이나 갔다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맛은 그냥 버거!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu7MAC/btsPBGDSkCy/FGXHX4N1xyetKN9tg5IDCk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu7MAC/btsPBGDSkCy/FGXHX4N1xyetKN9tg5IDCk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu7MAC/btsPBGDSkCy/FGXHX4N1xyetKN9tg5IDCk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu7MAC%2FbtsPBGDSkCy%2FFGXHX4N1xyetKN9tg5IDCk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;539&quot; height=&quot;719&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 점심을 해결하고 카페에서 그동안 못 나눈 수다를 떨다가 이제 나는 남서쪽으로 이동하기 위해 버스를 탔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fxarc/btsPBgTbOD0/RdlwU4JMQ7yavcCz3nebu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fxarc/btsPBgTbOD0/RdlwU4JMQ7yavcCz3nebu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fxarc/btsPBgTbOD0/RdlwU4JMQ7yavcCz3nebu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFxarc%2FbtsPBgTbOD0%2FRdlwU4JMQ7yavcCz3nebu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;545&quot; height=&quot;409&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기는 하늘여행글라이더체험장.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주올패스에서 할인해주는 곳이라 탔는데, 하늘을 자유롭게 나는 게 아니라 고정된 코스를 왕복하고 오는 거라 그냥 그랬다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;손님이 나 혼자라 사장님이 사진이랑 영상 찍어주셨다 ㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nCc38/btsPzWhd4Am/KIWw6q72zkPJtHYMkLKM1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nCc38/btsPzWhd4Am/KIWw6q72zkPJtHYMkLKM1K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nCc38/btsPzWhd4Am/KIWw6q72zkPJtHYMkLKM1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnCc38%2FbtsPzWhd4Am%2FKIWw6q72zkPJtHYMkLKM1K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;775&quot; height=&quot;581&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 근처에 있는 제주항공우주박물관에 갔다. 영어로 JAM 이라고 줄여 부르는 모양&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 항공우주쪽에 관심이 있기도 하고, 박물관도 종종 가는 편이라 들렀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기는 관내에 있는 그림카페라는 곳이랑 묶어서 제주올패스 할인을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4Prat/btsPAAdCs2S/PKt9vAZbrMkkzkiNaKh7sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4Prat/btsPAAdCs2S/PKt9vAZbrMkkzkiNaKh7sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4Prat/btsPAAdCs2S/PKt9vAZbrMkkzkiNaKh7sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4Prat%2FbtsPAAdCs2S%2FPKt9vAZbrMkkzkiNaKh7sK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;585&quot; height=&quot;620&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엘리베이터의 컨셉부터 재밌었던 곳&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XPWIJ/btsPz0w9455/5JYdet6HItK7Rp4lNso2fK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XPWIJ/btsPz0w9455/5JYdet6HItK7Rp4lNso2fK/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; width=&quot;594&quot; height=&quot;792&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XPWIJ/btsPz0w9455/5JYdet6HItK7Rp4lNso2fK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXPWIJ%2FbtsPz0w9455%2F5JYdet6HItK7Rp4lNso2fK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bslY1U/btsPBLFau2D/vzagrs8Hj82Y5wimy3YCCK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bslY1U/btsPBLFau2D/vzagrs8Hj82Y5wimy3YCCK/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bslY1U/btsPBLFau2D/vzagrs8Hj82Y5wimy3YCCK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbslY1U%2FbtsPBLFau2D%2Fvzagrs8Hj82Y5wimy3YCCK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4T0GA/btsPADuAN6k/qaW1VuhkC7k20MsUKwrBt0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4T0GA/btsPADuAN6k/qaW1VuhkC7k20MsUKwrBt0/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4T0GA/btsPADuAN6k/qaW1VuhkC7k20MsUKwrBt0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4T0GA%2FbtsPADuAN6k%2FqaW1VuhkC7k20MsUKwrBt0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림카페에서 항공우주박물관 입장권까지 발급해주고 있어서 먼저 들러서 아이스티 공짜로 마시면서 앉아있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 말 그대로 그림 카페였던 것.. 신기했다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 그냥 신기하기만 해서 좀 앉아 있다가 박물관 관람하러 내려갔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;791&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpzPEE/btsPB2msr4I/x8OeyLTuK8kWgCPde62r1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpzPEE/btsPB2msr4I/x8OeyLTuK8kWgCPde62r1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpzPEE/btsPB2msr4I/x8OeyLTuK8kWgCPde62r1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpzPEE%2FbtsPB2msr4I%2Fx8OeyLTuK8kWgCPde62r1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;791&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;791&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;박물관은 1층은 항공, 2층은 우주를 주제로 전시되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항공 쪽은 그렇게 큰 관심이 없어서 빠르게 훑으려 했는데 재밌는 체험들이 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH81DU/btsPAMETySa/QdBPOGdbxvOiUMjm4OWPi0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH81DU/btsPAMETySa/QdBPOGdbxvOiUMjm4OWPi0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH81DU/btsPAMETySa/QdBPOGdbxvOiUMjm4OWPi0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH81DU%2FbtsPAMETySa%2FQdBPOGdbxvOiUMjm4OWPi0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;522&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 옆의 조종기로 전투기 조종 체험도 해볼 수 있었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 건너편은 초등학생 애기였는데 주변에 사람이 없어서 나도 슬쩍하고 왔다 ㅋㅋㅋㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서울을 둘러볼 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전투기가 건물과 부딪히면 어떻게 되지? 싶어서 해봤는데 폭발하더라.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfzpuE/btsPBndvQWp/nskt37uhHWdkqBurtXCY9k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfzpuE/btsPBndvQWp/nskt37uhHWdkqBurtXCY9k/img.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfzpuE/btsPBndvQWp/nskt37uhHWdkqBurtXCY9k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfzpuE%2FbtsPBndvQWp%2Fnskt37uhHWdkqBurtXCY9k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HwQXb/btsPBfNwAvX/1VlHScxQ2fv2QNEtHVvMJk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HwQXb/btsPBfNwAvX/1VlHScxQ2fv2QNEtHVvMJk/img.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HwQXb/btsPBfNwAvX/1VlHScxQ2fv2QNEtHVvMJk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHwQXb%2FbtsPBfNwAvX%2F1VlHScxQ2fv2QNEtHVvMJk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2층으로 올라왔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전시장에 내 생일을 입력하면 360도 대형 스크린에서 내 별자리가 표시됐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;850&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4yfTt/btsPzXmVJCz/UpYP9YjZIEmBOdNTXSAIK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4yfTt/btsPzXmVJCz/UpYP9YjZIEmBOdNTXSAIK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4yfTt/btsPzXmVJCz/UpYP9YjZIEmBOdNTXSAIK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4yfTt%2FbtsPzXmVJCz%2FUpYP9YjZIEmBOdNTXSAIK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;850&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;850&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우주에 보내는 메시지를 만들어 볼 수 있는 체험이 있었는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 바보 라고 만들어뒀다 ㅋㅋㅋ 귀엽&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;747&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MLREF/btsPBLypyHb/R1i5dOaSQKU3ua3BaicdcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MLREF/btsPBLypyHb/R1i5dOaSQKU3ua3BaicdcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MLREF/btsPBLypyHb/R1i5dOaSQKU3ua3BaicdcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMLREF%2FbtsPBLypyHb%2FR1i5dOaSQKU3ua3BaicdcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;747&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;747&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 오설록 티 뮤지엄에 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉보기에는 작아보였는데 내부는 엄청 넓고 사람도 바글바글했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주 관광객 다 여기 있나 봐;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안에는 오설록 굿즈들, 상품들과 카페가 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;827&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEIY4k/btsPAzMuxFG/GKhoMxnxGRxEKF3YRRCV01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEIY4k/btsPAzMuxFG/GKhoMxnxGRxEKF3YRRCV01/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEIY4k/btsPAzMuxFG/GKhoMxnxGRxEKF3YRRCV01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEIY4k%2FbtsPAzMuxFG%2FGKhoMxnxGRxEKF3YRRCV01%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;827&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;827&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오설록 티는 주변에 많이 주기도 하고 또 나도 받기도 해서 집에 많아서... 제주올패스로 말차아이스크림 무료로 먹을 수 있어서 그것만 창가 1인석에서 먹고 주변 구경하고 왔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맛은 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3U9v/btsPBIhrgOb/CxhLlqWGgUmxClzWDt8I7K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3U9v/btsPBIhrgOb/CxhLlqWGgUmxClzWDt8I7K/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3U9v/btsPBIhrgOb/CxhLlqWGgUmxClzWDt8I7K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3U9v%2FbtsPBIhrgOb%2FCxhLlqWGgUmxClzWDt8I7K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WcyNS/btsPB201ENo/rfHnhXKm3Kk0gmIuq7kYGk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WcyNS/btsPB201ENo/rfHnhXKm3Kk0gmIuq7kYGk/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WcyNS/btsPB201ENo/rfHnhXKm3Kk0gmIuq7kYGk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWcyNS%2FbtsPB201ENo%2FrfHnhXKm3Kk0gmIuq7kYGk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숙소는 산방산 근처의 사계여행이라는 게스트하우스를 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수수료까지 해서 2.7만 원?이었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주변에 편의점과 식당가가 있으면서 값싼 곳을 골랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근처에 6인실 게하가 여기보다 만 원 싼 곳이 있긴 했는데 왠지 같은 방에 사람 있을 것 같고 여기가 시설이 더 깔끔하길래 골랐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데!!! 이 날 게스트하우스 전체를 혼자 썼다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사장님께는 매출이 없어서 안 좋을 것 같아 내색은 안 했지만 나는 너무 좋았다 ㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;814&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UiSee/btsPAzlv1Hw/VKxvPUXEXzuVcKCZhvOuQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UiSee/btsPAzlv1Hw/VKxvPUXEXzuVcKCZhvOuQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UiSee/btsPAzlv1Hw/VKxvPUXEXzuVcKCZhvOuQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUiSee%2FbtsPAzlv1Hw%2FVKxvPUXEXzuVcKCZhvOuQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;814&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;814&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숙소에서 1시간 정도 쉬면서 폰 충전도 하고 나서 저녁을 먹으러 나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주도는 7시에 대부분 식당을 닫기 때문에 후다닥 먹어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사계소희네국수 라는 곳에서 고기국수를 먹었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 외에도 다른 관광객 중년부부가 사장님과 이야기를 나누고 계셨는데 전국 음식 이야기를 하고 계셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 전라도가 상다리가 부러질 정도로 나온다고, 순천이 맛집이 많다고 해서 기분이 좋아졌다 ㅎㅎ 물론 아는 척 안 하고 먹고 바로 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ToJQN/btsPAzTjQsa/wEeYKT9oaQHCoKDcXCTx1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ToJQN/btsPAzTjQsa/wEeYKT9oaQHCoKDcXCTx1K/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ToJQN/btsPAzTjQsa/wEeYKT9oaQHCoKDcXCTx1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FToJQN%2FbtsPAzTjQsa%2FwEeYKT9oaQHCoKDcXCTx1K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qW4s1/btsPBm6H1Ff/LE9ksk1F6n7LHFdfpBNcb1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qW4s1/btsPBm6H1Ff/LE9ksk1F6n7LHFdfpBNcb1/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qW4s1/btsPBm6H1Ff/LE9ksk1F6n7LHFdfpBNcb1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqW4s1%2FbtsPBm6H1Ff%2FLE9ksk1F6n7LHFdfpBNcb1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v4w85/btsPBfNwKJA/s8iXpK9ZmTDHKmFFstmVzk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v4w85/btsPBfNwKJA/s8iXpK9ZmTDHKmFFstmVzk/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5581%;&quot; data-widthpercent=&quot;33.34&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v4w85/btsPBfNwKJA/s8iXpK9ZmTDHKmFFstmVzk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv4w85%2FbtsPBfNwKJA%2Fs8iXpK9ZmTDHKmFFstmVzk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밥을 먹고 산책 겸 산방산 보문사를 다녀오기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진은 갔다 오기 전후 사진인데, 제주 날씨가 엄청 휙휙 바뀌었다. 다녀오니 구름이 산방산 머리를 쓰다듬고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xpxFJ/btsPzWBtjor/SYc6KQDb91H5SubH9p8Gi0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xpxFJ/btsPzWBtjor/SYc6KQDb91H5SubH9p8Gi0/img.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xpxFJ/btsPzWBtjor/SYc6KQDb91H5SubH9p8Gi0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxpxFJ%2FbtsPzWBtjor%2FSYc6KQDb91H5SubH9p8Gi0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btPbVu/btsPCm50axj/AVAJsfVz5NG72uxsX2GIg0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btPbVu/btsPCm50axj/AVAJsfVz5NG72uxsX2GIg0/img.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btPbVu/btsPCm50axj/AVAJsfVz5NG72uxsX2GIg0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtPbVu%2FbtsPCm50axj%2FAVAJsfVz5NG72uxsX2GIg0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 경치가 좋아서 찍어본 사진들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGcZo6/btsPzVo3xug/l8vbL2zPeQHrZdK12wK851/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGcZo6/btsPzVo3xug/l8vbL2zPeQHrZdK12wK851/img.jpg&quot; data-origin-width=&quot;768&quot; data-origin-height=&quot;1024&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;36&quot; style=&quot;width: 35.5814%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGcZo6/btsPzVo3xug/l8vbL2zPeQHrZdK12wK851/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGcZo6%2FbtsPzVo3xug%2Fl8vbL2zPeQHrZdK12wK851%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;768&quot; height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tydFD/btsPCj2wpxV/JW8OZktbmynyNZXPIOkIQ0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tydFD/btsPCj2wpxV/JW8OZktbmynyNZXPIOkIQ0/img.jpg&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot; data-is-animation=&quot;false&quot; style=&quot;width: 63.2558%;&quot; data-widthpercent=&quot;64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tydFD/btsPCj2wpxV/JW8OZktbmynyNZXPIOkIQ0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtydFD%2FbtsPCj2wpxV%2FJW8OZktbmynyNZXPIOkIQ0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;산방산을 갔다가 그 밑의 용머리해안까지 다녀오기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 입장료를 받고 더 깊숙이 다녀올 수 있는데 마감돼서 근처 해안만 봤다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 숙소로 돌아갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H7s0S/btsPBl05Td6/8KzdpxPGB1XJen0Aaokb21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H7s0S/btsPBl05Td6/8KzdpxPGB1XJen0Aaokb21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H7s0S/btsPBl05Td6/8KzdpxPGB1XJen0Aaokb21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH7s0S%2FbtsPBl05Td6%2F8KzdpxPGB1XJen0Aaokb21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;667&quot; height=&quot;839&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1471&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅋㅋ 이거 캡처한 후에 편의점 다녀왔는데 그것까지 하면 딱 2만 보 채웠을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루 11킬로...&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rfVgp/btsPB2fD5Sp/9Fiv73WxZdG184gToyJHM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rfVgp/btsPB2fD5Sp/9Fiv73WxZdG184gToyJHM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rfVgp/btsPB2fD5Sp/9Fiv73WxZdG184gToyJHM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrfVgp%2FbtsPB2fD5Sp%2F9Fiv73WxZdG184gToyJHM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;687&quot; height=&quot;515&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㅋㅋㅋ 제주도까지 와서도 코테는 못 참지......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 1일 차가 끝났다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상</category>
      <category>글라이더</category>
      <category>뚜벅이</category>
      <category>배낭여행</category>
      <category>산방산</category>
      <category>오설록</category>
      <category>용머리해안</category>
      <category>제주공항</category>
      <category>제주도</category>
      <category>제주도여행</category>
      <category>제주항공우주박물관</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/238</guid>
      <comments>https://promisingmoon.tistory.com/238#entry238comment</comments>
      <pubDate>Sun, 27 Jul 2025 18:39:51 +0900</pubDate>
    </item>
    <item>
      <title>[제주도 여행] 0일 차</title>
      <link>https://promisingmoon.tistory.com/237</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취업했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 첫 출근 전까지 하루 빼고 매일 약속 잡고 놀러 다녔다..... ㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 1박 2일로 제주도나 다녀올까 싶어서 혼자 월화 다녀왔다. 뭔가 일본은 너무 멀고 비싸고, 국내는 여행 기분이 안 나서 그 사이 어딘가로 제주도로 결정했다 ㅋㅋㅋ 마침 친구가 제주 한 달 살기를 하고 있어서 겸사겸사 밥 한 번 먹기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남서쪽인 안덕/대정쪽 위주 + 제주시 공항 주변을 놀다가 왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 경비는 27만 원 들었다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-sheets-baot=&quot;1&quot; data-sheets-root=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항공권&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;84,100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;숙박비 (1박)&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;26,970&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관광패스 (제주올패스 24시간권)&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;11,900&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관광&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;85,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;식비&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;40,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;교통&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;16,950&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총액&lt;/td&gt;
&lt;td style=&quot;text-align: right;&quot;&gt;265,820&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항공권은 본가 내려가서 가족들과 친척들, 고향친구들을 보고 제주로 넘어간 거라 여수공항-&amp;gt;제주공항, 제주공항-&amp;gt;김포공항으로 다녀왔다. 스카이스캐너에서 젤 싼걸루 샀다. 아침 9시 반 비행기 타고 다음날에 밤 9시 비행기로 복귀했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숙소는 4인실 게스트하우스로 다녀왔는데 숙소 전체에 손님이 나밖에 없었다 ㅎㅎㅎㅎ 내가 쓰는 토스뱅크카드가 아고다에서 10% 할인을 해줘서 이걸로 할인받아 결제했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주올패스는 제주도의 관광패스인데, 주요 관광지, 카페, 식당, 체험장에서 할인이나 증정을 해준다. 나는 24시간권을 구매해서 5곳을 다녀왔는데 총 8만 원을 할인받았다..!!! 제주올패스 웹사이트에서는 15,800원인데, 카카오톡 채널에서는 30% 할인해서 11,900원으로 구매할 수 있다. 참고하길!! 대신 관광지 간 1시간 텀을 두고 다음 관광지를 이용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뚜벅이로 버스+택시 타고 다녔는데, 제주도는 택시가 비싸서 딱 2번만 탔다. 택시비로는 11,000원, 버스비로 5,750원 쓴 듯! 제주도는 제주버스정보시스템 사이트에서 각 버스번호 별로 시간표가 정해져 있으니 계획을 세울 때 참고하면 좋다. 안 그러면 배차간격이 30분, 1시간 이런 버스 놓쳐서 기다리기만 할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1일 차&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제주공항 -&amp;gt; 친구랑 식사/카페 -&amp;gt; 행글라이더 체험 -&amp;gt; 그림카페/항공우주박물관 -&amp;gt; 오셜록 -&amp;gt; 숙소 -&amp;gt; 산방산/보문사 -&amp;gt; 용머리해안&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2일 차&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;송악산 -&amp;gt; 카트체험 -&amp;gt; 돌고래투어 -&amp;gt; 승마체험 -&amp;gt; 넥슨컴퓨터박물관 -&amp;gt; 동문시장 -&amp;gt; 용두암 -&amp;gt; 제주공항&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 야무지게 다녔다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구가 너 행군하냐고 ㅋㅋㅋㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이틀 동안 4만보 걸었자나&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상</category>
      <category>제주도</category>
      <category>제주도 뚜벅이</category>
      <category>제주도 여행</category>
      <category>제주도 혼자</category>
      <category>혼자 여행</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/237</guid>
      <comments>https://promisingmoon.tistory.com/237#entry237comment</comments>
      <pubDate>Wed, 2 Jul 2025 16:19:44 +0900</pubDate>
    </item>
    <item>
      <title>TIL #132 : Arrays.fill의 얕은 복사로 인한 참조 공유 이슈</title>
      <link>https://promisingmoon.tistory.com/236</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요번 거는 간단한거.. 근데 뭐가 문제인지 모르고 좀 헤맸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 알고리즘 풀 때 리스트랑 배열을 같이 쓰는 것을 안 좋아하는데, 오늘은 무슨 바람이 불었는지 이렇게 풀고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749878717502&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt;[] graph = new ArrayList[3];
        Arrays.fill(graph, new ArrayList&amp;lt;&amp;gt;());
        
        for (int i = 0; i &amp;lt; 3; i++) {
            graph[i].add(i);
        }

        System.out.println(Arrays.toString(graph));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 List&amp;lt;Integer&amp;gt;[] 로 이차원 배?열을 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Arrays.fill로 각 배열 요소에 ArrayList를 넣어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 (원래는 입력 받아 넣겠지만 패스하고) 반복문으로 각 인덱스에 해당 인덱스를 리스트에 넣도록 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749878894730&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[[0], [1], [2]]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상 출력 결과는 위일 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749878905680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[[0, 1, 2], [0, 1, 2], [0, 1, 2]]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결과는 이랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 이렇게 for 문으로 순서대로 숫자를 넣으니 바로 리스트가 같은 인스턴스로 들어갔구나 유추가 쉬웠지만, 내가 문제 풀 때는 입력이 다양했어서 알아채기가 힘들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749879003507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Arrays {
    public static void fill(Object[] a, Object val) {
        for (int i = 0, len = a.length; i &amp;lt; len; i++)
            a[i] = val;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Arrays.fill 부분이었다. 각 요소별 넣어줄 줄 알았는데... 이 멍청이 ㅠㅠ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 배열이나 컬렉션 같이 주소를 넣어주는 거는 조심히 써야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749879179135&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; graph = new ArrayList&amp;lt;&amp;gt;();
        for (int i = 0; i &amp;lt; 3; i++) {
            graph.add(new ArrayList&amp;lt;&amp;gt;());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 해왔던 대로 앞으로는 그냥 이렇게 List&amp;lt;LIst&amp;lt;&amp;gt;&amp;gt; 쓰면 되겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 건데 어이없어서 글로 써본다...&lt;/p&gt;</description>
      <category>TIL</category>
      <category>array</category>
      <category>Arrays.fill</category>
      <category>Collection</category>
      <category>Java</category>
      <category>배열</category>
      <category>자바</category>
      <category>컬렉션</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/236</guid>
      <comments>https://promisingmoon.tistory.com/236#entry236comment</comments>
      <pubDate>Sat, 14 Jun 2025 14:35:50 +0900</pubDate>
    </item>
    <item>
      <title>TIL #131 : Spring Kafka에서 @KafkaListener 기본 컨테이너 팩토리 설정 오류 해결</title>
      <link>https://promisingmoon.tistory.com/235</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Kafka를 이용해서 아웃박스 패턴을 간단히 만들어보고 있었다. 도커로 카프카를 띄우고, 프로듀서가 메시지를 잘 전송하는 것을 확인했는데 컨슈머가 브로커와 연결을 못하고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 컴포즈로 카프카 실행하기&lt;/h3&gt;
&lt;pre id=&quot;code_1749433609898&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;services:
  kafka:
    image: bitnami/kafka:3.7.1
    container_name: kafka
    ports:
      - &quot;19092:19092&quot;
    environment:
      - KAFKA_KRAFT_CLUSTER_ID=A616ADF4-FA94-440F-BE52-CC89ED7EC507
      - KAFKA_CFG_NODE_ID=0
      - KAFKA_CFG_PROCESS_ROLES=controller,broker
      - KAFKA_CFG_LISTENERS=INTERNAL://:9092,EXTERNAL://:19092,CONTROLLER://:9093
      - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9092,EXTERNAL://localhost:19092
      - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
      - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER
      - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL
      - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@localhost:9093
      - ALLOW_PLAINTEXT_LISTENER=yes
    volumes:
      - kafka_data:/bitnami/kafka

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    ports:
      - &quot;8081:8080&quot;
    depends_on:
      - kafka
    environment:
      - DYNAMIC_CONFIG_ENABLED=true
      - KAFKA_CLUSTERS_0_NAME=local
      - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092
      - KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=PLAINTEXT

volumes:
  kafka_data:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컴포즈는 위와 같다. 사실 설정을 잘 한건지 아직도 모르겠다. 계속 공부하는데 어려워..ㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 간략하게 설명하자면, KAFKA_CFG_ADVERTISED_LISTENERS는 advertised.listeners 설정으로, 클라이언트에게 브로커의 접속 주소를 알려주는 설정이다. 그리고 EXTERNAL://localhost:19092는 내부 브로커 간이 아닌, 외부 클라이언트는 여기로 접속하라는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 클라이언트는 이를 통해 localhost:19092로 요청을 보내게 된다. 즉, 같은 로컬이면 상관 없겠지만, 다른 네트워크라면 못 찾게 되니 실제 서버를 띄울 때는 도메인을 입력해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 나는 도커를 사용했고, 컨테이너의 포트를 19092:19092로 외부-내부를 연결해뒀으니, 로컬 컴퓨터에서 19092로 요청을 보내면 이 카프카 컨테이너로 요청이 가고, 컨테이너는 이를 내부의 19092 포트로 포워딩한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 KAFKA_CFG_LISTENERS=EXTERNAL://:19092 설정은 카프카 브로커가 실제로 수신할 주소이다. 이때 :19092는 0.0.0.0:19092를 줄인 것으로, 모든 IP에 대해서 포트만 19092 이기만 하면 받겠다는 뜻.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 카프카로 컨슈머 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749436425217&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.ContainerProperties.AckMode;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.kafka.support.ExponentialBackOffWithMaxRetries;
import org.springframework.kafka.support.serializer.JsonSerializer;

@Configuration
@EnableKafka
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&amp;lt;String, Object&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:19092&quot;); // 브로커 접속 주소

        props.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;payment-group&quot;); // 컨슈머 그룹

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); // 키는 문자열
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); // 값은 문자열

        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 수동 커밋

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(props);
    }

    // 여기 !!!
    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, Object&amp;gt; paymentRefundListenerContainerFactory(
        KafkaTemplate&amp;lt;Object, Object&amp;gt; kafkaDlqTemplate) {

        ConcurrentKafkaListenerContainerFactory&amp;lt;String, Object&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());

        // 수동 커밋 설정
        factory.getContainerProperties().setAckMode(AckMode.MANUAL);

        // 에러 핸들러 + 재시도 5회 + DLQ 설정
        DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(
            kafkaDlqTemplate,
            (record, ex) -&amp;gt; new TopicPartition(record.topic() + &quot;.DLT&quot;, record.partition())
        );

        // 지수 백오프 설정: 초기 1초, 최대 5번 (4회 재시도 + 마지막 1회)
        ExponentialBackOffWithMaxRetries backOff = new ExponentialBackOffWithMaxRetries(4); // 4회 재시도
        backOff.setInitialInterval(1000L); // 1초
        backOff.setMultiplier(2.0); // x2씩 증가 &amp;rarr; 1s, 2s, 4s, 8s
        backOff.setMaxInterval(10000L); // 최대 10초

        factory.setCommonErrorHandler(new DefaultErrorHandler(recoverer, backOff));

        return factory;
    }

    @Bean
    public KafkaTemplate&amp;lt;Object, Object&amp;gt; kafkaDlqTemplate() {
        Map&amp;lt;String, Object&amp;gt; props = new HashMap&amp;lt;&amp;gt;();

        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:19092&quot;);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);

        return new KafkaTemplate&amp;lt;&amp;gt;(new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(props));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 전부 다 볼 필요는 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번째 빈에 여기 !!! 라고 적어둔 곳의 paymentRefundListenerContainerFactory라는 메서드명만 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 빈 등록 시 메서드명을 기준으로 빈이름이 등록된다. 그리고 빈 조회 시 타입을 우선으로 찾고, 같은 타입의 빈이 여러 개면 빈 이름으로 찾는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는, 스프링 카프카가 제공하는 @KafkaListener 어노테이션은 따로 속성으로 containerFactory 속성에 팩토리명을 지정하지 않으면 기본값으로 &quot;kafkaListenerContainerFactory&quot;를 조회한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749437846149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INFO [ntainer#0-0-C-1] org.apache.kafka.clients.NetworkClient   : [Consumer clientId=consumer-payment-group-1, groupId=payment-group] Node -1 disconnected.
WARN [ntainer#0-0-C-1] org.apache.kafka.clients.NetworkClient   : [Consumer clientId=consumer-payment-group-1, groupId=payment-group] Connection to node -1 (localhost/127.0.0.1:9092) could not be established. Node may not be available.
WARN [ntainer#0-0-C-1] org.apache.kafka.clients.NetworkClient   : [Consumer clientId=consumer-payment-group-1, groupId=payment-group] Bootstrap broker localhost:9092 (id: -1 rack: null) disconnected&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-09 오전 11.57.51.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx2TIA/btsOs38kRO8/9it3XuFcNckxETxixUZgnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx2TIA/btsOs38kRO8/9it3XuFcNckxETxixUZgnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx2TIA/btsOs38kRO8/9it3XuFcNckxETxixUZgnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx2TIA%2FbtsOs38kRO8%2F9it3XuFcNckxETxixUZgnk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;146&quot; data-filename=&quot;스크린샷 2025-06-09 오전 11.57.51.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 로그가 뜬다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 카프카의 자동 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 localhost:9092를 설정해준 적이 없다. 하지만 여기로 요청하는 이유는 스프링 카프카가 kafkaListenerContainerFactory가 없으면 스프링이 자체적으로 만들어주기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749443253744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.boot.autoconfigure.kafka;

// ...

@AutoConfiguration
@ConditionalOnClass(KafkaTemplate.class)
@EnableConfigurationProperties(KafkaProperties.class)
@Import({ KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class })
@ImportRuntimeHints(KafkaAutoConfiguration.KafkaRuntimeHints.class)
public class KafkaAutoConfiguration {

	private final KafkaProperties properties;

	KafkaAutoConfiguration(KafkaProperties properties) {
		this.properties = properties;
	}

	// ...

	@Bean
	@ConditionalOnMissingBean(KafkaTemplate.class)
	public KafkaTemplate&amp;lt;?, ?&amp;gt; kafkaTemplate(ProducerFactory&amp;lt;Object, Object&amp;gt; kafkaProducerFactory,
			ProducerListener&amp;lt;Object, Object&amp;gt; kafkaProducerListener,
			ObjectProvider&amp;lt;RecordMessageConverter&amp;gt; messageConverter) {
		PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
		KafkaTemplate&amp;lt;Object, Object&amp;gt; kafkaTemplate = new KafkaTemplate&amp;lt;&amp;gt;(kafkaProducerFactory);
		messageConverter.ifUnique(kafkaTemplate::setMessageConverter);
		map.from(kafkaProducerListener).to(kafkaTemplate::setProducerListener);
		map.from(this.properties.getTemplate().getDefaultTopic()).to(kafkaTemplate::setDefaultTopic);
		map.from(this.properties.getTemplate().getTransactionIdPrefix()).to(kafkaTemplate::setTransactionIdPrefix);
		map.from(this.properties.getTemplate().isObservationEnabled()).to(kafkaTemplate::setObservationEnabled);
		return kafkaTemplate;
	}

	@Bean
	@ConditionalOnMissingBean(ProducerListener.class)
	public LoggingProducerListener&amp;lt;Object, Object&amp;gt; kafkaProducerListener() {
		return new LoggingProducerListener&amp;lt;&amp;gt;();
	}

	// 여기 !!!
	@Bean
	@ConditionalOnMissingBean(ConsumerFactory.class)
	DefaultKafkaConsumerFactory&amp;lt;?, ?&amp;gt; kafkaConsumerFactory(KafkaConnectionDetails connectionDetails,
			ObjectProvider&amp;lt;DefaultKafkaConsumerFactoryCustomizer&amp;gt; customizers) {
		Map&amp;lt;String, Object&amp;gt; properties = this.properties.buildConsumerProperties();
		applyKafkaConnectionDetailsForConsumer(properties, connectionDetails);
		DefaultKafkaConsumerFactory&amp;lt;Object, Object&amp;gt; factory = new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(properties);
		customizers.orderedStream().forEach((customizer) -&amp;gt; customizer.customize(factory));
		return factory;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaAutoConfiguration 클래스는 별도의 ConsumerFactory가 없으면 KafkaConsumerFactory 빈으로 등록해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749444359478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ConfigurationProperties(&quot;spring.kafka&quot;)
public class KafkaProperties {

	/**
	 * List of host:port pairs to use for establishing the initial connections to the
	 * Kafka cluster. Applies to all components unless overridden.
	 */
	private List&amp;lt;String&amp;gt; bootstrapServers = new ArrayList&amp;lt;&amp;gt;(Collections.singletonList(&quot;localhost:9092&quot;));

	// ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 기본 설정은 KafkaProperties를 기반으로 하는데, spring.kafka.*를 기반으로 하고, 부트스트랩 서버의 기본값은 localhost:9092이다. 이래서 요청을 여기로 보냈던 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749444492455&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@ConditionalOnMissingBean(name = &quot;kafkaListenerContainerFactory&quot;)
ConcurrentKafkaListenerContainerFactory&amp;lt;?, ?&amp;gt; kafkaListenerContainerFactory(
        ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
        ObjectProvider&amp;lt;ConsumerFactory&amp;lt;Object, Object&amp;gt;&amp;gt; kafkaConsumerFactory,
        ObjectProvider&amp;lt;ContainerCustomizer&amp;lt;Object, Object, ConcurrentMessageListenerContainer&amp;lt;Object, Object&amp;gt;&amp;gt;&amp;gt; kafkaContainerCustomizer) {
    ConcurrentKafkaListenerContainerFactory&amp;lt;Object, Object&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
    configurer.configure(factory, kafkaConsumerFactory
        .getIfAvailable(() -&amp;gt; new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(this.properties.buildConsumerProperties())));
    kafkaContainerCustomizer.ifAvailable(factory::setContainerCustomizer);
    return factory;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 KafkaAutoConfiguration은 KafkaAnnotationDrivenConfiguration를 임포트 하고 있는데, 이 클래스 내에서는&amp;nbsp; kafkaContainerFactory를 기반으로 kafkaListenerContainerFactory를 빈 등록하는 코드가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749444645207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
@Repeatable(KafkaListeners.class)
public @interface KafkaListener {

	/**
	 * The bean name of the {@link org.springframework.kafka.config.KafkaListenerContainerFactory}
	 * to use to create the message listener container responsible to serve this endpoint.
	 * &amp;lt;p&amp;gt;
	 * If not specified, the default container factory is used, if any. If a SpEL
	 * expression is provided ({@code #{...}}), the expression can either evaluate to a
	 * container factory instance or a bean name.
	 * @return the container factory bean name.
	 */
	String containerFactory() default &quot;&quot;;
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-09 오후 1.56.57.png&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qTaXb/btsOsWa2F9D/WYMHDxOhCZGOipEiMfELKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qTaXb/btsOsWa2F9D/WYMHDxOhCZGOipEiMfELKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qTaXb/btsOsWa2F9D/WYMHDxOhCZGOipEiMfELKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqTaXb%2FbtsOsWa2F9D%2FWYMHDxOhCZGOipEiMfELKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1466&quot; height=&quot;366&quot; data-filename=&quot;스크린샷 2025-06-09 오후 1.56.57.png&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 @KafkaListener는 containerFactory를 따로 지정하지 않으면 기본값으로 kafkaListenerContainerFactory를 사용한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;방법 1&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아까 위의 빈 메서드명을 paymentRefundListenerContainerFactory에서 kafkaListenerContainerFactory로 변경해 주면 @KafkaListener가 해당 컨테이너 팩토리를 찾기 때문에 정상 동작한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;방법 2&lt;/h3&gt;
&lt;pre id=&quot;code_1749445441147&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@KafkaListener(
    // ...
    containerFactory = &quot;paymentRefundListenerContainerFactory&quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 카프카 리스너에 컨테이너 팩토리를 명시적으로 빈 이름으로 작성해주면 된다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>containerfactory</category>
      <category>Kafka</category>
      <category>kafkaautoconfiguration</category>
      <category>spring kafka</category>
      <category>스프링</category>
      <category>스프링 카프카</category>
      <category>자바</category>
      <category>카프카</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/235</guid>
      <comments>https://promisingmoon.tistory.com/235#entry235comment</comments>
      <pubDate>Mon, 9 Jun 2025 14:09:41 +0900</pubDate>
    </item>
    <item>
      <title>TIL #130 : 컨트롤러 테스트에서 커스텀 UserDetails 인증 객체 사용하기</title>
      <link>https://promisingmoon.tistory.com/234</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WithMockUser는 기본적인 인증만 제공한다. 하지만 우리는 도메인에 맞는 커스텀 UserDetails를 사용하는 상황이고, 이럴 경우 @WithMockUser만으로는 인증 객체의 테스트가 어렵다. 이 글에서는 그런 상황에서 커스텀 인증 어노테이션을 만들어 중복 없이 테스트하는 방법을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1744273535424&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/users&quot;)
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping
    public UserRes getUser(@AuthenticationPrincipal UserDetails userDetails) {
        return userService.getUser(userDetails.getUsername());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; getUser 메서드처럼 &lt;b&gt;@AuthenticationPrincipal UserDetails userDetails를&lt;/b&gt; 받는 컨트롤러 메서드가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 컨트롤러 메서드에서 &lt;b&gt;UserDetails&lt;/b&gt; 인터페이스만 받는다면, &lt;b&gt;@WithMockUser&lt;/b&gt;만으로도 충분하다.&amp;nbsp;&amp;nbsp;&lt;br /&gt;하지만 &lt;b&gt;UserDetailsImpl처럼&lt;/b&gt; 커스텀 인증 객체를 사용하고, 이 객체 안의 추가 필드나 로직을 테스트하고 싶다면 &lt;b&gt;@WithMockUser&lt;/b&gt;로는 부족하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 따로 @WithMockUser와 비슷한 역할을 하는 커스텀 어노테이션을 만들어주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 코드 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 User, UserDetailsImpl 코드이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744274018850&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;tb_user&quot;)
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String password;
    private String role;

    @CreatedDate
    private LocalDateTime createdAt;

    public User(String name, String password) {
        this.name = name;
        this.password = password;
        this.role = &quot;ROLE_USER&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744274049767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public record UserDetailsImpl(User user) implements UserDetails {

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return List.of(user::getRole);
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getName();
    }
    
    // 이후 생략 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 간단하게 작성했다. 사실 이 정도면 @WithMockUser 써도 된다 ㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 보여주기 전에, 컨트롤러 슬라이스 테스트를 위한 밑작업을 해두었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744274445968&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface UserTest {

    String TEST_USER_NAME = &quot;name01&quot;;
    String TEST_USER_PASSWORD = &quot;1234&quot;;
    String TEST_USER_ROLE = &quot;ROLE_USER&quot;;
    LocalDateTime TEST_USER_CREATED_AT = LocalDateTime.now();

    User TEST_USER = new User(TEST_USER_NAME, TEST_USER_PASSWORD);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 테스트용 인터페이스를 따로 만들어두는 편이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어두고, 테스트 객체에서는 인터페이스를 구현하여 사용한다. 클래스가 아닌 인터페이스인 이유는, 여러 도메인을 implements 받을 수 있어서이다. 인터페이스의 본연의 역할과는 다르게 사용하긴 하지만.. 아직까지 더 나은 방식을 못 찾아서 일단 쓰는 중,,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744274564648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

public abstract class BaseControllerTest {

    @Autowired
    protected WebApplicationContext context;

    protected MockMvc mockMvc;

    @BeforeEach
    void setUpMockMvc() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 컨트롤러 테스트용으로 공통 추상 클래스를 만들어서 웹 컨텍스트와 MockMvc를 상속하여 재사용하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 작업 끝!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨트롤러 테스트 코드 - 변경 전&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1744274241915&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// ... 

@WebMvcTest(controllers = {UserController.class})
class UserControllerTest extends BaseControllerTest implements UserTest {

    @Autowired
    private ObjectMapper objectMapper;
    @MockitoBean
    private UserService userService;

    @BeforeEach
    void setUp() {
        UserDetails userDetails = new UserDetailsImpl(TEST_USER);
        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
        Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        securityContext.setAuthentication(auth);
    }

    @Test
    @DisplayName(&quot;유저 조회 테스트&quot;)
    void get_authenticated_user_test() throws Exception {
        // given
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserRes res = new UserRes(userDetails.getUsername(), TEST_USER_ROLE);
        given(userService.getUser(anyString())).willReturn(res);

        // when
        ResultActions perform = mockMvc.perform(get(&quot;/users&quot;)
            .contentType(MediaType.APPLICATION_JSON)
        );

        // then
        perform.andExpectAll(status().isOk(),
            jsonPath(&quot;$.name&quot;).value(res.name()),
            jsonPath(&quot;$.role&quot;).value(res.role())
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드는 컨트롤러 슬라이스 테스트다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WebMvcTest는 컨트롤러 슬라이스 테스트를 할 수 있도록 도와주는 어노테이션이다. 전체 빈을 로딩하지 않고, 컨트롤러 관련 빈만 불러오기 때문에 테스트 속도가 빠르다. 그리고 컨트롤러도 테스트 대상인 UserController 만을 대상으로 지정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744274358502&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() {
    UserDetails userDetails = new UserDetailsImpl(TEST_USER);
    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
    Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    securityContext.setAuthentication(auth);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 보면&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;UserDetails 생성&lt;/li&gt;
&lt;li&gt;SecurityContext 생성&lt;/li&gt;
&lt;li&gt;UserDetails 기반으로 Authentication 객체 생성&lt;/li&gt;
&lt;li&gt;SecurityContext 내 인증 처리&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 인증 처리를 해주고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금은 이 컨트롤러 하나지만, 나중에 컨트롤러가 늘어날 때마다 중복 코드가 만들어지게 된다. 그래서 이러한 중복을 없애기 위한 방법이 있다. 바로 &lt;b&gt;@WithMockUser&lt;/b&gt;를 쓰는 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744277536153&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@WebMvcTest(controllers = {UserController.class})
@WithMockUser(username = &quot;hi&quot;) // 여기 !!!
class UserControllerTest extends BaseControllerTest implements UserTest {

    @Autowired
    private ObjectMapper objectMapper;
    @MockitoBean
    private UserService userService;
    
    // setUp 제거 !!!

    @Test
    @DisplayName(&quot;유저 조회 테스트&quot;)
    void get_authenticated_user_test() throws Exception {
        // given
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserRes res = new UserRes(userDetails.getUsername(), TEST_USER_ROLE);
        given(userService.getUser(anyString())).willReturn(res);

        // when
        ResultActions perform = mockMvc.perform(get(&quot;/users&quot;)
            .contentType(MediaType.APPLICATION_JSON)
        );

        // then
        perform.andExpectAll(status().isOk(),
            jsonPath(&quot;$.name&quot;).value(res.name()),
            jsonPath(&quot;$.role&quot;).value(res.role())
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 이렇게 해도 테스트는 통과한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744277808608&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;유저 조회 테스트&quot;)
void get_authenticated_user_test() throws Exception {
    UserDetailsImpl userDetails = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    Assertions.assertThat(userDetails.user().getCreatedAt()).isNull();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 &lt;b&gt;커스텀 UserDetails&lt;/b&gt;의 필드를 조회해야할 때는 사용이 불가능하다. 또 컨트롤러에서 &lt;b&gt;커스텀 UserDetails&lt;/b&gt;를 받을 때도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 커스텀 어노테이션을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 어노테이션 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1744274950374&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.test.context.support.WithSecurityContext;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

    String name() default UserTest.TEST_USER_NAME;

    String password() default UserTest.TEST_USER_PASSWORD;

    String role() default UserTest.TEST_USER_ROLE;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 어노테이션을 만든다. 나는 &lt;b&gt;@WithMockCustomUser이라고&lt;/b&gt; 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744275011205&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory&amp;lt;WithMockCustomUser&amp;gt;, UserTest {

    @Override
    public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
        User user = new User(customUser.name(), customUser.password());
        UserDetails userDetails = new UserDetailsImpl(user);
        Authentication auth = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());

        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(auth);

        return context;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 SecurityContextFactory를 커스텀으로 만들어주면 된다. 내부 로직은 위와 비슷하게, customUser로부터 User를 만들어서 인증 처리한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744275102310&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.security.test.context.support;

import java.lang.annotation.Annotation;
import org.springframework.security.core.context.SecurityContext;

public interface WithSecurityContextFactory&amp;lt;A extends Annotation&amp;gt; {
    SecurityContext createSecurityContext(A annotation);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WithSecurityContextFactory&amp;lt;A&amp;gt;에서 A 제네릭은 앞서 만든 WithMockCustomUser 어노테이션을 넣어주면 된다. 어노테이션 밖에 못 들어간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;컨트롤러 테스트 코드 - 변경 후&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1744275289091&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// ... 

@WebMvcTest(controllers = {UserController.class})
@WithMockCustomUser(name = &quot;hi&quot;) // 여기 1 !!! 
class UserControllerTest extends BaseControllerTest implements UserTest {

    @Autowired
    private ObjectMapper objectMapper;
    @MockitoBean
    private UserService userService;

    @Test
    @DisplayName(&quot;유저 조회 테스트&quot;)
    void get_authenticated_user_test() throws Exception {
        // given
        // 여기 2 !!!
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        UserRes res = new UserRes(userDetails.getUsername(), TEST_USER_ROLE, TEST_USER_CREATED_AT);
        given(userService.getUser(anyString())).willReturn(res);

        // when
        ResultActions perform = mockMvc.perform(get(&quot;/users&quot;).contentType(MediaType.APPLICATION_JSON));

        // then
        perform.andExpectAll(status().isOk(),
            jsonPath(&quot;$.name&quot;).value(res.name()),
            jsonPath(&quot;$.role&quot;).value(res.role()),
            jsonPath(&quot;$.createdAt&quot;).value(res.createdAt().toString())
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 아까 봤었던 컨트롤러에 적용해 보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러 클래스에 @WithMockCustomUser(name = &quot;hi&quot;) 어노테이션이 붙었다. 이를 통해 커스텀 UserDetails에 접근할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>controller</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>Test</category>
      <category>User</category>
      <category>UserDetails</category>
      <category>단위테스트</category>
      <category>슬라이스테스트</category>
      <category>테스트</category>
      <category>통합테스트</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/234</guid>
      <comments>https://promisingmoon.tistory.com/234#entry234comment</comments>
      <pubDate>Thu, 10 Apr 2025 19:05:54 +0900</pubDate>
    </item>
    <item>
      <title>TIL #129 : 구글 번역 &amp;rarr; next-intl 전환으로 렌더링 속도 70% 개선 (2.34s &amp;rarr; 0.71s)</title>
      <link>https://promisingmoon.tistory.com/233</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 구글 번역 기반의 느리고 문단이 깨지는 방식을 next-intl 도입으로 렌더링 평균 2.34초 &amp;rarr; 0.71초로 70% 개선한 적용하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인턴으로 입사한 직후, 대표님으로부터 받은 첫 업무는 서비스 내 영문 페이지의 성능 개선이었다. 기존에는 별도의 영문 페이지가 존재하지 않았고, 사용자가 전환 버튼을 누르면 국문 페이지에 구글 번역 스크립트를 삽입하여 클라이언트에서 전체 번역을 수행하는 방식이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1506&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size16&quot;&gt;이 방식은 국문 페이지가 먼저 렌더링된 뒤, 번역 스크립트가 로드되고 번역 요청이 수행된 후에야 화면이 바뀌는 구조라, 사용자 입장에서는 약 2초간 한글 화면을 계속 보게 되어 이질감을 느낄 수 있었다. 또한 번역 도중 줄바꿈이 깨지거나, 마우스 호버 시 구글 번역 UI가 노출되는 등의 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1506&quot; data-start=&quot;1335&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1766&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;성능 개선을 위한 기준 수립을 위해, localhost 환경에서 가장 단순한 페이지(이미지 1개, DB 요청 없음)를 선정하여 테스트를 진행했다. HTTPS 환경에서는 SSL/TLS 핸드셰이크에 의한 초기 지연이 발생하므로, 변경 전후를 모두 로컬에서 테스트했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1766&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1766&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;이후 캐시 비우기 및 강력 새로고침을 10회 반복하였고, MutationObserver를 활용해 텍스트가 마지막으로 변경된 시점을 1초 디바운스로 측정하여 평균 렌더링 시간을 구했다.&lt;/p&gt;
&lt;p data-end=&quot;1766&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744014667373&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useEffect } from 'react'

const GAP = 1000 // 디바운스 기준 시간 (ms)

export default function TranslateObserver() {
  useEffect(() =&amp;gt; {
    const target = document.getElementById('observe-target') // 감지할 최상위 DOM 요소
    if (!target) return

    let finalTimer: NodeJS.Timeout | null = null
	
    // 텍스트 변화 감지 &amp;rarr; 변화 이후 GAP 동안 추가 변화가 없으면 최종 반영 시점으로 판단
    const observer = new MutationObserver(() =&amp;gt; {
      if (finalTimer) clearTimeout(finalTimer)

      finalTimer = setTimeout(() =&amp;gt; {
        const timestamp = performance.now() - GAP // GAP만큼 보정한 최종 반영 시각
        console.log(`최종 텍스트 반영 시점: ${timestamp.toFixed(2)}ms`)
        observer.disconnect()
      }, GAP)
    })
	
    // target 이하 모든 자식 노드의 텍스트/구조 변경 감지
    observer.observe(target, {
      subtree: true,
      characterData: true,
      childList: true,
    })

    console.log('번역 감지 시작')

    return () =&amp;gt; {
      if (finalTimer) clearTimeout(finalTimer)
      observer.disconnect()
    }
  }, [])

  return null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744093059276&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// layout.tsx

// ...

export default async function RootLayout({ children, params }: RootLayoutProps) {

  return (
    &amp;lt;html lang={locale}&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;Header /&amp;gt;
        &amp;lt;main id={'observe-target'}&amp;gt;{children}&amp;lt;/main&amp;gt;
        &amp;lt;TranslateObserver /&amp;gt; // 타깃(observe-target)보다 아래에.
        &amp;lt;Footer /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1766&quot; data-start=&quot;1511&quot; data-ke-size=&quot;size16&quot;&gt;next.js 자체의 렌더링이 아니라 외부 번역 API로 DOM 값이 변경되고, 번역 시간도 길어서 디바운스 시간을 길게 주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1840.00ms&lt;/li&gt;
&lt;li&gt;1996.70ms&lt;/li&gt;
&lt;li&gt;2127.30ms&lt;/li&gt;
&lt;li&gt;2245.60ms&lt;/li&gt;
&lt;li&gt;2882.30ms&lt;/li&gt;
&lt;li&gt;2197.20ms&lt;/li&gt;
&lt;li&gt;2531.90ms&lt;/li&gt;
&lt;li&gt;2456.70ms&lt;/li&gt;
&lt;li&gt;2568.90ms&lt;/li&gt;
&lt;li&gt;2576.30ms&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 : 2342.29ms (2.34초)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next-intl 국제화 라이브러리를 도입하고 Accept-Language 기반 콘텐츠 협상으로 첫 접속 시 자동으로 해당 언어로 라우팅 되도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;국제화(i18n)/지역화(l10n)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;국제화 (i18n, internationalization)&lt;/b&gt; : 소프트웨어를 다양한 언어와 지역에 맞게 쉽게 현지화할 수 있도록 구조화하는 과정이다. i18n인 이유는 i와 n 사이에 18글자가 있기 때문. 쿠버네티스가 k8s라고 적는 이유와 동일하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지역화 (l10n, localization)&lt;/b&gt; : 국제화된 소프트웨어에 실제 언어, 날짜 형식, 통화, 문화적 요소 등을 적용하여 특정 지역 또는 언어권에 맞게 조정하는 과정이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 협상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;콘텐츠 협상(Content Negotiation)&lt;/b&gt; : HTTP에서 같은 URI에 대해 가장 적합한 자원의 &lt;b&gt;표현&lt;/b&gt;을 제공하는 메커니즘이다. 여기서 &lt;b&gt;표현&lt;/b&gt;이란 송수신 가능한 자원의 형태로, 서버가 클라이언트에게 리소스를 전달할 때 하나의 자원을 다양한 형식(표현)으로 제공할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 클라이언트가 /items/1 이라는 자원을 요청할 때, Accept : application/json 을 요청 헤더로 함께 보내면 서버는 json 형식으로 응답할 수 있고, application/xml로 요청하면 xml로 응답할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠 협상 관련 HTTP 헤더로는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Accept : 선호하는 미디어 타입 (application/json, application/ xml, text/html 등)&lt;/li&gt;
&lt;li&gt;Accept-Language : 선호 언어 (ko, en, ko-KR, 등)&lt;/li&gt;
&lt;li&gt;Accept-Charset: 선호하는 문자 인코딩(charset) (utf-8, iso-8859-1, 등)&lt;/li&gt;
&lt;li&gt;Accept-Encoding: 선호하는 콘텐츠 인코딩(압축 방식) (gzip, deflate, br 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;q(Quality Value)&lt;/b&gt; : 콘텐츠 협상을 할 때는 선호도로 우선순위를 반영할 수 있다. 0~1 사이값으로 값이 클수록 우선순위가 높다. 생략 시 1이다. 각 값에 ; 를 붙인 후 q=&amp;lt;값&amp;gt;을 붙인다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 : Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;223&quot; data-start=&quot;176&quot;&gt;ko-KR: 한국어 (대한민국), 가장 선호 (q 생략 시 기본값 1.0)&lt;/li&gt;
&lt;li data-end=&quot;264&quot; data-start=&quot;224&quot;&gt;ko;q=0.9: 한국어 (국가 상관 없음)&lt;/li&gt;
&lt;li data-end=&quot;298&quot; data-start=&quot;265&quot;&gt;en-US;q=0.8: 영어 (미국)&lt;/li&gt;
&lt;li data-end=&quot;338&quot; data-start=&quot;299&quot;&gt;en;q=0.7: 영어 (국가 상관 없음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;언어&amp;gt; 또는 &amp;lt;언어&amp;gt;-&amp;lt;국가&amp;gt; 로 locale을 지정할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이브러리 채택&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Next.js 특화된 국제화 라이브러리를 물색했다. next-intl, next-i18next&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;&amp;nbsp;로 일단 좁혀졌다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 선택 기준은 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1351&quot; data-start=&quot;1222&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1259&quot; data-start=&quot;1222&quot;&gt;러닝 커브가 낮을 것 (Next.js도 아직 익숙하지 않음)&lt;/li&gt;
&lt;li data-end=&quot;1283&quot; data-start=&quot;1262&quot;&gt;참고할 수 있는 자료가 많을 것&lt;/li&gt;
&lt;li data-end=&quot;1317&quot; data-start=&quot;1286&quot;&gt;지역화(l10n)까진 필요 없이 단순한 국제화 기능만 필요&lt;/li&gt;
&lt;li data-end=&quot;1351&quot; data-start=&quot;1320&quot;&gt;App Router 기반 프로젝트와 잘 호환될 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 next-intl을 택했다. 찾아보니 next-18next는 공식 깃허브 리드미에서도 앱 라우터는 i18next, react-i18next를 사용하라고 안내했다. 근데 Next.js에 i18next 얹혀서 넥스트에 맞게 또 설정해주는 것을 찾아보기에는 next-intl의 공식문서가 너무 잘 되어 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 라우터인지 페이지 라우터인지, i18n 라우팅하는지 아닌지에 따라 문서가 잘 나누어져 있고, 알려준대로 코드 복붙만 딱딱 해도 바로 작동했다. 또 번들 사이즈도 1.1kB에 주간 다운로드수도 약 55만에 꾸준히 성장하고 있어서 바로 채택했다. next-18next는 16.4kB, 42만 다운로드 수였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9tYXn/btsM5ZGGg8d/zyW3srHMkM06qEAeSIfKh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9tYXn/btsM5ZGGg8d/zyW3srHMkM06qEAeSIfKh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9tYXn/btsM5ZGGg8d/zyW3srHMkM06qEAeSIfKh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9tYXn%2FbtsM5ZGGg8d%2FzyW3srHMkM06qEAeSIfKh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1398&quot; height=&quot;552&quot; data-origin-width=&quot;1398&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;next-intl 예제&lt;/h3&gt;
&lt;pre id=&quot;code_1743595374264&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// /src/app/[locale]/page.tsx

import { useTranslations } from &quot;next-intl&quot;;
import Button from &quot;@/components/Button&quot;;
import Vertical from &quot;@/components/Vertical&quot;;

export default function Home() {
  const t = useTranslations(&quot;Home&quot;);

  return (
    &amp;lt;div style={{ textAlign: &quot;center&quot; }}&amp;gt;
      &amp;lt;div&amp;gt;{t(&quot;hello&quot;)}&amp;lt;/div&amp;gt;
      &amp;lt;Button /&amp;gt;
      &amp;lt;Vertical /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1744015384129&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// messages/ko.json

{
  &quot;Home&quot;: {
    &quot;hello&quot;: &quot;안녕 세상아&quot;,
    &quot;vertical&quot;: &quot;&amp;lt;br&amp;gt;&amp;lt;/br&amp;gt;세로&quot;
  }
}

// messages/en.json

{
  &quot;Home&quot;: {
    &quot;hello&quot;: &quot;Hello World&quot;,
    &quot;vertical&quot;: &quot;&amp;lt;br&amp;gt;&amp;lt;/br&amp;gt;vertical&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용법도 간단했다. t 함수 불러와서 따로 각 언어별 json에 저장된 키를 가져오면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743595411004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useLocale } from &quot;next-intl&quot;;
import { usePathname, useRouter } from &quot;@/i18n/navigation&quot;;

export default function Button() {
  const locale = useLocale();
  const pathname = usePathname();
  const router = useRouter();

  const handleClick = () =&amp;gt; {
    const afterLocale = locale === &quot;ko&quot; ? &quot;en&quot; : &quot;ko&quot;;
    router.push(pathname, { locale: afterLocale });
  };

  return (
    &amp;lt;button
      onClick={handleClick}
      className=&quot;rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600&quot;
    &amp;gt;
      {locale === &quot;ko&quot; ? &quot;영어로 보기&quot; : &quot;View in Korean&quot;}
    &amp;lt;/button&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 전환 라우팅도 복잡한 설정 없이 그냥 옵션에 locale 넣어주면 알아서 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743595420523&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useTranslations } from &quot;next-intl&quot;;

export default function Vertical() {
  const t = useTranslations(&quot;Home&quot;);

  return (
    &amp;lt;div&amp;gt;
      {t.rich(&quot;vertical&quot;, {
        br: () =&amp;gt; &amp;lt;br /&amp;gt;,
      })}
    &amp;lt;/div&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항 중, 언어 별로 문단을 분리하거나 합치는 등 별도로 관리할 수 있으면 좋겠다고 하셨었는데, rich 라는 함수를 쓰면 각 언어 json 중 필요한 부분에 원하는 태그를 넣어두면 해당 태그를 변환시킬 수도 있었다. 이를 이용해서 영문 부분에 &amp;lt;br&amp;gt;&amp;lt;/br&amp;gt;을 넣어두어서 문단 분리를 할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743597429233&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { NextRequest } from &quot;next/server&quot;;

const locales = [&quot;en&quot;, &quot;ko&quot;];
const defaultLocale = &quot;ko&quot;;

function parseAcceptLanguage(header: string | null): string[] {
  if (!header) return [];

  return header
    .split(&quot;,&quot;)
    .map((entry) =&amp;gt; {
      const [lang, qValue] = entry.trim().split(&quot;;q=&quot;);
      return {
        lang: lang.trim(),
        q: qValue ? parseFloat(qValue) : 1.0,
      };
    })
    .sort((a, b) =&amp;gt; b.q - a.q)
    .map((item) =&amp;gt; item.lang);
}

// 가장 먼저 일치하는 언어 반환
function getLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get(&quot;Accept-Language&quot;);
  const acceptedLanguages = parseAcceptLanguage(acceptLanguage);

  for (const lang of acceptedLanguages) {
    const baseLang = lang.split(&quot;-&quot;)[0];
    if (locales.includes(baseLang)) {
      return baseLang;
    }
  }

  return defaultLocale;
}

export const routing = {
  locales,
  defaultLocale,
  getLocale,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 따로 locale을 지정하지 않을 때는 Accept-Language 요청 헤더를 통해 언어 콘텐츠 협상으로 언어를 정하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;적용 후&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next-intl 적용 후에는 국제화를 적용하여 번역된 HTML을 내려주므로, 텍스트가 변경되지 않아서 최초 렌더링 시점을 기준으로 잡았다.&lt;/p&gt;
&lt;pre id=&quot;code_1744092753995&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useEffect } from 'react'

export default function TranslateObserver() {
  useEffect(() =&amp;gt; {
    const timestamp = performance.now()
    console.log(`최초 렌더링 시점: ${timestamp.toFixed(2)}ms`)
  }, [])

  return null
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시나 마찬가지로 캐시 비우기 및 강력 새로고침으로 10회 진행했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;716.50ms&lt;/li&gt;
&lt;li&gt;678.10ms&lt;/li&gt;
&lt;li&gt;654.30ms&lt;/li&gt;
&lt;li&gt;681.90ms&lt;/li&gt;
&lt;li&gt;700.50ms&lt;/li&gt;
&lt;li&gt;746.60ms&lt;/li&gt;
&lt;li&gt;680.20ms&lt;/li&gt;
&lt;li&gt;676.00ms&lt;/li&gt;
&lt;li&gt;787.30ms&lt;/li&gt;
&lt;li&gt;747.40ms&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 : 706.88ms (0.707초)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 2.34초가 걸리던 화면 렌더링 시간을 next-intl 도입을 통해 평균 0.706초로 69.83% 단축할 수 있었고, 불필요한 스크립트 로딩도 제거되었고, UI 깨짐도 완전히 사라졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/233</guid>
      <comments>https://promisingmoon.tistory.com/233#entry233comment</comments>
      <pubDate>Tue, 8 Apr 2025 15:28:23 +0900</pubDate>
    </item>
    <item>
      <title>TIL #128 : PNG 이미지를 WebP 확장자로 변환하여 95% 용량 절감</title>
      <link>https://promisingmoon.tistory.com/232</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PNG 형식의 퍼블릭 이미지를 &lt;span data-token-index=&quot;1&quot;&gt;WebP 확장자&lt;/span&gt;로 변경하여 &lt;span data-token-index=&quot;3&quot;&gt;용량 95% 절감&lt;/span&gt; (평균 2.46MB &amp;rarr; 128KB, 총 29.53MB &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;1.54MB)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 1 : public 이미지의 용량 비대&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 public 디렉터리에 있던 이미지들은 모두 PNG 확장자였고 기본적으로 MB 단위라서 불러오는데 체감 시간이 오래 걸렸다. 특히 가장 큰 이미지의 경우 11MB나 됐는데, 504 Gateway Timeout 에러가 발생했다. 이미지 로딩 이 너무 오래 걸려서 타임아웃이 난거다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_status504image.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lMmqx/btsNbwYw1T5/K0vMkZuxC5xe1TlrRaDKNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lMmqx/btsNbwYw1T5/K0vMkZuxC5xe1TlrRaDKNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lMmqx/btsNbwYw1T5/K0vMkZuxC5xe1TlrRaDKNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlMmqx%2FbtsNbwYw1T5%2FK0vMkZuxC5xe1TlrRaDKNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1281&quot; height=&quot;685&quot; data-filename=&quot;edited_status504image.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;685&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 Next.js의 Image 컴포넌트가 브라우저 호환여부에 따라 webp로 변환해주기는 하나, 원본의 용량 자체가 커서 다소 불러오는데 시간이 걸렸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 1 : WebP 변환하여 용량 절감&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebP 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebP는 구글이 2010년에 개발한 이미지 포맷으로, 손실 및 비손실 압축, 움짤, 알파를 모두 지원하여 JPG 대비 25~34%, PNG 대비 26% 정도 줄어듦(구글 발표 기준)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 12장의 PNG 이미지를 모두 WebP 확장자로 변경하여 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;평균 2.46MB에서 128KB로, 총&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;29.53MB에서&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;1.54MB로 95% 용량 절감하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 2 : 원본 이미지보다 응답 이미지의 용량이 더 커짐&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 도구의 네트워크 탭을 확인해 보니 응답 이미지의 용량이 public 내의 원본 이미지 용량보다 더 컸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 2 : quality 속성을 100에서 75로 축소&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 상에서는 &lt;a href=&quot;https://nextjs.org/docs/pages/api-reference/components/image#quality&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;next/image의 Image 컴포넌트&lt;/a&gt;를 사용하고 있었다. 이 컴포넌트는 요청된 사이즈에 맞게 리사이징 후 WebP로 재인코딩한다. 이 과정에서 원본보다 용량이 커질 수 있는데, quality 속성을 100으로 하면 압축을 거의 하지 않아서 결과적으로 원본보다 용량이 더 커지게 된다.&amp;nbsp;이를 기본값인 75로 지정해 주었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744009211218&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Image 
    src={images.image1} 
    alt=&quot;image1&quot; 
    fill 
    className=&quot;object-cover&quot; 
    quality={75} // 여기 !! (또는 생략)
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 3 : 이미지 경고 로그&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1744009605133&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mage with src &quot;/_next/static/media/image1.png&quot; has legacy prop &quot;layout&quot;. Did you forget to run the codemod?
Read more: https://nextjs.org/docs/messages/next-image-upgrade-to-13
Image with src &quot;/_next/static/media/image1.png&quot; has legacy prop &quot;objectFit&quot;. Did you forget to run the codemod?
Read more: https://nextjs.org/docs/messages/next-image-upgrade-to-13&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/messages/next-image-upgrade-to-13&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 링크&lt;/a&gt;를 참고하라고 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 3 : 문서 대로 수정&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1744009735156&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Image 
	src={images.image1} 
	alt=&quot;image1&quot; 
	layout=&quot;fill&quot; // 여기 1
	objectFit=&quot;cover&quot; // 여기 2
	quality={75} 
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 코드는 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744009750762&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Image 
	src={images.image1} 
	alt=&quot;image1&quot; 
	fill // 여기 1
	className=&quot;object-cover&quot; // 여기 2
	quality={75} 
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경한 코드는 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 개선 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 WebP 확장자로 변경하려고 하니, 일일이 페이지를 찾아가서 코드를 수정해야하는 문제가 있어서, 이미지 매핑 레이어를 추가해서 관리하였다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744010047899&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import image1 from '@images/image1.webp'
import image2 from '@images/image2.webp'
import image3 from '@images/image3.webp'
// ...

export const images = {
  image1,
  image2,
  image3,
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;png &amp;rarr; webp 확장자 변경으로 이미지 총합 29.53MB 에서 1.54MB로 95% 절감&amp;nbsp;&lt;/li&gt;
&lt;li&gt;12개의 이미지를 이미지 매핑 레이어로 중앙집중화 하여 유지보수성 증대&lt;/li&gt;
&lt;li&gt;레거시 속성의 개선으로 경고 메시지 제거&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>jpg</category>
      <category>next/image</category>
      <category>NextJs</category>
      <category>PNG</category>
      <category>WebP</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/232</guid>
      <comments>https://promisingmoon.tistory.com/232#entry232comment</comments>
      <pubDate>Mon, 7 Apr 2025 16:21:21 +0900</pubDate>
    </item>
    <item>
      <title>TIL #127 : Axios Interceptor 도입으로 인증 공통화 및 141줄 절감</title>
      <link>https://promisingmoon.tistory.com/231</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 매 페이지마다 인증 처리를 하던 방식에서 Axios interceptor로 요청 전후를 공통 관심사로 묶어서 총 7페이지에서 순 141줄을 절감했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;인증이 필요한 페이지에서 등록/수정/삭제 요청을 보낼 때마다 매 페이지에서 인증 처리를 구현하고 있었다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios interceptor 도입하여 반복되는 인증 로직을 공통 관심사로 묶어 유지보수성과 일관성을 확보했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Axios 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #373747; text-align: start;&quot;&gt;Axios는 브라우저와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #373747; text-align: start;&quot;&gt;Node.js를&lt;/span&gt;&amp;nbsp;위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Promise 기반&lt;span style=&quot;background-color: #ffffff; color: #373747; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;HTTP 클라이언트이다. 그러니까 브라우저든 Node.js든 동일하게 사용할 수 있는 웹 요청 라이브러리다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #373747; text-align: start;&quot;&gt;Axios Instance 란?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios instance는 공통된 설정(예: baseURL, 헤더 등)을 사전에 정의해 재사용할 수 있도록 하는 Axios의 기능이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Axios interceptor 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Axios interceptor는 요청 또는 응답이 처리되기 전/후에 가로채어 공통 로직(예: 토큰 자동 추가, 에러 처리 등)을 수행할 수 있도록 도와주는 기능이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Axios Interceptor 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 전의 코드는 아래와 같다. 매 인증 요청을 보내는 페이지마다 해당 로직이 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744000236925&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
  const token = getAccessToken()

  if (!token) {
    router.push('/login')
    return
  }

  const response = await fetch('/api/items', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
    },
    body: data,
  })

  if (response.ok) {
    router.push('/items')
  } else {
    const errorResponse = await response.json()

    if (response.status === 401) {
      alert('인증이 만료되었습니다. 다시 로그인해주세요.')
      removeAccessToken()
      router.push('/login')
    }
  }
} catch (error) {
  // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제로 만들었다. 단순화해서 Access token만 썼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이를 공통화하기 위해 Axios Interceptor를 만들었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744000451662&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import axios from 'axios'

export const authInstance = () =&amp;gt; {
  const instance = axios.create()

  instance.interceptors.request.use(
    (config) =&amp;gt; {
      const token = getAccessToken()
      if (!token) {
        throw new CustomError('인증 토큰이 없습니다.', 401)
      }
      config.headers.Authorization = `Bearer ${token}`
      return config
    },
    (error) =&amp;gt; Promise.reject(error),
  )

  instance.interceptors.response.use(
    (response) =&amp;gt; response,
    (error) =&amp;gt; {
      if (error?.response?.status === 401) {
        alert('인증이 만료되었습니다. 다시 로그인해주세요.')
        removeAccessToken()
        return Promise.reject(new CustomError('토큰 만료', 401))
      }
      return Promise.reject(error)
    },
  )

  return instance
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스를 만들고, 요청 전에 토큰을 가져와 헤더에 넣어주었고, 응답에 401 에러가 나면 인증 만료 처리를 해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이를 처음 코드에 적용하면,&lt;/p&gt;
&lt;pre id=&quot;code_1744000830702&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
  await authInstance().post('/api/items', data)
  router.push('/items')
} catch (error: CustomError) {
  if (error.status === 401) {
    alert('인증이 만료되었습니다. 다시 로그인해주세요.')
    router.push('/login')
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7개 페이지에서 중복된 인증 처리 로직을 제거하여, 순 141줄을 절감하고 유지보수성을 높였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>axios</category>
      <category>axios instance</category>
      <category>axios interceptor</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/231</guid>
      <comments>https://promisingmoon.tistory.com/231#entry231comment</comments>
      <pubDate>Mon, 7 Apr 2025 14:21:34 +0900</pubDate>
    </item>
    <item>
      <title>TIL #126 : SWR 도입으로 7개 페이지에서 93줄 코드 절감</title>
      <link>https://promisingmoon.tistory.com/230</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR이라는 데이터 패치 라이브러리를 도입하여 외부에서 데이터를 불러올 때 로딩, 에러, 데이터를 단순하게 관리하고, 7페이지에서 총 순 93줄을 줄였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인턴으로 간 회사에서 코드를 보니 매 페이지마다 외부 요청 페이지에서 요청, 로딩, 에러를 일일이 useState, fetch로 구현하고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR 라이브러리를 도입하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SWR란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR은 데이터 패치 리액트 훅이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stale-While-Revalidate이라는 &lt;span style=&quot;color: #334155; text-align: start;&quot;&gt;HTTP 캐시 무효 전략을 기반으로 만들어진 라이브러리로, Stale-While-Revalidate은 우선 캐시된 데이터를 반환하면서 데이터를 요청하고, 응답받으면 이를 최신화하는 전략이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743992281047&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import useSWR from 'swr'

const fetcher = (url: string) =&amp;gt; fetch(url).then((res) =&amp;gt; res.json());

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return &amp;lt;div&amp;gt;failed to load&amp;lt;/div&amp;gt;
  if (isLoading) return &amp;lt;div&amp;gt;loading...&amp;lt;/div&amp;gt;
  return &amp;lt;div&amp;gt;hello {data.name}!&amp;lt;/div&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에 나온 간단한 예제이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useSWR을 통해 원하는 url과 패처를 지정해 주면, 그에 대한 응답, 에러, 로딩처리를 해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SWR 채택 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 패치 라이브러리로 크게 SWR과 TanStack Query(구 React Query)가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 도입 전 상황은 이렇다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프론트 스택에 대해 잘 모르고 시간도 없어서 러닝 커브가 크지 않아야 한다.&lt;/li&gt;
&lt;li&gt;같은 이유로, 풍부한 레퍼런스가 있어야 한다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;대부분 단일 앤드 포인트의 단순 조회 로직만 있어서 풍부한 기능이 필요 없다. (조회, 로딩, 에러 처리만 필요)&lt;/li&gt;
&lt;li&gt;Mutation이 거의 없고 Pagination이 필요 없다.&lt;/li&gt;
&lt;li&gt;데이터가 실시간으로 변하지 않고, 변경 주기가 매우 길다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR이 위의 상황에 가장 적합했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TanStack Query에 비해 더 적은 보일러플레이트&lt;/li&gt;
&lt;li&gt;단순 기능&lt;/li&gt;
&lt;li&gt;적은 용량 (5.2kB vs 15.5kB)&lt;/li&gt;
&lt;li&gt;TanStack Query보단 낮지만 절대적으로 높은 주간 다운로드 수 (330만 vs 650만)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SWR 적용&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 각 페이지마다 요청, 에러처리, 로딩처리가 개별로 구현되어 있었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743992516053&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

// ...

export default function Home() {
  const [items, setItems] = useState&amp;lt;Item[]&amp;gt;()
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState&amp;lt;string | null&amp;gt;(null)

  useEffect(() =&amp;gt; {
    itemsHandler()
  }, [])

  const itemsHandler = async () =&amp;gt; {
    setIsLoading(true)
    setError(null)

    try {
      const response = await fetch(`/api/items`)
      if (!response.ok) {
        throw new Error('서버에서 데이터를 가져오는 데 실패했습니다.')
      }
      const data = await response.json()
      setItems(data.data)
    } catch (error) {
      setError('데이터를 불러오는 중 오류가 발생했습니다.')
    } finally {
      setIsLoading(false)
    }
  }

  if(isLoading) {
    return &amp;lt;p&amp;gt;로딩 중...&amp;lt;/p&amp;gt;
  }

  if(error) {
    return &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;
  }

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div&amp;gt;
        {items?.map((item) =&amp;gt; (&amp;lt;ItemCard ... /&amp;gt;))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 회사 코드 아니고 예제로 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 데이터를 가져오는 페이지마다 항상 useState 3개와 데이터 패치 함수를 만들어주어야 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 SWR을 적용하면 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743993456577&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

// ...

interface ItemResponse {
  data: Item[]
  message: string
}

export default function Home() {
  const { data, error, isLoading } = useSWR&amp;lt;ItemResponse&amp;gt;(`/api/items`)

  if(isLoading) {
    return &amp;lt;p&amp;gt;로딩 중...&amp;lt;/p&amp;gt;
  }

  if(error) {
    return &amp;lt;p&amp;gt;{error}&amp;lt;/p&amp;gt;
  }
  
  const items = data?.data || []

  return (
    &amp;lt;&amp;gt;
      &amp;lt;div&amp;gt;
        {items.map((item) =&amp;gt; (&amp;lt;ItemCard ... /&amp;gt;))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 예제를 보면 fetcher가 있었는데 여기는 없다. 이는 프로바이더에서 공통으로 쓰도록 했기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743993622660&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// SWRProvider.tsx

'use client'

import { SWRConfig } from 'swr'
import { ReactNode } from 'react'

const fetcher = (url: string) =&amp;gt; fetch(url).then((res) =&amp;gt; res.json())

export default function SWRProvider({ children }: { children: ReactNode }) {
  return (
    &amp;lt;SWRConfig
      value={{
        fetcher,
        revalidateOnFocus: false, // 탭 포커스 시 다시 요청 금지
        revalidateOnReconnect: false, // 온라인 복귀 시 다시 요청 금지
        refreshInterval: 0, // 주기적 요청 금지
      }}
    &amp;gt;
      {children}
    &amp;lt;/SWRConfig&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1743993985269&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// layout.tsx

export default async function RootLayout({ children, params }: RootLayoutProps) {
  return (
    &amp;lt;html lang={locale}&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;SWRProvider&amp;gt;
            &amp;lt;Header /&amp;gt;
            &amp;lt;main&amp;gt;{children}&amp;lt;/main&amp;gt;
            &amp;lt;Footer /&amp;gt;
        &amp;lt;/SWRProvider&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWRProvider를 만들어서, 공통으로 쓰일 패처와 설정들을 해주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 지정한 패처 말고 다른 패처를 쓰고 싶다면 useSWR에서 커스텀을 넣어주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에, SWR은 기본 설정이 탭 포커스가 될 때, 온라인 전환 시, 또 주기적으로 재요청을 보내서 요청이 과도하게 보내게 된다. 그래서 이 설정을 꺼주어서 요청 빈도를 줄였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 총 7페이지에 걸쳐서 순 93줄을 줄일 수 있었다. (늘어난 줄 수 - 줄어든 줄 수)&lt;/p&gt;</description>
      <category>TIL</category>
      <category>React Query</category>
      <category>SWR</category>
      <category>Tanstack Query</category>
      <category>useSWR</category>
      <category>데이터패치라이브러리</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/230</guid>
      <comments>https://promisingmoon.tistory.com/230#entry230comment</comments>
      <pubDate>Mon, 7 Apr 2025 13:03:37 +0900</pubDate>
    </item>
    <item>
      <title>TIL #125 : 멀티스테이지 &amp;amp; .dockerignore로 Next.js Docker 이미지 용량 절반 줄이기</title>
      <link>https://promisingmoon.tistory.com/229</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.99GB였던 Next.js Docker 이미지를 멀티스테이지 빌드와 .dockerignore 도입으로 1.01GB까지 줄여 약 49% 경량화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js로 도커 이미지로 빌드하여 ECR에 푸쉬하여 사용하고 있었다. 하지만 기존에는 도커 이미지가 약 2GB로 크기가 커서, 빌드 시간이 오래 걸리고 ECR 업로드/다운로드 속도도 느렸다. 또&amp;nbsp;ECR에 저장할 때는 1GB당 월 0.1달러씩 부과하며, 트래픽에도 요금을 받고 있었기 때문에 용량을 더 줄이고자 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 1 : 멀티 스테이지 도입&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1743577253868&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 Dockerfile

FROM node:version-alpine
ENV TZ=Asia/Seoul
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD [&quot;npm&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 도커파일이다. 싱글 스테이지로 빌드하고 있어서 빌드 도구, 테스트 파일, 소스코드, .git 등도 실행 이미지에 남게 되어 용량도 커지고 보안적인&amp;nbsp;문제가 발생한다.&amp;nbsp;그래서 이를 멀티 스테이지로 빌드 부분과 실행 부분을 분리하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743577330356&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 빌드 단계
FROM node:version AS builder

# 작업 디렉토리 설정
WORKDIR /app

# 종속성 설치
COPY package.json package-lock.json ./
RUN npm install

# 전체 소스 복사 후 빌드
COPY . .
RUN npm run build

# 2. 실행 단계 (경량화된 실행 환경)
FROM node:version-alpine AS runner

# 타임존 설정
ENV TZ=Asia/Seoul

# 작업 디렉토리
WORKDIR /app

# 실행에 필요한 파일만 복사
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next

# 포트 설정
EXPOSE 3000

# 실행 명령
CMD [&quot;npm&quot;, &quot;start&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 builder로 빌드를 진행하고, 빌드 후 필요한 파일만 runner로 옮겨와서 alpine으로 이미지를 구웠다. 빌드 후 builder는 폐기된다. 이를 통해 1.99GB -&amp;gt; 1.21GB로 용량이 감소했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 2 : .dockerignore 도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.dockerignore&lt;/b&gt; 파일은 &lt;b&gt;COPY . .&lt;/b&gt; 시 Docker가 읽는 컨텍스트에서 제외할 파일과 디렉터리를 지정하여, 불필요한 항목이 이미지에 포함되지 않도록 도와준다.&lt;/p&gt;
&lt;pre id=&quot;code_1743577689208&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// .dockerignore 

# node_modules는 컨테이너에서 설치됨
node_modules

# Next.js 빌드 결과물은 build 단계에서 새로 생성됨
.next
out

# Git, 환경 설정, 빌드 도구 등 불필요한 파일
.git
.gitignore
*.log
.vscode
.idea
*.tsbuildinfo
.DS_Store

# 테스트 및 문서
coverage
__tests__
docs&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.21GB &amp;rarr; 1.01GB로 용량이 줄었다. 하지만 builder는 폐기되는데 COPY가 없는 runner에서는 왜 용량이 줄었는지 찾아보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;용량이 줄어든 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 builder 측에서 npm install을 하면 /app/node_modules가 생기는데,&amp;nbsp; COPY --from=builder /app/node_modules ./node_modules 로 node_modules가 중복으로 생겨서, 이로 인해 용량이 늘어난 줄 알았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 중복의 디렉토리명을 추가하면 에러가 나지 않을까? 해서 찾아보니 도커는 명령어 별로 레이어를 구성하는데, 이후 명령으로 덮어써서 에러는 안 나고, node_modules가 레이어 별로 존재하기 때문에 용량이 늘어난 거인줄 알았다. 이는 로컬의 node_modules의 용량은 460MB였는데 레이어는 압축이 되기 때문에 납득이 될.... 뻔했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 레이어가 늘어난 부분은 builder여서, runner 측 이미지에는 영향을 주지 않는데도 용량이 늘어났다는 거였다. 그래서 dive라는 도커 이미지 분석 툴을 이용해서 .dockerignore 적용 전후의 이미지를 분석해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;without-dockerignore-post.png&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;2100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z4nKW/btsNbN1kMz8/XfOleDtBdzipFk0ZgyrsN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z4nKW/btsNbN1kMz8/XfOleDtBdzipFk0ZgyrsN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z4nKW/btsNbN1kMz8/XfOleDtBdzipFk0ZgyrsN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz4nKW%2FbtsNbN1kMz8%2FXfOleDtBdzipFk0ZgyrsN0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3360&quot; height=&quot;2100&quot; data-filename=&quot;without-dockerignore-post.png&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;2100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.dockerignore 적용 전이다. /app 디렉터리에는 node_modules 하나만 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;with-dockerignore-post.png&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;2100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgUyc4/btsNcliURWA/6F6n8zhks8kKcvLUuiD6z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgUyc4/btsNcliURWA/6F6n8zhks8kKcvLUuiD6z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgUyc4/btsNcliURWA/6F6n8zhks8kKcvLUuiD6z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgUyc4%2FbtsNcliURWA%2F6F6n8zhks8kKcvLUuiD6z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3360&quot; height=&quot;2100&quot; data-filename=&quot;with-dockerignore-post.png&quot; data-origin-width=&quot;3360&quot; data-origin-height=&quot;2100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.dockerignore 적용 후이다. 역시 /app 디렉토리에는 node_modules 하나만 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 점이 하나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COPY node_modules 부분이 적용 전보다 약 148MB 더 커졌고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COPY .next 부분이 적용 전보다 약 53MB 더 커졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 가설을 하나 세워봤다. 로컬의 node_modules와 .next를 가져왔고, 이게 용량이 더 크며, 이후 npm install 및 npm run build 시에는 이미 node_modules와 .next가 있으니 중복되는 것들은 덮어쓰기가 된 것. 이 포스팅 이전에 깃을 넘나들면서 의존성이 뒤섞여 설치하느라 충분히 용량이 커질만했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가설을 토대로 node_modules와 .next를 제거 후 도커 이미지를 빌드하니..... 1.01GB로 줄었다 ㅋㅋ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1.99GB &amp;rarr; 1.01GB로 이미지 49% 절감
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포 속도 향상 (업로드/다운로드 속도 향상, 컨테이너 실행 속도 향상)&lt;/li&gt;
&lt;li&gt;저장 비용 절감 (ECR 저장 비용, 트래픽 비용 절감)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또한 ECR은 Docker 이미지를 레이어 단위로 gzip 압축하여 저장하므로, 실제 저장 용량은 약 293MB까지 감소 (1.01GB &amp;rarr; 293.33MB)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2062&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K7PRW/btsM4mW1Pmc/jUjhZiPj1CO3A30U8cwzJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K7PRW/btsM4mW1Pmc/jUjhZiPj1CO3A30U8cwzJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K7PRW/btsM4mW1Pmc/jUjhZiPj1CO3A30U8cwzJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK7PRW%2FbtsM4mW1Pmc%2FjUjhZiPj1CO3A30U8cwzJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2062&quot; height=&quot;360&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2062&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFcG6v/btsM6iMmybt/XVUbtmkSoSznOxfxWk0Trk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFcG6v/btsM6iMmybt/XVUbtmkSoSznOxfxWk0Trk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFcG6v/btsM6iMmybt/XVUbtmkSoSznOxfxWk0Trk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFcG6v%2FbtsM6iMmybt%2FXVUbtmkSoSznOxfxWk0Trk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1308&quot; height=&quot;106&quot; data-origin-width=&quot;1308&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>docker</category>
      <category>DockerImage</category>
      <category>NextJs</category>
      <category>Til</category>
      <category>도커</category>
      <category>도커이미지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/229</guid>
      <comments>https://promisingmoon.tistory.com/229#entry229comment</comments>
      <pubDate>Wed, 2 Apr 2025 16:20:54 +0900</pubDate>
    </item>
    <item>
      <title>[백준 자바 2096] 내려가기 (골드5)</title>
      <link>https://promisingmoon.tistory.com/228</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2096&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2096&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;N은 1 이상 10만 이하로, 한 줄에는 3칸이 있고 각 칸에는 최대 9까지 올 수 있으므로, 최악의 경우의 합을 더해도 9 * 10만 = 90만으로 int의 최댓값보다 작다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;또한 메모리가 4MB인데, 4byte(int) * 3칸 * 10만 줄 = 1.2MB로, 입력받는 배열에 최댓값, 최솟값 + 기타 연산하면서 생기는 객체들까지 고려하면 메모리 초과가 날 것 같았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;그래서 입력 배열은 따로 만들지 않고 최댓값 배열과 최솟값 배열만 만들되, 각각 2 * 3 배열을 만들어서 값을 재사용하기로 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.15.35.png&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKOiPo/btsKV9EQONI/g28ci1jPC41gD1GTghy8w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKOiPo/btsKV9EQONI/g28ci1jPC41gD1GTghy8w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKOiPo/btsKV9EQONI/g28ci1jPC41gD1GTghy8w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKOiPo%2FbtsKV9EQONI%2Fg28ci1jPC41gD1GTghy8w1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;725&quot; height=&quot;462&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.15.35.png&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;우선 N과 최댓값 배열 maxArr, 최솟값 배열 minArr을 선언해준다. 그리고 각 배열을 2*3으로 초기화해준다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 42.093%; height: 71px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignCenter&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 24.954%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 30.5709%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 27.8084%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 24.954%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 30.5709%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 27.8084%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.6667%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 24.954%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 30.5709%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 27.8084%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(블로그 스킨이 표를 이상하게 표현해서.. 양해 부탁드려요)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 배열은 초기값이 이렇게 되어 있을 거다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터 0행은 기존값을, 1행은 기존값으로부터 최댓값을 계산한 후, 다 계산하면 이를 다시 0행으로 옮겨줄 거다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.19.27.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB2g6J/btsKU8fpAgR/FjGRzcqQfsKaouaikxKTeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB2g6J/btsKU8fpAgR/FjGRzcqQfsKaouaikxKTeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB2g6J/btsKU8fpAgR/FjGRzcqQfsKaouaikxKTeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB2g6J%2FbtsKU8fpAgR%2FFjGRzcqQfsKaouaikxKTeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;857&quot; height=&quot;331&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.19.27.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 최댓값을 구하는 부분부터.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 3칸의 입력을 받아서 x0, x1, x2에 넣어주었다. (최솟값 배열 계산할 때도 쓰여야 해서)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 1행에 대해서 0행 중 최댓값을 가져온 뒤, 입력값을 각각 열에 맞게 더해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignCenter&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 예제인 N=3, 그 다음 첫 줄인 1 2 3을 받을 때, 33번째 줄까지 처리하면 위와 같게 된다. (0행이 시작이라 0으로 초기화되어 있으므로 입력값만 받게 된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignCenter&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 1행의 계산이 끝나면 이를 다시 0행으로 옮겨준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignCenter&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2 + 4 = 6&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3 + 5 = 9&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3 + 6 = 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 다음 입력인 4 5 6이 입력될 때 33번째 줄까지 처리되면 위와 같고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignCenter&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2 + 4 = 6&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3 + 5 = 9&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;3 + 6 = 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 다시 0행으로 옮기면 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignCenter&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9 + 4 = 13&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9 + 9 = 18&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9 + 0 = 9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 입력이 4 9 0이 입력된 후 33번째 줄까지 처리하면 위와 같고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 54.8837%; height: 61px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignCenter&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;i / j&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;18&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;18&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 0행으로 옮기면 최종적으로 위와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.23.25.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY7ZOv/btsKXHmMBob/BulKWLSYWI25v2vFmOGwo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY7ZOv/btsKXHmMBob/BulKWLSYWI25v2vFmOGwo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY7ZOv/btsKXHmMBob/BulKWLSYWI25v2vFmOGwo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY7ZOv%2FbtsKXHmMBob%2FBulKWLSYWI25v2vFmOGwo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;685&quot; height=&quot;137&quot; data-filename=&quot;스크린샷 2024-11-26 오후 1.23.25.png&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;137&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산이 다 끝나면 마지막의 0행으로 옮기는 로직에 의해 0행의 값들 중 최댓값을 구하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최솟값도 최댓값 계산한 방식으로 똑같이 하면 돼서 넣진 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;전체 코드&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1732594364258&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    static int N;
    static int[][] maxArr;
    static int[][] minArr;

    static int stoi(String s) {
        return Integer.parseInt(s);
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = stoi(st.nextToken());

        maxArr = new int[2][3];
        minArr = new int[2][3];

        for (int i = 1; i &amp;lt;= N; i++) {
            st = new StringTokenizer(br.readLine());

            int x0 = stoi(st.nextToken());
            int x1 = stoi(st.nextToken());
            int x2 = stoi(st.nextToken());

            maxArr[1][0] = Math.max(maxArr[0][0], maxArr[0][1]) + x0;
            maxArr[1][1] = Math.max(maxArr[0][0], Math.max(maxArr[0][1], maxArr[0][2])) + x1;
            maxArr[1][2] = Math.max(maxArr[0][1], maxArr[0][2]) + x2;

            maxArr[0][0] = maxArr[1][0];
            maxArr[0][1] = maxArr[1][1];
            maxArr[0][2] = maxArr[1][2];

            minArr[1][0] = Math.min(minArr[0][0], minArr[0][1]) + x0;
            minArr[1][1] = Math.min(minArr[0][0], Math.min(minArr[0][1], minArr[0][2])) + x1;
            minArr[1][2] = Math.min(minArr[0][1], minArr[0][2]) + x2;

            minArr[0][0] = minArr[1][0];
            minArr[0][1] = minArr[1][1];
            minArr[0][2] = minArr[1][2];
        }

        int max = Math.max(maxArr[0][0], Math.max(maxArr[0][1], maxArr[0][2]));
        int min = Math.min(minArr[0][0], Math.min(minArr[0][1], minArr[0][2]));

        System.out.println(max + &quot; &quot; + min);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘</category>
      <category>DP</category>
      <category>Java</category>
      <category>PS</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>자바</category>
      <category>코딩테스트</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/228</guid>
      <comments>https://promisingmoon.tistory.com/228#entry228comment</comments>
      <pubDate>Tue, 26 Nov 2024 13:36:43 +0900</pubDate>
    </item>
    <item>
      <title>[백준 자바 11048] 이동하기 (실버2)</title>
      <link>https://promisingmoon.tistory.com/227</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11048&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11048&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전형적인 DP 문제이다. 문제에서는 &lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;&lt;b&gt;(r+1, c), (r, c+1), (r+1, c+1)로 이동할 수 있다&lt;/b&gt;는 부분 때문에 &lt;b&gt;현재 위치를 기준으로 다음 위치의 값을 어떻게 넣어줄지&lt;/b&gt; 생각하게 되는데, DP는 역으로 &lt;b&gt;현재 위치의 값이 과거의 어느 위치로부터 영향받았는지&lt;/b&gt;로 생각하면 조금 더 수월하게 풀린다. 물론 그래도 DP는 너무 어렵다.,...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;문제의 답이 N, M에 도착할 때 가장 많은 사탕을 가지고 있어야 하는 경우이고, 이동은 오른쪽, 아래, 오른쪽 아래 대각선 3방향으로 밖에 이동하지 못한다는 조건으로 생각해 보자.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;두 번째 조건을 반대로 생각해보면, 현재 위치에 있으려면 그전에는 왼쪽, 위, 왼쪽 위 대각선 중 하나에 있어야 한다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;여기서 첫 번째 조건을 생각해보면, 최대한 많은 사탕을 가지고 있으려면 왼쪽, 위, 대각선 중 가장 많은 사탕을 가지고 있는 경우에 현재 위치의 사탕값을 더하면 된다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1732521113560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;현재 위치 사탕 최댓값 = 현재 위치 사탕 개수 + Max(왼쪽 사탕 개수, 위쪽 사탕 개수, 왼쪽 위 사탕 개수)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;이를 수식으로 표현하면 위와 같다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;또 꽤 많은 DP 문제를 풀 때, N개의 요소가 있다면 배열을 N+1 크기로 만들어서 0번째 인덱스를 0으로 초기화해두면 코드가 더욱 간결해진다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1732520458395&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.*;
import java.util.*;

public class Main {

    static int stoi(String s) {
        return Integer.parseInt(s);
    }

    static int N, M;
    static int[][] dp;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        N = stoi(st.nextToken());
        M = stoi(st.nextToken());

        dp = new int[N + 1][M + 1]; // (1)

        // (2)
        for (int i = 1; i &amp;lt;= N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 1; j &amp;lt;= M; j++) {
                dp[i][j] = Math.max(dp[i][j - 1], Math.max(dp[i - 1][j], dp[i - 1][j - 1])) + stoi(st.nextToken());
            }
        }

        System.out.println(dp[N][M]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타 풀이에서는 2차원 배열을 2개 선언해서 하나는 입력용, 하나는 DP용으로 푸는 경우가 많았는데, 문제 특성상 왼쪽 위부터 오른쪽 아래까지 오른쪽으로 돌면서 차례차례 계산해 나가기 때문에 2차원 배열 하나만 써도 풀린다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 부분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 팁 부분에서 말한 것처럼, N, M 각각에 +1씩 해주어서 i = 0, j = 0 인 부분을 0으로 기본값 초기화를 해두었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 부분&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 i, j가 1부터 시작한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 첫 행(가로줄)을 StringTokenizer로 입력받고, 각 열(세로줄)마다 정수로 변환한 뒤, [i][j - 1], [i - 1][j], [i - 1][j - 1] 이렇게 왼쪽, 위쪽, 왼쪽 위 대각선의 값 중 최댓값을 가져와서, 이를 입력값(현재 위치의 사탕 개수)과 더하여 2차원 배열에 넣어준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i - 1 이나 j - 1을 해도 문제가 없는 이유는 i, j 가 1부터 시작해서이고, 각 0번째 인덱스는 0으로 초기화되기 때문에 계산에 문제가 생기지 않는다.&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘</category>
      <category>DP</category>
      <category>Java</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>오블완</category>
      <category>자바</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/227</guid>
      <comments>https://promisingmoon.tistory.com/227#entry227comment</comments>
      <pubDate>Mon, 25 Nov 2024 17:01:53 +0900</pubDate>
    </item>
    <item>
      <title>TIL #124 : Spring Data Redis에서 Lua Script 사용하기</title>
      <link>https://promisingmoon.tistory.com/226</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 각각의 명령어들은 원자적으로 실행이 되지만, 비즈니스로직을 작성하다 보면 여러 명령어들을 한 번에 처리해야 하는 경우가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해, 레디스를 루아스크립트를 이용해서 여러 명령어를 원자적으로 처리할 수 있도록 지원하고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레디스에서 LuaScript 사용하기&amp;nbsp;&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; eval &quot;return 'Hello world!'&quot; 0
&quot;Hello world!&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스로 스크립트를 실행하는 명령어는 EVAL 이며, 그 다임 인수로 &amp;ldquo;&amp;hellip;&amp;rdquo;를 감싸서 루아스크립트를 작성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 그 다음 인수로는 이후 입력될 KEY 개수이고, 키 개수만큼 입력한 다음의 인수로는 인수를 입력받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;EVAL &amp;lt;루아스크립트&amp;gt; &amp;lt;이후 입력될 KEY 개수&amp;gt; [&amp;lt;키1&amp;gt; &amp;lt;키2&amp;gt;, ..., &amp;lt;인수1&amp;gt; &amp;lt;인수2&amp;gt;, ...]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 위와 같이 입력할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 위의 예제에서 볼 수 있듯, 문자열은 &amp;lsquo;&amp;hellip;&amp;rsquo;로 감싸면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키나 인수를 이용하는 방법은 KEYS[i], ARGV[i] 키워드를 이용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 주의할 점은, i는 0이 아닌 1부터 시작된다는 점과, 무조건 대문자로 적어야 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; eval &quot;return 'hello, ' .. ARGV[1]&quot; 0 'world'
&quot;hello, world&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 명령어를 풀어서 설명해보자면, &amp;lsquo;hello, &amp;lsquo; 와 ARGV[1] 문자열을 결합하여 리턴하며 (루아 스크립트에서.. 는 문자열을 결합하는 명령어다), 키는 0개이며, 인수로는 &amp;lsquo;world&amp;rsquo;가 입력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ARGV[1]는 1번째 인수인 world가 넣어지며, 따라서 &amp;ldquo;hello, world&amp;rdquo; 가 리턴된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; set a b
OK

127.0.0.1:6379&amp;gt; get a
&quot;b&quot;

127.0.0.1:6379&amp;gt; eval &quot;return 'hello, ' .. redis.call('get', KEYS[1]) .. ARGV[1]&quot; 1 a 'aby'
&quot;hello, baby&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 redis.call(&amp;rsquo;명령어&amp;rsquo;, ARGV&amp;hellip;)를 통해 레디스에 명령을 내릴 수 있는데, 이를 응용해 보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 set a b 명령을 통해 키 a에 값 b를 넣고, 레디스에 get a를 루아스크립트로 해보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루아스크립트 이후의 인수로 1 a &amp;lsquo;aby&amp;rsquo;로 해뒀는데, 이는 키의 개수가 1개이며, 키는 a, 인수는 &amp;lsquo;aby&amp;rsquo;라는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 redis.call(&amp;rsquo;get&amp;rsquo;, KEYS [1]) 는 첫 번째 키인 a가 들어가서 redis.call(&amp;rsquo;get&amp;rsquo;, a)가 되어, get a 명령을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 ARGV[1]은 &amp;lsquo;aby&amp;rsquo;로 대체되므로, 리턴되는 문자열은 &amp;lsquo;hello, &amp;lsquo; + &amp;lsquo;b&amp;rsquo; + &amp;lsquo;aby&amp;rsquo;가 되어 &amp;lsquo;hello, baby&amp;rsquo;가 출력된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Data Redis에서 LuaScript 사용하기&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1731933391932&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- resources/script.lua
redis.call('set', KEYS[1], 'b')
return 'hello, ' .. redis.call('get', KEYS[1]) .. ARGV[1]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 스프링 프로젝트의 resources 디렉터리에 script.lua를 만들어주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 입력받은 키에 'b'를 넣고, 리턴으로 'hello, ' + 'b' + ARGV[1] 가 되는, 위의 CLI 예제랑 비슷한 예제로 구성해 보았다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 9.37.49.png&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;88&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJienI/btsKN6gj02W/QWEXndRKlaeMGy7fwVEQUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJienI/btsKN6gj02W/QWEXndRKlaeMGy7fwVEQUk/img.png&quot; data-alt=&quot;요로코롬&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJienI/btsKN6gj02W/QWEXndRKlaeMGy7fwVEQUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJienI%2FbtsKN6gj02W%2FQWEXndRKlaeMGy7fwVEQUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;235&quot; height=&quot;88&quot; data-filename=&quot;스크린샷 2024-11-18 오후 9.37.49.png&quot; data-origin-width=&quot;235&quot; data-origin-height=&quot;88&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요로코롬&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731933519437&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisConfig {

    @Value(&quot;${spring.data.redis.host}&quot;)
    private String host;

    @Value(&quot;${spring.data.redis.port}&quot;)
    private int port;

    @Value(&quot;${spring.data.redis.password}&quot;)
    private String password;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();

        config.setHostName(host);
        config.setPort(port);
        config.setPassword(password);

        return new LettuceConnectionFactory(config);
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        return new StringRedisTemplate(redisConnectionFactory());
    }

    @Bean
    public RedisScript&amp;lt;String&amp;gt; script() { // 여기 !!!
        Resource script = new ClassPathResource(&quot;/script.lua&quot;); 
        return RedisScript.of(script, String.class);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음, 레디스 설정 부분이다. 윗 부분은 다들 아실테니, 맨 밑의 빈을 보면 된다. 위에서 만들어둔 script.lua를 가져오는데, 이때 RedisScript&amp;lt;T&amp;gt;의 T는 스크립트의 리턴 타입이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731933643840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;

@SpringBootTest
public class RedisScriptTest {

    @Autowired
    private RedisScript&amp;lt;String&amp;gt; script;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void test() {
        // given
        String key1 = &quot;a&quot;;
        String argv1 = &quot;aby&quot;;
        
        // when
        String returnValue = stringRedisTemplate.execute(script, List.of(key1), argv1);

        // then
        Assertions.assertThat(returnValue).isEqualTo(&quot;hello, baby&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 테스트를 구성해 보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크립트를 빈으로 등록해 두어서 주입을 받을 수 있고, 키를 a로 두고 인수를 aby로 두어 위의 CLI 예제랑 똑같이 넣어보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 &quot;hello, baby&quot;가 되어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-18 오후 9.44.14.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z66EM/btsKOmcaABy/trZoEaD4qUlKPZkI28tFr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z66EM/btsKOmcaABy/trZoEaD4qUlKPZkI28tFr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z66EM/btsKOmcaABy/trZoEaD4qUlKPZkI28tFr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz66EM%2FbtsKOmcaABy%2FtrZoEaD4qUlKPZkI28tFr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;82&quot; data-filename=&quot;스크린샷 2024-11-18 오후 9.44.14.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 성공!!&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/interact/programmability/eval-intro/&quot;&gt;레디스 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>Java</category>
      <category>luascript</category>
      <category>redis</category>
      <category>redisscript</category>
      <category>Spring</category>
      <category>SpringDataRedis</category>
      <category>레디스</category>
      <category>스프링</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/226</guid>
      <comments>https://promisingmoon.tistory.com/226#entry226comment</comments>
      <pubDate>Mon, 18 Nov 2024 21:46:47 +0900</pubDate>
    </item>
    <item>
      <title>TIL #123 : 배치 작업에는 꼭 정렬 하기</title>
      <link>https://promisingmoon.tistory.com/225</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;pre id=&quot;code_1731852895176&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Bean(JOB_NAME + &quot;_reader&quot;)
  public ItemReader&amp;lt;Product&amp;gt; itemReader() {
      return new JpaPagingItemReaderBuilder&amp;lt;Product&amp;gt;()
          .name(JOB_NAME + &quot;_reader&quot;)
          .pageSize(CHUNK_SIZE)
          .entityManagerFactory(entityManagerFactory)
          .queryString(&quot;SELECT p FROM Product p&quot;)
          .build();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 JpaPagingItemReader로 엔티티를 읽어오도록 하고, 각 Product 엔티티별로 price를 +1000 하는 작업을 하기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;700&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdZ8pj/btsKNu1W5e3/0XlSRL5QLjAWYcXiDDcohk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdZ8pj/btsKNu1W5e3/0XlSRL5QLjAWYcXiDDcohk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdZ8pj/btsKNu1W5e3/0XlSRL5QLjAWYcXiDDcohk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdZ8pj%2FbtsKNu1W5e3%2F0XlSRL5QLjAWYcXiDDcohk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;568&quot; height=&quot;398&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;700&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;9개를 가지고 작업을 했었는데, 어느 건 그대로인데 어느 건 2번 작업이 되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1062&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMeMwJ/btsKNfDWsHS/ABaAIu6XfAj8QQsDemPpG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMeMwJ/btsKNfDWsHS/ABaAIu6XfAj8QQsDemPpG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMeMwJ/btsKNfDWsHS/ABaAIu6XfAj8QQsDemPpG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMeMwJ%2FbtsKNfDWsHS%2FABaAIu6XfAj8QQsDemPpG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1766&quot; height=&quot;1062&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;1062&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731853180856&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Bean(JOB_NAME + &quot;_reader&quot;)
  public ItemReader&amp;lt;Product&amp;gt; itemReader() {
      return new JpaPagingItemReaderBuilder&amp;lt;Product&amp;gt;()
          .name(JOB_NAME + &quot;_reader&quot;)
          .pageSize(CHUNK_SIZE)
          .entityManagerFactory(entityManagerFactory)
          .queryString(&quot;SELECT p FROM Product p ORDER BY p.id&quot;) // 여기 !!!
          .build();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id를 기준으로 정렬했다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGw4Z/btsKNfxbucF/qaJS6LTAyo2cfYoBLJZzuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGw4Z/btsKNfxbucF/qaJS6LTAyo2cfYoBLJZzuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGw4Z/btsKNfxbucF/qaJS6LTAyo2cfYoBLJZzuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGw4Z%2FbtsKNfxbucF%2FqaJS6LTAyo2cfYoBLJZzuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;398&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 +1000 된 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;1332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chJO8Q/btsKMNHTUMQ/YrH3y7u42YFbU4KmKT6Vz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chJO8Q/btsKMNHTUMQ/YrH3y7u42YFbU4KmKT6Vz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chJO8Q/btsKMNHTUMQ/YrH3y7u42YFbU4KmKT6Vz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchJO8Q%2FbtsKMNHTUMQ%2FYrH3y7u42YFbU4KmKT6Vz1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2014&quot; height=&quot;1332&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;1332&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 왜 쿼리가 2번 나가는지 모르겠다. 로그가 2번 찍히는 건가 싶기도 했는데 처리 시간이 0ms인 것과 1ms인 것이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이러지... 일단 p6spy로 로그를 찍고 있다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Batch</category>
      <category>Java</category>
      <category>jpapagingitemreader</category>
      <category>Spring</category>
      <category>배치</category>
      <category>스프링</category>
      <category>스프링배치</category>
      <category>오블완</category>
      <category>자바</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/225</guid>
      <comments>https://promisingmoon.tistory.com/225#entry225comment</comments>
      <pubDate>Sun, 17 Nov 2024 23:23:42 +0900</pubDate>
    </item>
    <item>
      <title>TIL #122 : 읽기전용/쓰기전용DB를 @Transactional의 readOnly로 구분하기</title>
      <link>https://promisingmoon.tistory.com/224</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB를 2개를 쓰고, 하나를 primary db로 읽기+쓰기 작업을, 나머지 하나는 replica db로 읽기 작업만 하는 DB로 구성이 되었을 때, 매번 기능을 만들 때마다 읽기/쓰기를 구분해서 데이터소스를 가져오기보다, 어노테이션의 readOnly 속성으로 알아서 데이터소스를 구분하도록 하고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731725850690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      main:
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://localhost:54326/sample
        username: sample
        password: 1234
      sub:
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://localhost:54327/sample
        username: sample
        password: 1234
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        show_sql: true # sql 로깅
        format_sql: true # SQL 문 정렬하여 출력
        highlight_sql: true # SQL 문 색 부여
        use_sql_comments: true # 콘솔에 표시되는 쿼리문 위에 어떤 실행을 하려는지 HINT 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml 설정은 위와 같다. spring.datasource.hikari의 main, sub로 나뉜 부분에서, jdbc-url의 포트 부분만 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;primary는 54326, replica는 54327 포트라는 것만 기억해두면 된다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731725903672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum DataSourceType {
    READ_WRITE,
    READ_ONLY
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 데이터소스 타입을 읽기쓰기가 모두 가능한 READ_WRITE, 읽기만 가능한 READ_ONLY 2개를 가지는 열거형 타입을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731725920830&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.support.TransactionSynchronizationManager;

public class TransactionRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
            ? DataSourceType.READ_ONLY
            : DataSourceType.READ_WRITE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음, AbstractRoutingDataSource를 상속받는 커스텀 데이터소스를 만들고, determieCurrentLookupKey를 오버라이딩 해서, 현재 트랜잭션의 readOnly 여부에 따라 위의 열거형 타입을 다르게 해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731725940049&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.zaxxer.hikari.HikariDataSource;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

@Configuration
public class DataSourceConfig {

    /**
     * 메인 DB 데이터소스 (읽기 + 쓰기)
     */
    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.main&quot;)
    public DataSource mainDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * 서브 DB 데이터소스 (읽기 전용)
     */
    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.sub&quot;)
    public DataSource subDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    /**
     * 라우팅 데이터소스 - readOnly 속성에 따라 라우팅
     */
    @Bean
    @DependsOn({&quot;mainDataSource&quot;, &quot;subDataSource&quot;})
    public DataSource routingDataSource(
        @Qualifier(&quot;mainDataSource&quot;) DataSource mainDataSource,
        @Qualifier(&quot;subDataSource&quot;) DataSource subDataSource
    ) {
        TransactionRoutingDataSource routingDataSource = new TransactionRoutingDataSource();

        Map&amp;lt;Object, Object&amp;gt; dataSourceMap = Map.of(
            DataSourceType.READ_WRITE, mainDataSource,
            DataSourceType.READ_ONLY, subDataSource
        );

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(mainDataSource);

        return routingDataSource;
    }

    /**
     * 데이터소스 프록시 - 트랜잭션 진입 후 readOnly 속성에 따라 데이터소스 결정
     */
    @Bean
    @Primary
    @DependsOn({&quot;routingDataSource&quot;})
    public DataSource dataSource(@Qualifier(&quot;routingDataSource&quot;) DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 긴데, 우선 맨 위의 2개의 데이터소스는 application.yml 파일의 속성값을 가져와서 HikariDataSource를 만들어주는 코드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음의 routingDataSource는 readOnly 유무에 따른 데이터소스를 라우팅하는 데이터소스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막은 스프링은 트랜잭션 진입 시 모든 데이터소스에 커넥션을 가져오고 이후에 데이터소스를 결정한다. 이러면 메인과 서브 모두 커넥션을 가져온다. (이에 대한 내용은 &lt;a href=&quot;https://promisingmoon.tistory.com/222&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내 다른 글&lt;/a&gt;에서 확인할 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이를 실제 DB요청할 때 커넥션을 점유하도록 하고, 그 전에 데이터소스를 결정하도록 LazyConnectionDataSourceProxy로 감싸주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731726014584&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.multisource.domain.Product;
import ex.multisource.dto.ProductCreateReq;
import ex.multisource.repository.ProductRepository;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j(topic = &quot;product-service&quot;)
@Service
@RequiredArgsConstructor
public class ProductService {

    private final DataSource dataSource;
    private final ProductRepository productRepository;

    @Transactional(readOnly = true)
    public Product getProduct(Long id) {
        logDataSourceInfo();
        return productRepository.findById(id)
            .orElseThrow(() -&amp;gt; new IllegalArgumentException(&quot;Product not found&quot;));
    }

    @Transactional
    public Product saveProduct(ProductCreateReq request) {
        logDataSourceInfo();
        Product product = Product.create(request.name(), request.price());
        return productRepository.save(product);
    }

    private void logDataSourceInfo() {
        try (Connection connection = dataSource.getConnection()) {
            log.info(&quot;Connected to database: {}&quot;, connection.getMetaData().getURL());
        } catch (SQLException e) {
            log.error(&quot;Failed to log DataSource information&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 테스트로 @Transactional 에 reradOnly에 따라 다른 DB로 요청되는지 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션의 요청 url로 확인할 거고, 위의 yml 파일에서 설정한 대로 54326이면 primary db, 54327이면 replica db이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731726033750&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;INFO 23092 --- [multi-source-service] [nio-8080-exec-1] product-service : Connected to database: jdbc:postgresql://localhost:54326/sample
[Hibernate] 
    /* insert for
        ex.multisource.domain.Product */insert 
    into
        products (name, price) 
    values
        (?, ?) 
    returning id
INFO 23092 --- [multi-source-service] [nio-8080-exec-3] product-service : Connected to database: jdbc:postgresql://localhost:54327/sample
[Hibernate] 
    select
        p1_0.id,
        p1_0.name,
        p1_0.price 
    from
        products p1_0 
    where
        p1_0.id=?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 insert 요청(product 생성)은 jdbc:postgresql://localhost:54326/sample로 54326 포트로 primary db로 요청이 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 select 요청(product 조회)은 jdbc:postgresql://localhost:54327/sample로 54327 포트로 replica db로 요청이 갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 작동하는 것을 볼 수 있다!!&lt;/p&gt;</description>
      <category>TIL</category>
      <category>datasource</category>
      <category>Java</category>
      <category>master</category>
      <category>Primary</category>
      <category>readonly</category>
      <category>Replica</category>
      <category>Spring</category>
      <category>transactional</category>
      <category>스프링</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/224</guid>
      <comments>https://promisingmoon.tistory.com/224#entry224comment</comments>
      <pubDate>Sat, 16 Nov 2024 12:04:16 +0900</pubDate>
    </item>
    <item>
      <title>TIL #121 : 스프링 배치 5버전에서 멀티 dataSource 중 하나 지정하기</title>
      <link>https://promisingmoon.tistory.com/223</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 데이터소스 환경에서, 스프링 배치 5 버전에서는 특별한 설정을 하지 않으면 @Primary 데이터소스를 가져온다. 하지만 나는 Primary가 아닌 다른 데이터소스를 가져오려고 설정을 했더니 Job이 실행이 되지 않고, 또 application.yml 파일에 적어둔 배치 설정도 적용이 되지 않던 문제가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;pre id=&quot;code_1731719860742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# application.yml
spring:
    batch:
        jdbc:
          platform: postgresql # DBMS, 안 적으면 자동으로 탐색 
          # 실행할 스키마, 배치 라이브러리 내부에 각 DBMS 별로 sql이 작성돼있다.
          schema: classpath:org/springframework/batch/core/schema-postgresql.sql 
          initialize-schema: always # 항상 스키마를 초기화할 건지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 이렇게 application.yml을 작성해두고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731720002854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Slf4j(topic = &quot;simple-job&quot;)
@Configuration
@RequiredArgsConstructor
public class SimpleJob {

    private static final String JOB_NAME = &quot;simple_job&quot;;

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;

    @Bean(JOB_NAME)
    public Job simpleJob() {
        return new JobBuilder(JOB_NAME, jobRepository)
            .start(simpleStep())
            .incrementer(new RunIdIncrementer())
            .build();
    }

    @Bean(JOB_NAME + &quot;_step&quot;)
    public Step simpleStep() {
        return new StepBuilder(JOB_NAME + &quot;_step&quot;, jobRepository)
            .tasklet((contribution, chunkContext) -&amp;gt; {
                log.info(&quot;Hello, world!&quot;);
                return null;
            }, transactionManager)
            .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헬로 월드 로그를 출력하는 간단한 잡을 만들어두고 실행하면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731720050792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[main] simple-job : Hello, world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 잘 출력이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731720118434&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@EnableBatchProcessing(dataSourceRef = &quot;subDataSourceProxy&quot;)
public class BatchConfig {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 스프링 배치 5 버전에서 데이터소스를 변경하려면 @EnableBatchProcessing 어노테이션의 dataSourceRef 속성에 원하는 데이터소스 빈이름을 작성해주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 작성하고 실행을 하면 application.yml에 작성해 둔 설정이 적용이 안 되고, 잡도 실행이 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 스프링 배치 5 버전으로 올라오면서 기존의 설정 방법과 달라지고, @EnableBatchProcessing 어노테이션이 필수가 아닌 선택이 되었는데, 이 어노테이션을 사용하면 자동 설정들이 되지 않기 때문.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 직접 설정을 해주고 또 잡도 실행시켜주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731720400971&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 커스텀으로 만든 DataSourceConfig
@Bean
public DataSourceInitializer batchDataSourceInitializer(@Qualifier(&quot;subDataSourceProxy&quot;) DataSource dataSource) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

    if (isBatchTablesPresent(jdbcTemplate)) {
        return null;
    }

    ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
    populator.addScript(resourceLoader.getResource(&quot;classpath:org/springframework/batch/core/schema-postgresql.sql&quot;));

    DataSourceInitializer initializer = new DataSourceInitializer();
    initializer.setDataSource(dataSource);
    initializer.setDatabasePopulator(populator);

    return initializer;
}

private boolean isBatchTablesPresent(JdbcTemplate jdbcTemplate) {
    try {
        jdbcTemplate.execute(&quot;SELECT 1 FROM BATCH_JOB_INSTANCE LIMIT 1&quot;);
        return true;
    } catch (Exception e) {
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, DataSourceInitializer를 통해 원하는 데이터소스에 배치 메타데이터 테이블이 존재하지 않는다면 새로 테이블을 생성하도록 코드를 작성해 두었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731720539703&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class Launcher {

    private final Job simpleJob;
    private final JobLauncher jobLauncher;

    @Scheduled(cron = &quot;0/10 * * * * *&quot;, zone = &quot;Asia/Seoul&quot;)
    public void run() {
        JobParameters jobParameters = new JobParameters(
            Map.of(&quot;requestTime&quot;, new JobParameter&amp;lt;&amp;gt;(System.currentTimeMillis(), Long.class))
        );

        try {
            jobLauncher.run(simpleJob, jobParameters);
        } catch (JobExecutionAlreadyRunningException
                 | JobInstanceAlreadyCompleteException
                 | JobParametersInvalidException
                 | JobRestartException e) {
            throw new RuntimeException(e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 BatchConfig에서 @EnableScheduling을 해주었기 때문에 스케줄링을 할 수 있어서, @Scheduled로 하긴 했는데, 그냥 JobLauncher로만으로도 실행시킬 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731720755817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[scheduling-1] settle-job : Hello, world!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10초마다 잘 뜬다!&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Batch</category>
      <category>Java</category>
      <category>Spring</category>
      <category>springbatch</category>
      <category>springbatch5</category>
      <category>스프링</category>
      <category>스프링배치5</category>
      <category>오블완</category>
      <category>자바</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/223</guid>
      <comments>https://promisingmoon.tistory.com/223#entry223comment</comments>
      <pubDate>Sat, 16 Nov 2024 10:32:59 +0900</pubDate>
    </item>
    <item>
      <title>TIL #120 : 멀티 데이터소스 환경에서 이중 커넥션 점유 막기</title>
      <link>https://promisingmoon.tistory.com/222</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 트랜잭션에 진입한 후에 커넥션을 점유하고, 이후에 데이터소스를 결정한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 두 개의 데이터소스를 채택해서, 이중으로 커넥션을 점유하는 문제가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;pre id=&quot;code_1731671566136&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  application:
    name: multi-source-service
  datasource:
    hikari:
      postgres:
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://localhost:54326/sample
        username: sample
        password: 1234
      postgres-two:
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://localhost:54327/sample
        username: sample
        password: 1234

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
    prometheus:
      enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우선 application.yml 은 위와 같고,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731671566140&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.postgres&quot;)
    public DataSource mainDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.postgres-two&quot;)
    public DataSource subDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기존 코드는 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731661408611&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.multisource.domain.Product;
import ex.multisource.dto.ProductCreateReq;
import ex.multisource.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j(topic = &quot;product-service&quot;)
@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public Product saveProduct(ProductCreateReq request) throws InterruptedException {
        Product product = Product.create(request.name(), request.price());
        Thread.sleep(1000L * 180);
        return productRepository.save(product);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 테스트용으로, @Transactional 어노테이션을 붙여서 요청이 들어오면 트랜잭션 내에서 180초 동안 멈춰있도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4xFce/btsKMgpI64Q/n3MOt41l4sGjns43Wdp1r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4xFce/btsKMgpI64Q/n3MOt41l4sGjns43Wdp1r1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4xFce/btsKMgpI64Q/n3MOt41l4sGjns43Wdp1r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4xFce%2FbtsKMgpI64Q%2Fn3MOt41l4sGjns43Wdp1r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;646&quot; height=&quot;385&quot; data-filename=&quot;edited_blob&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤 요청을 보내고 actuator로 현재 활성 커넥션 수를 확인해 본 결과, 메인 데이터소스와 서브 데이터소스 2개를 점유하고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 DB에 요청을 보낼 때 커넥션을 획득하는&amp;nbsp;&lt;b&gt;LazyConnectionDataSourceProxy&lt;/b&gt;로 데이터소스를 감싸주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731661849631&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource mainDataSourceProxy(DataSource mainDataSource) {
        return new LazyConnectionDataSourceProxy(mainDataSource);
    }

    @Bean
    public DataSource subDataSourceProxy(@Qualifier(&quot;subDataSource&quot;) DataSource subDataSource) {
        return new LazyConnectionDataSourceProxy(subDataSource);
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.postgres&quot;)
    public DataSource mainDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = &quot;spring.datasource.hikari.postgres-two&quot;)
    public DataSource subDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 코드는 위와 같다. 기존 데이터소스들은 그대로 두고, 각각 프록시로 감싼 데이터소스를 하나 더 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731662370623&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource
Caused by: java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따로 dataSource를 두지 않고, mainDataSource를 바로 해두고 싶었는데, 위의 에러가 계속 났다. 프록시로 데이터소스를 감싸느라, JPA에서 엔티티매니저를 만드는데 문제가 생기는 것 같은데 정확히는 알아보아야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjOrja/btsKMn9Ycp2/HofFALxmn1HVn9XVOeSOmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjOrja/btsKMn9Ycp2/HofFALxmn1HVn9XVOeSOmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjOrja/btsKMn9Ycp2/HofFALxmn1HVn9XVOeSOmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjOrja%2FbtsKMn9Ycp2%2FHofFALxmn1HVn9XVOeSOmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;642&quot; height=&quot;395&quot; data-filename=&quot;edited_edited_blob&quot; data-origin-width=&quot;642&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LazyConnectionDataSourceProxy&lt;/b&gt;로 데이터소스를 감싸면 실제 DB에 요청할 때 커넥션을 획득하기 때문에, 활성 커넥션이 0개인 것을 확인할 수 있다!!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>datasource</category>
      <category>Java</category>
      <category>lazyconnectiondatasourceproxy</category>
      <category>Spring</category>
      <category>멀티데이터소스</category>
      <category>스프링</category>
      <category>오블완</category>
      <category>자바</category>
      <category>트랜잭션</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/222</guid>
      <comments>https://promisingmoon.tistory.com/222#entry222comment</comments>
      <pubDate>Fri, 15 Nov 2024 20:40:35 +0900</pubDate>
    </item>
    <item>
      <title>노션으로 작성한 이력서 이쁘게 PDF로 뽑아내기</title>
      <link>https://promisingmoon.tistory.com/221</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;배경&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션으로 작성한 이력서를 PDF로 내보내기 하면 페이지로 나눠질 때 페이지 사이 간격이 우주만큼 커지게 된다. 또 상하좌우 여백도 조절하지 못해서 아쉬웠는데, 더 이쁘게 뽑아내는 방법을 공유하려고 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 좌우 너비 늘리기&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_edited_스크린샷 2024-11-14 오후 2.56.39.png&quot; data-origin-width=&quot;1601&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvBC6d/btsKIT9LYn5/vXRMYhXgJ2CBdG0kQy0pQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvBC6d/btsKIT9LYn5/vXRMYhXgJ2CBdG0kQy0pQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvBC6d/btsKIT9LYn5/vXRMYhXgJ2CBdG0kQy0pQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvBC6d%2FbtsKIT9LYn5%2FvXRMYhXgJ2CBdG0kQy0pQ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1601&quot; height=&quot;828&quot; data-filename=&quot;edited_edited_스크린샷 2024-11-14 오후 2.56.39.png&quot; data-origin-width=&quot;1601&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 위의 &lt;b&gt;... 버튼&lt;/b&gt;을 누르고, &lt;b&gt;전체 너비&lt;/b&gt;를 켜주고, &lt;b&gt;내보내기&lt;/b&gt;를 누른다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HTML 내보내기&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-14 오후 2.56.59.png&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oc02f/btsKI2rKfZx/vVIYggJKpYguqWnWlkuve0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oc02f/btsKI2rKfZx/vVIYggJKpYguqWnWlkuve0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oc02f/btsKI2rKfZx/vVIYggJKpYguqWnWlkuve0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOc02f%2FbtsKI2rKfZx%2FvVIYggJKpYguqWnWlkuve0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;317&quot; height=&quot;269&quot; data-filename=&quot;스크린샷 2024-11-14 오후 2.56.59.png&quot; data-origin-width=&quot;317&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내보내기 형식&lt;/b&gt;을 &lt;b&gt;HTML&lt;/b&gt;로 하고 &lt;b&gt;내보내기 버튼&lt;/b&gt;을 클릭한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 브라우저에서 인쇄 열기&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDWLT/btsKJPMbP27/qon5hNpdQzmOMHHt3QhOg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDWLT/btsKJPMbP27/qon5hNpdQzmOMHHt3QhOg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDWLT/btsKJPMbP27/qon5hNpdQzmOMHHt3QhOg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDWLT%2FbtsKJPMbP27%2Fqon5hNpdQzmOMHHt3QhOg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1596&quot; height=&quot;703&quot; data-origin-width=&quot;1596&quot; data-origin-height=&quot;703&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 위 &lt;b&gt;... 버튼&lt;/b&gt;을 누르고 &lt;b&gt;인쇄 버튼&lt;/b&gt;을 누른다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 페이지 커스텀 조절&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JfZeE/btsKJFJEJpb/lkANXGVix8l8at9K9MenZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JfZeE/btsKJFJEJpb/lkANXGVix8l8at9K9MenZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JfZeE/btsKJFJEJpb/lkANXGVix8l8at9K9MenZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJfZeE%2FbtsKJFJEJpb%2FlkANXGVix8l8at9K9MenZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1087&quot; height=&quot;717&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PDF로 저장&lt;/b&gt; -&amp;gt; &lt;b&gt;A4&lt;/b&gt; -&amp;gt; &lt;b&gt;여백 맞춤&lt;/b&gt; -&amp;gt; &lt;b&gt;배율 맞춤 (65 설정)&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 페이지의 상하좌우로 있는 mm 단위로 여백을 입맛에 맞추면 되는데, 나는 상하 10mm, 좌우 20mm로 정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝!!&amp;nbsp;&lt;/p&gt;</description>
      <category>지식 한 조각</category>
      <category>PDF</category>
      <category>노션</category>
      <category>노션이력서</category>
      <category>오블완</category>
      <category>이력서</category>
      <category>이쁘게</category>
      <category>티스토리챌린지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/221</guid>
      <comments>https://promisingmoon.tistory.com/221#entry221comment</comments>
      <pubDate>Thu, 14 Nov 2024 15:16:54 +0900</pubDate>
    </item>
    <item>
      <title>TIL #119 : Jacoco 테스트 커버리지 항목에 롬복이 생성한 코드 무시하기</title>
      <link>https://promisingmoon.tistory.com/220</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jacoco로 테스트 커버리지를 검사하다 보면, lombok이 만들어준 getter, setter, 생성자 등등까지 테스트 항목에 포함되게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getter, setter까지 테스트를 하는 것은 테스트를 작성하는 의미가 없다고 판단해서 이를 무시할 수 있는지 알아보았다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731481864061&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// lombok.config
# jacoco 테스트 커버리지 시, lombok이 생성한 코드는 제외
lombok.addLombokGeneratedAnnotation = true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 lombok.config 파일을 만들어서 위와 같이 작성해 주었다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>jacoco</category>
      <category>Java</category>
      <category>lombok</category>
      <category>Spring</category>
      <category>롬복</category>
      <category>스프링</category>
      <category>자바</category>
      <category>테스트커버리지</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/220</guid>
      <comments>https://promisingmoon.tistory.com/220#entry220comment</comments>
      <pubDate>Wed, 13 Nov 2024 16:13:00 +0900</pubDate>
    </item>
    <item>
      <title>TIL #118 : 롬복 @RequiredArgsConstructor 사용 시 @Qualifier 사용 불가 해결하기</title>
      <link>https://promisingmoon.tistory.com/219</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 데이터소스를 채택하면서, 메인 데이터소스는 &lt;b&gt;@Primary&lt;/b&gt;를 붙였고, 배치 데이터소스는 &lt;b&gt;@Qualifier&lt;/b&gt;를 사용해서 가져오려고 했었다. 하지만 &lt;b&gt;lombok&lt;/b&gt;의 &lt;b&gt;@RequiredArgsConstructor&lt;/b&gt; 사용 시 &lt;b&gt;@Qualifier&lt;/b&gt; 사용이 불가능했던 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731480774195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class TestService {

    private final DataSource dataSource;
    
    @Qualifier(&quot;batchDataSource&quot;)
    private final DataSource batchDataSource;

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TestService는 2개의 DataSource를 주입받고 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class TestService {
    @Generated
    private static final Logger log = LoggerFactory.getLogger(TestService.class);
    private final DataSource dataSource;
    @Qualifier(&quot;batchDataSource&quot;)
    private final DataSource batchDataSource;

    @Generated
    // 여기 아래 !!!
    public TestService(final DataSource dataSource, final DataSource batchDataSource) {
        this.dataSource = dataSource;
        this.batchDataSource = batchDataSource;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;롬복이 만들어준 빌드된 코드를 보면 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드를 보면 &lt;b&gt;@Qualifier&lt;/b&gt;가 잘 적혀있는 것 같이 보이지만, 생성자에서는 &lt;b&gt;@Qualifier&lt;/b&gt;가 없어서 주입을 못 받는다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731481184298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// lombok.config
# 롬복 애노테이션 프로세서가 생성자 생성 시 사용할 필드에 선언된 @Qualifier를 복사
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉터리에 &lt;b&gt;lombok.config&lt;/b&gt; 파일을 만들어서 위와 같이 작성해 주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class TestService {
    @Generated
    private static final Logger log = LoggerFactory.getLogger(TestService.class);
    private final DataSource dataSource;
    @Qualifier(&quot;batchDataSource&quot;)
    private final DataSource batchDataSource;

    @Generated
    // 여기 아래 !!!
    public TestService(final DataSource dataSource, @Qualifier(&quot;batchDataSource&quot;) final DataSource batchDataSource) {
        this.dataSource = dataSource;
        this.batchDataSource = batchDataSource;
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 생성자의 인수에 &lt;b&gt;@Qualifier&lt;/b&gt;가 잘 붙어있는 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Java</category>
      <category>lombok</category>
      <category>Qualifier</category>
      <category>requireargsconstructor</category>
      <category>Spring</category>
      <category>롬복</category>
      <category>스프링</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/219</guid>
      <comments>https://promisingmoon.tistory.com/219#entry219comment</comments>
      <pubDate>Wed, 13 Nov 2024 16:04:01 +0900</pubDate>
    </item>
    <item>
      <title>TIL #117 : 인텔리제이 프로젝트 구조의 하위 디렉토리 전부 펼치기 단축키</title>
      <link>https://promisingmoon.tistory.com/218</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링을 쓰다 보면, src클릭 main 클릭 java 클릭 com 클릭 ooo클릭 ooo 클릭해야만 드디어 스프링 애플리케이션을 볼 수 있다. 근데 이제 시작이다. 그 하위의 디렉터리를 또 하나하나 클릭하다 보면 화딱지가 난다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 클릭 한 번으로 하위 디렉토리를 모두 열 수 있는 방법을 알게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVbhcp/btsKH0tCXB3/KUyj68HOWcvSfMsboHtUj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVbhcp/btsKH0tCXB3/KUyj68HOWcvSfMsboHtUj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVbhcp/btsKH0tCXB3/KUyj68HOWcvSfMsboHtUj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVbhcp%2FbtsKH0tCXB3%2FKUyj68HOWcvSfMsboHtUj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;986&quot; height=&quot;719&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선 설정을 연다. (맥은 cmd + ,)&lt;/li&gt;
&lt;li&gt;왼쪽 메뉴에서 키맵(keymap)을 클릭 후&lt;/li&gt;
&lt;li&gt;트리 노드 전체 펼치기(Fully Expand Tree Node) 검색&lt;/li&gt;
&lt;li&gt;트리 노드 전체 펼치기 우클릭 후 마우스 단축키 추가 클릭&lt;/li&gt;
&lt;li&gt;편한 단축키 등록 (나는 cmd + 휠클릭으로 했다)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 너무 편하다....&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>intellij</category>
      <category>Java</category>
      <category>Spring</category>
      <category>단축어</category>
      <category>단축키</category>
      <category>디렉토리전부열기</category>
      <category>디렉토리펼치기</category>
      <category>스프링</category>
      <category>인텔리제이</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/218</guid>
      <comments>https://promisingmoon.tistory.com/218#entry218comment</comments>
      <pubDate>Wed, 13 Nov 2024 15:24:21 +0900</pubDate>
    </item>
    <item>
      <title>TIL #116 : 스프링/도커컴포즈에 Graceful shutdown 적용하기</title>
      <link>https://promisingmoon.tistory.com/217</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD를 구성하면서, CD 마지막 과정은 EC2 서버의 기존 도커 컴포즈를 down 하고 다시 up 하는 과정이 있다. 그리고 재시작하면서 스프링 컨테이너를 강제 종료하는데, 이때 진행 중인 로직도 그대로 강제 종료되는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 주문 처리 중 재고는 감소시켰는데 강제 종료가 되어서 재고가 롤백되지 않는 문제가 있을 수 있다. (MSA 환경이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Eureka server에 설정된 Eviction timer가 종료되기 전까지 유레카에 남게되는 문제도 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Graceful shutdown&lt;/b&gt;을 적용했다.&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
  application:
    name: payment-service
  config:
    import: classpath:application-database.yml # database 속성 가져오기용
  profiles:
    default: dev
  lifecycle:
    timeout-per-shutdown-phase: 10s # 여기 1 !!!!

server:
  shutdown: graceful  # 애플리케이션 종료 시 정상 종료 (Graceful Shutdown) # 여기 2 !!!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;spring.lifecycle.timeout-per-shutdown-phase&lt;/b&gt;를 &lt;b&gt;10초&lt;/b&gt;로 주어서 종료 전까지 받은 요청을 처리하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;server.shutdown&lt;/b&gt;을 &lt;b&gt;graceful&lt;/b&gt;로 설정해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 컴포즈&lt;/h3&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
    payment-server:
        # // ... 
      stop_grace_period: 15s # 여기 1 !!!
      stop_signal: SIGTERM # 여기 2 !!!
      depends_on:
        - eureka-server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;stop_signal&lt;/b&gt;을 &lt;b&gt;SIGTERM&lt;/b&gt;을 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;stop_grace_period&lt;/b&gt;를 &lt;b&gt;15초&lt;/b&gt;를 주어서, 스프링의 10초보다 5초 넉넉히 주어서 스프링이 종료 후 도커 컨테이너가 종료되도록 했다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>docker</category>
      <category>DockerCompose</category>
      <category>gracefulshutdown</category>
      <category>Java</category>
      <category>SHUTDOWN</category>
      <category>Spring</category>
      <category>도커</category>
      <category>도커컴포즈</category>
      <category>스프링</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/217</guid>
      <comments>https://promisingmoon.tistory.com/217#entry217comment</comments>
      <pubDate>Wed, 13 Nov 2024 15:15:10 +0900</pubDate>
    </item>
    <item>
      <title>TIL #115 : 자바에서 offset 있는 DateTime 파싱하기</title>
      <link>https://promisingmoon.tistory.com/216</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토스페이먼츠 API를 이용하던 중, 응답으로 주던 날짜 형식 &lt;b&gt;&quot;2024-10-02T21:16:13+09:00&quot;&lt;/b&gt; 이 &lt;b&gt;LocalDateTime으로&lt;/b&gt; 파싱이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;OffsetDateTime을&lt;/b&gt; 이용해서 파싱 할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. LocalDateTime 파싱 에러 발생&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1731477477002&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;LocalDateTime 파싱 - 에러남&quot;)
public void testLocalDateTimeParseException() {
    // given
    String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

    // when &amp;amp; then
    assertThatThrownBy(() -&amp;gt; LocalDateTime.parse(rawTime))
        .isInstanceOf(java.time.format.DateTimeParseException.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LocalDateTime으로 파싱 시, DateTimeParseException이 발생한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. OffsetDateTime 파싱&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1731477531974&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;OffsetDateTime 파싱&quot;)
public void testOffsetDateTimeParsing() {
    // given
    String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

    // when
    OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);

    // then
    assertThat(offsetDateTime.getOffset()).isEqualTo(ZoneOffset.of(&quot;+09:00&quot;));
}

@Test
@DisplayName(&quot;OffsetDateTime 파싱 후 LocalDateTime 변환&quot;)
public void testOffsetDateTimeToLocalDateTime() {
    // given
    String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;
    OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);

    // when
    LocalDateTime localDateTime = offsetDateTime.toLocalDateTime();

    // then
    assertThat(localDateTime.getHour()).isEqualTo(21);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OffsetDateTime으로 변환하면 예외가 발생하지 않는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Offset을 가져오면 &quot;+09:00&quot;가 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &quot;Asia/Seoul&quot;로 변환하고 싶었는데, 아쉽게도 +9 시간대를 쓰는 시간대가 여러 개가 있을 수 있어서 변환이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. offset 조정&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1731477642466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;OffsetDateTime 조정 - 2시간 차이의 시간대로 변경&quot;)
public void testOffsetDateTimeAdjustment() {
    // given
    String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

    // when
    OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);
    OffsetDateTime adjusted = offsetDateTime.withOffsetSameInstant(ZoneOffset.of(&quot;+07:00&quot;));

    // then
    assertThat(adjusted.getHour()).isEqualTo(19);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset을 +7로 조정이 잘 되는지 테스트해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 21시였던 것이, 19시로 변경이 잘 된 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 테스트 코드&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1731477440311&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class ZonedDateTimeTest {

    @Test
    @DisplayName(&quot;LocalDateTime 파싱 - 에러남&quot;)
    public void testLocalDateTimeParseException() {
        // given
        String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

        // when &amp;amp; then
        assertThatThrownBy(() -&amp;gt; LocalDateTime.parse(rawTime))
            .isInstanceOf(java.time.format.DateTimeParseException.class);
    }

    @Test
    @DisplayName(&quot;OffsetDateTime 파싱&quot;)
    public void testOffsetDateTimeParsing() {
        // given
        String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

        // when
        OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);

        // then
        assertThat(offsetDateTime.getOffset()).isEqualTo(ZoneOffset.of(&quot;+09:00&quot;));
    }

    @Test
    @DisplayName(&quot;OffsetDateTime 파싱 후 LocalDateTime 변환&quot;)
    public void testOffsetDateTimeToLocalDateTime() {
        // given
        String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;
        OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);

        // when
        LocalDateTime localDateTime = offsetDateTime.toLocalDateTime();

        // then
        assertThat(localDateTime.getHour()).isEqualTo(21);
    }

    @Test
    @DisplayName(&quot;OffsetDateTime 조정 - 2시간 차이의 시간대로 변경&quot;)
    public void testOffsetDateTimeAdjustment() {
        // given
        String rawTime = &quot;2024-10-02T21:16:13+09:00&quot;;

        // when
        OffsetDateTime offsetDateTime = OffsetDateTime.parse(rawTime);
        OffsetDateTime adjusted = offsetDateTime.withOffsetSameInstant(ZoneOffset.of(&quot;+07:00&quot;));

        // then
        assertThat(adjusted.getHour()).isEqualTo(19);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>TIL</category>
      <category>datetime</category>
      <category>Java</category>
      <category>LocalDateTime</category>
      <category>OffsetDateTime</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/216</guid>
      <comments>https://promisingmoon.tistory.com/216#entry216comment</comments>
      <pubDate>Wed, 13 Nov 2024 15:03:42 +0900</pubDate>
    </item>
    <item>
      <title>TIL #114 : Spring Security 없이 PasswordEncoder 이용하기</title>
      <link>https://promisingmoon.tistory.com/215</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간편결제를 구현하기 위해 6자리 숫자 비밀번호를 도입했다. 근데 평문으로 저장할 순 없으니 암호화를 해야 하는데, 스프링 시큐리티까지는 필요 없이 비밀번호 암호화만 하면 되었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;dependencies {
		
    // ...

    // spring security crypto
    implementation 'org.springframework.security:spring-security-crypto' // 여기 !!!
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;spring-security-crypto&lt;/b&gt; 라이브러리를 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시큐리티 없이 &lt;b&gt;PasswordEncoder&lt;/b&gt;를 이용할 수 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731471185468&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import radiata.common.exception.BusinessException;
import radiata.common.message.ExceptionMessage;

@Service
@RequiredArgsConstructor
public class PayUserValidator {

    private static final Pattern PASSWORD_PATTERN = Pattern.compile(&quot;^[0-9]{6}$&quot;);
    private final PasswordEncoder passwordEncoder;

    public void validatePassword(String payUserPassword, String inputPassword) {
        if (!PASSWORD_PATTERN.matcher(inputPassword).matches()) {
            throw new BusinessException(ExceptionMessage.INVALID_PASSWORD);
        }

        if (!passwordEncoder.matches(inputPassword, payUserPassword)) {
            throw new BusinessException(ExceptionMessage.INVALID_PASSWORD);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증할 땐 우선 숫자 6자리인지 검증 후, 그 비밀번호가 DB에 저장된 비밀번호와 맞는지 확인하도록 했다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Java</category>
      <category>passwordencoder</category>
      <category>Security</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>스프링</category>
      <category>스프링시큐리티</category>
      <category>시큐리티</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/215</guid>
      <comments>https://promisingmoon.tistory.com/215#entry215comment</comments>
      <pubDate>Wed, 13 Nov 2024 13:17:04 +0900</pubDate>
    </item>
    <item>
      <title>TIL #113 : Assertj로 LocalDateTime.now() 단위 테스트 코드 작성하기</title>
      <link>https://promisingmoon.tistory.com/214</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 엔티티에서 결제 승인 메서드를 실행하면 결제 승인 시간에 자동으로 현재 시간으로 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동으로 넣어준 LocalDateTIme을 어떻게 테스트할지 궁금했는데, 특정 시간과 가까운지 테스트하는 코드가 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&amp;nbsp;&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;/**
 * 결제 승인
 */
public void approve() {
    this.status = PaymentStatus.APPROVED;
    this.approvedAt = LocalDateTime.now(); // 여기 !!!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결제 승인 메서드 실행 시 approvedAt에 현재 시간을 넣어준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 테스트 코드&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import static org.assertj.core.api.Assertions.assertThat;

// ... 

@Test
@DisplayName(&quot;결제 성공&quot;)
void payment_approve() {
    // given 
    TemporalUnitWithinOffset acceptableTimeOffset = new TemporalUnitWithinOffset(1, ChronoUnit.SECONDS);

    // when
    payment.approve();

    // then
    assertThat(payment.getStatus()).isEqualTo(PaymentStatus.APPROVED);
    assertThat(payment.getApprovedAt()).isCloseTo(LocalDateTime.now(), acceptableTimeOffset);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 코드 설명&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;TemporalUnitWithinOffset acceptableTimeOffset = new TemporalUnitWithinOffset(1, ChronoUnit.SECONDS);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TemporalUnitWithinOffset&lt;/b&gt;를 통해 &lt;b&gt;Second 단위로 1인 범위&lt;/b&gt;(즉 1초 이내인지)의 객체를 생성한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;assertThat(payment.getApprovedAt()).isCloseTo(LocalDateTime.now(), acceptableTimeOffset);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;isCloseTo&lt;/b&gt;를 통해 &lt;b&gt;현재 시간과 오프셋 이내로 차이가 나는지 확인&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유닛테스트라서,, 테스트가 아무리 느려도 1초가 걸리지는 않을 거라고 생각이 들어서.. 이렇게 해봤다. 컴퓨터 환경에 따라 조절하면 될 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>assertj</category>
      <category>Java</category>
      <category>JUnit</category>
      <category>Spring</category>
      <category>단위테스트</category>
      <category>스프링</category>
      <category>유닛테스트</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/214</guid>
      <comments>https://promisingmoon.tistory.com/214#entry214comment</comments>
      <pubDate>Wed, 13 Nov 2024 13:03:41 +0900</pubDate>
    </item>
    <item>
      <title>TIL #112 : 자바 애플리케이션을 도커 이미지로 만들 때 용량 줄이기</title>
      <link>https://promisingmoon.tistory.com/213</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hub.docker.com/layers/library/eclipse-temurin/17-jre-alpine/images/sha256-d69f8cf3526fd75992366d2e96348682dfbc04c5d321c08d084e1fc26980d81d&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;eclipse-temurin:17-jre-alpine&lt;/b&gt;&lt;/a&gt;으로 이미지 굽자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느날 문득, 서버에서 배포할 때 이미 빌드되어 jar 파일만 실행한다면, JDK가 아닌 JRE만 있어도 되는 거 아닌가? 하는 생각이 들었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 배포할 때는 도커 이미지로 구워서 띄워버려서, JRE를 사용하면 용량을 더 줄일 수 있을 것 같아서 한번 알아봤다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.32.34.png&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;222&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuwEpx/btsKFIOB9qm/dCkOYSI0inzEjZ67jSFAc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuwEpx/btsKFIOB9qm/dCkOYSI0inzEjZ67jSFAc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuwEpx/btsKFIOB9qm/dCkOYSI0inzEjZ67jSFAc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuwEpx%2FbtsKFIOB9qm%2FdCkOYSI0inzEjZ67jSFAc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;558&quot; height=&quot;222&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.32.34.png&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;222&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openjdk를 우선으로 찾아보았는데 내 서치 실력이 부족한 건지 17-jre만 있는 것은 찾을 수가 없어서, temurin으로 테스트해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상대로 &lt;b&gt;17-JDK (419.47 MB)&lt;/b&gt; &amp;gt; &lt;b&gt;17-JDK-alpine (335.97 MB)&lt;/b&gt; &amp;gt; &lt;b&gt;17-JRE (262.53 MB)&lt;/b&gt; &amp;gt; &lt;b&gt;17-JRE-alpine (185.34 MB)&lt;/b&gt; 순으로 용량이 줄어들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JDK&lt;/b&gt;에서 &lt;b&gt;JRE&lt;/b&gt;로 바꾸기만 해도 &lt;b&gt;용량이 반토막&lt;/b&gt;이 나고, &lt;b&gt;alpine&lt;/b&gt;으로 바꾸면 거기에 또 약 &lt;b&gt;80 MB가 줄어든다!!!&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 alpine 이란? 보안과 경량화를 목표로 설계된 alpine linux를 기반으로 하여 이미지 크기가 작아 저장 공간을 절약하고 업로드/다운로드 시간이 줄어드는 장점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;jar 이미지 굽기&lt;/h2&gt;
&lt;pre id=&quot;code_1731466882816&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FROM eclipse-temurin:17-jre-alpine
# FROM --platform=linux/amd64 eclipse-temurin:17-jre-alpine # ARM 사용 시 

RUN mkdir /app

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} /app/app.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위 이미지와 도커파일을 토대로 JAR 파일을 만들어보면,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.52.03.png&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oT9bG/btsKFg52r8j/T5PePAlBGUTs2cGtjjZ5aK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oT9bG/btsKFg52r8j/T5PePAlBGUTs2cGtjjZ5aK/img.png&quot; data-alt=&quot;17-jre-alpine&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oT9bG/btsKFg52r8j/T5PePAlBGUTs2cGtjjZ5aK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoT9bG%2FbtsKFg52r8j%2FT5PePAlBGUTs2cGtjjZ5aK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;409&quot; height=&quot;45&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.52.03.png&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;45&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;17-jre-alpine&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.52.50.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjLllX/btsKGI1m9cD/mEg64Kqeg3YFQfzJistYFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjLllX/btsKGI1m9cD/mEg64Kqeg3YFQfzJistYFk/img.png&quot; data-alt=&quot;17-jre&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjLllX/btsKGI1m9cD/mEg64Kqeg3YFQfzJistYFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjLllX%2FbtsKGI1m9cD%2FmEg64Kqeg3YFQfzJistYFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;42&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.52.50.png&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;17-jre&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.53.33.png&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;40&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eoRHEt/btsKHITBtOL/GNTnn0xoquTBfT74bMk2B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eoRHEt/btsKHITBtOL/GNTnn0xoquTBfT74bMk2B0/img.png&quot; data-alt=&quot;17-jdk-alpine&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eoRHEt/btsKHITBtOL/GNTnn0xoquTBfT74bMk2B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeoRHEt%2FbtsKHITBtOL%2FGNTnn0xoquTBfT74bMk2B0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;391&quot; height=&quot;40&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.53.33.png&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;40&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;17-jdk-alpine&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.53.43.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;38&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yqLDL/btsKGAP1uQV/Z42CyDuO1mfv2ciJhPFH5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yqLDL/btsKGAP1uQV/Z42CyDuO1mfv2ciJhPFH5K/img.png&quot; data-alt=&quot;17-jdk&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yqLDL/btsKGAP1uQV/Z42CyDuO1mfv2ciJhPFH5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyqLDL%2FbtsKGAP1uQV%2FZ42CyDuO1mfv2ciJhPFH5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;38&quot; data-filename=&quot;스크린샷 2024-11-13 오전 11.53.43.png&quot; data-origin-width=&quot;397&quot; data-origin-height=&quot;38&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;17-jdk&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드된 jar 파일이 66.15 MB 이었는데, 딱 jar 파일만큼 용량이 늘어났다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 완벽한 것 같은 jre-alpine도 한 가지 단점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ARM을 지원하지 않는다는 것... 그래서 --platform=linux/amd64 를 넣었던 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2024-11-13 오후 12.06.50.png&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tHwoJ/btsKFBPEJ9d/QDvnxwjT4fkHk1JmBcG4W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tHwoJ/btsKFBPEJ9d/QDvnxwjT4fkHk1JmBcG4W0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tHwoJ/btsKFBPEJ9d/QDvnxwjT4fkHk1JmBcG4W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtHwoJ%2FbtsKFBPEJ9d%2FQDvnxwjT4fkHk1JmBcG4W0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;487&quot; data-filename=&quot;edited_스크린샷 2024-11-13 오후 12.06.50.png&quot; data-origin-width=&quot;511&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;17-jre는 멀티 플랫폼을 지원한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://bcho.tistory.com/1356&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;티스토리 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>alpine</category>
      <category>docker</category>
      <category>DockerImage</category>
      <category>image</category>
      <category>JAR</category>
      <category>Java</category>
      <category>jdk</category>
      <category>jre</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/213</guid>
      <comments>https://promisingmoon.tistory.com/213#entry213comment</comments>
      <pubDate>Wed, 13 Nov 2024 12:22:23 +0900</pubDate>
    </item>
    <item>
      <title>TIL #111 : ID를 가진 JPA 엔티티 생성 시 SELECT 문이 나가는 문제</title>
      <link>https://promisingmoon.tistory.com/212</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 JPA 엔티티를 &lt;b&gt;Ksuid&lt;/b&gt;라고 하는 고유 식별자를 사용하기로 했다. UUID 같은거다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성시간 기반의 20byte 고유 식별자로, 시간순으로 정렬이 가능해서 인덱싱의 이점을 누리면서도 랜덤값도 포함되어 중복 가능성이 거의 없으며, 길이도 짧다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 엔티티를 저장할 때 id를 넣어서 생성하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731305089190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public &amp;lt;S extends T&amp;gt; S save(S entity) {
    Assert.notNull(entity, &quot;Entity must not be null&quot;);
    if (this.entityInformation.isNew(entity)) {
        this.entityManager.persist(entity);
        return entity;
    } else {
        return this.entityManager.merge(entity);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 JpaRepository의 &lt;b&gt;save&lt;/b&gt; 메서드는 isNew 메서드를 통해 현재 엔티티가 새로 생성되었는지 여부를 확인하는데,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731305148188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transient
public boolean isNew() {
    return this.getId() == null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 조건은&lt;b&gt; id가 null 인 경우&lt;/b&gt;다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 id를 넣어준 후 save 하기 때문에 항상 isNew가 false 가 되어 persist가 아닌 merge를 실행하게 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731305669549&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select c1_0.id,ci1_0.coupon_id,ci1_0.id,ci1_0.status,ci1_0.user_id,c1_0.max_quantity,c1_0.name,c1_0.now_quantity,c1_0.version from coupons c1_0 left join coupon_issues ci1_0 on c1_0.id=ci1_0.coupon_id where c1_0.id=?
insert into coupons (max_quantity,name,now_quantity,version,id) values (?,?,?,?,?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그러면 생성 전에, id가 있는지 여부를 확인하기 위해 select 문이 나가고, id가 없으면 Insert 문이 나가게 된다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1731305280616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PostPersist;
import lombok.Getter;
import org.springframework.data.domain.Persistable;

@Getter
@MappedSuperclass
public abstract class BaseEntity implements Persistable&amp;lt;String&amp;gt; {

    private transient boolean isNew = true; // 새로운 엔티티 여부를 추적할 필드

    @Override
    public boolean isNew() {
        return this.isNew;
    }

    // persist 후에 기존 엔티티로 인식되도록 설정
    @PostPersist
    public void postPersist() {
        this.isNew = false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 엔티티에 대해서 Ksuid를 사용하기 때문에, 이를 BaseEntity에 넣어서 각각의 엔티티가 중복 구현하지 않도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;Persistable&amp;lt;T&amp;gt; 인터페이스&lt;/b&gt;를 구현했는데, 여기서 &lt;b&gt;T는 키의 타입&lt;/b&gt;을 넣어주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isNew 라는 변수를 통해 현재 엔티티가 새로 생성되었는지 아닌지를 판단하도록 했고, private를 통해 자식 엔티티가 접근하지 못하도록 하고, transient를 통해 DB에 실제로 저장이 되지 않도록 했다. 그리고 기본값으로 true를 두었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 @PostPersist 어노테이션을 통해, 새로 생성 시 isNew는 false가 되도록 했고, isNew 메서드를 override 해서 변수를 리턴하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731305486851&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.Set;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = &quot;coupons&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Coupon extends BaseEntity { // 여기!!!

    @Id
    private String id;

    private String name;
    private Integer nowQuantity;
    private Integer maxQuantity;

    @OneToMany(mappedBy = &quot;coupon&quot;, cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
    private Set&amp;lt;CouponIssue&amp;gt; couponIssues;

    public static Coupon create(String id, String name, Integer nowQuantity, Integer maxQuantity) {
        Coupon coupon = new Coupon();

        coupon.id = id;
        coupon.name = name;
        coupon.nowQuantity = nowQuantity;
        coupon.maxQuantity = maxQuantity;

        return coupon;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제 엔티티에서는 BaseEntity를 상속받아서 사용하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 엔티티에서는 생성인지 영속인지 모르고 사용할 수 있다!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1731305597217&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insert into coupons (max_quantity,name,now_quantity,version,id) values (?,?,?,?,?)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; insert 문만 나가는 것을 볼 수 있다!!&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Entity</category>
      <category>Java</category>
      <category>JPA</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/212</guid>
      <comments>https://promisingmoon.tistory.com/212#entry212comment</comments>
      <pubDate>Mon, 11 Nov 2024 15:18:14 +0900</pubDate>
    </item>
    <item>
      <title>24/10/04(금) 110번째 TIL : 컴포넌트 스캔 패키지 구조 문제</title>
      <link>https://promisingmoon.tistory.com/211</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티 모듈 구조에서 JpaRepository 빈을 읽어오지 못하는 문제가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 멀티 모듈을 채택했고, 스프링 앱, 도메인 코어, 데이터베이스 모듈로 3개를 분리했다. 스프링 앱에서 실행 시, 데이터베이스 모듈의 JpaConfig 설정을 읽어오지 못해서 도메인 코어의 repository에 JpaRepository를 빈이 없다고 주입을 못해주고 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 현제 문제인 것들만 정리하자면&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;payment-core : 결제 도메인의 핵심 부분 (서비스, 도메인, 리포지토리 포함)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 위치 : &lt;b&gt;service:payment:core&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;패키지 시작점 : &lt;b&gt;radiata.service.payment.core&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;payment-api : 결제 도메인의 외부 요청 부분 (스프링 애플리케이션, 컨트롤러, 앤드포인트 포함)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 위치 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;service:payment:api&amp;nbsp;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;패키지 시작점 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;radiata.service.payment.api&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;database : 데이터베이스 관련 부분 (설정 yml 및 Config 객체 포함)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 위치 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;database&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;패키지 시작점 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;radiata.database&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;객체&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;database 모듈&lt;/b&gt;에는 &lt;b&gt;JpaConfig&lt;/b&gt;가 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;payment-core 모듈&lt;/b&gt;&amp;nbsp;에는 &lt;b&gt;Repository&lt;/b&gt;, &lt;b&gt;JpaRepository&lt;/b&gt;가 있고 (둘을 분리해둠),&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;payment-api 모듈&lt;/b&gt; 에서는 &lt;b&gt;PaymentApplication&lt;/b&gt;의 스프링 앱이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텔리제이 측에서는 Repository가 빈이 있다고 표시되지만, 실제 실행 시 찾을 수 없다고 떴다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package radiata.service.payment.core;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
public class ComponentScanConfiguration {

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;payment-core&lt;/b&gt;의 최상의 패키지에 위 객체를 두면 해결이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...가 아니라 하나 더 해야 된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package radiata.service.payment.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = { &quot;radiata.service.payment&quot; })
public class PaymentApplication {

    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;payment-api 모듈&lt;/b&gt;의 스프링 애플리케이션에 스캔 패키지를 지정해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728273735476&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication(scanBasePackages = { &quot;radiata.service.payment&quot; })
@SpringBootApplication(scanBasePackages = { &quot;radiata.database&quot; })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 하나를 해주면 실행이 된다. 하지만 database 모듈은 언제든지 실행이될 수 있으니 위의 코드를 쓰기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;b&gt;api 모듈&lt;/b&gt;은 &lt;b&gt;radiata.service.payment.api&lt;/b&gt;에 &lt;b&gt;SpringApplication이 있는 최상위 패키지&lt;/b&gt;이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;core 모듈&lt;/b&gt;은 &lt;b&gt;radiata.service.payment.core&lt;/b&gt;가 &lt;b&gt;최상위 패키지&lt;/b&gt;라서,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘의 상위인 &lt;b&gt;radiata.service.payment&lt;/b&gt;로 지정을 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후로 스프링이 빈을 스캔하는 방식을 더 공부해서 정리해볼 예정이다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Java</category>
      <category>Spring</category>
      <category>Til</category>
      <category>스캔</category>
      <category>스프링</category>
      <category>스프링 스캔</category>
      <category>컴포넌트 스캔</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/211</guid>
      <comments>https://promisingmoon.tistory.com/211#entry211comment</comments>
      <pubDate>Mon, 7 Oct 2024 13:08:08 +0900</pubDate>
    </item>
    <item>
      <title>24/09/30(월) 109번째 TIL : 카프카 직렬화 및 역직렬화</title>
      <link>https://promisingmoon.tistory.com/210</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 :&amp;nbsp;카프카 직렬화/역직렬화 처리 중 만난 에러들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하도 많이 만나서 일단 생각나는 것만 적고..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 1 : 객체로 요청 및 응답받고 싶으면 Value의 시리얼라이저를 Json으로 하기.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 문자열로도 가능하긴 한데, 요청/응답마다 ObjectMapper로 직렬화/역직렬화하기보다, 카프카에서도 JsonSerializer를 제공해 주는데 굳이 문자열로 쓸 필요는 없을 것 같다. 또 컴파일 때 요청 보낼 객체의 타입검사를 해주기도 하고.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727601920055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.application.order.message.DeliveryMessage;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&amp;lt;String, DeliveryMessage&amp;gt; producerFactory() { // 여기 제네릭 부분 !!!
        Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
            ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
            ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
            ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class, // 여기 !!!
        );

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProperties);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, DeliveryMessage&amp;gt; kafkaTemplate() { // 여기 제네릭 부분 !!!
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 2 : 컨슈머 측은 역직렬화로 설정해주어야 한다.&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 당연한 건데.. 컨슈머 측에서 시리얼라이저로 했었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727602056489&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.application.product.message.DeliveryMessage;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;

@Configuration
@EnableKafka
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&amp;lt;String, DeliveryMessage&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, // 여기 !!!
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class, // 여기 !!!
        );

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(configProperties);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, DeliveryMessage&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, DeliveryMessage&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG 속성의 값을 JsonDeserializer.class 로 해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConsumerConfig 인 것도 몰랐고,, 복붙하느라,, Deserializer 인 줄도 몰랐다...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;에러 3 : 프로듀서, 컨슈머 측에서 서로 다른 패키지 및 타입 정보를 맞춰줘야 한다.&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맞다.. 난 이것도 모르는 감자다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위 코드로 보내면 컨슈머 측에서 에러난다.&lt;/p&gt;
&lt;pre id=&quot;code_1727602734818&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// key 
0059d5e2-dcfc-4cc7-853c-0207890c772f

// value
{
	&quot;orderId&quot;: &quot;0059d5e2-dcfc-4cc7-853c-0207890c772f&quot;,
	&quot;paymentId&quot;: null,
	&quot;userId&quot;: &quot;yun01&quot;,
	&quot;productId&quot;: 1,
	&quot;productQuantity&quot;: 10,
	&quot;payAmount&quot;: 100,
	&quot;errorType&quot;: null
}

// headers // 여기 !!!
{
	&quot;__TypeId__&quot;: &quot;ex.application.order.message.DeliveryMessage&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 프로듀서 측에서는 DeliveryMessage 객체의 위치를 헤더에 담아서 보낸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컨슈머 측의 DeliveryMessage 객체의 위치는 ex.application.product.message.DeliveryMessage이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 컨슈머는 ex.application.&lt;b&gt;order&lt;/b&gt;.message.DeliveryMessage 에서 DeliveryMessage객체를 찾다가 없어서 에러를 뱉는다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727602626797&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ProducerFactory&amp;lt;String, DeliveryMessage&amp;gt; producerFactory() {
    Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class,
        JsonSerializer.ADD_TYPE_INFO_HEADERS, false // 여기!!!
    );

    return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProperties);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 나는 프로젝트마다 패키지명을 다르게 해 줄 생각이어서 프로듀서 측에서 타입 정보를 보내지 않도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1727602653426&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public ConsumerFactory&amp;lt;String, DeliveryMessage&amp;gt; consumerFactory() {
    Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
        ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
        ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class,
        ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class,
        JsonDeserializer.USE_TYPE_INFO_HEADERS, false, // 여기 !!!
        JsonDeserializer.VALUE_DEFAULT_TYPE, &quot;ex.application.product.message.DeliveryMessage&quot; // 여기 !!!
    );

    return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(configProperties);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 컨슈머 측에서는 어차피 필요 없는 타입 정보를 사용하지 않도록 하고, 컨슈머 프로젝트의 DeliveryMessage 객체의 위치를 지정해 주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결..!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 에러 없는 코드&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로듀서 측 코드&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1727600579312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.application.order.message.DeliveryMessage;
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

@Configuration
public class KafkaProducerConfig {

    @Bean
    public ProducerFactory&amp;lt;String, DeliveryMessage&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
            ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
            ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class,
            ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class,
            JsonSerializer.ADD_TYPE_INFO_HEADERS, false
        );

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(configProperties);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, DeliveryMessage&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨슈머 측 코드&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1727600699146&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import ex.application.product.message.DeliveryMessage;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;

@Configuration
@EnableKafka
public class KafkaConsumerConfig {

    @Bean
    public ConsumerFactory&amp;lt;String, DeliveryMessage&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; configProperties = Map.of(
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;localhost:9092&quot;,
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class,
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class,
            JsonDeserializer.USE_TYPE_INFO_HEADERS, false, 
            JsonDeserializer.VALUE_DEFAULT_TYPE, &quot;ex.application.product.message.DeliveryMessage&quot; 
        );

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(configProperties);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, DeliveryMessage&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, DeliveryMessage&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/210</guid>
      <comments>https://promisingmoon.tistory.com/210#entry210comment</comments>
      <pubDate>Tue, 1 Oct 2024 10:04:38 +0900</pubDate>
    </item>
    <item>
      <title>24/08/28(수) 108번째 TIL : EmbeddedId 식별자 값객체</title>
      <link>https://promisingmoon.tistory.com/209</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값객체를 엔티티의 식별자에도 사용하기 시작했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725887784726&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class HubId implements Serializable { // JPA 식별자 타입은 Serializable 구현해야 함

    @Column(name = &quot;id&quot;)
    private UUID id;

    public static HubId of(UUID id) {
        HubId hubId = new HubId();
        hubId.id = id;
        return hubId;
    }

    public static HubId ofRandom() {
        HubId hubId = new HubId();
        hubId.id = UUID.randomUUID();
        return hubId;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구사항이 식별자는 UUID여서 랜덤의 UUID를 가지는 생성자를 정적 팩토리 메소드로 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725887797592&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Entity
@Table(name = &quot;p_hub&quot;)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Hub extends BaseEntity {

    @EmbeddedId
    private final HubId id = HubId.ofRandom();

    @Embedded
    private Address address;

    @Embedded
    private Coordinate coordinate;

    @OneToMany(mappedBy = &quot;hub&quot;)
    private List&amp;lt;Inventory&amp;gt; inventoryList = new ArrayList&amp;lt;&amp;gt;();

    @Builder
    private Hub(Address address, Coordinate coordinate) {
        this.address = address;
        this.coordinate = coordinate;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이렇게 @EmbeddedId 어노테이션으로 값객체를 식별자로 사용한다는 것을 표시했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA에서 식별자 타입은 Serializable 타입이어야 해서 이를 구현해주면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값객체를 식별자로 사용하면 아래와 같은 이점이 있다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;식별자라는 의미를, 그것도 Hub 엔티티의 식별자라는 의미를 부각시킬 수 있다.&lt;/li&gt;
&lt;li&gt;하나의 UUID 필드가 아닌, &quot;객체&quot; 이므로 기능을 추가할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위와 같이, UUID를 HubId로 변환한다든지, 랜덤한 UUID로 HubId를 만드는 기능을 추가했다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/209</guid>
      <comments>https://promisingmoon.tistory.com/209#entry209comment</comments>
      <pubDate>Mon, 9 Sep 2024 22:22:34 +0900</pubDate>
    </item>
    <item>
      <title>24/08/27(화) 107번째 TIL : Embeddable 값객체</title>
      <link>https://promisingmoon.tistory.com/208</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 DDD 개념을 일부 도입해 보면서 값객체를 시도해 볼 수 있는 부분에 적용을 해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷에 유명한 Money나 Address는 당연히 구현해봤고, 아래는 그 외에 잘 구현했다고 생각되는 것을 적어봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725885911818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Rating {

    private static final int MIN_SCORE = 1;
    private static final int MAX_SCORE = 5;

    private Integer score;

    public Rating(int score) {
        if (!isScoreInRange(score)) {
            throw new GlobalException(ResultCase.INVALID_INPUT);
        }

        this.score = score;
    }

    private boolean isScoreInRange(int score) {
        return MIN_SCORE &amp;lt;= score &amp;amp;&amp;amp; score &amp;lt;= MAX_SCORE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;final이나 Record 타입으로 하고 싶었는데, JPA는 기본 생성자를 필요로 해서 할 수 없었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 요구사항 중, 리뷰에 평점을 1~5점 남길 수 있다는 요구사항이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 int 타입으로 score를 표현할 수도 있겠지만, 단순히 int 필드보고 얘가 평점이라고 가스라이팅 하기보다, 보다 명확하게 평점임을 표시하고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Rating 이라는 객체를 만들었고, 일단 생성하면 더 이상 값을 변경하지 못하게 하여 불변을 보장했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 생성 시 정책이었던 1~5 사이 값인지를 검증하는 로직을 넣어서 일단 생성만 하면 이후 추가 검증을 할 필요 없이 안심하며 값을 사용할 수 있도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 상태와 행위를 한 곳에서 관리하여 응집도를 높일 수도 있다!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>embeddable</category>
      <category>Embedded</category>
      <category>JPA</category>
      <category>Value</category>
      <category>valueobject</category>
      <category>값객체</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/208</guid>
      <comments>https://promisingmoon.tistory.com/208#entry208comment</comments>
      <pubDate>Mon, 9 Sep 2024 22:11:53 +0900</pubDate>
    </item>
    <item>
      <title>24/08/26(월) 106번째 TIL : mapStruct와 @Getter 사용 시 boolean 필드 매핑</title>
      <link>https://promisingmoon.tistory.com/206</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Review 엔티티에 기본형 boolean 타입의 isReported 필드가 있었는데, 이를 DTO로 변환하는 mapStruct 매퍼를 사용 중, 컴파일 시 아래와 같은 경고가 떴다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725159675704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReviewMapper.class

// ...
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import static org.mapstruct.MappingConstants.ComponentModel.SPRING;

@Mapper(componentModel = SPRING)
public interface ReviewMapper {

    ReviewResponseDto toReviewResponseDto(Review review);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725159754790&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReviewResponseDto.class

public record ReviewResponseDto(
        // ...
        boolean isReported
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1725159646240&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ReviewMapper.java:17: warning: Unmapped target property: &quot;isReported&quot;.
    ReviewResponseDto toReviewResponseDto(Review review);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1725159895093&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReviewMapper 인터페이스의 구현체 

@Component
public class ReviewMapperImpl implements ReviewMapper {

    @Override
    public ReviewResponseDto toReviewResponseDto(Review review) {
        if ( review == null ) {
            return null;
        }
        
        // ...

        boolean isReported = false; // 여기 !!!

        ReviewResponseDto reviewResponseDto = new ReviewResponseDto( // ... , isReported );

        return reviewResponseDto;
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일된 매퍼를 보니 isReported가 false로 초기화 되어있고 따로 review에서 값을 가져오지는 않았다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1725160104906&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReviewMapper.class

// ...
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import static org.mapstruct.MappingConstants.ComponentModel.SPRING;

@Mapper(componentModel = SPRING)
public interface ReviewMapper {

    @Mapping(target = &quot;isReported&quot;, source = &quot;reported&quot;) // 여기 !!!
    ReviewResponseDto toReviewResponseDto(Review review);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이, is- 접두사를 제거한 reported 로 해두면 적용이 된다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설명&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1725160080826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;p_review&quot;)
public class Review extends BaseEntity {

    // ...
    
    @Column(name = &quot;is_reported&quot;)
    private boolean isReported;
    
    // ...

    @Generated // 롬복이 만들어준 코드는 @Generated 어노테이션이 붙음 
    public boolean isReported() {
        return this.isReported;
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Getter 롬복 어노테이션을 사용할 때, 기본형 boolean 타입의 경우 is- 프리픽스를 그대로 유지한 isReported() getter가 만들어진다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725161158733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ReviewMapper 인터페이스의 구현체 

@Component
public class ReviewMapperImpl implements ReviewMapper {

    @Override
    public ReviewResponseDto toReviewResponseDto(Review review) {
        if ( review == null ) {
            return null;
        }
        
        // ...

        boolean isReported = review.isReported(); // 여기 !!!

        ReviewResponseDto reviewResponseDto = new ReviewResponseDto( // ... , isReported );

        return reviewResponseDto;
    }
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 source = &quot;reported&quot;로 작성을 해두면 mapStruct는 &lt;a href=&quot;https://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java beans 8.3.2 스펙&lt;/a&gt;에 따라 isReported() getter로 isReported 필드의 값을 가져오도록 코드를 만들어준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>getter</category>
      <category>Java</category>
      <category>lombok</category>
      <category>mapstruct</category>
      <category>Spring</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/206</guid>
      <comments>https://promisingmoon.tistory.com/206#entry206comment</comments>
      <pubDate>Sun, 1 Sep 2024 12:31:07 +0900</pubDate>
    </item>
    <item>
      <title>24/08/23(금) 105번째 TIL : Gradle로 WAR 파일 빌드하기</title>
      <link>https://promisingmoon.tistory.com/205</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 1차 과제의 답안이 공개되어 보던 중 기존의 강의와 해설 영상에서도 다루지 않았던 코드를 발견했다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// ServletInitializer.class

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(YmlApplication.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;war 파일 빌드하는데 쓰이는 녀석이라는 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;War 빌드&lt;/h2&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;// build.gradle

plugins {
    id 'java'
    id 'war' // 추가 !!
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.6'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uqpUR/btsJnobiwqt/GXmePT2L5OAkPWdpcjDBbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uqpUR/btsJnobiwqt/GXmePT2L5OAkPWdpcjDBbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uqpUR/btsJnobiwqt/GXmePT2L5OAkPWdpcjDBbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuqpUR%2FbtsJnobiwqt%2FGXmePT2L5OAkPWdpcjDBbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;592&quot; height=&quot;712&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;2405&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로고침하면 bootWar, war 가 생긴 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build 를 눌러보면&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;오후 5:18:54: 실행 중 'build'...

&amp;gt; Task :compileJava
&amp;gt; Task :processResources UP-TO-DATE
&amp;gt; Task :classes
&amp;gt; Task :resolveMainClassName
&amp;gt; Task :bootWar
&amp;gt; Task :war
&amp;gt; Task :assemble
&amp;gt; Task :compileTestJava UP-TO-DATE
&amp;gt; Task :processTestResources NO-SOURCE
&amp;gt; Task :testClasses UP-TO-DATE
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
&amp;gt; Task :test
&amp;gt; Task :check
&amp;gt; Task :build

BUILD SUCCESSFUL in 24s
7 actionable tasks: 5 executed, 2 up-to-date
오후 5:19:19: 실행이 완료되었습니다 'build'.

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qZVLQ/btsJm9Fp5eU/rE1k4TBr2IOe95aagzVZ11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qZVLQ/btsJm9Fp5eU/rE1k4TBr2IOe95aagzVZ11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qZVLQ/btsJm9Fp5eU/rE1k4TBr2IOe95aagzVZ11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqZVLQ%2FbtsJm9Fp5eU%2FrE1k4TBr2IOe95aagzVZ11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;489&quot; height=&quot;462&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;war 파일이 생성된 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 링크&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://cordcat.tistory.com/103&quot;&gt;https://cordcat.tistory.com/103&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>Gradle</category>
      <category>Spring</category>
      <category>war</category>
      <category>그레들</category>
      <category>그레이들</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/205</guid>
      <comments>https://promisingmoon.tistory.com/205#entry205comment</comments>
      <pubDate>Fri, 30 Aug 2024 19:42:02 +0900</pubDate>
    </item>
    <item>
      <title>24/08/22(목) 104번째 TIL : Redis maxmemory 소숫점 설정</title>
      <link>https://promisingmoon.tistory.com/204</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ubuntu:~$ sudo docker logs redis

*** FATAL CONFIG FILE ERROR (Redis 7.4.0) ***
Reading the configuration file, at line 1133
&amp;gt;&amp;gt;&amp;gt; 'maxmemory 1.5gb'
argument must be a memory value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;maxmemory 설정을 소숫점으로 하니 실행이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1725014031983&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// redis.conf

maxmemory 1500mb

// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MB 단위로 설정해주어 해결했다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>maxmemory</category>
      <category>redis</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/204</guid>
      <comments>https://promisingmoon.tistory.com/204#entry204comment</comments>
      <pubDate>Fri, 30 Aug 2024 19:35:43 +0900</pubDate>
    </item>
    <item>
      <title>24/08/21(수) 103번째 TIL : EC2 EBS 재부팅 없이 용량 확장 도전기</title>
      <link>https://promisingmoon.tistory.com/203</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EBS 용량을 늘려주었음에도 EC2에서는 적용 안 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 이미지들 이것저것 다 다운받다보니 EBS 8GB 설정해둔 게 99% 사용 중&lt;/p&gt;
&lt;pre id=&quot;code_1725013495129&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu@ip:~$ df -h
Filesystem       Size  Used Avail Use% Mounted on
/dev/root        6.8G  6.7G   93M  99% /
tmpfs            208M     0  208M   0% /dev/shm
tmpfs             83M  1.4M   82M   2% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
efivarfs         128K  3.3K  125K   3% /sys/firmware/efi/efivars
/dev/nvme0n1p16  891M  105M  724M  13% /boot
/dev/nvme0n1p15   98M  6.4M   92M   7% /boot/efi
tmpfs             42M   12K   42M   1% /run/user/1000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1702&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0P2fu/btsJlVIfuuo/PEJ5LhIRlqnJAPVTqn3IX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0P2fu/btsJlVIfuuo/PEJ5LhIRlqnJAPVTqn3IX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0P2fu/btsJlVIfuuo/PEJ5LhIRlqnJAPVTqn3IX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0P2fu%2FbtsJlVIfuuo%2FPEJ5LhIRlqnJAPVTqn3IX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1702&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1702&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EBS 용량을 늘려주었다. 하지만 EC2에서 &lt;b&gt;df -h&lt;/b&gt; 해보니 위와 동일했다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사실 재부팅하면 바로 해결되나, 재부팅 없이 적용하고 싶었음&lt;/li&gt;
&lt;li&gt;아래는 재부팅 없이 적용 방법... 인데 실패했다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ubuntu@ip:~$ lsblk
NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0 21.9M  1 loop /snap/amazon-ssm-agent/7994
loop1          7:1    0 33.7M  1 loop /snap/snapd/21761
loop2          7:2    0 49.1M  1 loop /snap/core18/2826
nvme0n1      259:0    0   12G  0 disk
├─nvme0n1p1  259:1    0    7G  0 part /
├─nvme0n1p15 259:2    0   99M  0 part /boot/efi
└─nvme0n1p16 259:3    0  923M  0 part /boot&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;lsblk&lt;/b&gt; 명령어를 통해 블록 장치 목록을 확인한다. 이때 마운트 되지 않은 블록까지 포함됨.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ubuntu@ip:~$ sudo growpart /dev/nvme0n1 1
CHANGED: partition=1 start=2099200 old: size=14677983 end=16777182 new: size=23066591 end=25165790
ubuntu@ip-172-31-43-127:~$ lsblk
NAME         MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0 21.9M  1 loop /snap/amazon-ssm-agent/7994
loop1          7:1    0 33.7M  1 loop /snap/snapd/21761
loop2          7:2    0 49.1M  1 loop /snap/core18/2826
nvme0n1      259:0    0   12G  0 disk
├─nvme0n1p1  259:1    0   11G  0 part /
├─nvme0n1p15 259:2    0   99M  0 part /boot/efi
└─nvme0n1p16 259:3    0  923M  0 part /boot
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 아래쪽 &lt;b&gt;nvme0n1&lt;/b&gt; (인스턴스 타입에 따라 다를 수 있음) 참고하여 &lt;b&gt;sudo growpart /dev/nvme0n1 1 &lt;/b&gt;명령어를 쳐주면 파티션이 확장된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ubuntu@ip:~$ df -h
Filesystem       Size  Used Avail Use% Mounted on
/dev/root        6.8G  6.7G   93M  99% /
tmpfs            208M     0  208M   0% /dev/shm
tmpfs             83M  1.3M   82M   2% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
efivarfs         128K  3.3K  125K   3% /sys/firmware/efi/efivars
/dev/nvme0n1p16  891M  105M  724M  13% /boot
/dev/nvme0n1p15   98M  6.4M   92M   7% /boot/efi
tmpfs             42M   12K   42M   1% /run/user/1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 파티션만 확장됐을 뿐, 파일 시스템은 확장되지 않아서 여전히 99%를 차지하고 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ubuntu@ip:~$ sudo resize2fs /dev/nvme0n1
resize2fs 1.47.0 (5-Feb-2023)
resize2fs: Device or resource busy while trying to open /dev/nvme0n1
Couldn't find valid filesystem superblock.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이딴 에러가 뜨면 재부팅이 답이라고 한다..&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ubuntu@ip:~$ df -h
Filesystem       Size  Used Avail Use% Mounted on
/dev/root         11G  6.6G  4.0G  63% /
tmpfs            208M     0  208M   0% /dev/shm
tmpfs             83M  1.1M   82M   2% /run
tmpfs            5.0M     0  5.0M   0% /run/lock
efivarfs         128K  3.3K  125K   3% /sys/firmware/efi/efivars
/dev/nvme0n1p16  891M  105M  724M  13% /boot
/dev/nvme0n1p15   98M  6.4M   92M   7% /boot/efi
tmpfs             42M   12K   42M   1% /run/user/1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재부팅하니 해결.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://dkan.tistory.com/7&quot;&gt;https://dkan.tistory.com/7&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@harvey/AWS-EC2-인스턴스-용량-확장&quot;&gt;https://velog.io/@harvey/AWS-EC2-인스턴스-용량-확장&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>EBS</category>
      <category>EC2</category>
      <category>용량 확장</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/203</guid>
      <comments>https://promisingmoon.tistory.com/203#entry203comment</comments>
      <pubDate>Fri, 30 Aug 2024 19:28:49 +0900</pubDate>
    </item>
    <item>
      <title>24/08/20(화) 102번째 TIL : spring boot에서 active profile 선택하기</title>
      <link>https://promisingmoon.tistory.com/202</link>
      <description>&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// application.yml

spring:
  application:
    name: yml

server:
  port: 8088

fruit:
  list:
    - name: banana
      color: yellow
    - name: apple
      color: red

developer:
  name: yunjae
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// application-dev.yml

server:
  port: 8055
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application-dev.yml&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// DeveloperName.class 

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(&quot;developer&quot;)
public class DeveloperName {
    private String name;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일에서 developer 키 속, name 키의 값을 가져와서 넣어준다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// Test.class

@Slf4j
@Component
@RequiredArgsConstructor
public class Test {

    private final DeveloperName developerName;

    @Value(&quot;${server.port}&quot;)
    private String port;

    @Value(&quot;${spring.profiles.active}&quot;)
    private String activeProfile;

    @PostConstruct
    void setup() {
        log.info(&quot;dev name : {}&quot;, developerName);
        log.info(&quot;server port : {}&quot;, port);
        log.info(&quot;active profile {}&quot;, activeProfile);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ex.yml.YmlApplication : No active profile set, falling back to 1 default profile: &quot;default&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링은 특별히 profile을 지정해주지 않으면 default 로 프로필을 지정하며, application.yml 파일을 가져온다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'test': Injection of autowired dependencies failed
// ... 
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'spring.profiles.active' in value &quot;${spring.profiles.active}&quot;
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일대로 실행하면, 위와 같은 에러를 내며 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml 파일에는 spring.profiles.active를 지정해주지 않았기 때문에 당연쓰&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MjriC/btsJmNiiPlc/8gEsUEpxfDBscUQxjyVXwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MjriC/btsJmNiiPlc/8gEsUEpxfDBscUQxjyVXwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MjriC/btsJmNiiPlc/8gEsUEpxfDBscUQxjyVXwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMjriC%2FbtsJmNiiPlc%2F8gEsUEpxfDBscUQxjyVXwk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1692&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 인텔리제이 기준으로 실행/디버그 구성 창을 켜서&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빌드 및 실행 탭의 오른쪽에 옵션 수정 &amp;rarr; VM 옵션 추가 &amp;rarr; -Dspring.profiles.active={prifile이름} 을 입력해주거나,&lt;/li&gt;
&lt;li&gt;그 밑의 Active profiles 부분에 그냥 {profile이름} 만 넣어줘도 된다.&lt;/li&gt;
&lt;li&gt;아니면 application.yml 파일에 spring.profiles.default=dev 로 하기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진은 두 가지 방법 모두를 보여주려고 적은거라 둘 중 하나만 해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 후 실행해보면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ex.yml.YmlApplication                    : The following 1 profile is active: &quot;dev&quot;
o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8055 (http)
ex.yml.Test                              : dev name : DeveloperName(name=yunjae)
ex.yml.Test                              : server port : 8055
ex.yml.Test                              : active profile dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dev profile 로 지정된 것을 볼 수 있다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>application.yml</category>
      <category>profile</category>
      <category>Spring</category>
      <category>스프링</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/202</guid>
      <comments>https://promisingmoon.tistory.com/202#entry202comment</comments>
      <pubDate>Fri, 30 Aug 2024 19:22:18 +0900</pubDate>
    </item>
    <item>
      <title>24/08/19(월) 101번째 TIL : 어노테이션에는 상수만 가능</title>
      <link>https://promisingmoon.tistory.com/201</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;pre id=&quot;code_1724045483853&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import org.springframework.http.HttpMethod;

public class CorsConstant {

    public static final String[] ALLOWED_ORIGINS = {
            &quot;http://localhost:3000&quot;
    };

    public static final String[] ALLOWED_METHODS = {
            HttpMethod.GET.name(),
            HttpMethod.POST.name(),
            HttpMethod.PUT.name(),
            HttpMethod.DELETE.name(),
            HttpMethod.OPTIONS.name()
    };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.29.21.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbgMFU/btsI8PtA7iK/9rn6B4AI9L2YNv3zxOQczK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbgMFU/btsI8PtA7iK/9rn6B4AI9L2YNv3zxOQczK/img.png&quot; data-alt=&quot;Attribute &amp;amp;nbsp; value &amp;amp;nbsp;must be&amp;amp;nbsp; constant&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbgMFU/btsI8PtA7iK/9rn6B4AI9L2YNv3zxOQczK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbgMFU%2FbtsI8PtA7iK%2F9rn6B4AI9L2YNv3zxOQczK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;341&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.29.21.png&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Attribute &amp;nbsp; value &amp;nbsp;must be&amp;nbsp; constant&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333;&quot;&gt;&lt;b&gt;@CrossOrigin&lt;/b&gt; 어노테이션에 origins를 입력 중, 상수 클래스를 만들어 가져오면 관리하기 편리할까 싶어서 로직을 작성하다가 &lt;span style=&quot;text-align: left;&quot;&gt;위와 같은 에러를 만났다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;a href=&quot;https://www.baeldung.com/java-compile-time-constants#2-annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자바의 어노테이션은 컴파일 시점에 처리&lt;/a&gt;가 된다. 따라서 어노테이션의 속성들은 컴파일 시점 상수를 사용해야만 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 CorsConstant의 ALLOWED_ORIGINS는 static final을 붙이긴 했지만 우선은 배열이므로 참조위치만 안 바뀔 뿐이지, 내부 요소의 값은 바뀔 수 있다. 따라서 컴파일 시점의 상수가 아니라서 에러가 난 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결은.. 직접 문자열 배열로 써주거나 그냥 WebSecurityConfig로 중앙 집중식으로 써두었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725012679868&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
public class SampleController {

    @CrossOrigin(
            origins = {
                    &quot;http://localhost:3000&quot;
            }
    )
    @GetMapping(&quot;/api/sample&quot;)
    public String getSample() {
        return &quot;sample data&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 쓰거나&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1725012715282&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig {

    public static final String[] ALLOWED_ORIGINS = {
            &quot;http://localhost:3000&quot;
    };

    public static final String[] ALLOWED_METHODS = {
            HttpMethod.GET.name(),
            HttpMethod.POST.name(),
            HttpMethod.PUT.name(),
            HttpMethod.DELETE.name(),
            HttpMethod.OPTIONS.name(),
            RequestMethod.GET.name()
    };

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping(&quot;/**&quot;)
                        .allowedOrigins(ALLOWED_ORIGINS)
                        .allowedMethods(ALLOWED_METHODS)
                        .allowedHeaders(&quot;*&quot;)
                        .allowCredentials(true)
                        .maxAge(3600)
                        .exposedHeaders(&quot;Custom-Response-Header1&quot;, &quot;Custom-Response-Header2&quot;);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 써주었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ENUM으로도 테스트&lt;/h3&gt;
&lt;pre id=&quot;code_1724046325076&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum TestEnum {
    GET(&quot;GET&quot;);

    public final String value;
    public static final String testValue = &quot;GET&quot;;

    TestEnum(String value) {
        this.value = value;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.41.27.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;341&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br8xJv/btsI7geXMeR/wm2jr9utiDw1QBCLiakGkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br8xJv/btsI7geXMeR/wm2jr9utiDw1QBCLiakGkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br8xJv/btsI7geXMeR/wm2jr9utiDw1QBCLiakGkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr8xJv%2FbtsI7geXMeR%2Fwm2jr9utiDw1QBCLiakGkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;274&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.41.27.png&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;341&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.42.05.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;511&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R2OMR/btsI9bJNSZ5/Tsy7DHx8CbOaawWjfEAHl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R2OMR/btsI9bJNSZ5/Tsy7DHx8CbOaawWjfEAHl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R2OMR/btsI9bJNSZ5/Tsy7DHx8CbOaawWjfEAHl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR2OMR%2FbtsI9bJNSZ5%2FTsy7DHx8CbOaawWjfEAHl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;604&quot; height=&quot;429&quot; data-filename=&quot;스크린샷 2024-08-19 오후 2.42.05.png&quot; data-origin-width=&quot;719&quot; data-origin-height=&quot;511&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ENUM으로도 테스트 해봤는데 어노테이션 속성으로는 static 속성들만 표시가 되었다. (TestEnum.GET도 내부적으로는 public static final TestEnum GET = new TestEnum(&quot;GET&quot;) 이니깐.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바의 어노테이션은 컴파일 시에 처리가 되므로, 어노테이션의 속성들은 컴파일 시점 상수여야 한다.&lt;/li&gt;
&lt;li&gt;배열의 경우, static final을 붙여도 내부 요소는 변경 가능하므로(mutable) 컴파일 시점 상수가 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/java-compile-time-constants#2-annotations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;밸덩&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stir.tistory.com/321&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Annotation</category>
      <category>Java</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/201</guid>
      <comments>https://promisingmoon.tistory.com/201#entry201comment</comments>
      <pubDate>Fri, 30 Aug 2024 19:12:28 +0900</pubDate>
    </item>
    <item>
      <title>24/08/16(금) 100번째 TIL : CORS 커스텀 응답 헤더 가져오기</title>
      <link>https://promisingmoon.tistory.com/199</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 프로젝트에서 JWT을 이용한 인증 방식을 택해서, 액세스 토큰 및 리프레쉬 토큰을 커스텀 응답 헤더에 넣어서 보내주었다.&amp;nbsp;하지만 프론트에서 응답 헤더에서 받아올 수 없었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;기본적으로, &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/CORS-safelisted_response_header&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;응답 헤더는 &lt;/a&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/CORS-safelisted_response_header&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CORS-safelisted response header 만 노출&lt;/a&gt;된다. (&lt;span style=&quot;background-color: #ffffff; color: #1b1b1b; text-align: start;&quot;&gt;simple response header 라고도 한다) &lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;b&gt;CORS-safelisted response header&lt;/b&gt;란, 클라이언트의 스크립트에 노출되어도 안전하다고 여겨지는 헤더들이다. 기본적으로 아래와 같은 헤더들이 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1b1b1b; text-align: start;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1b1b1b; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control&quot;&gt;Cache-Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language&quot;&gt;Content-Language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length&quot;&gt;Content-Length&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type&quot;&gt;Content-Type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires&quot;&gt;Expires&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified&quot;&gt;Last-Modified&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma&quot;&gt;Pragma&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외의 헤더들은 &lt;b&gt;Access-Control-Expose-Headers&lt;/b&gt; 로 허용을 해주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 커스텀 응답 헤더를 프론트에서 받기 위해서는 스프링 기준으로 아래와 같이 작성해주면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;주석으로 밑부분에 여기 라고 적은 부분!&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Spring Security 미사용&lt;/h3&gt;
&lt;pre id=&quot;code_1724050994379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {

    public static final String[] ALLOWED_ORIGINS = {
            &quot;http://localhost:3000&quot;
    };

    public static final String[] ALLOWED_METHODS = {
            HttpMethod.GET.name(),
            HttpMethod.POST.name(),
            HttpMethod.PUT.name(),
            HttpMethod.DELETE.name(),
            HttpMethod.OPTIONS.name()
    };

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping(&quot;/**&quot;)
                        .allowedOrigins(ALLOWED_ORIGINS)
                        .allowedMethods(ALLOWED_METHODS)
                        .allowedHeaders(&quot;*&quot;)
                        .allowCredentials(true)
                        .maxAge(3600)
                        // 여기 !
                        .exposedHeaders(&quot;Custom-Response-Header1&quot;, &quot;Custom-Response-Header2&quot;);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Security 사용&lt;/h3&gt;
&lt;pre id=&quot;code_1724051751083&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.cors(getCorsConfigurerCustomizer());
        // ... 

        return http.build();
    }

    private Customizer&amp;lt;CorsConfigurer&amp;lt;HttpSecurity&amp;gt;&amp;gt; getCorsConfigurerCustomizer() {
        return corsConfigurer -&amp;gt; corsConfigurer.configurationSource(request -&amp;gt; {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(Collections.singletonList(&quot;*&quot;)); // 모든 오리진 허용
            config.setAllowedMethods(Collections.singletonList(&quot;*&quot;)); // 모든 메서드 허용
            config.setAllowedHeaders(Collections.singletonList(&quot;*&quot;)); // 모든 헤더 허용
            // 여기 !!
            config.setExposedHeaders(List.of(&quot;Custom-Response-Header1&quot;, &quot;Custom-Response-Header2&quot;); 
            return config;
        });
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 응답에 다음과 같은 헤더가 포함되게 된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724051349340&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Access-Control-Expose-Headers: Custom-Response-Header1, Custom-Response-Header2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팁으로 와일드 카드로 에스터리스크(*)를 사용할 수 있는데, Authorization 헤더는 지정할 수 없어서 명시적으로 작성해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1724051430753&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Access-Control-Expose-Headers: *, Authorization&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@rhqjatn2398/Access-Control-Expose-Headers-CORS-safelisted-response-header&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;벨로그1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Access-Control-Expose-Headers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN : Access-Control-Expose-Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Glossary/CORS-safelisted_response_header&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MDN : CORS-safelisted response header&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>access-control-expose-headers</category>
      <category>cors</category>
      <category>cors-safelisted response header</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/199</guid>
      <comments>https://promisingmoon.tistory.com/199#entry199comment</comments>
      <pubDate>Mon, 19 Aug 2024 16:19:01 +0900</pubDate>
    </item>
    <item>
      <title>24/08/14(수) 99번째 TIL : Spring Security 없이 CORS 설정하기</title>
      <link>https://promisingmoon.tistory.com/198</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 목록&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1724044027263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성이 web, lombok 밖에 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전역 설정&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1724044100389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig {

    public static final String[] ALLOWED_ORIGINS = {
            &quot;http://localhost:3000&quot;,
            &quot;http://yunjae.click&quot;,
            &quot;https://yunjae.click&quot;
    };

    public static final String[] ALLOWED_METHODS = {
            HttpMethod.GET.name(),
            HttpMethod.POST.name(),
            HttpMethod.PUT.name(),
            HttpMethod.DELETE.name(),
            HttpMethod.OPTIONS.name()
    };

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping(&quot;/**&quot;)
                        .allowedOrigins(ALLOWED_ORIGINS)
                        .allowedMethods(ALLOWED_METHODS)
                        .allowedHeaders(&quot;*&quot;)
                        .allowCredentials(true)
                        .maxAge(3600)
                        .exposedHeaders(&quot;Custom-Response-Header&quot;);
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;origin과 method 목록은 따로 빼서 변수로 만들어두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;headers나 exposedHeaders도 입맛에 맞게 따로 빼면 될 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컨트롤러 레벨 설정&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1724044165234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    @CrossOrigin(
            origins = {&quot;http://localhost:3000&quot;},
            methods = {RequestMethod.GET, RequestMethod.POST},
            maxAge = 3600L
    )
    @GetMapping(&quot;/api/sample&quot;)
    public String getSample() {
        return &quot;sample data&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@CrossOrigin&lt;/b&gt; 어노테이션을 이용하면 이용할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특이한게 전역설정 땐 허용 메서드를 &lt;b&gt;String&lt;/b&gt; 으로 받는데, &lt;b&gt;@CrossOrigin&lt;/b&gt; 어노테이션의 methods는 &lt;b&gt;RequestMethod 타입&lt;/b&gt;으로 받는다. 그래서 전역 설정 때도 &lt;b&gt;Httpmethod.GET.name()&lt;/b&gt; 말고 통일을 위해 &lt;b&gt;RequestMethod.GET.name()&lt;/b&gt; 으로 해도 될 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>cors</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>springsecurity</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/198</guid>
      <comments>https://promisingmoon.tistory.com/198#entry198comment</comments>
      <pubDate>Mon, 19 Aug 2024 15:26:28 +0900</pubDate>
    </item>
    <item>
      <title>24/08/13(화) 98번째 TIL : Spring boot에서 record로 application.yml 읽기</title>
      <link>https://promisingmoon.tistory.com/197</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트에서 Record 클래스로 application.yml 파일 값 가져오려다가 에러가 났다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;pre id=&quot;code_1723525282329&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml 

fruit:
  list:
    - name: banana
      color: yellow
    - name: apple
      color: red&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yml 파일에 fruit.list 속에 과일 리스트가 있는 형태를 스프링에서 가져다 쓰려고 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723525377420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Fruit.class

public record Fruit(
        String name,
        String color
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723525383817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// FruitList.class

import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;

@ConfigurationProperties(&quot;fruit&quot;) // 여기 !! 
public record FruitList(
        List&amp;lt;Fruit&amp;gt; list
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부에서 가져오는 프로퍼티인 만큼 레코드 타입으로 불변을 보장하려고 했는데 아래와 같은 에러가 났다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cloiWc/btsI26vMJ0B/p0oJ9Ja14hX3Rukik1SdnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cloiWc/btsI26vMJ0B/p0oJ9Ja14hX3Rukik1SdnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cloiWc/btsI26vMJ0B/p0oJ9Ja14hX3Rukik1SdnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcloiWc%2FbtsI26vMJ0B%2Fp0oJ9Ja14hX3Rukik1SdnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1135&quot; height=&quot;315&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결법은 스프링 부트 2.2 버전 이전과 이후로 나뉜다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트 2.2 이전 해결법&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. @Component 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1723526327166&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@ConfigurationProperties(&quot;fruit&quot;)
public record FruitList(
        List&amp;lt;Fruit&amp;gt; list
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법으로는 현재는 에러가 난다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. &lt;span style=&quot;color: #333333;&quot;&gt;@EnableConfigurationProperties 사용&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1723526388617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({FruitList.class}) // 사용할 클래스를 지정해주어야 함 
public class ConfigurationPropertyConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 동작은 되지만 매 프로퍼티 클래스를 작성해주어야 하는 불편함이 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스프링 부트 2.2 이후 해결법&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. @ConfigurationProperties 사용&lt;/h4&gt;
&lt;pre id=&quot;code_1723526450863&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationPropertiesScan // 여기 !! 
public class ConfigurationPropertyConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 &lt;b&gt;@ConfigurationProperties&lt;/b&gt; 만 붙여주면 끝난다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 전체 코드를 보여주면 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723526507745&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// YmlApplication.class

@SpringBootApplication
public class YmlApplication {

    public static void main(String[] args) {
        SpringApplication.run(YmlApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723526532451&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ConfigurationPropertyConfig.class

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationPropertiesScan
public class ConfigurationPropertyConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723526554347&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Fruit.class

public record Fruit(
        String name,
        String color
) {
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723526604289&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Test.class

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class Test {

    private final FruitList fruitList;

    @PostConstruct
    void setup() {
        for (Fruit fruit : fruitList.list()) {
            log.info(&quot;fruit name : {}, color : {}&quot;, fruit.name(), fruit.color());
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723526639988&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// console

fruit name : banana, color : yellow
fruit name : apple, color : red&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 결과는 위와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 나오는 것을 볼 수 있다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://sjparkk-dev1og.tistory.com/217&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;티스토리 블로그&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/66696828/how-to-use-configurationproperties-with-records&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스택오버플로우&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dahye-jeong.gitbook.io/spring/spring/2021-02-15-yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;깃북 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>application.yml</category>
      <category>Record</category>
      <category>yml</category>
      <category>레코드</category>
      <category>스프링</category>
      <category>스프링부트</category>
      <category>자바</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/197</guid>
      <comments>https://promisingmoon.tistory.com/197#entry197comment</comments>
      <pubDate>Tue, 13 Aug 2024 14:29:32 +0900</pubDate>
    </item>
    <item>
      <title>24/08/12(월) 97번째 TIL : Spring cloud gateway에서 최종 라우팅 서비스 URI 가져오기</title>
      <link>https://promisingmoon.tistory.com/196</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 과제에서 처리된 서비스의 서버 포트를 응답 헤더에 담으라는 요구사항이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723439795775&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/products&quot;)
public class ProductController {

    private final ProductService productService;

    @Value(&quot;${server.port}&quot;)
    private String port;

    /**
     * 상품 목록 조회 API
     */
    @GetMapping
    public ResponseEntity&amp;lt;List&amp;lt;GetProductsRes&amp;gt;&amp;gt; getProductList(GetProductsReq request, Pageable pageable) {

        List&amp;lt;GetProductsRes&amp;gt; productList = productService.getProductList(request, pageable);

        return ResponseEntity.ok()
                .header(GlobalConstant.CUSTOM_SERVER_PORT_HEADER, port)
                .body(productList);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 무식하게 일일이 컨트롤러에 이렇게 다 담았었는데, 제출 이후 팀원들과 이야기하다가 이 중복을 어떻게 해결했는지를 논의했었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 팀원분께서 게이트웨이에 넣으셨다는 말씀을 해주셔서 바로 커비 해버렸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1723439868420&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class AddServerPortInHeaderFilter implements GlobalFilter, Ordered {

    private static final String SERVER_PORT_HEADERS_NAME = &quot;Server-Port&quot;;

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        return chain.filter(exchange)
                .then(Mono.fromRunnable(() -&amp;gt; addServerPortInHeader(exchange)));
    }

    private void addServerPortInHeader(ServerWebExchange exchange) {
        URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);

        if (uri != null) {
            ServerHttpResponse response = exchange.getResponse();
            HttpHeaders headers = response.getHeaders();

            String port = String.valueOf(uri.getPort());
            headers.add(SERVER_PORT_HEADERS_NAME, port);
        }
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 클라우드 게이트웨이는 클라이언트의 요청이 게이트웨이에 도착한 후, 처리할 서비스로 요청을 보내는데, 이 과정에서 &lt;b&gt;GATEWAY_REQUEST_URL_ATTR&lt;/b&gt; 속성으로 속성값을 가져오면 &lt;b&gt;최종적으로 결정된 목적지 URI&lt;/b&gt;를 가져올 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723440264230&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;uri : http://&amp;lt;ip주소&amp;gt;:19093/products&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그로 가져와본 상품 조회 API URI이다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-12 오후 2.24.57.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2cX3C/btsI2jIsg4A/LJgERkhDkacYcbwsFlk370/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2cX3C/btsI2jIsg4A/LJgERkhDkacYcbwsFlk370/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2cX3C/btsI2jIsg4A/LJgERkhDkacYcbwsFlk370/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2cX3C%2FbtsI2jIsg4A%2FLJgERkhDkacYcbwsFlk370%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;357&quot; height=&quot;152&quot; data-filename=&quot;스크린샷 2024-08-12 오후 2.24.57.png&quot; data-origin-width=&quot;357&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductApplication인 19093 포트를 잘 가져오는 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723440203072&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final class ServerWebExchangeUtils {
    private static final Log log = LogFactory.getLog(ServerWebExchangeUtils.class);
    public static final String PRESERVE_HOST_HEADER_ATTRIBUTE = qualify(&quot;preserveHostHeader&quot;);
    public static final String URI_TEMPLATE_VARIABLES_ATTRIBUTE = qualify(&quot;uriTemplateVariables&quot;);
    public static final String CLIENT_RESPONSE_ATTR = qualify(&quot;gatewayClientResponse&quot;);
    public static final String CLIENT_RESPONSE_CONN_ATTR = qualify(&quot;gatewayClientResponseConnection&quot;);
    public static final String CLIENT_RESPONSE_HEADER_NAMES = qualify(&quot;gatewayClientResponseHeaderNames&quot;);
    public static final String GATEWAY_ROUTE_ATTR = qualify(&quot;gatewayRoute&quot;);
    public static final String GATEWAY_REACTOR_CONTEXT_ATTR = qualify(&quot;gatewayReactorContext&quot;);
    public static final String GATEWAY_REQUEST_URL_ATTR = qualify(&quot;gatewayRequestUrl&quot;);
    public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify(&quot;gatewayOriginalRequestUrl&quot;);
    public static final String GATEWAY_HANDLER_MAPPER_ATTR = qualify(&quot;gatewayHandlerMapper&quot;);
    public static final String GATEWAY_SCHEME_PREFIX_ATTR = qualify(&quot;gatewaySchemePrefix&quot;);
    public static final String GATEWAY_PREDICATE_ROUTE_ATTR = qualify(&quot;gatewayPredicateRouteAttr&quot;);
    public static final String GATEWAY_PREDICATE_MATCHED_PATH_ATTR = qualify(&quot;gatewayPredicateMatchedPathAttr&quot;);
    public static final String GATEWAY_PREDICATE_MATCHED_PATH_ROUTE_ID_ATTR = qualify(&quot;gatewayPredicateMatchedPathRouteIdAttr&quot;);
    public static final String GATEWAY_PREDICATE_PATH_CONTAINER_ATTR = qualify(&quot;gatewayPredicatePathContainer&quot;);
    public static final String WEIGHT_ATTR = qualify(&quot;routeWeight&quot;);
    public static final String ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR = &quot;original_response_content_type&quot;;
    public static final String CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR = qualify(&quot;circuitBreakerExecutionException&quot;);
    public static final String GATEWAY_ALREADY_ROUTED_ATTR = qualify(&quot;gatewayAlreadyRouted&quot;);
    public static final String GATEWAY_ALREADY_PREFIXED_ATTR = qualify(&quot;gatewayAlreadyPrefixed&quot;);
    public static final String CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR = &quot;cachedServerHttpRequestDecorator&quot;;
    public static final String CACHED_REQUEST_BODY_ATTR = &quot;cachedRequestBody&quot;;
    public static final String GATEWAY_LOADBALANCER_RESPONSE_ATTR = qualify(&quot;gatewayLoadBalancerResponse&quot;);
    public static final String GATEWAY_OBSERVATION_ATTR = qualify(&quot;gateway.observation&quot;);
    private static final byte[] EMPTY_BYTES = new byte[0];
    
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URI 말고도 다양한 속성값들을 가져올 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;그리고 팀원분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;AbstractGatewayFilterFactory&lt;/b&gt; 를 구현하셨는데, 이는 특정 라우트에만 적용되는 필터를 정의할 때 사용한다고 한다. 만약 전역적으로 적용되는 필터를 사용해야 한다면 &lt;b&gt;GlobalFilter&lt;/b&gt;를 구현하면 된다.&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>serverwebexchangeutils</category>
      <category>springcloudgateway</category>
      <category>springgateway</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/196</guid>
      <comments>https://promisingmoon.tistory.com/196#entry196comment</comments>
      <pubDate>Mon, 12 Aug 2024 14:31:25 +0900</pubDate>
    </item>
    <item>
      <title>24/08/09(금) 96번째 TIL : Spring Gateway 애플 실리콘 맥 에러</title>
      <link>https://promisingmoon.tistory.com/195</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;pre id=&quot;code_1723268779383&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service 
# ...&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1723268445820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2024-08-10T14:06:47.934+09:00 ERROR 63786 --- [gateway-service] [ctor-http-nio-2] i.n.r.d.DnsServerAddressStreamProviders  : 
Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider, fallback to system defaults. 
This may result in incorrect DNS resolutions on MacOS. 
Check whether you have a dependency on 'io.netty:netty-resolver-dns-native-macos'. 
Use DEBUG level to see the full stack: java.lang.UnsatisfiedLinkError: failed to load the required native library&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring gateway 를 리액티브로 설정해서 쓰려고 하니 에러가 났다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Netty는 네이티브 코드를 사용해서 DNS와 연동하는데, 애플 실리콘 (M1, M2, M3 등등의 ARM 기반 아키텍처) 에서는 호환되는 것이 없어서 필요한 라이브러리가 없어서 따로 라이브러리를 통해 제공해주어야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723268611702&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.112.Final'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MVN리포지토리&lt;/a&gt;에서 최신 버전을 찾아서 입력해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://shanepark.tistory.com/495&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://shanepark.tistory.com/495&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>TIL</category>
      <category>M1</category>
      <category>M2</category>
      <category>M3</category>
      <category>netty</category>
      <category>Reactive</category>
      <category>springgateway</category>
      <category>WebFlux</category>
      <category>맥</category>
      <category>애플실리콘</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/195</guid>
      <comments>https://promisingmoon.tistory.com/195#entry195comment</comments>
      <pubDate>Sat, 10 Aug 2024 14:47:18 +0900</pubDate>
    </item>
    <item>
      <title>24/08/08(목) 95번째 TIL : Spring Data Redis 및 Redis-cli 문자열 인코딩 방식</title>
      <link>https://promisingmoon.tistory.com/194</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;pre id=&quot;code_1723166294676&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;localhost:63790&amp;gt; subscribe home
1) &quot;subscribe&quot;
2) &quot;home&quot;
3) (integer) 1
1) &quot;message&quot;
2) &quot;home&quot;
3) &quot;{\&quot;messageId\&quot;:\&quot;abcde\&quot;,\&quot;sender\&quot;:\&quot;1234\&quot;,\&quot;message\&quot;:\&quot;abcde\&quot;}&quot;
1) &quot;message&quot;
2) &quot;home&quot;
3) &quot;{\&quot;messageId\&quot;:\&quot;abcde\&quot;,\&quot;sender\&quot;:\&quot;1234\&quot;,\&quot;message\&quot;:\&quot;\xed\x95\x98\xec\x9d\xb4\&quot;}&quot;
1) &quot;message&quot;
2) &quot;home&quot;
3) &quot;{\&quot;messageId\&quot;:\&quot;abcde\&quot;,\&quot;sender\&quot;:\&quot;1234\&quot;,\&quot;message\&quot;:\&quot;\xea\xb0\x80\&quot;}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스로 pub/sub 테스트를 해보던 중에 한글이 깨지던 문제가 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;pre id=&quot;code_1723167911202&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ redis-cli -h localhost -p 63790 --raw
localhost:63790&amp;gt; subscribe home
subscribe
home
1
message
home
{&quot;messageId&quot;:&quot;abcde&quot;,&quot;sender&quot;:&quot;1234&quot;,&quot;message&quot;:&quot;가&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;redis-cli로 접속 시 --raw를 붙여주면 해결&lt;/b&gt;된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스는 --raw를 통해 원시 출력을 강제할 수 있다. (출처 : &lt;a href=&quot;https://redis.io/docs/latest/develop/connect/cli/#string-quoting-and-escaping&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;레디스 공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-09 오전 10.49.30.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sxYYg/btsIYu5qjqv/WrklT26o0v9bfSVrTnF4z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sxYYg/btsIYu5qjqv/WrklT26o0v9bfSVrTnF4z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sxYYg/btsIYu5qjqv/WrklT26o0v9bfSVrTnF4z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsxYYg%2FbtsIYu5qjqv%2FWrklT26o0v9bfSVrTnF4z1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;783&quot; height=&quot;583&quot; data-filename=&quot;스크린샷 2024-08-09 오전 10.49.30.png&quot; data-origin-width=&quot;857&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1723170128850&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;❯ redis-cli -p 63790
127.0.0.1:63790&amp;gt; set my &quot;hello\nworld&quot;
127.0.0.1:63790&amp;gt; get my
&quot;hello\nworld&quot;
127.0.0.1:63790&amp;gt; set hangul &quot;한글 입니다.&quot;
OK
127.0.0.1:63790&amp;gt; get hangul
&quot;\xed\x95\x9c\xea\xb8\x80 \xec\x9e\x85\xeb\x8b\x88\xeb\x8b\xa4.&quot;

❯ redis-cli -p 63790 --raw
127.0.0.1:63790&amp;gt; get my
hello
world
127.0.0.1:63790&amp;gt; get hangul
한글 입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 &lt;b&gt;--raw&lt;/b&gt;를 붙이면 잘 나오는 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Data Redis 가 한글을 인코딩하는 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 위에서 봤듯, &lt;b&gt;한글 &quot;가&quot; 는 &quot;\xea\xb0\x80&quot;로 표기&lt;/b&gt;가 됐는데, 레디스는 아스키 문자로 저장이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 &quot;가&quot;는 \x가 3번 나왔으니까, 3byte 로 저장이 된다는 소린데, &lt;b&gt;3byte로 한글이 저장되는 인코딩 방식은 UTF8&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 인코딩 사이트로 UTF8 한글 &quot;가&quot;를 검색해보니,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-09 오전 11.25.58.png&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pSdfl/btsIZWTKrbB/0JYKQmEb8oluwXHNIm25Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pSdfl/btsIZWTKrbB/0JYKQmEb8oluwXHNIm25Xk/img.png&quot; data-alt=&quot;https://dencode.com/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pSdfl/btsIZWTKrbB/0JYKQmEb8oluwXHNIm25Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpSdfl%2FbtsIZWTKrbB%2F0JYKQmEb8oluwXHNIm25Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;128&quot; data-filename=&quot;스크린샷 2024-08-09 오전 11.25.58.png&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;128&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dencode.com/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스에서 저장이 되던 &lt;b&gt;eab080&lt;/b&gt;을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;자바에서 문자열을 UTF8로 변환&lt;/b&gt;하여 주지만, &lt;b&gt;레디스에서는 byte 단위로 읽어서 8비트씩 끊어서 저장&lt;/b&gt;하느라 저렇게 표기된 것.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;자바는 UTF16을 사용&lt;/b&gt;한다.(정확히 말하면 &lt;b&gt;ASCII 로 표기할 수 있으면 1byte, 하나라도 아니라면 2byte&lt;/b&gt;. 이에 대해서는 &lt;a href=&quot;https://promisingmoon.tistory.com/75#String%EC%9D%98%20%EB%82%B4%EB%B6%80%20%EC%A0%80%EC%9E%A5%20%EB%B0%A9%EC%8B%9D-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;내 또 다른 글을 참고&lt;/a&gt;하면 좋다,,) 그러면 &lt;b&gt;2byte여야 할 텐데 왜 3byte로 변환&lt;/b&gt;이 됐는지 찾아보니&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723168841012&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    public RedisTemplate&amp;lt;?, ?&amp;gt; redisTemplate() {

        RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        redisTemplate.setKeySerializer(RedisSerializer.string()); // 키 직렬화 
        redisTemplate.setValueSerializer(RedisSerializer.json()); // 값 직렬화 

        return redisTemplate;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 설정에 사용한 직렬화 방식.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;RedisSerializer.string()&lt;/b&gt; 내부를 들어가 보니&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723168875945&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    static RedisSerializer&amp;lt;String&amp;gt; string() {
        return StringRedisSerializer.UTF_8;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;키를 직렬화할 땐 UTF8로 직렬화&lt;/b&gt;를 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 &lt;b&gt;RedisSerializer.json()&lt;/b&gt;이 문자열을 어떤 인코딩 방식으로 직렬화하는지 살펴보자면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723169373545&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    static RedisSerializer&amp;lt;Object&amp;gt; json() {
        return new GenericJackson2JsonRedisSerializer();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RedisSerializer.json()&lt;/b&gt;은 &lt;b&gt;GenericJackson2JsonRedisSerializer 를 반환&lt;/b&gt;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723169510874&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public GenericJackson2JsonRedisSerializer() {
        this((String)null);
    }

    public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName) {
        this(typeHintPropertyName, JacksonObjectReader.create(), JacksonObjectWriter.create());
    }

    public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, JacksonObjectReader reader, JacksonObjectWriter writer) {
        this(new ObjectMapper(), reader, writer, typeHintPropertyName);
        registerNullValueSerializer(this.mapper, typeHintPropertyName);
        this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(this.getObjectMapper(), typeHintPropertyName));
    }

    public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
        this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create());
    }

    public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, JacksonObjectWriter writer) {
        this(mapper, reader, writer, (String)null);
    }

    private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, JacksonObjectWriter writer, @Nullable String typeHintPropertyName) {
        Assert.notNull(mapper, &quot;ObjectMapper must not be null&quot;);
        Assert.notNull(reader, &quot;Reader must not be null&quot;);
        Assert.notNull(writer, &quot;Writer must not be null&quot;);
        this.mapper = mapper;
        this.reader = reader;
        this.writer = writer;
        this.defaultTypingEnabled = Lazy.of(() -&amp;gt; {
            return mapper.getSerializationConfig().getDefaultTyper((JavaType)null) != null;
        });
        this.typeResolver = newTypeResolver(mapper, typeHintPropertyName, this.defaultTypingEnabled);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GenericJackson2JsonRedisSerializer 생성자&lt;/b&gt;를 맨 위에서부터 차례대로 보면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;this&lt;/b&gt;를 타고 내려가면서 &lt;b&gt;2번째 생성자&lt;/b&gt;를 보면 &lt;b&gt;JacksonObjectWriter.create()라는&lt;/b&gt; 메서드를 통해 JSON을 직렬화하기 위한 객체를 만든다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 6번째 생성자에서, 만든 JacksonObjectWriter를 this.writer 에 넣어준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;JacksonObjectWriter.create()&lt;/b&gt; 내부를 봐보면,&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723169625153&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface JacksonObjectWriter {
    byte[] write(ObjectMapper mapper, Object source) throws IOException;

    static JacksonObjectWriter create() {
        return ObjectMapper::writeValueAsBytes;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부를 보니 &lt;b&gt;ObjectMapper.writeValueAsBytes()라는 메서드를 실행&lt;/b&gt;시킨다. 안을 또 봐보면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723169899638&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    public byte[] writeValueAsBytes(Object value) throws JsonProcessingException {
        BufferRecycler br = this._jsonFactory._getBufferRecycler();

        byte[] var6;
        try {
            ByteArrayBuilder bb = new ByteArrayBuilder(br);
            Throwable var4 = null;

            try {
                // 여기 
                this._writeValueAndClose(this.createGenerator((OutputStream)bb, JsonEncoding.UTF8), value); 
                byte[] result = bb.toByteArray();
                bb.release();
                var6 = result;
            } catch (Throwable var26) {
                var4 = var26;
                throw var26;
            } finally {
                if (bb != null) {
                    if (var4 != null) {
                        try {
                            bb.close();
                        } catch (Throwable var25) {
                            var4.addSuppressed(var25);
                        }
                    } else {
                        bb.close();
                    }
                }

            }
        } catch (JsonProcessingException var28) {
            JsonProcessingException e = var28;
            throw e;
        } catch (IOException var29) {
            IOException e = var29;
            throw JsonMappingException.fromUnexpectedIOE(e);
        } finally {
            br.releaseToPool();
        }

        return var6;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 다 볼 필욘 없고 &lt;b&gt;2번째 try문의 여기라고&lt;/b&gt; 써둔 바로 밑 줄을 보면 &lt;b&gt;JsonEncoding.UTF8이라고&lt;/b&gt; 되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 목적은 자바 객체가 JSON으로 변환되면서 어떤 인코딩으로 변환되는지 확인하는 거였으니 더 깊게 안 들어가고 (들어가도 더 복잡하고 이해하기 힘들다,,) 여기까지 확인하는 걸로.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Redis는 기본적으로 byte-encoded character로 표기한다. (not human readable)&lt;/li&gt;
&lt;li&gt;Spring Data Redis는 UTF8로 인코딩한다.&lt;/li&gt;
&lt;li&gt;사람이 읽을 수 있게 표기하려면 redis-cli에서 --raw 옵션을 추가하면 된다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>TIL</category>
      <category>--raw</category>
      <category>redis</category>
      <category>redis-cli</category>
      <category>rediscli</category>
      <category>rediscli--raw</category>
      <category>SpringDataRedis</category>
      <category>레디스</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/194</guid>
      <comments>https://promisingmoon.tistory.com/194#entry194comment</comments>
      <pubDate>Fri, 9 Aug 2024 11:53:05 +0900</pubDate>
    </item>
    <item>
      <title>24/08/07(수) 94번째 TIL : EurekaServerConfig 빈 중복 해결하기</title>
      <link>https://promisingmoon.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유레카 서버 설정을 위해 따로 &lt;b&gt;EurekaServerConfig라고&lt;/b&gt; 하는 클래스를 만들어 실행했지만, 중복되는 빈객체로 실행이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;pre id=&quot;code_1723003500786&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 예제에는 애플리케이션 클래스 위에 붙여주었지만, 나는 애플리케이션에 덕지덕지 @Enable- 어노테이션을 붙이는 걸 좋아하지는 않아서 따로 ~Config 클래스를 만들어주기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723003543397&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 애플리케이션 클래스에서는 Enable- 어노테이션을 지워주었고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723003581171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ./config/EurekaServerConfig.java

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableEurekaServer
public class EurekaServerConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EurekaServerConfig.class를 만들어서 어노테이션을 붙여주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723003616374&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2024-08-07T12:48:05.345+09:00 ERROR 89334 --- [server] [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'eurekaServerConfig', defined in class path resource [org/springframework/cloud/netflix/eureka/server/EurekaServerAutoConfiguration$EurekaServerConfigBeanConfiguration.class], could not be registered. A bean with that name has already been defined in file [/Users/barami62/projects/adv-sparta/msa-exam-01/com.sparta.msa_exam.eureka/build/classes/java/main/com/sparta/msa_exam/eureka/config/EurekaServerConfig.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실행이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 길어서 줄넘김하려고 했는데 티스토리에서는 지원되지 않나 보다,,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면 유레카 라이브러리의 빈 이름이랑 겹치니까 이름을 고치던지 설정을 오버라이딩 가능하도록 변경하라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1723003676849&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.cloud.netflix.eureka.server;

public class EurekaServerAutoConfiguration implements WebMvcConfigurer {

    // ...
    
    @Configuration(
        proxyBeanMethods = false
    )
    protected static class EurekaServerConfigBeanConfiguration {
        protected EurekaServerConfigBeanConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) {
            EurekaServerConfigBean server = new EurekaServerConfigBean();
            if (clientConfig.shouldRegisterWithEureka()) {
                server.setRegistrySyncRetries(5);
            }

            return server;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 애가 우리 클래스 못 쓰게 만든건지 궁금해서 찾아보니 유레카 서버 라이브러리 속에 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 이 녀석 때문에 안 된다는 것만 확인하고 넘어갔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 1. 클래스 이름 변경&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1723003753035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ./config/CustomEurekaServerConfig.java

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableEurekaServer
public class CustomEurekaServerConfig {
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CustomEurekaServerConfig.class 로 만들어 주었더니 정상적으로 실행이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법 2. 빈 오버라이딩 허용&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1723003813522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// application.yml 

...

spring:
  main:
    allow-bean-definition-overriding: true

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈이 오버라이딩 되도록 허용을 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예상과는 다르게 정상적으로 실행되었다&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 라이브로리를 가져다 쓰는 입장에서 굳이 오버라이딩했다가 어떤 일을 당할지 모르기 때문에 1번 방법을 쓰기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Eureka</category>
      <category>eurekaserver</category>
      <category>eurekaserverconfig</category>
      <category>Server</category>
      <category>Spring</category>
      <category>스프링</category>
      <category>유레카</category>
      <category>유레카서버</category>
      <category>유레카설정</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/193</guid>
      <comments>https://promisingmoon.tistory.com/193#entry193comment</comments>
      <pubDate>Wed, 7 Aug 2024 13:12:17 +0900</pubDate>
    </item>
    <item>
      <title>24/08/06(화) 93번째 TIL : 도커 볼륨으로 레디스 데이터 공유하기</title>
      <link>https://promisingmoon.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커로 레디스를 띄울 때, 기존 데이터나 설정을 유지하고 싶었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커로 레디스를 띄울 때, 볼륨을 통해 저장 공간을 공유하고, redis.conf 라는 레디스 설정 파일도 공유하여 레디스 컨테이너를 띄울 때 데이터를 유지하도록 해결했다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 EC2 Ubuntu24.04 버전에 도커를 띄운 상황에서 설명하겠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722942746690&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker volume create redis-data&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 &lt;b&gt;redis-data&lt;/b&gt; 라고 하는 볼륨을 만들어준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722942807592&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ sudo docker volume inspect redis-data
[
    {
        &quot;CreatedAt&quot;: &quot;2024-08-05T20:43:25+09:00&quot;,
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: null,
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/redis-data/_data&quot;,
        &quot;Name&quot;: &quot;redis-data&quot;,
        &quot;Options&quot;: null,
        &quot;Scope&quot;: &quot;local&quot;
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;docker volume inspect &amp;lt;볼륨명&amp;gt;&lt;/b&gt; 을 통해 생성 시간, 저장 위치를 비롯한 볼륨 정보를 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722943172299&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd ~
mkdir redis-dir
cd ./redis-dir
sudo vi redis.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;b&gt;~/redis-dir&lt;/b&gt; 에 레디스 전용 디렉토리를 하나 만들고, 여기에 &lt;b&gt;redis.conf&lt;/b&gt; 파일을 만들어 두기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722943278012&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# redis.conf 
# i 를 눌러서 입력 모드로 전환 후, 아래처럼 입력. 

bind 0.0.0.0 # 외부 접근 가능 

port 6379 # 포트는 6379 

requirepass 1234 # 접속 시 인증 필요. auth 1234 하면 인증 처리 

maxmemory 1gb # 최대 논리 메모리는 1GB 

maxmemory-policy allkeys-lru # 논리 메모리 초과 시 LRU(가장 오래된 데이터 삭제) 선택 

appendonly yes # AOF 활성화 (모든 쓰기 로그 저장) -&amp;gt; 실행 시 RDB가 아닌 AOF 파일 읽어 복구 

# 다 입력 후, ESC를 눌러서 명령 모드로 돌아온 후 :wq 로 저장 후 나가기.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vi로 들어온 redis.conf 파일이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i를 눌러서 입력 모드로 전환 후, bind 0.0.0.0 부터 appendonly yes 까지 적고 ESC를 눌러 명령 모드로 돌아온 후, :wq를 입력하고 엔터를 눌러서 저장하고 빠져나온다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레디스 띄우기&lt;/h3&gt;
&lt;pre id=&quot;code_1722958440937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run -d \ 
  -p 6379:6379 \ 
  -v ~/redis-dir/redis.conf:/etc/redis/redis.conf \ 
  -v redis-data:/data \ 
  --name redis-ex \ 
  redis:latest redis-server /etc/redis/redis.conf&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 도커로 레디스를 실행해준다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-p는 포트포워딩,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 -v는 -v &amp;lt;redis.conf 저장 위치&amp;gt;:&amp;lt;컨테이너에 저장할 위치&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 -v는 -v &amp;lt;볼륨이름&amp;gt;:&amp;lt;컨테이너에 저장할 위치&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 줄은 redis:latest 이미지를 사용하는데, redis-server /etc/redis/redis.conf 로 redis.conf를 설정파일로 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡쳐한줄 알았는데 안 하고 지워버렸다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 redis-data 볼륨에는 레디스의 /data 디렉토리와 연동되어 저장되는데, 이 디렉토리에는 레디스의 ~.rdv, ~.aof, manifest 파일이 있다. (정확히는 옵션 기본값인 appendonlydir 디렉토리 안에.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 rdb, aof 파일로 레디스가 재시작될 때 복원을 진행하여 새로운 레디스 컨테이너를 띄워도 기존의 데이터를 유지할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레디스 스택 띄우기&lt;/h3&gt;
&lt;pre id=&quot;code_1722959389879&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run -d \ 
  -p 6379:6379 \ 
  -p 8001:8001 \ 
  --name redis \ 
  -v ~/redis-dir/redis-stack.conf:/redis-stack.conf \ 
  -v redis-data:/data \ 
  redis/redis-stack&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레디스 스택을 사용하는 경우에는 /etc/redis/redis.conf와 달리 바로 /redis-stack.conf 를 하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레디스 볼륨에 저장되는 데이터&lt;/h2&gt;
&lt;pre id=&quot;code_1722959576232&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu:/var/lib/docker/volumes/redis-data/_data$ ls
appendonlydir  dump.rdb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 위에서 redis-data 볼륨의 위치를 inspect 명령어로 확인한 것을 토대로 이동해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;appendonlydir 디렉토리와 dump.rdb가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dump.rdb는 레디스가 동작 중 스냅샷 개념으로 현재 메모리 정보를 저장해두는 파일이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722959668199&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu:/var/lib/docker/volumes/redis-data/_data/appendonlydir$ ls
appendonly.aof.1.base.rdb  appendonly.aof.1.incr.aof  appendonly.aof.manifest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;appendonlydir은 aof 백업 방식을 택할 때 디렉토리명 옵션 기본값으로, 이 안에 백업용 rdb, aof, manifest 파일이 저장된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722959719046&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu:/var/lib/docker/volumes/redis-data/_data/appendonlydir$ sudo cat appendonly.aof.manifest
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;appendonly.aof.manufest 파일에는 매번 백업 시마다 백업되는 .rdb, .aof 파일의 버전을 저장해둔다. 백업본 재작성마다 숫자가 올라간다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722960013249&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu:/var/lib/docker/volumes/redis-data/_data/appendonlydir$ sudo cat appendonly.aof.1.incr.aof
*2
$6
SELECT
$1
0
*3
$3
set
$1
a
$1
b
*2
$6
SELECT
$1
0
*3
$3
set
$1
c
$1
d&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.rdb는 바이너리파일이라 읽기도 어렵지만 .aof 파일은 repl로 저장이 되어 알아볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 flushall 로 모든 데이터가 날아갔대도, 쓰기작업마다 순차적으로 기록되는 .aof 파일의 마지막에 있는 flushall 명령어를 지우고 레디스를 다시 실행하면 복구가 가능하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>docker</category>
      <category>redis</category>
      <category>redis.conf</category>
      <category>volume</category>
      <category>도커</category>
      <category>도커볼륨</category>
      <category>레디스</category>
      <category>레디스설정</category>
      <category>레디스설정공유</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/192</guid>
      <comments>https://promisingmoon.tistory.com/192#entry192comment</comments>
      <pubDate>Tue, 6 Aug 2024 23:51:56 +0900</pubDate>
    </item>
    <item>
      <title>24/08/05(월) 92번째 TIL : 레디스 인증 설정</title>
      <link>https://promisingmoon.tistory.com/191</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 레디스 해킹을 당해놓고서 아직도 정신 못 차리고 비밀번호 설정을 귀찮아서 안 해두다가 이번에 제대로 알아두기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 도커로 레디스 설치부터 암호 설정, 암호로 접속하는 방법까지 다뤄보기로.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;레디스 도커 설치&lt;/h2&gt;
&lt;pre id=&quot;code_1722840832771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run -d \ 
  --name redis-ex \
  -p 63790:6379 \
  -e REDIS_ARGS=&quot;--requirepass 1234&quot; \
  redis/redis-stack-server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 레디스 스택을 포함하는 &lt;a href=&quot;https://hub.docker.com/r/redis/redis-stack-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;redis-stack-server 이미지&lt;/a&gt;이고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722842285924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker run -d \ 
  --name redis-ex \
  -p 63790:6379 \
  redis --requirepass 1234&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 그냥&lt;a href=&quot;https://hub.docker.com/_/redis&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt; 레디스일 때&lt;/a&gt;이다. 각각 비밀번호를 설정하는 방법이 다르다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--requiredpass 1234는 1234 라고 암호를 지정해주겠다는 명령어이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트도 기본 포트인 6379에 무작위로 요청보내서 비밀번호 안 걸려있는 경우에 기존 키를 싹다 flushall 해서 없앤 뒤 스크립트를 넣어두고 사용하는 식으로 해킹 당한 적이 있기 때문에 항상 0을 하나 더 붙여서 사용하는 편이다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ACL 설정&amp;nbsp;&lt;/h2&gt;
&lt;pre id=&quot;code_1722842487986&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo docker exec -it redis-ex redis-cli&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 레디스를 사용하려고 이와 같이 명령어를 입력하면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722842503627&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; ping
(error) NOAUTH Authentication required.
127.0.0.1:6379&amp;gt; set a b
(error) NOAUTH Authentication required.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 인증이 필요하다고 에러가 난다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722842527774&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;127.0.0.1:6379&amp;gt; auth 1234
OK
127.0.0.1:6379&amp;gt; ping
PONG&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth &amp;lt;비밀번호&amp;gt; 를 입력하면 인증이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 레디스 6버전 부터는 ACL(Access Control List)로 유저라는 개념이 도입됐다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 유저를 통해 실행 가능 커맨드와 접근 가능 키를 제한할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722843878939&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ACL SETUSER yunjae on &amp;gt;1234 ~* allcommands&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yunjae 라는 유저의 비밀번호를 1234로 설정하고 (&amp;gt;1234), 모든 키에 대해 접근 가능(~*), 모든 커맨드 허용(allcommands)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722844555943&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;acl setuser reader on &amp;gt;abcd ~read-only:* +@all -@set &amp;amp;*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 reader 라는 유저의 비밀번호를 abcd로 하고, 키는 read-only: 로 시작하는 키만 접근 가능하며, +@all 은 모든 커맨드 사용 가능하고, -@set 은 set 커맨드는 사용 불가능 하다는 뜻이다. &amp;amp;*는 모든 pub/sub채널에 대해 접근 가능하다는 뜻이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 상태는 on/off로 제어할 수 있다. on은 접근 허용, off는 유저는 만들어두지만 접근은 막아두겠다는 뜻이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 위에서 auth 1234 로 인증했는데, 이것은 구버전과의 호환성을 위해 남겨둔 것으로, 이후로는 auth &amp;lt;user&amp;gt; &amp;lt;password&amp;gt;로 접속하며, 기존의 requirepass로 한 경우 default 가 유저가 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722844930448&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;auth default 1234
auth reader abcd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 redis-cli로 접속 후 위와 같이 입력하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722845228023&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;redis-cli -h yunjae.click -p 6379 -a 1234
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.

redis-cli -h yunjae.click -p 6379 --user reader --pass abcd
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천하지는 않지만 redis-cli 에서 바로 인증이 가능하긴 하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;권한 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 위에서 reader 라는 유저는 read-only: 로 시작하는 키만 접근이 가능하고, set 커맨드는 사용이 불가능 하다고 설정했다. 이를 확인해보면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722845693532&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;localhost:6379&amp;gt; set a b
(error) NOPERM No permissions to access a key

localhost:6379&amp;gt; get a
(error) NOPERM No permissions to access a key

localhost:6379&amp;gt; get read-only:a
&quot;b&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 set read-only:a b 를 해둔 상태이다. 정상적으로 동작하는 것을 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 링크&amp;nbsp;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@inhwa1025/Redis-ACL-%EC%9D%B4%EB%9E%80-ACL-%EC%82%AC%EC%9A%A9-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;벨로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>ACL</category>
      <category>docker</category>
      <category>redis</category>
      <category>rediscli</category>
      <category>redisstack</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/191</guid>
      <comments>https://promisingmoon.tistory.com/191#entry191comment</comments>
      <pubDate>Mon, 5 Aug 2024 17:25:35 +0900</pubDate>
    </item>
    <item>
      <title>24/08/02(금) 91번째 TIL : EC2 Ubuntu 포트포워딩</title>
      <link>https://promisingmoon.tistory.com/190</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 자료에 나온대로 포트포워딩을 진행했지만 실제로는 포트포워딩이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 자료에 나온 명령어는 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722601914068&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 보안그룹 인바운드 규칙을 확인해봐도, 서버 내에 프로세스가 정상적으로 실행되었음을 확인해봐도 80포트로 접속이 되지 않았다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1722604297220&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ubuntu@ip-172-31-41-183:~$  sudo iptables -t nat -L --line-numbers
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination
1    REDIRECT   tcp  --  anywhere             anywhere             tcp dpt:http redir ports 8080

Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
num  target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
num  target     prot opt source               destination&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 명령어로 포트포워딩이 정상적으로 설정됐는지 확인했지만 잘 되어 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 위의 명령어에 대한 자세한 설명은&amp;nbsp;&lt;a href=&quot;https://magpienote.tistory.com/183&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 블로그&lt;/a&gt;를 참고하고, 간략하게 설명하면 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iptables : ip 관리하는 테이블로, 방화벽을 설정하는 명령어&lt;/li&gt;
&lt;li&gt;-t nat : NAT 테이블을 선택&lt;/li&gt;
&lt;li&gt;-A PREROUTING : -A는 새로운 정책을 추가, -A PREROUTING는 라우팅 전에 결정되는 규칙을 추가&amp;nbsp;&lt;/li&gt;
&lt;li&gt;-i eh0 : eth0인 입력 네트워크 인터페이스와 매칭&lt;/li&gt;
&lt;li&gt;-p tcp : TCP 프로토콜과 매칭&lt;/li&gt;
&lt;li&gt;-dPort 80 : 목적지 포트가 80에 대해 매칭&amp;nbsp;&lt;/li&gt;
&lt;li&gt;-j REDIRECT : -j는 jump로, 매칭되는 패킷을 어떻게 처리할지 결정, REDIRECT는 리디렉션.&lt;/li&gt;
&lt;li&gt;-to-port 8080 : 패킷을 8080으로 리디렉션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 eth0 네트워크 인터페이스로 들어오는 패킷 중, 목적지 포트가 80인 TCP 패킷을 8080 포트로 리디렉션 해주는 명령어이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 글을 찾아보니 입력 네트워크 인터페이스를 바꿔주니 해결되었고, 그래서 나는 우분투의 각 버전과 아키텍처 별로 NI를 조사해보았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 각 버전 별 ifconfig 명령어의 결과다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우분투 24.04 - ARM&lt;/h3&gt;
&lt;pre id=&quot;code_1722602940499&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ens5: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 9001
        inet 172.31.41.183  netmask 255.255.240.0  broadcast 172.31.47.255
        inet6 fe80::822:cff:fed1:371f  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 0a:22:0c:d1:37:1f  txqueuelen 1000  (Ethernet)
        RX packets 22538  bytes 29638477 (29.6 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 11742  bytes 917655 (917.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 170  bytes 18165 (18.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 170  bytes 18165 (18.1 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우분투 24.04 - X86(64)&lt;/h3&gt;
&lt;pre id=&quot;code_1722603012575&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enX0: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 9001
        inet 172.31.2.58  netmask 255.255.240.0  broadcast 172.31.15.255
        inet6 fe80::31:faff:fe19:a1b  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 02:31:fa:19:0a:1b  txqueuelen 1000  (Ethernet)
        RX packets 21329  bytes 29947584 (29.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2277  bytes 260858 (260.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 166  bytes 17850 (17.8 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 166  bytes 17850 (17.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우분투 22.04 - ARM&lt;/h3&gt;
&lt;pre id=&quot;code_1722603049910&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ens5: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 9001
        inet 172.31.37.9  netmask 255.255.240.0  broadcast 172.31.47.255
        inet6 fe80::84c:38ff:fe39:717  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 0a:4c:38:39:07:17  txqueuelen 1000  (Ethernet)
        RX packets 22354  bytes 32723724 (32.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 10664  bytes 802619 (802.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 174  bytes 18800 (18.8 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 174  bytes 18800 (18.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우분투 22.04 - X86(64)&amp;nbsp;&lt;/h3&gt;
&lt;pre id=&quot;code_1722603070025&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eth0: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 9001
        inet 172.31.8.217  netmask 255.255.240.0  broadcast 172.31.15.255
        inet6 fe80::e:5dff:fe7d:c547  prefixlen 64  scopeid 0x20&amp;lt;link&amp;gt;
        ether 02:0e:5d:7d:c5:47  txqueuelen 1000  (Ethernet)
        RX packets 24261  bytes 34927370 (34.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2865  bytes 297580 (297.5 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 166  bytes 18114 (18.1 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 166  bytes 18114 (18.1 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 및 아키텍처 별 네트워크 인터페이스 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표로 정리하면 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;우분투 버전&lt;/td&gt;
&lt;td style=&quot;width: 40%; text-align: center;&quot; colspan=&quot;2&quot;&gt;우분투 24.04&lt;/td&gt;
&lt;td style=&quot;width: 40%; text-align: center;&quot; colspan=&quot;2&quot;&gt;우분투 22.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;아키텍처&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;ARM&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;X86&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;ARM&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;X86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;네트워크&lt;br /&gt;인터페이스&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;ens5&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;enX0&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;ens5&lt;/td&gt;
&lt;td style=&quot;width: 20%; text-align: center;&quot;&gt;eth0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 버전 및 아키텍처에 따라 달라진다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위의 명령어에서 eth0 부분을 현재 서버의 스펙에 맞게 변경해주면 정상적으로 동작이 된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나와 같은 경우는 t4g.nano를 사용하여 ARM 아키텍처를 사용하고 있으므로 우분투 버전에 상관없이 eth0를 ens5로 변경해주면 정상적으로 포트포워딩이 적용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;NIC 명명법이 변경된 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투에서 Predictable Network Interface Names 라고 하는, 네트워크 인터페이스 이름을 관리하는 방식이 변경이 되었기 때문이다. (&lt;a href=&quot;https://flavono123.github.io/posts/get-net-dev-from-ip/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고 : 블로그&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 변화는 &lt;a href=&quot;https://lwn.net/Articles/531850/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;systemd 197&lt;/a&gt; 에서부터 시작됐고, 기존의 예측 불가능한 명명법에서 예측이 가능하도록 이름을 할당하도록 명명법이 바뀌었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;pf&amp;gt;&amp;lt;type&amp;gt;&amp;lt;bus_id&amp;gt; 로 구성되며(&lt;a href=&quot;https://www.ibm.com/docs/en/linux-on-systems?topic=interfaces-predictable-names&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;출처 : IBM&lt;/a&gt;), 이더넷(pf가en)으로 예시를 든다면 아래와 같다. (출처 : &lt;a href=&quot;https://systemd.io/PREDICTABLE_INTERFACE_NAMES/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;systemd&lt;/a&gt;, &lt;a href=&quot;https://www.thomas-krenn.com/en/wiki/Predictable_Network_Interface_Names&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;systemd를 기반으로 하는 위키&lt;/a&gt;)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #3f4042; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;eno&lt;/b&gt;: Names containing the index numbers provided by firmware/BIOS for on-board devices, example: eno1 (en&lt;b&gt;o&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;O&lt;/b&gt;nboard).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ens&lt;/b&gt;: Names containing the PCI Express hotplug slot numbers provided by the firmware/BIOS, example: ens1 (en&lt;b&gt;s&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;S&lt;/b&gt;lot).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;enp&lt;/b&gt;: Names containing the physical/geographical location of the hardware's port, example: enp2s0 (en&lt;b&gt;p&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;=&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;P&lt;/b&gt;osition).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;enx&lt;/b&gt;: Names containing the MAC address of the interface (example: enx78e7d1ea46da).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;eth&lt;/b&gt;: Classic unpredictable kernel-native ethX naming (example: eth0).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; AWS의 우분투 24.04 LTS버전에서는 enx* 이므로 맥주소를 기반으로 하는 명명법을 따랐다고 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TMI&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.37.29.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkspVJ/btsITvJqiB7/zohzqzujdS4EEDqWUq82L1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkspVJ/btsITvJqiB7/zohzqzujdS4EEDqWUq82L1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkspVJ/btsITvJqiB7/zohzqzujdS4EEDqWUq82L1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkspVJ%2FbtsITvJqiB7%2FzohzqzujdS4EEDqWUq82L1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1052&quot; height=&quot;290&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.37.29.png&quot; data-origin-width=&quot;1052&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이거 테스트 한다고 우분투 24.04부터 22.04까지 ARM과 X86기반 아키텍처 모두 만들어서 해봤다 ㅎㅎ&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.39.23.png&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOqLtZ/btsITQfuDdF/MSCzzgMrcgwjlVNr4UdUq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOqLtZ/btsITQfuDdF/MSCzzgMrcgwjlVNr4UdUq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOqLtZ/btsITQfuDdF/MSCzzgMrcgwjlVNr4UdUq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOqLtZ%2FbtsITQfuDdF%2FMSCzzgMrcgwjlVNr4UdUq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;761&quot; height=&quot;267&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.39.23.png&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투 20.04 - x86으로도 해보려고 했지만 (ARM은 아예 없다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.39.33.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqSxaB/btsISOiAdFH/pRTTBWNPUKolPKiXDuBTj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqSxaB/btsISOiAdFH/pRTTBWNPUKolPKiXDuBTj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqSxaB/btsISOiAdFH/pRTTBWNPUKolPKiXDuBTj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqSxaB%2FbtsISOiAdFH%2FpRTTBWNPUKolPKiXDuBTj1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;270&quot; data-filename=&quot;스크린샷 2024-08-02 오후 9.39.33.png&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우분투 20.04 버전으로는 Microsoft SQL Server와 함께 있는 AMI 인데, 일단 t2의 nano, micro, small은 모두 안 됐다,,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%9A%B0%EB%B6%84%ED%88%AC_%EB%B2%84%EC%A0%84_%EC%97%AD%EC%82%AC#cite_note-3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;20.04 버전은 LTS 종료&lt;/a&gt;가 1년도 안 남았으니 그냥 안 쓰는 걸로,,&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>버전</category>
      <category>우분투</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/190</guid>
      <comments>https://promisingmoon.tistory.com/190#entry190comment</comments>
      <pubDate>Fri, 2 Aug 2024 23:07:43 +0900</pubDate>
    </item>
    <item>
      <title>24/08/01(목) 90번째 TIL : AWS Amazon VPC CIDR 범위</title>
      <link>https://promisingmoon.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Amazon VPC&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS는 Amazon VPC라는 서비스로 가상의 네트워크를 구성할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VPC 내의 최대 개수 제한&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC는 리전마다 독립적으로 구성되어 있으며, 하나의 리전당 5개의 VPC를 구성할 수 있다. (&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/amazon-vpc-limits.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서 출처&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 VPC당 200개의 서브넷이 최대이며 CIDR 블록은 IPv4와 IPv6 각각 5개가 최대이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VPC를 만들 때는 IPv4 CIDR 블록을 지정해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VPC CIDR 허용 범위&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-cidr-blocks.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에서는 프라이빗 IP 주소 범위에 속하도록 권장하고 있지만, 허용된 블록의 크기는 /16 서브넷마스크부터 /28 서브넷마스크 까지다. 즉, IP주소는 65,536개부터 16개까지 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style8&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;사설 IP 범위 (출처 : &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1918#section-3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RFC 1918&lt;/a&gt;)&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;CIDR 블록 예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;10.0.0.0 ~ 10.255.255.255&lt;br /&gt;(10.0.0.0/8)&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;10.0.0.0/16&lt;br /&gt;(VPC는 /16 넷마스크부터로 제한됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;172.16.0.0 ~ 172.31.255.255&lt;br /&gt;(172.16.0.0/12)&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;172.31.0.0/16&lt;br /&gt;(VPC는 /16 넷마스크부터로 제한됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;192.168.0.0 ~ 192.168.255.255&lt;br /&gt;(192.168.0.0/16)&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;192.168.0.0/20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2024-08-01 오후 12.30.02.png&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kJ2Js/btsIRw9Yw4S/Q0cwMy0bOVo22le3cUs2SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kJ2Js/btsIRw9Yw4S/Q0cwMy0bOVo22le3cUs2SK/img.png&quot; data-alt=&quot;RFC 1918 section-3 : 사설 IP 주소공간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kJ2Js/btsIRw9Yw4S/Q0cwMy0bOVo22le3cUs2SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkJ2Js%2FbtsIRw9Yw4S%2FQ0cwMy0bOVo22le3cUs2SK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;148&quot; data-filename=&quot;스크린샷 2024-08-01 오후 12.30.02.png&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RFC 1918 section-3 : 사설 IP 주소공간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CIDR 범위 지정 시 주의할 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 일부 AWS 서비스의 경우(AWS Cloud9, Amazon SageMaker 등) 172.1.0.0/16 범위를 사용하고 있으므로, 충돌을 방지하려면 이 범위는 사용하지 않는 것을 권장하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이 외에 제한되는 IPv4 및 IPv6 VPC CIDR 블록은 &lt;a href=&quot;https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-cidr-blocks.html#add-cidr-block-restrictions&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식문서&lt;/a&gt;에 자세히 안내하고 있다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>AWS</category>
      <category>CIDR</category>
      <category>Network</category>
      <category>privateip</category>
      <category>subnet</category>
      <category>VPC</category>
      <category>사설IP</category>
      <author>yunjae62</author>
      <guid isPermaLink="true">https://promisingmoon.tistory.com/189</guid>
      <comments>https://promisingmoon.tistory.com/189#entry189comment</comments>
      <pubDate>Thu, 1 Aug 2024 12:37:00 +0900</pubDate>
    </item>
  </channel>
</rss>