TIL

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

Frontend Developer 2023. 5. 18. 18:14

 

디자인패턴

 

 

디자인패턴이란 프로그램 설계시에 발생했던 문제점들을 해결할 수 있도록 하나의 규약으로 만들어 놓은 형태를 말한다.

디자인패턴으로 10가지를 정리할 수 있다.

싱글톤 팩토리 전략 옵저버 프록시패턴
프록시서버
이터레이터 노출모듈 MVC MVP MVVM

 

 

1. 싱글톤패턴

  • 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
  • 보통 데이터베이스 연결모듈에 많이 사용
  • 장점 : 하나의 인스턴스로 다른 모듈들이 공유하므로 인스턴스를 생성할 때 드는 비용이 줄어듦
  • 단점
    • 하나의 인스턴스에 의존하므로 의존성이 높아짐
    • TDD(Test Driven Development)를 할 때 걸림돌이 됨
    • 단위테스트(Unit Test)는 테스트가 독립적이어야 하며 어떤 순서로든 실행할 수 있어야 하지만 싱글톤패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 독립적인 인스턴스를 만들기 어려움
  • 의존성주입
    • 의존성주입의 원칙 : 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 함, 둘 다 추상화에 의존해야하며 이때 추상화는 세부 사항에 의존하지 말아야 함
    • 의존성모듈을 주입하므로 인해서 모듈간의 결합을 느슨하게 만들어 의존성을 낮출 수 있음
    • 메인모듈에서 의존성주입자(Dependency Injector)를 생성하여 메인모듈이 하위모듈에 간접적으로 의존성을 주입
    • Decoupling 된다고 표현
    • 장점
      • 모듈들을 쉽게 교체가능한 구조가 되어 테스팅과 마이그레이션이 쉬워짐
      • 어플리케이션의 의존성 방향이 일관됨
      • 어플리케이션이 쉽게 추론될 수 있음
      • 모듈간의 관계가 더 명확해짐
    • 단점
      • 모듈들이 분리되므로 약간의 런타임패널티가 생성됨

* 인스턴스 :  객체지향프로그래밍(OOP)에서 class에 소속된 개별적인 객체를 의미한다.

* 모듈 : 프로그램을 구성하는 구성요소, 관련된 데이터와 함수를 하나로 묶은 단위를 의미한다.

 

 

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

Singleton.instance라는 하나의 인스턴스를 가지는 Singleton 클래스를 구현한 모습이다. 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

DB.instance를 기반으로 a, b를 생성하면 데이터베이스 연결에 관한 인스턴스 생성 비용을 아낄 수 있다.

 

 

2. 팩토리패턴

  • 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴
  • 상속관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정
  • 상위클래스와 하위클래스의 분리로 느슨한 결합을 가지고 많은 유연성을 가짐
  • 코드를 리팩토링해도 한 곳만 고칠 수 있어 유지보수성이 높아짐
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()

각 Latte, Espresso가 하위클래스이고, CoffeeFactory가 상위클래스이다.

 

 

3. 전략패턴

  • 정책패턴(Policy Pattern)라고도 함
  • 객체의 행위를 직접 수정하지 않고 캡슐화한 알고리즘(전략)을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만듦
  • 상품 결제시에 토스, 네이버, 카카오 등 결제 방식의 전략만 바뀌게 구성

* 컨텍스트 :  프로그래밍에서 상황, 맥락, 문맥을 의미한다. 개발자가 작업을 완료하는 데 필요한 모든 관련 정보를 말한다.

 

passport의 전략패턴

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);
        });
    }
));

Node.js에서 인증 모듈을 구현할 때 사용하는 미들웨어 라이브러리이다. passport.use()라는 메서드에 전략을 매개변수로 넣어 로직을 수행한다.

 

 

4. 옵저버패턴

옵저버패턴은 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때 마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인패턴이다. 옵저버패턴은 프록시 객체를 통해 구현할 수 있다.

 

프록시 객체

  • 어떠한 대상의 기본적인 동작의 작업을 가로챌 수 있는 객체
  • JS에서 프록시객체는 두개의 매개변수를 가짐
    • target : 프록시할 대상
    • handler : target 동작을 가로채고 어떤 동작을 할 것인지가 설정되어 있는 함수
const handler = {
	get : function(target, name) {
        return name === 'name' ? `${target.a} ${target.b}` : target[name]
        }
    }
const p = new Proxy({a: 'KUNDOL', b: 'IS AUMUMU ZANGIN'}, handler)
console.log(p.name) // KUNDOL IS AUMUMU ZANGIN

 

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

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 = {
    "형규" : "솔로"
} 
const b = createReactiveObject(a, console.log)
b.형규 = "솔로"
b.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다

set() 함수를 통해 속성에 대한 접근을 가로채 형규라는 속성이 솔로에서 커플로 되는 것을 감시할 수 있다.

 

 

5. 프록시패턴과 프록시서버

프록시패턴

