프로그래밍 패러다임 비교 | 객체지향 vs 함수형 프로그래밍 완벽 정리
이 글의 핵심
객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP)을 비교 분석합니다. 각 패러다임의 핵심 개념, 장단점, 실무 적용 사례를 상세히 설명합니다.
들어가며: 프로그래밍 패러다임이란?
”왜 같은 문제를 다르게 푸나요?”
프로그래밍 패러다임은 “코드를 어떻게 구조화하고 문제를 어떻게 접근할 것인가”에 대한 철학입니다. 이 글에서 다루는 것:
- 객체지향 프로그래밍 (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):
- 순수 함수와 불변성 강조
- 부작용 최소화
- 테스트와 병렬화에 유리 실무 권장:
- 두 패러다임을 적절히 혼합
- 상황에 맞는 도구 선택
- 팀 컨벤션 따르기
다음 단계
각 패러다임의 자세한 사용법은 아래 시리즈를 참고하세요:
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「프로그래밍 패러다임 비교 | 객체지향 vs 함수형 프로그래밍 완벽 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「프로그래밍 패러다임 비교 | 객체지향 vs 함수형 프로그래밍 완벽 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 객체지향 프로그래밍(OOP)과 함수형 프로그래밍(FP)을 비교 분석합니다. 각 패러다임의 핵심 개념, 장단점, 실무 적용 사례를 상세히 설명합니다. 프로그래밍패러다임·OOP·함수형프로그래밍 중심으로 설명합니다. St… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Python 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리
- JavaScript 디자인 패턴 | 싱글톤, 팩토리, 옵저버 패턴
- 프로그래밍 언어별 흔한 에러 해결 가이드 | C++, Python, Java, JavaScript
이 글에서 다루는 키워드 (관련 검색어)
프로그래밍패러다임, OOP, 함수형프로그래밍, 객체지향, Functional, C++, Python, JavaScript 등으로 검색하시면 이 글이 도움이 됩니다.