Design Patterns - Singleton Pattern

2021. 1. 23. 13:00Developments/Design Pattern

디자인 패턴이란?

  디자인 패턴이란 객체지향 프로그램을 설계할 때, 자주 발생하는 문제점들을 보완하고 개선하기 위해 고안된 패턴입니다. 디자인 패턴에는 수많은 종류가 있는데, 프로그래밍 언어의 특성, 소프트웨어의 특성, 개발 편의 등 여러가지 요소에 의해 어떤 디자인 패턴을 사용하는 것이 가장 적합한지 정해집니다.

  제가 현재 공부 중인 JAVA를 기준으로만 봐도 Factory Pattern, Singleton Pattern, Prototype Pattern, Structural Design Pattern Adapter Pattern 등 무수히 많이 존재합니다. 이제부터 공부하면서 하나씩 기록해 보겠습니다.


Singleton Pattern

  Singleton Pattern이란 이름에서도 유추할 수 있듯이 객체를 오직 한 개만 생성하여 다른 클래스에서도 그 한 개의 객체만을 사용하도록 하는 방법입니다. 이런 디자인 패턴이 당연히 필요한 경우도 있고, 오히려 방해되는 경우도 있겠죠?

  아주 간단한 예를 들어 보자면 프로그램의 Log를 남기는 Logger로 생각해 보겠습니다. Logger같은 경우에는 어느 부분에서 어떤 오류 혹은 경고가 발생했는지 시간과 함께 기록을 해주는 역할을 합니다. 따라서 객체가 한 개만 있어도 원하는 기능을 다 할 수 있고, 오히려 객체가 여러 개가 있다면, 로그가 중복으로 기록된다거나 혹은 어떤 객체가 기록을 해야할 지 선택해야 하므로 오버헤드가 발생합니다. 결과는 시간과 오류 사항을 기록하는 것으로 똑같은데 말이죠. 이런 경우 Singleton Pattern을 이용하여 소프트웨어를 설계하시는 것이 바람직합니다.

 

  그럼 Singleton Pattern을 제가 현재 공부 중인 JAVA를 이용하여 코드로 구현해 보겠습니다.


Logger.java

package design.singleton;

import java.time.LocalDateTime;

public class Logger {

	// static block
	static {
		// do something
		logger = new Logger();
		// do something
	}

	private static final Logger logger;

	private Logger() {

	}

	public static Logger getInstance() {
//		if (logger == null) {
		// do something
//			logger = new Logger();
		// do something
//		}
		return logger;
	}

	public void log(String message) {
		LocalDateTime ldt = LocalDateTime.now();
		String date = ldt.getYear() + "/" + ldt.getMonthValue() + "/" + ldt.getDayOfMonth();
		String time = ldt.getHour() + ":" + ldt.getMinute() + ":" + ldt.getSecond() + ":" + ldt.getNano();
		System.out.println("[" + date + " " + time + "] " + message);
	}
}

 

  일반 적으로 어떤 클레스의 객체를 생성할 때 public Logger logger = new Logger();와 같은 방법으로 많이 생성하는데,

Singleton Pattern에서는 객체를 오직 한개만 생성할 수 있게끔 제한해야하므로 사용자가 생성자에 접근하지 못하도록 위 코드와 같이 생성자를 private으로 설정해 주었습니다. 따라서 미리 private으로 객체를 생성해둡니다. 그렇다면 어떻게 사용자가 이 객체에 접근할 수 있을까요? 사용자가 접근하지 못하는 객체는 어디에서도 못 쓰인다는 말이니까 필요없는 클레스가 되겠죠? 따라서 사용자가 객체에 접근할 수 있게끔 getInstance()라는 멤버 함수를 public만들어 줍니다.

 

  그런데 위 코드를 보시면 static 블럭에서 객체를 초기화(사전 초기화) 해주거나, 주석처리되어있지만 getInstance() 함수에서 객체를 초기화(사후 초기화) 해줍니다. 물론 객체를 선언할 때 바로 생성을 해줘도 위 예시에서는 상관 없지만, 만약 복잡한 프로그램을 설계한다면, 그리고 객체를 생성하기 전후로 어떤 작업을 해야한다면 static 블럭 혹은 getInstacne() 함수에서 따로 초기화 해줍니다.

  static 블럭에서 초기화 해주는 사전 초기화란 인스턴스를 호출하지 않아도 클레스가 로딩될 때 최초 1회 초기화하는 방법으로 멀티쓰레드 환경에서 이중 객체 생성의 문제를 걱정하지 않아도 되지만, 객체를 사용하지 않는다면 메모리 효율이나 연산 효율이 낮아집니다.

  getInstacne() 함수에서 초기화 해주는 사후 초기화란 인스턴스를 실제로 사용할 시점에서 인스턴스를 생성하는 방법입니다. 세심한 방법을 쓰지 않으면 위에 언급한 이중 객체 생성 문제가 발생할 가능성이 높으나, 인스턴스를 실제로 사용하지 않는다면 메모리와 연산량을 아낄 수 있다는 장점이 있습니다.

 


UseLogger.java

package design.singleton;

public class UseLogger {
	public void log(String message) {
		Logger logger = Logger.getInstance();
		logger.log("UseLogger - log() " + message);
	}
}

  위 코드는 Logger 클레스를 사용자가 사용하기 편하게끔 public으로 만들어 놓은 클레스입니다. 

 


Test.java

package design.singleton;

public class Test {

	public static void main(String[] args) {
		 //Logger logger = new Logger();
		//String s = Logger.getInstance();
		Logger logger = Logger.getInstance();
		logger.log("Hello World!");
		
		UseLogger useLogger = new UseLogger();
		useLogger.log("Hello World!");
	}

}

  Test를 보시면 사용자가 직접 logger를 선언하여 LoggergetInstance() 함수를 이용하여 Logger 클레스를 이용할 수도 있지만 UserLogger 클레스를 이용한다면 더욱 편하겠죠?

  위 Test.java를 실행해 보시면 다음과 같은 결과를 얻을 수 있습니다.

Console


Singleton Pattern의 장단점

  앞서 Singleton Pattern의 특성을 알아보았듯이 키보드 리더, 프린터 스풀러, 점수기록표 등 클래스의 객체를 하나만 만들어야 하는 경우 사용됩니다. 클래스 내에서 인스턴스가 단 하나뿐임을 보장하므로, 프로그램 전역에서 해당 클래스의 인스턴스를 바로 얻어서 쉽게 데이터를 공유할 수 있고, 불필요한 메모리 낭비를 최소화할 수 있습니다.

  하지만 Singleton Pattern의 문제점은 객체가 오직 한 개밖에 없어서 그 객체가 너무 많은 작업을 하거나 너무 많은 데이터를 갖고 있는 경우 객체 지향 프로그래밍의 설계 원칙에 어긋나게 됩니다. 따라서 규모가 커질수록 코드의 양이 많아지고 수정이 어려워집니다. 따라서 적당한 규모의 프로젝트에서 Singleton Pattern을 잘 활용한다면 매우 효율적인 프로그램을 만들 수 있습니다.

'Developments > Design Pattern' 카테고리의 다른 글

Design Patterns - Factory Method Pattern  (0) 2021.02.04
Design Patterns - Method Chaining Pattern  (0) 2021.02.04