<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>내가 올리고 싶은대로 올리는 개발 블로그</title>
    <link>https://namamim.tistory.com/</link>
    <description>데브옵스 개발자를 꿈꾸는 유사 주니어 개발자</description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 15:30:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>나맘임</managingEditor>
    <image>
      <title>내가 올리고 싶은대로 올리는 개발 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/6326209/attach/ca6bab38b49142c8b20332ca4c253f3e</url>
      <link>https://namamim.tistory.com</link>
    </image>
    <item>
      <title>[네트워크]로드밸런서의 개념과 실제 구현까지</title>
      <link>https://namamim.tistory.com/122</link>
      <description>&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;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로드밸런서(Load Balancer)?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TUXn9/dJMb99UqtBn/vG7rOx5g96AkcOxtoP3WM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TUXn9/dJMb99UqtBn/vG7rOx5g96AkcOxtoP3WM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TUXn9/dJMb99UqtBn/vG7rOx5g96AkcOxtoP3WM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTUXn9%2FdJMb99UqtBn%2FvG7rOx5g96AkcOxtoP3WM0%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;945&quot; height=&quot;300&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서(Load Balancer)는 클라이언트로부터 들어오는 네트워크 트래픽을 여러 백엔드 서버로 분산시키는 장치 또는 소프트웨어다. 교통경찰처럼 수많은 요청(차량)을 각 서버(도로)로 적절히 흘려보내, 특정 서버에 트래픽이 몰려 죽는 일을 방지한다. 현대 서비스에서 &lt;b&gt;고가용성(HA), 수평&lt;/b&gt;&lt;b&gt; 확장(Scale-out), 무중단 배포&lt;/b&gt;는 로드밸런서 없이는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 이점은 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;가용성(Availability)&lt;/b&gt;: 특정 서버 장애 시 나머지 서버로 즉시 트래픽 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성(Scalability)&lt;/b&gt;: 서버를 추가하는 것만으로 처리 용량 증가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안(Security)&lt;/b&gt;: DDoS 방어, SSL 터미네이션 오프로드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능(Performance)&lt;/b&gt;: 응답 시간 개선 및 사용자 경험 향상&lt;/li&gt;
&lt;/ul&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;pre id=&quot;code_1782605136681&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[Client]
   │
   ▼
[Frontend IP / VIP]  &amp;larr; 클라이언트는 이 주소만 알고 있음
   │
   ▼
[Load Balancer]  &amp;larr; 알고리즘으로 서버 선택
   │       │       │
   ▼       ▼       ▼
[Server1][Server2][Server3]  &amp;larr; Backend Pool
   │
   ▼
