
오늘은 코드 리팩토링을 진행해볼것이다.
오늘은 Unity 프로젝트의 리소스 관리 코드를 리팩토링하면서 객체지향 설계 원칙과 디자인 패턴을 적용했습니다. 기존 코드는 모든 리소스 관리 기능을 하나의 클래스에 포함하고 있었기 때문에 유지보수와 확장이 어려웠습니다. 이를 해결하기 위해 단일 책임 원칙(SRP)을 적용하고 여러 클래스로 기능을 분리했습니다. 또한 옵저버 패턴을 사용하여 상태 변화에 따른 UI 업데이트를 구현했습니다.
리팩토링 전 코드
리팩토링 전에는 모든 리소스 관리 기능이 ResourceManager 클래스에 집중되어 있었습니다. 이는 클래스가 너무 많은 책임을 가지게 되어 유지보수와 테스트가 어려웠습니다.
public class ResourceManager : MonoBehaviour
{
[Header("Resource Fields")]
public int waterAmount = 0;
public int currentLevel = 1;
public int waterPerLevel = 10; // 레벨 당 필요한 물의 양
private const int maxWaterAmount = 200; // 물의 최대 양
[Header("UI Elements")]
public TextMeshProUGUI waterText; // 물의 재화량을 표시할 텍스트
public TextMeshProUGUI levelText; // 현재 레벨을 표시할 텍스트
public Image levelFillImage; // 레벨 바 이미지
public Image currentTreeImage; // 현재 나무 이미지
public Image upgradedTreeImage; // 강화된 나무 이미지
public SpriteRenderer outsideTreeSpriteRenderer; // 밖에 있는 나무 이미지
public TextMeshProUGUI upgradeRequirementText; // 강화에 필요한 재화를 표시할 텍스트
public SpriteRenderer groundSpriteRenderer; // 지면의 SpriteRenderer 컴포넌트
public Sprite[] treeImages; // 나무 이미지를 담을 배열
// 초기 설정
void Start()
{
UpdateUI(); // 초기 UI 업데이트
}
// 물의 양을 증가시키는 메서드
public void IncreaseWater(int amount)
{
waterAmount += amount;
UpdateUI();
}
// 물의 양을 감소시키는 메서드
public void DecreaseWater(int amount)
{
waterAmount -= amount;
UpdateUI();
}
// 레벨을 업그레이드하는 메서드
public void UpgradeLevel(int amount)
{
int waterNeededForUpgrade = (currentLevel + amount) * waterPerLevel;
if (waterAmount >= waterNeededForUpgrade)
{
DecreaseWater(waterNeededForUpgrade);
currentLevel += amount;
UpdateUI();
UpdateGroundSize();
}
else
{
Debug.Log("물이 부족하여 강화할 수 없습니다.");
}
}
// 지면 크기를 업데이트하는 메서드
private void UpdateGroundSize()
{
float groundScale = 8f + (currentLevel / 10f); // 레벨에 따른 지면 크기 계산
groundSpriteRenderer.transform.localScale = new Vector3(groundScale, groundScale, groundScale);
}
// UI를 업데이트하는 메서드
public void UpdateUI()
{
// 현재 레벨에 필요한 물의 양 계산
int waterNeededForCurrentLevel = (currentLevel + 1) * waterPerLevel;
waterText.text = $"Water: {waterAmount}";
levelText.text = $"Level: {currentLevel}";
levelFillImage.fillAmount = (float)waterAmount / waterNeededForCurrentLevel;
// 나무 이미지 업데이트
UpdateTreeImages();
// 업그레이드에 필요한 재화 텍스트 업데이트
upgradeRequirementText.text = $"필요한 재화: {waterNeededForCurrentLevel} 물 ";
}
// 나무 이미지를 업데이트하는 메서드
private void UpdateTreeImages()
{
int currentIndex = currentLevel / 5;
int nextIndex = (currentLevel + 1) / 5;
currentTreeImage.sprite = treeImages[currentIndex];
upgradedTreeImage.sprite = treeImages[nextIndex];
outsideTreeSpriteRenderer.sprite = treeImages[currentIndex];
}
}
리팩토링 후 코드
리팩토링 후 코드는 아래와 같이 여러 클래스로 분리되었습니다.
using UnityEngine;
public class WaterManager : MonoBehaviour
{
public int waterAmount = 0;
public int currentLevel = 1;
public int waterPerLevel = 10;
public delegate void WaterChanged(int newAmount);
public event WaterChanged OnWaterChanged;
public void IncreaseWater(int amount)
{
waterAmount += amount;
OnWaterChanged?.Invoke(waterAmount);
}
public void DecreaseWater(int amount)
{
waterAmount -= amount;
OnWaterChanged?.Invoke(waterAmount);
}
public bool HasSufficientWater(int requiredAmount)
{
return waterAmount >= requiredAmount;
}
public int CalculateWaterNeededForUpgrade(int amount)
{
return (currentLevel + amount) * waterPerLevel;
}
}
using UnityEngine;
public class EnergyManager : MonoBehaviour
{
public int energyAmount = 0;
public int maxEnergy = 200;
public float energyRechargeRate = 5f;
private float energyRechargeTimer = 0f;
public delegate void EnergyChanged(int newAmount);
public event EnergyChanged OnEnergyChanged;
private void Update()
{
RechargeEnergyOverTime();
}
private void RechargeEnergyOverTime()
{
energyRechargeTimer += Time.deltaTime;
if (energyRechargeTimer >= 1f)
{
energyRechargeTimer = 0f;
IncreaseEnergy((int)energyRechargeRate);
}
}
public void IncreaseEnergy(int amount)
{
energyAmount = Mathf.Min(energyAmount + amount, maxEnergy);
OnEnergyChanged?.Invoke(energyAmount);
}
public void DecreaseEnergy(int amount)
{
energyAmount = Mathf.Max(energyAmount - amount, 0);
OnEnergyChanged?.Invoke(energyAmount);
}
}
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
public TextMeshProUGUI waterText;
public TextMeshProUGUI levelText;
public TextMeshProUGUI touchLevelText;
public TextMeshProUGUI waterIncreaseLevelText;
public TextMeshProUGUI waterIncreaseUpgradeCostText;
public TextMeshProUGUI upgradeWaterCostText;
public TextMeshProUGUI energyText;
public Image levelFillImage;
public Image currentTreeImage;
public Image upgradedTreeImage;
public SpriteRenderer outsideTreeSpriteRenderer;
public TextMeshProUGUI upgradeRequirementText;
public SpriteRenderer groundSpriteRenderer;
public Sprite[] treeImages;
public void UpdateWaterUI(int waterAmount, int waterNeededForCurrentLevel)
{
waterText.text = $"물 : {waterAmount}";
levelFillImage.fillAmount = (float)waterAmount / waterNeededForCurrentLevel;
}
public void UpdateEnergyUI(int energyAmount, int maxEnergy)
{
energyText.text = $"에너지 : {energyAmount}/{maxEnergy}";
}
public void UpdateLevelUI(int currentLevel)
{
levelText.text = $"Level: {currentLevel}";
}
public void UpdateUpgradeRequirementUI(int waterNeededForCurrentLevel)
{
upgradeRequirementText.text = $"필요한 재화: {waterNeededForCurrentLevel} 물";
}
public void UpdateTreeImages(int currentLevel, Sprite[] treeImages)
{
int currentIndex = currentLevel / 5;
int nextIndex = (currentLevel + 1) / 5;
currentTreeImage.sprite = treeImages[currentIndex];
upgradedTreeImage.sprite = treeImages[nextIndex];
outsideTreeSpriteRenderer.sprite = treeImages[currentIndex];
}
}
using UnityEngine;
public class ResourceManager : MonoBehaviour
{
public WaterManager waterManager;
public EnergyManager energyManager;
public UIManager uiManager;
private void Start()
{
waterManager.OnWaterChanged += UpdateWaterUI;
energyManager.OnEnergyChanged += UpdateEnergyUI;
UpdateUI();
}
public void UpgradeLevel(int amount)
{
int waterNeededForUpgrade = waterManager.CalculateWaterNeededForUpgrade(amount);
if (waterManager.HasSufficientWater(waterNeededForUpgrade))
{
waterManager.DecreaseWater(waterNeededForUpgrade);
waterManager.currentLevel += amount;
UpdateUI();
UpdateGroundSize();
}
else
{
Debug.Log("물이 부족하여 강화할 수 없습니다.");
}
}
private void UpdateGroundSize()
{
float groundScale = 8f + (waterManager.currentLevel / 10f);
uiManager.groundSpriteRenderer.transform.localScale = new Vector3(groundScale, groundScale, groundScale);
}
private void UpdateUI()
{
int waterNeededForCurrentLevel = waterManager.CalculateWaterNeededForUpgrade(1);
uiManager.UpdateWaterUI(waterManager.waterAmount, waterNeededForCurrentLevel);
uiManager.UpdateLevelUI(waterManager.currentLevel);
uiManager.UpdateUpgradeRequirementUI(waterNeededForCurrentLevel);
uiManager.UpdateTreeImages(waterManager.currentLevel, uiManager.treeImages);
}
private void UpdateWaterUI(int newWaterAmount)
{
int waterNeededForCurrentLevel = waterManager.CalculateWaterNeededForUpgrade(1);
uiManager.UpdateWaterUI(newWaterAmount, waterNeededForCurrentLevel);
}
private void UpdateEnergyUI(int newEnergyAmount)
{
uiManager.UpdateEnergyUI(newEnergyAmount, energyManager.maxEnergy);
}
}
적용된 디자인 패턴
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
각 클래스는 하나의 책임만 가지도록 분리했습니다. WaterManager는 물 자원을, EnergyManager는 에너지 자원을, UIManager는 UI 업데이트를, ResourceManager는 매니저 간의 조율을 담당합니다.
WaterManager와 EnergyManager는 OnWaterChanged와 OnEnergyChanged 이벤트를 통해 상태 변화를 통지합니다. ResourceManager는 이러한 이벤트를 구독하여 UI를 업데이트합니다.리팩토링 결과
가독성 향상: 각 클래스가 명확한 역할을 가지게 되어 코드의 가독성이 크게 향상되었습니다.
유지보수 용이: 클래스 간의 결합도가 낮아져 유지보수와 확장이 용이해졌습니다.
테스트 용이: 각 클래스가 독립적으로 동작하므로 단위 테스트가 쉬워졌습니다.
이번 리팩토링을 통해 객체지향 설계 원칙과 디자인 패턴의 중요성을 다시 한번 깨달았습니다. 앞으로도 코드의 품질을 높이기 위해 이러한 원칙과 패턴을 적극적으로 적용해야겠습니다.