코드스피츠 강의 중 객체지향프로그래밍과 디자인패턴에 대한 전반적인 개념 및 철학과 몇몇 디자인패턴들의 특징에 대해서 학습한 내용을 공유하고자 합니다.
코드스피츠 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문을 하고 싶다면?데코레이터,체인오브리스판서빌리티패턴을 활용하여 문들을 제거하고 값으로 바꿀 수 있게 된다.- 절차적인 부분을 감싸서 값으로 만들고 싶다면?
커맨드패턴또는미디에이터 - 여러 개의 객체들을 조립하는 과정들이 있다면?
빌더패턴,파스드패턴
결국엔 문으로 구성하던 것들을 값으로 바꿔서 대체하고 싶은 것이다.
정리…
상태에 대한 분기는 사라지지 않는다. 그 분기가 필요해서 태어났기 때문에~
정의 시점에 제거하는 방법은?- 분기 수 만큼 객체를 만들고
- 실행시점에 경우의 수를 공급한다.
실행시점으로 분기를 옮길 때의 장단점
장점- 정의 시점에 모든 경우를 몰라도 됨
- 정의 시점에 그 경우를 처리하는 방법도 몰라도 됨
- 일정한 통제 범위 내에서 확장 가능한 알고리즘 설계 가능
단점- 실행 시점에 모든 경우를 반드시 기술해야 함
- 설정을 빼먹으면 런타임에 프로그램이 죽게된다.
- 이것에 대해서는 외부에서 더블체크를 해야한다.
- 실행 시점마다 알고리즘의 안정성을 담보해야 함
- 매 호스트코드마다 안정성을 따로 담보해야 함
- 실행 시점에 모든 경우를 반드시 기술해야 함
이러한 장단점을 기반으로 프로그램의 안정도와 확정성에 대한 밸런스를 잡아주는 것이 어렵다.
- 런타임 안정성이 담보되지 않기 때문에 이 단점을 커버하는 다양한 추가 패턴을 사용하게 된다.
- 런타임에 작성해야 할 코드들을 객체로 묶어서 관리하여 안정화 시킨다.
팩토리패턴,빌더패턴 - 패턴이 패턴을 부르게 된다. 패턴이 다른 패턴을 이용해서 자기 패턴의 약점을 보완하지 않으면 약점이 커지고 불안정해진다. 자신의 불안정성을 감추기 위해서 자신의 불안한 파트를 커버하는 커운터 패턴을 다시 사용하게 된다.
- 이 의미를 이해하지 못하면 패턴을 그냥 번복만 할 뿐이지 안정성이 높아지지 않는다.
- 런타임에 작성해야 할 코드들을 객체로 묶어서 관리하여 안정화 시킨다.
어떤 패턴을 쓸 때 어떤 패턴으로 카운터를 걸어서 안정성을 높여야 하는지 이해해야 한다.