<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>황현석 일지</title>
    <link>https://hy3ons.tistory.com/</link>
    <description>충남대학교 컴퓨터 융합학부 3학년 휴학중 202202658 황현석</description>
    <language>ko</language>
    <pubDate>Sun, 12 Jul 2026 09:39:51 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>황현석</managingEditor>
    <image>
      <title>황현석 일지</title>
      <url>https://tistory1.daumcdn.net/tistory/5832271/attach/3f925ad80b7d4ef28e30a76125643f01</url>
      <link>https://hy3ons.tistory.com</link>
    </image>
    <item>
      <title>클라우드 서버 구축 일지 05 - Host OS 제어 포기</title>
      <link>https://hy3ons.tistory.com/61</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회고.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저번 글에서는 Kite의 &lt;code&gt;uninstall.sh&lt;/code&gt;를 꽤 자세히 다뤘습니다. 설치된 리소스를 지우는 것뿐 아니라, Longhorn webhook, namespace finalizer, host sshd 복원 worker처럼 서버에 남은 흔적을 어디까지 책임져야 하는지에 가까운 이야기였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그 글을 쓰면서 &quot;내가 설치한 것들은 모두 깔끔하게 정리하자.&quot;는 쪽으로 생각이 많이 갔습니다. 그런데 이번에는 조금 반대 방향의 질문이 생겼습니다. 다녀간 자리를 잘 치우는 것도 중요하지만, 애초에 어디까지 들어가도 되는가를 먼저 정해야 하는 것 아닌가 싶었습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 Kite를 설치하는 경험을 굉장히 좋게 하고 싶었습니다. 사용자가 README에 있는 명령을 복사해서 &lt;code&gt;install.sh | bash&lt;/code&gt;로 실행하면, 필요한 것들이 전부 설치되고, 80번 포트로 웹 UI에 들어가고, VM도 만들고, SSH 접속도 자연스럽게 되는 구조를 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그 생각 자체는 좋아보였습니다. 운영자가 뭔가를 따로 신경쓰지 않아도 되고, 사용자는 &lt;code&gt;ssh vm-id@domain&lt;/code&gt;처럼 익숙한 명령으로 VM에 들어갑니다. host OS의 SSH와 Kite VM SSH gateway를 한 주소 안에서 통합하면, 제품 경험이 깔끔해질 거라고 봤습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 처음 방향은 22번 포트를 Kite gateway가 맡는 쪽이었습니다. 기존 host sshd는 스크립트 단에서, 다른 포트로 옮기고, gateway가 22번으로 들어오는 SSH를 먼저 받습니다. VM 계정이면 VM으로 보내고, host 계정이면 host sshd로 fallback합니다. 그림으로 보면 꽤 그럴듯했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사용자:
  ssh vm-id@domain

Kite gateway:
  VM 계정이면 VM으로 proxy
  host 계정이면 host sshd로 fallback

host sshd:
  22번에서 다른 관리 포트로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 막상 실제 서버에서 생각해보니, 이 구조는 운영자의 편의성을 극대화하는 것처럼 보이면서 동시에 운영자의 제어권을 잃게 만들 수 있었습니다. SSH 22번은 그냥 포트 하나가 아니었습니다. 원격 서버에 다시 들어갈 수 있는 가장 중요한 진입점이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이번 고민의 중심.&lt;/b&gt; Kite gateway를 host OS의 SSH 진입점과 어디까지 통합할 것인가였습니다. 처음에는 운영 UX를 위해 22번까지 자동으로 가져가고 싶었지만, 실제로는 그 통합이 host OS 접근성을 위험하게 만들 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.2rem auto 1.8rem auto; line-height: 1.6;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.2rem auto 1.8rem auto; line-height: 1.6;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #374151;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background: linear-gradient(to top, #d1d5db 42%, transparent 42%); padding: 0 4px;&quot;&gt;운영 UX를 좋게 만들려던 선택이, 오히려 운영 UX를 최악으로 만들 수 있었습니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 처음 목표는 운영 UX를 극대화 하는 쪽이었습니다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 설치 과정을 하나의 명령으로 끝내고 싶었습니다. Kubernetes, KubeVirt, CDI, Longhorn, API, controller, frontend, gateway가 전부 올라오고, 사용자는 웹 UI만 열면 됩니다. 운영자는 복잡한 포트 설정이나 gateway 설정을 직접 보지 않아도 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;SSH도 마찬가지였습니다. VM 접속 명령이 &lt;code&gt;ssh -p 1241 vm-id@domain&lt;/code&gt;처럼 보이면 뭔가 제품이 덜 완성된 느낌이 들 수 있다고 생각했습니다. 일반적인 사용자는 포트 번호가 붙는 순간 &quot;이건 뭔가 따로 설정해야 하는 개발용 도구인가&quot;라고 느낄 수도 있습니다. 그래서 22번을 gateway가 맡는 게 사용자 경험상 좋아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 생각&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;integrated&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;install.sh | bash&lt;/code&gt; 한 번으로 설치, 웹 UI, SSH gateway까지 전부 준비합니다. 사용자는 80번 웹과 22번 SSH만 기억하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;숨어 있던 전제&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;host control&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 UX를 만들려면 Kite installer가 host sshd, 22번 포트, systemd socket, gateway Service까지 건드려야 합니다. 즉 애플리케이션 설치가 host OS 진입점 변경까지 포함하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그때는 이 전제가 그렇게 위험하게 느껴지지 않았습니다. gateway가 host fallback을 제공하면 되는 것 아닌가 싶었습니다. 하지만 host fallback은 gateway가 제대로 떠 있고, host sshd가 옮겨진 포트에서 살아 있고, gateway가 그 주소로 잘 프록시할 때만 의미가 있습니다. 그중 하나라도 틀어지면 운영자는 서버에 못 들어갈 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 22번 포트 점유는 socket 충돌보다 더 애매했습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 22번 충돌을 단순하게 생각했습니다. host sshd가 22번을 잡고 있으면 gateway가 22번을 못 열고, 그러면 실패가 보일 거라고 봤습니다. 그런데 k3s 같은 환경에서는 그렇게 단순하지 않았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이건 특별히 이상한 재현 케이스라기보다, Kubernetes Service 노출 모델에 가까웠습니다. &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kubernetes Service 문서&lt;/a&gt;를 보면 &lt;code&gt;NodePort&lt;/code&gt;는 각 Node IP의 정적 포트로 Service를 노출하고, &lt;code&gt;LoadBalancer&lt;/code&gt;는 그 위에 외부 load balancer를 붙이는 타입입니다. 그리고 &lt;a href=&quot;https://kubernetes.io/docs/reference/networking/virtual-ips/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Virtual IPs and Service Proxies 문서&lt;/a&gt;에서는 kube-proxy가 Service와 EndpointSlice를 보고 node의 packet forwarding rule을 구성한다고 설명합니다. Linux에서는 그 구현이 iptables, IPVS, nftables 같은 계층으로 내려갑니다.&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;font-family: 'Noto Serif KR';&quot;&gt;k3s에서는 이 부분이 더 직접적입니다. &lt;a href=&quot;https://docs.k3s.io/networking/networking-services#how-servicelb-works&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;k3s ServiceLB 문서&lt;/a&gt;에 따르면, ServiceLB는 &lt;code&gt;LoadBalancer&lt;/code&gt; Service를 보고 &lt;code&gt;kube-system&lt;/code&gt; namespace에 DaemonSet을 만들고, 그 pod가 Service port를 &lt;code&gt;hostPort&lt;/code&gt;로 사용합니다. NAT 환경이면 트래픽이 hostPort를 통해 ServiceLB pod로 들어가고, 그 pod가 iptables로 Service의 ClusterIP와 port로 넘깁니다. NAT가 아니더라도 LoadBalancer 주소로 들어온 트래픽은 kube-proxy의 iptables chain이나 IPVS에 의해 Service 경로로 들어갈 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 &lt;code&gt;port: 22&lt;/code&gt;를 선언한다는 건 단순히 namespace 안의 앱 포트 하나를 고르는 문제가 아니었습니다. Service 객체는 namespace 안에 있지만, 외부에서 node로 들어오는 22번 트래픽을 어떻게 받을지는 hostPort, NAT, kube-proxy 규칙처럼 node 전체의 네트워크 계층으로 내려갑니다. host sshd가 이미 22번을 잡고 있으면 ServiceLB pod가 pending이 될 수도 있습니다. 반대로 해당 계층이 성공적으로 잡히면, 운영자가 생각한 host sshd의 22번과 Kubernetes가 노출하려는 gateway 22번이 같은 외부 진입점에서 충돌합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;host sshd는 살아 있음&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;운영자 입장에서는 22번이 host sshd의 진입점입니다. 하지만 Kubernetes Service 노출은 프로세스의 socket bind만으로 설명되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Service가 node 계층으로 내려옴&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;k3s ServiceLB는 LoadBalancer Service port를 hostPort와 iptables 경로로 연결합니다. 즉 Service 선언이 node 외부 트래픽 처리에 영향을 줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;host 접근 리스크가 됨&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ServiceLB가 pending이 되든, 외부 22번이 gateway 경로로 들어가든, installer가 host SSH 진입점과 Service 노출 정책을 동시에 건드린다는 점은 그대로 남습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;운영 UX가 오히려 나빠짐&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 이 문제는 앱 포트 충돌이 아니라, host OS 진입점과 VM gateway 책임 경계의 문제가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 지점에서 &quot;installer가 해줘야 한다&quot;는 전제를 다시 봤습니다. 22번은 namespace 안에서만 격리되는 값처럼 다루기 어려웠습니다. Service 객체는 namespace에 있어도, 외부에서 node로 들어오는 포트는 결국 hostPort와 netfilter 규칙, load balancer 구현으로 이어집니다. 설치 스크립트가 이 경로를 바꾸는 순간, 설치 실패는 단순한 앱 설치 실패가 아닙니다. 원격 서버 운영 실패가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. shell로 sshd를 조작하는 건 너무 얇아보였습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 다음에는 &quot;그래도 22번 takeover를 안전하게 만들 수는 없을까&quot;를 봤습니다. 단순히 22번을 내리고 다른 포트를 여는 방식은 위험합니다. 그래서 더 안전한 흐름을 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 22번은 유지한 채 새 host sshd 포트를 먼저 추가
2. 새 포트로 실제 SSH 접속이 되는지 확인
3. 확인되면 gateway를 22번에 노출
4. gateway fallback이 host sshd 새 포트로 가는지 확인
5. 실패하거나 확인이 없으면 rollback&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 정도는 되어야 맞아보였습니다. 그런데 이걸 shell script 하나로 처리하는 건 너무 불안했습니다. 사용자의 현재 SSH 세션에서 실행되는 shell이 중간에 끊기면 어떻게 할 것인지, systemd socket 환경이면 어떻게 볼 것인지, 새 포트가 외부 방화벽이나 NAT에서 정말 들어오는지, 실패했을 때 어느 상태까지 되돌릴 것인지가 전부 남습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;특히 찜찜했던 건 systemd의 &lt;code&gt;sshd.socket&lt;/code&gt;이나 &lt;code&gt;ssh.service&lt;/code&gt;를 직접 만져야 할 수도 있다는 점이었습니다. 어떤 환경에서는 sshd 포트를 바꾸려면 socket activation 쪽을 끄거나, service를 재시작하거나, 둘 중 무엇이 실제로 22번을 잡고 있는지 확인해야 합니다. 그런데 나는 바로 그 SSH로 원격 개발을 하고 있는 상태였습니다. 내가 타고 들어온 줄을 내가 끊는 식이 될 수 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;물론 기존 세션이 바로 끊기지 않을 수도 있고, 설정을 잘 적용하면 문제 없이 넘어갈 수도 있습니다. 하지만 그런 식의 &quot;아마 괜찮을 것&quot;을 installer 기본 동작에 넣는 건 이상했습니다. 특히 원격 서버에서는 작은 실수가 그냥 앱 오류가 아니라, 서버에 다시 못 들어가는 상황으로 이어질 수 있습니다. 여기서 이 방향은 아니라고 꽤 강하게 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 더 큰 방향도 생각했습니다. host-side daemon이나 작은 control plane을 두고, 상태가 충분히 확인될 때까지 단계적으로 조정합니다. 안 되면 전체를 rollback합니다. UI에는 현재 phase를 보여줍니다. 예를 들면 &lt;code&gt;PreparingHostPort&lt;/code&gt;, &lt;code&gt;WaitingForConfirmation&lt;/code&gt;, &lt;code&gt;SwitchingGateway&lt;/code&gt;, &lt;code&gt;Ready&lt;/code&gt;, &lt;code&gt;RolledBack&lt;/code&gt; 같은 식입니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;shell script 방식&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;thin&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구현은 빠르지만, 현재 SSH 세션과 host sshd를 동시에 건드립니다. 중간 실패와 재시도, rollback 상태를 믿고 맡기기에는 얇아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;host daemon/control plane 방식&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;heavy&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상태를 관찰하고, 확인을 기다리고, 실패하면 되돌릴 수 있습니다. 하지만 privileged host agent, systemd 조작, 상태 머신까지 들어가서 지금 단계의 기본 설치에는 너무 크게 느껴졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 다시 생각했습니다. 22번 takeover를 정말 제품의 핵심으로 가져갈 거라면 이 정도 시스템이 필요할 수 있습니다. 하지만 지금 Kite에서 당장 필요한 건 &quot;host OS의 SSH까지 안전하게 재구성하는 플랫폼&quot;이 아니라, VM SSH gateway를 안정적으로 제공하는 것이었습니다. 둘은 비슷해 보이지만 책임 범위가 달랐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. host sshd의 모든 조작은 운영주체로 넘기는 것으로 하였습니다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 방향을 바꿨습니다. Kite는 host sshd를 옮기지 않습니다. 삭제하지도 않습니다. 22번을 자동으로 가져가지도 않습니다. host OS의 SSH 진입점은 운영자가 책임지는 영역으로 남기는 것이 맞아보였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 이게 UX를 포기하는 것처럼 보였습니다. 그런데 다시 생각해보니 꼭 그렇지는 않았습니다. 포트는 여러 계층에서 위장하거나 매핑할 수 있습니다. 공유기, NAT, 라우터, cloud load balancer, 방화벽 규칙에서 외부 포트와 내부 포트를 다르게 둘 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;예시 1:
  host sshd      -&amp;gt; server:22
  Kite VM gateway -&amp;gt; server:40022

예시 2:
  외부 22     -&amp;gt; 내부 gateway:40022
  외부 12311  -&amp;gt; 내부 host sshd:22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉 Kite가 반드시 host sshd를 직접 옮겨야만 좋은 사용자 경험을 만들 수 있는 건 아니었습니다. 운영자가 자기 환경에 맞게 포트를 정하고, 필요하면 외부 라우팅에서 사용자에게 보이는 포트를 바꾸면 됩니다. Kite는 그 결정을 존중하고, 사용자가 복사할 SSH 명령어를 정확히 보여주면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;현재 판단.&lt;/b&gt; 사용자가 편하게 느끼는 것은 &quot;항상 22번을 쓰는 것&quot;만은 아니었습니다. 더 중요한 건 UI가 현재 운영자가 정한 접속 방법을 정확히 안내하고, host OS의 진입점을 망가뜨리지 않는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. VM gateway와 host sshd를 분리하니 UI 책임이 더 중요해졌습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;host sshd를 건드리지 않는다고 해서 끝은 아니었습니다. 그러면 이제 Kite는 사용자에게 정확한 VM 접속 방법을 알려줘야 합니다. 운영자가 gateway 외부 포트를 40022로 열었다면, UI는 &lt;code&gt;ssh -p 40022 vm-id@domain&lt;/code&gt;을 보여줘야 합니다. 포트가 22번이면 &lt;code&gt;-p&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;그리고 이 설정은 설치 시점에 박아두는 값이면 안 될 것 같았습니다. 운영자는 나중에 포트를 바꾸고 싶을 수 있습니다. NAT나 방화벽 정책이 바뀔 수도 있고, 특정 서버에서는 1241번을 쓰고 싶을 수도 있습니다. 그래서 admin config에서 SSH gateway 외부 노출과 포트를 다루는 것이 맞아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치 직후&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gateway pod는 뜨지만 외부 22번을 자동으로 가져가지 않습니다. host sshd는 그대로 둡니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;운영자 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;admin UI에서 VM SSH gateway를 외부에 열지, 몇 번 포트를 쓸지 정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;controller reconcile&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ConfigMap에 적힌 desired state를 보고 gateway external Service를 만들거나 내립니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자 안내&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VM dashboard와 connection drawer는 현재 포트를 읽어서 정확한 SSH 명령을 보여줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 되면 VM gateway와 host sshd는 분리됩니다. host sshd는 운영자가 관리합니다. Kite는 VM gateway를 관리합니다. 둘을 한 UX로 자연스럽게 이어주는 역할은 UI와 설정이 맡습니다. 처음에 생각했던 통합보다 덜 자동화되어 있지만, 실패했을 때의 피해가 훨씬 작습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. ConfigMap과 status는 그래서 필요했습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설정을 UI에서 바꿀 수 있게 하려면, 단순히 manifest에 port를 박아두면 안 됩니다. 운영자가 dashboard에서 &quot;VM SSH gateway를 1241번으로 열고 싶다&quot;라고 설정할 수 있어야 했습니다. 여기까지는 자연스러웠습니다. 문제는 그 설정을 backend가 어떻게 다뤄야 하느냐였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 그냥 적용형으로 갈 수도 있다고 봤습니다. admin API가 요청을 받으면 바로 Service를 patch하고, 성공하면 성공 응답을 주고, 실패하면 오류를 돌려주는 방식입니다. 일반적인 웹 설정 API처럼 생각하면 이쪽이 더 단순해보입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그런데 Kite는 전체적으로 Kubernetes 위에서 도는 KubeVirt를 한 번 더 간단한 추상화로 묶고, 그 위에 UI/UX를 제공하는 프로젝트입니다. Kubernetes의 기본 감각은 &quot;요청을 한 번 적용하고 끝&quot;이라기보다, 원하는 상태를 선언해두고 controller가 계속 현재 상태를 맞추는 쪽에 가깝습니다. 여기서도 그 철학을 적용하는 게 더 맞아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kite-runtime-config
  sshGatewayExternalEnabled=false
  sshGatewayExternalPort=
  sshGatewayHostFallbackEnabled=true
  sshGatewayHostSshdPort=22

kite-gateway-status
  phase=Disabled
  reason=ExternalDisabled
  observedExternalPort=
  message=SSH gateway external service is disabled&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 runtime config는 명령이 아니라 desired state로 보는 쪽이 맞았습니다. 운영자가 &lt;code&gt;sshGatewayExternalPort=1241&lt;/code&gt;을 저장했다는 건 &quot;지금 당장 반드시 성공시켜라&quot;가 아니라 &quot;Kite gateway를 1241번으로 열고 싶은 상태&quot;를 선언한 것입니다. controller는 그 상태를 맞추려고 계속 reconcile하고, 실제로 맞췄는지 못 맞췄는지는 status에 남깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;desired&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;운영자가 선언한 목표 상태입니다. gateway를 열지, 몇 번 포트로 열지, host fallback을 어디로 둘지를 담습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;observed&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;controller가 관찰한 현재 상태입니다. Service가 만들어졌는지, 막혔는지, 실패했다면 왜 실패했는지를 UI가 읽을 수 있게 둡니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 차이는 포트 충돌 같은 상황에서 더 중요해집니다. 만약 1241번 포트가 아직 어떤 이유로 열리지 못한다면, 적용형 API에서는 backend가 실패를 반환하고 끝납니다. 운영자가 host 쪽 포트 충돌을 풀어도 다시 버튼을 누르거나 API를 다시 호출해야 합니다. 반대로 선언형으로 두면 desired state는 그대로 남아 있고, 충돌이 풀린 뒤 다음 reconcile에서 controller가 다시 맞추려고 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 지금 구조에서는 새 CRD를 바로 만들지는 않더라도, 최소한 ConfigMap을 desired state처럼 사용하고 status ConfigMap을 따로 두는 방향이 좋아보였습니다. 나중에 이 설정이 커지면 &lt;code&gt;KiteGatewayConfig&lt;/code&gt; 같은 CRD로 옮길 수 있습니다. 하지만 지금 판단의 핵심은 CRD냐 ConfigMap이냐보다, 이 설정을 일회성 적용 명령으로 보지 않고 계속 reconcile되는 상태로 본다는 점이었습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7. 결론&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 고민은 테스트 스크립트를 표준화하다가 시작됐지만, 실제 중심은 SSH gateway와 host OS의 경계였습니다. 어디까지 Kite가 책임지고, 어디부터 운영자에게 남겨야 하는가의 문제였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 통합 운영 UX를 극대화하고 싶었습니다. 설치 한 번으로 80번 웹 UI와 22번 SSH gateway가 전부 열리고, 사용자는 포트 생각 없이 쓰는 구조입니다. 그런데 그 통합이 host sshd의 진입점을 가져가는 순간, 실패했을 때 운영자는 원격 제어권을 잃을 수 있었습니다. 이건 UX가 좋아지는 게 아니라, UX가 최악으로 가는 길이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 지금 결론은 단순합니다. host sshd는 Kite가 조작하지 않습니다. VM SSH gateway는 별도 포트와 별도 Service로 관리합니다. 운영자는 자기 환경에 맞게 포트를 정하고, 필요하면 NAT나 라우터에서 외부 포트를 원하는 대로 매핑합니다. Kite는 그 설정을 UI에 정확히 반영해서 일반 사용자가 헷갈리지 않게 안내합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;언젠가 22번 takeover를 다시 제대로 하고 싶다면, 그때는 shell script가 아니라 host-side daemon, 확인 토큰, rollback 가능한 상태 머신까지 들고 가야 할 것 같습니다. 하지만 지금 단계에서는 오버엔지니어링이라고 생각했습니다. kite-sshd와 host sshd의 통제를 쉽게 갈 수 있게 하는것이 최선이라고 생각했습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/61</guid>
      <comments>https://hy3ons.tistory.com/61#entry61comment</comments>
      <pubDate>Mon, 6 Jul 2026 23:00:41 +0900</pubDate>
    </item>
    <item>
      <title>클라우드 서버 구축 일지 04 - Uninstall.sh</title>
      <link>https://hy3ons.tistory.com/60</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회고.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저번 글에서는 Kite의 E2E 테스트를 잡으면서 SSH gateway와 host sshd가 어떤 식으로 이어져야 하는지 정리했습니다. gateway가 22번 포트를 맡고, 기존 host sshd는 다른 포트로 이동하고, VM 계정이 아니면 다시 host sshd로 fallback되는 흐름을 실제 서버에서 확인한 이야기였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이번 글은 그 다음 단계의 회고입니다. stage에서 E2E가 통과했고, &lt;code&gt;main&lt;/code&gt;에 push한 뒤 GHCR image build도 끝났습니다. 그런데 실제 사용자가 보게 될 README의 원격 &lt;code&gt;uninstall.sh&lt;/code&gt;, &lt;code&gt;ghcr-install.sh&lt;/code&gt; 흐름으로 기존 설치를 지우고 다시 설치해보니, stage E2E에서는 보이지 않던 Longhorn과 host sshd 경계 문제가 나왔습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;분명, Stage Branch에서 해야할 End-to-End 테스트, sshd 변경 테스트, uninstall.sh 테스트까지 모두 진행하였는데, main branch 즉 배포 브런치에서 사고가 나버린 거였습니다. 개인프로젝트라고 해도, 안일한 마음으로 배포사이클을 돌리고 그런건 아니였는데, 자괴감이 들긴 했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 분풀이로 왜 어째서 내가 뭘 놓쳤나를 좀 자세하게 보려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 &amp;ldquo;이제 배포됐으니 README에 적힌 설치 명령만 한 번 마지막으로 돌려보자. 이미 Stage Branch에서는 모든 테스트가 끝났으니 사고가 나지 않겠지.&amp;rdquo;라고 생각했습니다. 이미 코드도 올라갔고, GHCR 빌드도 됐고, SSH gateway도 어느 정도 봤으니 끝난 줄 알았습니다. 그런데 막상 실제 서버에서 기존 설치를 지우고 다시 설치해보니, 문제는 설치 자체보다 &lt;code&gt;uninstall.sh&lt;/code&gt;와 &lt;code&gt;ghcr-install.sh&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;지우는 스크립트가 끝났다고 해서 Kubernetes 안에서 모든 삭제가 즉시 끝난 것은 아니었습니다. namespace는 아직 Terminating 중일 수 있었고, Longhorn webhook 설정은 남아 있을 수 있었고, host sshd 복원 worker가 예약됐다고 해서 실제로 22번 포트가 돌아온 것도 아니었습니다. 이번에 한 일은 대단한 기능 개발이라기보다, 배포 이후 재설치라는 운영 흐름이 어디서 깨지는지 끝까지 따라간 일이었습니다.&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;font-family: 'Noto Serif KR';&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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;&lt;b&gt;더미 데이터&lt;/b&gt;&lt;/i&gt; 생각보다 그런 부분들은 개발자들의 프로그램 설계 역량에 많이 달려있습니다. 사실 자신의 프로그램이 만든 데이터 그리고 많은 의존성이 만든 더미데이터를 모두 책임지는것은 개발자에게는 웃지못 할 책임입니다. 하지만 그걸 책임져야 시스템을 관리하는 사람이라고 할 수 있지 않을까요?&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번에 Kubernetes에서도 비슷한 감각을 느꼈습니다. 내가 만든 &lt;code&gt;kite&lt;/code&gt; namespace만 지우면 끝나는 줄 알았는데, 실제로는 내가 설치 과정에서 끌어온 Longhorn, CDI, KubeVirt, host sshd 설정까지 하나의 운영 흔적처럼 남았습니다. 그러니까 uninstall은 &amp;ldquo;내 앱 리소스 삭제&amp;rdquo;가 아니라, 내가 서버에 남긴 약속들을 어디까지 책임질 것인가의 문제에 가까웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심.&lt;/b&gt; main에 push된 뒤의 테스트는 &amp;ldquo;설치가 되는지&amp;rdquo;만 보는 일이 아니었습니다. 실제로 지우고, 바로 다시 설치하고, 그 사이에 SSH 포트와 Longhorn namespace가 어떤 상태로 지나가는지 확인하는 일이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite API&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;http&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회원가입, 로그인, VM 생성 요청을 받는 HTTP 서버입니다. 직접 VM을 만들기보다 &lt;code&gt;KiteUser&lt;/code&gt;, &lt;code&gt;KiteVirtualMachine&lt;/code&gt; 같은 CRD에 사용자의 의도를 기록합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite controller&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;reconcile&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CRD에 적힌 desired state를 보고 namespace, quota, Secret, DataVolume, KubeVirt VM 같은 실제 Kubernetes 리소스를 맞춰주는 역할입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KubeVirt, CDI, Longhorn&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;runtime&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KubeVirt는 Kubernetes 안에서 VM을 실행하고, CDI는 VM disk image를 DataVolume/PVC로 가져오며, Longhorn은 그 PVC가 올라갈 storage backend 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;kite-gateway&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;ssh&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;외부 22번 포트를 잡는 SSH 입구입니다. VM 계정이면 VM으로 보내고, 기존 host 계정이면 옮겨둔 host sshd로 인증을 넘기는 fallback 역할도 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.2rem auto 1.8rem auto; line-height: 1.6;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.2rem auto 1.8rem auto; line-height: 1.6;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #374151;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background: linear-gradient(to top, #d1d5db 42%, transparent 42%); padding: 0 4px;&quot;&gt;배포 이후의 진짜 테스트는, 지우고 다시 설치할 때 시작됐습니다.&lt;/span&gt;&lt;/i&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 배포 이후에 확인하고 싶었던 것&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 목표는 이 정도였습니다. &lt;code&gt;main&lt;/code&gt;에 push된 상태에서 GitHub에 올라간 원격 shell script만 사용합니다. 로컬 checkout에서 직접 파일을 실행하지 않고, README에 적힌 것처럼 GitHub raw URL에서 &lt;code&gt;ghcr-install.sh&lt;/code&gt;와 &lt;code&gt;uninstall.sh&lt;/code&gt;를 받아 실행합니다. 그래야 &amp;ldquo;내 컴퓨터에 있는 최신 파일&amp;rdquo;이 아니라 &amp;ldquo;사용자가 실제로 받게 되는 main의 파일&amp;rdquo;을 테스트할 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 의문이 생겼습니다. stage에서는 E2E가 돌았는데, 왜 main에 올라간 뒤 README 방식으로 uninstall/install을 하니까 Longhorn 문제가 나왔을까. 처음에는 이게 좀 이상했습니다. 같은 코드라면 같은 결과가 나와야 할 것 같았기 때문입니다. 그런데 다시 보면 두 테스트가 보고 있던 표면이 달랐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;stage E2E&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;runtime&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 checkout의 스크립트와 이미지를 기준으로, Kite가 클러스터에서 실제로 뜨고 API, CRD, controller, SSH gateway가 연결되는지를 봅니다. 주로 &amp;ldquo;설치된 뒤 서비스가 일하는가&amp;rdquo;를 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;main 원격 재설치 테스트&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;release&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub main의 raw script와 GHCR image만으로, 기존 설치를 full uninstall한 직후 다시 설치합니다. 여기서는 &amp;ldquo;사용자가 공개된 배포 경로만 써도 지웠다가 다시 살릴 수 있는가&amp;rdquo;를 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Longhorn 문제는 후자에서 더 잘 드러났습니다. stage E2E가 잘못됐다는 뜻은 아니었습니다. 다만 stage E2E는 이미 올라간 클러스터에서 서비스의 정상 path를 강하게 봤고, main 원격 테스트는 Longhorn 설치 자체를 지우고 다시 만드는 destructive path를 봤습니다. 이 destructive path에서는 webhook configuration, namespace finalizer, host sshd restore worker 같은 설치 주변부가 제품의 일부처럼 튀어나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 full uninstall&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite namespace, CRD, golden image, Longhorn host data, Longhorn 설치까지 지웁니다. host sshd도 22번으로 복원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 다음 GHCR install&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;로컬 build가 아니라 GHCR image pull 기반으로 설치합니다. 사용자가 README에서 실행하는 경로와 같아야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;verify와 실제 표면 확인&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite workload, DataVolume, API health, frontend, SSH gateway fallback, host sshd direct login을 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막 상태를 설치 완료로 남김&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;테스트가 끝난 뒤 서버는 22번 gateway, 2222번 host sshd 상태로 남아 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자동화 환경에서는 &lt;code&gt;curl ... | bash&lt;/code&gt;를 그대로 쓰면 sudo password prompt를 받을 TTY가 없어서 실패할 수 있습니다. 그래서 테스트에서는 같은 GitHub main script를 다운로드하되, &lt;code&gt;sudo env ... bash script&lt;/code&gt; 형태로 실행했습니다. 중요한 점은 실행한 script가 로컬 파일이 아니라 GitHub &lt;code&gt;main&lt;/code&gt;에서 내려온 파일이었다는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 첫 번째로 걸린 것은 Longhorn 삭제였습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 full uninstall을 실행했을 때 Kite 리소스 자체는 지워졌습니다. golden image DataVolume과 PVC도 지워졌고, namespace와 CRD도 삭제 요청이 들어갔습니다. 그런데 &lt;code&gt;longhorn-system&lt;/code&gt; namespace가 Terminating에서 빠져나오지 못했습니다. 겉으로는 &amp;ldquo;Longhorn이 삭제 중에 멈췄다&amp;rdquo; 정도로 보였지만, 실제로는 Kubernetes admission webhook 쪽에서 막히고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;failed calling webhook &quot;validator.longhorn.io&quot;
service &quot;longhorn-admission-webhook&quot; not found&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 webhook은 HTTP API의 일반적인 webhook과 비슷한 단어지만, Kubernetes에서는 조금 더 낮은 위치에 있습니다. 사용자가 어떤 리소스를 만들거나 지우면 요청은 먼저 Kubernetes API server로 갑니다. API server는 그 요청을 etcd에 저장하기 전에 validating webhook이나 mutating webhook을 호출할 수 있습니다. 쉽게 말하면 &amp;ldquo;이 리소스 변경을 받아도 되는지 외부 검사 서버에게 물어보는 문지기&amp;rdquo;에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;webhook configuration&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API server에게 &amp;ldquo;Longhorn 리소스 변경 전에 이 webhook을 호출하라&amp;rdquo;고 알려주는 cluster-wide 규칙입니다. namespace 안에만 묶인 설정이 아니라 클러스터에 남을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;webhook backing Service&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 규칙이 실제로 호출하는 Kubernetes Service입니다. Longhorn에서는 보통 &lt;code&gt;longhorn-system&lt;/code&gt; namespace 안의 admission webhook pod로 연결됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삭제 순서가 꼬인 상태&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Service와 pod는 먼저 사라졌는데 webhook configuration만 남으면, API server는 없는 Service를 계속 호출하려고 합니다. 그러면 Longhorn 리소스를 정리하려는 요청 자체가 다시 실패합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;namespace finalization 실패&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;namespace 삭제의 마지막 정리 과정에서도 리소스 변경 요청이 발생할 수 있습니다. 이때 죽은 webhook 호출이 끼어들면 namespace가 Terminating에서 오래 멈출 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 메시지는 처음에는 좀 이상했습니다. 이미 Longhorn을 지우고 있는데 왜 Longhorn webhook이 다시 삭제를 막는가 싶었습니다. 그런데 위 구조로 보면 이유가 보입니다. webhook configuration은 클러스터 레벨 규칙이라 남아 있고, 그 규칙이 가리키는 backing Service는 namespace 안에서 이미 삭제됐습니다. API server 입장에서는 &amp;ldquo;검사를 받아야 하는데 검사 서버가 없다&amp;rdquo;가 됩니다. 그래서 삭제를 마무리하려는 요청도 실패할 수 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기에 finalizer도 같이 얽혔습니다. finalizer는 Kubernetes 리소스가 삭제되기 전에 controller가 정리 작업을 끝낼 시간을 주는 metadata hook입니다. 정상 상황에서는 좋은 안전장치입니다. 그런데 controller나 webhook 쪽이 이미 내려간 상태에서 finalizer만 남으면, 리소스는 삭제 요청을 받았는데 마지막 정리 확인을 끝내지 못한 상태가 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 &lt;code&gt;uninstall-kite.sh&lt;/code&gt;와 &lt;code&gt;build-clear.sh&lt;/code&gt;에 Longhorn webhook configuration 삭제를 명시적으로 추가했습니다. 그리고 Longhorn CR finalizer를 지우는 코드도 고쳤습니다. 기존에는 &lt;code&gt;kubectl get ... -A -o name&lt;/code&gt; 결과를 namespace 없이 patch하려고 했는데, namespaced Longhorn CR은 &lt;code&gt;longhorn-system&lt;/code&gt; namespace를 명시해야 안정적으로 patch할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존 cleanup&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;stuck&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;namespace 삭제를 요청하고 finalizer patch를 시도했지만, Longhorn webhook configuration이 남아 있으면 API server가 죽은 webhook을 계속 호출할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;수정한 cleanup&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;explicit&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Longhorn validating/mutating webhook configuration을 먼저 제거하고, namespaced CR과 cluster-scoped CR finalizer를 분리해서 정리한 뒤, Longhorn CRD 삭제도 &lt;code&gt;--wait=false&lt;/code&gt;로 요청합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 변경 뒤에는 실제 remote uninstall에서 &lt;code&gt;longhorn-webhook-validator&lt;/code&gt;, &lt;code&gt;longhorn-webhook-mutator&lt;/code&gt;, Longhorn CRD들이 script 안에서 삭제됐습니다. 여기까지는 괜찮아보였습니다. 그런데 다음 문제는 SSH 복원 쪽에서 나왔습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 생각이 조금 바뀌었습니다. 처음에는 삭제 대상만 잘 지우면 된다고 봤습니다. 그런데 실제로는 삭제 대상을 감시하던 장치가 더 오래 남아서 삭제를 막을 수 있었습니다. namespace 안의 Service는 사라졌는데, cluster-wide webhook configuration은 살아 있는 상태였습니다. 이걸 보고 나니 cleanup은 리소스 목록을 지우는 작업이 아니라, 리소스를 둘러싼 규칙과 감시자까지 같이 정리하는 작업으로 보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. host sshd 복원 worker는 로그 파일 때문에 실패했습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite gateway는 외부 22번 포트를 사용합니다. 그래서 설치할 때 기존 host sshd는 2222 같은 다른 포트로 이동합니다. 반대로 uninstall할 때는 gateway를 지우기 전에 host sshd를 22번으로 되돌리는 작업을 예약해야 합니다. 그렇지 않으면 gateway를 지우는 순간 원격 SSH 세션이 끊기고, 서버 22번도 비어버릴 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 프로젝트에서는 &lt;code&gt;manage-host-sshd.sh restore-after-port-free&lt;/code&gt;를 background worker로 띄워서, gateway가 22번을 내려놓으면 host sshd config를 원래대로 복원하게 했습니다. 그런데 실제 uninstall에서는 이 worker가 실패했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/tmp/kite-host-sshd-restore.log: Permission denied
[kite-deploy] host sshd restore worker failed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 root로 실행하는데 왜 &lt;code&gt;/tmp&lt;/code&gt; 로그 파일에 못 쓰는지 이상했습니다. 그런데 Linux에는 &lt;code&gt;/tmp&lt;/code&gt; 같은 sticky directory에서 기존 사용자 소유 regular file에 root가 redirection으로 append하는 것을 막는 보호 설정이 있을 수 있습니다. 예전에 만들어진 &lt;code&gt;/tmp/kite-host-sshd-restore.log&lt;/code&gt;가 일반 사용자 소유로 남아 있었고, uninstall은 root로 돌면서 그 파일에 append하려다가 막혔습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 점은 restore logic이 틀린 것이 아니라, worker를 시작하기 전 redirection 단계에서 실패했다는 점이었습니다. 그래서 로그 파일을 열 수 없으면 &lt;code&gt;mktemp&lt;/code&gt;로 고유한 새 로그 파일을 만들어 쓰도록 했습니다. 그리고 권한 검사 실패 메시지가 사용자에게 그대로 보이지 않도록 redirection 검사를 묶었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;host sshd restore worker pid=2966236
log=/tmp/kite-host-sshd-restore.r7HN3o.log
...
host sshd restore worker completed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;수정 뒤에는 실제 uninstall에서 worker가 고유 로그 파일을 잡았고, 마지막에 &lt;code&gt;host sshd restore worker completed&lt;/code&gt;까지 확인했습니다. 그 뒤 &lt;code&gt;hy3on.site:22&lt;/code&gt;는 다시 host sshd로 열렸고, &lt;code&gt;2222&lt;/code&gt;는 닫혔습니다. 이 단계에서야 &amp;ldquo;삭제가 끝났다&amp;rdquo;라고 말할 수 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 부분은 작아보였지만 꽤 좋은 경고였습니다. root로 실행하면 권한 문제는 거의 끝난다고 생각하기 쉽습니다. 그런데 shell redirection은 worker 로직 안으로 들어가기도 전에 실패할 수 있었습니다. 결국 restore worker의 상태는 sshd config만이 아니라, worker를 띄우는 로그 파일, 실행 사용자, &lt;code&gt;/tmp&lt;/code&gt; 보호 정책까지 포함하고 있었습니다. 배포 스크립트에서 주변 환경도 상태라는 걸 다시 느꼈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 바로 재설치하니 namespace Terminating race가 나왔습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 다음에는 다시 README install을 실행했습니다. 그런데 이번에는 Longhorn manifest apply가 실패했습니다. 이유는 &lt;code&gt;longhorn-system&lt;/code&gt; namespace가 아직 Terminating 중이었기 때문입니다. uninstall 명령은 끝났지만, Kubernetes namespace controller가 실제 namespace deletion을 마치는 데는 시간이 더 필요했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;namespace/longhorn-system unchanged
Warning: Detected changes to resource longhorn-system which is currently being deleted.

