코딩을 지탱하는 기술(1 ~ 3장)

July 27, 2018

1장 효율적으로 언어 배우기

1.1 비교를 통한 배움

여러분이 지금 x라는 언어를 배우고 있고, 무엇이 중요하고 무엇이 그렇지 않은지 고민하고 있다고 가정해보자.

또 같은 시기에 다른 언어인 y를 학습하게 된다면 깨닫게 되는 것이 있다. 무엇이 언어에 따라 다른 것인지, 무엇이 이 언어 x와 언어 y에서 공통적으로 사용되는 것인지,
그리고 무엇이 언어 x만을 위한 규칙인지 등이다.

많은 언어에서 공통적으로 사용되는 개념이야말로 중요한 지식이다. 그 지식을 습득하게 되면 또 다른 언어 z를 배우는 것도 매우 수월할 것이다.

각 언어들은 그만이 추구하는 철학과, 해결하고자 하는 문제들을 갖고 있다.

여러 언어를 접하다 보면, 언어들끼리의 유사성(공통된 규칙 등)을 발견하게 되고, 이런 지식들을 익히다 보면, 새로운 언어를 배울 때 좀 더 수월할 수 있다. 또 이 점은 언어뿐 아닌, 프레임워크, 라이브러리 등을 익히면서도 발견할 수 있는 부분이라 생각한다.

규칙은 언어마다 다르다

복수의 언어를 비교해서 학습할 때 알게 되는 것이 있다. 그것은 `규칙은 언어마다 다르다`는 것이다.

어떤 언어의 교과서에 `이런 규칙입니다`라고 쓰여있다 해도, 그것은 `이 언어에서는 이런 규칙을 씁니다`라는 의미일 뿐이다.

예를 들어, C언어에서의 정수 0은 `거짓`, 루비에서는 `참`, 자바에선 `참, 거짓`을 위한 별도(데이터) 형이 존재하기 때문에 판정 시, `컴파일 에러`가 발생하게 된다.

특정 언어(프레임워크, 라이브러리 등)의 규칙들이 모든 언어들에서 일반화될 수는 없다.

또 그 언어라 할지라도 버전에 따라, 그 규칙은 언제든 달라질 수 있다.

1.2 역사를 통한 배움

언어 설계자의 의도를 이해하자

프로그래밍 언어도 사람이 만든 것이다. 언어 설계자는 어떤 문제를 해결하기 위해 그 언어를 만든 것일까?
그 언어가 어떤 흐름에 따라 만들어졌는지 알게 되면 그 기능이 왜 필요한지 납득할 수 있게 된다.

언어(솔루션)가 갖는 모든 규칙(네이밍 등)들은, 설계자의 수많은 고민 끝에 특정 문제를 해결하기 위해 만들어지는 것이다. 그러기에 설계자 관점에서 언어를 이해해보는 것도 매우 좋은 학습 방법이라 생각한다.

어떤 언어를 배워야 하는지는 아무도 모른다

`프로그래밍을 배우고 싶은데 어떤 언어를 배우면 좋을까요?`라는 질문은 무의미하다. 많은 사람들이 `많이 사용되는 언어 x를 배우면 안전하다` 또는
`이 후는 이 분야가 발전할 테니까 지금 언어 y를 배우는 게 유리하다` 등의 조언을 한다. 하지만 앞날은 아무도 알 수 없다....

현재 트렌디한 언어라 할지라도, 그 언어의 지식이 5년 또는 10년 후에도 도움이 될지는 아무도 알 수 없다.

그렇기에 특정 언어만의 지식이 아닌, 언어가 갖는 보편적 특징(공통된 규칙)들을 깊게 이해하는 것이 더 중요하다고 생각한다.

2장 프로그래밍 언어를 조감하다.

2.1 프로그래밍 언어 탄생의 역사

많은 것이 과거의 것을 발전시켜 만들어졌다. 즉 새로운 것은 과거의 것을 알고 난 후에야 만들어지는 것이다. 지금은 `당연하다`고 여기는 것도
과거에는 아직 발견되지 않은 것들이었다. 그러므로 옛날 사람 시점에서 생각하는 방법은 새로운 것을 배우는 사람에겐 매우 유리하게 작용한다.

이미 만들어진 바퀴를 또다시 만들 필요는 없지만, 새로운 바퀴를 만들고자 할 때, 그것의 기반 지식(또는 역사)을 이해하는 것은 매우 중요한 부분이라 생각한다. 보통 그 지식을 통해 더욱 완고한 결과물을 만들 수 있기 때문이다.

FORTRAN의 등장

1954년, 드디어 지금 여러분이 사용하고 있는 것과 비슷한 프로그래밍 언어가 고안되었다. 바로 포트란이다.
포트란이란 이름은 Formula Translating System(수식 변환 시스템)을 의미한다. 지금은 프로그래밍 언어로 `x 곱하기 y 더하기 z`를 x * y + z라고 표현할 수 있는 것을
너무나 당연히 여기지만, 그것을 최초로 표현한 것이 포트란이다.

...

