<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>TIR: turn imagination into reality</title>
    <link>https://tobetirdev.tistory.com/</link>
    <description>TIR: turn imagination into reality</description>
    <language>ko</language>
    <pubDate>Sat, 30 May 2026 13:03:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>티디리</managingEditor>
    <image>
      <title>TIR: turn imagination into reality</title>
      <url>https://tistory1.daumcdn.net/tistory/7175342/attach/3ebeac8ef92c408b88da3f38fb59e33b</url>
      <link>https://tobetirdev.tistory.com</link>
    </image>
    <item>
      <title>쓰고 있는 AI Agent 정리</title>
      <link>https://tobetirdev.tistory.com/174</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ios-development.tistory.com/1840&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ios-development.tistory.com/1840&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Oh-my-claude&amp;nbsp;code&amp;nbsp;+&amp;nbsp;tmux&amp;nbsp;조합&lt;br /&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp;자연어로&amp;nbsp;명령&amp;nbsp;내려도&amp;nbsp;내장된&amp;nbsp;에이전트가&amp;nbsp;알아서&amp;nbsp;협업&lt;br /&gt;-&amp;gt; tmux는 ssh 접속 끊겨도 세션이 서버에서 돌아가서 attach로 다시 연결 가능 + pane 명령어로 패널 분할 및 병합 가능&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;(왼쪽 pane 으로 코드 편집, 오른쪽 상단에서 로그 보고 아래선 테스트 돌리기 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@takuya/claude-code-superpowers-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@takuya/claude-code-superpowers-guide&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775874390686&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;Claude Code &amp;times; Superpowers: 39k 스타 최강 플러그인 체험기&quot; data-og-description=&quot;GitHub에서 화제인 Superpowers를 사용해 Claude Code를 단순 코더에서 숙련된 아키텍트로 진화시키는 방법을 해설합니다. 서브 에이전트 기능과 TDD 강제를 통해 AI 개발 품질을 비약적으로 향상시키는 &quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@takuya/claude-code-superpowers-guide&quot; data-og-url=&quot;https://velog.io/@takuya/claude-code-superpowers-guide&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/QPGHU/dJMb9jOn2YO/D9StfGT6nrG8pQnJ7hKVY0/img.png?width=1536&amp;amp;height=672&amp;amp;face=0_0_1536_672,https://scrap.kakaocdn.net/dn/bTNrTD/dJMb8Rj22e5/nHtZAhk7bpjXpOADFlvGr0/img.png?width=1536&amp;amp;height=672&amp;amp;face=0_0_1536_672,https://scrap.kakaocdn.net/dn/ivA4q/dJMb9eTQMBW/fOqkJZF4N4BSLy3psJZdY1/img.png?width=2864&amp;amp;height=1524&amp;amp;face=0_0_2864_1524&quot;&gt;&lt;a href=&quot;https://velog.io/@takuya/claude-code-superpowers-guide&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@takuya/claude-code-superpowers-guide&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/QPGHU/dJMb9jOn2YO/D9StfGT6nrG8pQnJ7hKVY0/img.png?width=1536&amp;amp;height=672&amp;amp;face=0_0_1536_672,https://scrap.kakaocdn.net/dn/bTNrTD/dJMb8Rj22e5/nHtZAhk7bpjXpOADFlvGr0/img.png?width=1536&amp;amp;height=672&amp;amp;face=0_0_1536_672,https://scrap.kakaocdn.net/dn/ivA4q/dJMb9eTQMBW/fOqkJZF4N4BSLy3psJZdY1/img.png?width=2864&amp;amp;height=1524&amp;amp;face=0_0_2864_1524');&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;Claude Code &amp;times; Superpowers: 39k 스타 최강 플러그인 체험기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;GitHub에서 화제인 Superpowers를 사용해 Claude Code를 단순 코더에서 숙련된 아키텍트로 진화시키는 방법을 해설합니다. 서브 에이전트 기능과 TDD 강제를 통해 AI 개발 품질을 비약적으로 향상시키는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이제껏 수동으로 기획 -&amp;gt; 기획 문서 작성 -&amp;gt; 구현 계획 -&amp;gt; 구현 -&amp;gt; 테스트 -&amp;gt; 배포해야 했다면 (혹은 skill을 통해 커스텀 자동화)&lt;br /&gt;요걸로 클로드 코드가 알아서 다 해줌 + 구현시 의존도가 있는 작업은 순차적 task, 독립적 작업은 병렬 agent로 알아서 구현해 줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1775874596278&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;Claude Code와 Obsidian MCP 연동 가이드&quot; data-og-description=&quot;소개 Claude Code는 Anthropic의 공식 CLI 도구로, MCP(Model Context Protocol)를 통해 다양한 외부 도구와 연동할 수 있습니다. 이 가이드에서는 Claude Code와 Obsidian을 연동하여 AI 에이전트가 여러분의 노트를 &quot; data-og-host=&quot;tech.e3view.com&quot; data-og-source-url=&quot;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&quot; data-og-url=&quot;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/boCNsg/dJMb8Xkgs6S/MgoIPzUW3aoLH2PVi54Vn1/img.png?width=2870&amp;amp;height=1662&amp;amp;face=0_0_2870_1662,https://scrap.kakaocdn.net/dn/dvH3CH/dJMb8SpI4MP/hJHfkSZIFkYHhKb7mJ2SU0/img.png?width=2870&amp;amp;height=1662&amp;amp;face=0_0_2870_1662,https://scrap.kakaocdn.net/dn/pUK0Z/dJMb8ZvCzgA/EkPvJCyaSKGHVSh1u2PokK/img.png?width=808&amp;amp;height=1086&amp;amp;face=0_0_808_1086&quot;&gt;&lt;a href=&quot;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tech.e3view.com/claude-codewa-obsidian-mcp-yeondong-gaideu/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/boCNsg/dJMb8Xkgs6S/MgoIPzUW3aoLH2PVi54Vn1/img.png?width=2870&amp;amp;height=1662&amp;amp;face=0_0_2870_1662,https://scrap.kakaocdn.net/dn/dvH3CH/dJMb8SpI4MP/hJHfkSZIFkYHhKb7mJ2SU0/img.png?width=2870&amp;amp;height=1662&amp;amp;face=0_0_2870_1662,https://scrap.kakaocdn.net/dn/pUK0Z/dJMb8ZvCzgA/EkPvJCyaSKGHVSh1u2PokK/img.png?width=808&amp;amp;height=1086&amp;amp;face=0_0_808_1086');&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;Claude Code와 Obsidian MCP 연동 가이드&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소개 Claude Code는 Anthropic의 공식 CLI 도구로, MCP(Model Context Protocol)를 통해 다양한 외부 도구와 연동할 수 있습니다. 이 가이드에서는 Claude Code와 Obsidian을 연동하여 AI 에이전트가 여러분의 노트를&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tech.e3view.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;노션보다는, 파일 구조 그대로 저장하는 옵시디언이 좀 더 훑기 가능 + 그래프 시각화가 잘 돼서 나름 괜찮은 듯 + 문서 간 관계까지 프롬프트로 알아서 지정해줘서 편함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/174</guid>
      <comments>https://tobetirdev.tistory.com/174#entry174comment</comments>
      <pubDate>Sat, 11 Apr 2026 11:38:52 +0900</pubDate>
    </item>
    <item>
      <title>노트북 한 대가 온프레미스 서버가 되기까지 네트워크 2편 &amp;ndash; 공인 IP, SSH 보안, VPN</title>
      <link>https://tobetirdev.tistory.com/173</link>
      <description>&lt;p data-end=&quot;210&quot; data-start=&quot;190&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;238&quot; data-start=&quot;212&quot; data-ke-size=&quot;size16&quot;&gt;기능하는 서버와 안전한 서버는 다르다&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;이전 글에서 포트포워딩을 설정하고, SSH 접속이 가능하도록 만들었다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;276&quot; data-ke-size=&quot;size16&quot;&gt;원했던 건 다 됐다.&lt;br /&gt;밖에서 접속됐고, 명령도 실행됐고, 서버는 &amp;ldquo;잘 작동&amp;rdquo;했다.&lt;/p&gt;
