비트와 자장가

프로그램 언어의 기초 본문

개발/자료

프로그램 언어의 기초

eastriver 2020. 9. 19. 04:57
728x90

모든 프로그램은 순차, 분기, 반복의 집합체로서 동작한다.

 

 

1. 순차sequence

프로그램은 줄마다 순서대로 실행된다.

사람이 물을 컵에 받아 마시는 것을 의사코드pseudo-code(프로그래밍언어의 구조를 나타내기 위해 사용되는 가짜 프로그램언어) 나타낸다면 아래처럼 있다:

 

pick_up(cup)
pour(water, into: cup)
sip(water, inside: cup)

 

컵을 집고

물을 컵에 따르고

컵에 들은 물을 모금 마신다.

 

명령의 순서가 뒤바뀌면 ' 마시기' 실패한다.

순차적으로 실행된다는 사실을 전제하지 않고는 프로그래밍이 불가능하다.

프로그램은 모두 목표가 있고, 목표를 달성하기 위해 작은 절차들을 차례로 실행해야 한다.

 

 

2. 분기selection

프로그램은 조건(조건은 언제나 참/거짓으로 구분할 수 있다) 충족여부에 따라 흐름의 갈래가 나뉜다.

분기는 프로그램의 흐름 제어flow control 기초인 것이다.

따라서 프로그램에는 상황에 따라 실행되지 않는 코드도 존재할 있다.

물을 모금 마셔보자:

 

if(!i.am_holding(cup)) {
  pick_up(cup)
}
if(cup.is_empty) {
  pour(water, into: cup)
}
sip(water, inside: cup)

 

컵을 이미 쥐고 있는 것이 아니라면(i.am_holding(cup) 앞에 붙은 !은 부정negation을 의미한다)

컵을 들고,

컵이 비었다면

물을 채우고,

모금 마신다.

 

각각의 조건을 만족하지 못할 때는 중괄호 안의 명령을 모두 건너 뛴다.

이렇게 프로그램을 두면 손에서 컵을 놓았든 놓지 않았든,

물을 마셔서 컵이 비었든 비어있지 않든 모금을 마실 있는 것이다.

있을 만한 오류의 상황들을 미연에 최대한 방지하는 것이 프로그램의 유연성을 높여야 하는 이유다.

 

 

3. 반복iteration

프로그램은 같은 일을 반복하기 마련이다.

반복에 유용한 프로그램언어의 기능이 가지가 있다.

 

루프loop

 

기본적으로 while루프와 for루프가 있다.

for루프는 while루프로 대체될 있으므로 우선은 while루프만 알아보도록 하자.

물을 마시는 코드를 생각해보자:

 

while(!cup.is_empty) {
  sip(water, inside: cup)
}

 

코드의 실행 순서는 다음과 동일하다:

 

if(!cup.is_empty) {
  sip(water, inside: cup)
}
if(!cup.is_empty) {
  sip(water, inside: cup)
}

...

if(false) {
  sip(water, inside: cup)
}

 

물을 스스로 채우는 잔이 아니라면

반드시 컵은 언젠가 비워지기 때문에

조건은 컵이 비워졌을 false 되고 루프에서 탈출하게 되는 것이다.

 

함수function

 

프로그램 언어에서 함수는 크게 가지 목적을 위해 존재한다.

첫째, 코드의 특정 구간을 모듈화하여 여러 상황에 재사용할 있도록.

둘째, 명령들의 집합을 추상화하여 코드의 직관성을 높이기 위해.

함수는 프로그램 안에 있는 미니 프로그램이다.

 

모든 사람에게 '인생살이'라는 프로그램이 돌아가고 있다고 ,

그곳에는 당연히 '마시기' 함수가 있을 것이다.

(실제 프로그래밍언어가 아닌 의사코드일 뿐이니 구조에 너무 집착하진 말자)

 

func drink(something: liquid) {
  if(!i.am_holding(cup)) {
    pick_up(cup)
  }
  if(cup.is_empty) {
    pour(something, into: cup)
  }
  sip(something, inside: cup)
}

 

위의 drink 함수는 사람이 마시려는 (something) liquid 타입이라면 뭐든 마실 있도록 해준다.

콜라를 마실 ,

drink(coke)

 

물을 마실 ,

drink(water)

 

맥주를 마실 ,

