
객체지향 vs 함수형: 프로그래밍 패러다임의 이해
프로그래밍 패러다임은 코드를 어떻게 구조화하고 생각할 것인가에 대한 접근 방식입니다. 객체지향과 함수형은 가장 널리 사용되는 두 가지 패러다임입니다.
객체지향 프로그래밍 (OOP)
핵심 개념
OOP는 데이터와 그 데이터를 처리하는 메서드를 하나의 객체로 묶는 방식입니다.
// 객체지향 방식
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
return this.balance;
}
withdraw(amount) {
if (this.balance >= amount) {
this.balance -= amount;
return this.balance;
}
throw new Error("잔액 부족");
}
getBalance() {
return this.balance;
}
}
// 사용
const account = new BankAccount("홍길동", 10000);
account.deposit(5000); // 15000
account.withdraw(3000); // 12000
OOP의 4대 원칙
1. 캡슐화 (Encapsulation)
데이터와 메서드를 하나로 묶고, 외부 접근을 제한합니다.
class User {
#password; // private 필드
constructor(username, password) {
this.username = username;
this.#password = this.#hashPassword(password);
}
#hashPassword(password) {
// 비밀번호 해싱 (외부에서 접근 불가)
return btoa(password);
}
verifyPassword(inputPassword) {
return this.#hashPassword(inputPassword) === this.#password;
}
}
const user = new User("hong", "1234");
console.log(user.username); // "hong"
console.log(user.#password); // Error: Private field
2. 상속 (Inheritance)
기존 클래스의 속성과 메서드를 물려받습니다.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name}이(가) 소리를 냅니다.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name}이(가) 멍멍 짖습니다.`);
}
fetch() {
console.log(`${this.name}이(가) 공을 가져옵니다.`);
}
}
const dog = new Dog("바둑이");
dog.speak(); // "바둑이이(가) 멍멍 짖습니다."
dog.fetch(); // "바둑이이(가) 공을 가져옵니다."
3. 다형성 (Polymorphism)
같은 인터페이스로 다른 동작을 수행합니다.
class Shape {
area() {
throw new Error("구현 필요");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
// 다형성: 같은 메서드, 다른 동작
const shapes = [
new Circle(5),
new Rectangle(4, 6)
];
shapes.forEach(shape => {
console.log(shape.area()); // 각자 다른 방식으로 계산
});
4. 추상화 (Abstraction)
복잡한 내부 구현을 숨기고 간단한 인터페이스만 제공합니다.
class CoffeeMachine {
#water = 0;
#beans = 0;
addWater(amount) {
this.#water += amount;
}
addBeans(amount) {
this.#beans += amount;
}
makeCoffee() {
// 복잡한 내부 로직 숨김
if (this.#water < 100 || this.#beans < 10) {
throw new Error("재료 부족");
}
this.#water -= 100;
this.#beans -= 10;
return "☕ 커피 완성!";
}
}
const machine = new CoffeeMachine();
machine.addWater(500);
machine.addBeans(50);
console.log(machine.makeCoffee()); // 간단한 인터페이스
함수형 프로그래밍 (FP)
핵심 개념
FP는 순수 함수와 불변성을 중심으로 프로그램을 구성합니다.
// 함수형 방식
const createAccount = (owner, balance) => ({
owner,
balance
});
const deposit = (account, amount) => ({
...account,
balance: account.balance + amount
});
const withdraw = (account, amount) => {
if (account.balance >= amount) {
return {
...account,
balance: account.balance - amount
};
}
throw new Error("잔액 부족");
};
// 사용 (원본 데이터 변경 없음)
const account1 = createAccount("홍길동", 10000);
const account2 = deposit(account1, 5000); // 새 객체
const account3 = withdraw(account2, 3000); // 새 객체
console.log(account1.balance); // 10000 (원본 유지)
console.log(account3.balance); // 12000
FP의 핵심 원칙
1. 순수 함수 (Pure Functions)
같은 입력에 항상 같은 출력, 부수 효과 없음.
// 순수 함수 (Good)
const add = (a, b) => a + b;
add(2, 3); // 항상 5
// 순수하지 않은 함수 (Bad)
let total = 0;
const addToTotal = (value) => {
total += value; // 외부 상태 변경
return total;
};
2. 불변성 (Immutability)
데이터를 변경하지 않고 새로운 데이터를 생성합니다.
// 나쁜 예: 원본 변경
const numbers = [1, 2, 3];
numbers.push(4); // 원본 변경
console.log(numbers); // [1, 2, 3, 4]
// 좋은 예: 새 배열 생성
const numbers2 = [1, 2, 3];
const newNumbers = [...numbers2, 4]; // 새 배열
console.log(numbers2); // [1, 2, 3] (원본 유지)
console.log(newNumbers); // [1, 2, 3, 4]
3. 고차 함수 (Higher-Order Functions)
함수를 인자로 받거나 함수를 반환합니다.
// map, filter, reduce
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// 함수를 반환하는 함수
const multiply = (factor) => (number) => number * factor;
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
4. 함수 합성 (Function Composition)
작은 함수들을 조합하여 복잡한 기능 구현.
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x ** 2;
const calculate = compose(square, double, addOne);
console.log(calculate(3)); // ((3 + 1) * 2) ** 2 = 64
실전 비교: 쇼핑 카트 구현
객체지향 방식
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
const existingItem = this.items.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
removeItem(productId) {
this.items = this.items.filter(item => item.product.id !== productId);
}
getTotal() {
return this.items.reduce((sum, item) =>
sum + (item.product.price * item.quantity), 0
);
}
checkout() {
const total = this.getTotal();
this.items = []; // 장바구니 비우기
return total;
}
}
// 사용
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: "책", price: 10000 }, 2);
cart.addItem({ id: 2, name: "펜", price: 1000 }, 5);
console.log(cart.getTotal()); // 25000
함수형 방식
// 순수 함수들
const createCart = () => [];
const addItem = (cart, product, quantity) => {
const existingItem = cart.find(item => item.product.id === product.id);
if (existingItem) {
return cart.map(item =>
item.product.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
);
}
return [...cart, { product, quantity }];
};
const removeItem = (cart, productId) =>
cart.filter(item => item.product.id !== productId);
const getTotal = (cart) =>
cart.reduce((sum, item) => sum + (item.product.price * item.quantity), 0);
const checkout = (cart) => ({
total: getTotal(cart),
newCart: createCart()
});
// 사용 (불변성 유지)
let cart = createCart();
cart = addItem(cart, { id: 1, name: "책", price: 10000 }, 2);
cart = addItem(cart, { id: 2, name: "펜", price: 1000 }, 5);
console.log(getTotal(cart)); // 25000
const result = checkout(cart);
console.log(result.total); // 25000
cart = result.newCart; // 새 카트로 교체
장단점 비교
객체지향의 장점
- 직관적: 현실 세계를 모델링하기 쉬움
- 캡슐화: 데이터 보호 및 은닉
- 재사용성: 상속을 통한 코드 재사용
- 대규모 시스템: 복잡한 시스템 구조화에 유리
객체지향의 단점
- 상태 관리: 객체 상태 변경으로 인한 버그
- 테스트 어려움: 의존성이 많으면 테스트 복잡
- 병렬 처리: 공유 상태로 인한 동시성 문제
함수형의 장점
- 예측 가능: 순수 함수는 항상 같은 결과
- 테스트 용이: 부수 효과 없어 테스트 간단
- 병렬 처리: 불변성으로 동시성 문제 없음
- 디버깅: 상태 변경 추적 쉬움
함수형의 단점
- 학습 곡선: 개념이 어려움
- 성능: 불변성 유지로 메모리 사용 증가
- 현실 모델링: 일부 도메인은 OOP가 더 자연스러움
언제 무엇을 사용할까?
객체지향을 선택하세요
- 게임 개발: 캐릭터, 아이템 등 객체 중심
- GUI 애플리케이션: 버튼, 윈도우 등 컴포넌트
- 시뮬레이션: 현실 세계 모델링
- 대규모 엔터프라이즈: 복잡한 비즈니스 로직
// 게임 캐릭터 (OOP가 자연스러움)
class Character {
constructor(name, health, attack) {
this.name = name;
this.health = health;
this.attack = attack;
}
takeDamage(damage) {
this.health -= damage;
if (this.health <= 0) {
this.die();
}
}
attackEnemy(enemy) {
enemy.takeDamage(this.attack);
}
die() {
console.log(`${this.name}이(가) 사망했습니다.`);
}
}
함수형을 선택하세요
- 데이터 변환: 파이프라인 처리
- 비동기 처리: Promise, async/await
- 상태 관리: Redux, Zustand
- 수학적 계산: 알고리즘, 데이터 분석
// 데이터 변환 파이프라인 (FP가 자연스러움)
const processUserData = (users) =>
users
.filter(user => user.age >= 18)
.map(user => ({ ...user, isAdult: true }))
.sort((a, b) => a.name.localeCompare(b.name))
.slice(0, 10);
하이브리드 접근법
현대 JavaScript는 두 패러다임을 혼합하여 사용합니다.
// React: OOP + FP 혼합
class UserProfile extends React.Component { // OOP: 클래스
render() {
const { user } = this.props;
// FP: 순수 함수, 불변성
const fullName = [user.firstName, user.lastName]
.filter(Boolean)
.join(' ');
return <div>{fullName}</div>;
}
}
// 또는 함수형 컴포넌트 (더 선호됨)
const UserProfile = ({ user }) => {
const fullName = [user.firstName, user.lastName]
.filter(Boolean)
.join(' ');
return <div>{fullName}</div>;
};
| 비교 항목 | 객체지향 | 함수형 |
|---|---|---|
| 중심 개념 | 객체와 상태 | 함수와 데이터 |
| 데이터 변경 | 가능 (Mutable) | 불가능 (Immutable) |
| 코드 재사용 | 상속 | 함수 합성 |
| 상태 관리 | 객체 내부 상태 | 외부 상태 전달 |
| 학습 난이도 | 쉬움 | 어려움 |
| 적합한 분야 | UI, 게임, 시뮬레이션 | 데이터 처리, 병렬 처리 |
정답은 없습니다. 상황에 맞게 선택하거나, 두 가지를 혼합하여 사용하는 것이 현명합니다. JavaScript, Python 등 현대 언어들은 멀티 패러다임을 지원하므로, 각 패러다임의 장점을 활용할 수 있습니다.
