본문 바로가기

객체 지향과 함수형 프로그래밍

상태와 동작을 객체로 묶는 방식과 순수 함수·불변성을 중심으로 한 방식을 비교해 문제에 맞게 조합합니다.

우리는 지금까지 자바스크립트 기본 문법과 동작 원리, 객체/프로토타입 기반 데이터 관리 방법을 학습했습니다.

이제는 코드를 어떤 방식으로 조직하고 설계할지로 시야를 확장해야 하며, 이것이 프로그래밍 패러다임(Programming Paradigm)입니다.

프로그래밍 패러다임은 프로그래머가 프로그램을 작성할 때 따르는 일련의 사고방식이나 원칙을 의미합니다.

이는 문제 해결 방식과 코드 구조에 큰 영향을 미치며, 어떤 패러다임을 선택하느냐에 따라 코드의 가독성, 유지보수성, 확장성 등이 달라질 수 있습니다.

자바스크립트는 매우 유연한 언어여서 단일한 패러다임에 얽매이지 않고, 여러 패러다임의 특징을 수용할 수 있습니다.

특히 현대 자바스크립트 개발에서는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)과 함수형 프로그래밍(Functional Programming, FP)이라는 두 가지 주요 패러다임의 특징이 널리 사용됩니다.

이번 장에서는 두 핵심 패러다임의 기본 개념과 자바스크립트 적용 방식을 정리합니다.

상태 소유권과 변환 방식 선택

FLOWCHART · WEB PLATFORM CONTRACT

상태 소유권과 변환 방식 선택

상태의 수명과 규칙을 한 객체가 관리할지, 입력값을 순수 함수로 변환할지 판단한다. 두 접근은 조합할 수 있으며, 경계에서 변경·복사 책임을 정한다.

입금 상태의 소유와 이전 값 보존같은10000원에5000원을 입금하는 작업에서 객체의메서드, 순수변환, 두방식조합으로 세 비교 경로가 각각 갈라진다. 메서드는 같은계좌의 현재잔액을 갱신하고 순수변환은 이전값과 새값을 보관할 수 있다. 조합은 검증과 계산을 함수에 두고 계좌가 변경과이력을 관리하는 예시다. 모바일도 세 경로는 원입력에서 독립적으로 시작한다.같은 입금: 10000 + 5000무엇을 보존할 것인가?객체의 메서드같은 계좌의 잔액 갱신변경 규칙을 메서드에 둠현재 잔액 15000순수 변환입력 10000은 유지결과 15000을 반환이전·다음 값 보관두 방식 조합입금액 검증·계산은 함수계좌가 변경·이력 관리책임을 경계에서 구분세 가지 선택은 직렬 단계가 아니라 비교 대상
입금 상태의 소유와 이전 값 보존같은10000원에5000원을 입금하는 작업에서 객체의메서드, 순수변환, 두방식조합으로 세 비교 경로가 각각 갈라진다. 메서드는 같은계좌의 현재잔액을 갱신하고 순수변환은 이전값과 새값을 보관할 수 있다. 조합은 검증과 계산을 함수에 두고 계좌가 변경과이력을 관리하는 예시다. 모바일도 세 경로는 원입력에서 독립적으로 시작한다.입금액 5000이전 잔액 10000객체의 메서드같은 계좌의 잔액 갱신변경 규칙을 메서드에 둠현재 잔액 15000순수 변환입력 10000은 유지결과 15000을 반환이전·다음 값 보관두 방식 조합입금액 검증·계산은 함수계좌가 변경·이력 관리책임을 경계에서 구분

Object method

한 owner가 stable identity와 invariant를 캡슐화하고 공유 alias를 제한할 때

Pure function

이전 값과 입력에서 새 값을 만들며 snapshot 비교·history·undo·deterministic test가 중요할 때

Hybrid boundary

성능상 내부 hot loop를 mutable하게 유지하면서 외부에는 immutable snapshot을 제공할 때. 복사·동기화·identity 교체의 책임을 경계에 둔다.

순수한 변환과 부작용의 책임을 구분한다

순수 함수는 같은 입력에 같은 결과를 주고 외부 상태를 바꾸지 않는다. 모든 호출이 반드시 새 객체를 만들어야 하는 것은 아니다. 객체 내부의 상태 변경과 순수한 데이터 변환을 조합하되 공유 참조와 snapshot의 변경 책임을 명확히 한다.


