본문 바로가기
제로베이스 BE 21기/면접을 위한 CS 전공지식 노트

[CS] Ch.1 디자인 패턴과 프로그래밍 패러다임 : 싱글톤 패턴, 팩토리 패턴, 전략 패턴

by ♡˖GYURI˖♡ 2024. 5. 9.

1.1 디자인 패턴

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

 

1.1.1 싱글톤 패턴

하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴으로, 보통 데이터베이스 연결 모듈에 많이 사용된다.

 

장점 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어듦
단점 의존성이 높아짐

 

 

자바스크립트의 싱글톤 패턴

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

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

 

위 코드에서 obj와 obj2는 다른 인스턴스를 가진다.

 

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

 

위 코드는 Singleton.instance라는 하나의 인스턴스를 가지는 Singleton 클래스를 구현한 것이다. 이를 통해 a와 b는 하나의 인스턴스를 가진다.

 

 

데이터베이스 연결 모듈

// DB 연결을 하는 것이기 때문에 비용이 더 높은 작업 
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를 생성한다. 이를 통해 데이터베이스 연결에 관한 인스턴스 생성 비용을 아낄 수 있다.

 

 

자바에서의 싱글톤 패턴

class Singleton {
    private static class singleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return singleInstanceHolder.INSTANCE;
    }
}

public class HelloWorld{ 
     public static void main(String []args){ 
        Singleton a = Singleton.getInstance(); 
        Singleton b = Singleton.getInstance(); 
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());  
        if (a == b){
         System.out.println(true); 
        } 
     }
}
/*
705927765
705927765
true

1. 클래스안에 클래스(Holder), static이며 중첩된 클래스인 singleInstanceHolder를 
기반으로 객체를 선언했기 때문에 한 번만 로드되므로 싱글톤 클래스의 인스턴스는 애플리케이션 당 하나만 존재하며 
클래스가 두 번 로드되지 않기 때문에 두 스레드가 동일한 JVM에서 2개의 인스턴스를 생성할 수 없습니다. 
그렇기 때문에 동기화, 즉 synchronized를 신경쓰지 않아도 됩니다. 
2. final 키워드를 통해서 read only 즉, 다시 값이 할당되지 않도록 했습니다.
3. 중첩클래스 Holder로 만들었기 때문에 싱글톤 클래스가 로드될 때 클래스가 메모리에 로드되지 않고 
어떠한 모듈에서 getInstance()메서드가 호출할 때 싱글톤 객체를 최초로 생성 및 리턴하게 됩니다. 
*/

 

 

mongoose의 싱글톤 패턴

실제로 싱글톤 패턴은 Node.js에서 MongoDB 데이터베이스를 연결할 때 쓰는 mongoose 모듈에서 볼 수 있다.

 

mongoose의데이터베이스를 연결할 때 쓰는 connect()라는 함수는 싱글톤 인스턴스를 반환한다. 다음은 connect() 함수를 구현할 때 쓰인 실제 코드이다.

Mongoose.prototype.connect = function(uri, options, callback) {
    const_mongoose = this instanceof Mongoose ? this : mongoose;
    const conn = _mongoose.connection;
    
    return mongoose._promiseOrCallback(callback, cb => {
    	conn.openUri(uri, options, err => {
        	if (err != null) {
            	return cb(err);
            }
            return cb(null, _mongoose);
        });
    });
});

 

 

MySQL의 싱글톤 패턴

Node.js에서 MySQL 데이터베이스를 연결할 때도 싱글톤 패턴이 쓰인다.

// 메인 모듈
const mysql = require('mysql');
const pool = mysql.createPool({
	connectionLimit: 10,
    host: 'example.org',
    user: 'kundol',
    password: 'secret',
    database: '승철이디비'
});
pool.connect();

// 모듈 A
pool.query(query, function (error, results, fields) {
	if (error) throw error;
    console.log('The solution is: ', results[0].solution);
});

// 모듈 B
pool.query(query, function (error, results, fields) {
	if (error) throw error;
    console.log('The solution is: ', results[0].solution);
});

 

위 코드처럼 메인 모듈에서 데이터베이스 연결에 관한 인스턴스를 정의하고 다른 모듈인 A 또는 B에서 해당 인스터스를 기반으로 쿼리를 보내는 형식으로 쓰인다.

 

 

