Unity에서 실용적인 리팩토링 팁 & 예시

🔧 Unity에서 실용적인 리팩토링 팁 & 예시

리팩토링은 단순히 코드를 고치는 작업이 아닙니다.
기능은 그대로 유지하면서, 구조를 더 이해하기 쉽고 확장 가능하게 만드는 과정이죠.

Unity에서는 다음과 같은 맥락에서 자주 리팩토링을 하게 됩니다:

  • MonoBehaviour가 너무 많은 역할을 하고 있다면?
  • 반복되는 코드가 여러 오브젝트에 퍼져 있다면?
  • 씬에서 관리가 어려운 설정이나 참조가 많아진다면?

💡 팁 1: MonoBehaviour는 가볍게 유지하자 (단일 책임 원칙)

나쁜 예

public class Player : MonoBehaviour
{
    public float moveSpeed;
    public int health;
    public AudioClip hitSound;

    void Update()
    {
        Move();
        if (Input.GetKeyDown(KeyCode.Space))
            Shoot();
    }

    void Move() { /* 이동 코드 */ }
    void Shoot() { /* 공격 코드 */ }
    public void TakeDamage(int amount) { /* 데미지 처리 + 사운드 */ }
}

👎 문제점:

  • 이동, 공격, 데미지, 사운드 처리 등 여러 역할이 한 클래스에 몰려 있음

리팩토링 후 – 역할 분리

public class PlayerController : MonoBehaviour
{
    private Movement movement;
    private Shooter shooter;

    void Awake() {
        movement = GetComponent<Movement>();
        shooter = GetComponent<Shooter>();
    }

    void Update() {
        movement.HandleMovement();
        shooter.HandleShooting();
    }
}
public class Movement : MonoBehaviour
{
    public float speed = 5f;
    public void HandleMovement() {
        // 이동 처리
    }
}
public class Shooter : MonoBehaviour
{
    public void HandleShooting() {
        // 공격 처리
    }
}

👍 장점:

  • 각 컴포넌트는 하나의 책임만 담당
  • 테스트 및 재사용 쉬움

💡 팁 2: 하드코딩 값 제거 → ScriptableObject로 데이터 분리

기존 코드 (하드코딩)

public class Enemy : MonoBehaviour
{
    public int health = 100;
    public float speed = 2f;
}

리팩토링 – ScriptableObject로 설정 분리

[CreateAssetMenu(menuName = "Enemy/Stats")]
public class EnemyStats : ScriptableObject
{
    public int maxHealth;
    public float moveSpeed;
}
public class Enemy : MonoBehaviour
{
    public EnemyStats stats;

    void Start() {
        Debug.Log($"HP: {stats.maxHealth}");
    }
}

👍 장점:

  • 디자이너와 협업 가능
  • 프리팹에 따라 다양한 설정 구성 가능
  • 씬 파일 크기 감소

💡 팁 3: 복잡한 조건문은 의미 있는 함수로 분리

기존 코드

if (isDead == false && time > 0 && score >= targetScore) {
    // 게임 성공 처리
}

리팩토링

bool CanWinGame() {
    return !isDead && time > 0 && score >= targetScore;
}

if (CanWinGame()) {
    // 게임 성공 처리
}

👍 장점:

  • 조건의 의미가 함수명으로 드러남
  • 테스트 및 유지보수 쉬움

💡 팁 4: 오브젝트 간 직접 참조 → 이벤트 기반 구조로 변경

기존 코드 – 직접 참조

public class Enemy : MonoBehaviour {
    public GameManager gameManager;

    void OnDeath() {
        gameManager.AddScore(10);
    }
}

리팩토링 – 이벤트 시스템 사용

public class Enemy : MonoBehaviour {
    public UnityEvent onDeath;

    void Die() {
        onDeath.Invoke();
    }
}

GameManager는 Inspector에서 onDeath 이벤트에 연결해서 점수를 증가시킴

👍 장점:

  • 의존성 제거 (Enemy는 GameManager를 몰라도 됨)
  • 컴포넌트 재사용성 증가

💡 팁 5: 반복되는 구조 → 공통 부모 클래스 또는 인터페이스 사용

public interface IDamageable {
    void TakeDamage(int amount);
}
public class Player : MonoBehaviour, IDamageable {
    public void TakeDamage(int amount) { /* 처리 */ }
}

public class Enemy : MonoBehaviour, IDamageable {
    public void TakeDamage(int amount) { /* 처리 */ }
}