drink(beer)

형태로 함수를 호출call해서 말이다.

coke, water, beer 등을 함수의 인자parameter라고 한다.

 

보다시피 함수는 프로그램의 유연성을 높이는 동시에,

작은 절차로 쪼개진 것들을 추상화된 형태로 표현해 코드의 직관성을 높여준다.

 

챕터 2 분기에서 예시와 위의 drink(water) 비교할 , 후자가 이해하기 쉬운 것은 당연한 일이다.

 

 

+. 상식

일반적으로 컴퓨터는 기계어machine language라고 불리는 0 1 이진binary명령만으로 동작한다.

0 혹은 1, 컴퓨터가 명령을 구분하기 위한 가장 작은 단위를 비트bit라고 한다.

(다만, 양자컴퓨터는 0과 1의 중첩상태를 포함한 큐비트로 동작한다)

여덟 개의 비트는 다시 바이트라는 단위로 묶이는데,

일반적으로 우리가 기계어를 읽을 (그래야 할 때가 있다) 비트를 하나씩 읽는 것이 무척 괴로운 일일 것이다.

사람은 기계어를 이진법으로 읽지 않는다.

 

다행히 사람들은 (싸이코패스가 아니라면) 비트를 8개씩 묶어서 확인한다.

우선 4비트 묶고, 4비트 다시 두 개 묶는다.

4비트가 표현할 수 있는 경우의 수는 2^4이니, 16진법(123456789abcdef) 사용하면 두 자리만으로 하나의 바이트 표현할 있다.

이러한 묶음 방식은  현대의 컴퓨터 아키텍쳐가 byte-addressable이기 때문에 사용되는 것이다.

 

그리하여

십진법 200,

이진법으로 1100 1000이고,

십육진법으로 C8이 된다.

 

십진법 51400,

이진법 1100 1000 1100 1000이고,

십육진법으로 C8 C8 것이다.

 

숫자만 가지고 우리가 무엇을 있겠나 싶을 수도 있겠다.

이진의 모스부호로 통신을 있는 것처럼 숫자는 당연히 문자를 나타낼 있다. 아스키ASCII 코드가 그것이다.

 

ASCII conversion chart

 

화면은 수많은 픽셀들로 이뤄져 있고, 픽셀은 좌표의 숫자로 나타낼 있다.

색깔도 역시 RGB 삼원색을 각각 0부터 255 사이의 값으로 나타낸다.

3D 모델의 기본 단위인 폴리곤도 결국 이차원의 픽셀로 변환될, 삼차원 상의 좌표로 이뤄진 삼각형일 뿐이다.

마우스 커서도 당연히 좌표 숫자이다.

