본문 바로가기

💻 개발IT/Design Patterns

2. 객체지향 개념과 설계원칙 - SOLID Principle

Hierarchy of Pattern Knowledge

1. 패턴의 지식체계

패턴의 지식 체계

 1) 설계 패턴 : 알고리즘이 변경되어도 영향 받지 않고 진화할 수 있도록 도와주는 패턴

 2) OO 설계 원칙

     - 변경되는 것을 enpcapsule

     - 상속보다는 composition

     - interface에 맞춰서 프로그래밍

 3) OO 설계 기본 개념

     - Abstraction

     - Encapsulation

     - Polymorphism

     - Inheritance

2. Design Smells (설계 악취) : 잘못된 설계에 대한 증상

이름 증상 원인 해결
Rigidity (경직성) 시스템 의존성으로 인해 수정 어려움
(한 모듈의 수정이 다른 모듈의 수정을 야기)
1. 테스트, 빌드 시간 오래 걸림
2. 작은 변화에도 전체 빌드 필요
SOLID 원칙 지키기
Test 병렬 실행
Fragility (취약성) 한 모듈의 수정이 다른 모듈에 영향
(ex. 오작동)
모듈 간 강한 의존성 모듈 간 의존성 제거
Immobility (부동성) 재사용할 수 있는 컴포넌트를
추출하기 어려운 경우
(ex. 로그인 모듈이 특정 DB, 특정 UI
사용하면 다른 시스템에서 재사용 불가)
해당 컴포넌트와 다른 컴포넌트 간의
강한 의존성
결합도 낮추기
Viscosity (점착성) 코드 추가 및 수정시, 기존 설계를 지키는 코드는 그렇지 않은 코드보다 구현 어려움 여러 레이어를 거쳐 강한 의존성
개발 환경이 비효율적
Dependency는 유지한채로 decoupling
Needless Complexity
(불필요한 복잡성)
미래의 확장을 고려하여 설계하여
불필요하게 시스템이 복잡해짐
현재와 관련 없는 모듈끼리 미래를 위해 불필요한 coupling 현재 요구사항에 집중
Needless Repetition
(불필요한 반복)
복붙을 많이하여
불필요한 코드 반복 많음
개발자 게으름  
Opacity (불투명성) 이해하기 어려운 코드    

 

3. Dependency Management

  • Design Smells은 잘 관리되지 않은 dependency(spaghetti code)로 인해 발생
  • dependency 관리를 위해 OO 언어는 다양한 tools 제공
    • interface
    • Polymorphism

 

Object-Oriented Design Principles

1. R.C. Martin's Software design principles (SOLID)

SOLID : 로버트 마틴이 명명한 객체 지향 프로그래밍 및 설계의 5가지 기본 원칙

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

 

2. SRP

클래스는 단 한 가지의 변경 이유(책임)만 가지고 있어야 한다.

  ※ 책임 (Responsibility) : 클래스의 의무, 변경해야할 이유

  • 책임이 많을 수록 변경 가능성이 높음
  • 더 많은 변경이 있으면 버그 가능성 높음
  • 변경은 다른 부분에도 영향을 줄 수 있어 변경하지 않는 것이 좋음

책임이 여러개라면 클래스를 분리해라

  • Cohesion : 측정 척도

위반 예제 1

  • 정렬을 위해 Comparable을 Implements함
  • Worst : Student는 이름과 SSN을 알아야할 의무 외에 정렬 방법도 추가해야함. 정렬로 인해 다른 클래스에 대한 dependency가 생김
  • 개선 : SortStudentBySSN 클래스를 별도로 만듦

  • 위반예제 2
    • Computational Geometry Application은 draw()쓰지 않고 area만 사용.
      Graphical Application은 area()쓰지 않고 draw()만 사용


    • 1차 개선 : Graphic Rectangle 클래스 분리
      • 아직 개선 필요. 처음 모습과 동일하게 Geometric Rectangle이 변경되면 Graphic Rectangle도 변경되면서 Graphical Application도 영향 받음
    • 2차 개선 : 각 클래스 분리
      각 클래스의 영향도가 Rectangle에 영향을 미치지 않음

    • 책임을 식별하자!
      • 책임이 동종의 것인지 아예 다른 것(분리)인지 확인해야함
      • 파악 방안 : application이 앞으로 어떻게 변화할지 확인 (변화 방향)
      • 예시
  • 하지만 SRP 무분별하게 적용하면 복잡해지고 관리 cost가 높아지니 주의!


3. OCP

  • Software entity(class, module, function 등)이 확장성있게 만들어져야 하지만, 변경에 대해서는 최소화
    • 확장성있게 만들되, 수정이 과도하게 일어나서는 안 된다!
  • 위반시, Rigidity(경직성) Design Smells 발생 
  • 위반 예제 1
    • HRMgr는 모든 임직원의 임금을 올려주는 함수 존재
    • 문제
      • 만약 여기서 Engineer라는 emptype이 추가되면 else if문 추가되고 해당 type의 incEngineerSalary 함수 추가되어야함
      • 만약 다른 클래스에서 해당 함수를 재사용하고 싶은데 거기서는 Faculty, Staff 타입만 존재한다면? 
    • 개선 : Polymorphism 이용
      • Engineer 추가하면 클래스를 추가하면 됨

