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


디자인 패턴

프로그램을 설계할 때 발생했던 문제점들을 객체 간 상호 관계 등을 이용해 해결할 수 있도록 하나의 ‘규약’ 형태로 만들어 놓은 것

ㅁ 싱글톤 패턴

하나의 클래스에 오직 하나의 인스턴스(클래스에 소속된 개별적인 객체)만 자기는 패턴. 보통 데이터베이스 연결 모듈에 많이 사용한다.
인스턴스를 생성할 때 드는 비용이 줄어드는 장점과 의존성이 높아진다는 단점이 특징이다.

JS에서는 리터럴 {} 또는 new Object로 객체를 생성하게 되면 다른 어떤 객체와도 같지 않기 때문에 이를 이용해 싱글톤 패턴을 구현한다.

const obj = {
  a: 27
}
const obj2 = {
  b: 27
}

console.log(obj === obj2)
//false
class Singleton {
    constructor() {
        if (!Singleton.instance) {
            Singleton.instance = this
        }
        return Singleton.instance
    }
    getInstance() {
        return this 
    }
}
const a = new Singleton()
const b = new Singleton() 
console.log(a === b)
// true 
//a와 b는 하나의 인스턴스를 가진다.


싱글톤 패턴 > 데이터베이스 연결 모듈

DB.instance라는 하나의 인스턴스를 기반으로 a,b를 생성. 데이터베이스 연결에 관한 인스턴스 생성 비용 절감 효과.

// DB 연결을 하는 것이기 때문에 비용이 더 높은 작업 
const URL = 'mongodb://localhost:27017/kundolapp' 
const createConnection = url => ({"url" : url})    
class DB {
    constructor(url) {
        if (!DB.instance) { 
            DB.instance = createConnection(url)
        }
        return DB.instance
    }
    connect() {
        return this.instance
    }
}
const a = new DB(URL)
const b = new DB(URL) 
console.log(a === b) // true


싱글톤 패턴의 단점

  • 싱글톤 패턴은 TDD(Test Driven Development)를 할 때 걸림돌이 된다. 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 ‘독립적인’ 인스턴스를 만들기가 어렵다.
  • 모듈 간의 결합을 강하게 만들 수 있다. 이를 위해 의존성 주입(DI, Dependency Injection)을 통해 모듈 간 결합을 조금 더 느슨하게 만들어 해결 할 수 있다.


싱글톤 패턴 > 의존성 주입

메인 모듈과 다른 하위 모듈 사이에 의존성 주입자를 배치해 간접적으로 의존성을 주입하는 방식. 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 되는 ‘디커플링’이 된다.

  • 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션도 수원해지지만
  • 모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며 런타임 패널티가 생기기도 한다.

스레스세이프(thread safe)

다중 스레드 환경에서 코드나 프로그램이 동작할 때 여러 스레드가 동시에 접근하거나 수정하더라도 예기치 않은 동작이나 데이터 손상이 발생하지 않는 특성.

Enum을 기반으로 상수 집합을 관리하면 코드를 리팩터링할 때 상수 집합에 대한 로직 수정 시 이 부분만 수정하면 된다는 장점이 있고, 본질적으로 스레드 세이프 하기 때문에 싱글톤 패턴을 만들 때 도움이 된다.

ㅁ 팩토리 패턴

객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴. 상위 클래스가 중요한 뼈대를 결정, 하위 클래스에서 객체 생성에 관한 구체적 내용 결정.

상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 가진다. 객체 생성 로직이 따로 떼어져 있기에 유지 보수성이 증가한다.

마찬가지로 new Object()로 구현 가능하다.

const num = new Object(42)
const str = new Object('abc')
num.constructor.name //Number
str.constructor.name //String
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")  
    // 커피 이름을 부른다.  
    console.log(coffee.name) // latte
}
main()

CoffeeFactory라는 상위 클래스가 중요한 뼈대를 결정, 하위 클래스가 구체적인 내용을 결정하고 있다.

CoffeeFactory에서 LatteFactory의 인스턴스를 생성하는 것이 아닌 LatteFactory에서 생성한 인스턴스를 CoffeeFactory에 주입하고 있기에 의존성 주입이라고도 볼 수 있다.

