아마도 게임에서는 가장 유명하고 많이 다뤄지는 패턴이 오늘 다루는 바로 이 싱글턴 패턴이 아닐까 싶다.
그래서 놀랍게도 책에서는 싱글턴 패턴을 어떻게 해야 안 쓸 수 있는지에 대해 다룬다.
유니티를 다루는 대부분의 사람들이 알고 있는 패턴이니만큼 남용되는 경우도 부지기수라서 그런 것 같다.
싱글턴 패턴의 주요 특징
1. 전역 접근점을 제공
어디서든 싱글턴 패턴의 인스턴스를 접근할 수 있도록 만들어져있다.
따라서 대부분의 언어에서는 static
키워드로 구현하게 된다.
public class Singleton
{
// 인스턴스를 저장할 정적 변수
public static Singleton instance;
}
다음과 같이 구현하면 어느 코드에서든지
Singleton.instance
로 해당 인스턴스에 접근이 가능하다.
2. 오직 한 개의 클래스 인스턴스만 갖도록 보장
public class Singleton
{
// 인스턴스를 저장할 정적 변수
private static Singleton instance;
// 다른 클래스에서 인스턴스 생성을 막기 위한 private 생성자
private Singleton() { }
// 인스턴스에 접근하기 위한 메서드
public static Singleton GetInstance()
{
// 인스턴스가 없을 경우에만 생성
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
인스턴스가 여러 개면 작동하지 않는 상황을 위해 싱글턴 패턴이 고안된 것이기 때문에, 다음과 같이 클래스 내부에서 단 하나의 인스턴스만 갖도록 보장해줘야 한다.
싱글턴 패턴의 장점
싱글턴 패턴에 강력한 장점들이 있으니 이토록 많이 사용되는 것이라 생각한다.
장점들을 정리해보자면 다음과 같다.
장점 1) 한 번도 사용하지 않을 경우 인스턴스를 아예 생성하지 않는다.
처음 사용될 때 초기화가 되기 때문에, 전혀 사용하지 않으면 초기화가 이루어지지 않는다.
쓸데없는 메모리와 오버헤드가 발생하지 않는다는 것이다.
장점 2) 런타임에 초기화된다.
싱글턴은 최대한 늦게 초기화된다.
사용 직전에 초기화가 되는 것이다.
이러한 방식의 초기화를 게으른 초기화라고 한다.
이런 식의 게으른 초기화가 막아줄 수 있는 문제가 하나 있다.
정적 변수로만 초기화가 이루어질 경우 이 변수들의 초기화 순서는 매번 달라진다.
즉, 한 정적 변수가 다른 정적 변수에 의존하게 만들 수가 없다는 것이다.
이 경우 게으른 초기화를 사용하게 되면 런타임에 초기화가 이루어지기 때문에 정적 변수를 안전하게 참조할 수 있게 되는 것이다.
장점 3) 싱글턴을 상속할 수 있다.
싱글턴 패턴의 클래스를 상속받아 상황별 처리까지 가능하다.
또한 그렇게 만든 처리 코드를 원래 싱글턴 패턴의 코드 안으로 숨기는 효과까지 나니, 매우 강력한 기능이지 않을 수 없다.
싱글턴 패턴의 한계
사실, 싱글턴 패턴이 남용하면 안좋다지만 언제 안좋은지를 잘 몰랐었다.
그걸 해당 파트에서 정리하자면 이렇다.
전역 변수로부터 생기는 문제
유지보수를 쉽게 할 수 있는 코드를 작성하기 위해 디자인 패턴을 배워 사용하는 것인데, 전역 변수를 사용하는 것은 코드의 유지보수를 어렵게 만든다.
가장 큰 문제가 전역 변수가 들어가는 순간 코드의 흐름을 이해하기가 매우 어려워진다는 것이다.
전역 변수가 내 생각과 다른 방식으로 동작할 때, 이 전역 변수를 참조하는 모든 곳을 다 확인해봐야 한다는 것이다.
전역 변수는 또한 커플링을 조장한다.
어디서든 접근이 가능하기 때문에 두 모듈이 서로를 몰라야 한다라는 목적과 완전 반대로 가고 있는 것이다.
또한 멀티스레딩 환경에도 알맞지 않다.
각 스레드에서 동시에 전역 변수에 접근하려면 반드시 락을 잡아줘야 하고, 이는 곧 성능의 하락으로 이어지기 때문이다.
사용 목적에 비해 과도한 변형이 들어가는 문제
이게 무슨 소리냐면, 싱글턴은 항상 두 가지 특징을 가지고 있다는 점이다.
전역 접근, 하나의 인스턴스
문제는 내가 전역 접근 기능만 필요하던지, 하나의 인스턴스 기능만 필요하던지 할 수가 있다.
싱글턴 패턴은 해당 기능 하나만 가지고 있는 채로 구현할 수가 없기 때문에 다른 방법을 찾아야 한다.
이는 뒤에서 소개하도록 하겠다.
게으른 초기화로부터 발생하는 문제
게으른 초기화는 인스턴스가 최대한 늦게 초기화되기 때문에 가지는 장점이 있다고 앞에서 설명했다.
반대로 최대한 늦게 초기화되기 때문에 생기는 문제도 있다.
바로 초기화 시점을 내가 원하는 시점에 지정해줘야 될 때 그렇지 못한다는 점이다.
보통 게임에서는 메모리 최적화를 위해 메모리 단편화를 막는 방향으로 최적화를 하게 되는데, 그러기 위해선 메모리 할당 제어를 위해 초기화 시점을 내가 분명하게 지정해줘야 한다.
이럴 경우 싱글턴 패턴을 사용하면 최적화하기가 곤란해진다는 것이다.
싱글턴 패턴을 안쓰는 방법
각 상황마다 싱글턴 패턴을 쓰지 않고 문제를 해결하는 법에 대해 소개하겠다.
한 개의 인스턴스만 갖도록 보장하기
싱글턴 패턴이 해결해주는 문제 중 하나인데, 앞서 언급했듯이 전역 접근은 필요하지 않은 경우가 있다.
이럴 때 다음과 같은 방식으로 만들어줄 수 있다.
using System;
public class JustOne
{
private static bool _instantiated;
public JustOne()
{
if (_instantiated)
throw new Exception("JustOne 클래스의 인스턴스는 한 번만 생성할 수 있습니다.");
_instantiated = true;
}
~JustOne()
{
_instantiated = false;
}
}
대신 싱글턴 패턴은 컴파일 타임에 단일 인스턴스를 보장할 수 있는데, 위의 코드는 런타임에 확인한다는 것이 단점이겠다.
인스턴스에 쉽게 접근하도록 만들기
여러 방법이 있겠다.
나열하자면 다음과 같다.
넘겨주기
: 그냥 함수의 인수로 넘겨주면 되는 것 아닌가?
상위 클래스에서 가져오기
: 보통 상속받은 객체가 상위 클래스로부터 받은 protected 메서드로 구현하는 방식을 쓴다 (하위 클래스 샌드박스 패턴)
이미 전역인 객체에 빌붙기
: 기존 전역 객체에 변수를 추가하는 방식으로 만들어버린다면?
싱글턴 대신 다른 디자인 패턴 사용하기
하위 클래스 샌드박스 패턴
클래스가 같은 인스턴스들이 공용 상태를 전역으로 만들지 않고도 접근할 수 있는 방법을 제공한다.
서비스 중개자 패턴
이 패턴도 객체를 전역으로 접근할 수 있는 방법이다. 싱글턴 패턴보다 객체를 훨씬 유연하게 설정할 수 있다고 한다.
Uploaded by N2T
'개발 > 게임 디자인 패턴' 카테고리의 다른 글
게임 디자인 패턴 7. 이중 버퍼 패턴 (Double Buffer Pattern) (0) | 2023.10.31 |
---|---|
게임 디자인 패턴 6. 상태 패턴 (State Pattern) (1) | 2023.10.30 |
게임 디자인 패턴 4. 프로토타입 (Prototype) (0) | 2023.10.26 |
게임 디자인 패턴 3. 관찰자 패턴 (Observer Pattern) (0) | 2023.10.25 |
게임 디자인 패턴 2. 경량 패턴 (Flyweight Pattern) (0) | 2023.10.24 |