안녕하세요 주인장입니다.
오늘은 SOLID 원칙에 대해 알아보겠습니다.
개발을 하다 보면 기능이 조금만 복잡해져도 코드가 엉키고, 수정할 때마다 새로운 버그가 생기곤 합니다.
특히 Unity처럼 컴포넌트 기반 구조에서는 스크립트 사이의 의존성이 커지기 쉬워 유지보수가 어려워집니다.
이 문제를 해결해 주는 개념이 바로 SOLID 원칙입니다.
SOLID는 객체지향 설계의 다섯 가지 핵심 원칙의 앞 글자를 따온 말로, 더 안정적이고 확장 가능한 코드를 만들도록 도와줍니다.
SOLID란 무엇인가
SOLID는 다음 다섯 가지 원칙으로 이루어져 있습니다.
S - Single Responsibility Principle, 단일 책임 원칙
O - Open/Closed Principle, 개방-폐쇄 원칙
L - Liskov Substitution Principle, 리스코프 치환 원칙
I - Interface Segregation Principle, 인터페이스 분리 원칙
D - Dependency Inversion Principle, 의존성 역전 원칙
SRP(단일 책임 원칙) - 클래스는 단 하나의 책임만 가져야 한다
단일 책임 원칙은 "한 클래스는 반드시 하나의 일(책임)만 가져야 한다."라는 매우 간단하지만 강력한 원칙입니다.
하지만 실제 개발에서는
"어디까지를 하나의 책임으로 봐야 하는가?"
"나누면 너무 잘게 쪼개지는 거 아닌가?"
같은 고민 때문에 지키기 어려운 원칙이기도 합니다.
여기서 말하는 '책임'은 역할 이라고 이해하면 더 명확해집니다.
예를 들어:
- 플레이어의 체력이 바뀌는 역할
- 플레이어의 이동 방식이 바뀌는 역할
- 플레이어의 애니메이션 연출 방식이 바뀌는 역할
따라서 이들을 하나의 클래스에 넣으면 각 기능의 서로 영향을 줄 가능성이 큽니다.
SRP를 지키지 않으면 생기는 문제들
1. 수정할 때 다른 기능이 망가지는 Side Effect 발생
한 클래스가 여러 일을 하면, 그 안의 한 기능만 수정해도 다른 기능이 영향을 받을 가능성이 있습니다.
예를 들어 Player 클래스에 Attack(), TakeDamage(), ResetInvincible(), Move()가 모두 섞여있을 때
ex1) Attack() 호출 시 공격 애니메이션 동안 무적상태라고 가정했을 때,
애니메이션 길이를 수정한 후 ResetInvincible()을 같이 수정해주지 않으면 무적 상태임에도
TakeDamage()가 호출되는 버그가 발생
ex2) TakeDamage()를 호출할 때 피격받은 반대 방향으로 일정한 길이를 Move()로 밀려난다고 가정했을 때,
Move()에서 이동속도를 올리면 TakeDamage()를 호출할 때 캐릭터가 뒤로 밀리는 거리도 같이 길어짐
등의 문제가 발생할 수 있습니다.
2. 확장할수록 클래스가 거대해짐
특히 Unity에서의 PlayerController 같은 클래스가 아래처럼 되기 쉽습니다.
- Input 처리
- 이동
- 점프
- 공격
- 카메라 흔들기
- 변화에 따른 UI 업데이트
- 이펙트 재생
이 중 하나만 수정해도 다른 모든 클래스와 함수에 이상 없음을 확인하고 검증해야 하기 때문에 유지보수 하기가 까다롭습니다.
3. 개별 테스트가 불가능해짐
하나의 클래스가 너무 많은 일을 하면 각 기능만 따로 테스트할 수 없고,
항상 전체 로직이 도는 상황이 발생합니다.
각각의 기능을 안전하고 정확하게 테스트할 수 있는 개발환경을 구축하는 것은 매우 매우 중요합니다.
Unity에서 SRP를 적용한 간단 예시
SRP 위반 예시 - Player가 모든 일을 하는 경우
public class Player : MonoBehaviour
{
public int hp = 100;
void Update()
{
Move();
Attack();
PlayAttackEffect();
}
void Move() { /* 이동 로직 */ }
void Attack() { /* 공격 로직 */ }
void PlayAttackEffect() { /* 이펙트 로직 */ }
public void TakeDamage(int dmg)
{
hp -= dmg;
UpdateUI();
}
void UpdateUI() { /* UI 업데이트 */ }
}
Player.cs 클래스는 다음 이유로 변경될 수 있습니다.
- 이동 방식 변경 → Move() 수정
- 공격 방식 변경 → Attack() 수정
- 이펙트 변경 → PlayAttackEffect() 수정
- UI 변경 → UpdateUI() 수정
- 체력 계산 변경 → TakeDamage() 수정
이 클래스는 SRP 위반의 사례로 하나의 스크립트에 5개의 역할이 부여되어 있습니다.
따라서 변경될 이유도 다섯 가지 존재하며, 기능 간 결합도가 높을수록 실제 변경 이유는 더 늘어날 수 있습니다.
SRP 준수 예시 - 책임별로 분리
PlayerHealth.cs
public class PlayerHealth : MonoBehaviour
{
public int hp = 100;
public UnityEvent<int> onHealthChanged;
public void TakeDamage(int dmg)
{
hp -= dmg;
onHealthChanged?.Invoke(hp);
}
}
PlayerMovement.cs
public class PlayerMovement : MonoBehaviour
{
public void Move(Vector3 input)
{
// 이동 처리
}
}
PlayerAttack.cs
public class PlayerAttack : MonoBehaviour
{
public void Attack()
{
// 공격 처리
}
}
PlayerUI.cs
public class PlayerUI : MonoBehaviour
{
public void UpdateHealth(int hp)
{
// UI 변경
}
}
상위 스크립트 Player.cs
public class Player : MonoBehaviour
{
public PlayerHealth health;
public PlayerMovement movement;
public PlayerAttack attack;
public PlayerUI ui;
void Start()
{
health.onHealthChanged.AddListener(ui.UpdateHealth);
}
}
기존 Player.cs 에 다섯 가지 역할을 각각 독립된 클래스로 분리했습니다.
이제 각 클래스는 하나의 역할만 수행하므로, 변경이 한 클래스에만 국한됩니다.
그 결과 유지보수성, 확장성, 독립성이 모두 향상됩니다.
단일 책임 원칙 체크리스트
이 클래스는 2가지 이상의 이유로 수정될 수 있는가?
→ 변경 이유가 여러 개면 분리 필요
이 클래스가 지나치게 길거나, 기능이 많아 보이는가?
→ 단일 책임을 넘어섬, 분리 필요
이 클래스를 설명하려면 "그리고(and)"가 많이 붙는가?
→ "이 클래스는 이동도 하고, 공격도 하고, 체력도 관리하고..." , 분리 필요
하나의 기능을 테스트하기 위해 다른 기능도 반드시 실행되는가?
→ 서로 간의 결합도가 높음, 분리 필요
마치며
개발을 하다 보면 가끔 "SRP를 완벽히 지키지 않아도 지금은 잘 돌아가는데 뭐가 문제인가?"라는 생각이 들 수도 있지만 저는 유지보수와 업데이트는 개발자의 숙명이라고 생각합니다. 지금 당장 돌아간다고 해서 미래까지 안전한 건 아니기 때문에 장기적인 관점에서 봤을 때, 그리고 나중에 프로젝트를 유지 보수 할 다른 팀원을 위해서라도 SRP를 지키는 것이 훨씬 현명한 선택입니다.
오늘의 기록은 여기까지, 주인장은 이만 로그아웃 합니다. 모두 평안한 밤 보내세요!
'개발 > Unity' 카테고리의 다른 글
| [Unity] SOLID 원칙 (3) LSP : 리스코프 치환 원칙 (0) | 2025.11.26 |
|---|---|
| [Unity] SOLID 원칙 (2) OCP : 개방 폐쇄 원칙 (0) | 2025.11.25 |
| [Unity] Camera Stacking 사용법과 간단한 응용 (0) | 2025.11.12 |
| [에셋/툴] Build Report Tool (0) | 2025.11.06 |
| [Unity] 클라이언트에서 사용할 수 있는 안티치트 기법 (1) (0) | 2025.11.03 |