싱글톤 패턴의 단점

  • TDD의 어려움
    • TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어느 순서로든 실행할 수 있어야 한다.
    • 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이기에 각 테스트마다 '독립적인' 인스턴스를 만들기 어렵다.
  • (추가) private 생성자 때문에 상속이 어려움
    • 싱글톤 패턴은 기본 생성자를 private로 만들기 때문에 상속을 통한 자식 클래스를 만들 수 없다는 문제점이 있다
    • 즉, 자바의 객체지향 언어로서의 장점 중 하나인 다형성을 적용하지 못한다.
  • 모듈 간의 결합도 증가
    • 의존성 주입을 통해 해결 가능

 

의존성 주입이란?

위의 그림처럼 메인 모듈이 '직접' 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자가 이 부분을 가로채 메인 모듈이 '간접'적으로 의존성을 주입하는 방식이다.

→ 이를 통해 메인 모듈은 하위 모듈에 대한 의존성이 떨어지게 된다.

 

 

의존성 주입의 장점

  • 모듈을 쉽게 교체할 수 있는 구조
    • 테스팅하기 쉬움
    • 마이그레이션하기도 수월함
  • 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어줌
    • 애플리케이션 의존성 방향이 일관됨
    • 애플리케이션을 쉽게 추론할 수 있음
    • 모듈 간 관계가 명확해짐

 

의존성 주입의 단점

  • 모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있음
  • 약간의 런타임 페널티 발생

 

1.1.2 팩토리 패턴

객체를 사용하는 콛에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴

 

특징

  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가짐
  • 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 가짐
  • 객체 생성 로직이 따로 떼어져 있기 때문에 코드를 리팩터링하더라도 한 곳만 고칠 수 있게 되어 유지 보수성이 증가됨

 

 

자바스크립트의 팩토리 패턴

간단하게 new Object()로 구현할 수 있다.

const num = new Object(42)
const str = new Object('abc')
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() 정적 메서드를 정의하였는데, 정적 메서드를 사용하면 클래스의 인스턴스 없이 호출이 가능하여 메모리를 절약할 수 있고 개별 인스턴스에 묶이지 않으며 클래스 내의 함수를 정의할 수 있는 장점이 있다.

 

 

자바의 팩토리 패턴

위 코드를 자바로 구현하면 다음과 같다.

enum CoffeeType {
    LATTE,
    ESPRESSO
}

abstract class Coffee {
    protected String name;

    public String getName() {
        return name;
    }
}

class Latte extends Coffee {
    public Latte() {
        name = "latte";
    }
}

class Espresso extends Coffee {
    public Espresso() {
        name = "Espresso";
    }
}

class CoffeeFactory {
    public static Coffee createCoffee(CoffeeType type) {
        switch (type) {
            case LATTE:
                return new Latte();
            case ESPRESSO:
                return new Espresso();
            default:
                throw new IllegalArgumentException("Invalid coffee type: " + type);
        }
    }
}

public class Main {
    public static void main(String[] args) { 
        Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE); 
        System.out.println(coffee.getName()); // latte
    }
}

 

if ("Latte".equalsIgnoreCase(type))을 통해 문자열 비교 기반으로 로직이 구성되어 있는데, 이는 Enum 또는 Map을 이용하여 구현할 수도 있다.

 

 

1.1.3 전략 패턴

객체의 행위를 바꾸고 싶은 경우 '직접' 수정하지 않고 전략이라고 부르는 '캡슐화한 알고리즘'을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴

 

아래는 결제 시 네이버페이, 카카오페이 등 다양한 방법으로 결제하듯이 어떤 아이템을 살 때 LUNACard로 사는 것과 KAKAOCard로 사는 것을 구현한 예제이다. 결제 방식의 '전략'만 바꿔서 두 가지 결제 방식을 구현하였다.

 

 

자바의 전략 패턴

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의 전략 패턴

passport는 Node.js에서 인증 모듈을 구현할 때 쓰는 미들웨어 라이브러리로, 여러 가지 '전략'을 기반으로 인증할 수 있게 한다. 서비스 내의 회원가입된 아이디와 비밀번호를 기반으로 인증하는 LocalStrategy 전략과 페이스북, 네이버 등 다른 서비스를 기반으로 인증하는 OAuth 전략 등을 지원한다.

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()라는 메서드에 '전략'을 매개변수로 넣어서 로직을 수행하는 것을 볼 수 있다.

 

 

 

