본문 바로가기
Programming/Languages (Java, etc)

싱글톤 패턴 (Singleton pattern)

by kghworks 2022. 5. 10.

싱글톤패턴의 실생활 예시

목차

  • 싱글톤 패턴의 이해
  • 구현 (JAVA)

 

 디자인 패턴에서 자주 사용되는 싱글톤 패턴에 대한 필요성을 이해하고, 구현 방안의 종류를 알아봅니다.


싱글톤 패턴의 이해

 

 싱글톤 패턴은 애플리케이션에서 불변할 객체에 대하여 한번만 초기화 (할당)해놓고 가져다 쓰도록 하는 디자인 패턴입니다.

 

 실생활에서 예시를 들어보겠습니다. 가족 구성원이 4명이고 4명 모두 운전자라고 가정합시다. 일반적으로 가족 구성원 (사용자)만큼 자동차 (객체)를 구입하나요?

  자동차 (객체)를 사용하기 위한 비용 (메모리)이 굉장히 많이 듭니다. 따라서 자동차를 한대만 구입하여 구성원이 공유하여 사용합니다.

 이와 같은 원리를 애플리케이션 단에 적용하는 디자인 패턴을 싱글톤 패턴 (Singleton Pattern)이라 합니다. 애플리케이션 런타임 중 불변하는 단일 객체를 여러 사용자가 사용하는 경우에 아주 효율적인 패턴이 될 수 있습니다. 메모리 단에 하나의 객체만 할당해두기 때문에 메모리 공간도 효율적입니다. 실제로 구현해보겠습니다.

 


구현 (JAVA)

 

 싱글톤 패턴도 어느 시점에 데이터를 메모리에 초기화 할것인지, 어떤 방법으로 초기화할 것인지 그 종류가 다양합니다. 

 

Eager initialization (이른 초기화 방식)

package main.java.com.study;

public class SingleTonEager {

    //private static으로 선언
    private static SingleTonEager instance = new SingleTonEager();

    /*
     * 생성자를 private 으로 선언
     * */
    private SingleTonEager() {

        // SingleTonEager 객체 초기화 작업 (생략)

    }

    /*
     * 외부에서 호출 시에는 SingleTon01.getInstance(); 로 호출
     * */
    public static SingleTonEager getInstance() {
        return instance;
    }
}

 

객체 초기화 시점 : 클래스 로더가 클래스 로딩 시

 

 싱글톤 패턴의 가장 기본적인 형태입니다. 전역변수로 class 내에서 선언과 동시에 초기화하면서 메모리에 올립니다. Thread-Safe 합니다. 그러나 객체 사용 유무와 상관없이 클래스 로딩 시 메모리에 객체를 초기화하기 때문에 사용하지 않는 객체가 메모리에 올라가 있을 수 있다는 단점이 있습니다.

 

Lazy initialization (늦은 초기화 방식)

package main.java.com.study;

public class SingleTon {

    private static SingleTon instance;

    private SingleTon() {
    }

    public static SingleTon getInstance() {

        //객체가 null 이라면 객체 생성
        if (instance == null) {
            instance = new SingleTon();
        }

        return instance;
    }
}

객체 초기화 시점 : 클래스 객체 사용 시

 

 

 이른 초기화의 단점을 보완했기 때문에 메모리 누수를 방지합니다. 그러나 multi-thread 환경에서 여러 쓰레드(사용자)로부터 동시에 getInstance() 가 호출된다면 어떻게 될까요? 각 스레드가 null로 판단하여 객체를 메모리에 할당하는 경우가 생깁니다!

 

Thread safe Lazy initialization (스레드 안전한 늦은 초기화)

package main.java.com.study;

public class SingleTon {

    private static SingleTon instance;
    
    private SingleTon() {
    }

    /*
     * synchronized 키워드를 사용하여 동시성 문제 해결
     * */
    public static synchronized SingleTon getInstance() {

        //객체가 null 이라면 최초 할당
        if (instance == null) {
            instance = new SingleTon();
        }

        return instance;
    }
}

 

 

 synchronized 키워드를 사용하여 thread-safe하도록 하였습니다. 즉 객체가 2개 초기화될 일은 없습니다. 그러나 수많은 스레드들이 getInstance()를 호출하게 되면 매번 synchronized 키워드로 인한 lock, unlock이 발생하며 내부적인 성능 저하를 유발할 수 있습니다.

 

synchronized : java multi-thread 환경에서 하나의 자원에 여러 쓰레드가 접근하려 할 때 현재 사용 중이 스레드를 제외하고 접근을 막는 키워드. thread-safe를 위함

 

 

Thread safe Lazy initialization + Double-checked locking 기법

package main.java.com.study;

public class SingleTon {

    private static SingleTon instance;

    private SingleTon() {
    }

   
    public static synchronized SingleTon getInstance() {

        //객체가 null 이라면 최초 할당
        if (instance == null) {

            // synchronized 키워드를 사용하여 동시성 문제 해결
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }

        }

        return instance;
    }
}

 

 

 getInstance() 호출 시 객체가 NULL인 경우에만 synchronized 키워드를 이용하여 객체를 할당함으로써 thread-safe와 성능 저하 방지를 동시에 해결합니다.

 

Initialization on demand holder idiom (holder에 의한 초기화) 추천방법

package main.java.com.study;

public class SingleTon {

    private SingleTon() {
    }

    //내부클래스 LazyHolder 선언
    private static class LazyHolder {
        
        /*
        * LazyHolder 클래스 로드 시에 객체 할당
        * */
        private static final SingleTon instance = new SingleTon();
    }

    public static SingleTon getInstance() {

        //외부에서 호출 시 LazyHolder 내부클래스의 객체 리턴
        return LazyHolder.instance;
    }
}

 객체 초기화 시점 : 최초 getInstance() 호출 시 (=최초 LazyHolder 클래스 로드 시)

 

 내부 클래스를 이용하여 getInstance()가 최초 호출되는 시점이 LazyHodler 클래스의 최초 로딩 시점과 같다는 것을 이용한 방법입니다. 결과적으로 getInstance()가 최초로 호출될 시에만 객체가 생성됩니다. (getInstance() 최초 호출 시점 = LazyHodler 클래스 로드 시점 = 객체 최초 할당 시점)

 

 현재 가장 널리 사용 중인 방법입니다.

댓글