[Health Check]  &amp;larr; 죽은 서버는 풀에서 자동 제거&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;로드밸런서는 프론트엔드 구성, 백엔드 구성으로 구분한다.&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;프론트엔드는 &lt;b&gt;클라이언트가 실제로 접근하는 진입점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 뒤에 서버가 몇 대인지 전혀 모르고, 오직 이 IP 하나로만 통신한다.&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;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;VIP (Virtual IP)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;클라이언트가 접속하는 공인 또는 사설 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Frontend Port&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;수신할 프로토콜/포트 정의(ex. TCP 80, TCP 443)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;SSL Termination&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;HTTPS를 LB에서 끊고 백엔드엔 HTTP로 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Inbound NAT Rule&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;특정 프론드엔드 포트 -&amp;gt; 특정 백엔드 서버 직접 매핑 (ex. SSH 관리용)&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;당연하게도 백엔드 서버의 공인 IP는 제거해야만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부망에 존재해야 한다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공인 IP가 노출되어 있으면 로드밸런서를 우회할 수 있기 때문에 로드밸런서의 의미가 없어진다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;백엔드 구성&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;구성 요소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Backend Pool&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;Health Check&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;Load Balancing Rule&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;Session Persistence(Sticky Session)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;동일 클라이언트를 동일 서버로 고정(IP Hash 또는 쿠키 기반)&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;내부의 서버를 연결하기 위한 구성들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 인스턴스들이 여러 개를 있고 장애는 언제든지 생길 수 있기 때문에 헬스 체크와 로드 밸런싱 룰은 필연적 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션 유지는 구현하고자 하는 기능에 따라 사용 여부가 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 사용자가 해당 서버 인스턴스에 처리하고 있는 작업 또는 처리했던 메모리에 접근할 필요가 있으면 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;로드밸런싱 알고리즘&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 311px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;알고리즘&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;동작 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;적합한 상황&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Round Robin&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;순서대로 순환&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;구현 단순&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;서버 성능 차이 무시&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;동일 사양 서버, &lt;br /&gt;짧은 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Weighted Round Robin&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;가중치 비율만큼 분배&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;이기종 서버 대응&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;실시간 부하 미반영&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;서버 스펙이 다른 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Least Connections&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;현재 연결 수 최소 서버 선택&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;실시간 부하 반영&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;연결 수 추적 &lt;br /&gt;오버헤드&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;처리 시간이 &lt;br /&gt;가변적인 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Weighted Least Connections&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;연결 수 &amp;divide; 가중치가 최소인 &lt;br /&gt;서버&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;성능+부하 모두 고려&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;구현 복잡&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;이기종 서버 + 가변 요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;IP Hash&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;클라이언트 IP 해시 &lt;br /&gt;&amp;rarr; 고정 서버&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;세션 지속성 보장&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;트래픽 편중 가능&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;장바구니, &lt;br /&gt;로그인 세션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Least Response Time&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;응답시간+연결수 복합 &lt;br /&gt;최소 서버&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;최고의 UX&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;지속 모니터링 오버헤드&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;금융, 실시간 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 43px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;&lt;b&gt;Least Bandwidth&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://jinn-blog.tistory.com/176&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;대역폭 사용량이 최소인 &lt;br /&gt;서버 선택&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;네트워크 리소스 최적화&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;순간 변동에 민감&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 43px;&quot;&gt;CDN, &lt;br /&gt;비디오 스트리밍&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;자주 쓰는 툴 &amp;amp; 서비스&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소프트웨어 로드밸런서&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 101px;&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;height: 17px;&quot;&gt;&lt;b&gt;툴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;주요 사용처&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;Nginx&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://losskatsu.github.io/os-kernel/loadbalancer/&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;HTTP/TCP 프록시, 정적 파일 서빙 겸용, 설정 간단&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;웹 서비스 표준&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;HAProxy&lt;/b&gt;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;고성능 L4/L7, 상세한 통계 대시보드, 금융권 선호&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;고트래픽, 금융&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;Traefik&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;Kubernetes/Docker 네이티브, 자동 서비스 디스커버리&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;컨테이너 환경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;Envoy&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;Istio의 기반, L7 관찰성(Tracing/Metrics) 탁월&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&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;size18&quot;&gt;Nginx 설정 예시&lt;/p&gt;
&lt;pre id=&quot;code_1782653292687&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http {
  upstream backend_servers {
    # 기본: 라운드 로빈
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com;

    # 가중치 라운드 로빈
    # server backend1.example.com weight=3;

    # 최소 연결
    # least_conn;

    # IP 해시 (세션 고정)
    # ip_hash;
  }

  server {
    listen 80;
    location / {
      proxy_pass http://backend_servers;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;클라우드 관리형 LB&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 94px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;서비스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;AWS ELB/ALB/NLB&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://blog.whitemouse.dev/entry/%EB%A1%9C%EB%93%9C-%EB%B0%B8%EB%9F%B0%EC%8B%B1-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;L7(ALB), L4(NLB), 자동 스케일링 연동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;Azure Load Balancer&lt;/b&gt;&amp;nbsp;&lt;span data-pplx-citation-url=&quot;https://bokchi-cloud.tistory.com/13&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;프론트엔드 IP + 백엔드 풀 + 상태 프로브 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;GCP Cloud Load Balancing&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;글로벌 분산, Anycast IP 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;&lt;b&gt;Kubernetes Service (LoadBalancer type)&lt;/b&gt;&lt;span data-pplx-citation-url=&quot;https://kubernetes.io/ko/docs/tasks/access-application-cluster/connecting-frontend-backend/&quot; data-pplx-citation=&quot;&quot;&gt;&lt;span data-state=&quot;closed&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 21px;&quot;&gt;클라우드 LB와 자동 연동, 외부 IP 자동 할당&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;size18&quot;&gt;하드웨어 로드밸런서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고성능과 안정성이 최우선인 환경에서는 &lt;b&gt;F5 Networks, Citrix ADC, A10 Networks&lt;/b&gt; 같은 전용 어플라이언스를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 비용이 매우 높고 확장성에 제약이 있어 최근엔 클라우드 네이티브 환경에서 점점 대체되는 추세다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;출처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.cloudv.kr/others/lb.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.cloudv.kr/others/lb.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>컴퓨터 지식/네트워크</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/122</guid>
      <comments>https://namamim.tistory.com/122#entry122comment</comments>
      <pubDate>Sun, 28 Jun 2026 09:24:07 +0900</pubDate>
    </item>
    <item>
      <title>[K8S]클러스터 아키텍처</title>
      <link>https://namamim.tistory.com/121</link>
      <description>&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;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨트롤 플레인과 워커 노드&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;805&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFr9l/dJMcadoQHNF/zXsQkEk3v33pVGznu1TxkK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFr9l/dJMcadoQHNF/zXsQkEk3v33pVGznu1TxkK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFr9l/dJMcadoQHNF/zXsQkEk3v33pVGznu1TxkK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFr9l%2FdJMcadoQHNF%2FzXsQkEk3v33pVGznu1TxkK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;805&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;805&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 클러스터는 컨트롤 플레인과 워커 노드로 구성되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 노드들은 우리가 원하는 애플리케이션의 이미지 기반의 컨테이너화된 애플리케이션을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 클러스터는 파드를 실행하기 위해 최소한 하나의 워커 노드가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 노드는 애플리케이션 워크로드의 구성 요소인 파드를 호스팅한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤 플레인은 클러스터 내의 워커 노드와 파드를 관리한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경에서, 컨트롤 플레인은 보통 여러 대의 컴퓨터에서 실행되며, 클러스터는 일반적으로 여러 개의 노드를 실행하며 장애를 컨트롤하고 고가용성을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;노드와 파드&lt;/b&gt;&lt;br /&gt;노드는 서버(컴퓨터), 파드는 그 위에서 돌아가는 앱 실행 단위다.&lt;br /&gt;보통 노드는 마스터 노드(컨트롤 플레인이 설치된), 워커 노드(실제 애플리케이션인 파드가 돌아가는 데이터 플레인)으로 분리된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨트롤 플레인 컴포넌트&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤 플레인 컴포넌트는 클러스터에 대한 스케줄링, 이벤트 감지&amp;amp;대응과 같은 전역적인 작업을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시에 클러스터 안의 어떤 머신에서도 실행될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 (컨트롤 플레인 컴포넌트가 실행하는) 설정 스크립트를 일반적으로 동일한 머신에서 실행하기 때문에, 이 머신에서는 사용자 컨테이너를 실행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 일반적으론 여러 머신에 걸쳐 컨트롤 플레인을 실행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 여러 머신 환경에서 구축할 수도 있다.&lt;/p&gt;
&lt;h4 id=&quot;kube-apiserver&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;kube-api-server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;쿠버네티스 API를 노출하는 쿠버네티스 컨트롤 플레인의 프론트 엔드이다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;모든 통신은 여기를 걸쳐 간다고 보면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;api-server 또한 한 인스턴스가 아닌 더 많은 인스턴스를 배포해서 확장할 수 있다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 id=&quot;kube-apiserver&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;etcd&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 클러스터 데이터를 담는 쿠버네티스 백엔드 저장소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키-값(Key-Value) 형태의 저장소로 일관성, 고가용성 확보에 필수적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경에선 이 데이터를 백업하는 건 필수이다.&lt;/p&gt;
&lt;h4 id=&quot;kube-apiserver&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;kube-scheduler&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드가 배정되지 않은 새로 생성된 파드를 감지하고, 실행할 노드를 선택하는 컨트롤 플레인 컴포넌트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름대로 작업들의 스케줄링을 하는데, 리소스에 대한 요구사항, 하드웨어, 정책, 어피니티(Affinity) 명세 등을 고려한다고 알려져있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;어피니티(Affinity)&lt;/b&gt;&lt;br /&gt;쿠버네티스에서 파드를 어느 노드에 스케줄링할지 선호도 규칙을 통해 제어하는 기능&lt;br /&gt;파드 쪽에서 노드를 선택하는 방식으로, Taint/Toleration가 노드 쪽에서 파드를 밀어내는 방식과 대조된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;kube-controller-manager&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;kube-controller-manager&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러 프로세스를 실행하는 컨트롤 플레인 컴포넌트.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;논리적으로, 각&lt;span&gt;&amp;nbsp;컨트롤러&lt;/span&gt;는 분리된 프로세스이지만, 복잡성을 낮추기 위해 모두 단일 바이너리로 컴파일되고 단일 프로세스 내에서 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;컨트롤러에는 여러 가지 유형이 있다. 몇 가지 예시는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 컨트롤러(Node Controller): 노드가 다운될 때 이를 감지하고 대응한다.&lt;/li&gt;
&lt;li&gt;잡 컨트롤러(Job Controller): 일회성 작업을 나타내는 잡(Job) 오브젝트를 감시하고, 해당 작업을 수행하기 위한 파드를 생성한다.&lt;/li&gt;
&lt;li&gt;엔드포인트슬라이스 컨트롤러(EndpointSlice controller): 엔드포인트슬라이스 오브젝트를 채워서 파드와 서비스 사이의 연결을 제공한다.&lt;/li&gt;
&lt;li&gt;서비스어카운트 컨트롤러(ServiceAccount controller): 신규 네임스페이스에 기본 서비스어카운트를 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;노드 컴포넌트&lt;/b&gt;&lt;/h3&gt;
&lt;h4 id=&quot;kube-controller-manager&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;kubelet&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터의&amp;nbsp;각&amp;nbsp;노드에서&amp;nbsp;실행되는&amp;nbsp;에이전트.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubelet은 파드에서 컨테이너가 확실하게 동작하도록 관리.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Kubelet은 다양한 메커니즘을 통해 제공된 파드 스펙(PodSpec)의 집합을 받아서 컨테이너가 해당 파드 스펙에 따라 건강하게 동작하는 것을 확실히 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이때, Kubelet은 쿠버네티스를 통해 생성되지 않는 컨테이너는 관리하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 id=&quot;kube-controller-manager&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;kube-proxy&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터의 각 노드에서 실행되는 네트워크 프록시로, 쿠버네티스 서비스 개념의 구현부.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드의 네트워크 규칙을 유지 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네트워크 규칙이 내부 네트워크 세션이나 클러스터 바깥에서 파드로 네트워크 통신을 할 수 있도록 해줌.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 체제에 가용한 패킷 필터링 계층이 있는 경우, 이를 사용함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇지 않으면 kube-proxy는 트래픽 자체를 포워드 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스에 대한 패킷 포워딩을 자체적으로 구현하고, kube-proxy와 동등한 동작을 제공하는 네트워크 플러그인(Flannel 등)을 사용하면 kube-proxy를 사용할 필요 없다.&lt;/p&gt;
&lt;h4 id=&quot;kube-controller-manager&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;컨테이너 런타임&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;컨테이너 런타임은 컨테이너 실행을 담당하는 소프트웨어.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;docker를 과거에 많이 사용했다면 이제는 containered 위주이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;컨트롤 플레인-노드 간 통신&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;565&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjBi2C/dJMcacp27Qn/IvkJbbHt4hE8zMrGWoB4k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjBi2C/dJMcacp27Qn/IvkJbbHt4hE8zMrGWoB4k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjBi2C/dJMcacp27Qn/IvkJbbHt4hE8zMrGWoB4k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjBi2C%2FdJMcacp27Qn%2FIvkJbbHt4hE8zMrGWoB4k1%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;550&quot; height=&quot;565&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;565&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;노드에서 컨트롤 플레인으로의 통신&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 hub-and-spoke API 패턴을 기반으로 노드의 모든 API 사용은 API 서버에서 종료되도록 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 컨트롤 플레인 컴포넌트 중 어느 것도 원격 서비스를 노출하도록 설계되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버는 하나 이상의 클라이언트 인증 형식이 활성화된 보안 HTTPS 포트에서 원격 연결을 수신하도록 구성됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 노드가 유효한 자격 증명과 함께 API 서버에 안전하게 연결할 수 있도록 클러스터에 대한 공개 루트 인증서를 미리 셋팅(프로비전)해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet 클라이언트 인증서를 넣어주면 되는데 kubeadm의 init / join을 쓰면 자동으로 처리하지만 TLS 부트스트랩을 만들거나 수동으로 인증서를 셋팅할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 환경 여건 상 자동으로 처리되지 않는 경우들도 많으므로 수동 셋팅하는 방법을 알아두면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;결과적으로, 노드 및 노드에서 실행되는 파드에서 컨트롤 플레인으로 연결하기 위한 기본 작동 모드는 기본적으로 보호되며 신뢰할 수 없는 네트워크 및/또는 공용 네트워크에서 실행될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;컨트롤 플레인에서 노드로의 통신&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤&amp;nbsp;플레인(API&amp;nbsp;서버)에서&amp;nbsp;노드로는&amp;nbsp;두&amp;nbsp;가지&amp;nbsp;기본&amp;nbsp;통신&amp;nbsp;경로가&amp;nbsp;있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫&amp;nbsp;번째는&amp;nbsp;API&amp;nbsp;서버에서&amp;nbsp;클러스터의&amp;nbsp;각&amp;nbsp;노드에서&amp;nbsp;실행되는&amp;nbsp;kubelet&amp;nbsp;프로세스이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;두 번째는 API 서버의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;프록시&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기능을 통해 API 서버에서 모든 노드, 파드 또는 서비스에 이르는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;api-서버에서-kubelet으로의-통신&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API 서버에서 kubelet으로의 통신&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;API 서버에서 kubelet으로의 연결은 다음의 용도로 사용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파드에 대한 로그를 가져온다.&lt;/li&gt;
&lt;li&gt;실행 중인 파드에 (보통의 경우 kubectl을 통해) 연결한다.&lt;/li&gt;
&lt;li&gt;kubelet의 포트-포워딩 기능을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 연결은 kubelet의 HTTPS 엔드포인트에서 종료된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로, API 서버는 kubelet의 제공 인증서를 확인하지 않는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이는 연결이 중간자 공격(man-in-the-middle)에 시달리게 하며, 신뢰할 수 없는 네트워크 및/또는 공용 네트워크에서 실행하기에&lt;span&gt;&amp;nbsp;&lt;/span&gt;안전하지 않다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것이 가능하지 않은 경우, 신뢰할 수 없는 네트워크 또는 공용 네트워크를 통한 연결을 피하기 위해 필요한 경우, API 서버와 kubelet 간&lt;span&gt; SSH 터널링&lt;/span&gt;을 사용한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로, kubelet API를 보호하려면&lt;span&gt; Kubelet&amp;nbsp;인증&amp;nbsp;및/또는&amp;nbsp;인가&lt;/span&gt;를 활성화해야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p id=&quot;api-서버에서-노드-파드-및-서비스로의-통신&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;API 서버에서 노드, 파드 및 서비스로의 통신&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;API 서버에서 노드, 파드 또는 서비스로의 연결은 기본적으로 일반 HTTP 연결로 연결되므로 인증되거나 암호화되지 않는다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이 연결에서 URL을 노드, 파드 또는 서비스 이름에 접두어&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;https:&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 붙여 보안 HTTPS 연결이 되도록 실행할 수 있지만, HTTPS 엔드포인트가 제공한 인증서의 유효성을 검증하지 않으며 클라이언트 자격 증명도 제공하지 않는다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;그래서 연결이 암호화되는 동안 그 어떤 무결성도 보장되지 않는다. 이러한 연결은 신뢰할 수 없는 네트워크 및/또는 공용 네트워크에서 실행하기에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;현재는 안전하지 않다&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;.&lt;/span&gt; &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SSH 터널&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;쿠버네티스는 SSH 터널을 지원하여 컨트롤 플레인에서 노드로의 통신 경로를 보호한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이 구성에서, API 서버는 클러스터의 각 노드에 SSH 터널을 시작하고 (포트 22에서 수신 대기하는 ssh 서버에 연결) 터널을 통해 kubelet, 노드, 파드 또는 서비스로 향하는 모든 트래픽을 전달한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이 터널은 노드가 실행 중인 네트워크의 외부로 트래픽이 노출되지 않도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 도구 &amp;amp; 환경</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/121</guid>
      <comments>https://namamim.tistory.com/121#entry121comment</comments>
      <pubDate>Sun, 21 Jun 2026 15:28:41 +0900</pubDate>
    </item>
    <item>
      <title>[Spring]heapdump를 이용해서 메모리 누수(Memory Leak) 찾아보기 feat. Eclipse Memory Analyzer</title>
      <link>https://namamim.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 서버에서 메모리 누수가 있는지 확인하는 방법을 공부해서 정리해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;메모리 누수(Memory Leak)??&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;동적으로 할당하여 사용한&lt;span&gt;&amp;nbsp;메모리&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;가 해제될 수 없는 상태가 된 것을&lt;span&gt; 누수라고 표현하곤합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;쓸모 없는 데이터가 계속 쌓이니까 어느 순간 실제 필요한 데이터를 불러와야하지만 꽉차서 여러 오류가 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;그걸 해결하기 위한 대표적인 해결책이 가비지 콜렉터이나 이 방법으로 해결하지 못하는 경우도 많습니다.&lt;/span&gt;&lt;/span&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;heapdump를 사용해서 분석하면 더욱 더 편하게 할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Heapdump가 뭐에요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 실행 중인 특정 시점에서 JVM의 힙 메모리 영역을 스냅샷으로 캡처하여 저장한 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 메모리라는 게 Java나 Kotlin에서 클래스의 인스턴스 생성할 때 객체들이 동적으로 할당되는 공간을 말합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Heapdump에 포함된 정보&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 중인 애플리케이션의 모든 활성 객체의 상세 정보가 담겨있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 객체 인스턴스의 주소, 유형, 클래스 이름, 크기 같은 정보와 함께 다른 객체를 참조하는지 여부도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 메타 정보와 객체 간의 참조 관계도 포함되어 있기 때문에 전체 메모리 구조를 파악할 수 있습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Heapdump를 추출하기 전에 간단한 누수 상황을 만들어봅시다!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 API 접속에 대해 로그를 남긴다고 생각해보겠습니다.&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;LogggingService&lt;/h4&gt;
&lt;pre id=&quot;code_1761112358438&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Service
class LoggingService {
    private val requestLogs = mutableListOf&amp;lt;RequestLog&amp;gt;()

    fun logRequest(request: HttpServletRequest) {
        requestLogs.add(
            RequestLog(
                timestamp = LocalDateTime.now(),
                method = request.method,
                uri = request.requestURI,
                headers = request.headerNames.toList().associateWith {
                    request.getHeader(it)
                }
            )
        )
    }

    @Scheduled(fixedDelay = 1000) // 마다
    fun printStats() {
        println(&quot;Total logged requests: ${requestLogs.size}&quot;)
        // 로그를 지우지 않음!
    }
}

data class RequestLog(
    val timestamp: LocalDateTime,
    val method: String,
    val uri: String,
    val headers: Map&amp;lt;String, String&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;api 요청을 했을 때, 시간, method, uri, 헤더를 남깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1초마다 현재 로그가 얼만큼 쌓여있는지 콘솔에 print 시킵니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;LogggingInterceptor&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Component
class LoggingInterceptor (
    private val loggingService: LoggingService
) : HandlerInterceptor {

    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        repeat (100000){
            loggingService.logRequest(request)
        }
        return true
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빠르게 메모리를 점유시키기 위해서 한 번 요청으로 똑같은 로그 10만개를 만들었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;WebMvcConfig &amp;amp; Application(메인클래스)&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
class WebMvcConfig(
    private val loggingInterceptor: LoggingInterceptor
) : WebMvcConfigurer {

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(loggingInterceptor)
            .addPathPatterns(&quot;/**&quot;) // 모든 경로에 적용
            .excludePathPatterns(&quot;/actuator/**&quot;) // actuator는 제외
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@EnableScheduling
@SpringBootApplication
class Application

fun main(args: Array&amp;lt;String&amp;gt;) {
    runApplication&amp;lt;Application&amp;gt;(*args)
}
&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;TestController&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
class TestController {

    @PostMapping(&quot;/test&quot;)
    fun test(@RequestBody body: String): String {
        return &quot;Received: $body&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;TestController에 api 요청을 보내면 로그 10만개가 쌓입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 몇 번씩 해보셔도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 heapdump를 추출해볼까요?&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Heapdump를 추출하는 방법&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;JVM 옵션을 사용한 자동 생성&lt;/h4&gt;
&lt;pre id=&quot;code_1761108497328&quot; class=&quot;groovy&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump/heapdump.hprof&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-XX:+HeapDumpOnOutOfMemoryError:&lt;/b&gt;&amp;nbsp;메모리&amp;nbsp;부족&amp;nbsp;오류&amp;nbsp;발생&amp;nbsp;시&amp;nbsp;힙&amp;nbsp;덤프를&amp;nbsp;생성합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-XX:HeapDumpPath:&lt;/b&gt;&amp;nbsp;덤프&amp;nbsp;파일이&amp;nbsp;저장될&amp;nbsp;경로와&amp;nbsp;파일명을&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;가장&amp;nbsp;권장되는&amp;nbsp;방법입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션 실행 시 아래 JVM 옵션을 추가하면 OutOfMemoryError가 발생했을 때 자동으로 힙 덤프 파일을 생성해 줍니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;별도의 오버헤드가 거의 없어 운영 환경에서도 필수로 사용하는 옵션입니다.​&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring actuator 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1761107904828&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.properties&lt;/p&gt;
&lt;pre id=&quot;code_1761107942052&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management.endpoints.web.exposure.include=heapdump
management.endpoint.heapdump.access=unrestricted&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;blockquote data-ke-style=&quot;style3&quot;&gt;http://localhost:8080/actuator/heapdump&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0KCyu/dJMb8ZCa3f9/GxL3aV2EKlGlWkn5XJDCq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0KCyu/dJMb8ZCa3f9/GxL3aV2EKlGlWkn5XJDCq0/img.png&quot; data-alt=&quot;그림 1. actuator로 다운받은 heapdump&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0KCyu/dJMb8ZCa3f9/GxL3aV2EKlGlWkn5XJDCq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0KCyu%2FdJMb8ZCa3f9%2FGxL3aV2EKlGlWkn5XJDCq0%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;523&quot; height=&quot;176&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. actuator로 다운받은 heapdump&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;actuator의 heapdump 엔드포인트를 활성화해서 가져올 수 있습니다.&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;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;jcmd, jmap 명령어로 직접 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 리눅스에서 실행 중인 상태에서 직접 힙 덤프를 생성하기 위한 명령어로 사용량이 비정상적으로 높을 때 원인 분석을 위해 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 프로세스 ID 확인&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761111799302&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jps -l&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. jcmd를 이용한 덤프 생성(JDK 8 이상 권장)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761111805296&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jcmd &amp;lt;pid&amp;gt; GC.heap_dump /path/to/dump/heapdump.hprof&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. jmap을 이용한 덤프 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1761111811902&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;jmap -dump:live,format=b,file=/path/to/dump/heapdump.hprof &amp;lt;pid&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;:live 옵션은 이제 가비지 컬렉터의 대상이 되는 객체를 제외하고, 현재 활성화된 객체들만 덤프에 포함시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;추출한 Heapdump 분석 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heapdump 분석을 위한 여러 프로그램이 있지만 jdk 진영에서 무료로 쓸 수 있는 Eclipse MAT를 이용해서 분석해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다운로드 사이트:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.eclipse.org/mat/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.eclipse.org/mat/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761111936892&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Memory Analyzer (MAT) | The Eclipse Foundation&quot; data-og-description=&quot;The Eclipse Foundation provides our global community of individuals and organizations with a mature, scalable, and business-friendly environment for open source &amp;hellip;&quot; data-og-host=&quot;eclipse.dev&quot; data-og-source-url=&quot;https://www.eclipse.org/mat/&quot; data-og-url=&quot;https://eclipse.dev/mat/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.eclipse.org/mat/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.eclipse.org/mat/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Memory Analyzer (MAT) | The Eclipse Foundation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The Eclipse Foundation provides our global community of individuals and organizations with a mature, scalable, and business-friendly environment for open source &amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;eclipse.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MAT를 실행하고 File &amp;gt; Open Heap Dump 를 통해 덤프 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1133&quot; data-origin-height=&quot;209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAKGkh/dJMb87trGll/Sfx5ZcaMGYmqvymmF4WBC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAKGkh/dJMb87trGll/Sfx5ZcaMGYmqvymmF4WBC0/img.png&quot; data-alt=&quot;그림 2. heapdump 파일이 안보인다면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAKGkh/dJMb87trGll/Sfx5ZcaMGYmqvymmF4WBC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAKGkh%2FdJMb87trGll%2FSfx5ZcaMGYmqvymmF4WBC0%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;1133&quot; height=&quot;209&quot; data-origin-width=&quot;1133&quot; data-origin-height=&quot;209&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. heapdump 파일이 안보인다면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경우에 따라 heapdump 파일이 경로에 있는데도 불구하고 안보일 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 땐, All Files(*)를 선택해서 heapdump를 선택해주세요.&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;779&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RNJ5M/dJMb9WrTT5x/8E7MnD7vEUA7CWLlekkxT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RNJ5M/dJMb9WrTT5x/8E7MnD7vEUA7CWLlekkxT0/img.png&quot; data-alt=&quot;그림 3. 첫 화면 설정하는 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RNJ5M/dJMb9WrTT5x/8E7MnD7vEUA7CWLlekkxT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRNJ5M%2FdJMb9WrTT5x%2F8E7MnD7vEUA7CWLlekkxT0%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;779&quot; height=&quot;600&quot; data-origin-width=&quot;779&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. 첫 화면 설정하는 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;heapdump 분석을 위해선 Leak Suspects Report를 눌러 줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Leak Suspects Report 분석&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;897&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIlI8y/dJMb9jtSteg/wS663lKX56DsDEKXpjholk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIlI8y/dJMb9jtSteg/wS663lKX56DsDEKXpjholk/img.png&quot; data-alt=&quot;그림 4. Leak Suspects Report 메인 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIlI8y/dJMb9jtSteg/wS663lKX56DsDEKXpjholk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIlI8y%2FdJMb9jtSteg%2FwS663lKX56DsDEKXpjholk%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;1267&quot; height=&quot;897&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;897&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 4. Leak Suspects Report 메인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 화면에 보이는 건 Leak Suspects Report입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 눈에 바로 들어오는 원그래프가 나오는데 바로 직감적으로 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희가 생성한 로그들인거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하단에 보면 클래스패스를 통해 어떤 객체가 메모리를 사용하고 있는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JnIHJ/dJMb9WMcE9F/lH2d1uP4j2MjY1Nur9ZuvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JnIHJ/dJMb9WMcE9F/lH2d1uP4j2MjY1Nur9ZuvK/img.png&quot; data-alt=&quot;그림 5. 각 문제 상황에 대해 맨 밑을 보면 Details가 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JnIHJ/dJMb9WMcE9F/lH2d1uP4j2MjY1Nur9ZuvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJnIHJ%2FdJMb9WMcE9F%2FlH2d1uP4j2MjY1Nur9ZuvK%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;962&quot; height=&quot;592&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 5. 각 문제 상황에 대해 맨 밑을 보면 Details가 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설명 하단의 Details를 누르면 좀 더 자세히 어떤 상황인지 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UfiXW/dJMb9MiBv0s/jQvGoUuurLMghkqfMokDE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UfiXW/dJMb9MiBv0s/jQvGoUuurLMghkqfMokDE0/img.png&quot; data-alt=&quot;그림 6. 메모리 축적 과정을 보여준다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UfiXW/dJMb9MiBv0s/jQvGoUuurLMghkqfMokDE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUfiXW%2FdJMb9MiBv0s%2FjQvGoUuurLMghkqfMokDE0%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;1380&quot; height=&quot;687&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 6. 메모리 축적 과정을 보여준다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 식으로 메모리가 축적됐는지 볼 수 있는데 여기서 reqeustLogs 메서드를 통해서 쌓이고 있다는 것을 볼 수 있죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mxA6n/dJMb9Lxd1uD/nl1Xm42rm36QArcJq4Ypt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mxA6n/dJMb9Lxd1uD/nl1Xm42rm36QArcJq4Ypt1/img.png&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;971&quot; data-is-animation=&quot;false&quot; style=&quot;width: 21.2145%; margin-right: 10px;&quot; data-widthpercent=&quot;21.46&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mxA6n/dJMb9Lxd1uD/nl1Xm42rm36QArcJq4Ypt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmxA6n%2FdJMb9Lxd1uD%2Fnl1Xm42rm36QArcJq4Ypt1%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;1069&quot; height=&quot;971&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pIsog/dJMb9cuKJss/Ud1vjCqete7GccwSn8VUwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pIsog/dJMb9cuKJss/Ud1vjCqete7GccwSn8VUwk/img.png&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;248&quot; data-is-animation=&quot;false&quot; style=&quot;width: 77.6227%;&quot; data-widthpercent=&quot;78.54&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pIsog/dJMb9cuKJss/Ud1vjCqete7GccwSn8VUwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpIsog%2FdJMb9cuKJss%2FUd1vjCqete7GccwSn8VUwk%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;999&quot; height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그림 7. 도미네이터 트리를 이용한 메모리 축적 과정도 볼 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도미네이터 트리를 통한 메모리 축적 과정을 볼 수도 있습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;Overview - Histogram&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 자세히 알아보기 위해서&amp;nbsp; Leak Suspects Report에서 벗어나 Overview로 가보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yCDe0/dJMb9iuYcoY/hUQuvVif8iQvpMwteZu2T0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yCDe0/dJMb9iuYcoY/hUQuvVif8iQvpMwteZu2T0/img.png&quot; data-alt=&quot;그림 8. 상단에 있는 Overview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yCDe0/dJMb9iuYcoY/hUQuvVif8iQvpMwteZu2T0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyCDe0%2FdJMb9iuYcoY%2FhUQuvVif8iQvpMwteZu2T0%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;659&quot; height=&quot;156&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 8. 상단에 있는 Overview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창의 상단에 보면 현재 열린 창말고도 Overview가 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coNkYL/dJMb9OnbbrZ/fEiiBKg2C9FkprPsUCIHak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coNkYL/dJMb9OnbbrZ/fEiiBKg2C9FkprPsUCIHak/img.png&quot; data-alt=&quot;그림 9. Overview 하단에 있는 Histogram&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coNkYL/dJMb9OnbbrZ/fEiiBKg2C9FkprPsUCIHak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoNkYL%2FdJMb9OnbbrZ%2FfEiiBKg2C9FkprPsUCIHak%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;214&quot; height=&quot;540&quot; data-origin-width=&quot;214&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 9. Overview 하단에 있는 Histogram&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누른 뒤 Histogram이 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눌러봅시다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/opnfP/dJMb9O1M4w1/N1SjHu23UKieKVgZy4nkZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/opnfP/dJMb9O1M4w1/N1SjHu23UKieKVgZy4nkZ1/img.png&quot; data-alt=&quot;그림 10. Histogram 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/opnfP/dJMb9O1M4w1/N1SjHu23UKieKVgZy4nkZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FopnfP%2FdJMb9O1M4w1%2FN1SjHu23UKieKVgZy4nkZ1%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;988&quot; height=&quot;476&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 10. Histogram 화면&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;&lt;b&gt;Object:&lt;/b&gt; 객체 개수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Shallow Heap:&lt;/b&gt; 객체 자체가 차지하는 메모리 크기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Retained Heap:&lt;/b&gt; 그 객체가 사라질 경우, 연쇄적으로 GC(가비지 콜렉트)될 수 있는 모든 객체들의 메모리 크기 합계로 실질적으로 객체가 메모리 점유하고 있는 총량&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dL75mF/dJMb9LDZs9v/CCTRXezng7YpCOF8ZIU0fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dL75mF/dJMb9LDZs9v/CCTRXezng7YpCOF8ZIU0fk/img.png&quot; data-alt=&quot;그림 11. 패키지 별로 그룹화하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dL75mF/dJMb9LDZs9v/CCTRXezng7YpCOF8ZIU0fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdL75mF%2FdJMb9LDZs9v%2FCCTRXezng7YpCOF8ZIU0fk%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;518&quot; height=&quot;179&quot; data-origin-width=&quot;518&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 11. 패키지 별로 그룹화하기&lt;/figcaption&gt;
&lt;/figure&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;920&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b016Ue/dJMb87trGOi/TDHVuxlAieKieIfmqjWwX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b016Ue/dJMb87trGOi/TDHVuxlAieKieIfmqjWwX0/img.png&quot; data-alt=&quot;그림 12. 패키지 별로 그룹화 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b016Ue/dJMb87trGOi/TDHVuxlAieKieIfmqjWwX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb016Ue%2FdJMb87trGOi%2FTDHVuxlAieKieIfmqjWwX0%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;920&quot; height=&quot;544&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 12. 패키지 별로 그룹화 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 RequestLog가 엄청 많은 것을 볼 수 있죠.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Overview -&lt;span&gt; Dominator Tree&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLS3FX/dJMb9Onbbr9/wLIkk4QKzDWqTQLaqmOukk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLS3FX/dJMb9Onbbr9/wLIkk4QKzDWqTQLaqmOukk/img.png&quot; data-alt=&quot;그림 13. Overview 하단에 Dominator Tree가 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLS3FX/dJMb9Onbbr9/wLIkk4QKzDWqTQLaqmOukk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLS3FX%2FdJMb9Onbbr9%2FwLIkk4QKzDWqTQLaqmOukk%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;237&quot; height=&quot;540&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 13. Overview 하단에 Dominator Tree가 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Overview에서 Dominator Tree를 눌러줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqXS42/dJMb9MpmvWH/KEDfmwSHtskwqPZn73oqLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqXS42/dJMb9MpmvWH/KEDfmwSHtskwqPZn73oqLk/img.png&quot; data-alt=&quot;그림 14. Dominator Treee는 Retained Heap 내림차순 정렬이 되어있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqXS42/dJMb9MpmvWH/KEDfmwSHtskwqPZn73oqLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqXS42%2FdJMb9MpmvWH%2FKEDfmwSHtskwqPZn73oqLk%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;1085&quot; height=&quot;286&quot; data-origin-width=&quot;1085&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 14. Dominator Treee는 Retained Heap 내림차순 정렬이 되어있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 LoggingService가 범인임을 알려주고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mMNf6/dJMb9WSYjNi/K8we32yscbdgYVDNhpjny1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mMNf6/dJMb9WSYjNi/K8we32yscbdgYVDNhpjny1/img.png&quot; data-alt=&quot;그림 15. 자세한 내용&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mMNf6/dJMb9WSYjNi/K8we32yscbdgYVDNhpjny1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmMNf6%2FdJMb9WSYjNi%2FK8we32yscbdgYVDNhpjny1%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;1050&quot; height=&quot;383&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 15. 자세한 내용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class RequestLog(
    val timestamp: LocalDateTime,
    val method: String,
    val uri: String,
    val headers: Map&amp;lt;String, String&amp;gt;,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세히보면 RequestLog 객체의 정보가 그대로 있는 것까지 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 연관 관계의 의미가 LoggingService가 사라진다면 밑에 있는 모든 게 다 메모리 할당 해제가 된다는 의미입니다,.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'지배'의 관계를 표시하는 겁니다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;객체 내용 확인하는 방법&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k5Gxb/dJMb9hW8g9Z/HhIlkmbS68KNQs2w0x25O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k5Gxb/dJMb9hW8g9Z/HhIlkmbS68KNQs2w0x25O0/img.png&quot; data-alt=&quot;그림 16. 객체 내용 확인법&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k5Gxb/dJMb9hW8g9Z/HhIlkmbS68KNQs2w0x25O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk5Gxb%2FdJMb9hW8g9Z%2FHhIlkmbS68KNQs2w0x25O0%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;1083&quot; height=&quot;550&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 16. 객체 내용 확인법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoggingService 우클릭 -&amp;gt; List Objects -&amp;gt; 레퍼런스 유형 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;with outgoing references:&lt;/b&gt; 해당 객체가 참조하는 다른 객체들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;with incoming references:&lt;/b&gt; 이 객체를 참조하는 다른 객체들&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWGmCF/dJMb9OtWUbJ/RzkwJ6gS8dUyKfTGc6VOVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWGmCF/dJMb9OtWUbJ/RzkwJ6gS8dUyKfTGc6VOVK/img.png&quot; data-alt=&quot;그림 17. 객체 내용 확인 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWGmCF/dJMb9OtWUbJ/RzkwJ6gS8dUyKfTGc6VOVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWGmCF%2FdJMb9OtWUbJ%2FRzkwJ6gS8dUyKfTGc6VOVK%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;1238&quot; height=&quot;588&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;588&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 17. 객체 내용 확인 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;with outgoing references를 눌렀을 땐,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 어떤 객체를 지금 참조 하고 있는지 모두 볼 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그러면 왜 메모리 해제가 안되고 있는지 알 수 있을까요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paths to GC Roots를 이용하시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uk425/dJMb9Nhvu8V/kxJwDknvu1ujLRcMYqvY30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uk425/dJMb9Nhvu8V/kxJwDknvu1ujLRcMYqvY30/img.png&quot; data-alt=&quot;그림 18. GC Root 추적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uk425/dJMb9Nhvu8V/kxJwDknvu1ujLRcMYqvY30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuk425%2FdJMb9Nhvu8V%2FkxJwDknvu1ujLRcMYqvY30%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;1013&quot; height=&quot;430&quot; data-origin-width=&quot;1013&quot; data-origin-height=&quot;430&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 18. GC Root 추적&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 LoggingService를 우클릭해서 Merge Shortest Paths to GC Roots를 눌러줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 weak,soft phantom 참조등을 제외할 수 있는데 중요한 건 메모리 누수와 직접적인 관련이 있는 강한 참조를 찾기 위해서 'exclude all .. etc. references' 를 선택하시면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KCYra/dJMb9QrLrv0/XXtB5Kl4LHZ8Ae4LMP0Syk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KCYra/dJMb9QrLrv0/XXtB5Kl4LHZ8Ae4LMP0Syk/img.png&quot; data-alt=&quot;그림 19. 결국 메모리 해제가 안되고 있는 건 로그 저장 변수 때문이었다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KCYra/dJMb9QrLrv0/XXtB5Kl4LHZ8Ae4LMP0Syk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKCYra%2FdJMb9QrLrv0%2FXXtB5Kl4LHZ8Ae4LMP0Syk%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;1535&quot; height=&quot;464&quot; data-origin-width=&quot;1535&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 19. 결국 메모리 해제가 안되고 있는 건 로그 저장 변수 때문이었다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beanFactory 안에 있는 LoggingService 객체의 변수 val 때문에 GC가 되고 있지 않다는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>서버 공부/Spring</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/120</guid>
      <comments>https://namamim.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 22 Oct 2025 15:49:35 +0900</pubDate>
    </item>
    <item>
      <title>[Kotlin]코루틴(Kotlin Coroutine)에 대해 알아보자</title>
      <link>https://namamim.tistory.com/119</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;들어가며&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;코틀린 코루틴이 뭔가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기적으로 실행되는 코드를 간소화하기 위한 동시성 설계 패턴의 일종입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 처리는 보통 동시성이나 병렬성을 높이기 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 코드를 활용하는 병렬 처리를 위해 다중 스레드를 사용하기도 하지만, 코루틴은 주로 하나의 스레드에서 여러 작업을 번갈아 처리할 수 있는 동시성을 매우 효율적으로 다루기 위해 설계되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코루틴은 스레드와 달리 시스템 리소스를 거의 차지하지 않습니다. 따라서 수 천개, 수 만 개의 코루틴을 생성해도 전혀 문제가 없는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데도 동시 작업을 효율적으로 처리할 수 있는 거죠&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;어떻게 스레드와 달리 시스템 리소스를 거의 차지 않게 만든 건가요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;코루틴은&lt;span&gt; &lt;/span&gt;&lt;/span&gt;스레드를 점유하지 않고, 일시 중단(Suspending)과 재개(Resumption)를 통해 하나의 스레드를 여러 코루틴이 나눠 쓰기 때문&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;코루틴은 OS가 아니고 코틀린 런타임이 관리하는 작업 객체에 가깝습니다. 별도의 스택을 할당하지 않고, 현재 작업 상태를 저장하는 작은 데이터 구조(Continuation 객체)만 필요로 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;여기서 일시 중단 개념이 나오는데 마치 다중 스레드의 스케줄링처럼 작업 객체를 작업하다가 일시 중단 시키고 다른 작업하고 다시 돌아와서 작업 객체를 다시 재개하는 방식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47); color: oklch(0.3039 0.04 213.68); text-align: start;&quot;&gt;이를 통해 실질적으로 스레드 하나로 다중 작업을 처리할 수 있게 되는 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;스레드의 블로킹이 발생하지 않는 것이죠.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;이 대단한 코루틴이 실행하게 만든 것은 바로 컴파일러&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;코틀린 컴파일러는 suspend 함수를 상태 머신으로 변환한 다음 suspend 함수가 호출되면 컴파일러가 Continuation이라는 콜백 객체를 함수에 몰래 추가합니다.&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&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;이 객체가 담고 있는 정보&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;- 작업이 중단되면 다음에 어디서부터 다시 시작해야 하는가?&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;- 지금까지 계산된 지역 변수들의 상태는 무엇인가?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;delay 같은 함수의 일시 중단 지점을 만나면 코루틴은 이 Continuation 객체에 자신의 상태를 저장하고 실행을 멈춥니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: oklch(0.3039 0.04 213.68);&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;그리고 스케줄러에 의해 다시 실행될 차례가 오면, Continuation 객체를 통해 중단됐던 지점부터 정확히 이어서 작업을 재개합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;코루틴을 시작하는 방법&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;build.gradle.kts&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1760682185105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;코루틴 빌더(Coroutine Builders)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴 시작하는 함수로 launch, async가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;launch:&lt;/b&gt; 결과를 반환하지 않고 작업을 시작합니다. Job 객체를 반환하여 코루틴의 상태를 제어할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;async:&lt;/b&gt; 작업 결과가 필요한 경우 사용합니다. Deferred 객체를 반환하며 await() 함수를 호출하여 작업이 완료될 때까지 기다렸다가 결과를 받을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;디스패처(Dispatchers)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코루틴을 어떤 스레드에서 실행할지 결정합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Dispatchers.Main:&lt;/b&gt; 안드로이드 UI 스레드에 작업을 보내기 위해 설계된 것으로 백엔드 개발엔 IllegalStateException이 발생하며 사용하면 프로그램이 즉시 중단됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dispatchers.IO:&lt;/b&gt; 필요에 따라 스레드를 생성하고 공유하는 유동적인 스레드 풀을 사용하며 이름처럼 I/O 작업에 최적화 되어 있습니다. 보통 외부 API 호출, DB 접근, 메시지 큐 통신 등 대부분의 백엔드 비즈니스 로직 처리에 가장 적합하고 널리 사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dispatchers.Default:&lt;/b&gt; 복잡한 알고리즘 실행 등의 CPU를 많이 사용하는 계산 집약적인 작업에 최적화되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Deferred&amp;lt;T&amp;gt; 인터페이스: 미래 결과에 대한 약속&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바의 Future, JS의 Promise와 비슷한 개념으로 코루틴 빌더가 반환하는 객체의 일종입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async { .. } 코루틴 빌더가 반환하는 객체로 미래의 어느 시점에 계산이 완료되어 T 타입의 결과값을 돌려줄 것이라는 약속입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Defferred는 Job 인터페이스를 상속하기 때문에 Job이 할 수 있는 모든 것(취소, 상태 확인 등)을 할 수 있으면서 추가로 결과값을 담을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1760683537274&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val deferredResult: Deferred&amp;lt;String&amp;gt; = async {
    delay(1000L)
    &quot;Hello World&quot; // 이 블록의 최종 결과가 String 타입의 값으로 약속됨
}
// deferredResult는 &quot;1초 뒤에 String 값을 주겠다&quot;는 약속 어음(Deferred)입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Deferred를 실제 결과로 바꾸는 awailt()과 awaillAll()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deferred 객체에 대해 호출하며 해당 비동기 작업이 완료될 때까지 현재 코루틴을 일시 중단시킵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; await() 동작 방식:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;await()&lt;/span&gt;를 호출하는 시점에&lt;span&gt;,&amp;nbsp;async&amp;nbsp;&lt;/span&gt;작업이 아직 끝나지 않았다면 작업이 끝날 때까지&lt;span&gt;&amp;nbsp;&lt;/span&gt;기다립니다&lt;span&gt; (&lt;/span&gt;일시 중단&lt;span&gt;).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;작업이 완료되면&lt;span&gt;,&amp;nbsp;Deferred&lt;/span&gt;가 가지고 있던 결과값&lt;span&gt; (T&amp;nbsp;&lt;/span&gt;타입&lt;span&gt;)&lt;/span&gt;을 반환합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;만약&lt;span&gt;&amp;nbsp;async&amp;nbsp;&lt;/span&gt;블록 내에서 예외가 발생했다면&lt;span&gt;,&amp;nbsp;await()&lt;/span&gt;를 호출하는 시점에 그 예외가 다시 던져집니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1760683668021&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;println(&quot;작업 시작&quot;)
val deferred: Deferred&amp;lt;Int&amp;gt; = async { delay(1000L); 42 }

// ... 다른 작업을 수행 ...

println(&quot;결과가 필요합니다...&quot;)
val result: Int = deferred.await() // 1초가 다 지날 때까지 여기서 멈춰 기다림
println(&quot;결과: $result&quot;) // 1초 후 &quot;결과: 42&quot; 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;awaitAll() 동작 방식:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;입력받은 모든&lt;span&gt;&amp;nbsp;Deferred&amp;nbsp;&lt;/span&gt;작업들이 완료될 때까지 기다립니다&lt;span&gt;. &lt;/span&gt;전체 대기 시간은 가장 오래 걸리는 작업의 시간에 맞춰집니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;모든 작업이 완료되면&lt;span&gt;, &lt;/span&gt;각&lt;span&gt;&amp;nbsp;Deferred&lt;/span&gt;의 결과들을 모아서&lt;span&gt;&amp;nbsp;List&amp;lt;T&amp;gt;&amp;nbsp;&lt;/span&gt;형태로 반환합니다&lt;span&gt;. &lt;/span&gt;이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;결과 리스트의 순서는 입력으로 준&lt;span&gt;&amp;nbsp;Deferred&amp;nbsp;&lt;/span&gt;객체의 순서와 동일합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1760683702288&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 잘못된 예시: 순차적으로 기다리게 되어 병렬 처리의 이점이 없음
val result1 = deferred1.await() // 여기서 1초 대기
val result2 = deferred2.await() // 여기서 추가로 2초 대기 (총 3초)

// 올바른 예시: 동시에 기다림 (총 2초)
val results: List&amp;lt;Any&amp;gt; = awaitAll(deferred1, deferred2)
val result1 = results[0]
val result2 = results[1]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;간단한 예제 코드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;launch 사용법&lt;/h4&gt;
&lt;pre id=&quot;code_1760683255899&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

fun main() = runBlocking {
    println(&quot;메인 프로그램 시작&quot;)

    val job = launch(Dispatchers.IO) {
        println(&quot;백그라운드 작업 시작... (예: 로그 전송)&quot;)
        delay(1000L) // 실제 작업 시뮬레이션
        println(&quot;백그라운드 작업 완료.&quot;)
    }

    println(&quot;메인 프로그램은 다른 일을 계속합니다.&quot;)
    
    // 이 줄이 없으면 main이 먼저 끝나서 launch 작업이 완료되기 전에 프로그램이 종료될 수 있습니다.
    job.join() 
    
    println(&quot;메인 프로그램 종료&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;async와 await를 이용해서 결과가 필요한 단일 작업 처리&lt;/h4&gt;
&lt;pre id=&quot;code_1760683285108&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

suspend fun fetchUserProfile(): String {
    delay(1000L) // 네트워크 요청 시뮬레이션
    return &quot;{\&quot;name\&quot;: \&quot;홍길동\&quot;, \&quot;age\&quot;: 30}&quot;
}

fun main() = runBlocking {
    println(&quot;사용자 프로필을 가져옵니다...&quot;)

    val deferredProfile = async(Dispatchers.IO) {
        fetchUserProfile()
    }

    println(&quot;프로필을 기다리는 동안 다른 작업을 할 수 있습니다...&quot;)
    // 예를 들어, UI 업데이트나 다른 계산 수행

    val userProfile = deferredProfile.await() // 결과가 올 때까지 여기서 대기합니다.
    println(&quot;받은 데이터: $userProfile&quot;)
    println(&quot;프로필 처리 완료.&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;async와 awaitAll를 이용해서 결과가 필요한 다중 작업 처리&lt;/h4&gt;
&lt;pre id=&quot;code_1760683317956&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

suspend fun fetchUserInfo(): String {
    delay(1000L)
    return &quot;사용자 정보&quot;
}

suspend fun fetchFriendList(): String {
    delay(1500L)
    return &quot;친구 목록&quot;
}

fun main() = runBlocking {
    println(&quot;데이터 동시 요청 시작!&quot;)

    val deferredUser = async { fetchUserInfo() }
    val deferredFriends = async { fetchFriendList() }

    // awaitAll은 Deferred 객체 리스트를 받아 모든 작업이 끝날 때까지 대기합니다.
    // 결과는 List&amp;lt;String&amp;gt; 형태로 반환됩니다.
    val results = awaitAll(deferredUser, deferredFriends)
    
    val userInfo = results[0]
    val friendList = results[1]

    println(&quot;모든 데이터 수신 완료!&quot;)
    println(&quot; - $userInfo&quot;)
    println(&quot; - $friendList&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;코루틴을 예시 문제로 한 번 풀어봅시다.&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;문제 1: 기본 비동기 작업 실행하기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;span&gt;:&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;1&lt;/span&gt;초가 걸리는&lt;span&gt;&amp;nbsp;fetchUserData()&amp;nbsp;&lt;/span&gt;함수를 실행하세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;함수가 실행되는 동안&lt;span&gt; &quot;&lt;/span&gt;사용자 데이터 로딩 중&lt;span&gt;...&quot; &lt;/span&gt;메시지를 먼저 출력하세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;1&lt;/span&gt;초 후&lt;span&gt;,&amp;nbsp;fetchUserData()&lt;/span&gt;가 반환한 결과인&lt;span&gt; &quot;&lt;/span&gt;사용자 프로필&lt;span&gt;&quot;&lt;/span&gt;을 출력하세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힌트&lt;span&gt;:&amp;nbsp;launch&amp;nbsp;&lt;/span&gt;빌더와&lt;span&gt;&amp;nbsp;delay&amp;nbsp;&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;b&gt;기본 코드:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760682947293&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

suspend fun fetchUserData(): String {
    delay(1000L) // 네트워크 요청을 시뮬레이션
    return &quot;사용자 프로필&quot;
}

fun main() = runBlocking {
    // 여기에 코드를 작성하세요.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 정답 코드:&lt;/b&gt;&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;pre id=&quot;code_1760682985322&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*

suspend fun fetchUserData(): String {
    delay(1000L) // 네트워크 요청을 시뮬레이션
    return &quot;사용자 프로필&quot;
}

fun main() = runBlocking {
    val job = launch {
        val userData = fetchUserData()
        println(userData)
    }

    println(&quot;사용자 데이터 로딩 중...&quot;)
    job.join() // launch 코루틴이 끝날 때까지 기다립니다.
    println(&quot;로딩 완료.&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: oklch(0.9902 0.004 106.47);&quot;&gt;문제 2: 여러 API 동시에 호출하기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요구사항&lt;span&gt;:&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 정보&lt;span&gt;(1&lt;/span&gt;초 소요&lt;span&gt;)&lt;/span&gt;와 사용자 게시물 목록&lt;span&gt;(1.5&lt;/span&gt;초 소요&lt;span&gt;)&lt;/span&gt;을 가져오는 두 개의&lt;span&gt; API&lt;/span&gt;가 있다고 가정합니다&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;두&lt;span&gt; API&lt;/span&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;동시에&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출하여 총 실행 시간을 최소화하세요&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;두 가지 정보를 모두 가져온 후&lt;span&gt;, &lt;/span&gt;합쳐서 한 번에 출력하세요&lt;span&gt;. (&lt;/span&gt;예&lt;span&gt;: &quot;&lt;/span&gt;정보&lt;span&gt;: &lt;/span&gt;사용자 정보&lt;span&gt;, &lt;/span&gt;게시물&lt;span&gt;: &lt;/span&gt;최신 게시물 목록&lt;span&gt;&quot;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;총 걸린 시간을 측정하여 출력해 보세요&lt;span&gt;. (&lt;/span&gt;순차적으로 실행했다면 약&lt;span&gt; 2.5&lt;/span&gt;초&lt;span&gt;, &lt;/span&gt;동시 실행했다면 약&lt;span&gt; 1.5&lt;/span&gt;초가 걸릴 것입니다&lt;span&gt;.)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힌트&lt;span&gt;:&amp;nbsp;async&lt;/span&gt;를 사용하여 각 작업을 동시에 시작하고&lt;span&gt;,&amp;nbsp;await()&lt;/span&gt;를 사용하여 결과를 기다리세요&lt;span&gt;. &lt;/span&gt;실행 시간은&lt;span&gt;&amp;nbsp;measureTimeMillis&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;b&gt;기본 코드:&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1760683082651&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetchUserInfo(): String {
    delay(1000L)
    return &quot;사용자 정보&quot;
}

suspend fun fetchUserPosts(): String {
    delay(1500L)
    return &quot;최신 게시물 목록&quot;
}

fun main() = runBlocking {
    // 여기에 코드를 작성하세요.
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 정답 코드:&lt;/b&gt;&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;pre id=&quot;code_1760683173698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetchUserInfo(): String {
    delay(1000L)
    return &quot;사용자 정보&quot;
}

suspend fun fetchUserPosts(): String {
    delay(1500L)
    return &quot;최신 게시물 목록&quot;
}

fun main() = runBlocking {
    val totalTime = measureTimeMillis {
        val userInfoDeferred = async { fetchUserInfo() }
        val userPostsDeferred = async { fetchUserPosts() }

        val userInfo = userInfoDeferred.await()
        val userPosts = userPostsDeferred.await()

        println(&quot;정보: $userInfo, 게시물: $userPosts&quot;)
    }
    println(&quot;총 소요 시간: ${totalTime}ms&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;출처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko&quot;&gt;Kotlin 코루틴으로 앱 성능 향상 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760681362687&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Kotlin 코루틴으로 앱 성능 향상 &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-og-description=&quot;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴 명확하고 &quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko&quot; data-og-url=&quot;https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gRIEH/hyZLCekFUW/m8SsgJ03Mw69othV8MaJqk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot;&gt;&lt;a href=&quot;https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.android.com/kotlin/coroutines/coroutines-adv?hl=ko&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gRIEH/hyZLCekFUW/m8SsgJ03Mw69othV8MaJqk/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Kotlin 코루틴으로 앱 성능 향상 &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 페이지는 Cloud Translation API를 통해 번역되었습니다. Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴 명확하고&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.android.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>서버 공부/Kotlin</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/119</guid>
      <comments>https://namamim.tistory.com/119#entry119comment</comments>
      <pubDate>Fri, 17 Oct 2025 15:48:45 +0900</pubDate>
    </item>
    <item>
      <title>[백준]1039 - 교환 문제 풀이(자바,Java)</title>
      <link>https://namamim.tistory.com/118</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1039&quot;&gt;1039번: 교환&lt;/a&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전 탐색 기법으로 문제를 풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, dfs를 처음 이용했는데 메모리 초과로 계속해서 못풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겨우 답지보고 bfs 접근을 해야 함을 깨달았습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이 코드&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class 백준1039_교환 {
    static int k;
    static String originalNum;
    static int result = Integer.MIN_VALUE;

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

        search();
        if (result == Integer.MIN_VALUE){
            System.out.println(-1);
            return;
        }
        System.out.println(result);
    }

    private static void search() {
        Queue&amp;lt;Point&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
        HashSet&amp;lt;String&amp;gt;[] visited;
        visited = new HashSet[k + 1];
        for (int d = 0; d &amp;lt;= k; d++) {
            visited[d] = new HashSet&amp;lt;&amp;gt;();
        }
        queue.add(new Point(originalNum,0));
        while (!queue.isEmpty()) {
            Point current = queue.poll();

            if (current.depth == k) {
                result = Math.max(result, Integer.parseInt(current.data));
                continue;
            }
            char[] arr = current.data.toCharArray();
            for (int i = 0; i &amp;lt; arr.length; i++) {
                for (int j = i+1; j &amp;lt; arr.length; j++) {
                    if (arr[j] == '0' &amp;amp;&amp;amp; i == 0) continue; // leading zero check

                    // swap
                    char[] copy = current.data.toCharArray();
                    char temp = copy[i];
                    copy[i] = copy[j];
                    copy[j] = temp;

                    String next = new String(copy);
                    if (!visited[current.depth+1].contains(next)) {
                        visited[current.depth+1].add(next);
                        queue.add(new Point(next, current.depth+1));
                    }
                }
            }
        }
    }
    private static class Point{
        String data;
        int depth;

        public Point(String data, int depth) {
            this.data = data;
            this.depth = depth;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 무식한 방법인 bfs를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 dfs가 안되는 이유는 메모리 초과 문제도 있지만, recursion이 많이 발생하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 stack으로 하시거나 더 편한 bfs로 접근하는 것이 풀기 좋습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private static void search() {
    Queue&amp;lt;Point&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
    HashSet&amp;lt;String&amp;gt;[] visited;
    visited = new HashSet[k + 1];
    for (int d = 0; d &amp;lt;= k; d++) {
        visited[d] = new HashSet&amp;lt;&amp;gt;();
    }
    queue.add(new Point(originalNum,0));
    while (!queue.isEmpty()) {
        Point current = queue.poll();

        if (current.depth == k) {
            result = Math.max(result, Integer.parseInt(current.data));
            continue;
        }
        char[] arr = current.data.toCharArray();
        for (int i = 0; i &amp;lt; arr.length; i++) {
            for (int j = i+1; j &amp;lt; arr.length; j++) {
                if (arr[j] == '0' &amp;amp;&amp;amp; i == 0) continue; // leading zero check

                // swap
                char[] copy = current.data.toCharArray();
                char temp = copy[i];
                copy[i] = copy[j];
                copy[j] = temp;

                String next = new String(copy);
                if (!visited[current.depth+1].contains(next)) {
                    visited[current.depth+1].add(next);
                    queue.add(new Point(next, current.depth+1));
                }
            }
        }
    }
}
private static class Point{
    String data;
    int depth;

    public Point(String data, int depth) {
        this.data = data;
        this.depth = depth;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 k를 depth로 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 swap 하게 되면 depth + 1를 해서 층을 늘려가는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 했을 때, visited 처리가 애매해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 각 depth마다 만들어진 숫자인 data는 이전 depth에서 똑같은 숫자이더라도 다르게 취급해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 문제에선 어떤 자리를 swap 했는지가 중요함)&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;visited = new HashSet[k + 1];
for (int d = 0; d &amp;lt;= k; d++) {
    visited[d] = new HashSet&amp;lt;&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;visited를 2차원 배열로 처리해도 좋지만, 조금이라도 메모리를 아끼기 위해 HashSet 배열로 하여 같은 depth 내에서 똑같은 숫자에 도착했는지 안했는지 파악합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (current.depth == k) {
    result = Math.max(result, Integer.parseInt(current.data));
    continue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 depth를 늘려가며 모든 경우의 수를 탐색하다가 k가 되면 result와 비교해서 최대값인지 아닌지를 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfs는 끝나게 됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;if (result == Integer.MIN_VALUE){
    System.out.println(-1);
    return;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 연산이 안되는 경우엔 result가 변하지 않기 때문에 -1를 출력하는 걸 잊지맙시다.&lt;/p&gt;</description>
      <category>알고리즘 문제 풀이/백준</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/118</guid>
      <comments>https://namamim.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 13 Aug 2025 01:17:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring]부하테스트 후 서버 성능 개선해보기(feat. Valkey)</title>
      <link>https://namamim.tistory.com/117</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 현재 크래프톤 정글에서 &quot;넌! 런&quot;이라는 러닝앱을 개발하고 있습니다. 핵심 기능 개발이 끝나고 나서 서버 성능 테스트를 해보고 한계를 확인하고 이를 개선하고자 공부한 내용을 글로 남기고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기 아키텍처&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;286&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pyu5J/btsPysZUSzL/XoOYAv5vzctojKJyjXBD9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pyu5J/btsPysZUSzL/XoOYAv5vzctojKJyjXBD9K/img.png&quot; data-alt=&quot;그림 1. 초기 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pyu5J/btsPysZUSzL/XoOYAv5vzctojKJyjXBD9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPyu5J%2FbtsPysZUSzL%2FXoOYAv5vzctojKJyjXBD9K%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;552&quot; height=&quot;316&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. 초기 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기엔 개발이 급선무였기에 최대한 아키텍처를 잘 짜고 싶다는 사심을 배제해서 구성했습니다. 단일 인스턴스로 구축되었으며 HTTPS SSL/TLS 통신은 nginx가 담당하고 있었습니다. CI/CD는 Github Actions을 이용해서 Develop 브랜치에 코드가 머지가 되면 AWS ECR에 Docker Image를 저장하고 인스턴스에 ssh 접속해서 pull 해서 컨테이너를 올리는 방식으로 진행했습니다. 이 과정 속에서 nginx 또한 관리해야하므로 Docker Compose를 이용했습니다.&lt;/p&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초기 아키텍처 부하 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 프리 티어인 t2.micro를 쓰다가 크레딧이 생겨서 스케일업한 t2.small 인스턴스를 사용했기 때문에 한계가 어디까지인지 테스트부터 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Jmeter &quot;Concurrency Thread Group&quot; 플러그인 중 &amp;ldquo;Stepping Thread Group&amp;rdquo;과 &quot;3 Basic Graphs&quot; 플러그인을 사용해서 단계별로 사용자의 흐름을 만들어서 테스트해보기로 했습니다. 단순히 강제로 부하를 걸기보다는 세부적인 시나리오로 부하를 주는 것이 정확히 어디까지 이 서버가 감당가능한지 알 수 있을 것 같아 진행했습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;테스트 시나리오&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;827&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xBOQC/btsPDo5XWIC/a5JvZAsUMnhBBhdgY1W4K1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xBOQC/btsPDo5XWIC/a5JvZAsUMnhBBhdgY1W4K1/img.png&quot; data-alt=&quot;그림 2. 점진적 부하 테스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xBOQC/btsPDo5XWIC/a5JvZAsUMnhBBhdgY1W4K1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxBOQC%2FbtsPDo5XWIC%2Fa5JvZAsUMnhBBhdgY1W4K1%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;1123&quot; height=&quot;827&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;827&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. 점진적 부하 테스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 10명씩 30초마다 추가 투입(단, 1초에 6명씩 투입해서 단계적으로 증가)&amp;nbsp; &amp;rarr; 투입 후 30초 유지 &lt;b&gt;&amp;rarr;&lt;/b&gt; 300명도달 시 &amp;nbsp;1분 유지 &amp;rarr;&amp;nbsp; 1초마다 5명씩 감소&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트할 엔드포인트는 가장 부하가 클 것으로 예상되는 11km 길이의 경로 GPS 배열 불러오기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 서비스의 핵심 기능으로 DB에 저장된 공공데이터 트랙을 사용자에게 제공합니다. 그 중에선 24km 길이의 경로 GPS 배열이 있고 이는 JVM 메모리와 Network 부하에 큰 영향이 있을 것이라고 판단하여 선택하였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;부하 테스트 결과&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOhmFH/btsPDHYmNyB/21MKtEFtq3R9Vai9tig341/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOhmFH/btsPDHYmNyB/21MKtEFtq3R9Vai9tig341/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOhmFH/btsPDHYmNyB/21MKtEFtq3R9Vai9tig341/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOhmFH%2FbtsPDHYmNyB%2F21MKtEFtq3R9Vai9tig341%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;1117&quot; height=&quot;220&quot; data-origin-width=&quot;1117&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZXHAn/btsPGEMmycb/svHZmKaC5NO0heRJWyLZi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZXHAn/btsPGEMmycb/svHZmKaC5NO0heRJWyLZi1/img.png&quot; data-origin-width=&quot;1115&quot; data-origin-height=&quot;843&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.44%; margin-right: 10px;&quot; data-widthpercent=&quot;50.02&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZXHAn/btsPGEMmycb/svHZmKaC5NO0heRJWyLZi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZXHAn%2FbtsPGEMmycb%2FsvHZmKaC5NO0heRJWyLZi1%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;1115&quot; height=&quot;843&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJCLoO/btsPDW8XHa2/Qve8DexteitQxkdlzskjd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJCLoO/btsPDW8XHa2/Qve8DexteitQxkdlzskjd1/img.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;846&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.3972%;&quot; data-widthpercent=&quot;49.98&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJCLoO/btsPDW8XHa2/Qve8DexteitQxkdlzskjd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJCLoO%2FbtsPDW8XHa2%2FQve8DexteitQxkdlzskjd1%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;1118&quot; height=&quot;846&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;538&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2drsi/btsPPRZCeSo/1X5bmrosi1YlNKZ3jCYgjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2drsi/btsPPRZCeSo/1X5bmrosi1YlNKZ3jCYgjk/img.png&quot; data-alt=&quot;그림 3. 점진적 부하 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2drsi/btsPPRZCeSo/1X5bmrosi1YlNKZ3jCYgjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2drsi%2FbtsPPRZCeSo%2F1X5bmrosi1YlNKZ3jCYgjk%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;432&quot; height=&quot;354&quot; data-origin-width=&quot;657&quot; data-origin-height=&quot;538&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. 점진적 부하 테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 응답 속도는 0.8초, 최대 응답 속도는 7.8초로 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T 타입 인스턴스에서 작동하는 CPUCreditBalance가 급격히 떨어지는 성능 절벽 현상과 CPU 사용량이 약 99.8%까지 올라가는 문제가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대 인원이 300명까지는 에러율이 생기진 않았지만 충분히 성능 저하가 있다고 판단하여 캐시를 도입해서 성능을 올리기로 결정했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 캐시를 사용해야 하는가?(Redis VS Valkey)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지만 하더라도 Redis의 존재만 알고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, AWS ElastiCache를 사용하기 위해 홈페이지가 들어가니 다음과 같은 친구가 반겨주었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;619&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beXaw5/btsPQGb87bY/6fIee3uqWvjNwkbkjGpDtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beXaw5/btsPQGb87bY/6fIee3uqWvjNwkbkjGpDtk/img.png&quot; data-alt=&quot;그림 4. Valkey 홍보 모달&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beXaw5/btsPQGb87bY/6fIee3uqWvjNwkbkjGpDtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeXaw5%2FbtsPQGb87bY%2F6fIee3uqWvjNwkbkjGpDtk%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;994&quot; height=&quot;619&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;619&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 4. Valkey 홍보 모달&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 구미가 땡기는 모달이었는데, 비용 절감과 오픈 소스, 거기다가 기존 Redis와 완벽한 호환을 강조하고 있는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 찾아보았습니다. Valkey가 어떤 친구며, 왜 탄생하게 됐는지를 말입니다.&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;Valkey 유래&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사건의 시작은 2024년 Redis의 라이센스 변경으로부터 도래됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상업적 사용에 제약이 있는 BSL로 변경되면서 여러 글로벌 빅테크 기업과 커뮤니티가 협력해서 만든 완전 오픈소스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 버전 7.2.4 로부터 포크된 것으로 알려져 있습니다.&lt;br /&gt;Valkey 8.0이 발표되고 기존의 버전과 비교하여 읽기/쓰기 처리 성능, 메모리 효율성 등 성능이 향상되고 다양한 기능이 추가되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;그러면 Valkey가 Redis에 비해 얼마나 저렴한가요?&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcj5gy/btsPM2H4deg/jOX7kNAHHnvfSO2EEj6Vn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcj5gy/btsPM2H4deg/jOX7kNAHHnvfSO2EEj6Vn0/img.png&quot; data-origin-width=&quot;1107&quot; data-origin-height=&quot;869&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.2412%; margin-right: 10px;&quot; data-widthpercent=&quot;50.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcj5gy/btsPM2H4deg/jOX7kNAHHnvfSO2EEj6Vn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbcj5gy%2FbtsPM2H4deg%2FjOX7kNAHHnvfSO2EEj6Vn0%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;1107&quot; height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3h64D/btsPO3za73B/hm2uNZRsRyKlYditBWSMK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3h64D/btsPO3za73B/hm2uNZRsRyKlYditBWSMK0/img.png&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;883&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.596%;&quot; data-widthpercent=&quot;49.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3h64D/btsPO3za73B/hm2uNZRsRyKlYditBWSMK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3h64D%2FbtsPO3za73B%2Fhm2uNZRsRyKlYditBWSMK0%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;1088&quot; height=&quot;883&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그림 5. 좌 Redis, 우 Valkey 금액&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS ElastiCache 기준으로 약 25% Valkey가 저렴합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하지 않을 수 없었기에 바로 Valkey를 선택하게 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시를 어디에 도입해야 하는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 어떤 걸 사용할 지 정했으니, 어디에 이 캐시를 도입을 할 지가 문제였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1754141951002&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TrackInfoDto trackInfoDto = TrackInfoDto.convertToResponseDto(track);

public record TrackInfoDto(
        List&amp;lt;CoordinateDto&amp;gt; path,
        int totalDistance,
        String name,
        double rate
) {
    public static TrackInfoDto convertToResponseDto(RunningTrack track) {
        List&amp;lt;CoordinateDto&amp;gt; path = CoordinateConverter.convertLineStringToCoordinates(track.getPath());

        return new TrackInfoDto(
                path,
                track.getTotalDistance(),
                track.getName(),
                track.getRate()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 LineString으로 표현되어있던 긴 트랙의 배열을 다시 원래대로 돌리는 부분이 연산 및 메모리 부하가 클 것으로 예상이 되어 이 메서드를 캐시 처리하기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 모든 트랙을 다 캐시를 하면 캐시 서버 부하가 심해지기 때문에, &lt;b&gt;10km 넘는 트랙에만 캐시를 적용&lt;/b&gt;하도록 했습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TrackInfoService {
    private final TrackRepository trackRepository;

    @Cacheable(value = &quot;track-info&quot;, key = &quot;#trackId&quot;)
    public TrackInfoDto getCacheTrackInfo(Long trackId) {
        RunningTrack track = trackRepository.findById(trackId)
                .orElseThrow(() -&amp;gt; new ApiException(ErrorCode.TRACK_NOT_EXIST));
        return TrackInfoDto.convertToResponseDto(track);
    }

    public TrackInfoDto getTrackInfo(Long trackId){
        RunningTrack track = trackRepository.findById(trackId)
                .orElseThrow(() -&amp;gt; new ApiException(ErrorCode.TRACK_NOT_EXIST));
        return TrackInfoDto.convertToResponseDto(track);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;int trackTotalDistance = trackRepository.findTotalDistanceById(trackId)
        .orElseThrow(() -&amp;gt; new ApiException(ErrorCode.TRACK_NOT_EXIST));
TrackInfoDto trackInfoDto;
if (trackTotalDistance &amp;gt;= TRACK_CACHE_TOTAL_DISTANCE_STANDARD){
    trackInfoDto = trackInfoService.getCacheTrackInfo(trackId);
} else {
    trackInfoDto = trackInfoService.getTrackInfo(trackId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입 결과..?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdyu4B/btsPGq8uzOR/9Q5k69MHh5TBOcMXKDY4vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdyu4B/btsPGq8uzOR/9Q5k69MHh5TBOcMXKDY4vk/img.png&quot; data-alt=&quot;그림 6. 캐시 서버 도입 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdyu4B/btsPGq8uzOR/9Q5k69MHh5TBOcMXKDY4vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdyu4B%2FbtsPGq8uzOR%2F9Q5k69MHh5TBOcMXKDY4vk%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;1121&quot; height=&quot;257&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;257&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 6. 캐시 서버 도입 결과&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;서버에 문제가 있는지부터 로그를 봤는데 아니나다를까 에러가 마중해주네요.&lt;/p&gt;
&lt;pre id=&quot;code_1754203476839&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2025-08-02T18:40:19.270Z ERROR 1 --- [you-run] [o-8080-exec-208] c.r.y.g.e.GlobalExceptionHandler         : 예상치 못한 예외 발생: URI=/api/track, Exception=ServletOutputStream failed to write: java.io.IOException: Broken pipe

org.springframework.web.context.request.async.AsyncRequestNotUsableException: ServletOutputStream failed to write: java.io.IOException: Broken pipe
        at org.springframework.web.context.request.async.StandardServletAsyncWebRequest$LifecycleHttpServletResponse.handleIOException(StandardServletAsyncWebRequest.java:346) ~[spring-web-6.2.8.jar!/:6.2.8]
        ....
Caused by: java.io.IOException: Broken pipe
        at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method) ~[na:na]
        at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:62) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:132) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:97) ~[na:na]
        at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:53) ~[na:na]
        at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:532) ~[na:na]
        at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:125) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.doWrite(NioEndpoint.java:1411) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.tomcat.util.net.SocketWrapperBase.doWrite(SocketWrapperBase.java:732) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.tomcat.util.net.SocketWrapperBase.writeBlocking(SocketWrapperBase.java:572) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.tomcat.util.net.SocketWrapperBase.write(SocketWrapperBase.java:520) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.coyote.http11.Http11OutputBuffer$SocketOutputBuffer.doWrite(Http11OutputBuffer.java:548) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:84) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.coyote.http11.Http11OutputBuffer.doWrite(Http11OutputBuffer.java:193) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.coyote.Response.doWrite(Response.java:628) ~[tomcat-embed-core-10.1.42.jar!/:na]
        at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:330) ~[tomcat-embed-core-10.1.42.jar!/:na]
        ... 166 common frames omitted&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;왜 I/O Broken Pipe가 뜨지 곰곰이 고민해봤습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;143&quot; data-origin-height=&quot;17&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VBtsR/btsPE8HAENG/DsNkrFrjgZ4jnQJS4TUeQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VBtsR/btsPE8HAENG/DsNkrFrjgZ4jnQJS4TUeQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VBtsR/btsPE8HAENG/DsNkrFrjgZ4jnQJS4TUeQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVBtsR%2FbtsPE8HAENG%2FDsNkrFrjgZ4jnQJS4TUeQ1%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;143&quot; height=&quot;17&quot; data-origin-width=&quot;143&quot; data-origin-height=&quot;17&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;724&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qxMDw/btsPEMdSQ2i/X57Q9QZhr8Nxs8lKCyTDkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qxMDw/btsPEMdSQ2i/X57Q9QZhr8Nxs8lKCyTDkK/img.png&quot; data-alt=&quot;그림 7. 정상적으로 동작하는 캐시 서버&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qxMDw/btsPEMdSQ2i/X57Q9QZhr8Nxs8lKCyTDkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqxMDw%2FbtsPEMdSQ2i%2FX57Q9QZhr8Nxs8lKCyTDkK%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;724&quot; height=&quot;274&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 7. 정상적으로 동작하는 캐시 서버&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유력한 범인은 Cache와 관련이 있을 거 같아서 확인해봤는데 캐시에 데이터 저장은 문제가 없었고 캐시 히트도 100%이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 캐시에 저장되어 있는 너무 긴 트랙의 배열이 문제라고 판단했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O Broken Pipe는 타임아웃등의 이유로 연결되지 않은 소켓에 접속해서 발생하는 에러로 너무 큰 배열의 데이터를 차마 보내기도 전에 소켓이 닫혀서 발생하는 현상이었습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;문제 해결이 시급하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 공부했던 &lt;b&gt;Ramer-Douglas-Peucker (RDP) 알고리즘&lt;/b&gt;을 통해서 서버에서 경로를 변환해서 캐시 서버에 저장할 때, 단순화시켜 저장하는 방식으로 데이터의 양을 줄이는 방식으로 접근했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://namamim.tistory.com/114&quot;&gt;[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1754927303868&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)&quot; data-og-description=&quot;들어가며안녕하세요. 현재 크래프톤 정글에서 &amp;quot;넌! 런&amp;quot;이라는 러닝앱을 개발하고 있습니다. GPS를 많이 사용하는 러닝앱 특성상 수많은 좌표들을 최적화하는 문제에 직면하여 고생한 내용을 글&quot; data-og-host=&quot;namamim.tistory.com&quot; data-og-source-url=&quot;https://namamim.tistory.com/114&quot; data-og-url=&quot;https://namamim.tistory.com/114&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/oWAW5/hyZuyQYD4V/ZBEseGSRM3XrcCXhWqlOC1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/tKmLM/hyZuKDT2lr/0Gdejv9t0Cecr6CKbP4cX1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/bnunn7/hyZuDdEJ7n/e55OvyMm7KabE2L4e0rm0K/img.png?width=1175&amp;amp;height=288&amp;amp;face=0_0_1175_288&quot;&gt;&lt;a href=&quot;https://namamim.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://namamim.tistory.com/114&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/oWAW5/hyZuyQYD4V/ZBEseGSRM3XrcCXhWqlOC1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/tKmLM/hyZuKDT2lr/0Gdejv9t0Cecr6CKbP4cX1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/bnunn7/hyZuDdEJ7n/e55OvyMm7KabE2L4e0rm0K/img.png?width=1175&amp;amp;height=288&amp;amp;face=0_0_1175_288');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며안녕하세요. 현재 크래프톤 정글에서 &quot;넌! 런&quot;이라는 러닝앱을 개발하고 있습니다. GPS를 많이 사용하는 러닝앱 특성상 수많은 좌표들을 최적화하는 문제에 직면하여 고생한 내용을 글&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;namamim.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.running.you_run.running.payload.dto;

import com.running.you_run.running.entity.RunningTrack;
import com.running.you_run.running.util.CoordinateConverter;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public record TrackInfoDto(
        List&amp;lt;CoordinateDto&amp;gt; path,
        int totalDistance,
        String name,
        double rate
) {
	...
    public static TrackInfoDto convertToSimplifiedResponseDto(RunningTrack track) {
        double epsilon = 0.0005;
        LineString simplifiedLine = (LineString) DouglasPeuckerSimplifier.simplify(track.getPath(), epsilon);
        List&amp;lt;CoordinateDto&amp;gt; path = CoordinateConverter.convertLineStringToCoordinates(simplifiedLine);

        return new TrackInfoDto(
                path,
                track.getTotalDistance(),
                track.getName(),
                track.getRate()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&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;803&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmaFk/btsPOD12iFg/te0z4NMlhoT7x5KmNTWHX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmaFk/btsPOD12iFg/te0z4NMlhoT7x5KmNTWHX0/img.png&quot; data-alt=&quot;그림 8. 경로 단순화 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmaFk/btsPOD12iFg/te0z4NMlhoT7x5KmNTWHX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtmaFk%2FbtsPOD12iFg%2Fte0z4NMlhoT7x5KmNTWHX0%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;803&quot; height=&quot;204&quot; data-origin-width=&quot;803&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 8. 경로 단순화 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱실론 값은 0.0005로 위도와 경도 기준이기에 아주 작은 값을 사용하였고, 결과를 보시면 실제 경로와 큰 차이가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개선한 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aspdv/btsPGEMrmh3/7UxPtYV3qOQhqcZr6qmkKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aspdv/btsPGEMrmh3/7UxPtYV3qOQhqcZr6qmkKk/img.png&quot; data-alt=&quot;그림 8. 최종 점진적 부하 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aspdv/btsPGEMrmh3/7UxPtYV3qOQhqcZr6qmkKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAspdv%2FbtsPGEMrmh3%2F7UxPtYV3qOQhqcZr6qmkKk%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;1124&quot; height=&quot;239&quot; data-origin-width=&quot;1124&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 8. 최종 점진적 부하 테스트 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #2d2929;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #2d2929;&quot;&gt;최대 응답 시간 3.3초, &lt;/span&gt;&lt;span style=&quot;color: #2d2929;&quot;&gt;평균 응답 시간 0.52초 지연 발생으로 캐시 서버와 RDP 알고리즘 도입 이전보다 최대 응답 시간 약57%, &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #2d2929;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #2d2929;&quot;&gt;평균 응답 시간 약 37.5% 향상시켰습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>서버 공부/Spring</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/117</guid>
      <comments>https://namamim.tistory.com/117#entry117comment</comments>
      <pubDate>Tue, 12 Aug 2025 01:10:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring]Google Static Maps API 적용기(지도 로딩 속도 최적화하기)</title>
      <link>https://namamim.tistory.com/116</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 현재 크래프톤 정글에서 &quot;넌! 런&quot;이라는 러닝앱을 개발하고 있습니다. GPS 경로를 저장하고 이미지로 보여줘야 하는 문제로 Google Static Maps API를 사용하게 되었습니다. 그 과정 속에서 겪었던 문제에 대해 적어봤습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google Static Maps API 사용하기 이전엔..&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://namamim.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://namamim.tistory.com/114&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1753428676931&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)&quot; data-og-description=&quot;들어가며안녕하세요. 현재 크래프톤 정글에서 &amp;quot;넌! 런&amp;quot;이라는 러닝앱을 개발하고 있습니다. GPS를 많이 사용하는 러닝앱 특성상 수많은 좌표들을 최적화하는 문제에 직면하여 고생한 내용을 글&quot; data-og-host=&quot;namamim.tistory.com&quot; data-og-source-url=&quot;https://namamim.tistory.com/114&quot; data-og-url=&quot;https://namamim.tistory.com/114&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/GTA0q/hyZnClzMHd/GPPjiMkWEo9TYiEywy1KO0/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/gc6Ab/hyZq2QyIjb/yK7Ip67MflMEm3j6PlvsJ1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/ZuFea/hyZm7TzxRI/ahu9lkh2dN1SqxI87MAOc1/img.png?width=1175&amp;amp;height=288&amp;amp;face=0_0_1175_288&quot;&gt;&lt;a href=&quot;https://namamim.tistory.com/114&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://namamim.tistory.com/114&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/GTA0q/hyZnClzMHd/GPPjiMkWEo9TYiEywy1KO0/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/gc6Ab/hyZq2QyIjb/yK7Ip67MflMEm3j6PlvsJ1/img.png?width=477&amp;amp;height=903&amp;amp;face=0_0_477_903,https://scrap.kakaocdn.net/dn/ZuFea/hyZm7TzxRI/ahu9lkh2dN1SqxI87MAOc1/img.png?width=1175&amp;amp;height=288&amp;amp;face=0_0_1175_288');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;들어가며안녕하세요. 현재 크래프톤 정글에서 &quot;넌! 런&quot;이라는 러닝앱을 개발하고 있습니다. GPS를 많이 사용하는 러닝앱 특성상 수많은 좌표들을 최적화하는 문제에 직면하여 고생한 내용을 글&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;namamim.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랙의 경로를 리스트 형식으로 보여주는 기능이 있었습니다. 처음엔 직관적으로 서버로부터 모든 트랙의 좌표 배열을 불러와서 맵뷰에 이를 직접 그리는 방식으로 구현했습니다. 당연하게도 트랙의 길이가 엄청 긴 것이 있다면 화면이 그려지는 속도가 급격히 느려졌습니다. 10개 정도되는 트랙에도 불구하고 10초 이상의 로딩이 걸렸습니다. 해결하기 위해 &lt;b&gt;Ramer-Douglas-Peucker (RDP) 알고리즘&lt;/b&gt;을 사용해서 경로 단순화로 임시방편 처리를 하였습니다. 하지만 트랙이 많아지면 똑같은 문제가 발생할 것은 뻔하기에 근본적인 구조를 바꾸기로 했습니다. 그것이 트랙의 이미지를 찍어주는&lt;b&gt; Google Static Maps API&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google Static Maps API가 뭔가요??&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3dkyB/btsPAMRlzK5/1r4mLOXGT5RPtmvgDiWZA1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3dkyB/btsPAMRlzK5/1r4mLOXGT5RPtmvgDiWZA1/img.jpg&quot; data-alt=&quot;그림 1. Static Maps API 이미지 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3dkyB/btsPAMRlzK5/1r4mLOXGT5RPtmvgDiWZA1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3dkyB%2FbtsPAMRlzK5%2F1r4mLOXGT5RPtmvgDiWZA1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;205&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. Static Maps API 이미지 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Static Maps API는 웹페이지에 자바스크립트나 동적 페이지 로딩 없이 간단한 HTTP 요청을 통해 &lt;b&gt;정적인 지도 이미지를 삽입&lt;/b&gt;할 수 있게 해주는 서비스입니다. 사용자는 URL에 원하는 지도의 위치, 크기, 확대 수준 등 다양한 매개변수를 포함하여 요청하면, 구글 서버가 해당 조건에 맞는 지도 이미지를 생성하여 반환합니다. 즉, 지도 경로 렌더링에서 단순 이미지로 변경되기에 클라이언트에서의 부하가 매우 적어집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비용은 얼마나 드나요??&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKmHnI/btsPzHp7lxj/2eXxrlX6RvJNVrTEq9aB3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKmHnI/btsPzHp7lxj/2eXxrlX6RvJNVrTEq9aB3K/img.png&quot; data-alt=&quot;그림 2. 비용 설명 표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKmHnI/btsPzHp7lxj/2eXxrlX6RvJNVrTEq9aB3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKmHnI%2FbtsPzHp7lxj%2F2eXxrlX6RvJNVrTEq9aB3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;578&quot; height=&quot;291&quot; data-origin-width=&quot;578&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. 비용 설명 표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월별로 비용을 측정합니다. Google Static Maps API를 포함한 모든 Google Maps Platform를 사용한 횟수를 총합으로 계산하며 월별 10,000회까지는 무료로 사용할 수 있습니다. 그 이후엔 1,000개마다 비용을 측정합니다. &quot;넌!런&quot; 프로젝트에선 사용하고도 충분할 양이라 Google Static Maps API를 선택하기로 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google Static Maps API 작동 방식&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;URL 매개변수&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;https://maps.googleapis.com/maps/api/staticmap?&lt;i&gt;&lt;b&gt;parameters&lt;/b&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;URL 매개변수를 요청에 넣어주면 그의 반환값으로 지도 이미지가 나타납니다. 이 URL 매개변수는 필수인 것도 있고 선택사항인 것도 있습니다. 이 URL 매개변수를 포함해서 보내기 전에 &lt;b&gt;올바르게 URL 인코딩&lt;/b&gt;이 되어야 합니다. 예를 들어 ! * ' ( ) ; : @ &amp;amp; = + $ , / ? % # [ ] 이런 특수문자들은 이미 예약된 문자로 제어 또는 텍스트 문자열을 의미합니다. 그리고 영어를 제외한 특수 문자, UTF-8 문자들은 2자리 16진수 값으로 인코딩 됩니다. (공백은 +로 인코딩) ? and the Mysterians&amp;nbsp; -&amp;gt; %3F+and+the+Mysterians 이런 식으로 되는 셈이죠. 인코딩을 간과하게 되면 큰 문제가 생깁니다. 아래에 그 때 겪은 문제를 적어놓았습니다..&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대표적인 필수 URL 매개변수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; center(마커가 없는 경우 필수):&lt;/b&gt; 지도의 모든 가장자리에서 등거리에 있는 지도의 중심을 정의, {latitude,longitude} 쌍으로 되어있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;zoom(마커가 없는 경우 필수):&lt;/b&gt; 지도의 확대/축소 수준을 결정. 1=세계, 5=대륙, 10=도시, 15=거리, 20=건물&lt;/li&gt;
&lt;li&gt;&lt;b&gt;size:&lt;/b&gt; 지도 이미지의 직사각형 크기를 정의. {horizontal_value}x{vertical_value}, 500x400&lt;/li&gt;
&lt;li&gt;&lt;b&gt;key:&lt;/b&gt; 구글 API 키&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이 글에서 사용한 추가 URL 매개변수&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;format:&lt;/b&gt; 결과 이미지의 형식을 정의. GIF, JPEG, PNG 가능.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;path:&amp;nbsp;&lt;/b&gt;연결된 두 개 이상의 지점으로 구성된 단일 경로를 정의하여 지정된 위치의 이미지에 오버레이. 파이프 문자 (|)로 구분된 점 정의 문자열 또는 경로의 위치 선언 내에 enc: 접두사를 사용하여 인코딩된 다중선 객체를 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1753710437820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;path=color:0x0000ff|weight:5|40.737102,-73.990318|40.749825,-73.987963|40.752946,-73.987384|40.755823,-73.986397&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google Static Maps API을 Spring에서 사용해보자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지를 저장하는 것엔 다양한 방식이 존재하지만 무난하게 AWS S3에 이미지를 저장해두고 DB엔 URL을 두고 불러오는 방식을 택했습니다. 이렇게 되면 DB에선 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;먼저 Google Maps API Key를 받아야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1753430162902&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google Maps Platform &amp;nbsp;|&amp;nbsp; Google for Developers&quot; data-og-description=&quot;수백만 개의 웹사이트와 앱이 Google Maps Platform을 사용하여 사용자에게 효과적인 서비스 환경을 제공하고 있습니다.&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/maps?hl=ko&amp;amp;_gl=1*1gjhmy0*_up*MQ..*_ga*MTExMjAyODAwNy4xNzUzNDI5NDQy*_ga_NRWSTWS78N*czE3NTM0Mjk0NDIkbzEkZzEkdDE3NTM0MzAwNzUkajYwJGwwJGgw&quot; data-og-url=&quot;https://developers.google.com/maps?hl=ko&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBajUX/hyZq32Z53t/jVC3700S1xCZefY7ij9ueK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/cjpCyX/hyZnrK7o9y/QB8LPJgbe8NXHgI3HZg9v0/img.png?width=1750&amp;amp;height=750&amp;amp;face=0_0_1750_750,https://scrap.kakaocdn.net/dn/nYF5p/hyZnmpEv2h/TSN0iNEtMKmHvTTb5zjEEk/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534&quot;&gt;&lt;a href=&quot;https://developers.google.com/maps?hl=ko&amp;amp;_gl=1*1gjhmy0*_up*MQ..*_ga*MTExMjAyODAwNy4xNzUzNDI5NDQy*_ga_NRWSTWS78N*czE3NTM0Mjk0NDIkbzEkZzEkdDE3NTM0MzAwNzUkajYwJGwwJGgw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/maps?hl=ko&amp;amp;_gl=1*1gjhmy0*_up*MQ..*_ga*MTExMjAyODAwNy4xNzUzNDI5NDQy*_ga_NRWSTWS78N*czE3NTM0Mjk0NDIkbzEkZzEkdDE3NTM0MzAwNzUkajYwJGwwJGgw&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBajUX/hyZq32Z53t/jVC3700S1xCZefY7ij9ueK/img.png?width=1200&amp;amp;height=675&amp;amp;face=0_0_1200_675,https://scrap.kakaocdn.net/dn/cjpCyX/hyZnrK7o9y/QB8LPJgbe8NXHgI3HZg9v0/img.png?width=1750&amp;amp;height=750&amp;amp;face=0_0_1750_750,https://scrap.kakaocdn.net/dn/nYF5p/hyZnmpEv2h/TSN0iNEtMKmHvTTb5zjEEk/img.png?width=800&amp;amp;height=534&amp;amp;face=0_0_800_534');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google Maps Platform &amp;nbsp;|&amp;nbsp; Google for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;수백만 개의 웹사이트와 앱이 Google Maps Platform을 사용하여 사용자에게 효과적인 서비스 환경을 제공하고 있습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사이트에서 결제 계정을 만드시고 API 키를 만드시면 됩니다. 이 글에선 생략합니다. 얻은 API 키는 &lt;b&gt;application.properties&lt;/b&gt;에 저장해두고 사용하시면 좋습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;static-maps.google.api-key: api키
static-maps.google.base-url: https://maps.googleapis.com/maps/api/staticmap&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이를 이용해서 API를 호출할 Service나 Util을 만들어주면 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class StaticMapService {
    @Value(&quot;${static-maps.google.api-key}&quot;)
    private String googleApiKey;

    @Value(&quot;${static-maps.google.base-url}&quot;)
    private String baseUrl;

    private final PolylineEncoder polylineEncoder;

    public byte[] generateTrackThumbnail(List&amp;lt;CoordinateDto&amp;gt; trackPoints, int width, int height) {
        try {
            String encodedPath = encodePolyline(trackPoints);
            UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(baseUrl)
                    .queryParam(&quot;size&quot;, String.format(&quot;%dx%d&quot;, width, height))
                    .queryParam(&quot;path&quot;, &quot;enc:&quot; + encodedPath) // 'enc:' 접두사는 그대로 붙여서 파라미터로
                    .queryParam(&quot;key&quot;, googleApiKey)
                    .queryParam(&quot;format&quot;, &quot;jpg&quot;);

            // encode()를 사용하여 안전하게 인코딩된 URI를 생성
            URI uri = builder.build().encode().toUri();
            log.info(&quot;만들어진 썸네일 url&quot; + uri);
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity&amp;lt;byte[]&amp;gt; response = restTemplate.getForEntity(uri, byte[].class);

            return response.getBody();
        } catch (Exception e) {
            throw new RuntimeException(&quot;Static map 생성 실패&quot;, e);
        }
    }

    private String encodePolyline(List&amp;lt;CoordinateDto&amp;gt; points) {
        return polylineEncoder.encodeTrackPoints(points);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;URL 인코딩의 중요성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 Static Maps API를 요청할 때 만들 URL은 UriComponentsBuilder로 만들어야 합니다.&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;처음에 사용했던 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753713012657&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public byte[] generateTrackThumbnail(List&amp;lt;CoordinateDto&amp;gt; trackPoints, int width, int height) {
        try {
            String encodedPath = encodePolyline(trackPoints);
            String url = String.format(
                    &quot;%s?size=%dx%d&amp;amp;path=enc:%s&amp;amp;key=%s&amp;amp;format=jpg&quot;,
                    baseUrl, width, height, encodedPath, googleApiKey
            );
            log.info(&quot;만들어진 썸네일 url&quot; + url);
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity&amp;lt;byte[]&amp;gt; response = restTemplate.getForEntity(url, byte[].class);

            return response.getBody();
        } catch (Exception e) {
            throw new RuntimeException(&quot;Static map 생성 실패&quot;, e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 String으로 하드코딩한 셈이나 다름이 없는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들어도 작동은 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 특정 경우에 Static Map를 생성하지 못한 경우가 있었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1753713109052&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;java.lang.RuntimeException: Static map 생성 실패
	at com.running.you_run.running.service.StaticMapService.generateTrackThumbnail(StaticMapService.java:42) ~[main/:na]
   ....
Caused by: java.lang.IllegalArgumentException: Not enough variable values available to expand 'E'
	at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:370) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.HierarchicalUriComponents$QueryUriTemplateVariables.getValue(HierarchicalUriComponents.java:1098) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:263) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.HierarchicalUriComponents.lambda$expandQueryParams$6(HierarchicalUriComponents.java:456) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.util.UnmodifiableMultiValueMap.lambda$forEach$0(UnmodifiableMultiValueMap.java:115) ~[spring-core-6.2.8.jar:6.2.8]
	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:986) ~[na:na]
	at org.springframework.util.MultiValueMapAdapter.forEach(MultiValueMapAdapter.java:179) ~[spring-core-6.2.8.jar:6.2.8]
	at org.springframework.util.UnmodifiableMultiValueMap.forEach(UnmodifiableMultiValueMap.java:115) ~[spring-core-6.2.8.jar:6.2.8]
	at org.springframework.web.util.HierarchicalUriComponents.expandQueryParams(HierarchicalUriComponents.java:452) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:441) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:53) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.UriComponents.expand(UriComponents.java:172) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.build(DefaultUriBuilderFactory.java:459) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:204) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:800) ~[spring-web-6.2.8.jar:6.2.8]
	at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:442) ~[spring-web-6.2.8.jar:6.2.8]
	at com.running.you_run.running.service.StaticMapService.generateTrackThumbnail(StaticMapService.java:38) ~[main/:na]
	... 115 common frames omitted&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Caused&amp;nbsp;by:&amp;nbsp;java.lang.IllegalArgumentException:&amp;nbsp;Not&amp;nbsp;enough&amp;nbsp;variable&amp;nbsp;values&amp;nbsp;available&amp;nbsp;to&amp;nbsp;expand&amp;nbsp;'E'&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 에러는 쉽게 말해 E 를 변수로 쓸 수 없다고 나오는 에러입니다. &lt;b&gt;엥..? 왜 E가 뜬금없이 나오는 걸까요?&lt;/b&gt; 변수라하면 파라미터 변수를 말하는데 저희는 E라는 변수를 사용하지 않았습니다. 이는 encodePolyine 이라는 메서드에서 시작됐었습니다. 경로를 인코딩을 할 때, 우연치 않게 {EA+12% ... } 이런 식으로 인코딩이 됐던 겁니다. 근데 URL에선 이를 변수 바인딩으로 사용됩니다. 중괄호 안에 변수를 넣을 수 있는 겁니다. 하지만 저희는 저런 변수를 넣지 않았죠. 단지 파라미터로 넣을 값이었을 뿐입니다. 이를 &lt;b&gt;URL의 동적 치환 &lt;/b&gt;이라고 부릅니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해선 URL로 미리 인코딩한 뒤에 쿼리 파라미터로 넘겨야 합니다. 그러니까 encodPolyine으로 경로를 인코딩한 뒤에 또 다시 URL에 맞게 인코딩을 하고 나서 요청을 보내면 변수로 인식될 일이 없어지는 겁니다. UriComponentsBuilder나 URLEncoder를 사용해서 미리 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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과물&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;1284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t3lA5/btsPDEAGqgF/cCuyu8zlgbrqr5t2D5LYR1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t3lA5/btsPDEAGqgF/cCuyu8zlgbrqr5t2D5LYR1/img.gif&quot; data-alt=&quot;그림 3. 1초 이내의 빠른 로딩 속도&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t3lA5/btsPDEAGqgF/cCuyu8zlgbrqr5t2D5LYR1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/t3lA5/btsPDEAGqgF/cCuyu8zlgbrqr5t2D5LYR1/img.gif&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;337&quot; height=&quot;726&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;1284&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. 1초 이내의 빠른 로딩 속도&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/maps/documentation/maps-static/start?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/maps/documentation/maps-static/start?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/maps/billing-and-pricing/pricing?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developers.google.com/maps/billing-and-pricing/pricing?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>서버 공부/Spring</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/116</guid>
      <comments>https://namamim.tistory.com/116#entry116comment</comments>
      <pubDate>Fri, 1 Aug 2025 19:48:02 +0900</pubDate>
    </item>
    <item>
      <title>[백준]1033 - 칵테일 문제 풀이(자바,Java)</title>
      <link>https://namamim.tistory.com/115</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1033&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1033&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfs로 접근하는 것까진 성공했지만 최소 공배수, 최대 공약수를 어떻게 구해야 하는지 잊어먹어서 못 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이 코드&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class 백준1033_칵테일 {
    private static ArrayList&amp;lt;ArrayList&amp;lt;Edge&amp;gt;&amp;gt; arr = new ArrayList&amp;lt;&amp;gt;();
    private static long[] amount;
    private static ArrayList&amp;lt;Integer&amp;gt; ratio = new ArrayList&amp;lt;&amp;gt;();
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        amount = new long[n];
        for (int i = 0; i &amp;lt; n; i++) {
            amount[i] = 1;
            arr.add(new ArrayList&amp;lt;&amp;gt;());
        }
        long lcm = 1;
        for (int i = 0; i &amp;lt; n - 1; i++) {
            int[] s = Arrays.stream(br.readLine().split(&quot; &quot;)).mapToInt(
                    Integer::parseInt).toArray();
            int a = s[0];
            int b = s[1];
            int p = s[2];
            int q = s[3];

            arr.get(a).add(new Edge(b,p,q));
            arr.get(b).add(new Edge(a,q,p));

            lcm *= lcm(p,q);
        }
        
        amount[0] = lcm;
        bfs(0, n);

        long gcd = amount[0];
        for (int i = 1; i &amp;lt; amount.length; i++) {
            gcd = gcd(gcd, amount[i]);
        }

        StringBuilder result = new StringBuilder();
        for (int i = 0; i &amp;lt; amount.length; i++) {
            amount[i] /= gcd;
            result.append(amount[i])
                    .append(&quot; &quot;);
        }
        System.out.println(result);
    }
    private static void bfs(int start,int n){
        Queue&amp;lt;Integer&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
        boolean[] visited = new boolean[n];
        visited[start] = true;
        queue.add(start);
        while (!queue.isEmpty()){
            Integer poll = queue.poll();
            for (var neighbor : arr.get(poll)){
                if (!visited[neighbor.b]){
                    amount[neighbor.b] = ((amount[poll] * neighbor.q) / neighbor.p);
                    visited[neighbor.b] = true;
                    queue.add(neighbor.b);
                }
            }
        }
    }
    //최소 공배수
    private static long lcm(long a, long b){
        return (a / gcd(a, b)) * b;
    }
    //최대 공약수(유클리드 호제법)
    private static long gcd(long a, long b){
        if (b == 0){
            return a;
        } else {
            return gcd(b, a % b);
        }
    }

    private static class Edge{
        int b;
        int p;
        int q;

        public Edge(int b, int p, int q) {
            this.b = b;
            this.p = p;
            this.q = q;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 비율에 관한 문제입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 하나의 재료가 추가될 때마다 나머지 모든 재료 비율을 조정할 필요가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 상황에서 모든 재료의 비율은 어떻게 구할까요??&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 BFS를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 재료와 b 재료 사이에 비율 그러니까 관계가 생기면 이를 간선으로 간주하는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 간선에 비율인 p,q를 넣어주고 하나의 배열(amount)에 누적합을 쌓아가는 방식으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 진행했을 때, 시작점의 재료값이 제일 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 값에 따라 나머지 재료값들이 결정되기 때문인데, 만약 소수점으로 나머지가 나온다면 이를 정수로 만들어야 하는 번거로움이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작점은 정수이면 제일 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 p,q의 최소 공배수를 시작점으로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 어떤 비율을 곱해서 다음 재료로 넘어간다고 하더라도 시작이 최소 공배수이기 때문에 정수로 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소 공배수는 최대 공약수를 모두 곱하면 만들어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 유클리즈 호제법으로 최대 공약수 함수를 만들고 이를 이용해서 최소 공배수를 구하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 중요한 bfs로 넘어가보겠습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt; amount[poll] : amount[neighbor.b] = p : q&amp;nbsp;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선이 비율인 p,q를 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재점은 당연히 poll 이겠죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 간선으로 연결된 poll 과 neighbor.b 와 사이엔 p : q 비율이 성립하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 amount[neightbor.b] 는 어떻게 구할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학 시간에 배운대로 amount[neighbor.b] = (amount[poll] * q) / p가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 모든 그래프를 돌면 amount 배열에 누적합이 다 쌓이게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 끝난 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제엔 필요한 재료의 질량을 모두 더한 값이 최소를 구해야 한다고 말하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 최대 공약수로 나눠야 한다는 의미입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유클리드 호제법을 이용해서 구한 최대 공약수를 나눠주면 결과가 나오게 됩니다.&lt;/p&gt;</description>
      <category>알고리즘 문제 풀이/백준</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/115</guid>
      <comments>https://namamim.tistory.com/115#entry115comment</comments>
      <pubDate>Wed, 30 Jul 2025 00:39:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring]지도 경로 로딩 최적화 문제 해결하기 (feat. PostGIS, Ramer-Douglas-Peucker)</title>
      <link>https://namamim.tistory.com/114</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요. 현재 크래프톤 정글에서 &quot;넌! 런&quot;이라는 러닝앱을 개발하고 있습니다. GPS를 많이 사용하는 러닝앱 특성상 수많은 좌표들을 최적화하는 문제에 직면하여 고생한 내용을 글로 남기고자 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 문제가 있었나요?&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; data-alt=&quot;그림 1. 개발 버전 예시 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcqv37p%2FbtsPf1WgJHC%2F89GNa7jmzkpkoskV6k3h9k%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;302&quot; height=&quot;572&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 1. 개발 버전 예시 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 앱에 &lt;b&gt;지정된 경로들을 사용자에게 제시해주고 그중 하나를 선택하여 그걸 경로 안내와 함께 달리고 랭킹을 세우는 기능&lt;/b&gt;이 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능을 위해 &lt;b&gt;지도의 경로를 리스트형식으로 띄워야 했습니다.&lt;/b&gt;&lt;/p&gt;
&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;근데 잠깐만 이러면 DB Table을 어떻게 구성해야 하는 거지??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;첫 번째 난관: 어떻게 수많은 좌표들을 DB에 저장하는가?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 생각해 보면 다음과 같이 DB 테이블을 구성할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752251228460&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE gps_coordinates (
    point_id SERIAL PRIMARY KEY,
    track_id INT NOT NULL,
    point_order INT NOT NULL,
    latitude DECIMAL(10, 8) NOT NULL,
    longitude DECIMAL(11, 8) NOT NULL,
    timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);&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;여기서 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로의 정확성을 보장하기 위해 &lt;b&gt;1초에 한 번씩 GPS 좌표를 저장하고 속도는 7분 페이스로 가정해 보겠습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;러너들은 보통 한 번 달릴 때, 1km~ 7km를 많이 달립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 짧은 1km 기준 7분 페이스일 경우 420개의 좌표, 3km면? 1460개, 7km면? 2940개...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기하급수적으로 좌표의 수가 늘어나게 되고 이 gps_coordinates 테이블의 크기는 미친 듯이 커질 겁니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G1X0B/btsPguDJhWa/pwIdfbfHXwxluEaH6mKskK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G1X0B/btsPguDJhWa/pwIdfbfHXwxluEaH6mKskK/img.webp&quot; data-alt=&quot;그림 2. 미친듯이 증가하는 게 뻔한 테이블 구조&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G1X0B/btsPguDJhWa/pwIdfbfHXwxluEaH6mKskK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG1X0B%2FbtsPguDJhWa%2FpwIdfbfHXwxluEaH6mKskK%2Fimg.webp&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;683&quot; height=&quot;378&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 2. 미친듯이 증가하는 게 뻔한 테이블 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 이 수많은 좌표가 결국 하나의 경로만 표시하는 것이니 제공하는 경로가 많아지면??.. 펑 터지게 되는 건 두 눈뜨고 뻔한 상황이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 수 많은 탐색 끝에 찾은 것이 바로&lt;b&gt; PostGIS라는 친구&lt;/b&gt;였습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;PostGIS??? 그게 뭔가요?&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;155&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xI7qo/btsPgYqNQWI/UHapVsfuRXdNa2SLnpWdjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xI7qo/btsPgYqNQWI/UHapVsfuRXdNa2SLnpWdjK/img.png&quot; data-alt=&quot;그림 3. PostGIS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xI7qo/btsPgYqNQWI/UHapVsfuRXdNa2SLnpWdjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxI7qo%2FbtsPgYqNQWI%2FUHapVsfuRXdNa2SLnpWdjK%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;263&quot; height=&quot;155&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;155&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 3. PostGIS&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PostgreSQL 데이터베이스를 공간 데이터베이스로 확장시켜 주는&lt;/b&gt; 오픈 소스 소프트웨어로 이걸 통해 PostgreSQL은 지리적 객체를 저장하고, 인덱싱하며, 쿼리 할 수 있게 됩니다.&lt;/p&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;공간 데이터 타입 (Spatial Data Types)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 인덱스 (Spatial Indexes)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공간 함수 (Spatial Functions)&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 집중한 부분이 &lt;b&gt;&quot;공간 데이터 타입&quot;&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점(Point), &lt;b&gt;선(LineString),&lt;/b&gt; 다각형(Polygon)과 같은 기본적인 2D 도형은 물론, 3D 객체와 래스터(Raster) 데이터까지 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 원했던 경로인 &lt;b&gt;&quot;선&quot;&lt;/b&gt;을 저장할 수 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선을 어떻게 저장하나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 바이너리 스트림 형식으로 저장합니다. 그 많은 좌표들의 배열을 단 한 줄의 텍스트가 되는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;b&gt;PostGIS가 WKB(Well-Known Binary) 기법으로 공간의 좌표들을 압축&lt;/b&gt;하여 저장하기에 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HrVEv/btsPf3GCD6t/zdF7FKKy7YXrkE1fK8H601/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HrVEv/btsPf3GCD6t/zdF7FKKy7YXrkE1fK8H601/img.gif&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;396&quot; data-is-animation=&quot;true&quot; data-filename=&quot;녹화_2025_07_12_02_02_42_913.gif&quot; style=&quot;width: 20.3154%; margin-right: 10px;&quot; data-widthpercent=&quot;20.55&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HrVEv/btsPf3GCD6t/zdF7FKKy7YXrkE1fK8H601/img.gif&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHrVEv%2FbtsPf3GCD6t%2FzdF7FKKy7YXrkE1fK8H601%2Fimg.gif&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;418&quot; height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BSgml/btsPgV8PKO1/FY9iys6Q2gjJLB572fTURK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BSgml/btsPgV8PKO1/FY9iys6Q2gjJLB572fTURK/img.png&quot; data-origin-width=&quot;1175&quot; data-origin-height=&quot;288&quot; data-is-animation=&quot;false&quot; style=&quot;width: 78.5218%;&quot; data-widthpercent=&quot;79.45&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BSgml/btsPgV8PKO1/FY9iys6Q2gjJLB572fTURK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBSgml%2FbtsPgV8PKO1%2FFY9iys6Q2gjJLB572fTURK%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;1175&quot; height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그림 4. 실제 데이터와 DB에 저장된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림 4와 같이 수많은 좌표가 단 한 줄로 저장되는 모습을 보실 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 그림 4에 나오는 소공원은 총길이가 10km에 육박하는 데이터로 엄청난 배열의 크기를 보실 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;좋아 보이는데 어떻게 Spring에서 사용하나요?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 PostgreSQL의 확장 프로그램이므로 PostgreSQL을 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에선 PostGIS 설치법은 생략하고 Spring에서의 활용에 집중합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring의 Gradle Dependencies는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(Spring Boot 3.5.3 기준)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752253782236&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.locationtech.jts:jts-core:1.18.2' // PostGIS 공간 객체 등의 사용을 위해
implementation 'org.hibernate.orm:hibernate-spatial:6.4.1.Final' // 공간 객체 hibernate를 위해&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;다행히도 JPA와 호환이 잘 됩니다. 따라서 기존 JAP Repository 방식을 그대로 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1752253972398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Getter // Lombok 어노테이션만 유지
@Table(name = &quot;track&quot;)
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RunningTrack {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(columnDefinition = &quot;geometry(LineString, 4326)&quot;)
    private LineString path;
    
    //...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 Entity에다가 위 코드처럼 columnDefinition을 붙여주면 자동으로 DB에 hibernate가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(LineString 자료형은 jts 라이브러리 사용)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Column(columnDefinition = &quot;geometry(LineString, 4326)&quot;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 어노테이션에 대해 조금 더 상세히 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Geometry는 PostGIS에서 지원하는 큰 범위의 공간 객체 중 하나로 유클리드 좌표계 (평면 지도)를 기반으로 작동합니다. 이에 반해 구면 좌표계 (지구의 곡률 고려) 기반으로 작동하는 Geography가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넌!런 프로젝트에선 평면 지도로 충분히 구현할 수 있으므로 Geometry를 사용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Geometry에는 더 깊은 계층 구조를 가지고 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 156px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 98.6048%; text-align: center; height: 18px;&quot; colspan=&quot;4&quot;&gt;Geometry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 48.1396%; text-align: center; height: 20px;&quot; colspan=&quot;2&quot;&gt;기본 도형(Atomic Geometries)&lt;/td&gt;
&lt;td style=&quot;width: 50.4652%; text-align: center; height: 20px;&quot; colspan=&quot;2&quot;&gt;다중 도형(Multi-Part Geometries)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 18px; width: 22.0931%;&quot;&gt;POINT&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 26.0465%; height: 18px;&quot;&gt;공간 상의 단일 지점&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 18px; width: 22.5582%;&quot;&gt;MULTIPOINT&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 27.907%; height: 18px;&quot;&gt;여러 개의 점을 하나의 집합으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 22.0931%; height: 40px;&quot;&gt;LINESTRING&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 26.0465%; height: 40px;&quot;&gt;두 개 이상의 점을 &lt;br /&gt;순서대로 연결한 선&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 22.5582%; height: 40px;&quot;&gt;MULTILINESTRING&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 27.907%; height: 40px;&quot;&gt;여러 개의 선을 하나의 집합으로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;text-align: center; width: 22.0931%; height: 60px;&quot;&gt;POLYGON&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 26.0465%; height: 60px;&quot;&gt;하나 이상의 &lt;br /&gt;닫힌 선(Ring) 정의되는 면&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 22.5582%; height: 60px;&quot;&gt;MULTIPOLYGON&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 27.907%; height: 60px;&quot;&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;여기서 현재 LineString만 사용한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 뒤의 4326은 뭘까요??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 WGB 84(SRID 4326)을 의미하는 것으로 3차원 지구 모델에서 위도와 경도로 위치를 표현하는 GCS(Geographic Coordinate System, 지리 좌표계)의 한 종류입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;일상적으로 사용하는 GPS가 이 좌표계를 사용하고 있기 때문에 PostGIS에 적용하였습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀리멀리 돌아온 것 같지만 테이블이 무차별적으로 커지는 참사는 막을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만.. 난관은 여기가 시작이었습니다..&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 난관: 결국 불러오는 건 길고 긴 좌표 배열&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; data-alt=&quot;그림 5. 지도에 경로 그리기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqv37p/btsPf1WgJHC/89GNa7jmzkpkoskV6k3h9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcqv37p%2FbtsPf1WgJHC%2F89GNa7jmzkpkoskV6k3h9k%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;302&quot; height=&quot;572&quot; data-origin-width=&quot;477&quot; data-origin-height=&quot;903&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 5. 지도에 경로 그리기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쨌든 DB에 저장한 건 하나의 String이지만 결국 지도상에 이를 그리기 위해선 좌표 배열로 변환하고 모두 읽어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 예상하지 못한 건 아니었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사버와 DB에 페이지네이션과 인덱스를 도입하여 조금이라도 불러야 할 데이터를 적게 만들고 최적화하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;트랙 로딩 속도.gif&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;478&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMaGe/btsPfGENLaw/DjYPLmKeYCGtk8SNmZhlVk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMaGe/btsPfGENLaw/DjYPLmKeYCGtk8SNmZhlVk/img.gif&quot; data-alt=&quot;그림 6. 무려 10초나 걸리는 로딩&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMaGe/btsPfGENLaw/DjYPLmKeYCGtk8SNmZhlVk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/kMaGe/btsPfGENLaw/DjYPLmKeYCGtk8SNmZhlVk/img.gif&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;222&quot; height=&quot;478&quot; data-filename=&quot;트랙 로딩 속도.gif&quot; data-origin-width=&quot;222&quot; data-origin-height=&quot;478&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 6. 무려 10초나 걸리는 로딩&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만.. 한 페이지에 10km짜리 트랙이 2개만 있어도 앱에서 로딩 속도는 미친 듯이 걸리기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 크게 두 가지의 이유였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 좌표 배열이 너무 크다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 지도 뷰 자체가 많은 연산이 소모된다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위한 가장 좋은 방법은 2번을 해결하는 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Static Maps API 사용하여 지도 이미지 썸네일 이미지를 미리 만들어둔다면 단지 그걸 서버에서 불러오면 그만입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 구현할 당시에 2번을 위한 인프라가 덜 구성된 상태 + 중간 결과 발표를 위해 1번을 통해 성능 개선 급하게 해결했어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표 배열이 너무 크다면 그걸 줄이면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 단순화해 줄 알고리즘을 찾다가 알게 된 것이 바로 &lt;b&gt;Ramer-Douglas-Peucker (RDP) 알고리즘&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Ramer-Douglas-Peucker 알고리즘이 뭔가요??..&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N1BKO/btsPf1Pwsau/590si9dKU79oCk7I3AkiWK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N1BKO/btsPf1Pwsau/590si9dKU79oCk7I3AkiWK/img.gif&quot; data-alt=&quot;그림 7. 알고리즘 작동 예시&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N1BKO/btsPf1Pwsau/590si9dKU79oCk7I3AkiWK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/N1BKO/btsPf1Pwsau/590si9dKU79oCk7I3AkiWK/img.gif&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;458&quot; height=&quot;145&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 7. 알고리즘 작동 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 동작 과정을 간단히 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 경로의 첫 점과 끝 점을 이은 선에서 가장 먼 점을 중간점으로 잡는다. 이때 선에서의 거리가 &lt;span&gt;&amp;epsilon; 보다 커야만 한다. 만약 아니라면 선택하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 첫 점과 중간점에 선을 새로 긋는다. 그 선을 기준으로 그 사이에 있는 모든 점 중에서 &lt;span&gt;사이가 거리가 &lt;span&gt;&amp;epsilon; &lt;/span&gt;&amp;nbsp;보다 큰걸 다 고른다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;복잡한 알고리즘이지만 이를 쉽게 해주는 라이브러리가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이전에 설치했던 jts 라이브러리에 포함되어 있었습니다.&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;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static TrackListItemDto from(RunningTrack track){
    double epsilon = 0.0003;
    LineString simplifiedLine = (LineString) DouglasPeuckerSimplifier.simplify(track.getPath(), epsilon);
    List&amp;lt;CoordinateDto&amp;gt; coordinateDtos = CoordinateConverter.convertLineStringToCoordinates(simplifiedLine);
    return new TrackListItemDto(track.getId(), track.getName(), track.getTotalDistance(),coordinateDtos);
}&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;DB에 저장해 둔 LineString을 꺼내서 DouglasPeuckerSimplifier.simplify 메서드에 &lt;span&gt;&lt;span&gt;&amp;epsilon;&lt;/span&gt;&lt;/span&gt; (epsilon)과 넣어주면 끝!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 좌표의 저장을 위경도로 했기 때문에 거리가 아주 미세하기 때문에&amp;nbsp;&amp;nbsp;&lt;span&gt;&lt;span&gt;&amp;epsilon;&lt;/span&gt;&lt;/span&gt; 을 0.0001부터 차근차근 올리는 것을 추천합니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;근데 경로가 단순화되면 실제 경로하고 많이 달라지지 않을까요?&lt;/h4&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B24yx/btsPfLTLfhz/omGlZkbaa8kn7PrNJlaUL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B24yx/btsPfLTLfhz/omGlZkbaa8kn7PrNJlaUL1/img.png&quot; style=&quot;width: 47.7016%; margin-right: 10px;&quot; data-origin-width=&quot;223&quot; data-origin-height=&quot;244&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;48.26&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B24yx/btsPfLTLfhz/omGlZkbaa8kn7PrNJlaUL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB24yx%2FbtsPfLTLfhz%2FomGlZkbaa8kn7PrNJlaUL1%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;223&quot; height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IP1ih/btsPgTpH3D9/Nr1JiKWSx4zFK22mN2KueK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IP1ih/btsPgTpH3D9/Nr1JiKWSx4zFK22mN2KueK/img.png&quot; style=&quot;width: 51.1357%;&quot; data-origin-width=&quot;145&quot; data-origin-height=&quot;148&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;51.74&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IP1ih/btsPgTpH3D9/Nr1JiKWSx4zFK22mN2KueK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIP1ih%2FbtsPgTpH3D9%2FNr1JiKWSx4zFK22mN2KueK%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;145&quot; height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그림 8. 왼쪽 실제 경로, 오른쪽 단순화된 경로&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;썸네일의 목적으로 사용했기 때문에 심하게 단순화되지 않는 이상 문제는 없었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;span&gt;&lt;span&gt;&amp;epsilon; 0.0003 기준으로도 대략 원래 경로를 파악할 수 있었기에 괜찮다고 생각이 들었습니다.&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250712_034046831 (online-video-cutter.com).gif&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBN1MT/btsPfuEDv1E/KTMrd4uXEeigdrtEdB0Glk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBN1MT/btsPfuEDv1E/KTMrd4uXEeigdrtEdB0Glk/img.gif&quot; data-alt=&quot;그림 9. 1초 조금 넘는 시간까지 줄어든 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBN1MT/btsPfuEDv1E/KTMrd4uXEeigdrtEdB0Glk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cBN1MT/btsPfuEDv1E/KTMrd4uXEeigdrtEdB0Glk/img.gif&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;372&quot; height=&quot;802&quot; data-filename=&quot;KakaoTalk_20250712_034046831 (online-video-cutter.com).gif&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;802&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그림 9. 1초 조금 넘는 시간까지 줄어든 모습&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;끝내며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 제가 개발 초기에 경험했던 경로 관련 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 엄청 많았지만 다른 글로 돌아오도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;---&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화하는 게 재미있을 줄은 몰랐네요.&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;s&gt;사실 static maps로 변경하면 두 번째 문제는 쉽게 해결된다는 사실&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;출처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/dorergiverny/223113215510&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://m.blog.naver.com/dorergiverny/223113215510&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752257503856&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[C++] 다각형 근사화 Douglas-Peucker 알고리즘 원리 총정리 - DP Algorithm approxDP poly 간소화 간략화 polygon&quot; data-og-description=&quot;이전에는 영상의 외곽선 (contour)를 찾는 알고리즘에 대해 알아보았습니다. https://m.blog.naver.com/dor...&quot; data-og-host=&quot;blog.naver.com&quot; data-og-source-url=&quot;https://m.blog.naver.com/dorergiverny/223113215510&quot; data-og-url=&quot;https://blog.naver.com/dorergiverny/223113215510&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dBYzMs/hyZjypfdgM/4udM3QCGDh689PzskXWmJ0/img.png?width=743&amp;amp;height=396&amp;amp;face=0_0_743_396&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/dorergiverny/223113215510&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://m.blog.naver.com/dorergiverny/223113215510&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dBYzMs/hyZjypfdgM/4udM3QCGDh689PzskXWmJ0/img.png?width=743&amp;amp;height=396&amp;amp;face=0_0_743_396');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[C++] 다각형 근사화 Douglas-Peucker 알고리즘 원리 총정리 - DP Algorithm approxDP poly 간소화 간략화 polygon&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이전에는 영상의 외곽선 (contour)를 찾는 알고리즘에 대해 알아보았습니다. https://m.blog.naver.com/dor...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.naver.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752257690358&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Ramer&amp;ndash;Douglas&amp;ndash;Peucker algorithm - Wikipedia&quot; data-og-description=&quot;From Wikipedia, the free encyclopedia Curve simplification algorithm The Ramer&amp;ndash;Douglas&amp;ndash;Peucker algorithm, also known as the Douglas&amp;ndash;Peucker algorithm and iterative end-point fit algorithm, is an algorithm that decimates a curve composed of line segme&quot; data-og-host=&quot;en.wikipedia.org&quot; data-og-source-url=&quot;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&quot; data-og-url=&quot;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Ramer&amp;ndash;Douglas&amp;ndash;Peucker algorithm - Wikipedia&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;From Wikipedia, the free encyclopedia Curve simplification algorithm The Ramer&amp;ndash;Douglas&amp;ndash;Peucker algorithm, also known as the Douglas&amp;ndash;Peucker algorithm and iterative end-point fit algorithm, is an algorithm that decimates a curve composed of line segme&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;en.wikipedia.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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://postgis.net/docs/manual-3.3/postgis-ko_KR.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://postgis.net/docs/manual-3.3/postgis-ko_KR.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752257509350&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PostGIS 3.3.9dev 사용자 지침서&quot; data-og-description=&quot;SELECT f.geom AS before_geom, ST_MakeValid(f.geom) AS after_geom, ST_MakeValid(f.geom, 'method=structure') AS after_geom_structure FROM (SELECT 'MULTIPOLYGON(((186 194,187 194,188 195,189 195,190 195, 191 195,192 195,193 194,194 194,194 193,195 192,195 191&quot; data-og-host=&quot;postgis.net&quot; data-og-source-url=&quot;https://postgis.net/docs/manual-3.3/postgis-ko_KR.html&quot; data-og-url=&quot;https://postgis.net/docs/manual-3.3/postgis-ko_KR.html&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://postgis.net/docs/manual-3.3/postgis-ko_KR.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://postgis.net/docs/manual-3.3/postgis-ko_KR.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PostGIS 3.3.9dev 사용자 지침서&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SELECT f.geom AS before_geom, ST_MakeValid(f.geom) AS after_geom, ST_MakeValid(f.geom, 'method=structure') AS after_geom_structure FROM (SELECT 'MULTIPOLYGON(((186 194,187 194,188 195,189 195,190 195, 191 195,192 195,193 194,194 194,194 193,195 192,195 191&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;postgis.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>서버 공부/Spring</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/114</guid>
      <comments>https://namamim.tistory.com/114#entry114comment</comments>
      <pubDate>Sat, 12 Jul 2025 03:44:00 +0900</pubDate>
    </item>
    <item>
      <title>[백준]14502 - 연구소 문제 풀이(자바,Java)</title>
      <link>https://namamim.tistory.com/113</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/14502&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/14502&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 푸는 문제라 숨 막혔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배열 복제하는 등의 테크닉도 배운 문제라 좋았다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이 코드&lt;/h3&gt;
&lt;pre id=&quot;code_1751561298966&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package solved.ac.maraton;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class 백준14502 {
    static int[][] arr;
    static int[][] map;
    static int n, m;
    static boolean[][] visited = new boolean[n][m];
    static int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
    static int result = 0;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int[] s = Arrays.stream(br.readLine().split(&quot; &quot;)).mapToInt(Integer::parseInt)
                .toArray();
        n = s[0]; m = s[1];
        arr = new int[n][m];
        for (int i = 0; i &amp;lt; n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &amp;lt; m; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }
        visited = new boolean[n][m];
        map = Arrays.stream(arr).map(int[]::clone)
                .toArray(int[][]::new);
        makeWall(0);
        System.out.println(result);
    }

    private static void makeWall(int count){
        if (count == 3){
            // 바이러스 처리
            int[][] tempMap = Arrays.stream(map).map(int[]::clone)
                    .toArray(int[][]::new);
            for (int i = 0; i &amp;lt; n; i++) {
                for (int j = 0; j &amp;lt; m; j++) {
                    if (!visited[i][j] &amp;amp;&amp;amp; tempMap[i][j] == 2){
                        bfs(tempMap,i,j);
                    }
                }
            }
            int temp =0;
            for (int i = 0; i &amp;lt; n; i++) {
                for (int j = 0; j &amp;lt; m; j++) {
                    if (tempMap[i][j] == 0){
                        temp++;
                    }
                }
            }
            result = Math.max(result, temp);
            visited = new boolean[n][m];
            return;
        }

        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                if (map[i][j] == 0){
                    map[i][j] = 1;
                    makeWall(count+1);
                    map[i][j] = 0;
                }
            }
        }
    }

    private static void bfs(int[][] tempMap, int startX, int startY){
        Queue&amp;lt;Position&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
        queue.add(new Position(startX,startY));
        visited[startX][startY]= true;
        while (!queue.isEmpty()){
            Position poll = queue.poll();
            for (int i = 0; i &amp;lt; 4; i++) {
                int nextX = poll.x + dirs[i][0];
                int nextY = poll.y + dirs[i][1];
                if (!isPass(tempMap,nextX,nextY)){
                    continue;
                }
                queue.add(new Position(nextX,nextY));
                visited[nextX][nextY] = true;
                tempMap[nextX][nextY] = 2;
            }
        }
    }
    private static boolean isPass(int[][] tempMap,int x, int y){
        if (x &amp;lt;0 || y &amp;lt; 0|| x &amp;gt;=n || y &amp;gt;=m
            || visited[x][y] || tempMap[x][y] != 0){
            return false;
        }
        return true;
    }
    private static class Position{
        int x;
        int y;

        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;풀이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 오히려 엄청 무식하게 접근하면 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 무식한 방법이 뭘까?&lt;/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;1. 벽 3개를 설치하는 모든 경우를 탐사하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 그 경우마다 바이러스를 BFS 돌려 안전 영역의 값을 구하고&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;2번과 3번은 비교적 익숙한 맛이라 쉬울 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 1번에서 처음 접근 시에 좀 문제가 생긴다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;133&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnnhkH/btsO3GLVfkF/hu8WODWvSSNkJsKe45AeKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnnhkH/btsO3GLVfkF/hu8WODWvSSNkJsKe45AeKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnnhkH/btsO3GLVfkF/hu8WODWvSSNkJsKe45AeKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnnhkH%2FbtsO3GLVfkF%2Fhu8WODWvSSNkJsKe45AeKK%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;133&quot; height=&quot;182&quot; data-origin-width=&quot;133&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 수 많은 0 가운데 3개를 선택하는 걸 어떻게 구현할 수 있을까??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바는 늘 이 조합 기법에서 문제가 생긴다. 귀찮고 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조합 기법? 그러면 바로 백트래킹&lt;/b&gt;이 나와야 한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;private static void makeWall(int count){
    if (count == 3){
        // 바이러스 처리
        int[][] tempMap = Arrays.stream(map).map(int[]::clone)
                .toArray(int[][]::new);
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                if (!visited[i][j] &amp;amp;&amp;amp; tempMap[i][j] == 2){
                    bfs(tempMap,i,j);
                }
            }
        }
        int temp =0;
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                if (tempMap[i][j] == 0){
                    temp++;
                }
            }
        }
        result = Math.max(result, temp);
        visited = new boolean[n][m];
        return;
    }

    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; m; j++) {
            if (map[i][j] == 0){
                map[i][j] = 1;
                makeWall(count+1);
                map[i][j] = 0;
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중심이 바로 makeWall이라는 함수이다.&lt;/p&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중 반복문으로 모든 빈 곳에 1을 넣는 것을 구현하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 벽 3개를 다 놓았다면, bfs를 돌리면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, 기존 맵의 복사본을 bfs로 넘겨야 한다.&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차원 배열을 어떻게 복사할까??&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #080808;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int[][] tempMap = Arrays.stream(map).map(int[]::clone)
        .toArray(int[][]::new);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stream 메소드를 사용하면 비교적 쉽게 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 재귀가 끝나면 자연스럽게 답이 나오게 된다.&lt;/p&gt;</description>
      <category>알고리즘 문제 풀이/백준</category>
      <author>나맘임</author>
      <guid isPermaLink="true">https://namamim.tistory.com/113</guid>
      <comments>https://namamim.tistory.com/113#entry113comment</comments>
      <pubDate>Fri, 4 Jul 2025 01:55:18 +0900</pubDate>
    </item>
  </channel>
</rss>