요깨비's LAB

[Design Pattern, JAVA, 생성 패턴] Singleton Pattern - 인스턴스를 한개만 만들기 본문

컴퓨터 공학기초/디자인패턴

[Design Pattern, JAVA, 생성 패턴] Singleton Pattern - 인스턴스를 한개만 만들기

요깨비 2019. 12. 9. 02:50

1. 싱글턴 패턴이란?
프로그램을 실행할 때 보통은 많은 인스턴스가 생성됩니다. 예를 들어 java.lang.String 클래스의 인스턴스는 문자열 1개 당
1개씩 생성되기 때문에 문자열 1000개에 대해서 1000개의 인스턴스가 만들어집니다.

하지만 클래스의 인스턴스가 단 '하나만' 필요한 경우도 있습니다. 즉, 

  • 전역 변수를 사용하지 않고 객체를 하나만 생성하고, 이 객체를 어디서든지 참조할 수 있는 패턴

  • 지정한 클래스의 인스턴스가 1개밖에 존재하지 않는 것은 '보증'하는 패턴

  • 인스턴스가 1개 밖에 존재하지 않는 것을 프로그램 상에서 표현하고 싶을 때 사용하는 패턴

으로 정리할 수 있습니다.

* 생성(Creational) 패턴
- 객체 생성에 관련된 패턴
- 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 
유연성을 제공한다.

클래스 다이어그램

예시 1. 

public class Singleton {
	private static Singleton singleton = new Singleton();
	
	private Singleton() {
		System.out.println("인스턴스를 생성했습니다.");
	}
	
	public static Singleton getInstance() {
		return singleton;
	}
}

 

public class Main {
	public static void main(String[] args ) {
		System.out.println("Start.");
		Singleton obj1 = Singleton.getInstance();
		Singleton obj2 = Singleton.getInstance();
		
		if(obj1 == obj2) {
			System.out.println("obj1과 obj2는 같은 인스턴스입니다.");
		}else {
			System.out.println("obj1과 obj2는 다른 인스턴스입니다.");
		}
		System.out.println("End...");
	}
}

 

 

예시 2.

프린터 관리자 만들기

  • 프린트 하나를 10명이 공유해서 사용한다고 가정

public class Printer {
	public Printer() { }
	public void print(Resource r) { ... }
}
  • Printer 클래스를 사용해 프린터를 이용하려면 Client에서 new Printer()가 반드시 한 번만 호출되도록 주의해야 한다.

  • 이를 구현하는 방법은 생성자를 외부에서 호출할 수 없게 하는 것이다.

    - Printer 클래스의 생성자를 private로 선언

public class Printer {
	private Printer() { }
	public void print(Resource r) { ... }
}
  • 싱글톤 프린터에 대한 인스턴스를 스스로 만들어서 외부로부터의 생성은 막고, 외부로 제공해줄 메서드가 필요하다.

public class Printer {
	// 외부에 제공할 자신의 인스턴스
	private static Printer printer = null;

	private Printer() { }
	// 자기 자신의 인스턴스를 외부에 제공
	public static Printer getPrinter() {
		if(printer == null) {
			// Printer 인스턴스 생성
			printer = new Printer();
		}
		return printer;
	}
	
	// 만약 new Printer()가 호출되기 전이면 인스턴스 메서드인 print()메서드는 호출할 수 없다.
	public void print(String str) {
		System.out.println(str);
	}
}
  •  Client에서의 사용

public 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());
	}
}

public class Client {
	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 인스턴스 생성
			user[i] = new User((i+1));
			user[i].print();
		}
	}
}

 

문제점
다중 스레드에서 Printer 클래스를 이용할 때 인스턴스가 1개 이상 생성되는 경우가 발생할 수 있습니다.
- Race Condition을 발생시키는 경우
1. Printer 인스터스가 아직 생성되지 않았을 때 스레드 1이 getPrinter 메서드의 if문을 실행해 이미 인스턴스가 생성되었는지 확인한다.
현재 printer 변수는 null인 상태다.
2. 만약 스레드 1이 생성자를 호출해 인스턴스를 만들기 전 스레드 2가 if문을 실행해 printer 변수가 null인지 확인한다.
현재 printer 변수는 null이므로 인스턴스를 생성하는 생성자를 호출하는 코드를 실행하게 된다.
3. 스레드 1도 스레드 2와 마찬가지로 인스턴스를 생성하는 코드를 실행하게 되면 결과적으로 2개의 Printer가 만들어진다.

  • 스레드 스케줄링을 바꿔서 경합 조건을 만들어 보겠습니다.

public class Printer {
	// 외부에 제공할 자기 자신의 인스턴스
	private static Printer printer = null;
	private Printer() { }

	// 자기 자신의 인스턴스를 외부에 제공
	public static Printer getPrinter() {
		// 문제의 원인
		if(printer == null) {
			try {
				// 스레드 스케줄링 변경
				Thread.sleep(1);
			}catch(InterruptedException e) { }
	
			// Printer 인스턴스 생성
			printer = new Printer();
		}
		return printer;
	}

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

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 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++) {
        	// UserThread 인스턴스 생성
            user[i] = new UserThread((i+1));
            user[i].start();
		}
	}
}

 

해결책
Lazy Initialization은 다중 스레드 애플리케이션이 아닌 경우에는 아무런 문제가 되지 않습니다.

  • 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법 (Eager Initialization)

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);
	}
}

 

  • 인스턴스를 만드는 메서드에 동기화하는 방법(Thread-Safe initialization)

public class Printer {
	private static Printer printer = null;
	private int counter = 0;
	private Printer() { }

	// 인스턴스를 만드는 메서드 동기화 (임계 구역)
	public synchronized static Printer getPrinter() {
		if(printer == null) {
			printer = new Printer();
		}

		return printer;
	}

	public void print(String str) {
		// 오직 하나의 스레드만 접근을 허용함(임계 구역)
		// 성능을 위해 필요한 부분만을 임계 구역으로 설정한다.
		synchronized(this) {
			counter++;
			System.out.println(str + counter);
		}
	}
}

 

  • Enum 클래스를 이용한 구현

임의대로 짜봤습니다. 혹시 아예 잘못되었거나 고쳐야할 곳이 있으면 알려주세요.

public enum Printer {
	Instance;

	public void print(String str) {
		System.out.println(str);
	}
}
  • Enum 클래스를 이용하여 싱글톤을 구현하면 Thread-Safety와 Serialization을 보장합니다.
    * 여기에서 Serialization을 보장한다는 말은 "직렬화가 자동으로 처리되고 직렬화가 아무리 복잡하게 이루어져도 여러 객체가 생길 일이 없다"는 것을 의미합니다.

  • Reflection을 통해 싱글톤을 깰 수 없습니다.

 

* 관련 패턴

아래의 패턴은 인스턴스가 하나인 경우가 많습니다.

  1. Abstract Factory 패턴
  2. Builder 패턴
  3. Facade 패턴
  4. Prototype 패턴

한빛 미디어 - JAVA 객체지향 디자인패턴
영진닷컴 - JAVA 언어로 배우는 디자인 패턴 입문

Comments