[Java] SOLID (SRP, OCP, LSP, ISP, DIP)

rin·2020년 5월 29일
0
post-thumbnail

ref.http://www.nextree.co.kr/p6960/

SOLID

객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙
1. SRP (Single responsibility principle) 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야 한다.
2. OCP (Open/Close principle) 개방/폐쇄 원칙 : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
3. LSP (Liskov substitution principle) 리스코프 치환 원칙 : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
4. ISP (Interface segregtion principle) 인터페이스 분리 원칙 : 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
5. DIP (Dependency inversion principle) 의존관계 역전 원칙 : 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. (의존성 주입은 이 원칙을 따르느 방법 중 하나다.)

1. SRP

✏️정의
Single responsibility principle, 단일 책임 원칙

  • 작성된 클래스는 하나의 가능한 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어 있어야 한다.
  • 이는 객체지향 원리의 대전제 격인 OCP원리 뿐만 아니라 다른 원리들을 적용하는 기초가 된다.

✏️적용방법

  1. 여러 책임을 가지고 있는 경우
    혼재된 각 책임을 각각의 개별 클래스(Extract Class)로 분할 → 클래스 당 하나의 책임만 맡도록 한다.
    핵심은 단순히 책임"만" 분리하는 것이 아니라 분리된 두 클래스간의 관계의 복잡도를 줄이도록 설계하는 것이다.
    상속으로 분리된 클래스가 유사한 책임을 중복해서 가진다면 부모 클래스 정의를 통해 공유되는 요소를 위임한다.

  2. 산발적으로 여러 곳에 책임이 분산된 경우
    해당 책임을 Move Field와 Move Method를 통해 특정 클래스로 모은다. (마땅한 클래스가 없으면 새로운 클래스를 만든다.) → 응집성 증가

✏️효과

  • 책임 영역이 확실해 짐으로 특정 책임 A의 변화가 책임 B의 변화로 이뤄지는 잘못된 연쇄작용에서 자유로울 수 있다.
  • 책임을 적절히 분배함으로써 코드의 가독성을 향상하고 유지보수를 용이하게 한다.

✏️이슈

  • 클래스는 자신의 이름이 나타내는 일을 해야 한다(=하나의 개념을 나타내야 한다.). 즉, 올바른 클래스 이름은 해당 클래스의 책임을 나타낼 수 있는 가장 좋은 방법이다.

2. OCP

✏️정의
Open/Close principle, 개방/폐쇄의 원칙

  • 소프트웨어의 구성요소인 컴포넌트, 클래스, 모듈, 함수는 확장에는 열려있고 변경에는 닫혀있어야 한다.
  • 변경을 위한 비용을 최소화하고 확장을 위한 비용은 최대화한다. 즉, 요구사항의 변경에도 기존 구성요소는 수정이 일어나면 안되며 단순한 확장을 통한 재사용이 가능해야한다는 뜻이다.
  • OCP를 가능하게 하는 주요 메커니즘은 추상화와 다형성.

✏️적용방법

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

✏️효과

  • 관리가능하고 재사용 가능한 코드를 만드는 기반으로써 객체 지향의 장점을 극대화 하는 중요한 원리이다.

✏️이슈

  • 확장되는 것과 그렇지 않은 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 관계가 더 복잡해 질 수 있다.
  • 인터페이스를 정의할 때 여러 경우의 수에 대한 고려와 예측을 통해 인터페이스가 변경될 가능성은 최소화해야한다.
  • 인터페이스 설계에서 적당한 추상화 레벨을 선택해야 한다. 행위에 대한 본질적인 정의를 통해 인터페이스가 식별 가능하도록 설계해야한다.

3. LSP

✏️정의
Liskov substitution principle, 리스코프 치환 원칙

상속은 구현 상속(extends)이든 인터페이스 상속(implements)이든 궁극적으로는 다형성을 통한 확장성 획득을 목표로 한다. 다형성과 확장성을 극대화하기 위해선 하위 클래스를 사용하는 것보다는 상위 클래스(or 인터페이스)를 사용하는 것이 더 좋다.

  • 서브 클래스(자식)는 언제나 기반 클래스(조상)로 교체할 수 있어야 한다. 즉, 서브 클래스는 기반 클래스가 약속한 규약(public 인터페이스, 메소드가 던지는 예외 등)을 지켜야한다.
  • 일반적으로 선언은 기반 클래스로, 생성은 구체화된 서브 클래스로 대입하는 방법을 사용한다. (범용 작업을 가능하게 함)

✏️적용방법

  1. 두 "객체"가 같은 일을 한다면 둘을 하나의 클래스로 표현하고 구분 가능한 필드를 둔다. (EnumType 이용하는 것처럼..)
  2. 같은 연산을 제공하나 약간씩 차이가 있다면 공통의 인터페이스를 만들고 각각 이를 구현한다. (인터페이스 상속)
  3. 공통된 연산이 없다면 완전 별개의 각각의 클래스를 만든다.
  4. 두 객체가 하는 일에 추가적인 일이 있다면 구현 상속을 사용한다.

✏️효과

  • 다형성을 통한 확장의 원리인 OCP를 제공하게 된다. 즉, LSP는 OCP를 구성하는 구조가 된다.
  • 규약을 준수하는 상속 구조를 제공함으로써 OCP는 이를 바탕으로 확장에 대한 다형성을 제공할 수 있게 돼 변화에 열려있는 프로그램을 만들 수 있도록 한다.

