객체지향 설계 5원칙: SOLID

berry·2023년 4월 9일
0

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

정의

작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어 있어야 한다.

하나의 클래스에 여러 기능(책임)을 넣느냐, 클래스를 분리하여 기능(책임)을 분산시키느냐는 프로그램의 유지보수와 밀접한 관련이 있다.

단일 책임 원칙 준수 유무에 따른 가장 큰 특징 기준 척도는, '기능 변경(수정)' 이 일어났을때의 파급 효과 이다.
한 객체에 책임이 많아질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아지게 되어 한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용이 일어 나게 된다.

SRP를 적용한다면, 각 클래스의 책임 영역이 확실해지기 때문에 어떠한 역할에 대해 변경사항이 발생했을 때, 그 책임을 지니고 있는 클래스만 수정해주면 된다.
이것을 다르게 말하면, 모듈이 변경되는 이유가 한가지 여야 함을 뜻한다. 여러가지 책임을 가지고 있으면 각기 다른 사유에 의해서 모듈이 변경되는 이유가 여러가지가 되기 때문이다.

한 클래스는 한 가지 책임에 관한 변경사항이 생겼을 때만 코드를 수정하게 되는 구조가 좋은 구조이다.

적용 방법

  • Extract class를 통해 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 한다.
    여기서 관건은 책임만 분리하는 것이 아니라 분리된 두 클래스간의 관계의 복잡도를 줄이도록 설계하는 것이다.
    만약 Extract Class 된 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 갖고 있다면 Extract Superclass를 사용할 수 있다. 이것은 Extract 된 각각의 클래스들의 공유되는 요소를 부모 클래스로 정의하여 부모 클래스에게 위임하는 기법이다.
    따라서 각각의 Extract Class 들의 유사한 책임들은 부모에게 명백히 위임하고 다른 책임들은 각자에게 정의할 수 있다.
  • Move FieldMove Method를 통해 책임을 기존의 어떤 클래스로 모으거나, 이럴만한 클래스가 없다면 새로운 클래스를 만들어 해결할 수 있다.
    즉 산발적으로 여러 곳에 분포된 책임들을 한 곳에 모으면서 설계를 깨끗하게 한다. 즉 응집성을 높이는 작업이다.

적용 이슈

클래스는 자신의 이름이 나타내는 일을 해야 한다. 올바른 클래스 이름은 해당 클래스의 책임을 나타낼 수 있는 가장 좋은 방법이다.
무조건 책임을 분리한다고 SRP가 적용되는 것은 아니다. 각 객체 간의 응집력이 있다면 병합을, 결합력이 있다면 분리를 적용해야 한다.

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

정의

소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.

이것은 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화 해야 한다는 의미로, 요구사항의 변경이나 추가사항이 발생 하더라도, 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻이다.
OCP는 관리가능하고 재사용 가능한 코드를 만드는 기반이며, OCP를 가능케 하는 주요 메커니즘은 추상화와 다형성이라고 설명한다. OCP는 객체지향의 장점을 극대화하는 아주 중요한 원리이다.

적용 방법

  1. 변경(확장)될 것과 변하지 않을 것을 엄격히 구분한다.
  2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.
  3. 구현에 의존하기보다 정의한 인터페이스에 의존하도록 코드를 작성한다.

적용 이슈

확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 관계가 더 복잡해질 수 있다.
이런 갈등 상황을 잘 포착하여 자신의 기준에 따라 결정을 내리는 것이 필요하다.

인터페이스는 가능하면 변경되어서는 안된다. 따라서 인터페이스를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
인터페이스 설계에서 적당한 추상화 레벨을 선택해야 한다. '추상화란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징' 이라고 정의할 수 있다.
즉, 이 '행위'에 대한 본질적인 정의를 통해 인터페이스를 식별해야 한다.

3. LSP (리스코프 치환의 법칙)

정의

서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다.

달리 말하면 서브 타입은 기반 타입이 약속한 규약 (public 인터페이스, 물론 메서드가 던지는 예외까지 포함된다)을 지켜야 한다.
상속은 구현 상속(extends)이든 인터페이스 상속(implements)이든 궁극적으로는 다형성을 통한 확장성 획득을 목표로 한다. LSP 원리도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미한다.
다형성과 확장성을 극대화하려면 하위 클래스를 사용하는 것보다 상위의 클래스(인터페이스)를 사용하는 것이 더 좋다.
일반적으로 선언은 기반 클래스로, 생성은 구체 클래스로 대입하는 방법을 사용한다.
상속을 통한 재사용은 기반 클래스와 서브 클래스 사이에 IS-A 관계가 있을 경우로만 제한 되어야 한다. 그 외의 경우에는 합성을 이용한 재사용을 해야한다.
상속은 다형성과 따로 생각할 수 없다. 그리고 다형성으로 인한 확장 효과를 얻기 위해서는 서브 클래스가 기반 클래스와 클라이언트 간의 규약(인터페이스)을 어겨서는 안된다. 결국 이 구조는 다형성을 통한 확장 원리인 OCP를 제공하게 된다. 따라서 LSP는 OCP를 구성하는 구조가 된다.

