
우리는 왜 여전히 기본 함수를 의심해야 하는가: curl의 결단에서 배운 것
curl의 strcpy 제거 결정을 통해 본 엔지니어링 원칙. 개발자의 주의력에 의존하는 코드의 위험성과 시스템적 강제성의 중요성을 다룹니다.
송찬영
CTO

안녕하세요. 풀링포레스트 CTO 송찬영입니다.
연말이 다가오면서 팀원들과 올해 우리가 작성한 코드를 회고하는 시간을 가졌습니다. 기술 부채를 얼마나 줄였는지, 그리고 우리가 만든 안전장치들이 실제로 동작했는지 점검하면서 꽤 흥미로운 주제가 떠올랐습니다. 바로 "개발자의 주의력에 의존하는 코드는 결국 망가진다"는 사실입니다. 마침 제가 평소 즐겨보는 다니엘 스텐버그(Daniel Stenberg, curl 창시자)의 블로그에 이와 맥을 같이 하는 흥미로운 글이 올라와, 그 내용을 바탕으로 우리의 엔지니어링 원칙을 이야기해볼까 합니다.
C언어를 다뤄본 개발자라면 strcpy나 strncpy 같은 문자열 복사 함수가 얼마나 골치 아픈 존재인지 잘 아실 겁니다. 최근 curl 프로젝트는 코드베이스에서 strncpy를 멸종시킨 데 이어, 이제는 strcpy마저 완전히 제거하기로 결정했습니다.

strncpy는 원래부터 악명이 높았습니다. 버퍼를 0으로 패딩하느라 성능을 갉아먹거나, 대상 문자열을 널(null)로 종료하지 않아 대형 사고를 치기도 하죠. 그래서 많은 프로젝트가 이를 기피합니다. 하지만 strcpy는 조금 다릅니다. "개발자가 버퍼 크기만 확실히 제어한다면" 문제없다고 여겨져 왔습니다.
문제는 바로 그 '확실히 제어한다면'이라는 가정에 있습니다.
다니엘이 지적한 핵심은 기술적인 결함이 아니라 '시간과 사람'의 문제입니다. 코드를 처음 작성할 때는 복사할 문자열의 길이와 버퍼 크기를 바로 윗줄에서 체크했을 겁니다. 아주 안전하죠. 하지만 코드는 생물처럼 자라납니다. 수십 년 동안 수백 명의 기여자가 코드를 수정하고, 리팩토링하고, 기능을 덧붙입니다.
그 과정에서 크기를 검사하는 로직과 실제로 복사를 수행하는 strcpy 사이의 거리는 점점 멀어집니다. 처음에는 1줄 차이였던 것이 나중에는 함수가 분리되고, 파일이 쪼개지며 서로 다른 컨텍스트에 놓이게 됩니다. 체크 로직이 무효화되거나 조건이 바뀌었음에도, 멀리 떨어진 strcpy는 여전히 "누군가 검사했겠지"라고 믿으며 실행됩니다. 바로 이 지점에서 버퍼 오버플로우 같은 치명적인 보안 취약점이 발생합니다.
curl 팀은 이 문제를 해결하기 위해 타협하지 않았습니다. 개발자의 '주의력' 대신 '강제력'을 택했습니다. curlx_strcopy라는 자체 래퍼 함수를 만들어 대상 버퍼, 대상 크기, 소스 버퍼, 소스 길이까지 4개의 인자를 모두 받도록 했습니다. 그리고 memcpy를 사용해 명시적으로 복사하고 널 종료를 보장합니다.
void curlx_strcopy(char *dest, size_t dsize, const char *src, size_t slen) {
DEBUGASSERT(slen < dsize);
if(slen < dsize) {
memcpy(dest, src, slen);
dest[slen] = 0;
} else if(dsize) dest[0] = 0;
}솔직히 말해, 번거롭습니다. 인자가 늘어나고 타이핑할 양도 많아집니다. 개발자 경험(DX) 측면에서 보면 퇴보처럼 보일 수도 있습니다. 하지만 안전을 위한 불편함은 감수할 가치가 있습니다.
풀링포레스트 개발팀에서도 비슷한 고민을 자주 합니다. 우리는 C가 아닌 TypeScript나 Rust, Go 등을 주로 사용하지만 원리는 같습니다. "나중에 조심해서 쓰면 돼"라고 남겨둔 any 타입이나, 예외 처리가 느슨한 유틸리티 함수들은 언젠가 반드시 발목을 잡습니다. 그래서 우리는 다소 귀찮더라도 컴파일 타임에 에러를 잡을 수 있도록 엄격한 타이핑을 강제하고, 런타임 검증 로직을 비즈니스 로직과 최대한 가깝게 배치하려 노력합니다. '믿음'보다는 '시스템'으로 품질을 유지하는 것이죠.
재미있는 점은, curl의 이번 조치가 AI 봇들과의 싸움에도 도움이 된다는 것입니다. 최근 GitHub 등에서 활동하는 AI 기반 보안 스캐너들이 strcpy라는 단어만 보이면 문맥도 파악하지 못한 채 "심각한 보안 취약점 발견!"이라며 리포트를 쏟아낸다고 합니다. 일종의 '거짓 양성(False Positive)'이죠.
이런 AI의 엉터리 지적은 메인테이너들의 리소스를 낭비하게 만듭니다. 아예 strcpy를 없애버림으로써, 이런 소모적인 논쟁을 원천 차단하는 효과도 있다는 다니엘의 말이 꽤나 인상 깊었습니다. 우리 팀에서도 Cursor나 Copilot 같은 AI 도구를 적극적으로 활용하지만, AI가 뱉어내는 코드를 맹신하지 않고 반드시 시니어 레벨에서의 코드 리뷰를 거치는 이유도 이와 같습니다. 도구는 도구일 뿐, 최종 책임은 엔지니어에게 있으니까요.
코드의 안정성은 한 명의 천재 개발자가 작성한 완벽한 로직에서 나오는 것이 아닙니다. 실수를 저지를 수밖에 없는 평범한 인간인 우리가, 서로의 실수를 막아줄 수 있는 시스템과 컨벤션을 얼마나 잘 지키느냐에 달려 있습니다.
조금 더 귀찮게 코딩하세요. 검증 로직을 실행 로직 바로 옆에 붙여두세요. 그리고 익숙한 함수라도 그것이 잠재적 시한폭탄이라면 과감히 버릴 용기를 가지시길 바랍니다. 그것이 10년 뒤에도 살아남는 코드를 만드는 비결일 테니까요.


