<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Compile-Time Instrumentation on jyukki's Blog</title><link>https://jyukki.com/tags/compile-time-instrumentation/</link><description>Recent content in Compile-Time Instrumentation on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/compile-time-instrumentation/index.xml" rel="self" type="application/rss+xml"/><item><title>2026 개발 트렌드: Go Compile-Time Instrumentation, 관측성이 빌드 파이프라인으로 들어간다</title><link>https://jyukki.com/posts/2026-07-18-go-compile-time-instrumentation-trend/</link><pubDate>Sat, 18 Jul 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/posts/2026-07-18-go-compile-time-instrumentation-trend/</guid><description>OpenTelemetry Go Compile-Time Instrumentation v1 발표를 바탕으로, Go 서비스 관측성이 런타임 에이전트나 수동 계측을 넘어 빌드 파이프라인의 표준 단계로 이동하는 이유를 정리합니다.</description><content:encoded><![CDATA[<p>2026년 7월 16일 OpenTelemetry는 Go Compile-Time Instrumentation v1을 발표했습니다. Go 서비스 운영자에게 꽤 중요한 신호입니다. Java, Python, Node.js, .NET은 오래전부터 런타임 에이전트나 자동 계측 선택지가 많았지만, Go는 정적 바이너리로 컴파일되는 특성 때문에 같은 방식이 쉽지 않았습니다. 그래서 Go 팀은 수동으로 OpenTelemetry API를 붙이거나, 프로세스 밖에서 eBPF 기반 도구로 관측하는 선택을 많이 해왔습니다.</p>
<p>이번 v1의 핵심은 빌드 단계입니다. OpenTelemetry 쪽 설명에 따르면 <code>otelc</code>라는 도구가 Go toolchain의 <code>-toolexec</code> 경로를 활용해 컴파일 중 애플리케이션 코드, 의존성, 표준 라이브러리에 계측을 주입합니다. 사용자는 <code>go build</code> 대신 <code>otelc go build</code>를 실행하거나, <code>GOFLAGS</code>에 toolexec 설정을 넣는 식으로 빌드 경로를 바꿉니다. 즉 관측성이 런타임에 붙는 에이전트나 소스 코드 변경이 아니라 <strong>빌드 파이프라인의 계약</strong>으로 들어오는 흐름입니다.</p>
<p>이 글은 <a href="/posts/2026-07-01-opentelemetry-native-data-plane-trend/">OpenTelemetry 네이티브 데이터 플레인</a>, <a href="/learning/deep-dive/deep-dive-opentelemetry/">OpenTelemetry 실무 가이드</a>, <a href="/learning/deep-dive/deep-dive-distributed-tracing-adoption-playbook/">분산 트레이싱 도입 플레이북</a>, <a href="/learning/deep-dive/deep-dive-ebpf-production-debugging-playbook/">eBPF 기반 프로덕션 디버깅</a>과 이어집니다.</p>
<p>참고 신호:</p>
<ul>
<li>OpenTelemetry Blog, Announcing v1 of OpenTelemetry Go Compile-Time Instrumentation: <a href="https://opentelemetry.io/blog/2026/go-compile-time-instrumentation-v1/">https://opentelemetry.io/blog/2026/go-compile-time-instrumentation-v1/</a></li>
<li>OpenTelemetry Blog, The Voyage of a Small Environment Variable: <a href="https://opentelemetry.io/blog/2026/spring-boot-declarative-config/">https://opentelemetry.io/blog/2026/spring-boot-declarative-config/</a></li>
</ul>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>Go에서 자동 계측이 왜 다른 언어보다 어려웠고, 컴파일타임 계측이 어떤 빈칸을 채우는지 이해합니다.</li>
<li>수동 계측, 컴파일타임 계측, eBPF/OBI를 어떤 기준으로 나눠 쓸지 판단할 수 있습니다.</li>
<li><code>otelc</code>를 빌드 파이프라인에 넣기 전 검증해야 할 p95 latency, span cardinality, build reproducibility 기준을 가져갑니다.</li>
<li>관측성 표준이 코드 라이브러리 선택을 넘어 CI/CD와 플랫폼 정책으로 확장되는 흐름을 파악합니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-go의-자동-계측-문제는-런타임-구조에서-출발한다">1) Go의 자동 계측 문제는 런타임 구조에서 출발한다</h3>
<p>Java나 .NET은 런타임에 에이전트를 붙여 클래스 로딩, method interception, framework hook을 활용할 수 있습니다. Python과 Node.js도 동적 런타임 특성 덕분에 import나 monkey patch 기반 계측이 가능합니다. Go는 다릅니다. 배포 결과물이 정적 바이너리이고, 운영 시점에 attach할 풍부한 런타임 계층이 없습니다.</p>
<p>그래서 Go에서 자동 계측은 오래도록 세 갈래였습니다.</p>
<table>
  <thead>
      <tr>
          <th>방식</th>
          <th>장점</th>
          <th>한계</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>수동 OTel API 계측</td>
          <td>도메인 의미와 span 품질이 좋음</td>
          <td>팀별 구현 편차와 도입 비용</td>
      </tr>
      <tr>
          <td>eBPF/OBI</td>
          <td>재빌드 없이 외부에서 관측 가능</td>
          <td>애플리케이션 내부 도메인 의미는 제한적</td>
      </tr>
      <tr>
          <td>컴파일타임 계측</td>
          <td>재빌드 가능 서비스에 zero-code coverage 제공</td>
          <td>빌드 파이프라인 변경과 coverage 검증 필요</td>
      </tr>
  </tbody>
