[헤드퍼스트 디자인패턴] Chapter 05 - 싱글턴 패턴

뚱이·2023년 5월 15일
0
post-thumbnail

싱글턴 패턴 (Singleton Pattern)

클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

1. 싱글턴 패턴

(1) 싱글턴 패턴이란?

싱글턴 패턴은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 패턴 으로, 싱글턴 패턴을 사용하면 유일무이한 객체가 등장한다.

인스턴스가 여러 개 생성되면 안 되기 때문에 생성자가 private 으로 정의되는 것이 특징이며, 인스턴스가 필요하면 반드시 클래스 자신을 거치게 되어 있다.

싱글턴 패턴은

  • 클래스에서 하나뿐인 인스턴스를 관리한다.
    그래서 다른 어떤 클래스에서도 자신의 인스턴스를 멋대로 추가로 만들지 못 하게 하며, 이를 위해 인스턴스 생성 시 반드시 자신을 거치도록 한다.
  • 어디서든 그 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공한다.
    인스턴스가 단 하나뿐이므로, 언제든 누구든 인스턴스가 필요하면 클래스에 요청하게 만든 후 요청이 들어오면 이 인스턴스를 건네주도록 한다.

(2) 싱글턴 QnA

🤔❔ 클래스의 인스턴스를 하나만 만들어야 하면, 그냥 전역 변수를 쓰면 안 되나? Java에서는 정적 변수를 사용하면 될 거 같은데
💁‍♀️✔️ 전역 변수 사용에는 여러 단점이 있는데 그 중 하나는 자원 낭비 이다. 전역 변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성된다. 근데 이 때 이 객체가 자원을 굉 ~~ 장히 많이 차지하면 ? 근데 이 객체를 한 번도 사용하지 않는다면 ? 괜히 자원만 엄청 잡아먹고 결국 사용은 안 하는, 쓸데없는 객체가 된다.

🤔❔ 그럼 싱글턴은 어떤 용도로 사용하나?
💁‍♀️✔️ 스레드풀, 캐시, 사용자 설정, 대화상자, 레지스트리 설정 처리 객체, 로그 기록용 객체, 디바이스 드라이버 가 인스턴스 하나로도 충분히 잘 돌아가는 객체의 예시이다. 오히려 인스턴스가 2개 이상이면 자원을 낭비한다거나 결과의 일관성이 훼손된다거나 등의 문제가 발생할 수 있다.

🤔❔ 그렇다면 싱글턴 패턴의 장점은 뭐지?
💁‍♀️✔️ 싱글턴 패턴의 장점은 바로 유일함 이다. 인스턴스가 유일무이하기 때문에 한 애플리케이션에 들어있는 어떤 객체에서도 같은 자원을 활용할 수 있다.


(3) 싱글턴을 적용하는 방법의 base 과정

1개의 객체를 만드는 방법은?

new MyObject();

new 연산자를 활용한다. 이 때 다른 객체에서도 MyObject 를 사용하려면 클래스와 생성자가 public 으로 선언되어야 한다.

생성자를 private 으로 선언하면?

public MyClass {
	private MyClass() {}
}

생성자가 private 으로 선언되어 있기 때문에 다른 클래스에서 인스턴스를 생성할 수 없고, 오직 MyClass 내부에서만 생성이 가능하다.

그럼 정적 메소드(=클래스 메소드)를 사용하면?

public MyClass {
	public static MyClass getInstance() {}
}

정적 메소드를 활용하면 MyClass.getInstance(); 와 같은 방식으로 생성자를 호출해 사용할 수 있다.

그럼 위의 두 방법을 합치면!?

public MyClass {
	private MyClass() {}
    public static MyClass getInstance() {
    	return new MyClass();
    }
}

코드를 이렇게 작성하면 MyClass.getInstance(); 를 사용해 다른 클래스에서도 인스턴스를 생성할 수 있다.


(4) 고전적인 싱글턴 패턴 구현법: 게으른 인스턴스 생성

public class Singleton {
	private static Singleton uniqueInstance;
    
    // 기타 인스턴스 변수
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
    
    // 기타 메소드
    
}

생성자를 private 으로 선언하고,
getInstance() 메소드를 통해 인스턴스를 반환하되,
클래스 내에서 인스턴스가 유일무이하도록 관리하는 식으로 코드를 작성했다.

위 코드는
누군가 인스턴스가 필요해 Singleton.getInstance() 를 요청하기 전까지 인스턴스가 존재하지 않는다.
이렇게 객체가 처음 사용되기 전까지 인스턴스를 생성하지 않는 방법을 게으른 인스턴스 생성(Lazy instantiation) 라고 부른다.


(5) 클래스 다이어그램



2. 적용해보기: 초콜릿 보일러

(0) 초콜릿 공장의 초콜릿 보일러

초콜릿 공장에서 사용하는 초콜릿 보일러는 초콜릿을 끓이는 장치다.
이 초콜릿 보일러는 초콜릿과 우유를 받아서 끓인 후 초코바 만드는 단계로 넘겨준다.
이 초콜릿 보일러를 제어하는 프로그램 코드가 다음과 같다.

public class ChocolateBoiler {
	private boolean empty;
    private boolean boiled;
    
    private ChocolateBoiler() {
    	empty = true;
        boiled = false;
    }
    
    public void fill() {
    	if (isEmpty()) {
        	empty = false;
            boiled = false;
            // 보일러에 우유 + 초콜릿를 넣음
        }
    }
    
