go run의 첫 실행이 유독 느렸던 이유에 대한 고찰

go run의 첫 실행이 유독 느렸던 이유에 대한 고찰

Summary go run


요약

go run hello.go 를 작성했는데, 첫 빌드시에는 시간이 좀 지나야 출력되고, 그 다음부턴 바로바로 실행되었다. 그래서 같은 디렉토리에 빌드파일이 생기는건가? 하고 확인해보니, 또 그건 아니었다. 그렇다면, go의 빌드는 어떤식으로 이루어지고, 빌드파일되는 어디에 생성되는걸까?

go run은 단순한 실행 명령이 아니라, 보이지 않는 곳에서 컴파일과 실행, 그리고 뒷정리까지 스스로 완료하는 영리한 자동화 도구였다. 처음의 느린 속도는 온전한 컴파일 과정 그 자체였고, 이후의 빠른 속도는 영리하게 남겨둔 캐시 덕분이었다. 모든 과정이 사용자가 인지하지 못하는 임시 공간에서 일어나기에, 결과물은 보이지 않았던 것이다.


1
vi hello.go
1
2
3
4
5
6
7
package main

import "fmt"

func main() {
    fmt.Println("Hello, Golang!")
}
1
go run hello.go

go run hello.go. 터미널에 명령어를 입력하고 잠시 정적이 흘렀다. 찰나의 순간이었지만, 인터프리터 언어의 즉각적인 반응에 익숙했던 나에게는 어색하게 긴 시간이었다. 곧이어 “Hello, Golang!“이 출력되었다. 혹시나 싶어 다시 같은 명령어를 입력하니, 이번에는 눈 깜짝할 사이에 결과가 나타났다.

이 경험은 나에게 자연스러운 의문을 남겼다. 첫 실행과 두 번째 실행의 속도 차이. 가장 먼저 떠오른 가설은 ‘컴파일’이었다. 첫 실행 시 코드가 컴파일되어 실행 파일, 즉 빌드 결과물이 어딘가에 생성되었고, 두 번째 실행부터는 그 결과물을 즉시 실행했기 때문에 빨라진 것이라고 추측했다. 지극히 합리적인 생각이었다.

1
2
3
4
5
[root@localhost go]# ls -al
합계 8
drwxr-xr-x. 2 root root   22  7월 31일  11:17 .
dr-xr-x---. 8 root root 4096  7월 31일  11:18 ..
-rw-r--r--. 1 root root   78  7월 31일  11:17 hello.go

그래서 ls 명령어로 현재 디렉터리를 확인했다. 하지만 디렉터리에는 hello.go 파일 외에 아무것도 없었다. 내 가설이 틀렸나? 컴파일이 되긴 한 걸까? 만약 컴파일이 되었다면 그 실행 파일은 대체 어디로 사라진 것일까. 보이지 않는 무언가가 있다는 생각에 머릿속이 복잡해지기 시작했다. 이것이 나의 궁금증의 시작이었다.


그 답은 예상치 못한 곳에 있었다. go run은 내가 보고 있는 현재 디렉터리가 아닌, 시스템의 임시 폴더에 자신만의 작업 공간을 몰래 만들고 있었다. 리눅스 시스템이라면 /.cache 디렉터리 아래에 go-build로 시작하는 무작위 이름의 폴더를 생성하는 식이다. 그리고 바로 그곳에서 우리가 아는 컴파일과 링크 과정이 모두 이루어진다. 소스 코드는 오브젝트 파일(.o)로 컴파일되고, 최종적으로 하나의 실행 가능한 바이너리 파일로 만들어진다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[root@localhost ~]# ls .cache/go-build/
00  0f  1e  2d  3c  4b  5a  69  78  87  96      a4  b3  c2  d1  e0  ef  fe
01  10  1f  2e  3d  4c  5b  6a  79  88  97      a5  b4  c3  d2  e1  f0  ff
02  11  20  2f  3e  4d  5c  6b  7a  89  98      a6  b5  c4  d3  e2  f1  trim.txt
03  12  21  30  3f  4e  5d  6c  7b  8a  99      a7  b6  c5  d4  e3  f2
04  13  22  31  40  4f  5e  6d  7c  8b  9a      a8  b7  c6  d5  e4  f3
05  14  23  32  41  50  5f  6e  7d  8c  9b      a9  b8  c7  d6  e5  f4
06  15  24  33  42  51  60  6f  7e  8d  9c      aa  b9  c8  d7  e6  f5
07  16  25  34  43  52  61  70  7f  8e  9d      ab  ba  c9  d8  e7  f6
08  17  26  35  44  53  62  71  80  8f  9e      ac  bb  ca  d9  e8  f7
09  18  27  36  45  54  63  72  81  90  9f      ad  bc  cb  da  e9  f8
0a  19  28  37  46  55  64  73  82  91  README  ae  bd  cc  db  ea  f9
0b  1a  29  38  47  56  65  74  83  92  a0      af  be  cd  dc  eb  fa
0c  1b  2a  39  48  57  66  75  84  93  a1      b0  bf  ce  dd  ec  fb
0d  1c  2b  3a  49  58  67  76  85  94  a2      b1  c0  cf  de  ed  fc
0e  1d  2c  3b  4a  59  68  77  86  95  a3      b2  c1  d0  df  ee  fd

