생성자 디자인 패턴

Chang-__-·2023년 3월 16일
0

디자인패턴

목록 보기
1/3
post-thumbnail

팩토리 패턴

팩토리 패턴에는 객체를 생성하기 위한 인터페이스를 정의 하는데, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만드는 패턴이다.

객체 생성과 구현의 분리를 하기 위해서 팩토리는 새 인스턴스 생성을 감싸서 객체 생성시 유연성과 제어를 제공한다.
여기서 유연성제어에 집중을 해보자.
유연성은 뭘까? 객체는 속성과 함수가 변경 혹은 추가 될 수 있다. 이에 따라 객체의 생성을 담당하는 코드는 변경 가능성이 존재한다. 객체의 생성을 담당하는 클래스를 한 곳에서 관리하여 결합도를 줄이기 위해 팩토리 패턴이 나타나게 되었다.
그렇다면 제어는 뭘까? 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 한다. 그래서, 수정이 일어날 가능성이 큰 부분과 그렇지 않은 부분을 분리 하는 것이 좋다는 것이다.

간단하게 코드로 살펴보자.

class Image {
  constructor (path) {
    this.path = path
  }
}
class ImageFactory {
	getImage(name) {
    	return new Image(name)
    }
}
const image = new ImageFactory().getImage('photo.png')

여기서 ImageFactory는 전혀 불필요해 보인다. 그냥 Image 인스턴스를 바인딩해도 됬을텐데 굳이 팩토리 클래스를 하나 감싸서 생성하는가..

const image = new Image('photo.png')

그렇다면 만약 png, jpeg, gif 의 클래스를 나눠야 한다면 어떻게 할까?
일단 클래스를 각각 나눠보자.

class ImageGif extends Image {
  constructor (path) {
    if (!path.match(/\.gif/)) {
      throw new Error(`${path} is not a GIF image`)
    }
    super(path)
  }
}
class ImageJpeg extends Image {
  constructor (path) {
    if (!path.match(/\.jpe?g$/)) {
      throw new Error(`${path} is not a JPEG image`)
    }
    super(path)
  }
}
class ImagePng extends Image {
  constructor (path) {
    if (!path.match(/\.png$/)) {
      throw new Error(`${path} is not a PNG image`)
    }
    super(path)
  }
}
class ImageFactory {
  getImage(name) {
    if (name.match(/\.jpe?g$/)) {
      return new ImageJpeg(name)
    } else if (name.match(/\.gif$/)) {
      return new ImageGif(name)
    } else if (name.match(/\.png$/)) {
      return new ImagePng(name)
    } else {
      throw new Error('Unsupported format')
    }
  }
}

이 때 팩토리클래스는 유의미 해진다.
여기서 객체 생성과 구현을 분리할 때 가질 수 있는 장점이 명확하게 보여진다.

빌더

빌더 패턴은 복잡한 객체의 생성을 단순화하는 생성 디자인 패턴으로, 단계별로 객체를 만들 수 있다.
빌더 패턴의 장점을 살릴 수 있는 가장 명확한 상황은 constructor에 필드가 많거나, 복잡한 매개 변수를 입력으로 사용하는 생성자가 있는 클래스이다.

이 패턴을 왜 써야 하는지 코드로 알아보자.

class Url {
  constructor (protocol, username, password, hostname,
    port, pathname, search, hash) {
    this.protocol = protocol
    this.username = username
    this.password = password
    this.hostname = hostname
    this.port = port
    this.pathname = pathname
    this.search = search
    this.hash = hash

    this.validate()
  }
  toString () {
    let url = ''
    url += `${this.protocol}://`
    if (this.username && this.password) {
      url += `${this.username}:${this.password}@`
    }
    url += this.hostname
    if (this.port) {
      url += this.port
    }
    if (this.pathname) {
      url += this.pathname
    }
    if (this.search) {
      url += `?${this.search}`
    }
    if (this.hash) {
      url += `#${this.hash}`
    }
    return url
  }
}

이런 Url 클래스가 있다고 가정을 했을 때 url 을 만들기 위해선 많은 매개변수가 필요하다.

const url = new Url('https', null, null, 'example.com', null, null, null, null)
url.toString();

이럴 때 빌더 패턴을 적용하기에 딱 좋은데 빌더 클래스를 만들어 보겠다.

class UrlBuilder {
  setProtocol (protocol) {
    this.protocol = protocol
    return this
  }

  setAuthentication (username, password) {
    this.username = username
    this.password = password
    return this
  }

  setHostname (hostname) {
    this.hostname = hostname
    return this
  }

  setPort (port) {
    this.port = port
    return this
  }

  setPathname (pathname) {
    this.pathname = pathname
    return this
  }

  setSearch (search) {
    this.search = search
    return this
  }

  setHash (hash) {
    this.hash = hash
    return this
  }

  build () {
    return new Url(this.protocol, this.username, this.password,
      this.hostname, this.port, this.pathname, this.search,
      this.hash)
  }
}


const url = new UrlBuilder()
  .setProtocol('https')
  .setAuthentication('user', 'pass')
  .setHostname('example.com')
  .build()

