디자인 패턴 분류인 생성, 구조, 행위 중 객체 생성에 관련 된 생성 패턴으로 간주됩니다.
생성 패턴은 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공합니다.
단 하나의 인스턴스를 생성해 사용하는 디자인 패턴(유일한 객체를 생성하는 방법)
애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴입니다.
전역 변수를 사용하지 않고 객체를 하나만 생성 하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있습니다.
생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 반환합니다.
유일한 인스턴스가 서브클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때 사용합니다.
클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 만듭니다. 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 합니다. 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야 합니다.
장점
고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지 할 수 있습니다.
두 번째 이용시부터는 객체 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점이 있습니다.
싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽습니다.
인스턴스가 절대적으로 한개만 존재하는 것을 보증하기에 개발 시 실수를 줄일 수 있습니다.
싱글톤 객체를 사용하지 않는 경우 인스턴스를 생성하지 않을 수 있습니다.
싱글톤을 상속시킬 수 있습니다.
전역 변수의 단점
전역 변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성될 것입니다. 그런데 그 객체가 자원을 많이 차지하고 애플리케이션이 끝날 때까지 그 객체를 한 번도 쓰지 않는다면 괜히 자원만 잡아먹는, 아무 데도 쓸 데 없는 객체가 되고 맙니다. 싱글턴 패턴을 쓰면 필요할 때만 객체를 만들 수가 있습니다.
사실 플랫폼에 따라 달라질 수 있습니다. 어떤 JVM에서는 나중에 필요할 때 생성하기도 합니다.
단점
싱글톤의 역할이 커질수록 결합도가 높아져 객체 지향 설계 원칙에 어긋날 수 있다.
싱글톤 객체가 변경되면 이를 참조하고 있는 모든 값들이 변경됩니다.
수정이 어려워지고 테스트하기 어려워집니다. 싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 개방-폐쇄 원칙 을 위배하게 됩니다.
멀티쓰레드환경에서 동기화 처리를 하지 않으면 인스턴스가 두개가 생성되는 문제가 발생할 수 있습니다.
경합 조건 문제가 발생 할 수 있습니다. 경합 조건이란 메모리와 같은 동일한 자원을 2개 이상의 스레드가 이용하려고 경합하는 현상입니다.
다중 스레드 애플리케이션에서 발생하는 문제를 해결하는 방법은 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법 (Eager Initialization)과 인스턴스를 만드는 메서드에 동기화하는 방법 (Thread-Safe Initialization)이 있습니다.
전역변수보다 사용하기가 불편합니다.
객체의 파괴 시점을 컨트롤하기 어려울 수 있습니다.
활용처
인스턴스가 오직 1개만 생성되야 하는 경우 사용합니다.
DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용합니다.
쓰레드풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정, 로그 기록 객체 등
안드로이드 앱 같은 경우 각 액티비티나 클래스별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는 것이 편합니다.
인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을 경우 사용합니다.
레지스트리 같은 설정 파일의 경우 객체가 여러개 생성되면 설정 값이 변경될 위험이 생길 수 있기 때문에 사용하면 좋습니다.
주의할 점
책임의 불균형이 심화되고 있는 상태에서 객체의 생성 책임을 지우는 것은 설계를 하부의 특정한 메커니즘에 종속적으로 만들 확률이 높습니다. 불필요한 SINGLETON 패턴을 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있습니다. 핵심은객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것입니다.
정적 메서드로만 이루어진 정적 클래스를 사용하면 싱글턴과 동일한 효과를 얻을 수 있습니다.
정적 클래스를 이용하면 객체를 전혀 생성하지 않고 메서드를 사용합니다.
정적 메서드를 사용하므로 일반적으로 실행할 때 바인딩되는(컴파일 타임에 바인딩되는) 인스턴스 메서드를 사용하는 것보다 성능 면에서 우수합니다.
정적 클래스를 사용할 수 없는 경우
인터페이스를 구현해야 하는 경우, 정적 메서드는 인터페이스에서 사용할 수 없습니다.
인터페이스를 사용하는 주된 이유는 대체 구현이 필요한 경우입니다.
Mock 객체를 사용해 단위 테스트를 수행하는 경우입니다.
예제
싱글턴 패턴의 공통적인 특징은 private constructor를 가진다는 것과, static method를 사용한다는 점입니다.
private 생성자
static 변수로 객체 생성
객체의 getter 구현
classPrinter{// 외부에 제공할 자기 자신의 인스턴스privatestatic printer: Printer =null;privateconstructor(){console.log('Printer constructor');}// 자기 자신의 인스턴스를 외부에 제공publicstaticgetPrinter(): Printer {if(this.printer ==null){// Printer 인스턴스 생성this.printer =newPrinter();}returnthis.printer;}publicprint(str:string){console.log(str);}}classUser{private name:string;publicconstructor(name:string){this.name = name;}publicprint(){const printer = Printer.getPrinter();
printer.print(this.name +' print using ');}}constUSER_NUM=5;const user =[];for(let i =0; i <USER_NUM; i++){// User 인스턴스 생성
user[i]=newUser((i +1).toString());
user[i].print();}
Eager Initialization(이른 초기화, Thread-safe)
이른 초기화 방식은 클래스 로더에 의해 클래스가 최초로 로딩 될 때 객체가 생성되기때문에 Thread-safe 합니다.
classPrinter{// static 변수에 외부에 제공할 자기 자신의 인스턴스를 만들어 초기화privatestatic printer: Printer =newPrinter();privateconstructor(){console.log('constructor');}// 자기 자신의 인스턴스를 외부에 제공publicstaticgetPrinter(): Printer {returnthis.printer;}publicprint(str:string){console.log(str);}}
피닉스 싱글톤은 싱글톤 참조시 해당 객체의 소멸여부를 판단하고 소멸되었다면 다시 되살리게 됩니다.
마이어스 싱글턴(singleton)의 한계를 극복하는 싱글턴입니다.
C++에서 사용됩니다.
어떻게 피닉스 싱글턴(phoenix singleton)을 구현하는가?
마이어스 싱글턴을 기반하기 때문에, static 지역 변수를 기반으로 구현된 싱글턴입니다. 이 static 지역 변수의 특징은 여러개 있지만, 그 중 메모리 해제 시점 관점에서 본다면, 다음과 같다.
메모리의 생성은 프로그램이 해당 지역변수를 구동 코드를 읽을 때 합니다.
프로그램 종료시점에 메모리 파괴가 일어 난다 해도, 그 공간은 빈 공간으로 남아 있습니다. (즉, 다른것으로 채워지지 않는다) 이러한 특징 때문에, 프로그램 종료 시점에 그 메모리 공간에 다시 쓰기 위해 위치지정(또는 재배치)를 사용하며, 파괴 시점을 제어합니다.