양수와 음수도 부호를 나타내는 비트로 표현한다(two's complement에 대해서는 다음 기회에 설명하겠다).

클릭을 했는지 했는지도 숫자로, 스크롤의 속도도 숫자로, 원래 물리가 그렇듯 세상 모든 것을 숫자로 나타낼 있기 때문에

컴퓨터는 그렇게 받아들인다; 이진의 숫자로.

숫자의 연산을 위해 내부적으로 타입type 가진다.

크게는 가장 기본 타입이라 있는 정수integer 소수float 나뉜다.

정수는 이진 연산이 바로 가능하지만 정수부와 소수부를 구분해야 하는 소수의 이진 연산은 다소 복잡하다.

정수의 나눗셈과 소수의 나눗셈은 컴퓨터에게 완전히 다른 일로, 

정수 계산 1/3 값은 0이지만, 소수 계산 1.0/3.0 값은 0.33333333...이다.

다른 예시로, 위의 아스키 테이블에서 확인 가능한 것처럼 문자로서의 '1' 49 해당하지만 정수 1 1 뿐이다. 그래서 1+1 값은 2이지만 '1'+'1' 98 혹은 'b' 것이다.

그러므로 다른 타입끼리의 연산은 본래 서로 호환되지 않아, 연산을 위해서는 타입변환/형변환을 해야 한다.

다만 컴퓨터가 타입끼리의 변환을 자동화해줄 수는 있는데 그게 파이썬이나 자바스크립트 같은 동적타입 언어다.

동적타입 언어에 대해서는 뒤에서 보충 설명하겠다.

 

다시 말해, 우리가 접하는, 기계어를 제외한 모든 프로그래밍 언어는 모두 사람을 위해 존재한다.

이건 절대 과장이 아니다.

컴퓨터와 인간은 아래와 같은 순서로 연결되어 서로 소통한다:

 

🖥 - CPU - 기계어 - 어셈블리어 - 고급high-level언어 - UI - 👤

 

UI(user interface): UI는 컴퓨터의 사용자로부터 소프트웨어의 복잡함을 가리는 역할을 한다.

버튼, 아이콘, 메뉴, 키보드, 마우스, 텍스트 입력창, 경고음 따위가 모두 UI.

절대 다수의 컴퓨터 사용자들은 UI 아래부터는 신경쓰지 않는다.

밑은 모두 프로그래머와 엔지니어의 역할이다.

 

CPU: 0 1들을 해석해 하드웨어를 제어한다. 하드웨어가 제각기 다르기 때문에 CPU 0 1들을 어떻게 해석해야 할지 미리 알려줘야 한다. 이를 임베디드embeded 프로그래밍이라 한다. 아두이노로 AVR 칩을 제어하며 임베디드 프로그래밍을 맛보기할 있다. 똑같은 0 1 조합을 보냈다고 해도 당연히 CPU 따라, 그리고 붙어 있는 부품에 따라, 실행하는 행동은 달라진다.

 

기계어: 0 1 이뤄져 있다. 천공카드 때는 프로그래머가 직접 혹은 조수가 구멍을 일일이 찍어가며 만들었다. 요즘은 최소한 어셈블러가 어셈블리어를 어셈블하여 자동으로 생성한다.

 

어셈블리어: 하나의 언어가 아닌 언어 집합이다. x86, x86-64(amd64), ARM 아키텍쳐가 컴퓨터와 스마트폰에 쓰이며 가장 널리 알려져 있다. 기계어와 1 1.2 정도로 대응하지만 사람이 알아보기 편하도록 알파벳을 쓴다. RISC(reduced instruction set computer) 적은 인스트럭션 set 가져 인스트럭션의 부담이 적도록 설계된 컴퓨터이고, CISC(complex instruction set computer) 인스트럭션의 복잡도를 높이는 대신 인스트럭션의 호출 수를 줄이도록 설계된 컴퓨터이다. 초기에는 CISC 성능이 우수해 지금 대다수의 컴퓨터가 CISC 사용하고 있지만, 기술이 많이 발전하여 둘의 퍼포먼스 차이는 사실상 없다고 한다.

RISC 전력소모가 적어 발열관리에 유리하기 때문에 요즘의 대세는 RISC(최근 애플의 맥이 버린 x86_64 아키텍처가 CISC이고, 옮겨 타기로 한 ARM 칩이 대다수의 스마트폰이 이미 사용 중인 RISC에 해당한다).

 

RISC CISC 차이를 단순한 예시로 나타내자면

x 3 곱한다고 ,

 

RISC에서라면:

mov x a
shl x
add a x

 

x a 저장하고

x 좌측으로 비트쉬프트하고(2 곱하고)

x 저장해둔 a 더한다고 ,

 

CISC에서라면:

mul 3 x

x에 3을 곱한다

 

같은 인스트럭션을 갖고 있어(가능한 예시일 뿐 확인한 바 없다), 이를 한번에 처리할 있는 것이다.

다만, RISC CISC 내부적으로는 결국 같은 일을 수행하므로 속도의 근본적인 차이는 없다.


<깨알 상식>

Margaret Hamilton in 1969, standing next to listings of the software she and her MIT team produced for the Apollo project

마가렛 해밀턴은 생일에 아폴로 11호가 달에 안전히 착륙할 있도록 저만큼의 코드를 수정이 불가능한 방식의 어셈블리어로 짰다. 저기에(+ 해밀턴이 평생 작성한 코드 ) 버그는 없었고, 오히려 마지막 순간에 비행사들을 그들의 실수로부터 목숨을 구해주었다. 소스코드 깃헙에 공개되어 있다.

"There was no second chance" - Margaret Hamilton

 

고급언어는 다시 다음과 같은 순서로 유명한 프로그램 언어들을 대략 정리할 있다:

 

c/c++/rust - swift/go - c# - java/kotlin - python/javascript

 

하나씩 간단히 살펴보자.

 

c: c 모든 현대 언어의 어머니라고 있다. 고급언어임에도 메모리를 직접 조작하는 , 굉장히 하드웨어와 밀접하게 닿아 있어 성능이 달리는 CPU 제어하기 위한 임베디드 프로그래밍에 여전히 자주 쓰인다. 절차지향적으로 설계되었지만, 객체 지향 프로그래밍이 불가능한 것은 아니다. 키워드가 적어 배우기가 쉽지만, 메모리 관리를 수동으로 해주어야 해서 숙달하긴 어렵다.

 

c++: c 객체 지향을 이식한 언어다. c subset으로 c 컴파일해 실행할 있다. swift c++ 만들어졌다. spacex 우주선을 제어할 때도 쓴다고 한다. 일론 머스크가 싫어한다. 내가 보기에도 지지다.

 

rust: 시스템 프로그래밍(운영체제 제작/제어, 하드웨어 제어 임베디드 프로그래밍과 혼용)에서 c/c++ 대체하기 위해 모질라 재단에서 만든 언어. 깐깐한 컴파일러 덕분에 안전한 시스템프로그래밍을 있다. 메모리 관리를 *가비지 컬렉터 대신 소유권의 개념으로 관리한다. 요즘은 전세계 프로그래머들에게 배우고 싶은 언어 1위를 맨날 한다. 비공식 마스코트 ferris rustacean 있다.

ferris rustacean

 

*가비지 컬렉터: 해제되지 않은 메모리에 남은 쓰레기 데이터를 자동으로 수집해 준다. 대신 느려지고, 중간에 멈추기도 하고, 해제 시점을 예측하기 불가능하다는 단점이 있어 저수준low-level 프로그래밍에 불리하다.

 

swift: 애플이 ios/mac 개발에 사용되는 objective-c 대체하기 위해 내놓은 멀티패러다임 언어. 아주 예쁘게 생긴 언어지만 syntax 사실 코틀린을 베꼈다. 다만 구조적으로 훨씬 깨끗하다. 스위프트를 만든 chris lattner swift 처음부터 세계 정복world domination 목표로 하는 언어고, 목표는 겸손한 목표라고 말했다. 현재는 , 리눅스, ios에서 돌아가지만 5.3 기점으로 이제 안에 공식 윈도우즈 지원이 시작된다(비공식적으로 윈도우즈를 이미 지원한다). 시스템 프로그래밍을 목표로 만들어진 것은 아니지만, rust 종종 비교되며 영역을 점차 확장하는 중이다. 가비지 컬렉터 대신 레퍼런스 카운팅 방식으로 메모리를 관리한다. string을 값 복사로 전달한다. 시스템 프로그래밍을 막 지원하기 시작했다.

 

go gopher

go: 구글이 c 대체하고자 만들었지만, 그건 rust 하고 있고, 비동기 프로그램에 특화된 goroutine 기능 덕에 웹 개발의 백엔드 영역에서 퍼포먼스가 필요할 자주 사용된다. 말하자면 백엔드의 c 정도의 위상이다. 미니멀리즘을 지향하는 만큼 키워드가 25 밖에 없어 c처럼 배우기가 아주 쉽다는 장점을 가진다. 역시 가비지 컬렉터를 사용하며, go gopher라는 고퍼쥐 공식 마스코트가 있다. 시스템 프로그래밍 언어로서 사용하는 것도 가능하다.

c#: - 자바를 견제하기 위해 나온 마이크로소프트 자바. 유니티 개발에 사용돼서 사람들이 많이 쓴다. 생긴   자바랑 판박인데, 그래도 자바보단 빠르다. 자바처럼 가비지 컬렉터를 메모리를 관리한다.

 

java: 객체 지향 언어의 대명사 자바다. (끔찍하게도) 모든 게 객체다. 키워드가 많고 일일이 써줘야 해서 무척 빨리 지저분해진다. java virtual machine이라는 가상머신 위에서 바이너리를 돌리기 때문에 portability 가진다는 장점이 있어 세상을 지배했다. 대신 그런 middleman 없는 다른 언어에 비해 상대적으로 느리다. 역시 가비지 컬렉터 사용.

 

kotlin: 자바의 장점은 인정하지만, 자바의 지저분함에 분노를 느껴 jetbrain 개발한 언어. 파이썬스럽게 생겼지만 정적타입 언어다. 구글이 안드로이드 공식 개발 언어로 지정한 이후에 인기가 치솟는 중이다. 안드로이드 개발은 이제 옛날 앱의 유지보수가 아니면 대부분 kotlin으로 한다. 자바와 동일하게 jvm에서 돌아가는 만큼 자바와 100% 호환이 가능하다. 코틀린이 존재하는 지금, 특별한 이유가 없는 한 자바로 무언가를 새로 개발하는 2020년에 objective-c 배우려는 것만큼 모자란 일이라고 생각한다.

 

python: 귀도 로썸이 크리스마스 토이 프로젝트로 만들기 시작한 interpreted 언어. 중괄호가 없다는 언어의 가장 특징이다. 인터프리터를 거치는 만큼 느리고, *동적 타입 언어로서 런타임 에러를 맞닥뜨리기 쉽다. 입문 프로그래밍 언어로 자주 추천된다(나는 스위프트나 코틀린을 추천한다). 현재 머신 러닝에서 절대적인 위치를 차지하고 있다(파이썬의 단점들에 머신 러닝의 발전 속도가 느리다며, 헤게모니를 swift로 가져오려는 움직임이 꾸준하다). 기본 함수가 많아 쉽다는 평가를 많이 받고, 입문장벽이 낮은 만큼 사람들이 정말 많은 것들을 만들어 둬서, 그것들을 활용해 정말 많은 것들을 빠르게 만들어 있다. 파이썬의 진정한 장점은 모든 단점들을 무시할 만한 생산성이다. 91년에 발표된 언어로 커뮤니티의 규모도 상당해 배우는 것도 쉽다. 배워서 손해될 것도 없을 뿐더러 어차피 배우기 싫어도 몇 번 마주치면 배워진다(?).

 

*동적 타입 언어: 어차피 내부적으로는 타입을 사용하면서 없는 척하느라 버그 잡기를 어렵게 만드는 언어 

javascript: 자바랑은 정말 아무 상관이 없다. 당시 자바가 유명해서 이름을 이렇게 지었단다. 하지만 차라리 파이썬이랑 비슷한 interpreted 언어고, 요상한(다 나름의 이유는 있지만) 규칙 때문에 놀림을 많이 받으며, 여러 사람들의 미움을 받는다. 하지만 모질라 재단의 웹어셈블리wasm(아무 언어로나 프론트엔드 개발을 하여 브라우저에서 바이너리를 돌리는 게 가능해지는 중이다—이러면 브라우저로 플랫폼과 상관 없이 AAA 게임을 돌릴 수가 있어진다) 등장하기 전까지는 사실상 프론트엔드 개발에 네이티브로 사용될 있는 유일한 언어이다시피 했고, 앞으로도 오랫동안 현재의 독보적인 위치에서 물러날 같지 않아 보인다. 웹을 얘기할 절대로 빠뜨릴 없는 함수형 프로그래밍 언어로 json 세상에 낳았다는 업적이 있다.

 

처음에 설명했듯, 인간이 이해하기 쉽도록 작성 가능한 고급언어들은 대개 컴파일러로 어셈블리어로 컴파일된 어셈블러에 의해 기계어로 어셈블되어 실행되는 것이다.

고급언어지만 interpreted 언어인 경우에는 컴파일되는 대신 마치 사람처럼 한 줄씩 문자를 읽어가며 실행하는 방식으로 실행된다. 물론 코드를 해석하는 인터프리터 자체는 바이너리로 동작한다.

 

프로그래밍을 처음 접하는 사람이 던지는 흔한 질문 중 하나는,

기술이 이렇게나 발전했는데도 왜 아직도 프로그래밍 언어가 자연어와 다르게 생겼냐는 것이다.

프로그래밍 언어가 자연어와 다른 것은, 프로그램은 근본적으로 구체적이어야 하기 때문이다.

계속 추상화를 말해왔지만, 추상화는 구체성을 숨긴 것일 뿐 구체성이 존재하지 않는 것과는 다르다.

 

모든 프로그래밍 언어는 종류를 막론하고 0에서 1부터 정말 하나씩 모두 쌓아올려야 하는 것이다.

 

언젠가는 하드웨어가 어떻게 0 1 구분하고 0 1 저장하는지도 알려주겠다.

728x90
반응형
Comments