지난 시간에는 프로그래밍 언어 기초 다지기의 첫 번째 시간으로 컴퓨터 과학과 알고리즘, 프로그래밍이란 무엇일까?를 살펴보았습니다. 글의 마지막 부분에서는 프로그래밍 언어가 필요한 이유를 살펴보았죠. 프로그래밍 언어(programming language)란 컴퓨터와 의사소통하기 위해 만든 인공어(artificial language)입니다. 인간 공동체에서 자연스럽게 생겨난 자연어와 대비해 사람이 인공적으로 만든 것입니다.
이번 시간에는 프로그래밍 종류와 프로그래밍 언어별 차이점 및 장단점을 알아보는 시간을 갖겠습니다. 프로그래밍 언어를 분류하는 기준을 정리해보면 다음과 같습니다.
- 저수준 언어와 고수준 언어: 컴퓨터에 친화적인 언어(전자) 사람에 친화적인 언어(후자)
- 절차적 언어, 객체지향 언어, 함수형 언어: 프로그래밍의 초점이 각각 절차, 객체, 함수에 있음
- 명령형 언어와 선언형 언어: 프로그래밍의 초점이 각각 방법(전자)과 목적(후자)에 있음
- 컴파일 언어와 인터프리터 언어: 빌드(build) 과정 있음(전자), 빌드 과정 없음(후자)
- 정적 언어와 동적 언어: 자료형이 컴파일 단계에서 결정(전자), 실행 단계에서 결정(후자)
그럼 먼저 저수준 언어와 고수준 언어의 특징과 차이부터 알아보겠습니다.
저수준 언어와 고수준 언어
저수준 언어와 고수준 언어를 나누는 기준은 편의 대상입니다. 만약 컴퓨터가 사용하는 언어에 가까울수록 즉, 2진수 체계에 가깝다면 저수준 언어(low-evel language), 저급 프로그래밍 언어, 로우 레벨 프로그래밍 언어(low-level programming language)라고 합니다. 반면 사람이 일상적으로 사용하는 자연어에 가깝다면 고수준 언어(high-level language), 고급 프로그래밍 언어, 하이 레벨 프로그래밍 언어(high-level programming language)라고 합니다.
저수준 언어에는 기계어(machine language)와 어셈블리어(assembly language)가 있습니다. 기계어는 2진수, 비트(bit) 단위로 이루어진 명령어 집합입니다. CPU는 저마다 다른 기계어를 사용합니다. CPU가 달라지면 사용하는 기계어도 달라지죠. 기계어는 각 CPU에 특화된 0과 1의 나열로 특별한 변환 과정 없이 컴퓨터가 실행할 수 있는 유일한 언어입니다.
어셈블리어 또는 어셈블러 언어(assembler language)는 기계어와 1:1로 대응되는 언어입니다. 어셈블리어는 사람이 사용하는 알파벳으로 이루어져 있어 기계어보다 비교적 사용하기 편리합니다. 하지만 어셈블리어 역시 기계마다 다릅니다. CPU마다 다른 기계어를 사용하기 때문에 기계어와 일대일로 대응되는 어셈블리어도 각 기계에 적합하게 고쳐야 합니다.
어셈블리어를 사용하던 프로그래머들은 어셈블리어처럼 기계를 직접 제어할 수 있으면서도 따로 어셈블리어를 수정하지 않고도 어느 기계에서나 범용적으로 사용할 수 있는 언어를 만들어야겠다고 생각합니다. 그리고 마침내 1972년 벨 연구소(Bell Labs)의 데니스 리치는 유닉스(Unix) 운영 체제를 보급하기 위해 고수준 언어인 C언어를 개발합니다. C언어의 탄생 배경과 C언어의 기초 문법을 더 자세히 알고 싶은 분들은 C언어 기초 개념 다지기를 참조해주세요.
C 언어를 포함해 우리가 현재 배우 고 있는 대부분의 프로그래밍 언어는 모두 고수준 언어입니다. 과거에 비해 훨씬 프로그래밍하기 편한 환경에 있는 것이죠.
절차적 언어, 객체 지향 언어, 함수형 언어
절차적 프로그래밍과 절차적 언어
절차적 프로그래밍(Procedure Programming, PP)[1]은 프로그램의 작업이 프로그래머가 정한 순서에 따라서만 수행되는 언어입니다. C와 알골(ALGOL), 포트란(Fortran)은 대표적인 절차적 언어입니다. 절차적 프로그래밍에서는 순서를 엄격히 지키는 것이 중요합니다. 달리 말하면 절차적 프로그래밍에서는 한 부분이 잘못되면 전체 시스템이 작동하지 않는다는 뜻도 됩니다. 절차적 프로그래밍에서는 잘못된 부분이 있으면 그 부분부터 나머지 부분을 모두 하나하나 손봐야 합니다.
절차적 프로그래밍을 배우면 프로그램의 작동 순서와 원리를 익히기 좋습니다. 프로그램의 순서를 정확히 짜려는 노력을 많이 기울여야 하기 때문이죠. 하지만, 프로그램 규모가 커지면 커질수록 개발은 물론 개발 이후에도 유지보수를 하기가 어렵다는 단점도 있습니다. 소프트웨어가 갈수록 복잡해지고 하드웨어가 발전하면서 절차적 언어로만 프로그램을 만들기에는 한계가 생겨난 것이죠. 절차적 언어의 단점을 보완해 떠오른 대안이 객체 지향 언어(object oriented language)입니다.
객체 지향 프로그래밍과 객체 지향 언어
객체 지향 프로그래밍(Object-Oriented Programming, OOP)에서는 순서보다 기능을 중요시합니다. 절차적 프로그래밍이 단계적인 실행을 중요시한다면 객체 지향 프로그래밍에서는 특정 기능을 수행하는 대상 사이의 관계가 중요합니다.
객체 지향 프로그래밍에서는 이 기능을 수행하는 대상을 표현하기 위해 객체(objects)라는 개념을 이용합니다. 객체란 현실 세계에 존재하는 대상을 추상화하여 소프트웨어 세계에 구현할 대상입니다. 객체는 상태(state)와 행동(behavior)으로 이루어져 있는데, 상태와 행동은 마치 현실 세계의 대상처럼 특징과 동작과도 같습니다. 상태란 특정 시점에 객체가 지닌 정보의 집합, 대상이 지닌 특징 등 명사(nouns)와도 같습니다. 반면 객체의 행동이란 요청이 왔을 때 동사(verbs)와 같이 기능을 수행하는 것을 말합니다.
객체 지향 프로그래밍에서는 공통적인 속성과 기능을 지닌 객체를 묶어서 이 객체들을 한 번에 생성할 수 있는 설계도(blueprint)를 만듭니다. 이 설계도를 클래스(class)라고 합니다. 클래스는 객체의 상태와 행동을 속성(attribute)과 메서드(methods)라는 이름으로 클래스 안에 각각 변수(variable)와 함수(functions)로 정의합니다. 변수나 함수는 프로그래밍에서 값을 참조하고 기능을 수행하기 위한 도구입니다. 변수와 함수 등 프로그래밍 언어에서 사용되는 주요 도구들은 프로그래밍 언어의 핵심 개념(예정)에서 자세히 다루도록 하겠습니다.
객체는 인스턴스(instance)라는 말과 자주 번갈아 가며 사용됩니다. 하지만 객체와 인스턴스는 차이가 있습니다. 인스턴스란 클래스로 선언된 객체가 실제로 메모리에 할당되어 구체화된 것입니다. 클래스를 인스턴스화(initiation) 한다는 것은 만질 수 없는(intangible) 대상인 객체가 실재하는(tangible) 대상인 인스턴스로 만들어지는 과정을 의미합니다. 객체가 소프트웨어 세계에서 구현할 추상적인 대상이라면 인스턴스는 소프트웨어 세계에서 메모리에 할당되어 실제로 구현된 것입니다.
객체 지향 프로그래밍에서는 클래스를 잘 만드는 것이 중요합니다. 튼튼한 건물의 기초는 설계도에서 시작하니까요. 객체 지향 프로그래밍을 한마디로 정의하자면 다음과 같습니다.
객체지향 프로그래밍이란 객체의 설계도(클래스)를 만들고, 클래스를 인스턴스화하여 하나의 유기적인 생명체처럼 작동하는 프로그램을 만드는 일
객체 지향 프로그래밍을 정의하는 구체적인 특징에 대해서는 객체지향편에서 조금 더 자세히 다루겠습니다.
객체 지향 프로그래밍 언어의 종류에는 우리가 배우는 대부분의 언어가 포함됩니다. 안드로이드 앱 프로그래밍과 웹의 백엔드 서버 프로그래밍에 사용하는 자바(Java), 게임 프로그래밍 언어로 사용되는 C++, 웹, 모바일, 데스크톱 애플리케이션 개발에 사용되는 C#, 웹 프로그래밍 언어로 주로 프론트엔드(front-end) 개발에 사용되는 자바스크립트(Javascript), 데이터 사이언스 분야에서 인공 지능 프로그래밍에 많이 사용되는 파이썬(python), 통계 분석에 특화된 R 등은 모두 객체지향 언어이자 고급 프로그래밍 언어입니다.
객체 지향 프로그래밍은 절차적 프로그래밍에 비해 규모가 큰 소프트웨어를 개발하고 관리하는 데 편리합니다. 여러 작은 객체를 모아서 큰 문제를 해결하는 상향식(bottom-up) 접근 구조이기 때문입니다.
하지만 객체 지향 프로그래밍은 다른 객체에서 함수를 호출해오면 프로그램의 구조가 복잡해지는 단점도 있습니다. 독립적인 객체들이 함수 호출로 서로 연결되면 객체들 사이의 의존성이 높아집니다. 또한, 외부 경로에서 객체 내부에 있는 함수를 호출하면 객체 내에 상태가 어느 시점에 어떤 외부 경로를 통해 변경되었는지를 파악하기가 어려워집니다. 이 밖에도 객체 지향 프로그래밍에서 사용되는 함수가 동일한 입력에 대해 다른 결과를 반환(return)하는 경우도 있습니다.
그럼 객체 지향 프로그래밍이 지닌 단점을 보완하는 대안은 무엇일까요? 바로 함수형 프로그래밍(Functional Programming, FP)입니다.
함수형 프로그래밍과 함수형 언어
함수형 프로그래밍이란 순수 함수(pure function)로 코드를 작성해서 프로그래밍의 부작용(side-effects)을 줄이는 프로그래밍 기법입니다. 순수 함수란 부작용(side-effect)이 없는 함수 즉, 어떤 함수에 동일한 인자를 대입했을 때 항상 같은 값을 반환하는 함수입니다. 함수형 프로그래밍에서는 프로그램을 설계할 때 순수 함수를 이용해 기본적으로 객체 내에서 상태 변화가 없는 것을 목표로 합니다. 함수형 프로그래밍에서는 순수 함수를 이용하기 때문에 함수에서 함수로 전달되는 상태만 있을 뿐, 전역 상태 자체가 존재하지 않습니다. 외부 데이터에 의존하거나 외부 데이터를 변경하지 않습니다.
그래서 함수형 프로그래밍은 객체 내부의 상태 변화를 제어하는 어려움이 근본적으로 발생하지 않는 구조입니다. 전역 상태를 아예 제거해버려서 부수 효과를 뿌리부터 차단하는 것입니다. 함수형 프로그래밍은 객체지향 프로그래밍에서 사용하는 상태와 가변 데이터 개념을 제거하고 오로지 수학적 계산을 통한 자료 처리라는 목적에만 집중합니다.
따라서 함수형 프로그래밍에서는 문제가 발생했을 때 객체 지향 프로그래밍에 비해 비교적 문제의 원인을 빠르게 파악하고 유지 보수하기가 편리하다는 장점이 있습니다. 함수형 프로그래밍에서는 객체의 상태가 메서드와 상호작용하도록 설계되는 객체 지향 프로그래밍과는 달리 전체 시스템에 미치는 함수의 영향을 더 쉽게 제어할 수 있습니다.
또한, 객체지향 프로그래밍이 다수가 객체 내에 있는 함수에 동시에 접근하는 멀티스레딩 환경[2]에서 버그 발생의 원인을 찾는데 많은 시간과 비용이 소모되었던 문제가 함수형 프로그래밍에서는 발생하지 않습니다. 이러한 이점으로 함수형 프로그래밍은 대규모 시스템을 구현하는 데 적합합니다.
지금까지 살펴본 절차적 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍은 프로그래밍의 대표적인 패러다임(paradigm)입니다. 각 패러다임은 상호 배타적이지 않습니다. 대부분의 언어는 조금씩 각 언어 패러다임이 지닌 방식을 사용하죠. 패러다임에 붙은 이름은 각 패러다임에서 가장 두드러지고 특징적인 부분을 대표한다고 생각하면 좋습니다. 각 패러다임의 장점을 최대한 활용하면 더 균형 있고 효과적인 프로그램을 만들 수 있습니다.
지금까지 절차적 프로 그래밍과 객체 지향 프로그래밍, 함수형 프로그래밍의 개념을 살펴보았습니다. 절차적 프로그래밍과 객체 지향 프로그래밍을 묶어서는 명령형 프로그래밍(imperative programming)이라고 합니다. 함수형 프로그래밍은 선언형 프로그래밍(declatartive programming)의 한 갈래입니다. 지금부터는 명령형 언어와 선언형 언어의 개념과 특징을 살펴보겠습니다.
명령형 언어와 선언형 언어
명령형 프로그래밍은 무엇을 "어떻게(how)" 할 것인지에 대한 언어입니다. 문제를 "어떤 방법"을 이용해 해결할 것인지에 초점이 있죠. 문제를 해결하는 방법으로 순서를 따라가는 절차적 프로그래밍을 선택할 수도, 기능을 중시하는 객체지향 프로그래밍을 선택할 수도 있는 것입니다. 만약, 사과를 먹고 싶다면 사과를 어디서 구해서 어떻게 먹을 것인지 각 과정의 단계를 순서대로 나열하거나 사과를 먹을 수 있는 기능을 수행하는 객체를 만들어서 사과를 먹는 것이죠.
반면 명령형 프로그래밍의 반대인 선언형 프로그래밍에서는 "무엇(what)"에 초점이 있습니다. 선언형 프로그래밍에서는 "어떻게"는 이미 알고 있습니다. "무엇을" 원하는지가 더 중요합니다. 사과를 어디서 어떻게 얻을지는 관심이 없습니다. 그냥 "사과를 원해요"라고 말하면 사과를 "어떻게" 구해서 먹을지(문제를 해결할지)에 대한 부분은 컴퓨터가 알아서 수행합니다.
한 마디로 명령형 프로그래밍에서는 방법을 명시하고 목표를 명시하지 않습니다. 반대로 선언형 프로그래밍에서는 목표를 명시하고 방법은 명시하지 않습니다. 여기서 말하는 방법이 바로 알고리즘(algorithm)입니다.
명령형 언어(imperative language)의 종류로는 앞서 살펴본 절차적 언어인 C와 C의 전신 모델인 알골(ALGOL), 포트란(Fortran) 등이 있습니다. 선언형 언어(declarative language)에는 대표적으로 HTML, XML, CSS, JSON, SQL이 있습니다. 웹 페이지의 구조를 만드는 HTML은 웹 페이지 구조를 이렇게 설계하겠다고 말하면 컴퓨터가 그대로 구조를 만들어줍니다. 관계형 데이터베이스를 관리하는 데 사용하는 SQL을 사용할 때 우리는 "어떤 데이터를 원해"라고만 하면 데이터를 얻을 수 있습니다. SQL은 알라딘의 지니처럼 우리에게 알아서 데이터를 가져다줍니다.
함수형 프로그래밍 언어의 종류에는 대표적으로 스칼라(Scala), 하스켈(Haskell), 클로저(Clojure) 등이 있습니다. 자바스크립트나, 파이썬, 안드로이드 앱 개발에 사용되는 코틀린(Kotlin)은 함수형 프로그래밍과 객체지향 프로그래밍을 모두 지원하는 다중 패러다임 언어입니다.
컴파일 언어와 인터프리터 언어
다시 고수준 언어와 저수준 언어로 돌아오겠습니다. 고수준 언어는 컴퓨터가 이해할 수 있는 저수준 언어로 변환이 필요합니다. 이때 고수준 언어를 저수준 언어로 번역해주는 프로그램을 컴파일러(compiler) 또는 인터프리터(interpreter)라고 합니다. 컴파일러와 인터프리터는 모두 사람이 이해할 수 있는 언어(소스 코드)를 컴퓨터가 이해할 수 있는 언어(기계어)로 번역해줍니다. 여기서 컴파일러를 통해 번역되는 언어를 컴파일 언어(compiled language), 인터프리터를 통해 번역되는 언어를 인터프리터 언어(interpreted language)라고 합니다. 그럼 컴파일 언어와 인터프리터 언어의 차이점은 무엇일까요?
컴파일 언어와 인터프리터 언어의 차이점은 빌드(build) 과정의 유무입니다. 빌드란 우리가 작성한 소스 코드 파일을 최종적으로 실행 파일(executable files)로 만드는 과정입니다. 실행 파일은 플랫폼(platform)에서 바로 실행이 가능한 기계어로 이루어진 파일입니다. 컴파일(compile) 과정은 빌드 과정의 한 단계로 프로그램을 구성하는 여러 소스 코드 파일을 한곳에 모아 목적 파일(object files)을 만드는 과정입니다. 목적 파일은 CPU가 이해할 수 있는 바이너리 코드(binary code) 또는 가상 머신이 이해할 수 있는 바이트코드(Bytecode)로 이루어진 파일입니다. 목적 파일은 빌드 과정을 통해 최종적으로 실행 파일로 변환됩니다[3].
컴파일 언어는 빌드 과정이 있습니다. 빌드 과정은 코드를 실행하기 전에 1회 진행됩니다. 컴파일 언어에서는 빌드 과정을 거친 후에야 비로소 컴퓨터가 실행할 수 있는 실행 파일이 만들어집니다. 그래서 컴파일 언어는 코드 실행 속도가 빠릅니다. 빌드 과정을 통해 번역된 파일을 컴퓨터가 실행하기만 하면 되기 때문이죠.
반면 인터프리트 언어에는 빌드 과정이 없습니다. 빌드 과정이 없기 때문에 실행 파일도 없습니다. 인터프리터 언어에서는 인터프리터가 코드를 한 줄 한 줄씩 읽고 번역해줍니다. 그래서 인터프리터 언어는 코드 실행 속도가 컴파일 언어에 비해 상대적으로 느립니다. 사람이 작성한 소스 코드를 컴퓨터가 이해할 수 있는 언어로 한 줄씩 번역해주어야 하기 때문이죠. 하지만, 빌드 과정을 거치지 않아 도 되기 때문에 개발 속도가 빠르다는 장점이 있습니다.
컴파일 언어의 종류로는 대표적으로 C, C++, 베이직(Basic) 등이 있습니다. C언어의 빌드 과정을 자세히 알고 싶은 분들은 C언어 기초 개념 다지기를 참조해주세요. 인터프리트 언어의 종류에는 대표적으로 자바스크립트, SQL, 파이썬, 루비, 스크래치 등이 있습니다. 자바는 컴파일 언어와 인터프리트 언어 모두로 분류될 수 있습니다.
지금까지 컴파일 언어와 인터프리트 언어의 개념과 차이점을 알아보았습니다. 마지막으로 정적 언어(statically typed languages)와 동적 언어(dynamically typed languages)를 살펴보겠습니다.
정적 언어와 동적 언어
정적 언어와 동적 언어는 변수의 자료형(data type)을 컴파일 과정에서 결정하는지 아 니면 실행 과정에서 자료형을 결정하는지에 따라 분류됩니다. 자료형이란 변수에 사용되는 데이터 값의 종류(정수, 실수, 문자, 불린 등)를 말합니다.
정적 언어는 컴파일할 때 자료형이 결정됩니다. 그래서 변수를 선언할 때 반드시 변수에 들어가는 값의 자료형을 지정해주어야 합니다. 반면 동적 언어는 변수의 자료형이 실행 과정에서 결정됩니다. 그래서 변수의 자료형을 따로 선언할 필요가 없습니다.
대표적인 정적 언어의 종류에는 C, C++, 자바 등이 있습니다. 동적 언어의 종류에는 자바스크립트, 파이썬, 루비(Ruby), PHP 등이 있습니다.
지금까지 프로그래밍 언어의 정의와 프로그래밍 종류, 그리고 프로그래밍 언어별 차이와 장단점을 알아보았습니다. 다음 시간에는 어떤 프로그래밍 언어를 배우든 알아두면 도움이 되는 프로그래밍 언어의 핵심 개념을 다루겠습니다. 모두 수고 많으셨습니다.
참고 문헌
- [1] 김정민, 「변화하는 프로그래밍 언어. ‘함수형 프로그래밍’이 뜬다」, SPRI, "https://www.spri.kr/download/22433"
- [2] 금광캐는광부, 「정적언어(타입)과 동적언어(타입)」, IT 마이닝, "https://itmining.tistory.com/65"
- [3] 스어네, 「프로그래밍 패러다임(명령형 프로그램(절자적, 객채지향), 선언형 프로그램(함수형))의 정의, 특징, 비교를 간단히 알아가자」, Zzolab Blog :):티스토리, "https://okayoon.tistory.com/entry/프로그래밍-패러다임명령형-프로그램절자적-객채지향-선언형-프로그램함수형의-정의-특징-비교를-간단히-알아가자"
- [4] 양봉수, 「객체지향의 사실과 오해」, 양봉수 블로그, "https://yangbongsoo.gitbook.io/study/oo_fact_and_misunderstanding"
- [5] 위키백과, 「값」, 위키백과, "https://ko.wikipedia.org/wiki/값_(컴퓨터_과학)"
- [6] 위키백과, 「고급 프로그래밍 언어」, 위키백과, "https://ko.wikipedia.org/wiki/고급_프로그래밍_언어"
- [7] 위키백과, 「저급 프로그래밍 언어」, 위키백과, "https://ko.wikipedia.org/wiki/저급_프로그래밍_언어"
- [8] heejeong Kwon, 「[Java] 클래스, 객체, 인스턴스의 차이」, 티스토리, "https://gmlwjd9405.github.io/2018/09/17/class-object-instance.html"
- [9] Matthew Tyson, 「'깔끔한 코드 체계의 기초' 함수형 프로그래밍의 이해」, IT World, "https://www.itworld.co.kr/t/61023/개발자/189028"
- [10] Tyler McGinnis, 「Imperative vs Declarative Programming」, Ui, "https://ui.dev/imperative-vs-declarative-programming"
Footnotes
-
"절차적" 대신 "절차지향" 이라는 단어를 자주 사용하지만 "절차적"을 사용하는 것이 더 정확합니다. ↩
-
하나의 프로세스 내에서 둘 이상의 스레드(thread)가 동시에 작업을 수행하는 것 ↩
-
바이너리 코드는 CPU가 이해할 수 있는 이진 표현법이고, 바이트코드는 가상 컴퓨터가 이해할 수 있는 이진 표현법입니다. C처럼 가상 머신을 사용하지 않는 언어의 컴파일 과정에서는 이진 코드로 이루어진 목적 파일이 만들어집니다. 반면 자바(Java)와 같이 가상 머신 위에서 작동하는 언어의 컴파일 과정에서는 바이트코드가 만들어집니다. 자바의 컴파일 경우에는 자바 바이트코드로 이루어진 목적 파일이 만들어집니다. 자바 바이트코드는 실시간 컴파일러(just-in-time, JIT)를 통해 다시 각 하드웨어의 아키텍처에 맞는 기계어로 번역됩니다. ↩