소프트웨어 기존 환경 내에서 반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 일종의 솔루션입니다.
이는 코드에서 반복되는 디자인 문제들을 해결하기 위해 맞춤화할 수 있는 미리 만들어진 청사진과 비슷합니다.

설계에 대한 생각을 더욱 쉽게 하고, 개발자들 간의 의사소통을 원활하게 하기 위해 패턴 이름과 문제, 다룬 문제에 대하여 어떻게 해결할 수 있을지 해법 및 구현, 디자인 패턴을 적용해서 얻는 결과와 장단점을 다루겠습니다.
어떤 문제에 어떤 디자인 패턴을 사용할 지 항상 고민하거나 안 해버리는 상황을 직면하는 경우가 많아 어떻게 공부할지 정리해보자.
생성 패턴은 객체의 생성에 관련된 패턴으로 객체의 생성 절차를 추상화하는 패턴입니다.
기존 코드의 유연성과 재사용을 증가시키는 객체를 생성하는 다양한 방법을 제공합니다.
1. 시스템이 어떤 구체 클래스를 사용하는지에 대한 정보를 캡슐화합니다.
추상화된 인터페이스이고 스피커는 구체 클래스입니다.2. 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려줍니다.
쉽게 말하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는 데 유연성을 확보할 수 있다는 의미입니다.
캡슐화란 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호
클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 패턴입니다.
어떤 클래스의 인스턴스가 오직 하나임을 보장하며, 이 인스턴스에 접근할 수 있는 전역적인 접촉점을 제공하는 패턴입니다.
프로그램 시작부터 종료 시까지 어떤 클래스의 인스턴스가 메모리 상에 단 하나만 존재할 수 있게 하고 이 인스턴스에 대해 어디에서나 접근할 수 있도록 하는 패턴입니다.

한 번에 두 가지의 문제를 동시에 해결함으로써 단일 책임 원칙을 위반합니다.
단 하나의 인스턴스만을 갖도록 해서 로그를 찍는 객체라던가 커넥션 풀, 쓰레드 풀, 윈도우 관리자, 디바이스 설정 객체 등의 경우 인스턴스를 여러 개 만들면 자원을 낭비하게 되거나 버그를 발생시킬 수 있으므로 오직 하나만 생성하고 그 인스턴스를 사용하도록 하는 것이 목적입니다.
사람들이 클래스에 있는 인스턴스 수를 제어하려는 가장 일반적인 이유는 일부 공유 리소스(DB, 파일)에 대한 접근을 제어하기 위함입니다.
예를 들어 객체를 생성했지만 이후 새 객체를 생성하려고 할 때 새 객체를 생성하는 대신 이미 만든 객체를 받게 됩니다. (밑에 그림에 대한 설명)
물론 생성자 호출은 특성상 반드시 새 객체를 반환해야 하므로 위 행동은 일반 생성자로 구현할 수 없습니다.

