명령형 프로그래밍과 선언형 프로그래밍은 서로 대조되는 두 가지 주요 프로그래밍 패러다임이다. 절차형 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍은 모두 이 두 가지 패러다임을 기반으로 하며, 각각 명령형과 선언형 프로그래밍의 하위 계층으로 볼 수 있다.
"무엇 vs 어떻게" 비교는 명령형 프로그래밍과 선언형 프로그래밍의 차이를 이해하는 데 가장 기본적이고 효과적인 접근 방식이다.
예를 들어, 집을 짓는 과정을 명령형 프로그래밍으로 작성한다면 다음과 같은 단계가 포함될 것이다:
이 명령형 프로그램에서는 집을 짓기 위해 필요한 모든 단계를 구체적으로 설명한다.
반면, 선언형으로 집을 짓는다면:
이처럼, 선언형 프로그래밍은 무엇을 원하는지에 집중하며, 세부적인 구현 방법은 추상화된다.
명령형 프로그래밍은 모든 단계를 정의해야 하므로 추상화가 어려운 반면, 선언형 프로그래밍은 "나는 이것을 원하고, 그것을 얻는 방법은 상관없다"라는 방식으로 추상화를 가능하게 한다.
개발자의 관점에서 보면, 우리가 신경 쓰는 것은 어떤 데이터를 원하는지이지, 어떻게 그것을 얻을 것인지는 아니다. 엔진이나 컴파일러는 우리가 원하는 결과를 얻기 위해 최적의 방법을 찾아낼 수 있을 만큼 충분히 스마트하다.
선언형 패러다임을 사용하는 언어(e.g., Haskell, SQL)에서는 이러한 명령형 요소들이 개발자에게 추상화되어 숨겨져 있다. 우리는 이러한 세부 사항을 신경 쓰지 않아도 된다.
다중 패러다임 언어(e.g., JavaScript, Scala)를 사용하는 경우, 여전히 명령형 코드를 작성할 수 있다. 이를 통해 언어에 내장되지 않은 함수형 프로그래밍 기능을 지원하거나, 코드를 더 선언형으로 만들어 더 읽기 쉽고 이해하기 쉽게 만들 수 있다.
결국 모든 것은 CPU가 이해할 수 있는 명령어로 컴파일된다. 따라서 선언형 프로그래밍은 명령형 프로그래밍 위에 있는 추상화 계층이라고 할 수 있다.
선언형 코드에서 명령형 코드로의 변환은 일반적으로 엔진, 인터프리터 또는 컴파일러에 의해 수행된다. 예를 들어, 자바스크립트에서 배열의 각 요소에 대해 함수를 적용하는 작업을 map
함수를 사용해 선언형으로 표현할 수 있다:
const numbers = [1, 2, 3, 4, 5];
// 선언형 코드
const squares = numbers.map(x => x * x);
console.log(squares); // 출력: [1, 4, 9, 16, 25]
이 코드는 배열 numbers
의 각 요소를 제곱하여 새로운 배열 squares
를 생성한다. 여기서 우리는 무엇을 할 것인지를 선언했지만, 어떻게 이 작업이 이루어지는지는 명시하지 않았다.
이 선언형 코드는 자바스크립트 엔진에 의해 내부적으로 명령형 코드로 변환되어 실행된다. 아래는 map
함수가 내부적으로 어떻게 동작할 수 있는지를 명령형 코드로 표현한 예시이다:
const numbers = [1, 2, 3, 4, 5];
// 명령형 코드
const squares = [];
for (let i = 0; i < numbers.length; i++) {
squares.push(numbers[i] * numbers[i]);
}
console.log(squares); // 출력: [1, 4, 9, 16, 25]
여기서는 for
루프를 사용해 배열 numbers
의 각 요소를 순차적으로 순회하면서 제곱한 값을 새로운 배열 squares
에 추가한다. 이 방식은 어떻게 각 요소를 처리할지 명시적으로 보여주는 명령형 코드이다.
map
함수와 같은 선언형 코드는 자바스크립트 엔진에 의해 내부적으로 명령형 코드로 변환되어 실행된다. 우리가 map
함수를 사용하여 코드를 간결하고 이해하기 쉽게 작성하면, 자바스크립트 엔진이 그 뒤에서 명령형 코드로 변환하여 실제 작업을 수행한다. 이를 통해 선언형 프로그래밍의 추상성과 명령형 프로그래밍의 구체적인 실행 간의 차이를 효과적으로 관리할 수 있다.
"선언형"과 "명령형"이라는 용어들은 절차형, 함수형, 객체지향 프로그래밍 등 다양한 프로그래밍 패러다임의 차이를 이해하는 데 도움이 된다. 이 용어들을 이해하면 코드 라이브러리를 읽을 때, 저자들이 코드 디자인을 어떻게 결정했는지 알 수 있다.
명령형 프로그래밍과 선언형 프로그래밍을 논의할 때, 우리가 사용하는 함수나 라이브러리가 실제로 추상화된 형태임을 잊어버리기 쉽다. 다시 말해, 우리가 "명령형"이라고 부르는 코드도 그 내부에서 선언형 또는 더 높은 수준의 추상화된 코드로 이루어져 있을 수 있다.
예를 들어, 자바스크립트에서 문자열을 한 글자씩 출력하는 프로그램을 생각해보자:
javascript코드 복사
function printString(str) {
let counter = 0;
while (counter < str.length) {
console.log(str[counter]); // 한 글자씩 출력
counter++;
}
}
printString("Hello, World");
이 예시는 우리가 흔히 "명령형"이라고 부르는 코드의 예이다. 여기서 while
루프를 사용해 문자열의 각 문자를 출력하고 있다. 우리는 어떻게 이 작업을 수행할지 명시적으로 지시하고 있다.
하지만 console.log()
함수는 어디에서 왔을까? 우리는 console.log()
가 내부에서 어떻게 동작하는지 전혀 모르며, 실제로 이 함수가 어떻게 작동하는지도 알 필요가 없다. 단지 무엇을 하고 싶은지를 console.log()
에 선언할 뿐이다.
따라서 console.log()
를 사용하는 부분은 선언형으로 볼 수 있다. 이처럼 명령형 코드 안에서도 선언형의 요소가 포함되어 있을 수 있다.
이것이 바로 추상화 딜레마의 시작점이다. console.log()
내부를 살펴보면, 또 다른 함수들이 있을 것이며, 이 함수들 역시 내부적으로 어떻게 동작하는지 알 수 없다. 만약 이 과정을 계속 따라가다 보면, 결국 자바스크립트 엔진의 내부 구현, 그리고 그 뒤에 있는 시스템의 깊숙한 부분까지 파고들어야 할 것이다.
따라서, 명령형 프로그래밍이라고 할 때, 그것이 명령어를 직접 나열하는 방식이라 할 수 있지만, 그 내부에는 더 높은 수준의 추상화가 포함되어 있다는 것을 이해해야 한다. 너무 깊이 파고들면, 모든 것이 추상화된 레이어로 구성된 것임을 알게 될 것이다.
명령형과 선언형 프로그래밍은 완전히 분리된 것이 아니라, 서로 다른 레벨에서 상호작용하고 있다. console.log()
와 같은 함수는 선언형 요소를 명령형 코드 안에 자연스럽게 통합시킨 예라고 볼 수 있다.
명령형 프로그래밍은 프로그램이 어떻게 동작할 것인가를 명확히 기술하는 프로그래밍 스타일이다. 이 스타일에서는 컴퓨터가 수행해야 할 작업을 순서대로 나열하며, 각 작업이 컴퓨터의 상태를 어떻게 변경할지를 명시적으로 지시한다.
결과적으로, 명령형 프로그래밍은 프로그램의 상태 변화를 중점적으로 다루며, 실행 과정의 흐름을 세밀하게 제어할 수 있는 방법론이다. 이 방식은 컴퓨터의 하드웨어 동작 방식과 유사하여, 효율적인 프로그램을 작성하는 데 유리하다.
즉, 명령형 프로그래밍은 프로그램의 작동 방식을 세부적으로 기술하는 하나의 접근 방식이다. 다양한 프로그래밍 언어에서 적용될 수 있으며, 명령형 프로그래밍을 사용하면 코드의 실행 흐름을 명확하게 제어할 수 있다.
명령형 프로그래밍은 초기 컴퓨터 과학에서 가장 널리 사용된 패러다임이다. Fortran과 COBOL은 각각 과학적 계산과 비즈니스 데이터 처리에 사용된 초기 명령형 프로그래밍 언어였다. 이후 C 언어가 등장하면서 명령형 프로그래밍의 표준이 되었고, 현대의 많은 프로그래밍 언어들도 이 패러다임을 따르고 있다.
명령형 프로그래밍은 명령문(statements)을 통해 프로그램의 상태를 변경한다. 명령문은 프로그램이 실행되면서 수행할 구체적인 작업을 지시하며, 이들 명령문이 순차적으로 실행된다.
let count = 0; // 변수 선언 및 초기화 (명령문)
count = count + 1; // 변수의 값을 변경하는 명령문
이 예시에서 let count = 0;
는 변수를 선언하고 초기화하는 명령문이며, count = count + 1;
은 변수 count
의 값을 변경하는 명령문이다. 이 명령문들은 프로그램의 상태를 변경하며, 그 변화는 프로그램의 실행 흐름에 영향을 미친다.
명령형 프로그래밍에서 상태는 프로그램의 핵심 요소 중 하나이다. 프로그램의 상태는 변수로 표현되며, 각 명령문이 실행될 때마다 이 상태가 변화한다. 이 변화는 프로그램의 흐름과 결과에 직접적인 영향을 미친다.
명령형 프로그래밍에서는 변수를 통해 프로그램의 상태를 관리한다. 변수는 데이터를 저장하는 곳으로, 프로그램이 실행되는 동안 값이 변경될 수 있다. 예를 들어, 게임에서 플레이어의 점수나 위치를 변수로 관리하는 경우, 게임이 진행됨에 따라 이 변수들의 값이 변하게 된다. 이러한 변수의 변화가 프로그램의 상태 변화를 의미한다.
명령형 프로그래밍에서는 메모리 관리가 중요한 요소이다. 프로그램이 실행되면서 변수를 저장하거나 변경할 때 메모리가 사용된다. 이때 메모리를 효율적으로 관리하지 않으면 프로그램의 성능이 저하될 수 있다. 따라서, 변수의 생성과 소멸, 메모리 할당과 해제 등을 신경 써야 한다.
아래는 간단한 상태 변화의 예시이다. 이 예시는 사용자가 숫자를 입력할 때마다 합계를 계산하는 프로그램이다.
// 숫자를 입력받아 합계를 계산하는 프로그램
let total = 0; // 초기 상태 (변수)
function addToTotal(number) {
total += number; // 상태 변화: total 값이 변경됨
console.log(`현재 합계: ${total}`);
}
addToTotal(5); // 출력: 현재 합계: 5
addToTotal(10); // 출력: 현재 합계: 15
addToTotal(3); // 출력: 현재 합계: 18
total
변수는 프로그램의 초기 상태를 나타낸다. 이 변수는 사용자가 숫자를 입력할 때마다 상태가 변하게 된다.addToTotal
함수는 사용자가 입력한 숫자를 받아 total
변수에 더하고, 그 결과를 출력한다.addToTotal
이 호출될 때마다 total
변수의 값이 변경되며, 이는 프로그램의 상태 변화를 나타낸다.이 코드에서 변수의 값이 변함에 따라 프로그램의 상태도 변화하며, 그 결과가 달라진다. 이러한 방식이 명령형 프로그래밍에서 상태 변화가 중요한 역할을 하는 방법을 보여준다.
명령형 프로그래밍에서는 조건문, 반복문과 같은 제어 구조를 통해 프로그램의 흐름을 제어한다. 이는 프로그램의 특정 부분을 반복하거나 조건에 따라 분기시킬 수 있게 한다.
if-else
문은 조건문에 해당한다.for
문과 while
문이 이에 해당한다.switch-case
문이 분기문에 해당한다.명령형 프로그래밍의 철학은 프로그램을 일련의 명령문으로 구성하고, 이러한 명령문들이 순차적으로 실행되면서 프로그램의 상태를 변화시킨다는 것이다. 이 패러다임은 컴퓨터의 하드웨어 구조와 직접적으로 맞닿아 있으며, 프로그래머가 프로그램의 실행 순서와 메모리 관리를 직접 제어할 수 있도록 한다. 명령형 프로그래밍은 효율성과 제어력을 중시하며, 이를 통해 복잡한 시스템을 정밀하게 조작할 수 있게 해준다.
# Python의 명령형 프로그래밍 예시: 1부터 10까지의 합 계산
total = 0
for i in range(1, 11):
total += i
print(total)
위의 Python 코드는 명령형 프로그래밍의 전형적인 예를 보여준다. total
변수는 1부터 10까지의 숫자를 더하면서 상태가 변하고, 최종적으로 그 값을 출력한다. 이 과정에서 상태 변화는 명시적으로 관리되며, 프로그램의 흐름은 순차적으로 진행된다.
절차적 프로그래밍은 명령형 프로그래밍의 하위 개념으로, 프로그램을 여러 절차(Procedure)나 함수(Function)로 나누어 문제를 해결하는 방식이다. 이 절차들은 특정 작업을 수행하는 명령어들의 집합으로 구성되며, 프로그램은 이러한 절차들을 순서대로 실행하며, 코드의 재사용성과 모듈화를 통해 프로그램을 더 체계적으로 구성할 수 있게 한다.
절차적 프로그래밍의 중요한 특징 중 하나는 재사용성이다. 예를 들어, 어떤 기능을 여러 곳에서 반복해서 사용해야 한다면, 그 기능을 하나의 함수로 만들어 여러 번 호출할 수 있다. 이렇게 하면 코드가 더 간결하고 유지보수가 쉬워진다.
절차적 프로그래밍에서는 조건문, 반복문, 분기문 등을 사용하여 프로그램의 실행 흐름을 제어할 수 있다. 예를 들어, 특정 조건이 만족되면 어떤 작업을 수행하고, 그렇지 않으면 다른 작업을 수행하도록 프로그래밍할 수 있다.
아래는 절차적 프로그래밍의 간단한 예시이다. 이 예시는 주어진 숫자 배열에서 짝수만 골라 그 합을 계산하는 프로그램이다.
// 숫자 배열에서 짝수만 골라 합을 계산하는 절차적 프로그래밍 예시
function isEven(number) {
return number % 2 === 0; // 짝수인지 확인하는 함수
}
function sumOfEvens(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (isEven(numbers[i])) { // 짝수인 경우에만 합계에 더함
sum += numbers[i];
}
}
return sum; // 최종 합계를 반환
}
const numbers = [1, 2, 3, 4, 5, 6];
const result = sumOfEvens(numbers); // 함수 호출
console.log(result); // 출력: 12 (2 + 4 + 6)
isEven
함수는 숫자가 짝수인지 확인하는 작은 절차(함수)다.sumOfEvens
함수는 주어진 배열에서 짝수의 합을 계산하는 절차다. 이 함수는 배열을 반복하면서 짝수를 찾고, 찾은 짝수를 합계에 더한다.sumOfEvens(numbers)
를 호출하여, 주어진 배열에서 짝수의 합을 계산하고 그 결과를 출력한다.이 코드에서 각 기능이 절차(함수)로 나뉘어 있어, 필요할 때마다 쉽게 호출할 수 있고, 코드가 명확하고 재사용 가능하게 작성되었다. 이러한 방식이 절차적 프로그래밍의 대표적인 예이다.
선언형 프로그래밍은 프로그램의 목적이나 결과를 기술하는 방식으로, '무엇을 할 것인가'에 중점을 둔다. 선언형 프로그래밍에서는 수행해야 할 작업의 세부적인 절차나 방법을 기술하지 않으며, 대신 어떤 결과를 원하는지를 선언하는 방식으로 프로그램을 작성한다. 이를 통해 코드가 보다 간결해지고, 읽기 쉽고, 유지보수가 용이해진다. 이는 선언형 프로그래밍의 가장 큰 장점 중 하나이다.
즉, 어떻게 프로그래밍할 것인가에 대한 하나의 방법론이다. 따라서, 선언형 프로그래밍은 특정 도구나 언어에 국한된 것이 아니라, 다양한 프로그래밍 언어에서 적용될 수 있는 프로그래밍 스타일 또는 패러다임이라고 할 수 있다. 예를 들어, SQL, React, 함수형 프로그래밍 등이 선언형 프로그래밍의 철학을 잘 반영하는 대표적인 사례들이다.
결론적으로, 선언형 프로그래밍은 코드 작성의 철학과 접근 방식에 대한 하나의 방법론이라고 볼 수 있다.
이론적인 개요만 보고는 해당 키워드가 무엇이고 어떻게 사용하는 것인지 와닿지 않는다. 각 방식의 코드 조각들을 살펴보며 차이점을 확인하면 더 명확하게 이해하는데 도움이 된다.
명령형 프로그래밍은 '어떻게' 해야 하는지를 중점적으로 다루는 방식이다. 프로그램이 수행할 작업의 모든 세부 절차를 명시적으로 기술하는 것이 특징이다. 예를 들어, 주어진 배열에서 짝수의 합을 구하는 프로그램을 작성한다고 하자.
// 명령형 프로그래밍 방식
const numbers = [1, 2, 3, 4, 5, 6];
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i];
}
}
console.log(sum); // 출력: 12
이 코드에서는 배열을 하나하나 순회하면서 각 요소가 짝수인지 확인한 후, 짝수일 경우 그 값을 sum
변수에 더하는 과정을 거친다. 이러한 방식에서는 프로그램의 흐름과 상태 변화가 명확하게 드러나며, 절차를 하나씩 기술하는 것이 특징이다.
선언형 프로그래밍은 '무엇을' 할 것인지에 중점을 두는 방식이다. 여기서는 수행할 작업의 세부적인 절차나 방법보다는, 어떤 결과를 원하는지를 선언하는 데 집중한다. 같은 문제를 선언형 프로그래밍 방식으로 해결하는 방법을 살펴보자.
// 선언형 프로그래밍 방식
const numbers = [1, 2, 3, 4, 5, 6];
const sum = numbers.filter(n => n % 2 === 0).reduce((acc, n) => acc + n, 0);
console.log(sum); // 출력: 12
이 코드에서는 filter
메서드를 사용하여 배열에서 짝수만을 추출하고, reduce
메서드를 통해 이들의 합을 구한다. 여기서 중요한 점은 코드가 '어떻게' 이 결과를 도출하는지를 설명하지 않고, '무엇'을 도출하는지만 명확히 드러낸다는 것이다. 이러한 방식 덕분에 코드는 더 간결하고 읽기 쉬우며, 유지보수도 용이하다.
이와 같이, 명령형 프로그래밍과 선언형 프로그래밍은 코드 작성 방식에서 근본적인 차이를 보이며, 각각의 장단점이 존재한다.
초창기 컴퓨터 프로그래밍 언어들은 명령형 스타일을 따랐으나, 시간이 지나면서 더 높은 수준의 추상화가 가능하고 사용하기 쉬운 언어에 대한 필요성이 대두되었다. 이 과정에서 SQL과 같은 데이터베이스 언어와 Prolog와 같은 논리 프로그래밍 언어가 등장하면서 선언형 프로그래밍 패러다임이 확립되었다.
선언형 프로그래밍은 문제를 해결하기 위한 목표를 중심으로 설계된다. 예를 들어, 데이터베이스에서 데이터를 검색할 때, SQL 쿼리는 '어떻게' 데이터를 검색할지를 설명하지 않고, '어떤' 데이터를 원하는지를 선언한다. 이는 개발자가 특정 작업을 수행하는 방법에 집중하는 대신, 최종 결과에 집중할 수 있도록 도와준다.
명령형 프로그래밍에서는 프로그램이 일련의 명령문으로 구성되며, 각 명령문은 실행될 때 특정 작업을 수행한다. 반면, 선언형 프로그래밍에서는 표현식을 사용하여 프로그램의 상태를 기술한다. 표현식은 평가되어 값을 반환하며, 이 값이 프로그램의 결과를 결정한다.
명령문은 프로그램이 수행해야 할 작업을 나타내는 코드의 한 단위다. 명령문은 실행되며, 어떤 작업을 수행하고 나면 결과를 반환하지 않는다. 대신, 프로그램의 상태를 변경하거나 다른 명령문으로 흐름을 이동시키는 역할을 한다.
예를 들어, 다음은 자바스크립트에서의 명령문이다.
let x = 5; // 변수 선언 및 초기화 - 명령문
x = x + 1; // 변수의 값을 변경하는 명령문
위 코드에서 let x = 5;
와 x = x + 1;
은 각각 명령문이다. 이 명령문들은 프로그램의 상태를 변경하고, 그 자체로는 어떤 값을 반환하지 않는다.
표현식은 특정 값을 계산하고 그 값을 반환하는 코드의 단위다. 표현식은 평가(evaluate)되며, 그 결과로 하나의 값을 만들어낸다. 선언형 프로그래밍에서는 이러한 표현식을 사용하여 프로그램의 상태를 기술한다.
다음은 자바스크립트에서의 표현식 예시다.
let y = 10; // 명령문
let z = y + 5; // 표현식이 포함된 명령문
console.log(z * 2); // 표현식
위 코드에서 y + 5
와 z * 2
는 각각 표현식이다. y + 5
는 15를 반환하고, z * 2
는 z
의 값에 2를 곱한 값을 반환한다. 이처럼 표현식은 결과를 계산하여 반환하는 역할을 한다.
다음은 명령문과 표현식을 비교한 예시이다.
// 명령문 예시
let a = 10; // 이 명령문은 a라는 변수에 10을 할당하지만, 그 자체로는 값을 반환하지 않는다.
// 표현식 예시
let b = a + 5; // 이 표현식은 a + 5의 결과인 15를 계산하여 b에 할당한다.
명령형 프로그래밍에서는 명령문을 통해 프로그램의 흐름과 상태를 제어하며, 선언형 프로그래밍에서는 표현식을 통해 프로그램의 결과를 정의한다. 이러한 차이는 코드의 스타일과 작성 방식에서 근본적인 차이를 만들어낸다.
선언형 프로그래밍에서는 상태 변화가 눈에 보이지 않거나 추상화된다. 명령형 프로그래밍에서 변수의 값이 바뀌는 것과 달리, 선언형 프로그래밍에서는 데이터의 흐름이나 상태 변화가 명시적으로 나타나지 않는다. 이는 프로그램의 상태 관리가 자동으로 이루어지도록 하여, 코드가 더욱 단순하고 에러가 적게 발생하게 만든다.
선언형 프로그래밍은 추상화 수준이 높다. 이는 개발자가 저수준의 구현 세부 사항을 신경 쓰지 않고, 문제를 해결하기 위한 고수준의 전략을 세울 수 있게 한다. 예를 들어, SQL에서는 데이터베이스의 최적화 방법을 고려할 필요 없이 단순히 원하는 데이터를 쿼리하는 것으로 충분하다.
선언형 프로그래밍은 병렬 처리를 자연스럽게 지원할 수 있다. 이는 상태 관리가 자동으로 이루어지기 때문에, 프로그램의 각 부분이 독립적으로 실행될 수 있기 때문이다. 명령형 프로그래밍에서는 상태 변화가 병렬 처리의 주요 장애물이 될 수 있지만, 선언형 프로그래밍에서는 이와 같은 문제가 크게 줄어든다.
명령형 프로그래밍에서는 프로그램의 상태가 변경되기 때문에, 병렬 처리를 할 때 상태 변화로 인한 충돌이나 동기화 문제가 발생할 수 있다. 예를 들어, 다음과 같은 코드가 있다고 하자.
let sum = 0;
function addToSum(numbers) {
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
}
addToSum([1, 2, 3, 4, 5]);
addToSum([6, 7, 8, 9, 10]);
console.log(sum); // 출력: 55
이 코드에서 sum
은 전역 상태로, addToSum
함수가 호출될 때마다 그 값이 변경된다. 만약 이 코드가 병렬로 실행된다면, 두 함수가 동시에 sum
에 접근하려고 할 경우, 예상치 못한 결과가 발생할 수 있다. 예를 들어, 값이 중복되거나 손실될 수 있어 sum
의 최종 값이 잘못될 수 있다.
선언형 프로그래밍에서는 상태 변경을 피하고, 각 작업이 독립적으로 수행되도록 코드를 작성한다. 따라서 병렬 처리를 할 때 상태 변화로 인한 문제가 줄어들게 된다. 같은 예시를 선언형 방식으로 바꿔보자.
const numbers1 = [1, 2, 3, 4, 5];
const numbers2 = [6, 7, 8, 9, 10];
const sum1 = numbers1.reduce((acc, n) => acc + n, 0);
const sum2 = numbers2.reduce((acc, n) => acc + n, 0);
const totalSum = sum1 + sum2;
console.log(totalSum); // 출력: 55
여기서는 reduce
메서드를 사용하여 각각의 배열에 대해 독립적으로 합을 계산하고, 마지막에 두 값을 더해 최종 결과를 얻는다. 각 reduce
호출은 서로 독립적이며, 상태를 공유하지 않는다. 따라서 이 코드를 병렬로 실행하더라도 서로 간섭하지 않으며, 안전하게 병렬 처리를 수행할 수 있다.
병렬 처리의 진가가 발휘되는 예시는 데이터를 대량으로 처리해야 할 때이다. 예를 들어, 수백만 개의 숫자가 있는 배열에서 짝수의 합을 계산하는 작업을 병렬로 처리한다고 가정해보자.
명령형 프로그래밍에서는 배열을 나누어 각 부분을 처리한 후, 최종적으로 결과를 병합하는 과정에서 상태 관리 문제로 인해 병목이 생길 수 있다. 그러나 선언형 프로그래밍에서는 이러한 작업이 자연스럽게 병렬로 수행될 수 있다.
const numbers = Array.from({ length: 1000000 }, (_, i) => i + 1);
// 병렬 처리로 가정
const evenNumbersSum = numbers
.filter(n => n % 2 === 0) // 짝수 필터링
.reduce((acc, n) => acc + n, 0); // 합계 계산
console.log(evenNumbersSum); // 큰 숫자 출력
이 예시에서 filter
와 reduce
는 상태 변화를 일으키지 않고, 각각의 요소를 독립적으로 처리한다. 만약 이 작업을 여러 코어에서 병렬로 수행한다고 해도 각 코어가 처리한 부분의 결과를 손쉽게 합칠 수 있다. 상태를 공유하지 않기 때문에 동기화 문제도 발생하지 않는다.
결론적으로, 선언형 프로그래밍은 상태 관리의 복잡성을 줄이고, 병렬 처리에 적합한 구조를 제공하기 때문에, 대규모 데이터 처리나 고성능이 요구되는 상황에서 특히 유리하다.
선언형 프로그래밍의 철학은 '어떻게'보다는 '무엇을'에 집중하는 것이다. 이는 시스템의 복잡성을 줄이고, 사용자가 해결하고자 하는 문제에만 집중할 수 있게 해준다. 선언형 프로그래밍은 일반적으로 문제 해결을 위한 특정 도메인에 맞춰 설계된 언어 또는 프레임워크에서 주로 사용된다. 이러한 도메인 특화 언어(DSL, Domain-Specific Language)는 특정 문제를 해결하기 위해 고안되었으며, 선언형 프로그래밍의 특성을 잘 반영한다.
자바스크립트에서 선언형 프로그래밍의 대표적인 예로 filter
와 map
같은 배열 메서드를 사용할 수 있다.
const employees = [
{ name: 'Alice', department: 'Sales' },
{ name: 'Bob', department: 'HR' },
{ name: 'Charlie', department: 'Sales' },
{ name: 'David', department: 'Engineering' }
];
const salesEmployees = employees
.filter(employee => employee.department === 'Sales')
.map(employee => employee.name);
console.log(salesEmployees); // ['Alice', 'Charlie']
이 자바스크립트 예시는 다음과 같은 방식으로 동작한다.
filter
메서드를 사용하여 employees
배열에서 'Sales' 부서에 속한 직원들만 걸러낸다.map
메서드를 사용하여 이 직원들의 이름만 추출한다.이 과정에서 우리는 'Sales' 부서의 직원 이름을 얻기 위해 데이터를 어떻게 필터링하고 어떻게 가공할지를 명시하지 않고, 무엇을 원하는지를 선언적으로 표현한다. 데이터베이스에서 SQL이 어떻게 최적화된 쿼리를 수행하는지와 비슷하게, 자바스크립트에서는 filter
와 map
메서드가 내부적으로 최적화된 방법으로 배열을 처리한다. 이로 인해 코드가 더 간결하고 직관적이 된다.
함수형 프로그래밍은 선언형 프로그래밍의 한 형태로, 순수 함수(Pure Function)를 중심으로 코드를 작성하는 프로그래밍 스타일이다. 함수형 프로그래밍에서는 부작용(Side Effect)을 최소화하고, 상태 변화를 지양하며, 데이터를 불변(Immutable)으로 다루는 것이 특징이다.
동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부 상태에 영향을 미치지 않으며 외부 상태로부터도 영향을 받지 않는 함수이다.
다른 함수를 인자로 받거나, 함수를 반환하는 함수이다. 이를 통해 함수를 조합하거나 재사용할 수 있다.
함수형 프로그래밍에서는 데이터의 상태를 변경하지 않고, 변경이 필요한 경우 기존 데이터를 복사하여 새로운 데이터를 생성한다.
아래는 함수형 프로그래밍의 간단한 예시이다. 이 예시는 주어진 숫자 배열에서 짝수만 골라 그 합을 계산하는 프로그램이다.
// 함수형 프로그래밍 예시
const numbers = [1, 2, 3, 4, 5, 6];
const sumOfEvens = numbers
.filter(number => number % 2 === 0) // 짝수만 필터링
.reduce((sum, number) => sum + number, 0); // 짝수들의 합 계산
console.log(sumOfEvens); // 출력: 12 (2 + 4 + 6)
filter
메서드는 배열에서 짝수만을 걸러낸다.reduce
메서드는 걸러진 짝수들의 합을 계산한다.이 코드에서는 데이터의 상태를 변경하지 않고, filter
와 reduce
같은 순수 함수로 데이터를 처리한다. 이와 같은 방식이 함수형 프로그래밍의 대표적인 특징이다.
명령형 프로그래밍은 프로그램의 실행 흐름과 상태 변화를 명시적으로 관리하는 패러다임이다. 하드웨어와 직접적으로 연결된 효율적인 코드를 작성할 수 있는 장점이 있지만, 상태 변화와 제어 구조를 직접 다루어야 하므로 코드의 복잡성이 높아질 수 있으며, 병렬 처리에 어려움이 있을 수 있다.
선언형 프로그래밍은 프로그램의 목적을 기술하는 데 중점을 두며, 세부적인 실행 방법을 추상화하여 간결하고 읽기 쉬운 코드를 작성할 수 있다. 이러한 추상화 덕분에 병렬 처리에 유리하지만, 저수준 제어가 어렵고, 학습 곡선이 있을 수 있다.
결국, 각 패러다임은 고유한 장단점을 가지고 있으며, 특정 문제 도메인이나 프로젝트의 요구 사항에 따라 적절한 패러다임을 선택하는 것이 중요하다. 현대의 다중 패러다임 언어(e.g., JavaScript, Python)에서는 이러한 패러다임들을 혼합하여 사용하는 것이 일반적이다.