본문 바로가기
Unity3D/Design Patterns

유니티 C# 싱글턴 패턴 + Lazy를 이용한 버전

by 니키티스 2020. 8. 30.

2023.08.08 추가: 본문에 존재하는 코드(특히 Lazy를 사용한 코드!)는 충분히 검증된 코드는 아닙니다. 따라서 아이디어는 채용하되, 직접 복사-붙여넣기할 경우에 오류가 발생하지 않는다는 보장은 못 드립니다 ㅠ

 

1. 싱글톤이란?

싱글톤(Singleton)은 게임 개발 시 가장 많이 사용되는 패턴 중 하나입니다. 우선 싱글턴이 무엇인지부터 알아봐야겠지요.

 

오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공합니다.(『GOF의 디자인 패턴』 181쪽)

 

정의에 따르면 싱글톤은 단 하나의 클래스 인스턴스만을 갖도록 보장합니다.

전역적인 접근점을 제공한다는 것은 어디에서나 해당 클래스의 인스턴스에 접근할 수 있다는 뜻입니다.

 

유니티에 적용을 해보자면 게임을 관리하는 매니저(Manager) 계열의 클래스에 적합하겠죠.

이들은 게임을 관리하기 위해 딱 하나만 필요하고 어디에서나 쉽게 호출되어 참조될 수 있기 때문이죠.

 

예를 들어 농구 게임에서 스코어를 얻을 때, 스코어를 관리하는 GameManager 클래스의 인스턴스가 여러 개가 되어버리면 안 되겠죠. 그렇게 되면 어느 인스턴스로부터 스코어를 얻어야 할지 모르게 되니까요.

 

또한 현재 소리의 크기를 갖고 있는 SoundManager 클래스의 인스턴스는 사운드를 재생할 때마다 확인해야 하니, 어디에서나 접근할 수 있어야 합니다. 소리를 재생하려는데 누가 재생할지 모르니, 현재 소리의 크기는 누구나 알 수 있어야겠죠!

 

싱글톤의 '오직 한 개의 인스턴스', '전역적인 접근점'은 바로 이런 뜻입니다.

 

사용할 만한 곳을 찾아보자면 앞에서 말한 게임의 점수나 흐름을 담당하는 GameManager, 사운드 크기와 사운드 재생 이벤트를 갖는 SoundManager 등이 있겠죠.

 

이렇게 되면 어디에서나 접근할 수 있으니 오브젝트마다 GamaManager나 SoundManager 오브젝트를 할당할 필요가 없으니 무척 편리하겠죠. 잘못 오브젝트를 할당할 일도 없고 귀찮음도 덜으니 이만한 패턴이 없는 것처럼 느껴집니다.

 

이 뿐만 아니라 장점이 더 있습니다.

 

(1) 한 번도 사용하지 않는다면 아예 인스턴스를 생성하지 않습니다.

그 말인즉 게임 내내 쓸 일이 없다면 인스턴스를 생성하고, 멤버 변수로 가지느라 CPU와 메모리에 부담을 주지 않아도 된다는 것입니다. 한 번이라도 사용할 때에만 생성하니 이는 최적화에 장점이 됩니다.

 

이렇게 할 수 있는 것은 싱글톤은 게으른 초기화(Lazy initialization)를 사용하기 때문입니다. 처음에는 생성하지 않고 있다가 처음 접근하게 되면 그때 객체를 생성하는 방식을 사용하므로, 나중에 필요해지면 그때 연산의 부담을 지면 됩니다.

 

(2) 싱글톤은 상속할 수 있습니다.

싱글톤 클래스를 하나 만들어두고 이를 상속하면 하위 클래스의 오브젝트도 모두 싱글톤이 됩니다.

그리고 중요한 건 제네릭(Generic)을 활용할 때 나타납니다.

싱글톤을 한 번만 만들어두면 상속만 하면 싱글톤 오브젝트가 되므로 어디에서나 쉽고 간편하게 사용할 수 있습니다.

 

public class Singleton<T>
{
    /* 싱글톤 클래스 구현, 자세한 구현은 아래에 실어놓았음 */
}

public class OtherClass : Singleton<OtherClass>
{
    // 싱글톤을 구현하지 않아도 Singleton의 기능을 그대로 이어받는다
}

 

