TIL

디자인 패턴과 프로그래밍 패러다임

'면접을 위한 CS 전공지식 노트'라는 책을 읽게 되었습니다. 약 300페이지 정도로, 개발 서적 치고는 얇은 축에 속하는 책이었습니다. 그럴 때 있죠. 갑자기 책을 읽기 싫을 때. 책을 어떻게 하면 효율적이면서도 즐겁게 읽을 수 있을지에 대해 고민했습니다. 그러다가,

2025년 10월 7일8min read

0. 들어가며 🎯

'면접을 위한 CS 전공지식 노트'라는 책을 읽게 되었습니다. 약 300페이지 정도로, 개발 서적 치고는 얇은 축에 속하는 책이었습니다. 그럴 때 있죠. 갑자기 책을 읽기 싫을 때.

책을 어떻게 하면 효율적이면서도 즐겁게 읽을 수 있을지에 대해 고민했습니다. 그러다가, 평소에는 업신여기던 색인을 보게 되었습니다. 책의 맨 뒤편에 위치한 색인(index)은, 마치 사전처럼 키워드와 해당 키워드가 활용되는 페이지로 구성되어 있습니다.

키워드에 해당하는 페이지에 가서, 해당 키워드를 설명하는 단 한 줄을 형광펜으로 하이라이팅 합니다. '면접을 위한 CS 전공지식 노트'라는 책은 약 490 단어로 구성되어 있습니다. 한 단어의 하이라이팅에 1분 정도 소요됨을 확인했고, 실제로 이틀 만에 책을 다 읽게 되었습니다.

이때, 반드시 하이라이팅 한 요소들을 처음부터 끝까지 다시 읽으며 맥락을 파악해야 합니다. 하이라이팅은 책을 이해하기 쉽게 만드는 사전 작업에 불과하고, 인사이트는 맥락 속에 숨어 있기 때문이죠.

이처럼 반복적인 문제(=책을 읽기 싫다는 상황)에 대한 재사용 가능한 해결책(=색인을 활용한 키워드 중심 순환 독서)을 디자인 패턴이라고 합니다.

1. 디자인 패턴 🎯

1-1. 싱글톤 패턴 ✅

싱글톤 패턴은, 하나의 클래스는 하나의 인스턴스만 갖는 패턴입니다.

js
class Singleton {
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
  getInstance() {
    return this;
  }
}

const a = new Singleton();
const b = new Singleton();
// true
console.log(a === b);

싱글톤 패턴은 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있습니다. 이때 의존성 주입을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있습니다.

의존성 주입은, 객체가 스스로 필요한 의존성을 만들지 않고, 외부에서 필요한 의존성을 넣어주는 방식입니다.

1-2. 팩토리 패턴 ✅

팩토리 패턴은, 상위 클래스에서는 골격을, 하위 클래스에서는 내용을 결정하는 패턴입니다.

js
class CoffeeFactory {
  static createCoffee(type) {
    const factory = factoryList[type];
    return factory.createCoffee();
  }
}

class Latte {
  constructor() {
    this.name = "Latte";
  }
}

class Espresso {
  constructor() {
    this.name = "Espresso";
  }
}

class LatteFactory extends CoffeeFactory {
  static createCoffee() {
    return new Latte();
  }
}

class EspressoFactory extends CoffeeFactory {
  static createCoffee() {
    return new Espresso();
  }
}

const factoryList = { LatteFactory, EspressoFactory };

const main = () => {
  const coffee = CoffeeFactory.createCoffee("LatteFactory");
  // Latte
  console.log(coffee.name);
};

main();

CoffeeFactory 클래스에서는 음료 제조의 가장 일반적인 골격을 정의하고, 하위 클래스에서 라떼 혹은 에스프레소처럼 구체적인 내용을 결정합니다.

1-3. 전략 패턴 ✅

전략 패턴은, 객체의 행위를 직접 수정하지 않고, 전략을 수정하는 패턴입니다.

js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Local 전략
passport.use(new LocalStrategy((u, p, done) => {
  if (u === 'minkwan' && p === '1234') return done(null, { username: u });
  done(null, false);
}));

