1. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 단일 책임 원칙

2024. 10. 28. 14:07·디자인 패턴

디자인 패턴에 대해 이야기하기 전에, 우선 패턴이 작동하는 방식에 영향을 주는 객체 지향 설계 원칙에 대해 알아보겠습니다. 그 중, SOLID 원칙은 소프트웨어 디자인의 다섯 가지 핵심 원칙을 머리글자어로 만든 용어입니다.

 

이번 챕터에서는 단일 책임 원칙에 대해 알아보겠습니다.

 

1. 단일 책임 (Single Responsibility)

2. 개방 : 폐쇄 (Open-Closed)

3. 리스코프 치환 (Liskov Substitution)

4. 인터페이스 분리 (Interface Segregation)

5. 종속성 역전 (Dependency Inversion)


#1. 단일 책임 원칙

"Class를 변경해야 한다면 그 이유는 오직 단일 책임이어야 한다."

 

SOLID 원칙에서 가장 중요한 첫 번째 원칙은 SRP(단일 책임 원칙)로, 모듈, Class 또는 함수(Function & Method)가 오직 한 가지만 책임지며 로직의 특정 부분만 캡슐화할 것을 명시합니다. 단일 구조의 Class를 만들기보다는 여러 개의 작은 클래스로 프로젝트를 조합하는 방식을 지향합시다. 그 이유는 Class와 Method는 짧을수록 설명과 이해, 구현이 쉽기 때문입니다. Unity의 Component를 어느 정도 사용해 봤다면 이미 익숙한 개념일 것이라고 생각합니다. 게임 오브젝트를 만들면 더 작고 다양한 Component가 오브젝트 내부에 포함됩니다. 포함될 수 있는 컴포넌트는 아래와 같습니다.

  • 3D 모델에 대한 참조가 있는 "MeshFilter" Component
  • 모델 표현이 화면에 표시되는 방식을 제어하는 "Renderer" Component
  • Scale, Rotation, Position 정보가 있는 "Transform" Component
  • 물리 시뮬레이션과 상호 작용하는 데 필요한 "Rigidbody" Component

 

위의 각 Component들의 기능들을 살펴보면, 한 가지 작업을 올바르게 수행한다는 것을 알 수 있습니다. 이러한 각 Component들을 게임 오브젝트에 포함시켜 전체 씬을 만들고, 컴포넌트 간의 상호 작용으로 게임이 구동되도록 하는 것이죠. 스크립팅된 Component도 같은 방식으로 구성됩니다. 최대한 각 컴포넌트를 명확하게 이해할 수 있도록 디자인하고, 그런 다음 여러 Component가 함께 작동시켜, 복잡한 동작들을 최대한 이해하기 쉽게 설계한다면 유지 보수 측면에서도 상당한 이점을 가질 수 있을 겁니다. (처음에는 이해가 안되실 수 있겠지만, 많은 프로젝트들을 제작해보고 관리해보면서 필요성을 점차 가지게 될 것이라고 생각됩니다. 코드를 많이 짜보세요!)

단일 책임 원칙을 무시하면 다음과 같이 작동하는 커스텀 컴포넌트를 만들게 될 수 있습니다.


#2. 안 좋은 예시 & 코드

여러 기능을 수행하는 플레이어 스크립트

public class UnrefactoredPlayer : MonoBehaviour
 {
    [SerializeField] private string inputAxisName;
    [SerializeField] private float positionMultiplier;
    private float yPosition;
    private AudioSource bounceSfx;
    private void Start()
    {
        bounceSfx = GetComponent();
    }

    private void Update()
    {
        float delta = Input.GetAxis(inputAxisName) * Time.deltaTime;
        yPosition = Mathf.Clamp(yPosition + delta, -1, 1);
        transform.position = new Vector3(transform.position.x, 
        yPosition * positionMultiplier, transform.position.z);
    }
    private void OnTriggerEnter(Collider other)
    {
        bounceSfx.Play();
    }
}

위의 코드를 보면 UnrefactoredPlayer Class에 너무 많은 기능과 일관성 없는 작업들을 수행합니다. 플레이어가 무언가에 충돌하면 소리를 재생하고, 입력을 관리하고, 이동을 처리합니다. 지금은 비교적 짧은 Class 지만 프로젝트를 진행하다 보면 점점 유지하기 어려워질 것입니다. 특히 협업을 진행한다면 여러 작업자들이 해당 Class 의 이름만으로는 기능들을 유추할 수 없어 코드 해석에 대한 코스트가 높아질 가능성이 존재합니다.

Player Class를 더 작은 여러 Class로 분할이 필요해 보이네요. 아래는 개선된 구조 예시입니다.


#3. 개선된 구조 예시 & 코드

단일 책임 원칙을 따른 클래스로 리팩터링된 Player

 [RequireComponent(typeof(PlayerAudio), typeof(PlayerInput), typeof(PlayerMovement))]
 public class Player : MonoBehaviour
 {
    [SerializeField] private PlayerAudio playerAudio;
    [SerializeField] private PlayerInput playerInput;
    [SerializeField] private PlayerMovement playerMovement;
    private void Start()
    {
        playerAudio = GetComponent();
        playerInput = GetComponent();
        playerMovement = GetComponent();
    }
 }
 
 public class PlayerAudio : MonoBehaviour
 {
    …
 }
 
 public class PlayerInput : MonoBehaviour
 {
    …
 }
 
 public class PlayerMovement : MonoBehaviour
 {
    …
 }

