본문 바로가기

C#/Unity

[2D 카메라 액션 패키지 개발] #02 - 모듈화 기준, 설정 계층 정의

서론 

  플로우가 잡히고, 실제 개발에 들어가기 전에 거대한 개발 범위를 축소해야 한다. 이전에 내가 개발을 하다가 어려움에 부딪히거나 그 때문에 멈춰 섰던 이유가 무엇일까? 여러 이유가 있었지만 개발 범위를 너무 넓게 잡아서 그랬던 기억이 많다. 그래서 이번에는 개발에 앞서 범위를 작게 시작하여 독립적으로 개발하고 이후에 연결하고자 한다.


모듈화 기준

  모듈화의 기준은 무엇일까? 내가 참고한 블로그에 따르면 클래스의 기능을 컴퓨터 부품처럼 분리하여 마치 컴퓨터를 조립하듯 기능들을 조합해 객체를 완성시키는 것이라고 한다. 부품을 나누는 데에는 어떠한 의미가 있을까? '전체 플로우에서 각 부품이 어떠한 책임을 지는가'에 의미를 두어야 한다고 생각한다. 참고로 객체 지향 설계의 5원칙인 SOLID 원칙에서 S를 뜻하는 SRP(Single Responsibility Principle, 단일 책임 원칙) 역시 이 책임을 강조하고 있다. 그 이유는 기능 변경이나 추가에 대한 변경 영향을 최소화하기 위함이라 볼 수 있다. (실제로 책임(역할)을 여러 개 가지고 있거나 여러 객체가 하나의 책임을 나누어 가지는 경우 해당하는 기능에 대한 변경을 하려고 할 때에 상당히 곤란해진다는 것을 경험으로 알았다..)

 

  그만큼 '책임'에 기반하여 카메라 액션 기능에서도 부품을 나눠야 한다. 나는 일단 컴퓨터 부품을 기능별로 '처리 장치', '입출력 장치', '기억 장치'로 구분하는 것처럼 해당 프로젝트를 기능 별로 '계층'을 나누었다. 그리고 그 안에서 핵심 기능을 나열한 뒤 관련한 것끼리 묶어 이를 처리하는 객체를 정의하기로 했다.

 

계층 설정

데이터 계층 (Data Layer)

사용자 정의 가능한 동작(입력, 액션, 영역, 프로필 등)을 '데이터' 형태로 기술하고, 이를 직렬화/유효성 검증.

그 외 다양한 행동의 설계도를 그리는 역할. 파라미터와 규칙의 정의서.

실행은 Core에서 담당.

 

📍책임: 실행 로직(Core)이 사용할 설정값과 규칙을 정의. 입력/액션/후처리/프로필 등 동작 구조와 관계 정의. 데이터 직렬화, ScriptableObject 기반으로 구현되어 에디터에서 조작 가능.

🧠기능 정의:

주요 책임(기능) 클래스 예시
전체 프로필 데이터 CameraBehaviourProfile
입력, 액션, 후처리 묶음 데이터 CameraActionUnit
공통 설정 속성 정의 ConfigBase
입력 조건 정의 InputConfigBase
동작(액션) 정의 ActionConfigBase
후처리 정의 PostActionConfigBase
전체 동작 영역 정의 AreaConfigBase
최소 동작 값(범위) 정의 SectionConfigBase
IInputConfigBase를 상속 받아 여러 입력에 대한 정의 MouseDragConfig, TouchStartConfig, ...
IActionConfigBase를 상속 받아 여러 액션에 대한 정의 MoveActionConfig, ZoomActionConfig, ...
IPostActionConfigBase를 상속 받아 여러 후처리에 대한 정의 ShakeCameraConfig, ...
IAreaConfigBase를 상속 받아 여러 행동 영역에 대한 정의 FixedSizeAreaConfig, ...
ISectionConfigBase를 상속 받아 각 최소 작업 범위에 대한 정의 FixedSizeSectionConfig, ...
입력 설정 클래스의 속성/메서드 시그니처 정의 IInputConfig
동작 설정 클래스의 속성/메서드 시그니처 정의 IActionConfig
후처리 설정 클래스의 속성/메서드 시그니처 정의 IPostActionConfig
영역 설정 클래스의 속성/메서드 시그니처 정의 IAreaConfig
동작의 모드 열거형 ActionMode
줌 타입 열거형 ZoomType

 

  이 기능을 개발하는데 있어서 개발 목표가 여럿 있었는데 그중에는 아래와 같은 목표도 있었다.

 