</table>
<p>이번 v1은 세 번째 축을 안정화한 신호입니다. 런타임에 붙일 곳이 없다면 빌드할 때 넣자는 접근입니다. 플랫폼팀 입장에서는 이 방식이 매력적입니다. 서비스팀에게 모든 HTTP handler, DB client, Redis client, gRPC client 계측 코드를 직접 쓰라고 요구하지 않아도 기본 trace와 metric을 얻을 수 있기 때문입니다.</p>
<h3 id="2-빌드-한-줄-교체는-운영-변화의-시작일-뿐이다">2) 빌드 한 줄 교체는 운영 변화의 시작일 뿐이다</h3>
<p>OpenTelemetry 발표 글은 <code>otelc go build</code>처럼 빌드 명령을 바꾸는 간단한 시작점을 제시합니다. 하지만 운영팀이 봐야 할 것은 명령의 간단함이 아니라 결과물의 변화입니다.</p>
<p>검증할 항목은 최소 아래입니다.</p>
<ul>
<li>build time이 기준선 대비 얼마나 늘어나는가</li>
<li>binary size가 얼마나 증가하는가</li>
<li>HTTP, <code>database/sql</code>, gRPC, Redis 같은 주요 라이브러리가 실제로 span을 내는가</li>
<li>기존 수동 span과 중복 span이 생기지 않는가</li>
<li>attribute cardinality가 높아져 저장 비용이 급증하지 않는가</li>
<li>p95 latency와 CPU overhead가 기준선 대비 허용 범위 안인가</li>
<li>계측 실패 시 일반 <code>go build</code>로 되돌릴 수 있는가</li>
</ul>
<p>권장 기준은 staging에서 최소 1주일, 상위 3개 요청 경로, 정상/에러/slow request를 나눠 비교하는 것입니다. p95 latency가 기준선 대비 3~5% 이상 오르거나, span 수가 2배 이상 늘면서 분석 품질이 좋아지지 않는다면 바로 전체 적용하기보다 rule과 sampling을 조정해야 합니다.</p>
<h3 id="3-자동-span은-기술-경계를-채우고-수동-span은-업무-의미를-채운다">3) 자동 span은 기술 경계를 채우고, 수동 span은 업무 의미를 채운다</h3>
<p>컴파일타임 계측이 안정화되어도 수동 계측이 사라지지는 않습니다. 자동 계측은 <code>GET /orders</code>, <code>SELECT</code>, <code>redis.get</code>, <code>grpc.client</code> 같은 기술 경계를 잘 채웁니다. 하지만 서비스 운영자가 장애 때 묻는 질문은 더 업무적입니다.</p>
<ul>
<li>주문 승인 단계가 payment gateway 대기 때문에 늦었는가</li>
<li>정산 마감 job이 어떤 tenant에서 멈췄는가</li>
<li>권한 판정 cache miss가 특정 role에서만 늘었는가</li>
<li>retry가 사용자 요청 deadline 안에서 몇 번 발생했는가</li>
</ul>
<p>이런 의미는 자동 계측만으로 충분하지 않습니다. 그래서 실무 기준은 &ldquo;자동 계측으로 경계를 넓히고, 수동 계측으로 의미를 깊게 만든다&quot;입니다. <a href="/learning/deep-dive/deep-dive-distributed-tracing-adoption-playbook/">분산 트레이싱 도입 플레이북</a>에서도 초기 span은 HTTP/gRPC, DB, Redis, 큐 같은 경계부터 잡으라고 했습니다. 컴파일타임 계측은 이 초기 coverage를 빠르게 확보하는 도구가 될 수 있습니다.</p>
<h3 id="4-ebpf와-경쟁하는-것이-아니라-위치가-다르다">4) eBPF와 경쟁하는 것이 아니라 위치가 다르다</h3>
<p>OpenTelemetry는 v1 발표에서 컴파일타임 계측, eBPF/OBI, 수동 계측을 상호 보완적인 선택지로 설명합니다. 이 구분이 중요합니다. 재빌드 가능한 사내 Go 서비스라면 compile-time instrumentation이 좋을 수 있습니다. 반대로 외부 vendor binary, 레거시 배포물, 소스를 통제하지 못하는 에이전트는 빌드 타임에 손댈 수 없습니다. 이때는 eBPF/OBI가 더 현실적입니다.</p>
<p>또 eBPF는 애플리케이션 코드 밖에서 커널, 네트워크, syscall, off-CPU 같은 신호를 볼 수 있습니다. 컴파일타임 OTel은 애플리케이션과 라이브러리 호출 경계를 더 잘 보여줍니다. 장애 분석에서는 둘이 같이 필요할 수 있습니다. 예를 들어 trace상 <code>net/http</code> client span이 길어졌고, 같은 시간대 eBPF에서 TCP retransmit이 평시의 2배라면 애플리케이션 코드보다 네트워크 경로를 먼저 봐야 합니다.</p>
<p><a href="/learning/deep-dive/deep-dive-ebpf-production-debugging-playbook/">eBPF 기반 프로덕션 디버깅</a>의 기준을 적용하면, compile-time span은 &ldquo;어느 요청과 dependency가 느렸는가&quot;를 보여주고, eBPF는 &ldquo;왜 커널/네트워크 레벨에서 기다렸는가&quot;를 좁혀 줍니다.</p>
<h3 id="5-관측성은-점점-설정과-빌드-계약으로-이동한다">5) 관측성은 점점 설정과 빌드 계약으로 이동한다</h3>
<p>같은 주 OpenTelemetry는 Spring Boot starter의 declarative configuration 지원도 소개했습니다. Spring Boot starter 2.26.0부터 OpenTelemetry SDK의 YAML schema를 <code>application.yaml</code> 안의 <code>otel:</code> 아래에 둘 수 있다는 흐름입니다. Java 쪽에서는 설정이 env var 목록이나 코드 Bean에서 선언적 설정으로 이동하고, Go 쪽에서는 계측이 소스 코드 변경에서 빌드 도구 단계로 이동합니다.</p>
<p>두 신호를 묶으면 방향이 보입니다. 관측성은 이제 &ldquo;각 팀이 라이브러리 조금 붙이는 일&quot;이 아니라 플랫폼 표준입니다. 어떤 SDK 설정을 허용할지, 어떤 sampler를 쓸지, 어떤 빌드 단계에서 자동 계측을 넣을지, 어떤 attribute를 금지할지, 어떤 Collector pipeline으로 보낼지가 함께 관리됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-go-서비스-inventory부터-만든다">1) Go 서비스 inventory부터 만든다</h3>
<p>도입 전에는 Go 서비스 목록을 먼저 나눕니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">go_observability_inventory</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">service</span>: payment-api
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">build_control</span>: internal_dockerfile
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">current_instrumentation</span>: manual_partial
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">critical_paths</span>: [<span style="color:#f1fa8c">&#34;approve-payment&#34;</span>, <span style="color:#f1fa8c">&#34;refund&#34;</span>, <span style="color:#f1fa8c">&#34;settlement-callback&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">dependencies</span>: [<span style="color:#f1fa8c">&#34;net/http&#34;</span>, <span style="color:#f1fa8c">&#34;database/sql&#34;</span>, <span style="color:#f1fa8c">&#34;grpc&#34;</span>, <span style="color:#f1fa8c">&#34;redis&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">trace_coverage</span>: <span style="color:#f1fa8c">&#34;55%&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">p95_latency_budget_ms</span>: <span style="color:#bd93f9">300</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">candidate</span>: compile_time_instrumentation_pilot
</span></span></code></pre></div><p>핵심 기준은 재빌드 가능성입니다. 소스와 Dockerfile, CI workflow를 통제할 수 있는 서비스부터 시작합니다. vendor binary나 appliance 형태는 다른 접근이 맞습니다.</p>
<h3 id="2-staging-비교는-span-diff로-닫는다">2) staging 비교는 span diff로 닫는다</h3>
<p>단순히 &ldquo;trace가 더 많이 나온다&quot;는 성공이 아닙니다. 기존 빌드와 <code>otelc</code> 빌드를 나란히 돌리고 대표 요청을 replay합니다.</p>
<p>비교표는 아래처럼 둡니다.</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>통과 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>build time 증가</td>
          <td>20% 이하에서 시작</td>
      </tr>
      <tr>
          <td>binary size 증가</td>
          <td>10% 이하 또는 명시 승인</td>
      </tr>
      <tr>
          <td>p95 latency 증가</td>
          <td>3~5% 이하</td>
      </tr>
      <tr>
          <td>trace coverage</td>
          <td>핵심 경로 80% 이상</td>
      </tr>
      <tr>
          <td>duplicate span rate</td>
          <td>5% 이하</td>
      </tr>
      <tr>
          <td>forbidden attribute</td>
          <td>0건</td>
      </tr>
      <tr>
          <td>Collector ingest 증가</td>
          <td>30% 이하 또는 샘플링 조정</td>
      </tr>
  </tbody>