실제로 초기 포트란 컴파일러가 출력하는 기계어는 능숙한 프로그래머가 짠 기계어에 비교하면 효율이 매우 떨어졌다. 그러나 코드량이 눈에 띄게 줄었고, 코드를 읽기 쉬워짐에 따라 많은 사용자 층을 얻을 수 있었다.

이 부분은 근래의 파서들이 발전해 나가는 흐름도 크게 다르지 않다고 생각한다.

2.2 프로그래밍 언어 탄생의 목적

`프로그램은 어떤 것을 편하게 하기 위해 고안된 것이다.` 단, 편하게 하는 것은 부실하게 하는 것과는 다르다.
...

언어에 따라 다른 “편리함”의 의미

프로그래밍 언어의 목적은 `편리함`이라고 말했지만, 그럼 세상에는 왜 수많은 언어가 존재하는 것일까?

그것은 편하다는 의미가 사람에 따라 다르기 때문이다. 어떤 `편리함`을 목표로 했는지, 언어 설계자의 의도를 파악해보도록 하자

...

예를 들어 C++은 빠른 실행 속도를 중시하고 있는 언어이다. C로 만든 코드보다 빠른 속도가 날 수 있도록 고안했지만, 결과적으로 언어 사양이 더 복잡해지고 말았다.

...

모든 언어(솔루션)는 그만의 분명한 정체성(목표)을 반드시 가져야 한다고 생각한다. 그것이 곧, 앞으로 나아가야 할 지표가 되기 때문이다. 또 새로운 규칙을 설계하기 위한 모든 의사 결정 시에도, 이 정체성이 기준이 되어야 한다고 생각한다.

어떤 프로그램을 편하게 만들고 싶은가?

예를 들어 PHP는 웹 서비스를 쉽게 만들 수 있도록 해준다. 그러나 문장 처리를 편하게 하지는 못한다.
그러나 하스켈 등의 ML(Meta-Language) 처리 계통 언어는 ML이라는 이름이 나타내고 있듯이 문장 처리를 쉽게 만들어 준다.

올바른 설계는 사용하는 사람이 무엇을 목적으로 하고 있는지에 따라 달라진다.

다양한 설계 언어가 있는 것은 사람에 따라 다양한 목적이 존재하기 때문이다.

즉 빠른 속도를 위해 만들어진 C++과 쉬운 코드 해석을 위해 설계된 파이썬을 비교해서 C++이 읽기 어렵다느니, 파이썬이 느리다느니 하는 것은 유익하지 못한 논쟁이다.

모든 언어(솔루션)는 다양한 목적(사용자 타깃 등)에 따른 설계 방향을 갖고 있다. 그렇기에 상황에 따라 자신의 목적과 맞는 언어(솔루션)를 선택하면 그만이라 생각한다.

3장 문법의 탄생

3.1 문법이란?

프로그래밍 언어에는 여러 가지 규칙이 있다.

예를 들면 `덧셈보다는 곱셈이 우선순위가 높다. 1 + 2 * 3이라고 쓰면 2 * 3이 먼저 계산된다.`도 규칙(연산자 우선순위) 중 하나이다.

즉 문법이란, 프로그래밍 언어 설계자가 만든 `이렇게 쓰면 이런 의미로 해석된다`라고 정한 규칙이다.

규칙이란, 결국 문법을 나타내며, 또 그 문법은 언어에 따라 다르다.

연산자 우선순위

만약 1 + 2 * 3이라는 소스 코드가 있다면 이것이 `1에 2를 더해서 그 결과에 3을 곱하는 것` 또는 `2에 3을 곱해서 그 결과에 1을 더한 것` 중 어떤 규칙이 적용되는 것일까?

그것은 사람이 (이해하기) 편한 쪽으로 정하고 있다.(예를 들어 옛날 계산기 중에는 1 + 2 * 3의 계산 결과가 9가 되는 것이 있었다)

...

이는 프로그래밍 설계자가 `+ 보다 *가 우선순위가 높기 때문에 먼저 계산한다`는 규칙을 정했기 때문이다. 또 `그렇게 해야지 사칙연산과 같아서 이해하기 쉽다`는 생각이다.

제품(솔루션) 사양(규칙)을 정의할 때도 크게 고려했던 것 중 하나가 사용자가 부담해야 할 초기 진입장벽이었다. 즉 사용자가 받아들이기 쉬운 규칙들을 정의하는 것이 중요하다고 생각한다.

다만, 이 부분에 있어서는 제품과 언어 설계는 다소 차이가 있을 거라 생각한다.

3.2 스택 머신과 FORTH

계산 순서

1 2 +
위 코드가 어떻게 실행되는지 살펴보자. FORTH의 가장 큰 특징은 `스택(Stack)`, 즉 `값을 쌓아 두는 장소`를 사용한다는 것이다.

1. 1이라는 워드와 만난다. 그리고 스택에 1이라는 값을 담아둔다.

2. 2라는 워드와 만난다. 그리고 스택에 2라는 값을 담아둔다.

3. 마지막으로 +라는 워드와 만난다. 이 워드는 `스택에서 두 개의 값(1, 2)을 꺼내와 그것을 더한 결과를 다시 새로운 스택에 담아`라고 하는 명령과 연결되어 있다.