Player 스크립트가 여전히 스크립팅된 다른 컴포넌트를 관리할 수 있는 구조이지만, 적어도 각 Class는 오직 한 가지 역할만 수행합니다. 즉, 특정 Class가 다른 컴포넌트들을 포함해도, 해당 Player Class에서 일관성 없는 기능(Function, Method)이 구현되어 있지는 않습니다.

 

이러한 방식으로 디자인하면 코드를 더 쉽게 수정할 수 있으며, 특히 시간 지나면서 프로젝트 요구 사항이 바뀌는 상황에서 더욱 유용합니다. 왜냐하면 각 기능별로 Class 를 설계했기 때문에 해당 Class를 삭제, 수정을 하면 해결할 수 있는 구조이기 때문입니다. (Player Class의 모든 내용들을 파악하면서 방대한 코드를 수정할 이유가 없기 때문에 효율적이죠.)

하지만, 단일 책임 원칙이라도 합당한 상식선에서 적용해야 합니다. 하나의 메서드만으로 Class를 만드는 극단적인 간소화는 피하는 것이 좋습니다. (아무리 좋아도 Too much 한 것은... 독이 될 수 있겠죠? ㅎㅎ)

 

단일 책임 원칙을 따라 작업할 때 염두에 둘 만한 목표는 3가지로 분류할 수 있습니다.

  • 1. 가독성
    • Class가 짧으면 읽기 쉽습니다. 엄격하고 직관적인 규칙은 없지만 많은 개발자가 라인의 수를 200~300줄 정도로 제한합니다. 본인 또는 팀 차원에서 어느 정도를 짧다고 규정할지 원칙을 정하세요. 정해진 한도를 초과하면 더 작게 리팩터링할 것인지 결정하세요. (요점: Class는 최대한 짧고 간결하게, 기준은 팀원들과 원칙을 정해라!)
  • 2. 확장성
    • 작은 Class에서 상속하기가 더 쉽습니다. 의도치 않은 기능 장애를 걱정할 필요 없이 클래스를 수정하거나 대체하세요.
  • 3. 재사용성
    • 게임의 다른 부분에 재사용할 수 있도록 Class를 작은 모듈형으로 디자인하세요. (Unity에서 제공하는 Component 처럼 쉽게 추가/삭제 가능하도록 모듈식 개발하면 재활용하기 쉽지 않을까요?)

리팩터링할 때는 코드를 어떻게 재구성해야 자신과 팀원에게 도움이 될지 생각해봐야 합니다.

초반에 약간의 노력을 더 들이면 많은 문제를 미연에 방지할 수 있을테니까요.


단순성은 소프트웨어 디자인에서 자주 다루는 주제이며 신뢰성을 높이기 위한 전제 조건이기도 합니다. 단순히 코드를 생산하면서 기능만 구현되면 끝났다고 생각하기 전에 자기 스스로에게 이러한 질문을 던져보면 좋을 것 같습니다.

  • 내가 만든 코드가 소프트웨어가 제작 단계에서 변경이 이뤄져도 대응할 수 있도록 디자인 되었을까?
  • 내가 만든 코드가 앞으로 애플리케이션을 확장하고 유지 관리 가능할까?
  • 내가 만든 코드가 다른 동료들이 이해하기 쉽게 작성된걸까?

 

이번 챕터에서 소개하는 디자인 패턴과 원칙 중 상당수가 단순성을 강화하는 데 도움이 될 것이라고 생각합니다. 이러한 디자인 패턴과 원칙으로 코드의 확장성, 유연성, 가독성을 높일 수 있습니다. 하지만 추가 작업과 계획이 필요합니다.

'단순함'과 '쉬움'은 동의어가 아닙니다.

 

패턴을 사용하지 않아도 같은 기능을 더 빠르게 만들 수 있지만, 빠르고 쉬운 작업이 반드시 단순한 결과물로 이어지지는 않습니다. 단순하게 만들면 결과물의 집중도가 높아집니다. 하나의 작업만 수행하도록 디자인하고, 다른 작업으로 지나치게 복잡도를 높이지 마세요.


해당 글은 Unity 공식 문서 - Level_up_your_code_with_Game_Programming_Pattern 내용과 저의 경험을 바탕으로 재해석한 글입니다.

틀린 내용이나, 다른 관점을 가지고 계시다면 댓글로 말씀 부탁드리겠습니다. :)

 

"댓글과 공감 버튼은 양질의 글을 작성하는데 큰 힘이 됩니다!"

감사합니다.

'디자인 패턴' 카테고리의 다른 글

3. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 라스코프 치환 원칙  (0) 2024.10.30
2. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 개방 폐쇄 원칙  (0) 2024.10.29
'디자인 패턴' 카테고리의 다른 글
  • 3. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 라스코프 치환 원칙
  • 2. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 개방 폐쇄 원칙
Logger's
Logger's
생각을 담는 공간 이곳은 게임 개발 아카이브
  • Logger's
    Game Development Archive
    Logger's
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (24)
      • Unity (7)
      • C# (4)
      • 디자인 패턴 (3)
      • 자료구조&알고리즘 (0)
      • Tip (5)
      • 이슈 해결 (3)
      • IDE (2)
      • C++ (0)
      • C++ STL (0)
      • DirectX (0)
      • 잡담 (0)
      • 취미 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    drag
    모바일 환경
    한글 깨짐
    DirectX #Supersampling
    swipe
    DirectX #Texturing
    .editorconfig
    Computer Graphics
    utf-8
    유니티
    Unity
    DirectX #Refraction #Reflection
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
Logger's
1. [Design Pattern] 객체 지향 설계의 5가지 SOLID 원칙 - 단일 책임 원칙
상단으로

티스토리툴바