<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>kjw81024 님의 블로그</title>
    <link>https://kjw81024.tistory.com/</link>
    <description>kjw81024 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 22 Aug 2026 07:01:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kjw81024</managingEditor>
    <item>
      <title>WebSocket- NGINX 연결 오류</title>
      <link>https://kjw81024.tistory.com/112</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;영상 송출 및 person detection 을 Cloudflared 에서 WebSocket 으로 바꾸며 생겼던 오류 정리&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 1 : ConnectionAbortedError [WinError 10053]&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConnectionAbortedError (연결 중단 오류) 는 네트워크 통신 중 상대방이나 로컬 소프트웨어 (firewall, vaccine) 이 활성 연결을 강제로 끊을 때 발생 -&amp;gt; 이미 맺어진 연결을 일부러 중단 시킴 -&amp;gt; 윈도우 소켓 스택이 abort 처리함&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgLNFt/dJMcaasQYaz/Aq3rAkyvHu0pMeEpNwFu71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgLNFt/dJMcaasQYaz/Aq3rAkyvHu0pMeEpNwFu71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgLNFt/dJMcaasQYaz/Aq3rAkyvHu0pMeEpNwFu71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgLNFt%2FdJMcaasQYaz%2FAq3rAkyvHu0pMeEpNwFu71%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;1153&quot; height=&quot;211&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4seGf/dJMcahrZ4pi/vl1q9GBp9tBkJhKQzh3iL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4seGf/dJMcahrZ4pi/vl1q9GBp9tBkJhKQzh3iL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4seGf/dJMcahrZ4pi/vl1q9GBp9tBkJhKQzh3iL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4seGf%2FdJMcahrZ4pi%2Fvl1q9GBp9tBkJhKQzh3iL0%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;1033&quot; height=&quot;502&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 NGINX 가 직접 처리할건지 backend 로 보내줄건지 결정&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 이 요청을 받아서 뒤로 넘겨주는게 PROXY&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 nginx.conf 에 192.158.x.x:4327/ws/xx 가 들어왔을 때 어떻게 하면 되는지 적혀있지 않아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NGINX 가 백엔드로 넘기지 않고 본인이 처리하려고 함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 그래서 기본 동작 (정적 파일 서빙) 으로 처리하려다가 Websocket 을 어떻게 보낼지 몰라서 바로 연결을 끊음&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; abort 오류 발생&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) NGINX 에 이미 있는 conf 설정에 192.158.x.x:4327/ws/camera/ingest 를 backend proxy 하는 location 규칙을 추가하면 해결 됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(해당 경로를 백엔드로 넘겨주라는 말을 추가해주는거니까!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 넣으면 똑같은 요청도 오류가 나지 않음&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786585705228&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 4327;
    server_name 192.168.50.219;

    location /ws/ {
        proxy_pass http://localhost:8000;   # 백엔드(내부 8000)로 넘김
        proxy_http_version 1.1;
        # 추가 proxy 설정
    }
}&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;(2) 두번째 해결방법은 도메인 temp.url.co.kr 로 들어오는 요청은 이미 세팅되어서 돌아가고 있는 상황이기 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙을 새로 파지 않고 ,&amp;nbsp;IP 를 192.168.x.x:4327 로 직접 연결하지 않고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;proxy 되는 endpoint 로 전환 하기!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 서버 url = temp.url.co.kr/ws/camera/ingest 로 연결하니까 연결 이후에 바로 연결 끊김/ 실패가 뜨는게 사라짐&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786585806088&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 이 명령어에서 ws url 부분을 
python3 camera_daemon.py --source --backend --ws-url--ws-token

# 기존
--ws-url ws://192.168.x.x:4327/ws/camera/ingest

# 수정 
--ws-url wss://temp.url.co.kr/ws/camera/ingest&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;이번엔 (1) 대신 (2) 를 선택햇는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면... IP:4327 로 들어오는 요청은 오류 메시지에서 보이다시피 ws:// 로 시작함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 도메인은 HTTPS 니까 Websocket 도 wss://&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화 된 WebSocket 으로 붙는게 훨씬 좋기 때문에 IP 를 하드코딩하여 nginx 에 추가하지 않고 실행 방법을 수정했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 돌아감!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 2: 잘 고치고 나니까 새로운 오류가 떴다 ... WebSocketBadStatusException&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;[ws][WARN]&amp;nbsp;연결&amp;nbsp;끊김/실패:&amp;nbsp;WebSocketBadStatusException: &lt;br /&gt;Handshake&amp;nbsp;status&amp;nbsp;400&amp;nbsp;None&amp;nbsp;...&amp;nbsp;b'Can&amp;nbsp;&quot;Upgrade&quot;&amp;nbsp;only&amp;nbsp;to&amp;nbsp;&quot;WebSocket&quot;.'&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0PKRa/dJMcaasQYaF/Oyh8WVgbynEGnHkU0luZ3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0PKRa/dJMcaasQYaF/Oyh8WVgbynEGnHkU0luZ3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0PKRa/dJMcaasQYaF/Oyh8WVgbynEGnHkU0luZ3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0PKRa%2FdJMcaasQYaF%2FOyh8WVgbynEGnHkU0luZ3K%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;1377&quot; height=&quot;172&quot; data-origin-width=&quot;1377&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 해겨책이 에러메시지에 포함되어서 나왔다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Can &quot;Upgrade&quot; only to &quot;WebSocket&quot; 웹소켓은 Upgrade 로 바꿔달라는 의미&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client 는 upgrade websocket 헤더를 붙여서 handshake 요청을 보내는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 NGINX 가 Upgrade 헤더를 백엔드로 안 넘겨주면 그냥 HTTP 요청으로 받기 때문에 오류가 난다&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;428&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccj5Fx/dJMcaf8DgaN/znKmuDaKmhvbiapE14Vua1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccj5Fx/dJMcaf8DgaN/znKmuDaKmhvbiapE14Vua1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccj5Fx/dJMcaf8DgaN/znKmuDaKmhvbiapE14Vua1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccj5Fx%2FdJMcaf8DgaN%2FznKmuDaKmhvbiapE14Vua1%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;428&quot; height=&quot;349&quot; data-origin-width=&quot;428&quot; data-origin-height=&quot;349&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(NGINX 가 backend 로 어떻게 넘겨주는지는... 얘가 proxy 서버라 거기까진 관리자 모드로 확인이 불가능하긴 하다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이런 오류가 발생했을 때 NGINX /ws/ location 에 Upgrade 전달 설정을 추가해주기&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786586033308&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location /ws/ {
    proxy_pass http://localhost:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &quot;upgrade&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Upgrade header 는 client 가 만들지만, NGINX 가 그걸 backend 로 전달해줘야 handshake 가 완성&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 3&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가 전혀 안 뜨는 상황에서 카메라 화면이 미리보기 화면으로만 드고 CCTV 처럼 송출이 안 된다 ....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tomcat WebSocket 메시지 버퍼 기본값이 8KB 인데 JPEG 가 n00KB 라 크기 제한을 넘어버려서 프레임이 거부되는 현상이었다 ... 연결은 유지되지만 프레임이 안 넘어가는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결: 백엔드 WebSocket Config 에서 ServletServerContainerFactoryBean 의 버퍼 크기를 1MB 로 올리기&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;text 랑 binary 둘 다&lt;/p&gt;
&lt;pre id=&quot;code_1786586547002&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static final int WS_MAX_MESSAGE_BYTES = 1024 * 1024; // 1MB
container.setMaxTextMessageBufferSize(WS_MAX_MESSAGE_BYTES);
container.setMaxBinaryMessageBufferSize(WS_MAX_MESSAGE_BYTES);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 해결하고 나니까.....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 뜸!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coRveX/dJMcadCUSHJ/E84iv9SuW57FHrC7c5myP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coRveX/dJMcadCUSHJ/E84iv9SuW57FHrC7c5myP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coRveX/dJMcadCUSHJ/E84iv9SuW57FHrC7c5myP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoRveX%2FdJMcadCUSHJ%2FE84iv9SuW57FHrC7c5myP0%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;498&quot; height=&quot;282&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그새 좀 늙은듯한 나............ ㅎ&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/112</guid>
      <comments>https://kjw81024.tistory.com/112#entry112comment</comments>
      <pubDate>Thu, 13 Aug 2026 11:03:18 +0900</pubDate>
    </item>
    <item>
      <title>WebSocket 개념 정리</title>
      <link>https://kjw81024.tistory.com/111</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;국비지원 부캠 수업을 들을 때 Websocket 을 다루는 역할을 맡지 않아서 이번에 인턴하면서 Websocket 개념이랑 ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몰라서 생긴 여러 트러블슈팅을 정리하고 넘어가기&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 프로젝트에서 사용한 이유는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;crowd count, 영상 CCTV 처럼 송출 이 필요한데 그 때마다 client (dashboard) 에 즉시 알려줘야 하기 땜누에 HTTP polling 보다 효율적인 방식인 Websocket 사용이었다&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교 대상: HTTP&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.3488%;&quot;&gt;HTTP&lt;/td&gt;
