01 — OBJECT ORIENTED PROGRAMMING

객체지향 4원칙

객체지향 프로그래밍(OOP)은 데이터와 동작을 객체라는 단위로 묶어 소프트웨어를 설계하는 패러다임입니다. 4가지 핵심 원칙을 통해 유지보수성, 재사용성, 확장성을 달성합니다.

🔒
캡슐화
Encapsulation
데이터(속성)와 메서드를 하나의 단위로 묶고, 내부 구현을 외부에서 직접 접근하지 못하도록 보호합니다.
BankAccount 클래스
🔴 private _balance = 0
🔴 private _pin = "1234"
🟢 public deposit(amount)
🟢 public withdraw(amount)
🟢 public getBalance()
class BankAccount { // private - 외부 직접 접근 불가 #balance = 0; #pin; constructor(pin) { this.#pin = pin; } // public - 공개 인터페이스 deposit(amount) { if (amount > 0) this.#balance += amount; } getBalance() { return this.#balance; } }
내부 잔액(#balance)에 직접 접근 불가 → getBalance()를 통해서만 조회 가능. 데이터 무결성을 보장합니다.
🔗
상속
Inheritance
기존 클래스(부모)의 속성과 메서드를 새로운 클래스(자식)가 물려받아 코드 재사용성을 높입니다.
🐾 Animal
speak() · eat()
🐶 Dog
speak(): "멍멍"
🐱 Cat
speak(): "야옹"
🐦 Bird
speak(): "짹짹"
class Animal { constructor(name) { this.name = name; } eat() { return `${this.name}이 먹습니다`; } speak() { return '...'; } } class Dog extends Animal { speak() { return `${this.name}: 멍멍!`; } } class Cat extends Animal { speak() { return `${this.name}: 야옹~`; } }
Dog/Cat 모두 eat()을 재정의 없이 사용 가능. extends 키워드로 부모 클래스를 상속합니다.
🔀
다형성
Polymorphism
같은 이름의 메서드가 객체 유형에 따라 다르게 동작합니다. 하나의 인터페이스로 여러 구현을 다룹니다.
animals.forEach(a => a.speak()) 호출 결과:
🐶 dog .speak() "멍멍!"
🐱 cat .speak() "야옹~"
🐦 bird .speak() "짹짹!"
const animals = [ new Dog('바둑이'), new Cat('나비'), new Bird('참새'), ]; // 동일한 speak() 메서드 호출 // → 각 객체 유형에 따라 다르게 동작 animals.forEach(a => console.log(a.speak())); // "바둑이: 멍멍!" // "나비: 야옹~" // "참새: 짹짹!"
호출 코드는 구체적 타입을 몰라도 됩니다. 새로운 Animal 하위 클래스를 추가해도 루프 코드 변경 없음.
🎭
추상화
Abstraction
복잡한 내부 구현을 숨기고, 필요한 기능만 단순한 인터페이스로 노출합니다. 복잡성을 관리합니다.
사용자 코드: car.start()
↓ 추상화 경계
Car 인터페이스: start() / stop() / accelerate()
↓ 내부 구현 (숨김)
엔진점화 → 연료주입 → 크랭크샤프트 → 피스톤 ...
// 추상 클래스 / 인터페이스 abstract class Shape { abstract area(): number; // 구현 강제 abstract perimeter(): number; describe() { return `넓이: ${this.area()}`; } } class Circle extends Shape { area() { return Math.PI * this.r ** 2; } perimeter() { return 2 * Math.PI * this.r; } }
사용자는 area()가 어떻게 계산되는지 알 필요 없습니다. 인터페이스만 알면 됩니다.
02 — SOLID PRINCIPLES

SOLID 원칙

로버트 마틴이 정립한 객체지향 설계의 5가지 핵심 원칙입니다. 유지보수하기 쉽고 확장 가능한 소프트웨어 구조를 만드는 가이드라인입니다. 카드를 클릭하면 나쁜 예 / 좋은 예를 비교합니다.

S
단일 책임 원칙
Single Responsibility Principle
하나의 클래스는 하나의 책임만 가져야 합니다. 클래스를 변경하는 이유는 오직 하나여야 합니다.
↻ 클릭해서 예시 보기
❌ 나쁜 예
class UserManager { saveUser(user) { /* DB 저장 */ } sendEmail(user) { /* 이메일 발송 */ } generateReport() { /* 보고서 생성 */ } // 책임이 3개! → 변경 이유가 3개 }
✅ 좋은 예
class UserRepository { save(user) { /* DB 저장만 */ } } class EmailService { send(user) { /* 이메일만 */ } } class ReportGenerator { generate() { /* 보고서만 */ } }
O
개방-폐쇄 원칙
Open/Closed Principle
소프트웨어 요소는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다. 기존 코드를 변경하지 않고 기능을 추가해야 합니다.
↻ 클릭해서 예시 보기
❌ 나쁜 예
function getArea(shape) { if (shape.type === 'circle') { return Math.PI * shape.r ** 2; } else if (shape.type === 'rect') { return shape.w * shape.h; } // 새 도형 추가 → 함수 수정 필요! }
✅ 좋은 예
class Circle { area() { return Math.PI*this.r**2; } } class Rect { area() { return this.w*this.h; } } // 새 도형 → 클래스만 추가, 기존 코드 불변 class Triangle { area() { return ...; } }
L
리스코프 치환 원칙
Liskov Substitution Principle
하위 타입은 언제나 상위 타입을 대체할 수 있어야 합니다. 자식 클래스는 부모 클래스 계약을 위반하면 안 됩니다.
↻ 클릭해서 예시 보기
❌ 나쁜 예
class Bird { fly() { return '날아!'; } } class Penguin extends Bird { fly() { throw new Error('펭귄은 못 날아!'); // LSP 위반! Bird 대체 불가 } }
✅ 좋은 예
class Bird { move() { ... } } class FlyBird extends Bird { fly() { ... } } class SwimBird extends Bird { swim() { ... } } // Penguin extends SwimBird → 계약 준수
I
인터페이스 분리 원칙
Interface Segregation Principle
클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강요받지 않아야 합니다. 큰 인터페이스를 작게 분리하세요.
↻ 클릭해서 예시 보기
❌ 나쁜 예
interface Worker { work(); eat(); // 로봇은 먹지 않음! sleep(); // 로봇은 자지 않음! } class Robot implements Worker { eat() { throw Error('미구현'); } // 강제! }
✅ 좋은 예
interface Workable { work(); } interface Feedable { eat(); sleep(); } class Human implements Workable, Feedable {...} class Robot implements Workable {...}
D
의존성 역전 원칙
Dependency Inversion Principle
고수준 모듈은 저수준 모듈에 직접 의존해서는 안 됩니다. 둘 다 추상화(인터페이스)에 의존해야 합니다.
↻ 클릭해서 예시 보기
❌ 나쁜 예
class OrderService { constructor() { // 구체 클래스에 직접 의존! this.db = new MySQLDatabase(); } // MySQL → PostgreSQL 변경 시 코드 수정 필요 }
✅ 좋은 예
class OrderService { constructor(db: IDatabase) { this.db = db; // 인터페이스에 의존 } } // new OrderService(new MySQL()) → OK // new OrderService(new Postgres()) → OK
03 — FUNCTIONAL PROGRAMMING

함수형 프로그래밍

함수형 프로그래밍(FP)은 부작용을 최소화하고 순수 함수의 합성으로 프로그램을 구성하는 패러다임입니다. 데이터의 불변성과 선언적 코드 스타일이 핵심입니다.

순수 함수: 같은 입력 → 항상 같은 출력, 외부 상태 변경 없음
❌ 부작용 함수 ✅ 순수 함수
❌ 부작용 (impure)
let total = 0; function addToTotal(n) { total += n; // 외부 변수 변경! return total; } addToTotal(5); // 5 addToTotal(5); // 10 (달라짐!) addToTotal(5); // 15 (예측 불가)
✅ 순수 함수 (pure)
function add(a, b) { return a + b; // 외부 상태 없음 } add(10, 5); // 항상 15 add(10, 5); // 항상 15 add(10, 5); // 항상 15 // 테스트하기 쉽고 예측 가능!
순수 함수의 조건: ① 동일 입력 → 동일 출력 (결정론적) ② 부작용 없음 (외부 상태 변경, 콘솔 출력, 네트워크 요청 등 없음) ③ 외부 변수에 의존하지 않음
불변성: 데이터를 직접 변경하지 않고 새 복사본을 만들어 반환합니다.
현재 배열: [1, 2, 3, 4, 5]
❌ 가변 (mutable)
const arr = [1, 2, 3]; arr.push(4); // 원본 변경! arr.sort(); // 원본 변경! arr.splice(0,1); // 원본 변경!
✅ 불변 (immutable)
const arr = [1, 2, 3]; const arr2 = [...arr, 4]; // 새 배열 const arr3 = [...arr].sort(); // 새 배열 const arr4 = arr.filter(x=>x!==arr[0]);
고차함수: 함수를 인자로 받거나 함수를 반환하는 함수입니다.
입력 배열:
함수 합성: 여러 함수를 합쳐 새로운 함수를 만듭니다. compose(f, g)(x) = f(g(x))
입력
5
double
x × 2
addTen
x + 10
square
결과
400
입력값:
const double = x => x * 2; const addTen = x => x + 10; const square = x => x * x; // compose 유틸: 오른쪽에서 왼쪽으로 적용 const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x); const transform = compose(square, addTen, double); transform(5); // square(addTen(double(5))) // = square(addTen(10)) // = square(20) // = 400
직접 배열을 입력하고 map / filter / reduce 를 단계적으로 적용해 보세요.
배열:
Step 1
Step 2
최종
04 — PARADIGM COMPARISON

패러다임 비교

같은 문제를 3가지 프로그래밍 패러다임으로 해결합니다. 탭을 클릭해서 각 접근 방식을 비교해 보세요.

문제: 숫자 배열 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 에서 짝수의 제곱합 계산
순차적 실행 명시적 반복문 상태 변수
단계별로 명령을 나열하여 "어떻게 할 것인가"를 기술합니다. 변수의 상태가 변화하며 결과를 도출합니다.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum = 0; // 단계 1: 반복문으로 각 요소 처리 for (let i = 0; i < numbers.length; i++) { // 단계 2: 짝수인지 확인 if (numbers[i] % 2 === 0) { // 단계 3: 제곱을 누적합에 더함 sum += numbers[i] * numbers[i]; } } console.log(sum); // 220 (4 + 16 + 36 + 64 + 100)
PROS
  • 이해하기 직관적
  • 실행 흐름이 명확
  • 성능 최적화 용이
  • 낮은 추상화 비용
CONS
  • 코드가 길어짐
  • 재사용성 낮음
  • 상태 관리 복잡
  • 테스트 어려움
클래스 기반 캡슐화 재사용성
데이터와 동작을 객체로 묶어 관리합니다. 문제를 객체들의 협력으로 해결하며 확장성이 높습니다.
class NumberCollection { constructor(numbers) { this.numbers = numbers; } filterEvens() { return this.numbers.filter(n => n % 2 === 0); } squareAll(arr) { return arr.map(n => n * n); } sum(arr) { return arr.reduce((acc, n) => acc + n, 0); } sumOfEvenSquares() { const evens = this.filterEvens(); const squares = this.squareAll(evens); return this.sum(squares); } } const nums = new NumberCollection([1,2,3,4,5,6,7,8,9,10]); console.log(nums.sumOfEvenSquares()); // 220
PROS
  • 코드 재사용성 높음
  • 현실 세계 모델링
  • 유지보수 용이
  • 확장성 우수
CONS
  • 보일러플레이트 코드
  • 과도한 추상화
  • 상태 공유 문제
  • 학습 곡선 존재
선언적 불변성 합성
무엇을 할 것인가를 선언적으로 표현합니다. 순수 함수와 불변 데이터로 부작용을 최소화합니다.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // 순수 함수 정의 const isEven = n => n % 2 === 0; const square = n => n * n; const add = (acc, n) => acc + n; // 함수 합성으로 문제 해결 (선언적) const result = numbers .filter(isEven) // [2, 4, 6, 8, 10] .map(square) // [4, 16, 36, 64, 100] .reduce(add, 0); // 220 console.log(result); // 220 // isEven, square, add는 독립적으로 테스트 가능
PROS
  • 간결하고 읽기 쉬움
  • 테스트 용이 (순수함수)
  • 병렬 처리 안전
  • 버그 발생 적음
CONS
  • 학습 곡선 높음
  • 성능 오버헤드
  • I/O 처리 복잡
  • 재귀 사용 증가
패러다임 특성 비교표
특성 절차형 객체지향 함수형
핵심 단위 함수/절차 클래스/객체 순수 함수
상태 관리 전역/지역 변수 객체 내부 상태 불변 데이터
코드 재사용 함수 호출 상속/합성 함수 합성
부작용 많음 캡슐화로 관리 최소화
테스트 난이도 중간 중간~어려움 쉬움
대표 언어 C, Pascal Java, C++, Python Haskell, Clojure
사고 방식 어떻게(How) 무엇이(What is) 무엇을(What)