이 글은 유니티 자습서의 글을 번역한 글입니다. 원문은 여기에서 볼 수 있습니다.




이 주제는 그렇게 새로운 내용은 아니지만, 매우 중요한 주제입니다. 코딩 습관에 대한 이야기이고, 이는 여러분의 삶을 훨씬 더 쉽게 만들 수 있습니다.

코딩 습관에 깔려있는 생각은 여러분이 코드를 작성할 때 어떤 가이드 라인을 떠올린다는 것 입니다. 이러한 가이드라인은 여러분의 코드가 유지보수 가능하고, 확장 가능하고, 읽기 쉬워서 리팩토링을 할 때의 고통을 덜어주도록 설계되어 있습니다.

특정 코드를 참조하는 클래스가 많아서 생각했던 것보다 훨씬 어려워진 코드 때문에, 여러분의 프로젝트의 해당 코드를 고치고 싶었던 적이 얼마나 되나요? - 이는 키보드 선을 뽑기 위해 컴퓨터 뒤를 봤는데, 수많은 선들이 서로 엉망진창으로 꼬여 있어서 키보드 선을 찾을 수 있다는 희망을 버리게 되는 상황과 비슷합니다.

그래서, 여기에 여러분의 코드에 엉킨 부분을 풀고, 단정된 형태를 유지할 수 있는 일반적인 가이드 라인을 제공합니다.

단일 원칙 Single Responsibility

주어진 클래스는 이상적으로 오직 하나의 일에만 책임을 져야합니다.

단일 원칙 뒤에 있는 생각은 다음과 같습니다.

각 클래스는 작은 일(task)을 하고 이러한 작은 일을 조합함으로써 보다 큰 목표를 해결할 수 있습니다.


유니티는 기능을 분리해서 재사용 가능한 부분들로 설계했고, 단일 원칙은 여기에서 가장 중요한 원칙 중 하나 일지도 모릅니다.

이 원칙에 대한 예를 들기 위해서, Player 클래스가 있다고 생각해 봅시다. Player는 입력, 물리, 무기, 체력, 인벤토리를 다룹니다.

Player가 책임을 지고 있는 부분이 꽤 많아 보입니다. 우리는 이를 여러 개의 다른 컴포넌트로 쪼개서 각 컴포넌트가 오직 하나 만을 수행하도록 해야합니다.

  • PlayerInput - 입력을 관리하는 책임
  • PlayerPhysics - 물리 시뮬레이션을 관리하는 책임
  • WeaponManager - 플레이어 무기를 관리하는 책임
  • Health - 플레이어의 체력을 관리하는 책임
  • PlayerInventory - 플레이어 인벤토리를 관리하는 책임

아, 훨씬 낫네요. 이제 이 클래스들은 모두 서로에게 큰 의존성을 가지고 있고, 여전히 약간의 문제가 있습니다. 이를 해결하기 위한 방법을 보죠.

의존 관계 역전 Dependency Inversion

만약 어떤 클래스가 다른 클래스의 의존한다면, 그 관계를 추상화 시키십시오.


의존 관계 역전의 뒤에 있는 생각은 다음과 같습니다.

어떤 클래스가 다른 클래스를 호출할 때마다, 우리는 그러한 호출을 어떤 종류의 추상화로 바꿔야하며, 이로 인해 각 클래스가 서로 다른 클래스로부터 독립적일 수 있습니다.


가장 바람직한 방법은 클래스 의존 관계를 인터페이스로 대체하는 것 입니다. 그래서 클래스 A가 클래스 B의 메서드를 호출하는 것보다는, 클래스 B가 구현하고 있는 ISomeInterface의 메서드를 호출하도록 합니다.

다른 방법은 클래스 B가 상속받는 부모 클래스를 도입하는 것 입니다. 그래서 클래스 A가 클래스 B를 직접적으로 의존하는게 아닌, 부모 클래스를 다루도록 하는 것 입니다.

