<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>return new Story();</title>
    <link>https://parse.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Apr 2026 02:29:02 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Parse</managingEditor>
    <image>
      <title>return new Story();</title>
      <url>https://tistory1.daumcdn.net/tistory/5174213/attach/d492a5e3288b4fc38303dd007de6b23b</url>
      <link>https://parse.tistory.com</link>
    </image>
    <item>
      <title>맥북 아이폰 복사 붙여넣기 안 될 때 해결방법</title>
      <link>https://parse.tistory.com/23</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/06GXO/dJMcagZfMJC/mVK2zvsTdCklT2y4g787kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F06GXO%2FdJMcagZfMJC%2FmVK2zvsTdCklT2y4g787kk%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;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;맥&amp;harr;아이폰 복사 붙여넣기 안 될 때 터미널 명령어 한 줄로 해결 (Universal Clipboard)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Handoff 켜져 있고, Wi-Fi&amp;middot;Bluetooth 정상인데도 맥-아이폰 간 복붙이 안 된다면 이 글이 답입니다.&lt;/p&gt;
&lt;/blockquote&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;style6&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;/li&gt;
&lt;li&gt;아이폰에서 복사한 내용이 맥에서 안 나옴&lt;/li&gt;
&lt;li&gt;같은 Apple ID, 같은 Wi-Fi, Bluetooth ON, Handoff ON &amp;mdash; 전부 정상인데 안 됨&lt;/li&gt;
&lt;li&gt;&quot;유니버설 클립보드&quot; 자체가 죽어 있는 느낌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링하면 다 똑같은 해결법만 나옵니다. 저도 전부 따라해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 AI한테 물었는데도 같은 답변만 반복합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;1. Handoff 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥: 시스템 설정 &amp;rarr; 일반 &amp;rarr; AirDrop 및 Handoff &amp;rarr; Handoff 끄고 &amp;rarr; 10초 기다렸다가 &amp;rarr; 다시 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이폰: 설정 &amp;rarr; 일반 &amp;rarr; AirPlay 및 Handoff &amp;rarr; 똑같이 껐다 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Bluetooth 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽 기기 Bluetooth 끄고 &amp;rarr; 30초 기다렸다가 &amp;rarr; 다시 켜기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Wi-Fi 껐다 켜기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 Wi-Fi 문제인가 싶어서 양쪽 다 껐다 켜봄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. iCloud 로그아웃 후 재로그인&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥에서 Apple ID 로그아웃하고 다시 로그인. 아이폰도 마찬가지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ❌ 시간만 오래 걸리고 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 양쪽 기기 재부팅&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;rarr; ❌ 안 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 하면 보통 &quot;아 그냥 에어드롭 쓰자...&quot; 하고 포기하게 됩니다.&lt;/p&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;터미널 명령어 2줄&lt;/b&gt;로 해결됐습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 해결법: 터미널 명령어 2줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥에서 &lt;b&gt;터미널&lt;/b&gt;(Terminal.app)을 열고 아래 두 줄을 순서대로 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;defaults delete ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;defaults write ~/Library/Preferences/com.apple.coreservices.useractivityd.plist ClipboardSharingEnabled 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝입니다. &lt;b&gt;재시작 없이 바로 적용&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 후 맥에서 아무 텍스트나 복사(⌘+C)하고 아이폰에서 붙여넣기 해보세요.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이게 왜 되는 건가요?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useractivityd는 macOS에서 &lt;b&gt;Handoff, Universal Clipboard&lt;/b&gt; 등을 담당하는 백그라운드 데몬(daemon)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 데몬이 참조하는 plist(설정 파일)에 ClipboardSharingEnabled 값이 있는데, macOS 업데이트나 설정 충돌 등으로 이 값이 &lt;b&gt;꼬이는&lt;/b&gt; 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;명령어 하는 일&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;defaults delete ... ClipboardSharingEnabled&lt;/td&gt;
&lt;td&gt;기존의 꼬인 설정값을 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;defaults write ... ClipboardSharingEnabled 1&lt;/td&gt;
&lt;td&gt;클립보드 공유를 활성화(1)로 새로 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템 설정 UI에서는 Handoff가 &quot;켜짐&quot;으로 보여도, 이 plist 값은 별도로 관리되기 때문에 UI상으로는 확인이 안 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래도 안 된다면 &amp;mdash; 기본 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 해결 안 되는 경우, 기본 조건이 안 맞는 건 아닌지 확인해보세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 같은 Apple ID인지 확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맥: 시스템 설정 &amp;rarr; Apple ID&lt;/li&gt;
&lt;li&gt;아이폰: 설정 &amp;rarr; 맨 위 프로필&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Handoff 켜져 있는지 확인&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맥: 시스템 설정 &amp;rarr; 일반 &amp;rarr; AirDrop 및 Handoff&lt;/li&gt;
&lt;li&gt;아이폰: 설정 &amp;rarr; 일반 &amp;rarr; AirPlay 및 Handoff&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Wi-Fi &amp;amp; Bluetooth 둘 다 ON&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유니버설 클립보드는 Bluetooth로 기기를 인식하고, Wi-Fi로 데이터를 전송합니다&lt;/li&gt;
&lt;li&gt;두 기기가 물리적으로 가까이 있어야 합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 양쪽 재부팅&lt;/b&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;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&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;macOS 26.3.1&lt;/li&gt;
&lt;li&gt;iPhone (최신 iOS)&lt;/li&gt;
&lt;li&gt;동일 iCloud 계정, 동일 Wi-Fi&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;분명히 다 켜져 있는데 왜 안 되지?&quot;라는 상황이라면, 십중팔구 useractivityd의 plist 설정이 꼬인 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 명령어 2줄이면 해결되니 한번 시도해보세요.&lt;/p&gt;</description>
      <category>생산성</category>
      <category>맥북</category>
      <category>문제</category>
      <category>복붙</category>
      <category>아이폰</category>
      <category>안됨</category>
      <category>에러</category>
      <category>클립보드</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/23</guid>
      <comments>https://parse.tistory.com/23#entry23comment</comments>
      <pubDate>Mon, 30 Mar 2026 20:08:32 +0900</pubDate>
    </item>
    <item>
      <title>SSE | 실시간 결제 알림 시스템 개발기 (3) - SSE와 LoggingFilter 충돌 해결기</title>
      <link>https://parse.tistory.com/22</link>
      <description>&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;1021&quot; data-origin-height=&quot;872&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXW6yy/dJMcacPoDCs/PiizNYaHiWO5HNT6qWrHmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXW6yy%2FdJMcacPoDCs%2FPiizNYaHiWO5HNT6qWrHmk%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;1021&quot; height=&quot;872&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;872&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서는 로드밸런싱 환경에서의 문제를 Redis Pub/Sub으로 해결한 과정을 다뤘습니다. 기술적으로는 완성된 것처럼 보였지만, 실제 배포 전 테스트 환경에서 예상치 못한 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE 연결은 성공하는데, 거래 알림이 제대로 전달되지 않거나 연결이 비정상적으로 종료되는 현상이었습니다. 로그를 확인해보니 사내에서 공통으로 사용하는 LoggingFilter가 원인이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 SSE와 LoggingFilter의 충돌 원인, 해결 과정, 그리고 그 과정에서의 기술적 고민을 공유합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 발견&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;증상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 환경에 배포 후 다음과 같은 문제들이 발생했습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;1. SSE 연결은 성공하지만 메시지가 전달되지 않음
2. 연결이 예상보다 빨리 종료됨
3. 간헐적으로 500 에러 발생&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;문제의 원인은 &lt;b&gt;사내 공통 라이브러리의 LoggingFilter&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 HTTP 요청-응답 사이클은 이렇습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;Request &amp;rarr; Filter &amp;rarr; Controller &amp;rarr; Response &amp;rarr; Filter &amp;rarr; Client
           &amp;uarr;_____ 여기서 로깅 _____&amp;uarr;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggingFilter는 요청과 응답을 가로채서 로그를 남기는데, 이 과정에서:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Request body를 읽어서 로깅&lt;/li&gt;
&lt;li&gt;Response body를 읽어서 로깅&lt;/li&gt;
&lt;li&gt;연결 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그런데 SSE는 다릅니다:&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;Request &amp;rarr; Filter &amp;rarr; Controller &amp;rarr; [Connection Held Open]
                                        &amp;darr;
                                   [Streaming...]
                                        &amp;darr;
                                   [Event 1, 2, 3...]
                                        &amp;darr;
                                   [Still Open...]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 연결을 계속 유지하면서 스트리밍 방식으로 데이터를 전송합니다. 하지만 LoggingFilter는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Response를 즉시 읽으려고 시도&lt;/b&gt; &amp;rarr; SSE는 아직 데이터를 보내지 않았음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection을 빨리 종료&lt;/b&gt; &amp;rarr; SSE 스트림이 끊김&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Buffering 시도&lt;/b&gt; &amp;rarr; 메모리 문제 발생 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;시도 1: URL 패턴 제외 (실패)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 Filter 등록 시 URL 패턴으로 제외하려고 했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Bean
