포스트

SOLID 원칙

SOLID 원칙

머리말


Solid: 견고하고 단단한.
The SOLID principles: 견고하고 단단한 OOP 설계의 5가지 Principle (원칙).

  • Single Responsibility: 단일책임
  • Open-Closed: 개방-폐쇄
  • Liskov Substitution: 리스코프 치환
  • Interface Segregation: 인터페이스 분리
  • Dependency Inversion: 의존관계 역전

Single Responsibility: 단일책임


하나의 클래스는 하나의 책임(목적)만을 가진다

Like Unity Component, 각 컴포넌트는 하나의 기능만 가진다.

플레이어의 Audio, Input, Movement 기능을 Player라는 하나의 클래스로 합쳐서 만들지 않는다.
PlayerAudio, PlayerInput, PlayerMovement 각각의 클래스로 분리하여 단일 책임을 가지게 한다.

클래스가 제공하는 모든 기능은 이 책임과 부합한다.

클래스는 그 책임을 완전히 캡슐화해야 한다

클래스를 수정하는 이유도 단 하나의 이유여야한다.
클래스를 수정할 때 다른 클래스에 영향을 주지 않도록, 클래스의 책임을 분리해야한다.

S: Memo

  • 함수도 마찬가지
    • 하나의 함수는 하나의 책임(목적)만을 가진다.
  • 클래스 당 코드 감소
    • 클래스를 나누기 때문에, 한 클래스의 길이가 짧아진다.
  • 클래스 의존성 감소
    • 클래스가 수정되어도 다른 클래스에 영향을 주지 않는다.

=>

  • Readability (가독성)
    • 짧은 클래스가 읽기 쉽다.
  • Extensibility (확장성)
    • 작은 클래스로부터 상속이 쉽다.
  • Reusability (재사용성)
    • 부분에서 재사용할 수 있도록 작고 모듈식으로 설계
  • 유지보수성 향상

Open-Closed: 개방-폐쇄


클래스 내부 수정 없이 동작을 확장할 수 있어야 한다.

확장에는 열러있고

  • 모듈의 동작을 확장할 수 있다는 것을 의미
  • 요구 사항이 변경될 때, 새로운 동작을 추가해 모듈을 확장
  • 즉, 모듈이 하는 일을 변경할 수 있음

수정에는 닫혀있어야한다

  • 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경 가능
  • 모듈의 라이브러리 (ex:DLL) 의 수정이 필요 없음

O: Memo

AreaCalculator 안에 삼각형, 사각형의 넓이를 계산하는 코드를 직접 넣게 되면,
나중에 오각형, 육각형 을 추가할 때 AreaCalculator 클래스를 직접 수정해야한다.

대신 CalculateArea() 추상 메서드를 가진 Shape 추상 클래스를 만들고,
삼각형, 사각형, 오각형 등의 클래스에서 이를 상속받아 구현한다.

이렇게 하면 AreaCalculator 클래스는 Shape.CalculateArea()를 이용해 넓이를 계산하므로,
새로운 도형이 추가되어도 AreaCalculator 클래스를 직접 수정할 필요가 없다.

=> 코드를 직접 수정하지 않고도 새로운 기능을 추가할 수 있다.

Liskov Substitution: 리스코프 치환


파생 클래스는 기본 클래스를 대체할 수 있어야 한다

(파생, 자식, 하위) <-> (기존, 부모, 상위)

  • 파생 클래스는 기존 클래스를 대체할 수 있어야 한다.
  • 자식 클래스는 부모 클래스의 방향성을 유지해야 한다.
    • 자식 클래스가 부모 클래스의 기능을 필요로하지 않거나 제거하는 경우, 이는 원칙 위반 (방향성을 따르지 않으므로)
  • 부모 클래스를 자식 클래스로 바꿔도 동일하게 동작해야 한다.

  • 하위 클래스를 강력하고 유연하게 만드는 원칙
  • OOP의 상속을 사용하면 하위 클래스를 통해 기능을 추가할 수 있음
    • 그러나, 주의하지 않으면 불필요한 복잡성이 발생

상속보다는 구성

= 상속보다는 인터페이스를 구현하도록
= is-a 보다는 has-a

  • 클래스 계층 구조를 설정하기 전에 클래스 API를 고려
    • 현실의 분류가 항상 클래스 계층 구조로 변환되는 것은 아님
    • Ex: Car와 Train이 별도의 상위 클래스에서 상속받는 것이 더 합리적
      • X: Vehicle (GoFoward, Reverse, TurnRight, TurnLeft) -> Car, Train
        • 기차는 길을 따라 움직이기 때문에 TurnRight, TurnLeft가 필요하지 않음
      • O: IMovable (GoFoward, Reverse), ITurnable (TurnRight, TurnLeft)
        • RoadVehicle (IMovable, ITurnable) -> Car
        • RailVehicle (IMovable) -> Train

L: Memo

  • 추상화를 단순하게 유지

  • 하위 클래스에는 기본 클래스의 퍼블릭 멤버 존재
  • 서브클래싱할 때 기능을 제거하는 경우 리스코프치환원칙 위배
    • 부모 클래스의 방향성을 따르지 않는 것이므로

Interface Segregation: 인터페이스 분리


특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

큰 하나의 인터페이스보다 쪼개진 작은 여러 개의 인터페이스가 좋다.

  • 클라이언트가 자신이 이용하지 않는 메소드에 의존하지 않아야 한다는 원칙
  • 큰 덩어리의 인터페이스들을 구체적이고 작은 단위로 분리
    • 클라이언트들이 꼭 필요한 메서드들만 이용할 수 있게 인터페이스를 분리해야 한다.
  • 시스템 내부 의존성을 약화하고 유연성을 강화

Dependency Inversion: 의존관계 역전


객체 간의 상호작용은 추상화에 의존해야지, 구체화에 의존하면 안된다.

의존성 주입은 이 원칙을 따르는 방법 중 하나

  • 소프트웨어 모듈들을 분리하는 특정 형식
  • 상위(High-level) 모듈은 하위(Low-level) 모듈의 것을 직접 가져오면 안됨
    • 둘 다 추상화에 의존해야 함
  • 추상화는 세부 사항에 의존해서는 안됨.
    • 세부 사항이 추상화에 의존해야 함.
  • 클래스가 다른 클래스와 관계가 있으면 안됨
    • 클래스가 다른 클래스의 작동 방식을 많이 알고 있으면 안됨
    • 종속성(dependency) 또는 결합(coupling) 발생
    • 종속성은 어느 잠재적인 위험
  • X: SwitchDoor, Light, Fan을 모두 직접 제어 (의존)
    • 다른 기기가 추가되면 Switch를 수정해야 함
  • O: SwtichISwitchable을 제어
    • Door, Light, Fan은 각각 ISwitchable을 구현

메모


참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.