"깊이"가 다른 게임개발자 허민영

유저에서 게임까지, 철학에서 코딩까지, 본질을 보는 게임개발

잡학다식

리팩토링

허민영 2025. 5. 13. 00:13

4.3. 패턴 통합의 실제 사례

다음은 유니티 게임 개발에서 다양한 패러다임의 패턴을 통합한 사례입니다:

 
csharp
using System;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

// 불변 데이터 클래스들 (FP)
[Serializable]
public class PlayerState
{
    public readonly string Id;
    public readonly Vector3 Position;
    public readonly int Health;
    public readonly List<string> Inventory;
    
    public PlayerState(string id, Vector3 position, int health, List<string> inventory)
    {
        Id = id;
        Position = position;
        Health = health;
        Inventory = new List<string>(inventory); // 방어적 복사
    }
}

[Serializable]
public class GameState
{
    public readonly PlayerState Player;
    public readonly Dictionary<string, EnemyState> Enemies;
    public readonly int Score;
    
    public GameState(PlayerState player, Dictionary<string, EnemyState> enemies, int score)
    {
        Player = player;
        Enemies = new Dictionary<string, EnemyState>(enemies); // 방어적 복사
        Score = score;
    }
}

// 핵심 도메인 로직 (FP): 순수 함수와 불변 데이터
public static class GameRules
{
    // 순수 함수: 상태를 변경하지 않고 새 상태 반환
    public static GameState CalculateNewState(GameState currentState, GameAction action)
    {
        switch (action.Type)
        {
            case ActionType.MovePlayer:
                return MoveEntity(currentState, action.EntityId, action.Direction);
            case ActionType.Attack:
                return ResolveAttack(currentState, action.AttackerId, action.TargetId);
            // 기타 액션 처리...
            default:
                return currentState;
        }
    }
    
    // 순수 함수들로 구성된 게임 로직
    private static GameState MoveEntity(GameState state, string entityId, Vector3 direction)
    {
        if (entityId == state.Player.Id)
        {
            Vector3 newPosition = state.Player.Position + direction;
            
            // 충돌 검사 (순수 함수로)
            if (HasCollision(state, newPosition))
            {
                return state; // 이동 불가능한 경우 상태 유지
            }
            
            // 불변 업데이트 패턴
            PlayerState newPlayer = new PlayerState(
                state.Player.Id,
                newPosition,
                state.Player.Health,
                state.Player.Inventory
            );
            
            return new GameState(newPlayer, state.Enemies, state.Score);
        }
        
        // 적 이동 로직도 유사하게 구현...
        return state;
    }
    
    private static GameState ResolveAttack(GameState state, string attackerId, string targetId)
    {
        // 공격 해결 로직 (순수 함수)
        // ...
        return state;
    }
    
    private static bool HasCollision(GameState state, Vector3 position)
    {
        // 충돌 감지 로직 (순수 함수)
        // ...
        return false;
    }
}

// 이벤트 관리 (RP): 반응형 스트림 처리
public class EventManager : MonoBehaviour
{
    private Subject<GameAction> actionSubject = new Subject<GameAction>();
    private CompositeDisposable disposables = new CompositeDisposable();
    
    private IObservable<GameState> stateChanges;
    private IObservable<GameEffect> effectStream;
    
    private GameState currentState;
    
    private void Awake()
    {
        // 초기 상태 설정
        currentState = CreateInitialState();
        
        // 스트림 설정
        SetupEventStreams();
    }
    
    private GameState CreateInitialState()
    {
        PlayerState player = new PlayerState(
            "player1",
            Vector3.zero,
            100,
            new List<string>()
        );
        
        return new GameState(
            player,
            new Dictionary<string, EnemyState>(),
            0
        );
    }
    
    private void SetupEventStreams()
    {
        // 키보드 입력 -> 게임 액션 변환
        IObservable<GameAction> keyboardEvents = Observable.EveryUpdate()
            .Where(_ => Input.anyKeyDown)
            .Select(_ => MapInputToAction())
            .Where(action => action != null);
        
        // 다양한 이벤트 소스 병합
        IObservable<GameAction> gameEvents = Observable.Merge(
            keyboardEvents,
            actionSubject.AsObservable()
        );
        
        // 상태 변화 스트림
        stateChanges = gameEvents
            .Scan(currentState, (state, action) => GameRules.CalculateNewState(state, action))
            .Do(newState => currentState = newState)
            .Share();
        
        // 이펙트 스트림 (상태 변화에서 이펙트 감지)
        effectStream = stateChanges
            .Pairwise()
            .SelectMany(states => DetectStateChanges(states.Previous, states.Current))
            .Share();
    }
    
    private GameAction MapInputToAction()
    {
        if (Input.GetKeyDown(KeyCode.W))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.forward);
        if (Input.GetKeyDown(KeyCode.S))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.back);
        if (Input.GetKeyDown(KeyCode.A))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.left);
        if (Input.GetKeyDown(KeyCode.D))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.right);
        if (Input.GetKeyDown(KeyCode.Space))
            return new GameAction(ActionType.Attack, "player1", null, GetNearestEnemyId());
            
        return null;
    }
    
    private string GetNearestEnemyId()
    {
        // 가장 가까운 적 ID 찾기
        // ...
        return "enemy1";
    }
    
    private IEnumerable<GameEffect> DetectStateChanges(GameState oldState, GameState newState)
    {
        List<GameEffect> effects = new List<GameEffect>();
        
        // 플레이어 위치 변경 감지
        if (oldState.Player.Position != newState.Player.Position)
        {
            effects.Add(new GameEffect(EffectType.EntityMoved, "player1", newState.Player.Position));
        }
        
        // 기타 상태 변화 감지...
        
        return effects;
    }
    
    public void Dispatch(GameAction action)
    {
        actionSubject.OnNext(action);
    }
    
    public IDisposable SubscribeToState(IObserver<GameState> observer)
    {
        // 현재 상태 즉시 전달
        observer.OnNext(currentState);
        
        // 이후 상태 변화 구독
        return stateChanges.Subscribe(observer);
    }
    
    public IDisposable SubscribeToEffects(IObserver<GameEffect> observer)
    {
        return effectStream.Subscribe(observer);
    }
    
    private void OnDestroy()
    {
        disposables.Dispose();
    }
}

// 게임 시스템 관리 (OOP): 객체 간의 관계와 책임 분배
public class GameSystem : MonoBehaviour
{
    private EventManager eventManager;
    private List<IGameSubsystem> subsystems = new List<IGameSubsystem>();
    private CompositeDisposable disposables = new CompositeDisposable();
    