&lt;p data-end=&quot;402&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;402&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;그런데 이상하게도 마음이 편하지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;450&quot; data-start=&quot;404&quot; data-ke-size=&quot;size16&quot;&gt;서버는 분명 열려 있었는데,&lt;br /&gt;이게 과연 &amp;ldquo;괜찮은 상태&amp;rdquo;인지 확신이 들지 않았다.&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;452&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;472&quot; data-start=&quot;452&quot; data-ke-size=&quot;size16&quot;&gt;그래서 이런 질문이 머릿속에 남았다.&lt;/p&gt;
&lt;p data-end=&quot;500&quot; data-start=&quot;474&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;이 서버는 지금 얼마나 노출되어 있는 걸까?&amp;rdquo;&lt;/p&gt;
&lt;p data-end=&quot;595&quot; data-start=&quot;502&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;595&quot; data-start=&quot;502&quot; data-ke-size=&quot;size16&quot;&gt;서버가 작동하는 것과,&lt;br /&gt;서버가 안전하게 작동하는 건 전혀 다른 문제라는 생각이 들었다.&lt;br /&gt;이 글은 그 질문에서 시작해서, 접근 구조 자체를 바꾸기까지의 기록이다.&lt;/p&gt;
&lt;p data-end=&quot;503&quot; data-start=&quot;483&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;503&quot; data-start=&quot;483&quot; 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-end=&quot;596&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 든 생각은 단순했다. &amp;ldquo;밖에서는 이 서버를 어떻게 보고 있을까?&amp;rdquo;&lt;/p&gt;
&lt;p data-end=&quot;711&quot; data-start=&quot;685&quot; data-ke-size=&quot;size16&quot;&gt;그래서 외부 인터넷 기준에서 IP를 확인해봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042200417&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl ifconfig.me&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;596&quot; data-start=&quot;580&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;612&quot; data-start=&quot;598&quot; data-ke-size=&quot;size16&quot;&gt;출력 결과는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042196852&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;58.xxx.xx.xx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;669&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;669&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;처음엔 이게 서버의 주소라고 생각했지만, 곧 아니란 걸 알게 됐다.&lt;/p&gt;
&lt;p data-end=&quot;669&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;669&quot; data-start=&quot;628&quot; data-ke-size=&quot;size16&quot;&gt;이 IP는 서버의 IP가 아니다.&lt;br /&gt;공유기(NAT 장비)의 공인 IP다.&lt;/p&gt;
&lt;p data-end=&quot;688&quot; data-start=&quot;671&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;688&quot; data-start=&quot;671&quot; data-ke-size=&quot;size16&quot;&gt;구조를 단순화하면 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042190535&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ 인터넷 ]
&amp;darr;
[ 공인 IP: 58.xxx.xx.xx ] (공유기)
&amp;darr;
[ 사설 IP: 192.168.45.34 ] (서버)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;771&quot; data-start=&quot;690&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;778&quot; data-start=&quot;773&quot; data-ke-size=&quot;size16&quot;&gt;정리하면,&lt;/p&gt;
&lt;p data-end=&quot;892&quot; data-start=&quot;877&quot; data-ke-size=&quot;size16&quot;&gt;서버는 여전히 192.168.45.0/24 대역의 사설 IP를 쓰고 있고,&lt;br /&gt;외부에서는 공유기까지만 보인다.&lt;/p&gt;
&lt;p data-end=&quot;892&quot; data-start=&quot;877&quot; data-ke-size=&quot;size16&quot;&gt;다만 포트포워딩으로 특정 포트만 서버로 전달되는 상태였다.&lt;/p&gt;
&lt;p data-end=&quot;892&quot; data-start=&quot;877&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;892&quot; data-start=&quot;877&quot; data-ke-size=&quot;size16&quot;&gt;여기까지는 알고 있던 내용이었다.&lt;br /&gt;문제는 &amp;ldquo;외부에서 보이는 모습&amp;rdquo;이었다.&lt;/p&gt;
&lt;h3 data-end=&quot;892&quot; data-start=&quot;877&quot; data-ke-size=&quot;size23&quot;&gt;외부에서 보이는 구조&lt;/h3&gt;
&lt;p data-end=&quot;920&quot; data-start=&quot;894&quot; data-ke-size=&quot;size16&quot;&gt;외부 사용자, 혹은 공격자 입장에서 이 서버는 이렇게 보인다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042215234&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[공인 IP (공유기)] &amp;rarr; [열린 포트: 22] &amp;rarr; [?]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1014&quot; data-start=&quot;957&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1014&quot; data-start=&quot;957&quot; data-ke-size=&quot;size16&quot;&gt;그 뒤에 노트북이 있는지, 서버 랙이 있는지는 중요하지 않다.&lt;br /&gt;열린 포트가 있다는 사실만으로도 이미 대상이 된다.&lt;/p&gt;
&lt;p data-end=&quot;1036&quot; data-start=&quot;1016&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH는 왜 공격의 첫 번째 대상이 되는가&lt;/h3&gt;
&lt;h4 data-end=&quot;1077&quot; data-start=&quot;1066&quot; data-ke-size=&quot;size20&quot;&gt;SSH의 역할&lt;/h4&gt;
&lt;p data-end=&quot;1149&quot; data-start=&quot;1079&quot; data-ke-size=&quot;size16&quot;&gt;SSH(Secure Shell)는 원격 서버 접속을 위한 프로토콜이다.&lt;br /&gt;서버 관점에서 SSH는 다음과 같은 의미를 가진다.&lt;/p&gt;
&lt;p data-end=&quot;1149&quot; data-start=&quot;1079&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1149&quot; data-start=&quot;1079&quot; data-ke-size=&quot;size16&quot;&gt;서버를 관리하는 모든 작업은 SSH로 한다. 모니터도 없고, 키보드도 없는 서버에서&lt;br /&gt;SSH는 사실상 유일한 출입구다. 그래서 서버를 연다는 말은, 결국 SSH를 연다는 말과 거의 같다.&lt;/p&gt;
&lt;p data-end=&quot;1318&quot; data-start=&quot;1302&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1318&quot; data-start=&quot;1302&quot; data-ke-size=&quot;size20&quot;&gt;현재 SSH 상태 확인&lt;/h4&gt;
&lt;p data-end=&quot;1337&quot; data-start=&quot;1320&quot; data-ke-size=&quot;size16&quot;&gt;서버의 SSH 상태를 확인했다.&lt;/p&gt;
&lt;p data-end=&quot;1337&quot; data-start=&quot;1320&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042276783&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dpkg -l | grep openssh-server
systemctl status ssh
ss -tnlp | grep ssh&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1415&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1432&quot; data-start=&quot;1417&quot; data-ke-size=&quot;size16&quot;&gt;현재 상태는 다음과 같았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1514&quot; data-start=&quot;1434&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1454&quot; data-start=&quot;1434&quot;&gt;OpenSSH 서버 설치 완료&lt;/li&gt;
&lt;li data-end=&quot;1473&quot; data-start=&quot;1455&quot;&gt;SSH 데몬 상시 실행 중&lt;/li&gt;
&lt;li data-end=&quot;1488&quot; data-start=&quot;1474&quot;&gt;부팅 시 자동 시작&lt;/li&gt;
&lt;li data-end=&quot;1514&quot; data-start=&quot;1489&quot;&gt;모든 인터페이스에서 22번 포트 리스닝&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1531&quot; data-start=&quot;1516&quot; data-ke-size=&quot;size16&quot;&gt;이 상태의 의미는 명확하다.&lt;/p&gt;
&lt;p data-end=&quot;1585&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1585&quot; data-start=&quot;1533&quot; data-ke-size=&quot;size16&quot;&gt;포트포워딩 활성화&lt;br /&gt;&amp;rarr; SSH 포트 인터넷 노출&lt;br /&gt;&amp;rarr; 전 세계 어디서든 접근 시도 가능&lt;/p&gt;
&lt;p data-end=&quot;1623&quot; data-start=&quot;1587&quot; data-ke-size=&quot;size16&quot;&gt;서버는 지금 인터넷에 노출된 상태로 SSH 접속을 기다리고 있다.&lt;/p&gt;
&lt;p data-end=&quot;1645&quot; data-start=&quot;1625&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;1645&quot; data-start=&quot;1625&quot; 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;h4 data-end=&quot;1682&quot; data-start=&quot;1673&quot; data-ke-size=&quot;size20&quot;&gt;로그 확인&lt;/h4&gt;
&lt;p data-end=&quot;1715&quot; data-start=&quot;1684&quot; data-ke-size=&quot;size16&quot;&gt;당시에 SSH 접근 시도를 확인하기 위해 인증 로그를 확인했다.&lt;/p&gt;
&lt;p data-end=&quot;1715&quot; data-start=&quot;1684&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042324051&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo tail -f /var/log/auth.log&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1747&quot; data-start=&quot;1717&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1779&quot; data-start=&quot;1749&quot; data-ke-size=&quot;size16&quot;&gt;로그에는 다음과 같은 메시지가 반복해서 찍히고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1876&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042334765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Failed password for invalid user admin
Failed password for root
Connection closed [preauth]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1876&quot; data-start=&quot;1781&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1936&quot; data-start=&quot;1878&quot; data-ke-size=&quot;size16&quot;&gt;서버를 구축한 지 몇 시간도 지나지 않았는데,&lt;br /&gt;이미 전 세계에서 무차별 접근 시도가 들어오고 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1947&quot; data-start=&quot;1938&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;1947&quot; data-start=&quot;1938&quot; data-ke-size=&quot;size20&quot;&gt;공격 패턴&lt;/h4&gt;
&lt;p data-end=&quot;1972&quot; data-start=&quot;1949&quot; data-ke-size=&quot;size16&quot;&gt;로그에서 확인된 공격 패턴은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2038&quot; data-start=&quot;1974&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2001&quot; data-start=&quot;1974&quot;&gt;admin, root 같은 계정 이름 추측&lt;/li&gt;
&lt;li data-end=&quot;2017&quot; data-start=&quot;2002&quot;&gt;비밀번호 무차별 대입&lt;/li&gt;
&lt;li data-end=&quot;2038&quot; data-start=&quot;2018&quot;&gt;자동화된 봇에 의한 반복 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2085&quot; data-start=&quot;2054&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSH 보안 강화 시도&lt;/h3&gt;
&lt;h4 data-end=&quot;2142&quot; data-start=&quot;2126&quot; data-ke-size=&quot;size20&quot;&gt;비밀번호 인증 비활성화&lt;/h4&gt;
&lt;p data-end=&quot;2163&quot; data-start=&quot;2144&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 SSH 설정을 강화했다.&lt;/p&gt;
&lt;p data-end=&quot;2240&quot; data-start=&quot;2165&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호 인증을 끄고, 키 기반 인증만 허용하고, root 로그인도 막았다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042398766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin no&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2240&quot; data-start=&quot;2165&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2256&quot; data-start=&quot;2242&quot; data-ke-size=&quot;size16&quot;&gt;이후 SSH를 재시작했다.&lt;/p&gt;
&lt;p data-end=&quot;2274&quot; data-start=&quot;2258&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-end=&quot;2274&quot; data-start=&quot;2258&quot; data-ke-size=&quot;size20&quot;&gt;키 기반 인증으로 전환&lt;/h4&gt;
&lt;p data-end=&quot;2308&quot; data-start=&quot;2276&quot; data-ke-size=&quot;size16&quot;&gt;비밀번호 대신 SSH 키 기반 인증을 사용하도록 전환했다.&lt;/p&gt;
&lt;p data-end=&quot;2319&quot; data-start=&quot;2310&quot; data-ke-size=&quot;size16&quot;&gt;이 조치로 인해,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2373&quot; data-start=&quot;2321&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2347&quot; data-start=&quot;2321&quot;&gt;비밀번호 무차별 대입 공격은 무효화되었고&lt;/li&gt;
&lt;li data-end=&quot;2373&quot; data-start=&quot;2348&quot;&gt;키 파일 없이는 접속 자체가 불가능해졌다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-end=&quot;2384&quot; data-start=&quot;2375&quot; data-ke-size=&quot;size20&quot;&gt;포트 변경&lt;/h4&gt;
&lt;p data-end=&quot;2412&quot; data-start=&quot;2386&quot; data-ke-size=&quot;size16&quot;&gt;SSH 포트를 22번에서 다른 포트로 변경했다.&lt;/p&gt;
&lt;p data-end=&quot;2474&quot; data-start=&quot;2414&quot; data-ke-size=&quot;size16&quot;&gt;하지만 포트 변경은 보안이 아니라 난독화에 가깝다.&lt;br /&gt;전체 포트 스캔을 하는 공격자에게는 큰 의미가 없다.&lt;/p&gt;
&lt;p data-end=&quot;2496&quot; data-start=&quot;2476&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SSH 보안 강화의 근본적 한계&lt;/h4&gt;
&lt;p data-end=&quot;2549&quot; data-start=&quot;2520&quot; data-ke-size=&quot;size16&quot;&gt;설정을 아무리 강화해도 변하지 않는 사실들이 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2614&quot; data-start=&quot;2551&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2574&quot; data-start=&quot;2551&quot;&gt;포트는 여전히 인터넷에 열려 있다.&lt;/li&gt;
&lt;li data-end=&quot;2594&quot; data-start=&quot;2575&quot;&gt;공인 IP는 노출되어 있다.&lt;/li&gt;
&lt;li data-end=&quot;2614&quot; data-start=&quot;2595&quot;&gt;공격 시도 자체는 계속된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2636&quot; data-start=&quot;2616&quot; data-ke-size=&quot;size16&quot;&gt;현재 구조를 다시 보면 다음과 같다.&lt;/p&gt;
&lt;p data-end=&quot;2702&quot; data-start=&quot;2638&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042447650&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ 인터넷 ]
&amp;rarr; [ 공인 IP 노출 ]
&amp;rarr; [ 포트 개방 ]
&amp;rarr; [ SSH 인증 ]
&amp;rarr; [ 서버 ]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2702&quot; data-start=&quot;2638&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2702&quot; data-start=&quot;2638&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2737&quot; data-start=&quot;2704&quot; data-ke-size=&quot;size16&quot;&gt;이 구조의 본질은&lt;br /&gt;먼저 노출하고, 나중에 막는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;2755&quot; data-start=&quot;2739&quot; data-ke-size=&quot;size16&quot;&gt;이 시점에서 질문이 바뀌었다.&lt;/p&gt;
&lt;p data-end=&quot;2805&quot; data-start=&quot;2757&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;어떻게 더 잘 막을까?&amp;rdquo;가 아니라&lt;br /&gt;&amp;ldquo;애초에 공격 대상이 되지 않을 수는 없을까?&amp;rdquo;&lt;/p&gt;
&lt;p data-end=&quot;2805&quot; data-start=&quot;2757&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VPN: 접근 구조를 근본적으로 바꾸다&lt;/h3&gt;
&lt;p data-end=&quot;2903&quot; data-start=&quot;2855&quot; data-ke-size=&quot;size16&quot;&gt;VPN을 쓰면 보안이 좋아진다, 암호화된다 &amp;mdash; 이런 이야기는 많이 들었지만, 이 순간에 와닿은 건 다른 점이었다.&lt;/p&gt;
&lt;p data-end=&quot;2648&quot; data-start=&quot;2627&quot; data-ke-size=&quot;size16&quot;&gt;VPN은 서버를 먼저 보여주지 않는다. 서버는 내부 네트워크에 있고,&lt;br /&gt;먼저 인증된 경우에만 그 네트워크에 들어올 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;2916&quot; data-start=&quot;2905&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2916&quot; data-start=&quot;2905&quot; data-ke-size=&quot;size16&quot;&gt;기존 구조는 이랬다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042475117&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;외부 &amp;rarr; 인터넷 &amp;rarr; 공인 IP &amp;rarr; SSH 인증 &amp;rarr; 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2966&quot; data-start=&quot;2950&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2966&quot; data-start=&quot;2950&quot; data-ke-size=&quot;size16&quot;&gt;VPN 구조는 이렇게 바뀐다.&lt;/p&gt;
&lt;pre id=&quot;code_1770042480115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;외부 &amp;rarr; VPN 인증 &amp;rarr; 내부 네트워크 &amp;rarr; 서버&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3006&quot; data-start=&quot;2996&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3006&quot; data-start=&quot;2996&quot; data-ke-size=&quot;size16&quot;&gt;차이점은 명확하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3082&quot; data-start=&quot;3008&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3033&quot; data-start=&quot;3008&quot;&gt;SSH 포트를 외부에 열 필요가 없다.&lt;/li&gt;
&lt;li data-end=&quot;3061&quot; data-start=&quot;3034&quot;&gt;VPN 연결 전에는 서버가 보이지 않는다.&lt;/li&gt;
&lt;li data-end=&quot;3082&quot; data-start=&quot;3062&quot;&gt;접근 자체가 내부망 문제가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WireGuard VPN 구성&lt;/h3&gt;
&lt;p data-end=&quot;3214&quot; data-start=&quot;3185&quot; data-ke-size=&quot;size16&quot;&gt;VPN 솔루션 중에서 WireGuard를 선택했다.&lt;/p&gt;
&lt;p data-end=&quot;2940&quot; data-start=&quot;2903&quot; data-ke-size=&quot;size16&quot;&gt;설정이 단순했고, 커널 레벨에서 동작하며, 구조가 명확했다.&lt;/p&gt;
&lt;p data-end=&quot;2977&quot; data-start=&quot;2942&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2977&quot; data-start=&quot;2942&quot; data-ke-size=&quot;size16&quot;&gt;VPN을 구성하고 나니&lt;br /&gt;서버에는 두 개의 네트워크가 생겼다.&lt;/p&gt;
&lt;p data-end=&quot;3017&quot; data-start=&quot;2979&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3017&quot; data-start=&quot;2979&quot; data-ke-size=&quot;size16&quot;&gt;하나는 기존의 로컬 사설망.&lt;br /&gt;인터넷으로 나가는 출구 역할을 한다.&lt;/p&gt;
&lt;p data-end=&quot;3224&quot; data-start=&quot;3216&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042498314&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;로컬 사설망

