메모리 안전은 더 이상 “언젠가 고려할 언어 선택”만은 아닙니다. 취약점 보고, 규제 요구, 공급망 심사, 제품 보안 약속이 얽히면서 팀은 C/C++ 또는 광범위한 unsafe 경계를 어떻게 줄일지 설명해야 합니다. 그러나 이 흐름을 “전면 Rust 재작성”으로 받아들이면 실패하기 쉽습니다. 오래된 드라이버, 고성능 codec, OS API, vendor SDK, 수년간 검증된 라이브러리를 한 번에 옮기면 새로운 결함과 배포 위험도 함께 커지기 때문입니다.

최근 실무의 현실적인 방향은 새 위험 표면에는 memory-safe 기본값을 두고, 남는 native 경계에는 측정 가능한 위험 예산과 방어층을 붙이는 것입니다. 이 글은 Rust compiler 성능과 CI change budget, eBPF observability 플랫폼 도입, 런타임 reachability 기반 취약점 우선순위, Security Default rollout 계약을 연결해 언어 전환을 운영 가능한 위험 감소로 바꾸는 방법을 정리합니다.

이 글에서 얻는 것

  • 어떤 구성요소부터 memory-safe 기본값을 적용할지 공격 표면과 변경 비용으로 우선순위를 정할 수 있습니다.
  • Rust 같은 언어를 도입해도 남는 FFI·unsafe·native dependency 경계를 별도 위험으로 다룰 수 있습니다.
  • 코드 줄 수 대신 취약 표면·검증·복구 능력을 보여 주는 지표를 만들 수 있습니다.
  • 90일 파일럿에서 성능·빌드·운영 비용을 잃지 않고 전환 가설을 검증할 수 있습니다.

핵심 개념/이슈

1) 메모리 안전은 언어 라벨이 아니라 경계의 성질이다

Rust, Java, Go를 쓴다고 전체 시스템이 자동으로 안전해지는 것은 아닙니다. network packet을 parse하는 native library, unsafe 블록, FFI pointer, C ABI plugin, kernel interface는 여전히 경계에 남습니다. 반대로 C/C++도 작은 권한, 강한 sandbox, fuzzing, sanitizer, 최소 parsing surface를 적용하면 위험을 크게 낮출 수 있습니다.

첫 질문은 “무슨 언어를 표준으로 정할까?”가 아니라 “어떤 입력이 어떤 권한으로 메모리 경계를 넘는가?”여야 합니다. 인터넷·업로드 파일 같은 비신뢰 입력, parser·codec·compression의 bytes 처리, root·CI runner·secret 접근 권한, 주간 배포와 반복되는 crash가 동시에 있으면 우선순위를 올립니다. 안정된 내부 수치 계산보다 외부 ZIP을 푸는 upload worker와 privileged update agent가 먼저 후보입니다. Archive extraction 보안 예산처럼 입력 크기와 resource limit은 언어 교체 전에도 적용할 수 있습니다.

2) 위험 표면 예산이 전면 재작성을 실행 순서로 바꾼다

Rust code ratio 30%는 보기 쉬워도 low-risk CLI를 많이 옮겨 가장 위험한 parser가 남아 있으면 보안 효과를 설명하지 못합니다. 서비스별로 위험 표면 점수를 만들고 분기마다 줄일 예산을 정하는 편이 낫습니다.

점수 축1점3점5점
입력 신뢰도내부 고정 입력partner API인터넷·업로드 파일
권한·영향읽기 전용서비스 계정host·CI·고객 데이터 접근
복잡도단순 변환stateful protocolparser·codec·custom allocator
변경 빈도분기 1회 이하월간주간 이상
방어 증거unit test만sanitizer 또는 fuzzingfuzzing+isolation+rollback

합계 18점 이상은 첫 전환 후보, 12~17점은 방어 강화와 모듈 분리가 우선인 후보로 둡니다. 점수는 자동 승인 도구가 아닙니다. vendor component의 교체 비용, latency SLO, 운영 인력도 같이 적어야 합니다. 목적은 “왜 이것을 먼저 하는가”를 재현 가능하게 만드는 것입니다.

3) FFI는 안전한 섬과 위험한 바다의 접점이다

memory-safe wrapper를 작성해도 pointer·길이·lifetime 검사를 C library에 맡기면 안전한 API 모양만 얻고 위험은 남습니다. FFI boundary는 입력 길이, nullability, ownership, allocator, error code, thread affinity, cancellation을 명시한 작은 계약이어야 합니다.

  • borrowed buffer: native가 호출 중에만 읽고 caller가 해제한다.
  • transferred ownership: 누가 어떤 allocator로 언제 해제하는지 ABI에 명시한다.
  • opaque handle: 생성·사용·종료 thread와 종료 뒤 호출 금지를 wrapper가 강제한다.

경계 함수가 raw parameter를 20개 이상 받거나 void*와 길이가 여러 쌍으로 흩어져 있거나 오류가 boolean 하나라면 API를 분할할 신호입니다. 실패 의미가 모호한 native 호출은 Agent tool 오류 계약처럼 성공·실패·부분 실행을 추측하게 만드는 운영 부채가 됩니다.

4) 성능 우려는 금지 근거가 아니라 benchmark 계약이다

