코드스피츠 강의 중 객체지향프로그래밍과 디자인패턴에 대한 전반적인 개념 및 철학과 몇몇 디자인패턴들의 특징에 대해서 학습한 내용을 공유하고자 합니다.
코드스피츠 3rd-4 ES6+ 디자인패턴과 뷰패턴 2회차를 직접 보신 후에 복습용으로 이 글을 읽어 보시길 추천합니다.
객체지향 프로그래밍을 구현하기 위한 두가지 속성
이 두가지 성질을 가져야지만 객체지향 프로그래밍이 가능한 언어라고 할 수 있습니다. 이 성질은 언어마다 매커니즘이 다릅니다.
1. 대체가능성
- 폴리모피즘
- 하위형은 상위형(추상형)을 대신 할 수 있다.
- 확장된형은 확장이전형을 대체 할 수 있다
- 자식은 부모를 대신 할 수 있다.
1 | const Parent = class {}; |
- 이 구조는 몇단계로 확장 되어도 그 윗단계는 대체 할 수 있다는 뜻
- 객체지향언어는 이것을 만족하는 장치가 있어야 한다.
- 자바스크립트에서는 프로토타입 체인에 의해서
__proto__
가 기록되어 있는 객체 간의 체인으로 그 체인을 다 조사해서 그 중에 한 객체라도 걸리면instanceof
가 성립하는 결과를 만족시키고 있다.
2. 내적일관성(내적동질성)
1 | const Parent = class { |
- 내적동질성 이란
- 아무리 확장되기 전 객체들의 메소드나 다른 객체들의 메소드 계층 상에 있는 메소드를 호출해도 본래 객체의 본질은 변하지 않는다는 의미…?? 나의 본질은 Child이기 때문에 Parent로 보려고 해도 나의 본질은 Child라는 의미…
- 태생을 그대로 유지하려는 성질
대체가능성과, 내적동질성 이 두가지 성질이 객체지향의 학술적 정의이고 언어에서 어떤 방법으로든 지원을 해야지만 객체지향 언어라 할 수 있다.
디자인패턴
GOF의 디자인패턴 분류 - 생성패턴, 구조패턴, 행동패턴… 이런건 객체지향 설계가 익숙해진 후에 공부하는 걸로…
객체지향 설계를 위한 다양한 원리들이 있지만 … 격리를 통한 역할모델을 구현하는 것에 대해서 집중적으로 보려고 합니다.
디자인패턴을 역할모델을 기준으로 분류해서 어떤 방식으로 무슨 역할을 분리하기 위해 무슨 패턴을 쓰는지~ 라는 관점으로 학습해보고자 합니다.
디자인패턴과 제어문 기반의 알고리즘이 갖는 문제점
- 기존 제어문 기반으로 알고리즘을 짜면 코드 한줄만 수정을 해도 영향이 가는 모든 코드를 테스트 해봐야 한다. 코드를 건드리는것 자체가 엄청난 일이 된다.
알고리즘이 변화하는 부분만 수정하고 나머지는 건드리고 싶지 않다면?
1
2
3if(case == 1) { ... }
else if(case == 2) { ... }
else if(case == 3) { ... }코드를 안건드리고 어떻게 if를 조정하지? 이 질문이 아키텍쳐 이하 모든 디자인패턴의 원형적인 목표이다.
- 문제1. 경우가 변경될 때
- 문제2. 함수 간 공통부분
- 프로시져 지향, 공통 의존 데이터 문제, 상태에 대한 의존성이 강해지기 때문에 복잡해질수록 프로시져를 나눌수록 더욱더 원본 데이터의 수정이 불가능해진다.
- 절차지향적으로 어딘가 데이터의 바인딩이 많이 되게 개발을 하게 되면 변화를 수용할 수 없게 되고 데이터의 구조나 목표가 조금만 바뀌어도 프로그램 전체를 다시 개발하게 된다.
이 문제를 해결하기 위해 객체지향이 나온것이다.
- 프로시져가 가리키는 데이터를 은닉하고 캡슐화하여 노출해야지? 데이터를 쉐어하는 것이 문제의 원인이니까 원천적으로 데이터를 공유하지 못하도록 하겠다는 것이 객체지향이다.
- 문제는 절차가 아니라 절차들이 공유하고 있는 데이터인것이다.
- 이를 해결하기 위해서 데이터를 절차와 강력하게 바인딩하여 은닉시키고, 표준화된 메세지로만 통신하여 협력하는 모델로 바꿔 나간다.
- 데이터에 대한 서로간의 의존성이 없어지기 때문에 우리는 부분적으로 격리를 시킬수 있게 되는 것이 객체지향의 시발점이다.
알고리즘 분화 시 객체지향에서 선택할 수 있는 두가지 방법
상속위임 - 내부계약관계로 추상층에서 공통 요소를 해결하고 상태를 공유 할 수 있음. 공통 부분을 처리하기 위해 사용한다. 나는 너를 알지의 개념. 확장된 쪽이 부모쪽을 아는 것. 상속위임은 내부사정으로 약속이 확정되어 있다. 객체간 내부사정을 다 안다고 하면 통신을 따로 약속하지 않고 훨씬 더 안전하게 통신 할 수 있다는 장점이 있다.
소유위임 - 외부계약관계로 각각이 독립적인 문제를 해결하며 메세지를 주고 받는 것으로 문제를 해결함. 내 할일하고 나머지는 맡기는 방법. 소유위임은 분리된 객체끼리 통신할 통신망을 확보해야만 한다. 독립되어 있는 객체간의 약속이 필요하다. 객체망을 구성한다고도 함.
- 어찌됬든 소유와 상속을 사용하는 이유는 케이스에 대응하기 위해서와 케이스에 대한 처리를 확장하기 위해서이다.
- 디자인패턴은 소유위임을 위주로 설명한다.
- 디자인패턴을 사용하면 여러가지 프로토콜, 객체 간의 통신하는 층을 만들어 줘야 한다. 부속물이 많이 생기게 된다. 더 넗게 더 관계 없는 것과도 통신할 수 있게 하고 싶은 것.
- 연결해야 하는 대상이 많으면 많을수록 소유위임이 훨씬 유리해진다.
- 소유위임은 케이스의 확장, 처리기의 확장을 더 멀리 넓게 퍼뜨릴 수 있지만 처리속도는 낮아지고 더 많은 부하를 걸게 될 수도 있다.
- 상속위임은 그 반대가 일어난다.
- 속도와 메모리는 바꿀 수 있다.
- 상속위임은 속도나 효율이 좋아지고, 소유위임은 메모리를 많이 사용하는 방식이다.
- 소유위임은 더 많은 경우의 수를 처리 할 수 있는 범용적인 솔루션으로써의 패턴을 제안하는것이다.
1. 상속위임
1 | /////// TEMPLATE METHOD PATTERN ////// |
- 위 코드는 상속위임을 위한 기본 골격이 되는 코드이다.
공통부분
은 이미지를 로딩하던지 텍스트를 로딩하던지 이 로직은 변하지 않는 부분이다.위임부분
이 변하는 부분이고,if
케이스 별로 바뀌어야 하는 부분이다.위임부분
을 내적동질성을 이용해서 서브클래스에게 위임하는 것이다.- 자식 별로 다르게 처리해야 하는 부분만 처리 할 수 있도록 하는 것이다. 코드를 격리 할 수 있다.
ImageLoader
는Github
이라는 것을 상속 받아서_loaded
만 오버라이드해서 작성하면 이미지를 로드하는 동작을 하게 될것입니다.- 정의시점 부분을 건드리지 않고도 다양한 경우를 새로운 클래스만 추가해서 만들 수 있게 된다.
1 | const ImageLoader = class extends Github { |
1 | const MdLoader = class extends Github { |
- 이처럼 상속구현으로
if
의 분기를 해결할 수 있다. image일 때, md일 때…
1 | <!-- 정의 시점 - start --> |
if else
는 정의시점에서 확정되었었다. 지금 구조에서는 파일을 추가 할 때 마다 케이스를 추가 할 수 있다.- 뿐만 아니라 실행시점에서 사용될 때 코드에서 경우의 수의 분기를 담당하게 된다. 정의시점에 분기하던 것을 실행시점으로 밀어낸 것이다.
if else
를 제거하는 유일한 방법은 케이스 만큼의 객체를 만든 다음에 그 객체의 선택을 런타임으로 위임해버리는 수 밖에 없다라고 표현하는 것이다.- 기저층에서
if else
를 제거한 것 뿐이지 절대로 사라지지는 않는다. - OOP는 결국
if else
를 어떻게 제거하고 밑으로 밀어버릴까, 런타임에 결정 할 수 있는 권한으로 바꿔줄까, 되도록이면 정의시점에서 한단계라도 더 밑으로 내릴까?- 하위 레이어로 내릴 때 마다 비용은 발생 할 수 밖에 없다. 케이스 만큼 객체를 만들어야 하고 결국 다음번 런타임에 선택하는 로직으로 옮겨질 뿐이다. 어떤 패턴을 써도 이 본질은 변하지 않는다.
2. 소유위임
1 | ////// STARATEGY PATTERN ////// |
- 상속위임 코드와 비슷해 보이지만 소유를 나타내는 메소드가 추가되었다.
set parser(...)
로_parser
를 받아들이게 되어 있다. 상속계열의 this 메소드를 호출하는것이 아니라 소유하게 된 외뷔객체인_parser
에게 위임을 하게 되는것이다.- 소유위임을 하게 되면 내가 소유할 객체들을 원본 객체에게 전달할 책임이 생기게 된다. 이것을
injection
주입이라고 한다. 위임부분
을위임객체
가 처리해준다.override
나 내부의hook
을 이용한템플릿메소드패턴
을 소유모델로 바꾸게 되면전략패턴
으로 바뀌게 된다. 그래서템플릿메소드패턴
과전략패턴
은 형제 관계이다. 같은 내용을 상속위임으로 구현할 것인지 소유위임으로 구현할 것인지의 차이이다.템플릿메소드패턴
는 내적일관성 원리를 이용하여 상속을 통해서 상속된 인스턴스가 처리를 했었는데전략패턴
은 소유객체에게 전달하면 되니까 내적일관성을 사용하지 않고 있다.- 이렇게 소유위임은 객체지향의 개념을 덜 사용하고도 같은 기능을 처리할 수 있게 된다.
정의시점
이Github
클래스 자체가 되는 것이고, 더이상정의시점
에 서브 클래스는 안나오게 되고, 받아온 객체를실행시점
에 받아서 처리하게 된다. 받아온 객체 자체가 런타임 객체가 된다.
1 | const el = v => document.querySelector(v); |
- 새로운 객체를 만들지 않고
parser
만 변경하여서 바로 실행시킬수 있다. 이 부분이템플릿메소드패턴
과전략패턴
의 가장 큰 차이점이다. 템플릿메소드패턴
은 이미지로더를 사용하려면 이미지로더 기능만 하는 단일적인 책임을 가지는 인스턴스를 생성해줘야 하지만전략패턴
은 이미 정의된 객체를 가지고 몇 번이고 재활용 할 수 있다.mutation
이 일어난다고 한다.mutation
은 개발자가 흐름을 기억해야 할 부분이 많이 생긴다.
3. 커맨드패턴
1 | ////// COMMAND PATTERN ////// |
전략패턴
객체는 정해진 행동을 계속 일으키는 인스턴스이거나 함수인데커맨드패턴
은 특정 호출 상태를 박제해서 몇번이고 재현할 수 있는 패턴을 의미한다.- 그러면
전략패턴
에서 나온 객체가커맨드패턴
이 되려면 단지 인자만 기억해줘도 충분하다. - 우리가 함수 호출이라는 것을 문으로 적으면 문에서는 실행되고 휘발되서 아무것도 안남는다. 문은 인터프리터가 실행될 때 쓰이고 다 없어진다. 함수를 콜하는것도 문으로 구성하면 문은 호출하고나면 사라지기 때문에 호출을 재현할 수가 없다.
- 몇번이고 같은 형태로 호출을 하려면 함수와 인자1, 2, 3이 저장되 있어야지만 계속 호출 할 수 있는 상황을 재현할 수 있다.
- 어떤 문이 실행되는 환경을 저장하는 행위, 어떤 문이 실행되는 커맨드를 저장해두는 것을
커맨드패턴
이라고 한다. 전략패턴
과 달라진 점은 parser 함수가 그냥 실행 되는 것이 아니라 인자를 포함해서 실행된다는 환경까지 저장해서 몇번이고 다시 재현 할 수 있게 바꿔주면 그것이커맨드패턴
의 철학을 따르게 되는 것이다.전략패턴
객체와커맨드패턴
객체가 달라지것이 아니라전략패턴
객체를커맨드패턴
객체화시켜 버린 것이다.커맨드패턴
은 다음번에 똑같은 제어문을 재현 할 수 있는 환경을 저장해두는커맨드객체
와 이 저장된 것을 실행하는실행기
로 나누어진다.
이실행기
를커맨드패턴
에서는인보커
라고 부른다. 디자인패턴에서인보커
라고 하면 뭔가 실행시켜주는 러너라고 생각하면 된다.
1 | const el = v => document.querySelector(v); |
- 클래스로 정의하면 코드화되고 코드를 건드려야 하는데 그에 비해서
커맨드객체
가 보다 유연하기 때문에 인자를 여러개 보내거나 함수를 다양하게 보낼 수 있어서 그 모든 경우의 수를 클래스로 지정하지 않아도 사용 할 수 있게 되는 것이다. - 상태를 통합으로 관리해주는
커맨드객체
와 실행기인인보커
함수를 저장하는 방식으로 만들었기 때문에 다양한 함수와 인자가 몇개씩 있는 여러 가지 조합들도 다 대응 할 수 있게 된다.
4. 실행시점의 선택 위임
- 바로 위 코드에서
커맨드패턴
을 사용하더라도 img나 md를 사용하는 상황에 따른 분기를 처리해야 됨이 제거 된것은 아니다. - 이를 해결하기 위해서 우회적인 방법을 사용하게 된다. 모든 경우의 수를 알 수 있다면 경우의 수의 연산은 값 테이블로 바꿀 수 있다. 백가지 경우라면 백가지 경우를 모두 테이블에 넣어 놓고 키를 넣으면 값이 나오는 형태로 바꿀 수 있다. 이를
라우터
라고 한다. 라우터
는 필연적으로라우터테이블
을 데리고 다니는데 이는 선택이나 연산을 제거하고 경우의 수를 넣음으로써 대응하는 결과가 나오도록 만들어주고, 이 테이블을 이용해서 내가 원하는 결과를 출력해 주는 매칭기가라우터
이다.라우터
는 호스트의 if의 분기는 제거 할 수 없으니까 테이블 형태로 바꿔서 관리를 편하게 해주는 차선책을 선택한 것이다.
1 | const Loader = class { |
Loader
를 통해 if문 처리를 값만 계속 추가해서 처리 할 수 있는 형태로 변환하였다. 케이스가 늘어나도 if else 를 늘릴 필요 없이 값만 추가하면 되는 방식이다.if문
이 값으로 변환된 것이다. 디자인패턴의 원리 중 하나는 문을 제거해서 값으로 바꿀 수 있는 객체화 시키는 것이다. 값으로 바꿔야지만 코드를 고치지 않고 확장이 가능하다. 문이 되면 우리가 타이핑을 쳐서 저장해야만 한다.- 이를 활용해서 우리는 더 많은 확장자를 지원하도록 코드를 지연시켜서 작성 할 수도 있고, 업그레이드를 할 수도 있고, 플러그인을 추가 할 수도 있고, 개발자의 코드를 안전하게 위임 할 수 있게 된다. 이로써 진짜 병렬적인 업무를 가능하게 해준다.
1 | const loader = new Loader('hanwong', 'codespitz'); |
- 결론은 실행시점의 선택 위임을 통해서 발생가능한 경우의 수를 값으로 기술하게 하는 것이다.
소유위임
을 통해서 코드의 분산을 정적으로 정의하지 않고 경우의 수에 따라 동적으로 정의 할 수 있는 발판을 마련했다면,if문
에 대한 분기는 문을 계속해서 값으로 바꿔주는 것으로써 대체한다.- 디자인패턴 중
if문
을 제거하기 위한 패턴으로템플릿메소드패턴
,전략패턴
, 더 나아가서상태패턴
이라는 것이 있다.for문
을 제거하기 위한 패턴으로는이터레이터
,컴포지트
,비지터
패턴이 있다.for문
을 돌다가if문
을 하고 싶다면?데코레이터
,체인오브리스판서빌리티
패턴을 활용하여 문들을 제거하고 값으로 바꿀 수 있게 된다.- 절차적인 부분을 감싸서 값으로 만들고 싶다면?
커맨드패턴
또는미디에이터
- 여러 개의 객체들을 조립하는 과정들이 있다면?
빌더패턴
,파스드패턴
결국엔 문으로 구성하던 것들을 값으로 바꿔서 대체하고 싶은 것이다.
정리…
상태에 대한 분기는 사라지지 않는다. 그 분기가 필요해서 태어났기 때문에~
정의 시점에 제거하는 방법은?
- 분기 수 만큼 객체를 만들고
- 실행시점에 경우의 수를 공급한다.
실행시점으로 분기를 옮길 때의 장단점
장점
- 정의 시점에 모든 경우를 몰라도 됨
- 정의 시점에 그 경우를 처리하는 방법도 몰라도 됨
- 일정한 통제 범위 내에서 확장 가능한 알고리즘 설계 가능
단점
- 실행 시점에 모든 경우를 반드시 기술해야 함
- 설정을 빼먹으면 런타임에 프로그램이 죽게된다.
- 이것에 대해서는 외부에서 더블체크를 해야한다.
- 실행 시점마다 알고리즘의 안정성을 담보해야 함
- 매 호스트코드마다 안정성을 따로 담보해야 함
- 실행 시점에 모든 경우를 반드시 기술해야 함
이러한 장단점을 기반으로 프로그램의 안정도와 확정성에 대한 밸런스를 잡아주는 것이 어렵다.
- 런타임 안정성이 담보되지 않기 때문에 이 단점을 커버하는 다양한 추가 패턴을 사용하게 된다.
- 런타임에 작성해야 할 코드들을 객체로 묶어서 관리하여 안정화 시킨다.
팩토리패턴
,빌더패턴
- 패턴이 패턴을 부르게 된다. 패턴이 다른 패턴을 이용해서 자기 패턴의 약점을 보완하지 않으면 약점이 커지고 불안정해진다. 자신의 불안정성을 감추기 위해서 자신의 불안한 파트를 커버하는 커운터 패턴을 다시 사용하게 된다.
- 이 의미를 이해하지 못하면 패턴을 그냥 번복만 할 뿐이지 안정성이 높아지지 않는다.
- 런타임에 작성해야 할 코드들을 객체로 묶어서 관리하여 안정화 시킨다.
어떤 패턴을 쓸 때 어떤 패턴으로 카운터를 걸어서 안정성을 높여야 하는지 이해해야 한다.