[디자인 패턴]

=================================================================================

 디자인 패턴은 많은 프로그래머들이 성공적인 프로젝트에서 무수히 반복해 사용했던 문제 해결 기술을 발견해 정형화한 것이다.
 각 패턴은 특정 맥락의 문제를 효과적으로 해결할 수 있게 해주며, 패턴들은 해결하는

 문제의 맥락(GoF의 용어로는 Intent)이 다르다.

 분명 패턴을 잘 이해하고 사용하면, 수많은 개발자, 연구자들의 “아하(Aha)!” 경험을

 (그들이 들였던 노력에 비해서는) 손쉽게 사용할 수 있게 된다.
 하지만 패턴은 빈번히 발생하는 ‘특정’ 문제에 대해 적용할 수 있는 해결책이기
 때문에 객체들 간의 게임에 법칙에 관한 일반 룰까지 알려주지는 않는다.

 패턴이 좋은 게임의 규칙을 이용해 객체들의 관계를 맺고 있음은 분명하다
 (좋은 게임의 규칙을 사용하고 있지 않다면, 특정 문제에 대해서도 좋은 해결책이 나올 수 없다).
 그런데 이 때 패턴이 암묵적으로 사용하는 게임의 법칙을 모르면,
 패턴이란 고도의 게임 전술을 유용하게 구사할 수 없게 된다.
 패턴은 성공한 프로젝트 코드들에서 발견, 일반화되는 과정에서 코드에서
 디자인으로 추상화됐기 때문에 이들 다시 코드로 구체화하려면 패턴이 사용하는
 게임의 법칙을 알아야 한다.
 이를 모른다면 장님이 코끼리 다리 만지듯 패턴을 사용하게 될 위험이 있다.
 패턴을 사용해 프로그램을 만들었는데, 괜히 복잡해지기만 하던 걸? 글쎄….

 

===============================================================================================
[객체지향 설계의 원칙]
 
- 소프트웨어 설계 모델의 메타적인 원리가 디자인 패턴의 단위라고 한다면
  디자인 패턴에 등장하는 좋은 구조들에 대한 메타적인 원리가 (ISP, SRP, DIP, OCP, LSP)정도 된다.

=================================================================================================

 

- LSP::리스코프 치환 원리
  "서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다."
  상속은 구현 상속(extends 관계)이든 인터페이스 상속(implements 관계)이든
  궁극적으로는 다형성을 통한 확장성 획득을 목표로 한다.
  LSP 원칙도 역시 서브 클래스가 확장에 대한 인터페이스를 준수해야 함을 의미한다.
  다형성과 확장성을 극대화하려면 하위 클래스를 사용하는 것보다 상위의 클래스(인터페이스)를 이용하는 것이 좋다.


주의점::

 “서브 클래스에서는 기반 클래스의 사전 조건과 같거나 더 약한 수준에서 사전 조건을 대체할 수 있고,
   기반 클래스의 사후 조건과 같거나 더 강한 수준에서 사후 조건을 대체할 수 있다.”

=================================================================================================

 

- SRP::단일 책임 원리
  SRP는 하나의 클래스에 한 가지 책임을 가르치는 원칙이다.
  우리는 설계 관점에서 우리가 인식하지 못하는 SRP 위반을 자주 하게 된다.
  이 위반을 경계하기 위해 깊은 통찰력이 필요하지도 않다.
  단지 머리에 ‘책임’이란 단어를 상기하는 습관이면 된다

 

  적절한 책임 분배는 객체지향 원리들의 대전제 격인 OCP뿐 아니라 다른 원리들을 적용하는 기초가 되어준다

  설계자는 분리의 ‘크기(granularity)’를 고민한다.


  작은 단위로 섬세하게 사용할 수 있는 미세단위(fine-grained)로 구분할 것인가, 아니면 단순하지만

  입도가 큰(COARSE-GRAINED) 단위로 구분할 것인가에 대해서 말이다.
  이 문제의 경우 애플리케이션에서 문제 영역이 각각 서로 다른 케이스 바이 케이스로 이루어지기 때문에 명백히

  일관적으로 적용할 가이드라인을 제공하기 힘들다.

  하지만 그 기준은 대상에 대한 복잡도, 크기, 용도가 된다.
  복잡도가 높고 부피가 큰데 반해 그 용법이 단순하다면 COARSE-GRAINED가 적합하다.
  역으로 복잡도가 낮고 부피가 작으며 용법이 다양하다면 fine-grained가 적합하다.

 

 SRP 위반의 악취는
   -‘여러 원인에 의한 변경(divergent change)’
   -‘산탄총 수술(shotgun surgery)’
  을 들 수 있다.

 

  여러 원인에 의한 변경은 한 클래스를 여러 가지 다른 이유로 고칠 필요가 있을 때 발생한다.
  즉, 하나의 클래스에 여러 책임이 혼재하고 있어서 하나의 책임의 변화가 다른 책임에게 영향을 준다.

  산탄총 수술이란 하나의 책임이 여러 클래스에 분산되어 있기 때문에 발생한다.
  한 클래스가 너무 많은 책임을 맡고 있어도 곤란하지만,
  책임을 식별하지 못해 이를 담당할 클래스를 만들지 않고 여러 클래스에 흩뿌려 놓는 것 또한 문제가 있다.
  이는 보통 프로그램의 전체 책임을 올바로 분담하지 못해서 발생하게 된다.


 SRP를 적용하면 클래스의 숫자가 늘 수는 있다.
 하지만 클래스 숫자의 증가가 프로그램의 복잡도 증가와 비례하는 것은 아니다.
 오히려 SRP를 잘 따르는 프로그램은 적절한 책임 분배로 인해 클래스 숫자와
 프로그램의 복잡도가 반비례하는 경향이 있다고도 할 수 있게 된다.


