본문 바로가기

CS/면접을 위한 CS 전공노트

[디자인 패턴] 싱글톤/팩토리/전략/옵저버

반응형

2023년 4월 7일 16p~43p

 

라이브러리 & 프레임워크

라이브러리와 프레임워크는 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것을 의미한다. 라이브러리는 폴더명, 파일명 등에 대한 규칙이 없고 프레임워크에 비해 자유롭지만 프임워크는 규칙이 있으며 라이브러리에 비해 좀 더 엄격하다.

 

1.1 디자인 패턴

디자인 패턴이란 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 규약 형태로 만들어 놓은 것이다.

 

1.1.1 싱글톤 패턴

싱글톤 패턴이란 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다. 하나의 클래스를 기반으로 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만드는 데 쓰이며, 보통 데이터베이스 연결 모듈에 많이 사용한다고 한다.

// DB 연결을 하는 것이기 때문에 비용이 더 높은 작업 
// DB.instance라는 하나의 인스턴스를 기반으로 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

장점으로는 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스 생성 비용이 줄어든다는 점이 있다. 단점은 의존성이 높아진다는 것이다.

자바스크립트에서는 리터럴 {} 또는 new Object로 객체를 생성하게 되면 다른 어떤 객체와도 같지 않기 때문에 이 자체만으로도 싱글톤 패턴을 구현할 수 있다.

const obj = {
    a: 27
}
const obj2 = {
    a: 27
}
console.log(obj === obj2)
// false

실제로 싱글톤 패턴은 Node.js에서 MongoDB 데이터베이스를 연결할 때 쓰는 모듈 및 MySQL 데이터베이스를 연결할 때 사용된다.

싱글톤 패턴의 단점?

싱글톤 패턴은 TDD 시 걸림돌이 된다. TDD를 할 때 주로 단위 테스트를 진행하고 이때 테스트는 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 하기 때문에 각 테스트마다 독립적인 인스턴스를 만들기가 어렵다.

의존성 주입?

싱글톤 패턴은 모듈 간의 결합이 강하다. 의존성 주입을 통해 모듈간의 결합을 조금 더 느슨하게 만들어 이를 해결할 수 있다. 의존성이란 종속성이라고도 불리며 a가 b에 의존성이 있다는 것은 b의 변경 사항에 대해 a 또한 변해야 된다는 것을 말한다.

메인 모듈이 직접 하위 모듈에 대한 의존성을 주기보다 중간에 의존성 주입자를 투입하여 메인 모듈이 간접적으로 의존성을 주입하는 방식을 사용한다. 이를 통해 메인 모듈은 하위 모듈에 대한 의존성이 떨어지게 되며 이를 디커플링 된다라고도 한다.

의존성 주입의 장점은 테스팅과 마이그레이션을 수월하게 한다는 것이다. 또 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어주기 때문에 애플리케이션 의존성 방향이 일관되고 애플리케이션을 쉽게 추론할 수 있으며 모듈 간의 관계들이 조금 더 명확해진다. 

단점은 모듈들이 더욱 분리되므로 클래스 수가 증가하여 복잡성이 늘고 약간의 런타입 페널티가 생기기도 한다.

의존성 주입 원칙은 다음과 같다.

1. 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않는다.
2. 둘 다 추상화에 의존한다.
3. 이때 추상화는 세부 사항에 의존하지 말아야 한다.

 

1.1.2 팩토리 패턴

팩토리 패턴은 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴이다. 상위 클래스와 하위 클래스가 느슨한 결합을 가지며 상위 클래스는 인스턴스 생성 방식에 대해 알 필요가 없기에 더 많은 유연성을 갖게된다. 또한 객체 생성 로직이 따로 떼어져 있어 리팩터링에 수월하고 유지 보수가 편하다.

자바스크립트에서 팩토리 패턴을 구현할 때는 간단하게 new Object()를 사용한다.

예시를 살펴보자.

const num = new Object(42);
const str = new Object("hi");
num.constructor.name; // Number
str.constructor.name; // String

위와 같이 전달 받은 값에 따라 다른 객체를 생성하며 인스턴스의 타입 등을 정한다.

