디자인 패턴(구)/생성 패턴

싱글톤(Singleton) 패턴

공대키메라 2022. 4. 3. 12:41

1. 싱글톤 패턴이란?

인스턴스를 오직 한개만 제공하는 클래스

 

This pattern involves a single class which is responsible to create an object while making sure that only single object gets created. This class provides a way to access its only object which can be accessed directly without need to instantiate the object of the class.

reference : https://www.tutorialspoint.com/design_pattern/singleton_pattern.htm

 

*인스턴스 : 객체 지향 프로그래밍(OOP)에서 클래스(class)에 소속된 개별적인 객체를 말한다

*클래스 : 객체를 정의하는 틀 또는 설계도

 

사용 시기

  • 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점으로 모든 사용자가 접근할 수 있도록 해야할 때
  • 유일한 인스턴스가 서브클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때

 

장점

  • 유일하게 존재하는 인스턴스로 접근 통제 : 캡슐화를 통해 언제 어떻게 접근하는지를 제어 가능
  • 이름 공간을 좁힘 : 후에 생길 디버깅 문제를 미연에 차단
  • 연산 및 표현의 정제 허용 : 상속이 가능하고 이를 상속한 서브클래스를 통해 새로운 인스턴스을 만들 있다.
  • 인스턴스 개수 변경 용이
  • 클래스 연산보다 훨씬 유연함

 

2. 구현 - 1 : 쌩기본(?) singleton

 

Singleton.java

package designpattern.designpattern.singleton;

public class Singleton {

    public static void main(String[] args) {

        //global 하게 하나의 instance를 공유해서 사용할 것임.
//        SingletonObject so = new SingletonObject(); <= 오류남. private 설정이 되어있어 생성 불가
        SingletonObject instance = SingletonObject.getInstance();
        System.out.println(instance == SingletonObject.getInstance());

    }

}

 

SingletonObject.java

package designpattern.designpattern.singleton;

public class SingletonObject {

    private static SingletonObject instance = null;

    //외부에서 인스턴스를 생성 못하게 private 설정
    private SingletonObject(){}

    public static SingletonObject getInstance() {

        //맨 처음 인스턴스가 없으면 새로 생성해서 static 변수로 할당
        if(instance == null){
            instance = new SingletonObject();
        }
        //존재하면
        return instance;
    }
}

 

3. 문제점

 

What will happen if two threads call getInstance() method at the same time?

In that case the second thread is going to create a new instance of Singleton
class and override the existing instance which has been created by the first thread. In order to improve that we are going to implement a synchronization block.

=> 두개의 쓰레드가 동시에 접근시에 객체를 따로 생성할 가능성이 있음.

=> 멀티스레드에서도 안전한 코드를 구현해야 함

 

3. 구현 - 2 : synchronize 이용

 

Setting2.java

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

/**
 * synchronized 사용해서 동기화 처리
 */
public class Settings2 {

    private static Settings2 instance;

    private Settings2() { }

    public static synchronized Settings2 getInstance() {
        if (instance == null) {
            instance = new Settings2();
        }

        return instance;
    }

}

 단점 : 성능저하

=>getInstance를 호출할 때 마다 동기화 처리 작업이 일어나서 성능이 저하될 수 있음

 

setting2.java - 수정 : 이른 초기화 사용하기

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

/**
 * 이른 초기화를 통한 싱글톤 패턴 구현
 */
public class Settings2 {

    private static final Settings2 INSTANCE = new Settings2();

    private Settings2() { }

    public static synchronized Settings2 getInstance() {
      return INSTANCE;
    }

}

=> thread safe함. 상수로 선언해서 미리 만들어둔것을 반환해주는 것이다. 

 

단점 : 미리 만든다는 것 자체가 단점이 될 수 있다. 

 

 

Settings3.java - double checked locking 사용하기

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

/**
 * double checked locking
 */
public class Settings3 {

    private static volatile Settings3 instance;

    private Settings3() { }

    public static Settings3 getInstance() {
        if (instance == null) {
            synchronized (Settings3.class) {
                if (instance == null) {
                    instance = new Settings3();
                }
            }
        }

        return instance;
    }

}

 

이렇게 하면 getInstance()를 호출 할 때 마다 동기화 작업이 생기는게 아니라 정말 관리하는 singleton 인스턴스가 null일 때만 동기화 작업이 이뤄진다.

 

단점 : 복잡함(volatile도 노이해...)

 

Settings4.java - static inner 클래스 사용하기

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

/**
 * static inner 클래스 홀더
 */
public class Settings4 {

    private Settings4() { }

    private static class Settings4Holder {
        private static final Settings4 INSTANCE = new Settings4();
    }

    public static Settings4 getInstance() {
        return Settings4Holder.INSTANCE;
    }

}

 

 

 getInstance()가 호출될 때 Settings4Holder가 호출되고, 그때에 static final 변수를 생성한다.

=> lazy loading이 가능하다. 

이전과 비교해 코드도 간단해진다. 

 

Settings5.java - enum 클래스 사용하기

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

/**
 * Enum을 사용해서 싱글톤 만들기
 */
public enum  Settings5 {

    INSTANCE;

	//내부에 property도 정의도 정의 가능
}

장점 : reflection에 안전하다.  직렬화, 역직렬화도 동일한 인스턴스로 가능함.

 

App.java

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

import java.io.*;

public class App {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Settings5 settings = Settings5.INSTANCE;

        Settings5 settings1 = null;
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
            out.writeObject(settings);
        }

        try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
            settings1 = (Settings5) in.readObject();
        }

        System.out.println(settings == settings1);
    }

}

 

스프링에서 지원하는 기능

 

SpringConfig.java

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public String hello() {
        return "hello";
    }

}

 

SpringExample.java

package me.whiteship.designpatterns._01_creational_patterns._01_singleton;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringExample {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String hello = applicationContext.getBean("hello", String.class);
        String hello2 = applicationContext.getBean("hello", String.class);
        System.out.println(hello == hello2);
        //결과 true
    }

}