✏️이슈

  • 혼동될 여지가 없고 트레이드 오프를 고려한 선택이라면 그대로 둔다.
  • 다형성을 위한 상속 관계가 필요없다면 대신 합성을 사용하는 것이 좋다.
  • 상속 구조가 필요하다면 Extract Subclass, Push Down Field, Push Down Method 등의 리팩토링 기법을 이용하여 LSP를 준수하는 상속 계층 구조를 구성한다.
  • IS-A 관계를 판단할 때는 프로그램 내에서의 역할과 공유하는 연산, 연산간의 차이점 등을 종합적으로 검토해야한다.
  • Design by Contract의 적용 : 조건 = 제약, 사전 제약이 더 강하면 서브 클래스에서 실행조차 안될 수 있다. 반면에 사후 제약이 더 약하면 잘못된 결과를 반환할 수 있다.

❗️NOTE
Design by Contract : 서브 클래스에서는 기반 클래스의 사전 조건과 같거나 더 약한 수준에서 사전 조건을 대체할 수 있고, 기반 클래스의 사후 조건과 같거나 더 강한 수준에서 사후 조건을 대체할 수 있다.

4. ISP

✏️정의
Interface segregtion principle, 인터페이스 분리 원칙

  • 어떤 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야한다. 즉, 종속 관계를 가질 때는 최소한의 인터페이스만을 사용해야 한다.
  • 하나의 일반적인 인터페이스보다 여러개의 구체적인 인터페이스가 낫다 → 인터페이스 분리의 필요성
  • SRP : "클래스"의 단일 책임을 강조, ISP : "인터페이스"의 단일 책임을 강조

✏️적용방법

  1. 클래스 인터페이스를 통한 분리
    클래스의 상속을 이용해 인터페이스를 분리한다.
    분리된 인터페이스를 여러개 상속받아 클라이언트에게 변화를 주지 않을 수 있다.

  2. 객체 인터페이스를 통한 분리
    위임(Delegation)을 이용해 인터페이스를 나눌 수 있다.
    위임이란, 특정 작업의 책임을 다른 클래스나 메소드에 맡기는 것으로, 만약 다른 클래스의 기능을 "사용해야 하나" 그 기능을 "바꾸고 싶지 않다면" 상속 대신 위심을 사용한다.

❗️NOTE
상속과 위임
일반적으로 두 클래스 간의 관계가 'IS-A'이면 상속을 사용한다. (e.g student(서브 클래스) is a person(기반 클래스)) 상속을 통해 기반 클래스가 제공하는 모든 메소드를 사용할 수 있게된다. 즉, 상속을 통해 구현된 서브 클래스와 기반 클래스 사이에는 강한 연관관계가 생기게 된다.

상속과 다르게 위임다른 클래스의 객체를 멤버로 갖는 형태의 클래스 정의이다. 특정 메소드의 호출을 멤버 클래스의 메소드에 포워드(Forward)하는 식으로 구현하게 된다.

class B {

  private A a;

  public void prt(){
    A.prt();
  }

}

✔️일반적인 판단 기준

  1. 두 클래스의 관계가 IS-A이면 상속
  2. 기존에 존재하는 API에 넘겨줘야 하는 경우 상속
  3. final 클래스를 확장하고 싶은 경우 위임

✏️효과

  • 두 개 이상의 인터페이스가 공유하는 부분의 재사용을 극대화한다.
  • 서로 다른 성격의 인터페이스를 명백히 분리한다.

✏️이슈

  • 이미 구현된 클라이언트에 변경을 주지 않아야 한다.

5. DIP

✏️정의
Dependency inversion principle, 의존관계 역전 원칙

  • 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계 관계를 끊는 의미의 역전이다.
  • 실제 사용 관계는 바뀌지 않으며, 추상을 매개로 메시지를 주고 받음으로써 관계를 최대한 느슨하게 만드는 원칙이다.
  • 키워드는 IOC, Hook Method, 확장성

✏️적용방법

  1. 비동기적으로 커뮤니케이션이 이루어져도 될(혹은, 이뤄져야 할) 경우, 컴포넌트 간의 커뮤니케이션이 복잡할 경우, 컴포넌트 간의 커뮤니케이션이 (빈번하게 확인이 필요한 구조 등) 비효율적일 경우에 사용된다.
  2. 레이어링
    Transitive Dependency가 발생했을 때 상위 레벨의 레이어가 하위 레벨의 레이어를 바로 의존하게 하는 것이 아니라 이 둘 사이에 존재하는 추상레벨을 통해 의존해야 한다. 상위레벨의 모듈은 하위레벨의 모듈로의 의존성에서 벗어나 그 자체로 재사용되고 확장성도 보장 받을 수 있다.

✏️효과

  • IOC, Hook Method, 확장성 이라는 핵심 요소가 조합되어 복잡한 컴포넌트들의 관계를 단순화하고 컴포넌트 간의 커뮤니케이션을 효율적이게 한다.
  • 복잡하고 지난한 컴포넌트 간의 커뮤니케이션 관계를 단순화하기 위한 원칙

Hook Method
슈퍼 클래스에서 디폴트 기능을 정의 or 내용을 비워두어서 서브 클래스에서 선택적으로 오버라이드 할 수 있도록 만든 메소드.
서브 클래스에서는 훗 메소드를 오버라이드하거나 추상 메소드를 구현하는 방법을 이용해 기능의 일부를 확장한다.

profile
🌱 😈💻 🌱

0개의 댓글