개선

  • 추상화가 핵심!
    • Abstract base class : 고정 (Employee)
    • All the possible derived classes : 앞으로 추가될 수 있는 행위 (Staff, Secretary...)
    • implementation(concrete class)이 아닌 interface(or abstract class)로 프로그래밍해라
      • interface로 모델링하면 변화가 client까지 파급되지 않음

  • Polymorphism으로 보통 해결하기 때문에 런타임 cost 지불하고 복잡성이 증가되므로 주의!
    • 미리 어떠한 변화가 생길지 모든 case를 예상해서 모든 장치를 만들 필요가 없음.
      • 한 번은 희생.. 변화하지 않을 거라 생각하고 개발
      • 만약 변화가 생겨서 영향도를 파악하고 여러 번 반복되면 추상화 추가 (TDD)

 

3. LSP

Object는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 변경할 수 있어야 한다. 

  • Subtypes(Derived classes)은 그들의 base type(base classes)을 대체할 수 있어야 한다
    • P의 subtype의 C가 있다면, P타입 객체가 쓸 곳에 C 객체로 대체해도 아무런 영향이 없어야 한다.
  • 상속을 쓸지 말지 판단할 수 있는 근거
  • Subtyping vs. Implementation Inheritance
    • Subtyping(interface inheritance) : IS_A 관계
    • Implementation Inheritance(code inheritance) : 타입에는 영향 X  
    • 대부분 언어에서는 extends하면 위 경우가 동시에 일어남
  • 위반 예제 1
    • Queue 클래스 구현할 때 기존에 만들었던 List 클래스가 있어서 재사용하기 위해 상속
    • 문제 : 다른 개발자가 List 사용하는 곳에 Queue 넣어도 잘 동작하겠네? 라고 생각하지만 실제로 Queue는 List기능보다 더 제한적 (결국, Queue가 List를 대체할 개념이 아님. Queue가 List의 서브타입이 될 수 없기 때문!)
    • 해결 : Object Composition(결합)
      • 상속관계가 아니라서 LSP를 어기고 있지 않음

  • 위반 예제 2
    • sql.Date, sql.Time이 util.Date의 일부를 사용하기 위해서 상속함 (단지, 편의를 위해서 활용한 경우)
    • 문제 : util.Date 타입의 변수를 sql.Time으로 레퍼런스하고 해당 변수에서 getDate()를 하면 에러 발생(sql.Time에서 deprecated시킴)
  • LSP 위반은 다른 Solid 법칙 위반을 이끌 수 있음
    • C type은 P type의 서브 타입은 아니지만 일부를 reuse를 하기 위해 단순 상속을 함. C type에는 f method가 없어서 C 타입 객체에서 f method call시 에러 발생 > 개발자가 f method 맨 위에 object 타입 식별을 하여 코드 작성(더 설계가 안 좋아짐)
    • f method는 Ptype이 아닌 Ctype에도 종속적이게 되고, 또 다른 타입이 생겼을 때 또 추가해야함 (OCP 만족 X)

 

4. DIP

추상화에 의존해야지, 구체화에 의존하면 안 된다.

  • High-level module은 low-level module에 의존해서는 안 되고, 둘다 abstraction에 의존해야한다.
    • Abstractions은 details에 대해 의존하면 안 되지만 details은 abstraction에 의존해야한다.
  • dependency 방향의 역전
    • High-level resourse : Interface, abstract class
      Low-level resourse : concrete class
    • 반대되는 SASD 구조

  • Ownership의 역전
    • 서비스의 interface는 구현되어 있는 곳이 아닌 client와 같은 package에 있어야함 (client가 ownership을 가져야함)
    • 위반 예제
      • 3가지 레이어로 구성된 시스템. Policy Layer가 Mechanism Layer을 사용하고..
      • 문제 : 하위 수준인 Utility Layer의 변화가 Policy, Mechanism Layer의 변화를 가져옴
      • 개선 : interface와 implementation을 분리하고, interface는 사용하는 패키지 쪽으로 ownership을 옮겨줌 (간접적으로 영향을 미치도록)


5. ISP

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

  • 클라이언트는 자신이 사용하지 않는 method에 대해 의존성을 강제하면 안 됨
    • 잘게 만들어서 꼭 dependency를 가져야하는 Method만 가지도록 해야함
    • 클라이언트가 많아지면 interface가 커지게 되어 주의 > cohesive한 그룹끼리 interface를 만들어야함
  • 위반 예제
    • Roaster Application은 getName, getSSN만 사용하고 Account Application은 getInvoice, postPayment만 사용
    • 문제 : Student Enrollment 가 수정되면 두 application에 영향을 미침
    • 개선
      • Application별로 interface를 분리함

 

반응형