class CoffeeFactory {
    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 = () => {
    // 라떼 커피를 주문한다.  
    const coffee = CoffeeFactory.createCoffee("LatteFactory")  
    // 커피 이름을 부른다.  
    console.log(coffee.name) // latte
}
main()

CoffeeFactory라는 상위 클래스가 뼈대가 되고 LatteFactory가 하위클래스가 되어 구체적인 내용을 결정한다. 참고로 CoffeeFactory에서 LatteFactory의 인스턴스를 생성하는 구조가 아닌 LatteFactory에서 생성한 인스턴스를 CoffeeFactory에게 주입하고 있기 때문에 이는 의존성 주입이라고도 볼 수 있다. 

또한, CoffeeFactory의 클래스에서 static 키워들르 사용해 createCoffee() 메서드를 정적 메서드로 선언하였는데, 이렇게 하면 클래스 기반으로 객체를 만들지 않고 해당 메서드를 호출할 수 있으며, 해당 메서드에 대한 메모리 할당을 한 번 만 할 수 있다.

 

1.1.3 전략 패턴

전략 패턴은 정책 패턴이라고도 하며 객체의 행위를 바꾸고 싶은 경우 이를 직접 수행하지 않고 전략이라고 부르는 캡슐화한 알고리즘을 컨텍스트(상황,맥락,문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말함) 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다. 예를 들어, 물건을 살 때 네이버 페이, 카카오 페이 등 다양한 방법을 통해 '전략'만 바꿔가며 결제하는 것과 같다. 

예시를 살펴보자.

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.
*/

위 코드는 쇼핑 카트에 아이템을 담아 루나카드 또는 카카오카드라는 두 개의 전략으로 결제하는 코드이다.

 

1.1.4 옵저버 패턴

옵저버 패턴은 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴이다. 예를 들어, 트위터에서 누군가가 새로운 트윗을 작성하면 그 사람을 팔로우 하는 사람들에게 알람이 가는 것처럼 말이다. 

여기서 주체란 객체의 상태 변화를 보고 있는 관찰자이며 옵저버들이란 이 객체의 상태 변화에 따라 전달되는 메서드 등을 기반으로 '추가 변화 사항'이 생기는 객체들이다. 이때, 주체와 객체를 따로 두지 않고 상태가 변경되는 객체를 기반으로 구축하기도 한다. 

옵저버 패턴은 주로 이벤트 기반 시스템에 사용하며 MVC(model-view-controller)패턴에도 사용된다. 예를 들어 주체라고 볼 수 있는 모델에 변경 사항이 생기면 update() 메서드를 통해 옵저버인 뷰에게 알려주고 이를 기반으로 컨트롤러 등이 작동하는 것이다.

예시를 살펴보자.

import java.util.ArrayList;
import java.util.List;

interface Subject {
    public void register(Observer obj);
    public void unregister(Observer obj);
    public void notifyObservers();
    public Object getUpdate(Observer obj);
}

interface Observer {
    public void update(); 
}

class Topic implements Subject {
    private List<Observer> observers;
    private String message; 

    public Topic() {
        this.observers = new ArrayList<>();
        this.message = "";
    }

    @Override
    public void register(Observer obj) {
        if (!observers.contains(obj)) observers.add(obj); 
    }

    @Override
    public void unregister(Observer obj) {
        observers.remove(obj); 
    }

    @Override
    public void notifyObservers() {   
        this.observers.forEach(Observer::update); 
    }

    @Override
    public Object getUpdate(Observer obj) {
        return this.message;
    } 
    
    public void postMessage(String msg) {
        System.out.println("Message sended to Topic: " + msg);
        this.message = msg; 
        notifyObservers();
    }
}

class TopicSubscriber implements Observer {
    private String name;
    private Subject topic;

    public TopicSubscriber(String name, Subject topic) {
        this.name = name;
        this.topic = topic;
    }

    @Override
    public void update() {
        String msg = (String) topic.getUpdate(this); 
        System.out.println(name + ":: got message >> " + msg); 
    } 
}

public class HelloWorld { 
    public static void main(String[] args) {
        Topic topic = new Topic(); 
        Observer a = new TopicSubscriber("a", topic);
        Observer b = new TopicSubscriber("b", topic);
        Observer c = new TopicSubscriber("c", topic);
        topic.register(a);
        topic.register(b);
        topic.register(c); 
   
        topic.postMessage("amumu is op champion!!"); 
    }
}
/*
Message sended to Topic: amumu is op champion!!
a:: got message >> amumu is op champion!!
b:: got message >> amumu is op champion!!
c:: got message >> amumu is op champion!!
*/

위 코드는 topic을 기반으로 옵저버 패턴을 구현했다. 여기서 topic은 주체이자 객체가 된다. class Topic implements Subject를 통해 Subject interface를 구현했고 옵저버를 선언할 때 해당 이름과 어떤 토픽의 옵저버가 될지를 정한다.

자바에서의 상속과 구현의 차이
상속 : 자식 클래스가 부모 클래스의 메서드 등을 상속받아 사용하며 자식 클래스에서 추가,확장 하는 것 -> 재사용성, 중복성 최소화 가능(일반 클래스, abstract 클래스 기반으로 구현)
구현 : 부모 인터페이스를 자식 클래스에서 재정의하여 구현하는 것. 반드시 부모 클래스의 메서드를 재정의해야 함(인터페이스 기반으로 구현)

자바스크립트에서의 옵저버 패턴은 프록시 객체를 통해 구현할 수 있다.

프록시 객체?

프록시 객체는 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순회, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 뜻하며, target(프록시할 대상), handler(target 동작을 가로채고 어떤 동작을 할 것인지가 설정되어 있는 함수) 이 두 가지 파라미터를 가진다. 

정리하자면 프록시 객체란 특정 속성에 접근할 때 그 부분을 가로채서 어떠한 로직을 강제할 수 있도록 하는 것이다.

예시를 살펴보자.

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 
} 
const a = {
    "형규" : "솔로"
} 
const b = createReactiveObject(a, console.log)
b.형규 = "솔로"
b.형규 = "커플"
// 형규가 [솔로] >> [커플] 로 변경되었습니다

프록시 객체의 get() 함수는 속성과 함수에 대한 접근을, has() 함수는 in연산자의 사용을, set() 함수는 속성에 대한 접근을 가로챈다.

반응형