객체 지향 프로그래밍

앞 절의 class·클로저와 5장의 객체 참조·얕은 복사를 사용할 수 있으면 예시를 따라갈 수 있습니다. 이 절에서는 같은 잔액 변경을 두 방식으로 비교합니다. 어떤 이름의 패러다임인지보다 이전 잔액을 보존해야 하는지, 변경 규칙을 누가 검사하는지에 주목하세요.

객체 지향 프로그래밍(OOP)은 실제 세계의 개체(객체)를 모델링하여 프로그램을 설계하는 패러다임입니다.

데이터(속성)와 데이터를 처리하는 기능(메서드)을 하나의 객체 안에 캡슐화하고, 이 객체들 간의 상호작용을 통해 프로그램을 구성합니다.

OOP의 핵심 원칙은 다음과 같습니다.

캡슐화 (Encapsulation)
  • 데이터와 데이터를 조작하는 메서드를 하나의 단위(객체)로 묶는 것입니다.
  • 객체의 내부 구현을 외부로부터 숨기고, 미리 정의된 인터페이스(메서드)를 통해서만 접근하도록 합니다. 이는 코드의 응집도를 높이고, 외부 변경으로부터 내부를 보호하여 유지보수를 쉽게 합니다.
  • 자바스크립트에서는 클로저 또는 class의 # 프라이빗 필드로 외부의 직접 접근을 제한할 수 있습니다. 클래스 문법은 ES2015, 프라이빗 필드는 ES2022에 도입되었으며 서로 다른 시기의 기능입니다.
// class를 이용한 캡슐화 예시
class Account {
    #balance; // ES2022 private class fields (최신 문법)

    constructor(initialBalance) {
        if (initialBalance < 0) {
            throw new Error("초기 잔액은 0 이상이어야 합니다.");
        }
        this.#balance = initialBalance;
    }

    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`입금: ${amount}, 현재 잔액: ${this.#balance}`);
        }
    }

    withdraw(amount) {
        if (amount > 0 && this.#balance >= amount) {
            this.#balance -= amount;
            console.log(`출금: ${amount}, 현재 잔액: ${this.#balance}`);
        } else {
            console.log("잔액 부족 또는 유효하지 않은 금액.");
        }
    }

    getBalance() {
        return this.#balance;
    }
}

const myAccount = new Account(10000);
myAccount.deposit(5000);
myAccount.withdraw(2000);
console.log(myAccount.getBalance());
// console.log(myAccount.#balance); // 직접 접근 불가 (SyntaxError)

이 예시의 출력은 입금: 5000, 현재 잔액: 15000, 출금: 2000, 현재 잔액: 13000, 13000입니다. 모든 금액을 유한한 숫자로 넣는 학습용 조건입니다. #balance는 외부 접근을 제한하지만 잘못된 자료형을 자동으로 거르지 않습니다. 예컨대 문자열 "5000"을 입금하면 +=의 문자열 결합 문제가 생기므로 실제 금액 처리에는 자료형·범위 검사가 더 필요합니다.

상속 (Inheritance)
  • 부모 객체(클래스)의 속성과 메서드를 자식 객체(클래스)가 물려받아 재사용하거나 확장하는 기능입니다.
  • 코드의 재사용성을 높이고, 계층적인 구조를 통해 시스템을 모델링하는 데 유리합니다.
  • 자바스크립트에서는 프로토타입 체인이나 class extends 키워드를 통해 구현합니다.
// 상속 예시 (이전 3장에서 다룬 class 상속)
class Vehicle {
    constructor(brand) {
        this.brand = brand;
    }
    start() {
        console.log(`${this.brand} 시동을 겁니다.`);
    }
}

class Car extends Vehicle {
    constructor(brand, model) {
        super(brand);
        this.model = model;
    }
    drive() {
        console.log(`${this.brand} ${this.model}가 운전 중입니다.`);
    }
}