&lt;td style=&quot;width: 79.6512%;&quot;&gt;응답 이후 연결이 끊김 (매번 새로 요청, 검증)&lt;br /&gt;keyword: 비연결, 무상태&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.3488%;&quot;&gt;WebSocket&lt;/td&gt;
&lt;td style=&quot;width: 79.6512%;&quot;&gt;한 번 연결하면 계속 유지&amp;nbsp;&lt;br /&gt;server -&amp;gt; client 방향으로 먼저 데이터 보낼 수 있음 (양방향)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ws:// 랑 wss:// 는 Http 랑 동일하게 암호화 여부 (s 가 붙은게 security)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 사용하는 서버가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://tempurl.co.kr&quot;&gt;https://tempurl.co.kr&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 서빙되면 반드시 wss:// 를 사용하기&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아니면 브라우저가 Mixed Content 에러를 띄운다고 함 (Https page 에서 암호화 안 된 연결을 시도하는거니까!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연결 과정을 Handshake 라고 부르는 듯 함 귀엽다.!! 악수라니&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. client 가 일반 HTTP 요청을 보낼 때 header 에 Upgrade: websocket 이 포함됨&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;950&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wSie7/dJMcafnoiQg/HZ4qjk4Ggal47tv6NoEq90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wSie7/dJMcafnoiQg/HZ4qjk4Ggal47tv6NoEq90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wSie7/dJMcafnoiQg/HZ4qjk4Ggal47tv6NoEq90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwSie7%2FdJMcafnoiQg%2FHZ4qjk4Ggal47tv6NoEq90%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;950&quot; height=&quot;589&quot; data-origin-width=&quot;950&quot; data-origin-height=&quot;589&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;pre id=&quot;code_1786416617738&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ws = new WebSocket(&quot;wss://temp.url.co.kr/ws/crowd&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786416636446&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;GET /ws/crowd HTTP/1.1
Host: tmep.url.co.kr
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xxxxx
Sec-WebSocket-Version: 13&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 헤더가 붙은 HTTP 요청을 만들어서 보냄&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 NGINX 에서 해주는 아래 설정&lt;/p&gt;
&lt;pre id=&quot;code_1786416688370&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location /ws/ {
    proxy_pass http://localhost:8000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection &quot;upgrade&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Upgrade 요청을 붙여주기&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 서버가 101 Switching Protocols 응답하면 그때부터 HTTP 연결이 WebSocket 연결로 upgrade 됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이후로 계속 열린 채널로 메시지 주고 받기&amp;nbsp;&lt;br /&gt;(매번 요청을 새로 쓸 필요 없어짐)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억해둘만한 WebSocket 객체의 기본 이벤트&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786524607594&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ws.onopen = () =&amp;gt; console.log(&quot;연결됨&quot;);
ws.onmessage = (event) =&amp;gt; console.log(&quot;데이터 옴:&quot;, event.data);
ws.onerror = (error) =&amp;gt; console.log(&quot;에러:&quot;, error);
ws.onclose = () =&amp;gt; console.log(&quot;연결 끊김&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 onmessage 에서 event.data 가 문자열로 오기 때문에 JSON.parse(data) 해줘야함!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 onclose 되면 재시도를 해줘야하기 때문에 특정 시간 이후에 재시도 하는 로직을 추가해주는게 좋다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786524795062&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ws.onclose = () =&amp;gt; {
  setTimeout(() =&amp;gt; connectWebSocket(), 3000); // 3초 뒤 재시도
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++&amp;nbsp; 백엔드 서버 하나랑만 연결 가능함 - 한 번 연결된 백엔드 인스턴스랑 계속 붙어있어야한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket 프로토콜에는 ping/pong 이 있는데 java script 브라우저 에는 native ping 을 보내는 api 가 X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 근데 NGINX 의 proxy_read_timeout 은 backend 로부터 아무런 데이터가 안오는걸 기준으로 count 를 세기 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가짜메시지... 평범한 메시지를 주기적으로 보내는 방식 사용&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786525061748&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ws = new WebSocket(&quot;wss://temp.url.co.kr/ws/crowd&quot;);

setInterval(() =&amp;gt; {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: &quot;ping&quot; }));
  }
}, 30000); // 30초마다 살아있다고 알림&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client (front) 에서 이렇게 보내주면&lt;/p&gt;
&lt;pre id=&quot;code_1786525144157&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
    if (message.getPayload().contains(&quot;\&quot;type\&quot;:\&quot;ping\&quot;&quot;)) {
        session.sendMessage(new TextMessage(&quot;{\&quot;type\&quot;:\&quot;pong\&quot;}&quot;));
        return;
    }
    // 나머지 로직...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 쪽 WebSocket Handler 가 이렇게 받아서... 연결이 안 끊기게 해줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/111</guid>
      <comments>https://kjw81024.tistory.com/111#entry111comment</comments>
      <pubDate>Tue, 11 Aug 2026 15:29:29 +0900</pubDate>
    </item>
    <item>
      <title>NGINX 개념 정리</title>
      <link>https://kjw81024.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;NGINX: 웹서버이자 리버스 프록시 서버 ..... 라는데 뭐라는지 모르겠다.... ㅜㅜ&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 요청을 받아서 뒤의 실제 application server 로 전달&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;역할&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 외부 요청을 내부 백엔드로 전달&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. react 빌드 결과물 같은 정적 파일을 직접 응답 (백엔드까지 가지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. LB (로드밸런싱) : backend instance 가 여러개일때 분산시킴&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w8BKw/dJMcaalV2Fe/FVSc1fcL48Yx3OEuLipKt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w8BKw/dJMcaalV2Fe/FVSc1fcL48Yx3OEuLipKt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w8BKw/dJMcaalV2Fe/FVSc1fcL48Yx3OEuLipKt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw8BKw%2FdJMcaalV2Fe%2FFVSc1fcL48Yx3OEuLipKt0%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;895&quot; height=&quot;281&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;proxy-pass : 이 위치로 들어온 요청을 내가 지정한 다른 주소로 그대로 넘겨줘!! 라는 의미&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786413878166&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location /api/ {
    proxy_pass http://localhost:8000;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 적혀있으면 /api/ 로 시작하는 요청이 들어오면 localhost:8000 으로 전달해주라는 의미&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) client가 배포_URL/api/xxx 로 요청을 보내면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (2) nginx 가 받아서 localhost:8000/api/xxx 로 다시 요청을 보냄 (서버 내부 상황)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (3) 백엔드가 응답하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; (4) nginx 가 다시 client 한테 돌려줌&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 client 는 1, 3만 알고 f12 개발자 도구를 봐도 (2), (3) 은 알 수 없음&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react build 같은 정적 파일을 직접 응답&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 전에는 이런식의 .tsx, .jsx 같은 형태의 파일들로 작성됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 얘는 브라우저, nginx 둘 다 못 읽기 때문에 컴파일이 필요&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JODS1/dJMcai5kH9D/JgzKhN5i1rriTtYGwOIeTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JODS1/dJMcai5kH9D/JgzKhN5i1rriTtYGwOIeTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JODS1/dJMcai5kH9D/JgzKhN5i1rriTtYGwOIeTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJODS1%2FdJMcai5kH9D%2FJgzKhN5i1rriTtYGwOIeTK%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;463&quot; height=&quot;420&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1786414250250&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm run build&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 프론트엔드 실행 코드를 쓰면 Vite 가 모든 코드를 순수 JS 로 변환 -&amp;gt; 파일을 묶거나 압축해서&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786414385605&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dist/
  index.html
  assets/index-a1b2c3.js   # 순수 JavaScript
  assets/index-a1b2c3.css  # 순수 CSS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 파일로 뱉어냄 (이 시점에ㅓㅅ react 라는건 파일상으로 존재 X - 복잡한 부분들은 빌드할때 다 처리됨 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NGINX 는 dist/ 폴더를 보고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/요청이 왔으니까 index.html 파일 내용을 복사해서 응답 보내기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/assets/contents.js 요청이 오니까 그 파일 내용 복사해서 응답 보내기... 등등을 함&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786414501530&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;location / {
    root /path/to/front/dist;
    try_files $uri /index.html;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 그린다는건 브라우저 안에서 일어나는 일&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 NGINX 가 던져줘서 다운로드 한 JS 코드를 실행 -&amp;gt; 화면에 컴포넌트가 그려지고 ... 이벤트가 붙고... 라우팅 됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니까 렌더링이라는 실제 작업은 사용자 브라우저의 컴퓨터에서 일어남&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론! NGINX 가 정적파일을 직접 응답한다는 말은 파일을 나눠주는 분배자 느낌이라고 보면 됨&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런싱은 말 그대로... 요청이 들어오면 나눠져있는 여러 백엔드 인스턴스에게 값을 분배해주는거&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4vNhY/dJMcabkZHP5/fJS44D4cTRm6rolGltiXsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4vNhY/dJMcabkZHP5/fJS44D4cTRm6rolGltiXsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4vNhY/dJMcabkZHP5/fJS44D4cTRm6rolGltiXsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4vNhY%2FdJMcabkZHP5%2FfJS44D4cTRm6rolGltiXsK%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;848&quot; height=&quot;433&quot; data-origin-width=&quot;848&quot; data-origin-height=&quot;433&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;설정 구조&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1786414984264&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server {
    listen 80;
    server_name temp.serverurl.co.kr;

    location / {
        root /path/to/front/dist;   # 정적 파일
        try_files $uri /index.html; # SPA 라우팅 대응
    }

    location /api/ {
        proxy_pass http://localhost:8000; # 백엔드로 전달
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;location: URL 경로별로 다르게 처리하겠다는 규칙&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;proxy_pass: 이 경로는 다른 서버로 넘김&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-files: 파일 없으면 이거 시도하기 -&amp;gt; 404 방지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx.conf 는 관심사 분리 기준으로 두기~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx 가 프론트 서빙용이라면 front repo 에 둬도 되고 (nginx.conf 가 프론트 docker image 안에 포함되는 상황)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 서비스를 총괄하는 진입점이면 별도 infra repo 에 두는게 좋음&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 파일을 수정하고 nginx 를 그냥 재시작하면 문법 오류 있을 때 서비스가 통째로 다운될 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1786415382062&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo nginx -t              # 문법 검사만, 실제 반영 안 함
sudo nginx -s reload       # 무중단으로 설정만 다시 읽음 (재시작 X)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서로 확인해주는 습관이 국룰이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류가생겼는데 backend log 가 텅 비었을 때 : nginx 선에서 막혔을 가능성을 고려해서 nginx error log 를 확인해주자!&lt;/p&gt;
&lt;pre id=&quot;code_1786415410685&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/var/log/nginx/access.log   # 어떤 요청이 들어왔는지
/var/log/nginx/error.log    # nginx 레벨에서 뭔가 실패했을 때 (백엔드 연결 실패 등)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/110</guid>
      <comments>https://kjw81024.tistory.com/110#entry110comment</comments>
      <pubDate>Tue, 11 Aug 2026 11:32:02 +0900</pubDate>
    </item>
    <item>
      <title>pip3 install 트러블슈팅</title>
      <link>https://kjw81024.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;python : python interpreter 를 실행시키는 명령어&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python3 은 말 그대로 python3 버전 인터프리터...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 1이랑 2는 없는거지 했는데 옛날에는 python2, python3 이 같이 설치되어 있었기 때문에 버전 구분이 필요해서 3만 따로 생긴듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pip, pip3 은 package 설치 도구&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pip3 을 안 붙이면 PATH 에 잡힌 기본 pip 를 가리킴 (2일수도..있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786410749406&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# cv2 를 사용하는 python 파일 실행했는데 cv2가 없다고 함 

PS C:\Users\user\Documents\ai-cms\docs\tools&amp;gt; python3 test_person_count.py
Traceback (most recent call last):
  File &quot;C:\Users\user\Documents\ai-cms\docs\tools\test_person_count.py&quot;, line 7, in &amp;lt;module&amp;gt;
    import cv2
ModuleNotFoundError: No module named 'cv2'

# cv2 를 설치하려고 하니까 이미 존재 (Requirement already satisfied) 

PS C:\Users\user\Documents\ai-cms\docs\tools&amp;gt; pip3 install opencv-contrib-python
Requirement already satisfied: opencv-contrib-python in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (5.0.0.93)
Requirement already satisfied: numpy&amp;gt;=2 in c:\users\user\appdata\local\programs\python\python312\lib\site-packages (from opencv-contrib-python) (2.5.1)
[notice] A new release of pip is available: 25.0.1 -&amp;gt; 26.2.1
[notice] To update, run: python.exe -m pip install --upgrade pip&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 이런 문제가 발생했다 ....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 패키지가 없어서 오류가 뜸 -&amp;gt; pip3&amp;nbsp; 으로 install 받으려고 했는데 이미 있다고 함... &amp;lt; 예???&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python3 이랑 pip3 이 서로 다른 pyrhon 설치본을 가리키고 있어서 뜨는 오류&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786411018093&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;where.exe python3
where.exe pip3&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kRVJh/dJMcab6iGbL/WCTLqQo2M2BYzYKnQXMyr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kRVJh/dJMcab6iGbL/WCTLqQo2M2BYzYKnQXMyr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kRVJh/dJMcab6iGbL/WCTLqQo2M2BYzYKnQXMyr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkRVJh%2FdJMcab6iGbL%2FWCTLqQo2M2BYzYKnQXMyr0%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;594&quot; height=&quot;123&quot; data-origin-width=&quot;594&quot; data-origin-height=&quot;123&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python3, pip3 이 각각 어떤 실행파일, 인터프리터를 쓰는지 확인하는 명령어로 확인해보니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘이 다른 경로를 가리키고 있음 =&amp;gt; 다른 설치본&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 과정&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eEyLGD/dJMcagsZawx/D7vR8oVDJ3ZiWSuS8z6pb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eEyLGD/dJMcagsZawx/D7vR8oVDJ3ZiWSuS8z6pb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eEyLGD/dJMcagsZawx/D7vR8oVDJ3ZiWSuS8z6pb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeEyLGD%2FdJMcagsZawx%2FD7vR8oVDJ3ZiWSuS8z6pb1%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;632&quot; height=&quot;580&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;580&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 진짜 설치본보다 PATH 우선순위가 높아서 이상한곳으로 연결되는 경우가 흔하다고 해서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 - 앱 - 고급 앱 설ㄹ정 - 앱 실행 별칭 에서 꺼줬다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 powershell 에서 python list 를 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;51&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwOqwk/dJMcaiYxWr8/Ml4N3xrFuphPEKBlLmUQhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwOqwk/dJMcaiYxWr8/Ml4N3xrFuphPEKBlLmUQhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwOqwk/dJMcaiYxWr8/Ml4N3xrFuphPEKBlLmUQhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwOqwk%2FdJMcaiYxWr8%2FMl4N3xrFuphPEKBlLmUQhK%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;338&quot; height=&quot;51&quot; data-origin-width=&quot;338&quot; data-origin-height=&quot;51&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python 3.14.7 이 설치되어 있었다...! uninstall 해주기 -&amp;gt; 3.12 만 남김&lt;/p&gt;
&lt;pre id=&quot;code_1786411647662&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;py --list
pythonmanager uninstall 3.14&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;앞으로 실행할때 py -3.12 로 명시하는 습관 들여두면 이후에 다른 python 버전을 다운받을때도 편하다고 한다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 아예 python 이 참조하는 site-packages 에 설치되어 꼬이지 않도록 명시해주는 방법 사용하기 (아마 얘가 제일 편한듯)&lt;/p&gt;
&lt;pre id=&quot;code_1786411871390&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m pip install xxx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pip install xxx 는 path 에 잡힌 pip 실행 파일을 그대로 실행 -&amp;gt; 어떤 pyhton 에 연결된 pip 인지 헷갈림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python3 -m pip install xxx -&amp;gt; 지금 사용중인 python3 인터프리터에 정확하게 연결&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 2&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 venv (가상 환경 - virtual environment) 사용&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786412004443&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 가상환경 생성
python3 -m venv venv

# 가상환경 활성화
source venv/bin/activate

# 가상환경 활성화 (windows)
venv\Scripts\activate

# 활성화된 상태에서는 그냥 pip, python 써도 안전함
pip install xxx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 pip, python 이 다른데를 안 봄&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/109</guid>
      <comments>https://kjw81024.tistory.com/109#entry109comment</comments>
      <pubDate>Tue, 11 Aug 2026 10:33:59 +0900</pubDate>
    </item>
    <item>
      <title>cloudflared 사용</title>
      <link>https://kjw81024.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사람 수 카운트는 POST api 로 5초에 한 번씩 숫자만 보내주면 되는 형식&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표님 (ㄷㄷ) 요구사항으로 추가되는 기능이 CCTV 처럼 카메라가 비추는 화면을 확인할 수 있으면 좋을 것 같다고 하심&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그냥 사진 post 하던거랑 비슷하게 프레임 하나하나 post 하면 되는건가? 했는데 숫자 하나 보내는거랑은 다르다고 해서 ai 한테 무슨 차이인지 물어봤다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 프레임마다 POST 를 따로 날리면 요청마다 헤더 + 왕복시간 + 초당 10~15 프레임 뽑으려면 부담이 커짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) count POST 요청은 5초에 한번이라 가능하지만... 얘는 초당 몇 장씩 백엔드 -&amp;gt; 프론트 로 넘어가는거니까 트래픽 과도&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 터널 URL 로 MJPEG 스트림을 순서대로 받는 개념은 똑같지만, 요청을 매번 새로 여는 것이 아니라 연결 하나를 계속 열어둔 채로 프레임을 이어붙여 보낸느 방식이 필요해짐 (요청-응답 오버헤드 TCP, TLS handshake 등이 X)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 영상 스트림 주소 (camera_url) 이 노트북 로컬 IP 로 적혀있으면 같은 wifi 에 있는 사람만 배포서버 front 에 띄워지는 영상을 볼 수 있는 문제가 있기 때문에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 스트림도 외부에서 접근 가능한 주소 필요 !!!!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론: cloudflared 를 씁시다...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 cloudflared 는 터널링 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 로컬 서버 localhost 를 외부 접근 가능 공개 URL 로 노출시켜주는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) cloudflared 가 내 컴퓨터에서 cloudflare edge server 로 outbound 연결을 먼저 맺고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(2) 연결을 하면 공개 URL 이 생김&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3) 그 URL 로 들어오는 요청을 내 localhost 포트로 그대로 relay&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 내가 따로 inbound port 열 필요 X,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ngrok 이랑 비슷한 느낌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포트포워딩, 공유기 설정 없이 바로 사용할 수 있음 (공개 IP 없어도 , CGNAT 이어도 가능)&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;[카메라]&amp;nbsp;&amp;rarr;&amp;nbsp;[메인&amp;nbsp;스레드]&amp;nbsp;─&amp;nbsp;YOLO&amp;nbsp;검출&amp;nbsp;&amp;rarr;&amp;nbsp;count&amp;nbsp;백엔드로&amp;nbsp;POST&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;└&amp;nbsp;프레임&amp;nbsp;JPEG&amp;nbsp;인코딩&amp;nbsp;&amp;rarr;&amp;nbsp;공유&amp;nbsp;버퍼&lt;br /&gt;[서브&amp;nbsp;스레드(HTTP&amp;nbsp;서버)]&amp;nbsp;─&amp;nbsp;GET&amp;nbsp;/video_feed&amp;nbsp;&amp;rarr;&amp;nbsp;공유&amp;nbsp;버퍼를&amp;nbsp;MJPEG로&amp;nbsp;스트리밍&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치방법...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 맥이랑 윈도우 왔다갔다 쓰려니까 정신 나갈 것 같다........&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 적어놓는걸로&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786082636126&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# mac
brew install cloudflared

# windows
winget install --id Cloudflare.cloudflared&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행은&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786082655207&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cloudflared tunnel --url http://localhost:8090&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 된다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3004&quot; data-origin-height=&quot;1324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLNAyw/dJMcaft2j9u/RQWKl1G7putZtEBCUhLHEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLNAyw/dJMcaft2j9u/RQWKl1G7putZtEBCUhLHEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLNAyw/dJMcaft2j9u/RQWKl1G7putZtEBCUhLHEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLNAyw%2FdJMcaft2j9u%2FRQWKl1G7putZtEBCUhLHEK%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;3004&quot; height=&quot;1324&quot; data-origin-width=&quot;3004&quot; data-origin-height=&quot;1324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 화면이 쭉쭉 내려가는데 (시간이 약간 소요됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 네모에 있는 quick tunnel 메시지 아래 https:// xxxx.trycloudflare.com 주소가 공개 URL 이다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 저 공개 URL 을 복사해서&lt;/p&gt;
&lt;pre id=&quot;code_1786088150884&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python3 camera_daemon.py \
    --source 0 \
    --public-url https://ind-agent-poison-founded.trycloudflare.com/video_feed \ 
    --backend https://xxx/api/crowd/live&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 명령을 보내주면 영상이 송출되는것을 확인할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 이 때 생기는 공개 url 은 cloudflared 를 실행시키는 터미널이 꺼지면 같이 꺼지니까 매번 실행시켜서 새로운 값을 넣어줘야함)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한 코드는 대충&amp;nbsp; 아래 부분을 필수적으로 넣어줬다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1786089039673&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;_latest_jpeg = None
_lock = threading.Lock()

# --- MJPEG 서버 (stdlib만, flask 불필요) ---
class MJPEGHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header(&quot;Content-Type&quot;, &quot;multipart/x-mixed-replace; boundary=frame&quot;)
        self.end_headers()
        while True:
            with _lock:
                jpg = _latest_jpeg
            if jpg is None:
                time.sleep(0.05); continue
            self.wfile.write(b&quot;--frame\r\n&quot;)
            self.wfile.write(b&quot;Content-Type: image/jpeg\r\n\r\n&quot;)
            self.wfile.write(jpg + b&quot;\r\n&quot;)

# 카메라 루프를 안 막게 데몬 스레드로 서버 실행
server = ThreadingHTTPServer((&quot;0.0.0.0&quot;, 8090), MJPEGHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()

# --- 카메라 루프 (카메라 소유자) ---
while True:
    ret, frame = cap.read()
    results = model(frame, classes=[0])           # YOLO 사람 검출
    count = len(results[0].boxes)                 # &amp;rarr; 백엔드로 POST

    ok, buf = cv2.imencode(&quot;.jpg&quot;, results[0].plot())  # 박스 그린 프레임
    if ok:
        with _lock:
            _latest_jpeg = buf.tobytes()          # 공유 버퍼 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;... 사실 AI 한테 짜달라고 하면 잘 짜준다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짠 코드를 적당히 읽어보고 어색한 부분 물어보면서 고치면 됨...!!!!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;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;1196&quot; data-origin-height=&quot;1418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLflWL/dJMcagT3l7r/imqGhbu3EOkjxuSmpxf411/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLflWL/dJMcagT3l7r/imqGhbu3EOkjxuSmpxf411/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLflWL/dJMcagT3l7r/imqGhbu3EOkjxuSmpxf411/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLflWL%2FdJMcagT3l7r%2FimqGhbu3EOkjxuSmpxf411%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;1196&quot; height=&quot;1418&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;1418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 밤티같긴 하지만 잘 나옴, 인식도 잘 되고 프레임도 나쁘지않게 들어온다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 목표였던 제 3의 client 도 잘 보이는지 확인&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZXI2A/dJMcahZEXgx/FHxYwMyVBoRxBablRsm87k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZXI2A/dJMcahZEXgx/FHxYwMyVBoRxBablRsm87k/img.png&quot; data-alt=&quot;핸드폰 캡처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZXI2A/dJMcahZEXgx/FHxYwMyVBoRxBablRsm87k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZXI2A%2FdJMcahZEXgx%2FFHxYwMyVBoRxBablRsm87k%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;2188&quot; height=&quot;1100&quot; data-origin-width=&quot;2188&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;핸드폰 캡처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2376&quot; data-origin-height=&quot;1118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bi2fCh/dJMcaft2kyf/KHBZcNjPtsxrU6WHPXiuz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bi2fCh/dJMcaft2kyf/KHBZcNjPtsxrU6WHPXiuz0/img.png&quot; data-alt=&quot;배터리 23퍼센트.ㅎ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bi2fCh/dJMcaft2kyf/KHBZcNjPtsxrU6WHPXiuz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbi2fCh%2FdJMcaft2kyf%2FKHBZcNjPtsxrU6WHPXiuz0%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;2376&quot; height=&quot;1118&quot; data-origin-width=&quot;2376&quot; data-origin-height=&quot;1118&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;배터리 23퍼센트.ㅎ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 키고도 잘 보인다 화면 !!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌리다보면 cloudflared 를 켜둔 터미널창에 아래와 같은 오류가 뜨는데 (가끔????)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf3Z0R/dJMcafAPQiB/xFkBsTnQin1cn25venWmk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf3Z0R/dJMcafAPQiB/xFkBsTnQin1cn25venWmk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf3Z0R/dJMcafAPQiB/xFkBsTnQin1cn25venWmk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf3Z0R%2FdJMcafAPQiB%2FxFkBsTnQin1cn25venWmk1%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;1076&quot; height=&quot;796&quot; data-origin-width=&quot;1076&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&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;시각&lt;/td&gt;
&lt;td&gt;에러&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;심각도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07:15&lt;/td&gt;
&lt;td&gt;connection refused&lt;/td&gt;
&lt;td&gt;8090에 서버 없음&lt;/td&gt;
&lt;td&gt;❌ (근데 이제 해결됨)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07:26&lt;/td&gt;
&lt;td&gt;stream canceled, code 0&lt;/td&gt;
&lt;td&gt;브라우저가 스트림 연결 정상 종료&lt;/td&gt;
&lt;td&gt;⚠️ 대체로 무해&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;나는 자동으로 해결됐다 아마 초반에 렉걸려서 8090 서버 못 찾아서 생긴 문제인듯?!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/108</guid>
      <comments>https://kjw81024.tistory.com/108#entry108comment</comments>
      <pubDate>Fri, 7 Aug 2026 16:54:11 +0900</pubDate>
    </item>
    <item>
      <title>터미널에서 한글이 깨짐</title>
      <link>https://kjw81024.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;맥북에 있던 내용을 데스크탑으로 싹 옮기면서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDK 랑 도커랑 ... IDE 랑 등등 다운을 받고 ./gradlew 실행을 시켜봄&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z7qbg/dJMcaixy6PR/1SftgxsYjDfW1OJaWNdAX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z7qbg/dJMcaixy6PR/1SftgxsYjDfW1OJaWNdAX0/img.png&quot; data-alt=&quot;쒕뱶?꾨즺.....ㅋ&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z7qbg/dJMcaixy6PR/1SftgxsYjDfW1OJaWNdAX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz7qbg%2FdJMcaixy6PR%2F1SftgxsYjDfW1OJaWNdAX0%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;738&quot; height=&quot;400&quot; data-origin-width=&quot;738&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;쒕뱶?꾨즺.....ㅋ&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔이 UTF-8 로그를 못 읽어서 화면에서 깨져보이는거라구 함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785980643202&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;chcp 65001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 terminal 에 치고 run 하면 되는데 터미널 시작 할 때 마다 쳐야함 ...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발단계라 여러번 터미널을 닫고 켜서 귀찮기 때문에 영구 설정을 해보기로 했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785980697356&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;notepad $PROFILE&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파워쉘 프로필을 열어 설정해주면 되는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 저 명령어를 치니까 메모장이 뜨더니 지정된 경로를 찾을 수 없다는 오류가 발생&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필이 가리키는 파일/폴더 가 존재하지 않아서 뜨는 것 ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빈 메모장에 쳐도 되는데 그럼 저장할 폴더가 애매해지기 때문에 폴더부터 만들어줬다&lt;/p&gt;
&lt;pre id=&quot;code_1785980791993&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (!(Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 경로가 없다면 강제로 새 파일을 만들어서 저장하겠다는 의미&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 notepad 명령어를 치면&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;437&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o19rH/dJMcafHDuGB/ttdKa6LUIj148Ds4N1qIJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o19rH/dJMcafHDuGB/ttdKa6LUIj148Ds4N1qIJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o19rH/dJMcafHDuGB/ttdKa6LUIj148Ds4N1qIJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo19rH%2FdJMcafHDuGB%2FttdKa6LUIj148Ds4N1qIJ0%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;437&quot; height=&quot;83&quot; data-origin-width=&quot;437&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요런 파일 이름으로 뜬다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBXQUT/dJMcagGyIVG/JofyzuTzwOVk9PvvLLUTZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBXQUT/dJMcagGyIVG/JofyzuTzwOVk9PvvLLUTZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBXQUT/dJMcagGyIVG/JofyzuTzwOVk9PvvLLUTZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBXQUT%2FdJMcagGyIVG%2FJofyzuTzwOVk9PvvLLUTZk%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;924&quot; height=&quot;263&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용해주고싶었던건 UTF-8 을 읽는거니까 ..&lt;/p&gt;
&lt;pre id=&quot;code_1785980877134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용을 넣어주고 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 바로 실행시키려고 하면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KYR3B/dJMcad3ZwBa/DMZKv6WAKpvrLk9f971AA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KYR3B/dJMcad3ZwBa/DMZKv6WAKpvrLk9f971AA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KYR3B/dJMcad3ZwBa/DMZKv6WAKpvrLk9f971AA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKYR3B%2FdJMcad3ZwBa%2FDMZKv6WAKpvrLk9f971AA1%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;741&quot; height=&quot;158&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 오류가 뜸....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템에서 스크립트를 실행 할 수 없으므로 PowerShell_profile,ps1 파일을 로드할 수 없다 .......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로필 파일은 만들어졌지만 PowerShell 실행 정책 때문에 프로필 스크립트 로드가 막힘&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 파워쉘이 스크립트 실행을 막기 때문 (보안...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 사용자만 스크립트 허용해주기&lt;/p&gt;
&lt;pre id=&quot;code_1785981035639&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무슨 의미인지 검색해보니까&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RemoteSigned&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;= 내가 로컬에서 만든 스크립트(프로필 같은 거)는 실행 허용, 인터넷에서 받은 건 서명 있어야 실행 &amp;rarr; 안전하면서 프로필은 잘 돌아감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CurrentUser&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;= 시스템 전체 안 건드리고 내 계정만 &amp;rarr; 관리자 권한 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭐... 이런거라고 한다 ...... 잘 모름 ㅎ,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친 다음&amp;nbsp;터미널을 새로 열어주고 실행시키면 된다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;451&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lyaiu/dJMcagGyIKk/sec8krd2yRMBaR15d2VLDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lyaiu/dJMcagGyIKk/sec8krd2yRMBaR15d2VLDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lyaiu/dJMcagGyIKk/sec8krd2yRMBaR15d2VLDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flyaiu%2FdJMcagGyIKk%2Fsec8krd2yRMBaR15d2VLDk%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;451&quot; height=&quot;176&quot; data-origin-width=&quot;451&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간 글씨도 안 뜨고 안 깨진 상태로 잘 출력되는 모습&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/107</guid>
      <comments>https://kjw81024.tistory.com/107#entry107comment</comments>
      <pubDate>Thu, 6 Aug 2026 10:52:14 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL (Docker) 접속 (로컬)</title>
      <link>https://kjw81024.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발 DB 를 접속하려고 하는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 인턴하면서 스는 cursor 에는 내장 SQL 실행기가 따로 없다고 해서 터미널에서 docker exec 으로 여는거 메모&amp;nbsp;&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;1. 컨테이너 이름 확인하기&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1785811793142&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2019&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ca1jHX/dJMcaf8w7Rv/38oJw5fA3GNjCvWAW3S5rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ca1jHX/dJMcaf8w7Rv/38oJw5fA3GNjCvWAW3S5rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ca1jHX/dJMcaf8w7Rv/38oJw5fA3GNjCvWAW3S5rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fca1jHX%2FdJMcaf8w7Rv%2F38oJw5fA3GNjCvWAW3S5rk%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;2019&quot; height=&quot;146&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2019&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맨 뒤에 있는 NAMES 를 기억하면 된다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 접속하려고 하는건 postgres 니까 aicms-dev-postgres 를 기억하기 !&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IMAGE 이름이랑 착각하지 않게 조심....&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;2. psql 접속&lt;/h4&gt;
&lt;pre id=&quot;code_1785812031999&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it &amp;lt;NAMES&amp;gt; psql -U &amp;lt;USER&amp;gt; -d &amp;lt;DB명&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-it 는 psql 프롬프트로 들어간다는 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-U 뒤에는 DB 유저 (사수님께서 보내주셨다 ㅎ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-d 뒤에는 DB 이름 (이것두..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 접속 정보는 docker-compose.yml 또는 application.yml 파일에 가면 알 수 있다고 하심&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어를 잘 입력했다면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 입력 창이&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785812125642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;DB명&amp;gt;=#&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 바뀌었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 쿼리문 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널 창에서 찾고싶은 정보같은걸 SELECT 문을 사용하여 바로 찾아주면 됨!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;167&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhVOlc/dJMcahFjGKe/DQUdOf2PXgaCosXz3aq2KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhVOlc/dJMcahFjGKe/DQUdOf2PXgaCosXz3aq2KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhVOlc/dJMcahFjGKe/DQUdOf2PXgaCosXz3aq2KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhVOlc%2FdJMcahFjGKe%2FDQUdOf2PXgaCosXz3aq2KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;624&quot; height=&quot;167&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;167&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;추가.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 접속까진 안 하고 바로 실행해보고싶으면&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785812413376&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker exec -it &amp;lt;NAMES&amp;gt; psql -U &amp;lt;USER&amp;gt; -d &amp;lt;DB명&amp;gt; -c &quot;SELECT count(*) FROM contents;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-c 가 쿼리를 한 번 실행하고 바로 빠져나온다는 뜻이라고 함!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 그냥 접속하는게 편할 것 같긴 하다&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;트러블슈팅&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 쿼리를 클로드한테 부탁해서 붙여넣었는데 오류가 났다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAG6EN/dJMcaf8w72k/pHgXY5xFk4zNgrCnG6gYjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAG6EN/dJMcaf8w72k/pHgXY5xFk4zNgrCnG6gYjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAG6EN/dJMcaf8w72k/pHgXY5xFk4zNgrCnG6gYjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAG6EN%2FdJMcaf8w72k%2FpHgXY5xFk4zNgrCnG6gYjk%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;442&quot; height=&quot;106&quot; data-origin-width=&quot;442&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용한적도 없는 이상한 모양의 A 가 입력되었다고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유: 채팅/ 문서에서 명령어를 복사하면 하이픈 ( - ) 이 대시로 바뀌는 경우가 있어서 생기는 문제라고 한다 ....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 짧은 쿼리 안 쓰고 그냥 접속해서 했음 ㅎㅎ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 손으로 다 치면 생기지 않는 문제라 간단히 하고 패스&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/106</guid>
      <comments>https://kjw81024.tistory.com/106#entry106comment</comments>
      <pubDate>Tue, 4 Aug 2026 12:04:21 +0900</pubDate>
    </item>
    <item>
      <title>UnsatisfiedLinkError 트러블슈팅</title>
      <link>https://kjw81024.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인턴 하면서 노트북 필요하다고 그래서 친구한테 맥북을 빌려왔다......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 써보는 맥북이라 더 헤매는 중인듯&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와서 프로젝트 파악을 하면서 테스트를 돌려보고 있었늗네&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;./gradlew test로 &lt;/span&gt;&lt;span&gt;실행하니까 다른 테스트 40개는 성공하는데&amp;nbsp;&lt;/span&gt;&lt;span&gt;AiMediaTest&lt;/span&gt;&lt;span&gt;의 두 개 테스트만 실패&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;java.lang.UnsatisfiedLinkError
java.lang.NoClassDefFoundError&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;OpenCV를 사용하는 테스트에서만 오류가 발생한다는걸 알았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 프로젝트는 AI 로 영상을 제작해서 화면에 띄우는 프로젝트인데 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;OpenCV 랑 FFmpeg를 사용&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;os 에 맞는 native library (.dll, .so, .dylib)을 함께 사용하는 프로젝트&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;-&amp;gt; 현재 실행중인 운영체제 (난 arm64) 랑 CPU 아키텍처에 맞는 네이티브 라이브러리가 반드시 로드되어야 함&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;맥이 어떤 CPU 아키텍처인지 확인하는 쉬운 방법&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785315446164&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;uname -m&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해봤더니 arm64 가 떴고, 참고로 &lt;span&gt;Apple Silicon(M1, M2, M3)은 모두 &lt;/span&gt;&lt;span&gt;arm64&lt;/span&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;span&gt;일단 마저 gradle properties 를 확인해보니까 platfrom 이 linux-x86_64전용&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p9yuy/dJMcafgtgqi/IngyOKkeMGBsm7Hj3SpxDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p9yuy/dJMcafgtgqi/IngyOKkeMGBsm7Hj3SpxDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p9yuy/dJMcafgtgqi/IngyOKkeMGBsm7Hj3SpxDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp9yuy%2FdJMcafgtgqi%2FIngyOKkeMGBsm7Hj3SpxDk%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;1126&quot; height=&quot;216&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나를 위한 네이티브 바이너리는 없었다......&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 프로젝트는 linux-x86_64 용 OpenCV이고 내가 실행시키는 환경은 macOS arm64 라서 JVM 이 라이브러리 로딩에 실패해서 UnsatisfiedLinkError 가 뜬다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 잇는 NoClassDefFoundError 발생 이유&lt;/p&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;OpenCV 관련 클래스가 로딩되는 과정에서 내부적으로 네이티브 라이브러리를 먼저 불러오는데&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;네이티브&amp;nbsp;라이브러리&amp;nbsp;로드&amp;nbsp;실패&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;br /&gt;클래스&amp;nbsp;초기화&amp;nbsp;실패&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;br /&gt;NoClassDefFoundError&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;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&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;span&gt;back/gradle.properties&lt;/span&gt;&lt;span&gt;)은 Docker와 CI 서버에서도 사용하기 때문에 수정하면 안 되고&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;내 로컬에서만 Gradle 전역 설정을 넣어줬다&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785747570711&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# .gradle/gradle.properties 파일

javacppPlatform=macosx-arm64&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;그리고 쓸데없이 돌아가는 Gradle demon 은 죽임&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785747592221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./gradlew --stop&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle 데몬이 백그라운드에 계속 떠 있으면서 이전 javacppPlatform 값을 캐싱하고 있어서 옛날에 저장되어있던 linux-x86-64 값을 계속 가지고 있게 되기 때문....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얘때문에 고쳐졌는데도 고쳐졌는지 몰랏다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 테스트 할 때는 cursor 말고 별도 터미널에서 실행시킴으로써 해결할 수 잇엇다&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/105</guid>
      <comments>https://kjw81024.tistory.com/105#entry105comment</comments>
      <pubDate>Mon, 3 Aug 2026 18:00:48 +0900</pubDate>
    </item>
    <item>
      <title>실시간 카메라 - 백엔드 서버 연결</title>
      <link>https://kjw81024.tistory.com/104</link>
      <description>&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;인구를 감지하는 방법중 제시한건 2개&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 그냥 그 때 그 때 화면에 나오는 사람 수를 기준으로 추정하는 방법과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2 특정 사람을 인식해서 몇 명 정도인지 대략 측정하는 방법 (중복으로 카운트 되지 않도록 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번은 최대 가능한 인원이 3개정도고, 가격이 너무 비싼 문제 등 여러가지 하자가 많았기 때문에 1을 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1에서도 4개로 나눌 수 있었고 간단하게 정리했다&lt;/p&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 style=&quot;width: 50%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;연결 방법&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;웹캠 구매&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;라즈베리파이 사용 -&amp;gt; 직접 웹캠을 연결&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;맥북 등 노트북 카메라 사용&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;그냥 터미널에서 돌리면 됨&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;아이폰 연결&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;와이파이 스트림 사용 (맥북이나 라즈베리파이 아무거나 상관 x)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;안드로이드 연결&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;라즈베리파이 (USB 웹캠 모드로 하고 파이랑 연결)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중 아이폰 연결은 와이파이 스트림 (동일 와이파이를 사용하는 경우에 한함) 으로 연결하는 방식이었고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 깔아서 진행했다. 사용한 앱은 SIMPLE IP CAM&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;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zEBsh/dJMcacjHoCB/MFnouL83vbLkV6v2jXq8X0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zEBsh/dJMcacjHoCB/MFnouL83vbLkV6v2jXq8X0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zEBsh/dJMcacjHoCB/MFnouL83vbLkV6v2jXq8X0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzEBsh%2FdJMcacjHoCB%2FMFnouL83vbLkV6v2jXq8X0%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;310&quot; height=&quot;671&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어가면 FPS&amp;nbsp; 나 해상도 ? 등을 조정할 수 있었고&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;1170&quot; data-origin-height=&quot;2532&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0T589/dJMcabZvOEZ/KSVkmFS9HtCpKAgMPJaKO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0T589/dJMcabZvOEZ/KSVkmFS9HtCpKAgMPJaKO1/img.png&quot; data-alt=&quot;http://x.x.x.x:8080/stream.mjpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0T589/dJMcabZvOEZ/KSVkmFS9HtCpKAgMPJaKO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0T589%2FdJMcabZvOEZ%2FKSVkmFS9HtCpKAgMPJaKO1%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;310&quot; height=&quot;671&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;2532&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;http://x.x.x.x:8080/stream.mjpg&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조정 이후 Start server 누르면 이런 큐알 화면으로 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굳이 큐알을 사용할 필요는 없고 저 밑에잇는 URL 로 사용하면 됨&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 나는 보이다시피 데이터로 연결되어있어서 0.0.0.0 으로 뜨지만 와이파이 연결하면 그 ip 번호로 뜨고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비디오 화면 코드에 해당 url 값을 넣어주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1785741908060&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# webcam, android, macbook
cap = cv2.VideoCapture(0)
# iphone
cap = cv2.VedioCapture(&quot;http://x.x.x.x:8080/stream.mjpg&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 처음에&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785742076550&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot;
사람 수 검출 테스트 스크립트 (터미널 5초 주기 최대치 출력 버전)
- 화면 창: 실시간 박스 표시 (기존과 동일)
- 터미널: 5초마다 그 구간 동안 감지된 인원수의 최대값을 출력
- 종료: 영상 창 활성화 상태에서 'q' 키
&quot;&quot;&quot;
import cv2
import time
import requests

# 로컬 테스트용
# BACKEND_URL = &quot;http://localhost:8000/api/crowd/live&quot;

# 배포용 
BACKEND_URL = &quot;http://x.x.x.x:4327/api/crowd/live&quot;

# 경로는 백엔드 확정 후 교체
last_sent = 0


hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

# 1. 아이폰 카메라 (Simple IP Camera) 사용하여 연결
# cap = cv2.VideoCapture(&quot;http://x.x.x.x:8080/stream.mjpg&quot;)

# 2. 노트북 카메라, 웹캠, 안드로이드 USB-C 카메라 사용하여 연결
# 추가: 웹캠이랑 안드로이드 USB-C 사용하는 경우 라즈베리파이 필요 
cap = cv2.VideoCapture(0)

if not cap.isOpened():
    print(&quot;카메라를 열 수 없습니다.&quot;)
    print(&quot;- 맥북이면: 시스템 설정 &amp;gt; 개인정보 보호 및 보안 &amp;gt; 카메라에서 터미널 권한 확인&quot;)
    print(&quot;- 다른 프로그램이 카메라를 이미 쓰고 있는지도 확인해보세요 (Zoom, 카메라 앱 등)&quot;)
    exit()

print(&quot;카메라 열림. 영상 창에서 'q' 키를 누르면 종료됩니다.&quot;)
print(&quot;터미널에는 5초마다 그 구간의 최대 인원수가 출력됩니다.\n&quot;)

window_start = time.time()
window_max = 0

while True:
    ret, frame = cap.read()
    if not ret:
        # 아이폰으로 테스트 시 break 대신 countinue 로 바꿔주기 
        print(&quot;프레임을 읽지 못했습니다. 카메라 연결을 확인하세요.&quot;)
        break

    small = cv2.resize(frame, (640, 480))

    boxes, weights = hog.detectMultiScale(
        small, winStride=(8, 8), padding=(8, 8), scale=1.05
    )

    for (x, y, w, h) in boxes:
        cv2.rectangle(small, (x, y), (x + w, y + h), (0, 255, 0), 2)

    count = len(boxes)
    window_max = max(window_max, count)

    cv2.putText(
        small, f&quot;People: {count}&quot;, (10, 30),
        cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2
    )

    cv2.imshow(&quot;Person Detection Test&quot;, small)

    now = time.time()
    if now - window_start &amp;gt;= 5:
        print(f&quot;[{time.strftime('%H:%M:%S')}] 최근 5초간 최대 인원수: {window_max}&quot;)
        try:
            requests.post(BACKEND_URL, json={&quot;count&quot;: window_max}, timeout=1)
        except requests.RequestException:
            pass  # 백엔드 없거나 꺼져 있어도 검출은 계속
        window_start = now
        window_max = 0

    if cv2.waitKey(1) &amp;amp; 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 짰음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지가 들어오는 경로를 cap 으로 받아주고 (카메라를 킴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cap 이 잘 열려있다면 프레임 마다 사람이 몇 명 있는지 세어서 박스로 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hog 가 사람 검출기로 사용된다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785742377425&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())&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;해당 화면을 resizing 해준다 크기를 줄이면 계산이 빨라지기 때문에 매 프레임마다 사람을 인식해야하는 경우 유용함!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boxes, weights 를 찾고 사람을 인식하면 사각형 목록으로 받고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 winStride, scale 이 얼마나 ... 자세히 어떤 크기까지 인식할지를 정하는 세부 설정으로 사용된다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boxes 를 for문으로 돌면서 초록색 네모를 그리고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 매 프레임마다 서버로 보내지 않고 5초 내에 인식된 최대 사람수를 보내기 때문에 해당 값을 count 와 window_max 를 사용하여 저장하고 보낸다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785742526866&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;now = time.time()
if now - window_start &amp;gt;= 5:&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5초를 세기 위해 now 값을 저장해주고 현재 시각 - 타이머 시작시간 이 5초 이상이면 보내고 아니면 pass ...&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785742565069&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    print(f&quot;[{time.strftime('%H:%M:%S')}] 최근 5초간 최대 인원수: {window_max}&quot;)
    try:
        requests.post(BACKEND_URL, json={&quot;count&quot;: window_max}, timeout=1)
    except requests.RequestException:
        pass&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5초가 지나면 로그를 찍고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;post 를 사용해서 앞서 저장한 backend 서버로 몇 명이 인식되었는지 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 생긴 오류는 RequestException 으로 예외처리 해줌&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;requests 는 기본으로 제공되는 라이브러리가 아니기 때문에 pip3 install requests 로 다운받아주기!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복문 종료 이후 (q 또는 ctrl C) release 해줌&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 솔직히..&amp;gt;!!!! 성능이 너무 쓰레기였다&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RwXky/dJMcafHBzte/A04kMk7K8XRdA83UmeZui0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RwXky/dJMcafHBzte/A04kMk7K8XRdA83UmeZui0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RwXky/dJMcafHBzte/A04kMk7K8XRdA83UmeZui0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRwXky%2FdJMcafHBzte%2FA04kMk7K8XRdA83UmeZui0%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;788&quot; height=&quot;388&quot; data-filename=&quot;image.png&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 개발하고 있는.. 인턴이다보니 사람들을 부를수 없어서 핸드폰으로 여러 사람을 캡처해서 보여줘도 인원이 안 늘거나&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가방 같은것도 사람으로 인식해서 갑자기 2가 되는 경우가 생겼다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능개선 타임. . .&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날에 대학교 때 yolo 로 자전거 인식 프로그램 만들었던 기억이 나서 yolo 를 도입하기로 했다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785743179756&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;&quot;&quot;
사람 수 검출 테스트 스크립트 (YOLO 버전)
화면 창: 실시간 박스 표시
터미널: 5초마다 그 구간 최대 인원수 출력
종료: 영상 창에서 'q'
&quot;&quot;&quot;
import cv2
import time
import requests
from ultralytics import YOLO

# 로컬 테스트용
# BACKEND_URL = &quot;http://localhost:8000/api/crowd/live&quot;
# 배포 서버용
BACKEND_URL = &quot;http://xxxx.xxxxx.co.rk/api/crowd/live&quot;

모델 로드 (최초 실행 시 가중치 자동 다운로드, yolo11n.pt는 ~5MB)
model = YOLO(&quot;yolo11n.pt&quot;)   # 정확도 더 필요하면 yolo11s.pt / yolo11m.pt
CONF = 0.4                   # 신뢰도 임계값 (낮추면 더 많이 잡지만 오검출&amp;uarr;)
DEVICE = &quot;cpu&quot;               # 애플 실리콘이면 &quot;mps&quot;로 바꿔 GPU 가속 시도 가능

# 아이폰 카메라 (Simple IP Camera) 사용하여 연결
# cap = cv2.VideoCapture(&quot;http://x.x.x.x:8080/stream.mjpg&quot;)
# 노트북 카메라, 웹캠, 안드로이드 USB-C 카메라 사용하여 연결
# 추가: 웹캠이랑 안드로이드 USB-C 사용하는 경우 라즈베리파이 필요
cap = cv2.VideoCapture(0)
if not cap.isOpened():
    print(&quot;카메라를 열 수 없습니다.&quot;)
    exit()

print(&quot;카메라 열림. 'q'로 종료.\n&quot;)

window_start = time.time()
window_max = 0

while True:
    ret, frame = cap.read()
    if not ret:
        print(&quot;프레임을 읽지 못했습니다.&quot;)
        break

    small = cv2.resize(frame, (640, 480))

    # classes=[0] &amp;rarr; COCO 'person' 클래스만 검출
    results = model(small, classes=[0], conf=CONF, device=DEVICE, verbose=False)
    boxes = results[0].boxes

    count = len(boxes)
    for box in boxes:
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        score = float(box.conf[0])
        cv2.rectangle(small, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(small, f&quot;{score:.2f}&quot;, (x1, y1 - 6),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

    window_max = max(window_max, count)

    cv2.putText(small, f&quot;People: {count}&quot;, (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
    cv2.imshow(&quot;Person Detection Test&quot;, small)

    now = time.time()
    if now - window_start &amp;gt;= 5:
        print(f&quot;[{time.strftime('%H:%M:%S')}] 최근 5초간 최대 인원수: {window_max}&quot;)
try:
            requests.post(BACKEND_URL, json={&quot;count&quot;: window_max}, timeout=1)
        except requests.RequestException:
            pass
        window_start = now
        window_max = 0

    if cv2.waitKey(1) &amp;amp; 0xFF == ord('q'):
        break&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yolo 를 사용하기 위해 ultralytics 를 pip3 install 해주고 import 해줌&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785743262527&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;model = YOLO(&quot;yolo11n.pt&quot;)   # 학습된 모델 파일 불러오기
CONF = 0.4                   # 신뢰도 임계값
DEVICE = &quot;cpu&quot;               # 계산을 CPU로 할지 GPU로 할지&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hog 를 사용하던 검출기 대신 model 을 yolo11 로 불러왔다&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1785743301187&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;results = model(small, classes=[0], conf=CONF, device=DEVICE, verbose=False)
boxes = results[0].boxes&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yolo 는 진짜 여러 클래스들을 구분할 수 잇는데 여기선 [0] 을 사용해서 사람만 검출할 수 잇도록 지정해주고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;conf 값 (몇퍼센트 이상으로 사람이라고 판단되면 count 할지)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;device 는 뭘 쓸지 등등을 매개변수로 넘겨서 처리&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후는 박스를 그리고 사람을 세어 추가한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 다운로드랑 세팅에는 시간이 약간 걸렸는데 다 받고 나니까 성능이 진짜!!! 최고 . .. .&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-03_4.43.20.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ySLg8/dJMcaf8wK3t/hCvd3NKcoSyBq7YG6kUhP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ySLg8/dJMcaf8wK3t/hCvd3NKcoSyBq7YG6kUhP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ySLg8/dJMcaf8wK3t/hCvd3NKcoSyBq7YG6kUhP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FySLg8%2FdJMcaf8wK3t%2FhCvd3NKcoSyBq7YG6kUhP1%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;680&quot; height=&quot;408&quot; data-filename=&quot;2026-08-03_4.43.20.png&quot; data-origin-width=&quot;680&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2026-08-03_4.43.09.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1020&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7O9mJ/dJMcab6drzt/3vrosUQCtnlsxClASTPkK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7O9mJ/dJMcab6drzt/3vrosUQCtnlsxClASTPkK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7O9mJ/dJMcab6drzt/3vrosUQCtnlsxClASTPkK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7O9mJ%2FdJMcab6drzt%2F3vrosUQCtnlsxClASTPkK0%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;1098&quot; height=&quot;1020&quot; data-filename=&quot;2026-08-03_4.43.09.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1020&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒷모습만 겨우인식하던 hog 랑 다르게 사람 수를 거의 정확하게 세는 모습을 확인할 수 있었다&amp;nbsp;&lt;/p&gt;
&lt;div&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;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;HOG (기존)&lt;/td&gt;
&lt;td&gt;YOLO (신규)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;import&lt;/td&gt;
&lt;td&gt;cv2, time, requests&lt;/td&gt;
&lt;td&gt;+ ultralytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검출기 생성&lt;/td&gt;
&lt;td&gt;HOGDescriptor 2줄&lt;/td&gt;
&lt;td&gt;YOLO(&quot;yolo11n.pt&quot;) + CONF/DEVICE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;검출 실행&lt;/td&gt;
&lt;td&gt;detectMultiScale(...)&lt;/td&gt;
&lt;td&gt;model(small, classes=[0], ...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;좌표 형식&lt;/td&gt;
&lt;td&gt;(x, y, w, h)&lt;/td&gt;
&lt;td&gt;(x1, y1, x2, y2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;post 를 사용해서 보내준 값들은 백엔드 코드에서&lt;/p&gt;
&lt;pre id=&quot;code_1785743682501&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/live&quot;) 
public Map&amp;lt;String, Object&amp;gt; live (@RequestBody LiveRequest body) {
	return crowdService.sampleAndStore(body.peopleCount(), body.level());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 받고 서비스 코드에서 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시뮬레이션을 위해 만들어둔 수동 사람 조정 요청이 1순위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2순위가 캠으로 들어오는 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3순위가 목업 센서 (그냥 만들어지는 가짜 정보들)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;body 값으로 들어온 사람 수, level (혼잡도 레벨: 한산, 보통, 붐빔,, 등)을 사용해서 캐시와 DB 에 각각 저장&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;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/104</guid>
      <comments>https://kjw81024.tistory.com/104#entry104comment</comments>
      <pubDate>Mon, 3 Aug 2026 17:57:31 +0900</pubDate>
    </item>
    <item>
      <title>Race Condition 검증 Test 정리</title>
      <link>https://kjw81024.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;주요 비즈니스 로직에 분산 락을 적용하면서...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜터님께서 이렇게 고치는 이유가 뭐냐고 여쭤보셨다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TX 시작 -&amp;gt; Lock 획득 -&amp;gt; 비즈니스로직 -&amp;gt; Lock 해제 -&amp;gt; TX commit 이 이루어지면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock 해제와 TX commit 사이에 빈 공간이 생기기 때문에 해제된 락을 다른 스레드가 잡아서 읽어버리면 TX commit 전이기 때문에 바뀌기 전의 정보를 읽는다.... 라고 대답했는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그걸 직접 확인을 해봤냐고 여쭤보셔서... 아니라구 함&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 널리 알려져있는 문제라 내가 꼭 확인을 해야하나? 라는 생각도 들었다&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 테스트 하기 ....&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 1&lt;/h3&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;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 style=&quot;width: 100%;&quot;&gt;[ANTI-PATTERN] &lt;br /&gt;A:&amp;nbsp;@Transactional&amp;nbsp;시작&amp;nbsp;&amp;rarr;&amp;nbsp;락&amp;nbsp;획득&amp;nbsp;&amp;rarr;&amp;nbsp;expire()&amp;nbsp;&amp;rarr;&amp;nbsp;락&amp;nbsp;해제&amp;nbsp;&amp;rarr;&amp;nbsp;sleep(후속&amp;nbsp;로직)&amp;nbsp;&amp;rarr;&amp;nbsp;commit &lt;br /&gt;B:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;uarr; 이 시점에 깨어남 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&amp;rarr;&amp;nbsp;findById&amp;nbsp;&amp;rarr;&amp;nbsp;PENDING&amp;nbsp;읽음&amp;nbsp;(A&amp;nbsp;아직&amp;nbsp;commit&amp;nbsp;전) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&amp;rarr;&amp;nbsp;cancel()&amp;nbsp;&amp;rarr;&amp;nbsp;commit&amp;nbsp;(v=1) &lt;br /&gt;A:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/span&gt;&amp;rarr;&amp;nbsp;commit&amp;nbsp;시도 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; WHERE version=0 &amp;rarr; 0 rows &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;rarr; OptimisticLockException&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;운영 패턴 시나리오&lt;/p&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 style=&quot;width: 100%;&quot;&gt;[FIXED] &lt;br /&gt;A:&amp;nbsp;락&amp;nbsp;획득&amp;nbsp;&amp;rarr;&amp;nbsp;(tx&amp;nbsp;시작&amp;nbsp;&amp;rarr;&amp;nbsp;expire()&amp;nbsp;&amp;rarr;&amp;nbsp;commit)&amp;nbsp;&amp;rarr;&amp;nbsp;락&amp;nbsp;해제 &lt;br /&gt;B:&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;uarr;&amp;nbsp;이&amp;nbsp;시점에야&amp;nbsp;락&amp;nbsp;획득&amp;nbsp;가능 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;rarr;&amp;nbsp;findById&amp;nbsp;&amp;rarr;&amp;nbsp;EXPIRED&amp;nbsp;읽음 &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;rarr;&amp;nbsp;cancel&amp;nbsp;거부&amp;nbsp;(상태&amp;nbsp;가드)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CountDownLatch 로 A 가 락 해제한 직후, commit 전 시점을 B 의 시작시점으로 잡음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;시작시점까지 Sleep 으로 잡으니까 계속 애매하게 되다가 안 되다가 함....&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LockTransactoinRaceConditionTest.java&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1780560853345&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
class LockTransactionRaceConditionTest {

	private static final String LOCK_INSIDE_TX_KEY_PREFIX = &quot;lock:inside-tx:&quot;;
    private static final String CORRECT_LOCK_KEY_PREFIX = &quot;lock:reservation:&quot;;

    
    @BeforeEach
    void setup() {
		// reservation 생성, reservation Id 가져옴
    }


    @AfterEach
    void cleanup() {
    	// lock key 만들어진거 delete, reservationRepository 도 비워줌
    }

    @Test
    @DisplayName(&quot;[ANTI-PATTERN] 락이 트랜잭션 안쪽 &amp;mdash; B는 stale PENDING으로 cancel commit, A의 expire는 @Version에 막혀 손실됨&quot;)
    void race_condition_when_lock_inside_transaction() throws Exception {
        CountDownLatch aReleasedLock = new CountDownLatch(1);
        AtomicReference&amp;lt;Throwable&amp;gt; aError = new AtomicReference&amp;lt;&amp;gt;();
        AtomicReference&amp;lt;Throwable&amp;gt; bError = new AtomicReference&amp;lt;&amp;gt;();
        AtomicBoolean bCommitted = new AtomicBoolean(false);

        // A: expire (락 해제 후 sleep -&amp;gt; 나중에 commit 시도)
        Thread tA = new Thread(() -&amp;gt; {
            try {
                lockInsideTxExpireService.expireWithLockInside(reservationId, aReleasedLock);
            } catch (Throwable t) {
                aError.set(t);
            }
        });

        // B: A 가 락 해제하자마자 -&amp;gt; stale PENDING 읽고 cancel commit
        Thread tB = new Thread(() -&amp;gt; {
            try {
                aReleasedLock.await();
                cancelService.cancel(reservationId);
                bCommitted.set(true); // B 입장에선 정상 종료
            } catch (Throwable t) {
                bError.set(t);
            }
        });

        tA.start();
        tB.start();
        tA.join();
        tB.join();

        // (1) B 는 PENDING 을 읽고 cancel 까지 commit 했다 &amp;mdash; 락 직렬화 실패의 직접 증거
        assertThat(bError.get()).isNull();
        assertThat(bCommitted.get()).isTrue();

        // (2) A 의 commit 은 @Version 이 막아서 OptimisticLockException
        //	== race 가 @Version 까지 도달했다 == 분산 락만으론 못 막은 케이스
        assertThat(aError.get()).isInstanceOf(ObjectOptimisticLockingFailureException.class);

        // (3) 최종 DB 는 B 의 cancel 이 살아남고, A 의 expire 는 흔적 없음
        Reservation saved = reservationRepository.findById(reservationId).orElseThrow();
        assertThat(saved.getStatus()).isEqualTo(ReservationStatus.CANCELED);
        assertThat(saved.getCanceledBy()).isNotNull();
        assertThat(saved.getCanceledAt()).isNotNull();
        assertThat(saved.getCancelReason()).isNotNull();
        assertThat(saved.getExpiredAt()).isNull();
    }

    @Test
    @DisplayName(&quot;[FIXED] 락이 트랜잭션 바깥 &amp;mdash; B는 EXPIRED를 읽고 취소 자체가 거부된다&quot;)
    void no_race_when_lock_outside_transaction() throws Exception {
        CountDownLatch aAcquiredLock = new CountDownLatch(1);
        AtomicReference&amp;lt;Throwable&amp;gt; bError = new AtomicReference&amp;lt;&amp;gt;();
        AtomicReference&amp;lt;Throwable&amp;gt; error = new AtomicReference&amp;lt;&amp;gt;();

        // A: 락 획득 -&amp;gt; expireOne (commit) -&amp;gt; 락 해제
        Thread tA = new Thread(() -&amp;gt; {
            try {
                reservationLockService.executeWithReservationLock(reservationId, () -&amp;gt; {
                    aAcquiredLock.countDown();
                    reservationExpireService.expireOne(reservationId);
                    return null;
                });
            } catch (Throwable t) {
                error.set(t);
            }
        });

        // B: A 락 해제 후 락 획득 -&amp;gt; 이미 EXPIRED -&amp;gt; cancel 거부
        Thread tB = new Thread(() -&amp;gt; {
            try {
                aAcquiredLock.await();
                acquireLockAndCancel(reservationId);
            } catch (Throwable t) {
                bError.set(t); // 이건 기대하는 예외
            }
        });

        tA.start();
        tB.start();
        tA.join();
        tB.join();

        assertThat(error.get()).isNull(); // A는 정상
        assertThat(bError.get())
                .isInstanceOf(IllegalStateException.class)
                .hasMessageContaining(&quot;락 획득 타임아웃&quot;);

        Reservation saved = reservationRepository.findById(reservationId).orElseThrow();
        assertThat(saved.getStatus()).isEqualTo(ReservationStatus.EXPIRED);

        // 데이터가 오염되지 않았다
        assertThat(saved.getCanceledBy()).isNull();
        assertThat(saved.getCanceledAt()).isNull();
        assertThat(saved.getCancelReason()).isNull();
    }

    private void acquireLockAndCancel(Long id) throws InterruptedException {
        long deadline = System.currentTimeMillis() + 3_000;
        while (System.currentTimeMillis() &amp;lt; deadline) {
            try {
                reservationLockService.executeWithReservationLock(id,
                        () -&amp;gt; cancelService.cancel(id));
                return;
            } catch (ReservationException e) {
                // 락 획득 실패 (A가 아직 안 풀었음) &amp;rarr; 재시도
                Thread.sleep(10);
            }
            // ReservationException 이 아닌 예외 (INVALID_STATUS 등) 는 위로 전파
        }
        throw new IllegalStateException(&quot;락 획득 타임아웃&quot;);
    }

    // ─────────────────────────────────────────────────────────────────
    // 안티패턴 테스트 전용 빈
    // ─────────────────────────────────────────────────────────────────
    
    // 많은 생략 

    static class LockInsideTxExpireService {
        @Transactional
        public void expireWithLockInside(Long id, CountDownLatch lockReleasedSignal) {
			//@Transactional 메서드 내부에서 락을 잡고/해제
    	}

    static class LockOutsideReader {
		// 락 바깥에서 호출 - 자체 read-only 트랜잭션을 시작해서 commit 된 값 읽기 
    }

    static class CancelService {
        @Transactional
        public Void cancel(Long id) {
        	// 예약 취소 로직
        }
    }
}&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;결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;B&amp;nbsp;는&amp;nbsp;PENDING&amp;nbsp;을&amp;nbsp;읽고&amp;nbsp;cancel&amp;nbsp;commit&amp;nbsp;까지&amp;nbsp;성공 &lt;br /&gt;A&amp;nbsp;의&amp;nbsp;expire&amp;nbsp;는&amp;nbsp;@Version&amp;nbsp;에&amp;nbsp;의해&amp;nbsp;ObjectOptimisticLockingFailureException&amp;nbsp;으로&amp;nbsp;손실 &lt;br /&gt;즉 환불 처리, Proposal 복구 같은 후속 작업이 싹 다 누락될 수 있음&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인&amp;nbsp;목적:&amp;nbsp;락이&amp;nbsp;트랜잭션&amp;nbsp;안쪽이면&amp;nbsp;커넥션&amp;nbsp;자원도&amp;nbsp;낭비되는가? &lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스 A&lt;/p&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 style=&quot;width: 100%;&quot;&gt;@Transactional&amp;nbsp;진입&amp;nbsp;&amp;rarr;&amp;nbsp;(DB&amp;nbsp;접근&amp;nbsp;X)&amp;nbsp;&amp;rarr;&amp;nbsp;tryLock&amp;nbsp;실패&amp;nbsp;&amp;rarr;&amp;nbsp;rollback &lt;br /&gt;&amp;rarr;&amp;nbsp;커넥션&amp;nbsp;획득&amp;nbsp;안&amp;nbsp;됨&amp;nbsp;&amp;rarr;&amp;nbsp;낭비&amp;nbsp;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스 B&lt;/p&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 style=&quot;width: 100%;&quot;&gt;@Transactional&amp;nbsp;진입&amp;nbsp;&amp;rarr;&amp;nbsp;findById&amp;nbsp;&amp;rarr;&amp;nbsp;tryLock&amp;nbsp;실패&amp;nbsp;&amp;rarr;&amp;nbsp;rollback &lt;br /&gt;&amp;rarr;&amp;nbsp;커넥션&amp;nbsp;점유&amp;nbsp;+&amp;nbsp;무의미한&amp;nbsp;rollback&amp;nbsp;비용&amp;nbsp;발생&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionConnectionWasteTest.java&lt;/p&gt;
&lt;pre id=&quot;code_1780585497950&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
@TestPropertySource(properties = {
        // lazy connection acquisition 을 진짜로 동작시키기 위한 조합
        &quot;spring.datasource.hikari.auto-commit=false&quot;,
        &quot;spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true&quot;
})
class TransactionConnectionWasteTest {


    @BeforeEach
    void setup() {
		// reservation 과 reservation id 초기화 
    }

    @AfterEach
    void cleanup() {
		// repository 비움 
	}

    @Test
    @DisplayName(&quot;[A] @Transactional 진입만으론 커넥션 미획득 &amp;mdash; DB 접근 전 락 실패 = 낭비 0&quot;)
    void connection_not_acquired_when_lock_fails_before_db_access() {
        // given
        HikariPoolMXBean pool = hikariDataSource.getHikariPoolMXBean();
        int baseline = pool.getActiveConnections();
        AtomicInteger activeAtFailure = new AtomicInteger(-1);

        // when - DB 접근 없이 락 실패 시뮬레이트
        assertThatThrownBy(() -&amp;gt; probe.failLockWithoutDbAccess(activeAtFailure))
                .isInstanceOf(SimulatedLockFailure.class);

        // then
        // (1) 락 실패 직전, 트랜잭션은 진입했지만 커넥션은 아직 안 잡혀 있었음
        assertThat(activeAtFailure.get())
                .as(&quot;DB 접근 전이면 @Transactional 진입만으론 커넥션 획득 X&quot;)
                .isEqualTo(baseline);
        // (2) 사후에도 baseline 유지
        assertThat(pool.getActiveConnections()).isEqualTo(baseline);
    }

    @Test
    @DisplayName(&quot;[B] DB 조회 후 락 실패 &amp;mdash; 커넥션 점유 + 무의미한 rollback (낭비 발생)&quot;)
    void connection_acquired_and_wasted_when_lock_fails_after_db_access() {
        // given
        HikariPoolMXBean pool = hikariDataSource.getHikariPoolMXBean();
        int baseline = pool.getActiveConnections();
        AtomicInteger activeAtFailure = new AtomicInteger(-1);

        // when - findById 후 락 실패 시뮬레이트
        assertThatThrownBy(() -&amp;gt; probe.failLockAfterDbAccess(activeAtFailure, reservationId))
                .isInstanceOf(SimulatedLockFailure.class);

        // then
        // (1) 락 실패 시점에 커넥션이 이미 점유 중 -&amp;gt; rollback 비용까지 그대로 낭비됨
        assertThat(activeAtFailure.get())
                .as(&quot;DB 접근 후엔 커넥션 점유. 락 실패해도 rollback 까지 못 풀어줌&quot;)
                .isEqualTo(baseline + 1);
        // (2) rollback 종료 시점엔 풀로 반환됨
        assertThat(pool.getActiveConnections()).isEqualTo(baseline);
    }

    // ────────────────────────────────────────────────────────────
    // 테스트 전용 빈 + 시뮬레이션 예외
    // ────────────────────────────────────────────────────────────

    static class SimulatedLockFailure extends RuntimeException {
        // 실제 Redis 락 실패 대신 던지는 예외
    }

    @TestConfiguration
    static class Config {
        @Bean
        ConnectionWasteProbeService connectionWasteProbeService(
                ReservationRepository repo, HikariDataSource ds) {
            return new ConnectionWasteProbeService(repo, ds);
        }
    }
    
    static class ConnectionWasteProbeService {

        @Transactional
        public void failLockWithoutDbAccess(AtomicInteger activeRecorder) {
            // Case A: 트랜잭션 진입 -&amp;gt; DB 접근 X -&amp;gt; 락 실패
        }

        @Transactional
        public void failLockAfterDbAccess(AtomicInteger activeRecorder, Long id) {
			// Case B: 트랜잭션 진입 -&amp;gt; findById -&amp;gt; 락 실패
        }
    }
}&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;Redis&amp;nbsp;호출&amp;nbsp;대신&amp;nbsp;SimulatedLockFailure&amp;nbsp;로&amp;nbsp;lock&amp;nbsp;실패를&amp;nbsp;시뮬레이션,&amp;nbsp;HikariPoolMXBean.getActiveConnections()&amp;nbsp;로&amp;nbsp;커넥션&amp;nbsp;수&amp;nbsp;측정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트러블슈팅: &lt;a href=&quot;https://kjw81024.tistory.com/102&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kjw81024.tistory.com/102&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 34px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;케이스&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;락 실패 지점&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;커넥션 상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;A : DB 접근 전&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;try Lock 실패&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;미점유 - 낭비 0&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;B : DB 접근 후&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;try Lock 실패&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;점유중 (roll back 까지 낭비)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 3&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인&amp;nbsp;목적:&amp;nbsp;분산&amp;nbsp;락이&amp;nbsp;뚫렸을&amp;nbsp;때&amp;nbsp;@Version&amp;nbsp;이&amp;nbsp;단독으로&amp;nbsp;정합성을&amp;nbsp;보호하는가? &lt;br /&gt;&lt;br /&gt;시나리오&amp;nbsp;&amp;mdash;&amp;nbsp;락&amp;nbsp;없이&amp;nbsp;두&amp;nbsp;트랜잭션이&amp;nbsp;동시에&amp;nbsp;같은&amp;nbsp;reservation&amp;nbsp;을&amp;nbsp;confirm&lt;/p&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 style=&quot;width: 100%;&quot;&gt;A:&amp;nbsp;tx&amp;nbsp;시작&amp;nbsp;&amp;rarr;&amp;nbsp;read&amp;nbsp;(v=0)&amp;nbsp;&amp;rarr;&amp;nbsp;[barrier,&amp;nbsp;둘&amp;nbsp;다&amp;nbsp;read&amp;nbsp;완료&amp;nbsp;동기화]&amp;nbsp;&amp;rarr;&amp;nbsp;confirm&amp;nbsp;&amp;rarr;&amp;nbsp;commit&amp;nbsp;(v=1)&lt;br /&gt;B:&amp;nbsp;tx&amp;nbsp;시작&amp;nbsp;&amp;rarr;&amp;nbsp;read&amp;nbsp;(v=0)&amp;nbsp;&amp;rarr;&amp;nbsp;[barrier]&amp;nbsp;&amp;rarr;&amp;nbsp;A&amp;nbsp;commit&amp;nbsp;대기&amp;nbsp;&amp;rarr;&amp;nbsp;confirm&amp;nbsp;&amp;rarr;&amp;nbsp;commit&amp;nbsp;시도&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;UPDATE&amp;nbsp;...&amp;nbsp;WHERE&amp;nbsp;version=0&amp;nbsp;&amp;rarr;&amp;nbsp;0&amp;nbsp;rows&amp;nbsp;&amp;rarr;&amp;nbsp;StaleObjectStateException&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;rarr;&amp;nbsp;Spring&amp;nbsp;이&amp;nbsp;ObjectOptimisticLockingFailureException&amp;nbsp;으로&amp;nbsp;래핑&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;CyclicBarrier 로 두 스레드가 같은 v=0 을 읽은 시점을 동기화, CountDownLatch 로 A commit 이후에만 B 가 write 하도록 순서 고정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OptimisticLockFallbackTest.java&lt;/p&gt;
&lt;pre id=&quot;code_1780585627844&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@ActiveProfiles(&quot;test&quot;)
class OptimisticLockFallbackTest {

    @BeforeEach
    void setup() {
		// rservation, reservationId 초기화 
    }

    @AfterEach
    void cleanup() {
		// repository 클린
	}

    @Test
    @DisplayName(&quot;[FALLBACK] 동시에 confirm 시도 &amp;mdash; @Version 이 늦은 쪽을 OptimisticLock 으로 막는다&quot;)
    void optimistic_lock_blocks_second_writer() throws Exception {
        // given
        CyclicBarrier bothRead = new CyclicBarrier(2); // 두 스레드가 read 까지 완료한 시점에 동기화
        CountDownLatch aCommitted = new CountDownLatch(1); // A 의 commit 후 B 가 깨어남
        AtomicReference&amp;lt;Throwable&amp;gt; aError = new AtomicReference&amp;lt;&amp;gt;();
        AtomicReference&amp;lt;Throwable&amp;gt; bError = new AtomicReference&amp;lt;&amp;gt;();

        // when
        Thread tA = new Thread(() -&amp;gt; {
            try {
                probe.readBarrierConfirm(reservationId, bothRead);
            } catch (Throwable t) {
                aError.set(t);
            } finally {
                aCommitted.countDown(); // A 의 @Transactional 종료(commit/rollback) 이후에 시그널
            }
        });

        Thread tB = new Thread(() -&amp;gt; {
            try {
                probe.readBarrierWaitConfirm(reservationId, bothRead, aCommitted);
            } catch (Throwable t) {
                bError.set(t);
            }
        });

        tA.start();
        tB.start();
        tA.join();
        tB.join();

        // then
        // (1) A 는 성공
        assertThat(aError.get())
                .as(&quot;먼저 commit 한 A 는 정상 종료&quot;)
                .isNull();

        // (2) B 는 OptimisticLockException 으로 막힘
        assertThat(bError.get())
                .as(&quot;늦게 commit 시도한 B 는 version 불일치로 막힘&quot;)
                .isInstanceOf(ObjectOptimisticLockingFailureException.class);

        // (3) 최종 상태는 A 가 쓴 것 그대로, version 도 정확히 +1
        Reservation finalState = reservationRepository.findById(reservationId).orElseThrow();
        assertThat(finalState.getStatus()).isEqualTo(ReservationStatus.CONFIRMED);
        assertThat(finalState.getVersion()).isEqualTo(1L);
    }

    // ─────────────────────────────────────────────────────────────────
    // 테스트 전용 빈
    // ─────────────────────────────────────────────────────────────────

    @TestConfiguration
    static class Config {
        @Bean
        OptimisticLockProbe optimisticLockProbe(ReservationRepository repo) {
            return new OptimisticLockProbe(repo);
        }
    }

    static class OptimisticLockProbe {

        @Transactional
        public void readBarrierConfirm(Long id, CyclicBarrier bothRead) throws Exception {
        	// read &amp;rarr; 둘 다 read 한 시점 동기화 &amp;rarr; confirm &amp;rarr; (메서드 종료 시 commit)
        }

        @Transactional
        public void readBarrierWaitConfirm(Long id, CyclicBarrier bothRead,
                                          CountDownLatch aCommitted) throws Exception {
              // B: read &amp;rarr; 동기화 &amp;rarr; A 의 commit 이 끝날 때까지 대기 &amp;rarr; confirm &amp;rarr; (메서드 종료 시 commit 실패)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 &lt;br /&gt;&lt;br /&gt;A:&amp;nbsp;정상&amp;nbsp;commit,&amp;nbsp;status&amp;nbsp;=&amp;nbsp;CONFIRMED,&amp;nbsp;version&amp;nbsp;=&amp;nbsp;1 &lt;br /&gt;B:&amp;nbsp;ObjectOptimisticLockingFailureException&amp;nbsp;으로&amp;nbsp;차단 &lt;br /&gt;최종&amp;nbsp;DB:&amp;nbsp;A&amp;nbsp;의&amp;nbsp;write&amp;nbsp;만&amp;nbsp;반영,&amp;nbsp;version&amp;nbsp;=&amp;nbsp;1&amp;nbsp;정확히&amp;nbsp;일치&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 - 락을 트랜잭션 바깥으로 &lt;br /&gt;운영&amp;nbsp;코드에서&amp;nbsp;경계를&amp;nbsp;뒤집는&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(a)&amp;nbsp;Service&amp;nbsp;분리&lt;/p&gt;
&lt;pre id=&quot;code_1780584305029&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ReservationLockService.executeWithReservationLock(id, () -&amp;gt; {
    reservationExpireService.expireOne(id);  // @Transactional &amp;rarr; 이 안에서 commit 까지
});
// executeWithReservationLock 의 finally 에서 락 해제
// 즉 락 해제 시점 = commit 이후&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;(b)&amp;nbsp;AOP&amp;nbsp;&amp;mdash;&amp;nbsp;@DistributedLock&amp;nbsp;+&amp;nbsp;@Order(HIGHEST_PRECEDENCE)&lt;/p&gt;
&lt;pre id=&quot;code_1780584323149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DistributedLock(key = &quot;'reservation:' + #reservationId&quot;)
@Transactional
public void someMethod(Long reservationId) { ... }&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;@Order(HIGHEST_PRECEDENCE)&amp;nbsp;덕분에&amp;nbsp;락&amp;nbsp;Advice&amp;nbsp;가&amp;nbsp;트랜잭션&amp;nbsp;Advice&amp;nbsp;보다&amp;nbsp;바깥에서&amp;nbsp;감싼다.&amp;nbsp;두&amp;nbsp;방법의&amp;nbsp;효과는&amp;nbsp;같음&lt;br /&gt;&lt;br /&gt;추가 - 락이 뚫려도 @Version 이 마지막 방어선 &lt;br /&gt;분산&amp;nbsp;락은&amp;nbsp;강력하지만&amp;nbsp;절대적이지&amp;nbsp;않다.&amp;nbsp;TTL&amp;nbsp;만료,&amp;nbsp;락&amp;nbsp;키&amp;nbsp;누수,&amp;nbsp;신규&amp;nbsp;코드의&amp;nbsp;락&amp;nbsp;우회&amp;nbsp;등으로&amp;nbsp;두&amp;nbsp;트랜잭션이&amp;nbsp;동시에&amp;nbsp;진입할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;그래서&amp;nbsp;Reservation&amp;nbsp;엔티티에&amp;nbsp;@Version&amp;nbsp;을&amp;nbsp;추가했다.&lt;/p&gt;
&lt;pre id=&quot;code_1780584441175&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Version
private Long version;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Hibernate 는 UPDATE 시 WHERE id=? AND version=? 를 붙인다. 먼저 commit 한 쪽이 version 을 올리면, 나중 쪽의 UPDATE 는 0 rows 가 되어 ObjectOptimisticLockingFailureException 으로 막힌다. &lt;br /&gt;&lt;br /&gt;정리 - 2중 방어선&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 73.3716%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.5659%;&quot;&gt;방어선&lt;/td&gt;
&lt;td style=&quot;width: 25.6588%;&quot;&gt;수단&lt;/td&gt;
&lt;td style=&quot;width: 34.1473%;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.5659%;&quot;&gt;1차&lt;/td&gt;
&lt;td style=&quot;width: 25.6588%;&quot;&gt;분산 락 (락이 트랜잭션 바깥)&lt;/td&gt;
&lt;td style=&quot;width: 34.1473%;&quot;&gt;대부분의 경합을 직렬화. race 자체를 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.5659%;&quot;&gt;2차&lt;/td&gt;
&lt;td style=&quot;width: 25.6588%;&quot;&gt;@Version 낙관락&lt;/td&gt;
&lt;td style=&quot;width: 34.1473%;&quot;&gt;락이 어떻게든 뚫렸을 때 정합성 보호&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;락을&amp;nbsp;트랜잭션&amp;nbsp;안에&amp;nbsp;두면&amp;nbsp;1차&amp;nbsp;방어선이&amp;nbsp;제&amp;nbsp;역할을&amp;nbsp;못&amp;nbsp;하고,&amp;nbsp;결국&amp;nbsp;2차&amp;nbsp;방어선까지&amp;nbsp;race&amp;nbsp;가&amp;nbsp;도달한다.&amp;nbsp;2차&amp;nbsp;방어선은&amp;nbsp;&quot;막아주기는&amp;nbsp;하지만&amp;nbsp;A&amp;nbsp;의&amp;nbsp;작업이&amp;nbsp;손실된다&quot;&amp;nbsp;는&amp;nbsp;부작용이&amp;nbsp;있다.&amp;nbsp;1차에서&amp;nbsp;제대로&amp;nbsp;막혀야&amp;nbsp;2차가&amp;nbsp;보조&amp;nbsp;역할만&amp;nbsp;하게&amp;nbsp;된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;정리&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;진행한 테스트는 다음과 같다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 1 : LockTransactionRaceCondition (정합성 관점 - 후속 비즈니스 로직 복구 누락 위험)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 2 : OptimisticLockFallback (정합성 관점 - 2차 방어선)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;테스트 3 : TransactionConnectionWaste (자원 관점 - 커넥션 낭비 + Hikari autoCommit 함정)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table id=&quot;00579f0d-a885-48c4-94bb-10d79efce908&quot; 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 style=&quot;width: 9.53488%;&quot;&gt;테스트&lt;/td&gt;
&lt;td style=&quot;width: 42.907%;&quot;&gt;질문&lt;/td&gt;
&lt;td style=&quot;width: 47.4419%;&quot;&gt;결론&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;79d5c193-4817-467d-a198-4952d1d729da&quot;&gt;
&lt;td id=&quot;Oyz@&quot; style=&quot;width: 9.53488%;&quot;&gt;1 [anti]&lt;/td&gt;
&lt;td id=&quot;&amp;lt;k^L&quot; style=&quot;width: 42.907%;&quot;&gt;락이 안쪽이면 실제로 race 가 생기는가?&lt;/td&gt;
&lt;td id=&quot;qWdk&quot; style=&quot;width: 47.4419%;&quot;&gt;stale read &amp;rarr; @Version 까지 닿음 &amp;rarr; expire 손실&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;fd6485f0-0165-4b6f-86b9-626a5968390c&quot;&gt;
&lt;td id=&quot;Oyz@&quot; style=&quot;width: 9.53488%;&quot;&gt;1 [fixed]&lt;/td&gt;
&lt;td id=&quot;&amp;lt;k^L&quot; style=&quot;width: 42.907%;&quot;&gt;락을 바깥으로 빼면 race 가 차단되는가?&lt;/td&gt;
&lt;td id=&quot;qWdk&quot; style=&quot;width: 47.4419%;&quot;&gt;B 가 락을 잡는 순간 이미 commit 완료 &amp;rarr; race 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;f695c208-6f1c-472f-a7ec-edeb009fec25&quot;&gt;
&lt;td id=&quot;Oyz@&quot; style=&quot;width: 9.53488%;&quot;&gt;2&lt;/td&gt;
&lt;td id=&quot;&amp;lt;k^L&quot; style=&quot;width: 42.907%;&quot;&gt;락이 뚫려도 @Version 이 혼자 막을 수 있는가?&lt;/td&gt;
&lt;td id=&quot;qWdk&quot; style=&quot;width: 47.4419%;&quot;&gt;늦은 commit 을 OptimisticLockException 으로 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;1fd98c32-7f0d-436b-86dc-42b2b89ddf5b&quot;&gt;
&lt;td id=&quot;Oyz@&quot; style=&quot;width: 9.53488%;&quot;&gt;3 [A]&lt;/td&gt;
&lt;td id=&quot;&amp;lt;k^L&quot; style=&quot;width: 42.907%;&quot;&gt;DB 접근 전 락 실패는 커넥션을 낭비하는가?&lt;/td&gt;
&lt;td id=&quot;qWdk&quot; style=&quot;width: 47.4419%;&quot;&gt;낭비 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr id=&quot;9de5db9c-b27b-4d1d-950e-e388602ed817&quot;&gt;
&lt;td id=&quot;Oyz@&quot; style=&quot;width: 9.53488%;&quot;&gt;3 [B]&lt;/td&gt;
&lt;td id=&quot;&amp;lt;k^L&quot; style=&quot;width: 42.907%;&quot;&gt;DB 접근 후 락 실패는 커넥션을 낭비하는가?&lt;/td&gt;
&lt;td id=&quot;qWdk&quot; style=&quot;width: 47.4419%;&quot;&gt;점유 + rollback 비용 낭비&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;정합성 관점과 자원관점 전체에서 봤을 때 락은 반드시 트랜잭션 바깥에 있어야 함&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;구현방법 1: ReservationLockService 분리, 2: @DistributedLock + @Order&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래도 뚫리는 극단적인 케이스는 @Version 이 마지막 방어선으로 막아준다&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 먼저 고쳐놓고 문제를 증명하는 순서가 됐는데 (튜터님이 말해서 그제서야 테스트해봄...) ..,, 오히려 덕분에 왜 이 구조가 맞는지에 대해서 더 정확하게 설명할 수 있게 됨&amp;nbsp;&lt;/p&gt;</description>
      <author>kjw81024</author>
      <guid isPermaLink="true">https://kjw81024.tistory.com/103</guid>
      <comments>https://kjw81024.tistory.com/103#entry103comment</comments>
      <pubDate>Fri, 5 Jun 2026 00:09:01 +0900</pubDate>
    </item>
  </channel>
</rss>