싱글톤패턴
싱글톤패턴(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) 안에서 예외처리를 할 수 있다는 점이 다르다.인용 오류: <ref>
태그를 닫는 </ref>
태그가 없습니다 누군가 작성한 코드를 원본 수정없이 작업해야 할 때 이용한다. 아래의 예제는 기존에 작성했던 싱글톤 패턴의 인트턴스를 받아와 자바의 reflection의 constructor의 setAccessible 메소드를 활용해 설령 class의 생성자가 private일지라도 강제로 가져와서 새로운 인스턴스 생성이 가능하게 만든다. 결국 싱글톤을 깨뜨리는 것이다. 실행결과를 보면 hashcode() 메소드의 리턴값이 다른 것을 확인 할 수 있다.[5]
<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
활용
다중 스레드 상황[1]
프린터 관리자 만들기 (늦은 초기화 방식)
아래의 나오는 예제들은 싱글톤 패턴의 각 방식들이 가지는 문제점을 나타내며 어떻게 보완할 수 있는지 말해준다. 멀티 스레드 환경에서 늦은 초기화 방식에 의해 생기는 문제점인 인스턴스가 여러개 생성되는 점을 클래스 로더를 활용한 이른 초기화 방식과 메소드 동기화를 통한 스레드 안전한 늦은 초기화 방식으로 보완한 예제이다.
- 문제점 : 프린터 인스턴스를 하나만 만들고 여러곳에서 동일한 프린터를 사용하게 끔 하는게 목적이지만 스레드가 작용하면서 실행결과에서 볼 수 있듯이 각각 다른 인스터스에서 참조된 것을 알 수 있다. 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.
각주
- ↑ 1.0 1.1 정인상, 채흥석, 〈자바 객체지향 디자인 패턴: 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
- ↑ 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
같이 보기