4. 즉 스택에서 1과 2를 꺼내서 더한 결과인 3을 또 다른 스택에 담는다.

위와 같은 스택 기반의 언어를 직접 입력하는 경우는 거의 없지만 자바, 파이썬, 루비 등의 언어가 이 같은 스택 머신형의 VM을 사용하고 있다. 즉 자바로 프로그램을 짜면 그 프로그램은 내부적으로 FORTH와 같은 프로그램으로 변환(컴파일) 되어 동작한다는 말이다.

현재도 살아있는 스택 머신

파이썬에서는 표준으로 장착되어있는 dis 라이브러리를 사용해 VM이 실행할 명령열을 출력할 수 있다.

아래 순서는 (x + y) * z라는 소스 코드명령열로 컴파일 되는 과정을 보여주고 있다.

1. x를 스택에 담는다.

2. y를 스택에 담는다.

3. 스택에서 x와 y를 꺼내어 더한 후, 그 결과를 새로운 스택에 담는다.

4. z를 스택에 담는다.

5. 스택에 있는 상위 두 개를((x + y)의 결과, z) 곱한다는 명령열로 컴파일된다.

3.3 구문 트리와 LISP

현재도 살아있는 구문 트리

`구문 트리`가 사용되고 있는 것은 최신 언어도 마찬가지다.

파이썬에 기본으로 장착되어 있는 AST(추상 구문 트리) 라이브러리를 사용하면 특정 코드가 어떤 구문 트리로 구성되어 있는지 알 수 있다.

링크를 통해, 각 언어에서 사용되는 구문 트리 결과를 확인할 수 있다.

3.4 중위 표기법

`1 더하기 2를` LISP는 `+ 1 2`라 표현하고, FORTH는 `1 2 +` 일반 수식에서는 `1 + 2`로 표현한다.

이와 같이, 모든 항 앞에 연산자가 오는 경우를 `전위 표기법`, 뒤에 오는 경우를 `후위 표기법`, 중간에 오는 경우는 `중위 표기법`이라한다.

다만, 프로그래밍 언어가 탄생하기 이전부터 사람들은 중위 표기법에 익숙했다.

전위 표기법: + 1 2

후위 표기법: 1 2 +

중위 표기법: 1 + 2

구문 해석기

구문 해석기(파서, Parser)는 소스 코드를 문자열로 읽어 들여 해석하고, 그것을 구문 트리로 만드는 프로그램이다.

FORTRAN에서 프로그램이 컴파일될 때도 이 구문 해석기가 소스 코드를 문자열로 읽어 들여 구문 트리로 변환하는 작업을 하고 있다.

...

문법 설계와 구문 분석기 구현은 프로그래밍 언어의 외관을 결정하는 중요한 요소이다. 언어 설계자는 문법을 설계할 때 무엇을 쉽게 쓸 수 있도록 할 것인지,

어떤 실수를 줄이도록 할 것인지 등 프로그래밍 언어가 사용자에게 어떤 가치를 줄 수 있을지 생각한다.

언어에서 문법과 구분 분석기를 구현하는 것은, 그 언어의 목적을 구현하는 가장 중요한 작업이다.

칼럼

...

`무엇을 배울지, 무엇을 배우면 좋을지`를 정할 때 필연적으로 지표가 될 수 있는 것이 `무엇을 만들고 싶은가?`. 즉 목적을 명확히 하는 것이다.

...

또 `무엇을 만들까?`를 결정할 수 없다면, 혹시 처음부터 완벽한 것을 만들려 하거나, 모두를 만족시키는 것을 만들려고 생각하고 있지는 않은가?

처음부터 굉장한 것을 만들려고 생각해서 오히려 손도 대지 못하고 있다면, 시간이 흘러도 굉장한 것을 만들 수 없다. 우선은 간단한 것도 괜찮으니, 무엇이라도 만들어보는 것이 중요하다.

처음부터 무엇을 만들지에 대해 너무 고민하지 말자. 평소 만들어보고 싶었던 것이 있거나, 내부 구현이 궁금했던 것들부터 시작해보는 것도 나쁘지 않다..

또 그 목적의 결과가 너무 크다면, 그것을 위한 작은 부속들부터 하나하나 만들어가는 것도 하나의 방법이 될 수 있다. 너무 많은 것을 한 번에 다하려 하다, 시작조차 못하는 경우가 많기 때문이다.

3.5 정리

...
현재 대부분의 프로그래밍 언어는 FORTRAN 식의 `다가가기 쉬운 작성법`을 목표로 하고 있다.

하지만 모순 없이 해석할 수 있는 문법을 만들어내는 것은 어려운 작업이다.

특히 나중에 새로운 문법을 추가할 때 기존 문법과 마찰되지 않도록 하는 것은 더욱 그렇다.

이 때문에 현실의 프로그래밍 언어에는 이해하기 어려운 작성법이 여전히 존재하는 것이다.

현대 언어들은 자신만의 철학을 기반으로, 타 언어들의 장점을 흡수해나가는 것이 추세인듯하다.


Profile picture

Written by mohwa