// Google 전략
passport.use(new GoogleStrategy({
    clientID: 'ID', clientSecret: 'SECRET', callbackURL: '/auth/google/callback'
  },
  (token, refresh, profile, done) => done(null, { googleId: profile.id })
));

// 컨텍스트 사용 예시
// passport.authenticate('local')
// passport.authenticate('google')

passport 객체의 authenticate()라는 행위를 직접적으로 수정하지 않고, 전략을 변경한다는 점을 확인할 수 있습니다.

1-4. 옵저버 패턴 ✅

옵저버 패턴은, 상태 변화를 옵저버에게 알려주는 패턴입니다.

js
class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(o) {
    this.observers.push(o);
  }

  notify(data) {
    this.observers.forEach((o) => o(data));
  }
}

const subject = new Subject();

function observer1(msg) {
  console.log("옵저버1:", msg);
}

function observer2(msg) {
  console.log("옵저버2:", msg);
}

subject.subscribe(observer1);
subject.subscribe(observer2);

// [ [Function: observer1], [Function: observer2] ]
console.log("현재 옵저버 배열:", subject.observers);

// 옵저버1: 상태 변화 발생!
// 옵저버2: 상태 변화 발생!
subject.notify("상태 변화 발생!");

// 옵저버1: 또 다른 변화!
// 옵저버2: 또 다른 변화!
subject.notify("또 다른 변화!");

주체(Subject)의 상태가 변화했는데, 모든 옵저버 함수가 주체의 상태 변화에 반응하는 모습을 확인할 수 있습니다. 트위터에서 만약 내가 어떤 유명인(주체)을 팔로우했다면, 주체가 포스팅을 했을 때 모든 팔로워에게 알림이 가야겠죠. 트위터는 옵저버 패턴을 활용한 대표적인 서비스입니다.

1-5. 프록시 패턴 ✅

프록시 패턴은, 대상 객체 앞에 프록시를 두는 패턴입니다.

Node.js 창시자 라이언 달(Ryan Dahl)은 다음과 같이 말했습니다.

You just may be hacked when some yet-unknown buffer overflow is discoverd. Not that couldn't happen behind nginx, but somehow having a proxy in front makes me happy.

Node.js를 운영할 때 많은 사람들이 위 언급을 유념하여 Nginx를 당연하게 적용하고 있습니다.

이처럼 프록시 패턴을 통해 대상 객체에 도달하기 전, 그 접근에 대한 흐름을 가로채 해당 접근을 필터링하거나 수정할 수 있습니다.

CloudFlare와 같은 서비스를 통해 웹 서버 앞단에서 DDOS 공격을 방어하거나 HTTPS를 구축하는 것 역시 프록시 패턴의 일환으로 볼 수 있습니다.

1-6. 이터레이터 패턴 ✅

이터레이터 패턴은, 이터레이터를 통해 컬렉션 요소에 접근하는 패턴입니다.

js
const mp = new Map();

mp.set("a", 1);
mp.set("b", 2);
mp.set("c", 3);

const st = new Set();

st.add(1);
st.add(2);
st.add(3);

// [ 'a', 1 ]
// [ 'b', 2 ]
// [ 'c', 3 ]
for (let a of mp) console.log(a);

// 1
// 2
// 3
for (let a of st) console.log(a);

컬렉션이란, 리스트나 배열 등의 자료 구조를 의미합니다.

위 코드의 경우, Set과 Map은 서로 다른 자료 구조이지만, for a of b라는 동일한 이터레이터 프로토콜을 통해 순회하는 것을 확인할 수 있습니다.

1-7. 노출 모듈 패턴 ✅

노출 모듈 패턴은, 즉시 실행 함수로 접근 제어자를 만드는 패턴입니다.

js
const reveal = (() => {
  const a = 1;
  const b = () => 2;

  const public = {
    c: 2,
    d: () => 3,
  };
  return public;
})();

// { c: 2, d: [Function: d] }
console.log(reveal);

// undefined
console.log(reveal.a);
console.log(reveal.b);

즉시 실행 함수란, 함수를 정의하자마자 바로 호출하는 함수입니다.

접근 제어자란, 클래스의 속성과 메서드에 접근할 수 있는 범위를 제한하는 키워드입니다.

