프로그래밍 언어를 사고 도구가 아닌 구현 도구로 바라보고, 문제 해결과 설계 단계에서는 수학의 언어로 자유롭게 추상화하고 표현하라고 제안한다. 구현상의 제약, 추상화의 경직성, 데이터 표현의 선택 문제를 논의하고, 암호화폐 가격 API를 예로 수학적 사고가 어떻게 목표 정의와 검증 절차를 명료하게 만드는지 보여준다.
6/8/19
프로그래머는 프로그래밍 언어 이야기를 아주 좋아한다. 우리는 그 기술적 장단과 미적 특성을 두고 논쟁할 뿐만 아니라, 언어에 우리가 부여하는 가치와 성향과 함께 그것들이 개인적 정체성에까지 스며들곤 한다. 심지어 어떤 이들은 언어결정론의 한 형태, 즉 언어가 타이핑할 수 있게 해 주는 범위 안에서만 사고가 가능하다고까지 옹호한다.
우리는 많은 시간을 코드를 쓰며 보내기에, 언어 설계에 대한 예리한 관심은 정당화된다. 그러나 이러한 논의의 양상은 우리가 그것을 훨씬 더 큰 무엇으로 여기고, 그 본래의 역할을 어쩌면 잊었음을 시사한다. 프로그래밍 언어는 아이디어를 표현하는 사고 도구가 아니라 기계를 지시하는 구현 도구다. 그것들은 온갖 설계적 타협과 실용적 제약으로 가득한 엄격한 형식 체계다. 결국 우리는 그것들이 인간이 컴퓨터를 제어하는 일을 견딜 만하게 만들어 주길 바랄 뿐이다. 반대로, 생각은 자유롭고 유연한 매체를 통해 가장 잘 표현된다.
수천 년 동안 계산에 대해 사고하는 데 효과적으로 사용되어 온 자연스러운 언어는 수학이다. 대부분의 사람은 수학을 자유롭거나 유연한 것으로 여기지 않는다. 무서운 기호와 시험에서 토해 내기 위한 절차 암기를 떠올린다. 또 어떤 이들은 수학이라 하면 범주론, 람다 미적분처럼 계산 자체를 형식화하는 방법을 떠올리지만, 그것들은 프로그래밍 그 자체에 필수적이지는 않다.
이 글의 독자들이 그래프 이론, 알고리즘, 선형대수처럼 수학이 무엇을 다루는지 더 나은 경험을 해 보았기를 바란다. 즉, 논리와 정리를 포함하고, 산문과 기호가 섞여 쓰이는 종류의 수학 말이다(대부분의 기호는 16세기가 되어서야 발명되었다). 이런 수학은 실제 세계의 문제를 이해하기 위해, 신중한 정의와 연역을 통해 논리적 모형을 만드는 일이다. 만약 이런 모습이 잘 떠오르지 않는다면 Trudeau, Stepanov, Manber를 권한다.
수학은 다른 제약에서 벗어나 논리적 구조를 추론할 수 있게 해 준다. 프로그래밍이 요구하는 것도 바로 이것이다. 문제를 해결하기 위한 논리적 체계를 만드는 일 말이다. 프로그래밍의 기본 패턴을 보자.
실제로는 단계 간 상호작용이 있어 일이 그렇게 잘 정리되어 진행되지는 않는다. 설계를 돕기 위해 코드를 먼저 작성할 수도 있다. 그래도 이 기본 패턴은 계속 반복된다.
1번과 2번 단계가 우리 시간과 능력, 노력을 가장 많이 요구한다는 점을 눈여겨보자. 동시에 이 단계들은 프로그래밍 언어에 잘 녹아들지 않는다. 그렇다고 프로그래머들이 에디터에서 이들을 해결하려 드는 것을 막지는 못한다. 하지만 그렇게 하면 뒤죽박죽이고 느리거나, 엉뚱한 문제를 해결하는 코드가 나오기 일쑤다. 프로그래밍 언어가 아직 충분히 좋지 않아서가 아니다. 어떤 형식 언어도 그 일에 능할 수 없기 때문이다. 우리의 뇌는 그런 방식으로 생각하지 않는다. 문제가 어려워지면 우리는 도표를 그리고 동료와 토론한다.
이상적으로는 1, 2단계를 먼저 해결하고 나서 프로그래밍 언어로 3단계를 해결해야 한다. 이렇게 하면 구현 과정 자체가 변한다는 부가 이점도 있다. 수학적 해법을 손에 쥐고 나면, 최종 목표가 무엇인지 알기에 최적의 표현과 구현을 고르는 일, 더 나은 코드를 쓰는 데 집중할 수 있다.
왜 프로그래밍 언어는 사고 도구로는 부담스러울까? 한 가지 이유는 코드 쓰기가 구현상의 고려사항과 떼려야 뗄 수 없이 연결되어 있기 때문이다. 컴퓨터는 온갖 일을 처리해야 하면서 물리적·경제적 제약에 묶인 장치다. 단순한 함수 하나를 쓰는 데에도 얼마나 많은 것을 고려해야 하는지 생각해 보자.
목록은 계속될 수 있다. 요점은, 이런 고려사항들은 함수가 무엇을 하는지와 아무 상관이 없다는 것이다. 함수가 풀려는 문제에서 주의를 빼앗아 간다.
많은 언어가 이런 세부를 숨기려 한다. 이는 특히 평범한 작업에서는 도움이 된다. 하지만 언어는 구현 도구라는 역할을 넘어설 수는 없다. SQL은 가장 성공적인 예 중 하나지만, 궁극적으로 테이블, 행, 인덱스, 타입 같은 구현상의 고려에 매달려 있다. 그래서 프로그래머는 여전히 복잡한 질의를 작성하기 전에, 얻고 싶은 것이 무엇인지 같은 비공식적 용어로 먼저 설계하고 나서 JOIN을 잔뜩 쓴다.
프로그래밍 언어의 또 다른 한계는 추상화 도구로서의 빈약함이다. 공학에서 추상화를 이야기할 때 흔히 구현 세부를 숨기는 것을 뜻한다. 복잡한 연산이나 과정을 내용물이 숨겨지고, 잘 정의된 입출력만 노출되는 "블랙박스"로 포장하는 것이다. 이 상자에는 크게 단순화된 설명이라는 일종의 이야기가 따라붙는다.

