본문 바로가기
개발 - Coding/Go

Go 바이너리 실행시 ENOENT(No such file or directory) 문제 해결하기 1편 - C 표준 라이브러리 구현체를 찾아서

by dev_jinyeong 2025. 1. 20.

문제 발생 상황

Go 언어로 애플리케이션을 개발하고, 바이너리로 빌드한 다음 로컬에서 실행해보면 문제가 없는데 컨테이너로 띄우면 No such file or directory 에러가 뜨는 경우가 있습니다.

 

컨테이너 안에서 명령어를 쳐서 바이너리가 있는지 검사해봐도 아무런 문제가 없는데요.

실제로 실행하려고 보면 파일이나 디렉터리가 없다고 에러가 뜨는 것이죠.

 

개발자의 눈으로 보면 아무런 문제가 없기 때문에 이러한 에러는 해결하기 매우 어려웠습니다.

 

이 문제를 해결하면서 Go와 이외 주변 영역에 대한 다양한 지식을 얻게 되었는데요.

반대로 여러 배경지식이 있어야만 쉽게 풀 수 있는 문제였습니다.

 

결론부터 말씀드리면, C 표준 라이브러리 구현체를 빌드 환경과 실행 환경이 호환되도록 맞춰 줘야 합니다.

왜 ENOENT 에러가 발생하는가?

저의 케이스에서는 Go로 바이너리를 빌드한 다음에 alpine 리눅스 이미지에서 사용했는데요.

그냥 로컬 환경에서 바이너리를 실행했을 때는 전혀 문제없던 바이너리가 alpine 리눅스 이미지에서 실행하면 문제를 일으켰습니다.

 

컨테이너 로그를 보면 이런 식으로 남아 있었습니다.

-bash: /home/orc/go/bin/go: no such file or directory

 

그런데 ls를 이용해서 디렉터리를 보면 바이너리가 아무 문제 없이 존재합니다.

권한 문제인가 확인해봐도 읽기와 실행 권한은 적절히 설정되어 있었구요.

 

그런데 왜 이런 문제가 발생하냐면, C 표준 라이브러리 구현체를 찾으려고 하는데 못 찾았기 때문에 발생하는 것입니다.

 

뒤에서 더 설명하겠지만, Go 언어로 빌드된 바이너리는 특정한 C 표준 라이브러리 구현체를 의존하고, 이 구현체를 찾으려고 시도합니다.

 

예를 들어서 아래와 같은 구현체를 찾으려고 시도합니다.

 

Glibc 기반: /lib/ld-linux-x86-64.so.2

Musl 기반: /lib/ld-musl-x86_64.so.1

 

그런데 이 경로에 구현체가 없으면 이 구현체 파일이 없기 때문에 ENOENT 에러가 발생하는 것입니다.

 

개발자 입장에서는 바이너리가 분명 있는데, 파일이나 디렉터리가 없다는 에러가 떠서 당황할 수 있습니다.

 

다소 불친절한 에러 메시지이기 때문에, 원인을 찾으려면 오래 해매는 경우가 생깁니다.

 

C 표준 라이브러리에 대해서

C 표준 라이브러리는 C 언어를 위한 표준 라이브러리로서 ANSI C 표준에 의해서 명시되었습니다.

일반적으로 File I/O나 Thread 관련 작업을 할 때 운영체제에 의존해야 하는 부분이 있으면 동일한 인터페이스를 가져가기 위해서 만들어진 것입니다.

 

Go 언어는 독자적인 런타임이 있지만, 일부 작업은 C 표준 라이브러리에 의존합니다.

-> 이 부분은 Go 언어가 낮은 레벨까지 프로그래밍 할 수 있기 때문에 OS 구현에 대해서 알아야 하는 한계점인 것으로 보입니다.

 

그렇기 때문에 Go 언어로 빌드된 바이너리는 실행 환경에서 필요한 C 표준 라이브러리 구현체가 있는지 신경을 써 줘야 합니다.

 

C 표준 라이브러리는 규격이고, 이 규격을 구현한 여러 구현체가 있습니다.

Go 언어로 빌드된 바이너리는 설정에 따라 구체적인 C 표준 라이브러리 구현체에 의존합니다.

 

그렇다면 Go 언어는 실행 환경에 종속성이 있는 것인가?

그렇지는 않습니다. 더 설명하겠지만, 정적 링킹으로 빌드하면 실행 환경과 무관하게 실행 가능합니다.

 

이렇게까지 이해하고 나니까 "C 표준 라이브러리는 규격인데 구현체와 무관하게 동작해야 하는 것 아닌가?" 하는 생각이 들었는데요.

 

그러나 ldd 명령어를 이용해서 파일의 의존성을 보면 구체적인 구현체를 의존하고 있습니다.

 

만약 C 표준 라이브러리가 완전한 인터페이스라면, 왜 구현체를 알아야 동작하는걸까요?

Go 언어의 동적 링크와 정적 링크

