게임 프로그래밍 패턴 6장
싱글턴
싱글턴 패턴은 클래스의 인스턴스가 하나만 유지되고, 인스턴스에 대한 전역 접근을 제공하는 패턴이다.
책에서는 싱글턴 패턴의 남용에 대해 주의할 것을 당부한다.
프로그래밍 업계가 C에서 객체지향 프로그래밍으로 넘어가던 시절에 닥친 문제 하나는 '어떻게 하면 원하는 인스턴스에 접근할 수 있는가'였다. 호출하고 싶은 메서드는 있는데, 그 메서드를 제공하는 객체 인스턴스에 쉽게 접근할 방법이 없었다. 이럴 때 싱글턴, 다시 말해 전역 변수는 쉬운 해결책이었다. - 게임프로그래밍 패턴
싱글턴의 성격
1. 한 개의 클래스 인스턴스
싱글턴은 하나의 클래스에 하나의 인스턴스만 존재한다.
예를 들어, 파일 시스템을 만든다고 할 때, 한 파일에 대한 작업이 다른 곳에서 간섭받지 않도록 해야한다. 이를 위해 파일 시스템 클래스를 싱글턴으로 만들어 파일 시스템에 대한 접근이 하나의 인스턴스에서만 이루어지도록 보장한다.
2. 전역 접근
싱글턴은 유일한 인스턴스에 대해 전역적은 접근을 제공한다.
위에서 예로 든 파일 시스템은 게임 내 여러 위치에서 필요할 수 있다. 인스턴스에 전역 접근이 가능한 메서드를 제공한다.
# Singleton.cs
public class Singleton
{
private static Singleton _instance;
public static Singleton Instance
{
get
{
// 게으른 초기화
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
싱글턴의 문제
싱글턴 패턴의 남용은 다음과 같은 문제가 있다.
전역 변수가 초래하는 복잡성
local static variable과 non-local variable에 접근하지 않는 함수를 순수 함수라고 한다. 순수 함수에서는 문제를 해결하기 위해 해당 함수 코드와 매개변수만 확인하면 된다.
반면, 어디서든 접근할 수 있는 전역 인스턴스를 사용하면 문제를 발생시키는 곳을 찾기 위해 인스턴스에 대한 모든 접근점을 살펴봐야 한다. 갈수록 커플링이 심해지고, 수정이 어려워진다.
멀티쓰레딩 환경
전역 인스턴스는 모든 쓰레드에서 접근할 수 있는 영역이다. 쓰레드 간 전역 데이터 접근에 대한 관리가 제대로 이루어지지 않으면 교착상태, 경쟁상태에 빠질 수 있다.
이와 같은 문제는 C#에서 lock 키워드를 사용한 쓰레드 동기화로 해결할 수 있다.
# Singleton.cs
public class Singleton
{
private static Singleton _instance;
private static readonly object syncLock = new object();
public static Singleton Instance
{
get
{
lock (syncLock)
{
if (_instance == null)
{
_instance = new Singleton();
}
}
return _instance;
}
}
}
싱글턴의 대안
추후 원만한 유지 보수를 위해서 다음과 같은 대안을 적용할 수 있을지 항상 고민해 보자. (수많은 Manager, Controller, System의 이름을 띤 싱글턴 인스턴스를 줄이기 위해..)
클래스가 꼭 필요한가?
# Bullet.cs
public class Bullet
{
private int _x;
private int _y;
public int X
{
get => _x;
set => _x = value;
}
public int Y
{
get => _y;
set => _y = value;
}
}
# BulletManager.cs
public class BulletManager
{
public Bullet Create(int x, int y)
{
var bullet = new Bullet();
bullet.X = x;
bullet.Y = y;
return bullet;
}
public bool IsOnScreen(Bullet bullet)
{
return bullet.X >= 0 &&
bullet.Y >= 0 &&
bullet.X < SCREE_WIDTH &&
bullet.Y < SCREEN_HEIGHT;
}
}
위와 같이 관리자를 위한 관리자 클래스가 존재하는 경우가 있다. 단순히 다른 클래스에 도움을 주는 기능만 있는 경우 해당 기능을 본래 클래스에 담아두면 굳이 싱글턴으로 구현할 필요가 없다.
# Bullet.cs
public class Bullet
{
private int _x;
private int _y;
public Bullet(int x, int y)
{
_x = x;
_y = y;
}
public bool IsOnScreen()
{
return _x >= 0 &&
_y >= 0 &&
_x < SCREE_WIDTH &&
_y < SCREEN_HEIGHT;
}
}
오직 한 개의 클래스 인스턴스만 갖도록 보장하기
전역 접근은 필요없고, 한 개의 인스턴스만 갖도록 보장하는 경우 굳이 싱글턴으로 구현하지 않는다. 아래 코드에서는 클래스의 인스턴스가 존재하는데, 새로 생성을 할 경우 Assert에 걸리도록 되어 있다.
public class SingleInstance
{
private static bool _instantiated = false;
public SingleInstance()
{
Debug.Assert(_instantiated);
_instantiated = true;
}
~SingleInstance()
{
_instantiated = false;
}
}
인스턴스에 쉽게 접근하기
1. 넘겨주기 : 필요로하는 객체를 함수의 파라미터로 전달한다.
2. 상위 클래스로부터 얻기 : 부모 클래스가 갖고 있는 객체에 접근하여 사용한다.
3. 이미 전역인 객체로부터 얻기 : GameController와 같이 반드시 사용하는 싱글턴 객체를 통해 접근한다.
참고
1) [SINGLETON] 멀티스레드 환경에서의 싱글턴 - https://daddygoms.tistory.com/484
2) MSDN lock 문(C# 참조) - https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/lock-statement
'Design Pattern' 카테고리의 다른 글
상태 (State) (0) | 2020.11.25 |
---|---|
프로토타입 (Prototype) (0) | 2020.07.19 |
관찰자 (Observer) (0) | 2020.07.05 |