2025. 2. 15. 17:53ㆍ유니티 unity/디자인패턴
소프트웨어 설계에서 SOLID 원칙은 유지보수성과 확장성이 뛰어난 코드를 작성하기 위한 기본 철학입니다. SOLID는 다섯 가지 원칙(단일 책임, 개방-폐쇄, 리스코프 치환, 인터페이스 분리, 의존성 역전)을 의미하며, 각각의 원칙은 객체지향 프로그래밍에서 발생할 수 있는 문제들을 해결하는 데 도움을 줍니다.
1. 단일 책임 원칙 (Single Responsibility Principle, SRP)
설명:
단일 책임 원칙은 클래스나 모듈이 하나의 책임, 즉 하나의 기능 또는 역할만 수행해야 한다는 원칙입니다. 한 클래스가 여러 기능을 담당하게 되면 한 기능의 변경이 다른 기능에도 영향을 미치게 되어 유지보수가 어려워집니다. 또한, 각 클래스가 독립적인 역할을 수행하면 테스트 작성 및 디버깅이 용이해집니다.
예시:
예를 들어, 플레이어 캐릭터가 이동, 애니메이션, 사운드 재생 등의 기능을 하나의 클래스에서 모두 처리하는 경우를 생각해볼 수 있습니다. 이러한 구조는 기능이 추가되거나 수정될 때 예상치 못한 부작용을 일으킬 가능성이 높습니다. 따라서 각 기능을 담당하는 클래스를 분리하여 책임을 명확하게 나누는 것이 바람직합니다.
// 플레이어의 이동을 담당하는 클래스
public class PlayerMovement : MonoBehaviour
{
public float speed = 5f;
void Update()
{
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(moveHorizontal, 0, moveVertical);
transform.Translate(movement * speed * Time.deltaTime);
}
}
// 플레이어의 애니메이션을 담당하는 클래스
public class PlayerAnimation : MonoBehaviour
{
private Animator animator;
void Awake()
{
animator = GetComponent<Animator>();
}
public void PlayRunAnimation(bool isRunning)
{
animator.SetBool("isRunning", isRunning);
}
}
// 플레이어의 사운드를 담당하는 클래스
public class PlayerSound : MonoBehaviour
{
public AudioSource audioSource;
public AudioClip runClip;
public void PlayRunSound()
{
if (!audioSource.isPlaying)
{
audioSource.clip = runClip;
audioSource.Play();
}
}
}
이처럼 각 클래스가 한 가지 역할에 집중하도록 설계하면, 이후 기능 변경이나 버그 수정 시 관련된 클래스만 수정하면 되어 코드의 안정성을 높일 수 있습니다.
2. 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
설명:
개방-폐쇄 원칙은 소프트웨어 구성 요소(클래스, 모듈 등)가 확장에는 열려 있으면서, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 원칙입니다.
예시:
게임에서 적 캐릭터가 다양한 공격 패턴을 가지는 경우, 기존 코드를 수정하지 않고 새로운 공격 방식을 추가할 수 있도록 설계해야 합니다. 이를 위해 공격 패턴을 추상화한 인터페이스를 사용하여 새로운 공격 방식 클래스를 추가할 수 있습니다.
.
public interface IAttackPattern
{
void ExecuteAttack();
}
using UnityEngine;
public class MeleeAttack : IAttackPattern
{
public void ExecuteAttack()
{
// 근접 공격 로직 (예: 플레이어와의 거리를 계산하여 데미지 적용)
Debug.Log("근접 공격 실행: 적이 플레이어에게 가까이 접근하여 공격합니다.");
}
}
using UnityEngine;
public class RangedAttack : IAttackPattern
{
public void ExecuteAttack()
{
// 원거리 공격 로직 (예: 투사체 생성, 플레이어를 향해 발사)
Debug.Log("원거리 공격 실행: 적이 투사체를 발사합니다.");
}
}
using UnityEngine;
public class SpecialAttack : IAttackPattern
{
public void ExecuteAttack()
{
// 특수 공격 로직 (예: 일정 시간 동안 범위 내 모든 대상에 피해)
Debug.Log("특수 공격 실행: 적이 범위 공격을 시전합니다.");
}
}
using UnityEngine;
public class Enemy : MonoBehaviour
{
private IAttackPattern attackPattern;
// 외부에서 공격 패턴을 주입받음으로써 변경에 유연하게 대처할 수 있음
public void SetAttackPattern(IAttackPattern pattern)
{
attackPattern = pattern;
}
void Update()
{
// 예시: 공격 명령을 특정 키 입력으로 대체하거나 AI 로직에 따라 호출
if (Input.GetKeyDown(KeyCode.Space))
{
attackPattern?.ExecuteAttack();
}
}
}
이와 같이 인터페이스를 사용하면, 새로운 공격 방식을 추가할 때 기존의 Enemy 클래스나 다른 코드에 영향을 주지 않고 별도의 클래스를 추가함으로써 확장이 용이해집니다.
3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
설명:
리스코프 치환 원칙은 상위 타입(부모 클래스나 인터페이스)으로 정의된 객체를 하위 타입(자식 클래스나 구현체)으로 대체해도 프로그램이 정상적으로 작동해야 한다는 원칙입니다. 즉, 하위 클래스는 부모 클래스의 기능과 계약을 위반하지 않아야 합니다.
예시:
앞서 작성한 IAttackPattern 인터페이스를 사용하는 예시에서, 모든 공격 패턴 클래스는 동일한 ExecuteAttack() 메서드를 호출할 때 일관된 결과를 제공해야 합니다. 만약 한 공격 패턴 클래스가 예상과 다르게 동작하거나, 부모 인터페이스가 보장하는 전제 조건을 위반한다면, 해당 클래스는 LSP를 위반하게 됩니다.
예시 설명
예를 들어, RangedAttack 클래스가 실제로는 근접 공격 효과를 내거나, 투사체 생성 시 예외 상황을 제대로 처리하지 못한다면, 이를 사용하는 적 캐릭터 클래스에서는 예측하지 못한 동작이 발생할 수 있습니다. 따라서 모든 구현체는 부모 인터페이스의 계약을 철저하게 준수해야 합니다.
using UnityEngine;
public class ConsistentRangedAttack : IAttackPattern
{
public void ExecuteAttack()
{
// 정상적인 원거리 공격 로직 구현
Debug.Log("일관된 원거리 공격 실행: 투사체를 정상적으로 발사합니다.");
}
}
위와 같이 LSP를 준수하는 구현체를 사용하면, 적 캐릭터 클래스는 항상 IAttackPattern 인터페이스를 통해 일관된 공격 동작을 보장받을 수 있습니다.
4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
설명:
인터페이스 분리 원칙은 클라이언트가 사용하지 않는 메서드에 의존하지 않도록, 하나의 거대한 인터페이스보다 구체적인 여러 개의 인터페이스로 나누어야 한다는 원칙입니다. 이는 불필요한 구현 부담을 줄이고, 클래스 간의 의존성을 최소화하는 데 도움이 됩니다.
예시:
게임 오브젝트가 다양한 행동(예: 이동, 공격, 상호작용, UI 표시 등)을 수행해야 할 때, 모든 기능을 하나의 인터페이스에 담는 대신 각 역할별로 인터페이스를 분리하여 설계합니다
// 이동 기능에 관한 인터페이스
public interface IMovable
{
void Move();
}
// 공격 기능에 관한 인터페이스
public interface IAttackable
{
void Attack();
}
// 상호작용 기능에 관한 인터페이스
public interface IInteractable
{
void Interact();
}
public interface IUIUpdatable
{
void UpdateUI();
}
using UnityEngine;
public class Player : MonoBehaviour, IMovable, IAttackable, IInteractable, IUIUpdatable
{
public void Move()
{
// 이동 로직
Debug.Log("플레이어가 이동합니다.");
}
public void Attack()
{
// 공격 로직
Debug.Log("플레이어가 공격합니다.");
}
public void Interact()
{
// 상호작용 로직
Debug.Log("플레이어가 상호작용합니다.");
}
public void UpdateUI()
{
// UI 업데이트 로직 (예: 체력바, 점수 등)
Debug.Log("플레이어 UI가 업데이트됩니다.");
}
}
이와 같이 각 인터페이스를 역할별로 분리하면, 예를 들어 UI 업데이트가 필요 없는 다른 게임 오브젝트는 IUIUpdatable을 구현할 필요 없이 자신이 필요한 기능만 구현하면 됩니다.
5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
설명:
의존 역전 원칙은 고수준 모듈(비즈니스 로직)이 저수준 모듈(구체적 구현)에 의존하지 않고, 추상화(인터페이스, 추상 클래스 등)에 의존하도록 설계해야 한다는 원칙입니다. 이를 통해 모듈 간 결합도를 낮추고, 특정 구현체의 변경이 고수준 모듈에 영향을 미치지 않도록 할 수 있습니다.
예시:
예를 들어, 게임에서 데이터를 저장 및 불러오는 기능을 구현할 때, 데이터 저장 방식을 파일 시스템, 클라우드, 또는 데이터베이스 등으로 쉽게 변경할 수 있어야 합니다. 이를 위해 데이터 서비스를 추상화한 인터페이스를 정의하고, 각각의 저장 방식에 따른 구현 클래스를 따로 작성합니다.
public interface IDataService
{
void SaveData(string data);
string LoadData();
}
using UnityEngine;
public class FileDataService : IDataService
{
public void SaveData(string data)
{
// 파일 입출력 로직 (예: System.IO를 사용하여 파일 저장)
Debug.Log("파일에 데이터를 저장합니다: " + data);
}
public string LoadData()
{
// 파일 입출력 로직 (예: 파일에서 데이터를 읽어옴)
Debug.Log("파일에서 데이터를 불러옵니다.");
return "파일 데이터 예시";
}
}
using UnityEngine;
public class CloudDataService : IDataService
{
public void SaveData(string data)
{
// 클라우드 API를 이용한 데이터 저장 로직
Debug.Log("클라우드에 데이터를 저장합니다: " + data);
}
public string LoadData()
{
// 클라우드 API를 이용한 데이터 불러오기 로직
Debug.Log("클라우드에서 데이터를 불러옵니다.");
return "클라우드 데이터 예시";
}
}
using UnityEngine;
public class GameManager : MonoBehaviour
{
private IDataService dataService;
// 의존성 주입(DI)을 통해 외부에서 IDataService 구현체를 할당받음
public void Initialize(IDataService service)
{
dataService = service;
}
void Start()
{
if(dataService != null)
{
dataService.SaveData("현재 게임 상태");
string data = dataService.LoadData();
Debug.Log("불러온 데이터: " + data);
}
else
{
Debug.LogWarning("데이터 서비스가 초기화되지 않았습니다.");
}
}
}
이 예시에서는 GameManager가 구체적인 파일 저장이나 클라우드 저장 방식에 의존하지 않고, IDataService라는 추상화에 의존합니다. 따라서 나중에 저장 방식이 변경되더라도 GameManager 코드를 수정할 필요 없이, 다른 구현체를 주입하면 됩니다.
결론
SOLID 원칙은 복잡한 유니티 프로젝트에서도 코드의 가독성, 유지보수성, 확장성을 크게 향상시키는 데 도움을 줍니다.
- 단일 책임 원칙을 통해 각 클래스는 한 가지 기능에 집중하여, 기능 수정 시 서로 간섭을 최소화합니다.
- 개방-폐쇄 원칙을 적용하면, 새로운 기능 추가 시 기존 코드를 건드리지 않고 확장이 가능하여 안정적인 시스템 구성이 가능합니다.
- 리스코프 치환 원칙을 보장함으로써, 상속이나 인터페이스 구현 시 일관된 동작을 보장받아 예측 가능한 결과를 얻을 수 있습니다.
- 인터페이스 분리 원칙을 통해 클래스들이 자신이 실제 사용하는 기능만 구현하게 하여 불필요한 의존성을 줄입니다.
- 의존 역전 원칙을 활용하면, 모듈 간 결합도를 낮추고, 특정 구현체 변경 시 전체 시스템에 미치는 영향을 최소화할 수 있습니다.
이와 같이 풍부한 예시와 구체적인 코드 구현을 통해 SOLID 원칙을 유니티 프로젝트에 적용하면, 팀 단위 협업이나 확장 가능한 시스템 구축에 큰 도움이 될 것입니다.
'유니티 unity > 디자인패턴' 카테고리의 다른 글
Unity에서 KISS 원칙(Keep It Simple, Stupid) 적용하기 (0) | 2025.02.16 |
---|---|
Unity에서 전략 패턴(Strategy Pattern) 활용하기 (3) | 2025.02.15 |
Unity에서 데코레이터 패턴 을 활용한 버프 시스템 구현 (2) (0) | 2025.02.09 |
Unity에서 데코레이터 패턴 (Decorator Pattern) 활용하기 (1) (1) | 2025.02.09 |
유니티에서 커맨드 패턴, 언제 그리고 왜 사용해야 할까? (0) | 2025.02.07 |