하지만 무엇이든 장점이 있으면 단점도 있기 마련입니다. 이건 조금 있다 설명드릴게요.

 

2. 구현

유니티에서 싱글톤 패턴을 만드는 방법은 여러 가지입니다.

 

아래 글에서 다양한 방법을 잘 정리해놓았으므로 링크를 실어놓을게요.

https://unityindepth.tistory.com/38

 

싱글톤 패턴의 특징은 (1) 오직 한 개의 인스턴스만을 갖는다는 점과 (2) 전역적으로 접근할 수 있다는 점입니다.

 

전역적으로 접근할 수 있게 하기 위해 인스턴스를 정적 멤버 변수(static)로 저장해 두고 이를 불러오는 방식을 사용합니다. 이를 구현한 것은 다음과 같은 방식들이 있습니다.

 

(1) MonoBehaviour가 아닐 때의 싱글톤

일반적으로 사용하는 싱글톤입니다. MonoBehaviour를 상속하지 않으므로 게임 오브젝트가 아닐 때 유용합니다.

구현은 간단합니다. instance가 존재하면 그것을 반환하고, 아니면 새로 생성한 instance를 반환합니다.

Log를 관리하는 클래스나 DB를 관리하는 클래스 등에게 유용하겠네요.

 

public class Singleton
{
    private static Singleton instance;
    public static Singleton GetInstance()
    {
        // 만약 instance가 존재하지 않을 경우 새로 생성한다.
        if (!instance)
        {
            instance = new Singleton();
        }
        // instance를 반환한다.
        return instance;
    }
    
    public void DoSomething()
    {
    	Debug.Log("Singleton : DoSomething!");
    }
}

// 예시
public class ExampleClass : MonoBehaviour
{
	private void Start()
    {
    	Singleton singleton = Singleton.GetInstance();
        singleton.DoSomething();
        // 로그 창에 "Singleton : DoSomething"이 출력된다.
    }
}

 

여기서 GetInstance() 대신 C#의 기능인 프로퍼티를 이용하면 좀 더 깔끔하게 정리할 수 있습니다.

public class Singleton
{
    private static Singleton _instance;
    public static Singleton Instance
    {
        get
        {
            // 만약 _instance가 존재하지 않을 경우 새로 생성한다.
            if (!_instance)
            {
                _instance = new Singleton();
            }
            // _instance를 반환한다.
            return _instance;
        }
    }
}

// 예시
public class ExampleClass : MonoBehaviour
{
	private void Start()
    {
    	Singleton.Instance.DoSomething();
        // 로그 창에 "Singleton : DoSomething"이 출력된다.
    }
}

C#에서만 사용 가능한 방법으로, 구현은 동일하지만 프로퍼티로 호출할 수 있어 보다 간편합니다.

개인적으로 괄호를 사용하지 않아 제겐 프로퍼티가 마음에 드네요.

(2) 일반적인 싱글톤 (MonoBehaviour)

만약 MonoBehaviour를 상속하여 유니티 내에서 게임 오브젝트의 컴포넌트로 존재할 때에는 다른 방법을 사용해야겠죠.

구현은 여러 가지 방법이 있어 입맛대로 만드시면 됩니다.

아래에는 인스턴스가 없는 경우에 인스턴스를 찾고, 그래도 없으면 인스턴스를 새로 만들어서 반환하는 방식으로 가장 널리 사용되는 방식입니다.

public class Singleton : MonoBehaviour
{
    private static Singleton instance;
    public static Singleton GetInstance()
    {
        // 만약 instance가 존재하지 않을 경우
        if (!instance)
        {
            // Singleton 클래스의 instance를 찾는다
            instance = GameObject.FindObjectOfType(typeof(Singleton));
            if (!instance)
            {
                // 찾아봐도 존재하지 않을 경우 새로 만든다
                GameObject obj = new GameObject("GameManagers"); // 이름인 GameManagers인 오브젝트 생성
                instance = obj.AddComponent(typeof(Singleton)) as Singleton;
            }
        }
        // instance를 반환한다.
        return instance;
    }
}

 

 

이것도 프로퍼티를 이용할 수도 있습니다.

