내가 끊임없이 새로운 것을 배우는 이유
시작하기 전에, 이 글은 내가 배웠던 언어들에 대한 소개를 하려는 목적으로 쓰인 것이 아니다. 혹은 어떤 방법론에 대해서 설교하고자 쓰인 글은 더더욱 아니다. 이 글은 그저 호기심이 많은 주니어 개발자가 여러 프로그래밍 언어, 혹은 프레임워크 들을 여행하면서 얻었던 경험과 인사이트를 공유하고 싶다는 생각에서 출발하게 되었다.
언어는 사고를 결정한다
테드 창의 ‘네 인생의 이야기’ 를 원작으로 하는 영화 [컨택트(Arrival)] 는, 어느날 지구에 나타난 낯선 외계인 종족인 ‘헵타포드’와 소통하기 위해 언어학자인 ‘루이즈’가 그들의 언어를 배우기 시작한다. 헵타포드의 언어체계를 이해하면서 루이즈는 그들의 사고체계 마저 이해하게 됨으로써 헵타포드 종족과 점차 동화되기 시작한다.
조지오웰의 [1984] 에서는 영어를 인위적으로 가공한 신어(New word)라는 설정이 나온다. 어느 하나의 단어에 들어있는 여러 의미를 삭제하고 비유와 은유라는 개념을 삭제하면서, 구성원들이 당에 대한 반역을 감히 상상할 수도 없게끔 만드는 ‘통제의 수단’으로써 등장한다.
위에서 소개한 두가지 작품은 모두 ‘언어적 상대성’ (혹은 언어-사고 결정론)을 기반으로 쓰여졌다. 우리의 언어가 생각을 결정하고, 사고체계를 결정하며, 더 나아가 삶을 결정한다는 이론이다.
왜 느닷없이 프로그래밍 언어를 주제로 한 글에서 ‘언어적 상대성’을 설명하고 있는걸까. 혹시 프로그래밍 ‘언어’ 이기 때문에 ‘언어적 상대성’이 프로그래밍 에도 적용 될 수 있다는 말장난 같은 근거를 내세우려고 하는 것일까?
놀랍게도, 그렇다. 그리고 프로그래밍 언어 역시 사고를 결정할 수 있다고 나는 생각한다. 그리고 프로그래밍 언어 역시, 그 안에 담겨있는 철학적 차이가 사람에게 유의미한 영향을 끼친다고 생각한다. 다만 프로그래밍 언어가 영어, 일본어, 한국어 처럼 자연어가 아닐 뿐이다.
“Languages are not just a communication system; it is a control system,” “How Language Programs the Mind, G.Lupyan(2016)”
프로그래밍 언어를 배우면 보이는 것들
처음으로 프로그래밍 언어를 배웠던 때로 돌아가보자. 프로그래밍 언어가 생소한 여러분에게 이런 문제가 주어졌다고 생각해보자. 어떻게 풀 수 있을까?
문자열로 구성된 리스트 strings와, 정수 n이 주어졌을 때, 각 문자열의 인덱스 n번째 글자를 기준으로 오름차순 정렬하려 합니다. (출처: 프로그래머스 ‘문자열 내 마음대로 정렬하기’)
1
2
3
["sun", "bed", "car"]를 2번째 글자 기준으로 정렬 > ["car", "bed", "sun"]
["abce", "abcd", "cdx"]를 3번째 글자 기준으로 정렬 > ["abcd", "abce", "cdx"]
만약에 인간인 우리 입장에서 문제를 해결하고자 한다면 이런식으로 풀 것이다.
[“sun”, “bed”, “car”] 라는 단어들이 주어졌을때, 음… 쭉 둘러보니까 “car”의 두번째 글자가 a 니까 첫번째로 가야하고.. 남은 bed과 sun을 비교했을때 e, u니까 [“car”, “bed”, “sun”] 순으로 정렬하면 되겠다.
하지만 이런 생각의 프로세스를 컴퓨터에게 전달하려면 어떻게 해야 될까? 여러분은 곧 난관에 봉착하게 된다.
- 일단 컴퓨터에 입력값으로 넣을 ‘배열’ 이라는 자료구조에 대한 이해가 필요하다.
- 컴퓨터는 인간처럼 배열을 한눈에 둘러볼수가 없다. 항상 순차적으로 봐야한다.
- 배열 안의 어떤 값을 보고 싶으면 그냥 볼 수 없고, 인덱싱이라는 것을 알아야 한다.
- (프로그래밍 언어마다 다름) 단어도 직관적으로 그냥 볼 수 없고, 마치 배열처럼 인덱싱으로 접근해야 한다.
등등등…
프로그래밍 언어를 작성하기 위해서는 인간적인 직관에서 벗어나 컴퓨터가 이해할 수 있는 논리대로 작성해야 한다. 컴퓨터의 논리대로 작성하기 위해서는 컴퓨터만의 ‘사고’를 이해해야만 한다. 컴퓨터가 이해할 수 있는 형태만을 넣어야 하고, 컴퓨터가 세워놓은 규칙 아래에서 프로그래밍 언어를 기술해야 한다.
여기서 ‘프로그래밍 언어의 사고 결정’은 명백하다. 프로그래밍 언어는 우리가 컴퓨터와 비슷한 생각을 가질 수 있도록 유도하고, 사고 체계를 결정한다. 마치 [컨택트]에서 헵타포드의 언어가 언어학자의 사고체계를 물들인 것 처럼 말이다.
프로그래밍 언어, 저마다의 철학
대부분의 프로그래밍 언어의 공식 문서에는 ‘왜 우리가 이것을 만들었는지’ 에 대한 제작자들의 철학이 기술되어 있다. Python은 다음과 같이 적혀있다.
- 아름다움이 추함보다 낫다. 명시적인 것이 암시적인 것보다 낫다.
- 단순함이 복잡함보다 낫다.
- 복잡함이 난해함보다 낫다.
- 평평함이 중첩된 것보다 낫다.
- 드문드문 한 것이 밀집된 것보다 낫다.
- 가독성이 중요하다.
- etc…
반면 Rust는 다음과 같이 기술되어 있다.
(의역) … 예를 들어 메모리 관리, 데이터 표현, 그리고 동시성과 같은 “시스템 레벨” 작업을 살펴봅시다. 그동안 이러한 프로그래밍 영역은 어렵고 난해하게 여겨져 왔으며 , 오직 “시스템 레벨” 작업을 몇년동안 배워온 선택된 사람들만 다룰 수 있는 영역으로 간주되었습니다. 심지어 이것을 실제로 사용하는 사람들도 그 코드가 취약점, 충돌 또는 손상에 노출되지 않도록 주의 깊게 사용해야 했습니다.
Rust는 이러한 장벽을 허물어내어 예전의 함정을 제거하고 여러분이 안전하게 프로그래밍할 수 있도록 친근하고 다듬어진 도구들을 제공함으로써 이 문제를 해결합니다. “저수준 제어”에 집중해야 하는 프로그래머들은 Rust를 사용하여 관행적으로 발생하는 충돌이나 보안 취약점의 위험을 감수하지 않고도 이를 수행할 수 있으며, 변덕스러운 도구 체인의 세부 사항을 배우지 않아도 됩니다. 게다가 언어는 신속하고 메모리 사용량 면에서 효율적인 신뢰성 있는 코드로 자연스럽게 이끌어주도록 설계되어 있습니다. …
두 언어는 똑같은 프로그래밍 언어이지만, 어떤 문제를 해결하는지에 대한 방향성은 명백히 다르다. Python은 누구든지 쉽게 작성할 수 있는 쉽고 생산적인 프로그래밍을 위해 만들어졌다. Python은 ‘돌아가는 의사 코드(pseudo code)’ 라고 불릴 만큼 간결한 문법과 구조를 가지고 있다.
반면 Rust의 철학을 살펴보면 ‘안정성(stability)’에 대해 더 많이 포커스가 맞춰져 있는 것을 볼 수 있다. 이전의 Low-level(여기서 말하는 ‘저수준’은 컴퓨터와 밀접한 정도이다) 언어들을 사용하면서 감수해야 했던 메모리 안정성 문제를 해결하기 위해 등장했다.
프로그래밍 언어를 체득한다는 것은 그 언어에 깃들어 있는 철학을 습득하는 것과도 같다. 마치 우리가 일본어나 영어, 혹은 그 이외의 언어를 배울때 그 언어권의 문화를 이해하게 되는 것처럼 말이다.
Python으로는 이런 코드를 쓸 수 있다.
1
2
3
4
5
6
7
8
9
10
11
def some_function_A(a):
print(f"Hello {a}")
def some_function_B(b):
print(f"How are you {b}?")
a = "milky"
some_function_A(a)
some_function_B(a)
반면 Rust에서 동일한 형태로 작성하면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let a = String::from("milky");
some_function_a(a);
some_function_b(a);
}
fn some_function_a(a: String) {
println!("Hello {}", a)
}
fn some_function_b(b: String) {
println!("How are you {}?", b)
}
안타깝게도 실행되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Compiling my-project v0.1.0 (/home/runner/test)
Building [ ] 0/1: my
error[E0382]: use of moved value: `a`
--> src/main.rs:4:21
|
2 | let a = String::from("milky");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | some_function_A(a);
| - value moved here
4 | some_function_B(a);
| ^ value used here after move
|
note: consider changing this parameter type in function `some_function_A` to borrow instead if owning the value isn't necessary
...
이 글은 Rust에 관한 글이 아니기 때문에 자세하게 설명은 안하겠지만, 요지는 Rust는 Python에 반해 ‘안정성’을 크게 추구한 언어이기 때문에 Python에 존재하지 않는 새로운 개념이 존재한다. 그렇기에 Rust를 작성할때는 Python과는 다른 생각을 가지고 작성해야한다.
또 한가지 사례를 살펴보자. Python에서 배열을 순회하기 위해서는 다음과 같이 하면 된다.
1
2
3
4
5
some_array = ["apple", "banana", "kiwi"]
for fruit in some_array:
print(f"i want to eat {fruit}")
하지만 Elixir라는 언어에서는 이렇게 순회한다.
1
2
3
4
defmodule CustomList do
def len([]), do: []
def len([head | tail]), do: IO.puts head
end
왜냐하면 Elixir에서는 배열을 Linked List로 간주하기 때문이다. 그렇기에 Elixir에서 리스트는 [head | tail]
로 나눌수 있다. tail은 또 다른 Linked List 이므로 똑같이 head와 tail로 나누어 재귀적으로 순회 할 수 있는 것이다. Elixir는 근본적으로 함수형 프로그래밍을 채택했기에 이런 구조를 선택했다.
이 부분에 대해 더 자세한 이야기를 보고 싶다면 제가 공부한 기록을 구경해보세요 ^^!
이런 사례에서 보듯 프로그래밍 언어들이 가지고 있는 철학들, 해결하고자 하는 문제들은 저마다 다르다. 언어를 기술하는 코더들은 그 언어만의 철학을 자연스럽게 습득하고, 그 언어가 가진 규칙대로 설계를 익힌다.
여행을 떠나듯이 가벼운 마음으로 배우기
프로그래밍 언어를 완전히 체득하는 것은 쉽지 않다. 프로그래밍 언어들은 보통 많게는 몇백 페이지에 달하는 양의 문서를 가지고 있다. 그렇기에 프로그래밍 언어에 완전히 익숙해지기 위해서는 보통 몇개월, 길게는 년 단위까지 시간을 가져가야 할지도 모른다. 실제 자연어도 그렇듯, 언어에 익숙해진다는 것은 사람의 많은 사고력과 경험을 요구한다.
그러나 프로그래밍 언어가 자연어와 다른 것은, 어떤 규약이나 관습이 존재해서 그 틀을 크게 벗어나지 않는다는 것이다. 일본어나 독일어처럼 완전히 새로운 문자를, 발음을 익혀야 할 필요가 없다. 변수, 함수, 반복문, 조건문.. 등의 형식은 여러 프로그래밍 언어에서 반복해서 나타나며, 단지 예약어의 형태나 코드 구조가 조금씩 다를 뿐이다. 함수형 프로그래밍 언어들처럼 극단적으로 구조가 달라지지 않는 이상, 똑같은 개념은 여러 프로그래밍 언어에서 조금씩 변주되서 나타날 뿐이다.
내가 프로그래밍 언어를 새로 배우면서 중점적으로 보는 부분은 앞 챕터에서 설명했듯, ‘그 언어가 무엇을 해결하려고 했는지’이다. 앞서 설명했던 Rust는 모질라 재단에서 웹브라우저의 엔진을 손보던 도중, 기존의 Low-level 프로그래밍 언어들의 메모리 관리 문제를 해결해기 위해 ‘안정성’에 초점을 두고 개발되었다. Golang은 멀티코어 프로세서 시장이 대두되던 도중 C++의 단점을 해결하고, 동시에 생산성을 추구하기 위해 개발되었다.
프로그래밍 언어를 공부하면서 꼭 그 언어의 전문가가 될 필요는 없다. 그 프로그래밍 언어가 어떤 문제를 해결하려고 하는지, 그리고 어떤 철학을 가지고 있는지에 대해서 공부하는 것만으로도 많은 것을 얻어갈 수 있다. 마치 여행하듯, 가볍게 둘러보는 것이다. 내가 주로 새로운 언어를 공부하면서 중점적으로 봤던 부분들은 다음과 같다.
- 새로운 언어와 이전에 공부했던 언어로 짜여진 비슷한 프로그램을 비교해본다. 구체적으로 어떤 점이 다른가? 어떤 부분에서 나아졌고, 어떤 부분에서 배울점이 있는지 스스로 피드백을 해본다.
- 튜토리얼이나 강의에서 등장하는 예시코드들을 참고하는 것도 좋다. 하지만 잠시 예시코드를 끄고, 왜 그런 구조로 코드가 짜여져 있는지 고민해보는 시간을 가지는 것도 좋다.
- 새로운 언어를 체험하면서 좋다고 생각하는 부분을 중점적으로 본다. 단점이라고 생각되는 부분은 (개인적인 경험에 비추어봤을때) 이전 언어의 관습이 그대로 작용해서 일수도 있다. 조금 시간을 가지고 판단하는 것도 좋은 방법이다.
- 설령 그 언어를 계속해서 쓸 수 없는 상황이더라도, 장점을 다른 언어에 적용해서 코드를 더 향상 시킬 수 있다.
핵심은 스스로 계속 피드백 하는데 있다. 결국 우리는 프로그래밍 언어를 ‘문제 해결’을 위해 사용한다. 어떤 문제를 해결하고 싶은지, 프로그래밍 언어들이 내세운 철학과 내가 제기해왔던 문제의 이해관계가 서로 맞는지 스스로 피드백 하는 과정이 중요하다. 가벼운 마음으로 여행을 떠나듯 공부하되, 단순히 예시 코드들을 따라치며 돌아간다는 점에만 초점을 맞추어선 안된다. 건축학도가 여행하며 여러 나라의 건축양식에 영감을 받듯, 새로운 언어를 여행하는 프로그래머 또한 비슷한 마음을 가져야 한다.
그동안 여러 언어를 공부하면서 꼭 전문가가 되어야겠다는 마음으로 공부를 시작했던 적은 없었다. 물론 저마다 학습의 목표와 상황이 다를테지만, 내가 주요하게 느꼈던 동기는 ‘호기심’이다. 지금은 메인으로 두고 있는 Golang을 공부하기로 결심했던 것은 여러 주요 분야의 인프라가 C++에서 Golang으로 전환되고 있다는 아티클을 보고 나서였다. Rust를 공부해보고 싶다는 생각이 들었던 것도 ‘안정성’에 초점을 둔 언어는 어떤식으로 디자인 되어 있을지 궁금해서였다. 나의 공부 동기는 늘 항상 사소한 것 부터 시작했다.
사실 동기는 그렇게 중요하지 않다. 중요한 것은 그렇게 한단계씩 프로그래밍 언어들을 겪으면서 나의 사고가 바뀌어 갔다는 것이다. Python에서 Javascript/Typescript로 넘어가면서 더 넓은 생태계를 마주하면서 시야가 넓어졌고, Golang을 배우면서 코드 구조에 대해서 더 고민해볼 수 있었고, 더 안정성 있는 프로그램을 짤 수 있게 되었다. 이렇듯 프로그래밍 언어는 사용하는 이의 사고를 결정짓는다. 동시에, 발전시킨다.
물론 무작정 여러 언어를 마구 배워보라는 것은 아니다. 어디까지나 상황과 필요성에 맞게 공부해야 하는 것이 프로그래밍 언어이다. 하지만 자신이 한곳에 너무 머물러 있는듯한 느낌을 받는다면, 가끔은 여행을 떠나듯 다른 세계를 둘러보고 오는 것이 어쩌면 도움이 될지도 모른다. 그 길이 ‘프로그래머’라는 스스로의 정체성의 사고에서 새로운 활기를 불어넣어줄지도 모른다. 언어가 생각의 감옥이라면, 그 공간을 늘리고 장악하는 주체는 ‘나’가 되어야 하기 때문이다.