const myCar = new Car("현대", "쏘나타");
myCar.start(); // 부모 클래스의 메서드 사용
myCar.drive(); // 자식 클래스의 메서드 사용
추상화 (Abstraction)
  • 복잡한 시스템의 세부 구현을 숨기고, 사용자에게 필요한 핵심 기능만을 노출하는 것입니다.
  • 사용자는 객체가 무엇을 하는지는 알지만, 어떻게 하는지는 알 필요가 없게 만듭니다.
  • 이는 복잡성을 관리하고, 시스템을 더 쉽게 이해하고 사용할 수 있도록 돕습니다.
다형성 (Polymorphism)
  • 같은 이름의 메서드가 객체의 종류에 따라 다르게 동작하는 능력입니다.
  • 즉, 다양한 형태(다형)를 가질 수 있다는 의미로, 코드를 더 유연하고 확장 가능하게 만듭니다.
  • 자바스크립트에서는 메서드 오버라이딩(자식 클래스에서 부모 메서드 재정의) 등을 통해 구현됩니다.
class Animal {
    sound() {
        console.log("동물 소리!");
    }
}

class Cat extends Animal {
    sound() { // 부모의 sound 메서드를 재정의 (오버라이딩)
        console.log("야옹!");
    }
}

class Dog extends Animal {
    sound() { // 부모의 sound 메서드를 재정의 (오버라이딩)
        console.log("멍멍!");
    }
}

const animals = [new Animal(), new Cat(), new Dog()];

animals.forEach(animal => animal.sound());
/*
결과:
동물 소리!
야옹!
멍멍!
*/
// 동일한 sound() 메서드를 호출했지만, 객체 타입에 따라 다른 동작을 보입니다.
OOP의 장점
  • 재사용성: 클래스와 상속을 통해 코드를 재사용하고 확장하기 용이합니다.
  • 유지보수성: 캡슐화를 통해 코드 변경의 영향을 최소화할 수 있습니다.
  • 생산성: 잘 설계된 객체는 독립적으로 개발하고 테스트할 수 있습니다.
  • 확장성: 새로운 기능을 추가하기 용이하며, 기존 코드를 덜 변경하고도 기능을 확장할 수 있습니다.
OOP의 단점
  • 설계에 많은 시간과 노력이 필요할 수 있습니다.
  • 클래스 계층 구조가 복잡해지면 이해하기 어려워질 수 있습니다.
  • 상속의 깊이가 깊어지면 특정 변화가 예기치 않은 곳에서 문제를 일으킬 수 있습니다 (상속의 문제).

함수형 프로그래밍

함수형 프로그래밍(FP)은 프로그램을 순수 함수(Pure Function)들의 조합으로 생각하고, 데이터의 불변성(Immutability)을 강조하는 패러다임입니다.

상태 변경과 부작용(Side Effect)을 최소화하여 코드의 예측 가능성을 높이고 버그 발생 가능성을 줄이는 데 중점을 둡니다.

FP의 핵심 개념은 다음과 같습니다.

순수 함수 (Pure Function)
  • 동일한 입력에 대해 항상 동일한 출력을 반환합니다.
  • 함수 외부의 어떤 상태도 변경하지 않습니다. (Side Effect 없음)
  • 함수 외부의 변수를 수정하거나, 콘솔에 출력하거나, 네트워크 요청을 보내는 등의 행위는 부작용으로 간주됩니다.
// 순수 함수 (Pure Function)
function add(a, b) {
    return a + b; // 항상 동일한 입력에 동일한 출력, 외부 상태 변경 없음
}

// 순수하지 않은 함수 (Impure Function) 예시
let total = 0;
function addToTotal(num) {
    total += num; // 외부 변수 total을 변경 (부작용)
    return total;
}
// console.log() 또한 부작용입니다.

수치 입력을 사용하는 위 add는 호출 순서에 의존하지 않습니다. 반면 addToTotal(3)을 두 번 호출하면 3, 6으로 달라집니다. Date.now()처럼 외부 상태를 읽어 결과를 바꾸는 함수도, 외부 값을 직접 수정하지 않더라도 순수 함수로 보지 않습니다.

불변성 (Immutability)
  • 데이터를 한 번 생성하면 절대로 변경하지 않습니다.
  • 데이터를 변경해야 할 필요가 있을 때는 원본 데이터를 복사하여 새로운 데이터를 생성하고 변경된 내용을 적용합니다.
  • 이는 코드의 안정성을 높이고, 시간 경과에 따른 데이터의 변화를 추적하기 쉽게 만듭니다.
