싱글톤패턴
싱글톤패턴(singleton pattern)이란 인스턴스가 오직 하나만 생성되는 것을 보장하고 어디에서든 이 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다. 원래 싱글톤(singleton)이라는 단어는 "단 하나의 원소만을 가진 집합"이라는 수학이론에서 유래되었다.[1]
개요
디자인 패턴에서 싱글톤패턴(singleton pattern)을 따르는 클래스는 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용되며 GoF(Gang of Four), 흔히 말해서 4인방이라고 하는 인물들이 구분한 패턴에서 생성패턴에 속한다.[2]
특징
목적
DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용하고 안드로이드 앱 같은 경우 각 액티비티나 클래스 별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는것이 편하기 때문에 사용한다. 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을 경우에 사용한다.
장점
고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있고 싱글톤패턴으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.
단점
싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 OCP(Open-Closed Principle), 즉, 개방-폐쇄 원칙을 위배하게 된다. 따라서 수정이 어려워지고 테스트하기 어려워진다. 또한 멀티쓰레드 환경에서 동기화 처리를 하지 않는다면 인스턴스가 2개 생성되는 경우가 발생하기도 한다.
구조
싱글톤패턴은 단일 인스턴스의 클래스가 액세스와 처음 사용시 초기화를 담당한다. 단일 인스턴스는 전용 정적 속성으로 정의하고 접근자 함수는 퍼블릭 정적 메소드로 선언한다.[4] 싱글톤패턴을 사용한 클래스에 예제의 클라이언트(client)가 직접적으로 접근할 수 없도록 프라이빗(private) 접근제어자를 사용하고 객체가 여러개 생성되는 것을 막기 위해서 생성자에 정적인 선언을 해준다. 또 메소드에 객체에 접근할 수 있는 getInstance 메소드를 선언해 클라이언트(client)에서 간접적으로 참조가 가능하게 한다. 결과적으로 new를 사용해 객체를 생성하더라도 생성자가 정적으로 선언되어있기 때문에 객체를 참조할때 같은 객체를 참조하게 되고 하나의 객체로 어디서나 접근이 가능하도록 설정이 가능하다.
싱글톤패턴의 고도화
이른 초기화 방식
이른 초기화 방식(Eager initialization)은 싱글톤(Singleton)의 가장 기본적인 방식이다. 먼저 클래스 내에 전역변수로 instance 변수를 생성하고 private static을 사용하여 인스턴스화에 상관없이 접근이 가능하면서 동시에 private 접근 제어 키워드를 사용해 instance로 바로 접근 할 수 없도록 하고 생성자에도 private 접근 제어 키워드를 붙여 다른 클래스에서 new Singleton() 으로 새로운 인스턴스를 생성하는 것을 방지한다. 오로지 정적메소드인 getInstance() 메소드를 이용해서 인스턴스를 접근하도록 하여 유일무이한 동일 인스턴스를 사용하는 기본 싱글톤(Singleon) 원칙을 지키게 한다. 클래스 로더에 의해 클래스가 최초 로딩될 때 싱글톤 객체가 생성되긴 때문에 스레드 환경에서 다량의 객체가 생성되는 것을 방지할 수 있지만 싱글톤 객체 사용유무와 관계없이 클래스가 로딩되는 시점에는 항상 싱글톤 객체가 생성되어 메모리를 차지하기 때문에 비효율적이다.
<이른 초기화 예제> public class Singleton { //이른 초기화 private static Singleton Instance = new Singleton(); private Singleton() {} public staitic Singleton getInstance() { return Instance; } }
정적 블록 초기화 방식
정적 블록 초기화 방식(Static Block Initialization)은 이른 초기화(Eager initialization)과 유사하지만, 인스턴스가 정적 블록(static block) 내에서 만들어지고, 정적 블록(static block) 안에서 예외처리를 할 수 있다는 점이 다르다.[5]
<정적 블록 초기화> public class Singleton { private static Singleton instance; private Singleton(){} static{ try{ instance = new Singleton(); }catch(Exception e){ throw new RuntimeException("Exception occured") } } public static Singleton getInstance() { return instance; } }
늦은 초기화 방식
늦은 초기화 방식(Lazy initialization)은 이른 초기화 방식(Eager initialization)과 정반대로 클래스가 로딩되는 시점이 아닌 클래스의 인스턴스가 사용되는 시점에서 싱글톤 인스턴스를 생성한다. 즉, 사용시점까지 싱글톤 객체 생성을 미루기 때문에 사용하기 전까지 메모리를 차지하지 않는다. 싱글톤 객체가 필요할 때 인스턴스를 얻을 수 있기 때문에 이른 초기화 방식에서 클래스가 로딩됨과 동시에 비효율적으로 메모리를 차지했던 점을 보완할 수 있다. 하지만 멀티 쓰레드 환경에서 여러 곳에서 동시에 getInstance()로 호출할 경우 인스턴스가 두번 생성될 여지가 있다.
<늦은 초기화 예제> public class Singleton { //늦은 초기화 private static Singleton instane; private Singleton(){} public static Singleton getInstance() { if(instance == null) //만약 객체가 하나도 생성이 되어있지않다면 객체 생성 { instance = new Singleton(); } return instance; } }
스레드 안전한 늦은 초기화 방식
스레드 안전한 늦은 초기화(Thread safe Lazy initialization)은 Lazy initialization 방식에서 다중 스레드 환경에서 여러개의 인스터스가 생성될 수 있다는 단점을 보완하기 위해 스레드들이 동시접근하는 동시성을 가진 메소드에 sychronized 키워드로 동기화 시켜 해결하는 것을 말한다.
<스레드 안전한 늦은 초기화 예제> public class Singleton { //스레드 안전한 늦은 초기화 private static Singleton; private Singleton(){} public static synchronized Singleton getInstance() //메소드 동기화 { if(instance == null) { instance = new Singleton(); } } }
DCL 기법
DCL(Double-checked locking) 기법이란 다중스레드 환경에서 많은 스레드들이 synchronized 처리된 메소드를 접근하면 성능저하가 발생하는데 이를 좀더 완화시키기위해 사용하는 기법이다. 아래 예제에서 첫번째 if문에서 instance가 null인 경우 synchronized 블럭에 접근하고 한번 더 if문으로 instance가 null 유무를 체크한다. 2번 모두 다 instance가 null인 경우에 new Singleton()를 통해 인스턴스화 시킨다. 그 후에 instance가 null이 아니기 때문에 synchronized 블럭을 타지않는다. 처음 접근할 때만 동기화된 메소드를 사용하게 하여 동시에 접근한 스레드에 하나의 객체만 생성되게 하고 객체가 생성된 이후에는 동기화 메소드에 접근하지 않기 때문에 동기화로 발생했던 성능 저하 문제에 대한 해결책이 될 수 있다.
public static Singleton { //DCL 기법 private static Singleton instance; private Singleton(){} public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) instance = new Singleton(); } } return instance; } }
Holder에 의한 초기화 방식
Holder에 의한 초기화 방식(Initialization on demand holder idiom)은 Bill Pugh Solution 이라고도 하며 클래스안에 클래스(Holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법이다. 늦은 초기화 방식(Lazy initialization)을 가져가면서 Thread간 동기화 문제를 동시에 해결할 수 있다. 중첩클래스 Holder는 getInstance() 메소드가 호출되기 전에는 참조되지 않으며 최초로 getInstance() 메소드가 호출될 때 클래스 로더에 의해 싱글톤 객체를 생성하여 리턴한다. Holder안에 선언된 instance가 static이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한 것이다. final을 사용하여 다시 값이 할당되지 않도록한다. 현재까지 가장 많이 사용되는 방법으로 알려져있다.
<Holder에 의한 초기화> public class Singleton { //Holder에 의한 초기화 private singleton(){} private static class SingletonHolder //싱글톤 인스턴스를 생성하는 Holder클래스 정의 { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.instance; } }
Enum 초기화 방식
Enum 초기화 방식 (Enum initialization)은 자바 1.5버전부터 지원하는 enum을 활용한 방식이다. enum을 싱글톤패턴으로 사용할 수 있는 이유에는 인스턴스가 생성될때 멀티스레드로부터 안전하고 단 한번의 인스턴스 생성을 보장하고 enum value가 자바 프로그램 전역에서 접근이 가능하며 사용이 간편한 점들이 있다.
<Enum 초기화> public enum Singleton { INSTANCE; public static Singleton getInstance() { return INSTANCE; } }
reflection으로 싱글톤 깨기
reflection으로 싱글톤 깨기(using reflection to destroy singleton)는 싱글톤 패턴을 깨는 방식을 말하는데 그 어떤 형태의 싱글톤이라고 자바 reflection의 setAccessible(true)를 사용하면 모든 private 생성자, 메소드에 접근이 가능해진다.[5] 누군가 작성한 코드를 원본 수정없이 작업해야 할 때 이용한다. 아래의 예제는 기존에 작성했던 싱글톤 패턴의 인트턴스를 받아와 자바의 reflection의 constructor의 setAccessible 메소드를 활용해 설령 class의 생성자가 private일지라도 강제로 가져와서 새로운 인스턴스 생성이 가능하게 만든다. 결국 싱글톤을 깨뜨리는 것이다. 실행결과를 보면 hashcode() 메소드의 리턴값이 다른 것을 확인 할 수 있다.[6]
<reflection으로 싱글톤 깨기> public class usingReflectionToDestroySingleton { public static void main (String[] args) { Singleton instance = Singleton.getInstance(); Singleton instance2 = null; try { Constructor[] constructors = Singleton.class.getDeclaredConstructors(); for ( Construct construct : constructors ) { constructor.setAccessible(true); instance2 = (Singleton)constructor.newInstance(); } } catch (Exception e) {} System.out.println(instance.hashcode()); System.out.println(instance2.hashcode()); } }
<실행결과> 2008977864 5578467
활용
다중 스레드 상황
프린터 관리자 만들기 (늦은 초기화 방식)
아래의 나오는 예제들은 싱글톤 패턴의 각 방식들이 가지는 문제점을 나타내며 어떻게 보완할 수 있는지 말해준다. 멀티 스레드 환경에서 늦은 초기화 방식에 의해 생기는 문제점인 인스턴스가 여러개 생성되는 점을 클래스 로더를 활용한 이른 초기화 방식과 메소드 동기화를 통한 스레드 안전한 늦은 초기화 방식으로 보완한 예제이다.
- 문제점 : 프린터 인스턴스를 하나만 만들고 여러곳에서 동일한 프린터를 사용하게 끔 하는게 목적이지만 스레드가 작용하면서 실행결과에서 볼 수 있듯이 각각 다른 인스터스에서 참조된 것을 알 수 있다. if(printer == null) 조건문에서 스레드 접근시에 객체 생성전에 여러 스레드가 접근해 스레드가 여러개 생기는 문제가 발생한다. 이런 다중 스레드에서 문제점을 해결하기 위해서 두가지 방식을 사용한다. 가장 중요한 점은 어떤 상황에서 객체가 하나만 생성될 수 있는지 파악해야한다는 것이다.
<스레드 클래스> public class UserThread extends Thread { public UserThread(String name) { super(name); } public void run() { Printer printer = Printer.getPrinter(); printer.print(Thread.currentThread().getName() + " print using " + printer.toString() + "."); } }
<프린터 클래스> public class Printer { private static Printer printer = null; private Printer(){} public static Printer getPrinter() { if(printer ==null) { try { Thread.sleep(1); } catch(InterrupedException e){} printer = new Printer(); } return printer; } public void print(String str) { System.out.println(str); } }
<사용자 클래스(메인)> public class Client { private static final int THREAD_NUM = 5; public static void main(String[] args) { UserThread[] user = new UserThread[THREAD_NUM]; for(int i = 0; i < THREAD_NUM; i++) { user[i] = new UserThread(( i + 1 )+ "-thread"); user[i].start(); } } }
<실행결과> 4-thread print using Printer.Printer@7700b3c2. 1-thread print using Printer.Printer@51493995. 2-thread print using Printer.Printer@71f801f7. 3-thread print using Printer.Printer@24367013. 5-thread print using Printer.Printer@4f19c297.
프린터 관리자 만들기 (이른 초기화 방식)
스레드 클래스와 메인 클래스인 사용자 클래스는 기존의 코드와 동일하다.
- 해결법 : 정적 변수에 인스턴스를 만들어서 초기화 하는 방법으로 정적 변수는 객체가 생성되기 전 클래스가 메모리에 로딩될 때 만들어져 초기화가 한 번만 실행된다. 또한 정적 변수는 프로그램이 시작될 때부터 종료될 때까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있다. 정적 변수의 이러한 특징 때문에 private static Printer = new Printer(); 구문이 실행되면 정적 변수 printer에 Printer 클래스 인스터스가 바인딩되며 getPrinter라는 정적 메소드를 통해 참조되는 인스턴스를 얻어올 수 있다. 이 방법은 다중 스레드 환경에서 문제를 일으켰던 if(print == null)라는 조건 검사 구문을 원천적으로 제거하기 위한 방법이다. 실행결과를 보면 모두 같은 프린터 인스턴스값을 참조한 것을 알 수 있다.
<프린터 클래스> public class Printer { private static Printer printer = new Printer(); //정적 변수에 인스턴스를 만들어 초기화 private Printer(){} public static Printer getPrinter() { return printer; //프린터 객체 반환 } public void print(String str) { System.out.println(str); } }
<실행결과> 5-thread print using Printer.Printer@4f19c297. 1-thread print using Printer.Printer@4f19c297. 3-thread print using Printer.Printer@4f19c297. 4-thread print using Printer.Printer@4f19c297. 2-thread print using Printer.Printer@4f19c297.
프린터 관리자 만들기 (스레드 안전한 늦은 초기화 방식)
스레드 클래스와 메인 클래스인 사용자 클래스는 기존의 코드와 동일하다.
- 해결법 : 인스턴스를 만드는 메소드에 동기화를 적용하는 방법으로 다중 스레드 환경에서 동시에 여러 스레드가 getPrinter 메소드를 소유하는 객체에 접근하는 것을 방지한다. 결과적으로 Printer 클래스의 인스턴스가 오직 하나의 인스턴스만 생성한다. if( printer == null ) 조건문에서 이미 하나의 스레드가 접근해서 인스턴스 생성을 했다면 그 다음 접근한 스레드 부터는 printer 변수가 null 값이 아니게 되므로 결과적으로 하나에 인스턴스만 생성하는 것이 된다. 실행결과를 보면 모두 같은 프린터 인스턴스에서 값을 참조한 것을 알 수 있다.
<프린터 클래스> public class Printer printer { private static Printer printer = null; private Printer(){} public synchronized static Printer getPrinter() { if(print == null) printer = new Printer(); return printer; } public void print(String str) { System.out.println(str); } }
<실행결과> 2-thread print using Printer.Printer@7ff12373. 4-thread print using Printer.Printer@7ff12373. 5-thread print using Printer.Printer@7ff12373. 3-thread print using Printer.Printer@7ff12373. 1-thread print using Printer.Printer@7ff12373.
각주
- ↑ 정인상, 채흥석, 〈자바 객체지향 디자인 패턴: UML과 GoF 디자인 패턴 핵심 10가지로 배우는〉, 《한빛 미디어》, 2014-04-28
- ↑ 싱글턴 패턴 위키백과 - https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4
- ↑ 정아마추어, 〈싱글톤 패턴(Singleton pattern)을 쓰는 이유와 문제점〉, 《개인 블로그》, 2017-11-01
- ↑ 얇은생각, 〈싱글턴 디자인 패턴 : 정의, 개념, 구조 예시〉, 《개인 블로그》, 2019-10-05
- ↑ 5.0 5.1 YABOONG, 〈디자인패턴 - 싱글톤 패턴〉, 《개인 블로그》, 2018-09-28
- ↑ Seotory, <java singleton pattern(싱글톤 패턴)>, <<개인 블로그>>, 2016-03-19
- ↑ Lim-Ky, 〈(Design_Pattern) Singleton(싱글톤)의 고도화〉, 《개인 블로그》, 2017-07-27
참고자료
- 정인상, 채흥석, 〈자바 객체지향 디자인 패턴: UML과 GoF 디자인 패턴 핵심 10가지로 배우는〉, 《한빛 미디어》, 2014-04-28
- 싱글턴 패턴 위키백과 - https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4
- 정아마추어, 〈싱글톤 패턴(Singleton pattern)을 쓰는 이유와 문제점〉, 《개인 블로그》, 2017-11-01
- 얇은생각, 〈싱글턴 디자인 패턴 : 정의, 개념, 구조 예시〉, 《개인 블로그》, 2019-10-05
- Lim-Ky, 〈(Design_Pattern) Singleton(싱글톤)의 고도화〉, 《개인 블로그》, 2017-07-27
- YABOONG, 〈디자인패턴 - 싱글톤 패턴〉, 《개인 블로그》, 2018-09-28
- Seotory, 〈java singleton pattern(싱글톤 패턴)〉, 《개인 블로그》, 2016-03-19
같이 보기