인터페이스: wlp2s0
IP: 192.168.45.34
역할: 인터넷 출구

VPN 내부망

인터페이스: wg0
IP: 10.100.0.1
역할: 외부 접근 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3359&quot; data-start=&quot;3343&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3396&quot; data-start=&quot;3361&quot; data-ke-size=&quot;size16&quot;&gt;SSH 접속 방식도 자연스럽게 바뀌었다.&lt;/p&gt;
&lt;p data-end=&quot;3137&quot; data-start=&quot;3081&quot; data-ke-size=&quot;size16&quot;&gt;예전에는 공인 IP로 바로 접속했다면,&lt;br /&gt;이제는 VPN에 먼저 연결한 뒤 내부 IP로 접속한다.&lt;/p&gt;
&lt;p data-end=&quot;3171&quot; data-start=&quot;3139&quot; data-ke-size=&quot;size16&quot;&gt;이제 SSH는 인터넷이 아니라 내부 네트워크 문제였다.&lt;/p&gt;
&lt;p data-end=&quot;3418&quot; data-start=&quot;3398&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포트포워딩과 VPN 비교&lt;/h3&gt;
&lt;p data-end=&quot;3471&quot; data-start=&quot;3438&quot; data-ke-size=&quot;size16&quot;&gt;포트포워딩 구조는 항상 노출된 상태에서 인증을 시도한다.&lt;/p&gt;
&lt;p data-end=&quot;3500&quot; data-start=&quot;3473&quot; data-ke-size=&quot;size16&quot;&gt;VPN 구조는 인증된 경우에만 서버가 보인다.&amp;nbsp; 보안 장벽의 개수도 다르다.&lt;/p&gt;
&lt;p data-end=&quot;3565&quot; data-start=&quot;3519&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770042527466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;포트포워딩: SSH 인증 1단계
VPN: VPN 인증 + SSH 인증 2단계&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3584&quot; data-start=&quot;3567&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3584&quot; data-start=&quot;3567&quot; data-ke-size=&quot;size16&quot;&gt;공격자 관점에서도 차이는 크다.&lt;/p&gt;
&lt;p data-end=&quot;3623&quot; data-start=&quot;3586&quot; data-ke-size=&quot;size16&quot;&gt;포트포워딩은 &amp;ldquo;항상 시도 가능&amp;rdquo;&lt;br /&gt;VPN은 &amp;ldquo;시도 자체가 불가능&amp;rdquo;&lt;/p&gt;
&lt;p data-end=&quot;3645&quot; data-start=&quot;3625&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;3706&quot; data-start=&quot;3682&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3756&quot; data-start=&quot;3708&quot; data-ke-size=&quot;size16&quot;&gt;SSH 설정을 아무리 강화해도 포트포워딩이라는 구조는 공격 표면을 제거하지 못한다.&lt;/p&gt;
&lt;p data-end=&quot;3787&quot; data-start=&quot;3758&quot; data-ke-size=&quot;size16&quot;&gt;VPN은 서버를 숨기고, 접근을 조건부로 만든다.&lt;/p&gt;
&lt;p data-end=&quot;3839&quot; data-start=&quot;3819&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;3844&quot; data-start=&quot;3841&quot; data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-end=&quot;3927&quot; data-start=&quot;3846&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 공인 IP 노출 구조, SSH가 공격 대상이 되는 이유,&lt;br /&gt;포트포워딩의 한계, VPN을 통한 접근 구조 전환을 다뤘다.&lt;/p&gt;
&lt;p data-end=&quot;4005&quot; data-start=&quot;3929&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4005&quot; data-start=&quot;3929&quot; data-ke-size=&quot;size16&quot;&gt;다음 글에서는 이 온프레미스 경험을&lt;br /&gt;AWS VPC, Security Group, Private Subnet 개념으로 직접 매핑해본다.&lt;/p&gt;
&lt;p data-end=&quot;4043&quot; data-start=&quot;4007&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;4043&quot; data-start=&quot;4007&quot; data-ke-size=&quot;size16&quot;&gt;온프레미스에서 이해한 구조를&lt;br /&gt;클라우드 언어로 번역하는 단계다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;4057&quot; data-start=&quot;4045&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 관련 이야기가 끝나면, 다음으로는 CI/CD 및 도커 관련 이야기로 넘어가보자&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/173</guid>
      <comments>https://tobetirdev.tistory.com/173#entry173comment</comments>
      <pubDate>Mon, 2 Feb 2026 22:31:21 +0900</pubDate>
    </item>
    <item>
      <title>노트북 한 대가 온프레미스 서버가 되기까지: 네트워크 1편 &amp;ndash; 내부망, 사설 IP, 포트포워딩</title>
      <link>https://tobetirdev.tistory.com/172</link>
      <description>&lt;p data-end=&quot;264&quot; data-start=&quot;245&quot; data-ke-size=&quot;size16&quot;&gt;[지난 포스팅에 이어 온프레미스 서버를 더 뜯어보기]&lt;/p&gt;
&lt;h2 data-end=&quot;264&quot; data-start=&quot;245&quot; data-ke-size=&quot;size26&quot;&gt;서버의 전제부터 다시 정의&lt;/h2&gt;
&lt;p data-end=&quot;275&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;서버란 무엇일까.&lt;/p&gt;
&lt;blockquote data-end=&quot;324&quot; data-start=&quot;277&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;324&quot; data-start=&quot;279&quot; data-ke-size=&quot;size16&quot;&gt;서버 = 외부(또는 다른 시스템)에서&lt;br /&gt;네트워크로 접근 가능한 컴퓨터&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;326&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;326&quot; data-ke-size=&quot;size16&quot;&gt;정확히 말하면 서버란 &lt;b&gt;네트워크 상에서 식별되고(route), 도달 가능한(endpoint) 컴퓨터&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;326&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;378&quot; data-start=&quot;326&quot; data-ke-size=&quot;size16&quot;&gt;이 정의에 따르면 &lt;b&gt;전원이 켜져 있고 OS가 설치돼 있다는 것만으로는 서버가 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;429&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;429&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 우리는 이미 전원과 OS 상태를 확인했다.&lt;br /&gt;이제 남은 조건은 하나다.&lt;/p&gt;
&lt;p data-end=&quot;429&quot; data-start=&quot;380&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-end=&quot;441&quot; data-start=&quot;431&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;441&quot; data-start=&quot;433&quot; data-ke-size=&quot;size16&quot;&gt;네트워크&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-end=&quot;468&quot; data-start=&quot;448&quot; data-ke-size=&quot;size26&quot;&gt;네트워크가 없으면 서버가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 네트워크에 연결되어 있는지 확인하기 위해&lt;br /&gt;가장 먼저 실행한 명령어는 다음이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ip addr&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어는 이 컴퓨터가 어떤 네트워크 인터페이스를 가지고 있고, 어떤 IP 주소를 부여받았는지를 보여준다.&lt;/p&gt;
&lt;p data-end=&quot;640&quot; data-start=&quot;606&quot; data-ke-size=&quot;size16&quot;&gt;출력 결과 중 &lt;b&gt;서버 관점에서 중요한 부분은 하나&lt;/b&gt;다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;wlp2s0 &lt;br /&gt;inet 192.168.45.34/24 dynamic&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;789&quot; data-start=&quot;724&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;789&quot; data-start=&quot;724&quot; data-ke-size=&quot;size16&quot;&gt;wlp2s0는 무선 랜 인터페이스 이름이다. (원래 서버는 유선으로 연결되어 있었음)&lt;br /&gt;아무튼, &lt;b&gt;이 서버가 실제로 연결되어 있는 네트워크의 출입구&lt;/b&gt;다. 여기에 할당된 IP를 분해해 보자.&lt;/p&gt;
&lt;h3 data-end=&quot;829&quot; data-start=&quot;812&quot; data-ke-size=&quot;size23&quot;&gt;192.168.45.34&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;880&quot; data-start=&quot;830&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;855&quot; data-start=&quot;830&quot;&gt;RFC 1918에 정의된 &lt;b&gt;사설 IP&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;880&quot; data-start=&quot;856&quot;&gt;집이나 사무실 내부 네트워크에서만 사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;889&quot; data-start=&quot;882&quot; data-ke-size=&quot;size23&quot;&gt;/24&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;936&quot; data-start=&quot;890&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;902&quot; data-start=&quot;890&quot;&gt;같은 네트워크 대역&lt;/li&gt;
&lt;li data-end=&quot;936&quot; data-start=&quot;903&quot;&gt;192.168.45.0 ~ 192.168.45.255&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;949&quot; data-start=&quot;938&quot; data-ke-size=&quot;size23&quot;&gt;dynamic&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;977&quot; data-start=&quot;950&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;977&quot; data-start=&quot;950&quot;&gt;공유기(DHCP 서버)가 자동으로 IP를 할당&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1001&quot; data-start=&quot;984&quot; data-ke-size=&quot;size26&quot;&gt;여기서 얻을 수 있는 결론&lt;/h2&gt;
&lt;p data-end=&quot;1009&quot; data-start=&quot;1003&quot; data-ke-size=&quot;size16&quot;&gt;이 노트북은&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1067&quot; data-start=&quot;1011&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1032&quot; data-start=&quot;1011&quot;&gt;인터넷에 직접 연결된 서버가 아니다&lt;/li&gt;
&lt;li data-end=&quot;1067&quot; data-start=&quot;1033&quot;&gt;공유기 뒤 &lt;b&gt;사설망(192.168.x.x)&lt;/b&gt; 안에 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1071&quot; data-start=&quot;1069&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 상태의 노트북은 &lt;/span&gt;외부에서 접근할 수 없는 컴퓨터다.&lt;/p&gt;
&lt;p data-end=&quot;1152&quot; data-start=&quot;1111&quot; data-ke-size=&quot;size16&quot;&gt;네트워크 관점에서는 &lt;b&gt;그냥 집 PC와 완전히 동일한 위치&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;p data-end=&quot;1152&quot; data-start=&quot;1111&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;718&quot; data-start=&quot;615&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;666&quot; data-start=&quot;615&quot;&gt;공인 IP&lt;br /&gt;&amp;rarr; 인터넷 라우팅 테이블에 존재&lt;br /&gt;&amp;rarr; 전 세계 어디서든 도달 가능&lt;/li&gt;
&lt;li data-end=&quot;718&quot; data-start=&quot;668&quot;&gt;사설 IP&lt;br /&gt;&amp;rarr; 인터넷 라우팅 대상 아님&lt;br /&gt;&amp;rarr; 내부 네트워크에서만 의미 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 기억해두자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-end=&quot;1179&quot; data-start=&quot;1159&quot; data-ke-size=&quot;size26&quot;&gt;그런데&amp;hellip;. 나는 SSH로 접속했다&lt;/h2&gt;
&lt;p data-end=&quot;1193&quot; data-start=&quot;1181&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;사설망에 있는 컴퓨터는 &lt;/span&gt;원래 외부에서 접근할 수 없어야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1276&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1276&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;그런데 나는 실제로 &lt;b&gt;외부에서 이 서버로 SSH 접속을 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1292&quot; data-start=&quot;1278&quot; data-ke-size=&quot;size16&quot;&gt;왜 이런 일이 가능했을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h2 data-end=&quot;1320&quot; data-start=&quot;1299&quot; data-ke-size=&quot;size26&quot;&gt;공유기 = NAT&lt;/h2&gt;
&lt;p data-end=&quot;1358&quot; data-start=&quot;1322&quot; data-ke-size=&quot;size16&quot;&gt;이 모순을 풀기 위해 먼저 공유기가 어떤 역할을 하는지 보자.&lt;/p&gt;
&lt;p data-end=&quot;1434&quot; data-start=&quot;1360&quot; data-ke-size=&quot;size16&quot;&gt;공유기는 단순한 라우터가 아니다. &lt;b&gt;주소를 변환하는 장치&lt;/b&gt;, 즉 NAT(Network Address Translation)다.&lt;/p&gt;
&lt;p data-end=&quot;1434&quot; data-start=&quot;1360&quot; data-ke-size=&quot;size16&quot;&gt;정확히는&amp;nbsp;&lt;b&gt;사설망과 공인망을 나누는 네트워크 경계(boundary)&lt;/b&gt; 다.&lt;/p&gt;
&lt;p data-end=&quot;1434&quot; data-start=&quot;1360&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1490&quot; data-start=&quot;1436&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1450&quot; data-start=&quot;1436&quot;&gt;집에는 공인 IP 하나&lt;/li&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1451&quot;&gt;내부에는 사설 IP 여러 개&lt;/li&gt;
&lt;li data-end=&quot;1490&quot; data-start=&quot;1469&quot;&gt;공유기가 주소를 변환해 통신을 중계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1503&quot; data-start=&quot;1492&quot; data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같다.&lt;/p&gt;
&lt;p data-end=&quot;1503&quot; data-start=&quot;1492&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1770031624845&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ Internet ]
     &amp;darr;
