[면접을 위한 CS 전공지식 노트] (길벗, 주홍철 저) 를 참고하여 정리한 내용입니다.
디자인 패턴
- 프로그램 설계시 발생할 수 있는 문제점을 해결할 수 있도록 규약을 만들어 놓은 것
- 객체간의 상호 관계등을 이용해서 반복되는 문제를 해결할 수 있도록 만든 일종의 솔루션
종류는 아래와 같다.
싱글톤 패턴
- 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
- 보통 데이터 베이스 연결 모듈에 많이 사용한다고 한다.
- 하나의 인스턴스를 만들고 다른 모듈들이 이를 공유하기 때문에 인스턴스 생성에 드는 비용은 적지만, 그만큼 의존성이 높아진다는 단점이 있다.
코드로 보면 아래와 같다.
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
- 앞서 DB 연결 모듈을 구현할 때 많이 쓰인다고 했는데, DB 연결의 경우 여러개의 연결이 아닌 하나의 연결을 유지하며 전역으로 공유되어야 한다. 위 방식으로 DB 인스턴스를 구현하면 어디서 생성하더라도 같은 인스턴스에 접근할 수 있기 때문에 인스턴스 생성 비용을 아낄 수 있다.
- 책에 등장한 예시로는 mongoose 의 MongoDB 연결 함수인 connect 함수, Node.js 에서 MySql 연결시에도 사용 될 수 있다고 소개하고 있다.
- 단점으로는 TDD 에 어려움이 있다. 단위 테스트에서 각 테스트는 독립적이어야 하는데, 싱글톤 패턴은 하나의 인스턴스를 기반으로 구현되기 때문에 독립적인 인스턴스를 만들기 어렵다.
- 그리고 각 모듈간 결합을 강하게 만들 수 있다는 문제점이 있다. 이 패턴에서는 클래스 내부에서 인스턴스를 생성하고 관리하게 되는데, 모든 모듈이 한치의 변화도 없이 동일한 인스턴스를 공유하게 된다.
- 이는 의존성 주입을 통해 어느정도 해소가 가능하다.
- 의존성 주입은 '의존성 주입자'라는 하나의 추상화를 만들어서 메인 모듈과 그의 인스턴스 모두 이에 의존하도록 만드는 방식이다. 그럼으로써 하위 모듈에 대한 의존성을 떨어뜨릴 수 있는데, 이에 대한 조금 더 자세한 내용은 이전에 정리해 놓은 내용이 있으니 참고.
팩토리 패턴
- 객체 생성 부분을 떼어내서 추상화한 패턴으로, 상위 클래스에서 뼈대를 결정하고 하위 클래스에서 객체 생성에 대한 구체적인 내용을 결정하는 패턴이다.
- 상위 클래스와 하위 클래스의 분리로 느슨한 결합을 가지게 되고, 이 때문에 더 많은 유연성을 가질 수 있다. 객체를 생성하는 상위 클래스에서는 하위 클래스에서 구체적으로 어떤 객체를 생성하는지, 어떻게 생성되는지 자세히 알 필요가 없기 때문이다.
- 책에서의 예시는 바리스타 공장과 레시피를 예시로 들고 있다. 공장이 인스턴스를 생성하는 상위 클래스라면 레시피에는 어떤 인스턴스를 생성해야 하는지 구체적인 내용을 담고 있다. 상위 클래스에 구체적인 내용을 전달하면 그 것을 토대로 객체를 생성하는 것이다!
- 아래는 예제 코드이다.
// 커피를 만들 수 있는 커피 팩토리 클래스
class CoffeeFactory {
// factoryList 에서 주어진 타입에 맞는 커피 팩토리를 찾아서 리턴한다.
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 = () => {
// 라떼 커피를 주문한다.
// createCoffee가 정적 메소드이기 때문에 인스턴스를 만들지 않고도 바로 접근할 수 있다.
// CoffeeFactory는 factoryList 에서 해당하는 구체적인 커피 공장을 찾아 해당 클래스의 메서드를 실행한다.
const coffee = CoffeeFactory.createCoffee("LatteFactory")
// 커피 이름을 부른다.
console.log(coffee.name) // latte
}
main()
- 이 코드에서 CoffeeFactory 내에서 LatteFactory의 인스턴스를 생성하는 것이 아니라, 외부에서 LatteFactory 의 인스턴스를 생성한 뒤에 주입하고 있기 때문에 의존성 주입이라고 볼 수 있다.
- 위 코드에서 static 키워드를 통해 메서드를 정적으로 선언하고 있는데, 주석에 작성한 바와 같이 이렇게 선언된 메서드나 프로퍼티는 별도의 인스턴스를 생성하지 않고도 클래스를 기반으로 호출이 가능하다. 때문에 이 메서드에 대한 메모리 할당을 한 번만 할 수 있다는 장점이 있다.
전략 패턴
- 정책 패턴(policy pattern) 이라고도 하고, 객체 행위를 바꾸고 싶은 경우 직접 수정하는게 아니라 '전략'이라고 부르는 '캡슐화된 알고리즘'을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하도록 만드는 패턴이다.
- 책에서는 결제라는 행위에서 구체적인 결제 방식을 신용카드로 할지 카카오 페이로 할 지 선택하는 것을 예시로 들었는데, 결제가 행위라면 카카오페이, 네이버페이 등 결제 방식은 구체적인 전략으로 이 전략만 바꿔 끼우면서 행위를 구현하는 패턴을 의미하는 것 같다.
- 컨텍스트란 문맥이라는 의미로 프로그래밍에서는 이 코드의 배경, 환경, 조건을 의미한다. 예를 들어 자바스크립트에서 실행 컨텍스트란 해당 함수, 코드를 실행하기 위한 조건 ~ 스코프, 변수, this 등등을 포함하고 있다. 뭔가 정확하게 설명은 어렵지만 전략에 해당하는 코드를 실행하고 실질적으로 문제를 해결하는 코드 정도로 이해하고 넘어가겠다.
옵저버 패턴
- 어떤 객체의 상태 변화를 관찰하다가, 변화가 있을 때마다 메서드를 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다. 이를 감지하는 관찰자 있고, 이를 통해 일대다의 형태로 의존성을 가지고 있다. 옵저버란 관찰중인 객채에 변화가 생겼을 때 이로 인한 추가 변화가 생기는 객체들을 의미한다. 예를 들면 트위터.
- 이 옵저버 패턴은 이벤트 기반 시스템, MVC 패턴 등에서도 사용된다.
- 이벤트 기반 시스템이란? 구글에 검색하니 Event-driven Architecture 라는 항목으로 검색 결과가 뜬다. 처음 들어보는 말이라 간단히 정리하고 넘어가자면, 사용자의 입력이나 상태 변화 같은 변화를 이벤트로 보고, 이 이벤트가 발생 할 때마다 처리하는 방식으로 동작한다고 한다. 같이 등장하는 키워드로는 분산 아키텍쳐 ~ 마이크로 서비스 아키텍쳐, 이벤트 루프 등이 있다.
- 조금 생각해보면 자바스크립트의 addEventListner 라던가 브라우저에서 이벤트를 처리하는 방식이 이벤트 드리븐 방식인 것 같다. 이벤트가 발생하면 이를 기반으로 핸들러를 실행하고, 이 함수들은 브라우저에서 비동기적으로 동작할 때 이벤트 큐와 이벤트 루프에 의해서 실행된다.
- 아무튼 이야기가 좀 샜는데, 자바스크립트에서 옵저버 패턴은 프록시 객체를 통해서 구현할 수도 있다. 프록시 객체란 어떤 대상의 기본 동작을 가로챌 수 있는 객체를 의미하는데, 기본적인 동작이란 접근, 할당, 순회, 열거, 함수 호출 등을 의미한다. 이 프록시 객체는 프록시할 대상인 target과 동작을 가로채고 무슨 행동을 할 것인지 정의된 handler 두 개의 인자를 받는다.
- 아래는 예제 코드이다.
function createReactiveObject(target, callback) {
// target에 변화가 있을 때 set이 실행된다.
const proxy = new Proxy(target, {
// proxy 객체의 핸들러에는 다양한 메서드가 있다.
// set은 그 중에서 속성 값에 값을 할당할 경우 호출된다.
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.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다