** CoffeeFactory클래스를 보면 static 키워드를 통해 createCoffee() 메서드를 정적메서드로 선언하면 클래스를 기반으로 객체를 만들지 않고 호출이 가능하며, 해당 메서드에 대한 메모리 할당을 한 번만 할 수 있는 자점이 있다. **

ㅁ 전략 패턴

정책 패턴(policy pattern)이라고도 하며 객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고 전략이라고 부르는 ‘캡슐화한 알고리즘’을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴

컨텍스트

개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보

전략 패턴 > passport

Node.js에서 인증 모듈을 구현할 때 쓰는 미들웨어 라이브러리로 여러가지 전략을 기반으로 인증할 수 있게 한다.

var passport = require('passport')
    , LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
    function(username, password, done) {
        User.findOne({ username: username }, function (err, user) {
          if (err) { return done(err); }
            if (!user) {
                return done(null, false, { message: 'Incorrect username.' });
            }
            if (!user.validPassword(password)) {
                return done(null, false, { message: 'Incorrect password.' });
            }
            return done(null, user);
        });
    }
));

passport.use(new LocalStrategy(…)) 처럼 passport.use()라는 메서드에 전략을 매개변수로 넣어서 로직을 수행한다.


ㅁ 옵저버 패턴

주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 패턴.

주체란 객체의 상태 변화를 보고 있는 관찰자, 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 추가 변화 사항이 생기는 객체들을 의미한다.

image

주로 이벤트 기반 시스템에 사용하며 MVC(Model-View-Controller)패턴에도 사용된다.


옵저버 패턴 > 프록시 객체

자바스크립트에서의 옵저버 패턴은 프록시 객체를 통해 구현할 수도 있다.
프록시 객체란 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 뜻한다.

자바스크립트에서 프록시 객체는 두 개의 매개변수를 가진다.

  • target: 프록시할 대상
  • handler: target동작을 가로채고 어떠한 동작을 할 것인지가 설정되어 있는 함수
const handler = {
  get: function(target, name) {
    return name === 'name' ? `${target.a} ${target.b}`: target[name]
  }
}
const p = new Proxy({a: 'dtwogud', b: 'is programmer'}, handler)
console.log(p.name)
//dtwogud is programmer


ㅁ 프록시 객체를 이용한 옵저버 패턴

function createReactiveObject(target, callback) { 
    const proxy = new Proxy(target, {
        set(obj, prop, value){
            if(value !== obj[prop]){
                const prev = obj[prop]
                obj[prop] = value 
                callback(`${prop}가 [${prev}] >> [${value}] 로 변경되었습니다`)
            }
            return true
        }
    })
    return proxy 
} 
const a = {
    "proxy" : "old"
} 
const b = createReactiveObject(a, console.log)
b.proxy = "old"
b.proxy = "new"

ㅁ 프록시 패턴과 프록시 서버

대상 객체(subject)에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 패턴. 개체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱* 등에 사용.

프록시 패턴 > 프록시 서버

클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 시스템.


용어 정리

ㅁ 상속과 구현의 차이
상속은 일반 클래스, abstract 클래스를 기반으로 구현하며, 구현은 인터페이스를 기반으로 구현한다.

-. 상속
자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용, 자식 클래스에서 추가 및 확장을 할 수 있는 것을 말한다. 재사용성, 중복성의 최소화가 이루어진다.

-. 구현
부모 인터페이스를 자식 클래스에서 재정의해 구현하는 것을 말하며, 상속과는 달리 반드시 부모 클래스의 메서드를 재정의해 구현해야 한다.

ㅁ DOM(Document Object Model)
문서 객체 모델. 웹 브라우저상의 화면을 이루고 있는 요소들을 지칭

ㅁ 프록시 서버에서의 캐싱
멀리 있는 원격 서버에 요청하지 않고 캐시 안에 있는 데이터를 활용해 불필요한 외부와의 연결을 줄여 트래픽을 줄일 수 있다.

ㅁ 버퍼 오버플로우
저장되는 데이터가 버퍼(데이터가 저장되는 메모리 공간)를 벗어나는 경우를 말한다.

댓글남기기