[ 공인 IP ]
[ 공유기 (NAT) ]
     &amp;darr;
[ 192.168.45.34 ]&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;p data-end=&quot;1648&quot; data-start=&quot;1611&quot; data-ke-size=&quot;size16&quot;&gt;공유기는 기본적으로 &lt;b&gt;외부에서 시작된 연결을 전부 막는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1648&quot; data-start=&quot;1611&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1683&quot; data-start=&quot;1655&quot; data-ke-size=&quot;size26&quot;&gt;포트포워딩&amp;nbsp;&lt;/h2&gt;
&lt;p data-end=&quot;1709&quot; data-start=&quot;1685&quot; data-ke-size=&quot;size16&quot;&gt;여기서 등장하는 것이 &lt;b&gt;포트포워딩&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1742&quot; data-start=&quot;1711&quot; data-ke-size=&quot;size16&quot;&gt;포트포워딩은 공유기에 추가하는 &lt;b&gt;예외 규칙&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1757&quot; data-start=&quot;1744&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1757&quot; data-start=&quot;1744&quot; data-ke-size=&quot;size16&quot;&gt;예를 들면 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot; style=&quot;color: #abb2bf; text-align: left;&quot;&gt;&lt;code&gt;[공인 IP]:22 &amp;rarr; 192.168.45.34:22​&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의미는 단순하다.&lt;/p&gt;
&lt;blockquote data-end=&quot;1852&quot; data-start=&quot;1807&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;1852&quot; data-start=&quot;1809&quot; data-ke-size=&quot;size16&quot;&gt;외부에서 22번 포트로 들어오는 요청은&lt;br /&gt;이 사설 IP 서버로 전달하라.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1868&quot; data-start=&quot;1854&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1868&quot; data-start=&quot;1854&quot; data-ke-size=&quot;size16&quot;&gt;이 규칙이 추가되는 순간,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1912&quot; data-start=&quot;1870&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1886&quot; data-start=&quot;1870&quot;&gt;사설망 안에 있던 컴퓨터가&lt;/li&gt;
&lt;li data-end=&quot;1912&quot; data-start=&quot;1887&quot;&gt;인터넷에서 &lt;b&gt;도달 가능한 대상&lt;/b&gt;이 된다&lt;/li&gt;
&lt;/ul&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;1392&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBGW7E/dJMcaioESni/1diKtRPcoqEmpeeki4DzZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBGW7E/dJMcaioESni/1diKtRPcoqEmpeeki4DzZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBGW7E/dJMcaioESni/1diKtRPcoqEmpeeki4DzZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBGW7E%2FdJMcaioESni%2F1diKtRPcoqEmpeeki4DzZk%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;610&quot; height=&quot;353&quot; data-origin-width=&quot;1392&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온프레미스 서버로 처음 서비스 구동시킬 때 다들 한번씩 겪게 되는 삽질...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1938&quot; data-start=&quot;1919&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-end=&quot;1795&quot; data-start=&quot;1707&quot; data-ke-size=&quot;size16&quot;&gt;이&amp;nbsp;노트북은&amp;nbsp;OS&amp;nbsp;관점에서는&amp;nbsp;이미&amp;nbsp;서버였다.&lt;br /&gt;하지만&amp;nbsp;네트워크&amp;nbsp;관점에서는&amp;nbsp;식별되지도,&amp;nbsp;도달되지도&amp;nbsp;않는&amp;nbsp;존재였다.&lt;br /&gt;&lt;br /&gt;사설&amp;nbsp;IP는&amp;nbsp;내부&amp;nbsp;식별자일&amp;nbsp;뿐,&amp;nbsp;인터넷&amp;nbsp;기준의&amp;nbsp;주소가&amp;nbsp;아니다.&lt;br /&gt;&lt;br /&gt;포트포워딩은 이 컴퓨터에 &amp;ldquo;외부에서&amp;nbsp;들어오는&amp;nbsp;길&amp;rdquo;을&amp;nbsp;처음으로&amp;nbsp;만들어준&amp;nbsp;설정이었고,&lt;br /&gt;그&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;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 여기서 끝일까? 아직 아니다.&lt;/p&gt;
&lt;p data-end=&quot;247&quot; data-start=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;247&quot; data-start=&quot;196&quot; data-ke-size=&quot;size16&quot;&gt;하루는 외부에서 이 서버로 접속하려고 했다.&lt;br /&gt;노트북이 아닌, 전혀 다른 네트워크에서였다.&lt;/p&gt;
&lt;p data-end=&quot;268&quot; data-start=&quot;249&quot; data-ke-size=&quot;size16&quot;&gt;SSH 명령을 치려다 문득 멈췄다.&lt;/p&gt;
&lt;blockquote data-end=&quot;283&quot; data-start=&quot;270&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;283&quot; data-start=&quot;272&quot; data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;어디로 접속하지?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;363&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;363&quot; data-start=&quot;285&quot; data-ke-size=&quot;size16&quot;&gt;이 서버의 IP는 192.168.45.34였다.&lt;br /&gt;하지만 이 주소는 인터넷에 존재하지 않는다.&lt;br /&gt;외부에서는 이 서버를 찾을 수 없다. 그 순간 깨달았다.&lt;/p&gt;
&lt;blockquote data-end=&quot;413&quot; data-start=&quot;377&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;p data-end=&quot;413&quot; data-start=&quot;379&quot; data-ke-size=&quot;size16&quot;&gt;인터넷에서 이 서버는&lt;br /&gt;다른 주소로 불리고 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;441&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;441&quot; data-start=&quot;415&quot; data-ke-size=&quot;size16&quot;&gt;인터넷에서 보이는 주소는&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;563&quot; data-start=&quot;443&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;476&quot; data-start=&quot;443&quot;&gt;외부에서 접속할 때 사용하는 &lt;b&gt;유일한 식별자&lt;/b&gt;이며&lt;/li&gt;
&lt;li data-end=&quot;501&quot; data-start=&quot;477&quot;&gt;포트포워딩 규칙이 매달리는 기준점이고&lt;/li&gt;
&lt;li data-end=&quot;527&quot; data-start=&quot;502&quot;&gt;방화벽과 보안 정책이 적용되는 경계이며&lt;/li&gt;
&lt;li data-end=&quot;563&quot; data-start=&quot;528&quot;&gt;이 서버가 &lt;b&gt;어디까지 노출돼 있는지&lt;/b&gt;를 결정하는 선이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;607&quot; data-start=&quot;565&quot; data-ke-size=&quot;size16&quot;&gt;이 주소를 모르면 서버에 들어갈 수도 없고,&lt;br /&gt;서버를 지킬 수도 없다. 다음 글에서는 이 서버가 인터넷에서 실제로 어떤 주소로 보였는지,&lt;br /&gt;그리고 그 주소 하나로 무엇이 가능해지고 무엇이 위험해졌는지를 살펴본다.&lt;/p&gt;
&lt;p data-end=&quot;578&quot; data-start=&quot;497&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/172</guid>
      <comments>https://tobetirdev.tistory.com/172#entry172comment</comments>
      <pubDate>Mon, 2 Feb 2026 21:09:57 +0900</pubDate>
    </item>
    <item>
      <title>사용중인 리눅스 서버 뜯어보기 (Ubuntu 22.04 LTS)</title>
      <link>https://tobetirdev.tistory.com/171</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;러닝앱 프로젝트에서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCP로 마이그레이션되기 전 개발 서버로 사용된 우리 노트북을 한번 뜯어보자. (몇달째 방치된...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ssh로 서버에 접속하여 아래 명령어를 통해 사양을 파악해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS 정보&lt;/p&gt;
