[Design Pattern] 싱글톤 패턴(Singleton Pattern)

뚜비·2023년 3월 12일
0

Design Pattern

목록 보기
2/9

💻 싱글톤 구현 코드


싱글톤 패턴

  • 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴
  • 클래스의 인스턴스가 하나임을 보장하는 패턴
  • 생성자가 여러번 호출되도, 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환
  • GoF 디자인 패턴 중 생성 패턴에 해당

🤔언제 사용하나요?
1. 주로 공통된 객체를 여러 곳에서 참조해야 하는 경우
EX) 데이터베이스에서 커넥션풀, 스레드풀, 캐시, 로그 기록 객체
EX) 안드로이드 앱 : 각 액티비티 들이나, 클래스마다 주요 클래스들을 하나하나 전달하는게 번거롭기 때문에 싱글톤 클래스를 만들어 어디서든 접근하도록 설계

2. 인스턴스가 절대적으로 한 개만 존재하는 것을 보증하고 싶은 경우
EX) 컴퓨터 자체, 현재의 시스템 설정, 윈도우 시스템),


구조

  • singleton static 필드 : Singleton 클래스의 인스턴스에서 초기화, 즉 Singleton 클래스를 로드할 때 1회만 실행
  • Singleton() 생성자 : private으로 선언 즉, Singleton 클래스 외부에서 생성자 호출을 금지하기 위함.
  • getInstance() : Singleton 클래스의 유일한 인스턴스를 얻는 메소드, 즉 클래스 외부에서 new Singleton()을 하면 컴파일 에러 발생(생성자가 private이기 때문)
  • 코틀린에서는 싱글톤 패턴을 보장해주는 object 키워드가 존재한다.

장점

  • 하나의 인스턴스를 생성할 때 드는 비용이 줄어듬
    : 객체를 생성할 때마다 메모리 영역을 할당 받는데 한 번의 new를 통해 객체를 생성하여 메모리 낭비 방지
  • 유일하게 존재하는 인스턴스로의 접근을 통제
    : 인스턴스를 캡슐화하므로, 사용자의 접근을 제어
  • 전역변수보다 좋음
    : 싱글톤으로 구현한 인스턴스는 '전역'이므로, 다른 클래스의 인스턴스들이 데이터를 공유하는 것이 가능

단점

  • 의존성이 너무 높으며 모듈 간의 결합을 강하게 만들 수 있음
    : 만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이때 개방-폐쇄 원칙이 위배

의존성(=종속성)
A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A 또한 변해야 된다는 것을 의미

의존성 주입(DI, Dependency Injection)

  • 의존성 주입자(dependency injector)가 '직접' 다른 하위 모듈에 대한 의존성을 주고 메인 모듈이 '간접'적으로 의존성을 주입
  • 메인 모듈은 하위 모듈에 대한 의존성이 떨어짐 → 디커플링이 된다.
  • 원칙
    • 상위 모듈은 하위 모듈에서 어떠한 것도 가져오지 않아야 함
    • 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부 사항에 의존하지 말아야 함
  • TDD(Test Driven Development)에서 단위 테스트를 할 때 힘들다
    : 단위 테스트를 할 때 테스트는 서로 독립적이어야 함. 그러나 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 '독립적인' 인스턴스를 만들기 어려움

  • 멀티 스레드 환경에서 동기화 처리를 하지 않았을 때, 인스턴스가 2개가 생성되는 문제 -> 해결법



구현

  • SingletonPatternDemo : 걍 클라이언트 부분, 객체 생성 호출 부분
  • SingleObject : 생성자는 private로 두고, 정적 instance로 가짐

public class SingleObject {

   //create an object of SingleObject
   private static SingleObject instance = new SingleObject(); // 인스턴스 생성 

   //make the constructor private so that this class cannot be
   //instantiated -> 즉 외부에서 생성될 수 없음! 
   private SingleObject(){}  

   //Get the only object available
   public static SingleObject getInstance(){
      return instance; 
   }

   public void showMessage(){
      System.out.println("Hello World!");
   }
}

public class SingletonPatternDemo {
   public static void main(String[] args) {

      //illegal construct
      //Compile Time Error: The constructor SingleObject() is not visible
      //SingleObject object = new SingleObject();

      //Get the only object available
      SingleObject object = SingleObject.getInstance();

      //show the message
      // Hello World 
      object.showMessage();
   }
}

Lazy initialization

즉 클래스가 호출될 때가 아닌 정적 메서드(getInstance)가 처음 호출될 때 인스턴스가 생성된다.

public class SingleObject {
  private static SingleObject instance; 
  
