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);
핵심 리팩토링 패턴:
- Replace Mutation with Transformation: 상태 변이를 변환으로 대체
- Extract Pure Function: 부작용이 없는 순수 함수 추출
- Compose Method: 작은 함수들의 합성으로 복잡한 로직 구성
- 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; // 퀘스트 업데이트 여부
}
}
핵심 리팩토링 패턴:
- Replace Callback with Stream: 중첩된 콜백을 스트림으로 대체
- Decompose Stream: 복잡한 스트림을 작은 스트림으로 분해
- Extract Operator Chain: 연산자 체인 추출을 통한 가독성 향상
- 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);
}
}
핵심 리팩토링 패턴:
- Extract Class: 책임에 따른 클래스 추출
- Move Method: 적절한 클래스로 메서드 이동
- Replace Inheritance with Composition: 상속을 컴포지션으로 대체
- 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;
// 기본 타일은 아무 효과 없음
}
}
// 공격, 아이템 사용 등의 처리 메서드...
}
핵심 리팩토링 패턴:
- Replace Nested Conditional with Guard Clauses: 중첩 조건문을 보호 구문으로 대체
- Extract Method: 논리적 단계에 따른 메서드 추출
- Replace Conditional with Polymorphism: 조건문을 다형성으로 대체 (더 발전된 OOP 리팩토링)
- 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;
// 기본 타일은 아무 효과 없음
}
}
핵심 리팩토링 패턴:
- Replace Nested Conditional with Guard Clauses: 중첩 조건문을 보호 구문으로 대체
- Extract Method: 논리적 단계에 따른 메서드 추출
- Replace Conditional with Polymorphism: 조건문을 다형성으로 대체
- 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));
}
}
핵심 패턴:
- Introduce Mediator: 패러다임 간 중개자(GameStateManager) 도입
- Observer Pattern: 상태 변화 구독 메커니즘
- Immutable Snapshot: 불변 상태의 스냅샷 전달
- 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()
{
// 게임 렌더링
}
}
핵심 패턴:
- Command Queue: 반응형 이벤트를 명령 객체로 변환
- Adapter Pattern: 서로 다른 패러다임 간의 인터페이스 조정
- Batch Processing: 명령의 일괄 처리를 통한 효율성 확보
- Clear Boundary: 패러다임 전환 지점의 명확한 경계 설정
4. 패턴 적용의 전략적 접근
디자인 패턴의 적용은 단순히 코드의 구조를 개선하는 것을 넘어, 시스템의 진화 방향을 설정하는 전략적 결정입니다. 다층적 패러다임 아키텍처에서는 각 계층에 적합한 패턴을 선택하고 이를 적절히 통합하는 것이 중요합니다.
4.1. 패러다임별 핵심 패턴
함수형 프로그래밍(FP) 핵심 패턴:
- Option/Maybe 패턴: null/undefined 값의 안전한 처리
- 함수 합성(Function Composition): 작은 함수들을 조합하여 복잡한 연산 구성
- 커링(Currying): 다중 인자 함수를 단일 인자 함수들의 체인으로 변환
- 영속 자료구조(Persistent Data Structures): 효율적인 불변 데이터 구조
반응형 프로그래밍(RP) 핵심 패턴:
- Observable/Observer: 비동기 데이터 스트림의 생성과 구독
- Pub/Sub: 이벤트 발행과 구독을 통한 느슨한 결합
- Hot vs Cold Observables: 데이터 소스 공유 전략
- Backpressure Handling: 생산자-소비자 속도 불일치 관리
객체지향 프로그래밍(OOP) 핵심 패턴:
- Strategy: 알고리즘 캡슐화를 통한 런타임 교체
- Composite: 부분-전체 계층 구조의 일관된 처리
- Command: 요청의 객체화를 통한 실행 지연, 큐잉, 로깅
- Observer: 객체 간 일대다 의존 관계 관리
절차적 프로그래밍(PP) 핵심 패턴:
- 순차처리(Sequential Processing): 명확한 단계별 처리 흐름
- 테이블 조회(Table Lookup): 조건문을 테이블 조회로 대체
- 상태 머신(State Machine): 명시적 상태 전이 관리
- 버퍼링(Buffering): 데이터 처리와 I/O 분리
4.2. 패턴 적용 시 고려사항
패턴을 적용할 때는 다음과 같은 사항을 고려해야 합니다:
- 문제의 본질: 패턴은 문제 해결의 수단이지 목적이 아님을 인식
- 맥락 이해: 시스템의 특성과 요구사항에 맞는 패턴 선택
- 비용-이득 분석: 패턴 적용의 복잡성 증가와 이득 사이의 균형
- 진화 가능성: 미래의 변화와 확장을 수용할 수 있는 패턴 선택
- 팀 역량: 팀이 이해하고 유지보수할 수 있는 패턴 선택
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;
}
}
}
'잡학다식' 카테고리의 다른 글
| 차원론적 세계관 체계 (0) | 2025.07.13 |
|---|---|
| 인과성을 거슬러 해석하려는 직감적 인식론자들의 심리와 판단체계의 문제 (1) | 2025.07.05 |
| 배우는 과정과 일하는 과정은 역순이다. (0) | 2025.05.14 |
| 리팩토링 글버전 (0) | 2025.05.13 |
| 모나딕 2차 논리 (0) | 2025.01.26 |