public은 자식 클래스와 외부 클래스 모두에서 접근 가능한 범위를 의미합니다. protected는 자식 클래스에서는 접근 가능하지만 외부 클래스에서는 접근 불가능한 범위입니다. 마지막으로 private은 자식 클래스와 외부 클래스 모두에서 접근이 불가능한 범위입니다.

위 코드의 경우 함수를 즉시 실행했기에 a와 b는 private 범위에 속하고, c와 d는 public 범위를 갖게 됩니다.

1-8. MVC 패턴 ✅

MVC 패턴은, Model-View-Controller로 관심사를 분리하여 개발하는 패턴입니다.

MVC 패턴의 핵심은 Controller에 있습니다. View는 UI, Model은 데이터베이스로 이해할 수 있습니다.

사용자가 UI 클릭을 통해 특정 요청을 하면, Controller가 Model의 데이터를 변경하거나 비즈니스 로직을 처리한 뒤, 변경된 모델을 기반으로 뷰를 다시 보여주는 흐름을 지휘합니다.

1-9. MVP 패턴 ✅

MVP 패턴은, Model-View-Presenter로 관심사를 분리하여 개발하는 패턴입니다.

MVP 패턴의 핵심은 Presenter에 있습니다. Presenter는 뷰와 모델 사이를 완벽하게 분리합니다.

뷰는 사용자 입력을 받으면 어떤 처리도 하지 않고 즉시 프레젠터에게 "이런 이벤트가 발생했어요!"라고 보고만 합니다. 그러면 프레젠터가 모델에서 데이터를 가져와 가공한 뒤, 다시 뷰에게 "이 데이터를 이렇게 보여주세요."라고 직접 지시합니다. 이로 인해 뷰와 모델은 서로의 존재를 전혀 모르게 되고, 오직 프레젠터를 통해서만 소통하게 됩니다.

1-10. MVVM 패턴 ✅

MVVM 패턴은, Model-View-View Model로 관심사를 분리하여 개발하는 패턴입니다.

MVVM에서 데이터 바인딩은 뷰와 뷰모델(ViewModel)을 자동으로 동기화하는 강력한 접착제입니다. 뷰모델의 데이터가 변경되면, 데이터 바인딩이 이를 감지하고 별도의 명령 없이도 뷰의 내용을 즉시 갱신합니다. 반대로 사용자가 뷰에서 데이터를 변경하면, 이 역시 자동으로 뷰모델에 반영됩니다. 개발자가 직접 UI를 업데이트하는 코드를 짤 필요 없이, 데이터만 바꾸면 화면이 저절로 따라오는 것이 가장 큰 특징입니다.

MVC, MVP, MVVM 패턴의 차이는 아직 명확하게 와닿지 않아서 추가적인 공부가 필요하다고 생각합니다.

2. 프로그래밍 패러다임 🎯

2-1. 함수형 프로그래밍 ✅

함수형 프로그래밍은, 고차함수를 통해 재사용성을 높인 프로그래밍 패러다임입니다. 이때, 고차함수란 함수를 매개변수로 받는 함수입니다.

js
// 고차 함수: applyToArray
function applyToArray(arr, callback) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i]));
  }
  return result;
}

const double = (n) => n * 2;
const square = (n) => n ** 2;

const numbers = [1, 2, 3, 4];

// [2, 4, 6, 8]
console.log(applyToArray(numbers, double));

// [1, 4, 9, 16]
console.log(applyToArray(numbers, square));

콘솔 함수에 applyToArray라는 함수가 매개변수로 전달되고 있기에, 콘솔 함수는 고차함수라고 할 수 있습니다. applyToArray 함수에도 double 또는 square라는 함수가 전달되고 있기에, applyToArray 함수도 고차함수라고 할 수 있겠네요.

이처럼 함수의 연속으로 프로그래밍 하는 패러다임을 함수형 프로그래밍이라고 합니다.

2-2. 객체지향 프로그래밍 ✅

객체지향 프로그래밍은, 속성과 행위를 클래스로 묶는 프로그래밍 패러다임입니다.

js
// 클래스
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  // 행위(메서드)
  greet() {
    console.log(
      `안녕하세요, 저는 ${this.name}이고, 나이는 만 ${this.age}살입니다.`
    );
  }

  birthday() {
    this.age += 1;
    console.log(`생일 축하! 이제 만 ${this.age}살이에요.`);
  }
}

