[디자인패턴] 싱글톤 패턴(Singleton Pattern)이란?

싱글톤 패턴(Singleton Pattern)이란?

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase
Connection Pool)와 같은 상황에서 많이 사용된다. - 위키백과 -

 

 

 

싱글톤 패턴은 쉽게 말해 생성 할 수 있는 객체의 인스턴스를 하나로 제한하는 패턴이다. 싱글톤 패턴을 따르는 클래스는 아무리 생성자를 여러 번 호출하더라도 최초의 생성으로 생겨난 인스턴스가 반환된다.

 

예시를 통해 이해해 보자.

 

싱글톤 패턴 예시

public class Singleton {
    // static 선언
    private static Singleton instance;

    // private 생성자로 외부의 객체 생성 방지
    private Singleton() {
    }

    public static Singleton getInstance() {
        // instance 가 null 일 경우, 즉 최초 생성 한 번 보장 
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

싱글톤 패턴을 따르는 클래스의 구조는 위와 같다.

 

해당 클래스의 인스턴스를 static으로 선언해 해당 객체의 인스턴스가 필요할 때 반환하도록 한다.

또한, 생성자를 private으로 설정해 외부에서 임의로 객체의 인스턴스를 생성하지 못하도록 한다.

 

객체의 인스턴스가 필요할 때는 getInstance()와 같은 메서드로 인스턴스를 반환받도록 한다.

 

 

싱글톤 패턴의 장점

싱글톤 패턴을 사용하면 아래와 같은 장점들이 존재한다.

 

  • 객체를 고정된 메모리 영역에 하나만 생성하므로 메모리를 절약할 수 있다. 또한 이미 생성된 인스턴스를 사용함으로써 성능에 도움이 될 수 있다.
  • 싱글톤 패턴을 따르는 클래스의 인스턴스는 전역이기 때문에 데이터 공유가 쉽다.
  • 인스턴스가 하나여야만 하는 경우 유용하게 사용할 수 있다.
  • 인스턴스를 여러 부분에서 공유해야 하는 경우 유용하다.

 

싱글톤 패턴의 단점

싱글톤 패턴을 사용하면 아래와 같은 단점들이 존재한다.

 

  • 싱글톤 패턴을 구현하는데 코드가 많이 필요해 구현이 복잡해질 수 있다.
  • 자원을 공유하는 싱글톤 패턴의 특성상 테스트가 어렵다. 이곳저곳에서 공유해서 사용하기 때문에 추적이 어렵다.
  • 자식 클래스를 만들기 쉽지 않다.
  • 구체 클래스에 의존하기 때문에 SOLID 원칙을 위반할 가능성이 높다.

 

참고)

[프로그래밍 언어론] 객체 지향 설계 5원칙 (SOLID)란?

 

[프로그래밍 언어론] 객체 지향 설계 5원칙 (SOLID)란?

객체 지향 설계 5원칙 객체 지향 설계에는 다음과 같은 5가지 원칙이 있다. 1. SRP (Single Responsibility Principle) : 단일 책임 원칙 2. OCP (Open-Closed Principle) : 개방 폐쇄 원칙 3. LSP (Liskov Substitution Principle)

code-lab1.tistory.com

 

 

또한, 싱글톤 패턴은 동기화 문제가 발생할 수 있다.

 

 

싱글톤 패턴의 동기화 문제 및 해결법

public class Singleton {
    // static 선언
    private static Singleton instance;
    private static String name;
    
    // private 생성자로 외부의 객체 생성 방지
    private Singleton(String name) {
    	this.name = name;
    }

    public static Singleton getInstance(String name) {
        // instance 가 null 일 경우, 즉 최초 생성 한 번 보장 
        if (instance == null) {
            instance = new Singleton(name);
        }
        return instance;
    }
    
    public String getName(){
    	return name;
    }
}

public class Main{
    static int num = 0;
    