serviceaccounts &quot;longhorn-service-account&quot; is forbidden:
unable to create new content in namespace longhorn-system because it is being terminated&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이건 uninstall이 실패한 것과는 다른 문제였습니다. 삭제 요청이 정상적으로 들어갔고, 복원 worker도 성공했습니다. 다만 바로 다음 설치가 너무 빨랐습니다. Kubernetes에서는 namespace 이름이 같아도 Terminating 중인 namespace 안에는 새 리소스를 만들 수 없습니다. 그래서 install script가 Longhorn을 apply하기 전에 &lt;code&gt;longhorn-system&lt;/code&gt; namespace의 &lt;code&gt;deletionTimestamp&lt;/code&gt;를 확인하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Longhorn 설치 직전 namespace 조회&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;longhorn-system&lt;/code&gt;이 없으면 바로 설치합니다. 있더라도 삭제 중이 아니면 기존처럼 apply합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;deletionTimestamp가 있으면 대기&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이전 uninstall의 namespace deletion이 끝날 때까지 기다립니다. 기본 timeout은 &lt;code&gt;300s&lt;/code&gt;로 잡았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;완전히 사라진 뒤 apply&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;namespace가 사라진 뒤에야 Longhorn manifest를 다시 apply합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 수정은 &lt;code&gt;install-longhorn.sh&lt;/code&gt;에 들어갔습니다. 재설치 직후 race를 install 쪽에서 받아주는 것이 맞아보였습니다. uninstall이 항상 namespace deletion 완료까지 기다리면 사용자는 삭제가 오래 걸린다고 느낄 수 있고, install은 어차피 같은 namespace를 다시 만들려는 순간이므로 이 조건을 가장 잘 판단할 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 배운 건 timeout을 늘리는 법이 아니라, 누가 기다림을 책임져야 하는가였습니다. uninstall이 끝났다는 말과, 클러스터가 다시 install 가능한 상태가 됐다는 말은 달랐습니다. 지우는 쪽은 삭제 요청을 넣고 복원을 예약하면 자기 역할을 거의 끝낸 것일 수 있습니다. 하지만 다시 같은 namespace를 쓰려는 install 쪽은 그 namespace가 정말 사라졌는지를 확인해야 했습니다. 기다림도 아무 데나 붙이면 되는 게 아니라, 그 상태를 가장 필요로 하는 단계가 가져가야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. 중간중간 이전 수정도 실제로 검증됐습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 main 원격 검증에서는 새로 고친 것만 확인된 것이 아니었습니다. 이전에 넣었던 수정도 실제 main install에서 같이 검증됐습니다. 예를 들어 Longhorn disk tag 재시도는 fresh Longhorn install에서 바로 드러났습니다. Longhorn node CR은 먼저 생기지만 &lt;code&gt;status.diskStatus&lt;/code&gt;가 늦게 채워질 수 있습니다. 그래서 처음 몇 번은 disk가 Ready/Schedulable로 보이지 않았고, 몇 번 더 기다린 뒤에야 &lt;code&gt;node.longhorn.io/hhs2003 patched&lt;/code&gt;가 나오면서 &lt;code&gt;kite&lt;/code&gt; tag가 붙었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;skipping Longhorn disk default-disk... because ready= schedulable= allowScheduling=true
waiting for a Ready/Schedulable Longhorn disk tagged kite
...
node.longhorn.io/hhs2003 patched
found 1 Ready/Schedulable Longhorn disk(s) tagged kite&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GHCR pull timeout도 실제 환경에서 의미가 있었습니다. 원격 서버에서 GHCR이나 &lt;code&gt;pkg-containers.githubusercontent.com&lt;/code&gt; DNS가 순간적으로 느릴 수 있었고, 처음에는 rollout timeout이 짧아서 Kite workload가 늦게 뜨면 실패로 보일 수 있었습니다. 그래서 &lt;code&gt;KITE_ROLLOUT_TIMEOUT&lt;/code&gt; 기본값을 늘렸고, 이번 install에서는 &lt;code&gt;20m&lt;/code&gt;로 실행했습니다. 실제로는 이번 마지막 install에서 GHCR pull은 빨리 끝났지만, 이 옵션은 느린 네트워크 환경에서 필요해보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;여기서 다시 느낀 점.&lt;/b&gt; Kubernetes install script는 &amp;ldquo;resource를 만들었다&amp;rdquo;와 &amp;ldquo;controller가 상태를 채웠다&amp;rdquo;와 &amp;ldquo;workload가 실제로 서비스를 받을 수 있다&amp;rdquo;를 같은 단계로 보면 안 됩니다. 이번 문제들은 대부분 그 사이의 간격에서 나왔습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. 최종 확인은 연결이 아니라 역할 확인이었습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막으로 재설치를 끝낸 뒤에는 단순히 pod가 Running인지 보는 데서 멈추지 않았습니다. Kite는 여러 서비스가 붙어서 하나의 플랫폼처럼 보이는 구조입니다. 그래서 &amp;ldquo;프로세스가 떠 있다&amp;rdquo;가 아니라 &amp;ldquo;각 서비스가 맡은 역할을 실제로 하고 있다&amp;rdquo;를 봐야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;kite-api&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자 요청을 받는 HTTP API입니다. 여기서는 health endpoint와 CRD read path를 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;kite-controller&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CRD를 읽고 실제 Kubernetes/KubeVirt 리소스로 맞추는 reconciler입니다. 여기서는 golden image DataVolume과 PVC 상태가 주요 확인 대상이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;kite-frontend&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;브라우저에서 접근하는 UI입니다. 여기서는 서비스 응답과 page load가 되는지를 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;kite-gateway&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;외부 SSH 입구입니다. 여기서는 22번 포트가 gateway 역할을 하고, host 계정 fallback과 host key fingerprint가 맞는지 확인했습니다.&lt;/span&gt;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;kite-api&lt;/code&gt;, &lt;code&gt;kite-controller&lt;/code&gt;, &lt;code&gt;kite-frontend&lt;/code&gt;, &lt;code&gt;kite-gateway&lt;/code&gt; 네 pod는 모두 Running이었고, Ubuntu golden image DataVolume은 &lt;code&gt;Succeeded&lt;/code&gt;, PVC는 &lt;code&gt;Bound&lt;/code&gt;였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kite-api          1/1 Running
kite-controller   1/1 Running
kite-frontend     1/1 Running
kite-gateway      1/1 Running