이것이 첫 실행이 느렸던 이유, 즉 ‘왜(Why)?’에 대한 직접적인 답이다. go run은 인터프리터처럼 소스 코드를 한 줄씩 읽어 실행하는 것이 아니라, 명백히 컴파일 언어로서의 정석적인 단계를 밟고 있었던 것이다.

그렇다면 실행 파일은 왜 보이지 않았을까? 그 해답은 go run의 설계 목적에 있었다. 이 명령어의 본질은 ‘빠른 테스트와 실행’에 있다. 개발자가 소스 코드의 실행 결과만 잠시 확인하고 싶을 뿐, 그 과정에서 생성되는 바이너리 파일까지 관리하고 싶어 하지는 않을 것이라는 배려다. 그래서 go run은 임시 디렉터리에 생성했던 실행 파일을 실행하여 결과를 터미널에 보여준 직후, 자신이 만들었던 임시 폴더와 그 안의 실행 파일을 모두 깨끗하게 삭제하며 흔적을 지운다. 이것이 ls를 입력해도 아무것도 보이지 않았던 ‘어떻게(How)?’에 대한 설명이다.

마지막 의문. 그렇다면 왜 두 번째 실행부터는 빨라지는가. 임시 파일은 매번 삭제된다면서. 여기에 Go의 또 다른 영리함이 숨어있다. 바로 ‘빌드 캐시(Build Cache)‘다. Go는 1.10 버전부터 빌드 캐시 기능을 도입했다. 한번 컴파일한 패키지의 결과물은 사용자의 캐시 디렉터리(~/.cache/go-build 등)에 저장해 둔다. 그리고 다음 실행 시, 소스 코드에 변경 사항이 없다면 다시 컴파일하는 대신 캐시에 저장된 결과물을 가져다 쓴다. 첫 실행에서 만들어 둔 캐시 덕분에, 두 번째 go run은 컴파일 단계를 거의 건너뛰고 링크 및 실행만 진행하면 되니 즉각적인 반응이 가능했던 것이다.


이러한 일련의 과정을 이해하고 나니 go build와의 차이점 또한 명확해졌다. go run이 개발 과정의 편의성을 위해 임시 공간에서 모든 것을 처리하고 사라지는 유령 같은 존재라면, go build는 배포와 지속적인 사용을 위해 현재 디렉터리에 ‘hello’라는 이름의 실행 파일을 뚜렷하게 남기는 실체적인 명령어다. go run이 과정의 자동화와 은닉에 초점을 맞췄다면, go build는 결과물의 생성 그 자체에 집중한다.

결국, go run의 느린 첫 실행에서 시작된 작은 궁금증은 Go 언어의 도구가 사용자를 얼마나 깊이 배려하며 설계되었는지 깨닫게 되는 계기가 되었다. 보이지 않는 곳에서 묵묵히 컴파일하고, 흔적을 남기지 않으려 스스로를 정리하며, 다음을 위해 조용히 캐시를 남겨두는 그 과정 전체가 개발자의 편의를 위한 정교한 자동화 시스템이었던 것이다. 단순한 명령어 하나에도 이러한 철학이 담겨있다는 사실이, 언어에 대한 이해를 한층 더 깊게 만들어 주었다. 이 기록이 나와 비슷한 궁금증을 가졌을 누군가에게 생각의 실마리가 되기를 바란다.

💬 댓글

GitHub 계정으로 로그인하여 댓글을 남겨보세요. GitHub 로그인

🔧 댓글 시스템 설정이 필요합니다

GitHub Discussions 기반 댓글 시스템을 활성화하려면:

  1. Giscus 설정 페이지에서 설정 생성
  2. GISCUS_SETUP_GUIDE.md 파일의 안내를 따라 설정 완료
  3. Repository의 Discussions 기능 활성화

Repository 관리자만 설정할 수 있습니다. 설정이 완료되면 모든 방문자가 댓글을 남길 수 있습니다.

목차