본문 바로가기

Backend/Java Design Pattern

[JAVA 디자인 패턴] 싱글톤 패턴 (Singleton Pattern)

728x90

* 싱글톤 패턴 (Singleton pattern)

싱글턴 패턴은 인스턴스가 오직 하나만 생성되는 것을 보장하도록 하고, 어디서든 이 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다. 이를 구현하고자, 생성자를 외부에서 호출할 수 없게 만들기 위해 생성자를 private하게 만든다.  

 이 패턴은 Database에 접근하고자 DAO객체를 만들때이고, Spring framework 에서 Bean을 만들때 기본적으로 Singleton으로 만들도록 하는 데 쓰인다. 

 


그렇다면, 싱글톤을 안쓸때의 문제와 써야 하는 상황을 Printer관리자를 만드는 것을 예를 들어 설명해보겠다. 

 

 사무실에 Printer가 하나가 있고, 여기에 접근하려는 사용자가 여러명이 있는 상황이다. 

이런 경우 Printer 객체는 단 하나만 존재하도록 보장해줘야 한다. 이것을 구현한다면 이렇다. 

class User{
	private String name;
	
	public User(String name) {
		this.name = name;
	}
	public void print() {
		Printer printer = Printer.getPrinter();
		printer.print(this.name + " print using " + printer.toString() +".");
	}
}
class Printer{
	private static Printer printer = null;// 선언 해두고. 
	private Printer() {
		
	}
	public static Printer getPrinter() {
		if(printer == null)
			printer = new Printer();
		return printer;
	}
	public void print(String str) {
		System.out.println(str);
	}
}

public class Main {
	private static final int User_NUM = 5;
	
	public static void main(String[] args) {
		User [] user = new User[User_NUM];
		for(int i = 0 ; i<User_NUM; i++) {
			user[i] = new User((i+1)+"-user");
			user[i].print();
		}
		
	}
}

User 5명이 printer객체에 접근하여 print메서드를 부르는 상황이다.

 

Printer 객체에서 getPrinter 메서드는 프린터 인스턴스가 이미 생성되어 있는지를 검사한다. 만약 처음 호출되어 아직 인스턴스가 생서되지 않은 상황이라면 생성자를 호출해 인스턴스를 생성한다. 이렇게 생성된 인스턴스는 정적 변수 printer에 의해 참조된다. 만약 인스턴스가 생성되었다면 printer 변수에서 참조하는 인스턴스를 반환한다. 

 

결과 출력

결과를 보면, 모든 같은 Printer 객체를 참조한것을 알 수 있다. 


 하지만, 이렇게 구현한것은 문제점이 있다. 단일 스레드 상에서는 상관없지만, 다중스레드가 접근한다면, 문제가 생긴다. 다음과 같은 상황을 생각해보자.

 

1) Printer 인스턴스가 아직 생성되지 않았을 때 스레드 1이 getPriner 메서드의 if 문을 실행해 이미 인스턴스가 생성되었는지 확인된다. 현재 printer 변수는  null 이다
2) 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전 스레드 2가 if문을 실행해 printer 변수가 null인지 확인한다. 현재 null이므로 인스턴스를 생성하는 코드가 실행도니다.
3) 스레드 1, 스레드 2마찬가지로 인스턴스를 생성하는 코드를 실행하게 되어 결과적으로 Printer클래스의 인스턴스가 2개 생긴다.

 

앞서서 구현한 방식을 race condition을 발생한 상황으로 구현해보겠다. Thread.sleep(1)을 넣어 인위적으로 충돌을 발생하겠다. 

class Printer{
	private static Printer printer = null;// 선언 해두고. 
	private Printer() {	}
	public static Printer getPrinter() {
		if(printer == null) {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {}
			printer = new Printer();
		}
		return printer;
	}
	public void print(String str) {
		System.out.println(str);
	}
}

class UserThread extends Thread{
	public UserThread(String name) {
		super(name);
	}
	
	@Override
	public void run() {
		Printer printer = Printer.getPrinter();
		printer.print(Thread.currentThread().getName()+ " print using " + printer.toString() + ".");
	}
}

public class Main {
	private static final int User_NUM = 5;
	
	public static void main(String[] args) {
		UserThread [] user = new UserThread[User_NUM];
		for(int i = 0 ; i<User_NUM; i++) {
			user[i] = new UserThread((i+1)+"-user");
			user[i].start();
		}
		
	}
}

결과 출력

결과를 보면, Printer 인스턴스가 여러개 만들어져 참조된것을 알 수 있다.


이런 문제를 해결하기위한 방법은 정적 클래스를 이용하거나 싱글턴 패턴을 이용하는 방법으로 해결할 수 있다. 

 

다음은 정적클래스를 사용하여 counter를 동기화하고, Printer 객체를 생성하지 않고 참조하도록 만든 방식이다. 

 

- 정적클래스 방식

class Printer {
	private static int counter = 0;
	public synchronized static void print(String str) { // 동기화
		counter++;
		System.out.println(str + counter);
	}
}

class UserThread extends Thread {
	public UserThread(String name) {
		super(name);
	}

	@Override
	public void run() {
		Printer.print(Thread.currentThread().getName() + " print using " + ".");
	}
}

public class Main {
	private static final int User_NUM = 5;

	public static void main(String[] args) {
		UserThread[] user = new UserThread[User_NUM];
		for (int i = 0; i < User_NUM; i++) {
			user[i] = new UserThread((i + 1) + "-user");
			user[i].start();
		}
	}
}

결과 출력

이렇게 객체를 전혀 생성하지 않고 메서드를 사용함으로써 , 아무 문제 없이 counter 변수가 안전하게 공유되도록 만들었다. 


다음으로 싱글톤 패턴으로 구현해 보겠다.

 

- 싱글톤 패턴 방식

 

class Printer {
	private static Printer instance = null;
	private static int counter = 0;

	private Printer() {}

	public synchronized static Printer getInstance() {
		if (instance == null)
			instance = new Printer();
		return instance;
	}

	public synchronized void print(String str) {
		counter++;
		System.out.println(str + counter);
	}
}

class UserThread extends Thread {
	public UserThread(String name) {
		super(name);
	}
	@Override
	public void run() {
		Printer printer = Printer.getInstance();
		printer.print(Thread.currentThread().getName() + " print using " + printer.toString() + ".");
	}

}

public class Main {
	private static final int User_NUM = 5;

	public static void main(String[] args) {
		UserThread[] user = new UserThread[User_NUM];
		for (int i = 0; i < User_NUM; i++) {
			user[i] = new UserThread((i + 1) + "-user");
			user[i].start();
		}
	}
}

결과 출력

 

결과적으로 Printer 객체 하나만 생성하였고, counter 변수도 동기화가 보장된 것을 알 수 있다,. 

 

* 싱글톤 패턴 만들기

1) 생성자를 private하게 만든다.

2) 인스턴스를 참조할 수 있는 getInstance()메서드를 만든다.  synchronized 키워드를 사용해 동기화를 보장한다. 

 

그렇다면 정적클래스 방식과 싱글톤 패턴 방식의 차이는 무엇일까?

 

1) 정적클래스는 생성자를 생성하지 않고 메서드를 사용한다.

2) 정적메서드를 사용하는것은 컴파일시 바인딩되는 것이 인스턴스 메서드를 사용하는것보다 성능이 우수하다.

3) 정적클래스는 인터페이스를 구현해야 하는경우에서는 사용할 수 없다. 

 

 

참고자료 : Java 객체지향 디자인 패턴, 한빛미디어, 정인상 채흥석 저, 2014

728x90