  private SingleObject() {}
  
  public static SingleObject getInstance() {
    if(instance == null) { // 인스턴스가 생성되지 않았으면 생성! 
      instance = new SingleObject(); 
    }
    return instance;
  }
}

위의 코드는 멀티스레드 프로그램에서 여러 인스턴스가 생성되는 경합 조건이 발생할 수 있다.
다음 자바 예제는 스레드 세이프 구현으로, 이중 체크 잠금으로 느린 초기화를 사용한다

public class SingleObject {

    private static volatile SingleObject instance = null;

    private SingleObject() {}

    public static SingleObject getInstance() {
        if (instance == null) {
            synchronized(SingleObject.class) { // class 단위로 lock을 해준다.
                if (instance == null) {
                    instance = new SingleObject();
                }
            }
        }

        return instance;
    }
}


EXAMPLE

Java

class Singleton {
    private static Singleton instance;
    private String name;
    private int count = 0;

    private Singleton() {
        count++; // 다른 인스턴스가 생성되면 해당 count가 증가할 것임. 
        name = "This is the only reference : " + count;
    }

    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton(); // 인스턴스가 존재하지 않을 때만 인스턴스 생성 
        }
        return instance; // 기존의 인스턴스 return 
    }
    public String getName() {
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        Singleton singleton_reference1 = Singleton.getInstance();
        System.out.println("First Instance Name : "+singleton_reference1.getName());

        Singleton singleton_reference2 = Singleton.getInstance();
        System.out.println("Second Instance Name : "+singleton_reference2.getName());
    }
}

JavaScript

  • Javascript에서 1. 리터럴 {} 2. new Object로 객체를 생성할 때 이 자체만으로 싱글톤 패턴을 구현할 수 있다.

    🤔 Why?
    클래스 정의와 동시에 인스턴스가 자동으로 생성되기 때문
    자바스크립트에 대해서1
    {} vs new Object

const obj = {
    a: 27
}

console.log(obj === obj)
// true
// 객체 정의와 동시에 인스턴스로서 사용, 더 이상 인스턴스를 추가할 수 없음

  • Singleton.instance라는 하나의 인스턴스를 가지는 Singleton 클래스 구현
class Singleton {
    constructor() { // 생성자 
        if (!Singleton.instance) { // 인스턴스가 존재하지 않으면 
            Singleton.instance = this // 이 객체를 인스턴스로 지정 
        }
        return Singleton.instance // 존재하면 바로 인스턴스 return
    }
    getInstance() {
        return this 
    }
}
const a = new Singleton()
const b = new Singleton() 
console.log(a === b) // true 

데이터베이스 연결 모듈

싱글톤 패턴은 데이터베이스 연결 모듈에 많이 쓰인다. 이를 통해 데이터베이스 연결에 관한 인스턴스 생성 비용을 아낄 수 있다.

// DB 연결을 하는 것이기 때문에 비용이 더 높은 작업 
const URL = 'mongodb://localhost:27017/kundolapp' 
const createConnection = url => ({"url" : url})  // DB를 연결해주는 함수, 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
console.log(a.url) // mongodb://localhost:27017/kundolapp

Mongoose

  • 실제로 Node.js에서 MongoDB 데이터베이스를 연결할 때 쓰는 monogoose 모듈에서 싱글톤 패턴을 볼 수 있다.
  • 데이터베이스를 연결할 때 쓰는 connect()라는 함수는 싱글톤 인스턴스를 반환
// connect() 함수 구현 코드 
Mongoose.prototype.connect = function(uri, options, callback){
    const _mongoose = this instanceof Mongoose ? this : mongoose; // 인스턴스 존재하면 이거 없으면 mongoose 생성 
    const conn = _mongoose.connection; // 인스턴스의 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 데이터 베이스를 연결할 때도 싱글톤 패턴이 쓰인다.
  • 메인 모듈에서 데이터베이스 연결에 관한 인스턴스 정의 후 다른 모듈인 A 또는 B에서 해당 인스턴스를 기반으로 쿼리를 보내는 형식
// 메인 모듈 - DB 연결에 관한 인스턴스 정의, 객체 정의와 동시에 인스턴스 생성 
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);
});


😎 참고자료

면접을 위한 CS 전공지식 노트
Java 언어로 배우는 디자인 패턴 입문
GOF Design Pattern
디자인패턴 이해하기
Singleton pattern
Tech Interview for developer
[Design Pattern] GoF(Gang of Four) 디자인 패턴

profile
SW Engineer 꿈나무 / 자의식이 있는 컴퓨터

0개의 댓글