    private void Awake()
    {
        eventManager = GetComponent<EventManager>();
        
        // 서브시스템 초기화
        subsystems.Add(GetComponent<RenderingSystem>());
        subsystems.Add(GetComponent<PhysicsSystem>());
        subsystems.Add(GetComponent<AISystem>());
        subsystems.Add(GetComponent<AudioSystem>());
        
        // 상태 변화 구독
        eventManager.SubscribeToState(new AnonymousObserver<GameState>(
            state => UpdateSubsystems(state)
        )).AddTo(disposables);
        
        // 이펙트 구독
        eventManager.SubscribeToEffects(new AnonymousObserver<GameEffect>(
            effect => HandleEffect(effect)
        )).AddTo(disposables);
    }
    
    private void UpdateSubsystems(GameState newState)
    {
        foreach (var system in subsystems)
        {
            system.UpdateSystem(newState);
        }
    }
    
    private void HandleEffect(GameEffect effect)
    {
        switch (effect.Type)
        {
            case EffectType.EntityMoved:
                subsystems.OfType<AudioSystem>().FirstOrDefault()?.PlaySound("footstep");
                subsystems.OfType<RenderingSystem>().FirstOrDefault()?.ShowMovementEffect(effect.EntityId, effect.Position);
                break;
            case EffectType.EntityDamaged:
                subsystems.OfType<AudioSystem>().FirstOrDefault()?.PlaySound("hit");
                subsystems.OfType<RenderingSystem>().FirstOrDefault()?.ShowDamageEffect(effect.EntityId, effect.Value);
                break;
            // 다른 이펙트 처리...
        }
    }
    
    private void OnDestroy()
    {
        disposables.Dispose();
        
        foreach (var system in subsystems)
        {
            if (system is IDisposable disposableSystem)
            {
                disposableSystem.Dispose();
            }
        }
    }
}

// 서브시스템 인터페이스
public interface IGameSubsystem
{
    void UpdateSystem(GameState state);
}

// 렌더링 시스템 예시
public class RenderingSystem : MonoBehaviour, IGameSubsystem
{
    public GameObject playerPrefab;
    private Dictionary<string, GameObject> entityViews = new Dictionary<string, GameObject>();
    
    public void UpdateSystem(GameState state)
    {
        // 플레이어 위치 업데이트
        EnsureEntityView(state.Player.Id, playerPrefab);
        UpdateEntityPosition(state.Player.Id, state.Player.Position);
        
        // 적 위치 업데이트
        foreach (var enemyEntry in state.Enemies)
        {
            // 적 렌더링 로직...
        }
    }
    
    private void EnsureEntityView(string entityId, GameObject prefab)
    {
        if (!entityViews.ContainsKey(entityId))
        {
            GameObject view = Instantiate(prefab);
            entityViews[entityId] = view;
        }
    }
    
    private void UpdateEntityPosition(string entityId, Vector3 position)
    {
        if (entityViews.TryGetValue(entityId, out GameObject view))
        {
            view.transform.position = position;
        }
    }
    
    public void ShowMovementEffect(string entityId, Vector3 position)
    {
        // 이동 이펙트 표시
    }
    
    public void ShowDamageEffect(string entityId, int damage)
    {
        // 데미지 이펙트 표시
    }
}

// 게임 I/O 관리 (PP): 외부 시스템과의 인터페이스
public class GameIO : MonoBehaviour
{
    private EventManager eventManager;
    private float saveInterval = 60f; // 60초마다 저장
    private float timeSinceLastSave = 0f;
    
    private void Start()
    {
        eventManager = GetComponent<EventManager>();
        
        // 게임 데이터 로드
        LoadGameData();
    }
    
    private void Update()
    {
        // 주기적 저장
        timeSinceLastSave += Time.deltaTime;
        if (timeSinceLastSave >= saveInterval)
        {
            SaveGameData();
            timeSinceLastSave = 0f;
        }
    }
    
    private void LoadGameData()
    {
        try
        {
            // PlayerPrefs 또는 파일에서 데이터 로드
            string savedData = PlayerPrefs.GetString("GameSave", "");
            if (!string.IsNullOrEmpty(savedData))
            {
                // JSON 데이터를 GameState로 변환
                // GameState savedState = JsonUtility.FromJson<GameState>(savedData);
                
                // 상태 복원 액션 디스패치
                // eventManager.Dispatch(new GameAction(ActionType.LoadState, savedState));
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to load game data: {e.Message}");
        }
    }
    
    private void SaveGameData()
    {
        try
        {
            // 현재 상태 구독하여 저장
            eventManager.SubscribeToState(new AnonymousObserver<GameState>(
                state => {
                    // GameState를 JSON으로 변환하여 저장
                    // string json = JsonUtility.ToJson(state);
                    // PlayerPrefs.SetString("GameSave", json);
                    // PlayerPrefs.Save();
                    
                    Debug.Log("Game state saved");
                },
                ex => Debug.LogError($"Error saving game state: {ex.Message}"),
                () => {}
            )).Dispose(); // 일회성 구독
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to save game data: {e.Message}");
        }
    }
    
    private void OnApplicationQuit()
    {
        SaveGameData();
    }
}

// 메인 게임 클래스: 모든 패러다임의 통합 포인트
public class Game : MonoBehaviour
{
    private EventManager eventManager;
    private GameSystem gameSystem;
    private GameIO gameIO;
    
    private void Awake()
    {
        // 이벤트 관리자 (RP)
        eventManager = GetComponent<EventManager>();
        
        // 게임 시스템 (OOP)
        gameSystem = GetComponent<GameSystem>();
        
        // I/O 시스템 (PP)
        gameIO = GetComponent<GameIO>();
    }
    
    private void Start()
    {
        Debug.Log("Game started!");
    }
    