// 불변성 유지 (배열 복사)
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4]; // 전개 구문으로 새로운 배열 생성

console.log(originalArray); // 결과: [1, 2, 3] (원본은 변경되지 않음)
console.log(newArray);    // 결과: [1, 2, 3, 4]

// 불변성 유지 (객체 복사)
const originalUser = { name: "Alice", age: 30 };
const updatedUser = { ...originalUser, age: 31 }; // 새로운 객체 생성

console.log(originalUser); // 결과: { name: 'Alice', age: 30 } (원본은 변경되지 않음)
console.log(updatedUser);  // 결과: { name: 'Alice', age: 31 }

전개 구문은 얕은 복사입니다. 위 객체의 name·age는 원시값이어서 비교하기 쉽지만, 중첩 객체까지 자동으로 독립적인 것은 아닙니다. const도 재할당을 막을 뿐 객체의 변경을 막지 않습니다. 중첩 값을 바꿀 때는 바뀌는 경로의 객체를 각각 새로 만드는지 확인해야 합니다.

고차 함수 (Higher-Order Functions)
  • 함수를 인자로 받거나, 함수를 반환하는 함수를 의미합니다.
  • 자바스크립트의 map, filter, reduce 등이 대표적인 고차 함수입니다. 이는 함수형 프로그래밍에서 매우 중요한 개념입니다.
const numbers = [1, 2, 3, 4, 5];

// map: 배열의 각 요소에 함수를 적용하여 새로운 배열 반환 (불변성 유지)
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // 결과: [1, 4, 9, 16, 25]

// filter: 조건에 맞는 요소만 걸러내어 새로운 배열 반환 (불변성 유지)
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 결과: [2, 4]

// reduce: 배열의 모든 요소를 하나의 값으로 축소
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum); // 결과: 15

reduce의 합은 누적값 0 → 1 → 3 → 6 → 10 → 15로 확인할 수 있습니다. 이 map·filter 콜백은 원소를 변경하지 않지만, 메서드 이름 자체가 순수성을 보장하지는 않습니다. 콜백에서 입력 객체를 수정하면 새 배열을 반환해도 원본의 중첩 값은 달라질 수 있습니다.

같은 잔액 변경을 순수 함수로 표현하기

다음 함수는 입력 잔액을 바꾸는 대신 새 잔액을 반환합니다. 유효하지 않은 금액에서는 예외를 던지는 계약을 코드에 적습니다.

function depositValueForPractice(balance, amount) {
    if (!Number.isFinite(balance) || balance < 0 ||
        !Number.isFinite(amount) || amount <= 0) {
        throw new Error("유한한 잔액과 양수 입금액이 필요합니다.");
    }
    return balance + amount;
}
const previousBalanceForPractice = 10000;
const nextBalanceForPractice = depositValueForPractice(previousBalanceForPractice, 5000);
console.log(previousBalanceForPractice, nextBalanceForPractice);

출력 10000 15000에서 이전 값과 다음 값을 따로 보관할 수 있습니다. 함수 안에서 DOM·로그를 변경하지 않으며, console.log는 결과를 관찰하는 바깥 코드입니다. 금액의 숫자 범위를 검증하는 예시이고 실제 화폐의 소수 처리·동시 입금 문제까지 구현한 코드는 아닙니다.

설계 연습

입금 후 이전 잔액 10000으로 되돌리기 기능이 필요합니다. 위 코드에 다시 2000을 입금한 다음 값도 저장하고, 세 잔액을 출력하세요. 원래 Account 예시를 그대로 사용할 때 되돌리기에 무엇을 추가로 기록해야 하는지도 비교하세요.

값과 변경 이력의 비교

const finalBalanceForPractice = depositValueForPractice(nextBalanceForPractice, 2000); 뒤 세 값을 출력하면 10000 15000 17000입니다. 이전 값을 보관했으므로 화면이 사용할 값을 바꾸어 되돌릴 수 있습니다. Account는 같은 인스턴스의 내부 잔액을 갱신하므로 되돌리기 위한 이전 값 또는 거래 이력을 별도로 기록하고 그에 맞는 메서드를 설계해야 합니다. OOP에도 이력을 넣을 수 있고, 순수 변환만으로 전체 UI의 부작용이 사라지는 것은 아닙니다.