프록시패턴은 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴이다. 이를 통해 객체의 속성, 변환 등을 보완하며 보안, 데이터 검증, 캐싱, 로깅에 사용한다.

 

프록시서버

프록시서버는 서버와 클라이언트 사이에서 클라이언트가 자신을 통해 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해주는 시스템 혹은 응용 프로그램이다.

  • nginx를 프록시서버로 앞단에 놓고 Node.js를 뒤쪽에 놓으면 Node.js의 버퍼 오버플로우의 취약점을 예방할 수 있음
  • 익명의 사용자가 직접적으로 서버에 접근하는 것을 차단할 수 있음
  • 간접적으로 한 단계를 더 거치게 만들어 보안을 강화할 수 있음

* 버퍼 오버플로우 : 버퍼는 데이터가 저장되는 공간으로, 메모리 공간을 벗어나는 경우를 말한다.

 

CloudFlare

시스템의 콘텐츠 전달을 빠르게 할 수 있는 CDN서비스이다. 웹 서버 앞단에 프록시 서버로 두어 DDOS 공격 방어나 HTTPS 구축에 쓰인다. 해외에서 의심스러운 트래픽이 감지되면 CAPTCHA 등을 기반으로 일정 부분 막아주는 역할도 수행한다.

 

* CDN(Content Delivery Network) : 각 사용자가 인터넷에 접속하는 곳과 물리적으로 가까운 곳에서 콘텐츠를 캐싱 또는 배포하는 서버 네트워크를 의미한다. 사용자가 콘텐츠를 다운로드 하는 시간을 줄일 수 있다.

 

CORS와 프론트엔드의 프록시서버

CORS(Cross-Origin Resource Sharing)은 서버가 웹 브라우저에서 리소스를 로드할 때, 다른 오리진을 통해 로드하지 못하게 하는 HTTP 헤더 기반 메커니즘이다. 프론트엔드 서버에서 백엔드서버와 통신할 때, CORS에러를 방지하기 위해 프록시 서버를 만들기도 한다. 

 

 

6. 이터레이터 패턴

이터레이터 패턴은 이터레이터(Iterator)를 사용하여 컬렉션(collection)의 요소들에 접근하는 디자인패턴이다. 순회할 수 있는 여러가지 자료형의 구조와는 상관없이 이터레이터라는 하나의 인터페이스로 순회 가능하다.

 

const mp = new Map() 
mp.set('a', 1)
mp.set('b', 2)
mp.set('cccc', 3) 
const st = new Set() 
st.add(1)
st.add(2)
st.add(3) 
const a = []
for(let i = 0; i < 10; i++)a.push(i)

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

set과 map은 다른 자료구조이다. 똑같은 for a of b라는 이터레이터 프로토콜을 통해 순회할 수 있다.

 

* 프로토콜 : 컴퓨터 내부에서 데이터의 교환 방식을 정의하는 규칙 체계를 의미한다.

* 이터러블(iterable)한 객체 : 배열을 일반화한 객체이다.

 

 

7. 노출모듈 패턴

노출모듈(Revealing Module) 패턴은 즉시 실행 함수를 통해 private, public 같은 접근 제어자를 만드는 패턴을 말한다. JS는 전역범위에서 스크립트가 실행되는데, 노출모듈을 통해 접근제어자를 구현하기도 한다.

const pukuba = (() => {
    const a = 1
    const b = () => 2
    const public = {
        c : 2, 
        d : () => 3
    }
    return public 
})() 
console.log(pukuba)
console.log(pukuba.a)
// { c: 2, d: [Function: d] }
// undefined

a와 b는 다른 모듈에서 사용할 수 없는 변수나 함수이며 private 범위를 가진다. c와 d는 다른 모듈에서 사용할 수 있는 변수나 함수이며 public 범위를 가진다. 

 

 

8. MVC 패턴

MVC(Model View Controller)로 이루어진 디자인 패턴이다.

  • 어플리케이션의 구성 요소를 세 가지 역할로 구분하여 개발 프로세스에서 각각의 구성요소에만 집중해서 개발할 수 있음
  • 장점 : 재사용성과 확장성이 용이함
  • 단점 : 어플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해짐
  • Model
    • 어플리케이션의 데이터인 데이터베이스, 상수, 변수 등을 뜻함
  • View
    • inputbox, checkbox, text 등 사용자 인터페이스 요소를 나타냄
    • Model이 가진 정보를 저장하지 않아야 함
    • 단순한 인터페이스 정보만 가지고 있어야 함
  • Controller
    • 하나 이상의 Model과 하나 이상의 View를 잇는 역할을 함
    • 이벤트 등 메인 로직을 담당함
    • Model과 View의 생명주기도 관리함
    • Model이나 View의 변경통지를 해석하여 각각 구성요소에 해당 내용을 알려줌

 

9. MVP 패턴

MVC 패턴에서 Controller가 Presenter로 교체된 패턴이다. View와 Presenter가 일대일 관계이므로 더 강한 결합을 지닌 패턴이다.

 

 

10. MVVM 패턴