→ 공격 시스템에서 타입을 IDamageable로만 처리하면 범용화 가능

void Attack(IDamageable target) {
    target.TakeDamage(10);
}

🧼 리팩토링 핵심 정리

리팩토링 대상 추천 방식

너무 많은 기능 가진 MonoBehaviour 기능별로 Script 분리
설정값이 코드에 하드코딩됨 ScriptableObject로 분리
조건문이 복잡함 함수로 분리해 의미 드러내기
오브젝트 간 직접 참조 UnityEvent, Delegate로 느슨한 결합
같은 기능 반복 인터페이스, 공통 베이스 클래스

한 줄 요약

리팩토링은 "더 좋은 미래를 위한 구조화"다.
지금은 똑같이 작동해도, 시간이 지나면 유지보수의 차이가 난다.

 

코드를 작성할 때, 싱글턴 패턴을 사용해서 참조하면 비효율적인가?


Unity에서 instance 패턴, 즉 싱글턴(Singleton) 형태로 오브젝트를 여기저기서 참조하는 방식은 매우 자주 사용되지만, 장단점이 명확합니다. 결론부터 말하자면:

단순하거나 소규모 프로젝트에선 편리하지만, 지나치게 의존하면 유지보수와 확장성이 급격히 떨어질 수 있습니다.


✅ Singleton 패턴 (Instance 사용)의 장점

1. 언제 어디서든 접근 가능

GameManager.Instance.DoSomething();
  • 매 프레임마다 FindObjectOfType<GameManager>()처럼 성능을 잡아먹는 탐색 없이도 바로 접근 가능

2. 간편한 데이터 공유

  • 씬 전체에서 점수, 설정값, 전역 상태 등을 공유할 수 있어 구현이 쉬움

3. 에디터/런타임 혼용에 유리

  • 설정이 ScriptableObject보다 즉각적이고 코드 중심적

❌ 하지만 다음과 같은 문제도 발생합니다

1. 전역 상태에 대한 강한 의존성

  • 모든 클래스가 GameManager.Instance에 의존하게 되면 테스트가 어려워지고, 서로 강하게 연결된 "스파게티 구조"가 됩니다.

2. 테스트/모킹 불가

  • Unity 테스트 환경에서 Instance를 대체하기 어려움 → 단위 테스트가 어렵거나 불가능

3. 의존성 명시가 안 됨

  • 코드만 봐서는 어떤 컴포넌트를 사용하는지 명확하지 않음 → 디버깅이 어려워짐

4. 초기화 순서 문제

  • 씬 로딩 중 Instance가 아직 생성되지 않았는데 다른 오브젝트가 접근할 경우 null reference 발생 가능

🔁 대안: 의존성 주입 (Dependency Injection)

싱글턴 대신 RequireComponent나 public 필드로 외부에서 명시적으로 주입하면 훨씬 깔끔해집니다.

public class Enemy : MonoBehaviour {
    public GameManager gameManager;

    void Start() {
        gameManager.AddScore(10);
    }
}

혹은 Awake()에서 FindObjectOfType<GameManager>()로 가져오되, 로컬 변수에만 저장하고 쓰면 나쁠 건 없습니다.


💡 절충안: "컨트롤러 클래스만 Singleton"

GameManager, AudioManager, SceneLoader처럼 게임을 전역적으로 제어하는 1개 클래스 정도만 싱글턴으로 두고, 나머지는 주입하거나 참조 구조를 명시적으로 설계하는 것이 이상적입니다.

예:

public class GameManager : MonoBehaviour {
    public static GameManager Instance { get; private set; }

    void Awake() {
        if (Instance != null && Instance != this) {
            Destroy(gameObject);
        } else {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }
}

✍️ 요약: Instance 패턴은 효율적인가?

상황 사용 권장 여부 이유

작은 게임/프로토타입 ✅ 빠르게 만들 때 편리  
전역 컨트롤 클래스 ✅ GameManager, AudioManager 등에 적합  
복잡한 로직, 협업, 테스트 ❌ 유지보수 어려움, 테스트 불가능  
의존성 명확하게 설계 필요 ❌ 결합도 높아지고 확장 어려움  

추천 습관

  • 프로젝트 초반엔 간단한 싱글턴으로 시작해도 OK
  • 복잡해질수록 참조는 외부에서 주입하거나, ScriptableObject 기반 구조로 확장 고려
  • Instance는 무분별하게 사용하지 않고, “전역이어야만 하는 이유”가 있을 때만 도입