public class Singleton : MonoBehaviour
{
    private static Singleton _instance;
    public static Singleton Instance
    {
        get
        {
            // 만약 instance가 존재하지 않을 경우
            if (!_instance)
            {
                // Singleton 클래스의 instance를 찾는다
                _instance = GameObject.FindObjectOfType(typeof(Singleton));
                if (!_instance)
                {
                    // 찾아봐도 존재하지 않을 경우 새로 만든다
                    GameObject obj = new GameObject("GameManagers"); // 이름인 GameManagers인 오브젝트 생성
                    _instance = obj.AddComponent(typeof(Singleton)) as Singleton;
                }
            }
            // instance를 반환한다.
            return _instance;
        }
    }
}

 

(3) 멀티 스레드에서 스레드 안전한 싱글톤 (LOCK)

유니티는 기본적으로 싱글 스레드를 기반으로 동작합니다. 따라서 멀티 스레드를 사용하지 않을 경우에는 상관이 없지만, 여러분이 사용하는 프로그램에서 멀티 스레드를 사용한다면 멀티 스레드에서도 안전할 수 있도록 코드를 수정해야 합니다.

동기화 처리를 잘못하면 게임 도중 GameManager와 같은 중요한 객체가 두 개나 생성될 수도 있으니 주의해야 합니다.

 

이럴 때 유용한 키워드가 바로 lock문입니다. lock(object)는 object가 다른 스레드에서 사용 중이면 사용이 끝날 때까지 해당 스레드를 기다리게 합니다.

 

다음은 기본적인 버전입니다.

public class Singleton
{
    private static Singleton instance;
    private static object syncRoot = new Object();
    
    public static Singleton Instance
    {
        get
        {
            // lock은 비싼 연산에 속하므로 Double-checked locking을 사용한다
            // 자세한 설명 : https://stackoverflow.com/questions/12316406/thread-safe-c-sharp-singleton-pattern
            if (!_instance)
            {
                lock (syncRoot)
                {
                    if (!_instance)
                        _instance = new Singleton();
                }
            }
            
            return _instance;
        }
    }
}

// 출처 : MSDN, https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff650316(v=pandp.10)?redirectedfrom=MSDN

lock 문은 리소스를 많이 잡아먹어서 if (!_instance) 보다 비싼 연산입니다. 그러니까 우선 _instance가 존재하는지를 확인하고 lock 문을 건 다음에 다시 한번 _instance가 존재하는지를 확인해서 보다 빠르게 작동하게 할 수 있습니다.

 

MonoBehaviour를 상속하면 다음과 같습니다.

public class Singleton : MonoBehaviour
{
    private static Singleton _instance;
    private static object syncRoot = new Object();
    public static Singleton Instance
    {
        get
        {
            // 만약 _instance가 존재하지 않을 경우
            if (!_instance)
            {
                lock (syncRoot)
                {
                    if (!_instance)
                    {
                        // Singleton 클래스의 _instance를 찾는다
                        _instance = GameObject.FindObjectOfType(typeof(Singleton));
                        if (!_instance)
                        {
                            // 찾아봐도 존재하지 않을 경우 새로 만든다
                            GameObject obj = new GameObject("GameManagers"); // 이름인 GameManagers인 오브젝트 생성
                            _instance = obj.AddComponent(typeof(Singleton)) as Singleton;
                        }
                    }
                }
            }
            // _instance를 반환한다.
            return _instance;
        }
    }
}

 

2021.7.11 추가: lock을 쓰면 멀티 스레드에서도 안전이 보장된다고 말했지만, 사실 위와 같이 lock을 사용하면 문제가 있어요.

싱글턴 컴포넌트가 아직 없을 때는, if (!_instance) 다음에 lock 구문을 거쳐서 스레드 간의 병행성이 보장이 돼요.

그런데 싱글턴 컴포넌트가 이미 생성된 상태에서 멀티 스레드가 동시에 접근한다면, 데이터를 변경할 때 제대로 적용되지 않는 병행성 이슈가 발생하게 됩니다.

 

생명력을 나타내는 Life 변수가 있을 때, 두 쓰레드가 동시에 Life를 감소시키려고 한다면 하나의 쓰레드의 연산만 적용되는 경우가 생길 수 있다는 겁니다.

 

