Unity 프로젝트에서 일반적인 게임 프로그래밍 디자인 패턴을 구현하면 깔끔하고 체계적이며 가독성 높은 코드베이스를 효율적으로 빌드하고 유지 관리할 수 있습니다. 디자인 패턴은 리팩터링과 테스트 시간을 줄여줄 뿐만 아니라 온보딩 및 개발 프로세스의 속도를 높여 게임, 개발팀, 비즈니스 성장을 위한 탄탄한 기반이 됩니다.
디자인 패턴은 코드에 복사하여 붙여넣을 수 있는 완성된 솔루션이 아니라 올바르게 사용하면 더 크고 확장 가능한 애플리케이션을 구축하는 데 도움이 되는 추가 도구라고 생각하세요.
이 페이지에서는 관찰자 패턴과 이 패턴이 서로 상호작용하는 객체 간의 느슨한 결합 원리를 지원하는 데 어떻게 도움이 되는지 설명합니다.
이 콘텐츠는 무료 전자책을 기반으로 합니다, 게임 프로그래밍 패턴으로 코드 수준 높이기에서 잘 알려진 디자인 패턴을 설명하고 Unity 프로젝트에 사용할 수 있는 실용적인 예제를 공유합니다.
Unity 게임 프로그래밍 디자인 패턴 시리즈의 다른 글은 Unity 베스트 프랙티스 허브에서 확인하거나 다음 링크를 클릭하세요:
런타임에는 게임에서 여러 가지 상황이 발생할 수 있습니다. 플레이어가 적을 처치하면 어떻게 되나요? 아니면 파워나 레벨 업을 수집할 때? 일부 객체가 다른 객체를 직접 참조하지 않고도 다른 객체에 알릴 수 있는 메커니즘이 필요한 경우가 많습니다. 안타깝게도 코드베이스가 늘어날수록 불필요한 종속성이 추가되어 유연성이 떨어지고 코드 유지 관리에 과도한 오버헤드가 발생할 수 있습니다.
관찰자 패턴은 이 문제에 대한 일반적인 해결책입니다. 이를 통해 객체가 통신할 수 있지만 '일대다' 종속성을 사용하여 느슨하게 결합된 상태를 유지할 수 있습니다. 한 개체의 상태가 변경되면 모든 종속 개체에 자동으로 알림이 전송됩니다.
이를 시각화하는 데 도움이 되는 비유는 다양한 청취자를 대상으로 방송하는 라디오 타워입니다. 누가 시청하고 있는지는 알 필요가 없으며, 방송이 적절한 시간에 적절한 주파수로 생방송되고 있다는 사실만 알면 됩니다.
브로드캐스팅하는 객체를 피사체라고 합니다. 수신 중인 다른 객체를 관찰자 또는 구독자라고 합니다(이 페이지에서는 전체적으로 관찰자라는 이름을 사용합니다).
이 패턴의 장점은 피사체와 관찰자를 분리하여 관찰자가 신호를 수신한 후 무엇을 하든 상관하지 않는다는 점입니다. 관찰자는 대상에 대한 의존성을 가지고 있지만, 관찰자 자신은 서로에 대해 알지 못합니다.
관찰자는 피험자의 상태가 변경될 때마다 간단히 알림을 받고 그에 따라 자신을 업데이트할 수 있습니다. 이렇게 하면 시스템의 다른 부분에 영향을 주지 않고 코드를 수정하거나 확장하기가 더 쉬워집니다.
또 다른 이점은 옵저버 패턴을 사용하면 옵저버를 수정할 필요 없이 다른 컨텍스트에서 재사용할 수 있으므로 재사용 가능한 코드를 개발할 수 있다는 점입니다. 마지막으로, 객체 간의 종속성이 명확하게 정의되므로 코드 가독성이 향상되는 경우가 많습니다.
자체적인 주체-관찰자 클래스를 설계할 수도 있지만, C#은 이미 이벤트를 사용하여 패턴을 구현하고 있으므로 일반적으로 불필요한 작업입니다. 옵저버 패턴은 C# 언어에 내장되어 있을 정도로 널리 퍼져 있으며, 그럴 만한 이유가 있습니다: 보다 모듈화되고 재사용 가능하며 유지 관리가 용이한 코드를 만드는 데 도움이 될 수 있습니다.
이벤트란 무엇인가요? 알림은 어떤 일이 발생했음을 알리는 알림으로, 몇 가지 단계를 거쳐야 합니다:
게시자(주체라고도 함)는 특정 함수 서명을 설정하는 델리게이트를 기반으로 이벤트를 생성합니다. 이벤트는 피사체가 런타임에 수행할 동작(예: 데미지 받기, 버튼 클릭 등)입니다. 퍼블리셔는 종속자(관찰자) 목록을 관리하고 상태가 변경되면 이 이벤트로 표시되는 알림을 해당 종속자에게 보냅니다.
그런 다음 옵저버는 각각 이벤트 핸들러라는 메서드를 만들고, 이 메서드는 델리게이트의 서명과 일치해야 합니다. 옵저버는 게시자로부터 알림을 수신하고 그에 따라 스스로 업데이트하는 객체입니다.
각 옵저버의 이벤트 핸들러는 퍼블리셔의 이벤트를 구독합니다. 필요한 만큼 많은 옵저버를 구독에 참여시킬 수 있습니다. 모두 이벤트가 트리거될 때까지 기다립니다.
퍼블리셔가 런타임에 이벤트 발생을 알리는 것을 이벤트 발생이라고 합니다. 그러면 옵저버의 이벤트 핸들러가 호출되고, 이에 대한 응답으로 자체 내부 로직을 실행합니다.
이러한 방식으로 여러 컴포넌트가 피사체의 단일 이벤트에 반응하도록 만들 수 있습니다. 피사체가 버튼을 클릭하면 관찰자는 애니메이션이나 사운드를 재생하거나 컷씬을 트리거하거나 파일을 저장할 수 있습니다. 객체의 응답은 무엇이든 될 수 있기 때문에 객체 간에 메시지를 보낼 때 관찰자 패턴을 자주 볼 수 있습니다.
대리인 대 이벤트
델리게이트는 메서드 서명을 정의하는 유형입니다. 이를 통해 메서드를 다른 메서드에 인자로 전달할 수 있습니다. 값이 아닌 메서드에 대한 참조를 저장하는 변수처럼 생각하면 됩니다.
반면 이벤트는 본질적으로 클래스가 느슨하게 결합된 방식으로 서로 소통할 수 있도록 하는 특별한 유형의 델리게이트입니다. 델리게이트와 이벤트의 차이점에 대한 일반적인 정보는 C#에서 델리게이트와 이벤트 구분하기를 참조하세요.
아래 코드에서 기본 주제/발행인을 정의하는 방법을 살펴보겠습니다.
아래 코드 예제의 Subject 클래스에서는 필수 사항은 아니지만 게임 오브젝트에 더 쉽게 첨부할 수 있도록 MonoBehaviour에서 상속합니다.
사용자 지정 델리게이트를 자유롭게 정의할 수 있지만, 대부분의 사용 사례에서 작동하는 System.Action을 사용할 수도 있습니다. 코드 예시에서는 이벤트와 함께 파라미터를 전송할 필요가 없지만, 필요한 경우 Action<T> 델리게이트를 사용하여 괄호 안에 List<T>로 전달하면 됩니다(최대 16개의 파라미터).
코드 스니펫에서 ThingHappened는 실제 이벤트이며, 주체가 DoThing 메서드에서 호출합니다.
"?" 연산자는 널 조건부 연산자로, 이벤트가 널이 아닌 경우에만 호출된다는 의미입니다. Invoke 메서드는 이벤트를 발생시키는 데 사용되며, 이는 이벤트에 가입된 모든 이벤트 핸들러를 실행한다는 의미입니다. 이 경우 DoThing 메서드는 null이 아닌 경우 ThingHappened 이벤트를 발생시켜 이벤트에 가입된 모든 이벤트 핸들러를 실행합니다.
옵저버 및 기타 디자인 패턴이 실제로 작동하는 것을 보여주는 샘플 프로젝트를 다운로드할 수 있습니다. 이 코드 예제는 여기에서 확인할 수 있습니다.
이벤트를 수신하려면 아래의 축소된 코드 예시( Github 프로젝트에서도 사용 가능)와 같은 예제 Observer 클래스를 빌드하면 됩니다.
이 스크립트를 게임 오브젝트에 컴포넌트로 첨부하고 인스펙터에서 subjectToObserver를 참조하여 ThingHappened 이벤트를 수신합니다.
OnThingHappened 메서드에는 관찰자가 이벤트에 대한 응답으로 실행하는 모든 로직이 포함될 수 있습니다. 종종 개발자는 이벤트 핸들러를 나타내기 위해 접두사 "On"을 추가합니다(스타일 가이드의 명명 규칙 사용).
깨우기 또는 시작에서 += 연산자를 사용하여 이벤트를 구독할 수 있습니다. 이는 관찰자의 OnThingHappened 메서드와 피사체의 ThingHappened 메서드를 결합합니다.
주체의 DoThing 메서드를 실행하는 것이 있으면 이벤트가 발생합니다. 그러면 옵저버의 OnThingHappened 이벤트 핸들러가 자동으로 호출되어 디버그 문을 출력합니다.
참고: 옵저버가 ThingHappened에 가입된 상태에서 런타임에 삭제하거나 제거하면 해당 이벤트를 호출하면 오류가 발생할 수 있습니다. 따라서 객체의 수명 주기에서 적절한 시점에 -= 연산자를 사용하여 MonoBehaviour의 OnDestroy 메서드에서 이벤트의 구독을 취소하는 것이 중요합니다.
샘플 프로젝트를 다운로드하고 11 Observer라는 폴더로 이동하면 간단한 버튼(예제 주제)과 스피커(오디오 옵저버), 애니메이션(애니메이터), 파티클 효과(파티클 시스템 옵저버)를 보여주는 예제를 찾을 수 있습니다.
버튼을 클릭하면 예제 주체가 ThingHappened 이벤트를 호출합니다. 오디오 옵저버, 애님 옵저버, 파티클 시스템 옵저버는 응답으로 해당 이벤트 처리 메서드를 호출합니다.
옵저버는 동일하거나 다른 게임 오브젝트에 존재할 수 있습니다. 애니옵저버는 예제 오브젝트에서 버튼 애니메이션을 생성하는 반면, 오디오옵저버와 파티클시스템옵저버는 다른 게임 오브젝트를 차지한다는 점에 유의하세요.
버튼주체는 사용자가 마우스 버튼으로 클릭 이벤트를 호출할 수 있도록 합니다. 그러면 오디오옵저버 및 파티클시스템옵저버 컴포넌트가 있는 다른 여러 게임 오브젝트가 이벤트에 자체 방식으로 응답할 수 있습니다.
어떤 객체가 주체이고 어떤 객체가 관찰자인지 결정하는 것은 용도에 따라 달라집니다. 이벤트를 발생시키는 모든 것이 주체 역할을 하고, 이벤트에 반응하는 모든 것이 관찰자 역할을 합니다. 동일한 게임 오브젝트의 다른 컴포넌트는 피사체 또는 관찰자가 될 수 있습니다. 같은 구성 요소라도 어떤 상황에서는 주체가 될 수 있고 다른 상황에서는 관찰자가 될 수 있습니다.
예를 들어, 예제의 AnimObserver는 클릭 시 버튼에 약간의 움직임을 추가합니다. 버튼 서브젝트 게임 오브젝트의 일부이긴 하지만 옵저버 역할을 합니다.
유니티에는 별도의 시스템인 UnityEvents를 사용하는 UnityAction 델리게이트를 사용하는 별도의 시스템도 포함되어 있습니다. 인스펙터(옵저버 패턴을 위한 그래픽 인터페이스 제공)에서 구성할 수 있으므로 개발자는 이벤트가 발생할 때 어떤 메서드를 호출해야 하는지 지정할 수 있습니다.
유니티의 UI 시스템을 사용해 본 적이 있다면(예: UI 버튼의OnClick 이벤트 생성) 이미 이에 대한 경험이 있을 것입니다.
위 이미지에서 버튼의 OnClick 이벤트는 두 AudioObserver의 OnThingHappened 메서드에서 응답을 호출하고 트리거합니다. 따라서 코드 없이도 피사체의 이벤트를 설정할 수 있습니다.
디자이너나 프로그래머가 아닌 사람도 게임플레이 이벤트를 만들 수 있도록 하려는 경우 UnityEvents가 유용합니다. 그러나 시스템 네임스페이스의 동등한 이벤트나 작업보다 속도가 느릴 수 있다는 점에 유의하세요. 또한 UnityAction은 인수를 받는 메서드를 호출하는 데 사용할 수 있다는 추가적인 이점이 있는 반면, UnityEvents는 인수가 없는 메서드로만 제한됩니다.
UnityEvents와 UnityActions를 고려할 때 성능과 사용량을 비교합니다. UnityEvents는 더 간단하고 사용하기 쉽지만 호출할 수 있는 메서드 유형이 더 제한적입니다. 인스펙터에 모든 이벤트를 노출하면 오류가 더 많이 발생할 수 있다고 주장하는 사람들도 있습니다.
예제는 Unity Learn에서 이벤트를 사용한 간단한 메시징 시스템 만들기 모듈을 참조하세요.
이벤트를 구현하면 약간의 추가 작업이 필요하지만 이점이 있습니다:
관찰자 패턴은 오브젝트를 분리하는 데 도움이 됩니다: 이벤트 게시자는 이벤트 구독자에 대해 아무것도 알 필요가 없습니다. 한 클래스와 다른 클래스 사이에 직접적인 의존성을 만드는 대신, 피험자와 관찰자는 어느 정도의 분리(느슨한 결합)를 유지하면서 소통합니다.
직접 구축할 필요는 없습니다: C#에는 확립된 이벤트 시스템이 포함되어 있으며, 자체 델리게이트를 정의하는 대신 System.Action 델리게이트를 사용할 수 있습니다. 또는 Unity에는 UnityEvents 및 UnityActions도 포함되어 있습니다.
각 옵저버는 자체 이벤트 처리 로직을 구현합니다: 이러한 방식으로 각 관찰 대상은 응답하는 데 필요한 로직을 유지합니다. 이렇게 하면 디버깅과 단위 테스트가 더 쉬워집니다.
사용자 인터페이스에 적합합니다: 핵심 게임플레이 코드는 UI 로직과 별개로 존재할 수 있습니다. 그러면 UI 요소가 특정 게임 이벤트나 조건을 수신하고 적절하게 반응합니다. MVP 및 MVC 패턴은 이러한 목적으로 옵저버 패턴을 사용합니다.
하지만 다음과 같은 주의 사항도 숙지해야 합니다:
이는 복잡성을 더합니다: 다른 패턴과 마찬가지로 이벤트 중심 아키텍처를 만들려면 처음에 더 많은 설정이 필요합니다. 또한 피사체 또는 관찰자를 삭제할 때는 주의하세요. 옵저버가 더 이상 필요하지 않을 때 메모리 참조가 제대로 해제될 수 있도록 OnDestroy에서 옵저버 등록을 해제해야 합니다.
관찰자는 이벤트를 정의하는 클래스에 대한 참조가 필요합니다: 옵저버는 여전히 이벤트를 게시하는 클래스에 대한 종속성을 가집니다. 모든 이벤트를 처리하는 정적 이벤트 관리자(다음 섹션 참조)를 사용하면 오브젝트를 서로 분리하는 데 도움이 될 수 있습니다.
성능이 문제가 될 수 있습니다: 이벤트 중심 아키텍처는 오버헤드를 추가합니다. 큰 씬과 많은 게임 오브젝트는 성능을 저해할 수 있습니다.
여기서는 옵저버 패턴의 기본 버전만 소개하지만, 이를 확장하여 게임 애플리케이션의 모든 요구 사항을 처리할 수 있습니다.
참관인 패턴을 설정할 때 다음 제안 사항을 고려하세요:
ObservableCollection 클래스를 사용합니다: C#은 동적 관찰 가능한 컬렉션 을 제공하여 특정 변경 사항을 추적할 수 있습니다. 항목이 추가, 제거되거나 목록이 새로 고쳐질 때 관찰자에게 알림을 보낼 수 있습니다.
고유 인스턴스 ID를 인수로 전달합니다: 계층 구조의 각 게임 오브젝트에는 고유한 인스턴스 ID가 있습니다. 둘 이상의 참관인에게 적용될 수 있는 이벤트를 트리거하는 경우 이벤트에 고유 ID를 전달하세요( Action<int> 유형 사용). 그런 다음 게임 오브젝트가 고유 ID와 일치하는 경우에만 이벤트 핸들러에서 로직을 실행합니다.
정적 이벤트 관리자를 생성합니다: 이벤트는 게임플레이의 많은 부분을 주도할 수 있기 때문에 많은 Unity 애플리케이션은 정적 또는 싱글톤 이벤트 관리자를 사용합니다. 이렇게 하면 참관인이 게임 이벤트의 중앙 소스를 주제로 참조하여 설정을 더 쉽게 할 수 있습니다.
FPS 마이크로게임에는 커스텀 게임 이벤트를 구현하고 리스너를 추가하거나 제거하는 정적 헬퍼 메서드를 포함하는 정적 이벤트 매니저가 잘 구현되어 있습니다.
Unity 오픈 프로젝트는 스크립터블 오브젝트가 Unity 이벤트를 중계하는 게임 아키텍처도 선보입니다. 이벤트를 사용하여 오디오를 재생하거나 새 장면을 로드합니다.
이벤트 대기열을 만듭니다: 씬에 오브젝트가 많은 경우 이벤트를 한꺼번에 올리는 것이 좋지 않을 수 있습니다. 하나의 이벤트를 호출할 때 수천 개의 오브젝트가 소리를 재생하는 불협화음을 상상해 보세요. 관찰자 패턴과 명령 패턴을 결합하면 이벤트를 이벤트 대기열로 캡슐화할 수 있습니다. 그런 다음 명령 버퍼를 사용하여 이벤트를 한 번에 하나씩 재생하거나 필요에 따라 선택적으로 무시할 수 있습니다(예: 한 번에 소리를 낼 수 있는 개체의 수가 최대인 경우).
옵저버 패턴은 전자책에서 다루는 모델 뷰 프레젠터(MVP) 아키텍처 패턴에 크게 영향을 미칩니다. 게임 프로그래밍 패턴으로 코드 레벨 업.
무료 전자책에서 SOLID 원칙뿐만 아니라 Unity 애플리케이션에서 디자인 패턴을 사용하는 방법에 대한 더 많은 팁을 확인할 수 있습니다. 게임 프로그래밍 패턴으로 코드 레벨 업.
모든 고급 Unity 기술 전자책과 문서는 베스트 프랙티스 허브에서 확인할 수 있습니다. 이 전자책은 문서 내 고급 모범 사례 페이지에서도 확인할 수 있습니다.