public FilterRegistrationBean&amp;lt;LoggingFilter&amp;gt; loggingFilter() {
    FilterRegistrationBean&amp;lt;LoggingFilter&amp;gt; registration = 
        new FilterRegistrationBean&amp;lt;&amp;gt;();
    
    registration.setFilter(loggingFilter);
    registration.addUrlPatterns(&quot;/*&quot;);
    // SSE 경로 제외 시도
    registration.addInitParameter(&quot;excludePattern&quot;, &quot;/api/notifications/subscribe/*&quot;);
    
    return registration;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt; 사내 라이브러리의 LoggingFilter가 이미 @Component로 자동 등록되어 있었고, 제외 패턴 기능을 지원하지 않았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시도 2: Filter Order 조정 (부분적 성공)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE 요청만 먼저 처리하는 Filter를 추가했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SseBypassFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) {
        String uri = request.getRequestURI();
        
        if (uri.contains(&quot;/subscribe&quot;)) {
            request.setAttribute(&quot;SKIP_LOGGING&quot;, true);
        }
        
        filterChain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt; LoggingFilter가 SKIP_LOGGING 플래그를 체크하지 않았습니다. 사내 라이브러리를 수정할 수 없으므로 이 방법도 실패했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최종 해결: @Primary로 Bean 재정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 @Primary 어노테이션을 사용하여 LoggingFilter를 재정의하는 방식으로 해결했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LoggingFilter 래핑&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class FilterConfig {
    
    @Primary
    @Bean
    public OncePerRequestFilter customLoggingFilter(
            @Qualifier(&quot;loggingFilter&quot;) OncePerRequestFilter originalFilter) {
        
        return new OncePerRequestFilter() {
            
            @Override
            protected void doFilterInternal(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    FilterChain filterChain) throws ServletException, IOException {
                
                // SSE 요청인지 확인
                if (isSseRequest(request)) {
                    // SSE는 원본 필터를 거치지 않고 바로 통과
                    filterChain.doFilter(request, response);
                    return;
                }
                
                // 일반 요청은 원본 로깅 필터 실행
                originalFilter.doFilter(request, response, filterChain);
            }
            
            private boolean isSseRequest(HttpServletRequest request) {
                String uri = request.getRequestURI();
                String accept = request.getHeader(&quot;Accept&quot;);
                
                // URL 패턴으로 확인
                if (uri.contains(&quot;/api/notifications/subscribe&quot;)) {
                    return true;
                }
                
                // Accept 헤더로 확인
                if (accept != null &amp;amp;&amp;amp; accept.contains(&quot;text/event-stream&quot;)) {
                    return true;
                }
                
                return false;
            }
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작&amp;nbsp;원리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Primary 어노테이션은 같은 타입의 Bean이 여러 개 있을 때 &lt;b&gt;우선순위를 지정&lt;/b&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1768555975518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Before]
Application &amp;rarr; LoggingFilter (사내 라이브러리) &amp;rarr; Controller

[After]  
Application &amp;rarr; CustomLoggingFilter (@Primary) &amp;rarr; Controller
                      &amp;darr;
              SSE 요청? 
                ↙        ↘
             Yes          No
              &amp;darr;            &amp;darr;
          바로 통과    원본 Filter 실행&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;사내 라이브러리 코드 수정 불필요&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 로깅 기능 유지&lt;/b&gt; (일반 요청은 그대로)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSE만 선택적으로 우회&lt;/b&gt; 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;사내 라이브러리 팀에 개선 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;가장 근본적인 해결책&lt;/b&gt;입니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// 사내 라이브러리에 추가 요청할 기능
@Component
public class LoggingFilter extends OncePerRequestFilter {
    
    @Value(&quot;${logging.filter.exclude-patterns:}&quot;)
    private String[] excludePatterns;
    
    @Override
    protected void doFilterInternal(...) {
        if (shouldSkip(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        // 로깅 로직...
    }
    
    private boolean shouldSkip(HttpServletRequest request) {
        String uri = request.getRequestURI();
        
        for (String pattern : excludePatterns) {
            if (uri.matches(pattern)) {
                return true;
            }
        }
        
        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 설정 파일에서 간단하게 제외할 수 있습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;yaml&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;logging:
  filter:
    exclude-patterns:
      - &quot;/api/notifications/subscribe/.*&quot;
      - &quot;/api/streaming/.*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&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;깔끔한 설정 기반 제어&lt;/li&gt;
&lt;li&gt;유지보수성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&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;다른 팀의 일정에 의존&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;p data-ke-size=&quot;size16&quot;&gt;결국 @Primary 방식을 선택한 이유는:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시간 제약&lt;/b&gt;: 프로젝트 일정이 촉박했음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소 영향&lt;/b&gt;: 기존 시스템에 영향 없이 빠르게 해결&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실용성&lt;/b&gt;: 완벽하지 않지만 요구사항은 충족&lt;/li&gt;
&lt;/ol&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;기술 부채&lt;/b&gt;를 남깁니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&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;다른 개발자가 원본 Filter가 동작한다고 착각 가능&lt;/li&gt;
&lt;li&gt;암묵적인 오버라이드로 디버깅 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대응 방안:&lt;/b&gt;&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 레거시 시스템과의 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 프로젝트가 그린필드는 아닙니다. 기존 시스템, 공통 라이브러리와의 충돌은 &lt;b&gt;실무에서 흔한 일&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽한 해결책을 고집하기보다, 현실적인 제약 속에서 &lt;b&gt;최선의 선택&lt;/b&gt;을 하는 것이 중요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문서화의 중요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 해결책일수록 &lt;b&gt;명확한 문서화&lt;/b&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;어떤 제약사항이 있었는지&lt;/li&gt;
&lt;li&gt;향후 개선 방향은 무엇인지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월 후 다른 개발자(혹은 미래의 나)가 코드를 볼 때를 대비해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 기술 부채 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 부채는 &lt;b&gt;나쁜 것이 아닙니다&lt;/b&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;b&gt;인지&lt;/b&gt;하고 있는가&lt;/li&gt;
&lt;li&gt;부채를 &lt;b&gt;관리&lt;/b&gt;하고 있는가&lt;/li&gt;
&lt;li&gt;적절한 시점에 &lt;b&gt;상환&lt;/b&gt; 계획이 있는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 백로그에 개선 작업을 등록하고, 사내 라이브러리 팀에 feature request를 제출했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SSE vs WebSocket&lt;/b&gt;: 요구사항에 맞는 기술 선택의 중요성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 환경&lt;/b&gt;: 단일 서버와는 다른 고민과 해결책&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레거시 통합&lt;/b&gt;: 현실적 제약 속에서의 의사결정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기술 부채&lt;/b&gt;: 인지하고 관리하는 자세&lt;/li&gt;
&lt;/ol&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;p data-ke-size=&quot;size16&quot;&gt;이 시리즈가 실시간 통신 시스템을 구축하거나, 분산 환경에서의 문제를 해결하는 분들에게 도움이 되길 바랍니다.&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;SSE 재연결 처리&lt;/b&gt;: Last-Event-ID를 활용한 메시지 재전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Circuit Breaker 패턴&lt;/b&gt;: Redis 장애 시 자동 복구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사내 라이브러리 개선&lt;/b&gt;: 공통 LoggingFilter에 exclude 기능 추가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 최적화&lt;/b&gt;: 대규모 동시 접속 시 메모리 사용량 개선&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;</description>
      <category>개발 지식</category>
      <category>LoggingFilter</category>
      <category>ServerSentEvents</category>
      <category>Spring</category>
      <category>SSE</category>
      <category>기술부채</category>
      <category>레기서시스템</category>
      <category>실시간통신</category>
      <category>트러블슈팅</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/22</guid>
      <comments>https://parse.tistory.com/22#entry22comment</comments>
      <pubDate>Sun, 18 Jan 2026 10:30:51 +0900</pubDate>
    </item>
    <item>
      <title>SSE | 실시간 결제 알림 시스템 개발기 (2) - 로드밸런싱 환경에서의 문제와 Redis Pub/Sub 해결</title>
      <link>https://parse.tistory.com/21</link>
      <description>&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;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/by5Jrc/dJMb99ZrgMa/Ciegdwf1suuQpOgHgutP6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fby5Jrc%2FdJMb99ZrgMa%2FCiegdwf1suuQpOgHgutP6k%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;1024&quot; height=&quot;824&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;824&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;시작하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;%EB%A7%81%ED%81%AC&quot;&gt;이전 글&lt;/a&gt;에서는 SSE를 선택한 이유와 기본 구현 방법을 다뤘습니다. 단일 서버 환경에서는 모든 것이 완벽하게 동작했지만, 실제 운영 환경은 달랐습니다. 로드밸런서 뒤에 여러 서버 인스턴스가 배포된 환경에서 예상치 못한 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 분산 환경에서 마주한 문제와 Redis Pub/Sub을 활용한 해결 과정을 공유합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로드밸런싱(다중서버) 환경에서 발견한 문제&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경은 다음과 같은 구조였습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;gherkin&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;              [Load Balancer]
                    |
        +-----------+-----------+
        |                       |
   [Server A]            [Server B]
        |                       |
        +----------+------------+
                   |
            [RabbitMQ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 대의 서버가 로드밸런서 뒤에서 동작하고, 하나의 RabbitMQ 인스턴스를 공유하는 구조였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제는 이렇게 발생했습니다:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;가맹점주가 Server A에 SSE로 연결됨&lt;/li&gt;
&lt;li&gt;고객이 결제를 완료&lt;/li&gt;
&lt;li&gt;RabbitMQ에 거래 메시지가 발행됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Server B의 리스너가 메시지를 소비&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Server B는 자신에게 연결된 클라이언트를 찾지만, 가맹점주는 Server A에 연결되어 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가맹점주는 거래 알림을 받지 못함&lt;/b&gt;  &lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이런 일이?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ의 기본 동작 방식 때문입니다. 여러 컨슈머가 같은 큐를 구독하면 &lt;b&gt;라운드로빈 방식으로 메시지를 분배&lt;/b&gt;합니다. 즉, 메시지가 Server A와 Server B에 번갈아가며 전달되는데, 클라이언트가 어느 서버에 연결되어 있는지는 고려하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;거래 1: RabbitMQ &amp;rarr; Server A (가맹점주는 Server B 연결) ❌
거래 2: RabbitMQ &amp;rarr; Server B (가맹점주는 Server B 연결) ✅
거래 3: RabbitMQ &amp;rarr; Server A (가맹점주는 Server B 연결) ❌&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;style6&quot; /&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;1. Sticky Session (검토 후 제외)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서에서 특정 클라이언트를 항상 같은 서버로 라우팅하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&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;서버 간 부하 분산이 불균형해질 수 있음&lt;/li&gt;
&lt;li&gt;RabbitMQ 메시지가 여전히 라운드로빈으로 분배됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RabbitMQ Fanout Exchange (검토 후 제외)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버에 메시지를 브로드캐스트하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제점:&lt;/b&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;기존에 있던 Exchange를 활용하는 것을 목표로 함&lt;/li&gt;
&lt;li&gt;메시지가 중복 처리될 수 있음&lt;/li&gt;
&lt;li&gt;RabbitMQ 구조를 변경해야 함 (기존 시스템과의 호환성)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Redis Pub/Sub (채택!)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ에서 받은 메시지를 Redis Pub/Sub으로 재전송&lt;/b&gt;하여 모든 서버에 브로드캐스트하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 RabbitMQ 구조 변경 불필요&lt;/li&gt;
&lt;li&gt;모든 서버가 메시지를 받을 수 있음&lt;/li&gt;
&lt;li&gt;Redis는 이미 캐시용으로 사용 중이었음&lt;/li&gt;
&lt;li&gt;구현이 비교적 간단함&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Pub/Sub 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아키텍처 변경&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;cs&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;[고객 결제]
     &amp;darr;
[결제 시스템]
     &amp;darr;
[RabbitMQ Queue]
     &amp;darr;
[Server A or B가 메시지 수신]
     &amp;darr;
[Redis Pub/Sub으로 재전송] &amp;larr; 핵심 변경점
     &amp;darr;
[모든 서버가 Subscribe하여 수신]
     &amp;darr;
[각 서버가 자신의 SSE 클라이언트에게 전달]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 RabbitMQ에서 어느 서버가 메시지를 받든, Redis Pub/Sub을 통해 모든 서버에 전달됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 설정&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            MessageListenerAdapter listenerAdapter) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, 
            new PatternTopic(&quot;transaction.*&quot;));
        
        return container;
    }
    
    @Bean
    public MessageListenerAdapter listenerAdapter(
            RedisSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, &quot;onMessage&quot;);
    }
    
    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(
            RedisConnectionFactory connectionFactory) {
        
        RedisTemplate&amp;lt;String, Object&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer&amp;lt;&amp;gt;(Object.class));
        
        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ 리스너 수정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서 메시지를 받으면 Redis로 재전송하도록 수정했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
public class TransactionQueueListener {
    
    private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
    
    @RabbitListener(queues = &quot;transaction.queue&quot;)
    public void handleTransaction(TransactionMessage message) {
        // Redis Pub/Sub으로 브로드캐스트
        String channel = &quot;transaction.&quot; + message.getMerchantId();
        redisTemplate.convertAndSend(channel, message);
        
        log.info(&quot;Published transaction to Redis: merchantId={}, txId={}&quot;, 
            message.getMerchantId(), message.getTransactionId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Subscriber 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버는 Redis 채널을 구독하여 메시지를 수신합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class RedisSubscriber {
    
    private final ObjectMapper objectMapper;
    private final TransactionNotificationService notificationService;
    
    public void onMessage(String message, String pattern) {
        try {
            TransactionMessage transaction = 
                objectMapper.readValue(message, TransactionMessage.class);
            
            String merchantId = transaction.getMerchantId();
            
            log.info(&quot;Received transaction from Redis: merchantId={}, txId={}&quot;, 
                merchantId, transaction.getTransactionId());
            
            // 해당 가맹점에 연결된 SSE 클라이언트에게 전송
            notificationService.sendToMerchant(merchantId, transaction);
            
        } catch (JsonProcessingException e) {
            log.error(&quot;Failed to parse transaction message&quot;, e);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ConcurrentHashMap으로 가맹점별 필터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버는 자신에게 연결된 클라이언트 정보만 ConcurrentHashMap에 저장하고 있습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@Slf4j
public class TransactionNotificationService {
    
    // 가맹점 ID &amp;rarr; SSE Emitter 리스트 매핑
    private final ConcurrentHashMap&amp;lt;String, List&amp;lt;SseEmitter&amp;gt;&amp;gt; emitters = 
        new ConcurrentHashMap&amp;lt;&amp;gt;();
    
    public void sendToMerchant(String merchantId, TransactionMessage message) {
        List&amp;lt;SseEmitter&amp;gt; merchantEmitters = emitters.get(merchantId);
        
        // 이 서버에 해당 가맹점이 연결되어 있지 않으면 무시
        if (merchantEmitters == null || merchantEmitters.isEmpty()) {
            log.debug(&quot;No emitters found for merchant: {}&quot;, merchantId);
            return;
        }
        
        log.info(&quot;Sending to {} emitters for merchant: {}&quot;, 
            merchantEmitters.size(), merchantId);
        
        List&amp;lt;SseEmitter&amp;gt; deadEmitters = new ArrayList&amp;lt;&amp;gt;();
        
        for (SseEmitter emitter : merchantEmitters) {
            try {
                emitter.send(SseEmitter.event()
                    .name(&quot;transaction&quot;)
                    .data(message));
                    
                log.debug(&quot;Successfully sent transaction to emitter&quot;);
                
            } catch (IOException e) {
                log.warn(&quot;Failed to send to emitter, marking as dead&quot;, e);
                deadEmitters.add(emitter);
            }
        }
        
        // 전송 실패한 emitter 제거
        deadEmitters.forEach(emitter -&amp;gt; removeEmitter(merchantId, emitter));
    }
    
    // subscribe, removeEmitter 메서드는 1편과 동일
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;p data-ke-size=&quot;size16&quot;&gt;로드밸런서 없이 두 개의 서버 인스턴스를 직접 실행하여 테스트했습니다:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;bash&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;# Server 1
java -jar merchantApp.jar --server.port=8081

# Server 2
java -jar merchantApp.jar --server.port=8082&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 포트로 번갈아가며 SSE를 연결한 뒤, RabbitMQ에 메시지를 발행하여 모든 경우에 정상 동작하는지 확인했습니다.&lt;/p&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 Pub/Sub의 성능도 충분했고, 메시지 유실 없이 안정적으로 동작했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;고려사항 및 트레이드오프&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Pub/Sub의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&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;간단한 구현&lt;/li&gt;
&lt;li&gt;수평 확장 가능 (서버를 추가해도 동일하게 동작)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&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;b&gt;메시지 지속성 없음&lt;/b&gt;: Redis가 다운되면 메시지 유실 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구독자가 없으면 메시지 버려짐&lt;/b&gt;: 모든 서버가 다운된 상태에서 발행된 메시지는 사라짐&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이 트레이드오프를 수용했나?&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;거래내역 조회 기능 존재&lt;/b&gt;: 가맹점 포털에서 언제든지 거래내역을 조회할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간 알림은 부가 기능&lt;/b&gt;: 실시간으로 못 받아도 조회하면 되므로 치명적이지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사내 모니터링 알림&lt;/b&gt;: Redis 장애 시 즉시 감지하고 대응 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 복잡도 vs 비즈니스 가치&lt;/b&gt;: 완벽한 메시지 보장보다 빠른 출시가 우선&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;style6&quot; /&gt;
&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;Redis Pub/Sub은 완벽한 해결책은 아니지만, 우리 프로젝트의 요구사항과 제약사항을 고려했을 때 &lt;b&gt;가장 실용적인 선택&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 구현 과정에서 또 다른 문제가 있었습니다. 사내에서 공통으로 사용하는 LoggingFilter가 SSE와 충돌하는 문제였죠.&lt;/p&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;다음 글에서는 LoggingFilter 충돌 문제와 그 해결 과정, 그리고 기술 부채에 대한 고민&lt;/b&gt;을 다루겠습니다.&lt;/p&gt;</description>
      <category>개발 지식/Spring</category>
      <category>pub/sub</category>
      <category>redis</category>
      <category>로드밸런싱</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/21</guid>
      <comments>https://parse.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 17 Jan 2026 10:00:01 +0900</pubDate>
    </item>
    <item>
      <title>SSE | 실시간 결제 알림 시스템 개발기 (1) - SSE vs WebSocket 선택 과정</title>
      <link>https://parse.tistory.com/20</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;765&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQH1k5/dJMcadHuHUH/cAk2TqZyUIMnnVYrAu7isK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQH1k5/dJMcadHuHUH/cAk2TqZyUIMnnVYrAu7isK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQH1k5/dJMcadHuHUH/cAk2TqZyUIMnnVYrAu7isK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQH1k5%2FdJMcadHuHUH%2FcAk2TqZyUIMnnVYrAu7isK%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;1022&quot; height=&quot;765&quot; data-origin-width=&quot;1022&quot; data-origin-height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시작하며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 가맹점 결제 시스템에 실시간 알림 기능을 구현하는 프로젝트를 진행했습니다. 가맹점주가 상품과 가격을 설정하여 QR 코드를 생성하면, 고객이 해당 QR 코드를 스캔하여 결제를 진행하고, 결제가 완료되는 즉시 가맹점주의 화면에 거래내역이 실시간으로 표시되는 시스템입니다.&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 요구사항&lt;/h3&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;가맹점주가 상품명과 가격을 입력하여 결제용 QR 코드 생성&lt;/li&gt;
&lt;li&gt;고객이 QR 코드를 스캔하여 결제 진행&lt;/li&gt;
&lt;li&gt;결제 완료 시 가맹점주 화면에 실시간으로 거래내역 표시&lt;/li&gt;
&lt;li&gt;거래내역은 RabbitMQ를 통해 전달됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSE vs WebSocket 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 통신을 구현하기 위해 두 가지 주요 기술을 검토했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WebSocket의 특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&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;실시간성이 매우 뛰어남&lt;/li&gt;
&lt;li&gt;클라이언트와 서버 간 지속적인 상호작용이 필요한 경우 적합 (채팅, 게임 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&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;구현 복잡도가 상대적으로 높음&lt;/li&gt;
&lt;li&gt;HTTP가 아닌 별도 프로토콜 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSE(Server-Sent Events)의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 기반으로 구현이 간단함&lt;/li&gt;
&lt;li&gt;서버에서 클라이언트로의 단방향 통신에 최적화&lt;/li&gt;
&lt;li&gt;자동 재연결 기능 내장&lt;/li&gt;
&lt;li&gt;EventSource API로 클라이언트 구현이 간단함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&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;HTTP/1.1에서는 브라우저당 연결 수 제한 (6~8개)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSE 선택 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트의 요구사항을 분석한 결과, &lt;b&gt;SSE가 더 적합하다&lt;/b&gt;고 판단했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 단방향 통신으로 충분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가맹점주는 결제 완료 알림을 &lt;b&gt;받기만&lt;/b&gt; 하면 됩니다. 실시간으로 서버에 데이터를 보낼 필요가 없었기 때문에 양방향 통신 기능은 불필요했습니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;고객 결제 &amp;rarr; 결제 시스템 &amp;rarr; RabbitMQ &amp;rarr; 서버 &amp;rarr; [SSE] &amp;rarr; 가맹점주 화면&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 WebSocket을 사용했다면, 사용하지도 않을 클라이언트&amp;rarr;서버 채널을 위해 추가 리소스를 소비하게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 구현 복잡도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 HTTP 기반이기 때문에 기존 인프라와의 통합이 쉬웠습니다. 별도의 프로토콜 핸들러나 복잡한 설정 없이 Spring의 SseEmitter만으로 간단하게 구현할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 리소스 효율성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 연결을 유지하는 것은 서버 리소스 측면에서 오버헤드입니다. 우리 시스템에서는 거래가 발생할 때만 데이터를 전송하면 되므로, SSE의 단방향 특성이 더 효율적이었습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버 사이드 - SSE Emitter 관리&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Service
public class TransactionNotificationService {
    
    // 가맹점별로 SSE 연결 관리
    private final ConcurrentHashMap&amp;lt;String, List&amp;lt;SseEmitter&amp;gt;&amp;gt; emitters = 
        new ConcurrentHashMap&amp;lt;&amp;gt;();
    
    public SseEmitter subscribe(String merchantId) {
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
        
        // 가맹점 ID별로 emitter 저장
        emitters.computeIfAbsent(merchantId, k -&amp;gt; new CopyOnWriteArrayList&amp;lt;&amp;gt;())
                .add(emitter);
        
        // 연결 종료 시 정리
        emitter.onCompletion(() -&amp;gt; removeEmitter(merchantId, emitter));
        emitter.onTimeout(() -&amp;gt; removeEmitter(merchantId, emitter));
        emitter.onError(e -&amp;gt; removeEmitter(merchantId, emitter));
        
        return emitter;
    }
    
    private void removeEmitter(String merchantId, SseEmitter emitter) {
        List&amp;lt;SseEmitter&amp;gt; merchantEmitters = emitters.get(merchantId);
        if (merchantEmitters != null) {
            merchantEmitters.remove(emitter);
            if (merchantEmitters.isEmpty()) {
                emitters.remove(merchantId);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConcurrentHashMap을 사용한 이유:&lt;/b&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;가맹점 ID를 키로 하여 해당 가맹점에 연결된 모든 클라이언트 관리&lt;/li&gt;
&lt;li&gt;여러 탭이나 디바이스에서 동시 접속 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ 리스너 - 거래내역 수신&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@Component
public class TransactionQueueListener {
    
    private final TransactionNotificationService notificationService;
    
    @RabbitListener(queues = &quot;transaction.queue&quot;)
    public void handleTransaction(TransactionMessage message) {
        String merchantId = message.getMerchantId();
        
        // 해당 가맹점에 연결된 클라이언트들에게 전송
        notificationService.sendToMerchant(merchantId, message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;public void sendToMerchant(String merchantId, TransactionMessage message) {
    List&amp;lt;SseEmitter&amp;gt; merchantEmitters = emitters.get(merchantId);
    
    if (merchantEmitters != null) {
        List&amp;lt;SseEmitter&amp;gt; deadEmitters = new ArrayList&amp;lt;&amp;gt;();
        
        for (SseEmitter emitter : merchantEmitters) {
            try {
                emitter.send(SseEmitter.event()
                    .name(&quot;transaction&quot;)
                    .data(message));
            } catch (IOException e) {
                deadEmitters.add(emitter);
            }
        }
        
        // 전송 실패한 emitter 제거
        deadEmitters.forEach(emitter -&amp;gt; removeEmitter(merchantId, emitter));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨트롤러 - SSE 엔드포인트&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;java&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/api/notifications&quot;)
public class NotificationController {
    
    private final TransactionNotificationService notificationService;
    
    @GetMapping(value = &quot;/subscribe/{merchantId}&quot;, 
                produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter subscribe(@PathVariable String merchantId) {
        return notificationService.subscribe(merchantId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 사이드&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;javascript&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #383a42; text-align: left;&quot;&gt;&lt;code&gt;// SSE 연결 생성
const eventSource = new EventSource(`/api/notifications/subscribe/${merchantId}`);

// 거래 이벤트 수신
eventSource.addEventListener('transaction', (event) =&amp;gt; {
    const transaction = JSON.parse(event.data);
    
    // 화면에 거래내역 표시
    displayTransaction(transaction);
});

// 연결 에러 처리
eventSource.onerror = (error) =&amp;gt; {
    console.error('SSE connection error:', error);
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 구현이 매우 간단합니다. EventSource API는 자동 재연결 기능까지 제공하여 네트워크 불안정 상황에서도 안정적으로 동작합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;고객이 QR 코드로 결제&lt;/li&gt;
&lt;li&gt;결제 시스템에서 RabbitMQ에 거래 메시지 발행&lt;/li&gt;
&lt;li&gt;서버의 RabbitMQ 리스너가 메시지 수신&lt;/li&gt;
&lt;li&gt;해당 가맹점 ID에 연결된 SSE Emitter를 찾아 전송&lt;/li&gt;
&lt;li&gt;가맹점주 화면에 실시간으로 거래내역 표시&lt;/li&gt;
&lt;/ol&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 서버에서 클라이언트로의 단방향 실시간 통신이 필요한 경우 매우 효과적인 선택입니다. 우리 프로젝트처럼 알림, 피드 업데이트, 실시간 대시보드 등의 사용 사례에서 WebSocket보다 간단하면서도 충분한 성능을 제공합니다.&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;b&gt;다음 글에서는 로드밸런싱 환경에서 발견한 문제와 Redis Pub/Sub을 활용한 해결 과정&lt;/b&gt;을 다루겠습니다.&lt;/p&gt;</description>
      <category>개발 지식/Spring</category>
      <category>Spring</category>
      <category>SSE</category>
      <category>실시간</category>
      <category>웹소켓</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/20</guid>
      <comments>https://parse.tistory.com/20#entry20comment</comments>
      <pubDate>Fri, 16 Jan 2026 17:54:47 +0900</pubDate>
    </item>
    <item>
      <title>동기부여가 되는 생산성  TO-DO앱은 없을까?</title>
      <link>https://parse.tistory.com/19</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2976&quot; data-origin-height=&quot;1938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3WbDc/btsPTMEXIIs/2Xjlirop9laV4d2lfKfJI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3WbDc/btsPTMEXIIs/2Xjlirop9laV4d2lfKfJI1/img.png&quot; data-alt=&quot;ToDoker 프로토타입 메인 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3WbDc/btsPTMEXIIs/2Xjlirop9laV4d2lfKfJI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3WbDc%2FbtsPTMEXIIs%2F2Xjlirop9laV4d2lfKfJI1%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;2976&quot; height=&quot;1938&quot; data-origin-width=&quot;2976&quot; data-origin-height=&quot;1938&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ToDoker 프로토타입 메인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;나는 왜 생산형 TO-DO앱 ToDoker를 개발하는가?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생산형 TO-DO 앱에 대한 개발 의지의 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 처음 생산형 TO-DO 앱에 대한 개발 의지는 자주 사용하던 앱인 Things 3에서 TO-DO를 체크하면 사라져서 성취감을 느끼기 쉽지 않았던 것에서 시작됐다.&lt;/p&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 title=&quot;Read-Tracker&quot; href=&quot;https://chromewebstore.google.com/detail/read-tracker/jgolehhngmpokfjpegicibkoejikcbch?utm_source=item-share-cp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;b&gt;Read-Tracker&lt;/b&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;Things 3에서 체크했던 To-Do들이 남아있어서 현재 프로젝트나 오늘 일정을 얼마나 진행했는지 파악하고 싶었다. Things 3를 사용했던 이유는 예쁘고 직관적인 사용성에 있었는데, 여기에 내 니즈가 추가된다면 완벽할 것 같아서 프로젝트를 시작하게 됐다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그동안 망설였던 이유들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이런 아쉬움은 꽤 오래됐지만, 망설였던 이유가 있었다. 전체 프로젝트를 진행하려면 팀을 꾸리거나 1인 개발로서 전체 개발과 기획, 마케팅을 진행해야 하는데 그 부담감이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;Claude Code&lt;/b&gt;와 각종 AI Agent들, 그리고 &lt;b&gt;MCP(Model Context Protocol)&lt;/b&gt; 가 나오면서 가능성을 봤다. AI가 실제로 개발 파트너가 될 수 있다는 확신이 섰고, 그래서 ToDoker 프로젝트를 본격적으로 시작하게 됐다.&lt;/p&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ToDoker의 3가지 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 시작하면서 명확히 한 3가지 핵심 가치가 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 디자인이 예뻐야 한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&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;하지만 색상 커스터마이징 기능을 구현하면서도 새로운 고민들이 생겼다. 너무 많은 선택지가 오히려 사용자를 혼란스럽게 하지 않을까? 기존 디자인 요소들의 색상들을 커스텀된 색상에 맞춰 변경시켜주는 부분은 오버엔지니어링이 아닐까 등, 이 부분은 다음 블로그 글을 통해에 더 자세히 적어보려 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 사용하기 편해야 한다&lt;/h4&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;인간의 본성을 이해하는 것이 좋은 UX의 시작이라고 생각한다. 네덜란드 스히폴 공항에서 남자 화장실 변기에 파리 모양 스티커를 붙였더니 소변이 80% 더 정확하게 들어갔다는 유명한 사례가 있다. 사람들은 자연스럽게 목표물을 맞추려고 하는 본능이 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 ToDoker에서도 사용자가 자연스럽게 원하는 행동을 하도록 설계하고 있다. 드래그 앤 드롭으로 할 일을 카테고리 간에 이동시키거나, 체크로 완료 처리를 하는 것들이 그런 예시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 의지를 북돋아줘야 한다&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 기존 TO-DO 앱과의 차별성을 가질 수 있는 핵심이라고 생각한다. 핵심 기능은 &lt;b&gt;AI를 통한 인사이트&lt;/b&gt;와 &lt;b&gt;로그(통계) 기능&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 자신이 기록했던 TO-DO들을 통해서 달성을 어느 정도 했고, 앞으로 어떻게 진행해야 할지 AI를 통해 생각이 정리되는 경험을 하게 될 것이다. 그리고 Suggest 기능을 통해서 앞으로 할 일들에 대해서 추천을 받기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &quot;지난주에 비해 업무 관련 할 일 완료율이 20% 증가했네요. 할 일 목록을 보니 지난 주에 비해 조금 벅차 보이지만, 걱정하지 마세요. 지금부터 차근차근 하나씩 해결해 나가면 충분히 해낼 수 있습니다.&quot;와 같은 개인화된 조언을 받게 된다.&lt;/p&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;blockquote data-ke-style=&quot;style2&quot;&gt;데이터로 확인하는 성장, 그것만큼 확실한 동기부여는 없다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발 과정에서 느끼는 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ToDoker를 개발하면서 가장 재미있는 부분은 내가 만든 앱을 내가 직접 사용하면서 개선점을 찾아가는 과정이다. 개발자이면서 동시에 파워 유저가 되는 경험이랄까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 전에는 100% 완료율을 달성했을 때 폭죽 애니메이션을 추가했는데, 실제로 내가 그 애니메이션을 보는 순간 정말 뿌듯했다. 이런 작은 성취감들이 쌓여서 더 나은 하루를 만들어간다는 확신이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 AI 기능을 개발하면서 내 자신의 생산성 패턴을 객관적으로 보게 됐다. &quot;아, 내가 월요일에는 계획을 많이 세우지만 실제 실행률은 낮구나&quot;, &quot;금요일이 가장 집중력이 떨어지는 시간이구나&quot; 같은 인사이트들을 얻으면서, 이 앱이 정말 도움이 될 수 있겠다는 확신을 갖게 됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;앞으로의 여정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ToDoker는 단순한 할 일 관리 앱이 아니라, &lt;b&gt;개인 생산성의 AI 파트너&lt;/b&gt;가 되는 것이 목표다. 혼자 계획을 세우고 실행하는 모든 사람들이 &quot;내가 잘하고 있나?&quot;라는 불안감 대신 &quot;오늘도 한 걸음 성장했네&quot;라는 확신을 가질 수 있도록 돕고 싶다.&lt;/p&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>회고</category>
      <category>TO-DO</category>
      <category>개발</category>
      <category>개발회고</category>
      <category>생산성</category>
      <category>할일관리</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/19</guid>
      <comments>https://parse.tistory.com/19#entry19comment</comments>
      <pubDate>Fri, 15 Aug 2025 13:42:31 +0900</pubDate>
    </item>
    <item>
      <title>처음부터 완벽할 필요는 없다: 개발자에게 필요한 7가지 마인드셋</title>
      <link>https://parse.tistory.com/18</link>
      <description>&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;1734&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lY735/btsO6q3cXpi/woS075HDuUPeZUKEECwKkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lY735/btsO6q3cXpi/woS075HDuUPeZUKEECwKkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lY735/btsO6q3cXpi/woS075HDuUPeZUKEECwKkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlY735%2FbtsO6q3cXpi%2FwoS075HDuUPeZUKEECwKkk%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;1734&quot; height=&quot;1382&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;시작하며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 한지 6년차가 되었지만 아직도 완벽함을 추구하여 일을 시작하기를 주저할 때가 있습니다. 좀 더 튼튼한 제품을 만들 수 있었다는 장점은 있었지만, 많은 기회랑 시간이 든다는 점은 모든게 빠르게 바뀌는 현재에서 경쟁력을 갖기는 어렵다고 느꼈습니다. 유튜브 알고리즘을 통해 개발자 마인드셋 관련 영상을 봤는데 자극이되어서 공유하고자 합니다.&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;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;a title=&quot;유튜브 영상 | It took me 10+ years to realize what I&amp;rsquo;ll tell you in 8 minutes&quot; href=&quot;https://youtu.be/RGaW82k4dK4?si=WWt45nyuFiIhKmka&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;유튜브 영상 | It took me 10+ years to realize what I&amp;rsquo;ll tell you in 8 minutes&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;다른 개발자들은 다 알고 있는데, 왜 나만 이렇게 모르지?&amp;rdquo;&lt;/blockquote&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&gt;&lt;b&gt;정답은, 아무도 다 알지 못한다는 것&lt;/b&gt;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기, 제가 코딩을 배우고 개발자로 일하면서 얻은 7가지 통찰을 공유합니다.&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;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;  모든 것을 알 필요는 없다&lt;/b&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;처음에는 자바스크립트 문법 하나하나를 외우려 했습니다.&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;blockquote data-ke-style=&quot;style2&quot;&gt;진짜 중요한 건 &amp;ldquo;찾는 법&amp;rdquo;과 &amp;ldquo;문제를 해결하는 능력&amp;rdquo;이다.&lt;/blockquote&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;span&gt;오히려 &lt;/span&gt;&lt;b&gt;검색하고, 시도하고, 깨지는 과정 자체가 개발 그 자체&lt;/b&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt; &amp;zwj;  배우는 방법을 먼저 배우세요&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 진짜 학습은 &amp;lsquo;소비&amp;rsquo;가 아닌 &lt;span&gt;&lt;b&gt;&amp;lsquo;창작&amp;rsquo; 모드일 때 일어납니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;튜토리얼 1시간 = 실습 4시간&amp;rdquo;&lt;br /&gt;&amp;ldquo;손가락이 키보드에 닿지 않으면, 뇌는 학습하지 않는다&amp;rdquo;&lt;/blockquote&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&gt;듣기만 해서는 언어를 못 배우듯, &lt;/span&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  완벽주의는 함정이다&lt;/b&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;한 줄의 변수명을 짓는 데 30분을 썼던 적이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 돌아보면 그 시간은 &lt;span&gt;&lt;b&gt;실수를 두려워한 시간&lt;/b&gt;&lt;/span&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;완벽한 코드보다 작동하는 코드가 더 낫다&lt;/blockquote&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  준비가 안 됐어도, 그냥 시작하세요&lt;/b&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;ldquo;지금은 준비가 안 됐어&amp;hellip;&amp;rdquo;&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;blockquote data-ke-style=&quot;style2&quot;&gt;자신감은 &amp;ldquo;준비&amp;rdquo;해서 생기는 게 아니라, &amp;ldquo;행동&amp;rdquo;하면서 생깁니다.&lt;/blockquote&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;b&gt;두려워도 괜찮아요. 일단 해보는 거예요.&lt;/b&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  진짜 실력은 문제 해결 능력이다&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;디버깅은 실패가 아니라, 개발 그 자체입니다.&lt;/blockquote&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;구문은 도구일 뿐입니다. 실력은 그 너머에 있습니다.&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;✨ 아무도 당신의 코드에 관심 없다&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;아무도 당신의 코드에 관심 없습니다.&lt;br /&gt;코드가 무엇을 &amp;ldquo;하는지&amp;rdquo;에만 관심이 있죠.&lt;/blockquote&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;span&gt;&lt;b&gt;작동하는 코드&lt;/b&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;&lt;span&gt;&lt;b&gt;허영심보다는 가치&lt;/b&gt;&lt;/span&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;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  번아웃은 현실이다. 당신의 에너지를 보호하세요&lt;/b&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;진짜 개발자는 24시간 코딩하는 사람이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그건 &lt;/span&gt;&lt;b&gt;비현실적이고, 위험한 신화&lt;/b&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;당신의 뇌는 배터리입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;잠을 자고, 쉬고, 자연을 느끼며 재충전하세요.&lt;/blockquote&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개발은 &amp;ldquo;암기 과목&amp;rdquo;이 아니라 &amp;ldquo;생존 기술&amp;rdquo;입니다.&lt;br /&gt;계속 배우고, 시도하고, 성장하면 됩니다.&lt;/blockquote&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;완벽할 필요는 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작동하면 일단 성공입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;당신은 잘하고 있습니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>개발 지식</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/18</guid>
      <comments>https://parse.tistory.com/18#entry18comment</comments>
      <pubDate>Sun, 6 Jul 2025 13:55:41 +0900</pubDate>
    </item>
    <item>
      <title>Java에서 &amp;ldquo;is&amp;rdquo;로 시작하는 Boolean 필드 이름 문제</title>
      <link>https://parse.tistory.com/17</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qpINN/btsNeSHlUbn/zYcBsYcfibH20mz1ZSxMf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qpINN/btsNeSHlUbn/zYcBsYcfibH20mz1ZSxMf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qpINN/btsNeSHlUbn/zYcBsYcfibH20mz1ZSxMf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqpINN%2FbtsNeSHlUbn%2FzYcBsYcfibH20mz1ZSxMf1%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;1734&quot; height=&quot;1382&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;1382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 boolean 타입의 필드를 다룰 때 발생할 수 있는, 예상치 못한 문제와 그 해결 방법에 대해 이야기하고자 합니다. 필드 이름이 &amp;ldquo;is&amp;rdquo;로 시작할 경우 발생하는 getter와 setter 관련 문제를 중점적으로 다룰 예정입니다.&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;b&gt;문제 상황&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 boolean 타입의 변수에 대한 getter 메소드를 &lt;span&gt;is&lt;/span&gt;로 시작하는 이름으로 정의하는 것이 일반적입니다. 예를 들어, &lt;span&gt;active&lt;/span&gt;라는 boolean 필드에 대해서는 &lt;span&gt;isActive()&lt;/span&gt;라는 메소드를 사용하곤 합니다. 이는 JavaBeans 명세에서도 권장하는 방법입니다. 그런데, 문제는 필드 이름 자체가 &lt;span&gt;isActive&lt;/span&gt;, &lt;span&gt;isEnabled&lt;/span&gt; 같이 이미 &amp;lsquo;is&amp;rsquo;로 시작하는 경우가 종종 있다는 것입니다. 이 경우, Java의 일반적인 규칙에 따라 &lt;span&gt;isIsActive()&lt;/span&gt;, &lt;span&gt;isIsEnabled()&lt;/span&gt;와 같은 형태로 getter 메소드를 기대하게 되는데, 이는 분명 직관적이지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제는 특히 JSON이나 XML로 객체를 직렬화할 때, 또는 ORM 라이브러리를 사용할 때 자주 발생합니다. 객체의 필드를 자동으로 JSON 필드로 매핑하거나 데이터베이스 테이블에 매핑할 때, 이러한 규칙 때문에 예상치 못한 동작이 발생할 수 있습니다. 실제로 API로 isActive 값을 내려줬을 때, 예상했던 &quot;is&quot; 필드 이름이 아닌 active가 내려왔습니다.&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;b&gt;원인 분석&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 원인은 JavaBeans 명세에서 boolean 필드의 getter가 &amp;lsquo;is&amp;rsquo; 접두사를 사용하도록 규정하고 있기 때문입니다. 많은 Java 기반의 프레임워크와 라이브러리는 이 규칙을 따르며, 리플렉션을 통해 메소드 이름을 분석하고 객체를 자동으로 만들어 내곤 합니다. 따라서, 필드 이름이 &amp;lsquo;is&amp;rsquo;로 시작하는 경우, 이러한 프레임워크나 라이브러리가 올바른 getter나 setter 메소드를 찾지 못하고, 데이터 바인딩에 실패할 수 있습니다.&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;b&gt;해결 방법&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 필드명 변경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단하고 확실한 해결 방법은 &amp;lsquo;is&amp;rsquo;로 시작하는 필드 이름을 사용하지 않는 것입니다. 예를 들어, &lt;span&gt;isBoolean&lt;/span&gt; 대신 &lt;span&gt;booleanValue&lt;/span&gt;나 &lt;span&gt;booleanFlag&lt;/span&gt;와 같이 명확하고 혼동이 없는 이름을 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 명시적 Getter/Setter 정의 (비추천)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 필드 이름을 변경할 수 없는 상황이라면, getter와 setter 메소드를 명시적으로 정의해야 합니다. 이를 통해 프레임워크나 라이브러리가 올바르게 메소드를 인식할 수 있도록 해야 합니다. &lt;b&gt;일관성과 가독성 측면에서 사용을 지양해야합니다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BooleanExample {
    private boolean isBoolean;

    public boolean getIsBoolean() {
        return isBoolean;
    }

    public void setIsBoolean(boolean isBoolean) {
        this.isBoolean = isBoolean;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 직렬화 어노테이션 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jackson 같은 라이브러리를 사용할 때는 &lt;span&gt;@JsonProperty&lt;/span&gt; 어노테이션을 활용하여 직렬화될 때 사용될 필드명을 직접 지정할 수 있습니다. 이 방법은 필드명과 메소드명 사이의 불일치를 해결하는 데 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class BooleanExample {
    @JsonProperty(&quot;isBoolean&quot;)
    private boolean isBoolean;

    public boolean isBoolean() {
        return isBoolean;
    }

    public void setBoolean(boolean isBoolean) {
        this.isBoolean = isBoolean;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해서, boolean 필드명이 &amp;lsquo;is&amp;rsquo;로 시작할 때 발생할 수 있는 문제와 그 해결 방법에 대해 알아보았습니다. 이 포스트가 비슷한 문제를 겪고 계신 분들에게 도움이 되기를 바랍니다.&amp;nbsp;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;046&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/046.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/046.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>개발 지식/JAVA</category>
      <category>boolean</category>
      <category>getter</category>
      <category>is</category>
      <category>java</category>
      <category>setter</category>
      <category>역직렬화</category>
      <category>직렬화</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/17</guid>
      <comments>https://parse.tistory.com/17#entry17comment</comments>
      <pubDate>Wed, 9 Apr 2025 21:37:38 +0900</pubDate>
    </item>
    <item>
      <title>Read-tracker 크롬 확장프로그램 개발기 - (1)</title>
      <link>https://parse.tistory.com/16</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf9qDj/btsMPu8cHX0/BwMxpYvshGpZoG8mBWna00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf9qDj/btsMPu8cHX0/BwMxpYvshGpZoG8mBWna00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf9qDj/btsMPu8cHX0/BwMxpYvshGpZoG8mBWna00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf9qDj%2FbtsMPu8cHX0%2FBwMxpYvshGpZoG8mBWna00%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;1280&quot; height=&quot;800&quot; data-origin-width=&quot;1280&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Read-tracker 크롬 확장프로그램 개발기 - (1) 개발 배경과 아이디어&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;크롬 확장 프로그램을 만들게 된 계기&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;웹 서핑을 하다 보면 긴 글을 읽게 되는 경우가 많습니다. 특히 E-book을 읽을 때처럼 '내가 지금까지 얼마나 읽었고, 앞으로 얼마나 남았는지'를 확인하는 습관이 있다 보니, 웹에서도 같은 기능이 있으면 좋겠다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;일부 웹사이트에서는 읽기 진행률을 시각적으로 보여주기도 하지만, 이는 특정 사이트에 한정된 기능이었습니다. 포탈마다 위치가 다르고 일관성이 없기 때문에, 다양한 웹사이트에서 공통적으로 사용할 수 있는 확장 프로그램을 만들고자 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한, 모바일에서는 화면 상단을 터치하면 자동으로 최상단으로 이동하는 기능이 기본 제공되지만, PC에서는 이런 기능이 없어 불편함을 느꼈습니다. 따라서 상단 이동 버튼 기능도 함께 추가하면 보다 편리한 사용자 경험을 제공할 수 있을 것이라 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아이디어 기획 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Read-tracker를 개발하면서 가장 중요하게 고려한 부분은 사용자 경험(UX)과 실용성이었습니다. 따라서 다음과 같은 주요 기능을 구상했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  주요 기능&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;읽기 진행률 표시&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 현재 읽고 있는 위치를 %로 표시하여 가시성을 높임&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;읽은 시간 및 남은 시간 추정&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 사용자의 평균 읽기 속도를 기반으로 남은 시간을 예측하여 제공&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;상단 이동 버튼&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 클릭 한 번으로 페이지 최상단으로 이동하는 기능 추가&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  UX 고려 요소&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;크롬 확장 프로그램은 기존 웹페이지 위에 새로운 요소를 추가하기 때문에, 사용자에게 방해가 되지 않도록 UI를 설계해야 했습니다. 특히, 다음과 같은 UX 요소를 고려했습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;위치 조정 가능&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 일부 웹사이트에서는 추가된 버튼이 기존 UI를 가릴 수 있기 때문에, 사용자가 드래그 앤 드롭으로 버튼 위치를 조정할 수 있도록 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;사이트별 활성화 설정&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 특정 포탈에서는 기능이 필요하지 않을 수도 있기 때문에, 별도의 설정 페이지에서 사이트별로 기능을 On/Off할 수 있도록 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;성취감&lt;/b&gt;: 다 읽었으면 축하해주는 이모지를 통해서 축하해해주기.  &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 스택 및 개발 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;크롬 확장 프로그램을 개발하기 위해 기본적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;HTML, CSS, JavaScript&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 사용했습니다. 개발 도구로는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;b&gt;CursorAI&lt;/b&gt;&lt;/span&gt;&lt;span&gt;를 활용하여 효율적으로 개발을 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;  사용 기술 / 도구&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;사용기술&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;HTML / CSS / JavaScript&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: UI 및 기능 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;Manifest V3&lt;/b&gt;&lt;/span&gt;&lt;span&gt;: 보안 강화 및 최신 정책 적용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;도구&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Cursor&lt;/b&gt; : &lt;span style=&quot;background-color: #ffffff; color: #1f1f1f; text-align: start;&quot;&gt;AI 기반 통합 개발 환경(IDE)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Manifest V3를 적용한 이유는 다음과 같습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;기존 Manifest V2의 보안 문제 해결&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;크롬 웹스토어 정책에 맞추어 향후 업데이트 유지보수 용이&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;백그라운드 스크립트 대신 서비스 워커를 사용하여 성능 최적화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이렇게 Read Tracker의 초기 아이디어와 개발 배경을 정리했습니다. 다음 2부에서는 실제 개발 과정에 대해 다룰 예정입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기대해주세요!  &lt;/span&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>Cursor</category>
      <category>개발기</category>
      <category>크롬</category>
      <category>프로젝트</category>
      <category>확장프로그램</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/16</guid>
      <comments>https://parse.tistory.com/16#entry16comment</comments>
      <pubDate>Wed, 19 Mar 2025 20:17:44 +0900</pubDate>
    </item>
    <item>
      <title>유용하게 사용 중인 크롬 확장 프로그램 3가지</title>
      <link>https://parse.tistory.com/15</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o72gG/btsMMaVYXUU/p9xrJhhnEudisgzBDTQrvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o72gG/btsMMaVYXUU/p9xrJhhnEudisgzBDTQrvK/img.png&quot; data-alt=&quot;Read-tracker&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o72gG/btsMMaVYXUU/p9xrJhhnEudisgzBDTQrvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo72gG%2FbtsMMaVYXUU%2Fp9xrJhhnEudisgzBDTQrvK%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;440&quot; height=&quot;280&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Read-tracker&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span&gt;웹 브라우징을 더욱 효율적으로 만들어주는 다양한 크롬 확장 프로그램이 있지만, 개인적으로 &lt;b&gt;가장 유용하게 사용하고 있는 3가지를 정리&lt;/b&gt;해보았습니다. 개발자로서 업무 효율을 높이고, 글쓰기 보조 역할을 하며, 직접 개발한 확장 프로그램까지 포함하여 소개해드리겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1. Vimium - 키보드만으로 웹 브라우징을 빠르게&lt;br /&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3436&quot; data-origin-height=&quot;2466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbHhZ7/btsMMIEuFdh/A3K32YFTT8aqhc9R24ArsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbHhZ7/btsMMIEuFdh/A3K32YFTT8aqhc9R24ArsK/img.png&quot; data-alt=&quot;F키를 눌러 클릭 가능한 요소를 단축키로 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbHhZ7/btsMMIEuFdh/A3K32YFTT8aqhc9R24ArsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbHhZ7%2FbtsMMIEuFdh%2FA3K32YFTT8aqhc9R24ArsK%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;3436&quot; height=&quot;2466&quot; data-origin-width=&quot;3436&quot; data-origin-height=&quot;2466&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;F키를 눌러 클릭 가능한 요소를 단축키로 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/vimium/dbepggeogbaibhgnhhndojpepiihcmeb?utm_source=item-share-cp&quot;&gt;&lt;span&gt;Vimium 다운로드&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발자로서 마우스를 많이 사용하면 손목에 무리가 가는 경우가 많습니다. Vimium은 이러한 불편함을 해결해 주는 좋은 크롬 확장 프로그램 중 하나입니다. 이 확장 프로그램을 사용하면 웹 브라우징을 키보드만으로 컨트롤할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;✨ 주요 기능&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;F &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;키를 누르면 모든 클릭 가능한 요소에 단축키가 표시되어 빠르게 조작 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;J / K &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;키로 tab 이동&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;H / L &lt;/span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;키로 뒤로 가기/앞으로 가기&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&amp;nbsp;Shift + X &lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt;키를 눌러 닫았던 탭을 다시 열기&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마우스를 덜 사용하면서도 더욱 빠르고 효율적으로 웹 페이지를 탐색할 수 있어 개발자뿐만 아니라 파워 유저들에게도 추천드릴 만한 확장 프로그램입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2. Grammarly - 영문 글쓰기 보조 AI&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NRspP/btsMMbN5Nd7/XNH9v1uCYmAcgGGj0BKeh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NRspP/btsMMbN5Nd7/XNH9v1uCYmAcgGGj0BKeh1/img.png&quot; data-alt=&quot;텍스트 박스에서 수정이 필요한 영문법 예시를 들어줌&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NRspP/btsMMbN5Nd7/XNH9v1uCYmAcgGGj0BKeh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNRspP%2FbtsMMbN5Nd7%2FXNH9v1uCYmAcgGGj0BKeh1%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;500&quot; height=&quot;500&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;텍스트 박스에서 수정이 필요한 영문법 예시를 들어줌&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/grammarly-ai-writing-and/kbfnbcaeplbcioakkpcpgfkobkghlhen?utm_source=item-share-cp&quot;&gt;&lt;span&gt;Grammarly 다운로드&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;영어로 문서를 작성해야 하는 순간에 필수적으로 사용하는 크롬 확장 프로그램입니다. 문법과 철자를 자동으로 검사해 주며, 더 자연스럽고 세련된 표현을 추천해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;✨ 주요 기능&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;실시간 문법 및 철자 오류 수정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;어휘 개선 및 문장 가독성 향상 제안&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;다양한 문서 스타일(비즈니스, 학술적 글쓰기 등)에 맞춘 조정 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이메일, 문서, 블로그 글 작성 시 자동 적용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;영어로 이메일을 작성하거나 블로그 글을 작성할 때 상당히 유용하게 활용할 수 있는 도구로, 영문 작문 능력을 향상시키는 데도 큰 도움이 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3. Read-tracker - 웹페이지 읽기 진행률 및 시간 추적&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bS1guW/btsMK1yYVs8/8FGm160KEbW6hdKxQchYKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bS1guW/btsMK1yYVs8/8FGm160KEbW6hdKxQchYKK/img.png&quot; data-alt=&quot;우측 하단에 읽기 진행률을 표시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bS1guW/btsMK1yYVs8/8FGm160KEbW6hdKxQchYKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbS1guW%2FbtsMK1yYVs8%2F8FGm160KEbW6hdKxQchYKK%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;600&quot; height=&quot;375&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우측 하단에 읽기 진행률을 표시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://chromewebstore.google.com/detail/read-tracker/jgolehhngmpokfjpegicibkoejikcbch?utm_source=item-share-cp&quot;&gt;&lt;span&gt;Read Tracker 다운로드&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;제가 직접 개발한 크롬 확장 프로그램으로, 웹페이지에서 읽기 진행률을 시각적으로 확인하고 소요 시간을 측정할 수 있도록 제작하였습니다. e-book을 읽을 때 어느 정도 읽었는지 확인하는 기능을 웹페이지에서도 활용할 수 있도록 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;✨ 주요 기능&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; color: #000000; text-align: start;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;현재 읽고 있는 페이지의 진행률(%) 표시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;읽은 시간 추적 및 남은 시간 예측 기능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;한 번의 클릭으로 페이지 최상단으로 이동하는 'One-Tap Scroll to Top' 기능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모바일에서는 화면 상단을 터치하면 자동으로 페이지 최상단으로 이동할 수 있지만, PC에서는 별도의 버튼이 필요한 경우가 많습니다. 이 확장 프로그램을 통해 고정된 위치를 클릭하여 상단으로 이동하게 하여 웹사이트별로 일관된 조작 방식을 제공하며, 읽기 경험을 더욱 편리하게 만들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;color: #000000; text-align: start;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;마무리&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이처럼 크롬 확장 프로그램을 활용하면 업무 생산성을 높이고, 보다 효율적인 웹 브라우징이 가능합니다. &lt;b&gt;Vimium&lt;/b&gt;을 활용하면 키보드만으로 빠르게 탐색할 수 있고, &lt;b&gt;Grammarly&lt;/b&gt;를 통해 문서를 매끄럽게 다듬을 수 있으며, &lt;b&gt;Read-tracker&lt;/b&gt;로는 읽기 경험을 최적화할 수 있습니다. 앞으로도 더 좋은 확장 프로그램을 찾아보고, 필요하면 직접 개발해볼 계획입니다.  &lt;/span&gt;&lt;/p&gt;</description>
      <category>생산성</category>
      <category>grammarly</category>
      <category>read-tracker</category>
      <category>readtracker</category>
      <category>vimium</category>
      <category>생산성</category>
      <category>읽기</category>
      <category>진행률</category>
      <category>크롬</category>
      <category>확장프로그램</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/15</guid>
      <comments>https://parse.tistory.com/15#entry15comment</comments>
      <pubDate>Sun, 16 Mar 2025 21:11:10 +0900</pubDate>
    </item>
    <item>
      <title>AI 시대 생존 전략 | 기적의 독서법</title>
      <link>https://parse.tistory.com/14</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OBr1a/btsMnZnuvfT/lG8Kk8WDlf66FMgkNDj4k0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OBr1a/btsMnZnuvfT/lG8Kk8WDlf66FMgkNDj4k0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OBr1a/btsMnZnuvfT/lG8Kk8WDlf66FMgkNDj4k0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOBr1a%2FbtsMnZnuvfT%2FlG8Kk8WDlf66FMgkNDj4k0%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;3000&quot; height=&quot;2357&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size18&quot;&gt;드로우앤드류 '&lt;a title=&quot;AI 시대에 살아남을 기적의 독서법&quot; href=&quot;https://youtu.be/KuU33RNZWKc?si=PfHydyG4cKNR6Pmw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AI 시대에 살아남을 기적의 독서법&lt;/a&gt;' 영상을 보고 인상 깊어 개인적을 생각을 보태서 글을 작성하려합니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;현대 사회는 AI의 등장으로 급격히 변화하고 있습니다. 단순히 정보를 소비하는 것만으로는 경쟁력을 유지하기 어렵고, 이제는 긴 서사를 만들고 의미 있는 콘텐츠를 생산할 수 있는 능력이 필요합니다. 이러한 능력을 기르기 위해 가장 효과적인 방법 중 하나가 바로 독서입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;'도파민'이라는 키워드가 여기저기 자주 들릴 정도로 도파민이 넘치는 세상에서 독서는 유독 어렵게 다가옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;독서의 어려움과 극복 방법&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;많은 사람들이 독서를 어려워하는 이유는 긴 러닝 타임과 올바른 독서 방법을 모르기 때문입니다. 이를 극복하기 위해서는 작게 시작하는 것이 중요합니다. 하루에 단 한 페이지를 읽고, 그중 &lt;b&gt;마음에 드는 부분을 두 문장 정도 써보는 것&lt;/b&gt;으로 충분합니다. 이 습관이 점차 쌓이면 독서에 대한 거부감이 사라지고 자연스레 읽는 양이 늘어납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;특히 초보 독자는 처음부터 많은 양을 읽으려고 하기보다는 하루에 10페이지 정도만 읽고, 마음에 드는 내용 한두 가지를 메모하는 &amp;lsquo;전략적 독서법&amp;rsquo;을 활용하는 것이 효과적입니다. 이러한 방법은 단순히 정보를 입력하는 것이 아니라 장기 기억에 저장되도록 도와줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;콘텐츠 생산자의 시선으로 독서하기&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AI 시대에는 단순히 지식을 소비하는 것에서 벗어나 지식을 생산하는 사람이 되어야 합니다. 책을 읽을 때도 단순히 내용을 이해하는 데 그치지 않고, &lt;b&gt;콘텐츠 생산자의 시선&lt;/b&gt;으로 바라보아야 합니다. 예를 들어, 독서 중에 떠오른 생각이나 영감을 블로그에 글로 남기거나, 유튜브 영상을 제작해 공유하면 독서의 가치가 배가됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또한, 스터디 그룹을 활용하면 독서 습관을 꾸준히 유지하면서 다른 사람들과의 의견 교류를 통해 더 깊은 통찰력을 얻을 수 있습니다. 이러한 과정을 통해 단순히 지식을 습득하는 것을 넘어 자신의 생각을 구체화하고 이를 글쓰기나 말하기로 표현하는 능력이 길러집니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;AI와 인간의 차별화된 능력&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AI는 정보를 빠르게 검색하고 분석하는 데 뛰어난 도구지만, 인간만이 지닌 감성적 소통 능력과 창의성은 AI가 대신할 수 없습니다. 예를 들어, 법률 분야에서는 AI가 복잡한 법률 정보를 신속히 제공하지만, 최종적으로 의뢰인의 &lt;b&gt;마음을 이해하고 공감하며 소통하는 것은 인간의 역할&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;독서의 진정한 가치&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;독서는 단순한 정보 습득을 넘어 다양한 경험을 간접적으로 체험하고, 미래에 대한 불안감을 해소하며, 자기 계발을 위한 아이디어를 얻을 수 있는 소중한 도구입니다. 또한, 독서를 통해 얻은 생각을 글쓰기나 말하기로 표현하면서 더 나은 자신으로 성장할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;끊임없는 자기 변화의 중요성&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;AI 시대에는 &amp;lsquo;완성된 나&amp;rsquo;가 아니라 &amp;lsquo;계속 발전해 나가는 나&amp;rsquo;가 되어야 합니다. 이는 독서를 통해 끊임없이 새로운 지식을 접하고, 이를 통해 자신의 생각을 발전시키며, 나아가 이를 콘텐츠로 만들어 공유함으로써 실현할 수 있습니다. 결국, 독서는 AI 시대에도 변함없이 개인의 경쟁력을 높이는 핵심 도구로 남을 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;작은 습관으로 시작한 독서가 미래의 나를 더욱 빛나게 할 것입니다. 지금 바로 책을 한 페이지 펼쳐보세요!  &lt;/span&gt;&lt;/p&gt;</description>
      <category>회고</category>
      <category>Ai</category>
      <category>독서</category>
      <author>Parse</author>
      <guid isPermaLink="true">https://parse.tistory.com/14</guid>
      <comments>https://parse.tistory.com/14#entry14comment</comments>
      <pubDate>Wed, 19 Feb 2025 21:54:25 +0900</pubDate>
    </item>
  </channel>
</rss>