ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 싱글턴 패턴(Singleton Pattern)이란?
    Design Pattern/생성 디자인 패턴 2024. 3. 30. 22:15
    반응형

    I. 싱글턴 패턴이란?

     싱글턴 패턴(Singleton Pattern)은 객체의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴입니다. 이 패턴은 전역 상태를 관리하거나, 공통된 리소스에 대한 일관된 접근 지점을 제공하는 등의 필요성 때문에 소프트웨어 개발에서 중요한 역할을 합니다. 예를 들어, 데이터베이스 연결이나 로깅 시스템과 같은 공유 리소스 관리에 있어서 중복 생성을 방지하고, 전역적으로 접근 가능한 단일 인스턴스가 필요할 때 유용하게 사용됩니다.

     

     싱글턴 패턴을 올바르게 구현하기 위해서는 몇 가지 중요한 조건을 충족시켜야 합니다:

     

    Private 생성자: 싱글턴의 생성자는 private 접근 제한자를 사용하여 외부에서 new 키워드를 통한 인스턴스 생성을 방지합니다.

      - Private 생성자를 사용하는 이유는 객체의 인스턴스가 외부에서 new 키워드를 통해 무분별하게 생성되는 것을 방지하기 위함입니다. 이는 클래스 내부의 getInstance() 메서드를 통해서만 인스턴스에 접근할 수 있도록 제한하여, 인스턴스가 단 하나만 존재함을 보장합니다.

     

    스레드 안정성(Thread Safety): 멀티스레드 환경에서도 인스턴스가 단 하나만 생성되도록 보장합니다.

      - 멀티스레드 환경에서는 여러 스레드가 동시에 싱글턴 인스턴스를 요청할 수 있습니다. 이때, 인스턴스가 단 하나만 생성되도록 보장하기 위해 동기화 메커니즘이 필요합니다. Java에서는 synchronized 키워드를, Go에서는 sync.Once를 사용하여 이를 해결합니다.

     

    지연 로딩(Lazy Loading): 인스턴스가 필요한 순간까지 생성을 지연시켜, 리소스를 효율적으로 사용합니다.

      - 지연 로딩은 인스턴스가 실제로 필요한 순간까지 생성을 지연시키는 기법입니다. 이는 리소스를 효율적으로 사용하고, 애플리케이션의 시작 시간을 단축시키는 데 도움을 줍니다. Lazy Holder 방식의 Java 구현 예시는 이 원칙을 잘 따르고 있습니다.

     

    getInstance() 메서드의 성능: 인스턴스 접근 메서드가 빠르고, 효율적으로 동작해야 합니다.

      - getInstance() 메서드의 성능은 애플리케이션 전반에 걸쳐 중요한 영향을 미칠 수 있습니다. 특히, synchronized 블록을 사용하는 경우, 불필요한 오버헤드를 방지하기 위해 이중 검사 락(Double-Checked Locking)이나 Lazy Holder 방식과 같은 최적화 방법이 사용됩니다.

     

    II. Java와 Go의 차이점

     Java와 Go 언어는 싱글턴 패턴을 구현하는 방식에 차이가 있습니다. Java는 synchronized 키워드와 클래스 로더 메커니즘을 활용한 Lazy Holder 방식을 통해 스레드 안전성과 지연 로딩을 구현합니다. 반면, Go는 sync.Once를 활용하여 보다 간결하고 효율적인 싱글턴 구현을 제공합니다. 이는 Go의 동시성 모델이 기본적으로 스레드 안전하고 간결한 코드 작성을 지향하기 때문입니다.

     

    III. 싱글턴 패턴 구현 예시 (자바)

    아래는 자바에서 위의 4가지 조건을 만족하는 싱글턴 패턴의 예시입니다. 이 구현은 "Double-Checked Locking" 방식과 "Lazy Holder" 방식을 사용하여 스레드 안전성과 지연 로딩, 그리고 getInstance() 메서드의 성능을 모두 보장합니다.

    Double-Checked Locking 싱글턴

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

     

    • Private 생성자: 직접 인스턴스를 생성할 수 없습니다.
    • 스레드 안정성: synchronized 블록과 이중 검사를 통해 멀티스레드 환경에서도 인스턴스가 단 하나만 생성됩니다.
    • 지연 로딩: instance가 처음 필요한 순간에만 생성됩니다.
    • 성능: 인스턴스가 이미 생성된 후에는 synchronized 블록을 거치지 않으므로 접근이 빠릅니다.

     

    Lazy Holder 방식 싱글턴

    public class Singleton {
        private Singleton() {}
    
        private static class LazyHolder {
            static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {
            return LazyHolder.INSTANCE;
        }
    }
    • Private 생성자: 외부에서 인스턴스를 직접 생성할 수 없습니다.
    • 스레드 안정성: JVM의 클래스 초기화 과정이 스레드 안전을 보장합니다.
    • 지연 로딩: Singleton 클래스가 로딩되어도 LazyHolder는 getInstance()가 호출될 때까지 로딩되지 않습니다.
    • 성능: getInstance() 메서드는 단순히 LazyHolder.INSTANCE를 반환하기만 하므로 매우 빠릅니다.

     두 방식 모두 싱글턴 패턴의 핵심 요구 사항을 만족시키며, 특정 상황과 요구에 따라 적절한 방식을 선택할 수 있습니다. "Lazy Holder" 방식은 일반적으로 가장 권장되는 방식 중 하나입니다.

     

     

    IV. 싱글턴 패턴 구현 예시 (Golang)

     Go 언어에서 싱글턴 패턴을 구현하는 가장 권장되는 방법은 sync.Once를 사용하는 것입니다. sync.Once는 Go의 표준 라이브러리에서 제공하며, 어떤 작업을 단 한 번만 실행하도록 보장합니다. 이는 멀티스레드 환경에서도 안전하게 싱글턴을 초기화할 수 있게 해줍니다. 이 방법이 Go에서 싱글턴을 구현하기 위한 베스트 케이스로 널리 인정받고 있습니다.

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    type singleton struct{}
    
    var instance *singleton
    var once sync.Once
    
    // GetInstance는 singleton 인스턴스를 반환합니다.
    // 이 함수는 프로그램 실행 동안 단 한 번만 singleton 인스턴스를 생성합니다.
    func GetInstance() *singleton {
        once.Do(func() {
            fmt.Println("Creating singleton instance...")
            instance = &singleton{}
        })
        return instance
    }
    
    func main() {
        s1 := GetInstance()
        s2 := GetInstance()
    
        if s1 == s2 {
            fmt.Println("s1 and s2 are the same instance")
        }
    }

     sync.Once는 내부적으로 동기화를 처리하기 때문에, 복잡한 동기화 메커니즘을 직접 구현할 필요가 없습니다. 또한, 지연 로딩(lazy loading)을 지원하므로, 인스턴스가 필요한 시점에 처음으로 초기화됩니다.

     

    다른 방법: 패키지 초기화 함수 사용

     Go의 패키지는 초기화 함수(init)를 가질 수 있으며, 이 함수는 패키지가 처음 로드될 때 자동으로 호출됩니다. 싱글턴 인스턴스를 패키지 레벨 변수로 선언하고, init 함수 내에서 초기화하는 방법으로 싱글턴 패턴을 구현할 수도 있습니다. 하지만, 이 방법은 지연 로딩을 지원하지 않으며, 패키지가 로드되는 시점에 바로 인스턴스가 생성됩니다.

    package singleton
    
    import "fmt"
    
    type singleton struct{}
    
    var instance = newSingleton()
    
    func newSingleton() *singleton {
        fmt.Println("Creating singleton instance...")
        return &singleton{}
    }
    
    func GetInstance() *singleton {
        return instance
    }

     이 방법은 프로그램 시작 시점에 바로 싱글턴 인스턴스를 생성하므로, 프로그램의 시작 시간이 중요하지 않은 경우에 적합할 수 있습니다. 하지만 일반적으로는 sync.Once를 사용하는 첫 번째 방법이 더 권장됩니다.

     Go 언어에서 싱글턴 패턴을 구현할 때는 sync.Once를 사용하는 것이 가장 권장되는 방법입니다. 이는 스레드 안전성, 지연 로딩, 그리고 구현의 간결함을 모두 제공합니다. 패키지 초기화 함수를 사용하는 방법도 있지만, 특정한 상황을 제외하고는 일반적으로 sync.Once를 사용하는 것이 더 나은 선택입니다.

     

     

    V. Lazy Holder 싱글턴 로거 구현 (자바)

    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    // 싱글턴 패턴을 적용한 Logger 클래스
    public class Logger {
        // LazyHolder 방식을 사용한 싱글턴 구현
        private Logger() {
        }
    
        private static class LazyHolder {
            static final Logger INSTANCE = new Logger();
        }
    
        public static Logger getInstance() {
            return LazyHolder.INSTANCE;
        }
    
        public void log(String message) {
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            System.out.println("[" + now.format(formatter) + "]: " + message);
        }
    }
    
    // 로깅을 사용하는 클라이언트 클래스
    public class Application {
        public static void main(String[] args) {
            Logger logger1 = Logger.getInstance();
            logger1.log("This is a log message from the main application.");
    
            // 싱글턴 패턴을 통해 얻은 인스턴스는 동일함을 보여주는 예제
            Logger logger2 = Logger.getInstance();
            logger2.log("This is another log message.");
    
            // logger1과 logger2는 동일한 인스턴스인지 확인
            if (logger1 == logger2) {
                System.out.println("Logger1 and Logger2 are the same instance.");
            }
        }
    }

     이 코드에서는 Logger 클래스를 싱글턴으로 구현하였고, LazyHolder 내부 클래스를 통해 지연 로딩과 스레드 안전성을 보장합니다. Logger 클래스의 인스턴스는 전체 애플리케이션에서 하나만 존재하며, getInstance() 메서드를 통해 접근할 수 있습니다. 로그 메시지를 출력하는 log 메서드는 현재 시간과 전달받은 메시지를 콘솔에 출력합니다.

     Application 클래스에서는 Logger의 싱글턴 인스턴스를 얻어 두 번의 로그 메시지를 출력하고, 두 logger1과 logger2 변수가 실제로 같은 인스턴스를 참조하는지 확인합니다.

     이 예제는 싱글턴 패턴이 실제 프로젝트에서 어떻게 활용될 수 있는지 보여주며, 로깅 시스템과 같은 공유 자원에 대한 접근을 관리하는 데 매우 유용함을 보여줍니다.

     

     

    VI. Go로 구현한 싱글턴 로거 예제

     Go 언어에서도 싱글턴 패턴을 구현할 수 있으며, "sync.Once"를 활용하는 것이 일반적인 방법입니다. sync.Once는 지정된 함수를 한 번만 실행하도록 보장합니다. 이를 통해, Go에서 싱글턴 인스턴스를 스레드 안전하게 초기화할 수 있습니다.

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    // Logger 구조체 정의
    type Logger struct{}
    
    // 싱글턴 인스턴스를 저장할 변수와 sync.Once 변수 선언
    var (
        loggerInstance *Logger
        once           sync.Once
    )
    
    // Logger 인스턴스를 반환하는 GetInstance 함수
    func GetInstance() *Logger {
        once.Do(func() {
            fmt.Println("Creating logger instance...")
            loggerInstance = &Logger{}
        })
        return loggerInstance
    }
    
    // Logger의 Log 메서드 정의
    func (l *Logger) Log(message string) {
        fmt.Printf("[%s]: %s\n", time.Now().Format("2006-01-02 15:04:05"), message)
    }
    
    // 메인 함수
    func main() {
        logger1 := GetInstance()
        logger1.Log("This is a log message from the main application.")
    
        // 다른 인스턴스를 가져오려고 시도하지만, 같은 인스턴스를 반환받음
        logger2 := GetInstance()
        logger2.Log("This is another log message.")
    
        // logger1과 logger2가 동일한 인스턴스인지 확인
        if logger1 == logger2 {
            fmt.Println("Logger1 and Logger2 are the same instance.")
        }
    }

     이 예제에서 GetInstance 함수는 sync.Once를 사용하여 loggerInstance가 단 한 번만 생성되도록 합니다. 이 방식은 멀티 스레드 환경에서도 안전하게 싱글턴 인스턴스를 초기화할 수 있도록 보장합니다. Log 메서드는 현재 시간과 함께 메시지를 출력합니다.

    main 함수에서는 GetInstance를 호출하여 로거 인스턴스를 얻고 로그 메시지를 출력합니다. 두 변수 logger1과 logger2는 동일한 싱글턴 인스턴스를 참조합니다. 이는 GetInstance 함수 호출 시 출력되는 메시지가 한 번만 나타나는 것으로도 확인할 수 있습니다.

    Go에서 싱글턴 패턴을 이용한 이 로깅 시스템 예제는 효율적인 리소스 사용과 전역 접근 제어가 필요한 다양한 상황에 적용될 수 있습니다. sync.Once를 사용하는 싱글턴 구현은 간단하면서도 효과적인 방법으로, Go의 동시성 기능과 잘 어울립니다.

     

     

    VII. 싱글턴 패턴의 단점 및 대안

     싱글턴 패턴은 많은 이점을 제공하지만, 과도한 사용은 전역 상태 의존성을 증가시키고, 코드의 복잡성을 높일 수 있습니다. 또한, 테스트하기 어렵고, 멀티스레드 환경에서의 동기화 문제를 야기할 수 있습니다. 대안으로, 의존성 주입(Dependency Injection) 같은 패턴을 사용하여 객체의 생성과 사용을 보다 유연하게 관리할 수 있습니다.

    이러한 개선 사항을 통해 글의 내용을 보강하고, 독자들이 싱글턴 패턴의 핵심 개념과 구현 방법, 그리고 주의해야 할 사항들을 보다 명확하게 이해할 수 있도록 도울 수 있습니다.

    반응형
Designed by Tistory.