사용이 굉장히 직관적으로 된다. 코드의 가독성이 엄청 올라갔고, 각 setter 함수는 우리가 설정하는 매개 변수에 대한 힌트를 명확히 제공하며, 그 외에도 이러한 매개변수를 설정하는 방법에 대한 지침을 제공한다.

모듈 와이어링

모든 어플리케이션은 여러 컴포넌트를 연결한 결과이며, 어플리케이션이 커짐에 따라 이러한 컴포넌트들을 연결하는 방식은 프로젝트의 유지보수 및 성공을 위한 중요한 요인이 된다.

컴포넌트 A와B가 있을 때, A와 B는 종속적인 관계가 된다.

예를 들어 blog를 쓰는 API 를 개발한다 라고 했을 때 blog의 데이터를 다루는 로직, db 의 연결을 담당하는 로직이 있으면 모듈이 이렇게 될 것이다.

종속성을 맺을 수 있는 방법은 2가지가 있는데 싱글톤 패턴과, DI (종속성 주입) 방식이 있다.

싱글톤 종속성

모듈을 연결하는 가장 간단한 방법중 하나가 Node.js의 모듈 시스템을 통해 종속성을 묶는 것 이다.
실제 코드로 데이터베이스 연결을 위한 싱글톤 인스턴스를 사용하여 간단한 블로깅 어플리케이션을 만들어보면 이렇다.

db.js

import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import sqlite3 from 'sqlite3'

const __dirname = dirname(fileURLToPath(import.meta.url))
export const db = new sqlite3.Database(
  join(__dirname, 'data.sqlite')
)

db의 연결객체를 export 시킨다.

blog.js

import { promisify } from 'util'
import { db } from './db.js'

const dbRun = promisify(db.run.bind(db))
const dbAll = promisify(db.all.bind(db))

class Blog {
  initialize () {
    const initQuery = `CREATE TABLE IF NOT EXISTS posts (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      content TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );`

    return dbRun(initQuery)
  }

  createPost (id, title, content, createdAt) {
    return dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
      id, title, content, createdAt)
  }

  getAllPosts () {
    return dbAll('SELECT * FROM posts ORDER BY created_at DESC')
  }
}

const blog = new Blog()

자세히 보면 db.js 에서 db 를 import 해와서 dbRun, dbAll 에다가 싱글톤 형태로 바인딩 시켜준다.

의존성 주입(DI)

싱글톤에서는 blog 모듈과 db 모듈이 밀접하게 결합되어 있었다. 실제로 blog.js 모듈은 db.js 모듈 없이는 동작을 할 수 없으며 다른 DB를 사용하고 싶다고 할 때 다른 DB 모듈을 사용할 수 없다. 의존성 주입 패턴을 활용해서 두 모듈간의 관계를 조금 느슨하게 만들 수 있다.

인젝터는 다른 컴포넌트를 초기화하고 종속성을 함께 연결한다. 이것이 간단한 초기화일 수도 있고, 모든 종속성을 매핑하여 중앙 집중화하는 전역 컨테이너일 수도 있다.
DI는 각 종속성이 모듈에 하드코딩되는 대신 외부에서 주입된다.

일반적인 서비스가 미리 결정된 인터페이스를 통해 종속성을 연결하고, 이 인터페이스의 구현체에 대한 생성과 주입은 인젝트가 담당한다. 즉, 인젝터는 서비스에 대한 종속성을 넣어주는 인스턴스를 제공하는 것을 목표로 한다.

db.js 파일을 리팩터링 해보겠다.

import sqlite3 from 'sqlite3'

export function createDb (dbFile) {
  return new sqlite3.Database(dbFile)
}

바로 db 인스턴스에 바로 접근하는것이 아니라 함수를 통해 db 인스턴스에 접근 할 수 있다.
그 다음은 blog.js 를 리펙토링 해보겠다.

import { promisify } from 'util'
import { createDb } from './db.js'

class Blog {
  constructor (db) {
    this.db = db
    this.dbRun = promisify(db.run.bind(db))
    this.dbAll = promisify(db.all.bind(db))
  }

  initialize () {
    const initQuery = `CREATE TABLE IF NOT EXISTS posts (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      content TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );`

    return this.dbRun(initQuery)
  }

  createPost (id, title, content, createdAt) {
    return this.dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
      id, title, content, createdAt)
  }

  getAllPosts () {
    return this.dbAll(
      'SELECT * FROM posts ORDER BY created_at DESC')
  }
}

 const db = createDb(join(__dirname, 'data.sqlite'))
 const blog = new Blog(db)

여기서 달라진 점은.
1. Blog 클래스 안에서 db 모듈을 import 하여 쓰지 않았다.
2. Blog 클래스 생성자는 db를 인자로 취급한다.
3. Blog 클래스를 인스턴스화 할 때 인스턴스를 명시적으로 주입한다.

싱글톤의 blog 모듈보다 DI를 사용한 블로그 모듈은 db 모듈에서 분리되어 구성요소들이 더욱 격리된 상태로 존재할 수 있다.

0개의 댓글