Deff_Dev

[Unity/C#] SOLID 원칙 본문

Unity(유니티)/유니티 공부

[Unity/C#] SOLID 원칙

Deff_a 2024. 5. 24. 23:04

객체지향 언어로 코딩 해봤다면 한 번쯤은 들어봤을 SOLID 원칙을 유니티를 예를 들어 설명해보겠다.

SOLID 원칙이 뭘까 ?

SOLID 원칙은 객체지향 설계에서 지켜야하는 5가지 원칙이다.

  • 단일 책임의 원칙 (SRP: Single Responsibility Principle)
  • 개방 폐쇄 원칙 (OCP: Open Close Principle)
  • 리스코프 치환의 원칙 (LSP: Liskov Subsitution)
  • 인터페이스 분리의 원칙 (ISP: Interface Segregation Principle)
  • 의존성 역전의 원칙 (DIP: Dependency Inversion Principle)

단일 책임의 원칙 (SRP: Single Responsibility Principle)

  • 하나의 클래스는 하나의 목적을 위해 생성한다.

즉, 하나의 클래스는 하나의 기능만 담당해야 된다는 얘기이다

 

E는 다른 Player 스크립트와 구별하기 위해 임의로 붙혀놓은 것이다.

 

예를들어, Player를 구현하기위해 입력, 이동, 공격, 애니메이션 등이 필요 할 때, 해당 기능들을 위 사진처럼 Player 하나의 스크립트에 다 넣으면 안된다.

 

위 사진처럼 기능을 구현한다면 하나의 기능을 수정하더라도 스크립트 전체 수정이 발생하고 확장성, 가독성 측면에서 매우 안좋기 때문이다.

 

단일 책임 원칙을 지키기 위해서는 위 사진처럼 Player의 기능별로 스크립트를 작성한 다음 컴포넌트로 추가하면 된다.

 

필요한 기능은 같은 오브젝트에 있으니 GetComponent해 Player 스크립트에서 각 기능을 조합하는 형식으로 구현한다.


개방 폐쇄 원칙 (OCP: Open Close Principle)

  • 확장에는 열려있고 수정에는 닫혀있어야 한다.

즉, 기능이 추가될 때, 원래 존재하는 클래스는 수정하지말고 새로운 클래스를 추가해야 된다는 얘기이다.

 

예를들어, Player가 아이템을 먹었을 때 Item의 형태에 따라 다른 효과를 받는다고 했을 때, OCP를 지키지 않는다면 이런 식으로 코드를 작성하게된다. 

 

물론 이렇게 코드를 작성했다고 해서 이 코드는 틀린 코드냐고 한다면 답은 '아니다'이다.

경우에 따라 이 코드가 효율적일 때도 있을 것이다.

 

하지만 아이템의 개수가 10개, 20개, ... 100개까지 늘어난다면, UseItem 함수에는 아이템 효과를 처리하는 case 문이 계속 추가되어야 하고 이는 코드가 점점 길어지고 복잡해질 수 있다는 문제를 가지고있다.

 

그렇기 때문에 새로운 아이템을 추가할 때는 UseItem 함수를 수정하지 않고 새로운 클래스를 추가해야된다.

 

많은 방법이 있겠지만, 이 글에서는 Interface를 활용해 위 코드를 수정해보겠다.

 

Interface를 사용해서 위 코드와 똑같은 기능을 하는 코드를 작성해봤다.

 

지금은 "굳이 이렇게 할 필요가 있을까 ?" 라고 생각이 들 수 있다.

하지만 추후에 새로운 아이템이 추가된다고 했을 때, IUseable를 상속받는 클래스를 만들어 UseItem함수와 해당 아이템에 맞는 기능을 작성하기만 하면 기존 코드 수정없이 확장을 할 수 있다는 상당한 장점을 가질 것이다.


리스코프 치환의 원칙 (LSP: Liskov Subsitution)

  • 서브 타입(상속받은 하위 클래스)은 어디서나 자신의 기반 타입(상위 클래스)으로 교체할 수 있어야 한다.

즉, 자식 클래스의 공통된 기능들만 부모 클래스에 작성하라는 말이다.

 

예를 들어, 새들이 날아다니고 울음소리를 내는 기능을 만든다고 가정해보자.

 

위 사진 처럼 Bird 클래스에 Fly와 Cry 함수를 만들어 자식 클래스에서 해당 함수들을 사용할 수 있도록 구현했다.

 

하지만 여기서 날지 못하는 펭귄을 펭귄을 추가한다면 어떻게 구현해야될까 ?

이렇게 작성하게 된다면 펭귄은 날지 못하지만 해당 게임에서는 날게 된다.

물론, 펭귄을 Fly함수를 사용하지 않도록 조건 처리를 한다면 날지 못할 것이다.

 

하지만 이런식의 구현이 객체지향 관점에서 맞을까 ?

 

답은 "아니다" 이다.

 

리스코프 치환의 원칙은 자식 클래스에서 공통적으로 사용되는 기능만 부모 클래스에 작성해야된다는 점을 얘기한다.

 

위 코드에서 리스코프 치환의 원칙을 지키기 위해서는 어떻게 수정해야 될까 ?

 

 

Interface를 활용하여 날 수 있는 새들에게 상속시키는 방법을 사용할 수 있다.

 

그리고 팀 프로젝트를 진행하면서 리스코브 치환의 원칙을 지켰던 부분을 정리한 글이다. 참고해서 보면 좋을 것 같다.

 

[내배캠 Unity 4기] 정식 캠프 6주차 (1) [Generic 다운캐스팅]

오늘 배운 내용오브젝트 풀을 이용하여 오브젝트 생성을 구현했는데, 하나의 딕셔너리로 오브젝트를 관리하고 GetComponent를 최소화하 위해 오브젝트 풀로 생성되는 각각의 클래스들이 PoolObject

deff-dev.tistory.com


인터페이스 분리의 원칙 (ISP: Interface Segregation Principle)

  • 클라이언트가 사용하지 않는 인터페이스 때문에 영향을 받아서는 안된다. 

즉, 인터페이스에 하나의 기능만 넣어어야 된다는 얘기이다. 

 

예를들어, 공격, 이동, 죽음 기능이 있는 Unit을 만든다고 해보자

 

이런 식으로 하나의 인터페이스에 많은 기능을 넣어 구현을 했다면, 

만약에 공격과 죽음은 있지만 이동을 할 수 없는 Unit을 만든다고 했을 때, 구현할 필요가 없는 Run 함수까지 구현해야된다.

 

그렇기 때문에 인터페이스의 확장성을 위해 인터페이스마다 하나의 기능을 담당하도록 구현해야한다.

 

SRP와 ISP는 클래스와 인터페이스의 차이라고 생각하면 쉽다.


의존성 역전의 원칙 (DIP: Dependency Inversion Principle)

  • 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고받음으로써 관계를 최대한 느슨하게 만든다.

즉, 클래스를 직접 참조해 구현하지말고 상위 요소를 참조하라는 말이다.

 

예를들어, 문을 여는 스위치를 구현한다고 해보자.

 

위 사진 처럼 Switch를 구현했을 때, 전등 On/Off 하는 기능을 추가해야 한다면 ? 

Switch 클래스에 Light 클래스를 연결하고 Toggle함수를 수정해야된다.

 

이 말은 스위치 기능에서  Switch 클래스의 의존성 높아진다는 의미이다.

 

DIP는 의존성을 Switch 클래스가 아닌 각각의 기능 클래스로 역전시킨다는 의미라고 생각하면된다.

 

Interface를 활용하여 기능이 추가되도 Switch 클래스는 수정없이 스크립트만 추가하면되도록 구현할 수 있다.

 

앞에서 설명한 4가지의 원칙보다 DIP가 예제처럼 확실한 상황이 아닐 확률이 높기 때문에 가장 사용하기 어렵다고 생각한다.


글을 마치며

이렇게 SOLID 원칙에 대해 유니티 프로젝트를 예로 설명해봤다.

 

실무에서는 SOLID 원칙에 대해 생각하면서 코딩을 하지 않고, 이미 몸에 베어져 있기 때문에 코드를 작성하면 SOLID 원칙을 지키는 코드를 작성한다고 한다.

 

앞으로 코드를 작성하면서 이 부분에 대해 계속 고민하면서 코드를 작성해야겠다.