    public static void main(String[] args){
    
        Runnable task = () -> {
            try{
                num++;
                Singleton singleton = Singleton.getInstance(num +"번");
                System.out.println(singleton.getName() +" 인스턴스");
            } catch(Exception e){
            }
        };

        for(int i=0; i<5; i++){
            Thread t = new Thread(task);
            t.start();
        }
   }
}

 

위와 같은 멀티스레드 프로그램을 실행한다고 하자.

 

싱글턴 패턴을 적용했기 때문에 Singleton 객체는 최초 num = 0 일 때만 한 번 생성되어서 결과는 아래와 같아야 할 것이다.

 

0번 인스턴스
0번 인스턴스
0번 인스턴스
0번 인스턴스
0번 인스턴스

 

하지만 실제로 실행해 보면 아래와 같이 결과가 나온다.

 

0번 인스턴스
2번 인스턴스
4번 인스턴스
3번 인스턴스
1번 인스턴스

※ 쓰레드가 실행되고 결과가 나오는 것은 순서가 정해져 있지 않기 때문에 결괏값은 바뀔 수 있음

 

이는 쓰레드의 실행속도가 매우 빠르기 때문에 거의 동시에 getInstance() 메서드가 실행되어 이때 instance가 없어(null) 여러 개의 인스턴스가 동시에 생성돼버리기 때문에 발생하는 문제이다.

 

이러한 문제를 해결할 수 있는 방법은 다음과 같다.

 

 

1. Synchronized 예약어 사용

...

    // synchronized를 이용해 동시성 문제 해결
    public synchronized static Singleton getInstance(String name) {
        if (instance == null) {
            instance = new Singleton(name);
        }
        return instance;
    }
    
...

 

첫 번째 방법은 getInstance() 메서드에 synchronized 예약어를 사용하는 것이다. 이렇게 하면 동시성 문제가 발생하지 않고 쓰레드를 순서대로 실행시킬 수 있다. 따라서 아래와 같이 결과가 나온다.

 

0번 인스턴스
0번 인스턴스
0번 인스턴스
0번 인스턴스
0번 인스턴스

 

하지만 이 방법은 동시성 문제를 해결하지만 성능 저하가 발생한다. 쉽게 말해 멀티쓰레드의 장점인 병렬적인 수행을 포기하고 직렬로(순서가 있이) 줄을 세워 실행하기 때문에 병목 현상이 발생하는 것이다.

 

이를 해결할 수 있는 방식은 다음과 같다.

 

2. 인스턴스 미리 생성하기

public class Singleton {
    // static 선언
    private static Singleton instance = new Singleton();
    private static String name;
    
    private Singleton() {
    }
   
    public static Singleton getInstance() {
        // 최초에 생성해둔 인스턴스를 반환하기만 하면 됨
        return instance;
    }
}

 

위와 같이 초기에 인스턴스를 미리 생성해 놓는다면, 멀티쓰레드 환경에서 각 쓰레드가 각각 인스턴스를 생성할 일을 방지할 수 있다. 이렇게 하면 성능 저하도 발생하지 않고 동시성 문제를 해결할 수 있다.

 

다만, 위와 같이 변경하면 getInstance()에 파라미터로 name을 넣을 수 없다. 따라서 기존의 코드처럼 사용하고 싶다면 setter를 이용해 name을 변경하는 등으로 코드를 수정해야 하는 단점이 있다.

 

 

 

정리

싱글톤 패턴은 장단점이 확실하다. 메모리를 절약하고 속도나 데이터 공유 측면에서 이점이 있다. 하지만 멀티쓰레드 환경에서 동시성 문제가 발생할 수 있고, 코드 구현이 복잡할 수 있다.

 

따라서 꼭 필요한 부분에만 싱글톤 패턴을 적용해야 한다.

 

참고로 스프링의 Bean은 기본적으로 싱글톤 패턴을 적용한다. 

궁금하다면 다음 글을 참고하자.

 

 

 

[Spring] 싱글톤 패턴 (Singleton Pattern), 싱글톤 컨테이너

싱글톤 패턴이란? 만약 스프링이 없는 순수한 DI 컨테이너 Appconfig에 고객들이 요청을 보낸다고 하자. DI 컨테이너는 요청을 받을 때마다 객체를 새로 생성할 것이다. 따라서 고객 트래픽이 초당 1

code-lab1.tistory.com

 

 


참고

 

1. https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4

 

반응형

댓글

Designed by JB FACTORY