    private void OnDestroy()
    {
        Debug.Log("Game resources released.");
    }
}.MovePlayer, "player1", Vector3.forward);
        if (Input.GetKeyDown(KeyCode.S))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.back);
        if (Input.GetKeyDown(KeyCode.A))
            return new GameAction(ActionType.MovePlayer, "player1", Vector3.left);
        if (Input.GetKeyDown(KeyCode.D))
            return new GameAction(ActionType# 다층적 패러다임 개발에서의 리팩토링과 패턴 적용

## 서론: 개발 과정의 진화

소프트웨어 개발은 단순히 코드를 작성하는 행위를 넘어, 지속적인 구조 개선과 패턴 적용의 순환 과정입니다. 특히 다층적 패러다임(FP/RP 코어, OOP/PP 셸)을 활용하는 현대적 아키텍처에서는, 리팩토링과 디자인 패턴의 적용이 단순한 코드 개선을 넘어 시스템 전체의 균형과 조화를 이루는 핵심 요소가 됩니다. 이 글에서는 각 패러다임 계층에 적합한 리팩토링 기법과 패턴 적용 방법을 구체적인 코드와 사례를 통해 살펴보겠습니다.

## 1. 코어 레이어: FP/RP 영역의 리팩토링

### 1.1. 함수형 코어의 리팩토링

함수형 프로그래밍(FP)이 적용된 코어 영역에서는 순수성(purity)과 불변성(immutability)이 핵심입니다. 이 영역의 리팩토링은 이러한 원칙을 강화하는 방향으로 진행됩니다.

#### 사례: 상태 변이를 제거하는 리팩토링

**Before:**
```javascript
function calculateDamage(character, weapon, target) {
  let damage = weapon.baseDamage;
  
  // 상태 변이가 발생하는 로직
  if (character.hasSkill('power_attack')) {
    damage *= 1.5;
  }

// 게임 시스템 관리 (OOP): 객체 간의 관계와 책임 분배
class GameSystem {
  constructor(eventManager) {
    this.eventManager = eventManager;
    this.systems = [
      new RenderingSystem(),
      new PhysicsSystem(),
      new AISystem(),
      new AudioSystem()
    ];
    
    // 상태 변화 구독
    this.stateSubscription = this.eventManager.subscribeToState(
      this.onStateChange.bind(this)
    );
    
    // 이펙트 구독
    this.effectsSubscription = this.eventManager.subscribeToEffects(
      this.handleEffect.bind(this)
    );
    
    // 초기 상태
    this.currentState = initialGameState;
  }
  
  onStateChange(newState) {
    // 이전 상태 저장
    const prevState = this.currentState;
    this.currentState = newState;
    
    // 모든 시스템 업데이트
    for (const system of this.systems) {
      system.update(newState, prevState);
    }
  }
  
  handleEffect(effect) {
    switch (effect.type) {
      case 'ENTITY_DAMAGED':
        this.systems.find(s => s instanceof AudioSystem)
          .playSound('hit', effect.damage);
        this.systems.find(s => s instanceof RenderingSystem)
          .showDamageEffect(effect.entityId, effect.damage);
        break;
      case 'ENTITY_DEFEATED':
        this.systems.find(s => s instanceof AudioSystem)
          .playSound('defeat');
        this.systems.find(s => s instanceof RenderingSystem)
          .showDefeatAnimation(effect.entityId);
        break;
      // 다른 이펙트 처리...
    }
  }
  
  dispose() {
    this.stateSubscription.unsubscribe();
    this.effectsSubscription.unsubscribe();
    
    for (const system of this.systems) {
      if (typeof system.dispose === 'function') {
        system.dispose();
      }
    }
  }
}

// 절차적 입출력 처리 (PP): 외부 시스템과의 인터페이스
class GameIO {
  constructor(eventManager) {
    this.eventManager = eventManager;
    this.saveInterval = null;
    this.initialized = false;
  }
  
  async initialize() {
    if (this.initialized) return;
    
    try {
      // 게임 데이터 로드
      const savedData = await this.loadGameData();
      if (savedData) {
        this.eventManager.dispatch({ type: 'LOAD_STATE', state: savedData });
      }
      
      // 주기적 저장 설정
      this.saveInterval = setInterval(() => this.saveCurrentState(), 60000);
      
      // 네트워크 연결 설정
      await this.setupNetworkConnection();
      
      this.initialized = true;
    } catch (error) {
      console.error('Game IO initialization failed:', error);
      // 에러 처리 로직
    }
  }
  
  async loadGameData() {
    try {
      const data = localStorage.getItem('gameState');
      return data ? JSON.parse(data) : null;
    } catch (error) {
      console.error('Failed to load game data:', error);
      return null;
    }
  }
  
  async saveCurrentState() {
    let currentState = null;
    
    // 일회성 상태 구독을 통해 현재 상태 가져오기
    const subscription = this.eventManager.subscribeToState(state => {
      currentState = state;
      subscription.unsubscribe();
    });
    
    if (currentState) {
      try {
        localStorage.setItem('gameState', JSON.stringify(currentState));
      } catch (error) {
        console.error('Failed to save game state:', error);
      }
    }
  }
  
  async setupNetworkConnection() {
    // 네트워크 초기화 로직 
    // ...
  }
  
  dispose() {
    if (this.saveInterval) {
      clearInterval(this.saveInterval);
    }
    // 네트워크 리소스 정리
    // ...
  }
}

// 메인 게임 애플리케이션: 모든 패러다임의 통합 포인트
class Game {
  constructor() {
    // 이벤트 관리자 생성 (RP)
    this.eventManager = new EventManager();
    
    // 게임 시스템 초기화 (OOP)
    this.gameSystem = new GameSystem(this.eventManager);
    
    // IO 시스템 초기화 (PP)
    this.gameIO = new GameIO(this.eventManager);
  }
  
  async start() {
    console.log('Starting game...');
    
    // IO 시스템 초기화
    await this.gameIO.initialize();
    
    // 게임 로직 설정 (FP)
    // (이미 EventManager에서 GameRules와 연결됨)
    
    console.log('Game started!');
  }
  
  dispose() {
    this.gameSystem.dispose();
    this.gameIO.dispose();
    console.log('Game resources released.');
  }
}

// 게임 인스턴스 생성 및 시작
const game = new Game();
game.start().catch(error => {
  console.error('Game failed to start:', error);
});
  
  if (target.isVulnerableTo(weapon.damageType)) {
    damage *= 2;
  }
  
  return Math.max(1, Math.floor(damage - target.defense));
}

After:

 
javascript
// 순수 함수들로 분해
const applySkillModifier = (damage, character) => 
  character.hasSkill('power_attack') ? damage * 1.5 : damage;

const applyVulnerabilityModifier = (damage, target, damageType) => 
  target.isVulnerableTo(damageType) ? damage * 2 : damage;

const applyDefense = (damage, defense) => 
  Math.max(1, Math.floor(damage - defense));

// 함수 합성을 통한 파이프라인
const calculateDamage = (character, weapon, target) => 
  [
    weapon.baseDamage,
    damage => applySkillModifier(damage, character),
    damage => applyVulnerabilityModifier(damage, target, weapon.damageType),
    damage => applyDefense(damage, target.defense)
  ].reduce((damage, fn) => fn(damage), weapon.baseDamage);

핵심 리팩토링 패턴:

  1. Replace Mutation with Transformation: 상태 변이를 변환으로 대체
  2. Extract Pure Function: 부작용이 없는 순수 함수 추출
  3. Compose Method: 작은 함수들의 합성으로 복잡한 로직 구성
  4. Replace Imperative Logic with Collection Pipeline: 명령형 로직을 컬렉션 파이프라인으로 대체

1.2. 반응형 코어의 리팩토링

반응형 프로그래밍(RP)이 적용된 영역에서는 데이터 흐름과 변화의 전파가 핵심입니다. 이 영역의 리팩토링은 명시적인 상태 관리에서 선언적인 데이터 흐름으로 전환하는 데 중점을 둡니다.

사례: 콜백 중첩을 UniRx 반응형 스트림으로 리팩토링

Before:

 
csharp
public class PlayerTracker : MonoBehaviour
{
    public Player player;
    private Coroutine trackingCoroutine;
    
    private void Start()
    {
        trackingCoroutine = StartCoroutine(TrackPlayerPosition());
    }
    
    private IEnumerator TrackPlayerPosition()
    {
        while (true)
        {
            Vector3 position = player.GetPosition();
            
            // UI 위치 업데이트
            UpdateUIPosition(position);
            
            // 콜백 지옥 시작
            CheckCollision(position, (collidedObject) => {
                if (collidedObject != null)
                {
                    HandleCollision(player, collidedObject, () => {
                        CheckQuest(player, collidedObject, (questUpdated) => {
                            if (questUpdated)
                            {
                                UpdateQuestUI();
                            }
                        });
                    });
                }
            });
            
            yield return new WaitForSeconds(0.1f);
        }
    }
    
    private void OnDestroy()
    {
        if (trackingCoroutine != null)
        {
            StopCoroutine(trackingCoroutine);
        }
    }
}

After (UniRx 사용):

 
csharp
using System;
using UniRx;
using UnityEngine;

public class PlayerTracker : MonoBehaviour
{
    public Player player;
    private CompositeDisposable disposables = new CompositeDisposable();
    
    private void Start()
    {
        // 위치 스트림 생성
        IObservable<Vector3> positionStream = Observable
            .Interval(TimeSpan.FromSeconds(0.1f))
            .Select(_ => player.GetPosition())
            .Share();
        
        // UI 업데이트 스트림
        positionStream
            .Subscribe(position => UpdateUIPosition(position))
            .AddTo(disposables);
        
        // 충돌 감지 스트림
        positionStream
            .SelectMany(position => Observable.FromCoroutine(() => CheckCollisionAsync(position)))
            .Where(collidedObject => collidedObject != null)
            .SelectMany(collidedObject => 
                Observable.FromCoroutine(() => HandleCollisionAsync(player, collidedObject))
                    .Select(_ => collidedObject))
            .SelectMany(collidedObject => 
                Observable.FromCoroutine(() => CheckQuestAsync(player, collidedObject)))
            .Where(questUpdated => questUpdated)
            .Subscribe(_ => UpdateQuestUI())
            .AddTo(disposables);
    }
    
    private void OnDestroy()
    {
        disposables.Dispose();
    }
    
    // 비동기 버전의 메서드들
    private IEnumerator CheckCollisionAsync(Vector3 position)
    {
        // 비동기 충돌 체크 로직
        yield return null;
    }
    
    private IEnumerator HandleCollisionAsync(Player player, GameObject collidedObject)
    {
        // 비동기 충돌 처리 로직
        yield return null;
    }
    
    private IEnumerator CheckQuestAsync(Player player, GameObject collidedObject)
    {
        // 비동기 퀘스트 체크 로직
        yield return null;
        yield return true; // 퀘스트 업데이트 여부
    }
}

핵심 리팩토링 패턴:

  1. Replace Callback with Stream: 중첩된 콜백을 스트림으로 대체
  2. Decompose Stream: 복잡한 스트림을 작은 스트림으로 분해
  3. Extract Operator Chain: 연산자 체인 추출을 통한 가독성 향상
  4. Replace Temporal Coupling with Stream Composition: 시간적 결합을 스트림 합성으로 대체

2. 셸 레이어: OOP/PP 영역의 리팩토링

2.1. 객체지향 셸의 리팩토링

객체지향 프로그래밍(OOP)이 적용된 셸 영역에서는 책임 분배와 객체 간 협력이 핵심입니다. 이 영역의 리팩토링은 응집도를 높이고 결합도를 낮추는 데 중점을 둡니다.

사례: 블롭 객체를 책임 기반 시스템으로 리팩토링

Before:

 
csharp
// 너무 많은 책임을 가진 "블롭" 클래스
public class GameManager : MonoBehaviour
{
    public List<GameObject> entities;
    public Player player;
    public UIManager ui;
    public AudioManager audio;
    
    private void Update()
    {
        float deltaTime = Time.deltaTime;
        
        // 플레이어 입력 처리
        ProcessPlayerInput();
        
        // 물리 시뮬레이션
        SimulatePhysics(entities, deltaTime);
        
        // 충돌 감지 및 처리
        DetectAndResolveCollisions();
        
        // AI 업데이트
        UpdateAI(deltaTime);
        
        // UI 업데이트
        ui.UpdateUI();
        
        // 오디오 처리
        audio.UpdateAudio();
    }
    
    private void ProcessPlayerInput()
    {
        // 수백 줄의 입력 처리 코드
        if (Input.GetKeyDown(KeyCode.W))
        {
            player.MoveForward();
        }
        
        if (Input.GetKeyDown(KeyCode.A))
        {
            player.MoveLeft();
        }
        
        // 기타 많은 입력 처리...
    }
    
    private void SimulatePhysics(List<GameObject> gameObjects, float deltaTime)
    {
        // 수백 줄의 물리 시뮬레이션 코드
    }
    
    private void DetectAndResolveCollisions()
    {
        // 수백 줄의 충돌 처리 코드
    }
    
    private void UpdateAI(float deltaTime)
    {
        // 수백 줄의 AI 업데이트 코드
    }
    
    // 기타 수십 개의 메서드들...
}

After:

 
csharp
// 상위 레벨 조정자로서의 GameManager
public class GameManager : MonoBehaviour
{
    private InputSystem inputSystem;
    private PhysicsSystem physicsSystem;
    private CollisionSystem collisionSystem;
    private AISystem aiSystem;
    private UISystem uiSystem;
    private AudioSystem audioSystem;
    private EntityManager entityManager;
    
    private void Awake()
    {
        entityManager = new EntityManager();
        
        inputSystem = GetComponent<InputSystem>();
        physicsSystem = GetComponent<PhysicsSystem>();
        collisionSystem = GetComponent<CollisionSystem>();
        aiSystem = GetComponent<AISystem>();
        uiSystem = GetComponent<UISystem>();
        audioSystem = GetComponent<AudioSystem>();
        
        // 의존성 주입
        inputSystem.Initialize(entityManager);
        physicsSystem.Initialize(entityManager);
        collisionSystem.Initialize(entityManager);
        aiSystem.Initialize(entityManager);
        uiSystem.Initialize(entityManager);
        audioSystem.Initialize(entityManager);
    }
    
    private void Update()
    {
        float deltaTime = Time.deltaTime;
        
        inputSystem.UpdateSystem(deltaTime);
        physicsSystem.UpdateSystem(deltaTime);
        collisionSystem.UpdateSystem(deltaTime);
        aiSystem.UpdateSystem(deltaTime);
        uiSystem.UpdateSystem(deltaTime);
        audioSystem.UpdateSystem(deltaTime);
    }
}

// 단일 책임을 가진 시스템 클래스의 예
public class CollisionSystem : MonoBehaviour, IGameSystem
{
    private EntityManager entityManager;
    
    public void Initialize(EntityManager entityManager)
    {
        this.entityManager = entityManager;
    }
    
    public void UpdateSystem(float deltaTime)
    {
        List<GameObject> entities = entityManager.GetEntities();
        // 충돌 감지 및 처리 로직
        DetectCollisions(entities);
        ResolveCollisions(entities);
    }
    
    private void DetectCollisions(List<GameObject> entities)
    {
        // 충돌 감지 로직
    }
    
    private void ResolveCollisions(List<GameObject> entities)
    {
        // 충돌 해결 로직
    }
}

// 시스템 인터페이스
public interface IGameSystem
{
    void Initialize(EntityManager entityManager);
    void UpdateSystem(float deltaTime);
}

// 엔티티 관리자
public class EntityManager
{
    private List<GameObject> entities = new List<GameObject>();
    
    public List<GameObject> GetEntities()
    {
        return entities;
    }
    
    public void RegisterEntity(GameObject entity)
    {
        entities.Add(entity);
    }
    
    public void UnregisterEntity(GameObject entity)
    {
        entities.Remove(entity);
    }
}

핵심 리팩토링 패턴:

  1. Extract Class: 책임에 따른 클래스 추출
  2. Move Method: 적절한 클래스로 메서드 이동
  3. Replace Inheritance with Composition: 상속을 컴포지션으로 대체
  4. Introduce Parameter Object: 매개변수 객체 도입으로 응집도 향상

2.2. 절차적 셸의 리팩토링

절차적 프로그래밍(PP)이 적용된 영역에서는 명확한 제어 흐름과 데이터 변환이 핵심입니다. 이 영역의 리팩토링은 알고리즘의 명확성과 효율성을 높이는 데 중점을 둡니다.

사례: 복잡한 조건문을 단계적 처리로 리팩토링

Before:

 
csharp
public class TurnManager : MonoBehaviour
{
    public void ProcessPlayerTurn(Player player, GameState state)
    {
        // 복잡한 중첩 조건문
        if (player.HasActionPoints())
        {
            PlayerAction action = GetPlayerAction();
            if (action != null)
            {
                if (action.Type == ActionType.Move)
                {
                    if (IsValidMove(player, action.TargetPosition, state))
                    {
                        MovePlayer(player, action.TargetPosition);
                        player.actionPoints--;
                        
                        // 이동 후 특수 타일 처리
                        Tile tile = GetTileAt(action.TargetPosition, state);
                        if (tile.Type == TileType.Treasure)
                        {
                            GiveTreasureToPlayer(player, tile);
                        }
                        else if (tile.Type == TileType.Trap)
                        {
                            ApplyTrapEffect(player, tile);
                        }
                        else if (tile.Type == TileType.Portal)
                        {
                            TeleportPlayer(player, tile.LinkedPosition);
                        }
                    }
                    else
                    {
                        ShowInvalidMoveMessage();
                    }
                }
                else if (action.Type == ActionType.Attack)
                {
                    // 공격 관련 복잡한 조건문들...
                }
                else if (action.Type == ActionType.UseItem)
                {
                    // 아이템 사용 관련 복잡한 조건문들...
                }
            }
        }
        else
        {
            EndPlayerTurn(player, state);
        }
    }
}

After:

 
csharp
public class TurnManager : MonoBehaviour
{
    public void ProcessPlayerTurn(Player player, GameState state)
    {
        if (!player.HasActionPoints())
        {
            EndPlayerTurn(player, state);
            return;
        }
        
        PlayerAction action = GetPlayerAction();
        if (action == null)
        {
            return;
        }
        
        switch (action.Type)
        {
            case ActionType.Move:
                ProcessMoveAction(player, action, state);
                break;
            case ActionType.Attack:
                ProcessAttackAction(player, action, state);
                break;
            case ActionType.UseItem:
                ProcessItemAction(player, action, state);
                break;
            default:
                ShowUnknownActionMessage();
                break;
        }
    }
    
    private void ProcessMoveAction(Player player, PlayerAction action, GameState state)
    {
        if (!IsValidMove(player, action.TargetPosition, state))
        {
            ShowInvalidMoveMessage();
            return;
        }
        
        MovePlayer(player, action.TargetPosition);
        player.actionPoints--;
        
        Tile tile = GetTileAt(action.TargetPosition, state);
        ProcessTileEffect(player, tile);
    }
    
    private void ProcessTileEffect(Player player, Tile tile)
    {
        switch (tile.Type)
        {
            case TileType.Treasure:
                GiveTreasureToPlayer(player, tile);
                break;
            case TileType.Trap:
                ApplyTrapEffect(player, tile);
                break;
            case TileType.Portal:
                TeleportPlayer(player, tile.LinkedPosition);
                break;
            // 기본 타일은 아무 효과 없음
        }
    }
    
    // 공격, 아이템 사용 등의 처리 메서드...
}

핵심 리팩토링 패턴:

  1. Replace Nested Conditional with Guard Clauses: 중첩 조건문을 보호 구문으로 대체
  2. Extract Method: 논리적 단계에 따른 메서드 추출
  3. Replace Conditional with Polymorphism: 조건문을 다형성으로 대체 (더 발전된 OOP 리팩토링)
  4. Decompose Conditional: 복잡한 조건식 분해ureToPlayer(player, tile); } else if (tile->type == TILE_TRAP) { applyTrapEffect(player, tile); } else if (tile->type == TILE_PORTAL) { teleportPlayer(player, tile->linkedPosition); } } else { showInvalidMoveMessage(); } } else if (action->type == ACTION_ATTACK) { // 공격 관련 복잡한 조건문들... } else if (action->type == ACTION_USE_ITEM) { // 아이템 사용 관련 복잡한 조건문들... } } } else { endPlayerTurn(player, state); } }
 
**After:**
```c
void processPlayerTurn(Player* player, GameState* state) {
    if (!player->hasActionPoints()) {
        endPlayerTurn(player, state);
        return;
    }
    
    Action* action = getPlayerAction();
    if (action == NULL) {
        return;
    }
    
    switch (action->type) {
        case ACTION_MOVE:
            processMoveAction(player, action, state);
            break;
        case ACTION_ATTACK:
            processAttackAction(player, action, state);
            break;
        case ACTION_USE_ITEM:
            processItemAction(player, action, state);
            break;
        default:
            showUnknownActionMessage();
    }
}

void processMoveAction(Player* player, Action* action, GameState* state) {
    if (!isValidMove(player, action->targetPosition, state)) {
        showInvalidMoveMessage();
        return;
    }
    
    movePlayer(player, action->targetPosition);
    player->actionPoints--;
    
    processTileEffect(player, getTileAt(action->targetPosition, state));
}

void processTileEffect(Player* player, Tile* tile) {
    switch (tile->type) {
        case TILE_TREASURE:
            giveTreasureToPlayer(player, tile);
            break;
        case TILE_TRAP:
            applyTrapEffect(player, tile);
            break;
        case TILE_PORTAL:
            teleportPlayer(player, tile->linkedPosition);
            break;
        // 기본 타일은 아무 효과 없음
    }
}

핵심 리팩토링 패턴:

  1. Replace Nested Conditional with Guard Clauses: 중첩 조건문을 보호 구문으로 대체
  2. Extract Method: 논리적 단계에 따른 메서드 추출
  3. Replace Conditional with Polymorphism: 조건문을 다형성으로 대체
  4. Decompose Conditional: 복잡한 조건식 분해

3. 패러다임 간 경계에서의 리팩토링

다층적 패러다임 아키텍처에서 가장 어려운 부분은 서로 다른 패러다임 간의 경계를 관리하는 것입니다. 이 영역의 리팩토링은 패러다임 간 전환을 명확하고 안전하게 만드는 데 중점을 둡니다.

3.1. FP/RP 코어와 OOP 셸 사이의 경계

사례: 불변 데이터와 가변 UI 연결

Before:

 
csharp
// 불변 상태를 관리하는 함수형 코어
public static class GameLogic
{
    public static GameState ProcessAction(GameState state, GameAction action)
    {
        switch (action.Type)
        {
            case ActionType.MovePlayer:
                // 불변 업데이트 패턴
                Vector3 newPosition = CalculateNewPosition(state.Player, action.Direction);
                Player updatedPlayer = new Player(
                    state.Player.Id,
                    newPosition,
                    state.Player.Health,
                    state.Player.Inventory
                );
                
                return new GameState(
                    updatedPlayer,
                    state.Enemies,
                    state.Environment,
                    state.Score
                );
            // 다른 액션 처리...
            default:
                return state;
        }
    }
    
    private static Vector3 CalculateNewPosition(Player player, Vector3 direction)
    {
        // 위치 계산 로직
        return player.Position + direction;
    }
}

// OOP UI 컴포넌트
public class GameView : MonoBehaviour
{
    public GameObject playerSprite;
    private GameState currentState;
    
    public void Initialize(GameState initialState)
    {
        currentState = initialState;
        Render();
    }
    
    public void Update(GameState newState)
    {
        // 직접적인 상태 갱신
        currentState = newState;
        Render();
    }
    
    private void Render()
    {
        // 상태를 직접 읽어 UI 업데이트
        playerSprite.transform.position = currentState.Player.Position;
        // 기타 렌더링 로직...
    }
}

// 사용 예
public class GameController : MonoBehaviour
{
    private GameState gameState;
    public GameView view;
    
    private void Start()
    {
        gameState = new GameState(/*초기 상태*/);
        view.Initialize(gameState);
    }
    
    public void HandleInput(Vector3 direction)
    {
        gameState = GameLogic.ProcessAction(gameState, 
            new GameAction(ActionType.MovePlayer, direction));
        view.Update(gameState);
    }
}

After:

 
csharp
// 불변 상태를 관리하는 함수형 코어 (변경 없음)
public static class GameLogic
{
    public static GameState ProcessAction(GameState state, GameAction action)
    {
        switch (action.Type)
        {
            case ActionType.MovePlayer:
                Vector3 newPosition = CalculateNewPosition(state.Player, action.Direction);
                Player updatedPlayer = new Player(
                    state.Player.Id,
                    newPosition,
                    state.Player.Health,
                    state.Player.Inventory
                );
                
                return new GameState(
                    updatedPlayer,
                    state.Enemies,
                    state.Environment,
                    state.Score
                );
            // 다른 액션 처리...
            default:
                return state;
        }
    }
    
    private static Vector3 CalculateNewPosition(Player player, Vector3 direction)
    {
        return player.Position + direction;
    }
}

// 경계 관리를 위한 중개자
public class GameStateManager : MonoBehaviour
{
    private GameState currentState;
    private List<IGameStateObserver> observers = new List<IGameStateObserver>();
    
    public void Initialize(GameState initialState)
    {
        currentState = initialState;
        NotifyObservers();
    }
    
    public void Dispatch(GameAction action)
    {
        GameState oldState = currentState;
        currentState = GameLogic.ProcessAction(currentState, action);
        
        // 상태가 실제로 변경된 경우에만 옵저버 호출
        if (!oldState.Equals(currentState))
        {
            NotifyObservers();
        }
    }
    
    public void AddObserver(IGameStateObserver observer)
    {
        if (!observers.Contains(observer))
        {
            observers.Add(observer);
            observer.OnStateChanged(currentState);
        }
    }
    
    public void RemoveObserver(IGameStateObserver observer)
    {
        observers.Remove(observer);
    }
    
    private void NotifyObservers()
    {
        // 불변 상태의 스냅샷을 옵저버에게 전달
        GameState snapshot = currentState;
        foreach (var observer in observers)
        {
            observer.OnStateChanged(snapshot);
        }
    }
}

// 옵저버 인터페이스
public interface IGameStateObserver
{
    void OnStateChanged(GameState newState);
}

// 렌더러는 상태의 구독자가 됨
public class GameRenderer : MonoBehaviour, IGameStateObserver
{
    public GameObject playerSprite;
    private GameStateManager stateManager;
    
    public void Initialize(GameStateManager manager)
    {
        stateManager = manager;
        stateManager.AddObserver(this);
    }
    
    public void OnStateChanged(GameState newState)
    {
        Render(newState);
    }
    
    private void Render(GameState state)
    {
        playerSprite.transform.position = state.Player.Position;
        // 기타 렌더링 로직...
    }
    
    private void OnDestroy()
    {
        if (stateManager != null)
        {
            stateManager.RemoveObserver(this);
        }
    }
}

// 사용 예
public class GameController : MonoBehaviour
{
    private GameStateManager stateManager;
    public GameRenderer renderer;
    
    private void Start()
    {
        stateManager = GetComponent<GameStateManager>();
        stateManager.Initialize(new GameState(/*초기 상태*/));
        
        renderer.Initialize(stateManager);
    }
    
    public void HandleInput(Vector3 direction)
    {
        stateManager.Dispatch(new GameAction(ActionType.MovePlayer, direction));
    }
}

핵심 패턴:

  1. Introduce Mediator: 패러다임 간 중개자(GameStateManager) 도입
  2. Observer Pattern: 상태 변화 구독 메커니즘
  3. Immutable Snapshot: 불변 상태의 스냅샷 전달
  4. Unidirectional Data Flow: 단방향 데이터 흐름 유지

3.2. RP 코어와 PP 셸 사이의 경계

사례: 이벤트 스트림과 명령형 I/O 연결

Before:

 
csharp
// RP 스타일의 게임 이벤트 처리 (UniRx 사용)
public class GameEvents : MonoBehaviour
{
    private CompositeDisposable disposables = new CompositeDisposable();
    public Player player;
    
    private void Start()
    {
        // 플레이어 이동 스트림 설정
        IObservable<Vector3> playerMove = Observable.EveryUpdate()
            .Where(_ => Input.anyKeyDown)
            .Select(_ => {
                if (Input.GetKeyDown(KeyCode.W)) return Vector3.forward;
                if (Input.GetKeyDown(KeyCode.S)) return Vector3.back;
                if (Input.GetKeyDown(KeyCode.A)) return Vector3.left;
                if (Input.GetKeyDown(KeyCode.D)) return Vector3.right;
                return Vector3.zero;
            })
            .Where(dir => dir != Vector3.zero);
        
        // 게임 타이머 스트림
        IObservable<long> gameTimer = Observable.Interval(TimeSpan.FromMilliseconds(16))
            .Select(_ => new { Type = "TICK" });
        
        // 충돌 필터링 스트림
        IObservable<Vector3> validMove = playerMove
            .Select(direction => {
                Vector3 newPos = player.position + direction;
                return new {
                    Direction = direction,
                    HasCollision = Physics.CheckSphere(newPos, 0.5f)
                };
            })
            .Where(move => !move.HasCollision)
            .Select(move => move.Direction);
        
        // 구독 및 부작용 처리
        validMove.Subscribe(direction => {
            // 절차적 코드로의 직접 호출
            UpdatePlayerPosition(direction);
            RenderGame();
            SaveGameState();
        }).AddTo(disposables);
        
        gameTimer.Subscribe(_ => {
            UpdateGameLogic();
            RenderGame();
        }).AddTo(disposables);
    }
    
    private void OnDestroy()
    {
        disposables.Dispose();
    }
    
    // 절차적 메서드들
    private void UpdatePlayerPosition(Vector3 direction)
    {
        player.transform.position += direction;
    }
    
    private void RenderGame()
    {
        // 게임 렌더링 로직
    }
    
    private void SaveGameState()
    {
        // 게임 상태 저장 로직
    }
    
    private void UpdateGameLogic()
    {
        // 게임 로직 업데이트
    }
}

After:

 
csharp
// 명령 큐 시스템
public class CommandQueue
{
    private Queue<GameCommand> commands = new Queue<GameCommand>();
    
    public void Enqueue(GameCommand command)
    {
        commands.Enqueue(command);
    }
    
    public GameCommand[] ProcessAll()
    {
        GameCommand[] pendingCommands = commands.ToArray();
        commands.Clear();
        return pendingCommands;
    }
}

// 명령 클래스 계층
public abstract class GameCommand
{
    public abstract void Execute(GameContext context);
}

public class MovePlayerCommand : GameCommand
{
    public Vector3 Direction { get; private set; }
    
    public MovePlayerCommand(Vector3 direction)
    {
        Direction = direction;
    }
    
    public override void Execute(GameContext context)
    {
        context.Player.transform.position += Direction;
    }
}

// RP와 PP 사이의 어댑터
public class ReactiveToProceduralAdapter : MonoBehaviour
{
    public Player player;
    private CommandQueue moveQueue = new CommandQueue();
    private CompositeDisposable disposables = new CompositeDisposable();
    
    private void Start()
    {
        SetupReactiveStreams();
    }
    
    private void SetupReactiveStreams()
    {
        // 반응형 입력 스트림
        IObservable<Vector3> playerMove = Observable.EveryUpdate()
            .Where(_ => Input.anyKeyDown)
            .Select(_ => {
                if (Input.GetKeyDown(KeyCode.W)) return Vector3.forward;
                if (Input.GetKeyDown(KeyCode.S)) return Vector3.back;
                if (Input.GetKeyDown(KeyCode.A)) return Vector3.left;
                if (Input.GetKeyDown(KeyCode.D)) return Vector3.right;
                return Vector3.zero;
            })
            .Where(dir => dir != Vector3.zero);
        
        // 충돌 필터링 스트림
        IObservable<Vector3> validMove = playerMove
            .Select(direction => {
                Vector3 newPos = player.transform.position + direction;
                return new {
                    Direction = direction,
                    HasCollision = Physics.CheckSphere(newPos, 0.5f)
                };
            })
            .Where(move => !move.HasCollision)
            .Select(move => move.Direction);
        
        // 명령 큐에 추가
        validMove.Subscribe(direction => {
            moveQueue.Enqueue(new MovePlayerCommand(direction));
        }).AddTo(disposables);
    }
    
    public GameCommand[] GetCommands()
    {
        return moveQueue.ProcessAll();
    }
    
    private void OnDestroy()
    {
        disposables.Dispose();
    }
}

// 게임 컨텍스트 (명령 실행 환경)
public class GameContext
{
    public Player Player { get; set; }
    // 기타 게임 상태...
}

// 절차적 게임 루프
public class GameLoop : MonoBehaviour
{
    public ReactiveToProceduralAdapter adapter;
    public Player player;
    private GameContext context;
    
    private void Start()
    {
        context = new GameContext { Player = player };
    }
    
    private void Update()
    {
        // 어댑터에서 명령 가져오기
        GameCommand[] commands = adapter.GetCommands();
        
        // 명령 처리
        foreach (GameCommand command in commands)
        {
            command.Execute(context);
        }
        
        // 게임 상태 업데이트
        UpdateGameLogic();
        
        // 렌더링
        RenderGame();
    }
    
    private void UpdateGameLogic()
    {
        // 게임 로직 업데이트
    }
    
    private void RenderGame()
    {
        // 게임 렌더링
    }
}

핵심 패턴:

  1. Command Queue: 반응형 이벤트를 명령 객체로 변환
  2. Adapter Pattern: 서로 다른 패러다임 간의 인터페이스 조정
  3. Batch Processing: 명령의 일괄 처리를 통한 효율성 확보
  4. Clear Boundary: 패러다임 전환 지점의 명확한 경계 설정

4. 패턴 적용의 전략적 접근

디자인 패턴의 적용은 단순히 코드의 구조를 개선하는 것을 넘어, 시스템의 진화 방향을 설정하는 전략적 결정입니다. 다층적 패러다임 아키텍처에서는 각 계층에 적합한 패턴을 선택하고 이를 적절히 통합하는 것이 중요합니다.

4.1. 패러다임별 핵심 패턴

함수형 프로그래밍(FP) 핵심 패턴:

  1. Option/Maybe 패턴: null/undefined 값의 안전한 처리
  2. 함수 합성(Function Composition): 작은 함수들을 조합하여 복잡한 연산 구성
  3. 커링(Currying): 다중 인자 함수를 단일 인자 함수들의 체인으로 변환
  4. 영속 자료구조(Persistent Data Structures): 효율적인 불변 데이터 구조

반응형 프로그래밍(RP) 핵심 패턴:

  1. Observable/Observer: 비동기 데이터 스트림의 생성과 구독
  2. Pub/Sub: 이벤트 발행과 구독을 통한 느슨한 결합
  3. Hot vs Cold Observables: 데이터 소스 공유 전략
  4. Backpressure Handling: 생산자-소비자 속도 불일치 관리

객체지향 프로그래밍(OOP) 핵심 패턴:

  1. Strategy: 알고리즘 캡슐화를 통한 런타임 교체
  2. Composite: 부분-전체 계층 구조의 일관된 처리
  3. Command: 요청의 객체화를 통한 실행 지연, 큐잉, 로깅
  4. Observer: 객체 간 일대다 의존 관계 관리

절차적 프로그래밍(PP) 핵심 패턴:

  1. 순차처리(Sequential Processing): 명확한 단계별 처리 흐름
  2. 테이블 조회(Table Lookup): 조건문을 테이블 조회로 대체
  3. 상태 머신(State Machine): 명시적 상태 전이 관리
  4. 버퍼링(Buffering): 데이터 처리와 I/O 분리

4.2. 패턴 적용 시 고려사항

패턴을 적용할 때는 다음과 같은 사항을 고려해야 합니다:

  1. 문제의 본질: 패턴은 문제 해결의 수단이지 목적이 아님을 인식
  2. 맥락 이해: 시스템의 특성과 요구사항에 맞는 패턴 선택
  3. 비용-이득 분석: 패턴 적용의 복잡성 증가와 이득 사이의 균형
  4. 진화 가능성: 미래의 변화와 확장을 수용할 수 있는 패턴 선택
  5. 팀 역량: 팀이 이해하고 유지보수할 수 있는 패턴 선택

4.3. 패턴 통합의 실제 사례

다음은 실제 게임 개발에서 다양한 패러다임의 패턴을 통합한 사례입니다:

 
typescript
// 핵심 도메인 로직 (FP): 순수 함수와 불변 데이터
const GameRules = {
  // 순수 함수: 상태를 변경하지 않고 새 상태 반환
  calculateNewState: (currentState, action) => {
    switch (action.type) {
      case 'MOVE':
        return moveEntity(currentState, action.entityId, action.direction);
      case 'ATTACK':
        return resolveAttack(currentState, action.attackerId, action.targetId);
      // 기타 액션 처리...
      default:
        return currentState;
    }
  }
};

// 순수 함수들로 구성된 게임 로직
function moveEntity(state, entityId, direction) {
  const entity = state.entities[entityId];
  if (!entity) return state;
  
  const newPosition = calculateNewPosition(entity.position, direction);
  
  // 충돌 검사
  if (hasCollision(state, newPosition)) {
    return state; // 이동 불가능한 경우 상태 유지
  }
  
  // 불변 업데이트 패턴
  return {
    ...state,
    entities: {
      ...state.entities,
      [entityId]: {
        ...entity,
        position: newPosition
      }
    }
  };
}

function resolveAttack(state, attackerId, targetId) {
  const attacker = state.entities[attackerId];
  const target = state.entities[targetId];
  
  if (!attacker || !target) return state;
  
  // 공격 범위 검사
  if (!isInRange(attacker, target, attacker.attackRange)) {
    return state;
  }
  
  // 데미지 계산 (순수 함수)
  const damage = calculateDamage(attacker, target);
  const newHealth = Math.max(0, target.health - damage);
  
  // 타겟 상태 업데이트
  const updatedTarget = {
    ...target,
    health: newHealth
  };
  
  // 타겟이 제거되었는지 확인
  if (newHealth <= 0) {
    // 불변 업데이트: 엔티티 제거
    const { [targetId]: removed, ...remainingEntities } = state.entities;
    
    return {
      ...state,
      entities: remainingEntities,
      events: [...state.events, { type: 'ENTITY_DEFEATED', entityId: targetId }]
    };
  }
  
  // 일반적인 경우: 타겟 업데이트
  return {
    ...state,
    entities: {
      ...state.entities,
      [targetId]: updatedTarget
    },
    events: [...state.events, { type: 'ENTITY_DAMAGED', entityId: targetId, damage }]
  };
}

// 이벤트 관리 (RP): 반응형 스트림 처리
class EventManager {
  constructor() {
    this.actionSubject = new Subject();
    this.setupEventStreams();
  }
  
  setupEventStreams() {
    // 입력 이벤트를 게임 액션으로 변환
    this.keyboardEvents$ = fromEvent(document, 'keydown').pipe(
      map(this.mapKeyToAction),
      filter(action => action !== null)
    );
    
    // 다양한 이벤트 소스를 병합
    this.gameEvents$ = merge(
      this.keyboardEvents$,
      this.actionSubject.asObservable()
    );
    
    // 액션 처리를 위한 스트림
    this.stateChange$ = this.gameEvents$.pipe(
      scan((state, action) => GameRules.calculateNewState(state, action), initialGameState),
      distinctUntilChanged(),
      share()
    );
    
    // 부작용 스트림 (애니메이션, 사운드 등)
    this.effects$ = this.stateChange$.pipe(
      pairwise(),
      map(([oldState, newState]) => detectStateChanges(oldState, newState)),
      filter(changes => changes.length > 0),
      mergeMap(changes => from(changes))
    );
  }
  
  dispatch(action) {
    this.actionSubject.next(action);
  }
  
  subscribeToState(observer) {
    return this.stateChange$.subscribe(observer);
  }
  
  subscribeToEffects(effectHandler) {
    return this.effects$.subscribe(effectHandler);
  }
  
  mapKeyToAction(event) {
    // 키보드 입력을 게임 액션으로 매핑
    switch(event.key) {
      case 'ArrowUp': return { type: 'MOVE', entityId: 'player', direction: 'up' };
      case 'ArrowDown': return { type: 'MOVE', entityId: 'player', direction: 'down' };
      case 'ArrowLeft': return { type: 'MOVE', entityId: 'player', direction: 'left' };
      case 'ArrowRight': return { type: 'MOVE', entityId: 'player', direction: 'right' };
      case ' ': return { type: 'ATTACK', entityId: 'player', targetId: getNearestEnemy() };
      default: return null;
    }
  }
}