MVVM 패턴 MVC에서 C에 해당하는 컨트롤러가 View Model로 바뀐 패턴이다.

  • 커맨드와 데이터바인딩을 가지는 것이 특징
  • 양방향 데이터 바인딩을 지원
  • UI를 코드수정 없이 재사용할 수 있음
  • 단위테스팅이 쉬움

 

 

프로그래밍 패러다임

 

프로그래밍 패터다임은 개발자에게 프로그래밍 관점을 갖게 해주는 역할을 하는 개발 방법론이다. 객체지향 프로그래밍은 개발자들이 프로그램을 상호작용하는 객체들의 집합으로 볼 수 있게 하는 반면, 함수형 프로그래밍은 상태값을 지니지 않는 함수값들의 연속으로 생각 할 수 있게 해준다. 

 

1. 선언형 - 함수형 프로그래밍

선언형 프로그래밍이랑 무엇을 풀어내는가에 집중하는 페러다임이다. "프로그램은 함수로 이루어진 것이다."라는 명제가 담겨있다. 함수형 프로그래밍은 순수 함수를 블록처럼 쌓아 로직을 구현하고, 고차 함수를 통해 재사용성을 높인 페러다임이다. 

 

순수함수

const pure = (a, b) => {
	return a + b
}

출력이 입력에만 의존하는 것을 의미한다. a, b말고 다른 전역 변수 c 등이 출력에 영향을 주면 순수 함수가 아니다.

 

고차함수

함수가 함수를 값처럼 매개변수로 받아 로직을 생성할 수 있는 것을 말한다. 이때 고차함수를 쓰기 위해서는 해당 언어가 일급 객체라는 특징을 가져야한다.

  • 일급객체는 변수나 메서드에 함수를 할당할 수 있음
  • 함수 안에 함수를 매개변수로 담을 수 있음
  • 함수가 함수를 반환할 수 있음

 

2.1 명령형 - 객체지향형 프로그래밍

객체지향 프로그래밍(OOP, Object-Oriented Programming)은 객체들의 집합으로 프로그램의 상호작용을 표현하며 데이터를 객체로 취급하여 객체 내부에 선언된 메서드를 활용하는 방식을 말한다.

const ret = [1, 2, 3, 4, 5, 11, 12]
class List {
    constructor(list) {
        this.list = list
        this.mx = list.reduce((max, num) => num > max ? num : max, 0)
    }
    getMax() {
        return this.mx
    }
}
const a = new List(ret)
console.log(a.getMax()) // 12

(자연수로 이루어진 배열에서 최댓값을 찾는 로직)

 

  • 설계에 많은 시간이 소요됨
  • 처리속도가 다른 프로그래밍 패러다임에 비해 상대적으로 느림
  • 추상화, 캡슐화, 상속성, 다형성이라는 특징이 있음
    • 추상화 : 복잡한 시스템으로 부터 핵심적인 개념 또는 기능을 간추려내는 것 (여러 특징 중 일부 간추려 나타냄)
    • 캡슐화 : 객체의 속성과 메서드를 하나로 묶고 일부를 외부에 감추어 은닉하는 것
    • 상속성 : 상위클래스의 특성을 하위클래스가 받아 재사용하거나 추가확장하는 것
    • 다형성 : 하나의 메서드나 클래스가 다양한 방법으로 동작하는 것
  • 설계원칙(SOLID)
    • 단일 책임 원칙(SRP, Single Responsibility Principle)
      • 모든 클래스는 각각 하나의 책임만 가져야 함. A라는 로직에 대한 클래스는 A에 관한 클래스여야 하고 수정 또한 A에 관한 수정이어야 함
    • 개방-폐쇄 원칙(OCP, Open Closed Principle)
      • 유지보수의 사항이 생긴다면 코드를 쉽게 확장할 수 있도록 하고 수정할 때는 닫혀있어야 함
    • 리스코프 치환 원칙(LSP, Liskov Substitution Principle)
      • 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 함
      • 부모객체에 자식객체를 넣어도 시스템이 문제 없이 돌아가는 것
    • 인터페이스 분리 원칙(ISP, Interface Segregation Principle)
      • 하나의 일반적인 인터페이스보다 구체적인 여러 개의 인터페이스를 만들어야 함
    • 의존 역전 원칙(DIP, Dependency Inversion Principle)
      • 상위계층은 하위계층의 변화에 대한 구현으로부터 독립해야 함

 

2.2 명령형 - 절차형 프로그래밍

  • 로직이 수행되어야 할 연속적인 계산과정으로 이루어져 있음
  • 코드의 가독성이 좋고 실행속도가 빠름
  • 대표적으로 계산작업이 많은 곳에 쓰임
  • 모듈화하기 어렵고 유지보수성이 낮음
const ret = [1, 2, 3, 4, 5, 11, 12]
let a = 0
for(let i = 0; i < ret.length; i++){
    a = Math.max(ret[i], a)
} 
console.log(a) // 12

(자연수로 이루어진 배열에서 최댓값을 찾는 로직)