왜 이런 현상이 생기는지 궁금하신 분은 운영체제에서의 병행성 이슈나, lock을 한 번 찾아보시면 좋을 것 같습니다.

1-2장. 운영체제 개요 - 병행성 (tistory.com)

 

1-2장. 운영체제 개요 - 병행성

운영체제는 여러 가지 일을 동시에 수행하고 있다. 프로세스를 하나 실행하고, 다음 프로세스를 실행하고, 또 다음 프로세스를 실행하고... 이 동작을 반복한다. 마치 곡예사가 여러 물건을 저글

lipcoder.tistory.com

 

아예 병행성 문제를 없애려면, 싱글턴 컴포넌트를 호출할 때마다 lock을 걸거나, 그 아래의 변수를 수정할 때마다 lock을 거는 수밖에 없습니다.

다만 lock은 한 스레드 외의 스레드가 이용할 수 없게 만들다 보니 성능을 저하시킵니다. 또, 데드락 문제를 발생시켜서 아예 프로그램을 정지시켜버릴 수 있으니 주의해야 해요.

 

 

 

3. 싱글톤의 문제점

책 『게임 프로그래밍 패턴』에서는 가능하면 싱글톤을 사용하지 말라고 경고합니다. 

 

첫 번째 이유는 정적 변수입니다.

싱글톤은 전역적인 접근점을 제공하기 위해 정적 멤버 변수(static)를 사용하게 됩니다.

그런데 정적 변수는 모든 클래스에서 접근할 수 있어서 전혀 상관없는 클래스와 커플링이 생길 수 있습니다.

여기서 커플링이란 두 클래스 사이에 결합이 생겨서, 만약 한 클래스를 수정한다면 다른 한 클래스도 수정해야 하는 상황을 말합니다.

 

돌멩이가 땅에 떨어질 때 소리가 나게 할 때, 만약 물리 코드에서 사운드 코드의 소리 재생을 실행하게 한다고 생각해봅시다. 이렇게 되면 사운드 코드를 한 줄 수정한다고 치면 물리 코드도 함께 수정해야 합니다. 최악의 상황에는 사운드 코드를 하나 수정하게 되면 관련된 수백 줄의 코드를 하나하나 고쳐야 하는 상황에 부딪힙니다. 이러한 상황은 최대한 피해야겠죠.

 

커플링을 줄이기 위해선 클래스 간의 참조를 최대한 줄여야 하고, 아예 인자로 오브젝트를 넘겨주거나, 변수를 최대한 적게 노출시키는 것이 좋습니다.

전역적으로 접근 가능한 객체를 최대한 줄이는 것 또한 방법이 될 수 있겠죠. 이미 정적으로 접근 가능한 객체가 있다면, 새로운 싱글톤을 만드는 대신 그쪽으로 접근을 한다던가 하는 방식으로요.

 

 

두 번째 이유는 게으른 초기화입니다.

게으른 초기화(Lazy initialization)는 CPU와 메모리의 부담을 줄이는 데에 일조하지만, 게임에서는 조금 다릅니다.

게임에서는 시스템을 초기화할 때 메모리 할당, 리소스 로딩 등으로 인해, 할 일이 많아 시간이 많이 걸립니다. 

필요할 때 초기화한다는 건 그 순간을 최대한 미룰 수 있어 괜찮은 아이디어입니다.

하지만 전투 도중에 시스템 초기화 작업을 수행한다면 중간에 프레임이 떨어지거나 게임 화면이 뚝뚝 끊길 수도 있겠죠.

따라서 객체의 초기화 작업은 언제 시행할지 시점을 잘 골라야 합니다. 가능하면 로딩 씬에서 이를 모두 초기화한다면 게임 플레이에 영향이 적을 테니 좋겠죠.

 

싱글톤은 게으른 초기화로 인해 초기화 시점을 선택을 하기 어렵습니다. 무조건 싱글톤을 사용해야 하는 것이 아니라면 다른 방법을 찾는 것도 좋은 선택입니다.

싱글톤 대신 정적 변수(static) 하나로 대체하거나, 싱글톤을 쓰더라도 다음과 같이 시작과 동시에 정적 변수를 초기화하는 방법을 사용하는 등 우회 방안을 생각해볼 수 있겠죠.

 