1. 정적 메서드 호출 : 싱글톤 클래스에서 정의한 정적 메서드인 getInstance() 를 호출합니다. 이 메서드는 항상 동일한 인스턴스를 반환합니다.
2. 인스턴스 메서드 호출 : 반환된 싱글톤 인스턴스를 통해 클래스에 정의된 다양한 인스턴스 메서드를 호출할 수 있습니다.
// 싱글톤 클래스 정의
public class Singleton {
private static Singleton instance;
private Singleton() {
// private 생성자
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void performAction() {
System.out.println("싱글톤 인스턴스에서 작업을 수행합니다.");
}
public void anotherMethod() {
System.out.println("다른 작업을 수행합니다.");
}
}
// 클라이언트 코드
public class Client {
public static void main(String[] args) {
// 싱글톤 인스턴스 얻기
Singleton singletonInstance = Singleton.getInstance();
// 인스턴스 메서드 호출
singletonInstance.performAction(); // "싱글톤 인스턴스에서 작업을 수행합니다." 출력
singletonInstance.anotherMethod(); // "다른 작업을 수행합니다." 출력
}
}
다른 객체들이 싱글톤 클래스와 함께 new 연산자를 사용하지 못하도록 디폴트 생성자를 비공개로 설정합니다.
생성자 역할을 하는 정적 생성 메서드를 만듭니다.
public class Printer {
private static Printer printer = null;
private Printer(){} // 생성자 private으로
public static Printer getInstance() {
if(printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String input) {
System.out.println(input);
}
기본 생성자를 사용하여 생성을 불가능하게 하고 getInstance를 통해서만 생성할 수 있다. getInstance는 내부적으로 생성되지 않았다면 생성하고, 기존에 생성된 값이 존재하면 생성된 인스턴스를 리턴하는 형태로 프로그램 전반에 걸쳐 하나의 인스턴스를 유지합니다.
당연히 기본 생성자를 통해 생성이 불가능하므로 외부에서 인스턴스에 접근하려면 클래스 변수 및 메서드에 접근을 허용하려면 두 메서드는 정적 타입으로 선언되어야 한다.
싱글톤 클래스의 인스턴스를 클래스 로딩 단계에서 생성하는 방법입니다.
기본 생성자보다 앞서 초기화를 진행.
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
싱글톤 클래스가 다소 적은 리소스를 다룰 때 사용해야 합니다.
File System, Database Connection 등 큰 리소스들을 다루는 싱글톤을 구현할 때는 위와 같은 방식보다는 getInstance() 메소드가 호출될 때까지 싱글톤 인스턴스를 생성하지 않는 것이 더 좋습니다.
게다가 Eager Initializaion은 Exception에 대한 Handling도 제공하지 않습니다.
Eager Initialization과 유사하지만 static block을 통해서 Exception Handling에 대한 옵션을 제공합니다
public class Singleton {
// private static final Singleton instance = new Singleton();
private static Singleton instance;
private Singleton() {}
static{
try{
instance = new Singleton();
}
catch(Exception e){
throw new RuntimeException("Exception occured in singleton class");
}
}
public static Singleton getInstance() {
return instance;
}
}
위 두가지 방법에는 사용하지 않는 인스턴스에 대해 낭비가 생김.
나중에 초기화하는 방법.
Multi-Thread 환경에서는 안전하지가 않다는 점입니다.
여러 쓰레드가 공유되고 있는 상황에서는 아래와 같은 코드에서 조건문이 동시에 2번 돌 수 있기 때문에 (여러 쓰레드가 동시에 getInstance()를 호출하는 상황) 하나의 인스턴스가 아닌 여러 개의 인스턴스가 발생할 위험이 있습니다.
public static Singleton getInstance() {
if(instance == null) {
printer = new Singleton();
}
return instance;
}
상태를 유지해야 하는 상황이라면 예제에서처럼 count 값은 각기 다른 쓰레드에서 공유하고 있고 서로 다른 프로세스에서 처리하고 있기 때문에 값이 일관되지 않을 수 있습니다.
public class Printer {
private static Printer printer = null;
private int count = 0;
private Printer(){}
public static Printer getInstance() {
if(printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String input) {
count++;
System.out.println(input + "count : "+ count);
}
}
1. 정적 변수에 인스턴스를 만들어 바로 초기화 하는 방법
정적 변수는 객체가 생성되기 전 클래스가 메모리에 로딩할 때 만들어져 초기화가 한 번만 실행됩니다. 또한 정적 변수는 프로그램이 시작될 때부터 종료될 때까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있습니다.
따라서 기존에 조건문에서 체크하던 부분이 원천적으로 제거됩니다.
public synchronized static void print(String input){
count++;
System.out.println(input + "count : " + count);
}
synchronized 라는 키워드를 getInstance() 메서드에 걸어 여러 쓰레드에서 동시에 접근하는 것을 막는 방법이 있습니다.
정적 클래스를 사용하면 객체를 전혀 생성하지 않고 메서드 사용 가능하고 인스턴스 메서드를 사용하는 것보다 성능 면에서 우수합니다.
Synchronized 란 임계 영역을 형성해 해당 영역에 오직 하나의 쓰레드만 접근 가능하게 해주는 키워드 입니다.
이렇게 하면 getInstance() 메서드 내에 진입하는 쓰레드가 하나로 보장받기 때문에 멀티 쓰레드 환경에서도 정상 동작하게 됩니다.
그러나 synchronized 자체에 대한 비용이 크기 때문에 싱글톤 인스턴스 호출이 잦은 애플리케이션에서는 성능이 떨어집니다.
정적 변수 특징
1. 클래스 로딩 시 초기화
- 정적 변수는 클래스가 메모리에 로드될 때 초기화된다.
- 즉, 클래스가 처음 사용될 때 JVM에 의해 한 번만 생성되고 초기화된다.
- 한 번만 생성
- 정적 변수는 프로그램이 실행되는 동안 한 번만 생성되고 이후에는 계속해서 동일한 메모리 공간을 사용한다.
- 프로그램 종료 시까지 존재
- 정적 변수는 프로그램이 종료될 때까지 메모리에 남아있고 프로그램이 종료될 때 JVM이 할당된 메모리를 해제합니다.
- 모든 객체에서 공유
- 정적 변수는 클래스의 모든 인스턴스에서 공유되고 어떤 인스턴스에서도 동일한 정적 변수에 접근하고 값을 변경할 수 있습니다.
2. 인스턴스를 만드는 메서드에 동기화하는 방법
정적 클래스를 사용할 수 없는 경우가 있는데 그것은 인터페이스를 구현하는 경우입니다. (인터페이스는 정적 메서드를 가질 수 없기 때문에)
그래서 고안된 방식이 Double Checked Locking입니다.
getInstance() 메서드 수준에 lock을 걸지 않고 인스턴스가 null일 경우에만 synchronized가 동작하도록 합니다.
public class Singleton {
private static volatile Singleton singletonObject;
private Singleton() {}
// Double checked locking
public static synchronized Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
이 방법이 앞선 방식이 안고 있는 문제점들을 대부분 해결한 방식입니다. 그래서 현재 가장 널리 쓰이고 있습니다.
public class Singleton {
private Singleton(){}
private static class SingletonHelper{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
private inner static class를 두어 싱글톤 인스턴스를 갖게 합니다.
이때 1번이나 2번 방식과의 차이점이라면 SingletonHelper 클래스는 Singleton 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출됐을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 됩니다.
synchronized를 사용하지 않기 때문에 성능 저하도 해결됩니다.
지연 초기화와 Thread Safe한 싱글톤 패턴을 구현할 수 있습니다.
위 방식 모두 완전히 안전할 수 없습니다. Java의 Reflection을 통해서 싱글톤을 파괴할 수 있기 때문입니다.
그래서 Enum으로 싱글톤을 구현하는 방법을 제안했습니다.
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
//do something
}
}
Enum 클래스는 생성자를 private으로 갖게 하고 상수만 갖는 클래스입니다. 단순한 코드로 싱글톤 사용 가능합니다.
하지만 이 방법 또한 1, 2번과 같이 사용하지 않았을 경우의 메모리 문제를 해결하지 못한 것과 유연성이 떨어진다는 면에서의 한계를 지니고 있습니다.
프로그램 전체에서 하나의 객체만을 공통으로 사용하고 있기 때문에 각 객체 간의 결합도가 높아지고 변경에 유연하게 대처할 수 없습니다.
싱글톤 객체가 변경되면 이를 참조하고 있는 모든 값들이 변경되어야 하기 때문에 대처 불가능합니다.
멀티쓰레드 환경에서 대처가 어느 정도 가능하지만 고려해야 할 요소가 많아 사용이 어렵지만 프로그램 전반에 걸쳐서 필요한 부분에만 사용한다면 장점이 있습니다. (그 포인트를 잡기 어려운 게 문제.)
멀티 쓰레드 환경에서의 싱글톤
Synchronized로 관리 가능하고 다양한 변화에 대응하기 위해 인터페이스의 형태로 관리하면 좋습니다.
단일 쓰레드 환경에서의 싱글톤
- 정적 클래스의 형태로 사용하면 됩니다.
- 테스트를 위한 모의객체를 만들거나 (인터페이스 사용) 혹은 다른 목적으로 사용한다면 멀티쓰레드 환경에서 싱글톤을 사용하듯이 사용하면 됩니다.
- 즉 싱글톤 패턴이 객체의 유일성과 전역적인 접근성 외에도 다른 목적(예: 모의 객체)에서도 유용하게 사용될 수 있다는 것을 의미합니다.
| 장점 | 단점 |
|---|---|
| 클래스가 하나의 인스턴스만 갖는다는 것을 확신 | 단일 책임 원칙을 위반. |
| 이 인스턴스에 대한 전역 접근 지점을 얻음. | 다중 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 특별한 처리가 필요. (동시성 문제가 발생.) |
| 싱글톤 객체는 처음 요청될 때만 초기화 | 잘못된 디자인 (예를 들어 프로그램의 컴포넌트들이 서로에 대해 너무 많이 알고 있는 경우)을 가릴 수 있다. |
| 메모리 측면 이점, 속도 측면, 데이터 공유 쉬움. | private 생성자 때문에 테스트가 어렵다. |
| 의존 관계 상 클라이언트가 구체 클래스에 의존한다. | |
| 객체 인스턴스를 하나만 생성해서 공유하는 방식 때문에 싱글톤 객체를 stateful하게 설계 했을 경우 큰 장애 발생요인이 된다. |
단점을 해결하기 위해 무상태로 설계해야 합니다.
📎 https://refactoring.guru/ko/design-patterns/singleton
📎 https://velog.io/@kyle/자바-싱글톤-패턴-Singleton-Pattern
📎 https://readystory.tistory.com/116
📎 https://ittrue.tistory.com/563