목차12개 항목
출처와 참고
ByteByteGo의 System Design 101 가이드 「Top 8 Programming Paradigms」, 「Imperative vs Functional vs Object-oriented Programming」, 「The Fundamental Pillars of Object-Oriented Programming」, 「8 Key OOP Concepts Every Developer Should Know」를 바탕으로 재구성한 노트다. 개념 묶음으로서의 패러다임은 Peter Van Roy의 「Programming Paradigms for Dummies: What Every Programmer Should Know」(2009)를, 구조적 프로그래밍은 Edsger Dijkstra의 「Go To Statement Considered Harmful」(1968)를 바탕으로 보강했다. 「구조적 프로그래밍: goto를 없앤 첫 전환」 절의 기본 구성 요소 표와 함수 설명은 CrashCourse의 「Crash Course Computer Science」 강의를 바탕으로 보강했다. 「함수형 프로그래밍의 핵심 개념」 절과 클래스와 객체 지향의 관계는 fru1tworld/cs-basic 모의 면접 질문 목록의 객체지향·함수형 질문을 바탕으로 보강했다. 추상화·캡슐화·정보 은닉의 구분과 is-a·has-a 관계는 GitHub 자료 모음 「Subjective-studies_OS-CN-DBMS」의 OOP 면접 정리를 바탕으로 보강했다.
패러다임이란
프로그래밍 패러다임은 프로그램을 무엇의 조합으로 볼지에 대한 관점이다. 명령의 나열로 볼 수도, 함수의 합성으로 볼 수도, 협력하는 객체로 볼 수도 있다. 패러다임은 언어에 묶여 있지 않다. 현대 언어 대부분은 여러 패러다임을 섞어 쓰는 멀티 패러다임 언어다.
언어는 수천 개지만 패러다임은 수십 개다. 패러다임의 눈으로 보면 Java, JavaScript, C#, Ruby, Python은 거의 같은 객체 지향 패러다임을 구현한 언어다.
패러다임은 개념의 묶음이다
패러다임을 정확하게 정의하는 방법은 그 패러다임이 쓰는 프로그래밍 개념의 집합으로 보는 것이다. 개념들을 작은 핵심 언어(kernel language)로 묶으면 패러다임 하나가 된다. 겉보기에 전혀 다른 함수형과 객체 지향도 개념 하나 차이일 때가 많다.
| 핵심 개념 | 뜻 | 더하면 생기는 것 |
|---|---|---|
| 레코드(record) | 이름 붙은 필드의 묶음 | 자료구조 |
| 렉시컬 클로저(lexically scoped closure) | 정의된 환경을 기억하는 일급 함수 | 함수형 프로그래밍. 클로저만으로 람다 계산과 같은 튜링 완전성을 얻는다 |
| 독립성(concurrency) | 서로 순서가 정해지지 않은 활동 | 동시성 프로그래밍 |
| 이름 붙은 상태(named state) | 이름 하나가 시간에 따라 여러 값을 가진다 | 명령형·객체 지향 프로그래밍 |
패러다임을 가르는 축은 둘이다.
- 관찰 가능한 비결정성(observable nondeterminism): 같은 입력으로 시작해도 스케줄러의 선택에 따라 결과가 달라질 수 있는가. 경쟁 상태(race condition)가 대표 증상이다. 이름 붙은 상태와 동시성을 함께 가진 언어(Java 등)는 이것을 표현할 수 있어 동시성 프로그래밍이 어렵다. 실세계 상호작용(클라이언트-서버)에는 필요하지만, 필요할 때만 허용한다. 모든 프로그램이 결정적인 선언적 동시성(declarative/dataflow concurrency) 패러다임도 있다.
- 상태의 표현력: 상태가 이름 붙었나(named, 변수·셀) 이름 없나(unnamed, 모나드처럼 인자로 실려 다니나), 결정적인가, 순차인가 동시인가. 함수형(이름 없음·결정적·순차) → 선언적 동시성 → 동시 논리 프로그래밍 → 메시지 전달·공유 상태 동시성(이름 있음·비결정적·동시) 순으로 표현력이 커진다.
이름 붙은 상태는 모듈성을 위해 필요하다. 모듈 M의 함수 F가 몇 번 불렸는지 세고 싶을 때, 상태가 없으면 F의 인자에 카운트 입력·출력을 더해야 하고 F를 부르는 모든 사용자 코드가 바뀐다. 모듈 안에 셀 하나를 두면 인터페이스를 그대로 두고 해결된다. 대신 상태를 가진 구성 요소는 한 번 옳다고 증명해도 계속 옳다는 보장이 없다. 그래서 상태는 밖에서 들여다볼 방법을 남겨 둔다.
새 개념이 필요하다는 신호: 창조적 확장 원칙
프로그램이 풀려는 문제와 무관한 기술적 이유로 프로그램 전체를 고쳐야 하는 일(pervasive modification) 이 생기면, 언어에 개념 하나가 빠져 있다는 신호다. 이를 창조적 확장 원칙(creative extension principle)이라 한다.
| 함수형 언어에서 하려는 일 | 개념 없이 하면 | 더할 개념 |
|---|---|---|
| 독립적인 활동 여럿을 모델링 | 실행 스택 여러 개, 스케줄러, 선점 장치를 직접 구현 | 동시성 |
| 과거를 기억하고 갱신하는 개체 | 모든 함수 호출에 메모리 입력·출력 인자 두 개를 추가 | 이름 붙은 상태 |
| 어디서든 오류를 감지해 복구 루틴으로 이동 | 모든 반환값에 오류 코드, 모든 호출 뒤에 검사 조건문 | 예외 |
개념은 많을수록 좋은 것이 아니다. 너무 적으면 프로그램이 복잡해지고, 너무 많으면 추론이 복잡해진다. 문제에 딱 맞는 개념만 가진 패러다임을 고른다. 상태가 복잡성의 주원인이라는 관점은 소프트웨어 복잡성에 있다.
여덟 가지 패러다임
| 패러다임 | 중심 생각 | 대표 언어·도구 |
|---|---|---|
| 명령형(Imperative) | 상태를 바꾸는 명령을 순서대로 적는다. 어떻게 할지를 쓴다 | C, 대부분의 절차형 코드 |
| 선언형(Declarative) | 제어 흐름 없이 무엇을 원하는지 쓴다 | SQL, HTML, 함수형 언어 |
| 객체 지향(OOP) | 데이터와 행동을 묶은 객체들이 메시지를 주고받는다 | Java, C#, C++, Python, Ruby |
| 함수형(FP) | 계산을 수학 함수의 평가로 본다. 불변 데이터, 순수 함수, 함수 합성 | Haskell, Lisp, Erlang, Scala, JS·Python의 함수형 기능 |
| 반응형(Reactive) | 비동기 데이터 스트림과 변화의 전파를 다룬다 | RxJS, Reactor, 스트림 처리 |
| 관점 지향(AOP) | 로깅·트랜잭션·보안처럼 여러 곳에 흩어지는 횡단 관심사를 모듈로 뺀다 | AspectJ, Spring AOP |
| 제네릭(Generic) | 타입에 독립적인 알고리즘과 자료구조를 쓴다 | C++ 템플릿, Java·TypeScript 제네릭 |
| 동시성(Concurrent) | 여러 작업을 겹쳐 실행하도록 구조를 짠다 | Go, Erlang, 멀티스레드 서버 |
구조적 프로그래밍: goto를 없앤 첫 전환
명령형 안에서도 가장 먼저 자리 잡은 규율은 구조적 프로그래밍(structured programming) 이다. 프로그램을 순차·선택(if)·반복(while)·프로시저 호출만으로 짜고 임의 점프 goto를 쓰지 않는다.
이 네 재료는 거의 모든 언어가 갖춘 기본 구성 요소다.
| 구성 요소 | 하는 일 | 형태 |
|---|---|---|
| 순차(sequence) | 문(statement)을 위에서 아래로 하나씩 실행한다. 문을 짜는 규칙이 문법(syntax)이다 | 대입문 c = a + b. 오른쪽을 먼저 계산해 왼쪽 변수에 덮어쓰므로 n = n + 1이 성립한다 |
| 선택(selection) | 조건식의 참·거짓에 따라 갈 길을 하나 고르고 지나간다 | if … else |
| 반복(iteration) | 같은 문을 여러 번 실행한다. 조건 제어 반복은 조건이 거짓이 될 때까지, 횟수 제어 반복은 정해 둔 횟수만큼 돈다 | while / for i in 1..n |
| 호출(procedure call) | 이름 붙인 코드 묶음을 불러 쓰고 결과를 돌려받는다. 언어에 따라 함수·메서드·서브루틴(subroutine)이라 부른다 | 매개변수로 값을 받고 return으로 돌려준다 |
함수는 코드 재사용 수단이자 추상화 단위다. 같은 계산을 여러 곳에 복사하면 버그를 고칠 때 모든 복사본을 찾아야 한다. 함수로 묶으면 부르는 쪽은 이름과 매개변수·반환값만 알고 안의 반복문과 변수는 보지 않는다. 함수가 다른 함수를 부르며 층을 쌓기 때문에 큰 프로그램을 함수 단위로 나눠 여러 사람이 동시에 짤 수 있다. 자주 쓰는 함수를 미리 짜서 검증해 둔 묶음이 라이브러리(library)다. 함수를 얼마나 작게 나눌지는 클린 코드: 함수와 이름에 있다.
근거는 「실행 중인 프로세스의 진행 상황을 어떤 좌표로 설명할 수 있는가」다. 구조적 제어만 쓰면 지금 위치는 코드 안의 위치(텍스트 인덱스) 와 호출 스택·반복 횟수(동적 인덱스) 로 기술된다. 이 좌표는 프로그래머가 통제하지 않아도 정해지므로 변수 값이 무엇을 뜻하는지 해석할 기준이 된다. goto를 마구 쓰면 그런 좌표 체계를 찾기가 끔찍하게 어려워진다. 이 논증(Dijkstra, 1968)은 이후 「~ considered harmful」이라는 제목 관용구를 낳았다. 같은 논리를 동시성으로 옮긴 것이 구조적 동시성이다. C의 goto cleanup처럼 함수 안에서 한 방향으로만 뛰는 제한된 쓰임은 지금도 관용구로 남아 있다.
반응형은 반응형 프로그래밍과 RxJS, 동시성은 동시성·병렬·비동기·논블로킹과 동시성 모델에서 자세히 다룬다.
명령형·함수형·객체 지향 비교
| 비교 | 명령형 | 함수형 | 객체 지향 |
|---|---|---|---|
| 기본 단위 | 문장(statement) | 함수 | 객체 |
| 상태 | 변경 가능한 상태를 직접 바꾼다 | 불변. 새 값을 만든다 | 객체 안에 캡슐화하고 메서드로만 바꾼다 |
| 흐름 제어 | 반복문, 조건문 | 재귀, 고차 함수(map, filter, reduce) | 메서드 호출과 다형성 |
| 부수 효과 | 자연스럽다 | 피하고, 경계로 몰아낸다 | 객체 내부로 가둔다 |
| 강점 | 하드웨어에 가깝고 직관적이다 | 추론·테스트·병렬화가 쉽다 | 큰 도메인을 역할별로 나누기 쉽다 |
같은 일을 세 방식으로 쓰면 차이가 보인다.
# 명령형: 상태를 바꿔 가며 어떻게 할지 적는다
total = 0
for p in prices:
if p > 100:
total += p
# 함수형: 데이터를 변환하는 식으로 무엇을 원하는지 적는다
total = sum(filter(lambda p: p > 100, prices))
# 객체 지향: 책임을 가진 객체에게 메시지를 보낸다
total = cart.total_of_items_over(100)함수형 프로그래밍의 핵심 개념
| 개념 | 뜻 |
|---|---|
| 순수 함수(pure function) | 같은 입력에 항상 같은 출력을 내고, 부수 효과가 없다 |
| 부수 효과(side effect) | 반환값 말고 바깥에 남기는 흔적. 전역·인자 객체 변경, 입출력, 네트워크, 로그, 현재 시각·난수 읽기, 예외로 흐름 바꾸기 |
| 참조 투명성(referential transparency) | 식을 그 결과값으로 바꿔도 프로그램의 의미가 같다. 순수 함수 호출의 성질이다 |
| 불변성(immutability) | 값을 고치지 않고 바뀐 새 값을 만든다. 구조 공유로 복사 비용을 줄인다 |
| 일급·고차 함수 | 함수를 값처럼 넘기고 돌려준다. map·filter·reduce가 반복문 대신 흐름을 맡는다 |
| 함수 합성(composition) | 작은 함수를 f(g(x))처럼 이어 큰 함수를 만든다 |
순수 함수가 함수형의 메커니즘 전체를 받친다. 출력이 입력에만 달려 있으므로 함수 호출을 결과값으로 바꿔 생각할 수 있고(등식 추론), 결과를 캐시해도(메모이제이션), 호출 순서를 바꾸거나 미뤄도(지연 평가), 여러 코어에 나눠도(병렬화) 의미가 같다. 테스트는 입력과 기대 출력만 있으면 된다. 고차 함수와 합성도 조립하는 부품이 순수해야 조립 결과를 예측할 수 있다. 치환이 깨지는 예는 프로그래밍 언어 의미론의 「순수성과 참조 투명성」에 있다.
부수 효과를 모두 없애는 것은 이상이 아니다. 화면에 그리고, 파일에 쓰고, 요청에 답하는 것이 프로그램의 목적이라 효과가 없는 프로그램은 아무 일도 하지 않는다. 목표는 효과를 없애는 것이 아니라 분리하고 경계로 모으는 것이다. 계산은 순수 함수로 짜고, 입출력은 프로그램 가장자리의 얇은 층에 모은다(함수형 코어, 명령형 셸). Haskell은 효과를 IO 타입으로 표시해 이 분리를 타입으로 강제한다(타입 시스템의 효과 시스템). 성능이 중요한 곳에서는 지역 변수를 바꾸는 명령형 반복문이 더 단순하고 빠를 수도 있다. 바깥에서 관찰되지 않는 지역 변경은 함수의 순수성을 해치지 않는다.
순수 함수는 스레드 안전하다. 경쟁 상태는 여러 스레드가 공유 가변 상태를 동시에 읽고 쓸 때 생기는데, 순수 함수는 공유 상태를 읽거나 쓰지 않고 인자와 지역 변수만 쓰므로(지역 변수는 스레드마다 스택에 따로 있다) 몇 개의 스레드가 동시에 불러도 서로 간섭하지 않는다. 락이 필요 없다. 단, 인자로 받은 가변 객체를 다른 스레드가 동시에 고치고 있다면 함수가 읽는 값 자체가 흔들리므로, 불변 데이터와 함께 써야 완전히 안전하다.
함수형 메커니즘을 쓰는 이유:
- 추론과 테스트: 상태 변화를 따라가지 않고 함수 하나만 보고 이해한다. 버그 후보가 효과가 있는 경계로 좁혀진다.
- 동시성과 병렬화: 불변 데이터와 순수 함수는 락 없이 나눠 처리할 수 있다. 멀티코어와 분산 처리(MapReduce, Spark)의 기본 모델이다.
- 조합과 재사용: 고차 함수로 「무엇을 할지」를 인자로 넘겨 반복 구조를 한 번만 쓴다.
- 선언적 표현: 「어떻게」가 아니라 「무엇을」 쓰므로 의도가 드러난다.
비용도 있다. 불변 갱신은 복사·할당이 늘고, 재귀와 고차 함수는 익숙하지 않은 사람에게 읽기 어렵다. 그래서 현대 언어는 순수 함수형으로 가기보다 람다·스트림·불변 컬렉션·record 같은 함수형 도구를 섞는다. JavaScript에서의 실천(액션·계산·데이터, 카피 온 라이트, 커링)은 함수형 프로그래밍에 있다.
객체 지향의 핵심 개념
구성 요소
| 개념 | 뜻 |
|---|---|
| 클래스(Class) | 객체를 만드는 설계도. 속성과 메서드를 정의한다 |
| 객체(Object) | 클래스로 만든 실체. 자기 상태를 가진다 |
| 메서드(Method) | 객체가 할 수 있는 행동 |
| 인터페이스(Interface) | 구현 없이 "무엇을 할 수 있어야 하는가"만 정한 계약(추상 클래스와 인터페이스) |
네 기둥
| 기둥 | 뜻 | 예 |
|---|---|---|
| 추상화(Abstraction) | 구현 세부를 숨기고 꼭 필요한 특징만 드러낸다 | Vehicle에 추상 메서드 stop()만 둔다 |
| 캡슐화(Encapsulation) | 데이터와 메서드를 한 단위로 묶고 접근 제한자로 직접 접근을 막는다 | private 필드 + 공개 메서드 |
| 상속(Inheritance) | 기존 클래스(부모)의 속성과 메서드를 새 클래스(자식)가 물려받아 재사용한다 | Car extends Vehicle |
| 다형성(Polymorphism) | 같은 메시지에 객체마다 다르게 반응한다. 같은 상속 계보의 타입은 서로 바꿔 쓸 수 있다. 정적·동적 다형성의 구분은 타입 시스템 | vehicle.stop()이 차와 자전거에서 다르게 동작 |
추상화와 캡슐화는 자주 섞어 쓰지만 묻는 질문이 다르다.
| 개념 | 묻는 것 | 수단 | 예 (TV 리모컨) |
|---|---|---|---|
| 추상화 | 무엇을 보여 줄까. 쓰는 쪽에 필요한 특징만 고른다 | 인터페이스, 추상 클래스, 함수 이름 | 사용자는 "음량 올리기" 버튼만 안다 |
| 캡슐화 | 데이터와 그것을 다루는 동작을 한 단위로 묶는다 | 클래스 | 음량 값과 그 값을 바꾸는 회로가 한 기기에 있다 |
| 정보 은닉 | 묶은 것 중 무엇을 숨길까. 바깥이 내부에 기대지 못하게 막는다 | 접근 지정자(private, protected) | 회로를 바꿔도 버튼 사용법은 그대로다 |
추상화는 설계 수준에서 경계를 정하고, 캡슐화와 정보 은닉은 그 경계를 코드로 지킨다. 정보 은닉이 결합도를 낮추는 원리는 계층과 API에 있다.
객체 사이의 관계도 두 가지로 나눠 말한다. is-a는 "Car는 Vehicle이다"처럼 상속으로 표현하고, has-a는 "Car는 Engine을 가진다"처럼 다른 객체를 멤버로 들고 쓰는 것이다. has-a는 부분의 수명이 전체에 묶이는 합성(composition)과 따로 사는 집합(aggregation)으로 갈리며, UML 표기와 코드 모양은 UML 관계에 있다.
클래스와 객체 지향은 같은 말이 아니다
클래스가 있다고 객체 지향 언어인 것도, 객체 지향 언어에 클래스가 꼭 있어야 하는 것도 아니다.
- 클래스 없는 객체 지향: JavaScript(ES2015 전)와 Self는 객체가 다른 객체를 프로토타입으로 삼아 직접 상속한다(객체 지향 프로그래밍과 JavaScript). Go는 클래스·상속 없이 구조체, 메서드, 암묵적 인터페이스, 임베딩으로 캡슐화와 다형성을 얻는다(Go 언어). 객체 지향의 본질을 메시징·캡슐화·늦은 바인딩으로 보면 클래스는 그 구현 수단의 하나일 뿐이다(객체 지향의 본질: 메시징).
- 클래스가 있어도 객체 지향이 아닌 코드: Java로도 static 메서드와 데이터만 든 클래스로 절차적 코드를 짤 수 있다. C++·Python·Kotlin은 객체 지향을 지원하는 멀티 패러다임 언어다. 클래스는 데이터와 함수를 묶는 문법이고, 객체 지향은 책임을 가진 객체들이 메시지로 협력하는 설계 방식이다.
상속은 재사용 수단이지만 결합을 강하게 만든다. 행동을 재사용하려면 상속보다 조합(composition)을 먼저 고려하는 것이 일반적인 원칙이다. 객체 지향을 클래스가 아니라 메시지를 주고받는 객체로 보는 관점은 객체 지향의 본질: 메시징에, JavaScript에서의 객체 지향은 객체 지향 프로그래밍과 JavaScript에 있다.
정리
- 패러다임은 프로그램을 보는 관점이고, 현대 언어는 여러 패러다임을 섞는다.
- 패러다임은 레코드·클로저·동시성·이름 붙은 상태 같은 개념의 묶음이다. 비결정성과 상태는 필요할 때만 들인다. 코드 전체를 고쳐야 하는 일이 반복되면 개념이 하나 빠졌다는 신호다.
- 명령형은 "어떻게", 선언형·함수형은 "무엇을" 쓴다. 객체 지향은 상태와 행동을 객체로 묶어 책임을 나눈다.
- 함수형은 불변성과 순수 함수로 추론과 병렬화를 쉽게 하고, 객체 지향은 큰 도메인을 역할로 나누기 쉽게 한다.
- 순수 함수는 공유 상태를 건드리지 않아 스레드 안전하다. 부수 효과는 없애는 것이 아니라 경계로 모은다.
- 클래스는 객체 지향의 한 구현 수단이다. 프로토타입·구조체 기반 객체 지향도 있다.
- 객체 지향의 네 기둥은 추상화, 캡슐화, 상속, 다형성이다. 상속보다 조합을 먼저 생각한다.
관련 노트: 소프트웨어 복잡성, 상태와 결합을 다루는 열다섯 가지 설계, 객체 지향의 본질: 메시징, 객체 지향 프로그래밍과 JavaScript, 반응형 프로그래밍과 RxJS, 동시성·병렬·비동기·논블로킹과 동시성 모델, GoF 디자인 패턴, 프로그램이 실행되기까지, 프로그래밍 언어 의미론, 함수형 프로그래밍
원본 기록9
- 원문 (새 창) bytebytego.com/guides/top-8-programming-paradigms
- 원문 (새 창) bytebytego.com/guides/imperative-vs-functional-vs-object-oriented-programming
- 원문 (새 창) bytebytego.com/guides/the-fundamental-pillars-of-object-oriented-programming
- 원문 (새 창) bytebytego.com/guides/8-key-oop-concepts-every-developer-should-know
- 원문 (새 창) www.info.ucl.ac.be/~pvr/VanRoyChapter.pdf
- 원문 (새 창) homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf
- 원문 (새 창) www.youtube.com/watch?v=l26oaHV7D40
- GitHub 원문 (새 창) github.com/fru1tworld/cs-basic/blob/main/interview/cs/etc.md
- GitHub 원문 (새 창) github.com/avgspacelover/Subjective-studies_OS-CN-DBMS