6화: 자바만 하던 선배가 점심엔 Go를 짠다




지난주 한빛상사 레거시 ERP의 슬로우 쿼리 장애가 오라클 DB 복합 인덱스 누락 하나로 설명된 이후, 나는 며칠간 명치끝이 뻐근한 기분을 떨치지 못했다. 내가 자바 런타임과 GC 구조의 본질적 한계라며 당당하게 제시했던 가설이 실행 계획(Execution Plan) 수치 앞에서 허무하게 밀려났기 때문이다.


하지만 나는 내적 오만을 순순히 접을 생각은 없었다.


'그래, 이번엔 DB 옵티마이저의 문제였다 치자. 하지만 내 러스트(Rust) 정적 분석 도구가 사내 넥서스(Nexus)에 정상 등록되고 젠킨스 파이프라인에 탑재되었더라면, 어설픈 동적 SQL 매퍼가 유발하는 쿼리 파싱 오버헤드와 인덱스 탈락 가능성을 컴파일 타임에 수치로 미리 경고할 수 있었을 거다.'


나는 사내 노트북 화면 한쪽에 여전히 띄워진 인프라팀 보안 승인 포털의 '외부 바이너리 사내 반입 및 넥서스 등록 신청서' 창을 가만히 내려다보았다. '보안 검토 진행 중'이라는 조용한 상태 표시는 변함이 없었다. 언젠가 승인이 떨어지면 내 접근법이 왜 레거시 시스템 개선에 필요한지 증명해 보이겠다는 고집은 여전히 내 마음속에 서슬 푸르게 살아 있었다.


* * *


점심시간을 알리는 사내 안내 방송이 울리고 10분이 지나자, 플랫폼 1팀 공간은 정적에 휩싸였다. 박정수 팀장님과 서지수 선배님은 고객사 미팅을 겸해 외부 식당으로 나가셨다.


나는 사내 카페테리아로 내려가는 대신, 1층 편의점에서 샌드위치와 캔커피를 사 들고 자리로 돌아왔다. 사내 표준 노트북 모니터에는 로컬에서 개인적으로 만지작거리던 러스트 오너십 검증 스크립트 코드가 띄워져 있었다.


그때, 조용한 팀룸 안에서 기계식 키보드의 타건음이 일정하게 울려왔다.


한지훈 선배였다.


한 선배 역시 외식을 나가지 않고 자기 자리에 조용히 앉아 있었다. 그런데 그의 책상 풍경은 평소와 사뭇 달랐다.


새벽기술에서 지급한 업무용 모니터 옆에, 한 선배 개인 소유로 보이는 매끈한 은색 랩톱이 펼쳐져 있었다. 랩톱 옆에서는 작은 검은색 LTE 모바일 핫스팟의 초록색 LED가 깜박였다. 사내 내부망(LAN)이나 고객사 VPN이 아니라 개인 회선에 연결된, 업무망과 분리된 환경이었다.


나는 샌드위치 비닐을 쓰레기통에 버리러 일어서다가, 지나가는 길에 무심코 한 선배의 개인 랩톱 모니터를 힐끗 보게 되었다.


당연히 이클립스(Eclipse)나 스프링 부트의 XML 파일이 떠 있을 거라 생각했던 내 눈에, 전혀 예고 없는 생소한 코드 구조가 들어왔다.