public class Singleton
{
    private static readonly Singleton _instance = new Singleton() ;
    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }
}

 

중요한 건 싱글톤 패턴을 남용하지 않는 것입니다.

필요할 때는 사용하더라도 가능하면 싱글톤 패턴을 사용하지 않도록 고민해야 한다는 것이죠.

 

4. 제네릭(Generic) 버전

싱글톤은 제네릭으로 만들어 상속시킬 때 가장 큰 힘을 발휘합니다.

모든 클래스를 싱글톤으로 하나하나 만들 수는 없으니 제네릭 클래스를 상속하면 무척 편리합니다.

(1) 싱글 스레드 환경의 싱글톤

public class Singleton<T> where T : class
{
    private static Singleton _instance;
    public static Singleton Instance
    {
        get
        {
            // 만약 instance가 존재하지 않을 경우 새로 생성한다.
            if (!_instance)
            {
                _instance = new T();
            }
            // _instance를 반환한다.
            return _instance;
        }
    }
}

// 예시
public class ExampleClass : Singleton<ExampleClass>
{
    // 상속하기만 해도 Singleton 기능을 이용할 수 있다.
}

 

기본적인 싱글톤으로 상속 시에 편리하게 Singleton을 사용할 수 있습니다.

다양한 매니저 클래스에 Singleton을 적용할 수 있겠죠.

 

MonoBehaviour 버전은 다음과 같습니다. 기본 골자는 똑같습니다.

public class Singleton<T> : MonoBehaviour where T : class
{
    private static Singleton _instance;
    public static Singleton Instance
    {
        get
        {
            if (!_instance)
            {
                _instance = GameObject.FindObjectOfType(typeof(T));
                if (!_instance)
                {
                    GameObject obj = new GameObject("GameManagers");
                    _instance = obj.AddComponent(typeof(T)) as T;
                }
            }
            
            return _instance;
        }
    }
}

// 예시
public class ExampleClass : Singleton<ExampleClass>
{
}

public class AnotherClass : MonoBehaviour
{
    private void Start()
    {
    	Debug.Log(ExampleClass.Instance.name); // GameManagers가 출력된다.
    }
}

MonoBehaviour의 기능을 사용해야 한다면(예를 들어, 실제 씬에 매니저 오브젝트를 배치할 필요가 있을 때), MonoBehaviour 버전의 싱글톤을 사용할 수 있겠죠.

 

(2) Lock 이용 (Thread-safe)

2-(3)의 구현을 제네릭의 형식에 맞추어 변형하면 됩니다.

같은 코드를 여러 번 반복해서 적는 것 같아서 생략할게요. 

기본 형태는 위에서 말씀드렸던 것과 거의 똑같습니다.

혹시 필요하신 분들은 댓글로 남겨주세요.

 

(3) Lazy 이용 (Thread-safe)

lock 대신 Lazy<T>를 사용하여 구현할 수도 있습니다.

Lazy<T>는 초기화를 지연시켜서 접근하려고 하면 그때 객체를 생성하는 클래스입니다.

 

Lazy 클래스는 세 가지 경우에 유용합니다.

- 생성이 오래 걸리는 큰 오브젝트를 필요할 때만 생성할 때

- 리소스를 많이 사용하는 실행을 필요할 때만 할 때

- 자원 생성을 멀티 스레드 환경에서 안전하게 해야 할 때

 

Lazy는 멀티 스레드에서도 안전하기 때문에 lock 대신 사용할 수도 있어요.

2021.7.11 추가: (주의) 어떤 분께서 덧글을 달아주셔서 추가합니다.

Lazy는 싱글턴을 생성할 때만 멀티 스레드 간에 안전을 보장합니다. 즉, 두 개의 싱글턴 객체가 동시 생성되는 문제만을 막을 뿐입니다. 2.(3) lock 부분에서 설명드린 것처럼, 이미 생성된 싱글턴에 접근할 땐 여전히 병행성 문제가 발생할 수 있습니다!!!

 

기본적인 구현은 다음과 같습니다.

using System;

public class Singleton<T> where T : class
{
    private static readonly Lazy<T> _instance =
        new Lazy<T>(()=> new T());

    public static T Instance
    {
        get 
        {
            return _instance.Value;
        }
    }
}

 