블랙박스는 머릿속에 담기엔 너무 버거운 세부들을 감당해야 하는 대규모 시스템 공학에서 필수적이다. 잘 알려진 한계도 많다. 간단한 설명으로는 동작을 완전히 규정할 수 없기에 블랙박스는 누수를 일으킨다. 불투명한 인터페이스는 비효율을 초래하는데, 예컨대 중복과 파편화된 설계 같은 것들이다.
문제 해결의 관점에서 가장 중요한 건, 블랙박스가 경직되어 있다는 점이다. 사용자에게 무엇을 노출할지, 무엇을 잡음으로 숨길지에 대해 어떤 다이얼과 노브를 드러내고 어떤 것은 감출지 명시적으로 결정해야 한다. 그러면서 문제에는 너무 고수준이거나 너무 저수준일 수 있는 고정된 추상화 수준을 제시한다. 예를 들어, 고수준 웹 서버는 JSON을 서빙하는 훌륭한 인터페이스를 제공할 수 있지만, 프로그램의 출력처럼 불완전한 데이터 스트림을 서빙하고자 할 때는 쓸모가 없을 수 있다. 이론적으로는 항상 상자 안을 들여다볼 수 있지만, 코드에서는 어느 한 시점의 추상화 수준이 고정된다.
반대로, 수학에서의 추상화는 정보를 숨기는 것과는 전혀 다르다. 여기서 추상화란 특정 맥락과 관련하여 어떤 대상의 본질적 특징이나 성질을 추려내는 것을 뜻한다. 블랙박스와 달리 아무 정보도 숨지 않는다. 같은 방식으로 새는 일도 없다. 적절한 추상화 수준으로 조절하고, 관점을 재빨리 넘나들도록 권장된다. 이런 질문을 던질 수 있다.
함수를 바라보는 다양한 방식만 봐도 알 수 있다.
수학으로 생각하면, 그때그때 가장 명료함을 주는 방식을 마음껏 쓸 수 있다.
대부분의 추상 개념은 함수처럼 다양한 관점에서 이해될 수 있음이 드러난다. 수학을 공부하면 온갖 문제를 연구하기 위한 다재다능한 관점 도구 상자를 얻게 된다. 먼저 공식을 써서 문제를 설명하고, 이어서 기하학적으로 이해로 전환한 뒤, 군론(추상대수)의 작용을 알아차리고, 이 모든 것이 통합되어 통찰과 이해를 준다.
요약하면, 프로그래밍 언어는 블랙박스를 조립하는 훌륭한 공학 도구다. 함수, 클래스, 모듈을 제공해 코드를 말끔한 인터페이스로 싸는 데 도움을 준다. 그러나 문제를 풀고 해법을 설계할 때 실제로 필요한 것은 수학식 추상화다. 키보드 앞에서 생각하려 들면, 손닿는 블랙박스들이 당신의 시야를 왜곡할 것이다.
프로그래밍 언어가 추상화 능력에서 경직되어 있듯, 데이터 표현 방식에서도 경직되어 있다. 알고리즘이나 자료구조를 구현한다는 행위는 무언가를 표현하는 수많은 가능한 방법 가운데 오직 하나만 고르는 일이며, 거기에 딸려오는 모든 트레이드오프까지 함께 선택하는 일이다. 사용 사례를 염두에 두고 문제를 잘 이해하고 있을수록 트레이드오프를 치르기가 항상 더 쉽다.
예를 들어, 그래프(정점과 간선의 집합)는 인터넷 네트워크, 경로 탐색, 소셜 네트워크 같은 많은 프로그래밍 문제에 등장한다. 정의는 단순하지만, 그것을 어떻게 표현할지 선택하는 일은 어렵고 사용 사례에 따라 크게 달라진다.

