Node.js 개발자가 알아야할 디자인 패턴

곽태민·2023년 5월 11일
0

TIL

목록 보기
26/63

🤔 디자인 패턴이란?


디자인 패턴은 소프트웨어 디자인 과정에서 자주 발생하는 문제들에 대한 전형적인 해결책이다. 이는 코드에서 반복되는 디자인 문제들을 해결하기 위해 맞춤화 할 수 있는 미리 만들어진 청사진과 비슷하다.

쉽게 풀어보자면 개발 방법 중 구조적인 문제 해결에 목적을 두고, 알고리즘과 같이 특정 문제 해결하는 Logic보다는 특정 상황에 적용할 수 있는 방법이다.

재사용과 팀원과의 의사소통으로 자주 반복되는 문제에 대한 해결책을 놓고, 23가지의 디자인 패턴이 존재한다. 연관되는 개념으로는 SOLID 원칙Clean Code 등이 있다.

👻 Node.js 디자인 패턴


먼저 디자인 패턴은 목적에 따라서 세가지로 구분을 할 수 있다.

  1. Creational: 객체 인스턴스 생성
  2. Structural: 객체 설계 방식
  3. Behavioural: 객체가 상호 작용하는 방식

본인은 Node.js가 주특기라 Node.js 개발자가 알아야할 디자인 패턴을 알아보려고한다.

Singleton

클래스의 단일 인스턴스만을 원할 때 이 패턴을 사용한다. 즉, 여러개의 인스턴스를 생성하는게 아니라 하나만 생성하는 것이다. 인스턴스가 없으면 새 인스턴스를 생성한다. 인스턴스가 있는 경우, 해당 인스턴스를 사용한다.

class DatabaseConnection {
	constructor() {
    	this.databaseConnection = 'Test'
    }
  
  	getNewDBConnection() {
    	return this.databaseConnection
    }
}

class Singleton {
	constructor() {
    	throw new Error('Use the getInstance() method on the singleton object!')
    }
  
  	getInstance() {
    	if (!Singleton.instance) {
        	Singleton.instance = new DatabaseConnection()
        }
      
      	return Singleton.instacne
    }
}

module.exports = Singleton

위 코드처럼 싱글톤을 구축할 수 있는 많은 예제가 있다. 이 외에 싱글톤 패턴을 구현하는 더 짧은 방법은 아래와 같다.

class DatabaseConnection() {
	constructor() {
    	this.databaseConnection = 'Test'
    }
  
  	getNewDBConnection() {
    	return this.databaseConnection
    }
}

module.exports = new DatabaseConnection()

위 코드가 작동할 수 있는 이유는 module caching system이다. module caching system은 모듈이 처음 로딩 된 이후에 캐싱이 되는 것을 의미한다. 즉, 위 코드는 새롭게 exported된 인스턴스는 캐싱이 되고, 이것이 재사용될 때, 캐쉬된 내용을 불러온다.

따라서 Node.js에서 싱글톤 구현 방법을 위 코드 예시처럼 두가지로 볼 수 있다. 요약을 하자면 *싱글톤은 단 하나의 클래스 인스턴스가 필요할 때 유용, Node.js에서는 module caching system을 활용해서 export한 모듈을 바로 쓸 수 있다.

Factory

객체를 생성하는데 사용되는 인터페이스 또는 추상 클래스를 정의하는 것이다. 이렇게 생성된 인터페이스 및 추상 클래스를 사용하여 다른 객체를 초기화 한다.

import Motorvehicle from "./Motorvehicle"
import Aircraft from "./Aircraft"
import Railvehicle from "./Railvehicle"

const VehicleFactory = (type, make, model, year) => {
	if (type === car) {
    	return new Motorvehicle("car", make, model, year)
    } else if (type === airplane) {
    	return new Aircraft("airplane", make, model, year)
    } else if (type === helicopter) {
    	return new Aircraft("helicopter", make, model, year)
    } else {
    	return new Railvehicle("train", make, model, year)
    }
}

module.exports = VehicleFactory

이렇게 각 클래스 인스턴스를 별개로 만드는 대신에 VehicleFactory를 활용해서 타입을 명시하는 방법을 택할 수 있다. 위 코드를 활용해서 car인스턴스를 만들려면 아래와 같이 실행하면 된다.

// 첫 매개변수만 타입을 지정하고, 나머지는 그대로 변수로 넘긴다.
const audiAllRoad = VehicleFactory("car", "Audi", "A6 Allroad", "2020")

팩토리 디자인 패턴을 사용하면 객체의 구조가 객체 그 차제 사이를 디커플링 시킬 수 있다는 장점이 있다. 기존 코드를 손상시키지 않더라도 새 객체를 응용프로그램에 사용할 수 있다. 그리고 인스턴스 생성과 관련된 모든 코드가 한 곳에 있으므로 코드를 더 잘 꾸밀 수 있다.