// 객체 생성
const person1 = new Person("민관", 26);

// 메서드 호출
person1.greet(); // 안녕하세요, 저는 민관이고, 나이는 만 26살입니다.
person1.birthday(); // 생일 축하! 이제 만 27살이에요.

객체지향 프로그래밍은 4가지 특징을 갖고 있습니다.

1. 추상화: 핵심적인 특징만 추출하는 것입니다. 자동차는 핸들 / 페달 / 바퀴 등으로 추상화할 수 있습니다. 2. 캡슐화: 속성과 메서드를 하나로 묶고 일부를 외부에 감추어 은닉하는 것을 뜻합니다. 3. 상속성: 상위 클래스의 특성을 하위 클래스가 이어받아 재사용하거나 추가 / 확장하는 것을 말합니다. 4. 다형성: 하나의 메서드가 다양한 방법으로 동작하는 것을 말합니다. 이때, 오버로딩은 동일한 이름을 가진 메서드를 여러 개 두는 것을 의미하고, 오버라이딩은 상속받은 메서드를 하위 클래스가 재정의하는 것을 의미합니다.

추가적으로, 객체지향 프로그래밍은 SOLID라 불리는 다섯 가지 설계 원칙을 갖고 있습니다.

1. Single Responsibility Principle: 모든 클래스는 각각 하나의 책임만 가져야 한다는 원칙입니다. 단일 책임 원칙이라고 합니다. 2. Open Closed Principle: 기존의 코드는 잘 변경하지 않으면서도, 확장은 쉽게 할 수 있어야 한다는 원칙입니다. 개방-폐쇄 원칙이라고 합니다. 3. Liskov Substitution Principle: 상속 관계에서 부모 타입을 자식 타입으로 바꿔도 문제없이 동작해야 한다는 원칙입니다. 리스코프 치환 원칙이라고 합니다. 4. Interface Segregation Principle: 하나의 일반적인 인터페이스가 아니라, 구체적인 여러 개의 인터페이스를 만들어야 한다는 원칙입니다. 5. Dependency Inversion Principle: 구체적인 것이 아니라 추상적인 것에 의존해야 한다는 원칙입니다. 의존 역전 원칙이라고 합니다.

2-3. 절차형 프로그래밍 ✅

절차형 프로그래밍은, 연속적인 계산 과정으로 이루어진 프로그래밍 패러다임입니다.

js
a = 5;
b = 3;

c = a + b;
// 8
console.log(c);

d = c * 2;
// 16
console.log(d);

위에서 아래로 순차적으로 코드가 실행된다면, 절차형 프로그래밍 방식이라고 볼 수 있습니다.

3. 정리 🎯

1. 싱글톤 패턴은, 하나의 클래스는 하나의 인스턴스만 갖는 패턴입니다. 2. 팩토리 패턴은, 상위 클래스에서는 골격을, 하위 클래스에서는 내용을 결정하는 패턴입니다. 3. 전략 패턴은, 객체의 행위를 직접 수정하지 않고, 전략을 수정하는 패턴입니다. 4. 옵저버 패턴은, 상태 변화를 옵저버에게 알려주는 패턴입니다. 5. 프록시 패턴은, 대상 객체 앞에 프록시를 두는 패턴입니다. 6. 이터레이터 패턴은, 이터레이터를 통해 컬렉션 요소에 접근하는 패턴입니다. 7. 노출 모듈 패턴은, 즉시 실행 함수로 접근 제어자를 만드는 패턴입니다. 8. MVC 패턴은, Model-View-Controller로 관심사를 분리하여 개발하는 패턴입니다. 9. MVP 패턴은, Model-View-Presenter로 관심사를 분리하여 개발하는 패턴입니다. 10. MVVM 패턴은, Model-View-View Model로 관심사를 분리하여 개발하는 패턴입니다. 11. 함수형 프로그래밍은, 고차함수를 통해 재사용성을 높인 프로그래밍 패러다임입니다. 12. 객체지향 프로그래밍은, 속성과 행위를 클래스로 묶는 프로그래밍 패러다임입니다. 13. 절차형 프로그래밍은, 연속적인 계산 과정으로 이루어진 프로그래밍 패러다임입니다.