서론
지난 9월 미국 시애틀에 도착해서 약간의 적응과정을 거친 후 제가 받은 첫 번째 업무는 이쪽 팀에서 만들고 있는 IMS라는 시스템에 멀티 리전(Multi region) 기능을 추가하라는 것이었습니다. 그 전의 적응 기간 동안 지라, 슬랙, 줌, 깃헙을 활용하는 일련의 프로세스에 익숙해질 기회가 있었는데, 그 적용 수준이나 적극성의 차이는 있을지 몰라도 한국에서도 이미 오랫동안 경험했던 업무 환경이었기에 큰 어려움을 느끼지 않았었습니다. 하지만 새로운 기능을 개발해서 기존 시스템에 추가하라는 구체적이고 실질적인 업무를 받고 나서, 막상 코드를 작성하려고 익숙한 에디터를 켰을 때는 친숙하고 분명한 느낌 보다는 뭔가 생소하고 막막한 느낌이 크게 들었습니다. 사실 회사에서 일을 하면서 새로운 환경에 적응해서 업무를 수행해야 할 때는 꽤 많았습니다. 하지만 그런 경우에도 보통 완전히 막막한 기분이 드는 경우는 그렇게 많지 않았는데, 개발만 하던 사람에게 영업을 하고 제안서를 쓰라고 하는 등의 극적인 전환이 아닌 이상에야 대부분의 새로운 환경은 어느 정도는 이전에 익숙했던 환경과 겹쳐지는 부분들이 있기 마련이기 때문입니다.
예를 들어서 제가 처음 입사했을 때는 이미 회사가 스프링 프레임워크 기반의 자바 프레임워크를 보유하고 있고, 대부분의 프로젝트들이 프레임워크를 이용해서 개발하고 있었는데, 당시에 저는 모델 2가 엄청나게 세련된 최신 기술인줄 알고 있던 상태였습니다. 하지만 자바 프레임워크들은 당연히 프로그래밍 언어로 자바를 사용하고 있고, 화면을 그리기 위해서 여전히 JSP를 사용하고 있었기 때문에 이미 알고 있는 것들에서 버릴 것과 새로운 것들을 구분해가면서 새로운 환경에 익숙해질 수 있었습니다. 예를 들어서 스크립틀릿(Scriptlet)은 절대 쓰지 말아야 하고, 웹 프로그램에서 사용하는 컴포넌트들의 객체는 내가 만드는 것이 아니라 프레임워크에서 만들어준다는 점들을 배워나가는 식이었습니다.
경우에 따라 다르겠지만, 기본적으로 사업과 기술이 모두 급격하게 변하기 쉬운 우리 업의 특성 상 하나의 기술 스택에 익숙해진다고 해서 그 익숙함을 끝까지 끌고 갈 수 있는 사람은 많지 않을 것입니다. 제 경우에도 여러 가지 사정에 의해서 포지션 변경을 해야 할 일이 꽤 자주 있었는데, 그때마다 기존의 경험을 바탕으로 새로움에 익숙해져야 할 때가 많았습니다. 예를 들어서 스프링 프레임워크를 기반으로 웹 개발을 자주 하다가 갑자기 안드로이드 개발을 시작해야 한다고 하면, 프로그래밍 언어나 개발 도구가 동일하다는 점을 기반으로 HTTP와 JSON을 이용한 통신을 서버 입장에서 이해하다가 클라이언트 입장에서 이해하기 시작하고, 화면 구성과 컴포넌트 단위의 모듈화를 각각의 시각에서 이해하고, 더 나아가서 스프링에서 수행하던 의존성 주입(Dependency Injection)을 안드로이드에서도 할 수 있을까를 고민하는 등의 단계로 점점 새로운 환경에 익숙해지게 됩니다. 이렇게 새로운 개발 환경에 적응하는 능력은 제가 특출나게 뛰어나거나 했던 것은 아니었습니다. 하지만 새로운 기술이 끊임없이 등장하는 업의 특성상 저와 같이 엔지니어링 업무를 수행하는 사람들은 이러한 적응이 일종의 생존을 위한 필수요소가 되기도 하고, 그렇기에 이 바닥에서 살아남은 사람들은 어느 정도의 적응력을 확보한 사람들이라고 가정을 해도 크게 틀리지는 않았습니다. 그러니까 새로운 환경에 직면하는 일 자체가 평범한 소프트웨어 엔지니어들에게는 어느 정도는 일상적인 일이 되어버린 것입니다.
Go 언어 문법에 익숙해지기
다시 돌아와서, 이러한 배경을 가지고 있었음에도 시애틀에서 새로운 업무를 받았을 때 막막한 기분이 들었던 것은 개발을 시작하기 위해 뚫고 들어갈 익숙함의 틈이 별로 보이지 않았기 때문이었습니다. 이 곳에서는 개발을 위해서 Go 언어를 사용하였는데 이는 제가 처음 접하는 언어였습니다. 물론 제가 가지고 있던 새로운 개발환경에 적응했던 경험 중에는 새로운 언어에 적응했던 경험도 있었습니다. 저는 그래서 사실 잘 모르는 언어로 개발을 하는 것에 대해서는 거의 걱정을 하지는 않고 있었습니다. 하지만 이 프로젝트가 서비스 간의 통신을 위해서 gRPC를 사용하고 있고, 대부분의 서비스는 CLI를 기반으로 실행되고 있다는 사실을 알게 되었을 때는 다소 낭패라는 생각이 들었습니다. 프론트 엔드 개발을 위해서 Vue.js를 사용하고 있다는 사실을 알게 되었을 때는 그래도 아는 기술이 나왔다고 생각해서 살짝 반가운 기분이 들었지만, 막상 화면을 고치려고 하다가 제가 Vue.js는커녕 자바스크립트 기반의 프론트 엔드 개발 경험 자체가 거의 없다는 사실을 알게 되었고, 여기에 Vue.js를 감싸고 있는 Go Buffalo라는 Go언어 기반의 웹 개발 프레임워크가 하나 더 있다는 사실을 알게 되었을 때쯤 여태까지 경험하지 못했던 막막함을 느끼게 되었습니다.
제가 잘 몰랐지만 이 곳에서 빠르게 익숙해졌던 것들은 주로 코드 바깥쪽에 있는 것들이었습니다. 예를 들면 음식점에서 팁을 계산하는 방법이나, 우버를 불러서 이동하는 방법, 코드 리뷰와 반영 프로세스, 원격지와 사무실 간의 모호한 업무 경계와 이를 위한 협업 환경 그리고 이를 받아들이는 문화와 가치관의 차이, 써클CI(Circle CI)와 하버(Harbor)의 사용법 같은 것들이 있었습니다. 하지만 코드 내부에 있는 것들은 대부분 모르는 것들이 타이트하게 응집되어 있어서 코드를 자세히 들여다보거나 프로젝트 전체의 구조를 크게 봐도 도통 감이 오지 않았습니다. 이렇게 되면 내가 주어진 업무를 수행하기 위해서 어떤 부분을 고치고 어떤 부분을 수행해야 하는지 알 수 없기 때문에 일을 제대로 시작할 수가 없고, 그렇기 때문에 진전이 없어서 막막한 기분을 느끼게 됩니다.
사실 저는 그래서 한동안 옛날 생각을 조금 했었습니다. 예전에 저는 개발리더의 포지션에서 다수의 신입사원들과 같이 일해야 했던 시절이 있었는데, 사실 그 후배들이 겪던 어려움이 지금 제가 처한 상황과 비슷하다는 생각을 했습니다. 상당수의 신입사원들은 학교에서 배우지 않았거나 크게 경험이 없던 프로그래밍 언어로, 처음 보는 프레임워크를 쓰면서, 생소한 협업 환경과 처음 겪는 일정의 압박과 내가 자신 없게 짠 코드가 실제로 ‘반영’된다는 두려움 속에서 업무를 수행했어야 했는데, 그럴 때 그들에게 업무에 익숙해지고 자신감을 가질 수 있도록 어떤 도움을 주었을지를 떠올리려고 애를 썼습니다. 사실 그런 경우에 대단한 도움을 주거나 세심한 멘토링을 하기는 어려웠습니다. 보통은 프로그래밍 언어에 익숙하지 않은 경우 언어에 익숙해지는 것이 첫번째이고, 가급적 쉬운 업무, 그러니까 기존 프로그램을 살짝 수정하거나 이미 만들어진 화면과 거의 비슷한 화면을 하나 더 만들도록 해서 점차적으로 환경에 익숙해지도록 배정하거나 조언하고 물어보는 것에 대답해주는 정도의 느슨하고 수동적인 책임감을 가지는 것이 전부인 것 같았습니다.
비슷한 맥락으로, 막막한 상황에서 제가 가장 먼저 시도한 일은 Go 언어를 익히기 시작했던 것이었는데, 언어에 대한 이해가 바탕이 되지 않으면 무언가를 시도하거나 도움을 받을때 어려움이 많을 것이라고 생각했기 때문입니다. 다행히도 Go 언어는 매우 단순한 문법과 직관적인 구조를 가지고 있었습니다. 동시성 처리를 위한 메시지 채널을 자체적으로 가지고 있으면서도 C언어와 동일한 의미의 포인터를 가지고 있기도 하고, 멤버 함수의 가시성이 이름이 대문자로 시작하냐 소문자로 시작하냐에 따라 결정된다든지, 다른 언어에서는 경고로 끝날 행동이 여기서는 컴파일 오류를 야기시킨다거나 하는 등의 혼란스러운 부분이 있기는 했지만 불필요한 것들을 상당히 쳐낸 언어의 특성 때문에 다른 언어에 비해서는 비교적 빠르게 구조와 문법을 익힐 수 있었던 것 같았습니다.
물론 프로그래밍 언어의 문법을 배우는 것과 그 언어를 능숙하게 사용하는 것은 완전히 다른 문제입니다. 하지만 경험상 프로그래밍 언어에 능숙해지는 것은 책을 보거나 예제를 따라치거나, 인터넷을 검색하는 것으로는 어려운 일이었습니다. 실제로 그 언어를 이용해서 모르는 것들을 찾아가며 무언가를 해결해내야 점점 그 언어의 사용 수준이 높아지는 경우가 훨씬 많았던 것 같았습니다. 그래서 실제로 저도 새로운 언어를 이용해서 프로젝트를 수행해야 할 때는 언어 자체를 배우는 시간은 기초 문법만 익히는 하루나 이틀 정도만 잡아두고 그 이후에는 무조건 개발을 하면서 익숙해지는 방식을 선호했는데, 이번에도 문법을 대충 익히고서는 내가 해야 할 일을 다시 살펴보기 시작했습니다.
코드를 살펴보고 들어갈 틈을 찾기
처음에 말했듯이 제가 처음 받은 업무는 IMS라는 시스템에 멀티 리전 기능을 추가하는 것이었습니다. 운이 없게도 IMS는 이미 개발이 막바지에 이른 시스템이었고, 멀티 리전 기능을 추가하는 것 역시 시스템과 언어에 익숙해지기 좋은 쉬운 업무는 아니었습니다. IMS는 베어 메탈 머신(Bare metal machine)이나 클라우드 시스템에 쿠버네티스 클러스터를 자동으로 배포해주고 관리하는 시스템이었는데, 이쪽 팀에서는 꽤나 높은 성숙도로 DevOps 프로세스와 코드 리뷰를 수행했기 때문에 이미 IMS는 언제나 시연 가능한 상태로 견고하게 만들어져 있던 상태였습니다.
저는 이미 거의 끝나가는 프로젝트에 들어온 경험이 그렇게 많지 않아서, 견고하게 잘 만들어진 시스템이 개발자 입장에서 문제가 될 것이라고 생각한 적이 한 번도 없었는데, 막상 이미 잘 만들어진 시스템을 고치려다보니 유기적으로 잘 맞물려서 돌아가는 시스템의 어느 부분에 틈을 내고 내가 추가하고자 하는 기능을 집어넣을지 감이 잘 오지 않았습니다.
멀티 리전 기능은 기존의 IMS가 하나의 데이터 센터만 관리할 수 있도록 만들어졌기 때문에 이를 N개의 데이터 센터를 관리할 수 있도록 바꿔달라는 요구사항 이었습니다. 이 요구사항은 리전을 선택하고 전환하는 부분, 그러니까 프론트 엔드부터 모든 API, 그리고 각각의 마이크로 서비스들을 모두 수정해야 하는 요구사항이었기 때문에 작업 범위도 꽤 넓은 편이었습니다.
코드를 조금 살펴보니, Go 언어를 이용해서 개발된 마이크로 서비스들은 대부분 CLI(Command Line Interface)를 통해 기동되는 하나의 프로세스이며 대부분 소켓을 열어두고 들어오는 요청들을 적절한 코드로 라우팅 해서 응답을 처리하는 구조를 가지고 있었습니다. 이는 살짝 구조화된 소켓 프로그래밍과 비슷하다는 인상을 받았는데, 프레임워크 기반의 개발에 비해서는 그 처리 구조가 굉장히 단순한 편이었습니다. 이에 대해서 이쪽 개발자들에게 살짝 물어보니 Go언어나 gRPC를 사용하는 쿠버네티스 분야의 오픈소스 개발에는 매우 일반적인 구조라고 했습니다.
또 여기서 개발한 서비스들은 모두 클라우드 네이티브 프로그래밍에 맞게 상태가 없는(Stateless) 구조로 만들어졌기 때문에 별도의 DB는 찾아볼 수 없었고 상태 저장이 필요한 서비스, 그러니까 사용자 관리나 클러스터 정보 관리 등은 모두 외부 서비스의 API를 호출해서 처리하는 형태였습니다. 꼭 Go 언어가 아니더라도 이러한 아키텍처는 마이크로 서비스 아키텍처나 클라우드 네이티브 아키텍처 구성에 적합한 구조, 그러니까 가용성(Availability)이나 확장성(Scalability)을 확보하기 좋은 구조입니다.
물론 가용성이나 확장성이 보장되는 아키텍처와 멀티 리전 기능은 얼핏보면 비슷해 보이기도 하지만 실제로는 그 맥락이 다릅니다. 클라우드 네이티브 아키텍처 관점에서의 확장성이 동일한 서비스의 확장과 축소에 관심을 가지고, 따라서 동일한 서비스로 향하는 요청은 전체 시스템의 레벨에서 적절한 로드밸런싱 알고리즘에 의해서 라우팅 된다면 멀티 리전 기능은 애플리케이션 레벨에서 원하는 목적지, 그러니까 리전으로 사용자의 요청을 전달해야 하는 애플리케이션 레벨의 기능이기 때문입니다. 그렇기 때문에 실제로 멀티 리전 기능을 추가하기 위해서는 상당히 구체적인 부분들을 건드려야 했습니다. 그러니까 시스템 설정이나, 환경변수, 쿠버네티스 설정이나 헬름(Helm) 차트를 수정하는 정도로 끝날 일은 아니었다는 사실을 파악했습니다.
UI를 수정하는 부분은 레퍼런스가 많을 것이라고 생각했기에 크게 어렵거나 문제될 부분은 아니었으므로 관심에서 내려두고, 백엔드 부분을 어떻게 수정할지에 대해서 이쪽 팀의 개발자들에게 조언을 요청했습니다. 그리고 (1) CLI를 통해 리전 정보를 받아서 실행할 수 있도록 변경하고 (2) 프론트 엔드의 모든 요청이 리전 정보를 전달할 수 있도록 API 스펙을 수정하고 (3) API 서버가 리전 정보에 맞추어서 해당 리전에 API를 전달할 수 있도록 라우팅 하는 코드를 추가할 것이라는 방향을 가이드받을 수 있었습니다.
CLI를 통해 리전 정보를 받아서 실행할 수 있도록 바꾸라는 말은, 현재 시스템이 상태가 없는 구조로 만들어진 프로세스 기반의 서비스들이라는 점을 생각해보면 이해하기 쉬웠습니다. 리전 정보를 별도로 저장할 DB 등이 없으니 프로세스를 실행할 때 각각의 리전의 이름과 URL을 CLI의 파라메터를 이용해서 받아서 실행하도록 변경해야 했던 것이었습니다. 이는 기존 프로그램이 이미 CLI 파라미터를 이용해서 목적지 URL을 받도록 구성되어 있었기 때문에 이를 복수의 파라미터를 받을 수 있도록 변경하고, 복수의 리전 정보를 메모리에 유지하도록 변경하는 일로 생각할 수 있었습니다. 이 부분이 이 기능을 추가하기 위해 기존 시스템에 내야 하는 틈에 해당했는데, 단일 스트링 변수에 저장하고 있던 목적지 URL을 리전의 이름을 키로, 리전의 URL을 값으로 가지는 맵 형태의 자료구조로 바꾸는 일이 기존 코드에 많은 균열을 발생시켰기 때문입니다.
아는 부분부터 시작하기
API 서버의 기존 코드는 프론트 엔드에서 전달받은 요청을 가지고 있던 목적지로 그대로 보내는 역할을 수행하고 있었기 때문에 실제로 하는 일이 엄청 많지는 않았습니다. 말하자면 라우팅 기능이 빠진 API 게이트웨이 같은 역할이었고 보는 관점에 따라서는 없어도 될 서비스였습니다. 이 서버는 실행 시에 목적지 URL을 받아서 커넥션을 맺은 다음 프론트 엔드 쪽의 요청이 들어오기를 기다리고 있었는데, 목적지 URL이 복수로 바꾸자 커넥션도 복수로 맺고 유지해야 하는 변경이 생기게 되었습니다. 이 커넥션은 이 시점에 실제 네트워크 레벨로 이루어지는 것은 아니었고 추상적인 연결 정보를 유지한 다음 실제 요청을 전송해야 할 때 개별적으로 이루어지는 형태였는데, 이 전송은 gRPC를 이용해서 이루어졌습니다.
gRPC는 다양한 언어로 이루어진 서비스 간의 인터페이스 명세 통일을 위해 만들어졌던 프로토콜 버퍼(Protocol Buffer) 규격에 실제 서비스 호출에 관련된 스펙을 추가한 프로토콜 입니다. 이는 멤버 변수만 가지고 있던 C언어의 구조체와 여기에 멤버 함수를 추가할 수 있게 된 C++ 언어의 클래스 간의 관계와 얼핏 유사하게 생각할 수도 있습니다. 실제로 IMS는 모든 영역의 통신을 gRPC로 수행하였는데, 프론트 엔드와 API 서버 간의 통신도 예외는 아니었습니다. Vue.js로 만들어진 프론트 엔드는 axios를 통해서 HTTP 형태로 서비스 요청을 하는데, 여기서 살짝 혼란스럽기는 했지만 이 HTTP 요청은 실제로 API 서버로 전송되는 것이 아니라 Go Buffalo라는 내부 웹 프레임워크로 전송되고, Buffalo는 이를 gRPC로 변환하여 실제 API 서버와 통신을 합니다. API 서버는 같은 스펙으로 gRPC와 Rest API를 모두 지원하고 있어서 처음에는 매우 혼란스러웠는데 그냥 이 구조에서 Rest가 없다고 생각하니까 조금 이해하기 편했습니다.
프로토콜 버퍼는 실제로 써본 적은 없었지만, 일전에 관심을 가지고 문서를 몇 번 읽어봤던 기억이 있어서 이 규격이 고도로 직렬화된 규격이고, 스펙만 정의하면 각 언어에 맞게 코드를 제네레이션 해주는 도구들이 있다는 사실을 떠올릴 수 있었습니다. 그래서 실제로 API 스펙을 변경하는 과정은 매우 직관적이었기에 큰 문제가 없었는데, 가장 큰 문제는 이 API를 각 리전으로 라우팅 해주는 부분이었습니다.
어려운 문제 해결하기
사실 이쯤 왔을때는 처음에 느꼈던 막막함은 거의 사라진 상태였는데, 반대로 어느 정도 익숙해진 코드들을 여기저기 뜯어보아도 기능을 추가할 부분을 찾기 어려웠습니다. 가장 큰 문제는 최초에 각 리전의 서버와 커넥션을 맺는 부분과, 실제로 요청이 들어와서 그 요청을 알맞은 리전으로 전송하는 부분이 서로 다른 영역이었다는 점입니다. gRPC 스펙을 변경했기 때문에 이제 프론트 엔드에서 들어오는 요청에는 각 리전 ID가 포함되어 있었습니다. 그러면 그 요청을 핸들링하는 함수는 리전에 맞는 목적지로 요청을 전달해주면 되는 것이었는데, 문제는 이 핸들링하는 함수가 실제로는 프로그램이 실행되는 시점에 생성되며, 목적지에 종속되어 있었다는 점입니다. 즉, 최초에 목적지와 여러 개의 커넥션을 맺을 때 핸들러도 같이 여러 개 생성이 되는데, 실제 요청을 보내야 할 리전 ID는 핸들러가 요청을 받고 나서야 알 수 있으니, 핸들러에 들어가기 전에는 리전 ID를 알 수 없었고, 코드 상에서 리전 ID를 알게 되는 가장 빠른 시점은 이미 핸들러에 들어간 이후였기 때문에 리전 ID를 이용하여 요청을 적절한 리전의 핸들러로 매핑하는 것이 어려웠습니다. 즉, 타이밍이 기대했던 것과 맞지 않았습니다.
그래서 최초에 생각한 방식은 인터셉터(Interceptor)를 사용하는 것이었습니다. 네트워크를 다루는 대부분의 프레임워크나 오픈소스들은 요청이 나가기 전, 돌아와서 처리하기 전, 혹은 요청을 받아서 로직에 전달하기 전, 혹은 그 결과를 처리하는 시점 등등에 다양하게 인터셉터를 걸어둘 수 있도록 하였고 이를 통해서 주어진 흐름 사이에 내가 원하는 로직을 추가할 수 있었습니다. 그래서 검색을 했더니 역시나 인터셉터는 있었고, 인터셉터를 통해 리전ID를 먼저 꺼내서 해당 요청을 적절한 핸들러로 보내줄 방법을 찾았습니다. 하지만 인터셉터를 통해서 리전 ID를 꺼내는 일은 가능했으나 아무리 API나 인터넷을 뒤져봐도 이 인터셉터를 이용해서 로깅은 할 수 있어도, 이를 적절한 핸들러로 라우팅 할 방법은 아무리 찾아도 없었습니다. 요청과 응답의 값을 읽을 수는 있어도, 그 흐름을 제어하고 바꿀 수 있는 API가 없었던 것입니다.
그 다음으로 생각한 방식은 핸들러에서 요청을 처리할 때 목적지의 엔드포인트(Endpoint) 정보를 마찬가지로 리전 별로 저장하는 맵 형태로 관리하도록 바꾸는 것이었습니다. 즉 핸들러를 구분하는 것이 어려우니, 핸들러를 하나만 만들어두고 해당 핸들러 내부에서 라우팅을 처리하는 방식이었습니다. 하지만 이런 해결 방식에도 약간의 문제가 있었는데, 기존에는 엔드포인트가 단 하나만 있을 것을 가정하고 만들어졌기 때문에 모든 엔드포인트가 정적으로 구조체에 정의되어 있었고, 이를 동적으로 생성하는 것이 쉽지 않았습니다. 리전 정보가 들어오는 시점은 프로그램이 실행되는 시점이고, 핸들러가 생성되고 핸들러에서 엔드포인트를 정의하는 것도 실행 시점인데, 이 엔드포인트가 코드에 정적으로 정의되었던 탓에 리전 숫자에 맞추어서 동적으로 엔드포인트를 생성하는 것이 불가능 했던 것입니다. 정적인 정의를 동적으로 생성하기 위해서는 리플렉션(Reflection)이 필요했고, 큰 기대를 하지 않고 찾아봤는데 Go 언어에도 리플렉션이 존재한다는 사실을 알게 되어서 살짝 희망을 가졌지만 매우 강하게 타입을 체크하고 제네릭(Generic)을 지원하지 않는 Go 언어의 특성 때문에 리플렉션을 통해 생성된 엔드포인트에서 내가 원하는 메서드를 실행해서 요청을 전달하는 일은 불가능해 보였습니다.
사실 제가 익숙하게 사용했던 스프링 프레임워크에서는 이렇게 요청을 동적으로 해석해서 라우팅하는 것이 전혀 어려운 일이 아니었기 때문에 새로운 환경에서 일견 쉬워 보이는 요구사항을 해결하지 못해서 이리저리 왔다 갔다 하는 것은 조금 괴로운 일이었습니다. 사실은 처리가 불가능한 요구사항은 아니었을까 하는 의심이 들 무렵, 머리를 식힐 겸 프론트 엔드 코드를 고치다가 문득 그런 생각이 들었습니다.
자바스크립트를 포함해서 요즘 대부분의 프로그래밍 언어들이 가지고 있는 특성, 그리고 자바가 구닥다리 언어 취급을 받는 이유 중 하나는 바로 함수를 매개변수로 전달할 수 있느냐 없느냐, 즉 함수의 일급객체(First class object) 취급 여부입니다. 대부분의 언어들은 함수를 함수의 인자로 전달할 수 있었고, 하다못해 C언어도 함수포인터를 이용해서 비슷하게 구현할 수 있었는데, 자바는 유독 함수 전달을 막아놨기 때문에, 익명 클래스로 함수 전달을 대체하느라 코드가 길어지는 문제가 있었습니다. Go 언어는 비록 포인터를 가지고 있고 컴파일을 해야 하는 언어이기는 하지만 어쨌든 요즘 언어이므로 당연히 함수를 일급 객체로 취급하고 있었고, 당연히 함수를 함수에 전달하거나 함수가 함수를 반환하거나 하는 등의 일이 자유로웠습니다.
이러한 특성이 프로그래밍 언어에 주는 혜택은 여러 가지가 있겠지만, 지금의 제가 처한 문제에서 중요한 부분은 함수의 정의는 정적인 타이밍에 이루어지지만 그 함수의 실행은 실제 실행 시점, 그러니까 동적인 타이밍에 이루어지는 선언형 프로그래밍(Declarative programming)의 특성을 가지게 할 수 있다는 점이었습니다. 각 목적지의 엔드포인트를 정적으로 전달하는 것이 아니라, 그 엔드포인트를 생성하는 로직만 별도의 함수로 분리하여 해당 함수 자체를 생성시점에 전달해주면, 실제 리전 정보를 받아오는 실행 시점에서는 정적으로 정의된 엔드포인트를 동적인 시점에 선언적으로 생성하여 사용할 수 있게 됩니다. 이는 전반적으로 리플렉션 등을 사용하는 방식보다는 훨씬 더 정적으로 처리할 수 있으면서도 목표한 기능을 구현할 수 있는 방안이었습니다.
그래서 실제로 작성된 코드는 엄청 복잡한 것은 아니었지만, 함수 파라메터와 익명 함수 전달을 이용해서 작성해야 했기 때문에 다소 난해한 코드가 만들어졌습니다. 익숙하지 않은 환경에서 난해한 코드를 작성하는 것은 위험성이 큰 일이었지만 다행히도 이 곳에서는 코드 리뷰 프로세스가 꽤 성숙하게 자리 잡고 있었기 때문에 걱정을 조금 덜 수 있었습니다. 이후 일련의 프로세스를 통해 코드가 병합되고, 제품에 포함되어 배포되었습니다. IMS는 이제 멀티 리전 기능을 지원하게 되었고 저는 첫 번째 일감을 마무리할 수 있었습니다. 물론 그 과정에서 빠뜨린 부분들이 꽤 있었지만 다행히도 팀의 다른 개발자들이 커버를 잘해줘서 큰 문제없이 마무리할 수 있었습니다.
새로운 환경에 익숙해지는 것에 익숙해지기
시애틀에 처음 도착했을 때, 팀원들에게 10분간 자기소개를 하는 시간이 있었습니다. 그래서 이런 저런 이야기를 하다가 마지막에 그런 이야기를 했었습니다. 사실 Go 언어든 영어든 실제로 쓰는 것이 그렇게 친숙하지도 않고, 미국이라는 곳도 처음 와보는 곳이고, 먹는 것이나 출퇴근 등등 새로운 것들이 너무 많아서 살짝 걱정되는 부분도 있다. 그런데 새로운 환경에 적응하는 것은 개발자에게는 일상적인 일이니까 아마 하다 보면 점점 괜찮아지지 않을까? 그럴 수 있게 잘 도와줬으면 좋겠어. 대충 이런 이야기로 마무리했던 기억이 있습니다.
사람에 따라서는 새로운 환경을 끔찍하게 싫어하고 익숙한 것들만 사용해서 일을 하려는 사람들도 있는데, 사실 그게 무조건 비판 받을 태도는 아니라고 봅니다. 경험과 가치관에 따라서 기술에 대해서도 보수적으로 접근하는, 혹은 그래야만 하는 사람들이 있고 그렇지 않은 사람들도 있으니까요. 하지만 제가 생각하기에 무언가 진득하게 오래가는 기술이라는 게 별로 없고 자꾸 뭐가 새로 나오고 바뀌는 업의 특성상 새로운 환경에 적응해야 한다는 숙제는 원치 않더라도 가끔 앞에 던져질 때가 있을 것 같습니다. 그리고 그런 것에 익숙해지는 경험이 쌓이다 보면, 나중에 정말 상상치도 못한 시대가 왔을 때도, 그러니까 설령 양자컴퓨터 같은 것이 상용화되더라도 겁에 질려서 은퇴하기보다는 또 그 환경에도 나름대로 적응하는 선택을 할 수 있지 않을까 그런 생각이 들었습니다