🔧 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는 무분별하게 사용하지 않고, “전역이어야만 하는 이유”가 있을 때만 도입