요약을 해보자면 팩토리 디자인 패턴은 객체 생성을 위한 인터페이스 및 추상 클래스를 제공하고, 동일한 인터페이스 및 추상 클래스를 사용하여 다른 객체를 만들 수 있다. 마지막으로, 코드 구조를 개선하고 유지관리가 더 쉬워진다.

Builder

객체 구조와 객체를 분리할 수 있다. 따라서 복잡한 객체를 생성하는 코드를 단순화 한다. 단순한 객체를 만들 때는 과한 패턴일 수 있지만 복잡한 객체를 만들 때는 단순화 하는데 도움을 준다.

class Car {
	constructor(make, model, year, isForSale = true, isInStock = false) {
    	this.make = make
      	this.model = model
      	this.year = year
      	this.isForSale = isForSale
      	this.isInStock = isInStock
    }
  
  	toString() {
    	return console.log(JSON.stringify(this))
    }
}

class CarBuilder {
	constructor(make, model, year) {
    	this.make = make
      	this.model = model
      	this.year = year
    }
  
  	notForSale() {
      this.ifForSale = false
      
      return this
    }
  
  	addInStock() {
   		this.isInstock = true
      
      	return this
    }
  
  	build() {
    	return new Car(
        	this.make,
          	this.model,
          	this.year,
          	this.isForSale,
          	this.isInStock,
        )
    }
}

module.exports = CarBuilder

위 패턴을 사용하면 Car대신에 CarBuilder를 사용하여 객체를 만들 수 있다.

const CarBuilder = require("./CarBuilder")

const bmw = new CarBuilder('bmw', 'x6', 2020).addInStock().build()
const audi = new CarBuilder('audi', 'a8', 2021).notForSale().build()
const mercedes = new CarBuilder('mercedes-benz', 'c-class', 2019).build()

만약 이런 빌더 패턴없이 복잡한 객체를 만들게 되면 에러를 발생할 가능성이 크다.

const bmw = new CarBuilder('bmw', 'x6', 2020, true, true)

뒤에 있는 true가 각각 무엇을 의미하는지 알아야 하기 때문에 객체 생성이 복잡해 지고, 에러를 만들어낼 가능성이 커딘다. 따라서 빌더 디자인 패턴은 복잡한 객체 생성과 사용을 분리하는데 도움을 준다.

Prototype

Javascript는 프로토타입 기반 언어이기 때문에, 프로토타입으로 상속이 구현되어 있다. 즉, 모든 객체는 어떤 객체를 상속하고 있다는 뜻이다.

따라서 예제 객체라고 불리는 프로토타입 객체의 값을 복제하여 새로운 객체를 만든다. 이는 프로토타입이 새 객체의 일종의 청사진 역할을 하는 것이다.

이 설계 패턴을 활용하면 객체에 정의된 함수가 참조에 의해 생성된다는 이점을 얻을 수 있다. 즉, 모든 객체가 해당 기능의 복사본을 보유하는 것이 아니라 동일한 기능을 가르키게 된다.

간단히 말해서 프로토타입 기능은 프로토타입에 상속된 모든 객체를 사용할 수 있다.

const atv = {
	make: "Honda",
  	model: "Rincon 050",
  	year: 2018,
  	mud: () => {
    	console.log("Mudding")
    },
}

const secondATV = Object.craete(atv)

프로토타입에서 새로운 객체를 생성하기 위해서는, Obecjt.craete() 기능을 사용하면된다. 두번째 객체인 secondATV는 첫번째 객체인 atv와 같은 값을 가지게 된다. mud()를 호출해보면 같은 값을 찍는 것을 알 수 있다.

프로토타입 디자인 패턴을 활용하는 다른 방법은 클래스 안에 프로토타입을 명시하는 것이다.

const atvPrototype = {
  mud: () => {
    console.log('Mudding')
  },
}

function Atv(make, model, year) {
  function constructor(make, model, year) {
    this.make = make
    this.model = model
    this.year = year
  }

  constructor.prototype = atvPrototype

  let instance = new constructor(make, model, year)
  return instance
}

const atv1 = Atv()
const atv2 = Atv('Honda', 'Rincon 650', '2018')

마찬가지로 두 인스턴스 보두 Atv 객체에 정의된 항목에 액세스 할 수 있다. 결론적으로, 프로토타입 설계 패턴은 객체가 동일한 기능 또는 속성을 공유하기를 원할 때 유용하다.

profile
Node.js 백엔드 개발자입니다!

0개의 댓글