비개발자라도 원하는대로 세팅하고 테스트할 수 있도록 최대한 다양한 설정을 Scriptable Object로 분리하자

 

  그 이유로 테스트가 필요한 모든 값을 설정으로 빼고, 따로 관리할 수 있도록 했다. 이 외에도 기능에 대한 여러 가지 규칙을 정의하는 것을 이 '데이터 계층(Data Layer)'로 설정하였다.

 

  '데이터 계층'의 주 기능은 핵심 로직, 규칙을 정의하는 것이다. 'Core Layer(코어 계층)'이 이후에 이 계층의 데이터를 보고서 실제 실행을 하게 되는데 이 때 필요한 파라미터를 정의하거나 알맞은 동작(Strategy: 전략)을 찾을 수 있도록 타입 정보를 제공한다. 또한, 동작에 대한 규칙을 선언하고, 이 역시 'Core Layer'에 전달하게 된다.(Scriptable Object를 통해 설정한 내용)

 

  그러므로 'Data Layer'의 객체는 실행 로직을 가지고 있지 않고, 행동에 대한 규칙, 필요한 파라미터 등만 정의해 놓아야 한다. 이를 잘 분리해야만 기능에 대한 변경/추가가 있을 때에 수정 범위를 줄이고, 명확하게 할 수 있다.

 

코어 계층 (Core Layer)

Data Layer의 선언을 실제 연산으로 변환하는 실행 엔진. "무엇을 어떻게 실행할지"를 담당하는 규칙 집행 계층

입력이나 실행 타이밍에 대해 아무것도 몰라야 함

 

📍책임: Data Layer의 데이터 기반으로 실제 카메라 동작 수행, 입력 delta나 이벤트를 받아 Action/Strategy를 통해 계산 수행

🧠기능 정의:

주요 책임(기능) 클래스 예시
액션 실행 ZoomProcessor, MoveProcessor, RotateProcessor, ...
후처리 실행 ShakePostProcessor, FadePostProcessor, ...
전략(Strategy) 관리 ActionStrategyFactory, PostActionStrategyFactory, ...
실행 흐름 관리 ActionRunner

 

 

  특정 액션을 위한 중요 로직을 실행하는 계층으로 카메라가 이동하거나 줌을 하는데 얼마나 움직이고 사이즈가 얼마나 커져야 하는 등의 계산을 처리하는 계층이다. 또한, 데이터 기반으로 어떤 전략을 사용해야 하는지 연결하는 역할이나 액션의 실행 흐름도 관리한다. 여기서 실행 흐름이란 데이터 내부의 하나의 '액션을 어떻게 실행하느냐'의 의미이다. 후에 나올 System 계층의 실행 순서 보장과 다른 의미이다. 아래는 Core의 실행 흐름 관리에 관한 예시 코드이다.

public class ActionRunner
{
    public IEnumerator Execute(Camera cam, CameraActionUnit unit, float delta)
    {
        // 1. 메인 액션 실행
        var strategy = ActionStrategyFactory.Resolve(unit.action);
        strategy.Execute(cam, unit.action, delta);

        // 2. 후처리(PostAction) 실행
        if (unit.postAction != null)
        {
            var post = PostActionStrategyFactory.Resolve(unit.postAction);
            yield return post.Execute(cam, unit.postAction);
        }
    }
}

 

표현 계층 (presentation Layer)

사용자의 입력과 Unity 환경을 연결하는 외부 인터페이스 계층.

로직과 외부 세계(사용자, Unity)와의 접점.

입력은 System으로 보내고, 출력은 Unity에 반영(카메라 오브젝트 이동 등)하는 브릿지 역할.

 

📍책임: 실제 Unity Input, UI, Touch, Mouse 이벤트 감지, 감지된 입력을 System Layer에 전달, 카메라, UI 등 외부 요소를 직접 제어하거나 Core에서 나온 결과를 반영

🧠기능 정의:

주요 책임(기능) 클래스 예시
입력 감지 및 전달 PinchInputAdapter, MouseDragInputAdapter, ...
카메라 객체 참조 및 출력 제어 CameraOutputViewer, ...
에디터 상의 디버그 표시, 시각화 DebugVisualizer, ...

 

  코드로 명시되어 있는 것이 아니라 외부에 동적으로 발생하는 여러 입력/이벤트를 감지하고, 전달하거나 Core를 통해 계산된 결과 값을 외부 Unity 오브젝트 등에 반영하는 역할을 한다.

 

  예를 들면, 카메라의 이동이나 줌(Orthographic size 변경)을 실제로 적용하는 것이 이 Presentation Layer에서 이루어진다.

 

시스템 계층 (System Layer)

Core와 Data를 조합해 실행 순서와 흐름을 제어하는 조정 계층.

언제, 어떤 Action을 실행할지 결정하는 중추 계층.

Core가 '실행기', Data가 '설계도'라면 'System'은 '감독자' 역할.

 

📍책임: 여러 ActionUnit과 Profile을 관리하고 실행 순서를 제어, 입력 이벤트에 따라 적절한 액션을 선택 및 실행 요청, 씬 전환과 상태 리셋, 액션 중단 등 라이프사이클 관리

🧠기능 정의:

주요 책임(기능) 클래스 예시
프로필 로드 및 적용 ProfileManager
입력 이벤트 라우팅 SystemController.OnPinch, SystemController.OnDrag, ...
실행 요청 순서 보장 및 코루틴 제어 LifecycleManager, ...
실행 중인 액션의 상태 플래그 관리 ActionExecutionHandler

 

  Presentation을 통해 입력을 받으면, Data에서 받아둔 여러가지 설계도(데이터)를 적절한 액션을 찾아 Core에 실행을 요청한다. 또한 이 결과를 Presentation에 전달하여 실제 외부에 반영하도록 지시한다. 카메라 액션 플로우를 제어하는 지휘자와 같은 역할을 하며, 이곳에서 여러 계층의 객체를 풀로 붙여 알맞은 순서대로 사용된다.

 

  앞서 Core의 실행 흐름 제어가 하나의 액션에 대한 흐름을 관리하는 것이라면, 여기서의 실행 순서 보장(실행 요청 순서 보장)은 Data에서 받은 커스텀 액션 프로필을 통해 실행되어야 할 액션을 선택하고 순서대로 실행하는 시스템 전체의 흐름을 관리하는 것이다. 아래는 System 계층에서 실행 순서를 보장하는 예시 코드이다. 

public class SystemController : MonoBehaviour
{
    public CameraBehaviorProfile profile;
    private ActionRunner _runner;

    void Awake() => _runner = new ActionRunner();

    public void OnPinch(float delta)
    {
        // 1. 핀치 입력에 해당하는 유닛 선택
        var unit = profile.actions
            .Where(u => u.enabled && u.input is PinchInputConfig)
            .OrderBy(u => u.order)
            .FirstOrDefault();
        if (unit == null) return;

        // 2. 실행 순서 및 충돌 제어
        if (IsActionRunning(unit)) return; // 이미 실행 중이라면 skip

        // 3. 실행 요청
        StartCoroutine(_runner.Execute(Camera.main, unit, delta));
    }
}

 

공용 계층 (Common Layer)

모든 계층에서 사용 가능한 기능.

특정 의존성 없이 사용 가능해야 함.

 

📍책임: 클래스나 필드에 메타데이터(의미)를 부여하는 Custom Attribute나 모든 계층에서 범용적으로 사용 가능한 순수 연산/수학/형변환 로직

🧠기능 정의:

주요 책임(기능) 클래스 예시
벡터의 범위 한정 VectorExtensions.ClampMagnitude
에디터 인스펙터에 라벨 표시 ClassLabelAttribute, FieldLabelAttribute, ...
Enum을 Flag Field로 표시 EnumFlagAttribute

 

  특정 계층에 한하지 않고 동작해야 하는 유틸리티 객체들로 구성된 계층이다. 대표적으로 확장 메서드(단, 카메라 기능/규칙 등과 연관된 것이라면 Core/Data 계층으로 분류한다.)나 어트리뷰트(Attribute)가 이에 포함된다.

 

  에디터 클래스는 Editor 폴더에 별도로 빼기로 하였다. 언밀히 말하면 Editor 계층이라는 보조 계층이 하나 더 있는 셈이다. 이 역시 주요 계층인 4개 계층 모두 사용할 수 있는 것에 한하여 Editor 계층으로 분류한다. 만약 특정 기능을 타깃으로 사용된다면 Core 계층이 된다.