게임 개발자라면 한번쯤은 들어본 싱글턴 패턴에대해 얘기해보자.
OOP에서 클래스의 인스턴스는 여러 개 존재할 수 있다.
단순히 new 연산자로 인스턴스를 생성해주면 그만이다.
하지만 필요에 의해서, 어떤 경우든 인스턴스가 최대 한개까지만 존재하도록 하고싶은 경우가 있을 수 있다.
싱글턴 패턴은 위와 같은 경우에 사용된다.
싱글턴 패턴이 사용되는 경우
먼저 인스턴스가 최대 한개까지만 존재해야 되는 경우를 생각해보자.
가장 대표적인 예로는 사용자 설정, 레지스트리, I/O 디바이스를 다룰 때 이다.
인스턴스가 여러개 존재하면 어떤 인스턴스의 설정 값, 혹은 멤버 변수를 참조할지 모호해지므로
무조건 최대 하나만 존재해야 한다.
알기 쉽게 롤을 예시로 들어보자
롤 클라이언트의 설정 값
롤에는 각 스킬별로 스마트키 설정 여부, 마우스 감도, 인터페이스 크기 등을 설정할 수 있는 창이 존재한다.
코드단으로 생각해보면, 이들의 값들과 값 변경 메소드들을 가지고 있는 Manager가 존재할것이다.
문제는 이 Manager 클래스의 인스턴스가 최대 한개만 생성되도록 하고 싶은데, 어떻게 해야할지 모르겠다.
public class SettingManager
{
// ???
}
그럼 반대로 생각해서, 이 클래스의 인스턴스를 여러개 만들고 싶으면 어떻게 해야 할까?
이에대한 답은 간단한데, 그냥 new 연산자로 마구마구 만들면 될 것이다.
그리고 new 연산자에 대응되는 생성자를 public으로 만들어 놓아야 할 것이다.
public 생성자
public class SomeClass
{
SettingManager manager1 = new SettingManager();
SettingManager manager2 = new SettingManager();
SettingManager manager3 = new SettingManager();
//...
}
public class SettingManager
{
public SettingManager()
{
}
}
생성자를 public으로 선언하게 되면, 클래스 외부에서도 마음대로 인스턴스 생성이 가능하다는것을 보았다.
그러면 생성자를 만약에 private으로 바꾸면 어떻게 될까?
private 생성자
public class SomeClass
{
SettingManager manager1 = new SettingManager(); // Error
SettingManager manager2 = new SettingManager(); // Error
SettingManager manager3 = new SettingManager(); // Error
//...
}
public class SettingManager
{
private SettingManager()
{
}
}
클래스 외부에서는 다시 인스턴스 생성이 불가능해진다.
다시, 클래스 외부에서도 인스턴스 생성이 가능하도록 코드를 바꾸려는데
이때, 조금의 트릭을 써보도록 하자.
바로 static 메소드를 이용하는 것이다.
static 메소드는 인스턴스에 속하는것이 아닌, 클래스에 속하는 메소드로
public이기만 하면 외부에서도 자유롭게 호출 할 수 있다.
static을 이용한 생성
public class SomeClass
{
SettingManager manager1 = SettingManager.GetInstance(); // 성공!
SettingManager manager2 = SettingManager.GetInstance(); // 성공!
SettingManager manager3 = SettingManager.GetInstance(); // 성공!
//...
}
public class SettingManager
{
private SettingManager()
{
}
public static SettingManager GetInstance()
{
return new SettingManager();
}
}
다시 SettingManager는 클래스 외부에서도 여러개의 인스턴스 생성이 가능해졌다.
이렇게 다시 원점으로 돌아왔는데, 한가지를 생각해보자.
우리에게는 static 메소드가 생겼다.
static 메소드는, 인스턴스가 아닌 클래스의 다른 static 멤버나 static 메소드에 접근할 수 있다.
우리가 가진 static 메소드에서는 무지성으로 new 연산자를 통해 인스턴스를 생성해주고있다.
이 static 메소드가 자신의 클래스가 인스턴스가 생성되었는지 판별하려면 어떻게 해줘야 할까?
바로 static 멤버로 인스턴스를 가지고 있도록 하면 될것이다.
코드는 다음과 같아진다.
싱글턴 패턴 완성
public class SomeClass
{
SettingManager manager1 = SettingManager.GetInstance(); // 성공!
SettingManager manager2 = SettingManager.GetInstance(); // 성공??
SettingManager manager3 = SettingManager.GetInstance(); // 성공??
//...
}
public class SettingManager
{
private static SettingManager uniqueInstance;
private SettingManager()
{
}
public static SettingManager GetInstance()
{
if (uniqueInstance == null) // 인스턴스가 아직 한번도 생성되지 않았다면
{
uniqueInstance = new SettingManager();
}
return uniqueInstance;
}
}
manager2, 3이 성공?? 인 이유는, 컴파일 에러는 안나지만 싱글턴 패턴과는 다른 의도로 쓰였기 때문이다.
manager1과 manager2, 3은 서로, 같은 static 멤버인 uniqueInstance를 가리키게 되어
이름을 굳이 나눌 필요가 없기 때문이다.
new 연산자는 uniqueInstance가 null일때만 호출되며 이를 Lazy Initialization이라고 한다.
이게 전부일까?
일반적인 상황에서는 위의 코드로 완벽한 싱글턴 패턴이라 할 수 있다.
다만, 멀티 쓰레드 환경에서는 오동작하는 코드인데,
서로 다른 쓰레드가 동시에 GetInstance 메소드를 호출 하는 경우에 Race Condition이 일어나
인스턴스가 두개 이상 생길 수 있는 위험이 있기 때문이다.
Race Condition을 해결해보자
우선, 우리에게 익숙한 방법인 lock을 통해 문제를 해결해보자.
문제가 되는 부분은 GetInstance 메소드이므로 이부분을 lock 해주면 되겠다.
public class SomeClass
{
SettingManager manager1 = SettingManager.GetInstance(); // 성공!
SettingManager manager2 = SettingManager.GetInstance(); // 성공??
SettingManager manager3 = SettingManager.GetInstance(); // 성공??
//...
}
public class SettingManager
{
private static SettingManager uniqueInstance;
private SettingManager()
{
}
[MethodImpl(MethodImplOptions.Synchronized)] // Synchronized Attribute
public static SettingManager GetInstance()
{
if (uniqueInstance == null) // 인스턴스가 아직 한번도 생성되지 않았다면
{
uniqueInstance = new SettingManager();
}
return uniqueInstance;
}
}
lock 키워드로 scope를 lock하는 대신, synchronized 애트리뷰트로 메소드 전체를 lock하였다.
이제 멀티 쓰레드 환경에서도 싱글턴 패턴을 사용할 수 있지만
한가지 마음에 걸리는 문제는 locking의 오버헤드이다.
GetInstance가 많이 호출될수록 오버헤드가 늘어나 성능에 영향이 있을 수 있다.
이 문제는 어떻게 해결할 수 있을까?
정답은 인스턴스를 필요할 때 생성하지 않고, 선언과 동시에 생성하는것이다.
public class SomeClass
{
SettingManager manager1 = SettingManager.GetInstance(); // 성공!
SettingManager manager2 = SettingManager.GetInstance(); // 성공??
SettingManager manager3 = SettingManager.GetInstance(); // 성공??
//...
}
public class SettingManager
{
private static SettingManager uniqueInstance = new SettingManager(); // 선언과 동시에 생성
private SettingManager()
{
}
public static SettingManager GetInstance()
{
return uniqueInstance;
}
}
부담이 되는 lock을 없애면서 Thread-Safe한 싱글턴 패턴이 완성되었다.
'Programming > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 6. 커맨드 패턴 (0) | 2022.03.22 |
---|---|
[디자인 패턴] 4. 팩토리 패턴 (0) | 2022.03.17 |
[디자인 패턴] 3. 데코레이터 패턴 (0) | 2022.03.17 |
[디자인 패턴] 2. 옵저버 패턴 (0) | 2022.03.10 |
[디자인 패턴] 1. 스트래티지 패턴 (0) | 2022.03.10 |