Computer Science

Singleton pattern | 싱글톤패턴

연_우리 2022. 7. 29. 20:03
반응형

목차

     

     

     

    어떤 상황에서 쓰일까?

    시스템 런타임, 환경셋팅정보 등의 객체가 여러개 생성되면 어떻게 될까??

    여기저기서 각기 다른 셋팅정보를 가지고 있을 수 있게되고,

    동일한 설정값이라면 중복된 만큼 메모리를 차지하고 있게된다.

     

    셋팅정보의 인스턴스는 오직 하나만 존재해야한다. 이럴때 사용되는 것이 싱글톤 패턴이다.

     

     

     

    싱글톤 패턴이란?

    인스턴스를 오직 하나만 제공하는 클래스를 말한다

    하나의 인스턴스를 여러 곳에서 공유하여 사용한다. (공유하는 만큼 동시성 문제도 고려해야한다!)

     

     

    장점

    - new가 아닌 static한 인스턴스를 생성하여 고정된 메모리 영역을 가지게된다. 메모리 낭비를 방지할 수 있다

    - 싱글톤 인스턴스는 전역으로 만들어지기 때문에, 여러 곳에서 데이터를 공유할 수 있다.

    - 인스턴스의 2번째 접근부터는 객체 로딩시간이 줄어든다

     

     

    단점

    - 싱글톤 인스턴스가 너무 많은 역할을 가지고 있거나, 너무 많은 데이터를 공유하고 있을 때에는

      인스턴스들 간의 결합도가 높아져 OCP(개방 폐쇄 원칙)을 위배할 수 있다.

    - 멀티스레드 환경에서 동기화처리를 놓치면 인스턴스가 2개가 생성될 수 있다!!

    - 동시성 해결을 위해 필요한 코드가 많다.

     

     

     

    구현방법

     

    방법 1 : private 생성자에 static 메서드

    외부에서 new 키워드로 Settings 인스턴스를 만들 수 없도록 생성자를 private로 지정하고,

    여러 곳에서 Settings의 인스턴스를 가져가 사용할 수 있도록 getInstance()메서드를 static 으로 생성한다

    public class Settings {
        
        //private 생성자
        private Settings() {}
    
        //static 인스턴스
        private static Settings instance;
    
        //인스턴스 반환하는 static getter 메서드
        public static Settings getInstance(){
        	if(instance == null){
            	instance = new Settings();
            }
            return instance;
        }
        
    }
    --------------------------------------------------
    Settings setting1 = Settings.getInstance();
    Settings setting2 = Settings.getInstance();
    
    System.out.println(setting1 == setting2);
    //결과값 true

     

    방법1의 문제점

    웹애플리케이션의 경우, 멀티스레드 환경이 이루어진다.

    스레드A가 getInstance()의 if문을 통과하여 new Settings()를 통해 인스턴스를 생성하기 이전에

    스레드B가 getInstance()의 if문을 통과하여 new Settings()를 통해 인스턴스를 생성해야겠다! 라고하면

     

    결과적으로 스레드A와 스레드B가 가진 instance는 서로 다른 instance가 되어버린다.

    싱글톤은 무조건 instance가 1개만 존재해야한다.

    스레드A가 getInstance() 메서드에 접근하면 스레드B가 getInstance()에 접근하지 못하게 할 수 없을까?

     

     

     

    방법 2 : getInstance를 동기화(synchronized)처리한다.

    synchronized 키워드는 한번에 하나의 스레드만 접근을 허용하게된다.

    때문에 멀티스레드 환경에서 동시성 문제 없이 싱글톤을 보장할 수 있게된다.

    public class Settings {
        
        //private 생성자
        private Settings() {}
    
        //static 인스턴스
        private static Settings instance;
    
        //인스턴스 반환하는 static getter 메서드
        public static synchronized Settings getInstance(){ //🔥
        	if(instance == null){
            	instance = new Settings();
            }
            return instance;
        }
        
    }

     

     

     

    방법2의 문제점

    synchronized 키워드는 내부적으로 block/unblock 처리하여 하나의 스레드만 접근할 수 있게 하는데,

    이 block/unblock 작업의 비용이 비싼편이라 성능이슈가 발생할 가능성이 높다.

     

     

    멀티스레드환경에서 안전하게 최적화하는 방법은 없을까?

     

     

     

     

     

    방법 3 : 이른 초기화(eager initialization)을 사용한다

    사실 Settings가 하나밖에 없어야한다면 객체의 인스턴스를 미리 만들고,

    변경 불가능하게 final키워드를 붙여서도 해결할 수 있다.

     

    getInstance를 호출하는 시점에 객체 존재 여부를 확인하고, 생성하는 if문도 굳이 없어도 되는 것이다.

     

    또한 이미 만들어져있는 인스턴스를 반환만 해주면 되니 멀티스레드 환경에서도 안전하게 사용할 수 있다!

    public class Settings {
        
        //private 생성자
        private Settings() {}
    
        //static final 인스턴스
        private static final Settings instance = new Settings(); //🔥
    
        //인스턴스 반환하는 static getter 메서드
        public static Settings getInstance(){ //🔥
            return instance;
        }
        
    }

     

     

     

    방법3의 문제점

    Settings가 아주 무거운 객체이면 어떨까??

    애플리케이션 로딩 시점에 많은 자원을 할애하여 Settings의 인스턴스를 만들었는데

    사용되는 빈도가 그렇게 높지 않다면(쓰지 않는 경우도 포함) 이른 초기화가 오히려 독이 될 수 있다.

     

     

     

     

    방법 4 : 효율적인 synchronized block : double checked locking 

    방법 3번과 달리 인스턴스를 나중에 만들고 싶다하면

    방법 2번의 synchronized 방법을 사용하되, double checked locking방법을 사용하면 된다

     

    인스턴스 변수에 volatile 키워드를 사용하는데

    volatile은 여러 스레드에서 접근하는 변수의 값을 항상 최신값으로 읽게해준다.

    (메인 메모리 영역을 참조하므로 다른 스레드에서도 같은 메모리 주소를 참조하게됨)

    public class Settings {
        
        //private 생성자
        private Settings() {}
    
        //static volatile 인스턴스
        private static volatile Settings instance;
    
        //인스턴스 반환하는 static getter 메서드
        public static Settings getInstance(){ //🔥
        	if(instance == null){
                synchronized (Settings.class){
                    if(instance == null){
                        instance = new Settings();
                    }
                }
            }
            return instance;
        }
        
    }

    이 방법은 매번 synchronized 처리가 되지 않는다.

    드물게 동시에 접근한 멀티스레드의 경우에만 synchronized 처리가 일어나기 때문에

    방법 2에서 걱정하던 synchronized 키워드의 비용처리가 훨씬 줄어들었다.

     

    또한 인스턴스를 미리 만들어두는 것이 아닌, 호출하는 시점에서야 인스턴스가 만들어지기 때문에 

    방법 3에서 걱정하던 이른 초기화의 문제도 해결할 수 있게되었다.

     

     

     

    방법4의 문제점

    volatile 키워드를 왜 사용하는지, 전반적인 메모리에 대한 학습곡선이 있다

     

     

     

    방법 5 : static inner 클래스 사용하기 (가장 권장하는 방법❗)

    가장 권장하는 방법중에 하나이다.

    또한 가장 간단하게 Thread-safe한 싱글톤을 구현할 수 있는 코드이다.

     

    클래스의 로딩 및 초기화는 여러 스레드가 동시에 시도해도 오직 한번만 수행된다.

    = 싱글톤 보장

     

    SettingsHolder클래스를 static inner class로 선언했기 때문에

    SettingsHolder클래스는 SettingsHolder.instance로 호출되는 시점에 초기화가 일어난다.

    = lazy loading 보장

    public class Settings {
        
        //private 생성자
        private Settings() {}
    
        //private static inner class
        private static class SettingsHolder{    //🔥
            private static final Settings instance = new Settings();
        }
    
        //static getter
        public static Settings getInstance(){   //🔥
            return SettingsHolder.instance;
        }
        
    }

     

     

    🔥헷갈리지 말자!

    잠깐.. static은 jvm이 올라갈때 전부 초기화되는것이 아닌가?!

    로딩와 초기화의 차이가 있기때문이다.
    로딩 ? 클래스 파일을 바이트코드로 읽어와 메모리로 가져온다
    초기화 ? 슈퍼 클래스 및 정적필드를 초기화한다

    간단히 얘기하면 클래스 로딩순서는 아래와 같다 
    1. Settings(outer) 클래스 로딩&초기화
        SettingsHolder(inner) 클래스 로딩
    2. SettingsHolder.instace 호출하며 SettingsHolder(inner) 클래스 초기화O

    (참고 https://kdhyo98.tistory.com/70)

     

     

     

     

    싱글톤을 깨트리는 방법

    사실.. 외부에서 싱글톤을 유지할 수 있는 방법은 생성자를 private하게 선언하여 막는 방법 밖에는 없다.

    리플렉션, 직렬화&역직렬화 한다면 싱글톤을 깨트릴 수 있다!

     

    • 직렬화&역직렬화
      • 역직렬화는 Object readResolve() 를 사용해서 싱글톤이 깨지는 것을 막을 수 있다
    • 리플렉션
      • Enum을 사용하여 막을 수 있다
        • 자바 자체에서 Enum의 리플렉션을 막고 있기때문
        • 직렬화&역직렬화 해도 동일한 인스턴스로 간주한다.
        • 장점 & 단점 : 클래스 로딩 시 미리 만들어진다는 것!

     

     

     

     

     

     

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기