팩토리 패턴에는 객체를 생성하기 위한 인터페이스를 정의 하는데, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만드는 패턴이다.
객체 생성과 구현의 분리를 하기 위해서 팩토리는 새 인스턴스 생성을 감싸서 객체 생성시 유연성과 제어를 제공한다.
여기서 유연성 과 제어에 집중을 해보자.
유연성은 뭘까? 객체는 속성과 함수가 변경 혹은 추가 될 수 있다. 이에 따라 객체의 생성을 담당하는 코드는 변경 가능성이 존재한다. 객체의 생성을 담당하는 클래스를 한 곳에서 관리하여 결합도를 줄이기 위해 팩토리 패턴이 나타나게 되었다.
그렇다면 제어는 뭘까? 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 한다. 그래서, 수정이 일어날 가능성이 큰 부분과 그렇지 않은 부분을 분리 하는 것이 좋다는 것이다.
간단하게 코드로 살펴보자.
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 에다가 싱글톤 형태로 바인딩 시켜준다.
싱글톤에서는 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 모듈에서 분리되어 구성요소들이 더욱 격리된 상태로 존재할 수 있다.