</table>
<p>span diff를 사람이 봐야 합니다. 자동 계측이 만든 span 이름이 너무 세밀하거나, URL path에 고카디널리티 값이 들어가거나, DB statement가 과하게 노출되면 운영 품질이 떨어집니다. 이 부분은 <a href="/posts/2026-07-01-opentelemetry-native-data-plane-trend/">OpenTelemetry 네이티브 데이터 플레인</a>의 cardinality와 wrapper audit 관점과 이어집니다.</p>
<h3 id="3-cicd에는-버전-고정과-fallback을-넣는다">3) CI/CD에는 버전 고정과 fallback을 넣는다</h3>
<p>빌드 파이프라인에 들어간 도구는 운영 의존성입니다. <code>otelc@latest</code>를 매번 받는 구조는 피하는 편이 좋습니다. 최소한 버전 고정, checksum, 캐시, 실패 시 fallback 기준을 둡니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#ff79c6">RUN</span> go install go.opentelemetry.io/otelc/tool/cmd/otelc@v1.0.0
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">RUN</span> otelc go build -trimpath -o /out/app ./cmd/server
</span></span></code></pre></div><p>fallback 기준도 명시합니다.</p>
<ul>
<li>보안 패치 긴급 배포에서 <code>otelc</code> 자체가 실패하면 일반 <code>go build</code>로 우회 가능</li>
<li>우회 배포는 관측성 coverage 하락으로 기록하고 24시간 안에 복구 티켓 생성</li>
<li><code>otelc</code> 버전 업그레이드는 서비스 코드 변경처럼 staging diff와 canary 필요</li>
<li>계측 rule 변경은 빌드 산출물 차이와 trace schema 변경으로 취급</li>
</ul>
<p>관측성 도구가 빌드를 막는 순간 팀은 우회하고 싶어집니다. 우회를 금지하기보다, 우회 조건과 복구 시간을 정해 두는 편이 현실적입니다.</p>
<h3 id="4-canary는-비용과-품질을-같이-본다">4) canary는 비용과 품질을 같이 본다</h3>
<p>운영 canary는 5~10% 트래픽 또는 내부 tenant부터 시작합니다. 단순히 서비스 에러가 없는지만 보면 부족합니다.</p>
<p>확인 지표:</p>
<ul>
<li>canary와 control의 p95/p99 latency 차이</li>
<li>span 수/request, attribute cardinality/request</li>
<li>trace missing rate와 root span 누락률</li>
<li>Collector CPU/memory, queue latency, drop rate</li>
<li>alert rule이 중복 또는 과민하게 울리는지</li>
<li>장애 회고에서 느린 경로 설명 시간이 줄었는지</li>
</ul>
<p>저는 성공 기준을 &ldquo;span이 늘었다&quot;가 아니라 &ldquo;상위 3개 장애 질문에 답하는 시간이 줄었다&quot;로 잡는 편이 맞다고 봅니다. 예를 들어 결제 API p95 회귀 원인을 15분 걸려 찾던 팀이 5분 안에 downstream HTTP와 DB 중 어느 쪽인지 말할 수 있어야 합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, 컴파일타임 계측은 재빌드 가능한 서비스에 강합니다. 소스와 빌드 파이프라인을 통제하지 못하면 맞지 않습니다. 이 경우 eBPF/OBI, proxy telemetry, sidecar 관측이 더 현실적입니다.</p>
<p>둘째, 자동 계측은 span 품질을 보장하지 않습니다. coverage가 늘어도 span 이름, attribute, sampling, 중복 span이 정리되지 않으면 화면은 더 복잡해집니다. 도입 초기에 trace schema와 금지 attribute 목록을 같이 고정해야 합니다.</p>
<p>셋째, 빌드 도구가 늘면 supply chain과 재현성 관리가 필요합니다. <code>otelc</code> 버전, Go 버전, Docker build cache, CI runner 환경이 모두 산출물에 영향을 줄 수 있습니다. 운영 서비스에서는 &ldquo;빌드 한 줄&quot;도 변경 관리 대상입니다.</p>
<p>넷째, 수동 계측을 모두 없애면 도메인 맥락이 사라집니다. 자동 계측은 HTTP, DB, Redis, gRPC 같은 기술 경계를 채우고, 수동 span은 주문, 정산, 권한, 재시도 정책 같은 업무 의미를 남기는 역할로 나눠야 합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> Go 서비스별 재빌드 가능성, 현재 trace coverage, 수동 계측 상태를 inventory했다.</li>
<li><input disabled="" type="checkbox"> <code>otelc</code> 적용 전후 build time, binary size, p95 latency, span 수를 비교했다.</li>
<li><input disabled="" type="checkbox"> 중복 span, 고카디널리티 attribute, 민감 attribute가 없는지 확인했다.</li>
<li><input disabled="" type="checkbox"> CI/CD에서 <code>otelc</code> 버전을 고정하고 일반 <code>go build</code> fallback 조건을 문서화했다.</li>
<li><input disabled="" type="checkbox"> canary에서 Collector 비용과 trace 품질을 함께 봤다.</li>
<li><input disabled="" type="checkbox"> 자동 계측으로 채워지지 않는 도메인 span 목록을 따로 정했다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>Go 서비스 하나를 골라 HTTP, DB, Redis, gRPC 중 현재 trace가 빠진 경계를 표시해 보세요. 컴파일타임 계측이 채울 수 있는 부분과 수동 span이 필요한 부분을 나눕니다.</li>
<li>기존 <code>go build</code>와 <code>otelc go build</code>를 staging에서 비교한다고 가정하고, p95 latency, build time, span count 통과 기준을 숫자로 작성해 보세요.</li>
<li>장애 회고 질문 3개를 적고, 자동 계측 span만으로 답할 수 있는 질문과 도메인 span이 필요한 질문을 구분해 보세요.</li>
</ol>
]]></content:encoded></item></channel></rss>