본문 바로가기
개발/게임 디자인 패턴

게임 디자인 패턴 5. 싱글턴 패턴 (Singleton Pattern)

by 석시 2023. 10. 27.



아마도 게임에서는 가장 유명하고 많이 다뤄지는 패턴이 오늘 다루는 바로 이 싱글턴 패턴이 아닐까 싶다.

그래서 놀랍게도 책에서는 싱글턴 패턴을 어떻게 해야 안 쓸 수 있는지에 대해 다룬다.

유니티를 다루는 대부분의 사람들이 알고 있는 패턴이니만큼 남용되는 경우도 부지기수라서 그런 것 같다.


싱글턴 패턴의 주요 특징

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

대신 싱글턴 패턴은 컴파일 타임에 단일 인스턴스를 보장할 수 있는데, 위의 코드는 런타임에 확인한다는 것이 단점이겠다.

인스턴스에 쉽게 접근하도록 만들기

여러 방법이 있겠다.

나열하자면 다음과 같다.

  1. 넘겨주기 : 그냥 함수의 인수로 넘겨주면 되는 것 아닌가?
  1. 상위 클래스에서 가져오기 : 보통 상속받은 객체가 상위 클래스로부터 받은 protected 메서드로 구현하는 방식을 쓴다 (하위 클래스 샌드박스 패턴)
  1. 이미 전역인 객체에 빌붙기 : 기존 전역 객체에 변수를 추가하는 방식으로 만들어버린다면?

싱글턴 대신 다른 디자인 패턴 사용하기

하위 클래스 샌드박스 패턴

클래스가 같은 인스턴스들이 공용 상태를 전역으로 만들지 않고도 접근할 수 있는 방법을 제공한다.

게임 디자인 패턴 8. 하위 클래스 샌드박스 (Subclass Sandbox Pattern)
하위 클래스 샌드박스 패턴언제 써야 하는가?깨지기 쉬운 상위 클래스 (Fragile Base Class) 하위 클래스 샌드박스는 서로 상속 관계에 있는 두 클래스 사이에서 부모와 자식 어느 곳에 주요 기능을 넣을 건지에 대한 것을 이야기 한다. 책에서는 다음과 같이 이야기하고 있다. 상위 클래스가 제공하는 기능들을 통해서 하위 클래스에서 행동을 정의한다. 하위 클래스 샌드박스 패턴 캐릭터마다 다른 효과를 가진 공격 기능을 구현한다고 해보자. 부모 클래스가 되는 공격 클래스를 구현하고 그것을 상속받아 각 캐릭터 공격 클래스를 구현한다고 해보자. 각 캐릭터 공격마다 아주 열심히 상세하게 클래스를 작성하면 다음과 같은 문제들이 발생한다. 중복 코드가 많아진다.거의 모든 게임 코드가 내가 작성하는 클래스와 커플..
https://seoksii.tistory.com/75

서비스 중개자 패턴

이 패턴도 객체를 전역으로 접근할 수 있는 방법이다. 싱글턴 패턴보다 객체를 훨씬 유연하게 설정할 수 있다고 한다.

게임 디자인 패턴 12. 서비스 중개자 패턴 (Service Mediator Pattern)
“어디서든” 접근을 할 수 있게 만드려면 보통은 정적 클래스나, 싱글톤 패턴을 생각하게 될 것이다. 하지만 이는 강한 커플링을 발생시킨다.개인정보가 모두에게 노출되는 느낌이랄까? 따라서 전역 접근이 가능한 서비스를 한 단계 숨겨서 구체적인 내용은 숨긴 채 서비스를 이용만 할 수 있도록 구현한 패턴이 오늘 정리할 이 서비스 중개자 패턴이다. 서비스 중개자의 형태 몬스터 스폰 서비스를 만든다고 해보자. class MonsterSpawnService { // 대충 스폰하는 기능들 } (작성중) Uploaded by N2T
https://seoksii.tistory.com/79


Uploaded by N2T