현대적 컴퓨터는 매우 복잡하지만 단순하게 메모리(Memory), 입력과 출력(I/O; Input and Output)장치, CPU로 나눌 수 있다. 그리고 이런 장치들은 버스(Bus)를 통해 연결된다.
메모리(Memory)는 데이터를 저장하는 부품이다. 메모리는 여러 단독 주택이 모인 주택 복합 단지에 비유할 수 있는데, 각 집마다 주소(Address)가 있으며, 메모리를 사용하려면 꼭 주소를 알아야 한다.
메모리의 종류에는 여러 가지가 있고, 각각의 쓰임새가 있다. 이를 표현한 계층도를 메모리 계층 구조(Memory Hierarchy)라 한다. 아래 그림에서 위에 위치할 수록 접근 빈도 수가 높으며 속도도 빠르지만, 가격이 비싸고 용량이 매우 작다.
레지스터(Register)는 CPU 안에 존재하는 메모리로, 프로그램을 실행하는 데 필요한 값을 임시로 저장할 수 있다. 레지스터는 여러 개가 존재하며, 각기 다른 이름과 용도를 갖고 있다.
캐시(Cache) 메모리는 주기억장치나 보조기억장치에 접근하는 시간을 줄이기 위한 임시 저장장치다. 주기억장치(Main Memory)는 현재 실행하고 있는 프로그램에 대한 정보가 저장되는 부품이다. 여기에는 RAM(Random Access Memory)과 ROM(Read Only Memory)이 있는데, 보통 RAM을 일컫는다. 주기억장치의 단점은 저장 용량이 작고, 전원이 꺼지면 저장된 내용을 잃는다는 것이다. 그래서 이런 단점을 보완하기 위한 메모리가 보조기억장치(Secondary Memory)다. 하드디스크, SSD, USB 메모리, DVD, CD-ROM 등이 여기에 속한다.
살펴본 것처럼 메모리는 데이터를 저장하는 부품이다. 그런데, 데이터는 어떻게 저장이 될까? 컴퓨터가 사용하는 모든 데이터는 비트(Bit)로 저장된다. 비트는 Binary와 Digit의 합성어로, 2가지 상태를 나타낼 수 있는 숫자다.
비트에 저장할 수 있는 데이터는 무엇이든지 상관 없다. 그냥 수로서 0과 1이 될 수도 있고, 화재가 발생했다 / 발생하지 않았다, 왼쪽 / 오른쪽, 존재한다 / 존재하지 않는다 등 추상적인 의미도 저장할 수 있다. 즉, 메모리에는 비트가 저장된다고 할 수 있다. 하지만 비트가 나타낼 수 있는 상태는 오직 2가지이기에 비트 하나만으로는 많은 데이터를 저장할 수 없다. 그래서 컴퓨터는 비트를 여러 개 묶어 바이트(Byte)라는 단위로 데이터를 다루게 된다.
간혹, 워드(Word)라는 단위를 사용하기도 하는데, 이는 CPU가 한번에 처리할 수 있는 데이터 크기를 의미한다. CPU를 구매할 때나 프로그램을 설치할 때 32bit, 64bit 등의 용어를 확인할 수 있는데, 이것이 CPU가 한번에 처리할 수 있는 데이터 크기라고 할 수 있다. 워드의 절반을 하프 워드(Half Word), 워드의 1배를 풀 워드(Full Word), 워드의 2배를 더블 워드(Double Word)라고 한다.
엔디안(Endian)은 비트를 저장하는 방식이다. 다른 컴퓨터로 데이터를 전송할 때는 이를 염두해 둬야 하는데, 데이터 순서가 뒤섞일 수 있기 때문이다. 첫 번째 바이트가 최하위 비트(LSB; Least Significant Bit)쪽에 위치하면 리틀 엔디안(little endian), 최상위 비트(MSB; Most Significant Bit)쪽에 위치하면 빅 엔디안(big endian)이라고 한다.
아래 그림은 같은 데이터가 엔디안에 따라 메모리에 어떻게 저장되는지를 나타낸 것이다.
CPU(Central Processing Unit)는 우리가 입력한 명령어의 처리를 담당하는 부품이다. CPU도 컴퓨터처럼 여러 부품으로 구성되는데, 크게 산술 논리 장치와 레지스터 및 제어 장치로 구성된다.
산술논리장치(ALU; Arithmetic Logic Unit)는 CPU의 핵심 부품으로 연산을 담당하는 장치다. 산술논리장치 내부에는 연산을 수행할 수 있는 여러 가지 논리 회로가 존재한다. 덧셈을 위한 가산기, 뺄셈을 위한 보수기, 시프트 연산을 위한 시프터 등 이러한 논리 회로 등이 얽히고 섥혀 연산을 수행하게 되는 것이다.
산술논리장치는 연산을 의미하는 명령코드(Opcode; Operation Code)와 피연산자(Operand)를 갖고 결과를 산출하게 된다. 명령코드는 CPU의 종류에 따라 다른데 이는 미리 정의되어 있다. 명령코드의 집합을 명령어 집합 (Instruction Set)이라고 하며, 우리가 흔히 쓰는 x86이란 x86 명령어 집합 아키텍처를 일컫는 것이다.
x86 아키텍처의 명령어 개수는 매우 많고 복잡하다. 그래서 이런 컴퓨터를 CISC(Complex Instruction Set Computer)라고 한다. 반대로 모바일 칩셋에 들어가는 ARM 아키텍처의 경우 CISC에서 주로 사용되는 명령어만 남겼는데, 이를 RISC(Reduced Instruction Set Computer)라고 한다.
레지스터는 상술했듯 CPU에 있는 메모리다. ALU가 사용할 데이터, 명령코드, 결과 데이터 등 모두 레지스터에 저장된다. 다시 말해 메모리에 있는 데이터를 조작하기 위해서는 그 데이터를 레지스터로 가져와야 한다는 것이며, 메모리에 데이터를 저장하고 싶을 때도 레지스터에서 메모리로 보내는 과정이 필요하다.
컴퓨터의 내부 부품들은 멋대로 동작하지 않는다. 복잡한 교차로에서 경찰이 도로의 질서를 정리하는 것처럼 컴퓨터 또한 이런 경찰의 역할을 하는 부품이 필요하다. 이런 신호를 제어 신호(Control Signal)라고 하며, 제어장치(Control Unit)는 제어 신호를 내보내, 메모리에서 명령코드와 피연산자들을 가져와서 ALU에게 어떤 연산을 수행할지 알려주고, 결과를 메모리에 돌려준다.
컴퓨터는 계산하다는 의미의 Compute와 행위자를 뜻하는 er의 합성어로 본래 계산을 위해 만들어진 기계이다. 그럼 컴퓨터를 사용하기 위해 우리의 데이터를 넣어주는 방법과 그 결과를 보는 방법이 필요할 것이다. 컴퓨터로 데이터를 주는 것을 입력(Input)이라고 하며, 컴퓨터가 우리에게 데이터를 주는 것을 출력(Output)이라고 한다. 입출력 장치는 여러 가지가 있으며 우리가 그 중 흔히 사용하는 입력 장치는 키보드 및 마우스, 출력 장치는 모니터라고 할 수 있다.
그러면 컴퓨터는 입력을 어떻게 감지할까? 즉, 마우스의 I 버튼이 눌렸는지, 키보드의 버튼이 눌렸는지 등을 어떻게 알 수 있을까? 각 입력 장치는 특정 주기를 가지고, 입력 여부를 감지해 컴퓨터로 보낸다. 이를 폴링 레이트(Polling Rate)라고 한다. 폴링(Polling)은 주기적으로 무언가를 처리하는 기법을 의미하는데, 예를 들어 어떤 입력 장치의 폴링 레이트가 1000Hz라고 한다면 1000분의 1초마다 입력을 감지한다는 것을 의미한다.
그러면 모니터는 어떻게 화면을 그리게 될까? 폴링 레이트와 비슷하게 리프레시 레이트(Refresh Rate)가 있다. 모니터는 특정 주기마다 새로운 화면을 그리는 데, 이를 리프레시(Refresh)라고 한다. 어떤 모니터의 리프레시 레이트가 75Hz라면 1초에 75번 화면을 그려주는 것이다. 헌데, 여기서 문제가 생긴다. 게임의 프레임과 모니터의 리프레시 레이트가 일치하지 않는다면 테어링(Tearing) 현상이나 스터터링(Stuttering) 현상이 발생할 수 있다. 이를 해결하기 위한 방법이 수직 동기화(Vertical Synchronization)이다.
컴퓨터 아키텍처(computer architecture)는 컴퓨터의 여러 구성요소를 배치하는 방법을 말한다. 가장 흔한 컴퓨터 구조는 폰 노이만(Von Neumann) 구조와 하버드(Harvard) 구조다. 두 구조는 메모리 배열 외에는 모두 동일한 구조로 하버드 구조가 명령어와 데이터를 동시에 가져올 수 있어 좀 더 빠르지만 두 번째 메모리를 처리하기 위한 버스가 더 필요하다.
자, 컴퓨터를 조립하는 방법까지 알았으니 이제는 정말 사용할 수 있는 것일까? 그렇지 않다. 각 부품을 전체적으로 관리해줄 프로그램이 필요하다. 이를 운영체제(Operating System)라고 한다. 운영체제는 프로그램을 실행하기 위해서 메모리, CPU와 같은 여러 물리적인 자원을 관리한다. 모든 응용 프로그램은 운영체제 위에서 동작한다는 것을 잊지 말자.
그럼 프로그램을 실행하면 무슨 일이 일어날까? 프로그램을 실행하면 프로세스(Process)가 된다. 프로세스는 운영체제로부터 자원을 할당 받은 개체를 의미한다. 프로세스는 프로그램을 실행하기 위한 여러 가지 데이터를 갖고 있으며, 아래와 같이 4가지 영역이 존재한다.
스레드(Thread)는 프로세스 내에서 실행 흐름의 단위를 말한다. 프로세스는 데이터만 관리하고, 이 데이터를 갖고 실행을 담당하는 건 스레드이다. 프로세스는 하나 이상의 스레드를 가질 수 있는데, 이를 멀티스레드(Multi-thread)라고 한다. 스레드가 여러 개면 여러 개의 실행 흐름을 가지므로, 여러 가지 일을 동시에 병렬적으로 처리할 수 있다.
이번에는 프로그래밍 언어에 대해서 알아보자. 프로그래밍 언어에는 저급 언어(Low-level Programming Language)와 고급 언어(High-level Programming Language)가 있는데, 기계에 가까울수록 저급이라고 표현하며, 사람에게 가까울수록 고급이라고 표현한다. 다시 말해 저급 언어는 컴퓨터가 이해하기 쉬운 언어이며, 고급 언어는 사람이 이해하기 쉬운 언어라고 할 수 있다.
프로그램의 명령어는 메모리에 저장된다. 즉, 명령어도 0과 1로 이루어져 있으며 이를 기계어(Machine Language)라고 한다. 하지만 사람이 기계어를 읽기에는 굉장히 힘들기 때문에 이와 1:1로 대응되는 어셈블리어(Assembly Language)가 있다. 이 두 언어는 저급 언어에 속하며, 이 외의 C, C++, C#, Java, Javascript, Python 등의 언어는 고급 언어다. 예시를 하나 보도록 하자.
왼쪽은 기계어, 오른쪽은 어셈블리어다.
왼쪽 이미지와 오른쪽 이미지 모두 같은 프로그램이다. 기계어는 아예 사람이 읽을 수 없고, 어셈블리어는 그나마 기계어보다는 낫지만 그래도 꽤 복잡하다.
현대의 프로그램은 거의 대부분 고급 언어를 이용해 작성되지만, 프로그램을 실행하려면 결국 고급 언어로 작성된 코드를 저급 언어로 변환하는 과정이 필요하다. 고급 언어를 저급 언어로 변환하는 방법에는 2가지가 있다. 바로 컴파일(Compile)과 인터프리트(Interpret)다.
컴파일은 코드 전체를 저급 언어로 변환하는 것인데, 이는 컴파일러라는 프로그램을 통해 처리된다. 컴파일러는 코드를 분석하며 문법 오류는 없는지, 실행 가능한 코드인지, 실행하는 데 불필요한 코드는 없는지 등을 검사하며 변환한다. 만약 컴파일 중 오류가 하나라도 발견되면 컴파일을 실패하게 된다.
인터프리트는 이와 달리 코드 전체를 변환하는 과정을 거치지 않고, 일단 실행 후 코드를 한 줄씩 그때그때 변환한다.그래서 만약 코드에 오류가 있다면 오류를 일으키는 코드를 해석하기 전까진 코드를 쭉 실행하며, 비로소 오류가존재하는 코드에 도달해야 변환이 실패하게 된다. 마찬가지로 인터프리트를 하는 프로그램이 있는데, 이를 인터프리터(Interpreter)라고 한다.
코드를 작성한 후 이를 실행 가능한 프로그램으로 만드는 과정을 빌드(Build)라고 하는데, 빌드 중 컴파일이 필요한 언어를 컴파일 언어(Compiled Language), 인터프리트가 필요한 언어를 인터프리트 언어(Interpreted Language)라고 한다. 두 언어는 개발에 있어서도 차이점을 가지는데, 컴파일 언어는 코드 전체를 변환해야 하기에 빌드 시간이 다소 걸리지만 실행이 빠르고, 인터프리트 언어는 빌드 시간이 짧지만 실행이 다소 느리다. 다만, 이를 무 가르듯 양단할 순 없으며 일부 언어는 두 과정이 동시에 존재하기도 한다.