ubuntu-22.04 DataVolume  Succeeded  100.0%
ubuntu-22.04 PVC         Bound&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;API health는 단순 process health만 보지 않았습니다. &lt;code&gt;KiteUser&lt;/code&gt;와 &lt;code&gt;KiteVirtualMachine&lt;/code&gt; CRD read path도 같이 봤습니다. 즉 API server가 Kubernetes API server를 통해 CRD list를 읽을 수 있는지 확인했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;status&quot;: &quot;ok&quot;,
  &quot;checks&quot;: [
    &quot;kiteusers.crd.etcdReadPath: ok&quot;,
    &quot;kitevirtualmachines.crd.etcdReadPath: ok&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSH 쪽은 더 중요했습니다. 최종 상태에서 22번은 Kite gateway가 잡고 있어야 하고, host sshd는 2222에서 직접 로그인되어야 합니다. 그리고 gateway fallback도 host 계정으로 password login을 넘길 수 있어야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;hy3on.site:22&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite gateway입니다. VM sshId가 아니면 host sshd fallback으로 password 인증을 넘깁니다. 실제 password probe가 통과했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;hy3on.site:2222&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존 host sshd입니다. 직접 password login이 통과했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;fingerprint&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gateway가 내보내는 SSH host key fingerprint가 실제 host &lt;code&gt;/etc/ssh/ssh_host_ed25519_key&lt;/code&gt; fingerprint와 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막 fingerprint는 이 값으로 일치했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;host_fp=SHA256:/Kq/jk65Fulh/8SiQuE8POAdrYLiEI3KiDy2l1fDE2Q
gateway_fps=SHA256:/Kq/jk65Fulh/8SiQuE8POAdrYLiEI3KiDy2l1fDE2Q
fingerprint-match-ok&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서도 생각보다 중요한 기준이 하나 생겼습니다. SSH 22번 포트가 열렸다는 것만으로는 충분하지 않았습니다. gateway가 SSH 입구 역할을 한다면, 사용자가 보는 host identity도 함부로 바뀌면 안 됩니다. 접속은 되는데 fingerprint가 바뀌면, 운영자 입장에서는 같은 서버인지 다른 서버인지 다시 의심해야 합니다. 그래서 이 확인은 단순 연결 테스트가 아니라, gateway가 기존 host의 정체성을 얼마나 자연스럽게 이어받는지 보는 테스트에 가까웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7. 왜 stage에서는 됐는데 main에서 문제가 나왔나&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마지막에 정리하고 싶었던 질문은 이거였습니다. stage에서는 E2E가 됐는데 왜 main 원격 배포 스크립트를 적용하니까 Longhorn 문제가 나왔을까. 결론부터 말하면, 서비스 자체의 E2E와 release script의 destructive 재설치 테스트는 같은 테스트가 아니었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 테스트 표면이 달랐습니다&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;scope&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;stage E2E는 Kite workload가 잘 떠서 API, CRD, controller, gateway가 이어지는지를 봤습니다. main 원격 테스트는 공개 script로 Longhorn까지 지웠다가 바로 다시 설치하는 흐름을 봤습니다. Longhorn webhook 문제는 후자에서 나오는 종류였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 삭제는 생성보다 상태가 더 많았습니다&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;state&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치는 리소스를 만드는 쪽에 가깝지만, 삭제는 finalizer, admission webhook, namespace controller, background worker가 얽힙니다. Service는 사라졌는데 webhook configuration이 남거나, namespace가 아직 Terminating 중인 상태가 생길 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 바로 재설치하는 타이밍이 문제를 드러냈습니다&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Noto Serif KR'; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;race&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;uninstall 명령이 끝난 직후에도 Kubernetes 내부 삭제가 끝났다고 볼 수는 없었습니다. 그래서 install 쪽에서 &lt;code&gt;longhorn-system&lt;/code&gt; namespace가 완전히 사라질 때까지 기다리는 처리가 필요했습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해결은 각 문제를 한 번 더 실행해도 버티는 스크립트 동작으로 넣었습니다. Longhorn을 지울 때는 validating/mutating webhook configuration을 먼저 정리하고, namespaced Longhorn CR finalizer는 namespace를 명시해서 patch하도록 했습니다. host sshd restore worker는 고정된 &lt;code&gt;/tmp&lt;/code&gt; 로그 파일에 의존하지 않고 &lt;code&gt;mktemp&lt;/code&gt; 로그 파일을 잡게 했습니다. 그리고 재설치 직전에는 &lt;code&gt;longhorn-system&lt;/code&gt; namespace가 Terminating 상태인지 확인하고, 완전히 사라진 뒤 Longhorn manifest를 apply하도록 했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이게 최선이었냐고 하면, 현재 단계에서는 꽤 현실적인 선택이라고 봤습니다. release script는 이미 망가진 중간 상태를 만날 수 있고, 운영자는 &amp;ldquo;&lt;i&gt;설치전의 상태 &lt;/i&gt;&amp;rdquo;를 기대합니다. 그래서 script가 Longhorn의 webhook 잔여물, namespace Terminating, sshd 복원 로그 문제를 직접 받아주는 쪽이 맞아보였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;다만 더 깔끔한 방향은 있습니다. uninstall을 단순 shell 명령 묶음으로 두기보다, 먼저 cluster 상태를 진단하는 preflight/audit 단계를 두고, 삭제 plan을 출력한 뒤, 각 단계가 끝났는지 관찰하는 방식으로 나누는 겁니다.&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;font-family: 'Noto Serif KR';&quot;&gt;조금 더 나아가면, 지금의 shell script는 &amp;ldquo;명령을 실행하는 도구&amp;rdquo;이고, 앞으로 필요한 것은 &amp;ldquo;클러스터가 어느 상태인지 판단하는 도구&amp;rdquo;에 가깝습니다. 예를 들어 Longhorn webhook configuration이 남아 있는지, backing Service가 살아 있는지, namespace deletion이 끝났는지, host sshd restore가 실제로 listen 상태까지 갔는지를 하나의 release health check로 볼 수 있어야 합니다. 그러면 stage에서 성공한 E2E와 main에서 필요한 release 재설치 테스트 사이의 빈틈이 줄어듭니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이번 테스트의 결론은 &amp;ldquo;main script로 설치가 된다&amp;rdquo;보다 조금 더 구체적입니다. 현재 main의 원격 script는 full uninstall 뒤 재설치 흐름을 버팁니다. Longhorn은 지워지고 다시 올라오며, Kite workload는 GHCR image로 뜨고, golden image는 import되고, API는 CRD read path를 통과하고, frontend는 응답하고, SSH 22번은 gateway로, 2222번은 host sshd로 역할이 분리됩니다. gateway fingerprint도 기존 host key와 같습니다. 다만 다음부터는 stage에서 E2E가 통과했다는 사실만으로 release uninstall/install까지 안전하다고 보지 않고, 이 destructive 재설치 path를 별도의 테스트 축으로 계속 가져가야겠다고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번에 제일 크게 바뀐 생각은, 삭제가 설치의 반대가 아니라는 점이었습니다. 처음에는 &lt;code&gt;install&lt;/code&gt;이 리소스를 만들고, &lt;code&gt;uninstall&lt;/code&gt;이 그 리소스를 지우면 된다고 봤습니다. 그런데 실제 Kubernetes에서는 그렇게 대칭적으로 움직이지 않았습니다. 삭제에는 finalizer가 있고, namespace controller가 있고, admission webhook이 있고, host sshd 복원처럼 Kubernetes 바깥의 상태도 같이 끼어 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그러니까 이번 문제는 단순히 Longhorn cleanup 버그 하나를 잡은 일이 아니었습니다. 제가 갖고 있던 테스트의 모델이 조금 바뀐 일이었습니다. stage E2E는 &amp;ldquo;원하는 상태가 만들어진 뒤 서비스가 도는가&amp;rdquo;를 확인했습니다. 하지만 release 재설치 테스트는 &amp;ldquo;원하는 상태를 지우고 다시 적는 동안, 시스템이 중간 상태를 어떻게 지나가는가&amp;rdquo;를 봐야 했습니다. 둘은 비슷해 보이지만 실제로는 전혀 다른 질문이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 얻은 깨달음은, 배포 스크립트도 어느 순간부터 작은 controller처럼 행동해야 한다는 것이었습니다. 그냥 명령을 순서대로 실행하는 shell script로는 부족했습니다. 지금 namespace가 정말 사라졌는지, webhook configuration은 남아 있는지, backing Service는 살아 있는지, host sshd가 실제로 listen 중인지 관찰해야 했습니다. 그리고 관찰한 상태에 따라 기다리거나, 정리하거나, 실패 이유를 말해야 했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;결국 테스트는 &amp;ldquo;성공했다&amp;rdquo;는 도장을 찍는 도구라기보다, 내가 시스템을 어떤 식으로 오해하고 있었는지 드러내는 도구에 가까웠습니다. 이번에는 제가 Kubernetes 삭제를 너무 즉시적인 동작으로 보고 있었다는 것을 알게 됐습니다. 앞으로 Kite의 release 테스트는 서비스가 잘 도는지만 보지 않고, 삭제 중간 상태와 재설치 경계까지 계속 봐야 할 것 같습니다. 그래야 &amp;ldquo;플랫폼이 뜬다&amp;rdquo;와 &amp;ldquo;운영자가 다시 살릴 수 있다&amp;rdquo;를 같은 말로 착각하지 않을 수 있습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/60</guid>
      <comments>https://hy3ons.tistory.com/60#entry60comment</comments>
      <pubDate>Fri, 3 Jul 2026 18:57:34 +0900</pubDate>
    </item>
    <item>
      <title>시스템 프로그래밍 번외 - Kernel Linux Posix VirtualMem</title>
      <link>https://hy3ons.tistory.com/59</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;551&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ownC2/dJMcabdDO8g/licKZc9JFpvJp70AZl0Bx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ownC2/dJMcabdDO8g/licKZc9JFpvJp70AZl0Bx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ownC2/dJMcabdDO8g/licKZc9JFpvJp70AZl0Bx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FownC2%2FdJMcabdDO8g%2FlicKZc9JFpvJp70AZl0Bx1%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;2172&quot; height=&quot;551&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;551&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab 글을 마무리하면서 마지막에 syscall 이야기를 조금 붙였습니다. 처음에는 그냥 &lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;waitpid()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;그런데 syscall을 정리하다 보니, 새로운 공부를 할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.2rem auto 1.8rem auto; line-height: 1.6;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #374151;&quot;&gt;&lt;i&gt;&lt;span style=&quot;background: linear-gradient(to top, #d1d5db 42%, transparent 42%); padding: 0 4px;&quot;&gt;POSIX 기준으로 signal handler 안에서 printf를 호출하는 건 안전한 패턴이라고 말하기 어렵다.&lt;/span&gt;&lt;/i&gt;&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab에서는 &lt;code&gt;sigchld_handler&lt;/code&gt; 안에서 &lt;code&gt;printf&lt;/code&gt;를 호출하는 식으로 구현했습니다. trace 출력 형식도 맞춰야 했고, 과제 풀이 문맥에서는 그렇게 작성한 코드가 흔합니다. 하지만 그것들은 사실 안티패턴이었습니다. 그리고 문득 궁금해진것은&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; text-align: start;&quot;&gt;데, &lt;code&gt;printf&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;printf&lt;/code&gt;도 결국 마지막에는 &lt;code&gt;write&lt;/code&gt; syscall을 부를 텐데, 왜 &lt;code&gt;printf&lt;/code&gt;는 위험하고 &lt;code&gt;write&lt;/code&gt;는 안전하다고 하는 걸까? 버퍼를 안 거치면 정말 안전한 건가? 문장 하나를 쓰는 도중에 signal이 끼어들면 결국 출력이 섞이는 건 똑같은 것 아닌가?&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;font-family: 'Noto Serif KR';&quot;&gt;이 글은 그 질문을 따라가면서 정리한 글입니다. Shell Lab 본문이 &quot;어떻게 작은 shell을 구현할 것인가&quot;에 가까웠다면, 이번 글은 그 구현의 끝부분에서 만난 &lt;b&gt;async-signal-safe&lt;/b&gt;, &lt;b&gt;재진입성&lt;/b&gt;, &lt;b&gt;stdio buffer&lt;/b&gt;, &lt;b&gt;write syscall&lt;/b&gt;의 경계를 복기하는 글입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 처음에는 syscall만 보려고 했습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab에서 쓰는 함수들은 겉으로 보면 전부 평범한 C 함수처럼 보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pid_t pid = fork();
waitpid(pid, &amp;amp;status, WUNTRACED);
kill(-pid, SIGINT);
sigprocmask(SIG_BLOCK, &amp;amp;mask, &amp;amp;old_mask);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 이 함수들은 그냥 라이브러리 내부에서 끝나는 함수가 아닙니다. 일반 프로세스는 유저 모드에서 실행되기 때문에, 자기 마음대로 process table을 고치거나 다른 process group에 signal을 꽂아 넣을 수 없습니다. 그래서 정해진 시스템 콜 경로로 커널에게 요청합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;glibc wrapper 호출&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&gt;, &lt;code&gt;write()&lt;/code&gt; 같은 C 함수 호출이 먼저 보입니다. 이 단계에서는 아직 유저 공간 코드입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;레지스터에 syscall 번호와 인자 배치&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;x86-64 Linux 기준으로 &lt;code&gt;rax&lt;/code&gt;에는 syscall number가 들어가고, &lt;code&gt;rdi&lt;/code&gt;, &lt;code&gt;rsi&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt; 같은 레지스터에는 인자가 들어갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CPU의 syscall instruction 실행&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서부터가 진짜 경계입니다. CPU가 유저 모드에서 커널 모드로 전환하고, 커널이 등록해둔 syscall entry로 점프합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널 작업 후 유저 모드 복귀&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널은 process 생성, signal 전달, 파일 쓰기 같은 실제 작업을 처리하고 결과값을 다시 유저 코드에 돌려줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기까지는 어느 정도 예상한 흐름이었습니다. 그런데 &lt;code&gt;write()&lt;/code&gt;를 같이 보면서, 자연스럽게 출력 함수 쪽으로 시선이 갔습니다. Shell Lab의 handler 안에서는 &lt;code&gt;printf()&lt;/code&gt;를 쓰고 있었고, POSIX 문서에서는 handler 안에서 호출해도 되는 함수 목록을 따로 두고 있었습니다. 여기서부터 조금 재미있어졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. POSIX는 signal handler 안에서 아무 함수나 부르지 말라고 합니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;signal handler는 평범한 함수처럼 생겼지만, 호출 타이밍이 평범하지 않습니다. 제가 &lt;code&gt;handler()&lt;/code&gt;를 직접 호출하는 게 아니라, OS가 &quot;지금 signal이 왔다&quot;면서 기존 실행 흐름을 끊고 handler로 들어옵니다.&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;font-family: 'Noto Serif KR';&quot;&gt;문제는 이 interrupt 지점이 예쁘게 정해져 있지 않다는 점입니다. 메인 흐름이 &lt;code&gt;printf&lt;/code&gt; 내부에서 버퍼 포인터를 바꾸는 중일 수도 있고, &lt;code&gt;malloc&lt;/code&gt; 내부에서 heap 구조를 건드리는 중일 수도 있고, 어떤 lock을 잡은 직후일 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심.&lt;/b&gt; async-signal-safe라는 말은 &quot;signal handler 안에서 호출해도 안전한 함수&quot;라는 뜻입니다. 반대로 말하면, handler 안에서 아무 함수나 부르면 기존 실행 흐름이 잡고 있던 내부 상태를 다시 건드릴 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Linux man-pages의 &lt;code&gt;signal-safety(7)&lt;/code&gt;는 이 문제를 &lt;code&gt;stdio&lt;/code&gt; 예시로 설명합니다. &lt;code&gt;stdio&lt;/code&gt; 계열 함수는 buffered I/O를 위해 정적 버퍼와 카운터, 인덱스, 포인터를 관리합니다. 메인 프로그램이 &lt;code&gt;printf&lt;/code&gt;를 호출해서 그 내부 상태를 일부만 갱신한 순간 signal handler가 다시 &lt;code&gt;printf&lt;/code&gt;를 호출하면, 두 번째 &lt;code&gt;printf&lt;/code&gt;는 일관되지 않은 데이터 위에서 동작할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;safe로 지정된 함수&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;POSIX list&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;write&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;waitpid&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;, &lt;code&gt;sigprocmask&lt;/code&gt; 같은 함수들은 async-signal-safe 목록에 들어갑니다. 이 말은 handler 안에서 호출해도 POSIX가 안전성을 요구한다는 뜻입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;safe로 지정되지 않은 함수&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;stdio / heap&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;sprintf&lt;/code&gt;, &lt;code&gt;malloc&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt; 같은 함수는 이런 문맥에서 조심해야 합니다. 특히 &lt;code&gt;printf&lt;/code&gt;는 보기에는 단순 출력 함수지만, 내부적으로 &lt;code&gt;FILE&lt;/code&gt; 객체와 버퍼 상태를 만집니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 제가 처음에 했던 질문은 이거였습니다. &quot;그래도 결국 &lt;code&gt;printf&lt;/code&gt;도 마지막에는 &lt;code&gt;write&lt;/code&gt; syscall을 부르는 것 아닌가? 그럼 뭐가 그렇게 다르지?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. printf도 결국 write를 부릅니다. 그런데 그 전에 할 일이 너무 많습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;printf(&quot;hello&quot;)&lt;/code&gt;를 호출한다고 해서 글자가 바로 커널로 들어가는 건 아닙니다. 보통은 유저 공간의 &lt;code&gt;stdio&lt;/code&gt; 버퍼에 먼저 들어갑니다. 그리고 버퍼가 차거나, 줄바꿈이 나오거나, &lt;code&gt;fflush&lt;/code&gt;가 호출되는 등의 타이밍에 내부적으로 &lt;code&gt;write&lt;/code&gt; syscall을 호출합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;format 해석&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;%d&lt;/code&gt;, &lt;code&gt;%s&lt;/code&gt; 같은 포맷 문자열을 해석하고 실제 출력 문자열을 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;stdio buffer와 FILE 상태 갱신&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;stdout의 내부 버퍼, 현재 위치 포인터, 남은 공간 카운터 같은 상태를 수정합니다. 이 구간이 signal에 취약한 지점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;필요하면 write syscall 호출&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;버퍼를 실제 파일 descriptor로 밀어낼 때 비로소 &lt;code&gt;write(fd, buf, len)&lt;/code&gt; 형태의 syscall 경로로 들어갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;버퍼 상태 정리 후 반환&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;write가 끝나면 stdio는 내부 버퍼 상태를 다시 정리하고 &lt;code&gt;printf&lt;/code&gt; 호출자에게 돌아갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러니까 &lt;code&gt;printf&lt;/code&gt;와 &lt;code&gt;write&lt;/code&gt;는 같은 층의 함수가 아니었습니다. &lt;code&gt;printf&lt;/code&gt;는 유저 공간에서 꽤 많은 상태를 만지는 고수준 함수이고, &lt;code&gt;write&lt;/code&gt;는 파일 descriptor에 bytes를 쓰라고 커널에 요청하는 더 낮은 층의 함수입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;정리.&lt;/b&gt; &lt;code&gt;printf&lt;/code&gt;도 언젠가는 &lt;code&gt;write&lt;/code&gt; syscall을 부릅니다. 하지만 handler 안에서 문제가 되는 건 &quot;마지막에 write를 부르느냐&quot;가 아니라, 그 전에 유저 공간의 공유 버퍼와 lock을 건드리는 과정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 재진입성이 없다는 말이 이제 조금 명확해졌습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;재진입성이 없다는 말은, 아주 투박하게 말하면 &lt;b&gt;함수가 아직 끝나지 않았는데 같은 함수로 다시 들어오면 상태가 꼬일 수 있다&lt;/b&gt;는 뜻입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이걸 &lt;code&gt;printf&lt;/code&gt;로 보면 바로 감이 옵니다. 메인 흐름이 &lt;code&gt;printf&lt;/code&gt;를 실행하면서 stdout 내부 버퍼의 포인터를 바꾸고 있습니다. 그런데 바로 그 순간 &lt;code&gt;SIGCHLD&lt;/code&gt;가 들어오고, handler 안에서 다시 &lt;code&gt;printf&lt;/code&gt;를 호출합니다. 그러면 handler의 &lt;code&gt;printf&lt;/code&gt;는 메인 흐름이 건드리다 만 stdout 상태를 다시 만지게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/*
 * 실제 glibc 구현이 아니라, 위험한 모양을 보기 위한 개념 코드입니다.
 * 핵심은 stdout 내부 상태를 공유한다는 점입니다.
 */
void rough_printf(const char *s)
{
    lock(stdout);

    while (*s) {
        stdout-&amp;gt;buffer[stdout-&amp;gt;pos] = *s;
        stdout-&amp;gt;pos++;
        s++;

        /*
         * 이 지점에서 SIGCHLD가 들어오고,
         * handler가 다시 printf를 호출하면 같은 stdout 상태를 만집니다.
         */
    }

    write(1, stdout-&amp;gt;buffer, stdout-&amp;gt;pos);
    stdout-&amp;gt;pos = 0;

    unlock(stdout);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 제가 계속 걸렸던 지점은 &quot;handler 쪽 &lt;code&gt;printf&lt;/code&gt;가 먼저 버퍼를 비우고 끝나면, 원래 &lt;code&gt;printf&lt;/code&gt;가 다시 이어서 가면 되는 것 아닌가?&quot;였습니다. 그런데 그게 그렇게 깔끔하게 되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현실적인 문제: lock deadlock&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;stdout lock&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CS:APP 보충 문서에서는 &lt;code&gt;printf&lt;/code&gt;가 console lock을 잡고 &lt;code&gt;write&lt;/code&gt; syscall을 부른 뒤 lock을 푼다고 설명합니다. 메인 흐름의 &lt;code&gt;printf&lt;/code&gt;가 lock을 잡은 상태에서 &lt;code&gt;SIGCHLD&lt;/code&gt; handler로 들어가고, handler가 다시 &lt;code&gt;printf&lt;/code&gt;를 호출하면 같은 lock을 기다리다가 멈출 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;lock이 없다고 해도: 내부 상태 오염&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;buffer state&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;lock이 없는 단순한 버퍼라고 해도 문제가 없어지는 건 아닙니다. handler가 같은 버퍼 인덱스와 포인터를 바꾸고 돌아오면, 메인 흐름은 자신이 멈췄던 지점의 가정과 달라진 상태 위에서 계속 실행됩니다. 출력이 깨질 수도 있고, 더 나쁘면 메모리 상태가 망가질 수도 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 재진입성이라는 단어가 여기서 실감이 났습니다. &quot;함수가 다시 들어와도 괜찮은가?&quot;는 단순히 같은 코드를 두 번 실행해도 되는지의 문제가 아니었습니다. 그 함수가 내부적으로 공유 상태, lock, heap, static buffer를 만지고 있는지가 핵심이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. write는 안전하지만, 출력이 절대 안 섞인다는 뜻은 아닙니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 &lt;code&gt;write&lt;/code&gt;는 왜 안전하다고 할 수 있을까요? &lt;code&gt;write(STDOUT_FILENO, buf, len)&lt;/code&gt;는 유저 공간의 &lt;code&gt;stdio&lt;/code&gt; 버퍼를 조작하지 않습니다. 호출자는 이미 만들어진 bytes의 주소와 길이를 커널에 넘깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const char msg[] = &quot;child reaped\n&quot;;
write(STDOUT_FILENO, msg, sizeof(msg) - 1);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 &quot;안전하다&quot;는 말의 뜻을 좁혀서 봐야 합니다. &lt;code&gt;write&lt;/code&gt;가 안전하다는 말은, handler 안에서 호출해도 &lt;code&gt;printf&lt;/code&gt;처럼 유저 공간의 stdio 내부 상태를 망가뜨리지 않는다는 뜻에 가깝습니다. 화면에 보이는 문장이 절대 안 섞인다는 뜻은 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;중요한 구분.&lt;/b&gt; async-signal-safe는 &quot;출력이 예쁘게 나온다&quot;가 아니라 &quot;handler 안에서 호출해도 프로그램 내부 상태를 깨뜨리지 않는다&quot;에 더 가깝습니다. &lt;code&gt;write&lt;/code&gt;도 signal에 의해 partial write가 될 수 있고, 출력이 시각적으로 섞이는 문제는 별도로 다뤄야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 메인 흐름이 이미 &lt;code&gt;write&lt;/code&gt;로 긴 문장을 출력하고 있었고, 그 사이 handler가 또 &lt;code&gt;write&lt;/code&gt;로 짧은 메시지를 출력하면 터미널 출력은 보기 안 좋게 섞일 수 있습니다. 하지만 이것은 &lt;code&gt;printf&lt;/code&gt;의 내부 buffer나 lock이 깨지는 문제와는 다른 층의 문제입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;printf가 위험한 이유&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;유저 공간의 &lt;code&gt;FILE&lt;/code&gt; 구조체, 내부 버퍼, 포인터, lock 같은 공유 상태를 건드립니다. 그 도중 handler에서 다시 들어오면 같은 상태를 다시 건드립니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;write가 상대적으로 안전한 이유&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미 준비된 bytes를 file descriptor로 쓰라고 커널에 요청합니다. POSIX async-signal-safe 목록에도 들어갑니다. stdio의 내부 버퍼를 다시 타고 들어가지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래도 남는 문제&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;출력 순서가 사용자가 보기 좋게 보장되는 건 아닙니다. 또한 &lt;code&gt;write&lt;/code&gt;는 partial write나 &lt;code&gt;EINTR&lt;/code&gt;도 고려해야 합니다. 안전하다는 말과 출력 UX가 완벽하다는 말은 다릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. 그러면 실제로는 어떻게 짜는 게 맞을까&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab trace를 통과하는 코드와, 실제 시스템 프로그램에서 오래 버틸 코드는 기준이 조금 다릅니다. 과제에서는 handler 안에서 바로 상태를 출력하는 구현이 이해하기 쉽고 trace도 맞추기 좋습니다. 하지만 시스템 프로그래밍 관점에서는 handler가 하는 일을 최대한 줄이는 쪽이 좋아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/*
 * 가장 단순한 방향:
 * handler 안에서는 flag만 세우고, 실제 출력은 main loop에서 처리합니다.
 */
static volatile sig_atomic_t child_changed = 0;

void sigchld_handler(int sig)
{
    int old_errno = errno;
    child_changed = 1;
    errno = old_errno;
}

int main(void)
{
    while (1) {
        if (child_changed) {
            child_changed = 0;

            /*
             * 여기서는 평범한 실행 흐름이므로 printf를 써도 됩니다.
             * 필요하면 waitpid로 child 상태를 회수하고 출력합니다.
             */
            printf(&quot;child state changed\n&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 패턴은 handler를 아주 얇게 만듭니다. handler 안에서는 &lt;code&gt;volatile sig_atomic_t&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 제가 지금 다시 쓴다면, Shell Lab 본문에서는 여전히 과제 trace 기준의 구현을 설명하되, 옆에 &quot;이건 과제 문맥의 구현이고, 실제 프로그램에서는 handler를 더 얇게 만드는 편이 좋다&quot;는 주석을 더 분명하게 달 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7. syscall을 보다가 오히려 libc의 존재감이 보였습니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 syscall이 커널로 들어가는 문이라고 생각했습니다. 그건 맞습니다. 그런데 이번에 signal safety를 같이 보면서, 시스템 프로그래밍에서 정말 자주 실수하기 쉬운 지점은 &lt;b&gt;커널과 유저 공간 사이의 층을 뭉뚱그려 보는 것&lt;/b&gt;이라고 느꼈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;라이브러리 함수&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;printf&lt;/code&gt;처럼 유저 공간에서 format 처리, buffering, lock, 내부 상태 관리를 합니다. 편하지만 내부 동작이 꽤 두껍습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시스템 콜 wrapper&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;write&lt;/code&gt;, &lt;code&gt;waitpid&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;처럼 결국 커널에 요청하는 경로입니다. libc wrapper는 있을 수 있지만, 핵심 작업은 커널 경계 너머에서 일어납니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;signal handler&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;언제 끼어들지 모르는 비동기 실행 지점입니다. 그래서 평범한 함수 호출 규칙보다 훨씬 보수적으로 생각해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구분이 잡히니까 Shell Lab에서 signal mask를 왜 신경 쓰는지, handler에서 왜 일을 많이 하면 위험한지, &lt;code&gt;printf&lt;/code&gt;와 &lt;code&gt;write&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;결론은 단순합니다. syscall은 커널로 들어가는 문이고, signal handler는 그 문맥을 비동기로 끊고 들어올 수 있는 특수한 흐름입니다. 그 안에서 유저 공간의 두꺼운 라이브러리 함수를 아무렇지 않게 부르면, 평소에는 안 보이던 내부 lock과 buffer 상태가 갑자기 문제가 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab은 작은 과제처럼 보이지만, 다시 보면 이런 경계들을 계속 건드립니다. process를 만들고, child를 회수하고, signal을 전달하고, signal handler에서 공유 상태를 건드리고, 마지막에는 &quot;이 출력 함수 하나를 handler 안에서 불러도 되는가&quot;까지 내려갑니다. 저는 이런 지점 때문에 이 과제가 재미있었다고 생각합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8. 여기서 진짜 재미있는거&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;쉘랩을 추가로 이해하고 자료들을 정리하는 과정에서 희열을 얻는 미친 학습을 할 수 있었습니다. 처음에는 &lt;code&gt;printf&lt;/code&gt;가 왜 signal handler 안에서 위험한지만 보려고 했는데, 그 다음에는 &lt;code&gt;volatile sig_atomic_t&lt;/code&gt;와 &lt;code&gt;sigaltstack&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;이 두 개는 Shell Lab 기본 풀이에 반드시 필요한 주제는 아닐 수 있습니다. 그런데 signal을 진짜로 이해하려면 한 번은 밟고 가야 하는 주제처럼 보였습니다. 하나는 &quot;handler와 main 흐름이 변수를 공유할 때 컴파일러가 뭘 할 수 있는가&quot;이고, 다른 하나는 &quot;handler 자체가 어느 stack 위에서 실행되는가&quot;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8-1. volatile sig_atomic_t: signal이 바꾼 값을 컴파일러가 보게 만들기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 foreground job을 기다리기 위해 전역 flag 하나를 둔다고 해봅시다. 대충 이런 형태입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;

static int should_wait = 1;

void sigchld_handler(int sig)
{
    should_wait = 0;
}

void wait_foreground_job(void)
{
    while (should_wait) {
        /*
         * foreground job이 끝날 때까지 기다린다고 생각한 코드입니다.
         */
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사람이 보기에는 &lt;code&gt;SIGCHLD&lt;/code&gt;가 오면 handler가 &lt;code&gt;should_wait = 0&lt;/code&gt;으로 바꿔주고, 그러면 &lt;code&gt;while&lt;/code&gt;문이 끝날 것 같습니다. 그런데 컴파일러 입장에서는 다르게 볼 수 있습니다. &lt;code&gt;wait_foreground_job&lt;/code&gt; 함수 안에서는 &lt;code&gt;should_wait&lt;/code&gt;를 바꾸는 코드가 없습니다. 그러면 최적화 단계에서 &quot;이 값은 루프 안에서 변하지 않겠네&quot;라고 보고, 값을 메모리에서 매번 다시 읽지 않고 레지스터에 올려둔 채 계속 돌 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심.&lt;/b&gt; signal handler는 평범한 함수 호출처럼 코드 흐름에 보이지 않습니다. 컴파일러는 현재 함수 안의 코드만 보고 최적화할 수 있으므로, signal handler가 전역 값을 바꿀 수 있다는 사실을 타입으로 알려줘야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 signal handler와 main 흐름이 공유하는 단순 flag는 보통 이렇게 씁니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;

static volatile sig_atomic_t should_wait = 1;

void sigchld_handler(int sig)
{
    should_wait = 0;
}

void wait_foreground_job(void)
{
    while (should_wait) {
        /*
         * volatile 때문에 컴파일러는 should_wait를
         * 매번 다시 읽어야 한다고 봅니다.
         */
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;volatile&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컴파일러에게 &quot;이 값은 네가 보는 코드 밖에서 바뀔 수 있으니, 마음대로 캐싱하지 말고 매번 실제로 읽어라&quot;라고 말하는 쪽에 가깝습니다. signal handler가 바꾸는 flag에서 이 의미가 중요했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sig_atomic_t&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;signal handler와 main 흐름 사이에서 원자적으로 읽고 쓸 수 있는 정수 타입입니다. 복잡한 구조체를 handler에서 막 고치는 게 아니라, 단순한 flag 하나만 바꾸는 패턴이 여기서 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 이걸 너무 크게 해석하면 안 됩니다. &lt;code&gt;volatile sig_atomic_t&lt;/code&gt;는 signal handler와 main 흐름 사이의 작은 flag를 위한 도구에 가깝습니다. 멀티스레드 동기화 전체를 해결해주는 물건도 아니고, 복잡한 job table을 안전하게 공유하게 해주는 마법도 아닙니다. 그래서 handler에서는 flag만 바꾸고, 실제 복잡한 처리는 일반 흐름에서 하는 패턴이 계속 좋아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8-2. sigaltstack: 힙 메모리를 signal handler용 stack처럼 쓰기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 다음으로 재미있었던 건 &lt;code&gt;sigaltstack&lt;/code&gt;이었습니다. 일반적으로 signal handler는 현재 스레드의 기본 user stack 위에서 실행됩니다. Linux signal 문서도 기본적으로 handler가 normal process stack에서 호출된다고 설명합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그런데 만약 스택이 이미 거의 터지기 직전이면 어떻게 될까요? 무한 재귀를 돌고 있거나, 너무 큰 local buffer를 잡아서 기본 stack이 거의 꽉 찬 상태에서 &lt;code&gt;SIGSEGV&lt;/code&gt;나 다른 signal이 들어오면, 커널은 handler 실행에 필요한 signal frame을 또 stack에 올려야 합니다. 이미 stack이 벼랑 끝이면 handler가 시작되기도 전에 더 망가질 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;생각이 바뀐 지점.&lt;/b&gt; stack은 &quot;반드시 원래 stack segment만 stack이다&quot;라는 느낌보다, CPU의 stack pointer가 가리키는 메모리 영역이라고 보는 쪽이 더 정확해보였습니다. 어떤 메모리든 현재 stack pointer가 그쪽을 가리키고, 함수 호출과 push/pop이 그 영역에 쌓이면 그 순간 그 메모리는 stack처럼 쓰입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sigaltstack&lt;/code&gt;은 이 직관을 그대로 씁니다. &lt;code&gt;malloc&lt;/code&gt;으로 힙에 메모리 덩어리를 잡고, 그 주소와 크기를 커널에 알려줍니다. 그리고 &lt;code&gt;sigaction&lt;/code&gt;에서 &lt;code&gt;SA_ONSTACK&lt;/code&gt;을 켜면, 해당 handler는 기본 stack이 아니라 등록해둔 alternate signal stack 위에서 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

static void segv_handler(int sig)
{
    /*
     * 이 handler는 SA_ONSTACK으로 등록되면
     * alternate signal stack 위에서 실행됩니다.
     */
}

int main(void)
{
    stack_t ss;
    struct sigaction sa;

    ss.ss_sp = malloc(SIGSTKSZ);
    ss.ss_size = SIGSTKSZ;
    ss.ss_flags = 0;
    sigaltstack(&amp;amp;ss, NULL);

    memset(&amp;amp;sa, 0, sizeof(sa));
    sa.sa_handler = segv_handler;
    sa.sa_flags = SA_ONSTACK;
    sigemptyset(&amp;amp;sa.sa_mask);
    sigaction(SIGSEGV, &amp;amp;sa, NULL);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;힙에 비상용 메모리를 잡습니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;malloc(SIGSTKSZ)&lt;/code&gt;로 받은 메모리는 원래 힙 영역입니다. 하지만 커널에게 alternate signal stack으로 등록하면 handler 실행 때 stack처럼 쓰일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;sigaltstack으로 커널에 위치를 알려줍니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널은 이 스레드의 alternate stack 주소와 크기를 기억합니다. man-page 기준으로 &lt;code&gt;ss_sp&lt;/code&gt;는 시작 주소, &lt;code&gt;ss_size&lt;/code&gt;는 크기입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SA_ONSTACK handler가 들어오면 그쪽으로 실행합니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sigaction&lt;/code&gt;에 &lt;code&gt;SA_ONSTACK&lt;/code&gt;을 지정해야 실제로 handler가 alternate stack을 씁니다. 등록만 해두고 이 flag를 안 켜면 기본 stack을 씁니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;handler가 끝나면 원래 문맥으로 돌아갑니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;signal return 과정에서 원래 레지스터와 signal mask, stack 관련 문맥이 복원됩니다. 유저 코드 입장에서는 끊겼던 지점으로 돌아온 것처럼 보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 건, &lt;code&gt;malloc&lt;/code&gt;으로 잡은 메모리를 진짜로 &quot;계속 stack처럼 써도 된다&quot;는 식으로 아무렇게나 만지는 게 아니라는 점입니다. 커널에게 alternate signal stack으로 빌려준 메모리는 그 용도로 살아 있어야 합니다. 등록해놓고 바로 &lt;code&gt;free&lt;/code&gt;하면, 나중에 signal이 왔을 때 커널은 여전히 그 주소를 stack으로 쓰려고 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;주의.&lt;/b&gt; alternate signal stack은 자동으로 늘어나지 않습니다. man-page도 할당된 크기를 넘기면 결과를 예측하기 어렵다고 말합니다. 그러니까 이건 무한정 튼튼한 비상구가 아니라, &quot;기본 stack이 망가졌을 때 handler를 시작할 최소한의 발판&quot;에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 부분이 좋았습니다. 처음에는 stack, heap, data segment를 교과서 그림처럼 분리해서 봤는데, &lt;code&gt;sigaltstack&lt;/code&gt;을 보면 운영체제가 훨씬 실용적으로 움직입니다. 커널은 &quot;이 주소 범위를 signal handler용 stack으로 써라&quot;라는 약속을 들고 있다가, signal delivery 시점에 그 stack 위로 handler 문맥을 얹습니다. 힙 메모리가 진짜 힙이라는 이름표를 달고 있어도, stack pointer가 그쪽을 가리키고 함수 프레임이 거기에 쌓이면 그 순간에는 stack처럼 일합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;결국 Shell Lab에서 출발한 질문이 여기까지 왔습니다. &lt;code&gt;printf&lt;/code&gt;는 왜 handler 안에서 위험한가, handler가 바꾼 flag는 왜 &lt;code&gt;volatile sig_atomic_t&lt;/code&gt;여야 하는가, handler는 기본적으로 어느 stack에서 실행되는가, 기본 stack이 무너지면 어디서 handler를 실행할 수 있는가. 이 정도까지 이어지니까 과제 하나를 다시 복기하는데도 꽤 깊게 내려온 느낌이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9. OS는 메모리를 계속 속입니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sigaltstack&lt;/code&gt;에서 힙 메모리를 signal handler용 stack처럼 쓰는 구조가 보이니까, 비슷한 생각이 계속 이어졌습니다. 운영체제는 생각보다 자주 이런 식으로 움직입니다. 이름표는 heap, stack, file, child process처럼 붙어 있지만, 실제로는 page table, permission bit, page fault, mapping metadata로 그 이름표를 구현합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이걸 조금 과격하게 말하면, OS는 프로그램을 계속 속입니다. 모든 프로세스에게 자기만의 주소 공간이 있는 것처럼 보여주고, &lt;code&gt;fork()&lt;/code&gt;에서는 메모리를 복사한 것처럼 보여주고, &lt;code&gt;mmap()&lt;/code&gt;에서는 파일이 메모리에 올라온 것처럼 보여줍니다. 그런데 실제로는 거의 다 lazily 처리합니다. 필요해지는 순간까지 진짜 작업을 미룹니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;이번에 잡힌 감각.&lt;/b&gt; OS가 메모리를 관리한다는 말은 &quot;RAM을 바로 나눠준다&quot;가 아니라, 먼저 가상 주소 공간에 장부를 만들고, 접근이 발생하면 MMU와 page fault를 통해 그때그때 물리 페이지를 붙인다는 뜻에 가까웠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9-1. 가상 메모리: 같은 주소를 쓰는데 왜 안 부딪힐까&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로세스마다 &lt;code&gt;0x400000&lt;/code&gt;, &lt;code&gt;0x7fff...&lt;/code&gt; 같은 주소가 비슷하게 보일 수 있습니다. 같은 프로그램을 여러 개 켜도 내부에서 보는 주소 모양은 비슷합니다. 처음 보면 이상합니다. 같은 주소면 같은 RAM 위치를 가리켜야 할 것 같은데, 실제로는 서로 덮어쓰지 않습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이유는 프로세스가 보는 주소가 물리 RAM 주소가 아니라 &lt;b&gt;가상 주소&lt;/b&gt;이기 때문입니다. CPU의 MMU는 가상 주소를 물리 주소로 바꿀 때, 현재 프로세스의 page table을 기준으로 번역합니다. 그래서 두 프로세스가 같은 &lt;code&gt;0x400000&lt;/code&gt;을 보고 있어도, page table이 다르면 실제 물리 페이지는 전혀 다를 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로세스는 가상 주소를 냅니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드는 &lt;code&gt;ptr[0]&lt;/code&gt; 같은 접근을 하고, CPU는 그 주소를 현재 프로세스의 가상 주소로 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MMU가 page table을 봅니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;같은 가상 주소라도, 현재 프로세스의 page table이 어느 물리 페이지를 가리키는지에 따라 실제 RAM 위치가 달라집니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;각 프로세스는 자기 방 안에 있다고 착각합니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로세스 입장에서는 주소 공간 전체가 자기 것처럼 보입니다. 실제 격리와 매핑은 커널과 MMU가 뒤에서 맞춥니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Linux kernel 문서를 보면, 유저 공간 메모리 범위는 VMA, 즉 Virtual Memory Area로 추적됩니다. 하나의 VMA는 연속된 가상 주소 범위와 권한, 파일 backing 여부 같은 속성을 담습니다. 그러니까 커널은 &quot;이 프로세스의 어느 주소부터 어느 주소까지는 이런 성질의 메모리다&quot;라는 장부를 들고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9-2. fork와 Copy-on-Write: 복사한 척하고 버티기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab에서 &lt;code&gt;fork()&lt;/code&gt;는 계속 나왔습니다. 설명만 보면 parent의 주소 공간을 복사해서 child를 만든다고 합니다. 그런데 이걸 진짜 byte 단위로 매번 다 복사하면 너무 비쌉니다. parent가 큰 메모리를 들고 있는데 shell이 명령 하나 실행할 때마다 그걸 전부 복사하면 말이 안 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 Copy-on-Write가 들어갑니다. 처음에는 parent와 child가 같은 물리 페이지를 가리키게 두고, 쓰기 권한을 막아둡니다. 둘 중 하나가 값을 바꾸려고 쓰는 순간 page fault가 나고, 그때 커널이 해당 page를 진짜로 복사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;fork 직후&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;parent와 child는 독립된 프로세스처럼 보이지만, 많은 page는 아직 같은 물리 메모리를 공유할 수 있습니다. page table과 권한 설정으로 &quot;아직 복사하지 않음&quot; 상태를 버팁니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;누군가 write 시도&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;읽기 전용으로 막아둔 page에 쓰려고 하면 page fault가 납니다. 이건 커널 입장에서는 &quot;이제 진짜 분리해야 할 때&quot;라는 신호입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 page만 복사&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;전체 주소 공간을 복사하는 게 아니라, 실제로 수정하려는 page 단위로만 복사합니다. 자식이 바로 &lt;code&gt;execve()&lt;/code&gt;로 다른 프로그램이 되면 대부분의 page는 끝까지 복사되지 않을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이게 Shell Lab이랑도 이어집니다. shell은 &lt;code&gt;fork()&lt;/code&gt;로 child를 만들고, child는 거의 바로 &lt;code&gt;execve()&lt;/code&gt;로 다른 프로그램이 됩니다. 이 흐름에서는 &quot;parent 메모리 전체 복사&quot;를 실제로 다 해버릴 이유가 거의 없습니다. OS가 복사한 척만 하고 버티는 쪽이 훨씬 맞아보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9-3. mmap: 파일을 메모리에 올리는 게 아니라, 메모리처럼 보이게 하기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;mmap()&lt;/code&gt;도 같은 계열의 착시였습니다. 처음에는 &quot;파일을 메모리에 올린다&quot;라고 말하기 쉽습니다. 그런데 정확히는 파일 전체를 RAM에 먼저 올리는 게 아니라, 파일의 특정 범위를 프로세스의 가상 주소 공간에 mapping합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char *file_in_memory = mmap(
    NULL,
    file_size,
    PROT_READ,
    MAP_PRIVATE,
    fd,
    0
);

char c = file_in_memory[10000000000ULL];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 조건이 있습니다. &lt;code&gt;file_in_memory[10000000000]&lt;/code&gt;이 되려면 그 인덱스가 &lt;code&gt;mmap&lt;/code&gt;으로 잡은 길이 안에 있어야 합니다. mapping 밖을 찌르면 그냥 잘못된 주소 접근입니다. 그리고 파일 mapping에서 실제 파일 범위를 넘어선 page를 건드리면 &lt;code&gt;SIGBUS&lt;/code&gt; 같은 문제도 날 수 있습니다. &quot;큰 숫자를 넣으면 OS가 알아서 다 해준다&quot;가 아니라, &lt;b&gt;mapping된 가상 주소 범위 안에서만 배열처럼 보이는 것&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;mmap은 VMA를 만듭니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널은 &quot;이 가상 주소 범위는 이 파일의 이 offset부터 연결된다&quot;는 mapping 정보를 기록합니다. 이 시점에 파일 전체가 RAM으로 올라온다고 보면 안 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 접근할 때 page fault가 납니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MMU가 page table을 봤는데 아직 RAM에 물리 page가 없으면 fault가 납니다. 이것은 실패라기보다, kernel에게 &quot;이제 이 page가 필요하다&quot;라고 알려주는 신호에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;필요한 page 단위로 채웁니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1byte를 읽더라도 커널은 보통 page 단위로 다룹니다. 8바이트만 딱 가져온다기보다, 그 주소가 포함된 page를 RAM에 붙이고 실패했던 명령을 다시 진행하게 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러니까 &lt;code&gt;mmap&lt;/code&gt;은 파일 읽기를 없애는 마법이라기보다, 파일 I/O를 page fault와 page cache 흐름에 얹는 방식이라고 보는 게 더 맞아보였습니다. 개발자는 배열처럼 읽지만, OS는 뒤에서 page 단위로 필요한 부분만 메모리에 붙입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9-4. malloc도 비슷합니다. VIRT와 RSS가 갈라지는 이유&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 다시 힙으로 돌아오면, &lt;code&gt;malloc&lt;/code&gt;도 비슷한 질문을 던집니다. 힙 공간에도 한계가 있을 텐데, 큰 메모리를 요청했을 때 왜 바로 물리 RAM을 다 먹지 않는 것처럼 보일까요?&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;font-family: 'Noto Serif KR';&quot;&gt;Linux에서는 큰 할당이 &lt;code&gt;brk&lt;/code&gt;나 &lt;code&gt;mmap&lt;/code&gt; 경로로 가상 주소 공간을 확보하는 식으로 처리될 수 있습니다. 이때 중요한 건, 가상 주소 공간에 잡혔다고 해서 그 크기만큼 물리 RAM page가 즉시 붙는 건 아니라는 점입니다. 실제로 읽거나 쓰는 순간 page fault가 나고, 그때 물리 page가 붙습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;char *p = malloc(10ULL * 1024 * 1024 * 1024);

/*
 * 이 시점에는 큰 가상 주소 범위를 받은 것에 가깝습니다.
 * 실제 물리 page는 아직 대부분 붙지 않았을 수 있습니다.
 */

p[0] = 'A';          // 이 주소가 포함된 page가 실제로 필요해집니다.
p[4096] = 'B';       // 다음 page가 필요해집니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VmSize / VIRT&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로세스가 가진 가상 메모리 크기입니다. 큰 &lt;code&gt;mmap&lt;/code&gt;이나 아직 다 만지지 않은 큰 heap 예약도 여기에 크게 잡힐 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VmRSS / RES&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제로 RAM에 올라와 있는 resident memory 쪽입니다. 아직 접근하지 않은 가상 주소 범위는 VIRT에는 크게 보이지만, RSS에는 바로 그만큼 잡히지 않을 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 이것도 무한정 공짜는 아닙니다. 커널 metadata, VMA, page table 같은 비용은 있고, overcommit 정책이나 &lt;code&gt;RLIMIT&lt;/code&gt;에 따라 &lt;code&gt;malloc&lt;/code&gt;이나 &lt;code&gt;mmap&lt;/code&gt; 자체가 실패할 수도 있습니다. 그리고 실제로 그 거대한 영역을 전부 쓰기 시작하면 결국 물리 RAM과 swap이 필요해집니다. 거기서도 감당이 안 되면 OOM 상황으로 갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;정리.&lt;/b&gt; 아직 안 쓴 메모리는 실제로 안 쓴 것에 가깝습니다. 가상 주소 공간에는 예약되어 있어도, 물리 page는 첫 접근 시점에 붙을 수 있습니다. 이게 &lt;code&gt;mmap&lt;/code&gt;, 큰 heap 할당, COW를 한 줄로 이어주는 감각이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9-5. malloc은 syscall인가? page fault는 누가 판정하는가?&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 &lt;code&gt;malloc&lt;/code&gt;을 다시 보면 질문이 조금 더 정확해집니다. &lt;code&gt;malloc&lt;/code&gt;은 시스템 콜 그 자체가 아닙니다. 유저 공간의 allocator입니다. 다만 allocator가 들고 있던 공간이 부족하면 커널에게 더 큰 가상 메모리 범위를 달라고 요청할 수 있습니다. glibc &lt;code&gt;malloc&lt;/code&gt; 문서도 일반적으로 heap을 쓰고 필요하면 &lt;code&gt;sbrk&lt;/code&gt; 계열로 heap 크기를 조정하며, 큰 할당은 private anonymous &lt;code&gt;mmap&lt;/code&gt;을 쓴다고 설명합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;malloc은 먼저 자기 장부를 봅니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미 확보해둔 arena나 free list에서 잘라줄 수 있으면 유저 공간 계산만으로 끝납니다. 이 경우 매번 syscall을 부르는 구조가 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;공간이 부족하면 brk/sbrk 또는 mmap으로 커널에 요청합니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CS:APP의 &lt;code&gt;mem_sbrk&lt;/code&gt; 같은 이름은 이 흐름을 학습용으로 추상화한 느낌이고, Linux에서는 &lt;code&gt;brk&lt;/code&gt;/&lt;code&gt;sbrk&lt;/code&gt;나 &lt;code&gt;mmap&lt;/code&gt; 계열이 실제 경계에 더 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 범위 안은 합법적인 가상 주소가 됩니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널 장부에는 이 주소 범위가 heap이나 anonymous mapping으로 잡힙니다. 아직 물리 RAM이 붙지 않았더라도, 주소 범위 자체는 합법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 실제 접근에서 page fault가 날 수 있습니다&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;MMU가 아직 present하지 않은 page를 만나면 fault를 냅니다. 커널이 VMA를 보고 합법적인 범위면 물리 page를 붙이고, 실패했던 명령을 다시 진행하게 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반대로 그 주소가 heap도 아니고, stack도 아니고, 어떤 VMA에도 속하지 않으면 다른 결론이 납니다. 이때도 시작은 page fault입니다. CPU는 &quot;이 주소를 물리 주소로 번역할 수 없다&quot;는 사실만 보고 커널로 넘깁니다. 그 다음 커널이 장부를 보고 &quot;합법적인 지연 할당인가, 아니면 그냥 잘못된 주소 접근인가&quot;를 판정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;합법적인 page fault&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주소가 VMA 안에 있고 권한도 맞습니다. 아직 물리 page만 없었던 상태입니다. 커널은 새 page를 붙이거나 파일에서 읽어오고, 프로그램은 계속 실행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;불법적인 page fault&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;주소가 어떤 VMA에도 없거나, 쓰기 금지 page에 쓰는 것처럼 권한이 맞지 않습니다. 이 경우 커널은 보통 프로세스에 &lt;code&gt;SIGSEGV&lt;/code&gt;를 보냅니다. &lt;code&gt;SIGINT&lt;/code&gt;가 아니라 segmentation fault 쪽입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 이 검사는 누가 하는가? 어셈블리 코드에 &lt;code&gt;if (address_is_valid)&lt;/code&gt; 같은 검사문이 박혀 있는 건 아닙니다. 예를 들어 &lt;code&gt;mov [rax], 1&lt;/code&gt; 같은 명령은 그냥 메모리에 쓰려고 합니다. 그런데 그 메모리 접근이 실제 RAM으로 나가기 전에, CPU 안의 MMU가 현재 활성화된 page table을 통해 가상 주소를 물리 주소로 번역합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심.&lt;/b&gt; OS는 page table을 만들고, CPU는 그 page table을 하드웨어적으로 참조합니다. x86 계열에서는 현재 page table의 최상위 구조를 가리키는 값이 &lt;code&gt;CR3&lt;/code&gt; 같은 제어 레지스터에 들어갑니다. context switch 때 이 기준이 바뀌므로, 같은 가상 주소라도 프로세스마다 다른 물리 주소로 번역될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 &quot;OS는 장부만 주고 끝인데, 체크를 안 하면 엉뚱한 곳에 쓰는 것 아닌가?&quot;라는 질문의 답은 이렇습니다. OS가 장부를 만들고 CPU에게 현재 장부의 위치를 알려줍니다. 그 다음부터 모든 메모리 접근은 MMU의 번역 경로를 지나갑니다. 매번 소프트웨어 if문을 넣는 게 아니라, 하드웨어 경로 자체가 page table 규칙을 통과해야 물리 RAM으로 나갈 수 있게 되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OS는 RAM을 단순히 나눠주는 관리자가 아니라, 가상 주소와 page fault를 이용해서 프로그램에게 꽤 그럴듯한 세계를 보여주는 시스템입니다. 프로그램은 &quot;내 메모리&quot;, &quot;내 파일&quot;, &quot;내 child 복사본&quot;이라고 생각하지만, 커널은 그 뒤에서 page table과 mapping 장부를 들고 최대한 늦게, 최대한 필요한 만큼만 진짜 일을 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;저는 그동안 구간 Lazy 쿼리 업데이트 문제를 굉장히 많이 풀었습니다. segment tree에서 값을 바로 끝까지 밀어 넣지 않고, 나중에 정말 필요해지는 순간까지 미뤄두는 그 방식이 처음에는 그냥 알고리즘 테크닉처럼 보였습니다. 그런데 OS의 메모리 관리도 비슷한 냄새가 났습니다. &lt;code&gt;fork&lt;/code&gt;도, &lt;code&gt;mmap&lt;/code&gt;도, 큰 heap 할당도, 전부 일단 약속만 해두고 실제 비용은 필요한 순간까지 미뤄둡니다.&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;font-family: 'Noto Serif KR';&quot;&gt;맞습니다. 세상에는 Lazy하게 풀어나가야 할 문제들이 굉장히 많은 것 같습니다. 지금 당장 모든 걸 계산하고, 복사하고, 확정하려고 하면 버티지 못하는 문제들이 있습니다. 그래서 똑똑한 시스템은 먼저 장부를 만들고, 약속을 걸고, 경계를 세운 다음, 진짜 사건이 발생한 순간에만 움직입니다. 저는 이 지점이 꽤 좋았습니다. 알고리즘 문제에서 보던 lazy propagation이 운영체제 안에서 훨씬 거대한 형태로 다시 나타난 느낌이었습니다. 그냥 뭔가 아귀가 맞는 느낌이었습니다. 찔러왔던 다양한 길이 무언가를 이해하는데 종합적으로 도움을 주는 느낌입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고한 문서&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;font-family: 'Noto Serif KR'; line-height: 1.8; color: #374151;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man7/signal-safety.7.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;signal-safety(7)&lt;/a&gt; - async-signal-safe 함수 목록. &lt;code&gt;stdio&lt;/code&gt; buffer 위험 설명.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/write.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;write(2)&lt;/a&gt; - file descriptor write syscall. partial write, &lt;code&gt;EINTR&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://csapp.cs.cmu.edu/2e/waside/waside-safety.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CS:APP Async-signal-safety&lt;/a&gt; - handler 안 &lt;code&gt;printf&lt;/code&gt;와 deadlock 예시.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/sigaltstack.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sigaltstack(2)&lt;/a&gt; - alternate signal stack 등록. &lt;code&gt;stack_t&lt;/code&gt;, stack exhaustion 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/sigaction.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;sigaction(2)&lt;/a&gt; - signal handler 등록. &lt;code&gt;SA_ONSTACK&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://manpages.ubuntu.com/manpages/jammy/man7/signal.7.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;signal(7)&lt;/a&gt; - signal delivery 흐름. signal frame, &lt;code&gt;sigreturn&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cppreference.com/w/cpp/utility/program/signal&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cppreference: std::signal&lt;/a&gt; - C/C++ signal handler 규칙. &lt;code&gt;volatile sig_atomic_t&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pvs-studio.com/en/blog/posts/cpp/0950/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;PVS-Studio POSIX signals&lt;/a&gt; - signal flag 최적화 문제. 레지스터 캐싱과 &lt;code&gt;volatile&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/mmap.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;mmap(2)&lt;/a&gt; - virtual address mapping. &lt;code&gt;MAP_PRIVATE&lt;/code&gt;, COW, page 단위 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man3/malloc.3.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;malloc(3)&lt;/a&gt; - glibc allocator. heap, &lt;code&gt;sbrk&lt;/code&gt;, 큰 할당의 &lt;code&gt;mmap&lt;/code&gt; 사용 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man2/brk.2.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;brk(2)&lt;/a&gt; - program break 조정. heap 확장과 &lt;code&gt;sbrk&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/mm/process_addrs.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux Kernel Docs: Process Addresses&lt;/a&gt; - process address space. VMA, &lt;code&gt;mm_struct&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/mm/page_tables.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linux Kernel Docs: Page Tables&lt;/a&gt; - virtual address to physical address mapping. page table 계층 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;proc_pid_maps(5)&lt;/a&gt; - mapped memory region 목록. 권한, offset, pathname 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_status.5.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;proc_pid_status(5)&lt;/a&gt; - process memory status. &lt;code&gt;VmSize&lt;/code&gt;, &lt;code&gt;VmRSS&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.kernel.org/mm/overcommit-accounting.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Overcommit Accounting&lt;/a&gt; - Linux overcommit 정책. &lt;code&gt;CommitLimit&lt;/code&gt;, &lt;code&gt;Committed_AS&lt;/code&gt; 내용.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man3/fork.3p.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;fork(3p)&lt;/a&gt; - &lt;code&gt;fork()&lt;/code&gt;와 mapping 상속. &lt;code&gt;MAP_PRIVATE&lt;/code&gt; 수정 내용.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>황현석 어록</category>
      <category>Async Signal Safe</category>
      <category>Copy on Write</category>
      <category>malloc</category>
      <category>mmap</category>
      <category>mmu</category>
      <category>Operating System</category>
      <category>page fault</category>
      <category>Signal Safety</category>
      <category>System Programming</category>
      <category>virtual memory</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/59</guid>
      <comments>https://hy3ons.tistory.com/59#entry59comment</comments>
      <pubDate>Mon, 22 Jun 2026 06:35:22 +0900</pubDate>
    </item>
    <item>
      <title>시스템 프로그래밍 - Shell Lab</title>
      <link>https://hy3ons.tistory.com/58</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저는 보통 저작권이 교수님한테 저당잡혀 있는 학교 과제에 대하여 글을 작성하지 않습니다. 과제중에 유일하게 포스팅을 하고 싶었던 주제는 &lt;b&gt;Shell Lab&lt;/b&gt;입니다. 다행히, 이미 Shell Lab은 블로그에 글들이 널린 공공재입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;학교 과제를 하면서 Shell Lab이 가장 재미있엇습니다. 우선은 코딩이고, 저같은 레벨의 학부생 수준에서는 &lt;b&gt;Low-Level&lt;/b&gt;에서 개발할 일이 별로 없어, 더더욱 재미를 느꼇던 것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;미루고 미루다가, 거의 10개월이 지난 지금 포스팅을 합니다. 과제를 다시 읽어보게 되서 설렜습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab은 처음 보면 진입장벽이 높아보입니다. 실제로 Shell Lab은 그 수업을 듣는 학생들의 문맥에서는 요구하는 배경지식이 많은 과제입니다. C 기본 문법만 어느 정도 아는 상태에서 갑자기 &lt;code&gt;fork&lt;/code&gt;, &lt;code&gt;execve&lt;/code&gt;, &lt;code&gt;waitpid&lt;/code&gt;, &lt;code&gt;sigprocmask&lt;/code&gt;, &lt;code&gt;setpgid&lt;/code&gt;, &lt;code&gt;kill&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 시스템 콜 계열 함수라는 말은, 일반 프로세스가 리눅스 커널에게 정해진 창구로 작업을 요청한다는 뜻입니다. 프로세스는 유저 모드에서 실행되므로 직접 다른 프로세스를 만들거나, signal을 보내거나, signal handler 정보를 커널에 등록할 수 없습니다. 그래서 &lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&gt;, &lt;code&gt;waitpid()&lt;/code&gt;, &lt;code&gt;sigprocmask()&lt;/code&gt; 같은 호출을 통해 커널에게 &quot;이 작업을 대신 해달라&quot;고 요청합니다. 글 맨 뒤에 이 syscall 진입 과정도 따로 정리해두겠습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 &quot;명령어를 파싱해서 실행하면 되는 거 아닌가?&quot;라고 생각하기 쉽습니다. 그런데 Shell Lab의 핵심은 명령어 실행이 아니라, &lt;b&gt;실행 중인 프로세스를 shell이 어떻게 추적하고, signal이 들어왔을 때 어떤 job에게 전달해야 하는지&lt;/b&gt;를 맞추는 부분이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글은 제가 작성했던 제출 PDF와 최종 &lt;code&gt;tsh.c&lt;/code&gt; 구현, 그리고 trace 00~21을 기준으로 Shell Lab을 공략하듯이 정리한 글입니다. 그대로 답을 외우는 글이라기보다는, 어떤 테스트가 무엇을 요구하는지, 그래서 구현을 어떤 순서로 쌓아야 하는지를 보는 글에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서론. 이 과제에서 우리가 채울 함수들&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab을 처음 열면 함수 이름이 한 번에 많이 나옵니다. 여기서 모든 함수를 같은 무게로 보면 구현 순서를 잡기 어렵습니다. 그런데 다시 보면 이 과제는 결국 &lt;code&gt;eval&lt;/code&gt;에서 명령어 한 줄을 처리하고, &lt;code&gt;builtin_cmd&lt;/code&gt;에서 shell 내장 명령을 처리하고, signal handler에서 child 상태를 정리하는 흐름입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 저는 함수 원형을 먼저 보고 들어가는 게 낫다고 생각했습니다. 아래 함수들이 handout에서 &quot;여기 구현하세요&quot;라고 비워져 있는 핵심 함수들입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void eval(char *cmdline);
int builtin_cmd(char **argv);
void waitfg(pid_t pid, int output_fd);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 이 글에서 나오는 함수 이름은 출처가 다릅니다. 이걸 전부 똑같은 함수콜로 읽으면 구현 경계가 흐려집니다. &lt;code&gt;sigprocmask()&lt;/code&gt;를 호출한다는 말과 &lt;code&gt;eval()&lt;/code&gt;을 호출한다는 말은, 생긴 건 비슷해도 의미가 다릅니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 우리가 직접 채우는 함수&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;eval()&lt;/code&gt;, &lt;code&gt;builtin_cmd()&lt;/code&gt;, &lt;code&gt;waitfg()&lt;/code&gt;, &lt;code&gt;sigchld_handler()&lt;/code&gt;, &lt;code&gt;sigint_handler()&lt;/code&gt;, &lt;code&gt;sigtstp_handler()&lt;/code&gt;처럼 handout이 비워둔 함수입니다. 글의 큰 흐름은 결국 이 함수들의 구현부를 채우는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. &lt;code&gt;tsh.c&lt;/code&gt; 안의 job helper&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;addjob()&lt;/code&gt;, &lt;code&gt;deletejob()&lt;/code&gt;, &lt;code&gt;pid2jid()&lt;/code&gt;, &lt;code&gt;fgpid()&lt;/code&gt;, &lt;code&gt;getjobjid()&lt;/code&gt;, &lt;code&gt;getjobpid()&lt;/code&gt;처럼 shell이 들고 있는 &lt;code&gt;jobs&lt;/code&gt; 배열을 다루는 함수입니다. 이 글에서는 구현부를 같이 살펴볼 &lt;code&gt;tsh.c&lt;/code&gt; 내부 helper로 다룹니다. 중요한 건 외부 헤더에서 가져온 함수가 아니라 우리 shell 코드 안의 도우미라는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. handout이 제공하는 wrapper/helper&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;Signal()&lt;/code&gt;, &lt;code&gt;parseline()&lt;/code&gt;, &lt;code&gt;listjobs()&lt;/code&gt;처럼 과제 코드가 이미 제공하는 함수입니다. 표준 C 함수처럼 외워야 하는 게 아니라, handout이 shell 구현을 돕기 위해 미리 만들어둔 도구라고 보는 게 편했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 헤더에서 가져와 호출하는 시스템/libc 함수&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;execve()&lt;/code&gt;, &lt;code&gt;waitpid()&lt;/code&gt;, &lt;code&gt;sigprocmask()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&gt;, &lt;code&gt;setpgid()&lt;/code&gt;, &lt;code&gt;strcmp()&lt;/code&gt;, &lt;code&gt;printf()&lt;/code&gt; 같은 함수입니다. 우리가 구현하는 함수가 아니라, 헤더를 include하고 OS/libc에게 일을 시키는 호출입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 코드를 읽을 때는 &quot;이 함수가 어디서 온 함수인가?&quot;를 계속 구분하면 좋습니다. &lt;code&gt;eval()&lt;/code&gt; 안에서 &lt;code&gt;fork()&lt;/code&gt;를 호출한다면, 앞의 &lt;code&gt;eval&lt;/code&gt;은 우리가 작성 중인 shell 함수이고, 뒤의 &lt;code&gt;fork&lt;/code&gt;는 OS에게 child process를 만들어달라고 요청하는 함수입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 인자를 변수 사전처럼 외우면 별로 도움이 안 됩니다. 함수 호출을 기준으로, 괄호 안에 들어가는 값이 왜 필요한지부터 보는 편이 좋았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;eval(char *cmdline)&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;cmdline&lt;/code&gt;에는 사용자가 입력한 한 줄 전체가 들어옵니다. 예를 들면 &lt;code&gt;./myspin 5 &amp;amp;&lt;/code&gt; 같은 문자열입니다. &lt;code&gt;eval&lt;/code&gt;은 이 한 줄을 받아서, builtin인지 외부 프로그램인지, foreground인지 background인지 판단합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;builtin_cmd(char **argv)&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;argv&lt;/code&gt;에는 &lt;code&gt;cmdline&lt;/code&gt;을 단어 단위로 쪼갠 결과가 들어옵니다. &lt;code&gt;argv[0]&lt;/code&gt;은 명령 이름이고, &lt;code&gt;argv[1]&lt;/code&gt;부터는 그 명령의 인자입니다. 그래서 &lt;code&gt;quit&lt;/code&gt;인지 볼 때는 &lt;code&gt;argv[0]&lt;/code&gt;을 보고, &lt;code&gt;fg %1&lt;/code&gt;을 처리할 때는 &lt;code&gt;argv[1]&lt;/code&gt;의 &lt;code&gt;%1&lt;/code&gt;을 봅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;waitfg(pid_t pid, int output_fd)&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;pid&lt;/code&gt;는 shell이 기다려야 하는 foreground child의 process id입니다. &lt;code&gt;output_fd&lt;/code&gt;는 출력 대상입니다. 보통은 &lt;code&gt;STDOUT_FILENO&lt;/code&gt;로 생각하면 되고, handout의 출력 함수들이 어디로 출력할지 맞추기 위해 같이 넘겨줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sigchld_handler(int sig)&lt;/code&gt;, &lt;code&gt;sigint_handler(int sig)&lt;/code&gt;, &lt;code&gt;sigtstp_handler(int sig)&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sig&lt;/code&gt;에는 OS가 handler를 호출하면서 넘겨준 signal 번호가 들어옵니다. &lt;code&gt;ctrl-c&lt;/code&gt;면 &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;ctrl-z&lt;/code&gt;면 &lt;code&gt;SIGTSTP&lt;/code&gt;, child 상태 변경이면 &lt;code&gt;SIGCHLD&lt;/code&gt;입니다. 우리가 직접 이 함수를 평범하게 호출한다기보다, handout이 등록해둔 handler를 OS가 불러주는 구조에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 헤더에서 가져와 쓰는 시스템 함수들도 인자를 보고 읽어야 합니다. 예를 들어 &lt;code&gt;execve(argv[0], argv, environ)&lt;/code&gt;은 &quot;이 경로의 프로그램을, 이 인자 목록과, 이 실행 환경으로 실행해라&quot;라는 뜻입니다. &lt;code&gt;waitpid(pid, &amp;amp;status, options)&lt;/code&gt;는 &quot;이 child의 상태를 status에 받아오되, options 방식으로 기다려라&quot;에 가깝습니다. 이런 식으로 괄호 안을 문장으로 바꾸면 코드의 역할이 또렷해집니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 글에서는 이 함수들을 한 번에 완성하지 않겠습니다. trace가 요구하는 기능에 맞춰 조금씩 키워가는 식으로 보겠습니다. 먼저 &lt;code&gt;quit&lt;/code&gt;과 외부 명령 실행을 만들고, 그 다음 background와 &lt;code&gt;jobs&lt;/code&gt;, 그 다음 signal과 race, 마지막으로 &lt;code&gt;bg&lt;/code&gt;/&lt;code&gt;fg&lt;/code&gt;와 process group을 붙입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;0. handout에서 이미 해주는 것부터 확인하기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구간에서는 trace를 깨기 전에, handout이 이미 어디까지 해주는지부터 보겠습니다. 특히 trace 00의 EOF 처리와 shell의 입력 루프는 직접 새로 만드는 부분이 아니었습니다. 먼저 &lt;code&gt;main&lt;/code&gt;이 어떻게 &lt;code&gt;eval&lt;/code&gt;까지 연결되는지 보는 게 목표입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과제는 빈 C 파일에서 시작하지 않습니다. &lt;code&gt;tsh.c&lt;/code&gt;를 열면 이미 코드가 꽤 많습니다. 그래서 먼저 &quot;내가 새로 짜야 하는 것&quot;과 &quot;handout이 이미 해주는 것&quot;을 나누는 게 좋아보였습니다. 안 그러면 EOF 처리나 prompt 출력 같은 곳에서 괜히 시간을 씁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;먼저 볼 것.&lt;/b&gt; 아래 코드의 &lt;code&gt;Signal&lt;/code&gt;은 표준 C 문법이라기보다 handout이 제공하는 포장 함수라고 보면 됩니다. 뜻은 &quot;이 사건이 오면 이 함수를 대신 실행해줘&quot;라고 OS 쪽에 등록하는 것입니다. 예를 들어 사용자가 &lt;code&gt;ctrl-c&lt;/code&gt;를 누르면 평소 흐름을 잠깐 멈추고 &lt;code&gt;sigint_handler&lt;/code&gt;를 실행합니다. 지금은 구현하지 말고, handout이 이런 연결만 미리 해뒀다고 보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아래는 &lt;code&gt;main&lt;/code&gt;을 읽을 때 제가 먼저 봤던 흐름입니다. 실제 handout에 있는 코드를 전부 외울 필요는 없고, 주석으로 표시한 위치만 잡고 가도 충분했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int main(int argc, char **argv)
{
    char cmdline[MAXLINE];

    /*
     * 여기까지는 옵션 처리, prompt 설정 같은 준비 코드입니다.
     * 과제 초반에는 거의 건드릴 일이 없습니다.
     */

    /*
     * 이 세 함수의 &quot;등록&quot;은 handout이 이미 해줍니다.
     * 우리가 할 일은 아래 handler 함수들의 구현부를 채우는 것입니다.
     *
     * Signal은 handout wrapper이고,
     * sigint_handler 같은 함수는 우리가 채우는 함수입니다.
     */
    Signal(SIGINT,  sigint_handler);   // 사용자가 ctrl-c를 눌렀을 때
    Signal(SIGTSTP, sigtstp_handler);  // 사용자가 ctrl-z를 눌렀을 때
    Signal(SIGCHLD, sigchld_handler);  // child 상태가 바뀌었을 때

    initjobs(jobs);

    while (1) {
        printf(&quot;%s&quot;, prompt);
        fflush(stdout);

        /*
         * 사용자가 입력한 한 줄을 읽습니다.
         * shell은 계속 이 루프를 돌면서 한 줄씩 처리합니다.
         */
        if ((fgets(cmdline, MAXLINE, stdin) == NULL) &amp;amp;&amp;amp; ferror(stdin)) {
            app_error(&quot;fgets error&quot;);
        }

        /*
         * EOF, 즉 입력 끝입니다.
         * trace 00은 보통 이 기본 코드가 이미 처리합니다.
         */
        if (feof(stdin)) {
            fflush(stdout);
            exit(0);
        }

        /*
         * 과제의 핵심 진입점입니다.
         * 사용자가 친 한 줄이 전부 eval로 들어갑니다.
         */
        eval(cmdline);
        fflush(stdout);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러니까 처음부터 &lt;code&gt;main&lt;/code&gt;을 고치려고 달려들 필요는 없었습니다. 과제의 중심은 &lt;code&gt;eval&lt;/code&gt;입니다. &lt;code&gt;eval&lt;/code&gt;이 입력 한 줄을 받고, 그게 shell 내부 명령인지, 외부 프로그램인지, foreground인지, background인지 판단합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. builtin_cmd: shell 안에서 직접 처리하는 명령&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간에서는 trace 01의 &lt;code&gt;quit&lt;/code&gt;를 처리하고, 이후 trace 05~18에서 사용할 &lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;의 자리를 먼저 잡겠습니다. 외부 프로그램을 실행하기 전에, shell이 직접 처리해야 하는 명령부터 걸러야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;builtin_cmd&lt;/code&gt;는 말 그대로 shell에 내장된 명령을 처리하는 함수입니다. 여기서 핵심은 &lt;code&gt;quit&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;는 외부 프로그램을 실행하는 게 아니라는 점입니다. shell이 자기 상태를 보고 직접 처리해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 반대로 &lt;code&gt;./myspin&lt;/code&gt;, &lt;code&gt;./myenv&lt;/code&gt;, &lt;code&gt;/bin/echo&lt;/code&gt; 같은 것은 shell 내부 명령이 아닙니다. 이런 명령은 새 process를 만든 다음 그 process에서 실행해야 합니다. 이 구분이 잡히면 &lt;code&gt;builtin_cmd&lt;/code&gt;가 왜 따로 있는지 바로 보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int builtin_cmd(char **argv)
{
    char *cmd = argv[0];

    /*
     * 빈 줄이 들어오면 argv[0]이 NULL일 수 있습니다.
     * 이걸 먼저 막지 않으면 strcmp에서 바로 터질 수 있습니다.
     */
    if (cmd == NULL) {
        return 1;
    }

    /*
     * quit은 실행 파일이 아닙니다.
     * shell이 직접 종료해야 하는 내장 명령입니다.
     */
    if (!strcmp(cmd, &quot;quit&quot;)) {
        exit(0);
    }

    /*
     * jobs도 실행 파일이 아닙니다.
     * shell이 들고 있는 jobs 배열을 출력하는 명령입니다.
     */
    if (!strcmp(cmd, &quot;jobs&quot;)) {
        listjobs(jobs, STDOUT_FILENO);
        return 1;
    }

    /*
     * bg와 fg도 shell 내부 명령입니다.
     * 다만 초반 trace 00~07에서는 아직 완성하지 않아도 됩니다.
     * 나중에 stopped job을 다시 움직일 때 여기 구현을 채웁니다.
     */
    if (!strcmp(cmd, &quot;bg&quot;)) {
        // Part 7에서 구현합니다.
        return 1;
    }

    if (!strcmp(cmd, &quot;fg&quot;)) {
        // Part 7에서 구현합니다.
        return 1;
    }

    /*
     * 여기까지 왔다는 건 builtin이 아니라는 뜻입니다.
     * eval이 fork/execve로 외부 프로그램을 실행해야 합니다.
     */
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 반환값을 대충 정하면 뒤에서 계속 꼬입니다. &lt;code&gt;return 1&lt;/code&gt;은 &quot;shell 내부에서 처리했으니 eval이 더 할 일이 없다&quot;는 뜻이고, &lt;code&gt;return 0&lt;/code&gt;은 &quot;builtin이 아니니 eval이 외부 프로그램으로 실행해야 한다&quot;는 뜻입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. eval 첫 번째 구현: 외부 프로그램 실행하기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 02~04입니다. &lt;code&gt;./myenv&lt;/code&gt;, &lt;code&gt;./myspin&lt;/code&gt;, &lt;code&gt;/bin/echo&lt;/code&gt; 같은 외부 프로그램을 실제로 실행해야 합니다. 이때 shell이 직접 그 프로그램이 되는 게 아니라, child process를 만들고 그 child를 다른 프로그램으로 바꿉니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;eval&lt;/code&gt;은 사용자가 친 한 줄을 처리하는 함수입니다. 처음부터 signal, job control, process group까지 다 보려고 하면 범위가 너무 커집니다. 일단 trace 00~04부터 깨봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;먼저 볼 것.&lt;/b&gt; &lt;code&gt;fork()&lt;/code&gt;는 일반 함수처럼 보이지만 결과가 좀 특이합니다. 호출 후에 같은 코드가 parent process와 child process에서 이어서 실행됩니다. 대신 반환값이 다릅니다. child는 &lt;code&gt;0&lt;/code&gt;을 받고, parent는 child의 &lt;code&gt;pid&lt;/code&gt;를 받습니다. 그래서 &lt;code&gt;if (pid == 0)&lt;/code&gt; 안쪽은 child만 실행하는 구역입니다. 그 child 안에서 &lt;code&gt;execve&lt;/code&gt;를 호출하면, child가 &lt;code&gt;./myspin&lt;/code&gt; 같은 실제 프로그램으로 바뀝니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아래 구현은 &lt;code&gt;eval&lt;/code&gt; 구현부에 처음 적어볼 만한 형태입니다. 코드 안에는 &quot;왜 이 줄이 여기 들어가는지&quot;를 주석으로 적어뒀습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void eval(char *cmdline)
{
    char *argv[MAXARGS];
    int background;
    pid_t pid;

    /*
     * parseline은 입력 한 줄을 argv 배열로 바꿉니다.
     *
     * 예를 들어 사용자가 이렇게 입력하면:
     *   ./myspin 1
     *
     * 대략 이렇게 쪼개집니다:
     *   argv[0] = &quot;./myspin&quot;
     *   argv[1] = &quot;1&quot;
     *   argv[2] = NULL
     *
     * 명령 끝에 &amp;amp;가 있으면 background = 1을 반환합니다.
     */
    background = parseline(cmdline, argv);

    /*
     * 빈 줄이면 실행할 명령이 없습니다.
     */
    if (argv[0] == NULL) {
        return;
    }

    /*
     * quit, jobs, bg, fg 같은 내장 명령인지 먼저 확인합니다.
     * builtin이면 shell이 직접 처리했으므로 여기서 끝냅니다.
     */
    if (builtin_cmd(argv)) {
        return;
    }

    /*
     * 여기부터는 외부 프로그램입니다.
     * shell 자체가 ./myspin으로 변하면 안 되므로 child를 만듭니다.
     */
    pid = fork();

    if (pid == 0) {
        /*
         * child process입니다.
         * 이 child가 실제 프로그램으로 바뀝니다.
         *
         * argv[0]: 실행할 파일 경로
         * argv: 새 프로그램에게 넘길 인자 배열, 마지막은 NULL이어야 합니다.
         * environ: 현재 shell의 환경 변수 배열을 child에게 넘깁니다.
         */
        execve(argv[0], argv, environ);

        /*
         * execve가 성공하면 여기로 돌아오지 않습니다.
         * 여기까지 왔다는 건 실행 실패입니다.
         */
        printf(&quot;%s: Command not found\n&quot;, argv[0]);
        exit(0);
    }

    /*
     * parent process, 즉 shell입니다.
     * foreground 명령이면 shell은 child가 끝날 때까지 기다립니다.
     */
    if (!background) {
        waitfg(pid, STDOUT_FILENO);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여러분들이 가장 낯선 부분은 &lt;code&gt;fork&lt;/code&gt; 이후 일 겁니다. &lt;code&gt;fork&lt;/code&gt;는 코드 흐름을 두 개로 갈라놓습니다. &lt;code&gt;pid == 0&lt;/code&gt;인 쪽은 child이고, 그 child에서 &lt;code&gt;execve&lt;/code&gt;를 호출합니다. parent shell은 그대로 남아서 기다리거나 다음 명령을 받습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;execve(argv[0], argv, environ)&lt;/code&gt;에서 세 인자는 각각 의미가 있습니다. &lt;code&gt;argv[0]&lt;/code&gt;은 실행할 파일 이름이고, &lt;code&gt;argv&lt;/code&gt;는 새 프로그램이 받을 인자 배열입니다. C 문자열 배열이라 마지막이 &lt;code&gt;NULL&lt;/code&gt;로 끝나야 합니다. &lt;code&gt;environ&lt;/code&gt;도 그냥 장식이 아닙니다. trace 02의 &lt;code&gt;myenv&lt;/code&gt;는 환경 변수가 제대로 전달되는지 확인합니다. 그냥 프로그램 파일만 실행한다고 끝나는 게 아니라, 실행 환경도 같이 넘겨야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 여기서 실행 환경은 환경변수 쪽 이야기입니다. 개발을 조금 해봤다면 &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;HOME&lt;/code&gt;, &lt;code&gt;LANG&lt;/code&gt; 같은 값을 들어봤을 겁니다. &lt;code&gt;execve&lt;/code&gt;의 세 번째 인자인 &lt;code&gt;environ&lt;/code&gt;은 child가 새 프로그램으로 바뀔 때 이런 환경을 같이 넘겨주는 역할을 합니다. 그래서 trace 02는 단순히 프로그램이 켜지는지만 보는 게 아니라, 실행 환경까지 제대로 이어지는지를 보는 테스트였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. waitfg: foreground 명령이면 shell이 기다려야 합니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 03~04에서 foreground 명령의 순서를 맞추는 부분입니다. foreground 프로그램이 끝나기 전에 shell prompt가 다시 나오면 안 됩니다. 그래서 shell은 &quot;앞에서 실행 중인 child가 아직 끝나지 않았는지&quot;를 봐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;foreground 명령은 shell이 기다리는 실행입니다. 사용자가 &lt;code&gt;./myspin 5&lt;/code&gt;를 쳤는데 prompt가 바로 돌아오면 잘못된 동작입니다. 실제 shell을 써봤다면, 명령어가 도는 동안 다음 입력이 막히는 그 느낌입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;trace 03~04만 볼 때는 아래처럼 &lt;code&gt;waitpid&lt;/code&gt;로 시작해도 됩니다. 이 버전은 초반 trace용입니다. &lt;code&gt;waitpid&lt;/code&gt;는 단순히 &quot;기다리는 함수&quot;처럼 보이지만, 실제로는 child가 끝났을 때 그 종료 상태를 parent가 받아오는 함수입니다. 이걸 보통 child를 &lt;b&gt;회수한다&lt;/b&gt;고 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 프로세스는 끝났다고 바로 완전히 사라지는 게 아니었습니다. parent가 &lt;code&gt;wait&lt;/code&gt; 계열 함수로 종료 상태를 받아줘야 정리가 됩니다. 이 과정을 하지 않으면 종료된 child가 좀비 프로세스처럼 남을 수 있습니다. 그래서 &quot;기다린다&quot;와 &quot;회수한다&quot;는 여기서 거의 같이 붙어 다니는 말이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void waitfg(pid_t pid, int output_fd)
{
    int status;

    /*
     * 초반 trace용 버전입니다.
     * foreground child가 끝날 때까지 parent shell이 기다립니다.
     */
    waitpid(pid, &amp;amp;status, 0);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 trace 08 이후 &lt;code&gt;SIGCHLD&lt;/code&gt; handler를 구현하는 순간부터는 이 방식을 바꿔야 합니다. 나중에는 handler가 child의 종료 상태를 받아서 job list를 정리합니다. 그러면 &lt;code&gt;waitfg&lt;/code&gt;가 또 &lt;code&gt;waitpid&lt;/code&gt;로 같은 child를 회수하려고 해서 역할이 겹칩니다. 그러니까 이 코드는 &quot;초반 trace를 위한 버전&quot;이고, 최종 제출에서는 아래 방식으로 갈아타는 게 좋습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 최종 형태의 &lt;code&gt;waitfg&lt;/code&gt;는 child를 직접 회수하지 않는 쪽이 좋아보였습니다. 대신 &lt;code&gt;jobs&lt;/code&gt; 배열을 계속 보면서 &quot;내가 기다려야 하는 pid가 아직도 foreground인가?&quot;만 확인합니다. child가 끝나거나 멈추면 handler가 실행되고, handler가 job list를 고칩니다. 그러면 &lt;code&gt;waitfg&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;단, 아래 버전은 &lt;code&gt;eval&lt;/code&gt;이 먼저 &lt;code&gt;addjob(jobs, pid, FG, cmdline)&lt;/code&gt;으로 foreground job을 등록하는 Part 5 이후에만 맞습니다. 앞쪽 trace 03~04 코드처럼 job list에 넣지 않고 바로 &lt;code&gt;waitfg(pid)&lt;/code&gt;를 부르면, &lt;code&gt;fgpid(jobs)&lt;/code&gt;가 찾을 job이 없어서 바로 빠져나올 수 있습니다. 이 부분은 꽤 위험한 함정입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 여기서 중요한 건 코드가 병렬로 동시에 돈다는 뜻이 아닙니다. 더 정확히는 OS가 실행 흐름의 주도권을 잠깐 가져가서 signal handler를 실행시키는 느낌입니다. shell이 &lt;code&gt;while&lt;/code&gt; 안에서 기다리고 있어도, &lt;code&gt;ctrl-c&lt;/code&gt;, &lt;code&gt;ctrl-z&lt;/code&gt;, child 종료 같은 사건이 생기면 OS가 handler 함수로 흐름을 넘깁니다. handler가 jobs 배열을 수정하고 돌아오면, &lt;code&gt;waitfg&lt;/code&gt;는 다시 조건을 확인합니다. 이 흐름에서는 handler가 공유 상태를 바꾸는 타이밍을 특히 조심해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void waitfg(pid_t pid, int output_fd)
{
    /*
     * 최종 사고방식입니다.
     *
     * child를 직접 waitpid로 회수하지 않습니다.
     * child의 종료/정지는 sigchld_handler가 처리합니다.
     *
     * waitfg는 &quot;아직 이 pid가 foreground job인가?&quot;만 확인합니다.
     */
    while (fgpid(jobs) == pid) {
        usleep(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 &lt;code&gt;usleep(1)&lt;/code&gt;로 계속 확인하는 방식은 과제 trace를 깨기 위한 간단한 polling 구현에 가깝습니다. 더 정석적인 signal 대기 구조를 공부한다면 &lt;code&gt;sigsuspend&lt;/code&gt;로 &lt;code&gt;SIGCHLD&lt;/code&gt;를 기다리는 방식까지 봐야 합니다. 여기서는 Shell Lab 공략 흐름을 먼저 잡기 위해 단순 버전으로 설명했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. job_t와 helper: shell이 process를 기억하는 방식&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;background로 넘어가기 전에 &lt;code&gt;jobs&lt;/code&gt; 배열을 한 번 보고 가는 게 좋습니다. 이 위치가 적당하다고 생각했습니다. trace 05~07부터는 shell이 child를 그냥 실행하고 끝내면 안 됩니다. shell이 &quot;이 pid가 몇 번 job인지&quot;, &quot;지금 foreground인지 background인지&quot;, &quot;원래 명령어가 무엇이었는지&quot;를 기억해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;제출본 기준으로는 아래 helper들이 실제로 job list를 관리합니다. 과제 handout에 따라 이미 제공된 함수도 있고, 직접 채워야 하는 형태로 보일 수도 있습니다. 중요한 건 이 함수들이 단순 부가 기능이 아니라, 뒤쪽 trace 전체의 기준점이라는 점입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct job_t {
    pid_t pid;              // 실제 child process id
    int jid;                // shell이 보여주는 job id: [1], [2] 같은 번호
    int state;              // FG, BG, ST 중 하나
    char cmdline[MAXLINE];  // 사용자가 입력한 원래 명령어
};

struct job_t jobs[MAXJOBS];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; &lt;code&gt;pid&lt;/code&gt;는 OS가 붙인 process 번호이고, &lt;code&gt;jid&lt;/code&gt;는 shell이 사용자에게 보여주기 위해 붙인 번호입니다. 사용자는 &lt;code&gt;fg %1&lt;/code&gt;, &lt;code&gt;bg %1&lt;/code&gt;처럼 jid로 job을 다루지만, 실제 signal을 보낼 때는 pid 또는 process group이 필요합니다. 그래서 &lt;code&gt;pid2jid&lt;/code&gt;, &lt;code&gt;getjobjid&lt;/code&gt;, &lt;code&gt;getjobpid&lt;/code&gt; 같은 변환 함수들이 계속 나옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;먼저 빈 job slot이 어떤 상태인지 정해둬야 합니다. handout마다 이 부분은 제공되어 있을 수도 있지만, 직접 구현해야 하는 버전이라면 아래 두 함수부터 보는 게 좋습니다. &lt;code&gt;initjobs&lt;/code&gt;는 처음에 배열 전체를 비우고, &lt;code&gt;clearjob&lt;/code&gt;은 한 칸을 빈 칸으로 되돌립니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void clearjob(struct job_t *job)
{
    /*
     * pid가 0이면 비어 있는 job slot으로 봅니다.
     * state도 UNDEF로 돌려놓고, command line도 비웁니다.
     */
    job-&amp;gt;pid = 0;
    job-&amp;gt;jid = 0;
    job-&amp;gt;state = UNDEF;
    job-&amp;gt;cmdline[0] = '\0';
}

void initjobs(struct job_t *jobs)
{
    int i;

    /*
     * shell이 시작될 때 jobs 배열 전체를 비웁니다.
     */
    for (i = 0; i &amp;lt; MAXJOBS; i++) {
        clearjob(&amp;amp;jobs[i]);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이걸 먼저 잡아두면 &lt;code&gt;addjob&lt;/code&gt;과 &lt;code&gt;deletejob&lt;/code&gt;이 훨씬 단순해집니다. 여기서부터 나오는 &lt;code&gt;addjob()&lt;/code&gt;, &lt;code&gt;deletejob()&lt;/code&gt;, &lt;code&gt;pid2jid()&lt;/code&gt;는 &lt;code&gt;fork()&lt;/code&gt;나 &lt;code&gt;waitpid()&lt;/code&gt;처럼 헤더에서 가져오는 함수가 아닙니다. 이 글 기준으로는 구현부를 같이 확인하는 &lt;code&gt;tsh.c&lt;/code&gt; 내부 helper입니다. 빈 칸은 &lt;code&gt;pid == 0&lt;/code&gt;, 사용 중인 칸은 &lt;code&gt;pid &amp;gt; 0&lt;/code&gt;이라는 기준 하나로 보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
    int i;

    if (pid &amp;lt; 1) {
        return 0;
    }

    /*
     * jobs 배열에서 비어 있는 칸을 찾습니다.
     * pid가 0이면 비어 있는 job slot으로 봅니다.
     */
    for (i = 0; i &amp;lt; MAXJOBS; i++) {
        if (jobs[i].pid == 0) {
            jobs[i].pid = pid;
            jobs[i].state = state;
            jobs[i].jid = nextjid++;

            if (nextjid &amp;gt; MAXJOBS) {
                nextjid = 1;
            }

            strcpy(jobs[i].cmdline, cmdline);
            return 1;
        }
    }

    printf(&quot;Tried to create too many jobs\n&quot;);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;addjob&lt;/code&gt;은 새 child를 shell의 기억 속에 넣는 함수입니다. trace 05에서 background job을 실행했을 때 바로 prompt가 돌아와도, shell은 그 child를 잊으면 안 됩니다. 그래서 &lt;code&gt;addjob&lt;/code&gt;으로 pid, state, cmdline을 저장합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int deletejob(struct job_t *jobs, pid_t pid)
{
    int i;

    if (pid &amp;lt; 1) {
        return 0;
    }

    /*
     * 종료된 child의 pid를 찾아서 job slot을 비웁니다.
     * 이 함수는 SIGCHLD handler에서 자주 호출됩니다.
     */
    for (i = 0; i &amp;lt; MAXJOBS; i++) {
        if (jobs[i].pid == pid) {
            clearjob(&amp;amp;jobs[i]);
            nextjid = maxjid(jobs) + 1;
            return 1;
        }
    }

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;deletejob&lt;/code&gt;은 끝난 job을 shell의 기억에서 지우는 함수입니다. 단, 멈춘 job은 지우면 안 됩니다. &lt;code&gt;ctrl-z&lt;/code&gt;로 멈춘 job은 나중에 &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;로 다시 움직일 수 있어야 하므로 &lt;code&gt;ST&lt;/code&gt; 상태로 남겨야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int pid2jid(pid_t pid)
{
    int i;

    if (pid &amp;lt; 1) {
        return 0;
    }

    /*
     * 출력 형식에는 [jid]가 필요하지만,
     * waitpid나 signal handler는 pid를 기준으로 들어오는 경우가 많습니다.
     * 그래서 pid를 jid로 바꿔주는 함수가 필요합니다.
     */
    for (i = 0; i &amp;lt; MAXJOBS; i++) {
        if (jobs[i].pid == pid) {
            return jobs[i].jid;
        }
    }

    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;pid2jid&lt;/code&gt;는 출력 형식을 맞추려고 필요했습니다. 예를 들어 signal 때문에 job이 종료되면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;Job [1] (12345) terminated by signal 2&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;handler는 pid를 받지만, 출력에는 jid도 필요하니 변환이 들어갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5. trace 05~07: background job을 기록하고 jobs로 보여주기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 05~07입니다. 이제 &lt;code&gt;&amp;amp;&lt;/code&gt;로 실행한 background job을 shell이 기다리지 않게 만들고, &lt;code&gt;jobs&lt;/code&gt; 명령으로 현재 job 목록을 보여줘야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;trace 05부터 &lt;code&gt;&amp;amp;&lt;/code&gt;가 들어옵니다. &lt;code&gt;&amp;amp;&lt;/code&gt;는 &quot;이 명령을 background로 실행하라&quot;는 뜻입니다. foreground는 shell이 기다리고, background는 shell이 기다리지 않습니다. 쉘에 명령어를 칠 때 뒤에 붙이는 옵션 같은 느낌으로 보면 괜찮습니다. 실제 쉘에서도 그대로 씁니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# foreground
# shell이 myspin이 끝날 때까지 기다립니다.
./myspin 5

# background
# myspin은 뒤에서 돌고, shell은 바로 다음 입력을 받습니다.
./myspin 5 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 background라고 해서 child를 잊어버리면 안 됩니다. 나중에 &lt;code&gt;jobs&lt;/code&gt;로 출력해야 하고, 종료되면 지워야 하고, 멈추면 상태를 바꿔야 합니다. 그래서 &lt;code&gt;jobs&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;이제 &lt;code&gt;eval&lt;/code&gt; 구현부가 아래처럼 조금 커집니다. 단순히 &lt;code&gt;fork&lt;/code&gt;하고 기다리는 함수가 아니라, child를 shell의 job list에 등록하는 함수가 됩니다. 이때부터 Shell Lab이 그냥 실행기가 아니라 job 관리 프로그램처럼 보이기 시작했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void eval(char *cmdline)
{
    char *argv[MAXARGS];
    int background;
    pid_t pid;

    background = parseline(cmdline, argv);

    if (argv[0] == NULL) {
        return;
    }

    if (builtin_cmd(argv)) {
        return;
    }

    pid = fork();

    if (pid == 0) {
        execve(argv[0], argv, environ);
        printf(&quot;%s: Command not found\n&quot;, argv[0]);
        exit(0);
    }

    /*
     * 여기부터가 trace 05~07을 위해 추가되는 부분입니다.
     *
     * shell은 child를 그냥 실행하고 잊으면 안 됩니다.
     * child가 foreground인지 background인지 기록해야 합니다.
     */
    if (background) {
        addjob(jobs, pid, BG, cmdline);
        printf(&quot;[%d] (%d) %s&quot;, pid2jid(pid), pid, cmdline);
    } else {
        addjob(jobs, pid, FG, cmdline);
        waitfg(pid, STDOUT_FILENO);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;jobs&lt;/code&gt; 명령은 이 배열을 출력하는 builtin입니다. 외부 프로그램 실행이 아니라 shell 내부 상태를 보여주는 명령이므로 &lt;code&gt;builtin_cmd&lt;/code&gt;에 들어가는 게 맞습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;6. trace 08 이후: addjob 전에 SIGCHLD가 오는 race 막기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 08 이후에서 나오는 타이밍 문제를 막는 부분입니다. 이제 child가 종료되면 signal handler가 job list를 건드리기 시작합니다. 그래서 parent가 job을 등록하는 중간에 handler가 끼어들면 상태가 꼬일 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기까지는 꽤 자연스럽습니다. 그런데 child가 아주 빨리 종료되면 문제가 생깁니다. parent가 아직 &lt;code&gt;addjob&lt;/code&gt;을 하기 전에 child가 죽고, shell이 &lt;code&gt;SIGCHLD&lt;/code&gt;를 받을 수 있습니다. 그러면 handler는 job을 지우려고 하는데 job list에는 아직 등록조차 안 되어 있습니다. 이 타이밍을 놓치기 쉽습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 최종 &lt;code&gt;eval&lt;/code&gt;은 &lt;code&gt;fork&lt;/code&gt;와 &lt;code&gt;addjob&lt;/code&gt; 사이에 signal이 끼어들지 못하게 잠깐 막습니다. 이 부분은 조각 코드로 나눠 보면 순서를 놓치기 쉬워서, 아래처럼 함수 전체 흐름 안에서 보는 게 좋아보였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;먼저 signal 관련 함수 이름을 조금 풀어보겠습니다. &lt;code&gt;sigset_t&lt;/code&gt;는 막거나 풀 signal 목록을 담는 자료형입니다. &lt;code&gt;sigemptyset&lt;/code&gt;은 그 목록을 비우고, &lt;code&gt;sigaddset&lt;/code&gt;은 목록에 signal을 하나씩 추가합니다. 마지막으로 &lt;code&gt;sigprocmask&lt;/code&gt;가 현재 process의 signal mask를 실제로 바꿉니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 구간의 최소 목표는 &lt;code&gt;fork&lt;/code&gt; 뒤 &lt;code&gt;addjob&lt;/code&gt; 전까지 &lt;code&gt;SIGCHLD&lt;/code&gt;가 끼어들지 못하게 막는 것입니다. 다만 원칙은 더 넓습니다. &lt;code&gt;jobs&lt;/code&gt; 배열을 읽거나 수정하는 구간과 &lt;code&gt;SIGCHLD&lt;/code&gt; handler가 동시에 끼어들면 shell의 기억이 꼬일 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 아래 코드에서는 &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;SIGTSTP&lt;/code&gt;도 같이 막고 있는데, 이건 foreground job 상태와 jobs 배열이 바뀌는 동안 다른 signal까지 끼어들지 않게 하려는 확장에 가깝습니다. 처음 구현할 때는 &quot;일단 SIGCHLD race를 막는다&quot;에 초점을 두는 게 좋았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; &lt;code&gt;sigprocmask(SIG_BLOCK, &amp;amp;mask, &amp;amp;old_mask)&lt;/code&gt;는 &quot;mask에 들어있는 signal들을 지금부터 막고, 막기 전 상태는 old_mask에 저장해줘&quot;라는 뜻입니다. 여기서 &lt;code&gt;old_mask&lt;/code&gt;를 저장하는 이유가 중요했습니다. 그냥 마지막에 모든 signal을 풀면 되는 게 아니었습니다. 이 함수가 호출되기 전에도 이미 다른 이유로 막혀 있던 signal이 있을 수 있습니다. 그래서 원래 상태를 저장했다가, 끝나면 &lt;code&gt;SIG_SETMASK&lt;/code&gt;로 정확히 그 상태로 되돌립니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아래 코드에서 &lt;code&gt;sigemptyset()&lt;/code&gt;, &lt;code&gt;sigaddset()&lt;/code&gt;, &lt;code&gt;sigprocmask()&lt;/code&gt;는 우리가 만든 함수가 아닙니다. 헤더를 통해 가져와서 OS의 signal mask를 조작하는 함수입니다. 반대로 같은 코드 안의 &lt;code&gt;builtin_cmd()&lt;/code&gt;, &lt;code&gt;addjob()&lt;/code&gt;, &lt;code&gt;fgpid()&lt;/code&gt;는 우리 shell 코드 안의 함수입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;실전 코드라면 &lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;sigprocmask()&lt;/code&gt;, &lt;code&gt;setpgid()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&gt; 같은 시스템 함수가 실패할 수 있다는 점도 확인해야 합니다. 과제 trace가 항상 실패 상황을 만들지는 않지만, 시스템 콜은 성공한다고 가정하는 습관을 들이면 나중에 디버깅 비용이 커집니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void eval(char *cmdline)
{
    char *argv[MAXARGS];
    pid_t pid;
    sigset_t mask;
    sigset_t old_mask;

    int background = parseline(cmdline, argv);

    if (argv[0] == NULL) {
        return;
    }

    /*
     * SIGCHLD: child가 너무 빨리 종료되는 상황을 막기 위해 필요합니다.
     * SIGINT, SIGTSTP: foreground job과 jobs 배열이 바뀌는 중에 끼어들면 상태가 꼬일 수 있습니다.
     */
    sigemptyset(&amp;amp;mask);
    sigaddset(&amp;amp;mask, SIGCHLD);
    sigaddset(&amp;amp;mask, SIGINT);
    sigaddset(&amp;amp;mask, SIGTSTP);

    /*
     * 여기서부터 잠깐 signal을 막습니다.
     * 특히 fork 이후 addjob 전까지가 위험한 구간입니다.
     */
    sigprocmask(SIG_BLOCK, &amp;amp;mask, &amp;amp;old_mask);

    /*
     * builtin도 jobs 배열을 만질 수 있습니다.
     * 그래서 signal을 막은 상태에서 먼저 처리합니다.
     */
    int builtin_result = builtin_cmd(argv);

    if (builtin_result == 1) {
        /*
         * quit/jobs/bg 같은 명령이 여기로 옵니다.
         * 처리 끝났으니 signal mask를 원래대로 돌리고 끝냅니다.
         */
        sigprocmask(SIG_SETMASK, &amp;amp;old_mask, NULL);
        return;
    }

    if (builtin_result == 2) {
        /*
         * fg는 job을 foreground로 바꾼 뒤 기다려야 합니다.
         * 기다리는 동안 SIGCHLD를 받아야 하므로 mask를 먼저 풉니다.
         */
        sigprocmask(SIG_SETMASK, &amp;amp;old_mask, NULL);

        while (fgpid(jobs) != 0) {
            usleep(1);
        }
        return;
    }

    /*
     * builtin이 아니면 외부 프로그램입니다.
     */
    pid = fork();

    if (pid == 0) {
        /*
         * child는 parent의 signal mask를 물려받습니다.
         * execve 전에 원래대로 돌려야 합니다.
         */
        sigprocmask(SIG_SETMASK, &amp;amp;old_mask, NULL);

        /*
         * 뒤쪽 process group trace까지 생각하면 여기서 분리합니다.
         */
        setpgid(0, 0);

        execve(argv[0], argv, environ);
        printf(&quot;%s: Command not found\n&quot;, argv[0]);
        exit(0);
    }

    /*
     * parent shell은 child를 jobs 배열에 등록합니다.
     * signal을 막아둔 상태이므로, addjob 전에 SIGCHLD가 끼어들 수 없습니다.
     */
    addjob(jobs, pid, background ? BG : FG, cmdline);

    /*
     * 이제 job list에 등록했으니 signal을 다시 받습니다.
     */
    sigprocmask(SIG_SETMASK, &amp;amp;old_mask, NULL);

    if (background) {
        printf(&quot;[%d] (%d) %s&quot;, pid2jid(pid), pid, cmdline);
    } else {
        waitfg(pid, STDOUT_FILENO);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 코드에서 가장 유명한 위험 구간은 &lt;code&gt;fork&lt;/code&gt; 이후 &lt;code&gt;addjob&lt;/code&gt; 전입니다. 이 사이에 child가 죽으면 shell의 job list와 실제 child 상태가 어긋납니다. 더 넓게 보면 &lt;code&gt;jobs&lt;/code&gt; 배열을 만지는 코드와 handler가 충돌하지 않게 만드는 것이 핵심입니다. 그래서 &lt;code&gt;sigprocmask&lt;/code&gt;를 쓰는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;7. trace 08~14: 종료, 중단, 재개 상태 구분하기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 08~14입니다. child가 그냥 끝나는 경우, signal 때문에 죽는 경우, &lt;code&gt;ctrl-z&lt;/code&gt; 때문에 멈추는 경우를 shell이 구분해야 합니다. 그래야 job을 삭제할지, &lt;code&gt;ST&lt;/code&gt; 상태로 남길지 결정할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; Shell Lab trace driver 기준으로는 shell handler가 &lt;code&gt;SIGTSTP&lt;/code&gt;나 &lt;code&gt;SIGINT&lt;/code&gt;를 받고, 그 signal을 현재 foreground job group에 다시 전달한다고 보면 됩니다. 실제 Linux shell은 terminal foreground process group 설정까지 얽혀 있어서 더 복잡합니다. 이 과제에서는 shell이 signal을 자기 자신에게 적용하고 끝내면 안 되고, 현재 foreground job에게 전달해야 한다는 점이 중요했습니다. &lt;code&gt;ctrl-z&lt;/code&gt;는 job을 죽이는 게 아니라, 나중에 &lt;code&gt;bg&lt;/code&gt;나 &lt;code&gt;fg&lt;/code&gt;로 다시 움직일 수 있게 잠깐 세워두는 동작입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;child가 종료되거나, 멈추거나, 다시 실행되면 parent shell은 &lt;code&gt;SIGCHLD&lt;/code&gt;를 받습니다. 그러면 &lt;code&gt;sigchld_handler&lt;/code&gt;가 실행됩니다. 이 함수는 child 상태를 보고 jobs 배열을 정리하는 곳이라고 보면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void sigchld_handler(int sig)
{
    int status;
    pid_t pid;

    /*
     * -1은 &quot;상태가 바뀐 child 아무나&quot;라는 뜻입니다.
     *
     * WNOHANG:
     *   바뀐 child가 없으면 기다리지 않고 바로 빠져나옵니다.
     *
     * WUNTRACED:
     *   종료된 child뿐 아니라 멈춘 child도 잡습니다.
     *
     * WCONTINUED:
     *   SIGCONT로 다시 실행된 child도 잡습니다.
     *
     * while을 쓰는 이유:
     *   child 여러 개가 거의 동시에 상태가 바뀔 수 있기 때문입니다.
     */
    while ((pid = waitpid(-1, &amp;amp;status,
                          WNOHANG | WUNTRACED | WCONTINUED)) &amp;gt; 0) {

        if (WIFEXITED(status)) {
            /*
             * 정상 종료입니다.
             * job list에서 제거합니다.
             */
            deletejob(jobs, pid);
        }
        else if (WIFSIGNALED(status)) {
            /*
             * signal 때문에 종료됐습니다.
             * 예: ctrl-c로 SIGINT를 받은 경우입니다.
             */
            printf(&quot;Job [%d] (%d) terminated by signal %d\n&quot;,
                   pid2jid(pid), pid, WTERMSIG(status));

            deletejob(jobs, pid);
        }
        else if (WIFSTOPPED(status)) {
            /*
             * signal 때문에 멈췄습니다.
             * 예: ctrl-z로 SIGTSTP를 받은 경우입니다.
             *
             * 여기서 deletejob을 하면 안 됩니다.
             * 멈춘 job은 bg/fg로 다시 실행할 수 있어야 합니다.
             */
            struct job_t *job = getjobpid(jobs, pid);

            if (job != NULL) {
                job-&amp;gt;state = ST;
            }

            printf(&quot;Job [%d] (%d) stopped by signal %d\n&quot;,
                   pid2jid(pid), pid, WSTOPSIG(status));
        }
        else if (WIFCONTINUED(status)) {
            /*
             * SIGCONT를 받아 다시 실행된 상태입니다.
             * 출력 형식은 과제 요구에 맞춰 조정하면 됩니다.
             */
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 이 코드는 Shell Lab 풀이 문맥에서 보는 것이 좋습니다. handler 안에서 &lt;code&gt;printf&lt;/code&gt;를 바로 호출하고, &lt;code&gt;jobs&lt;/code&gt; 배열을 수정하는 식의 구현은 일반 프로그램에서 그대로 안전한 패턴이라고 말하기는 어렵습니다. POSIX 기준으로는 signal handler 안에서 호출해도 안전한 함수가 제한되어 있고, 보통은 handler에서 최소한의 일만 하는 쪽이 좋습니다. 이 과제에서는 trace 요구 출력과 job 상태 갱신을 맞추기 위해 이런 형태로 설명했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 여기서 특히 조심할 건 &lt;code&gt;WIFSTOPPED&lt;/code&gt;였습니다. 멈춘 job은 죽은 게 아닙니다. &lt;code&gt;ST&lt;/code&gt; 상태로 남겨야 합니다. 그래야 뒤에서 &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;fg&lt;/code&gt;가 의미를 가집니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;8. trace 15~18: bg와 fg로 stopped job 다시 움직이기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 15~18입니다. 앞에서 &lt;code&gt;ctrl-z&lt;/code&gt;로 멈춘 job을 &lt;code&gt;bg&lt;/code&gt;나 &lt;code&gt;fg&lt;/code&gt; 명령으로 다시 움직이게 만들어야 합니다. 그래서 &lt;code&gt;builtin_cmd&lt;/code&gt;의 빈자리였던 &lt;code&gt;bg&lt;/code&gt;, &lt;code&gt;fg&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;bg&lt;/code&gt;와 &lt;code&gt;fg&lt;/code&gt;도 shell 내장 명령입니다. 외부 프로그램이 아닙니다. 둘 다 멈춘 job에게 &lt;code&gt;SIGCONT&lt;/code&gt;를 보내서 다시 실행시키지만, shell이 기다리는 방식이 다릅니다. 이 차이는 구현에서 실수하기 쉬운 지점입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;bg %1&lt;/code&gt;은 1번 job을 background로 다시 실행합니다. shell은 바로 prompt를 돌려줘도 됩니다. &lt;code&gt;fg %1&lt;/code&gt;은 1번 job을 foreground로 가져옵니다. 그러면 shell은 그 job이 끝나거나 다시 멈출 때까지 기다려야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;아래 코드는 핵심 흐름을 보여주는 형태입니다. 실제 trace를 끝까지 맞추려면 &lt;code&gt;bg %1&lt;/code&gt;, &lt;code&gt;fg %1&lt;/code&gt;뿐 아니라 &lt;code&gt;bg 1234&lt;/code&gt;, &lt;code&gt;fg 1234&lt;/code&gt;처럼 pid로 들어오는 경우도 처리해야 합니다. 인자가 없는 경우, &lt;code&gt;%&lt;/code&gt; 뒤가 숫자가 아닌 경우, 존재하지 않는 jid/pid를 가리키는 경우의 에러 메시지도 handout 요구에 맞춰 정확히 출력해야 합니다. 여기서는 &lt;code&gt;%1&lt;/code&gt; 흐름만 처리하면 입력 검증을 빼먹기 쉬우니, 최종 제출 전에 따로 체크하는 게 좋습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;최종 제출 체크.&lt;/b&gt; &lt;code&gt;bg/fg&lt;/code&gt;는 보통 별도 &lt;code&gt;do_bgfg&lt;/code&gt; 함수로 빼도 좋습니다. 최종형은 &lt;code&gt;%jid&lt;/code&gt;와 &lt;code&gt;pid&lt;/code&gt; 입력을 모두 job으로 해석하고, 실패 상황마다 trace가 기대하는 문구를 그대로 출력해야 합니다. 아래 코드는 그중 &quot;찾은 job에 &lt;code&gt;SIGCONT&lt;/code&gt;를 보내고 BG/FG 상태로 바꾸는 핵심부&quot;만 보여주는 축약 버전입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int builtin_cmd(char **argv)
{
    char *cmd = argv[0];

    if (cmd == NULL) {
        return 1;
    }

    if (!strcmp(cmd, &quot;quit&quot;)) {
        exit(0);
    }

    if (!strcmp(cmd, &quot;jobs&quot;)) {
        listjobs(jobs, STDOUT_FILENO);
        return 1;
    }

    if (!strcmp(cmd, &quot;bg&quot;) || !strcmp(cmd, &quot;fg&quot;)) {
        int jid;

        /*
         * Shell Lab에서는 보통 bg %1, fg %1처럼 들어옵니다.
         * argv[1]은 &quot;%1&quot;이고, argv[1] + 1은 &quot;1&quot;입니다.
         */
        if (argv[1] == NULL || argv[1][0] != '%') {
            return 1;
        }

        if (sscanf(argv[1] + 1, &quot;%d&quot;, &amp;amp;jid) != 1) {
            return 1;
        }

        /*
         * jid로 job을 찾습니다.
         * 찾지 못하면 더 진행할 수 없습니다.
         */
        struct job_t *job = getjobjid(jobs, jid);

        if (job == NULL) {
            return 1;
        }

        /*
         * 멈춘 job을 다시 실행합니다.
         * process group 전체에 보내야 하므로 -pid를 사용합니다.
         */
        kill(-job-&amp;gt;pid, SIGCONT);

        if (!strcmp(cmd, &quot;bg&quot;)) {
            /*
             * background로 다시 실행합니다.
             * shell은 기다리지 않습니다.
             */
            job-&amp;gt;state = BG;
            printf(&quot;[%d] (%d) %s&quot;, job-&amp;gt;jid, job-&amp;gt;pid, job-&amp;gt;cmdline);
            return 1;
        }

        /*
         * foreground로 다시 실행합니다.
         * 실제 기다림은 eval에서 signal mask를 푼 뒤 처리하게 합니다.
         */
        job-&amp;gt;state = FG;
        return 2;
    }

    /*
     * builtin이 아니므로 eval이 fork/execve로 처리해야 합니다.
     */
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 &lt;code&gt;fg&lt;/code&gt;가 &lt;code&gt;return 2&lt;/code&gt;를 하는 건 제 구현 방식입니다. &lt;code&gt;fg&lt;/code&gt;는 builtin이긴 하지만, 처리 후에 shell이 기다려야 합니다. 그래서 &lt;code&gt;eval&lt;/code&gt;이 signal mask를 원래대로 돌린 다음 기다릴 수 있게, 신호를 주는 느낌으로 2를 썼습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;9. trace 19~21: process group 전체에 signal 보내기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 구간은 trace 19~21까지 마무리하는 부분입니다. 이제 foreground job 하나가 process 하나라고 가정하면 안 됩니다. foreground job 안에서 다시 child process가 생길 수 있으므로, signal을 process 하나가 아니라 process group 전체에 보내야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자가 &lt;code&gt;ctrl-c&lt;/code&gt;를 눌렀다고 shell이 죽으면 안 됩니다. foreground job이 죽어야 합니다. &lt;code&gt;ctrl-z&lt;/code&gt;도 shell이 멈추는 것이 아니라 foreground job이 멈춰야 합니다. 이게 shell 입장에서는 꽤 중요한 역할 분리였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 설명도 Shell Lab trace driver 기준으로 읽는 게 좋습니다. 실제 shell은 터미널의 foreground process group 자체를 관리하지만, 과제에서는 handler가 signal을 받아 현재 foreground job의 process group에 다시 전달하는 구조를 구현한다고 보면 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;예를 들어 foreground job이 내부에서 또 다른 child를 만들 수 있습니다. 그러면 &lt;code&gt;kill(pid, SIGINT)&lt;/code&gt;처럼 대표 process 하나에만 signal을 보내면, 그 안에서 만들어진 다른 process가 남을 수 있습니다. 그래서 child를 새 process group의 대표로 만들고, &lt;code&gt;kill(-pid, SIGINT)&lt;/code&gt;처럼 음수 pid로 group 전체에 signal을 보냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; handler는 현재 foreground job을 찾고, 그 job의 process group 전체에 signal을 넘깁니다. &lt;code&gt;kill(pid, sig)&lt;/code&gt;가 아니라 &lt;code&gt;kill(-pid, sig)&lt;/code&gt;인 이유가 여기서 나옵니다. 여기서 음수 pid는 group 전체로 보낸다는 뜻입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void sigint_handler(int sig)
{
    /*
     * 현재 foreground job의 대표 pid를 찾습니다.
     */
    pid_t pid = fgpid(jobs);

    if (pid == 0) {
        return;
    }

    /*
     * pid 하나가 아니라 process group 전체에 SIGINT를 보냅니다.
     * 그래야 foreground job 안의 여러 process가 같이 종료됩니다.
     */
    kill(-pid, sig);
}

void sigtstp_handler(int sig)
{
    /*
     * ctrl-z도 shell이 아니라 foreground job에게 전달해야 합니다.
     */
    pid_t pid = fgpid(jobs);

    if (pid == 0) {
        return;
    }

    /*
     * process group 전체를 멈춥니다.
     */
    kill(-pid, sig);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이게 되려면 child를 만들 때 process group을 분리해야 합니다. 그래서 최종 &lt;code&gt;eval&lt;/code&gt;의 child 부분에 &lt;code&gt;setpgid(0, 0)&lt;/code&gt;가 들어갑니다. child의 pid를 그 group의 id로 만드는 식입니다. 후반 trace에서 갑자기 이 코드의 의미가 살아납니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;c arduino&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;c&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void eval(char *cmdline)
{
    /*
     * 앞부분은 Part 5의 최종 eval과 같습니다.
     * 여기서는 child 구현부만 다시 봅니다.
     */

    pid = fork();

    if (pid == 0) {
        sigprocmask(SIG_SETMASK, &amp;amp;old_mask, NULL);

        /*
         * child를 shell과 다른 process group으로 분리합니다.
         * 후반 trace에서는 foreground job 안에 process가 여러 개 생길 수 있습니다.
         */
        setpgid(0, 0);

        execve(argv[0], argv, environ);
        printf(&quot;%s: Command not found\n&quot;, argv[0]);
        exit(0);
    }

    /*
     * parent 구현부는 addjob, signal mask 복구, waitfg로 이어집니다.
     */
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;10. trace를 깨는 순서&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정리하면 trace를 번호로만 외우지 말고, 필요한 개념을 붙이는 순서로 보는 게 좋았습니다. 처음부터 &lt;code&gt;sigprocmask&lt;/code&gt;, &lt;code&gt;setpgid&lt;/code&gt;까지 한 번에 다루면 범위가 너무 커집니다. 먼저 &lt;code&gt;eval&lt;/code&gt;과 &lt;code&gt;builtin_cmd&lt;/code&gt;가 무엇인지 잡고, 그 위에 하나씩 올리는 편이 낫습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;trace 00~04: quit과 외부 명령 실행&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;EOF는 기본 코드 확인, &lt;code&gt;quit&lt;/code&gt;은 builtin, 외부 명령은 &lt;code&gt;fork&lt;/code&gt;/&lt;code&gt;execve&lt;/code&gt;로 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;trace 05~07: background job과 jobs 출력&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;&amp;amp;&lt;/code&gt;를 background로 보고 &lt;code&gt;addjob&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt; 출력을 붙입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;trace 08~14: signal로 종료/정지 상태 정리&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;SIGCHLD&lt;/code&gt;, &lt;code&gt;SIGINT&lt;/code&gt;, &lt;code&gt;SIGTSTP&lt;/code&gt; handler를 붙이고, &lt;code&gt;sigprocmask&lt;/code&gt;로 race를 막습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;trace 15~18: stopped job을 bg/fg로 재개&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;bg&lt;/code&gt;/&lt;code&gt;fg&lt;/code&gt;로 stopped job에게 &lt;code&gt;SIGCONT&lt;/code&gt;를 보내고, foreground 대기를 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;5&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;trace 19~21: process group 전체 제어&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;setpgid&lt;/code&gt;와 &lt;code&gt;kill(-pid, sig)&lt;/code&gt;로 process group 전체에 signal을 보냅니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과제는 코드 없이 개요만 보면 핵심 흐름이 늦게 보입니다. &lt;code&gt;parseline&lt;/code&gt;, &lt;code&gt;builtin_cmd&lt;/code&gt;, &lt;code&gt;fork&lt;/code&gt;, &lt;code&gt;execve&lt;/code&gt;, &lt;code&gt;waitfg&lt;/code&gt;를 표처럼 요약하는 것보다, 함수 구현부 위에 한 줄씩 주석을 달아가면서 보는 쪽이 훨씬 낫다고 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추가. 우리가 한것은 syscall을 가지고 논 것 밖에 없다. 이거하나가 힘들다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab에서 계속 호출한 &lt;code&gt;fork()&lt;/code&gt;, &lt;code&gt;waitpid()&lt;/code&gt;, &lt;code&gt;kill()&lt;/code&gt;, &lt;code&gt;sigprocmask()&lt;/code&gt; 같은 함수들은 일반 함수처럼 생겼지만, 최종적으로는 커널에게 작업을 요청하는 통로로 이어집니다. 일반 프로세스는 유저 모드에서 실행됩니다. 유저 모드 코드는 다른 프로세스를 마음대로 만들거나, 커널의 process table을 직접 고치거나, 다른 process group에 signal을 직접 꽂아 넣을 권한이 없습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 CPU와 OS는 정해진 입구를 둡니다. 프로세스는 레지스터에 &quot;어떤 작업을 할지&quot;와 &quot;인자값&quot;을 채운 뒤, CPU가 제공하는 특수한 명령어로 커널 모드 진입을 요청합니다. x86-64 Linux 기준으로 그 명령어가 보통 &lt;code&gt;syscall&lt;/code&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;핵심 개념.&lt;/b&gt; 프로세스가 커널 안으로 걸어 들어가는 게 아닙니다. 유저 모드에서 &lt;code&gt;syscall&lt;/code&gt; 명령어를 실행하면, CPU 하드웨어가 권한을 커널 모드로 바꾸고, 커널이 부팅 때 미리 등록해둔 진입점으로 점프합니다. 프로세스는 커널에게 직접 명령하는 게 아니라, CPU가 보장하는 합법적인 문을 통해 요청하는 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 커널은 부팅 때 syscall 입구를 CPU에 등록합니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;x86-64에서는 커널이 부팅 과정에서 CPU의 특수 레지스터인 MSR에 syscall 진입 주소를 적어둡니다. 대표적으로 &lt;code&gt;IA32_LSTAR&lt;/code&gt;에는 &lt;code&gt;syscall&lt;/code&gt;이 실행됐을 때 점프할 커널 엔트리 주소가 들어갑니다. 이 주소는 일반 유저 프로세스가 마음대로 바꿀 수 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 유저 프로세스는 레지스터에 요청 내용을 채웁니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;glibc 같은 라이브러리 함수는 syscall 번호와 인자를 정해진 레지스터에 넣습니다. x86-64 Linux에서는 보통 &lt;code&gt;rax&lt;/code&gt;에 syscall 번호를 넣고, 인자는 &lt;code&gt;rdi&lt;/code&gt;, &lt;code&gt;rsi&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt;, &lt;code&gt;r10&lt;/code&gt;, &lt;code&gt;r8&lt;/code&gt;, &lt;code&gt;r9&lt;/code&gt; 순서로 둡니다. 예를 들어 &lt;code&gt;kill(pid, sig)&lt;/code&gt;라면 pid와 signal 번호가 인자로 들어갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. &lt;code&gt;syscall&lt;/code&gt; 명령어가 유저 모드에서 커널 모드로 넘깁니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;syscall&lt;/code&gt;은 그냥 함수 이름이 아니라 CPU instruction입니다. 이 명령어가 실행되면 CPU는 현재 유저 코드로 돌아올 주소와 플래그를 저장하고, 실행 권한을 Ring 3에서 Ring 0으로 바꾼 뒤, 앞에서 MSR에 등록해둔 커널 진입점으로 점프합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: bold; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 커널은 syscall table을 보고 실제 작업을 실행합니다&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;커널 진입점은 레지스터에 들어 있는 syscall 번호를 보고, 내부 syscall table에서 해당 작업을 찾습니다. 작업이 끝나면 결과값을 보통 &lt;code&gt;rax&lt;/code&gt;에 담고, &lt;code&gt;sysret&lt;/code&gt; 계열 경로로 다시 유저 모드로 돌아갑니다. 유저 코드 입장에서는 &lt;code&gt;fork()&lt;/code&gt;나 &lt;code&gt;kill()&lt;/code&gt; 함수 호출이 return된 것처럼 보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre; font-size: 13px; line-height: 1.65;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;; x86-64 Linux syscall 호출 모양의 개념도입니다.
; 실제 glibc wrapper와 커널 엔트리는 더 많은 보존/검증 코드를 포함합니다.

mov rax, 62      ; syscall number: kill
mov rdi, pid     ; 1번째 인자
mov rsi, sig     ; 2번째 인자
syscall          ; CPU가 커널 모드로 전환하고 커널 엔트리로 점프&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Shell Lab의 함수들과 연결하면 아래처럼 볼 수 있습니다. &lt;code&gt;fork()&lt;/code&gt;는 새 child process를 만들라고 커널에 요청합니다. Linux 내부에서는 현대적으로 &lt;code&gt;clone&lt;/code&gt; 계열 구현으로 이어지는 경우가 많습니다. &lt;code&gt;waitpid()&lt;/code&gt;는 child 상태 변화를 기다리고 회수하는 요청입니다. &lt;code&gt;kill()&lt;/code&gt;은 특정 pid나 process group에 signal을 보내달라는 요청입니다. &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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;sigprocmask()&lt;/code&gt;는 현재 프로세스가 어떤 signal을 잠깐 막을지 커널에 반영하는 요청입니다. &lt;code&gt;Signal()&lt;/code&gt; 같은 handout wrapper도 결국에는 &lt;code&gt;signal&lt;/code&gt;이나 &lt;code&gt;sigaction&lt;/code&gt; 계열을 통해 커널에 handler 등록 정보를 알려주는 흐름으로 이어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-theory-note&quot; style=&quot;max-width: 650px; margin: 1.6rem auto; padding: 14px 0 14px 18px; border-left: 3px solid #111827; background: #f8fafc; color: #1f2937; font-family: 'Noto Serif KR', serif;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.75;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;정리.&lt;/b&gt; 유저 프로세스는 힘이 센 존재가 아닙니다. 커널이 허용한 syscall 입구와 CPU의 모드 전환 규칙을 통해서만 process 생성, signal 전달, wait, signal mask 변경 같은 일을 요청할 수 있습니다. Shell Lab은 이 syscall들을 조합해서 작은 shell처럼 보이게 만드는 과제였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;line-height: 1.8; overflow-wrap: anywhere;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://csapp.cs.cmu.edu/3e/shlab.pdf&quot;&gt;CMU CS:APP Shell Lab writeup&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://www.cs.cornell.edu/courses/cs3410/2024fa/assignments/tsh/instructions.html&quot;&gt;Cornell CS3410 Tiny Shell instructions&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>황현석 어록</category>
      <category>Carnegie Mellon</category>
      <category>csapp</category>
      <category>execve</category>
      <category>Fork</category>
      <category>Process</category>
      <category>Shell Lab</category>
      <category>Signal</category>
      <category>System Programming</category>
      <category>unix shell</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/58</guid>
      <comments>https://hy3ons.tistory.com/58#entry58comment</comments>
      <pubDate>Mon, 22 Jun 2026 04:15:52 +0900</pubDate>
    </item>
    <item>
      <title>Judge Blog 구축 일지 02 - JavaScript의 메모리 문제</title>
      <link>https://hy3ons.tistory.com/56</link>
      <description>&lt;div class=&quot;tt_article_useless_p_margin contents_style&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회고.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이전 글에서는 HWJ에서 C++ WASM runner를 붙이며 겪은 I/O 병목, WASI 전환, 표준 checker fast path 같은 이야기를 정리했습니다. 그때의 결론은 꽤 단순했습니다. 브라우저 안에서 채점을 하려면 모든 것을 WASM으로 밀어 넣는 것이 정답이 아니라, &lt;b&gt;WASM이 필요한 곳과 JavaScript에서 바로 끝내야 하는 곳을 구분해야 한다&lt;/b&gt;는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번에는 JavaScript 제출에 대한 고민입니다. 처음부터 지금 구조를 정답으로 잡은 것은 아니었습니다. 출발점은 훨씬 단순했습니다. 브라우저 native V8 JIT로 사용자 JS를 실행하면 빠릅니다. 그런데 온라인 저지처럼 &amp;ldquo;이 제출이 메모리 제한을 넘었는가?&amp;rdquo;를 판정하려고 하니, 갑자기 문제가 흐릿해졌습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 한동안 다른 아키텍처를 선택하려고 했습니다. JS를 격리된 sandbox에서 실행할 수 있는지, 경량 interpreter를 WASM으로 올리면 메모리를 단단히 묶을 수 있는지, Porffor나 AssemblyScript 같은 다른 JS 컴파일러 계열은 쓸 만한지 찾아봤습니다. 결론부터 말하면 지금의 선택은 최고라기보다는 차선에 가까운 것 같습니다. 다만 HWJ가 중요하게 보는 UX와 서버 비용을 생각하면, 이쪽이 그나마 현실적으로 보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;출발점: V8 JIT는 빠르지만 메모리가 애매하다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;브라우저에서 JavaScript를 가장 빠르게 실행하는 방법은 사실 이미 브라우저 안에 있습니다. V8입니다. 사용자가 작성한 코드를 worker 안에서 &lt;code&gt;new Function()&lt;/code&gt;으로 감싸고, &lt;code&gt;fs.readFileSync(0, &quot;utf8&quot;)&lt;/code&gt;, &lt;code&gt;console.log&lt;/code&gt;, &lt;code&gt;process.exit&lt;/code&gt; 같은 최소 Node.js shim만 제공하면 PS 스타일 코드는 꽤 자연스럽게 돌아갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;stdin 준비&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;generator가 만든 입력을 worker 안의 JS runner에 문자열 또는 byte buffer로 전달합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;Native V8 JIT 실행&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;new Function()&lt;/code&gt;으로 감싼 사용자 JS를 브라우저 V8 위에서 실행해 빠른 정답성 피드백을 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;stdout hash 계산&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;출력 문자열을 canonical byte stream으로 보고 case별 output hash를 계산합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;case index에 저장&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;worker 완료 순서가 섞여도 안정적으로 fold할 수 있게 &lt;code&gt;caseHashes[caseIndex]&lt;/code&gt;에 꽂아둡니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정답성만 보면 이 방식은 꽤 좋았습니다. 간단한 &lt;code&gt;N log N&lt;/code&gt; 성격의 테스트 문제에서 native V8 JIT 경로는 약 &lt;b&gt;30ms&lt;/b&gt; 정도가 나왔습니다. 사용자가 제출 버튼을 눌렀을 때 바로 로그가 움직이고, 오답이면 빠르게 떨어집니다. 온라인 저지에서 이 감각은 생각보다 큽니다. 틀린 코드를 오래 기다리는 것은 이미 충분히 어려운 알고리즘 디버깅을 더 질척하게 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제는 메모리였습니다. 사용자가 큰 배열을 만들거나, 문자열을 많이 이어 붙이거나, &lt;code&gt;ArrayBuffer&lt;/code&gt; 비슷한 큰 데이터를 잡기 시작하면 브라우저의 JS heap, external memory, native allocation, GC pressure가 섞입니다. judge가 알고 싶은 질문은 단순합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: center; margin: 2.4rem auto 2rem; line-height: 1.65;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #333333; background: linear-gradient(to top, rgba(148, 163, 184, 0.35) 43%, transparent 43%); padding: 0 0.18em;&quot;&gt;이 제출은 문제의 memory limit을 넘겼는가?&lt;/span&gt;&lt;/i&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그런데 native V8 path에서 제출 하나의 peak memory를 온라인 저지처럼 깔끔하게 뽑는 것은 쉽지 않았습니다. V8은 JS heap과 native 영역을 나눠 관리하고, GC 타이밍도 런타임이 결정합니다. 브라우저 worker에서는 서버 Node처럼 RSS, external, heapUsed를 제출 단위로 안정적으로 회계 처리하기도 어렵습니다. 시간은 빨리 보이는데, 메모리 판정은 묘하게 손에 안 잡히는 상태였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 JS를 격리하면 되지 않을까&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 다음 생각은 sandbox였습니다. &amp;ldquo;사용자 JS를 완전히 격리된 환경에서 실행하고, 그 환경 자체에 메모리 제한을 걸면 되지 않을까?&amp;rdquo;라는 방향이었습니다. worker를 따로 띄우고, 전역 객체를 제한하고, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, &lt;code&gt;indexedDB&lt;/code&gt; 같은 API를 막고, Node.js shim만 노출하면 꽤 닫힌 세계처럼 보였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 이것은 보안과 메모리 격리 관점에서 생각보다 애매했습니다. 브라우저가 제공하는 worker는 OS process나 container 같은 judge sandbox가 아니었습니다. worker를 죽일 수는 있지만, &amp;ldquo;이 제출에게만 V8 heap을 128MB로 제한한다&amp;rdquo; 같은 식의 강한 격리를 웹 API로 직접 얻기는 어려웠습니다. 전역 API를 가리는 것은 편의 기능을 막는 일에 가까웠고, 런타임 내부 메모리 accounting을 해결해주지는 못했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_sandbox_problem&quot; class=&quot;text&quot; style=&quot;white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box;&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;worker sandbox로 할 수 있는 것:
  API 노출 제한
  timeout 후 worker terminate
  stdout/stderr 수집

worker sandbox로 어려운 것:
  제출 단위의 정확한 V8 heap limit
  OS/container 수준의 강한 격리
  native/external allocation까지 포함한 memory peak 판정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제로 native JS runner에는 &lt;code&gt;restrictNativeJsGlobals()&lt;/code&gt; 같은 방어 코드를 넣었습니다. &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt;, &lt;code&gt;WebSocket&lt;/code&gt;, &lt;code&gt;EventSource&lt;/code&gt;, &lt;code&gt;indexedDB&lt;/code&gt;, &lt;code&gt;caches&lt;/code&gt;, &lt;code&gt;importScripts&lt;/code&gt; 같은 전역 API를 막고, 실행이 끝나면 원래 descriptor로 복구합니다. 그런데 이건 어디까지나 API masking입니다. 브라우저 전역 중에는 configurable이 아닌 것도 있고, 실패해도 worker 안의 문제로만 격리할 수 있을 뿐입니다. 즉, 이 시도는 &amp;ldquo;유저 코드가 네트워크를 만지지 못하게 하자&amp;rdquo;에는 도움이 됐지만, &amp;ldquo;이 제출의 메모리를 judge처럼 정확히 제한하자&amp;rdquo;에는 답이 아니었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;즉, V8 native JIT는 빠르지만 메모리 판정이 어렵고, worker sandbox는 실행을 분리해주는 느낌은 있지만 judge가 원하는 강한 memory limit이 되지는 않았습니다. 그래서 생각이 한 번 더 꺾였습니다. &amp;ldquo;V8을 억지로 통제하려 하지 말고, 통제 가능한 런타임 위에서 JS를 실행하면 되지 않을까?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WASM 위에 경량 인터프리터를 올려보자는 생각&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 다음 후보가 QuickJS, Javy 같은 JS-on-WASM 계열이었습니다. JS 엔진 자체를 WASM으로 빌드해서 브라우저 worker 안에 올리는 방식입니다. 그러면 사용자 코드는 native V8에서 도는 것이 아니라, WASM module 안의 JS interpreter에서 실행됩니다. 속도는 손해를 보더라도 메모리 제한은 훨씬 다루기 쉬워질 것 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;WASM runtime 제한&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;runtime.setMemoryLimit()&lt;/code&gt;으로 문제의 memory limit을 QuickJS runtime에 적용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;stdout discard&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;정답성은 이미 fast check에서 봤기 때문에 출력 문자열을 누적하지 않고 byte 수만 셉니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;memoryBytes 측정&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;computeMemoryUsage()&lt;/code&gt;로 QuickJS runtime이 보고한 byte sample을 읽습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;MLE 판정&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;측정값이 문제 제한을 넘으면 matching hash가 있어도 최종 verdict를 MLE로 남깁니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 삽질은 &amp;ldquo;QuickJS를 직접 커스텀 빌드하다가 망했다&amp;rdquo; 쪽은 아니었습니다. 지금 구현은 &lt;code&gt;quickjs-emscripten&lt;/code&gt; 런타임을 &lt;code&gt;public/runtimes/quickjs/index.global.js&lt;/code&gt;로 복사하고, worker에서 &lt;code&gt;importScripts()&lt;/code&gt;로 로드합니다. 모듈 promise는 캐시하지만, 제출 실행마다 &lt;code&gt;newRuntime()&lt;/code&gt;과 &lt;code&gt;newContext()&lt;/code&gt;를 새로 만듭니다. 처음에는 재사용하면 빨라질 것 같았습니다. 그런데 제출 사이의 전역 상태와 handle, memory sample이 섞이면 memory probe의 의미가 무너질 것 같았습니다. 그래서 런타임 로드는 캐시하고, 실행 context는 매번 버리는 쪽으로 타협했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;실제 memory probe에서는 &lt;code&gt;runtime.setMemoryLimit(problem.memoryLimitMb * 1024 * 1024)&lt;/code&gt;를 걸고, stack도 1MB로 제한했습니다. 실행 후에는 &lt;code&gt;runtime.computeMemoryUsage()&lt;/code&gt;를 읽어 byte 단위 memory sample을 뽑습니다. 그리고 stdout은 정답 검증용으로 다시 보지 않기로 했습니다. 이미 V8 fast check와 서버 precheck에서 answer hash를 확인했기 때문에, QuickJS phase에서는 출력 문자열을 누적하지 않고 &lt;code&gt;TextEncoder&lt;/code&gt;로 byte 수만 셉니다. &lt;code&gt;discardOutput&lt;/code&gt;이 켜져 있으면 &lt;code&gt;stdoutStr += addition&lt;/code&gt;을 하지 않는 식입니다. 이걸 안 하면 메모리 측정이 유저 알고리즘 메모리가 아니라 &amp;ldquo;출력을 저장한 비용&amp;rdquo;에 오염될 것 같았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 이게 꽤 괜찮을 줄 알았습니다. PS 코드는 일반적인 웹 애플리케이션 JS와 다르게 동기적이고, 외부 의존성이 거의 없고, 입력을 읽고 계산한 뒤 출력을 만드는 CPU bound 코드입니다. 경량 JS interpreter를 WASM으로 구워서 브라우저에 올리면, 적당한 성능과 tight한 메모리 관리를 동시에 얻을 수 있지 않을까 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그런데 숫자가 생각보다 냉정했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_js_runtime_measurement&quot; class=&quot;text&quot; style=&quot;white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box;&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;N log N 테스트에서 본 대략적인 차이:
  native V8 JIT        약 30ms
  fast JS/WASM runtime 약 300ms

의미:
  WASM sandbox는 메모리 관리에는 좋아 보이지만
  사용자가 느끼는 1차 피드백 경로로는 너무 무거울 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;30ms와 300ms는 단순한 숫자 차이가 아니었습니다. 사용자가 &amp;ldquo;한 번 제출해볼까?&amp;rdquo; 하고 누르는 순간에 10배 차이가 나면, 이건 구현 취향이 아니라 UX 문제라고 봤습니다. 특히 HWJ는 블로그 글을 읽다가 바로 실행하고 고쳐보는 경험을 목표로 하고 있었습니다. 그 자리에서 매번 느린 interpreter를 기다리게 하면 글의 흐름이 끊길 것 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 다른 JS 컴파일러도 찾아봤다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 자연스럽게 다른 선택지를 봤습니다. &amp;ldquo;JS interpreter를 WASM 위에서 돌리는 게 느리다면, 아예 JS를 WASM으로 컴파일하면 안 되나?&amp;rdquo; C++은 WASI WASM으로 굽고, Java도 TeaVM 같은 선택지가 있습니다. 그러면 JS도 뭔가 있을 것 같았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;문제는 JavaScript가 정적 컴파일과 잘 맞지 않는 언어라는 점입니다. JavaScript에서는 같은 변수가 숫자였다가 문자열이 되고, 객체가 되고, 배열이 되는 것이 자연스럽습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_js_dynamic_type&quot; class=&quot;javascript&quot; style=&quot;white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box;&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let value = 1;
value = &quot;hello&quot;;
value = [1, 2, 3];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;V8은 이런 동적 성질을 실행 중에 관찰하고, hidden class, inline cache, speculative optimization 같은 런타임 기법으로 빠르게 만듭니다. JS의 빠름은 &amp;ldquo;처음부터 C++처럼 확정적으로 컴파일된다&amp;rdquo;보다 &lt;b&gt;실행 중 관찰한 형태를 바탕으로 낙관적으로 최적화한다&lt;/b&gt;에 가깝다고 봤습니다. 그래서 JS를 WASM으로 굽는다는 말은 생각보다 까다로워 보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 6px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 18px 0; border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; gap: 10px;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; align-items: baseline; gap: 14px;&quot;&gt;
&lt;h5 style=&quot;margin: 0; font-size: 16px; font-weight: 800; letter-spacing: 0.02em; color: #111827;&quot; data-ke-size=&quot;size20&quot;&gt;01. AssemblyScript&lt;/h5&gt;
&lt;span style=&quot;flex-shrink: 0; font-size: 12px; font-weight: 800; border: 1px solid #111827; padding: 3px 7px; color: #111827; text-transform: uppercase; letter-spacing: 0.03em;&quot;&gt;Static Subset&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; color: #374151; line-height: 1.75; list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JavaScript/TypeScript와 비슷한 문법을 공유하지만, 정적 타입을 강하게 요구합니다.&lt;/li&gt;
&lt;li&gt;기존 JavaScript 생태계를 그대로 수용하기보다는, JS처럼 생긴 별도 언어에 가까워 보였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 18px 0; border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; gap: 10px;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; align-items: baseline; gap: 14px;&quot;&gt;
&lt;h5 style=&quot;margin: 0; font-size: 16px; font-weight: 800; letter-spacing: 0.02em; color: #111827;&quot; data-ke-size=&quot;size20&quot;&gt;02. Porffor&lt;/h5&gt;
&lt;span style=&quot;flex-shrink: 0; font-size: 12px; font-weight: 800; border: 1px solid #111827; padding: 3px 7px; color: #111827; text-transform: uppercase; letter-spacing: 0.03em;&quot;&gt;AOT Compiler&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; color: #374151; line-height: 1.75; list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS/TS 소스코드를 WASM 또는 C 쪽으로 정적 컴파일하려는 흥미로운 시도입니다.&lt;/li&gt;
&lt;li&gt;PS 도메인에는 좋아 보였지만, 표준 JS 호환성과 edge behavior를 judge 기본 런타임으로 믿기에는 아직 이르다고 봤습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 18px 0; display: flex; flex-direction: column; gap: 10px;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; align-items: baseline; gap: 14px;&quot;&gt;
&lt;h5 style=&quot;margin: 0; font-size: 16px; font-weight: 800; letter-spacing: 0.02em; color: #111827;&quot; data-ke-size=&quot;size20&quot;&gt;03. Javy / QuickJS WASM&lt;/h5&gt;
&lt;span style=&quot;flex-shrink: 0; font-size: 12px; font-weight: 800; border: 1px solid #111827; padding: 3px 7px; color: #111827; text-transform: uppercase; letter-spacing: 0.03em;&quot;&gt;WASM Interpreter&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; color: #374151; line-height: 1.75; list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 엔진 자체를 WASM module로 들고 와서 브라우저 안에서 기존 JS 코드를 실행합니다.&lt;/li&gt;
&lt;li&gt;표준 JS 경험을 가장 덜 깨지만, native V8보다 느리고 runtime overhead가 큽니다.&lt;/li&gt;
&lt;/ul&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PS 도메인만 보면 AssemblyScript나 Porffor류가 꽤 좋아 보였습니다. 입력, 반복문, 배열, 정렬, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt; 정도만 잘 되면 많은 문제가 풀릴 것 같았습니다. 하지만 사용자가 &amp;ldquo;JavaScript 제출&amp;rdquo;이라고 기대하는 것은 Node.js 스타일의 &lt;code&gt;fs.readFileSync(0, &quot;utf8&quot;)&lt;/code&gt;, &lt;code&gt;Array.prototype.sort()&lt;/code&gt;, 문자열 메서드, BigInt 같은 표준 JS 경험이었습니다. 그 기대를 버리고 &amp;ldquo;우리 JS는 사실 AssemblyScript입니다&amp;rdquo;라고 말하는 순간, 이건 JS 지원이 아니라 새 언어를 하나 추가하는 일에 가까워질 것 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 이중 구조가 좋아 보였다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이쯤 오니 각 선택지의 장단점이 꽤 또렷해졌습니다. V8 native JIT는 빠르지만 메모리 판정이 어렵고, WASM interpreter는 메모리 판정은 쉬워 보이지만 느렸습니다. AssemblyScript나 Porffor는 흥미롭긴 했지만 사용자가 기대하는 &amp;ldquo;일반 JS 제출&amp;rdquo;과는 거리가 있어 보였습니다. 그러면 한쪽만 고르는 대신, 각자의 장점을 나눠 쓰는 구조가 떠올랐습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;Source 저장&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;제출 코드를 먼저 서버의 &lt;code&gt;source_codes&lt;/code&gt;에 저장하고, 이후 precheck와 최종 기록을 같은 source에 묶습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;Fast JIT Check&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;브라우저 native V8 JIT로 정답성과 시간 통계를 빠르게 확인하고 aggregate answer hash를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;Server Precheck&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;서버가 공개 문제의 &lt;code&gt;answer_hash_manifest&lt;/code&gt;와 클라이언트 hash를 비교해 precheck attempt를 남깁니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;WASM Memory Probe&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;precheck를 통과한 제출만 QuickJS WASM sandbox에서 다시 실행해 memory limit을 확인합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;5&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;최종 기록&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;hash와 memory probe가 모두 통과한 경우에만 AC로 저장하고, MLE/RTE는 그대로 보존합니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식은 처음 보기에는 꽤 좋아 보였습니다. 오답 대부분은 정답성 단계에서 빠르게 떨어질 것이고, 정답 hash까지 맞은 제출만 느린 memory probe를 태우면 될 것 같았습니다. 서버는 여전히 테스트를 직접 실행하지 않고, 공개 문제의 &lt;code&gt;answer_hash_manifest&lt;/code&gt;와 클라이언트가 만든 aggregate hash를 비교해 precheck 경계만 잡으면 됐습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;구현하면서 조심한 부분은 worker 완료 순서였습니다. 브라우저 worker pool은 동적 큐로 케이스를 가져가므로, case 7이 case 3보다 먼저 끝날 수 있습니다. 그래서 fast check에서는 완료되는 순서대로 hash를 접지 않고, &lt;code&gt;caseHashes[caseIndex]&lt;/code&gt;에 canonical output hash를 꽂아둔 뒤 마지막에 case index 순서로 &lt;code&gt;foldCaseOutputHashes()&lt;/code&gt;를 호출합니다. 이 디테일을 빼먹으면 같은 코드도 worker scheduling에 따라 aggregate hash가 달라질 수 있습니다. 이건 꽤 사소해 보이지만, 서버 precheck가 붙는 순간 사소하지 않습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 곧 다른 문제가 보였습니다. 두 번의 전체 채점은 사용자로 하여금 피로감을 느끼게 만들 수 있을 것 같았습니다. 1차에서 &amp;ldquo;오, 맞는 것 같은데?&amp;rdquo;라고 느꼈는데 2차가 길게 따라오면, 시스템이 무겁고 답답하게 느껴질 것 같았습니다. 특히 블로그 안에서 학습 흐름을 이어가야 하는 HWJ에서는 제출 하나가 긴 의식처럼 느껴지면 곤란할 것 같았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 관점을 조금 바꿨습니다. 모든 제출을 두 번 엄격하게 채점하는 것이 아니라, &lt;b&gt;빠른 JIT로 먼저 정답성을 확인하고, 정답성이 확인된 제출에 대해서만 2차 메모리 검증을 수행&lt;/b&gt;하는 방식입니다. 메모리 문제는 물론 별개의 문제지만, 일반적인 알고리즘 학습 흐름에서는 시간 복잡도와 정답성이 해결되면 메모리도 같이 해결되는 경우가 많습니다. 항상 그렇다는 뜻은 아닙니다. 다만 모든 오답과 실험 제출에 느린 memory probe를 태울 만큼 흔한 실패 원인은 아니라는 뜻입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시간과 메모리 기준은 새롭게 세우면 된다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 한 가지 생각이 정리됐습니다. 백준 같은 온라인 저지에서도 제출 기록에 시간과 메모리를 보여줍니다. 그런데 그 숫자는 코드의 절대적인 본질이라기보다는, 여러 테스트케이스 중에서 가장 오래 걸렸거나 가장 많이 쓴 케이스를 judge 환경 기준으로 요약한 값에 가까워 보였습니다. 즉, 시간과 메모리는 언제나 &amp;ldquo;어떤 환경에서, 어떤 기준으로, 어떤 케이스들을 돌렸는가&amp;rdquo;에 묶여 있다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 HWJ도 기준을 새롭게 세울 수 있겠다고 생각했습니다. V8 fast path에서 측정한 시간은 &amp;ldquo;사용자가 실제 브라우저에서 빠르게 검증받은 시간&amp;rdquo;으로 보면 되고, QuickJS WASM memory probe에서 나온 메모리는 &amp;ldquo;통제된 WASM sandbox 안에서 같은 제출이 요구한 메모리&amp;rdquo;로 보면 될 것 같았습니다. 둘이 완전히 같은 runtime의 숫자는 아니지만, 기준을 명확히 설명하고 일관되게 적용하면 기록으로 쓸 수 있겠다고 봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;timeMs&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;native V8 JIT fast check에서 측정한 시간입니다. 사용자가 체감하는 1차 실행 시간에 가장 가깝습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;memoryKb&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;QuickJS WASM memory probe에서 얻은 peak memory를 KB 정수로 반올림해 기록합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;final verdict&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;server precheck와 memory probe를 모두 통과해야 AC입니다. 둘 중 하나가 실패하면 그 verdict를 그대로 남깁니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 기록 단위도 의도적으로 단순하게 만들었습니다. worker가 주는 memory sample은 byte지만, 제출 기록과 UI에는 KB 정수로만 남깁니다. 실제 테스트도 &lt;code&gt;1536 bytes -&amp;gt; 2KB&lt;/code&gt;, &lt;code&gt;91.6KB -&amp;gt; 92KB&lt;/code&gt;처럼 소수 없이 반올림하는 정책을 박았습니다. 이건 예쁘게 보이려고 한 것이 아니라, 브라우저 기반 runner의 memory sample을 지나치게 정밀한 척하지 않기 위한 선택이었습니다. KB 아래 단위까지 보여주면 숫자는 더 과학적으로 보이지만, 실제 신뢰도는 올라가지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 선택한 쪽: UX를 우선한 차선&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 현재 HWJ는 &amp;ldquo;가장 이론적으로 완벽한 JS judge&amp;rdquo;보다 &amp;ldquo;사용자가 덜 답답한 judge&amp;rdquo; 쪽을 우선했습니다. 이 말은 정확히 해야 할 것 같습니다. 지금의 해결책이 최고라는 뜻은 아닙니다. 오히려 차선에 가까웠습니다. 다만 HWJ의 목적을 생각하면 꽤 괜찮은 차선이라고 봤습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;HWJ는 대회용 보안 judge가 아닙니다. 지금 단계에서는 사용자가 악의적으로 런타임을 깨는 것을 막는 것보다, 블로그 글을 읽다가 바로 코드를 실행하고, 빠르게 실패하고, 다시 고치는 흐름이 더 중요하다고 봤습니다. 그러면 판단 기준도 달라질 수밖에 없었습니다. 모든 edge case를 완벽하게 잡는 구조보다, 대부분의 학습 흐름에서 빠르고 일관된 피드백을 주는 구조가 더 맞아 보였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 이 구조를 이렇게 해석하고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_current_tradeoff&quot; class=&quot;text&quot; style=&quot;white-space: pre-wrap; overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box;&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;최고의 정답:
  서버 수준의 완전한 격리
  언어별 동일한 memory accounting
  강한 보안
  대신 서버 비용과 대기 시간 증가

HWJ의 현재 차선:
  브라우저 실행
  빠른 JIT 피드백
  필요한 범위의 서버 precheck
  가능한 범위의 WASM memory probe
  대신 완벽한 contest judge와는 다른 신뢰 모델&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 차선은 포기라기보다 방향을 맞춘 것에 가까웠습니다. 서비스가 무엇을 가장 중요하게 보는지에 맞춰 구조를 고르는 문제라고 생각했습니다. HWJ는 서버 비용을 줄이고, 글 안에서 바로 실행되는 경험을 만들고, 사용자가 코드를 만지며 배우게 하는 쪽이라고 봤습니다. 그렇다면 &amp;ldquo;완벽한 메모리 검증&amp;rdquo;보다 &amp;ldquo;빠른 피드백과 적당히 통제된 검증&amp;rdquo;이 더 맞을 수 있겠다고 봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;정답성은 hash로, 기록은 precheck로 묶는다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다고 클라이언트 결과를 그대로 믿는 것은 아닙니다. 공개 문제에는 출제 검증 단계에서 만든 &lt;code&gt;answer_hash_manifest&lt;/code&gt;가 있습니다. 브라우저는 generator를 돌리고 제출 코드를 실행한 뒤 case별 canonical output hash를 만듭니다. Worker 완료 순서는 섞일 수 있으므로 &lt;code&gt;caseHashes[caseIndex]&lt;/code&gt;에 저장한 뒤, case index 순서대로 aggregate hash를 접습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;case별 output hash&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;각 테스트케이스의 canonical output을 hash로 바꾸고, 원래 case index에 맞춰 저장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;index 순서로 fold&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;worker 완료 순서가 아니라 case index 순서로 접어 aggregate answer hash를 만듭니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;server manifest 비교&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;서버가 저장해둔 &lt;code&gt;answer_hash_manifest&lt;/code&gt;와 aggregate hash가 같은지 확인합니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서버는 이 aggregate hash가 manifest와 맞는지 확인합니다. 그리고 JavaScript 제출의 경우, 최종 AC 기록에는 &lt;code&gt;precheckId&lt;/code&gt;가 필요합니다. 즉, 클라이언트가 &amp;ldquo;나 AC야&amp;rdquo;라고 말한다고 바로 기록하지 않습니다. 먼저 서버가 &amp;ldquo;이 sourceCodeId로 만든 fast check hash가 이 문제의 manifest와 맞다&amp;rdquo;는 attempt를 남겨야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 경계는 중요합니다. HWJ가 서버에서 테스트를 실행하지 않더라도, 최소한 제출 기록은 서버가 알고 있는 문제 manifest와 source attempt에 묶여야 합니다. 이게 없으면 브라우저 기반 judge는 너무 쉽게 &amp;ldquo;좋은 말만 하는 클라이언트&amp;rdquo;가 됩니다. 음, 클라이언트가 착하면 좋겠지만 시스템 설계가 착함에 기대면 나중에 대체로 머리가 아픕니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시간은 V8, 메모리는 QuickJS가 맡는다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 점은 QuickJS가 V8과 다른 런타임이라서 못 믿겠다는 이야기가 아니라는 것입니다. 오히려 반대에 가깝습니다. V8은 JavaScript를 빠르게 실행하는 데 제일 좋아 보였고, QuickJS WASM은 메모리 제한을 걸고 memory sample을 읽는 데 더 좋아 보였습니다. 그러면 각자 잘하는 역할을 나눠 맡기면 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;C++ WASI 쪽은 하나의 런타임이 두 역할을 어느 정도 같이 할 수 있었습니다. 같은 WASM 실행에서 시간도 보고, memory도 보고, stdout도 잡으면 됩니다. 그런데 JavaScript는 달랐습니다. V8 native path는 시간과 정답성 확인에는 빨랐지만, 제출 단위 memory limit을 정밀하게 걸기 어려웠습니다. 반대로 QuickJS WASM은 memory limit과 측정은 훨씬 통제하기 좋았지만, 시간 측정용으로 쓰기에는 너무 느렸습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 QuickJS에서의 시간은 알고리즘 시간으로 보지 않기로 했습니다. 시간은 이미 V8 JIT fast check에서 봤습니다. QuickJS phase에서 보고 싶은 것은 &amp;ldquo;이 코드가 제한된 memory sandbox 안에서도 버티는가&amp;rdquo;였습니다. 만약 QuickJS가 느려서 오래 걸린다면 그것은 사용자 알고리즘의 시간초과라기보다, 2차 probe가 실패한 상태로 봐야 한다고 생각했습니다. 역할이 다른 것입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;출력은 materialize하지 않고 버립니다. stdout을 계속 문자열로 쌓으면 메모리 측정 자체가 출력 저장 비용에 오염되기 때문입니다. memory sample은 byte 단위로 받고, 제출 기록에는 KB 정수로 반올림해 저장하기로 했습니다. 이 값은 &amp;ldquo;V8에서 실제로 이렇게 썼다&amp;rdquo;가 아니라, &amp;ldquo;메모리 측정을 위해 선택한 QuickJS sandbox 기준에서 이 제출이 요구한 peak&amp;rdquo;입니다. 이쪽이 더 정직한 설명이라고 봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;출력 누적 제거&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;discardOutput&lt;/code&gt;으로 stdout 문자열을 버리고 &lt;code&gt;outputBytes&lt;/code&gt;만 증가시킵니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;runtime memory 측정&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;QuickJS runtime의 &lt;code&gt;memoryBytes&lt;/code&gt;를 읽고, 여러 케이스 중 가장 큰 값을 peak로 잡습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;KB 정수 기록&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;byte sample은 &lt;code&gt;Math.round(bytes / 1024)&lt;/code&gt;로 반올림해 제출 기록에 저장합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 2px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 4px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;초과 시 MLE&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 14px; color: #64748b; line-height: 1.6;&quot; data-ke-size=&quot;size16&quot;&gt;문제 memory limit을 넘으면 즉시 &lt;code&gt;SOLUTION_LIMIT_FAILED&lt;/code&gt;로 보고 최종 MLE를 유지합니다.&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;worker 쪽 판정도 단순하게 유지했습니다. &lt;code&gt;solutionResult.limitKind === &quot;memory&quot;&lt;/code&gt;이면 바로 memory failure로 보고, 아니더라도 &lt;code&gt;solutionResult.memoryBytes / 1024 / 1024 &amp;gt; memoryLimitMb&lt;/code&gt;이면 &lt;code&gt;SOLUTION_LIMIT_FAILED&lt;/code&gt;로 바꿉니다. 상위 JS hybrid judge는 각 케이스의 &lt;code&gt;memoryBytes&lt;/code&gt; 중 최댓값을 &lt;code&gt;memoryPeakBytes&lt;/code&gt;로 잡고, 첫 실패가 나오면 &lt;code&gt;stopRequested&lt;/code&gt;를 세워 더 이상 의미 없는 케이스를 밀어붙이지 않습니다. 즉, 이 값은 &amp;ldquo;V8이 실제로 이만큼 썼다&amp;rdquo;가 아니라 &amp;ldquo;memory probe runtime에서 이 제출이 제한을 넘겼고, 그때까지 본 peak가 이 정도였다&amp;rdquo;에 가깝다고 보는 게 맞을 것 같습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 정책은 matching hash가 있어도 memory probe의 verdict를 덮지 않는다는 것이었습니다. fast check가 정답성을 통과했고 서버 precheck도 통과했더라도, 2차 memory probe에서 MLE가 나오면 최종 기록은 MLE로 남깁니다. 정답은 맞지만 제한을 넘은 코드로 보는 게 맞다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 정책은 테스트로도 고정했습니다. &lt;code&gt;resolvePrecheckedJavaScriptJudgeResult(&quot;MLE&quot;, 2, 3, manifest, matchingHash)&lt;/code&gt;는 &lt;code&gt;AC&lt;/code&gt;가 아니라 &lt;code&gt;{ verdict: &quot;MLE&quot;, passedCases: 2, totalCases: 3 }&lt;/code&gt;를 반환해야 합니다. 반대로 fast-check hash가 없으면 precheck path에서 &lt;code&gt;AC&lt;/code&gt;를 기록하지 못하게 했습니다. 이 지점이 없으면 &amp;ldquo;1차에서 hash가 맞았으니 AC&amp;rdquo;라는 편한 결론으로 미끄러지기 쉽습니다. 하지만 그 순간 2차 memory probe는 장식이 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조가 최고가 아닌 이유&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;첫째, 한 런타임이 모든 역할을 해주지는 않는다는 점입니다. C++ WASI에서는 하나의 실행 경로가 시간과 메모리를 같이 담당할 수 있었지만, JS에서는 그렇게 하면 한쪽이 망가졌습니다. V8으로 메모리를 잡으려 하면 회계가 흐릿해지고, QuickJS로 시간을 보려 하면 UX가 느려졌습니다. 결국 이 구조는 &amp;ldquo;하나의 절대 런타임&amp;rdquo;이 아니라 &amp;ldquo;역할이 나뉜 두 런타임&amp;rdquo;을 받아들이는 방식입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;둘째, UX 상태가 복잡해집니다. 사용자는 fast check를 통과했는데, 그 뒤 memory check에서 실패할 수 있습니다. 그래서 화면에서는 상태를 아예 분리해서 보여주는 게 맞다고 봤습니다. 1차는 &amp;ldquo;정답성/시간 precheck 통과&amp;rdquo;, 2차는 &amp;ldquo;memory probe 진행 중&amp;rdquo;, 마지막에만 &amp;ldquo;최종 AC&amp;rdquo;라고 말해야 합니다. &amp;ldquo;정답입니다&amp;rdquo;라고 먼저 말해놓고 나중에 &amp;ldquo;아니었습니다&amp;rdquo;라고 하면 기분이 썩 좋지 않을 것 같았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;셋째, 비용은 서버에서 브라우저로 이동했을 뿐 사라진 것이 아닙니다. HWJ는 서버 비용을 줄이지만, 사용자의 기기와 브라우저가 일을 합니다. 이것은 프로젝트의 철학이기도 하지만, 동시에 예의가 필요한 지점입니다. worker 수를 무식하게 늘리지 않고, 하드 킬 타이머와 진행 로그를 잘 보여주고, 멈춘 느낌을 주지 않아야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 현재 구조를 최고라고 말하고 싶지는 않습니다. 대신 지금 HWJ가 풀고 있는 문제에는 꽤 잘 맞는 차선이라고 보고 있습니다. 빠른 피드백, 낮은 서버 비용, 브라우저 로컬 실행, 그리고 최소한의 서버 검증. 이 네 가지를 동시에 만족하려면 어느 정도의 찝찝함은 남았습니다. 시스템 설계는 가끔 이런 식인 것 같습니다. 완벽한 정답 대신, 덜 나쁜 타협을 예쁘게 포장하지 않고 정확히 이름 붙이는 일.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JavaScript 제출에 대해 처음 꿈꿨던 것은 조금 더 매끈한 구조였습니다. V8 JIT로 시간은 빠르게 보고, QuickJS WASM으로 메모리는 엄격하게 보고, 서버는 hash precheck만 하는 구조. 실제로 그 방향은 아직도 좋아 보입니다. 각자의 장점이 분명했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;다만 구현을 진행하면서 느낀 것은, judge architecture에서 기술적으로 멋진 구조와 사용자가 편하게 느끼는 구조가 항상 같지는 않다는 점이었습니다. 특히 HWJ처럼 블로그와 학습 경험 안에 judge를 넣으려는 프로젝트에서는, 엄격성 하나만으로 설계를 고르면 전체 경험이 무거워질 수 있겠다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 지금은 UX를 중심에 둔 차선을 선택했습니다. 사용자는 빠르게 피드백을 받고, 서버는 최소한의 precheck와 기록만 맡고, 메모리 검증은 가능한 범위에서 WASM sandbox로 보강합니다. 완벽한 contest judge는 아니지만, HWJ가 만들고 싶은 &amp;ldquo;글 안에서 바로 실행되는 알고리즘 학습 경험&amp;rdquo;에는 이 방향이 더 맞는 것 같았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;어쩌면 지금의 결론은 기술적으로 대단히 멋진 무언가를 한 것은 아닙니다. 팩트는, 타협을 했을 뿐입니다. 다만 그 타협은 아무렇게나 한 것이 아니라, V8과 QuickJS가 각각 잘하는 일을 나눠 맡기는 쪽으로 정리한 타협이었습니다.&lt;br /&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;font-family: 'Noto Serif KR';&quot;&gt;그리고 이 지점에서 조금 더 긴 생각도 생겼습니다. 지금의 구조는 JS를 그대로 지원하려고 하니 생긴 타협입니다. JS는 동적이고, V8은 빠르고, QuickJS는 메모리를 잡기 좋고, 브라우저라는 실행환경은 생각보다 제약이 많았습니다. 그래서 시간과 메모리를 하나의 런타임에서 동시에 예쁘게 해결하지 못했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 장기적으로는 JS 비슷한 언어에 관심이 생겼습니다. 문법과 사용감은 JS를 따라가지만, 변수 타입과 실행 모델은 더 제한해서 컴파일과 메모리 관리가 쉬운 언어입니다. 지금 당장 HWJ의 결론은 아닙니다. 이번 글의 결론은 어디까지나 &lt;b&gt;JS 제출은 V8으로 빠르게 검증하고, QuickJS WASM으로 메모리를 확인한다&lt;/b&gt;는 것입니다. 다만 이 타협을 하고 나니, 왜 사람들이 TypeScript의 타입 시스템을 더 밀어붙이거나 JS-like 언어를 새로 만들고 싶어 하는지 조금 알 것 같았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>개발 일지</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/56</guid>
      <comments>https://hy3ons.tistory.com/56#entry56comment</comments>
      <pubDate>Sun, 21 Jun 2026 05:12:54 +0900</pubDate>
    </item>
    <item>
      <title>Judge Blog 구축 일지 01 - MEMFS 채택 그리고 폐기</title>
      <link>https://hy3ons.tistory.com/55</link>
      <description>&lt;div class=&quot;tt_article_useless_p_margin contents_style&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;
&lt;script type=&quot;text/x-mathjax-config&quot;&gt;MathJax.Hub.Config({  tex2jax: {inlineMath: [['$','$'], ['\(','\)']]}});&lt;/script&gt;
&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/latest.js?config=TeX-MML-AM_CHTML&quot;&gt;&lt;/script&gt;
&lt;/span&gt;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회고.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;안녕하세요. 최근에는 알고리즘 블로그 글을 조금 더 &lt;b&gt;전문적으로 포스팅하고&lt;/b&gt; 싶다는 생각이 들었습니다. 단순히 풀이를 적고 끝내는 것이 아니라, 글 안에서 바로 코드를 실행하고 결과를 확인할 수 있으면 좋겠다고 생각했습니다. 그래서 Judge 기능이 달린 Blog를 만들자는 간단한 프로젝트를 진행하고 있었습니다. 프로젝트 명은 HWJ입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;HWJ는 Hybrid WASM Judge의 줄임말입니다&lt;/b&gt;. 지금 만들고 있는 것은 기존 온라인 저지처럼 서버가 모든 테스트를 실행하는 구조가 아니라, 사용자의 브라우저 안에서 Web Worker와 WebAssembly를 이용해 테스트 생성, 정답 실행, 제출 코드 실행, 출력 비교를 위임하는 구조를 만들고 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;사용자의 어뷰징은 관심이 없습니다.&lt;/b&gt; 물론 실제 서비스라면 언젠가는 반드시 다뤄야 하는 문제이지만, 지금 단계의 타겟층은 정말 배움에 뜻이 있는 사람으로 잡았습니다. 그래서 대회 플랫폼처럼 강한 격리와 검증을 먼저 만들기보다는, GitHub와 같은 overview 형태의 UI를 간략하게 제공하고, 글을 읽는 사람이 직접 코드를 돌려보는 경험을 우선으로 잡았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;프로젝트는 구체적으로 브라우저를 작은 채점 클러스터처럼 쓰는 방향에 가깝습니다. 출제자는 generator, validator, checker, solution을 작성하고, 사용자가 문제를 풀면 브라우저 worker들이 여러 테스트를 나눠 실행합니다. 서버는 C++ 코드를 WASM으로 컴파일하거나 artifact를 관리하는 역할만 맡고, 실제 반복 실행 비용은 가능한 한 클라이언트로 넘기는 구조를 목표로 하고 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;사용자의 브라우저에 &lt;b&gt;테스트케이스를 네트워크로 다 넘기면, 너무 손해&lt;/b&gt;라고 생각했습니다. 문제마다 수많은 입력을 서버에서 만들고, 다시 네트워크로 내려보내고, 제출 결과를 다시 서버로 올리는 구조라면 결국 기존 온라인 저지와 크게 다르지 않기 때문입니다. 그래서 테스트 생성과 실행 자체를 브라우저 안으로 최대한 밀어 넣는 방향을 생각했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로젝트를 진행하기 이전에, 간단하게 &lt;code&gt;emcc&lt;/code&gt;를 이용해 WASM을 만들고, $O(1)$ 입력에 대해 테스팅을 맞췄습니다. 굉장히 빨랐고, 이 구조를 채택하여 구현해야겠다고 생각했습니다. C++ 코드는 &lt;code&gt;emcc&lt;/code&gt;로 컴파일해 Emscripten artifact를 만들고, worker에서는 Emscripten이 생성한 JS glue와 &lt;code&gt;.wasm&lt;/code&gt;을 로드한 뒤 &lt;code&gt;Module.callMain()&lt;/code&gt;을 호출했습니다. 입력과 출력은 &lt;b&gt;Emscripten의 MEMFS&lt;/b&gt;를 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 이 선택이 꽤 자연스럽다고 생각했습니다. 사용자의 C++ 코드 안에 &lt;code&gt;freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin)&lt;/code&gt;과 &lt;code&gt;freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout)&lt;/code&gt;를 주입하고, worker가 실행 전에 &lt;code&gt;FS.writeFile(&quot;in.txt&quot;, input)&lt;/code&gt;을 호출한 뒤 실행이 끝나면 &lt;code&gt;FS.readFile(&quot;out.txt&quot;)&lt;/code&gt;로 출력을 읽으면 됩니다. 기존 BOJ식 &lt;code&gt;cin&lt;/code&gt;, &lt;code&gt;cout&lt;/code&gt;, &lt;code&gt;scanf&lt;/code&gt;, &lt;code&gt;printf&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;MEMFS&lt;/b&gt;라는 이름도 이 판단에 영향을 줬습니다. 실제 디스크를 쓰는 파일 시스템이 아니라 메모리 기반 가상 파일 시스템이므로, 입력 파일을 쓰고 출력 파일을 읽더라도 충분히 빠를 것이라고 생각했습니다. 브라우저 안에서 사용자의 코드를 고치지 않고 표준 입출력을 흉내 낼 수 있다면, 이 정도 추상화 비용은 감수할 만하다고 봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 C++을 WebAssembly로 컴파일해 브라우저에서 실행하면 JavaScript보다 빠를 가능성이 높다고 생각한 것도 있습니다. 알고리즘 문제 풀이에서 C++은 보통 가장 빠른 선택지이고, WASM도 네이티브에 가까운 실행 성능을 기대하게 만드는 기술이기 때문입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 실제로 여러 테스트를 돌려보니 특정 케이스에서는 C++ WASM이 JavaScript보다 느리게 나왔습니다. 처음에는 WebAssembly 자체가 느린 것인지, 메모리 증가가 병목인지, 아니면 worker의 wasm 할당문제인지, 컴파일 옵션을 잘못 잡은 것인지부터 의심했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 구현을 실수했다고 생각했습니다. worker pool에서 잘못 WASM을 호출하거나, artifact를 매번 비효율적으로 잡거나, 실행 시간을 재는 위치가 잘못돼서 생기는 문제라고 생각했습니다. 하지만 여러 시도 끝에도 실행시간에 극적인 차이를 주지 못했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그러다가 Java, Python, JS가 더욱더 빠르게 실행시간을 가지는 것을 보고, 정말 미쳐버릴 것 같았습니다. C++을 WASM으로 구웠는데도 JavaScript보다 느리게 나오는 순간이 있었고, 이건 단순히 언어 성능으로는 설명이 되지 않았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 의심을 I/O쪽으로 잡았고, 그 판단은 꽤 맞는 방향이었습니다. 저는 WASM이 무조건 더 빠른 건 맞다는 전제를 가지고 있었기 때문에, 병목을 가지는 곳이 있다면 그럼 I/O라고 생각을 했습니다. 정확히 말하면 Java, Python, JS가 C++보다 본질적으로 빠르다기보다는, 현재 HWJ에서 돌아가는 런타임 경로가 C++의 &lt;code&gt;freopen&lt;/code&gt;, libc FILE*, iostream, MEMFS 경로보다 훨씬 얇았습니다. 쉽게 말하면 C++만 표준 입출력을 살리기 위해 여러 계층을 더 통과하고 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;C++은 하지만 여러 계층을 통과해야 했었고, 이때부터 WASM을 조금 더 공부하게 되었습니다. 단순히 C++을 WASM으로 컴파일하면 빠르다는 생각만으로는 부족했고, 브라우저 WASM에서 메모리, 런타임, 입출력이 실제로 어떤 경로를 타는지 봐야 했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;먼저 C++과 JS의 최적화 방식부터 다시 봤습니다. C++은 &lt;code&gt;emcc&lt;/code&gt;가 LLVM을 통해 컴파일하는 시점에 대부분의 최적화가 끝납니다. 반면 JS는 코드가 실행되는 도중에 V8 같은 엔진이 실제 데이터 흐름을 보면서 JIT 최적화를 수행합니다. 그래서 단순히 &quot;C++이니까 무조건 빠르다&quot;라고 보기에는 브라우저 안의 실행 조건이 생각보다 복잡했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음에는 &lt;b&gt;wasm_gc&lt;/b&gt; 같은 것도 의심했습니다. Java, Python, JS가 더 자연스럽게 도는 것처럼 보이니, 브라우저 런타임 쪽에서 관리형 언어가 더 유리한 길을 타는 건가 싶었습니다. 하지만 조금 더 따져보니 그렇게 단순한 문제는 아니었습니다. Python은 Pyodide 같은 WASM 런타임 위에서 돌고, Java도 별도 런타임을 거치며, JS는 애초에 브라우저 엔진의 네이티브 실행 경로를 탑니다. 결국 중요한 것은 GC 하나가 아니라 각 언어 runner가 입력과 출력을 어떤 경로로 주고받는지였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;메모리 접근 방식도 다시 보게 되었습니다. 네이티브 x86 환경에서는 정렬되지 않은 메모리 접근을 CPU가 어느 정도 알아서 처리해 줍니다. 약간의 비용이 있을 수는 있지만, 보통 알고리즘 코드를 짤 때 그 부분을 깊게 의식하지는 않습니다. 그런데 WASM에서는 메모리 접근이 linear memory 위에서 일어나고, 브라우저 엔진은 명령에 담긴 정렬 정보를 바탕으로 접근을 최적화합니다. 정렬되지 않은 접근이 항상 실패한다는 뜻은 아니지만, C++ 네이티브에서 당연하게 기대하던 메모리 감각을 그대로 가져오면 성능을 잘못 판단할 수 있겠다고 느꼈습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 &lt;code&gt;WebAssembly.Memory.grow()&lt;/code&gt;도 의심했습니다. Wasm에서 메모리가 부족해지면 브라우저에 메모리 페이지를 더 달라고 요청해야 하고, 이 과정에서 기존 &lt;code&gt;ArrayBuffer&lt;/code&gt;의 view가 바뀌거나 메모리 재매핑이 일어날 수 있습니다. 실행 중 이런 일이 반복되면 순수 연산보다 메모리 관리 비용이 더 눈에 띌 수 있다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;또 하나는 JS와 WASM 사이의 버퍼 공유였습니다. 메모리를 처음부터 충분히 크게 잡아두고, C++ 쪽에 입력 버퍼와 출력 버퍼를 만들어둔 뒤, JS가 포인터와 길이를 받아 &lt;code&gt;Module.HEAPU8.subarray(pointer, pointer + size)&lt;/code&gt; 형태로 직접 view를 잡으면 불필요한 복사를 줄일 수 있습니다. 엄밀히 말하면 JS 입력을 WASM memory에 넣는 복사 자체가 완전히 사라지는 것은 아니지만, 적어도 파일 시스템 계층을 거치는 것보다 훨씬 얇은 경로를 만들 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;메모리 제한을 기준으로 &lt;code&gt;INITIAL_MEMORY&lt;/code&gt;를 키워보고, &lt;code&gt;ALLOW_MEMORY_GROWTH=0&lt;/code&gt;도 실험해봤지만 결과는 크게 달라지지 않았습니다. 이 과정에서 적어도 이번 문제의 1차 병목은 C++의 순수 계산 속도나 &lt;code&gt;WebAssembly.Memory.grow()&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;다시 보니 현재 C++ 실행 경로는 사용자가 작성한 표준 입출력을 그대로 살리기 위해 Emscripten의 libc, iostream, &lt;code&gt;freopen&lt;/code&gt;, MEMFS 계층을 통과하고 있었습니다. 반면 JavaScript runner는 같은 worker 안에서 문자열이나 &lt;code&gt;Uint8Array&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;이 글은 그 차이를 확인하면서, Emscripten 안에서 입출력 경로를 얇게 만드는 방법과 WASI로 별도 C++ runner를 두는 방향을 정리한 기록입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;문제의 핵심: 산술보다 두꺼운 입출력 경로&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 구조를 단순화하면 C++ 실행 경로는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JS input에서 시작&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;FS.writeFile(&quot;in.txt&quot;)&lt;/code&gt; &amp;rarr; &lt;code&gt;freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin)&lt;/code&gt; &amp;rarr; &lt;code&gt;libc FILE*&lt;/code&gt; &amp;rarr; &lt;code&gt;iostream / scanf&lt;/code&gt; &amp;rarr; &lt;b&gt;user C++ code&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;user C++ output 반환&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;cout / printf&lt;/code&gt; &amp;rarr; &lt;code&gt;libc / iostream&lt;/code&gt; &amp;rarr; &lt;code&gt;freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout)&lt;/code&gt; &amp;rarr; &lt;code&gt;MEMFS&lt;/code&gt; &amp;rarr; &lt;code&gt;FS.readFile(&quot;out.txt&quot;)&lt;/code&gt; &amp;rarr; &lt;b&gt;JS output&lt;/b&gt;&lt;/span&gt;&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식은 사용자 코드 호환성 면에서는 좋습니다. BOJ에 제출하던 코드를 거의 그대로 브라우저에서 실행할 수 있기 때문입니다. 하지만 성능 관점에서는 결코 얇은 경로가 아닙니다. 실제 디스크를 쓰는 것은 아니고 MEMFS도 메모리 기반이지만, 그래도 Emscripten이 제공하는 POSIX 파일 시스템과 JS glue를 계속 통과합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;특히 출력이 많은 문제에서 &lt;code&gt;cout &amp;lt;&amp;lt;&lt;/code&gt;를 반복하면 작은 write가 계속 발생합니다. C++ 코드 안에서 문자열에 모아 한 번에 출력했을 때 실행 시간이 줄어드는 현상은, 병목이 알고리즘보다 stdout 경로에 있을 수 있다는 신호였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;첫 번째 후보: streambuf로 MEMFS 우회하기&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 현실적인 다음 실험은 Emscripten을 유지하되, &lt;code&gt;freopen(&quot;in.txt/out.txt&quot;)&lt;/code&gt; 주입을 끄고 C++ wrapper에서 &lt;code&gt;std::cin.rdbuf()&lt;/code&gt;와 &lt;code&gt;std::cout.rdbuf()&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;즉, JS가 입력을 WASM linear memory에 한 번 복사하고, C++의 &lt;code&gt;cin&lt;/code&gt;은 그 메모리 버퍼를 직접 읽도록 만듭니다. 출력도 &lt;code&gt;cout&lt;/code&gt;이 MEMFS 파일에 쓰는 대신, 미리 잡아둔 output buffer에 바로 쓰게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;JS input&lt;/b&gt;을 &lt;code&gt;HEAPU8.set(inputBytes, inputPtr)&lt;/code&gt;로 WASM memory에 복사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;init_fast_io(inputLen)&lt;/code&gt;로 입력 범위를 알려주고, &lt;code&gt;cin.rdbuf(customInputBuf)&lt;/code&gt;를 붙입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;user C++ code&lt;/b&gt;는 기존처럼 &lt;code&gt;cin&lt;/code&gt;을 읽지만, 실제로는 memory buffer를 직접 읽습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;4&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;cout.rdbuf(customOutputBuf)&lt;/code&gt;가 output buffer에 쓰고, JS는 그 byte를 바로 읽습니다.&lt;/span&gt;&lt;/p&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식은 엄밀히 말하면 완전한 zero-copy는 아닙니다. JS의 &lt;code&gt;Uint8Array&lt;/code&gt;를 WASM memory로 옮기는 &lt;code&gt;HEAPU8.set()&lt;/code&gt; 한 번은 필요합니다. 다만 &lt;code&gt;FS.writeFile&lt;/code&gt;, &lt;code&gt;freopen&lt;/code&gt;, libc FILE*, MEMFS를 통과하는 것보다 훨씬 얇습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_streambuf_skeleton&quot; class=&quot;cpp&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class FastInputBuf : public std::streambuf {
public:
  FastInputBuf(char* base, size_t length) {
    setg(base, base, base + length);
  }
};

class FastOutputBuf : public std::streambuf {
public:
  FastOutputBuf(char* base, size_t length) {
    setp(base, base + length);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 실험에서 C++ 실행 시간이 줄어든다면 원인은 꽤 좁혀집니다. C++ WASM 산술 자체보다 Emscripten의 MEMFS/POSIX I/O glue가 현재 구조에서 더 큰 비용을 만들고 있었다고 볼 수 있기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;streambuf 방식의 한계&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다만 이 방식도 만능은 아닙니다. &lt;code&gt;std::cin&lt;/code&gt;과 &lt;code&gt;std::cout&lt;/code&gt;을 사용하는 solution에는 잘 맞지만, C의 &lt;code&gt;scanf&lt;/code&gt;, &lt;code&gt;printf&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;fread&lt;/code&gt;, &lt;code&gt;fwrite&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;또한 output buffer가 꽉 찼을 때의 정책도 필요합니다. 조용히 잘리면 사용자는 Wrong Answer를 받게 되지만, 실제로는 Output Limit Exceeded나 Runtime Error에 가까운 상황입니다. 따라서 runner가 output cap을 명확히 관리하고, overflow를 verdict로 바꿔야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;매 실행마다 &lt;code&gt;new FastInputBuf&lt;/code&gt; 같은 heap allocation을 만들면 실험 결과가 흐려질 수도 있습니다. 가능하면 wrapper 안에서 재사용 가능한 object를 두고, 실행마다 buffer 범위만 다시 설정하는 편이 낫다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두 번째 후보: cpp-wasi&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;더 낮은 계층을 건드리는 후보는 Emscripten을 거치지 않고 WASI로 C++을 컴파일하는 방식입니다. 기존에는 &lt;code&gt;emcc&lt;/code&gt;로 C++을 컴파일했다면, WASI 방식에서는 보통 WASI SDK의 &lt;code&gt;clang++ --target=wasm32-wasi&lt;/code&gt;를 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_wasi_compile&quot; class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;clang++ --target=wasm32-wasi main.cpp -O2 -o main.wasm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Emscripten은 &lt;code&gt;module.wasm&lt;/code&gt;과 함께 JS glue, FS, runtime을 제공합니다. 반면 WASI는 보통 순수 &lt;code&gt;.wasm&lt;/code&gt;을 만들고, 실행 환경이 &lt;code&gt;fd_read&lt;/code&gt;, &lt;code&gt;fd_write&lt;/code&gt;, &lt;code&gt;proc_exit&lt;/code&gt; 같은 import를 제공해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;READ&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;C++ cin / scanf&lt;/code&gt; &amp;rarr; &lt;code&gt;wasi-libc&lt;/code&gt; &amp;rarr; &lt;code&gt;fd_read import&lt;/code&gt; &amp;rarr; &lt;code&gt;HWJ JS host&lt;/code&gt; &amp;rarr; &lt;b&gt;input Uint8Array / WASM memory&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;h4 style=&quot;margin: 0 0 6px 0; font-size: 16px; font-weight: bold; color: #1e293b;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WRITE&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;C++ cout / printf&lt;/code&gt; &amp;rarr; &lt;code&gt;wasi-libc&lt;/code&gt; &amp;rarr; &lt;code&gt;fd_write import&lt;/code&gt; &amp;rarr; &lt;code&gt;HWJ JS host&lt;/code&gt; &amp;rarr; &lt;b&gt;output byte chunks&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;중요한 점은 WASI가 MEMFS를 더 빠르게 읽는 기술이 아니라는 것입니다. 오히려 MEMFS 자체를 제거하고, stdin/stdout을 얇은 fd 기반 import로 직접 연결하는 방식에 가깝습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두 컴파일러가 바라보는 실행 환경&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;둘 다 C++을 WebAssembly로 만들 수 있지만, 애초에 바라보는 목적지는 다릅니다. Emscripten은 브라우저라는 제약 많은 환경에 기존 C++ 프로그램을 최대한 그대로 이식하는 도구에 가깝고, WASI-SDK는 Wasm을 위한 표준 시스템 인터페이스에 맞춰 모듈을 만드는 도구에 가깝습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 Emscripten은 브라우저와의 결합이 강합니다. SDL2, OpenGL/WebGL, canvas, input event 같은 웹 쪽 기능과 붙는 프로젝트라면 Emscripten이 제공하는 glue가 큰 장점이 됩니다. 기존 C++ 코드를 브라우저 앱처럼 보이게 만들기 위해 필요한 것들을 많이 대신 처리해주기 때문입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;반대로 WASI-SDK는 그런 브라우저 편의 기능을 자동으로 제공하지 않습니다. 대신 &lt;code&gt;fd_read&lt;/code&gt;, &lt;code&gt;fd_write&lt;/code&gt; 같은 표준화된 시스템 인터페이스에 맞춰 훨씬 단순한 실행 모델을 만듭니다. 브라우저에서 쓰려면 JS host를 직접 만들어야 하지만, stdin/stdout 중심의 judge runner에서는 그 단순함이 오히려 장점이 될 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Emscripten&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;glue/runtime&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;C++ project&lt;/code&gt; &amp;rarr; &lt;code&gt;.wasm&lt;/code&gt; + &lt;code&gt;JS glue/runtime&lt;/code&gt; + &lt;code&gt;FS/MEMFS&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;목표: 기존 C/C++ 프로그램을 브라우저에서 최대한 그대로 실행&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WASI-SDK&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;host imports&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;C++ project&lt;/code&gt; &amp;rarr; &lt;code&gt;.wasm&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;목표: WASI import를 제공하는 host 위에서 실행&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;wasm32-wasi의 32는 느리다는 뜻이 아니다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;wasm32-wasi&lt;/code&gt;라는 이름에서 &lt;code&gt;32&lt;/code&gt;가 붙어 있어서 32비트 CPU처럼 느려지는 것인지 헷갈릴 수 있습니다. 하지만 여기서 32는 WASM linear memory의 주소가 32비트라는 뜻입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;브라우저에서 일반적으로 사용하는 WebAssembly도 대부분 wasm32입니다. Emscripten 역시 기본적으로 wasm32 계열입니다. 따라서 WASI로 간다고 해서 32비트라서 느려지는 구조는 아닙니다.&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;font-family: 'Noto Serif KR';&quot;&gt;순수 계산 성능은 Emscripten WASM과 WASI WASM이 크게 다르지 않을 수도 있습니다. 둘 다 결국 브라우저의 WebAssembly 엔진 위에서 실행됩니다. 차이가 나는 부분은 주로 stdin/stdout, 작은 write 반복, 파일 시스템 에뮬레이션, JS glue입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;generator 병목도 줄어들 수 있다&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조는 solution뿐 아니라 generator에도 의미가 있습니다. 현재 generator가 C++ WASM으로 실행되고 stdout으로 큰 테스트 입력을 만든다면, 그 출력 역시 MEMFS의 &lt;code&gt;out.txt&lt;/code&gt;를 거쳐 다시 JS로 읽힙니다.&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;font-family: 'Noto Serif KR';&quot;&gt;WASI에서는 generator의 stdout을 &lt;code&gt;fd_write&lt;/code&gt;에서 바로 byte chunk로 받을 수 있습니다. 그러면 &lt;code&gt;out.txt&lt;/code&gt;를 만들고, 다시 읽고, 다음 solution의 &lt;code&gt;in.txt&lt;/code&gt;로 쓰는 과정이 줄어듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-steps-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #1e293b;&quot;&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;1&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;generator stdout&lt;/b&gt;이 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; margin-bottom: 24px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-line&quot; style=&quot;position: absolute; left: 15px; top: 32px; bottom: -24px; width: 2px; background: #e2e8f0; z-index: 1;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;2&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;fd_write&lt;/code&gt;에서 바로 &lt;b&gt;JS byte chunks&lt;/b&gt;로 받습니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-step-item&quot; style=&quot;display: flex; gap: 16px; position: relative;&quot;&gt;
&lt;div class=&quot;hyeon-step-number&quot; style=&quot;flex-shrink: 0; width: 32px; height: 32px; border-radius: 50%; border: 2px solid #1e293b; background: #fff; color: #1e293b; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; z-index: 2; box-sizing: border-box;&quot;&gt;3&lt;/div&gt;
&lt;div class=&quot;hyeon-step-content&quot; style=&quot;padding-top: 4px;&quot;&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #475569; line-height: 1.65;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 byte를 다시 &lt;b&gt;solution stdin&lt;/b&gt;으로 넘깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 되면 solution과 generator처럼 stdin/stdout 중심으로 움직이는 C++ artifact는 WASI의 효과를 볼 가능성이 있습니다. 다만 checker는 조금 다릅니다. testlib checker는 보통 &lt;code&gt;input.txt&lt;/code&gt;, &lt;code&gt;output.txt&lt;/code&gt;, &lt;code&gt;answer.txt&lt;/code&gt; 같은 파일을 직접 열기 때문에, WASI에서 파일 API나 preopen directory를 별도로 설계해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;cpp-wasi를 실제로 붙이며 밟은 삽질&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;계획 단계에서는 WASI로 바꾸면 Emscripten MEMFS를 걷어내고 &lt;code&gt;fd_read&lt;/code&gt;, &lt;code&gt;fd_write&lt;/code&gt;만 얇게 구현하면 될 것처럼 보였습니다. 실제 구현에서는 &lt;code&gt;wasm_artifacts.target&lt;/code&gt;에 &lt;code&gt;wasi-wasm32&lt;/code&gt;를 추가하고, 기존 &lt;code&gt;module.wasm&lt;/code&gt; 옆에 &lt;code&gt;module.wasi.wasm&lt;/code&gt;을 저장한 뒤, worker 쪽에 WASI host를 붙이는 방식으로 시작했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서 확인한 첫 번째 삽질은 링크 옵션이었습니다. 처음에는 메모리를 export하려고 &lt;code&gt;-Wl,--export=memory&lt;/code&gt;를 넣었는데, &lt;code&gt;wasm-ld&lt;/code&gt;가 &lt;code&gt;symbol exported via --export not found: memory&lt;/code&gt;를 냈습니다. WASI SDK에서 memory 자체를 export하려면 &lt;code&gt;-Wl,--export-memory&lt;/code&gt;를 써야 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 14px 0; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; gap: 18px; align-items: baseline;&quot;&gt;&lt;span style=&quot;font-size: 15px; font-weight: bold;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;잘못 넣은 옵션&lt;/span&gt;&lt;/span&gt; &lt;code style=&quot;font-size: 13px;&quot;&gt;-Wl,--export=memory&lt;/code&gt;&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 14px 0; display: flex; justify-content: space-between; gap: 18px; align-items: baseline;&quot;&gt;&lt;span style=&quot;font-size: 15px; font-weight: bold;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제로 필요했던 옵션&lt;/span&gt;&lt;/span&gt; &lt;code style=&quot;font-size: 13px;&quot;&gt;-Wl,--export-memory&lt;/code&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;두 번째 삽질은 C++ 런타임과 exception ABI였습니다. 단순한 알고리즘 코드라도 &lt;code&gt;iostream&lt;/code&gt;, &lt;code&gt;vector&lt;/code&gt;, STL을 쓰면 내부적으로 &lt;code&gt;std::logic_error&lt;/code&gt;나 exception 관련 심볼이 엮일 수 있습니다. 그래서 처음에는 &lt;code&gt;__cxa_allocate_exception&lt;/code&gt;, &lt;code&gt;__cxa_throw&lt;/code&gt; 같은 undefined symbol이 터졌습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그 다음에는 &lt;code&gt;-fwasm-exceptions&lt;/code&gt;와 WASI SDK의 &lt;code&gt;eh&lt;/code&gt; 라이브러리를 붙여서 링크를 통과시켰습니다. 컨테이너 안에서는 컴파일이 됐지만, 브라우저 제출에서는 &lt;code&gt;module uses a mix of legacy and new exception handling instructions&lt;/code&gt;로 &lt;code&gt;WebAssembly.compile()&lt;/code&gt; 단계에서 죽었습니다. 여기서 얻은 결론은 명확합니다. WASI는 링크 성공만 보면 안 되고, 실제 브라우저 WebAssembly validator가 받아들이는 instruction set인지까지 봐야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 14px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;컨테이너&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;링크 성공&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 14px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;브라우저&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;WebAssembly.compile&lt;/code&gt; 실패&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 14px 0;&quot;&gt;
&lt;p style=&quot;margin: 0 0 6px 0; font-size: 15px; font-weight: 800;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원인 방향&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; color: #374151; line-height: 1.7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;C++ exception ABI / wasm exception instruction 조합이 현재 브라우저 실행 경로와 맞지 않음&lt;/span&gt;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 이 문제를 피하려고 &lt;code&gt;-fno-exceptions&lt;/code&gt;와 &lt;code&gt;noeh&lt;/code&gt; libc++ 조합을 시도했습니다. 단순 solution은 이 방식으로 지나가는 것처럼 보였습니다. 하지만 곧바로 틀렸다는 것이 드러났습니다. 유저 C++ 코드는 당연히 &lt;code&gt;try/catch&lt;/code&gt;를 쓸 수 있어야 하고, Polygon 계열 generator/checker가 자주 쓰는 &lt;code&gt;testlib.h&lt;/code&gt;도 내부에서 &lt;code&gt;try&lt;/code&gt;, &lt;code&gt;throw&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;-fno-exceptions&lt;/code&gt;를 켜면 &lt;code&gt;testlib.h&lt;/code&gt; 포함 source는 컴파일 단계에서 바로 실패합니다. 실제로 &lt;code&gt;cannot use 'try' with exceptions disabled&lt;/code&gt;, &lt;code&gt;cannot use 'throw' with exceptions disabled&lt;/code&gt;가 나왔습니다. 따라서 judge runner의 기본 C++ ABI에서 exception을 끄는 것은 선택지가 아닙니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_wasi_final_flags&quot; class=&quot;bash&quot; style=&quot;max-width: 100%; overflow-x: auto; white-space: pre;&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;clang++ main.cpp \
  --target=wasm32-wasip1 \
  --sysroot=/opt/wasi-sdk/share/wasi-sysroot \
  -std=c++20 \
  -O2 \
  -fexceptions \
  -I /app/src/lib/headers \
  -L/opt/wasi-sdk/share/wasi-sysroot/lib/wasm32-wasip1/eh \
  -lc++ -lc++abi -lunwind \
  -Wl,--initial-memory=... \
  -Wl,--max-memory=... \
  -Wl,--export-memory \
  -o module.wasi.wasm&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최종적으로 통과한 조합은 &lt;code&gt;-fexceptions&lt;/code&gt;, WASI SDK의 &lt;code&gt;wasm32-wasip1/eh&lt;/code&gt; 라이브러리, 그리고 &lt;code&gt;-lc++ -lc++abi -lunwind&lt;/code&gt;였습니다. 여기서 중요한 함정은 &lt;code&gt;-fwasm-exceptions&lt;/code&gt;를 넣지 않는 것입니다. &lt;code&gt;-fwasm-exceptions&lt;/code&gt;를 넣으면 링크는 되지만 Chromium에서 다시 &lt;code&gt;module uses a mix of legacy and new exception handling instructions&lt;/code&gt;가 재현되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 16px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-fno-exceptions + noeh&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;compile fail&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;testlib.h&lt;/code&gt;의 &lt;code&gt;try/throw&lt;/code&gt; 때문에 컴파일 실패&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 16px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-fwasm-exceptions + eh&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;browser fail&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;링크 성공, Chromium &lt;code&gt;WebAssembly.compile&lt;/code&gt; 실패&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 8px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 16px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-fexceptions + eh + -lunwind&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;pass&lt;/span&gt;&lt;/div&gt;
&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.7; color: #374151;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;testlib&lt;/code&gt; 포함 generator 컴파일 성공, Chromium &lt;code&gt;WebAssembly.compile&lt;/code&gt; 성공&lt;/span&gt;&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또 하나의 착시가 있었습니다. 서버 컨테이너의 Node에서 &lt;code&gt;WebAssembly.compile()&lt;/code&gt;을 돌리면 문제 artifact가 통과하는데, 실제 Chromium에서는 실패했습니다. 그래서 WASI artifact 검증은 Node만 믿으면 안 됩니다. 브라우저에서 &lt;code&gt;fetch&lt;/code&gt;로 실제 artifact URL을 가져온 뒤 &lt;code&gt;WebAssembly.compile(bytes)&lt;/code&gt;를 직접 해보는 smoke test가 필요합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;마지막 삽질은 stale artifact였습니다. 예전 &lt;code&gt;-fwasm-exceptions&lt;/code&gt; 조합으로 만들어진 &lt;code&gt;module.wasi.wasm&lt;/code&gt;이 DB에서는 &lt;code&gt;ready&lt;/code&gt;로 남아 있었고, 파일도 그대로 있었기 때문에 새 컴파일러 옵션을 넣어도 기존 artifact가 재사용되었습니다. 이 문제는 compiler version에 ABI signature를 붙이고, 저장된 &lt;code&gt;compiler_version&lt;/code&gt;이 현재 compiler signature와 다르면 &lt;code&gt;ready&lt;/code&gt;라도 재컴파일하도록 막았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;details class=&quot;hyeon-toggle&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 14px 0;&quot;&gt;
&lt;summary style=&quot;cursor: pointer; font-size: 16px; font-weight: 800; list-style-position: outside;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;cpp-wasi ABI signature와 재컴파일 조건&lt;/span&gt;&lt;/summary&gt;
&lt;div style=&quot;margin-top: 14px; padding-top: 14px; border-top: 1px solid #e5e7eb;&quot;&gt;
&lt;p style=&quot;margin: 0 0 10px 0; font-size: 15px; line-height: 1.7;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;현재 cpp-wasi ABI signature: &lt;code&gt;hwj-wasi:wasip1:eh-fexceptions:v1&lt;/code&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;margin: 0; padding-left: 20px; font-size: 15px; line-height: 1.8; color: #374151;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;source.need_compile == true&lt;/code&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;artifact 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;artifact.status != ready&lt;/code&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;artifact 파일 없음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;artifact.compiler_version != 현재 compiler signature&lt;/code&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&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;font-family: 'Noto Serif KR';&quot;&gt;또 하나 중요한 점은 target 이름입니다. 최근 WASI SDK에서는 &lt;code&gt;wasm32-wasi&lt;/code&gt;보다 &lt;code&gt;wasm32-wasip1&lt;/code&gt;가 현재 명시적인 target입니다. 예전 이름으로도 어느 정도 돌아갈 수는 있지만, 경고와 라이브러리 경로 혼선을 줄이려면 compile target과 sysroot library path를 &lt;code&gt;wasm32-wasip1&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;운영 쪽 삽질도 있었습니다. production container는 소스가 bind mount된 개발 컨테이너가 아니라 이미지 빌드 결과로 뜹니다. 따라서 WASI runner나 compiler adapter를 고쳤으면 &lt;code&gt;docker compose -f docker-compose.prod.yaml up -d --build&lt;/code&gt;로 이미지를 다시 구워야 합니다. 파일만 고치고 기존 prod 컨테이너를 보고 있으면 아무 변화가 없어 보입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;현재까지의 정리는 이렇습니다. WASI는 MEMFS를 빠르게 쓰는 방법이 아니라 MEMFS 경로를 없애는 방법입니다. 다만 C++ 표준 라이브러리, exception ABI, 브라우저 WebAssembly validator, checker의 파일 API까지 같이 맞아야 실제 judge engine으로 안정화됩니다. generator와 solution처럼 stdin/stdout만 중심인 artifact부터 붙이는 것이 가장 현실적인 순서입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 과정을 통해 적어도 이번 케이스에서는 C++ WASM 산술 자체보다, BOJ식 표준 입출력을 브라우저 안에서 재현하는 계층이 더 큰 병목일 수 있다는 것을 알게 되었습니다. C++의 계산은 빠를 수 있지만, 입력을 넣고 출력을 꺼내는 통로가 두꺼우면 전체 실행 시간은 쉽게 밀립니다.&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;font-family: 'Noto Serif KR';&quot;&gt;단기적으로는 &lt;code&gt;std::streambuf&lt;/code&gt;를 이용해 &lt;code&gt;cin/cout&lt;/code&gt;을 WASM memory buffer에 직접 붙이는 실험이 가장 현실적입니다. 이 방식은 현재 Emscripten 기반 구조를 크게 흔들지 않으면서도 MEMFS 병목을 우회할 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;장기적으로는 &lt;code&gt;cpp-wasi&lt;/code&gt;가 더 자연스러운 후보로 보입니다. WASI는 MEMFS를 최적화하는 것이 아니라 제거합니다. &lt;code&gt;fd_read&lt;/code&gt;, &lt;code&gt;fd_write&lt;/code&gt;를 HWJ worker가 직접 구현하면, C++의 stdin/stdout을 훨씬 얇은 경로로 연결할 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;결국 HWJ의 C++ runner는 두 방향으로 실험해볼 가치가 있습니다. 하나는 Emscripten 안에서 최대한 얇게 만드는 방향이고, 다른 하나는 WASI로 별도 실행 엔진을 두는 방향입니다. 지금 필요한 것은 어느 쪽이 이론적으로 더 멋진가가 아니라, 같은 generator와 solution을 놓고 실제 worker 안에서 &lt;code&gt;callMainMs&lt;/code&gt;, &lt;code&gt;stdoutReadMs&lt;/code&gt;, &lt;code&gt;caseTotalMs&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;이후 실제 구현에서는 우선 &lt;code&gt;cpp-wasi&lt;/code&gt;를 별도 C++ 실행 target으로 붙였습니다. 기존 Emscripten artifact와 구분하기 위해 artifact target을 나누고, worker에는 &lt;code&gt;cpp-wasi&lt;/code&gt; runtime을 추가했습니다. WASI host는 브라우저 worker 안에서 &lt;code&gt;fd_read&lt;/code&gt;, &lt;code&gt;fd_write&lt;/code&gt;, &lt;code&gt;path_open&lt;/code&gt;, &lt;code&gt;fd_seek&lt;/code&gt; 같은 import를 직접 제공하도록 만들었습니다. 즉, C++ solution과 generator는 가능한 한 MEMFS 파일을 통하지 않고 byte stream에 가깝게 stdin/stdout을 주고받는 방향으로 정리했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;메모리 옵션에 대해서도 결론이 조금 바뀌었습니다. 처음에는 문제 메모리 제한만큼 &lt;code&gt;INITIAL_MEMORY&lt;/code&gt;를 크게 잡고 &lt;code&gt;ALLOW_MEMORY_GROWTH=0&lt;/code&gt;처럼 고정하면 빨라질 것이라고 생각했습니다. 하지만 실제로는 worker 여러 개가 동시에 WASM memory를 잡는 구조라서, 큰 초기 메모리는 브라우저 메모리 압박과 GC pause를 키울 수 있었습니다. 그래서 초기 메모리는 linker 기본값에 맡기고, &lt;code&gt;--max-memory&lt;/code&gt;만 문제 메모리 제한으로 거는 쪽으로 바꿨습니다. 메모리 제한은 지키되, 시작부터 모든 worker가 큰 memory를 들고 출발하지 않게 한 것입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;또 하나 크게 정리한 부분은 checker입니다. 처음에는 checker도 전부 WASM으로 실행하는 것이 자연스럽다고 생각했습니다. 하지만 표준 checker까지 매 케이스마다 WASM으로 실행하면, 실제 비교보다 &lt;code&gt;input.txt&lt;/code&gt;, &lt;code&gt;output.txt&lt;/code&gt;, &lt;code&gt;answer.txt&lt;/code&gt;를 준비하고 testlib checker를 띄우는 비용이 더 커졌습니다. 어떤 케이스에서는 checker만 약 20ms 이상 잡아먹는 것을 확인했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 표준 checker는 JS fast path로 빼고, custom checker만 WASM으로 실행하도록 정리했습니다. 출제자가 직접 작성하거나 파일로 올린 checker는 의미를 알 수 없으므로 반드시 custom WASM checker를 사용합니다. 반대로 UI에서 선택한 기본 checker는 marker를 보고 JS에서 바로 비교합니다. 현재는 &lt;code&gt;wcmp.cpp&lt;/code&gt;는 token compare, &lt;code&gt;ncmp.cpp&lt;/code&gt;는 줄 단위 오른쪽 공백 trim 비교, &lt;code&gt;yesno.cpp&lt;/code&gt;는 대소문자 무시 token 비교, &lt;code&gt;rcmp4.cpp&lt;/code&gt;와 &lt;code&gt;rcmp9.cpp&lt;/code&gt;는 각각 &lt;code&gt;1e-4&lt;/code&gt;, &lt;code&gt;1e-9&lt;/code&gt; 기준 실수 비교로 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;hyeon-eval-container&quot; style=&quot;max-width: 650px; margin: 2rem auto; font-family: 'Noto Serif KR', serif; color: #111827; border-top: 2px solid #111827; border-bottom: 2px solid #111827; padding: 8px 0;&quot;&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0; border-bottom: 1px solid #e5e7eb;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 10px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;표준 checker&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;JS fast path&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; line-height: 1.8; color: #374151;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;wcmp.cpp&lt;/code&gt; &amp;rarr; JS token byte compare&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;ncmp.cpp&lt;/code&gt; &amp;rarr; JS line right-trim byte compare&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;yesno.cpp&lt;/code&gt; &amp;rarr; JS case-insensitive token compare&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;rcmp4.cpp&lt;/code&gt; &amp;rarr; JS real compare, &lt;code&gt;1e-4&lt;/code&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;code&gt;rcmp9.cpp&lt;/code&gt; &amp;rarr; JS real compare, &lt;code&gt;1e-9&lt;/code&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class=&quot;hyeon-eval-item&quot; style=&quot;padding: 16px 0;&quot;&gt;
&lt;div style=&quot;display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 10px;&quot;&gt;
&lt;h4 style=&quot;margin: 0; font-size: 17px; font-weight: 800;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;custom checker&lt;/span&gt;&lt;/h4&gt;
&lt;span style=&quot;font-family: 'Fira Code', monospace; font-size: 12px; font-weight: bold; border: 1px solid #111827; padding: 2px 6px;&quot;&gt;WASM checker&lt;/span&gt;&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; line-height: 1.8; color: #374151;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;직접 작성 / 파일 업로드 / 템플릿 수정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;checker artifact 준비&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;WASM checker 실행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 결론은 꽤 중요했습니다. HWJ에서 빠르게 만들어야 하는 것은 모든 것을 WASM으로 돌리는 구조가 아니라, &lt;b&gt;WASM으로 돌려야 하는 것과 JS에서 바로 끝내야 하는 것을 구분하는 구조&lt;/b&gt;였습니다. generator와 solution처럼 사용자 코드 자체는 WASM으로 실행해야 하지만, 의미가 고정된 표준 checker는 JS byte stream 비교가 훨씬 얇습니다. 반대로 custom checker는 문제별 의미가 들어 있으므로 느리더라도 WASM 실행을 유지해야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;마지막으로 측정값도 다시 해석해야 했습니다. UI에 보이는 worst time이 항상 사용자가 느끼는 전체 wall time은 아니었습니다. solution의 순수 실행 시간은 낮게 나오더라도, generator 실행, main solution 실행, stdout materialize, checker, worker restart, artifact cache miss, 브라우저 GC가 합쳐진 &lt;code&gt;caseTotalMs&lt;/code&gt;는 훨씬 크게 보일 수 있습니다. 따라서 앞으로는 &lt;code&gt;callMainMs&lt;/code&gt; 하나만 보지 않고 &lt;code&gt;stdoutReadMs&lt;/code&gt;, &lt;code&gt;checkerDurationMs&lt;/code&gt;, &lt;code&gt;caseTotalMs&lt;/code&gt;, worker pause를 같이 봐야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;현재 결론은 이렇습니다. C++ WASM이 JS보다 느리게 보였던 핵심 원인은 C++ 산술 성능이 아니라, 브라우저 judge runner 안에서 C++만 더 두꺼운 I/O와 checker 경로를 타고 있었기 때문입니다. 그래서 HWJ는 C++ solution/generator에는 WASI 기반의 얇은 stdin/stdout 경로를 붙이고, 표준 checker는 JS fast path로 빼며, custom checker만 WASM으로 남기는 방향으로 정리했습니다. 이 방향이 지금까지의 삽질에서 얻은 가장 실용적인 결론입니다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/55</guid>
      <comments>https://hy3ons.tistory.com/55#entry55comment</comments>
      <pubDate>Sun, 14 Jun 2026 07:41:51 +0900</pubDate>
    </item>
    <item>
      <title>클라우드 서버 구축 일지 03 - 선언적 성질과 수렴적 철학</title>
      <link>https://hy3ons.tistory.com/54</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회고.&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 클라우드 서버를 만들어보겠다는 생각으로 API 서버를 하나 띄우고, Helm으로 KubeVirt 관련 YAML에 값을 주입하면서 VM을 조정하고 있었습니다. 사용자 정보나 상태도 Kubernetes 안에 두기보다는 외부 데이터베이스를 참조하는 구조였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;문제는 KubeVirt를 제어한다고 하면서도, 실제로는 Kubernetes의 동작 방식을 충분히 활용하지 못하고 있었다는 점입니다. KubeVirt가 이미 Kubernetes 위에서 CRD와 controller 패턴으로 동작하는데, 저는 그 위에 다시 별도의 추상 계층을 얹어 명령형으로 제어하려고 했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;결국 이 방식은 제가 이해하고 있는 기술 스택 안에서 Kubernetes를 너무 멀리서, 너무 추상적으로 다루는 접근이었습니다. 그래서 방향을 바꾸어 KubeVirt가 어떤 방식으로 Kubernetes API를 확장하고, controller가 어떤 방식으로 desired state와 actual state를 맞추는지부터 다시 살펴보기 시작했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그 과정에서 KubeVirt의 VirtualMachine, VirtualMachineInstance, CDI DataVolume, PVC, Service가 서로 어떤 관계로 이어지는지 확인했고, VM을 만든다는 일이 단순히 가상 머신 프로세스를 실행하는 것이 아니라 Kubernetes 리소스들을 선언하고 수렴시키는 일이라는 것을 알게 되었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;SSH 접속 방식도 다시 고민하게 되었습니다. 학교 네트워크 환경에서는 22번 포트와 추가 포트 개방이 제한적이었기 때문에, 처음에는 bastion 계정을 두고 내부 VM으로 프록시하는 방식을 생각했습니다. 하지만 VM마다 포트를 열기보다는 하나의 SSH 진입점을 두고, 로그인 계정명을 기준으로 사용자의 VM을 찾아 라우팅하는 방식이 더 자연스럽다는 결론에 도달했습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개요 및 상황&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Kite는 Kubernetes 위에서 사용자별 KubeVirt VM을 만들고, 웹 API와 프론트엔드, SSH 접속까지 하나의 플랫폼으로 묶어보려는 프로젝트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음 목표는 단순했습니다. 사용자가 회원가입을 하면 사용자 정보를 Kubernetes CRD로 저장하고, 그 사용자가 VM을 요청하면 controller가 Namespace, KubeVirt VirtualMachine, CDI DataVolume, Service를 생성하는 구조였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 실제로 단일 노드 K3s, KubeVirt, CDI, Longhorn, GHCR, SSH gateway를 모두 연결하다 보니, 단순히 YAML 몇 개를 적용하는 수준이 아니라 여러 계층의 문제를 계속 마주하게 되었습니다. 이 글은 그 과정에서 겪은 문제와, 어떤 방식으로 우회하고 설계를 바꾸었는지에 대한 기록입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;초기 설계: API와 Controller 사이의 gRPC 제거&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;초기에는 &lt;code&gt;kite-api&lt;/code&gt;가 &lt;code&gt;kite-controller&lt;/code&gt;에게 gRPC로 명령을 보내고, controller가 그 요청을 받아 VM을 만드는 구조를 생각했습니다. 하지만 Kubernetes 위에서 controller를 만든다면, API와 controller가 직접 통신하는 것보다 CRD를 중심으로 상태를 맞추는 방식이 더 자연스럽다고 판단했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;따라서 gRPC는 폐기하고, &lt;code&gt;kite-api&lt;/code&gt;는 Kubernetes API server에 &lt;code&gt;KiteUser&lt;/code&gt;, &lt;code&gt;KiteVirtualMachine&lt;/code&gt; CRD를 쓰는 역할만 맡도록 정리했습니다. &lt;code&gt;kite-controller&lt;/code&gt;는 CRD를 watch하면서 실제 Kubernetes 리소스를 reconcile합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_kite_crd_flow&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;사용자 요청
  -&amp;gt; kite-api
  -&amp;gt; KiteUser / KiteVirtualMachine CRD spec 기록
  -&amp;gt; kite-controller watch
  -&amp;gt; Namespace / VM / DataVolume / Service / Secret 생성
  -&amp;gt; CRD status 갱신&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조로 바꾸면서 API process가 죽었다가 다시 살아나도, 이미 etcd에 저장된 CRD spec을 기준으로 controller가 계속 상태를 수렴할 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회원가입과 사용자 CRD&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회원가입 로직에서는 사용자를 일반 DB에 저장하지 않고 &lt;code&gt;KiteUser&lt;/code&gt; CRD로 저장하도록 구성했습니다. 사용자가 처음 가입하면 username, email, password hash, namespace, profile image, access level이 CRD spec에 기록됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;처음 가입한 사용자는 관리자 권한을 주고, 그 이후 가입자는 기본 권한으로 시작하도록 했습니다. password는 평문이 아니라 단방향 hash로 저장하도록 정리했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_kite_user_example&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: hy3ons.github.io/v1
kind: KiteUser
metadata:
  name: ku-599eeabe-009a-4d49-926b-059bf1748a9f
spec:
  username: test
  email: test@gmail.com
  password: &amp;lt;hashed-password&amp;gt;
  namespace: kite-user-ku-599eeabe-009a-4d49-926b-059bf1748a9f
  profile_image: base64encodedimage
  access_level: 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 중요한 점은 CRD 이름을 username 같은 변경 가능한 값으로 두지 않는 것입니다. username은 나중에 바뀔 수 있고, 중복 정책도 바뀔 수 있습니다. 따라서 CRD 이름은 내부 primary key 역할을 하는 안정적인 ID로 두는 방향이 더 좋았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;KubeVirt VM 생성과 CDI DataVolume 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VM 생성은 생각보다 단순하지 않았습니다. &lt;code&gt;KiteVirtualMachine&lt;/code&gt; CRD가 생성되면 controller가 DataVolume, VirtualMachine, SSH Service를 만들어야 했습니다. 처음에는 VM마다 Ubuntu 이미지를 HTTP로 새로 다운로드하는 구조를 고민했지만, VM이 늘어날수록 같은 이미지를 계속 다운로드하는 것은 너무 무거웠습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 &lt;code&gt;kite&lt;/code&gt; namespace에 golden image DataVolume을 하나 만들어두고, 사용자 namespace의 VM disk는 이 golden image PVC를 clone하는 방식으로 바꾸었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_datavolume_flow&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kite/ubuntu-22.04 DataVolume
  -&amp;gt; PVC ubuntu-22.04

사용자 VM DataVolume
  -&amp;gt; source pvc: kite/ubuntu-22.04
  -&amp;gt; 사용자 namespace에 VM disk PVC 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서 CDI CRD가 설치되지 않은 상태에서는 아래와 같은 오류를 만났습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_cdi_not_found&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;no matches for kind &quot;DataVolume&quot; in version &quot;cdi.kubevirt.io/v1beta1&quot;
ensure CRDs are installed first&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해결은 명확했습니다. KubeVirt만 설치해서는 부족하고, CDI도 별도로 설치하고 준비 상태를 기다려야 합니다. 따라서 설치 스크립트에 KubeVirt, CDI 설치 및 wait 단계를 분리해서 넣었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NetworkPolicy가 CDI clone을 막은 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사용자 namespace를 격리하기 위해 NetworkPolicy를 적용했는데, 이 정책이 CDI의 host-assisted clone 통신까지 막아버리는 문제가 있었습니다. local-path나 일부 환경에서는 스냅샷 기반 clone이 되지 않아서 CDI가 source pod와 target pod 사이에서 직접 데이터를 복사합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 사용자 namespace의 egress를 사설 대역으로 막아두면, target pod가 &lt;code&gt;kite&lt;/code&gt; namespace 또는 &lt;code&gt;cdi&lt;/code&gt; namespace와 통신하지 못합니다. 증상은 DataVolume이 계속 Pending에 머무는 형태로 나타났습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_datavolume_pending&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Normal  Pending  datavolume-import-controller  PVC asdf-disk Pending&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;우회 방법은 전체 보안 정책을 풀어버리는 것이 아니라, CDI가 필요한 namespace만 명시적으로 허용하는 것이었습니다. 사용자 namespace 내부 통신은 허용하고, &lt;code&gt;kite&lt;/code&gt;, &lt;code&gt;cdi&lt;/code&gt;, &lt;code&gt;kube-system&lt;/code&gt; DNS 정도만 필요한 만큼 열어주는 방향으로 정리했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Longhorn 단일 노드 환경 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VM disk를 안정적으로 관리하기 위해 Longhorn StorageClass를 사용했습니다. 하지만 단일 노드 환경에서는 Longhorn의 기본 replica 수가 문제가 되었습니다. Longhorn은 기본적으로 여러 replica를 다른 노드에 분산하려고 하는데, 노드가 하나뿐이면 볼륨이 Faulted 상태가 될 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;Longhorn UI에서 확인했을 때 새로 만든 PVC 볼륨들이 &lt;code&gt;Faulted&lt;/code&gt;로 보였고, 실제 DataVolume importer도 제대로 진행되지 않았습니다. 해결은 Kite 전용 StorageClass를 만들고 replica 수를 단일 노드에 맞게 조정하는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_longhorn_storageclass&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: kite-vm-storage
parameters:
  numberOfReplicas: &quot;1&quot;
  diskSelector: &quot;kite&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또한 기존 Longhorn 사용자가 있을 수 있으므로, &lt;code&gt;clear.sh&lt;/code&gt;가 Longhorn 전체를 무조건 지우는 것은 위험했습니다. 따라서 Kite가 할당한 디스크 데이터와 Longhorn 리소스 삭제는 opt-in으로 분리했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_clear_longhorn&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CLEAR_LONGHORN=true KITE_CLUSTER=k3s ./clear.sh
CLEAR_LONGHORN_DATA=true CLEAR_LONGHORN_DATA_CONFIRM=true KITE_CLUSTER=k3s ./clear.sh&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;NodePort 기반 SSH 접속을 버린 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 VM마다 SSH 접속용 NodePort Service를 만들려고 했습니다. Kubernetes가 nodePort를 자동 할당하고, controller가 그 값을 CRD status에 기록하면 된다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만 이 방식은 VM이 늘어날수록 포트를 계속 소모하고, Service가 삭제되지 않으면 고아 포트가 남을 수 있습니다. 또한 사용자가 접속할 때마다 VM별 포트를 기억해야 해서 UX도 좋지 않았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 NodePort를 제거하고, VM별 Service는 ClusterIP로 유지했습니다. 외부 SSH 진입점은 하나의 &lt;code&gt;kite-gateway&lt;/code&gt;가 맡도록 설계를 바꾸었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_gateway_flow&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh &amp;lt;sshId&amp;gt;@&amp;lt;server-ip&amp;gt; -p 22
  -&amp;gt; kite-gateway
  -&amp;gt; KiteVirtualMachine.spec.sshId lookup
  -&amp;gt; vps-access-&amp;lt;vmName&amp;gt;.&amp;lt;namespace&amp;gt;.svc.cluster.local:22
  -&amp;gt; VM sshd&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 하면 사용자 입장에서는 VM마다 포트를 알 필요가 없습니다. username 자체가 라우팅 키가 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;host Linux 계정 방식에서 SSH Gateway 방식으로&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;중간에는 host OS에 Linux 계정을 만들고, 그 계정의 shell을 custom proxy shell로 바꾸는 방식도 고민했습니다. 사용자가 &lt;code&gt;ssh asdf@host&lt;/code&gt;로 들어오면 host 계정의 shell이 실행되고, 그 shell이 내부 VM Service로 다시 SSH를 연결하는 구조였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_host_shell&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash
PRIVATE_KEY=&quot;/home/asdf/.ssh/id_rsa&quot;
VM_TARGET=&quot;asdf@vps-access-asdf.kite-user-xxx.svc.cluster.local&quot;
exec ssh -i &quot;$PRIVATE_KEY&quot; -o StrictHostKeyChecking=no &quot;$VM_TARGET&quot; &quot;$@&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 host OS 계정을 직접 만들기 시작하면 정리 문제가 생깁니다. CRD가 삭제되었는데 host 계정이 남아 있거나, agent가 죽은 동안 finalizer가 끝나면 고아 계정이 생길 수 있습니다. 또한 host의 DNS가 &lt;code&gt;*.svc.cluster.local&lt;/code&gt;을 해석하지 못하는 문제도 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_cluster_dns_fail&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh: Could not resolve hostname vps-access-asdf...svc.cluster.local:
Temporary failure in name resolution&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 host OS를 최대한 건드리지 않는 방향이 맞다고 판단했습니다. 그래서 자체 SSH server인 &lt;code&gt;kite-gateway&lt;/code&gt;를 Kubernetes 내부에 띄우고, 이 gateway가 Kubernetes 내부 DNS를 사용해 VM Service로 연결하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;kite-gateway에서 SSH를 한 번 terminate해야 했던 이유&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 raw TCP L4 proxy처럼 단순히 바이트를 넘기면 가장 가볍지 않을까 생각했습니다. 하지만 사용자를 라우팅하려면 SSH login username을 알아야 합니다. 즉, 어떤 VM으로 보낼지 결정하려면 SSH handshake를 한 번 받아야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 &lt;code&gt;kite-gateway&lt;/code&gt;는 Go의 SSH server를 사용해 클라이언트 SSH 연결을 terminate하고, username/password를 검증합니다. 그 후 backend VM에는 controller가 만든 private key로 SSH client 연결을 열고, 두 SSH channel을 proxy합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_gateway_auth&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;client SSH
  -&amp;gt; kite-gateway SSH server
  -&amp;gt; username = sshId
  -&amp;gt; KiteVirtualMachine route lookup
  -&amp;gt; password hash 검증
  -&amp;gt; VM private key Secret 조회
  -&amp;gt; backend VM SSH 연결
  -&amp;gt; channel proxy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조는 완전한 L4 proxy보다는 복잡하지만, VM 라우팅과 인증을 플랫폼이 제어할 수 있다는 장점이 있었습니다. 또한 VM 내부에는 public key만 cloud-init으로 넣고, private key는 Kubernetes Secret에 보관하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기존 host SSHD와 22번 포트 충돌&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gateway를 22번 포트에 노출하려면 기존 host sshd와 충돌합니다. 처음부터 host sshd를 없애는 것은 위험하므로, 설치 스크립트에서 사용자의 확인을 받고 기존 sshd를 2222로 옮기는 방식으로 정리했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_sshd_handoff&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;기존 host sshd: 22
설치 후 host sshd: 2222
kite-gateway: 22&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또한 gateway가 기존 host 계정 접속도 어느 정도 유지할 수 있도록 fallback을 넣었습니다. &lt;code&gt;KiteVirtualMachine.spec.sshId&lt;/code&gt;와 매칭되는 VM route가 있으면 VM이 우선이고, route가 없으면 gateway가 host의 &lt;code&gt;node-ip:2222&lt;/code&gt;로 SSH 연결을 넘깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_host_fallback&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh hhs2003@host -p 22
  -&amp;gt; kite-gateway
  -&amp;gt; Kite VM route 없음
  -&amp;gt; host sshd node-ip:2222 fallback&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서 gateway host key도 중요했습니다. 사용자는 기존에 host 22번 fingerprint를 신뢰하고 있었기 때문에, gateway가 완전히 다른 host key를 쓰면 경고가 뜹니다. 그래서 gateway Secret을 만들 때 기존 &lt;code&gt;/etc/ssh/ssh_host_*&lt;/code&gt; key를 우선 재사용하도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;cloud-init과 VM 내부 계정 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VM 내부에도 사용자가 로그인할 Linux 계정이 필요했습니다. 처음에는 password 기반 접속도 고민했지만, gateway가 이미 password 인증을 맡고 있으므로 VM 내부는 key 기반 접속으로 가는 편이 더 안전했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;controller는 VM을 만들 때 keypair Secret을 만들고, public key를 cloud-init user-data에 넣습니다. gateway는 Secret의 private key를 읽어서 VM 내부 sshd에 접속합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_cloud_init_user&quot; class=&quot;yaml&quot; data-ke-language=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;users:
  - name: asdf
    groups: sudo
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAA...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 구조에서는 사용자가 외부에서 입력하는 password와 VM 내부 SSH key가 분리됩니다. 외부 password는 route 인증용이고, VM 내부 접속은 플랫폼이 만든 keypair로 처리됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSH 접속 타이밍 문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;VM CRD가 생성되었다고 해서 바로 SSH가 되는 것은 아닙니다. DataVolume clone, VM scheduling, virt-launcher pod 생성, cloud-init 실행, VM 내부 sshd 기동까지 시간이 걸립니다. 이 사이에 gateway가 VM Service로 연결하면 아래와 같은 오류가 나타날 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_ssh_starting_errors&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ssh: connect to host vps-access-asdf... port 22: No route to host
ssh: connect to host vps-access-asdf... port 22: Connection refused&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 메시지를 사용자에게 그대로 보여주면 내부 구현이 노출되고 UX도 좋지 않습니다. 그래서 gateway는 backend 연결을 일정 시간 재시도하고, 아직 준비되지 않았다면 사용자가 이해할 수 있는 메시지를 보여주는 방향으로 정리했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_starting_message&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;VirtualMachine is starting sshd server. Please retry shortly.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;또한 SSH 세션 종료 시 goroutine이나 channel이 제대로 닫히지 않으면 사용자가 &lt;code&gt;exit&lt;/code&gt;를 입력한 뒤 터미널이 멈춘 것처럼 보이는 문제가 있었습니다. 이를 막기 위해 backend channel, request stream, client channel을 명확하게 닫고, exit-status 전달 후 대기 시간을 제한했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GHCR 이미지 배포와 ImagePullBackOff&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배포 이미지는 GitHub Container Registry(GHCR)에 올리고, 설치 스크립트가 &lt;code&gt;ghcr.io/hy3ons/kite-*&lt;/code&gt; 이미지를 pull하도록 구성했습니다. main 브랜치에 push하면 GitHub Actions가 production 이미지를 build/push합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_ghcr_images&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ghcr.io/hy3ons/kite-api:latest
ghcr.io/hy3ons/kite-controller:latest
ghcr.io/hy3ons/kite-gateway:latest
ghcr.io/hy3ons/kite-frontend:latest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 실제 설치 시 Pod가 &lt;code&gt;ImagePullBackOff&lt;/code&gt;에 빠졌습니다. 처음에는 DNS나 k3s containerd 문제를 의심했지만, event를 확인해보니 원인은 GHCR 인증이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_ghcr_401&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;failed to authorize:
failed to fetch anonymous token:
unexpected status from GET request to https://ghcr.io/token... 401 Unauthorized&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, 클러스터가 GHCR까지 접근은 했지만 package가 public pull 가능한 상태가 아니었습니다. 이 문제는 Kubernetes 내부 문제가 아니라 GitHub Packages visibility 문제였습니다. Dockerfile에는 OCI 표준 label을 추가해 GitHub Packages가 source repository와 description을 표시할 수 있게 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_oci_label&quot; class=&quot;dockerfile&quot; data-ke-language=&quot;dockerfile&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LABEL org.opencontainers.image.source=&quot;https://github.com/Hy3ons/KiteVirtualMachines&quot; \
      org.opencontainers.image.description=&quot;Kite SSH gateway for routing user SSH sessions to KubeVirt VM services&quot; \
      org.opencontainers.image.licenses=&quot;MIT&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;익명 설치를 목표로 한다면 GHCR package를 public으로 열어야 하고, private package를 유지하려면 imagePullSecret을 배포 과정에서 만들어야 합니다. 이 구분을 명확히 알게 된 것이 중요한 포인트였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치 스크립트와 클린 스크립트&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음에는 사용자가 repository를 clone한 뒤 &lt;code&gt;./dev.sh&lt;/code&gt;나 &lt;code&gt;./install.sh&lt;/code&gt;를 실행하는 구조였습니다. 하지만 실제 사용자는 git이 없을 수도 있고, 단순히 한 줄 명령으로 설치하고 싶을 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그래서 루트의 &lt;code&gt;install.sh&lt;/code&gt;와 &lt;code&gt;clean.sh&lt;/code&gt;는 GitHub archive를 다운로드해서 내부 스크립트를 실행하는 bootstrap 구조로 바꾸었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_install_clean&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -fsSL https://raw.githubusercontent.com/Hy3ons/KiteVirtualMachines/main/install.sh | bash

curl -fsSL https://raw.githubusercontent.com/Hy3ons/KiteVirtualMachines/main/clean.sh | bash&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 방식은 &lt;code&gt;git clone&lt;/code&gt;이 필요 없습니다. bootstrap script가 선택된 ref의 tar archive를 임시 디렉토리에 풀고, &lt;code&gt;build/deploy/scripts/install-all.sh&lt;/code&gt; 또는 &lt;code&gt;build/dev/clear.sh&lt;/code&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;font-family: 'Noto Serif KR';&quot;&gt;이 과정에서도 작은 쉘 버그를 만났습니다. &lt;code&gt;set -u&lt;/code&gt;가 켜진 상태에서 cleanup trap이 아직 초기화되지 않은 임시 디렉토리 변수를 참조하면 &lt;code&gt;unbound variable&lt;/code&gt;로 실패했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_tmpdir_bug&quot; class=&quot;text&quot; data-ke-language=&quot;text&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;secret/kite-gateway-host-key created
ensure-gateway-host-key-secret.sh: line 1: tmpdir: unbound variable&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해결은 cleanup에서 변수를 직접 참조하지 않고, Bash parameter expansion의 기본값을 사용하는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_tmpdir_fix&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cleanup() {
  if [[ -n &quot;${KITE_INSTALL_TMPDIR:-}&quot; ]]; then
    rm -rf &quot;${KITE_INSTALL_TMPDIR}&quot;
  fi
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 작업을 하면서 가장 크게 느낀 점은, Kubernetes 기반 구현은 단순히 controller 코드만 잘 짜면 끝나는 것이 아니라는 점이었습니다. KubeVirt, CDI, StorageClass, NetworkPolicy, GHCR, SSH, host systemd까지 여러 계층이 서로 영향을 줍니다.&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;font-family: 'Noto Serif KR';&quot;&gt;초기에는 NodePort, host 계정, gRPC, 직접 명령형 API 같은 방향도 고민했지만, 작업을 진행하면서 점점 Kubernetes다운 선언적 구조로 정리되었습니다. API는 CRD spec을 쓰고, controller는 status를 맞추고, gateway는 SSH 진입점 하나만 책임지는 구조가 되었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;특히 문제를 해결할 때마다 단순히 에러 하나를 지우는 것이 아니라, 왜 이 계층에서 문제가 생겼는지 확인하는 과정이 중요했습니다. &lt;code&gt;ImagePullBackOff&lt;/code&gt;는 DNS 문제가 아니라 GHCR visibility 문제였고, DataVolume Pending은 권한 문제가 아니라 NetworkPolicy와 CDI clone 통신 문제였고, SSH 접속 실패는 Service가 없는 것이 아니라 VM 내부 sshd 준비 타이밍 문제였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;결국 Kite는 단일 노드 K3s 환경에서도 VM을 생성하고, 디스크를 준비하고, 사용자가 하나의 SSH 포트로 자기 VM에 들어갈 수 있는 방향으로 정리되었습니다. 아직 고도화할 부분은 남아 있지만, 이 과정 자체가 Kubernetes 위에서 작은 클라우드 플랫폼을 설계할 때 어떤 문제를 만나게 되는지 잘 보여주는 경험이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;무엇보다 이번 작업은 Kubernetes가 계속 이야기하는 선언 상태와 수렴 상태의 의미를 실제로 이해하게 된 경험이었습니다. 사용자가 원하는 상태는 CRD의 spec에 남기고, controller는 현재 클러스터의 상태를 관찰하면서 그 차이를 줄여나갑니다. 문제가 생기면 다시 관찰하고, 필요한 리소스를 만들거나 지우고, status에 결과를 남깁니다. 결국 중요한 것은 한 번 명령을 성공시키는 것이 아니라, 시스템이 원하는 상태로 계속 돌아오게 만드는 것이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;현재 구현은 &lt;a href=&quot;https://github.com/Hy3ons/KiteVirtualMachines&quot;&gt;https://github.com/Hy3ons/KiteVirtualMachines&lt;/a&gt; 에서 진행하고 있습니다. host 서버 쪽 SSH 제어, gateway, CRD 기반 VM 생성 흐름은 큰 방향이 잡혔고, 앞으로는 프론트엔드를 서서히 고도화하면서 사용자 관리, VM 관리, 관리자 기능을 점진적으로 추가해 나갈 예정입니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/54</guid>
      <comments>https://hy3ons.tistory.com/54#entry54comment</comments>
      <pubDate>Mon, 8 Jun 2026 03:28:02 +0900</pubDate>
    </item>
    <item>
      <title>proto3 (gRPC) 에 대하여,</title>
      <link>https://hy3ons.tistory.com/53</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://protobuf.dev/programming-guides/proto3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://protobuf.dev/programming-guides/proto3/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1776546821985&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;Language Guide (proto 3)&quot; data-og-description=&quot;Covers how to use the proto3 revision of the Protocol Buffers language in your project.&quot; data-og-host=&quot;protobuf.dev&quot; data-og-source-url=&quot;https://protobuf.dev/programming-guides/proto3/&quot; data-og-url=&quot;https://protobuf.dev/programming-guides/proto3/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://protobuf.dev/programming-guides/proto3/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://protobuf.dev/programming-guides/proto3/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Language Guide (proto 3)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Covers how to use the proto3 revision of the Protocol Buffers language in your project.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;protobuf.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gRPC &amp;amp; Proto3가 무엇인가요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gRPC(Google Remote Procedure Call)는 구글이 개발한 오픈소스 고성능&amp;nbsp;&lt;b data-index-in-node=&quot;57&quot; data-path-to-node=&quot;4&quot;&gt;RPC(원격 프로시저 호출) 프레임워크&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 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;일반적인 REST API가 HTTP/1.1과 JSON을 사용하여 텍스트 기반으로 통신하는 것과 달리, gRPC는&amp;nbsp;&lt;b data-index-in-node=&quot;63&quot; data-path-to-node=&quot;5&quot;&gt;HTTP/2&lt;/b&gt;를 기반으로 하며&amp;nbsp;&lt;b data-index-in-node=&quot;79&quot; data-path-to-node=&quot;5&quot;&gt;이진(Binary) 데이터&lt;/b&gt;로 통신합니다. 이를 통해 네트워크 효율성이 극대화되고, 서로 다른 언어로 작성된 서비스 간에도 마치 로컬 함수를 호출하듯 매끄럽게 통신할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최근에는 Kubernetes에서 각 Go 프로그램들이 MicroService 형태를 띄고 있고, 그것들이 어떻게 통신을 하는지에 대해 공부하고 있었습니다. 이때, 각 Service들은 gRPC &amp;amp; proto3 라는 프로토콜로 굉장히 빠르게 통신을 하고 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.proto 파일에 대하여,&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.proto&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;&amp;nbsp;파일은 IDL(Interface Definition Language)로, 어떤 데이터를 주고받을지, 어떤 기능을 제공할지를 정의하는 일종의 명세서 입니다. 이 파일 하나만 있으면 서버와 클라이언트가 서로 어떤 언어(Go, Java, Python 등)를 쓰든 상관없이 소통할 수 있게 됩니다.&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;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CHOsj/dJMcacCPeLj/Sxo9QAH7bSi21k4cKiXUGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CHOsj/dJMcacCPeLj/Sxo9QAH7bSi21k4cKiXUGk/img.png&quot; data-alt=&quot;worker.proto 파일에는 다음과 같이&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CHOsj/dJMcacCPeLj/Sxo9QAH7bSi21k4cKiXUGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCHOsj%2FdJMcacCPeLj%2FSxo9QAH7bSi21k4cKiXUGk%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;531&quot; height=&quot;381&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;worker.proto 파일에는 다음과 같이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;JS/TS 에서 흔히, Data를 보내기 전에, 객체의 상태로 Data Transfer Object를 정의하곤 합니다. proto3에서도 유사하게, Transfer 상태들을 다 정의한다고 생각하면 됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;쿠버네티스 내의 여러 Go 서비스들이 서로 통신할 때, &quot;내가 이런 데이터를 보낼 테니, 너는 저런 형식으로 응답해줘&quot; 라는 규칙을 이 파일에 정의합니다. 텍스트 기반의 JSON과 달리,&amp;nbsp;&lt;/span&gt;.proto&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;는 필드에 고유한 번호를 부여하여 이진(Binary) 데이터로 직렬화하기 때문에 압도적인 통신 속도와 효율성을 자랑합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;Go 언어로의 Compile&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;이제 proto확장자를 가진 파일에 IDL을 다 작성했으면, 이것을 Go 언어로 컴파일 해봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선은 각 언어마다, 컴파일 하는 방법이 다릅니다. Go의 경우에는 Go 언어로 컴파일 해주는 protoc을 설치해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1776547529540&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protoc --go_out=. --go-grpc_out=. worker.proto #Go의 경우
protoc --java_out=. --grpc-java_out=. worker.proto #Java의 경우
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. worker.proto&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1776547602284&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protoc --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` worker.proto&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;204&quot; data-origin-height=&quot;86&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caHeB3/dJMcabKHJor/LLgBmnBMCiLvgTqhOSwuS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caHeB3/dJMcabKHJor/LLgBmnBMCiLvgTqhOSwuS0/img.png&quot; data-alt=&quot;Go의 경우, 두개의 파일이 생깁니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caHeB3/dJMcabKHJor/LLgBmnBMCiLvgTqhOSwuS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaHeB3%2FdJMcabKHJor%2FLLgBmnBMCiLvgTqhOSwuS0%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;204&quot; height=&quot;86&quot; data-origin-width=&quot;204&quot; data-origin-height=&quot;86&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Go의 경우, 두개의 파일이 생깁니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Go는 폴더 단위로 패키징 된다. 즉, 저 worker 패키지에는 gRPC 통신에 필요한 Client와 Server Struct를 제공하고 있다고 생각하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실제 Client와 Server 의 사용&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;160&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qQk2H/dJMcadn9mcs/8s61OOWQMl2E185sjrLVpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qQk2H/dJMcadn9mcs/8s61OOWQMl2E185sjrLVpk/img.png&quot; data-alt=&quot;Go로 컴파일 된 패키지를 import 하고, 그 안의 UnimplementedWorkerServiceServer를 임베딩 패턴으로 작성합니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qQk2H/dJMcadn9mcs/8s61OOWQMl2E185sjrLVpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqQk2H%2FdJMcadn9mcs%2F8s61OOWQMl2E185sjrLVpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;160&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;160&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Go로 컴파일 된 패키지를 import 하고, 그 안의 UnimplementedWorkerServiceServer를 임베딩 패턴으로 작성합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선, gRPC를 받는 Server는 proto3에서 명세 된 모든 services들을 구현해야 합니다. worker에서 서버 등록함수를 제공하기 때문에, 우린 그 proto3로 구현된 인터페이스를 Duct type으로 만족시키는 구조체를 작성하여 구현해야합니다. 이때, 임베딩 패턴을 사용합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;만약, gRPC에 새로운 매서드를 추가 했다고 합시다. 그럼, 우리는 코드에서 그 매서드가 무엇을 실제로 하는지 implement 해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선적으로, UnimplementedWorkerServiceServer를 구조체에 포함 할 경우, 모든 메서드를 &quot;아직 구현되지 않음&quot; 에러를 반환하도록 기본적으로 구현됩니다. 따라서, 기존 코드가 잘 작동하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhGwT8/dJMcaadV97R/bIUghlTdjbrgbK3ERSDom1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhGwT8/dJMcaadV97R/bIUghlTdjbrgbK3ERSDom1/img.png&quot; data-alt=&quot;서버를 만드는 함수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhGwT8/dJMcaadV97R/bIUghlTdjbrgbK3ERSDom1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhGwT8%2FdJMcaadV97R%2FbIUghlTdjbrgbK3ERSDom1%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;593&quot; height=&quot;124&quot; data-origin-width=&quot;593&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서버를 만드는 함수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;grpc.Server 객체를 만들 시간입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선 gRPC의 핵심 엔진을 초기화 합니다. 그 후, 자동으로 구현 된 worker패키지에 있는 RegisterWorkerServiceServer에, source인 gRPC서버와, 우리가 실제로 구현한 Server 구조체를 넣어 gRPC서버에 등록합니다. 후, server를 리턴합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HSn9J/dJMcahjQMbx/4iD5NakWOdFOpOY0FfFo90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HSn9J/dJMcahjQMbx/4iD5NakWOdFOpOY0FfFo90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HSn9J/dJMcahjQMbx/4iD5NakWOdFOpOY0FfFo90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHSn9J%2FdJMcahjQMbx%2F4iD5NakWOdFOpOY0FfFo90%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;524&quot; height=&quot;271&quot; data-origin-width=&quot;609&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추후, net을 이용해 tcp통신하는 포트하나를 뚫고, 이곳을 gRPC서버가 사용하도록 하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t1Ax0/dJMcagrFisq/Bdutn6jHwEsmplC2galeRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t1Ax0/dJMcagrFisq/Bdutn6jHwEsmplC2galeRk/img.png&quot; data-alt=&quot;Client입니다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t1Ax0/dJMcagrFisq/Bdutn6jHwEsmplC2galeRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft1Ax0%2FdJMcagrFisq%2FBdutn6jHwEsmplC2galeRk%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;794&quot; height=&quot;272&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Client입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번에는 Client입장입니다. 기본적으로 Go의 gRPC 모듈을 사용하여, gRPC Server에 연결할 수 있는 객체를 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그 후, worker Package 에 있는 NewWorkerServiceClient, Go 로 컴파일 된 Client를 호출하여 사용하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uLvMQ/dJMcagrFisQ/KQI5aFRwYwVZaPqWHdJ3dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uLvMQ/dJMcagrFisQ/KQI5aFRwYwVZaPqWHdJ3dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uLvMQ/dJMcagrFisQ/KQI5aFRwYwVZaPqWHdJ3dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuLvMQ%2FdJMcagrFisQ%2FKQI5aFRwYwVZaPqWHdJ3dK%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;927&quot; height=&quot;361&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;361&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음과 같이, client는 proto3에 명세 된 모든 함수와 인자가 구현되어 있습니다. 따라서, 강력한 타입검사와 더불어, 안정적인 구현이 가능합니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/53</guid>
      <comments>https://hy3ons.tistory.com/53#entry53comment</comments>
      <pubDate>Sun, 19 Apr 2026 06:47:09 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes - Registry:2 적용기</title>
      <link>https://hy3ons.tistory.com/52</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개요 및 상황&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a href=&quot;https://bada.anacnu.kr&quot;&gt;https://bada.anacnu.kr&lt;/a&gt; - 충남대학교 ANA 동아리 내의 실시간 추첨 결과 온보딩 페이지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 홈페이지에 필요한 정보를 백준에서 최소한으로 쿼리해서 처리하고 있었습니다. 하지만, 학내 IP가 solved.ac 에서 Cloudflare 단에서 차단되어, AWS Lambda에서 크롤러를 실행시키고 있었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;그런데, AWS 실습과 더불어, AWS 에서 사용하는 서비스가 점차 늘어나, Crawler라도 줄이기 위해, Crawler를 동아리 내의 온프레미스 서버로 이전하는 과정이 이 포스팅이 되겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;시스템 설계&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우선적으로는 저희의 동아리 서버에는 단일 클러스터 환경에서 돌아가는 K3s 기반의 쿠버네티스가 설치 되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;동아리 서버에 크롤러를 5분 마다 돌려야 하는 것이 이번의 Task 였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;크롤러는 이미 AWS 에 올려놓기 위해, Image로 잘 만들어져 있었습니다. AWS Lambda에 올릴 때는, AWS ECR (Elastic Container Registry) 에 이미지를 올리고, 이미지를 pull 해오는 식으로 람다를 만들었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;추후에, 동아리에 람다나 깃허브 액션과 같은 시스템을 구현하고 싶었습니다. 따라서, AWS Lambda 에 배포할 때와 같은 방식으로 동아리 서버에 배포해보는 과정을 거치는게 좋다고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;첫 번째 과정&lt;/b&gt;은, 일련의 Docker Image 업로드 시스템을 직접 구축하는 것에 있었습니다. 따라서, K3s내에서 Registry:2 Image 를 Deploy하여, 이미지 저장소를 운영하는 것이 첫 번째 Task가 되었습니다. 찾아 본 결과, PVC 관련하여 서비스를 띄우고, 서비스를 참조하여, Registry:2 이미지를 운영해야 Persistance Volume을 유지 할 수 있음을 알았습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;또한 Registry:2 에서 사용할 계정을 만들기 위해, htpasswd 라는 파일형식을 공부 하였습니다. htpasswd 를 사용하여, 계정 정보를 관리하고, 이를 K3s 내의 Secret으로 만들었습니다. Secret을 Registry:2의 컨테이너에 마운트하여 사용하게 하였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;두 번째 과정&lt;/b&gt;은, 이제 만들어놓은 Registry:2 서비스에 잘 접근하게 하는 것입니다. 모든 도커내의 pull과 push는 암호화를 위해 https 형식으로 진행된다는 것을 알고 있습니다. 외부 ip로 나가지 않고 내부에서 image 를 push 하는 것은 http 통신을 허용 설정을 추가로 해야합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;하지만, 외부 에서 접근가능한 Registry:2를 만드는게 더 좋아보였습니다. 이미 소유한 Domain이 있고, 이미 도메인에 대해 와일드카드 인증서를 발급받아서 Ingress에서 잘 사용 중 이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;따라서, 단순하게 Registry:2 에 대해서, Service를 만들고, Ingress Controller를 만들어서, https 통신으로 접근하게 하였습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;세 번째 과정&lt;/b&gt;은, 이제 단순하게 구축된 Registry:2에 현재 Docker Image 를 Push하는 것입니다. 이것은 아주 쉽습니다. Docker login 을 통해 로그인 후, docker tag 로 push할 도메인과 버전명을 적어주고 docker push를 하면 되는 아주 간단한 작업이었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;네 번째 과정&lt;/b&gt;은, 이제 cronJob을 통해서, 5분마다 해당 이미지의 컨테이너를 띄워 실행시키는 것이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Deployment 비슷한 걸 하면 되는 아주 기초적인 작업이었습니다. 후에, 크롤러가 잘 연산을 하는 것을 볼 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Registry:2 를 띄우기 위해, 우선은 인증정보 부터 생성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775638178785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;htpasswd -Bc auth admin&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;admin 이라는 계정에 비밀번호는 임의로 설정하여, auth 라는 이름의 htpasswd 파일을 생성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775638216818&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create secret generic registry-auth --from-file=&amp;lt;만들어진 htpasswd 파일 경로&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 이름이 registry-auth 라는 secret Resource를 만들었습니다. 후에, 이 정보를 Registry:2 에 마운트하여 사용하게 할 것입니다.&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;font-family: 'Noto Serif KR';&quot;&gt;선행작업을 모두 작업했습니다. 이제는 Registry:2 띄울 것입니다. 도커 이미지 저장소를 하는 프로그램을 띄운다고 생각하면 됩니다. 하지만, 모종의 이유로 파드가 죽고 부활 했을 때, 데이터가 소실 되면 안됩니다.&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;font-family: 'Noto Serif KR';&quot;&gt;따라서, PersistentVolumeClaim (PVC)를 띄워, Docker Image가 저장될 공간을 예약 설정하였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775637845882&quot; class=&quot;yaml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음과 같은 PVC Resource를 생성하였습니다. 후에는 이 Resource 를 사용하는 Registry:2를 띄워야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775638111839&quot; class=&quot;yaml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: docker-registry

spec:
  replicas: 1 # 레플리카 1로 설정

  selector:
    matchLabels:
      app: registry # 셀렉터 설정

  template:

    metadata:
      labels:
        app: registry # 레이블 설정

    spec:
      containers:
      - name: registry
        image: registry:2

        ports:
        - containerPort: 5000 # 컨테이너 포트 설정

        env:
        - name: REGISTRY_AUTH
          value: &quot;htpasswd&quot; # 인증 방식 설정
        - name: REGISTRY_AUTH_HTPASSWD_REALM
          value: &quot;Registry Realm&quot; # 인증 방식 설정
        - name: REGISTRY_AUTH_HTPASSWD_PATH
          value: &quot;/auth/htpasswd&quot; # 인증에 필요한 아이디 정보가 담긴 파일

        volumeMounts:
        - name: storage
          mountPath: /var/lib/registry # 컨테이너 내의 레지스트리 저장소 경로

        - name: auth-volume
          mountPath: /auth # 컨테이너 내의 인증 정보 경로
          readOnly: true

      volumes:
      - name: storage
        persistentVolumeClaim:
          claimName: registry-pvc

      - name: auth-volume
        secret:
          secretName: registry-auth # 인증 정보가 담긴 시크릿&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;deployment로 Registry:2 만 하면 편하겠으나, 파드가 죽었다가 다시 생성되면, 모든 저장소가 초기화됩니다. 따라서, PVC를 선언하고, 컨테이너의 파일을 PVC 에 Mount 시켜야 합니다.&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;font-family: 'Noto Serif KR';&quot;&gt;추가로, 우리가 생성한 registry-auth 라는 secret Resource를 컨테이너 안에 mount 시켰습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775639029749&quot; class=&quot;yaml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: registry-service
spec:
  type: NodePort
  selector:
    app: registry
  ports:
    - port: 5000
      targetPort: 5000
      nodePort: 32000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;추가로, selector를 사용하여, Service를 하나 만들었습니다. 추후에, Ingress를 사용할 때는, 이 Service를 참조하여 사용하면 되겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1775639257790&quot; class=&quot;yaml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: registry-ingress
  annotations:

    kubernetes.io/ingress.class: traefik
    # k3s 기본 Traefik 사용 시

    # 이미지 업로드 용량이 크므로 제한을 늘려줍니다 (필요 시)
    ingress.kubernetes.io/proxy-body-size: &quot;0&quot; 

    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: &quot;true&quot;

    #traefik.ingress.kubernetes.io/router.tls.certresolver: &quot;letsencrypt&quot; # 또는 사용 중인 인증서 리졸버

    # traefik Interceptor 설정. 기존 인클러스터 백엔드에 넘겨줌. (지금은 비활성화)
    #traefik.ingress.kubernetes.io/router.middlewares: cloud-admin-vm-traffic-interceptor@kubernetescrd
    
spec:
  tls:
  - hosts:
    - docker.anacnu.com

  rules:
  - host: docker.anacnu.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: registry-service
            port:
              number: 5000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이런식으로, pvc - deployment - service - Ingress 를 생성하면, 접속 할 수 있게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vyWDM/dJMcabKzYJg/kImFFP2OKBysYfBe8ZPG1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vyWDM/dJMcabKzYJg/kImFFP2OKBysYfBe8ZPG1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vyWDM/dJMcabKzYJg/kImFFP2OKBysYfBe8ZPG1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvyWDM%2FdJMcabKzYJg%2FkImFFP2OKBysYfBe8ZPG1k%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;1782&quot; height=&quot;368&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;368&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이런식으로, 이제 자체적인 Registry:2 저장소를 구축할 수 있게 되었습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;마지막으로는 docker tag를 통해 push 할 이미지에 태그를 달아주어야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;lt;domain.com&amp;gt;/&amp;lt;Image Name&amp;gt;:&amp;lt;Version&amp;gt; 형식으로 태그를 달아주고 push 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775639562849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;kubectl create secret docker-registry regcred \
  --docker-server=docker.anacnu.com \
  --docker-username=&amp;lt;name&amp;gt; \
  --docker-password=&amp;lt;password&amp;gt; \
  --docker-email=&amp;lt;email&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만, 쿠버네티스가 이미지를 가져올 때, 이 이미지에 접근 할 수 있는 권리가 없습니다. 따라서, 접근 할 수 있는 정보들을 또 secret으로 만들고, Image를 끌어올 때, 필요한 정보들을 넘겨 주어야 합니다. 위에 명령어는 regcred라는 secret을 만드는 명령어 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775639482933&quot; class=&quot;yaml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: bada-crawler
spec:
  schedule: &quot;*/5 * * * *&quot; 

  jobTemplate:
    spec:
      template:
        spec:
          imagePullSecrets:
          - name: regcred
          
          containers:
          - name: bada-crawler-worker
            image: docker.anacnu.com/bada-crawler:latest
          
          # OnFailure: 실패했을 때만 재시작, Never: 재시작 안 함 (작업이 끝나면 끝)
          restartPolicy: Never&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이미지를 끌어 올 때, imagePullSecrets 에 해당 secret을 넘겨 사용할 수 있습니다. 이러면 Image 를 잘 끌어와서 실행 시키는 모습을 볼 수 있습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;후, CronJob이라는 컴포넌트를 만들어서, 컨테이너 실행을 스케줄링 화 시켰습니다. 따라서, 이제 선언적 구성으로, Image를 실행할 수 있게 되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/87b7E/dJMcahDX9ue/3A7XREekcYGjICUhvNhiU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/87b7E/dJMcahDX9ue/3A7XREekcYGjICUhvNhiU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/87b7E/dJMcahDX9ue/3A7XREekcYGjICUhvNhiU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F87b7E%2FdJMcahDX9ue%2F3A7XREekcYGjICUhvNhiU0%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;898&quot; height=&quot;515&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;515&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음과 같이 모든 생성된 자원들 입니다. docker-registry 가 떠있고, crawler가 최대 3개까지 로그화 되어 보관되어 있는 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;레지스트리 저장소를 실행시키는 것은, 서버 환경에서 코드를 pull하고 env를 다 넣은다음에, 빌드하여 사용하는 것 보다는, 로컬 환경에서 빌드한 결과물을 push 하는 방식이 앞으로 생산성 측면에서 좋을 것이라고 생각했습니다.&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;font-family: 'Noto Serif KR';&quot;&gt;또한, 다양한 사람들이 직접 서버에 와서 코드를 가져와서 이미지로 빌드하는 것보다는 각각의 컴퓨터에서 Image를 빌드해서 push하는 것이 더 편하고 좋을 것 같았습니다. 물론 docker build 시에 &amp;nbsp;--platform은 linux/amd64로 해야합니다. (mac 기준)&lt;/span&gt;&lt;/p&gt;</description>
      <category>Kubernetes</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/52</guid>
      <comments>https://hy3ons.tistory.com/52#entry52comment</comments>
      <pubDate>Wed, 8 Apr 2026 18:18:44 +0900</pubDate>
    </item>
    <item>
      <title>소프트웨어 마에스트로 17기 잡담</title>
      <link>https://hy3ons.tistory.com/51</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4pQ8F/dJMcaibGOmR/TNhyjxJtvI3SFFHqZm0Wzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4pQ8F/dJMcaibGOmR/TNhyjxJtvI3SFFHqZm0Wzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4pQ8F/dJMcaibGOmR/TNhyjxJtvI3SFFHqZm0Wzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4pQ8F%2FdJMcaibGOmR%2FTNhyjxJtvI3SFFHqZm0Wzk%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;252&quot; height=&quot;155&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아이디어 고도화와 마케팅 등을 거쳐 수익화를 낼 수 있을 정도로&amp;nbsp;&lt;b&gt;완성도 있는 서비스를&lt;/b&gt;&amp;nbsp;만들어 보고, 여러 좋은 사람들과&amp;nbsp;&lt;b&gt;협업을 진행하고&lt;/b&gt;, 나 나름도 이제 CS기술과 개념 및 이론들을 한번에 정리하는 시간을 갖기 위해, 소프트웨어 마에스트로 17기에 신청했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/calRtR/dJMcabXU4uP/foFEHWu5FrKOSrdl8icIAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/calRtR/dJMcabXU4uP/foFEHWu5FrKOSrdl8icIAk/img.png&quot; data-alt=&quot;2026년 17기 선발 일정은 다음과 같다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/calRtR/dJMcabXU4uP/foFEHWu5FrKOSrdl8icIAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcalRtR%2FdJMcabXU4uP%2FfoFEHWu5FrKOSrdl8icIAk%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;2048&quot; height=&quot;476&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2026년 17기 선발 일정은 다음과 같다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;a title=&quot;충남대학교 SW마에스트로 추천 안내&quot; href=&quot;https://computer.cnu.ac.kr/computer/notice/project.do?mode=view&amp;amp;articleNo=574578&amp;amp;article.offset=0&amp;amp;articleLimit=10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://computer.cnu.ac.kr/computer/notice/project.do?mode=view&amp;amp;articleNo=574578&amp;amp;article.offset=0&amp;amp;articleLimit=10&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1774527608281&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;공지사항  &amp;gt;  사업단소식 게시판읽기 ( 우수학생 추천 - 2026년도 AI SW마에스트로 과정 제17기 연수&quot; data-og-description=&quot;아래 2026년도 SW마에스트로 과정 제17기 연수생 모집 건과 관련하여소프트웨어중심대학사업단에서 최대 3명의 학생에게 &amp;quot;우수학생추천서&amp;quot;를 제공하고자 합니다.세상을 움직이는 최고급 SW인재 &quot; data-og-host=&quot;computer.cnu.ac.kr&quot; data-og-source-url=&quot;https://computer.cnu.ac.kr/computer/notice/project.do?mode=view&amp;amp;articleNo=574578&amp;amp;article.offset=0&amp;amp;articleLimit=10&quot; data-og-url=&quot;https://computer.cnu.ac.kr/computer/notice/project.do?article.offset=0&amp;amp;articleLimit=10&amp;amp;articleNo=574578&amp;amp;mode=view&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://computer.cnu.ac.kr/computer/notice/project.do?mode=view&amp;amp;articleNo=574578&amp;amp;article.offset=0&amp;amp;articleLimit=10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://computer.cnu.ac.kr/computer/notice/project.do?mode=view&amp;amp;articleNo=574578&amp;amp;article.offset=0&amp;amp;articleLimit=10&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;공지사항 &amp;gt; 사업단소식 게시판읽기 ( 우수학생 추천 - 2026년도 AI SW마에스트로 과정 제17기 연수&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;아래 2026년도 SW마에스트로 과정 제17기 연수생 모집 건과 관련하여소프트웨어중심대학사업단에서 최대 3명의 학생에게 &quot;우수학생추천서&quot;를 제공하고자 합니다.세상을 움직이는 최고급 SW인재&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;computer.cnu.ac.kr&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위는 충남대학교 2026년 SW마에스트로 관련하여, 우수학생을 추천해준다는 공지였다. 일단 저는 상남자기 때문에, 이런 우수전형은 쓰지 않았습니다.&lt;/span&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;26. 2. 28 - 1차 코딩 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Programmers라는 곳에서 코딩 테스트를 진행 하였고, Vim이 사용가능해서, 코딩하면서 본실력을 꽤 발휘했던 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아주 기초적인 구현문제 4문제 &lt;span style=&quot;color: #9d9d9d;&quot;&gt;&lt;i&gt;(실버5-실버1)&lt;/i&gt;&lt;/span&gt; 와 SQL문제가 나왔으며, 문제는 진짜 이사람이 코딩을 할 줄 아는 사람인지 정도를 검증했던것 같다. 1시간동안 다 풀었다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;26. 3. 7 - 2차 코딩 테스트&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;Programmers 라는 곳에서 2차 코딩 테스트를 진행했습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;문제 1 : 구현 (골드 5)&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;문제 2 : dp (골드 5)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;문제 3 : priority_queue, bfs, treee (골드 5)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;문제 4 : dp (골드 1 이상)&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: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;4문제가 나왔으며, 문제 4번은 정말 차분히 dp의 정석대로 따라가기만 하면 풀리는 스탠다드 하면서 굉장히 좋은 문제였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;SQL은 시간이 없어, 보지 못했습니다. 뭐, Unsigned 컬럼을 signed로 캐스팅하는게 다들 어려웠다고 하더라고요.&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: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;코딩테스트를 준비하면서, SQL은 3일전부터 모든 윈도우 함수를 외우고, SQL고득점 킷을 다 풀었습니다. 풀면서, 알고리즘 비슷하게 푸는 맛이 있었습니다. 재귀를 사용해서 bfs 비스무리한 걸 해본다던지, SQL 고득점 Kit에는 좋은 문제들이 많았습니다.&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: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;그럼에도 중요한 사실은 지금은 SQL 윈도우 함수를 모두 까먹었다는 것입니다.&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: #333333; text-align: start; font-family: 'Noto Serif KR';&quot;&gt;요즘 AI 시대에는 굉장히 deep한 도메인 지식을 공부하는 것이 중요하지, 단순히 SQL 문법을 외우는건 중요하지 않다고 생각합니다. 중요한건 포기하지 않고 문제를 해결하려는 의지와 끈기라고 생각하고, 알고리즘을 잘한다는 건 요즘 세상에서 중요한 요소를 오히려 더 잘 보여줄 수 있는게 아닐까 싶습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;854&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/r8uh1/dJMcagx8gJd/6PQ30cVFgPByDOAwNKcvP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/r8uh1/dJMcagx8gJd/6PQ30cVFgPByDOAwNKcvP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/r8uh1/dJMcagx8gJd/6PQ30cVFgPByDOAwNKcvP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fr8uh1%2FdJMcagx8gJd%2F6PQ30cVFgPByDOAwNKcvP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;333&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;854&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;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코딩테스트 2차를 통과하고 저는 다음과 같은 메일을 받았습니다. 거의 첫 타임, 첫 면접으로 면접을 보게 되었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;26. 3. 19 - 3차 심층 면접&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;2261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bd4UJo/dJMcadOT7GS/hkl5evIXeX03SvrNlLRmX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bd4UJo/dJMcadOT7GS/hkl5evIXeX03SvrNlLRmX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bd4UJo/dJMcadOT7GS/hkl5evIXeX03SvrNlLRmX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbd4UJo%2FdJMcadOT7GS%2Fhkl5evIXeX03SvrNlLRmX0%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;287&quot; height=&quot;383&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1706&quot; data-origin-height=&quot;2261&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;span style=&quot;font-family: 'Noto Serif KR';&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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;저는 여태까지 경험이 전무 한 상태였기 때문에, 최근에 진행한 &lt;b&gt;K8s를 api 단에서 제어하는&lt;/b&gt; 프로젝트랑 &lt;b&gt;협업을 보여주는&lt;/b&gt; 프로젝트를 선택하여, 포트폴리오를 작성했습니다. 같은 면접장 사람들은 대학원생들이 쟁쟁해서 저의 프로젝트는 정말 장난감 수준이었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기억에 남는 것은, 어떤분은 신발 12000장 사진만을 가지고 각 사진에 메타데이터를 생성하는 Task 일화가 기억에 남습니다. 전 AI와 관련한 실력이 미천하여, 그분이 어떤식으로 문제를 해결했는지는 지금와서는 기억이 잘 안나지만, 그때 저는 나였으면 어떻게 했을까? 를 진지하게 고민하고 있었던 생각이 듭니다.&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;font-family: 'Noto Serif KR';&quot;&gt;AI SW마에스트로 여서 그런지, AI에 관련된 질문이 정말 많았습니다. 저는 AI 모델에 관하여, 단순 사용하여 프로덕트를 한번 만든 적이있어서, 그에 관련한 경험을 한 문장으로 간결하게 사용해본적있다라고 적었습니다. 면접관님들은 저의 다른 프로젝트보다는 그 AI 서비스 프로젝트에 대해 질문을 많이 주셨습니다.&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;font-family: 'Noto Serif KR';&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;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 1년은 제가 많이 성장할 수 있는 시간이 되었으면 좋겠습니다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>황현석 어록</category>
      <author>황현석</author>
      <guid isPermaLink="true">https://hy3ons.tistory.com/51</guid>
      <comments>https://hy3ons.tistory.com/51#entry51comment</comments>
      <pubDate>Thu, 26 Mar 2026 23:32:31 +0900</pubDate>
    </item>
  </channel>
</rss>