FP의 장점
  • 예측 가능성: 순수 함수와 불변성으로 인해 코드의 동작을 예측하기 쉽습니다.
  • 테스트 용이성: 부작용이 없어 단위 테스트 작성이 매우 용이합니다.
  • 병렬 처리: 데이터 변경이 없으므로 병렬 프로그래밍에 유리합니다.
  • 모듈성: 함수 단위로 코드가 잘 분리되어 재사용성이 높습니다.
FP의 단점
  • 데이터 불변성을 유지하기 위한 추가적인 복사 작업이 성능에 영향을 줄 수 있습니다.
  • 복잡한 상태 관리가 필요한 경우, 데이터 흐름이 다소 복잡해질 수 있습니다.
  • 익숙하지 않은 개발자에게는 초기에 학습 곡선이 높을 수 있습니다.

자바스크립트에서의 멀티 패러다임 활용

자바스크립트는 객체 지향적 특성(객체 리터럴, 프로토타입, class)과 함수형 특성(함수가 일급 객체, 클로저, 고차 함수)을 모두 강력하게 지원하는 멀티 패러다임(Multi-paradigm) 언어입니다.

따라서 개발자는 상황과 문제의 특성에 따라 두 패러다임을 유연하게 조합하여 사용할 수 있습니다.

  • 객체 지향: 잔액처럼 오래 유지할 상태와 변경 규칙을 한 소유자가 관리할 때 유용합니다. UI 컴포넌트라는 이름만으로 객체 지향인지 판정하지 않고, 상태와 변경 책임이 실제로 어떻게 묶였는지 봅니다.
  • 함수형: 데이터 처리, 변환, 상태 관리의 예측 가능성이 중요할 때, 순수하고 독립적인 유틸리티 함수를 만들 때 강력합니다. 특히 웹 애플리케이션의 상태 변화를 관리하거나, 비동기 작업을 처리할 때 함수형 접근이 빛을 발합니다.

효율적이고 가독성 높은 자바스크립트 코드를 작성하기 위해서는 이 두 패러다임의 장점을 이해하고, 어느 한쪽에 치우치기보다는 문제 해결에 가장 적합한 방식을 선택하여 조합하는 능력을 기르는 것이 중요합니다.


프로그래밍 패러다임 정리

마무리에서는 OOP와 FP를 대립 개념으로 보기보다 문제 특성에 맞춰 조합하는 기준을 확인합니다.

이번 장에서는 자바스크립트 코드를 설계하고 구성하는 주요 사고방식인 프로그래밍 패러다임에 대해 학습했습니다.

특히 객체 지향 프로그래밍(OOP)의 캡슐화, 상속, 추상화, 다형성 원칙과, 함수형 프로그래밍(FP)의 순수 함수, 불변성, 고차 함수 개념을 상세히 살펴보았습니다.

자바스크립트가 멀티 패러다임 언어라는 점을 이해하고, 각 패러다임이 가진 장점과 단점을 바탕으로 실제 개발에서 어떻게 활용될 수 있는지 그 가능성을 보았습니다.

특정 문제에 직면했을 때, 이것을 객체로 모델링하는 것이 좋을까?, 아니면 데이터를 변경하지 않는 순수 함수들의 조합으로 해결하는 것이 더 효율적일까?와 같은 질문을 스스로 던져보며 사고의 폭을 넓히는 것이 중요합니다.

프로그래밍 패러다임을 비교하면 같은 문제도 객체, 함수, 상태 흐름 중 어떤 관점으로 나눌지 판단하기 쉬워집니다.

자바스크립트에서는 한 방식만 고집하기보다 문제의 경계에 맞는 표현을 선택하는 것이 중요합니다.

실무에서는 OOP와 FP를 이분법으로 나누기보다 상태의 수명, 데이터 변환의 빈도, 협업 경계를 보고 조합합니다.

기능을 설계할 때는 상태 수명과 데이터 변환 빈도를 기준으로 객체 지향과 함수형 접근의 우선순위를 판단합니다.

객체 지향과 함수형 프로그래밍은 대립 구도가 아니라 상태의 수명과 데이터 변환 방식을 기준으로 조합하는 선택지입니다.