    public void drain() {
    	if (!isEmpty() && isBoiled()) {
        	// 끓인 재료를 다음 단계로 넘김
            empty = true;
        }
    }
    
    public void boil() {
    	if (!isEmpty() && !isBoiled()) {
        	// 재료를 끓임
            boiled = true;
        }
    }
    
    public boolean isEmpty() {
    	return empty;
    }
    
    public boolean isBoiled() {
    	return boiled;
    }
    
}

위 코드는 나름 세심한 주의를 기울인 코드다.
emptyboiled 변수 체크를 통해 가득 찬 상태에서 재료를 더 붓는다거나, 빈 보일러에 불을 지핀다거나 등의 문제 상황이 발생하지 않도록 하였다.

그러나 이 코드는 일단 생성자가 private 으로 선언되어 있으나 클래스 메소드가 존재하지 않아 다른 클래스에서 인스턴스를 생성할 수 없다.

(1) 싱글턴 패턴 적용하기: 게으른 인스턴스 생성

public class ChocolateBoiler {
	private boolean empty;
    private boolean boiled;
    
    private static ChocolateBoiler uniqueInstance;
    
    private ChocolateBoiler() {
    	empty = true;
        boiled = false;
    }
    
    public static ChocolateBoiler getInstance() {
    	if (uniqueInstance == null) {
        	uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }
    
    // 이하 동일
    
}

고전적인 싱글턴 패턴을 적용해 코드를 수정했다.
그럼 이제 우리는 하나의 인스턴스만 생성할 수 있고, 다른 객체들은 해당 인스턴스의 내용을 공유해 동일한 내용에 접근할 수 있다.

그러나 이 코드에도 문제는 있다.
멀티스레드 사용 시 동기화 문제 가 발생한다.


(2) 멀티스레딩 문제


(3) 멀티스레딩 문제 해결하기: synchronized

getInstance() 메소드에 synchronized 키워드를 걸어줌으로써 동기화 문제를 해결할 수 있다.

public class Singleton {
	private static Singleton uniqueInstance;
    // 기타 인스턴스 변수
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
    	// 내용 동일
    }
    
    // 기타 메소드
 
}

이런 식으로 코드를 작성하면 2개 이상의 스레드가 메소드를 동시에 실행하는 일이 일어나지 않는다.
그러나 이렇게 코드를 작성하면 속도 문제가 있다. 오버헤드가 많이 발생해 성능이 다소 떨어진다.

조금만 더 생각해보면,
사실 동기화가 필요한 시점은 메소드가 시작되는 때 뿐이다.
즉, uniqueInstance 변수에 Singleton 인스턴스를 대입하고 난 후에는 메소드를 동기화된 상태로 유지할 필요가 없다.


(4) 더 효율적으로 멀티스레딩 문제 해결하기 - 3가지 방법

[방법 1] 그냥 둔다.

getInstance() 메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 둔다.
다만, 메소드를 동기화하면 성능이 100배정도 저하된다.

[방법 2] 인스턴스를 처음부터 만든다.

지금까지 본 코드에서는 인스턴스가 필요할 때, 그 시점에 인스턴스를 (처음) 생성하는 방법이었다.
인스턴스를 생성하고 계속 사용하거나 인스턴스를 실행 중에 수시로 만들고 관리하기가 귀찮다면, 인스턴스를 처음부터 만드는 방법도 있다.

이러한 방법은 클래스가 로딩될 때 JVM에서 하나뿐인 인스턴스를 생성해주고,
인스턴스가 생성되기 전까지는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

public class Singleton {
	private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return uniqueInstance;
    }
}

[방법 3] 'DCL' 사용하기

DCL(Double-Checked Locking) 을 사용하면 인스턴스가 생성되어 있는 확인한 다음, 생성되지 않았을 때만 동기화할 수 있다.
이렇게 하면 처음 인스턴스를 생성할 때만 동기화하고 나중에는 동기화하지 않아도 되기 때문에 속도를 개선할 수 있다.

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

(5) 사실 Enum 을 사용하면 다 해결된다

싱글턴 패턴에는 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화, 역직렬화 문제 등이 있다.

그리고 이러한 문제들은 enum 으로 싱글턴을 생성하면 다 해결된다.

public enum Singleton {
	UNIQUE_INSTANCE;
    // 기타 필요한 필드
}

---
public class SingletonClient {
	public static void main(String[] args) {
    	Singleton singleton = Singleton.UNIQUE_INSTANCE;
        // 여기서 싱글턴 사아ㅛㅇ
    }
}

그러면 지금까지 우리가 본,
getInstance() 메소드가 들어있는 _Singleton 클래스를 만들고 동기화하는 방법
은 왜 공부했는지 궁금할 수 있는데,

지금까지 나온 설명은 이해를 위해 싱글턴의 작동 원리를 하나하나 확인해본 것이고,
enum을 쓰지 않고 싱글턴을 구현하는 방법을 누군가 물어본다면 이제 대답할 수 있게 되었다. 😚




3. 정리

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화 한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그램이한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  • OCP: 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
  • 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

객체지향 패턴

전략 패턴

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

옵저버 패턴

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.

데코레이터 패턴

객체에 추가 요소를 동적으로 더할 수 있다.
데코레이터를 사용하면 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.

팩토리 메소드 패턴

객체에서 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
이 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡긴다.

추상 팩토리 패턴

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만든다.

싱글턴 패턴(Singleton Pattern)

클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

2개의 댓글

comment-user-thumbnail
2024년 3월 23일

저는 밀크초콜릿을 좋아합니다.

1개의 답글