vertices: vector<NodeData> edges: vector<pair<Int, Int>> (연결성만 신경 쓴다면 정점은 제거할 수 있다.)
Node { id: Int, neighbors: vector<Node*> }
인접 행렬을 쓸 수도 있다. 각 행이 특정 노드의 이웃을 저장한다: connectivity: vector<vector<int>> 그리고 노드 자체는 암묵적이다.
경로 탐색 알고리즘은 종종 격자 보드에서 암묵적으로 그래프를 다룬다:
walls: vector<vector<bool>>.
수학은 그래프 그 자체를 추론하고 문제를 해결한 다음 적절한 표현을 선택할 수 있게 해 준다. 프로그래밍 언어로 생각하면, 첫 줄의 코드만으로도 특정 표현에 커밋되어 이 결정을 미룰 수 없다.
그래프 표현들이 다형 인터페이스 하나에 싸 넣기에는 너무 다양하다는 점에 주목하라. (컴퓨터 네트워크, 이를테면 인터넷 전체를 나타내는 그래프를 다시 떠올려 보라.) 따라서 완전히 재사용 가능한 라이브러리를 만드는 것은 비현실적이다. 소수의 형식에만 동작하거나, 모든 그래프를 부적절한 표현으로 몰아넣을 수밖에 없다. 그렇다고 라이브러리나 인터페이스가 쓸모없다는 뜻은 아니다. 비슷한 표현은 반복적으로 필요하다(예: std::vector). 하지만 "그래프"라는 개념을 한 번에 영원히 캡슐화하는 라이브러리는 쓸 수 없다. 염두에 둔 몇 가지 형식을 대상으로 한 단순한 제네릭이나 인터페이스가 적절하다.
귀결로서, 프로그래밍 언어는 이론적 도구가 아니라 유용한 구현 도구가 되는 데 초점을 맞춰야 한다. 이를 잘 보여 주는 현대 언어 기능의 좋은 예가 async/await이다. 복잡한 세부를 숨기거나 새로운 개념 이론을 들여오는 게 아니다. 흔한 실무 문제를 더 쉽게 쓰게 해 줄 뿐이다.
수학으로 사고하면 "C 스타일"의 프로그래밍도 더 매력적으로 보인다. 문제를 잘 이해하고 있다면, "만약에"를 대비해 프레임워크와 추상화의 층을 쌓아 올릴 필요가 없다. 문제에 꼭 맞는 프로그램을, 신중히 고른 트레이드오프와 함께 작성할 수 있다.
그렇다면 수학으로 생각한다는 건 어떤 모습일까? 이 절은 좀 더 천천히, 주의 깊게 읽어야 할 것이다. 최근 직장에서 가맹점 대상 암호화폐 가격 책정을 위한 API를 작업했다. 최근 가격 변동을 고려하여 변동성이 큰 시기에는 가맹점이 더 높은 가격을 받도록 권고한다.
이론 공부를 어느 정도 했지만, 다양한 시장 상황에서 어떻게 동작하는지 경험적으로 검증하고 싶었다. 이를 위해, 우리의 API로 거래하는 가맹점을 시뮬레이션하는 봇을 설계해 성능을 확인했다.
BTC/USD (1 day)