&lt;pre id=&quot;code_1770027551005&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;uname -a
lsb_release -a
cat /etc/os-release&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;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1770027584985&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;df -h
lsblk&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리/디스크&lt;/p&gt;
&lt;pre id=&quot;code_1770027597518&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;free -h
top&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력을 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Ubuntu 22.04.2 LTS (Jammy) -&amp;gt;&amp;nbsp; (LTS: 장기 지원 우분투 서버)&lt;br /&gt;Linux 5.15.0-164-generic&amp;nbsp; -&amp;gt;&amp;nbsp; (커널 버전 (장애&amp;middot;드라이버&amp;middot;보안 관련)&lt;br /&gt;x86_64 (아키텍쳐)&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;/dev/mapper/ubuntu--vg-ubuntu--lv 98G 35G 58G 38% /&amp;nbsp; &amp;nbsp;-&amp;gt;&amp;nbsp; 사용률 38%&lt;br /&gt;(루트 파티션은 LVM으로 구성)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 루트 파티션(/)이란?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 파티션은 &lt;b&gt;디스크를 논리적으로 나눈 영역이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;118&quot; data-start=&quot;72&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;92&quot; data-start=&quot;72&quot;&gt;리눅스 파일시스템의 &lt;b&gt;최상위&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;118&quot; data-start=&quot;93&quot;&gt;서버가 부팅되고 동작하는 &lt;b&gt;기본 토대&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;257&quot; data-start=&quot;194&quot;&gt;/ 가 꽉 차면
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;257&quot; data-start=&quot;209&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;218&quot; data-start=&quot;209&quot;&gt;로그인 안 됨&lt;/li&gt;
&lt;li data-end=&quot;232&quot; data-start=&quot;221&quot;&gt;서비스 기동 실패&lt;/li&gt;
&lt;li data-end=&quot;257&quot; data-start=&quot;235&quot;&gt;로그 기록 불가&amp;rarr;&lt;b&gt;&amp;nbsp;장애&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;286&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;그래서 운영자는 항상 / 사용률을 본다고 한다.&lt;/p&gt;
&lt;p data-end=&quot;286&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;286&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;286&quot; data-start=&quot;259&quot; data-ke-size=&quot;size16&quot;&gt;다음으로 메모리&lt;/p&gt;
&lt;blockquote data-end=&quot;286&quot; data-start=&quot;259&quot; data-ke-style=&quot;style3&quot;&gt;Mem: 15Gi &lt;br /&gt;total used: 2.2Gi &lt;br /&gt;available: 12Gi&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;939&quot; data-start=&quot;920&quot; data-ke-size=&quot;size18&quot;&gt;핵심은 available?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1017&quot; data-start=&quot;940&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;967&quot; data-start=&quot;940&quot;&gt;Linux는 &lt;b&gt;메모리를 캐시로 적극 사용&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;993&quot; data-start=&quot;968&quot;&gt;used 보고 &amp;ldquo;메모리 부족&amp;rdquo; 판단하면 안되고,&amp;nbsp;&lt;b&gt;available 기준으로 판단&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;왜일까? Linux는 메모리를 &lt;b&gt;놀리면 손해&lt;/b&gt;라고 본다.&lt;/p&gt;
&lt;p data-end=&quot;197&quot; data-start=&quot;169&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Linux의 기본 철학은&amp;nbsp;&lt;b&gt;&quot;메모리를 비워두는 건 낭비&quot; 다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;컴퓨터를 사용하다 보면 디스크에서 파일을 읽는 작업이 매우 자주 일어난다.&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;디스크는 메모리보다 훨씬 느리기 때문에, Linux는 한 번 읽은 데이터를 메모리에 보관해둔다.&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;나중에 같은 데이터가 필요하면 느린 디스크 대신 빠른 메모리에서 바로 가져다 쓸 수 있도록.&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; 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;남는 메모리가 있으면 Linux는 이걸 캐시 용도로 채운다. 그래서 메모리 사용률이 80~90%로 높게 나오는 게 정상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 문제가 아니라 오히려 시스템이 똑똑하게 작동하고 있다는 뜻!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 메모리를 더 필요로 하면? Linux는 캐시로 쓰던 메모리를 즉시 비워서 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 &quot;있으면 좋고 없어도 괜찮은&quot; 데이터니까.&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;260&quot; data-start=&quot;256&quot; data-ke-size=&quot;size16&quot;&gt;그래서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;307&quot; data-start=&quot;261&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;284&quot; data-start=&quot;261&quot;&gt;메모리가 꽉 차 보이는 게 &lt;b&gt;정상&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;307&quot; data-start=&quot;285&quot;&gt;캐시 = 언제든 버릴 수 있는 메모리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&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;used&lt;/b&gt;: 현재 사용 중인 메모리 (캐시 포함) &amp;rarr; 높아도 정상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;free&lt;/b&gt;: 완전히 비어있는 메모리 &amp;rarr; 보통 매우 적음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;available&lt;/b&gt;: 지금 당장 쓸 수 있는 메모리 &amp;rarr; &lt;b&gt;이게 핵심!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;available&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;고 한다... 아니 몰랐네 이걸&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로세스 &amp;amp; 서비스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 프로세스 &amp;amp; 서비스 관련을 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 서버에서 가장 먼저 확인해야 하는 것은 &amp;lsquo;어떤 프로세스가 리소스를 잡아먹고 있는가&amp;rsquo; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 명령어를 통해 현황을 확인해본다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ps aux --sort=-%cpu | head &lt;br /&gt;ps aux --sort=-%mem | head &lt;br /&gt;systemctl list-units --type=service --state=running&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 기준 상위 프로세스 출력을 보면 아래와 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;/usr/bin/cadvisor &lt;br /&gt;java -jar ngrinder-controller &lt;br /&gt;docker-driver &lt;br /&gt;loki &lt;br /&gt;mongod &lt;br /&gt;prometheus &lt;br /&gt;redis &lt;br /&gt;node_exporter&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;231&quot; data-start=&quot;212&quot; data-ke-size=&quot;size23&quot;&gt;출력으로 알 수 있는 것&amp;nbsp;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;444&quot; data-start=&quot;232&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;290&quot; data-start=&quot;232&quot;&gt;&lt;b&gt;Docker 기반 서버&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;290&quot; data-start=&quot;253&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;290&quot; data-start=&quot;253&quot;&gt;dockerd, containerd, cadvisor&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;357&quot; data-start=&quot;291&quot;&gt;&lt;b&gt;모니터링 스택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;357&quot; data-start=&quot;307&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;357&quot; data-start=&quot;307&quot;&gt;prometheus, loki, grafana, node_exporter&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;358&quot;&gt;&lt;b&gt;데이터 계층&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;392&quot; data-start=&quot;373&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;373&quot;&gt;mongod, redis&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;444&quot; data-start=&quot;393&quot;&gt;&lt;b&gt;메인 애플리케이션&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;444&quot; data-start=&quot;411&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;444&quot; data-start=&quot;411&quot;&gt;java -jar ngrinder-controller&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;455&quot; data-start=&quot;446&quot; data-ke-size=&quot;size16&quot;&gt;이 서버는:&lt;/p&gt;
&lt;blockquote data-end=&quot;488&quot; data-start=&quot;456&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;488&quot; data-start=&quot;458&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모니터링 + 테스트 컨트롤러 + 컨테이너 호스트 임을 알 수 있다! (와!)&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 메모리 기준 상위 프로세스다. 출력을 보면 아래와 같다.&lt;/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;java (RSS ~600~700MB) &lt;br /&gt;grafana &lt;br /&gt;mongod &lt;br /&gt;loki&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;892&quot; data-start=&quot;821&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;841&quot; data-start=&quot;821&quot;&gt;Java,DB, 모니터링 도구들이 메모리를 많이 사용하고 있음을 알 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;921&quot; data-start=&quot;899&quot; data-ke-size=&quot;size26&quot;&gt;systemctl 결과 해석&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;docker.service&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;running &lt;br /&gt;containerd.service&amp;nbsp; &amp;nbsp; running &lt;br /&gt;ssh.service&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;running &lt;br /&gt;rsyslog.service&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; running&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Docker 기반 서버이며,&lt;br /&gt;nGrinder Controller(Java)를 중심으로&lt;br /&gt;Prometheus / Loki / Grafana 모니터링 스택과&lt;br /&gt;MongoDB, Redis가 함께 동작 중&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 정리할 수 있겠다...!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;(VPN, Docker, CI/CD)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DevOps</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/171</guid>
      <comments>https://tobetirdev.tistory.com/171#entry171comment</comments>
      <pubDate>Mon, 2 Feb 2026 19:55:42 +0900</pubDate>
    </item>
    <item>
      <title>[나만무] SnapAgent의 기술적 접근</title>
      <link>https://tobetirdev.tistory.com/170</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tobetirdev.tistory.com/169&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tobetirdev.tistory.com/169&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765777240417&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;[나만무] SnapAgent의 의사 결정 과정&quot; data-og-description=&quot;https://tobetirdev.tistory.com/168 [나만무] 10기 4조 팀장의 후기** 개인의 의견이 다수 포함되어 있음 **** 프로젝트에 대한 포스팅은 나중에 진행 ** 대략 2달만의 포스팅...ㅋㅋㅋ핀토스가 어떻게 끝났는&quot; data-og-host=&quot;tobetirdev.tistory.com&quot; data-og-source-url=&quot;https://tobetirdev.tistory.com/169&quot; data-og-url=&quot;https://tobetirdev.tistory.com/169&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bdFiET/hyZPv6oHFb/Nk86961v9b2lQBJ4U4uFHk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/TnNJp/hyZPxpBsGG/4VLUsOjPcCOQVyrMkgOcOk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rqwnP/hyZPoMXBKB/ITduBIeUcUUhnxPNG37DT0/img.png?width=1373&amp;amp;height=744&amp;amp;face=0_0_1373_744&quot;&gt;&lt;a href=&quot;https://tobetirdev.tistory.com/169&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tobetirdev.tistory.com/169&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bdFiET/hyZPv6oHFb/Nk86961v9b2lQBJ4U4uFHk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/TnNJp/hyZPxpBsGG/4VLUsOjPcCOQVyrMkgOcOk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/rqwnP/hyZPoMXBKB/ITduBIeUcUUhnxPNG37DT0/img.png?width=1373&amp;amp;height=744&amp;amp;face=0_0_1373_744');&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;[나만무] SnapAgent의 의사 결정 과정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://tobetirdev.tistory.com/168 [나만무] 10기 4조 팀장의 후기** 개인의 의견이 다수 포함되어 있음 **** 프로젝트에 대한 포스팅은 나중에 진행 ** 대략 2달만의 포스팅...ㅋㅋㅋ핀토스가 어떻게 끝났는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tobetirdev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나만무에 대한 마지막 포스팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 SnapAgent를 제작하면서 어떠한 기술적 접근과 고민을 했을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;노드 구현&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 AI Agent를 만들기 위해, 어떤 노드가 필요했고 그를 어떻게 구현할지부터 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 RAG 파이프라인을 담당할 지식 노드가 필요했고, 또한 LLM을 호출하여 임베딩된 결과를 바탕으로 답변할 LLM 노드도 필요했다.&lt;/p&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;포트로 query를 받아 벡터 연산을 처리하고 documents/context를 출력 포트에 저장한다.&lt;br /&gt;LLM이 템플릿 없이도 바로 context를 읽을 수 있게 포트 이름을 고정한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 노드&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;입력 포트(query, context, variables등)로 들어온 값을 템플릿에 채워 LLM을 호출한다. 출력은&amp;nbsp;&lt;br /&gt;response 포트로 VariablePool에 저장하며, 토큰과 모델 메타데이터도 실행 기록으로 저장한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 노드가 13개 정도 되기 때문에,&amp;nbsp; 간단히 요약하자면&amp;nbsp;노드는 &amp;ldquo;입력 포트 &amp;rarr; 서비스 호출/계산 &amp;rarr; 출력 포트&amp;rdquo; 패턴을 따르며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 업데이트는 Assigner, 흐름 제어는 IfElse/Classifier, 외부 연동은 HTTP, 결과 확정은 Answer/End로 구현하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;노드 - 엣지 구조 워크플로우&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워크플로우의 전체적인 실행 흐름은 다음과 같다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 클라이언트가 보낸 그래프 JSON에서 nodes/edges를 꺼낸다.&lt;br /&gt;2. Validator로 그래프가 괜찮은지 검사하고, 위상정렬로 실행 순서를 얻는다(선행 노드가 끝나야 후행 노드가 실행됨)&lt;br /&gt;3. 노드 인스턴스를 만들고, 엣지 정보를 바탕으로 입력/출력 관계를 연결한다.&lt;br /&gt;4. 노드를 순서대로 실행한다. 노드가 만든 결과가 저장되고, 다음 노드가 그 결과를 꺼내쓴다.&lt;br /&gt;5. End 노드까지 실행되면 response를 최종 응답으로 반환합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BaseNode를 부모 노드로 설정&lt;/b&gt;하여, 모든 노드가 공통으로 갖는 속성(입출력 연결, 상태)를 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실행 엔진이 노드 타입을 몰라도, BaseNode를 사용하면 &lt;b&gt;공통 인터페이스로 동일하게 호출할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 및 해결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드들이 컨텍스트 내부 구조와 &lt;b&gt;키에 강하게 의존&lt;/b&gt;해 결합도가 커졌다. V2 버전은 포트/변수/Validator로 &amp;ldquo;이 포트에 이런 타입이 들어와야 한다&amp;rdquo;는 계약을 명시하고, 서비스는 ServiceContainer를 통해 주입해 노드-컨텍스트 결합을 낮추도록 재설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 어떻게 V2 Validator가 포트/매핑을 검사하는가?&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;노드 정의에 입력/출력 포트가 있는지 확인하고, 엣지에 source_port/target_port가 비어 있으면 에러로 막는다.&lt;br /&gt;입력이 필요한 노드(LLM 등)는 variable_mappings가 채워져 있는지 검사해 &amp;ldquo;어떤 포트가 어디 값을 읽는지&amp;rdquo;를 확인한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 검사한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-pm-slice=&quot;1 1 []&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실행 엔진(V2)에서 분기/미선택 경로 처리 흐름&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적 챌린지에도 언급한 분기/미선택 경로 처리 흐름이다. 문제가 되는 상황은 분기 발생시 미선택 경로의 incoming_count를 처리하지 않으면, 그 다음 노드가 실행되지 않는다라는 문제다.&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;978&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1Abzl/dJMcadmYlpt/M8fnhNs3x4NtqkM4bkTvh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1Abzl/dJMcadmYlpt/M8fnhNs3x4NtqkM4bkTvh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1Abzl/dJMcadmYlpt/M8fnhNs3x4NtqkM4bkTvh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1Abzl%2FdJMcadmYlpt%2FM8fnhNs3x4NtqkM4bkTvh1%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;978&quot; height=&quot;704&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;실행 순서는 여전히 위상 정렬이지만, IfElse/Classifier 같은 분기 노드는 edge_handles로 &amp;ldquo;선택된 엣지&amp;rdquo;를 알려준다.&lt;br /&gt;선택된 엣지들만 실행 큐에 반영하고, 미선택 분기의 downstream 노드 incoming_count를 0으로 만들면서도,&lt;br /&gt;ready_queue에는 올리지 않아 &amp;ldquo;타지 않은 브랜치&amp;rdquo;를 건너뛰도록 했다.&lt;br /&gt;따라서 조건 분기가 명확히 적용되고, 잘못된 브랜치가 실행되거나 순서가 꼬이는 일을 방지하였다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;임베딩 처리&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 모델에 따라 다르지만, 임베딩은 무겁고 오래 걸리는 작업이다. S3 다운로드 -&amp;gt; 파싱 -&amp;gt; 청킹 -&amp;gt; Bedrock 호출 -&amp;gt; pgvector 저장까지 한번에 하면 API 워커의 이벤트 루프를 오래 잡아먹어 요청이 지연될 수 있다. 따라서 업로드된 문서는 API에서 바로 임베딩하지 않고 SQS 큐에 작업을 넣고, 별도 프로세스가 폴링하여 처리하도록 했다.&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;b&gt;비동기 처리와 한계&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서비스는 FastAPI 기반 async 서버이다. DB를 동기호출로 묶어두면, await을 하는 동안 이벤트 루프가 막혀 다른 요청을 받지 못해 동시 처리량이 낮아질 우려가 있었다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;물론, 우리 서비스를 실제로 많은 사용자가 이용하도록 설계를 끝마치고 실제 운영을 해본 것은 아니다.&amp;nbsp;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다만 트래픽이 늘어날때 유연하게 대응할 수 있는 시스템으로 설계하고자 했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;워크플로우가 한번 돌때, 실행 기록, 노드 기록등 여러 DB I/O가 발생한다. 이걸 비동기로 돌려야 다른 사용자 요청이나 응답이 그 사이에 실행되어 응답 지연과 타임아웃을 줄일 수 있을 것이라 생각했다.&amp;nbsp;&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: #000000;&quot;&gt;실제로 실행기록 저장이나 로그 발행(SQS)등은 실행 완료 후 비동기로 송신하도록 분리하여 응답 시간을 14%가량 줄인 효과가 있었다.&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;span style=&quot;color: #000000;&quot;&gt;특히 외부 호출인 AWS LLM/Bedrock은 기본 SDK인 boto3이 동기 방식이라 이벤트 루프를 막을 수 있어서, 동기 호출을 스레드 풀에 던져놓고, 그 태스크를 await 하는 방식으로 구현했다. 비용문제와 에러 대비를 위해 &lt;/span&gt;asyncio.Semaphore(10)으로 동시 호출을 Rate Limit(초당 약 13회) 이하로 묶어 서비스의 안정성을 확보하기도 했다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 그런데...&lt;/span&gt;&lt;/b&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: #000000;&quot;&gt;사실 await, 코루틴에 대해서 확실히 알고 시작한것도 아니고(파이썬 개발 자체가 처음이라) 그냥 서비스를 모두 동기로 돌릴까도 생각했지만, 그래도 품질과 안정성, 확장성을 고려해서 한번 await를 적극적으로 이용해보자 하는 마음에 벌인 일이다.&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: #000000;&quot;&gt;이 판단이 맞는 판단일까? 그냥 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;잘못된 설계, &lt;/span&gt;오버엔지니어링이자 &quot;저는 ~~해봤습니다&quot; 하기 위한 시도라고 생각할 수도 있을 것 같다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&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;그러나 서비스 특성상, DB/네트워크 I/O가 많고, 워크플로우 실행중 외부 호출이 여러번 발생하는 경우, 유저가 많아진다면 비동기 처리가 적합하다는 생각은 하고 있다.&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: #000000;&quot;&gt;실제 유저가 100명, 1000명이 된다면 어떻게 요청을 처리할지에 대한 생각을 꾸준히 하면서, 앞으로도 계속 조금씩 개선할 생각이라 잘 배우고 다시 도전한다면 좋은 결과가 있을 것 같다...!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;캐시&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 LLM 호출 비용&amp;middot;지연이 크고, 같은 질의/유사 질의가 반복될 가능성이 높아서 먼저 캐시를 뒤져보고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;없으면 LLM에 요청하는 흐름으로 응답 시간을 줄이고 비용을 절감하려고 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 키 캐시만으로는 프롬프트가 조금만 달라도 재사용이 안 돼서, 의미 기반(시맨틱) 재사용도 함께 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하지만 이는 정말 신중히 선택해야 할 전략이라는 생각이 들었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리팀은 스타트업 쇼핑몰 개발자라는 페르소나를 설정하여 그에 적합한 시나리오를 구성했다.&lt;/p&gt;
&lt;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;예를 들어 &quot;&lt;b&gt;20대 여자 옷 추천&quot;과, &quot;20대 남자 옷 추천&quot;&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;또한 가이드라인이 바뀔때, 즉 RAG 문서의 내용이 업데이트 된다면 캐시에는 여전히 예전 데이터가 남아있어 부정확한 응답을 내놓을 가능성이 있었다. 따라서 TTL 설정도 필요하고, 여하튼 전략이 매우 달라져야 한다. 따라서 이를 &quot;초보 개발자도 설정할 수 있도록 많은 파라미터를 설정하는 기능&quot;이 있어야 했는데... 한달은 너무 짧은 것 같다 ㅎ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;인프라&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;우선 로컬 환경은 도커 컴포즈 파일을 만들어 팀원들에게 동일 환경에서 테스트를 할 수 있도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 pgvector 대신 chromaDB를 사용했었는데, 프로덕션으로 올라가면서 운영/모니터링을 RDS 하나로 통일하고 싶어 변경했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포는 GitHub Actions가 Docker 빌드&amp;rarr;ECR/Hub 푸시&amp;rarr;ECS 배포했고, 프론트는 Vercel로 배포했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 도커 컨테이너를 ECS Fargate에 올려 워커 프로세스 단위로 수평 확장했고, 무거운 임베딩/배치 작업은 아예 별도 워커 프로세스로 분리(SQS 폴링)해 API 워커를 가볍게 유지했다.&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;업로드 문서는 S3로 올렸으며, 별도 워커가 다운로드&amp;rarr;파싱&amp;rarr;청킹&amp;rarr;Bedrock 임베딩&amp;rarr;pgvector에 저장하도록 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 워커는 DB/pgvector/Redis에 await으로 접근해 이벤트 루프를 막지 않도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시와 외부 호출은 ElastiCache에 키 캐시+시맨틱 캐시를 두어 LLM 호출 전에 먼저 캐시 적용을 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Bedrock/OpenAI/Anthropic 호출은 동기 SDK가 많아 스레드풀로 오프로딩한 뒤 await해 루프를 비워두었다(임베딩/LLM)&lt;/p&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 Lambda도 사용했다. 흐름은 아래와 같다&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;1. API/워크플로우 실행 중 나온 실행 로그&amp;middot;토큰 사용량을 바로 DB에 쓰지 않고 SQS 큐(로그 큐, 사용량 큐)에 넣는다&lt;br /&gt;2. 메시지가 쌓이면 Lambda가 자동으로 깨어나 PostgreSQL에 적재&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 피크 시간에도 API/ECS를 덜 건드리고, Lambda가 필요한 만큼만 잠깐 늘어났다가 줄어든다. 실패하면 DLQ로 빠져서 나중에 재처리할 수 있는 환경을 구축해 놓았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에도 Export/Import 기능이라던지 다른 문제도 있지만, 내가 담당한 기술은 아니기 때문에 이정도로 적어둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 이 프로젝트를 사이드 프로젝트로 계속 폴리싱할 계획이기 때문에, 간간히 들러 업데이트를 진행할 예정!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>크래프톤정글10기/나만무</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/170</guid>
      <comments>https://tobetirdev.tistory.com/170#entry170comment</comments>
      <pubDate>Mon, 15 Dec 2025 17:05:37 +0900</pubDate>
    </item>
    <item>
      <title>[나만무] SnapAgent의 의사 결정 과정</title>
      <link>https://tobetirdev.tistory.com/169</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tobetirdev.tistory.com/168&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tobetirdev.tistory.com/168&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1765532053375&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;[나만무] 10기 4조 팀장의 후기&quot; data-og-description=&quot;** 개인의 의견이 다수 포함되어 있음 **** 프로젝트에 대한 포스팅은 나중에 진행 ** 대략 2달만의 포스팅...ㅋㅋㅋ핀토스가 어떻게 끝났는지 복기할 여유도 없이 바로 나만무 프로젝트가 시작되&quot; data-og-host=&quot;tobetirdev.tistory.com&quot; data-og-source-url=&quot;https://tobetirdev.tistory.com/168&quot; data-og-url=&quot;https://tobetirdev.tistory.com/168&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/LoLbq/hyZPueODNO/evsBsoBvRkKVx6laGPDWM0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cLikFx/hyZO9XChYb/v1uKChZ7M6FDs1I4hS62Kk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://tobetirdev.tistory.com/168&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://tobetirdev.tistory.com/168&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/LoLbq/hyZPueODNO/evsBsoBvRkKVx6laGPDWM0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cLikFx/hyZO9XChYb/v1uKChZ7M6FDs1I4hS62Kk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[나만무] 10기 4조 팀장의 후기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;** 개인의 의견이 다수 포함되어 있음 **** 프로젝트에 대한 포스팅은 나중에 진행 ** 대략 2달만의 포스팅...ㅋㅋㅋ핀토스가 어떻게 끝났는지 복기할 여유도 없이 바로 나만무 프로젝트가 시작되&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;tobetirdev.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트는 정말 기획이 힘들었던 프로젝트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 글에서 언급했듯이, 도메인 지식이 없는 채로 프로젝트를 시작했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜, 도메인 지식이 없는 프로젝트를 시작했을까??&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 기획 주차&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코치님의 피드백&lt;/b&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;레포지토리에서 PR시에 코드변경점을 시각화하여, AI 시대에 올라간 코드 생산성을 뒷받침하기 위해&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;거부당했다&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;p data-ke-size=&quot;size16&quot;&gt;우리는 당연히(?) 반발했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 반발까지는 아니고 우리 서비스의 가치를 어떻게든 증명하여 코치님을 설득하려고 했지만, 한계는 명확해보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어찌보면 당연한 결과일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서비스에 대한 좁은 시야&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 아직 유저가 사용할만한 서비스를, 상용 가능한 서비스를 만들어 본 적이 없다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그말인 즉, 서비스에 대한 시야 자체가 좁다는 소리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의를 거듭했지만 모두가 만족할 만한 기획은 나오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 코치님의 도움을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;시각화를 해볼거면, 요즘 뜨고 있는 AI 쪽은 어떻습니까?&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 랭체인으로 AI 서비스를 구축하려는 시도가 늘고 있지만, 이를 시각화 툴로 제공하는 서비스는 잘 없는 것 같다 하시며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 주제를 추천해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;'그럼 AI 관련 기능들을 유저가 접근하기 쉽게, 예쁘게 정리해서 웹으로 제공하면 되는건가?'&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 AI에 대해 몰랐다.&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;AI에 대해 아는 것이라곤 Chat GPT, Claude, Gemini 정도?&lt;br /&gt;거기에 MCP를 이용해 생산성을 높이는 코딩이 가능하다... 였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 AI Agent인지 뭔지 모르겠고, 일단 가장 간단한 챗봇을 어떻게 만드는지부터 알아야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;챗봇은 그냥 gpt 아닌가...? &lt;br /&gt;그런데 공용 LLM이니까 새로운 지식을 학습하지 못하는구나&lt;br /&gt;그럼 새로 문서를 가지고 어떤 과정을 거쳐 그걸 LLM이 알아듣는거지?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 RAG에 대해 알아보고, 임베딩이 뭔지 학습해 나갔다.&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;RAG, VectorDB, Prompt, Parameter, Embedding, Query ....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점점 모르는 단어가 늘어갔고, 각 용어의 개념을 찾아보며 동작 원리를 알아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그럼 프롬프트가 쿼리야?&lt;br /&gt;유저 쿼리가 사용자의 입력을 말하는건가?&lt;br /&gt;그럼 시스템 프롬프트는 뭐지?&amp;nbsp;&lt;br /&gt;임베딩이 어떻게 동작하지? 컨텍스트는 뭐고 청크는 또 뭐야....&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀이 기본적인 지식 도메인은 전부 함께 알아야 한다라고 생각했기에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새벽까지 함께 공부하면서 RAG 동작 원리에 대한 싱크를 맞추었다.&lt;/p&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;유저가 쉽게 서비스를 이용하기 위해 &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;&lt;u&gt;이 생각은 나중에 큰 혼란을 가져오게 된다....&lt;/u&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;아무튼 그 결과, 그동안 학습한 RAG 기반 서비스를 Form 형식으로 제공하는 초안이 나왔다.&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;1373&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkDQBR/dJMcajgpwqj/6cEqKybO6ysK70FUbIws3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkDQBR/dJMcajgpwqj/6cEqKybO6ysK70FUbIws3K/img.png&quot; data-alt=&quot;믿기 힘든 UI 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkDQBR/dJMcajgpwqj/6cEqKybO6ysK70FUbIws3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkDQBR%2FdJMcajgpwqj%2F6cEqKybO6ysK70FUbIws3K%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;670&quot; height=&quot;363&quot; data-origin-width=&quot;1373&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;믿기 힘든 UI 상태&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;고안한 기능은 RAG 기반의 챗봇, 이후의 Prompt 테스트, 성능 평가 그리고 비용 통계를 제공하는 서비스였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;돌이켜 생각해보니 이 정도도 굉장히 복잡하지 않나?&amp;nbsp;&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;아무튼 이런 과정들을 거치며 조금씩 도메인 지식을 갖추었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 도메인을 공부하느라 시간이 부족했고, PPT도 제작하지 못한 채 대강의 UI만 excalidraw로 그려가서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이딴 식으로 할거면 하지 마세요&quot; 라는 피드백을 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그치만 도메인 모르면 아무것도 못하는 걸...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;s&gt;(후에 코치님은 이렇게까지 AI 지식이 없으리라곤 생각을 미처 못했다고 하셨다.... 저흰 정말 맨땅에 헤딩이었습니다ㅎㅎ)&lt;/s&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. MVP 개발 1주차&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차는 MVP 기능을 개발하는 시간이다.&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;Dify, Botpress, NotebookLM...&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;우리는 우선 Dify의 방식대로 챗봇을 한번 만들어보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 유저 입장에서 가장 접근이 쉬운 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Form&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;코치님들의 피드백&amp;nbsp;&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;우선 잘했고, 기능도 잘 동작하기는 하는데.....&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;노드를 유저가 직접 만들어야지 왜 Form 형식으로 받아 생성하는 거죠?&amp;nbsp; 유저가 자유롭게 쓸 수 있게 해야죠.&lt;br /&gt;이 서비스가 과연 유저가 이걸 사용하기 쉬울까요?&lt;br /&gt;문서는 DB에서도 가져오는 건가요? 보안 위험은요?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피드백 중 1,2번을 보면 뭔가 의아할 것이다. 조금 엇갈리는 피드백이라 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;유저에게 자율성을 보장해야 한다 VS 애초에 코드를 잘 모르는 사람이 이걸 쓴다고?&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우리 서비스의 의도를 명확히 전달하지 못했다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;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;우리는 서비스 이용 대상을 초보 개발자, 혹은 아예 비개발자로 잡았다.&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;오히려 &lt;b&gt;가독성이 떨어지고 UI가 구려질 것 &lt;/b&gt;같은 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 결국 챗봇에 머무르는 서비스가 아니라 우리 서비스가 최종적으로 유저에게 어떠한 가치를 제공할지에 대한&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리가 제대로 되지 않아서, 성공적인 발표를 하지 못했던 것 같다.&lt;/p&gt;
&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;b&gt;3. MVP 2주차&lt;/b&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;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;span style=&quot;color: #000000;&quot;&gt;동작원리까지&lt;/span&gt; 이해하지 못했고, 결국 서비스에 대한 방향을 하나로 모으는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;소통 비용이 너무 많이 들었다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간을 아껴야 하는 나만무이지만 함께 싱크를 맞춰야 할 때는 모두가 같은 작업에 참여할 필요도 있다는 걸 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 온프레미스 환경에서 도커 컴포즈로 컨테이너 다 때려박은거 확장하고 Bedrock 추가하느라 시간이 없었기도 했지만...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당한 소통비용을 지불하며 우리는 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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;거의 1주일동안 뭐한거에요?&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에도 날 선 피드백이 들어왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한편으론 이런 생각도 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이번엔 진짜 거의 다 결정되었는데, 하루 이틀만 더 있었으면 진짜 제대로 구현할 수 있는데...&lt;br /&gt;왜 매주 발표(그것도 PPT까지)에 이상한 일정 잡아서 구현할 시간 뺏어가는거야!&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이내 잘못된 생각이라는 걸 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매주 발표는 10기 교육생의 중간 점검, 피드백이란 것도 있겠지만&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;결국 코치님이 교육장에 직접 오셔서 서비스의 컨셉에 대해 대화를 나눴고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리도 이제는 피드백을 정말 제대로 반영할 준비가 되어있어서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜진짜진짜진짜진짜 구현만 남았다.&lt;/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;b&gt;4. 폴리싱 1주차&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현의 연속이었다.&amp;nbsp; IF/ELSE 노드, 대화변수 설정 및 할당기, 뉴스 검색 노드 등을 구현했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 바탕으로 다음과 같은 시나리오를 구상했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;워크플로우 실행 -&amp;gt; 유저 입력 -&amp;gt; 입력에 따른 뉴스 검색 및 LLM으로 정리 -&amp;gt; 유저 피드백(마음에 드는지)&lt;br /&gt;마음에 드는 경우 -&amp;gt; 자동으로 SNS 스타일로 변환&lt;br /&gt;마음에 들지 않는 경우 -&amp;gt; 자동으로 다른 뉴스 검색 결과를 찾아 LLM으로 정리한뒤 다시 유저 피드백 요구&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 인프라를 확장하고 워크플로우 버전관리 / 웹 앱 및 임베딩 코드 배포관리를 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 마켓플레이스 게시 기능과 워크플로우 Import/Export를 구현해서 에이전트끼리 연결할 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Agent를 마음껏 만들고 테스트 및 버전 관리할 수 있는 시각 워크플로우 런타임&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 4주차에 와서야 드디어 서비스의 최종 컨셉이 완성되었다.&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;b&gt;코치님 피드백&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기능도 많이 들어갔고, 에이전트 연결도 좋은거 같습니다. 조금 더 다듬어보죠&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;휴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 최종발표에서 사용할 시연용 시나리오를 확정해야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 이때까지 방치한 버그 및 기타 문제를 해결해야 했다.&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;b&gt;5. 폴리싱 2주차 및 발표&lt;/b&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;테스트를 지속하고 QA를 통해 완성도를 높여야했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대본도 쓰고 포스터도 준비해야 했지만 그전에 마지막으로 해야할 것이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;최종 시나리오&lt;/b&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;/p&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;이 의견 충돌과 해결 과정에 대해선 이전 포스팅을 참고하길 바란다!&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사실 의사결정 과정이라 썼지만, 팀의 갈등 해소 과정이 정확한 표현인 것 같다.&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;시간이 갈수록 팀합이 잘 맞았다는 증거이지 않을까 싶다.&lt;/p&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;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;DB 접근이든 도커 이미지화&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스팅은 기술적 접근에 대한 내용을 다룰 예정!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>크래프톤정글10기/나만무</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/169</guid>
      <comments>https://tobetirdev.tistory.com/169#entry169comment</comments>
      <pubDate>Fri, 12 Dec 2025 22:33:40 +0900</pubDate>
    </item>
    <item>
      <title>[나만무] 10기 4조 팀장의 후기</title>
      <link>https://tobetirdev.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;** 개인의 의견이 다수 포함되어 있음 **&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** 프로젝트에 대한 포스팅은 나중에 진행 **&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핀토스가 어떻게 끝났는지 복기할 여유도 없이 바로 나만무 프로젝트가 시작되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나만무를 진행하며 몇가지 키워드에 대해 기록해둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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;나만무가 시작되기 전부터 팀장에 지원할 생각이었어서, 핀토스 3주차(VM)부터 계속해서 팀원을 찾아다녔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0주차 미니프로젝트부터 마음이 잘 맞는 친구 하나와 무조건 같이 하기로 했어서, 3명의 팀원을 구하기 위해&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영업을 뛰었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(팀 선정 방식은 운에 맡길 수밖에 없는 구조이지만, 그 확률을 최대한 높이기 위해서였다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원을 최대한 운에 맡기지 않기 위해 노력했는데 그 이유는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 좋은 결과물(당당하게 1등이라 말할 수 있는)을 내기 위해서&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;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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적인 목표는 1번이었지만, 나만무를 겪으며 가장 크게 와닿았던 것은 2번이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 친한 사람들과 팀플을 해도 갈등이 생길 수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 그 갈등을, 완성될 서비스를 위해 의견을 하나로 모으는 과정이라 생각하는 사람들과 함께하기를 바랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 나중에 더 자세히 포스팅을 하겠지만, 우리 도메인에 대해 제대로 아는 사람이 아무도 없었고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 싱크를 맞추기 위해 많은 시간을 투자해야 했다.&lt;/p&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;정말 고마운 부분&lt;/b&gt;이다. 다들&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;고집은 정말 강했지만&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서비스를 위한 의견 제시 이상으로 감정을 표출하는 일은 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 구현과 발표 준비를 하면서도 물론 의견은 달랐지만 오히려 서비스 기획할 때보다 의견 모으기는 수월했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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;다시는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모르는 도메인에 함부로&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;뛰어들지 않으리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 코치님도 힘드셨을거다. (실제로 10기가 역대급으로 힘들었다 하심)&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;딱 웹 에디터 형식(워크플로우)에 노드를 MVP 수준으로 구현해서(그것도 유연한 설계가 아닌 강한 결합도로) 발표만 겨우 끝마친 정도?&lt;/p&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;구현이 가능할지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무나 많은 고민거리에 결정을 내리지 못하고 심지어 멘토님께도 &quot;그래서 여러분들이 만들고 싶은게 뭐에요?&quot;&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;다행히 팀원들이 &quot;난 모르겠다~&quot; 하고 &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;&quot;&lt;i&gt;개발자라면 이렇게 쉽게 에이전트를 만들 수 있다면 좋아할 것이고, 이러한 기능이 있으면 진짜 좋을 듯&quot;&lt;/i&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주만 더 있었으면 RAG에 DB 연동이든 도커 이미지 배포든 멀티모달 지원이든 더 할 수 있었다....&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그게 너무 아쉽다 ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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;이 부분도 정말... 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 아까 말한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;도메인 지식 부족&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;내가 할 수 있는 일은 최대한 코드 구조를 파악한 뒤, 각 팀원의 구현 역량을 어림짐작하여 충돌하지 않는 디렉토리에서 구현하도록&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;5명이 교육장에 &lt;b&gt;같이 있는 시간&lt;/b&gt;이 그리 길지 않아 가장 중요한 안건에 대한 회의만 우선 진행하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 시간에는 팀원들의 진행상황을 파악한 뒤 시간이 비는 팀원에게 UI 개선이나 발표 시나리오, QA 등을 맡겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 작업이 끝날 때까지 어느 정도 시간을 벌었다(?)고 생각해야 하나...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각자 최대한 병렬적으로 작업&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;'팀장이 모든 일정을 관리해야 한다' 라는 생각이 어찌보면 당연할 수도, 어찌보면 &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;후에 공유해주는 것인데, 사실 이는 풀스택+인프라 구조까지 다 아는 5명이 아니기 때문에 불가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 백엔드+인프라를 담당한 나는 프론트엔드와의 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;&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;s&gt;(PM의 길은 멀고도 험하구나...)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; 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;당연히 AI를 사용했다. 코드를 많이 짜지 않았다. &quot;돌아가는 완성품&quot;을 만드는 것이 우선이었고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;확장 가능한 구조로 설계하는 것&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;이외에는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능에 수정이 필요하더라도 그리 시간이 오래 걸리지 않았다. 이건 그냥 개인의 역량에 따라 달라지는 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 AI에게 무조건 맡기는 것이 아닌 구체적인 전략이 필요했다. 우리 팀의 경우는 codex, claude code의 superclaude와 use seq MCP를 사용하여 아래의 절차에 따라 구현을 맡겼다.&lt;/p&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;i&gt;명확한 요구사항(직접 작성 + AI) -&amp;gt; 이를 구현하기 위한 구현 계획서 제작(plan mode) -&amp;gt; codex로 검증 및 예상되는 문제에 대한 의문 제시 -&amp;gt; 피드백을 바탕으로 구현 계획서 수정 -&amp;gt; codex로 검증 완료 -&amp;gt; Phase별 상세 구현 명세서(API 명세등) -&amp;gt; 구현 시작&lt;/i&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;AI 활용에 대한 자세한 내용은 다른 포스팅에서 다루겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 스택을 공부하는 실력 다지기 기간에, React에 대한 기초와 AI 활용에 대한 내용을 많이 공부했고, 도움이 많이 되었다.&lt;/p&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;&quot; 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;결국 기능은 잘 나왔는데, 한 두명의 팀원은 조금 더 욕심을 내고 싶은 모양이었다.&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;이때가 아마 발표 4,5일전이었을 것이고, 포스터 세션 제작, 발표 대본 작성 및 연습, 최종 점검 등 해야할 것이 너무나 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Restful 배포 방식의 시나리오를 바꿔야 한다는 주장이 있었고, 나도 어느정도 동의하고 있었다.&lt;/p&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;Http 요청 및 템플릿 변환 노드&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;&lt;b&gt;사실 이때 가장 서로가 날카로웠고, 팀의 화합이 어그러질 수도 있었던 것 같다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 일리가 있는 말이라 결정을 내리기 어려웠지만 작업 분배를 잘하면 해볼만하다라는 생각에,&lt;/p&gt;
&lt;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;일종의 보험을 들어놨다&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;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;결과적으로 만족할 만한 기능을 완성&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;마지막으로 무한 발표 연습을 통해 7분이라는 엄격한 시간 제한안에 우리가 담고 싶었던 내용을 다 담을 수 있었고, 무사히 발표를 마쳤다.&lt;/p&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;&quot; 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;정말 끝까지 힘들었던 프로젝트.&lt;span&gt;&amp;nbsp;&lt;/span&gt;최선을 다했기에 높은 벽을 느낄 수 밖에 없던 경험이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제 팀장 경력이 아닌 같은 동기로서 팀장을 맡았다는 쉴드를 받으면, 그래도 제법 잘하지 않았나 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(뒷풀이에서 서로가 서로를 많이 믿어준 것에 대한 고마움을 나눴다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PL의 역량은 정말, 협업에서 너무 중요하다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀장이 아닌 팀의 의사결정 과정은 다음에 포스팅할 예정!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>크래프톤정글10기/나만무</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/168</guid>
      <comments>https://tobetirdev.tistory.com/168#entry168comment</comments>
      <pubDate>Thu, 4 Dec 2025 21:49:14 +0900</pubDate>
    </item>
    <item>
      <title>[백준/G5] 최소비용구하기</title>
      <link>https://tobetirdev.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1916&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1916&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라를 안 풀다 보니 (+파이썬으로 풀다보니) 구현중에 정말 많은 부분을 틀렸다...&lt;/p&gt;
&lt;pre id=&quot;code_1758680082587&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
import heapq as hq

MAX = 9999999999 

N = int(input())
M = int(input())

graph = [[] for _ in range(N+1)]

for _ in range(M):
    a, b, c = map(int, input().split())
    graph[a].append((b, c))

dist = [MAX] * (N+1)

start, end = map(int, input().split())
dist[start] = 0

q = []
# (비용, 노드) 순서로 넣기
hq.heappush(q, (0, start))

def dijkstra():
    while q:
        # (비용, 노드) 순서로 받기
        curCost, curNode = hq.heappop(q)
        
        # 이미 더 짧은 경로가 발견된 경우 건너뛰기
        if curCost &amp;gt; dist[curNode]:
            continue
        
        for nextNode, nextCost in graph[curNode]:
            newCost = dist[curNode] + nextCost
            
            if newCost &amp;lt; dist[nextNode]:
                dist[nextNode] = newCost
                # (비용, 노드) 순서로 넣기
                hq.heappush(q, (newCost, nextNode))

dijkstra()
print(dist[end])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. float('inf') 기억 안나서 대충 MAX 욱여넣기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. graph 리스트 선언할 때 크기 설정 실수하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. q 습관적으로 deque로 선언하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 힙큐인데 popleft로 빼기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 비용이 적은 순으로 정렬해야 하니 비용이 앞에 와야하는데 노드 - 비용 순으로 넣기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. visit 배열을 써야 하나 고민하기 -&amp;gt; &lt;b&gt;최소 비용부터 처리하므로 한번 처리된 노드는 확정된 최단 거리임&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7.&amp;nbsp; for문 안 로직 헷갈리기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.... 다익스트라의 개념 문제 그 이상 그 이하도 아닌데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 알고리즘은 한두달만 안 풀어도 치명적인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>크래프톤정글10기/파이썬</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/167</guid>
      <comments>https://tobetirdev.tistory.com/167#entry167comment</comments>
      <pubDate>Wed, 24 Sep 2025 11:26:23 +0900</pubDate>
    </item>
    <item>
      <title>[백준/G4] 탈출</title>
      <link>https://tobetirdev.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 풀 때 주의할 점은 고슴도치가 이동하기 전에 물이 먼저 번져야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 시간(혹은 레벨)별로 구분해서 처리를 할 필요가 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 매 시간마다 물 먼저, 고슴도치는 나중에 처리를 하도록 해야한다.&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;결국 FIFO라는 큐의 특성 때문에, 물 웅덩이가 몇개로 시작하든 한 단계(하나의 시간)에서 물이 다 퍼지고 나서&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;간결함은 주석 처리된 1번째 코드를, 이해하기에는 2번째 코드가 더 좋은 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능은 두 코드가 거의 비슷했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758167001638&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# from collections import deque

# R, C = map(int, input().split())
# grid = []
# for _ in range(R):
#     grid.append(list(input().strip()))

# dy = [-1, 1, 0, 0]
# dx = [0, 0, -1, 1]

# q = deque()
# visit = [[False] * C for _ in range(R)]

# # 물의 위치들을 먼저 큐에 넣기
# for i in range(R):
#     for j in range(C):
#         if grid[i][j] == '*':
#             q.append(('*', i, j, 0))

# # 고슴도치 위치 넣기
# for i in range(R):
#     for j in range(C):
#         if grid[i][j] == 'S':
#             q.append(('S', i, j, 0))
#             visit[i][j] = True

# while q:
#     type, cy, cx, time = q.popleft()
    
#     if type == '*':  # 물 처리
#         for i in range(4):
#             ny, nx = cy + dy[i], cx + dx[i]
#             if 0 &amp;lt;= ny &amp;lt; R and 0 &amp;lt;= nx &amp;lt; C and grid[ny][nx] == '.':
#                 grid[ny][nx] = '*'
#                 q.append(('*', ny, nx, time + 1))
    
#     else:  # 고슴도치 처리
#         if grid[cy][cx] == 'D':  # 도착
#             print(time)
#             exit()
        
#         for i in range(4):
#             ny, nx = cy + dy[i], cx + dx[i]
#             if 0 &amp;lt;= ny &amp;lt; R and 0 &amp;lt;= nx &amp;lt; C and not visit[ny][nx]:
#                 if grid[ny][nx] == '.' or grid[ny][nx] == 'D':
#                     visit[ny][nx] = True
#                     q.append(('S', ny, nx, time + 1))

# print(&quot;KAKTUS&quot;)

from collections import deque

R,C= map(int,input().split())
grid=[]

for _ in range(R):
    grid.append(list(input().strip()))

dx=[0,0,-1,1]
dy=[-1,1,0,0]

water_q= deque()
hedgehog_q= deque()

#물과 고슴도치 위치 찾기

for i in range(R):
    for j in range(C):
        if grid[i][j]==&quot;*&quot;:
            water_q.append((i,j))
        elif grid[i][j]==&quot;S&quot;:
            hedgehog_q.append((i,j))

visit= [[False] * C for _ in range(R)]

# 고습도치 시작점 방문처리
for i in range(R):
    for j in range(C):
        if grid[i][j]==&quot;S&quot;:
            visit[i][j]=True
            break

time=0

# 조건은 고슴도치가 움직일 수 있을 때
while hedgehog_q:
    time+=1

    #1단계: 물 먼저 퍼뜨리기
    water_size= len(water_q)
    for _ in range(water_size):
        cx,cy= water_q.popleft()

        for i in range(4):
            nx,ny=cx+dx[i], cy+dy[i]
            if 0 &amp;lt;= nx &amp;lt; R and 0 &amp;lt;= ny &amp;lt; C and grid[nx][ny] == '.':
                grid[nx][ny]=&quot;*&quot;
                water_q.append((nx,ny))

    hedgehog_size= len(hedgehog_q)
    for _ in range(hedgehog_size):
        cx,cy= hedgehog_q.popleft()

        for i in range(4):
            nx, ny= cx+dx[i], cy+dy[i]
            if 0 &amp;lt;= nx &amp;lt; R and 0 &amp;lt;= ny &amp;lt; C and not visit[nx][ny]:
                if grid[nx][ny]==&quot;D&quot;:
                    print(time)
                    exit()
                elif grid[nx][ny]==&quot;.&quot;:
                    visit[nx][ny]=True
                    hedgehog_q.append((nx,ny))
    

print(&quot;KAKTUS&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0922 수정&lt;/p&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;그 이유는 타입을 알 수 없다면 마지막 D에 도달할 때 그게 고슴도치가 도착한 것인지 물이 도착한 것인지 알 수 없기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 복잡한 처리를 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758508833980&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
from collections import deque

R, C = map(int, input().split())
li = []
for _ in range(R):
    li.append(list(input().strip()))

q = deque()
dx = [1, 0, -1, 0]
dy = [0, 1, 0, -1]
visit = [[False] * C for _ in range(R)]

# 물 먼저 큐에 넣기
for i in range(R):
    for j in range(C):
        if li[i][j] == '*':
            q.appendleft((i, j, 0))
        if li[i][j] == &quot;S&quot;:
            q.append((i, j, 0))
            visit[i][j] = True

while q:
    cx, cy, ctime = q.popleft()
    
    # 고슴도치가 D에 도달했는지 체크 (이동하기 전에 체크)
    if li[cx][cy] == 'D':
        print(ctime)
        exit()

    for i in range(4):
        nx = cx + dx[i]
        ny = cy + dy[i]

        if 0 &amp;lt;= nx &amp;lt; R and 0 &amp;lt;= ny &amp;lt; C and not visit[nx][ny]:
            if li[cx][cy] == '*':  # 물 처리
                if li[nx][ny] == '.':  # 물은 빈 공간으로만 퍼짐
                    visit[nx][ny] = True
                    li[nx][ny] = '*'
                    q.append((nx, ny, ctime + 1))
                    
            elif li[cx][cy] == 'S':  # 고슴도치 처리
                if li[nx][ny] == '.' or li[nx][ny] == 'D':
                    visit[nx][ny] = True
                    if li[nx][ny] == '.':  # 빈 공간일 때만 S로 변경
                        li[nx][ny] = 'S'
                    # D일 때는 격자를 수정하지 않음
                    q.append((nx, ny, ctime + 1))

print(&quot;KAKTUS&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;/p&gt;</description>
      <category>크래프톤정글10기/파이썬</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/166</guid>
      <comments>https://tobetirdev.tistory.com/166#entry166comment</comments>
      <pubDate>Thu, 18 Sep 2025 12:47:53 +0900</pubDate>
    </item>
    <item>
      <title>[백준/G4] 빙산</title>
      <link>https://tobetirdev.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;까맣게 잊고 있었던 &quot;동시에 녹일 수 있는 방법&quot;....&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 배열에 영향을 주지않고(이미 녹은 빙산의 값이 0이 되어 다른 빙산에 영향을 주지않게)&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;또한 visit배열을 한번만 선언하면 안되고 매번 선언해주어야 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아침에 정신없을때 풀어서 그런가 진짜 집중이 안된다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758084648424&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input=sys.stdin.readline
from collections import deque

n,m= map(int,input().split())

li= []
dx=[0,1,0,-1]
dy=[1,0,-1,0]

for _ in range(n):
    li.append(list(map(int,input().split())))


def bfs(i,j,visit):
    q=deque()
    q.append((i,j))
    visit[i][j]=True
    
    while q:
        cx,cy= q.popleft()

        for k in range(4):
            nx= cx+dx[k]
            ny= cy+dy[k]

            if(0&amp;lt;=nx&amp;lt;n and 0&amp;lt;=ny&amp;lt;m and not visit[nx][ny] and li[nx][ny]&amp;gt;0 ):
                visit[nx][ny]=True
                q.append((nx,ny))

def check():
    for i in range(n):
        for j in range(m):
            if li[i][j]&amp;gt;0:
                return False
    return True

def melt():
    new_li = [row[:] for row in li]  # 깊은 복사로 동시 녹이기
    
    for i in range(n):
        for j in range(m):
            if li[i][j]&amp;gt;0:
                cnt=0
                for x in range(4):
                    tx= i+dx[x]
                    ty= j+dy[x]

                    if(0&amp;lt;=tx&amp;lt;n and 0&amp;lt;=ty&amp;lt;m and li[tx][ty]==0):  # 경계 체크 추가
                        cnt+=1

                new_li[i][j]= max(0, li[i][j]-cnt)
    
    # 원래 배열에 복사
    for i in range(n):
        for j in range(m):
            li[i][j] = new_li[i][j]

def count_groups():
    visit=[ [False] * m for _ in range(n) ]  # 매번 새로 생성
    count=0
    
    for i in range(n):
        for j in range(m):
            if(li[i][j]&amp;gt;0 and not visit[i][j]):
                bfs(i,j,visit)
                count+=1
    return count

time=0

while(True):
    # 먼저 현재 상태에서 빙산 덩어리 개수 체크
    groups = count_groups()
    
    if groups &amp;gt;= 2:  # 2개 이상이면 현재 시간 출력
        print(time)
        break
    elif groups == 0:  # 모두 녹았으면 0 출력
        print(0)
        break
    
    # 1년 경과 (빙산 녹이기)
    melt()
    time+=1&lt;/code&gt;&lt;/pre&gt;</description>
      <category>크래프톤정글10기/파이썬</category>
      <author>티디리</author>
      <guid isPermaLink="true">https://tobetirdev.tistory.com/165</guid>
      <comments>https://tobetirdev.tistory.com/165#entry165comment</comments>
      <pubDate>Wed, 17 Sep 2025 13:54:36 +0900</pubDate>
    </item>
  </channel>
</rss>