etc

컨텍스트
프로그래밍에서의 컨텍스트는 상황, 맥락, 문맥을 의미하며 개발자가 어떠한 작업을 완료하는 데 필요한 모든 관련 정보를 말한다.
 

[Java] static은 언제, 어떻게 사용해야 할까?

자바 공부를 하면서 static이 무엇인지는 배웠는데... 도대체 어떤 상황에서는 꼭 사용해야 하고, 어떤 상황에서는 사용하지 말아야 하는지가 궁금해졌다. static static은 고정된, 정적인 이라는 의

newbie-in-softengineering.tistory.com

 

[스터디] 의존관계 주입(DI : Dependency Injection)

최근 김영한님의 Spring 핵심 원리 - 기본편 강의를 듣고 있는데 DI라는 단어가 자주 나왔다. 도대체 DI가 정확히 뭘까? 이번 기회에 한 번 알아보겠다. 의존관계란? "A가 B를 의존한다."라는 표현은

newbie-in-softengineering.tistory.com

프레임워크
뼈대나 기반구조를 뜻하고, 제어의 역전 개념이 적용된 대표적인 기술이다.
소프트웨어에서의 프레임워크는 '소프트웨어의 특정 문제를 해결하기 위해서 상호 협력하는 클래스와 인터페이스의 집합'이라 할 수 있으며, 완성된 애플리케이션이 아닌, 프로그래머가 완성시키는 작업을 해야한다.
객체 지향 개발을 하게 되면서 통합성, 일관성의 부족이 발생되는 문제를 해결할 방법 중 하나라 할 수 있다.

특징
- 특정 개념들의 추상화를 제공하는 여러 클래스나 컴포넌트로 구성되어 있다.

- 추상적인 개념들이 문제를 해결하기 위해 같이 작업하는 방법을 정의한다.
- 컴포넌트들은 재사용이 가능하다.
- 높은 수준에서 패턴들을 조작화 할 수 있다.
라이브러리
단순 활용가능한 도구들의 집합을 말한다.

즉, 개발자가 만든 클래스에서 호출하여 사용, 클래스들의 나열로 필요한 클래스를 불러서 사용하는 방식을 취하고 있다.
프레임워크와 라이브러리의 차이점
제어 흐름에 대한 주도성이 누구에게/어디에 있는가에 따라 다르다.

즉, 애플리케이션의 Flow(흐름)을 누가 쥐고 있느냐에 달려있다.

프레임워크는 전체적인 흐름을 스스로 쥐고 있으며 사용자는 그 안에서 필요한 코드를 짜 넣는다.
반면 라이브러리는 사용자가 전체적인 흐름을 만들며 라이브러리를 가져다 쓰는 것이다.

다시 말해, 라이브러리는 라이브러리를 가져다가 사용하고 호출하는 측(사용자)에 전적으로 주도성이 있으며 프레임워크는 그 틀 안에 이미 제어 흐름에 대한 주도성이 내재(내포)하고 있다.

프레임워크는 사용자가 가져다가 사용한다기보다는 거기에 들어가서 사용한다는 느낌으로 접근할 수 있다.
 

[스터디] IoC : 제어의 역전

저번 스터디에서 DI에 대해 발표했었다. 그것과 연결하여 이번에는 IoC에 대해 알아보았다. [스터디] 의존관계 주입(DI : Dependency Injection) 최근 김영한님의 Spring 핵심 원리 - 기본편 강의를 듣고 있

newbie-in-softengineering.tistory.com

 

 

 

 


참고

 

싱글톤 패턴의 사용 이유와 문제점

싱글톤 패턴은 특정 클래스의 인스턴스를 1개만 생성되는 것을 보장하는 디자인 패턴이다. 즉, 생성자를 통해서 여러 번 호출이 되더라도 인스턴스를 새로 생성하지 않고 최초 호출 시에 만들어

velog.io

 

 

프레임워크와 라이브러리의 차이점

Framework Vs Library 프레임워크와 라이브러리의 정확한 차이점은 무엇일까요? 대중 알것 같지만 정확히 어떠한 차이점이 있는지 모르고 있는 경우가 많을지도 모릅니다. 프레임워크는 단지 미리 만

webclub.tistory.com