정의: 환율 r(t) 는 법정화폐/암호화폐의 시장 환율이다.
정의: 가맹점 환율 r'(t) 는 가맹점이 고객에게 청구하도록 권고되는 수정된 환율이다.
정의: 고객이 상품을 구매하면 그 사건을 구매라 한다. 구매는 법정화폐 가격과 시점으로 구성된다. p = (f, t).
정리: 구매에 대한 암호화폐 수량은 수정된 환율을 적용하여 구한다. t(p) = p(1) / r'(p(2)).
증명: p(1) / r'(p(2)) = 법정화폐 / (법정화폐/암호화폐) = 법정화폐 * 암호화폐/법정화폐 = 암호화폐
정의: 가맹점이 보유한 암호화폐를 판매하면 그 사건을 판매라 한다. 판매는 암호화폐 수량과 타임스탬프로 구성된다. s = (c, t).
정리: 판매로 가맹점이 획득한 법정화폐 금액은 해당 시점의 환율을 적용하여 구한다. g(s) = s(1) * r(s(2)).
증명: s(1) * r(s(2)) = 암호화폐 * (법정화폐/암호화폐) = 법정화폐
정의: 한 묶음의 구매들과 판매들의 잔고는 모든 구매의 암호화폐 합에서 모든 판매의 암호화폐 합을 뺀 값이다. b(P, S) = i = 1..N 합 t(p_i) - j = 1..M 합 s_j(1)
b(P, S) >= 0는 항상 성립해야 한다.
정의: 한 묶음의 구매들과 판매들의 수익은 판매의 법정화폐 합에서 구매의 법정화폐 합을 뺀 값이다. e(P, S) = j = 1..M 합 g(s_j(1)) - i = 1..N 합 p_i(1) >= 0.
정의: 우리는, 전형적인 구매·판매 묶음의 _대부분_에 대해 수익이 음이 아닌 경우에 한해 가맹점 환율이 유리하다고 말한다. r'(t)가 유리하다는 것은 e(P, S) >= 0일 때이다.
유리한 경우, 가맹점은 암호화폐를 수납했다고 해서 법정화폐를 잃지 않는다.
_대부분_과 _전형적인_은 엄밀히 정의하지 않는다.
_전형적인_의 일부로, 가맹점이 제때 암호화폐를 매도한다고 가정할 수 있다. 따라서 어떤 경계 W가 있어 i,j in {1..M}에 대해 s_i(2) - s_j(2) < W라고 가정하자. 구매 금액은 상거래가 이뤄지는 합리적 범위 안에서 무작위로 분포해야 한다. 이를테면 $10-100.
봇의 목표는 r'(t)가 유리함을 검증하는 것이다.
이 정의는 품질을 재는 수단 중 하나일 뿐임에 유의하라. 유리함보다 최악의 경우를 방어하는 것이 더 중요할 수도 있다. 그 경우 우리는 매우 큰 음수의 수익을 내는 구매 묶음을 구성할 수 있는지에 관심을 둘 것이다.
여러 번 반복:
[t0, t1]을 무작위로 선택한다.[t0, t1] 구간 안의 무작위 시점에 구매 집합을 생성한다. 가격은 전형적인 가격 범위 [p0, p1]에 들도록 한다.[t0, t1] 구간 안의 고르게 떨어진 시점(약간의 무작위 노이즈를 줄 수 있다)에 판매 집합을 생성한다. 각 판매는 해당 시점의 전체 잔고에 대해 이뤄져야 한다.이후:
이 예시를 읽으며, 당신은 그 진술들이 자명하다고 느낄지 모른다. 물론 어느 단계도 어렵지는 않다. 하지만 내게 놀라웠던 점은, 얼마나 많은 가정이 수정되었는지와, 유리한 결과의 목표 정의를 선택하는 일이 얼마나 어려웠는지였다. 이 과정 덕분에, 그냥 코드를 쓰는 것으로 시작했다면 고려조차 하지 않았을 가정들을 자각할 수 있었다. 아마 가장 큰 이점은, 글을 써 놓고 나니 동료와 재빨리 검토하여 종이에선 쉬운 수정들을 할 수 있었고, 코드에서라면 바꾸기 힘들었을 것들을 바꿀 수 있었다는 점일 것이다.
수학의 언어로 생각하는 일이 당신의 프로젝트에도 비슷한 이점을 가져오길 바란다! 이 예시는 수학적 사고를 사용하는 여러 스타일 중 하나일 뿐임을 덧붙인다.