면접을 위한 CS 전공지식 노트
디자인 패턴이 뭐야?
디자인 패턴이란 프로그램을 설계할 때 발생했던 문제점들을 객체 간 상호 관계 등을 이용해 해결할 수 있도록 하나의규약
형태로 만들어 놓은 것이다.
싱글톤 패턴이란 하나의 클래스에 오직 하나의 인스턴스만을 가지는 패턴이며 보통 데이터베이스 연결 모듈에 많이 쓰인다.
자바스크립트에서는 리터럴 {}
또는 new Object
로 객체를 생성하게 되면 독립된 객체이기 때문에 그 자체만으로 싱글톤 패턴이 구현된다.
const obj = {
a: 27
}
const obj2 = {
a: 27
}
console.log(obj === obj2)
// false
new Object
라는 클래스에서 나온 단 하나의 인스턴스니 어느 정도 싱글톤 패턴이라 보지만 실제 싱글톤 패턴은 보통 다음과 같은 코드로 구성된다.
class Singleton{
constructor(){
if(!Singleton.instance){
Singleton.instance = this
}
return Singleton.instance
}
getInstacne(){
return this.instance
}
}
const a = new Singleton()
const b = new Singleton()
console.log(a === b) // true
Singleton.instance
라는 하나의 인스턴스를 가지는 Singleton
클래스를 구현했다.
이를 통해 a
와 b
는 하나의 인스턴스를 가진다. (공유)
싱글톤 패턴은 데이터베이스 연결 모듈에 많이 쓰인다.
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
를 생성해 데이터베이스 연결에 관한 인스턴스 생성 비용을 줄일 수 있다.
싱글톤 패턴은 TDD를 할때 걸림돌이 된다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다.
하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.
모듈 간 결합을 강하게 만들 수 있다는 단점이 있다. 이 때 의존성 주입을 통해 결합을 느슨하게 만들어 해결 가능하다. (디커플링이 된다)
의존성이란 종속성이라고도 하며 A가 B에 의존성이 있다는 것은 B에 변경 사항에 대해 A 또한 변해야 된다는 것이다.
모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하고 쉽고 마이그레이션도 수월하다.
구현시 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기에 애플리케이션 의존성 방향이 일관되고 쉽게 추론할 수 있으며 모듈 간의 관계들이 명확해진다.
모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있으며 약간의 런타임 패널티가 생기기도 한다.
"상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 한다. 또한 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 않아야 한다" 라는 의존성 주입 원칙을 지키면서 만들어야 한다.
라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하면 된다.
자바스크립트에서는 new Object()
로 구현할 수 있다.
const num = new Object(42)
const str = new Object("abc")
num.constructor.name; // Number
str.constructor.name; // String
숫자를 전달하거나 문자열을 전달함에 따라 다른 타입의 객체를 생성한다. 즉 전달받은 값에 따라 다른 객체를 생성해 인스턴스의 타입 등을 정한다.
커피 팩토리 기반 코드
class Latte{
constructor(){
this.name = "latte"
}
}
class Espresso{
constructor(){
this.name = "Espresso"
}
}
class LatteFactory{
static createCoffee(){
return new Latte()
}
}
class EspressoFactory{
static createCoffee(){
return new Espresso()
}
}
const factoryList = {LatteFactory, LatteFactory}
class CoffeeFactory{
static createCoffee(type){
const factory = factoryList[type]
return factory.createCoffee()
}
}
const main = () => {
// 라떼 커피를 주문한다.
const coffee = CoffeeCactory.createCoffee("LatteFactory")
// 커피 이름을 부른다.
console.log(coffee.name) // latte
}
main()
CoffeeFactory
라는 상위 클래스가 중요한 뼈대를 결정하고 하위 클래스인 LatteFactory
가 구체적인 내용을 결정하고 있다.
이는 의존성 주입이라고 볼 수 있다. CoffeeFactory
에서 LatteFactory
의 인스턴스를 생성하는 것이 아닌 LatteFactory
에서 생성한 인스턴스를 CoffeeFactory
에 주입하고 있기 때문이다.
또한 CoffeeFactory
를 보면 static
으로 createCoffe()
정적 메서드를 정의했는데 정적 메서드를 쓰면 클래스의 인스턴스 없이 호출이 가능하며 메모리를 절약할 수 있고 개별 인스턴스에 묶이지 않으며 클래스 내의 함수를 정의할 수 있는 장점이 있다.
전략 패턴은 정책 패턴이라고도 하며, 객체의 행위를 수정시 직접 수정하지 않고 전략이라고 부르는 캡슐화한 알고리즘을 컨텍스트안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다.
컨텍스트
프로그래밍에서의 컨텍스트는 상황, 맥락, 문맥을 의미. 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말한다.
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
interface PaymentStrategy {
public void pay(int amount);
}
class KAKAOCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate){
this.name=nm;
this.cardNumber=ccNum;
this.cvv=cvv;
this.dateOfExpiry=expiryDate;
}
@Override
public void pay(int amount) {
System.out.println(amount +" paid using KAKAOCard.");
}
}
class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String email, String pwd){
this.emailId=email;
this.password=pwd;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using LUNACard.");
}
}
class Item {
private String name;
private int price;
public Item(String name, int cost){
this.name=name;
this.price=cost;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class ShoppingCart {
List<Item> items;
public ShoppingCart(){
this.items=new ArrayList<Item>();
}
public void addItem(Item item){
this.items.add(item);
}
public void removeItem(Item item){
this.items.remove(item);
}
public int calculateTotal(){
int sum = 0;
for(Item item : items){
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy paymentMethod){
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
public class HelloWorld{
public static void main(String []args){
ShoppingCart cart = new ShoppingCart();
Item A = new Item("kundolA",100);
Item B = new Item("kundolB",300);
cart.addItem(A);
cart.addItem(B);
// pay by LUNACard
cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
// pay by KAKAOBank
cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
}
}
/*
400 paid using LUNACard.
400 paid using KAKAOCard.
*/
이 코드는 쇼핑 카트에 아이템을 담아 LUNACard 또는 KAKAOCard 라는 두 개의 전략으로 결제하는 코드이다.
전략 패턴을 활용한 라이브러리로는 passport가 있다.
Node.js에서 인증 모듈을 구현할 때 쓰는 미들웨어 라이브러리이며 여러가지 전력을 기반으로 인증할 수 있게 한다.
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);
});
})
);
passport.use(new LocalStrategy( ... 처럼 passport.use()라는 메서드에 전략을 매개변수로 넣어서 로직을 수행하는 것을 볼 수 있다.
옵저버 패턴은 객체의 상태를 보고있는 관찰자가(주체) 객체의 상태 변화가 있을 때 마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다.
혹은 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 한다.
옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC 패턴에도 사용된다.
주체라고 볼 수 있는 model에서 변경사항이 생겨 update() 메서드로 옵저버인 뷰에게 알려주고 이를 기반으로 controller 등이 작동하는 것이다.
트위터의 팔로우한 사람이 글을 올리면 알림이 팔로워에게 가는게 사용 예이다.
자바스크립트에서의 옵저버 패턴은 프록시 객체를 통해 구현할 수도 있다.
프록시 객체
프록시 객체는 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체이다.
const handler = {
get: function(target, name){
return name === "name" ? `${taget.a} ${target.b}` : target[name]
}
}
const p = new Proxy({a: "KUNDOL", b: "IS AUMUMU ZANGIN"}, handler)
console.log(p.name) // KUNDOL IS AUMUMU ZANGIN
name이라는 속성에 접근할 때는 a, b를 합친 문자열을 만드는 것을 구현한 코드입니다.
p라는 변수에 name이라는 속성을 선언하지 않았는데도 p.name으로 name 속성에 접근할 때 그 부분을 가로채 문자열을 만드는 것을 볼 수 있습니다.
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;
}
프록시 함수를 통해 속성 접근을 가로채 형규라는 속성이 커플로 변경되는 것을 감시할 수 있다.
짜잔 앞에 설명한 프록시 객체는 사실 디자인 패턴 중 하나인 프록시 패턴이 녹아들어 있다.
프록시 패턴은 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 한다.
주로 객체의 속성, 변환 등을 보안해 보아느 데이터, 검증 캐싱, 로깅에 사용한다.
프록시 서버에서의 캐싱
캐시에 정보를 담아 두고 캐시 안의 정보를 다시 요청할 때 캐시 데이터를 활용하는 것이다. 이를 통해 불필요하게 외부와 연결하지 않아 트래픽을 줄일 수 있다.