적용 방법

  1. 만약 두 개체가 똑같은 일을 한다면 둘을 하나의 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.
  2. 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 한다면 공통의 인터페이스를 만들고 둘이 이를 구현한다. (인터페이스 상속)
  3. 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만든다.
  4. 만약 두 개체가 하는 일에 추가적으로 무언가를 더 한다면 구현 상속을 사용한다.

적용 이슈

  1. 다형성을 위한 상속 관계가 필요 없다면 Replace with Delegation을 한다. 상속은 깨지기 쉬운 기반 클래스 등 을 지니고 있으므로 IS-A 관계가 성립되지 않는다. LSP를 지키기 어렵다면 상속대신 합성(composition)을 사용하는 것이 좋다.
  2. 상속 구조가 필요 하다면 Extract Subclass, Push Down Field, Push Down Method 등의 리팩토링 기법을 이용하여 LSP를 준수하는 상속 계층 구조를 구성한다.
  3. IS-A 관계가 성립한다고 프로그램에서 까지 그런것은 아니다. 이들간의 관계 맺음은 이들의 역할과 이들 사이에 공유하는 연산이 있는지, 그리고 이들 연산이 어떻게 다른지 등을 종합적으로 검토해 봐야 한다.
  4. 기반 클래스를 서브 클래스로 치환 가능하게 하려면 받아들이는 선 조건에서 서브 클래스의 제약사항이 기반 클래스의 제약 사항 보다 느슨하거나 같아야 한다. 만약 제약조건이 더 강하다면 기반 클래스에서 실행되던 것이 서브 클래스의 강 조건으로 인해 실행되지 않을 수도 있기 때문이다. 반면 서브 클래스의 후 조건은 같거나 더 강해야 하는데, 약하 다면 기반 클래스의 후 조건이 통과시키지 않는 상태를 통과시킬 수도 있기 때문이다.

4. ISP (인터페이스 분리의 법칙)

정의

한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.

즉, 어떤 클래스가 다른 클래스에 종속될 때에는 가능한 최소한의 인터페이스만을 사용해야 한다.
ISP를 '하나의 일반적인 인터페이스보다는, 여러 개의 구체적인 인터페이스가 낫다'라고 정의할 수도 있다. 만약 어떤 클래스를 이용하는 클라이언트가 여러 개고 이들이 해당 클래스의 특정 부분집합만을 이용한다면, 이들을 따로 인터페이스로 빼내어 클라이언트가 기대하는 메시지만을 전달할 수 있도록 한다.
SRP가 클래스의 단일책임을 강조한다면 ISP는 인터페이스의 단일책임을 강조한다.
하지만 ISP는 어떤 클래스 혹은 인터페이스가 여러 책임 혹은 역할을 갖는 것을 인정한다. 이러한 경우 ISP가 사용되는데 SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달한다.

적용 방법

  • 클래스 인터페이스를 통한 분리
    - 클래스의 상속을 이용하여 인터페이스를 나눌 수 있다.
    이와 같은 구조는 클라이언트에게 변화를 주지 않을 뿐 아니라 인터페이스를 분리하는 효과를 갖는다. 하지만 거의 모든 객체지향 언어에서는 상속을 이용한 확장은 상속받는 클래스의 성격을 디자인 시점에 규정해 버린다. 따라서 인터페이스를 상속받는 순간 인터페이스에 예속되어 제공하는 서비스의 성격이 제한된다.
  • 객체 인터페이스를 통한 분리
    - 위임을 이용하여 인터페이스를 나눌 수 있다.
    위임이란, 특정 일의 책임을 다른 클래스나 메소드에 맡기는 것이다. 만약 다른 클래스의 기능을 사용해야 하지만 그 기능을 변경하고 싶지 않다면, 상속 대신 위임을 사용한다.

적용 이슈

  1. 기 구현된 클라이언트에 변경을 주지 말아야 한다.
  2. 두 개 이상의 인터페이스가 공유하는 부분의 재사용을 극대화 한다.
  3. 서로 다른 성격의 인터페이스를 명백히 분리한다.

5. DIP (의존성역전의 원칙)

정의

구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전 원칙이다.

실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙이다.
DIP의 키워드는 'IOC', '훅 메소드'(슈퍼클래스에서 디폴트 기능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 훅 메소드라고 한다. 서브클래스에서는 추상 메소드를 구현하거나, 훅 메소드를 오버라이드하는 방법을 이용해 기능의 일부를 확장한다.), '확장성'이다.
이 세 가지 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트 간의 커뮤니케이션을 효율적이게 한다.


출처

profile
공부 내용 기록

0개의 댓글