프로그래밍 패러다임 비교 | 객체지향 vs 함수형 프로그래밍 완벽 정리
이 글의 핵심
프로그래밍 패러다임 비교 가이드입니다. 객체지향과 함수형 프로그래밍의 핵심 개념과 실무 선택 기준을 제시합니다.
들어가며: 프로그래밍 패러다임이란?
”왜 같은 문제를 다르게 푸나요?”
프로그래밍 패러다임은 “코드를 어떻게 구조화하고 문제를 어떻게 접근할 것인가”에 대한 철학입니다.
이 글에서 다루는 것:
- 객체지향 프로그래밍 (OOP) 핵심 개념
- 함수형 프로그래밍 (FP) 핵심 개념
- 두 패러다임의 장단점 비교
- 실무 적용 사례
목차
1. 객체지향 프로그래밍 (OOP)
OOP의 핵심 개념
객체지향 프로그래밍은 데이터와 그 데이터를 처리하는 메서드를 하나의 객체(Object)로 묶는 방식입니다.
OOP의 4대 원칙:
graph TB
A[OOP 4대 원칙] --> B[캡슐화 Encapsulation]
A --> C[상속 Inheritance]
A --> D[다형성 Polymorphism]
A --> E[추상화 Abstraction]
B --> B1[데이터 은닉]
B --> B2[접근 제어]
C --> C1[코드 재사용]
C --> C2[계층 구조]
D --> D1[오버라이딩]
D --> D2[인터페이스]
E --> E1[복잡도 감소]
E --> E2[핵심만 노출]
OOP 예제 (C++)
#include <iostream>
#include <string>
#include <vector>
// 캡슐화: 데이터와 메서드를 하나로
class BankAccount {
private:
std::string owner;
double balance;
public:
// 생성자
BankAccount(const std::string& name, double initial)
: owner(name), balance(initial) {}
// 메서드
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
}
double getBalance() const {
return balance;
}
std::string getOwner() const {
return owner;
}
};
// 상속: 기존 클래스 확장
class SavingsAccount : public BankAccount {
private:
double interestRate;
public:
SavingsAccount(const std::string& name, double initial, double rate)
: BankAccount(name, initial), interestRate(rate) {}
void applyInterest() {
double interest = getBalance() * interestRate;
deposit(interest);
}
};
int main() {
BankAccount account("Alice", 1000.0);
account.deposit(500.0);
account.withdraw(200.0);
std::cout << account.getBalance() << std::endl; // 1300
SavingsAccount savings("Bob", 1000.0, 0.05);
savings.applyInterest();
std::cout << savings.getBalance() << std::endl; // 1050
return 0;
}
OOP 예제 (Python)
# 캡슐화
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # private (관례상)
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
def get_balance(self):
return self.__balance
# 상속
class SavingsAccount(BankAccount):
def __init__(self, owner, balance, interest_rate):
super().__init__(owner, balance)
self.interest_rate = interest_rate
def apply_interest(self):
interest = self.get_balance() * self.interest_rate
self.deposit(interest)
# 사용
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_balance()) # 1300
savings = SavingsAccount("Bob", 1000, 0.05)
savings.apply_interest()
print(savings.get_balance()) # 1050
OOP의 장단점
장점:
- ✅ 캡슐화: 데이터 은닉, 접근 제어
- ✅ 재사용성: 상속으로 코드 재사용
- ✅ 유지보수: 모듈화로 변경 영향 최소화
- ✅ 직관적: 현실 세계 모델링
단점:
- ❌ 복잡도: 과도한 상속 계층
- ❌ 상태 관리: 가변 상태로 인한 버그
- ❌ 테스트 어려움: 의존성 많으면 목 객체 필요
2. 함수형 프로그래밍 (FP)
FP의 핵심 개념
함수형 프로그래밍은 순수 함수(Pure Function)와 불변성(Immutability)을 강조하는 패러다임입니다.
FP의 핵심 원칙:
graph TB
A[함수형 프로그래밍] --> B[순수 함수]
A --> C[불변성]
A --> D[고차 함수]
A --> E[함수 합성]
B --> B1[부작용 없음]
B --> B2[같은 입력 → 같은 출력]
C --> C1[상태 변경 금지]
C --> C2[새 값 생성]
D --> D1[함수를 인자로]
D --> D2[함수를 반환]
E --> E1[작은 함수 조합]
E --> E2[파이프라인]
FP 예제 (JavaScript)
// 순수 함수: 부작용 없음, 같은 입력 → 같은 출력
function add(a, b) {
return a + b;
}
// 비순수 함수: 외부 상태 변경
let total = 0;
function addToTotal(x) {
total += x; // 부작용!
return total;
}
// 불변성: 원본 변경 없이 새 값 생성
const arr = [1, 2, 3];
// 나쁜 예: 원본 변경
arr.push(4); // arr이 [1, 2, 3, 4]로 변경됨
// 좋은 예: 새 배열 생성
const newArr = [...arr, 4]; // arr은 그대로, newArr은 [1, 2, 3, 4]
// 고차 함수: 함수를 인자로 받거나 반환
function map(arr, fn) {
const result = [];
for (const item of arr) {
result.push(fn(item));
}
return result;
}
const doubled = map([1, 2, 3], x => x * 2); // [2, 4, 6]
// 함수 합성
const compose = (f, g) => x => f(g(x));
const addOne = x => x + 1;
const double = x => x * 2;
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(5)); // (5 + 1) * 2 = 12
FP 예제 (Python)
from functools import reduce
# 순수 함수
def add(a, b):
return a + b
# 고차 함수: map, filter, reduce
numbers = [1, 2, 3, 4, 5]
# map: 각 요소에 함수 적용
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10]
# filter: 조건에 맞는 요소만 선택
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
# reduce: 누적 연산
total = reduce(lambda acc, x: acc + x, numbers, 0)
print(total) # 15
# 불변성: 원본 변경 없이 새 값 생성
original = [1, 2, 3]
new_list = original + [4] # 새 리스트 생성
print(original) # [1, 2, 3] (변경 안 됨)
print(new_list) # [1, 2, 3, 4]
# 함수 합성
def compose(f, g):
return lambda x: f(g(x))
add_one = lambda x: x + 1
double = lambda x: x * 2
add_one_then_double = compose(double, add_one)
print(add_one_then_double(5)) # 12
FP 예제 (C++)
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <functional>
// 순수 함수
int add(int a, int b) {
return a + b;
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// map: transform
std::vector<int> doubled(numbers.size());
std::transform(numbers.begin(), numbers.end(), doubled.begin(),
[](int x) { return x * 2; });
// doubled: {2, 4, 6, 8, 10}
// filter: copy_if
std::vector<int> evens;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(evens),
[](int x) { return x % 2 == 0; });
// evens: {2, 4}
// reduce: accumulate
int total = std::accumulate(numbers.begin(), numbers.end(), 0);
std::cout << total << std::endl; // 15
// 함수 합성
auto add_one = [](int x) { return x + 1; };
auto double_it = [](int x) { return x * 2; };
auto compose = [](auto f, auto g) {
return [=](auto x) { return f(g(x)); };
};
auto add_one_then_double = compose(double_it, add_one);
std::cout << add_one_then_double(5) << std::endl; // 12
return 0;
}
FP의 장단점
장점:
- ✅ 테스트 용이: 순수 함수는 입출력만 테스트
- ✅ 병렬화 쉬움: 부작용 없어 동시 실행 안전
- ✅ 버그 감소: 불변성으로 예측 가능
- ✅ 합성 가능: 작은 함수를 조합
단점:
- ❌ 학습 곡선: 개념이 추상적
- ❌ 성능 오버헤드: 불변성 유지 비용
- ❌ 가독성: 과도한 함수 합성 시 읽기 어려움
3. 비교 분석
OOP vs FP 비교표
| 특징 | 객체지향 (OOP) | 함수형 (FP) |
|---|---|---|
| 핵심 개념 | 객체, 클래스, 상속 | 순수 함수, 불변성 |
| 상태 관리 | 가변 상태 (Mutable) | 불변 상태 (Immutable) |
| 데이터와 동작 | 함께 묶음 (캡슐화) | 분리 |
| 코드 재사용 | 상속, 다형성 | 함수 합성 |
| 부작용 | 허용 | 최소화 |
| 테스트 | 목 객체 필요 | 입출력만 테스트 |
| 병렬화 | 어려움 (상태 공유) | 쉬움 (불변성) |
같은 문제, 다른 접근
문제: 학생 목록에서 성적이 80점 이상인 학생의 이름을 추출
OOP 방식 (Java):
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() { return name; }
public int getScore() { return score; }
public boolean isPassing() { return score >= 80; }
}
class StudentService {
public List<String> getPassingStudentNames(List<Student> students) {
List<String> result = new ArrayList<>();
for (Student student : students) {
if (student.isPassing()) {
result.add(student.getName());
}
}
return result;
}
}
// 사용
List<Student> students = Arrays.asList(
new Student("Alice", 85),
new Student("Bob", 75),
new Student("Charlie", 90)
);
StudentService service = new StudentService();
List<String> passing = service.getPassingStudentNames(students);
// ["Alice", "Charlie"]
FP 방식 (JavaScript):
// 데이터와 함수 분리
const students = [
{ name: 'Alice', score: 85 },
{ name: 'Bob', score: 75 },
{ name: 'Charlie', score: 90 }
];
// 순수 함수들
const isPassing = student => student.score >= 80;
const getName = student => student.name;
// 함수 합성
const passingNames = students
.filter(isPassing)
.map(getName);
console.log(passingNames); // ['Alice', 'Charlie']
FP 방식 (Python):
students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 75},
{'name': 'Charlie', 'score': 90}
]
# 함수형 스타일
passing_names = [
s['name'] for s in students if s['score'] >= 80
]
print(passing_names) # ['Alice', 'Charlie']
# 또는 map/filter 사용
passing_names = list(map(
lambda s: s['name'],
filter(lambda s: s['score'] >= 80, students)
))
코드 비교
OOP:
- 명시적 클래스 정의
- 메서드로 동작 캡슐화
- 상태를 객체 내부에 보관
FP:
- 데이터와 함수 분리
- 함수 체이닝 (filter → map)
- 불변 데이터
4. 실무 적용
멀티 패러다임 접근
현대 프로그래밍은 두 패러다임을 혼합합니다.
예: React (JavaScript)
// OOP: 클래스 컴포넌트 (과거)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
);
}
}
// FP: 함수 컴포넌트 + Hooks (현재)
function Counter() {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
트렌드: React는 클래스 컴포넌트에서 함수 컴포넌트로 전환했습니다.
언어별 패러다임 지원
| 언어 | OOP | FP | 권장 스타일 |
|---|---|---|---|
| C++ | ✅ | ⚠️ (제한적) | OOP 중심, FP 요소 활용 |
| Python | ✅ | ✅ | 멀티 패러다임 |
| Java | ✅ | ⚠️ (Java 8+) | OOP 중심, Stream API |
| JavaScript | ✅ | ✅ | FP 선호 (React, Vue) |
| Rust | ⚠️ | ✅ | FP 중심, trait로 다형성 |
| Haskell | ❌ | ✅ | 순수 FP |
실무 선택 기준
OOP를 선택하는 경우:
- 복잡한 상태 관리 (게임, GUI 앱)
- 명확한 계층 구조 (조직도, 권한 시스템)
- 팀이 OOP에 익숙함
FP를 선택하는 경우:
- 데이터 변환 파이프라인 (ETL, 스트림 처리)
- 병렬 처리 (맵리듀스, 분산 시스템)
- 테스트 중요 (금융, 의료)
혼합 접근:
- 데이터 모델은 OOP (클래스)
- 비즈니스 로직은 FP (순수 함수)
- 예: Django (OOP 모델 + FP 뷰 함수)
5. 정리
핵심 요약
객체지향 (OOP):
- 데이터와 메서드를 객체로 묶음
- 캡슐화, 상속, 다형성, 추상화
- 상태 관리에 강함
함수형 (FP):
- 순수 함수와 불변성 강조
- 부작용 최소화
- 테스트와 병렬화에 유리
실무 권장:
- 두 패러다임을 적절히 혼합
- 상황에 맞는 도구 선택
- 팀 컨벤션 따르기
다음 단계
각 패러다임의 자세한 사용법은 아래 시리즈를 참고하세요:
- C++ 객체지향 프로그래밍
- Python 객체지향
- JavaScript 함수형 프로그래밍
관련 주제:
- 디자인 패턴
- SOLID 원칙
- 함수형 프로그래밍 고급