물론, 가장 바람직하지 않는 방법은 클래스 A가 직접적으로 클래스 B에 의존하는 것 입니다. 이는 우리가 피하고 싶은 방향이죠.

우리의 이전 예에서, 컴포넌트들은 서로 상당히 강한 커플링을 가지고 있었습니다. PlayerInput은 PlayerPhysics에 대해 의존하고 있었을테고, WeaponManager는 Inventory에 의존하는 등 말이죠.

이 들을 추상화시켜 보죠.

  • IActorPhysics - 입력을 받아들일 수 있고, 아마 물리 시뮬레이션에 영향을 미칠 수 있는 클래스를 위한 인터페이스
  • IDamageable - 대미지를 입을 수 있는 클래스를 위한 인터페이스
  • IInventory - 아이템을 저장하고 가져올 수 있는 클래스를 위한 인터페이스

이제 우리의 PlayerPhysics는 IActorPhysics를 구현하고, Health는 IDamageable을 구현하고, PlayerInventory는 IInventory를 구현합니다.

그러면 이제 PlayerInput은 어떤 IActorPhysics의 존재에 의존하게 되고, WeaponManager는 어떤 IInventory의 존재에 의존하게 되고, 플레이어의 대미지를 다루게 되는 어떤 것은 어떤 IDamageable의 존재에 의존하게 됩니다.

의존 관계 역전은 단일 책임을 어느정도 보강하는데에 도움이 됩니다. PlayerInput은 플레이어가 어떤게 움직이는지 신경쓰지 않아도 되고, 무엇이 플레이어를 움직이는지도 신경쓰지 않아도 됩니다. 그냥 그런 기능을 약속하는 어떤 것이 있다는 사실만 알면됩니다.

모듈화

저는 유니티에서 개인적으로 이런 형태의 코드를 많이 보았습니다.

switch( behavior )
{
    case 1: // some behavior
    case 2: // another behavior
    case 3: // yet another behavior
    case 4: // the last behavior
}

물론 이 코드는 동작하지만, 더 나아질 수 있습니다. 이 코드는 실제로 모듈화해달라고 소리지르고 있습니다. 각각의 행동들은 모듈화될 수 있으며, 그렇게 하면 "행동"은 enum에서 모듈의 인스턴스로 바꿀 수 있습니다. ( 이 경우에 우리의 모듈은 delegate라고 하죠.) 그러면 코드는 이렇게 될 것 입니다.

behavior();

그리고 어딘가 다른 곳에 이 모듈을 할당할 수 있습니다 :

MyClass.behavior = someBehavior;

여기에서 가정상 someBehvior는 함수입니다. ( 만약 우리의 모듈이 클래스라면, 클래스의 인스턴스가 될 수도 있겠죠.)

우리가 상태 머신이나 메뉴 시스템을 만들고 있다면 이 개념을 특히나 유용하게 사용할 수 있죠. 모든 가능한 상태나 메뉴를 enum으로 정의하는 것보다, 쉽게 변경할 수 있는 모듈로 상태와 메뉴를 정의하는 것이 더 이치에 맞습니다. 이렇게 하면 우리는 enum에 새로운 항목을 추가할 필요도 없고, 무엇인가 추가할 때마다 switch case 문을 확장해야할 필요도 없습니다.

결론

이 글은 유일한 코딩 습관에 관한 내용은 아닙니다. 하지만 따라야할 굉장히 중요한 것이죠.

여러분이 항상 이 내용을 따르지는 않아도 됩니다 - 가끔은, 이렇게 하는게 과할 수도 있고, 이치에 맞지 않을 수도 있습니다. 하지만, 비판적인 생각을 연습하고 여러분의 코드가 이로 인해 이득을 얻을 수 있을지 결정해보세요. 명심하십시오. 이는 여러분의 코드 몇 줄을 줄여주는 그런게 아니라, 나중에 올 수 있는 두통으로부터 여러분을 지켜주는 것 입니다.

+ Recent posts