====================================================================================================

 

- OCP::개방 폐쇄 원리
 소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 대해서는 개방돼야 하지만
 변경에 대해서는 폐쇄되어야 한다고 말한다.
 변경을 위한 비용은 가능한 줄이고 확장을 위한 비용은 가능한 극대화해야 한다는 의미다.
 목적은 앞의 예처럼 객체간의 관계를 단순화해 복잡도를 줄이고, 확장·변경에 따른 충격을 줄이는 데 있다.

 

주의점::

 OCP에서 주의할 점은 확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려
 관계가 더 복잡해져서 설계를 망치는 경우가 있다는 것이다.
 설계자의 좋은 자질 중 하나는 이런 크기 조절과 같은 갈등 상황을 잘 포착하여
 (아깝지만) 비장한 결단을 내릴 줄 아는 능력에 있다.

 모듈 영역에서 예측하지 못한 확장 타입을 만났을 때 인터페이스 변경하려는 안과 어댑터를 사용하려는

 안 사이에서 갈등하게 된다.
 위의 두 예에서처럼 변경의 충격이 적은 후자를 택하는 경우가 대부분이다.
 한 번 정해진 인터페이스는 시간이 갈수록 사용하는 모듈이 많아지기 때문에 바꾸는

 데 엄청난 출혈을 각오해야 한다.
 자바의 deprecated API가 대표적인 경우다.

 즉, 인터페이스는 가능하면 변경해서는 안 된다.
 따라서 인터페이스를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
 물론 과도한 예측은 불필요한 작업을 만들고 보통, 이 불필요한 작업의 양은 크기 마련이다.
 따라서 설계자는 적절한 수준의 예측 능력이 필요한데, 설계자에게 필요한 또 하나의 자질은 예지력이다.
 

 인터페이스 설계에서 적당한 추상화 레벨을 선택하는 것이 중요하다.
 우리는 추상화라는 개념에 '구체적이지 않은' 정도의 의미로 약간 느슨한 개념을 갖고 있다.
 그래디 부치(Grady Booch)에 의하면 ‘추상화란 다른 모든 종류의 객체로부터 식별될 수 있는
 객체의 본질적인 특징’이라고 정의하고 있다.
 즉, 이 '행위'에 대한 본질적인 정의를 통해 인터페이스를 식별해야 한다.


=====================================================================================================
High Cohesion, Loose Coupling::높은 응집도, 낮은 결합도
=====================================================================================================

 

낮은 응집도를 갖는 구조는 변경이나,확장 단계에서 많은 비용을 지불해야 하며 높은 결합도의 경우도 마찬가지이다.

응집도는 ‘하나의 클래스가 하나의 기능(책임)을 온전히 순도 높게 담당하고 있는 정도’를 의미하며
이들은 서로 조화될수록 그 구조는 단순해진다.

응집도가 높은 동네에서 내부 개체가 변했을 때 다른 개체에 충격을 주는 것은 오히려 당연한 징후이다.
이들은 하나의 책임아래 서로 유기적인 관계를 갖고 있기 때문에 내부 개체가 변했을 때 다른 개체의 변경 확률이 높아진다.
마치 예쁜 부츠를 사면 부츠에 어울리는 치마를 입어야 하듯이…

응집도의 종류는 다양한데 다음은 권장할 만한 순기능적 응집 관계들이다.

 

* 기능적 응집(Functional Cohesion)
일관된 기능들이 집합된 경우를 말하며 <그림 3>의 데이터 맵퍼는 DB 처리라는 기능 항목의 높은 응집성을 갖는다.

 

* 순차적 응집(Sequential Cohesion)
한 클래스 내에 등장하는 하나의 소작업(메쏘드)의 결과가 다음 소작업(메쏘드)의
입력으로 사용되는 관계(파이프라인 방식의 처리 체인 관계).

 

* 교환적 응집(Communicational Cohesion)
동일한 입력과 출력 자료를 제공하는 메쏘드들의 집합을 말하며,
팩토리 클래스는 전형적인 교환적 응집도가 높은 인터페이스를 갖는다.

 

* 절차적 응집(Procedural Cohesion)
순서적으로 처리되어야 하는 소작업(메쏘드)들이 그 순서에 의해 정렬되는 응집관계

 

* 시간적 응집(Temporal Cohesion)
시간의 흐름에 따라 작업 순서가 정렬되는 응집관계

 

* 논리적 응집(Logical Cohesion)
유사한 성격의 개체들이 모여 있을 경우를 말하며 java.io 클래스들의 경우가 대표적인 예이다.

 

이와 반해 결합도는 ‘클래스간의 서로 다른 책임들이 얽혀 있어서 상호의존도가 높은 정도’
를 의미하며 이들이 조합될수록 코드를 보기가 괴로워진다.
이유는 서로 다른 책임이 산만하고 복잡하게 얽혀있기 때문에 가독성이 떨어지고 유지보수가 곤란해지기 때문이다.
이유는 필요 없는 의존성에 있다.
마치 키보드의 자판 하나가 고장나도 키보드 전체를 바꿔야 하는 것처럼.
하나의 변경이 엄청난 민폐를 야기하는 관계이다.


다음은 수용할 수 있는 수준의 결합 관계들이다.

 

* 자료 결합(Data Coupling)
두 개 이상의 클래스가 매개변수에 의해서 결합 관계를 가지므로 낮은 수준의 결합도로 연관되는 경우

 

* 스탬프 결합(Stamp Coupling)
자료 결합의 경우에서 매개변수 일부만을 사용하는 경우

 

* 제어 결합 (Control Coupling)
두 클래스간의 제어 이동이 매개변수를 이용하여 사용되는 경우로 커맨드 패턴이 대표적인 사례이다.


+ Recent posts