싱글톤(Singleton) 패턴

이창호·2022년 4월 2일
0

디자인패턴

목록 보기
4/4

Singleton Pattern?

  • instance를 오직 한개만 제공하는 class
  • system runtime, 환경 setting에 대한 정보 등, instance가 여러개 일 때 문제가 생길 수 있는 경우가 있다. ( IDE 툴의 UI, 환경 설정같은 건 하나만 존재해야함 )
  • 그래서 instance를 오직 한 개만 만들어 제공하는 class가 필요하다.

Static을 버무려서 간단하게 Singleton 만들기

	public class Tools {
    	private static Tools instnace;
        
    	private Tools(){}
        
        public satatic Tools getInstance() {
        	if (instance == null) {
            	instance = new Tools();
			}
            return instance;
		}
	}
    
    public class App {
    	public static void main(String[] args) {
        	Tools tools1 = Tools.getInstance();
            Tools tools2 = Tools.getInstance();
            
           	System.out.println(tools1 == tools2);
        }
	}
  • 위 코드는 Static method를 활용해 singleton을 구현했다.
  • 이런 static method는 이펙티브 자바 아이템1의 주제인 static factory method와도 연관이 있다.
  • 이것도 문제점이 하나 있다. 바로 thread safe하지 않단 것이다.
  • 만약 thread1이 instance가 null인지 체크한 뒤, 새로운 instance를 생성할 때, thread2도 null 체크를 한다면 instance는 두 개가 생성될 것이다!

Thread Safe한 Singleton 만들기

Synchronized

	public satatic synchronized Tools getInstance() {
		if (instance == null) {
			instance = new Tools();
		}
		return instance;
	}
  • synchronized keyword(예약어)를 사용하면 하나의 thread만 허용하게 된다.
  • 이 방법은 한 thread가 점유하고 있을 때, 다른 thread의 접근을 막는 과정에서 자원이 소모된다. ( 한마디로 lock(block)한다는 뜻이다. )

Eager Initialization

	public class Tools {
    	private static final Tools INSTANCE = new Tools();
        
    	private Tools(){}
        
        public satatic Tools getInstance() {
            return instance;
		}
	}
  • 미리 instance를 생성하는 방법을 사용합니다.
  • thread safe도 됩니다.
  • static field인 만큼 instance를 생성하지 않아도 사용할 수 있다.(memory 공간차지)
  • 미리 초기화 하기 때문에 instnace를 생성하는 과정에 자원 소모가 크다면 Application을 loading하는 과정이 느려질 것이다.

Double Checked Locking

	private static volatile Tools instnace;
    
	public satatic Tools getInstance() {
		if (instance == null) {
        	synchronized (Setting.class) {
            	if (instance == null) {
                	instance = new Setting();
                }
            }
		}
		return instance;
	}
  • 만약 instance가 null이라면 synchronized 키워드를 사용하여 thread safe한 상황에서 다시 instance가 null인지 다시 확인(double check) 한 후 instance를 생성하는 방법이다.
  • 처음부터 thread block을 유발하지 않아 자원소모를 방지할 수 있다.
  • instance를 생성하여 저장하는 field에 volatile이란 keyword를 붙여줘야 한다.
  • volatile은 multi thread 환경에서 변수의 값을 일치 시키기 위해 사용합니다. ( CPU Cache )

Static Inner

    priavet static class ToolsHolder {
    	private static final Tools INSTANCE = new Tools();
    }
    
	public satatic Tools getInstance() {
		return ToolsHolder.INSTANCE;
	}
  • class의 내부에 static class(inner class)를 만든 후 inner class에 instance를 생성하는 방법이다.
  • 이 방법은 thread safe도 된다.
  • getInstance가 호출될 때, inner class가 loading이 되고 instance를 생성하기 때문에 lazy loading도 가능하다.

Singleton 마구 부수기

	import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    public class App {
    	public static void main(String[] args) thorws NoSuchMethodException, InvocationTargetException, InstantiationException {
        	Tools tools1 = Tools.getInstance();
            
        	Constructor<Tools> constructor = Tools.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Tools tools2 = constructor.newInstance();
            
           	System.out.println(tools1 == tools2);
        }
	}
  • java의 reflection을 사용하면 Tools의 새로운 instance를 만들 수 있다.
	import java.io.Serializable;
    
	public class Tools implements Serializable {
    	private Tools() { }

    	private static class ToolsHolder {
        	private static final Tools INSTANCE = new Tools();
    	}

    	public static Tools getInstance() {
        	return ToolsHolder.INSTANCE;
    	}
	}
        
    public class App throws IOException {
    		public static void main(String[] args) {
        		Tools tools1 = Tools.getInstance();
        		Tools tools2 = null;

        		try (ObjectOutput output = new ObjectOutputStream(new FileOutputStream("tools.obj"))) {
            		output.writeObject(tools1);
        		}
                
				try (ObjectInput input = new ObjectInputStream(new FileInputStream("tools.obj"))) {
            		tools2 = (Tools) input.readObject();
        		}
                
                System.out.println(tools1 == tools2);
        	}
	}
  • java는 file을 저장할 땐 직렬화(serialization), file을 불러 올 땐 역직렬화(deserialization)를 한다.
  • 직렬화한 instance를 역직렬화하여 불러올 때는 생성자를 사용하여 instance를 다시 만들기 때문에 singleton이 깨진다.
	
	public class Tools implements Serializable {
    	...
        protected Object readResolve() {
        	return getInstance();
        }
    }
  • 다만, 역직렬화의 경우 readResolve란 method를 사용하기 때문에 이미 생성한 instance를 반환하도록 만들면 역직렬화를 막을 수 있다.

위 문제점들을 보완한 Singleton 방법

public enum Tools {
    INSTANCE;
}

public class App {
    public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException {
        Tools tools1 = Tools.INSTANCE;
        Tools tools2 = null;

        Constructor<?>[] declaredConstructors = Tools.class.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            declaredConstructor.setAccessible(true);
            tools2 = (Tools) declaredConstructor.newInstance("INSTANCE");
        }

        System.out.println(tools1 == tools2);
    }
}
  • enum을 사용하면 reflection도 막고 역직렬화도 막을 수 있습니다.
  • 실제로 위 코드를 실행시켜보면 enum object는 reflection을 할 수 없다는 오류가 발생합니다.
  • application을 loading 할 때, 생성된다는 점이 단점이다.
  • 상속을 쓰지 못하는 것도 단점이다.
public class App {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Tools tools1 = Tools.INSTANCE;
        Tools tools2 = null;

        try (ObjectOutput output = new ObjectOutputStream(new FileOutputStream("tools.obj"))) {
            output.writeObject(tools1);
        }

        try (ObjectInput input = new ObjectInputStream(new FileInputStream("tools.obj"))) {
            tools2 = (Tools) input.readObject();
        }

        System.out.println(tools1 == tools2);
    }
}
  • enum은 enum이란 class를 상속받고 있는데, 이미 직렬화를 구현하고 있어서 직렬화와 역직렬화에 안전하다.

실제로 어디서 쓰일까

  • spring bean의 scope 중에 하나로 singleton scope ( 실제로 singleton pattern은 아니다 )
public class SpringExample {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String example1 = applicationContext.getBean("example", String.class);
        String example2 = applicationContext.getBean("example", String.class);
        System.out.println(example1 == example2);
    }
}
  • java의 java.lang.Runtime
	Runtime runtime = Runtime.getRuntime();
  • 다른 design pattern(builder, facade, abstract factory, etc) 구현체의 일부로 쓰이기도 한다.

출처

profile
이타적인 기회주의자

0개의 댓글