개요
선언형 프로그래밍은 프로그램이 어떻게 실행되어야 하는지를 단계별로 지시하지 않고, 계산의 논리와 원하는 결과가 무엇인지를 기술하는 프로그래밍 패러다임이다. 개발자는 목표 상태나 관계를 명세하며, 그 명세를 실제로 어떻게 실행할 것인지에 대한 결정은 언어의 구현체, 컴파일러, 런타임 엔진에게 위임된다. 이는 계산의 각 단계를 순서대로 지시하는 명령형 프로그래밍과 대비되는 개념이다.
선언형 프로그래밍은 단일한 언어나 도구를 지칭하는 것이 아니라, 함수형 프로그래밍, 논리 프로그래밍, 제약 프로그래밍, 데이터베이스 질의 언어, 마크업 언어, 설정 언어 등 다양한 하위 패러다임과 언어군을 포괄하는 우산 용어이다. SQL, HTML, CSS, Prolog, Haskell, 정규 표현식, Terraform의 HCL, Kubernetes의 YAML 매니페스트가 모두 이 범주에 속한다.
명확한 단일 정의가 존재하지 않는다는 점도 이 개념의 특징이다. 흔히 다음과 같은 방식으로 정의된다.
- 명령형이 아닌 모든 프로그래밍 스타일
- 계산이 무엇을 수행해야 하는지를 기술하는 고수준 프로그램
- 부작용이 없거나 참조 투명성을 갖는 프로그래밍 언어
- 수학적 논리와 명확한 대응 관계를 갖는 언어
이러한 정의들은 서로 겹치는 부분이 많으며, 실무에서는 대체로 통제 흐름을 명시하지 않고 문제 영역의 언어로 무엇을 계산할지 표현하는 스타일 전반을 가리키는 용어로 통용된다.
정의와 핵심 개념
무엇을 기술할 것인가, 어떻게 기술할 것인가
선언형 프로그래밍의 가장 널리 알려진 요약은 왓(what)과 하우(how)의 구분이다. 명령형 코드는 목적지에 도달하기 위한 경로를 한 단계씩 지시하는 반면, 선언형 코드는 목적지 자체를 기술한다. 예를 들어 택시 기사에게 정확한 회전 경로를 알려주는 대신 목적지 주소만 알려주고 나머지는 기사의 판단에 맡기는 방식에 비유할 수 있다.
이 관점에서 선언형 코드는 상태 변경 명령의 나열이 아니라, 입력과 출력 사이의 관계를 정의하는 표현식의 집합으로 구성된다. 프로그램은 절차가 아니라 원하는 결과에서 발견되어야 할 속성들의 집합으로 구조화된다. 데이터베이스나 규칙 집합이 주어졌을 때, 컴퓨터는 그 속성을 모두 만족하는 해를 스스로 탐색한다.
제어 흐름의 위임
명령형 언어에서 통제 흐름, 즉 어떤 문장이 어떤 순서로 실행되는지는 소스 코드 내에서 문장이 배치된 순서에 의해 결정된다. 선언형 언어는 이러한 통제 흐름을 완전히 기술하지 않는다는 점에서 구분된다. 대신 프로그램은 문제 영역의 용어로 무엇을 달성해야 하는지를 기술하고, 그 결과를 언어 원시 명령의 순차열로 어떻게 달성할지는 언어 구현체에 남겨둔다.
이 위임 구조 덕분에 하나의 선언적 명세로부터 서로 다른 실행 전략이 도출될 수 있다. 예를 들어 동일한 SQL 질의라도 데이터베이스 엔진에 따라 인덱스 스캔, 해시 조인, 정렬 병합 조인 등 서로 다른 실행 계획으로 처리될 수 있으며, 이는 질의를 작성한 사람이 아니라 질의 최적화기가 결정한다.
참조 투명성과 부작용 최소화
많은 선언형 언어는 부작용을 최소화하거나 제거함으로써 이 스타일을 실현한다. 부작용이 없는 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 프로그램의 다른 부분에 영향을 주는 상태 변경을 일으키지 않는다. 이러한 함수는 참조 투명하다고 불리며, 표현식을 그 평가 결과 값으로 치환해도 프로그램의 동작이 달라지지 않는다는 성질을 가진다.
Haskell과 같은 순수 함수형 언어에서는 모든 함수가 부작용이 없으며, 상태 변화는 명시적으로 프로그램의 일급 객체로 표현되는 함수를 통해서만 나타난다. 다만 모든 선언형 언어가 순수한 것은 아니다. Prolog와 SQL처럼 원칙적으로 선언적인 언어도 절차적 스타일의 확장을 함께 제공하는 경우가 흔하다.
논리와 제어의 분리
논리 프로그래밍의 이론적 토대를 정립한 로버트 코왈스키는 1979년 논문에서 알고리즘을 논리(logic)와 제어(control)라는 두 요소로 분해할 수 있다고 제안했다. 논리 요소는 문제 해결에 사용될 지식을 명시하고, 제어 요소는 그 지식을 사용하는 문제 해결 전략을 결정한다. 논리 요소는 알고리즘의 의미를 결정하는 반면, 제어 요소는 알고리즘의 효율성에만 영향을 미친다.
이 공식, 즉 알고리즘 = 논리 + 제어는 선언형 프로그래밍의 핵심 통찰을 압축적으로 표현한다. 프로그래머가 논리 요소만 작성하고 제어 요소는 실행 엔진의 일반화된 탐색 전략에 맡길 수 있다면, 프로그램은 더 짧아지고 검증하기 쉬워지며 동일한 논리에 대해 다양한 실행 전략을 적용해 성능을 개선할 여지가 생긴다.
명령형 프로그래밍과의 대조
두 패러다임의 근본적 차이
명령형 프로그래밍은 컴퓨터가 계산을 어떻게 수행해야 하는지에 대한 구체적인 지시를 제공하는 패러다임이다. 자연어의 명령형 어법이 명령을 표현하는 것과 마찬가지로, 명령형 프로그램은 컴퓨터가 수행할 명령들로 구성된다. 프로그램은 대입문을 통해 변수의 값을 바꾸어 가며 상태를 관리하고, 반복문과 조건문으로 실행 순서를 제어한다.
선언형 프로그래밍은 이러한 상태 변화의 명시적 관리 대신, 데이터 사이의 관계나 계산 결과가 만족해야 할 조건을 표현식으로 기술한다. 명령형 코드가 문장으로 구성되는 반면 선언형 코드는 값으로 평가되는 표현식으로 구성된다는 설명도 널리 통용된다. 문장은 아무런 값도 반환하지 않고 상태를 변경하는 것이 목적인 반면, 표현식은 입력을 출력으로 매핑하는 것이 목적이다.
코드로 보는 비교
다음은 숫자 배열의 합을 구하는 동일한 작업을 명령형과 선언형 스타일로 각각 구현한 예시이다.
// 명령형: 반복문으로 상태(total)를 직접 갱신하며 "어떻게" 합을 구할지 지시한다
function sumImperative(numbers: number[]): number {
let total = 0;
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
// 선언형: "숫자 배열을 하나의 값으로 축약한다"는 관계만 기술한다
function sumDeclarative(numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
짝수만 걸러내는 작업 역시 동일한 대비를 보여준다.
// 명령형: 빈 배열을 만들고, 반복하며 조건에 맞는 값을 하나씩 밀어 넣는다
function evensImperative(numbers: number[]): number[] {
const result: number[] = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
result.push(numbers[i]);
}
}
return result;
}
// 선언형: "짝수인 원소들의 집합"이라는 결과 자체를 표현한다
const evensDeclarative = (numbers: number[]): number[] =>
numbers.filter((n) => n % 2 === 0);
두 스타일 모두 동일한 결과를 산출하지만, 선언형 버전은 반복 변수나 중간 상태를 노출하지 않는다. filter와 reduce가 내부적으로 어떻게 순회하는지는 언어 런타임의 구현 세부 사항으로 남는다.
사용자 인터페이스 갱신에서도 동일한 대비가 나타난다. 다음은 클릭할 때마다 숫자가 증가하는 버튼을 브라우저 DOM API로 직접 조작하는 명령형 구현과, React로 상태를 선언하는 구현을 비교한 것이다.
// 명령형: DOM 요소를 직접 생성하고, 이벤트가 발생할 때마다 텍스트를 수동으로 갱신한다
const button = document.createElement("button");
let count = 0;
button.textContent = `클릭 수: ${count}`;
button.addEventListener("click", () => {
count += 1;
button.textContent = `클릭 수: ${count}`;
});
document.body.appendChild(button);
// 선언형: 상태(count)에 따라 UI가 어떤 모습이어야 하는지만 기술한다
// DOM을 실제로 어떻게 갱신할지는 React가 결정한다
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
클릭 수: {count}
</button>
);
}
실행 모델의 차이
두 패러다임의 실행 모델은 다음과 같이 요약할 수 있다. 명령형 모델에서는 프로그래머가 초기 상태에서 최종 상태로 이르는 각 단계를 순서대로 지정하며, 반복과 조건 분기를 통해 흐름을 직접 통제한다. 선언형 모델에서는 프로그래머가 원하는 결과의 명세만 제공하고, 그 명세를 실제 단계들로 변환하는 책임은 실행 엔진이 맡는다.
이러한 위임 구조는 병렬화에도 유리하게 작용한다. 실행 순서가 소스 코드에 고정되어 있지 않으므로, 실행 엔진은 독립적인 부분들을 병렬로 처리하거나 실행 순서를 재배열할 자유를 가진다. 이는 선언형 프로그래밍이 병렬 프로그램 작성을 단순화하는 경향이 있다고 설명되는 이유이기도 하다.
역사적 배경
초기 논리 프로그래밍의 태동
선언형 프로그래밍의 이론적 뿌리 가운데 하나는 자동 정리 증명과 1차 논리에 대한 연구에서 찾을 수 있다. 1930년대 쿠르트 괴델과 자크 에르브랑의 계산 가능성 연구는 계산을 연역의 한 형태로 바라보는 관점의 초기 형태로 평가된다. 이러한 계산-연역 패러다임은 1970년대 들어 실용적인 프로그래밍 언어로 결실을 맺었다.
1972년 알랭 콜메로에와 필리프 루셀은 프랑스 마르세유에서 자연어 처리를 목적으로 한 연구 과정에서 Prolog를 개발했다. Prolog라는 이름은 프랑스어 Programmation en logique, 즉 논리 안에서의 프로그래밍의 줄임말이다. 프로그램은 사실과 규칙의 집합으로 관계를 정의하며, 계산은 그 관계에 대한 질의를 실행함으로써 시작된다. 같은 시기 에든버러 대학교의 로버트 코왈스키 역시 콜메로에의 연구팀과 교류하며 논리 프로그래밍의 이론적 토대를 다졌고, 1979년 알고리즘을 논리와 제어로 분해하는 유명한 공식을 발표했다.
폰 노이만 스타일에 대한 도전
1977년 존 배커스는 튜링상 수상 강연 폰 노이만 스타일로부터 프로그래밍은 해방될 수 있는가에서 당대의 주류 명령형 프로그래밍 관행을 정면으로 비판했다. 그 자신이 포트란의 설계를 이끈 인물이었다는 점에서 이 강연은 특히 주목받았다. 이 강연은 이후 1978년 커뮤니케이션스 오브 디 에이시엠에 정식 논문으로 게재되었다.
배커스는 당시의 관습적인 언어들이 폰 노이만 컴퓨터 구조, 즉 중앙처리장치와 저장소, 그리고 그 사이를 한 번에 한 단어씩 전송하는 통로로 구성된 구조에 지적으로 종속되어 있다고 주장했다. 그는 이를 폰 노이만 병목이라 불렀으며, 대입문 중심의 워드 단위 사고방식이 프로그래밍 언어를 표현식의 세계와 문장의 세계로 분열시키고, 프로그램을 조합하는 강력한 결합 형식의 부재와 프로그램에 대해 추론할 수학적 성질의 결여로 이어진다고 지적했다. 그는 대안으로 함수 수준 프로그래밍 언어인 FP를 제안하며 함수형 스타일로의 전환을 촉구했다.
함수형 프로그래밍의 발전
배커스의 강연 이후로도 함수형 프로그래밍은 람다 계산법이라는 훨씬 오래된 수학적 토대 위에서 독자적으로 발전해 왔다. 1950년대 후반 존 매카시가 설계한 Lisp은 함수형 스타일과 절차적 스타일을 함께 지원하는 초기 언어였으며, 이후 Scheme, ML, Erlang, OCaml 등이 이어졌다. 1990년에 발표된 순수 함수형 언어 Haskell은 모든 함수가 부작용을 갖지 않도록 강제하는 설계를 통해 참조 투명성을 언어 차원에서 보장한 대표적 사례로 꼽힌다.
관계형 데이터베이스와 SQL의 등장
선언형 프로그래밍이 학계를 넘어 산업 전반에 가장 널리 확산된 경로 중 하나는 관계형 데이터베이스 질의 언어이다. IBM의 도널드 챔벌린과 레이먼드 보이스는 에드거 커드의 관계형 모델을 접한 뒤 1970년대 초 SEQUEL이라는 이름으로 질의 언어를 설계했으며, 이는 이후 SQL로 이어졌다. SQL은 집합 기반의 선언형 언어로, 사용자는 어떤 데이터를 원하는지만 기술하고 데이터베이스 엔진이 그 데이터를 실제로 어떻게 찾아낼지를 결정한다. 다만 표준 SQL에도 제어 흐름 구문과 같은 절차적 확장이 존재하며, SELECT 문은 대체로 선언적이지만 UPDATE나 DELETE 같은 문장은 절차적 성격이 더 강하다는 점도 함께 지적된다.
선언형 프로그래밍의 하위 패러다임
선언형 프로그래밍은 단일 패러다임이 아니라 여러 하위 패러다임을 아우르는 우산 개념이다. 아래 분류는 대표적인 하위 갈래와 각 갈래를 대표하는 언어를 정리한 것이다.
함수형 프로그래밍
함수형 프로그래밍은 계산을 수학적 함수의 평가로 취급하며 상태 변경과 가변 데이터를 지양한다. 순수 함수, 불변성, 일급 함수, 고차 함수, 함수 합성이 핵심 개념이다. 부작용이 없는 순수 함수는 참조 투명성을 가지므로, 결과가 사용되지 않는 순수 표현식은 프로그램에 영향을 주지 않고 제거될 수 있고, 동일한 인자로 호출된 순수 함수는 항상 동일한 결과를 반환한다는 성질을 활용해 최적화가 가능해진다.
// 순수 함수의 합성으로 데이터 처리 파이프라인을 구성한다
interface Order {
id: string;
amount: number;
status: "paid" | "pending" | "cancelled";
}
const isPaid = (order: Order): boolean => order.status === "paid";
const toAmount = (order: Order): number => order.amount;
const sumAmounts = (amounts: number[]): number =>
amounts.reduce((total, n) => total + n, 0);
function totalPaidRevenue(orders: Order[]): number {
return sumAmounts(orders.filter(isPaid).map(toAmount));
}
Haskell과 같은 순수 함수형 언어 외에도 Lisp, Scheme, OCaml, Erlang처럼 절차적 스타일과 함수형 스타일을 혼합해 지원하는 언어들이 널리 쓰인다. JavaScript와 TypeScript 역시 map, filter, reduce와 같은 배열 메서드를 통해 함수형 스타일의 선언적 데이터 처리를 지원하는 다중 패러다임 언어이다.
논리 프로그래밍
논리 프로그래밍은 프로그램을 사실과 규칙으로 이루어진 논리 문장의 집합으로 표현하며, 계산은 그 문장들을 이용해 질의를 증명하는 과정으로 이루어진다. Prolog가 가장 널리 알려진 논리 프로그래밍 언어이며, Datalog와 답 집합 프로그래밍(answer set programming) 역시 이 갈래에 속한다.
% 사실(facts): 부모 관계를 있는 그대로 서술한다
parent(tom, bob).
parent(bob, ann).
parent(bob, pat).
% 규칙(rule): "조부모"라는 관계를 다른 관계로부터 논리적으로 정의한다
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
% 질의: 위의 사실과 규칙만으로 답이 스스로 도출된다
?- grandparent(tom, ann).
이 예시에서 프로그래머는 조부모 관계를 어떻게 탐색할지 지시하지 않는다. 대신 조부모라는 관계가 부모 관계로부터 어떻게 정의되는지만 서술하며, Prolog의 실행 엔진이 하향식 후진 추론을 통해 질의에 대한 답을 스스로 탐색한다. Datalog 계열 시스템은 이와 달리 대체로 상향식 전진 추론으로 계산한다는 차이가 있다.
제약 프로그래밍
제약 프로그래밍은 결정 변수들 사이의 관계를 제약 조건의 형태로 선언하는 패러다임이다. 제약 조건은 명령형 언어의 원시 명령과 달리 실행할 단계나 순서를 지정하지 않고, 해가 만족해야 할 속성만을 지정한다. 사용자가 제약 조건을 기술하면 일반화된 해결기(solver)가 그 조건을 모두 만족하는 변수 할당을 탐색한다. 제약 프로그래밍은 스케줄링, 배차, 설정 문제 등 조합 최적화 문제에 널리 쓰이며, 논리 프로그래밍과 결합된 제약 논리 프로그래밍(CLP)이라는 갈래도 존재한다.
% 두 변수의 합이 10이고 x가 y보다 작다는 제약만 선언한다
var 1..9: x;
var 1..9: y;
constraint x + y = 10;
constraint x < y;
solve satisfy;
질의 언어
데이터베이스 질의 언어는 선언형 프로그래밍이 산업 현장에 가장 폭넓게 자리잡은 영역이다. SQL은 원형적인 선언형 언어로 자주 소개되며, 사용자는 원하는 데이터의 조건만 기술하고 어떤 인덱스를 사용할지, 어떤 순서로 테이블을 조인할지와 같은 실행 세부 사항은 질의 최적화기가 결정한다.
SELECT name, department
FROM employees
WHERE salary > 50000
ORDER BY department;
XQuery, SPARQL, JSONiq와 같은 질의 언어 역시 각각 XML 문서, RDF 그래프, JSON 문서 컬렉션을 대상으로 동일한 선언적 접근을 취한다.
데이터플로우 및 반응형 프로그래밍
데이터플로우 프로그래밍은 프로그램을 데이터가 흐르는 노드들의 그래프로 모델링하며, 각 노드의 값은 입력 값들 사이의 관계로 정의된다. 스프레드시트의 수식이 가장 친숙한 예시이다. 셀에 수식을 입력하면 그 셀의 값이 다른 셀들의 값에 어떻게 의존하는지만 선언되며, 의존하는 셀이 바뀔 때 값을 언제 어떻게 재계산할지는 스프레드시트 엔진이 처리한다.
반응형 프로그래밍은 이러한 발상을 비동기 이벤트 스트림으로 확장한 것으로, RxJS와 같은 라이브러리를 통해 TypeScript에서도 흔히 쓰인다.
import { fromEvent } from "rxjs";
import { map, filter } from "rxjs/operators";
const clicks$ = fromEvent<MouseEvent>(document, "click");
// 클릭 스트림으로부터 원하는 최종 스트림의 모양만 선언한다
clicks$
.pipe(
filter((event) => event.clientX > 200),
map((event) => event.clientX)
)
.subscribe((x) => console.log(x));
subscribe 이전의 파이프라인에는 반복문이나 이벤트 리스너 해제와 같은 절차적 코드가 전혀 등장하지 않는다. 오직 스트림이 어떻게 변환되어야 하는지에 대한 관계만 선언되어 있으며, 실제 구독과 해제, 스케줄링은 RxJS 런타임이 담당한다.
마크업 및 설정 언어
HTML과 CSS는 흔히 인용되는 선언형 언어의 예시이다. HTML 문서는 문서의 구조를 기술할 뿐 브라우저가 그 구조를 어떤 절차로 파싱하고 레이아웃을 계산할지 지시하지 않는다. CSS 역시 요소가 어떤 시각적 속성을 가져야 하는지를 선언할 뿐, 그 속성을 실제로 어떻게 적용할지는 렌더링 엔진의 몫이다.
.card {
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
}
Makefile이나 yacc의 문법 명세처럼 선언적 요소와 명령형 요소가 뒤섞인 언어도 있다. Makefile은 파일 사이의 의존 관계를 선언적으로 명시하지만, 각 규칙을 만족시키기 위해 실행할 동작은 명령형 목록으로 함께 기술한다. 이는 순수 선언형과 순수 명령형 사이에 다양한 혼합 지점이 존재함을 보여주는 대표적 사례이다.
실무에서의 선언형 프로그래밍
사용자 인터페이스: React의 선언적 렌더링
React 공식 문서는 선언형 프로그래밍을 명령형으로 UI를 세세히 관리하는 대신 각 시각적 상태에 대한 UI를 기술하는 것으로 설명한다. 개발자는 버튼을 비활성화하라거나 스피너를 표시하라는 식의 명령을 코드로 직접 내리지 않는다. 대신 컴포넌트가 가질 수 있는 시각적 상태들을 식별하고, 상태에 따라 UI가 어떤 모습이어야 하는지를 선언하면, 실제 DOM 갱신은 React가 맡는다.
이 방식은 개별 예제에서는 명령형 접근과 큰 차이가 없어 보일 수 있지만, 상호작용의 종류가 늘어날수록 진가를 발휘한다. 새로운 UI 요소나 상호작용을 추가할 때 기존 코드 전체를 점검하며 특정 요소를 숨기거나 보이는 처리를 빠뜨리지 않았는지 확인해야 하는 명령형 방식과 달리, 선언형 방식에서는 새로운 시각적 상태를 정의에 추가하기만 하면 되고 기존 상태 전이 로직은 그대로 유지된다.
인프라스트럭처를 코드로: Terraform
인프라스트럭처를 코드로 관리하는 대표적 도구인 Terraform은 설정 언어 자체가 선언적으로 설계되어 있다. 설정 파일은 인프라가 도달해야 할 최종 상태를 기술하며, 그 상태에 도달하기 위해 어떤 API를 어떤 순서로 호출해야 하는지는 Terraform이 자원 사이의 의존 관계를 그래프로 분석해 스스로 결정한다.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
이 설정은 어떤 API 호출을 어떤 순서로 수행해야 하는지 지시하지 않는다. 단지 t3.micro 크기의 인스턴스 하나가 특정 태그를 갖고 존재해야 한다는 최종 상태만 선언되어 있으며, Terraform은 계획(plan) 단계에서 현재 상태와 목표 상태의 차이를 계산하고, 적용(apply) 단계에서 그 차이를 없애는 데 필요한 작업을 수행한다.
컨테이너 오케스트레이션: Kubernetes와 조정 루프
Kubernetes 역시 선언적 오브젝트 관리를 핵심 원칙으로 삼는다. 사용자는 kubectl apply 명령으로 원하는 오브젝트의 설정을 클러스터에 제출하며, 그 설정을 실제로 어떻게 실현할지는 컨트롤러가 결정한다. Kubernetes 공식 문서는 이를 명령형 오브젝트 관리와 대비되는 방식으로 설명하며, 선언적 관리에서는 사용자가 로컬에 저장된 오브젝트 설정 파일을 다루되 그 파일에 대해 어떤 연산을 수행할지는 직접 정의하지 않는다고 명시한다. 생성, 갱신, 삭제 연산은 kubectl이 오브젝트별로 자동 판별한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27
Terraform과 Kubernetes 모두 선언적 명세와 실제 시스템 상태 사이의 차이를 지속적으로 비교하고 좁혀 나가는 조정 루프(reconciliation loop) 구조를 공유한다. 이 구조는 선언형 인프라 관리 도구들이 공통으로 채택하는 실행 모델이다.
함수형 스타일의 비동기 데이터 처리
Node.js 및 TypeScript 생태계에서는 비동기 작업을 다룰 때도 선언적 조합이 흔히 쓰인다. Promise 체이닝이나 async/await 구문 자체는 절차적으로 보이지만, 배열 메서드나 유틸리티 함수와 결합하면 데이터 파이프라인을 선언적으로 표현할 수 있다.
interface User {
id: string;
active: boolean;
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function activeUserCount(ids: string[]): Promise<number> {
const users = await Promise.all(ids.map(fetchUser));
return users.filter((user) => user.active).length;
}
이 코드는 여러 사용자를 어떤 순서로 요청할지, 응답을 어떻게 대기할지에 대한 세부 반복 로직을 노출하지 않는다. ids 배열로부터 활성 사용자 수를 얻어내는 관계만 map, Promise.all, filter의 합성으로 선언되어 있다.
장점과 한계
장점
| 항목 | 설명 |
|---|---|
| 가독성 | 통제 흐름의 세부 사항이 제거되어 코드가 짧고 문제 영역의 언어에 가까워진다 |
| 추론 용이성 | 부작용이 최소화되어 함수나 표현식을 독립적으로 이해하고 검증할 수 있다 |
| 병렬화 잠재력 | 실행 순서가 소스 코드에 고정되지 않아 실행 엔진이 병렬 처리를 선택할 수 있다 |
| 관심사 분리 | 무엇을 계산할지(논리)와 어떻게 계산할지(제어)를 분리해 각각을 독립적으로 개선할 수 있다 |
| 유지보수성 | 새로운 요구사항을 명세에 추가하는 방식으로 확장할 수 있어 기존 로직을 깨뜨릴 위험이 줄어든다 |
한계와 트레이드오프
선언형 프로그래밍이 모든 상황에서 우월한 것은 아니다. 추상화가 실행 세부 사항을 감추기 때문에, 실행 엔진이 항상 가장 효율적인 실행 경로를 선택한다는 보장이 없으며 성능이 명령형 코드에 비해 예측하기 어려울 수 있다. 문제를 해결하는 방식이 언어나 프레임워크의 구현에 위임되어 있는 만큼, 예상과 다른 결과가 나왔을 때 그 원인을 추적하는 디버깅 과정이 더 까다로울 수 있다.
많은 선언형 시스템이 도메인 특화 언어(DSL) 형태로 제공되는데, 이러한 DSL은 고유한 문법과 의미론을 새로 학습해야 하므로 학습 곡선이 가팔라질 수 있다. 또한 실행 순서나 메모리 배치를 세밀하게 통제해야 하는 상황, 예를 들어 게임 엔진의 실시간 렌더링이나 저수준 성능 최적화가 중요한 영역에서는 선언적 추상화가 필요한 만큼의 제어권을 제공하지 못할 수 있다. 이러한 경우에는 명령형 접근이 여전히 더 적합한 선택으로 남는다.
선언형과 명령형의 스펙트럼
순수성의 정도
실제로는 완전히 순수한 선언형 언어도, 완전히 순수한 명령형 언어도 드물다. Prolog와 SQL은 원칙적으로 선언적이지만 각각 컷(cut) 연산자나 절차적 확장, 갱신 및 삭제 문장과 같은 절차적 요소를 함께 제공한다. 반대로 절차형 프로그래밍은 상태 변화를 프로시저 내부나 명시적인 인자와 반환값으로 국한시킨다는 점에서 선언형으로 가는 중간 단계로 평가되기도 한다. 프로시저의 이름과 인자, 반환 타입만 보고도 내부 구현을 보지 않은 채 그 프로시저가 무엇을 하는지 짐작할 수 있다면, 이는 이미 선언적 사고방식에 가까워진 것이다.
다중 패러다임 언어
JavaScript와 TypeScript는 동일한 언어 안에서 명령형 스타일과 선언형 스타일을 모두 자연스럽게 지원하는 대표적인 다중 패러다임 언어이다. for 반복문으로 배열을 순회할 수도 있고, map과 filter, reduce의 합성으로 동일한 작업을 선언적으로 표현할 수도 있다. C++ 역시 표준 템플릿 라이브러리가 제공하는 std::sort와 같은 알고리즘 함수를 통해 명령형 언어 안에서 선언적 스타일을 부분적으로 채택할 수 있게 되었다. 이처럼 현대의 많은 범용 언어는 두 패러다임 사이의 경계를 유연하게 넘나들 수 있도록 설계되어 있으며, 어떤 스타일을 선택할지는 문제의 성격과 팀의 관례에 따라 결정된다.
관련 개념
명령형 프로그래밍과 절차형 프로그래밍
명령형 프로그래밍은 선언형 프로그래밍과 대비되는 상위 개념이며, 절차형 프로그래밍은 프로시저 또는 서브루틴을 기본 단위로 삼는 명령형 프로그래밍의 한 형태이다. 상태 변화가 프로시저 내부나 명시적인 인자와 반환값으로 국한되는 무거운 절차형 프로그래밍은 구조적 프로그래밍의 한 형태로 분류되기도 한다.
객체지향 프로그래밍과의 관계
객체지향 프로그래밍은 대체로 명령형 계열로 분류되며, 객체의 메서드 호출을 통해 내부 상태를 변경하는 방식이 기본이다. 다만 메서드 체이닝을 활용한 빌더 패턴이나 유창한 인터페이스(fluent interface)처럼, 객체지향 언어 안에서도 무엇을 만들고자 하는지를 선언적으로 표현하는 스타일을 부분적으로 구현할 수 있다.
하이브리드 및 다중 패러다임 언어
Lisp, OCaml, Erlang과 같은 함수형 언어는 절차적 스타일과 함수형 스타일을 함께 지원하며, Prolog와 SQL 같은 논리 및 질의 언어 역시 절차적 확장을 포함한다. 이러한 혼합은 예외적 현상이 아니라 실무 언어 설계의 일반적인 경향이며, 순수한 패러다임보다는 문제에 따라 적절한 스타일을 선택하고 혼용하는 다중 패러다임 접근이 널리 받아들여지고 있다.
참고 자료
- Backus, John (1978), “Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs”, Communications of the ACM, 21(8), pp. 613-641
- Kowalski, Robert A. (1979), “Algorithm = Logic + Control”, Communications of the ACM, 22(7), pp. 424-436
- Lloyd, John W. (1994), “Practical Advantages of Declarative Programming”, Proceedings of the Joint Conference on Declarative Programming (GULP-PRODE’94), pp. 18-30
- Van Roy, Peter; Haridi, Seif (2004), Concepts, Techniques, and Models of Computer Programming, MIT Press
- Wikipedia contributors, “Declarative programming”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Declarative_programming
- Wikipedia contributors, “Imperative programming”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Imperative_programming
- Wikipedia contributors, “Functional programming”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Functional_programming
- Wikipedia contributors, “Prolog”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Prolog
- Wikipedia contributors, “Constraint programming”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Constraint_programming
- Wikipedia contributors, “SQL”, Wikimedia Foundation, https://en.wikipedia.org/wiki/SQL
- Wikipedia contributors, “Pure function”, Wikimedia Foundation, https://en.wikipedia.org/wiki/Pure_function
- Wikipedia contributors, “John Backus”, Wikimedia Foundation, https://en.wikipedia.org/wiki/John_Backus
- Meta Platforms, Inc., “Reacting to Input with State”, React Documentation, https://react.dev/learn/reacting-to-input-with-state
- HashiCorp, “What is Infrastructure as Code with Terraform?”, HashiCorp Developer, https://developer.hashicorp.com/terraform/tutorials/aws-get-started/infrastructure-as-code
- The Kubernetes Authors, “Declarative Management of Kubernetes Objects Using Configuration Files”, Kubernetes Documentation, https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/
- The Kubernetes Authors, “Kubernetes Object Management”, Kubernetes Documentation, https://kubernetes.io/docs/concepts/overview/working-with-objects/object-management/
- IBM, “What Is Structured Query Language (SQL)?”, IBM Think, https://www.ibm.com/think/topics/structured-query-language
- Encyclopaedia Britannica, “PROLOG”, https://www.britannica.com/technology/PROLOG