MonoBehaviour를 이용해 게임 오브젝트로 만든다면 다음처럼 할 수 있겠네요.

using System;
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : class
{
    private static readonly Lazy<T> _instance =
        new Lazy<T>(()=>
        {
            T instance = FindObjectOfType(typeof(T)) as T;

            if (instance == null)
            {
                GameObject obj = new GameObject("GameManagers");
                instance = obj.AddComponent(typeof(T)) as T;
                    
                DontDestroyOnLoad(obj);
            }
                
            return instance;
        });

    public static T Instance
    {
        get 
        {
            return _instance.Value;
        }
    }
}

여기서 DontDestroyOnLoad(obj)는 씬이 바뀌어도 게임 오브젝트가 사라지지 않게 만드는 역할을 합니다. 보통 게임 매니저에서 주로 사용해요.

필요한 분만 추가하셔서 사용하시면 됩니다.

당연하지만, 멀티스레드는 유니티에서는 제대로 지원이 안 되는 경우가 많은 걸로 알고 있습니다. 저도 학부생 찌끄레기이기 때문에 실제로 제대로 사용해본 적은 없습니다... 따라서 꼭 필요할 때에만 (2), (3)과 같은 코드를 사용하시길 추천드립니다.

 

 

2021.7.11 추가: 어떤 고마운 분께서 덧글로 남겨주셨네요. Lazy는 싱글턴을 생성할 때만 멀티 스레드 간에 안전을 보장합니다.

2.(3) lock 부분에서 설명한 것과 똑같은 문제가 생기니 참고해주세요.

2023.08.08 추가: 본문에 존재하는 코드(특히 Lazy를 사용한 코드!)는 충분히 검증된 코드는 아닙니다. 따라서 아이디어는 채용하되, 직접 복사-붙여넣기할 경우에 오류가 발생하지 않는다는 보장은 못 드립니다 ㅠ

 

 

+추가

MonoBehaviour를 상속받은 싱글톤의 경우 게임 종료 직후 오브젝트가 파기되었을 때 호출될 수도 있으므로 그에 대한 대처가 필요하다는 의견도 있습니다. 필요하신 분들을 위해 링크를 걸어드릴게요.

https://skuld2000.tistory.com/26

 

[Unity] 싱글톤(Singleton) 패턴을 제너릭 클래스로 구현해서 범용적으로 사용하는 방법

싱글톤(Singleton) 디자인 패턴은 게임 개발 시 단 하나의 유일한 인스턴스만 존재해야 하는 Manager 클래스 들을 구현하기 위해 가장 많이 사용되는 디자인 패턴이다. 개발 언어, 플랫폼과 관련없이

skuld2000.tistory.com

 

참고 문헌

에릭 감마 외, GoF의 디자인 패턴, Pearson, 2020년 11월 1일

로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어, 2016년 6월 1일

"Singletons in Unity3D," John Grden, last modified Oct 12, 2010, accessed Aug 30, 2020, https://unityindepth.tistory.com/38

"유니티 디자인패턴 - 싱글톤 (Unity Design Patterns - Singleton)", 글릭의 만들어가는 세상, 2019년 7월 12일 수정, 2020년 8월 30일 접속, https://glikmakesworld.tistory.com/2

"[C#] Lazy 소개 및 Singleton 패턴", 수까락의 프로그래밍 이야기, 2015년 10월 2일 수정, 2020년 8월 30일 접속, http://egloos.zum.com/sweeper/v/3157853

"lock 문", Microsoft Docs, 2020년 4월 2일 작성, 2020년 8월 30일 접속, https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/lock-statement

"Implementing Singleton in C#", Microsoft Docs, last modified Mar 17, 2014, accessed Aug 30, 2020, https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff650316(v=pandp.10)?redirectedfrom=MSDN

"Thread Safe C# Singleton Pattern", stack overflow, last modified May 9, 2018, accessed Aug 30, 2020, https://stackoverflow.com/questions/12316406/thread-safe-c-sharp-singleton-pattern

"Lazy<T> 클래스", Microsoft Docs, 2020년 8월 30일 접속, https://docs.microsoft.com/ko-kr/dotnet/api/system.lazy-1?view=netcore-3.1

댓글