만약 Go 언어로 작성한 프로젝트를 정적 링크로 빌드한다면, 이러한 문제는 발생하지 않습니다.

즉, CGO_ENABLED=0 파라미터를 넣고 빌드하게 되면, 아무런 문제 없이 실행됩니다.

 

왜냐면 정적 링크로 빌드하게 되면 C 표준 라이브러리 구현체를 실행 파일 내부에 포함시키기 때문입니다.

 

Go 언어 개발진과 커뮤니티에서는 실행 환경에서 최대한 독립적일 수 있도록 정적 링크로 빌드하는 것을 추천합니다.

 

그러나 실제로 개발해보면, 정적 링크로만 빌드할 수 없는 경우가 발생합니다.

 

제 경우에는, 오픈소스로 개발된 Go 언어 프로젝트가 있고 제가 개발한 플러그인 로직을 추가해야 했기 때문입니다.

제가 개발한 플러그인 로직 또한 Go로 개발되었으며, .so(Shared Object) 파일로 빌드되어 오픈소스 Go 프로젝트의 바이너리 빌드에서 이 파일을 동적 링크해야 했습니다.

 

이런 경우에는 동적 링크를 사용해야지만 원하는 대로 동작하게 됩니다.

 

이외에도 Go 런타임이 OS 환경에 대해서 구체적으로 알아야지만 동작하는 경우가 발생하고는 합니다.

 

이와 관련되어서는 아래의 2가지 링크를 참고 부탁드립니다.

https://stackoverflow.com/questions/41720090/does-go-depend-on-c-runtime

http://garrett.damore.org/2015/09/on-go-portability-and-system-interfaces.html

 

만약에 동적 링크로 Go 바이너리를 빌드하고 실행하게 되면, 해당 파일이 의존하는 인터프리터(동적 링커) 경로가 명시되게 됩니다.

커널이 이 파일을 열 때, ELF 헤더에 명시된 동적 링커 경로를 찾습니다.

만약에 이 동적 링커가 존재하지 않으면 필요한 파일이나 디렉터리가 없다는 ENOENT 에러를 보게 되는 것입니다.

 

결론적으로, 동적 링킹이 필요한 바이너리 파일을 실행할 때, C 표준 라이브러리 구현체를 맞춰주지 않으면 동적 링커를 찾을 수 없어서 갑자기 ENOENT 에러를 마주하게 되는 것입니다.

 

개발자 입장에서는 분명 바이너리가 있어서 실행했는데 ENOENT 에러를 보게 되니 혼란스러울 수 있습니다.

 

문제를 해결하는 가장 간단한 방법

문제를 해결하는 가장 간단한 방법은, 빌드할 때 사용했던 이미지를 그대로 사용하는 것입니다.

예를 들어서 golang 이미지를 이용해서 Go 바이너리를 빌드했다면, 실행하는 이미지도 golang 이미지로 맞춰주면 됩니다.

 

빌드한 이미지에서 사용한 C 표준 라이브러리 구현체가 당연히 실행하는 이미지에서도 있기 때문에, 문제 없이 컨테이너가 생성됩니다.

 

Go 언어와 C 런타임의 관계, 동적 링크/정적 링크, C 표준 라이브러리 구현체의 관계를 정확히 이해하기 전까지는 계속 이 방법을 써 왔는데요.

 

이 방법도 아주 큰 문제는 없습니다만, 문제의 본질을 이해한 이상 더 나은 답을 찾아 나설 수 있겠죠.

 

golang 이미지를 배포에 사용하는 것은 다음과 같은 문제가 있습니다.

  1. 프로덕션 환경에서 실행되는 컨테이너 이미지는 최소한의 기능과 권한을 갖는 것이 적절하다
  2. golang 이미지에 포함된 기능 중 배포에 불필요한 기능들이 많다
    -> 즉, 배포 이미지의 크기를 더 줄일 수 있다

사실, golang 이미지를 프로덕션 환경에 올리고 로드 테스트도 많이 진행해보았는데요.

 

그렇게 큰 문제는 없었습니다.

 

golang 이미지 자체도 꽤나 경량화된 이미지인데다, 특히 golang alpine 이미지를 사용하면 더 가볍기 때문입니다.

 

그런데 왜 더 신경을 써야 하느냐?

 

그것은 최적화를 할 수 있는 부분이 더 남아있기 때문입니다.

개발자로서 최대 효율을 포기할 수는 없죠.

 

또 이 부분을 공부하면서 Golang에 한정되지 않고 컨테이너 이미지에 대한 깊은 이해 + 다른 언어에도 도움되는 부분을 깨달을 수 있었습니다.

 

다음 글에서는 distroless 이미지에 대해서 공부하면서 어떤 깨달음을 얻을 수 있었는지 공유하겠습니다.

'개발 - Coding > Go' 카테고리의 다른 글

Go 언어 개발기 - Go로 gelf-forwarder를 만들다  (1) 2025.01.19