프로그래밍 언어가 정체된 듯한 현상을 배경으로, 함수 호출의 느슨화, 케이퍼빌리티, 프로덕션 친화 표준 라이브러리, 반(半)동적 언어, 값 기반 영속 저장소, 진정한 관계형 언어, 모듈러 모놀리스 장려 설계, 모듈식 린팅 등 여러 아이디어를 개략적으로 제안한다.
프로그래밍 언어는 다소 정체된 듯하다. 기존에 있던 아이디어들을 이리저리 재배치할 뿐, 새로운 것은 그리 많지 않다.
이게 반드시 나쁜 것만은 아니다. 기존 아이디어를 이리저리 바꾸어 보는 건 그것들을 다듬는 자연스러운 과정이기도 하다. 기존 관습을 급진적으로 갈아엎는 것보다 안전하기도 하다. 이미 존재하는 언어에 새로운 표준 라이브러리를 제공하는 것만으로도 가치가 있을 수 있다.
다만 때때로 내게 떠오르는 아이디어들이 있고, 그게 또 다른 사람들의 아이디어를 자극할 수도 있기에, 머릿속에서 꺼내 두기 위해 여기 적어 둔다.
이 아이디어들 중 어느 것도 사양(spec) 수준으로 다듬어져 있지 않다. 어떤 것은 실전 언어로 어떻게 구현해야 할지 감도 잡히지 않는 목표 수준에 불과하다.
대부분 이런 걸 하는 언어를 알지 못하지만, 그렇다고 해서 존재하지 않는다고 주장하는 건 아니다. 내가 모를 뿐이다. 당연히 지금까지 쓰인 모든 언어에 정통할 리가 없으니까.
이 아이디어들 중에는 아마 나쁜 것, 심지어 미친 것들도 있을 것이다. 최소한 하나는 내가 나쁘다고 이미 아는 범주에 들어가지만, “만약 누가 이걸 고쳐낼 방법을 찾는다면?” 하는 정도의 생각거리로 넣었다.
서로 모순되어 한 언어 안에 같이 들어갈 수 없는 것들도 있다.
전반적으로, 이 목록에 사람들이 어떻게 반응할지 내가 통제할 수는 없지만, 이를테면 해커 뉴스 같은 데 올라간다면 “흥미롭고, 이런 다른 흥미로운 아이디어를 떠올리게 한다” 같은 반응을 더 기대한다. “그건 멍청하고 X, Y, Z 때문에 절대 안 되니까 새 아이디어 얘기는 다들 그만둬라”라거나 “왜 jerf는 30년 전에 그걸 시도한 obscure 언어를 모르지?” 같은 반응은 덜 보고 싶다. (다시 말하지만, 당연히 지금까지 시도된 모든 것을 내가 다 알지는 못한다.)
이 아이디어는 얼랭(Erlang)에서 왔지만, 내가 여기서 말하는 정도만큼 끝까지 밀어붙이지는 않는다.
함수 호출은 매우 강한 원시(primitive)다. 함수 호출이 실패할 가능성은 없다. 이건 너무 깊이 내재화되어 있어서 우리가 그 사실조차 보지 못한다. 여기서 말하는 건 “함수가 에러를 리턴한다”거나 “예외를 던진다”는 차원이 아니다. 코드가 strlen을 호출하려고 손을 뻗었는데 그게 _없다_는 상황이다. 동적 언어의 경우조차 “특정 함수를 못 찾았다”가 아니라, 그 함수를 찾는 코드 자체가 없어져 버린 상황이다.
가장 가까운 예는 “이 호출을 만들 메모리가 부족했다” 정도인데, 대부분의 우리는 대부분의 시간에 그 가능성을 그냥 무시한다.
예전에 해커 뉴스에 네트워크 RPC가 90년대에 왜 실패했는지, 로컬 네이티브 함수인 척하려 했기 때문이라는 댓글을 쓴 적이 있다. RPC는 로컬 네이티브 함수가 아니고, 그럴 수도 없다. RPC 함수는 일반 함수에는 없는 오류가 발생할 수 있다. 단지 에러 값을 리턴하는 문제가 아니다. 때로는 “이 호출은 250나노초면 끝나야 하는데 1분이나 멈춰 있다가 타임아웃 났다” 같은 오류가 난다. 컴퓨터가 1분 기다리는 건 문제 없지만, “실행 시간이 9자릿수 범위 어딘가”일 수 있는 연산들을 기반으로 프로그램을 짜는 건 유용한 원시가 아니다.
그렇다면 함수 개념을 느슨하게 풀어, 모든 함수 호출이 더 “느슨”해지고 RPC에서 생기는 모든 실패 케이스를 표현할 수 있게 하면 어떨까? 예를 들면:
등등.
그다음 이렇게 만들고 나면, 단일 함수 호출을 하면서 필요한 모든 에러 처리를 하는 게 귀찮아질 것이므로, 이를 더 쉽게 만들기 위해 스코프 단위의 에러 처리 선언 같은 것을 넣거나 기본값을 늘리거나 하는 식으로 개선할 것이다.
그러면 RPC가 정말로 프로그램 내의 함수 호출만큼 쉬워질 수 있다. 왜냐하면 함수가 약속하는 바를 낮췄기 때문이다.
얼랭은 다양한 gen_* 서비스에서 어느 정도 이를 구현하지만, 전반적으로는 여전히 전통적인 함수 호출 개념을 가진다.
단점은, 함수 호출의 단순함을 프로그래머가 받아들일 수준까지 회복할 수 있느냐가 의문이라는 점이다. 궁극적으로 strlen이 없어질 일은 없고, 아마 나는 그냥 기다리기만 하면 된다. 그리고 문법이 그걸 꽤 쉽게 만들어 줄 필요가 있다. 이를테면 그런 호출 앞에 “그냥 로컬인 척해” 같은 접두 기호를 붙이거나, “이건 실제로 로컬 호출이야”라는 정보를 타입 시스템에 집어넣는 식이다.
아니면 그냥 그대로 두고, 컴파일러가 가능한 경우 로컬 함수 호출을 최적화하도록 둘 수도 있다. 실제로는 그런 경우가 많다.
새 아이디어는 아니니 자세히 설명하진 않겠다. 일부 언어가 실험하고 있다는 얘기도 들었다. 다만 이런 걸 더 보고 싶다는 바람은 있다.
E라는 언어가 이걸 실현하려 했지만, 내 인상으로는 자바 위에 구축되었고, 자바 위에서 시도하기엔 변화 폭이 너무 컸던 것 같다. 위아래 모두 이 개념으로 설계된 독자 언어가 필요하다.
시기가 무르익지 않았던 듯하다. 신기하다고 주장하려는 게 아니라, 아마 이제는 때가 된 것 같다는 관찰 정도로 봐 주면 좋겠다.
잠재적 위험은 케이퍼빌리티에 너무 많은 걸 우겨 넣는 것이다. 예컨대 러스트 스타일의 변경 가능성 통제를 같이 넣는 시도. 아니면 아주 잘 먹힐지도. 나도 모르겠다.
우리는 기존 프로그램 옆구리에 케이퍼빌리티를 반쯤 덕지덕지 붙이려 시도해 왔고, 솔직히 말해 그다지 잘 되지 않았다. 케이퍼빌리티를 모르고 작성된 코드와 개발자들이 어떤 권한을 필요로 하는지 사후적으로 추려 내기보다는, 아예 언어 차원에서 이걸 넣어야 할 때가 된 건 아닐까.
우리는 지난 수년간 프로덕션급 릴리스에 대해 많은 것을 배웠다. 그중 많은 것이 언어로 환류되지는 않았다. 아마 실험의 자유를 보장해야 했기 때문에 그게 좋았을 것이다. 하지만 이제는 이 문제들에 대한 해법을 언어에 내장해 그 혜택을 수확할 때가 되었다고 본다.
다음과 같은 것들이 가능해야 한다.
이건 “언어 기능”조차 아닐 수 있다. 메트릭을 위해 사용자 정의 문법이 필요한 건 아니다. (아주 “순수한” 언어를 노린다면, 메트릭을 신뢰성 있게 찍으면서도 그것이 “불순”으로 간주되지 않게 하는 약간의 장치가 있으면 좋겠다.) 이런 것들을 표준 라이브러리에 충분히 괜찮은 수준으로 넣어 두기만 해도 충분하다.
이미 성숙해 언어로 끌어올릴 수 있는 것들이 여럿 있다. 예컨대 “구조적 로깅”은 아마 그 수준에 도달한 듯하다.
언어로 끌어올리는 단점은 1.0이 나오면 변경이 어려워진다는 것이다. 장점은 표준 라이브러리와 서드파티 라이브러리가 그것과 통합할 수 있다는 점이다. 언어 X에 쓸 만한 로깅 라이브러리가 7개나 있는 건 좋지만, 다른 라이브러리들이 “로깅”이 어떤 모습일지에 대해 전제하기가 어려워진다.
이는 새로운 언어가 다른 오래된 아이디어를 “단지” 재배열하더라도 경쟁자보다 우위를 점할 수 있는 사례다. 다만 이런 인터페이스를 제대로 쓰려면 상당한 성숙도가 필요하다. 내가 언급한 항목 하나하나마다 많은 경험을 가진 개발자들을 모아 가장 잘 검증된 능력을 끌어와야 한다. 좋든 싫든, 이런 건 해당 분야를 한 번도 다뤄 본 적 없는 19살짜리 누군가가 즉석에서 내놓은 인터페이스 명세를 돌에 새기듯 확정해 버릴 자리가 아니다. 그런 건 이미 우리에게 많다.
많은 프로그래머가 동적 언어의 편의성을 좋아한다. 나는 경력의 처음 약 15년을 100%에 가깝게 동적 언어로 보냈지만, 지금은 내가 그 집단에 속하는지 모르겠다. 그래도 동적 언어가 꽤 인기 있는 건 사실이다.
문제는 근본적으로 느리다는 점이다. 어떤 사람들은 아직도 성능을 2000년대처럼 얘기하며 “언젠가 충분히 똑똑한 컴파일러가 나오리라”는 희망을 말하지만, 현실은 이걸 빠르게 만들기 위해 어마어마한 노력이 쏟아졌고, 더는 “언젠가”를 희망할 때가 아니라는 것이다. 그 결과는… 어느 정도 성공. 반반의 성공. 동적 언어를 더 빠르게 만들 수는 있지만, 엄청난 RAM을 소모하고, 그래도 보통 C보다 10배쯤 느린 데서 상한선이 걸린다. 그리 큰 보상이 아닌데도 일이 많다. 그나마 이들이 워낙 인기라서 “전 세계 동적 코드”에 4배 속도 향상이 곱해지면 여전히 싸울 가치가 있기 때문에 하는 것이다.
대안으로 LuaJIT처럼 JIT에 안 맞는 언어 부위를 잘라내는 길도 있다. 하지만 이건 그다지 대중적이진 않은 듯하다. 그래도 좋은 생각이긴 하다.
그런데 “동적성”을 자세히 보면, 거의 대부분이 시작 시점이나 “지금 사용자 플러그인을 하나 로드한다” 같은 명확한 시점에 일어난다. 실행 내내 이것저것을 지속적으로 동적으로 바꾸는 코드는 거의 없다. 그런데도 이런 동적성에 대해 항상 비용을 치른다. 모든 속성 조회가 올바르게 값을 찾기 위해 여러 코드를 돌려야 한다. 누군가가 지난번 조회 이후 조회 절차를 바꿔 놓았을 수 있기 때문이다. JIT의 경우도 마찬가지로, 마치 이게 항상 일어날 수 있는 것처럼 올바르게 동작해야 하므로, 애초에 그런 일이 불가능한 코드보다 느릴 수밖에 없다.
그렇다면, 특정 코드 단위에 대해 동적성을 컴파일 단계의 한 위상(phase) 으로만 두는 언어는 어떨까? 초기화 중에는 무엇이든 할 수 있다. 데이터베이스 테이블을 읽어 클래스를 동적으로 구성해도 된다. 하지만 그게 끝나면, 어느 시점에서 잠그고, 거의 정적 타입(완전히 그럴 필요는 없다. 점진적 타이핑으로 볼 수도 있다)에 가까워지며, 더는 동적일 수 없게 만든다는 것.
이게 정확히 어떻게 보일지는 모르겠다. 아이디어의 스케치다. 부분적으로는 내가 내 프로그래밍 세계에서 정적 언어에 꽤 만족하고 있기 때문이기도 하다.
하지만 모든 것이 잠기는 단계가 있다면, JIT은 코드를 안전하게 최적화할 훨씬 더 큰 힘을 얻게 된다. JIT은 “나중에 누가 이 함수에 정말 병적인 걸 넘기면 어떻게 하지?”에 대비해 엄청난 작업을 해야 한다. 최종 컴파일된 코드의 타입이 그게 불가능하다고 딱 잘라 보장해 준다면 훨씬 더 빨라질 수 있을 듯하다.
또한 일종의 하이브리드 컴파일 단계를 만들 수도 있다. 코드를 “컴파일”하진 않더라도, 잠근 프로그램이 런타임이나 사용자가 구현하려는 규칙에 따라 정합적인지 검증하는 “체크” 같은 걸 돌릴 수 있을 것이다.
정확히 내가 말하는 방식으로 동작하는 것을 알지는 못하지만, 분명 “컴파일은 정확히 언제 일어나는가?”라는 오래 탐구된 연속체 위의 한 지점이다. 새롭다고 주장하지 않는다. 공용 언어 런타임(CLR)의 ILR과 이후 타깃 시스템에서의 컴파일은 이와 꽤 가깝지만, 초점이 다르다. 어떤 리스프들은 이 모든 것을 할 수 있을지도 모르겠다. 다만 내가 말하는 건 프로그래머가 “자, 이제 이 코드 단위에 대해 동적이길 그만두자”라고 아주 명확히 선언하는 시점이 있다는 것이다. 비디오 게임의 셰이더 컴파일도 이런 요소가 있을 수 있다. 특히 컴파일 결과를 캐시하는 능력까지 포함해서.
이 아이디어에 대한 또 다른 관점은 “처음부터 JIT하기 쉬운 동적 스크립팅 언어를 누가 하나 만들 때가 되지 않았나?”다. 지금 존재하는 것들은, 동적 스크립팅 언어가 먼저 있고 JIT은 그로부터 한두 세대(십수 년) 뒤에 따라왔으며, 유용하려면 즉시 언어와 100% 호환되어야 해서, JIT을 염두에 두지 않고 설계된 언어를 위해 JIT이 처음부터 고생해야 하는 경우이거나, LuaJIT처럼 기존 언어에서 일부를 도려낸 경우다. 하지만 내가 아는 한, 태생부터 동적이면서도 JIT하기 쉬운 것을 목표로 설계된 언어는 없다.
그럴 거라면, 스레딩도 처음부터 제대로 다루는 동적 스크립팅 언어를 자연스럽게 얻게 될 것이다. C 대비 2~3배 정도 느린(달리 말하면 Go와 거의 같은 속도의) 동적 스크립팅 언어가, 정적 언어에 가까운 스레딩 성능을 네이티브로 제공한다면 많은 이들의 눈길을 끌 것이다.
스몰토크와, 한동안 내가 사용했던 Frontier라는 다른 에소테릭 프로그래밍 환경에는 영속 데이터 저장 환경 아이디어가 있었다. 기본적으로 global.x = 1이라고 설정하고 프로그램을 종료한 뒤 다시 시작해도 그대로 남아 있다. 그리고 “값을 영속 저장하는 것”이 말 그대로 그 정도로 쉬웠다. 파일을 열어 JSON을 덤프하고 나중에 다시 로드하는 것도 아니고, SQLite를 만지며 당신 언어의 네이티브 패러다임이 아닌 외부 SQL 인터페이스를 상대할 필요도 없다. 그냥 “이 값을 설정하고 영원히 유지”하는 것이다.
이건… 겉보기에 매력적이지만 안 좋은 아이디어다. 이건 내가 사람들이 “프로그래밍이 다음 단계로 도약하려면 꼭 필요하다”고 화내며 주장하는 목록에 올라 있는 것 중 하나다. 모든 것이 비주얼 프로그래밍이어야 한다는 주장, 최근 한동안 요란했다가 다시 수그러든 “로우 코드”와 같은 레벨의 항목이다. 자주 등장하진 않지만, 몇 번은 봤기에 내 리스트에 들어 있다.
하지만 여기에 상당한 단점이 있다. 특히 엔트로피가 이 공유 저장소를 심하게 파괴한다는 점이다. 개발자가 저장소에 값을 설정하고 코드를 프로덕션에 배포했는데, 이런, 그 값이 설정되어 있어야 코드가 돌아가고, 다른 곳에서는 전부 실패하는 일이 일어난다. 스테이징 환경에서 디버깅을 위해 런타임 변수를 만지는 것이, 그 값에 대한 우발적 의존 때문에 그대로 프로덕션의 버그로 전파되게 만들기는 사실 꽤 어렵다. 하지만 영속 저장소는 그 도전에 기꺼이 응한다!
그래서 내 경험으로도 이게 모든 문제의 마법 같은 해법과는 거리가 멀다는 걸 잘 안다.
하지만 여전히, 더 나은 통제를 붙여서 역사 속 쓰레기통에서 다시 꺼내 올 수는 없을까 궁금하다. 이 저장소에 무엇이 들어갈 수 있는지 더 좋은 통제. 이벤트 스트리밍 기반? 접근 제어? 어떤 “테이블”的 형태를 먼저 직접 보장하는 구조적 타이핑으로, 그 값을 사용하려다 실패하기 전에 모양부터 검증하는 방식? 그냥 강력한 타이핑?
왜냐하면, 한편으로는 엄청난 엉망진창을 만들 수도 있겠지만… 다른 한편으로는 myval.x = 5라고만 하면, 쿼리도, 매핑도, ORM도, 파일도, 실패도 없이, 내일도 그대로 myval.x로 거기 있는 게 너무나도 편하기 때문이다. 그냥 쾅, 끝.
그런데 “SQLite에 얽매일 필요 없다”는 얘기가 나온 김에, 언어의 근본 데이터 타입이 관계형 DB 테이블인 언어는 어떨까?
실제로는 SQL을 원하지 않을 것이다. 관계형 원칙으로 돌아가 SQL을 붙여 두들기는 대신, 프로그래밍 언어로서 동작하는 무언가를 구축해야 한다. .NET 세계의 LINQ나 SQLAlchemy 같은 수많은 기술이 그 모습의 가능성을 보여 준다. 다만 언어 문법 레벨에서 더 깊게 통합할 수 있다면 LINQ보다도 더 흥미로운 가능성이 열린다.
(정말로 SQL 데이터베이스와 대화하고 싶을 때를 위해 언어에서 SQL을 내보낼 수 있게 하는 건 아마 좋은 생각이다. 처음 보면 생각보다 어렵다. LINQ를 반드시 연구해야 하고, 사용자가 SQL_NO_CACHE 또는 SQL_CALC_FOUND_ROWS 같은 것을 어떻게 사용할 수 있게 할지 고려해야 한다. 특정 쿼리마다 이런 걸 할 수 있어야 한다. 매번 필요한 건 아닐지라도.)
관계형 데이터베이스는 분명 오래 갈 기술이지만, 대부분의 현대 언어는 여전히 이것을 가끔 큰 비용을 치르고 살짝 담그는 이국적인 것으로 다룬다. 데이터 프로그래머들이 쓰는 “데이터 프레임” 같은 특별한 “테이블” 데이터 타입을 주거나, LINQ처럼 멋진 것을 주기도 하지만, 결국 언어의 네이티브 표현은 곱(product)과 합(sum) 타입의 자료구조이고, 관계형 데이터에서 “진짜” 데이터로 가는 이질적인 변환 단계가 항상 있다.
이런 게 가능해질 때를 상상해 보자. 코드에서 서로 다른 세 가지 데이터 타입을 가로질러 쿼리하고, 그 쿼리를 위해 즉석으로 생성된 임의의(ad-hoc) 데이터 타입 형태로 결과를 얻는다. 그리고 그것을 네이티브하게 전달하며, 심지어 그 타입에 메서드를 직접 추가할 수도 있다.
타입 이론 세계에서는 이것이 row 타입과 밀접하게 관련된다. 이를 네이티브하게 사용하는 언어를 나는 알지 못한다. (물론, 충분히 squint하면 동적 타입 언어가 row 타입처럼 “보일” 수도 있다. 하지만 그건 타입을 통째로 포기했기 때문에 많은 것을 “그럴듯하게” 보이게 만들 수 있다는 얘기일 뿐이고, 결국 타입을 어기면 예외가 던져질 뿐이다.) 이 아이디어를 작동시키기 위해 더 많은 작업이 필요하지만, row 타입은 내가 시작하고 싶은 지점이다. 예컨대 “row 타입에 메서드를 붙일 수 있는가? 그 메서드가 그 row 타입이 어떻게 구성됐는지 신경 쓰지 않게 할 수 있는가?” 같은 것도 검토해야 한다.
예를 들어, 한 테이블에는 사용자 ID와 사용자명이 있고, 다른 항목에는 그들의 실명(인적 식별)이 있다면, 이 둘을 가로지르는 쿼리를 통해 시스템에 데이터 타입으로 선언되어 있지는 않지만 사용자ID/사용자명/실명의 튜플을 얻게 될 수 있다. 그렇다면 이 위에, 예를 들어 그 세 가지를 디버그로 덤프하는 같은 메서드를 붙일 방법을 고안할 수 있을까? 선언된 적이 없는 데이터 타입에 대한 메서드? 흥미로운 가능성이 있다.
(이건 위에서 말한 JIT 아이디어와도 조화를 이룰 수 있다. 이는 어떤 코드가 사용할 수 있는 가능한 타입의 폭발을 만들어 낼 경향이 있다. 구현 방식에 따라서는 심지어 무한 개일 수도 있다. 전통적 제네릭 기반의 사전 컴파일은 가능하지 않을 수 있다. 하지만 실제로는 그중 유한하고 대체로 작은 부분만이 사용되며, 어떤 타입이 실제로 사용되는지 파악하고 그것들만 네이티브에 가까운 속도로 JIT 컴파일하는 방식으로 성능을 많이 회복할 수 있다. “사용자ID/사용자명/실명”을 포함할 수 있는 무수한 타입 전부를 정적 오프셋까지 미리 컴파일하지 않고, 실제로 그 타입이 사용될 때만.)
모듈러 모놀리스는 요즘 레이더 아래로 날며 다소 간과되었지만, 점점 더 자주 언급되는 구조다. 개인적으로, 아키텍처가 필요할 만큼 큰 건 전부 모듈러 모놀리스로 쓴다. 적어도 중간 규모에서는 훌륭한 방식이라고 느낀다. 아주 큰 프로젝트에선 아직 시도해 보지 않았다. 내 추측으로는 더 많은 규율이 필요할 수 있지만 계속 스케일할 것이다. 내 사용 범위에서는 아직 지치지 않았다.
모듈러 모놀리스를 갖추려면 의존성 주입과 인터페이스를 써야 한다. 가능한 한 많은 코드를 “내가 필요한 건 이거야. DNS 주소를 IP 주소로 바꾸는 방법, 이메일 주소를 사용자 계정으로 바꾸는 방법, 이것, 저것…” 같은 선언적 필요로 작성하고, 모놀리스의 각 구성 요소를 조립할 때 그 컴포넌트가 필요로 하는 모든 서비스를 주입한다.
나는 “모듈러 모놀리스”가 논트리비얼한 프로젝트의 “기본” 아키텍처가 되어야 한다고 본다.
하지만 현대 언어들은 대체로 이에 역행한다.
정적 언어는 이를 위해 광범위한 인터페이스 선언이 필요하고, 그만큼 모든 것을 구체 타입으로 단단히 결합하는 것보다 훨씬 더 많은 규율을 요구한다. 그래서 실제 코드에서는, 인터페이스 사용이 쉬운 언어에서도, 인터페이스의 번거로움 때문에 많은 것이 결국 하드와이어로 얽히게 된다.
동적 언어는 표면적으로는 더 쉽다. 다시 말하지만 거의 모든 것을 포기하기 때문이다. 하지만 대가는, 주입받은 서비스들이 실제로 원하는 일을 해 준다는 컴파일 타임 보장이 전혀 없다는 점이다. 규모가 커질수록 이건 무서워진다. 어떤 전달된 파라미터에서 새로운 메서드를 호출할 때마다, 그 메서드에 전달될 수 있는 모든 것들의 인터페이스를 바꾸는 셈인데, 그 변경을 호출자에게 알릴 방법이 사실상 없다. 심지어 이게 라이브러리라면, 호출자와 인간적으로 연결될 가능성조차 없다.
둘 사이의 중간 지점이 흥미롭다. 컴파일 타임 보장을 갖춘 정적 언어이되, 모든 함수 매개변수가 타입 시그니처에 “대표(exemplar)” 타입을 적어 줬더라도 자동으로 인터페이스가 되게 하는 것. 내가 어떤 걸 “string”으로 선언했고, 그 string으로 하는 일이 다른 string과 이어 붙이고, 유니코드 코드 포인트를 순회하는 것이라면, 컴파일러가 자동으로 “문자열에 자신을 이어 붙일 수 있고”, “유니코드 코드 포인트를 순회할 수 있는” 무엇이든 받을 수 있게, 마치 그 자리에 인터페이스 선언이 있는 것처럼 처리해 줄 수는 없을까?
(타입 추론을 더 밀어붙이면 “대표 타입”조차 필요 없게 만들 수 있을지 보는 것도 흥미롭다. 하지만 그걸 파악하는 일은 여기서 다루는 수준을 한참 넘는다.)
함수에 들어오는 모든 매개변수에 대해, 사용자가 그 값을 어떻게 다루는지로부터 자동으로 인터페이스를 추출할 수 있게 할 수 있다. 언어 서버와 통합해 그 인터페이스를 자동 추출하게 할 수도 있다. 이걸 암묵적으로 두고 컴파일 타임에 체크할지, 아니면 “저장 시, 모든 인터페이스를 사람이 볼 수 있는 실제 선언으로 자동 정식화(reify)한다” 같은 옵션을 둘지 모르겠다. 암묵으로 두면, 언어 서버에서 “이 매개변수의 실제 인터페이스는 이것” 같은 질의를 반드시 제공해야 한다.
(그런 의미에서, 별도로 섹션을 할애할 정도는 아니지만, “언어 서버를 가정하고 매우 풍부한 질의 능력을 제공하는 정적 언어”에 흥미로운 것들이 많다. 최고의 IDE에서 그런 아이디어를 많이 보지만, 아이디어가 언어와 분리된 채로 남아 있다가 IDE 라인이 단종되면 결국 고립되곤 한다. 그 능력들을 언어 프로젝트 자체로 흡수하고, 언어 서버를 언어 설계 전 과정에 깊게 통합하면 흥미로운 효과가 있을 것이다.)
파이썬의 모듈 시스템처럼, 기술적으로 라이브러리 자체가 객체인 모델을 보고 싶다. 즉, 전체 라이브러리를 다른 것으로 바꿔 끼울 수 있다.
또 다른 흥미로운 아이디어와 결합할 수도 있다. 동적 스코프를 더 폭넓게 사용해, 근사한 DI 라이브러리처럼 보이는 내장 서비스 레지스트리를 제공하는 식이다. 그래서 어떤 코드는 “현재 레지스트리를 포크하고, UserProvider를 이 다른 객체로 바꾼 뒤, 이 테스트 코드를 실행해” 같은 일을 할 수 있다. 혹은 “트랜잭션”의 정의를 바꿔 끼우거나, 기타 등등.
이 시스템에 백도어가 없도록 확실히 할 수 있다면(예: “int 같은 원시 타입은 그냥 int이고, 셈할 수 없다”), 이 언어로 쓰인 어떤 시스템이라도 거의 자동으로 모듈러 모놀리스가 될 것이다. 물론 엄청 지저분한 모듈러 모놀리스가 될 수도 있지만, 원칙적으로는 시스템의 어떤 함수라도, 전체가 정적 타입임에도 불구하고, 의도적으로 교체 가능하게 쓰이지 않았더라도, 충분한 작업만 하면 그 함수가 의존하는 모든 것을 교체 가능한 방식으로 실행할 수 있다. 파일 시스템을 읽고 쓰는 어떤 코드라도, 그 코드가 그런 사실을 명시적으로 선언하지 않아도, 테스트를 위해 가짜 파일 시스템을 제공하는 컨텍스트에서 실행할 수 있다.
진짜 전역 변수는 완전히 막고 싶다. 다만 동적 스코프 아이디어와 결합하면 비슷한 용도를 위해 동적 스코프에 값을 넣을 수 있다.
구조화된 동시성과의 시너지도 흥미롭다. 동적 스코프를 실행 컨텍스트에 붙이는 것이다. 이 동적 스코프에 Go의 컨텍스트가 가진 능력까지 갖추게 하면 꽤 흥미로운 가능성이 생긴다.
여기서도, 전 세계 수많은 언어 커뮤니티에서 무슨 일이 일어나는지에 대해 내가 아는 바가 많다고 주장하지 않는다. 내가 깊이 들어가 있는 커뮤니티 중 하나에서 관찰한 걸 좋은 패턴으로 제안하는 것이지, 유일한 사례라고 말하는 게 아니다. 게다가 그 커뮤니티에서도 나는 이게 충분히 일어나지 않는다고 본다.
전제는 이렇다. Go 세계에는 세월을 거치며 다양한 린터가 많아졌다. 결국 그것들이 golangci-lint라는 프로젝트로 묶였고, 커뮤니티의 사실상 표준(de facto) 린터가 되었다. 무엇이 들어 있는지 보라고 린터 목록에 링크를 걸었다.
흥미로운 점은, golangci-lint의 다양한 린터가 대체로 서로 독립적이라는 것이다. 각자 특정한 가려움을 긁기 위해 여러 개발자가 작성한 독립 프로젝트였다. 나중에 기술적으로는 합쳐졌지만, 원칙적으로는 여전히 예쁜 모듈식 인터페이스 위에 올라간 커뮤니티 린터 묶음이다.
이건 언어 자체의 문제가 아니다. 하지만 언어 _프로젝트_가 이걸 구체화(reify)하는 건 흥미롭다. golangci-lint는 결국 많은 린터 간에 AST 뷰를 공유했다. 프로젝트 차원에서 그 아이디어를 일찍부터 복제할 수 있다. 린터를 완전히 모듈식으로 만들고, 표준화된 인터페이스만 맞으면 GitHub 프로젝트를 직접 지정해 붙일 수 있게 하자. 제3의 프로젝트가 하나의 실행 파일로 “통합”할 필요 없이.
이 접근의 멋진 점은, 언어 설계 과정의 일부로 만들면, 반드시 중요 하지는 않은 많은 것을 선택적 린팅으로 떼어낼 수 있다는 것이다. 예컨대 Go는 처음 나왔을 때 가져온 패키지는 반드시 사용하고, 선언된 변수도 반드시 사용해야 한다고 강제해서 꽤 악명 높았다. 세월이 지나며 불평은 잦아들었지만, 이런 건 컴파일러에서 빼 내고 메인 프로젝트가 제공하는 린터로 돌렸어도 되는 좋은 예다.
단점도 분명 있다. 언어의 “방언”이 생긴다. 하지만 사실 어차피 그렇게 된다. 대개 개발자들은 자기들의 특제 린팅 설정을 남의 라이브러리에 휘두르지 않게 꽤 빨리 학습한다.
그렇지만 얼마나 많은 것을 린터로 밀어낼 수 있을지 보는 건 흥미롭다. 열거형(고전적 int든, 최근의 “sum 타입” 의미로 쓰이든)의 모든 값을 switch에서 검사하도록 강제할 것인가, 말 것인가? printf 파라미터의 정확성을 검증할 것인가? 외부 프로그램을 호출할 때마다 인자를 확인 루틴을 거치라고 린터가 경고하도록 할 것인가? 어쩌면 HTML 템플릿 라이브러리 같은 게 라이브러리의 정식 일부로서 의심스러운 주입 패턴을 표시하는 자체 린터를 함께 제공할 수도 있다.
이는 언어 서버에 대한 앞선 괄호 속 언급과도 흥미롭게 페어링된다. 언어 서버를 프로젝트의 핵심으로 삼으면 언어 작업의 범위는 커지지만, 커뮤니티 린터를 허용하고 언어의 비본질적인 측면을 린터로 밀어내면, 언어 코어가 신경 써야 할 범위는 줄어든다.
또 이 목록에서 유일하게, 언어가 완전히 처음부터 갖고 있을 필요가 없는 아이디어 중 하나다. 비교적 젊은 언어의 설계 팀이 나중에 추가해도 되고, 심지어 의욕적인 외부 개발자가 기존 프로젝트에 더해도 된다. 파이썬이나 C# 같은 건 관성이 너무 크지만, Nim이나 Zig처럼 아직 어린 프로젝트라면 모멘텀을 붙일 수도 있다.