“Rust가 C++보다 느린가”에는 일반 답이 없습니다. hot loop, SIMD, allocator, syscall, copy 수, build profile이 결과를 바꿉니다. endpoint와 artifact에 맞는 측정 계약을 만드세요. throughput, p50/p99 latency, peak RSS, CPU seconds, binary size, cold start, build time을 함께 봅니다. 기존 대비 p99가 5% 이상 악화하거나 RSS가 15% 이상 늘면 원인을 분해하기 전 승격을 멈추는 guardrail을 둡니다. 평균 속도보다 fallback·오류·tail latency가 운영 결정을 바꾼다는 점은 HTTP/3와 QUIC edge fallback 관측과 같습니다.

실무 적용

1) 90일 파일럿은 고위험 경계 하나로 제한한다

공용 runtime을 교체하지 말고 외부 파일 metadata 검사, webhook signature verifier, format decoder처럼 입력과 출력이 작고 rollback 가능한 component 하나를 고릅니다. 첫 30일에는 threat model, input corpus, ownership, benchmark 기준선을 문서화합니다. 다음 30일에는 safe implementation 또는 wrapper를 만들고 fuzzing·sanitizer·negative test를 CI에 넣습니다. 마지막 30일에는 1% canary, crash-free rate, latency, 운영자 진단 시간을 보고 승격 또는 중단합니다.

신규 native component에는 외부 입력의 size·time·memory 상한, unsafe·FFI의 owner review와 boundary test, network-facing component의 fuzz target과 24시간 안정 실행 증거를 요구합니다. 권한 높은 process는 sandbox·least privilege·crash restart 제한을 함께 검토합니다. 이는 기존 코드를 벌주는 정책이 아니라 새 위험이 쌓이는 속도를 줄이는 기본값입니다.

2) 보고 지표를 코드량에서 위험 감소로 옮긴다

지표초기 목표해석
신규 high-risk native surface분기당 0 또는 승인 100%위험이 다시 쌓이는지
18점 이상 component의 방어 증거 충족률90% 이상fuzzing·isolation·rollback 누락
FFI boundary당 raw pointer parameter기준선 대비 20% 감소계약 단순화 정도
sanitizer/fuzz 발견 후 완화 p9514일 이하발견을 수정으로 잇는 속도
memory-safety production incident분기별 감소사용자 영향의 실제 변화

LOC는 보조 지표로만 둡니다. high-risk parser 하나를 격리한 결과가 low-risk utility 5만 줄을 포팅한 것보다 더 큰 가치를 가질 수 있습니다. Runtime reachability 기반 취약점 우선순위처럼 “취약점이 존재한다”와 “배포 경로에서 도달 가능하다”를 구분해야 투자 순서가 맞습니다.

3) 기존 C/C++도 방어층을 먼저 올린다

재작성 전에도 compiler hardening, ASan/UBSan nightly test, fuzz regression corpus, parser process 격리, egress 축소, restart storm 방지, crash dump 민감정보 마스킹을 적용할 수 있습니다. sanitizer가 CI 한 번 통과했다고 안전하다고 말하면 안 됩니다. representative input, 장시간 실행, release build 차이를 확인해야 합니다. 바꾸기 어려운 vendor SDK나 kernel-facing module은 owner, version, 입력 source, 권한, sandbox, rollback artifact를 inventory에 남기고, 변경 계획이 없으면 만료일과 재검토자를 가진 risk acceptance로 다룹니다.

트레이드오프/주의점

memory-safe 언어 도입에는 학습·채용·빌드·debugging 비용이 있습니다. 작은 팀이 넓은 shared library를 급히 포팅하면 review 품질이 떨어지고 낯선 abstraction이 새 performance bug를 만들 수 있습니다. 반대로 unsafe를 0으로 만드는 숫자 게임도 위험합니다. OS API, SIMD, FFI, lock-free 구조에는 안전하지 않은 경계가 필요할 수 있으므로 작은 모듈에 가두고 안전한 wrapper와 test로 호출자를 보호해야 합니다. 언어 전환은 인증 오류, 과도 권한, dependency 탈취도 해결하지 않습니다. Publish-time supply chain gate처럼 provenance·review·권한 분리는 계속 필요합니다.

체크리스트 또는 연습

  • 외부 입력·권한·복잡도·변경 빈도·방어 증거로 component 위험 점수를 매겼다.
  • 신규 고위험 component의 memory-safe 기본값과 예외 승인 경로가 있다.
  • FFI boundary마다 ownership, allocator, 길이, 오류, 취소, thread 규칙을 문서화했다.
  • high-risk parser 또는 codec에 malformed corpus와 fuzz regression이 있다.
  • benchmark는 p99, RSS, CPU, build time, fallback 오류를 함께 측정한다.
  • 기존 native component에는 owner·version·권한·sandbox·rollback 정보가 있다.
  • unsafe 예외는 만료일과 재검토자를 가진 risk acceptance로 남긴다.

연습으로 외부 입력을 받는 native 또는 unsafe 경계 세 개를 고르세요. 다섯 점수 축으로 순위를 매기고 1위 component의 “새 구현”, “wrapper 강화”, “sandbox·fuzzing 우선”을 비용·rollback·예상 위험 감소로 비교합니다. 이 판단표가 있으면 언어 논쟁이 다음 분기 투자 결정으로 바뀝니다.

관련 글