ringbuffer.go
func (w *WorkerPool) Dispatch(ctx context.Context, job Job) error {
    select {
    case w.jobChan <- job:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}
 
Created with dc-code-paste


'어... 이건?'


매개변수 타입이 이름 뒤에 오고, `func` 키워드와 `chan`, `select` 구문이 명확하게 조화된 언어. 고(Go) 언어였다.


나는 나도 모르게 발걸음을 멈추고 내적 냉소를 살짝 띠었다.


'고(Go)라니... 타입 시스템도 단순하고 zero-cost abstraction이나 강력한 매크로도 없는 언어 아닌가. 코드마다 `if err != nil`을 수십 번씩 반복 작성해야 하는 밋밋한 언어인데, 한 선배가 점심시간에 고를 다루고 있었다니.'


러스트의 엄격한 오너십과 표현력 높은 타입 시스템에 매료되어 있던 나로서는, 고 언어가 지닌 특유의 단순함을 다소 낮게 평가하는 경향이 있었다. 자바만 오랫동안 다뤄온 시니어 개발자가 다른 언어를 접한다기에 내심 기대를 했건만, 결국 단순한 언어로 옮겨간 것인가 싶은 지레짐작이 들었다.


그러나 한 선배의 화면에 띄워진 소스 코드를 한 줄 한 줄 아래로 내려 읽기 시작하자, 내 눈빛은 이내 심각하게 가라앉았다.


그 코드는 단순한 토이 프로젝트나 흔한 웹 서버 예제가 아니었다.


대용량 스트리밍 트래픽에서 밀려드는 작업과 고루틴(Goroutine)의 종료 순서를 함께 다루는 이벤트 큐 코드였다.


컨텍스트(`context.WithCancelCause`)를 통한 취소 전파, 버퍼가 찼을 때 생산자를 기다리게 하는 백프레셔(Backpressure), 채널을 닫는 주체와 순서가 한 흐름 안에 엮여 있었다. 화려한 추상화는 없었다. 대신 종료 신호와 전송이 맞물리는 경계마다 무엇을 먼저 확인하는지가 끈질기게 드러났다.


심지어 상단 IDE 터미널 헤더에는 낯익은 프로젝트 이름이 찍혀 있었다.


`queueweave / internal / queue / ringbuffer.go`


'큐위브(queueweave)...?!'


순간 내 머릿속에서 경종이 울렸다.


`queueweave`는 백엔드 고성능 분산 파이프라인 생태계에서 초당 수십만 건의 메시지 큐잉과 백프레셔 제어로 널리 알려진 저명한 오픈소스 고(Go) 라이브러리였다. 백엔드 개발자 커뮤니티나 성능 관련 아티클에서 수없이 언급되던 바로 그 분산 이벤트 큐 엔진이었다.


한 선배가 지금 개인 랩톱에서 다루고 있는 코드는 바로 그 `queueweave`의 내부 패키지였다.


'잠깐... 한 선배가 지금 큐위브의 내부 코어 코드를 직접 고치고 있는 건가?'


나는 큐위브라는 유명 오픈소스 이름과 정교한 고루틴 동시성 제어 코드를 번갈아 보며 충격을 금치 못했다. 다만 한 선배가 단순한 기여자(Contributor)인지, 아니면 머지 권한을 가진 메인테이너(Maintainer)인지는 이 공개 화면만으로 확정할 수 없었다. 분명한 건, 그가 작성하고 있는 동시성 코드가 내가 생각했던 어설픈 수준이 전혀 아니라는 사실이었다.


기척을 느꼈는지, 한 선배가 키보드 입력을 멈추고 고개를 돌려 나를 바라보았다.


"강현호 씨, 점심 안 드시러 가셨어요?"


특유의 낮고 덤덤한 목소리였다. 나는 순간 당황하여 어색하게 입을 열었다.


"아... 편의점에서 샌드위치를 사 와서 자리에서 먹었습니다. 지나가다가 모니터를 본의 아니게 보게 되었는데... 선배님, 이건 고(Go) 언어로 작성된 코드인가요?"


한 선배는 가볍게 미소를 지으며 개인 랩톱 화면을 가리켰다.


"네, 공개 저장소 코드예요. 점심시간이나 퇴근 후에 개인 노트북으로만 보고 있습니다. 회사나 고객사 자료는 없고, 네트워크도 제 모바일 핫스팟만 써요."


한 선배의 대답은 짧았지만 업무와 개인 작업 사이의 선은 분명했다.


나는 침을 삼키며 물었다.


"선배님... 화면에 보이는 프로젝트가 혹시 그 분산 이벤트 큐로 유명한 `queueweave`인가요? 이 종료 경합을 직접 보고 계신 겁니까?"


한 선배는 자랑하는 기색이나 거들먹거리는 태도 없이 잔잔하게 답했다.


"네. 종료 신호가 온 직후에도 작업을 넣으려는 고루틴이 하나 남는 것 같아서요. 재현부터 다시 보고 있습니다."


자바도, Go도, 자신의 경력도 입에 올리지 않았다. 한 선배가 보고 있는 건 언어가 아니라 재현되지 않는 한 번의 경합이었다.


그때 한 선배가 내 자리에 띄워져 있던 러스트 코드 화면을 힐끗 보더니, 약간의 호기심을 담아 물어왔다.


"현호 씨는 러스트에서 채널 닫는 쪽을 많이 보셨죠?"


한 선배는 랩톱 화면을 조금 더 기울여 특정 함수 블록을 짚었다.


"종료와 전송이 겹칠 때만 드물게 깨지는 것 같은데, 제가 놓친 순서가 있는지 같이 봐주시겠어요? 러스트 쪽 시각은 다를 수도 있으니까요."


순간 내 가슴속에서 묘한 승부욕과 뜨거운 전율이 일어났다.


선배가 후배를 훈계하거나 지시하는 자리가 아니었다. 내가 그토록 떠들던 러스트의 동시성 감각을 실제 코드 앞에 꺼내 보라는 제안이었다.


'말로만 잘난 척한 건지, 진짜 볼 줄 아는지 확인할 기회잖아.'


나는 허리를 곧게 펴고 진지한 눈빛으로 답했다.


"좋습니다. 코드부터 같이 보시죠."


나는 식어 가는 캔커피를 책상에 내려놓고 한 선배의 랩톱 앞으로 의자를 끌어왔다.