코드스피츠 - 객체지향프로그래밍과 디자인패턴

코드스피츠 강의 중 객체지향프로그래밍과 디자인패턴에 대한 전반적인 개념 및 철학과 몇몇 디자인패턴들의 특징에 대해서 학습한 내용을 공유하고자 합니다.

코드스피츠 3rd-4 ES6+ 디자인패턴과 뷰패턴 2회차를 직접 보신 후에 복습용으로 이 글을 읽어 보시길 추천합니다.


객체지향 프로그래밍을 구현하기 위한 두가지 속성

이 두가지 성질을 가져야지만 객체지향 프로그래밍이 가능한 언어라고 할 수 있습니다. 이 성질은 언어마다 매커니즘이 다릅니다.

1. 대체가능성

  • 폴리모피즘
  • 하위형은 상위형(추상형)을 대신 할 수 있다.
  • 확장된형은 확장이전형을 대체 할 수 있다
  • 자식은 부모를 대신 할 수 있다.
1
2
3
4
5
const Parent = class {};
const Child = class extends Parent{};
const a = new Child();
console.log(a instanceof Parent); // true
// Child는 Parent를 대신 할 수 있어! 를 의미하는 코드입니다.
  • 이 구조는 몇단계로 확장 되어도 그 윗단계는 대체 할 수 있다는 뜻
    • 객체지향언어는 이것을 만족하는 장치가 있어야 한다.
    • 자바스크립트에서는 프로토타입 체인에 의해서 __proto__가 기록되어 있는 객체 간의 체인으로 그 체인을 다 조사해서 그 중에 한 객체라도 걸리면 instanceof가 성립하는 결과를 만족시키고 있다.

2. 내적일관성(내적동질성)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Parent = class {
wrap() {
this.action();
}
action() {
console.log('Parent');
}
};
const Child = class extends Parent{
action() {
console.log('Child');
}
};

const a = new Child();
a.wrap(); // Child
  • 내적동질성 이란
    • 아무리 확장되기 전 객체들의 메소드나 다른 객체들의 메소드 계층 상에 있는 메소드를 호출해도 본래 객체의 본질은 변하지 않는다는 의미…?? 나의 본질은 Child이기 때문에 Parent로 보려고 해도 나의 본질은 Child라는 의미…
    • 태생을 그대로 유지하려는 성질

대체가능성과, 내적동질성 이 두가지 성질이 객체지향의 학술적 정의이고 언어에서 어떤 방법으로든 지원을 해야지만 객체지향 언어라 할 수 있다.


디자인패턴

GOF의 디자인패턴 분류 - 생성패턴, 구조패턴, 행동패턴… 이런건 객체지향 설계가 익숙해진 후에 공부하는 걸로…

객체지향 설계를 위한 다양한 원리들이 있지만 … 격리를 통한 역할모델을 구현하는 것에 대해서 집중적으로 보려고 합니다.

디자인패턴을 역할모델을 기준으로 분류해서 어떤 방식으로 무슨 역할을 분리하기 위해 무슨 패턴을 쓰는지~ 라는 관점으로 학습해보고자 합니다.

디자인패턴과 제어문 기반의 알고리즘이 갖는 문제점

  • 기존 제어문 기반으로 알고리즘을 짜면 코드 한줄만 수정을 해도 영향이 가는 모든 코드를 테스트 해봐야 한다. 코드를 건드리는것 자체가 엄청난 일이 된다.
  • 알고리즘이 변화하는 부분만 수정하고 나머지는 건드리고 싶지 않다면?

    1
    2
    3
    if(case == 1) { ... }
    else if(case == 2) { ... }
    else if(case == 3) { ... }
  • 코드를 안건드리고 어떻게 if를 조정하지? 이 질문이 아키텍쳐 이하 모든 디자인패턴의 원형적인 목표이다.

    • 문제1. 경우가 변경될 때
    • 문제2. 함수 간 공통부분
    • 프로시져 지향, 공통 의존 데이터 문제, 상태에 대한 의존성이 강해지기 때문에 복잡해질수록 프로시져를 나눌수록 더욱더 원본 데이터의 수정이 불가능해진다.
    • 절차지향적으로 어딘가 데이터의 바인딩이 많이 되게 개발을 하게 되면 변화를 수용할 수 없게 되고 데이터의 구조나 목표가 조금만 바뀌어도 프로그램 전체를 다시 개발하게 된다.
  • 이 문제를 해결하기 위해 객체지향이 나온것이다.

    • 프로시져가 가리키는 데이터를 은닉하고 캡슐화하여 노출해야지? 데이터를 쉐어하는 것이 문제의 원인이니까 원천적으로 데이터를 공유하지 못하도록 하겠다는 것이 객체지향이다.
    • 문제는 절차가 아니라 절차들이 공유하고 있는 데이터인것이다.
    • 이를 해결하기 위해서 데이터를 절차와 강력하게 바인딩하여 은닉시키고, 표준화된 메세지로만 통신하여 협력하는 모델로 바꿔 나간다.
      • 데이터에 대한 서로간의 의존성이 없어지기 때문에 우리는 부분적으로 격리를 시킬수 있게 되는 것이 객체지향의 시발점이다.

알고리즘 분화 시 객체지향에서 선택할 수 있는 두가지 방법

상속위임 - 내부계약관계로 추상층에서 공통 요소를 해결하고 상태를 공유 할 수 있음. 공통 부분을 처리하기 위해 사용한다. 나는 너를 알지의 개념. 확장된 쪽이 부모쪽을 아는 것. 상속위임은 내부사정으로 약속이 확정되어 있다. 객체간 내부사정을 다 안다고 하면 통신을 따로 약속하지 않고 훨씬 더 안전하게 통신 할 수 있다는 장점이 있다.

소유위임 - 외부계약관계로 각각이 독립적인 문제를 해결하며 메세지를 주고 받는 것으로 문제를 해결함. 내 할일하고 나머지는 맡기는 방법. 소유위임은 분리된 객체끼리 통신할 통신망을 확보해야만 한다. 독립되어 있는 객체간의 약속이 필요하다. 객체망을 구성한다고도 함.

  • 어찌됬든 소유와 상속을 사용하는 이유는 케이스에 대응하기 위해서와 케이스에 대한 처리를 확장하기 위해서이다.
  • 디자인패턴은 소유위임을 위주로 설명한다.
    • 디자인패턴을 사용하면 여러가지 프로토콜, 객체 간의 통신하는 층을 만들어 줘야 한다. 부속물이 많이 생기게 된다. 더 넗게 더 관계 없는 것과도 통신할 수 있게 하고 싶은 것.
    • 연결해야 하는 대상이 많으면 많을수록 소유위임이 훨씬 유리해진다.
    • 소유위임은 케이스의 확장, 처리기의 확장을 더 멀리 넓게 퍼뜨릴 수 있지만 처리속도는 낮아지고 더 많은 부하를 걸게 될 수도 있다.
    • 상속위임은 그 반대가 일어난다.
    • 속도와 메모리는 바꿀 수 있다.
    • 상속위임은 속도나 효율이 좋아지고, 소유위임은 메모리를 많이 사용하는 방식이다.
    • 소유위임은 더 많은 경우의 수를 처리 할 수 있는 범용적인 솔루션으로써의 패턴을 제안하는것이다.

1. 상속위임

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/////// TEMPLATE METHOD PATTERN //////
const Github = class { ////// 정의시점 - 변하지 않는 부분 //////
constructor(id, repo) {
this._base = `https://api.github.com/repos/${id}/${repo}/contents/`;
}
load(path) {
/////////////////// 공통부부분 - start ///////////////////
const id = 'callback' + Github._id++;
const f = Github[id] = ({data: {content}}) => {
delete Github[id];
document.head.removeChild(s);
/////// 위임부분 - start ///////
this._loaded(content);
/////// 위임부분 - end ///////
};
const s = document.createElement('script');
s.src = `${this._base + path}?callback=Github.${id}`;
document.head.appendChild(s);
/////////////////// 공통부부분 - end ///////////////////
}
_loaded(v){throw 'override!'}; /////// HOOK //////
};

Github._id = 0;

const ImageLoader = class extends Github { ////// 실행시점 - 변하는 부분 //////
_loaded(v) {...}
};
  • 위 코드는 상속위임을 위한 기본 골격이 되는 코드이다.
  • 공통부분은 이미지를 로딩하던지 텍스트를 로딩하던지 이 로직은 변하지 않는 부분이다.
  • 위임부분이 변하는 부분이고, if 케이스 별로 바뀌어야 하는 부분이다.
  • 위임부분을 내적동질성을 이용해서 서브클래스에게 위임하는 것이다.
  • 자식 별로 다르게 처리해야 하는 부분만 처리 할 수 있도록 하는 것이다. 코드를 격리 할 수 있다.
  • ImageLoaderGithub이라는 것을 상속 받아서 _loaded만 오버라이드해서 작성하면 이미지를 로드하는 동작을 하게 될것입니다.
  • 정의시점 부분을 건드리지 않고도 다양한 경우를 새로운 클래스만 추가해서 만들 수 있게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const ImageLoader = class extends Github {
constructor(id, repo, target) {
super(id, repo);
this._target = target;
}
_loaded(v) {
this._target.src = 'data:text/plain;base64,' + v;
}
};

const woongImg = new ImageLoader(
'hanwong',
'codespitz',
document.querySelector('#a')
)
woomgImg.load('test.png');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const MdLoader = class extends Github {
constructor(id, repo, target) {
super(id, repo);
this._target = targe;
}
_loaded(v) {
this._target.innerHTML = this._parseMD(v);
}
_parseMD(v) {
return d64(v).split('\n').map(v => {
let i = 3;
while(i--) {
if(v.startsWith('#'.repeat(i+1))) {
return `<h${i+1}>${v.substr(i+1)}</h${i+1}>`;
}
}
return v;
}).join('<br>');
}
}
const d64 = v => decodeURIComponent(
atob(v).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
);

const woongMd = new MdLoader('hanwong', 'codespitz'. document.querySelector('#b'));
woongMd.load('README.md');
  • 이처럼 상속구현으로 if의 분기를 해결할 수 있다. image일 때, md일 때…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 정의 시점 - start -->
<script src="Github.js"></script>
<script src="ImageLoader.js"></script>
<script src="MdLoader.js"></script>
<!-- 정의 시점 - end -->
<script>
/////// 실행 시점 선택 - start ///////
const img = new ImageLoader(...);
img.load(...);

const md = new MdLoader(...);
md.load(...);
/////// 실행 시점 선택 - end ///////
</script>
  • if else 는 정의시점에서 확정되었었다. 지금 구조에서는 파일을 추가 할 때 마다 케이스를 추가 할 수 있다.
  • 뿐만 아니라 실행시점에서 사용될 때 코드에서 경우의 수의 분기를 담당하게 된다. 정의시점에 분기하던 것을 실행시점으로 밀어낸 것이다.
  • if else 를 제거하는 유일한 방법은 케이스 만큼의 객체를 만든 다음에 그 객체의 선택을 런타임으로 위임해버리는 수 밖에 없다라고 표현하는 것이다.
  • 기저층에서 if else를 제거한 것 뿐이지 절대로 사라지지는 않는다.
  • OOP는 결국 if else를 어떻게 제거하고 밑으로 밀어버릴까, 런타임에 결정 할 수 있는 권한으로 바꿔줄까, 되도록이면 정의시점에서 한단계라도 더 밑으로 내릴까?
    • 하위 레이어로 내릴 때 마다 비용은 발생 할 수 밖에 없다. 케이스 만큼 객체를 만들어야 하고 결국 다음번 런타임에 선택하는 로직으로 옮겨질 뿐이다. 어떤 패턴을 써도 이 본질은 변하지 않는다.

2. 소유위임

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
////// STARATEGY PATTERN //////
const Github = class {
constructor(id, repo) {
this._base = `https://api.github.com/repos/${id}/${repo}/contents/`;
}
load(path) {
if(!this._parser) return;
const id = 'callback' + Github._id++;
const f = Github[id] = ({data: {content}}) => {
delete Github[id];
document.head.removeChild(s);
this._parser(content); ////// 위임부분 //////
};
const s = document.createElement('script');
s.src = `${this._base + path}?callback=Github.${id}`;
document.head.appendChild(s);
}
////// STARATEGY OBJECT //////
set parser(v){this._parser = v;}; ////// 위임객체 //////
}

Github._id = 0;
  • 상속위임 코드와 비슷해 보이지만 소유를 나타내는 메소드가 추가되었다.
  • set parser(...)_parser를 받아들이게 되어 있다. 상속계열의 this 메소드를 호출하는것이 아니라 소유하게 된 외뷔객체인 _parser에게 위임을 하게 되는것이다.
  • 소유위임을 하게 되면 내가 소유할 객체들을 원본 객체에게 전달할 책임이 생기게 된다. 이것을 injection 주입이라고 한다.
  • 위임부분위임객체가 처리해준다.
  • override나 내부의 hook을 이용한 템플릿메소드패턴을 소유모델로 바꾸게 되면 전략패턴으로 바뀌게 된다. 그래서 템플릿메소드패턴전략패턴은 형제 관계이다. 같은 내용을 상속위임으로 구현할 것인지 소유위임으로 구현할 것인지의 차이이다.
  • 템플릿메소드패턴는 내적일관성 원리를 이용하여 상속을 통해서 상속된 인스턴스가 처리를 했었는데 전략패턴은 소유객체에게 전달하면 되니까 내적일관성을 사용하지 않고 있다.
  • 이렇게 소유위임은 객체지향의 개념을 덜 사용하고도 같은 기능을 처리할 수 있게 된다.
  • 정의시점Github클래스 자체가 되는 것이고, 더이상 정의시점에 서브 클래스는 안나오게 되고, 받아온 객체를 실행시점에 받아서 처리하게 된다. 받아온 객체 자체가 런타임 객체가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
const el = v => document.querySelector(v);
const parseMD = v => ... ;
const loader = new Github('hanwong', 'codespitz');

// imgLoader
const img = v => el('#a').src = 'data:text/plain;base64,' + v;
loader.parser = img;
loader.load('test.png');

// mdLoader
const md = v => el('#b').innerHTML = parserMD(v);
loader.parser = md;
loader.load('README.md');
  • 새로운 객체를 만들지 않고 parser만 변경하여서 바로 실행시킬수 있다. 이 부분이 템플릿메소드패턴전략패턴의 가장 큰 차이점이다.
  • 템플릿메소드패턴은 이미지로더를 사용하려면 이미지로더 기능만 하는 단일적인 책임을 가지는 인스턴스를 생성해줘야 하지만 전략패턴은 이미 정의된 객체를 가지고 몇 번이고 재활용 할 수 있다. mutation이 일어난다고 한다.
  • mutation은 개발자가 흐름을 기억해야 할 부분이 많이 생긴다.

3. 커맨드패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
////// COMMAND PATTERN //////
const Github = class {
constructor(id, repo) {
this._base = `https://api.github.com/repos/${id}/${repo}/contents/`;
}
load(path) {
if(!this._parser) return;
const id = 'callback' + Github._id++;
const f = Github[id] = ({data: {content}}) => {
delete Github[id];
document.head.removeChild(s);
this._parser[0](content, ...this._parser[1]); ////// 커맨드 인보커 //////
};
const s = document.createElement('script');
s.src = `${this._base + path}?callback=Github.${id}`;
document.head.appendChild(s);
}
setParser(f, ...arg){
this._parser = [f, arg]; ////// 커맨드 객체화 //////
}

Github._id = 0;
  • 전략패턴 객체는 정해진 행동을 계속 일으키는 인스턴스이거나 함수인데 커맨드패턴은 특정 호출 상태를 박제해서 몇번이고 재현할 수 있는 패턴을 의미한다.
  • 그러면 전략패턴에서 나온 객체가 커맨드패턴이 되려면 단지 인자만 기억해줘도 충분하다.
  • 우리가 함수 호출이라는 것을 문으로 적으면 문에서는 실행되고 휘발되서 아무것도 안남는다. 문은 인터프리터가 실행될 때 쓰이고 다 없어진다. 함수를 콜하는것도 문으로 구성하면 문은 호출하고나면 사라지기 때문에 호출을 재현할 수가 없다.
  • 몇번이고 같은 형태로 호출을 하려면 함수와 인자1, 2, 3이 저장되 있어야지만 계속 호출 할 수 있는 상황을 재현할 수 있다.
  • 어떤 문이 실행되는 환경을 저장하는 행위, 어떤 문이 실행되는 커맨드를 저장해두는 것을 커맨드패턴이라고 한다.
  • 전략패턴과 달라진 점은 parser 함수가 그냥 실행 되는 것이 아니라 인자를 포함해서 실행된다는 환경까지 저장해서 몇번이고 다시 재현 할 수 있게 바꿔주면 그것이 커맨드패턴의 철학을 따르게 되는 것이다.
  • 전략패턴 객체와 커맨드패턴 객체가 달라지것이 아니라 전략패턴객체를 커맨드패턴객체화시켜 버린 것이다.

  • 커맨드패턴은 다음번에 똑같은 제어문을 재현 할 수 있는 환경을 저장해두는 커맨드객체와 이 저장된 것을 실행하는 실행기로 나누어진다.
    실행기커맨드패턴에서는 인보커라고 부른다. 디자인패턴에서 인보커라고 하면 뭔가 실행시켜주는 러너라고 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const el = v => document.querySelector(v);
const parseMD = v => ... ;
const loader = new Github('hanwong', 'codespitz');

// imgLoader
const img = (v, el) => el.src = 'data:text/plain;base64,' + v;
loader.setParser(img, el('#a'));
loader.load('test.png');

// mdLoader
const md = (v, el) => el.innerHTML = parserMD(v);
loader.setParser(md, el('#b'));
loader.load('README.md');
  • 클래스로 정의하면 코드화되고 코드를 건드려야 하는데 그에 비해서 커맨드객체가 보다 유연하기 때문에 인자를 여러개 보내거나 함수를 다양하게 보낼 수 있어서 그 모든 경우의 수를 클래스로 지정하지 않아도 사용 할 수 있게 되는 것이다.
  • 상태를 통합으로 관리해주는 커맨드객체와 실행기인 인보커함수를 저장하는 방식으로 만들었기 때문에 다양한 함수와 인자가 몇개씩 있는 여러 가지 조합들도 다 대응 할 수 있게 된다.

4. 실행시점의 선택 위임

  • 바로 위 코드에서 커맨드패턴을 사용하더라도 img나 md를 사용하는 상황에 따른 분기를 처리해야 됨이 제거 된것은 아니다.
  • 이를 해결하기 위해서 우회적인 방법을 사용하게 된다. 모든 경우의 수를 알 수 있다면 경우의 수의 연산은 값 테이블로 바꿀 수 있다. 백가지 경우라면 백가지 경우를 모두 테이블에 넣어 놓고 키를 넣으면 값이 나오는 형태로 바꿀 수 있다. 이를 라우터라고 한다.
  • 라우터는 필연적으로 라우터테이블을 데리고 다니는데 이는 선택이나 연산을 제거하고 경우의 수를 넣음으로써 대응하는 결과가 나오도록 만들어주고, 이 테이블을 이용해서 내가 원하는 결과를 출력해 주는 매칭기가 라우터 이다.
  • 라우터는 호스트의 if의 분기는 제거 할 수 없으니까 테이블 형태로 바꿔서 관리를 편하게 해주는 차선책을 선택한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Loader = class {
constructor(id, repo) {
this._git = new Github(id, repo);
this._router = new Map;
}
add(ext, f, ...arg) {
ext.split(',').forEach( v => this._router.set(v, [f, ...arg]));
}
load(v) {
const ext = this._v.split('.').pop();
if(!this._router.has(ext)) return;
this._git.setParser(...this._router.get(ext));
this._git.load(v);
}
};
  • Loader 를 통해 if문 처리를 값만 계속 추가해서 처리 할 수 있는 형태로 변환하였다. 케이스가 늘어나도 if else 를 늘릴 필요 없이 값만 추가하면 되는 방식이다.
  • if문이 값으로 변환된 것이다. 디자인패턴의 원리 중 하나는 문을 제거해서 값으로 바꿀 수 있는 객체화 시키는 것이다. 값으로 바꿔야지만 코드를 고치지 않고 확장이 가능하다. 문이 되면 우리가 타이핑을 쳐서 저장해야만 한다.
  • 이를 활용해서 우리는 더 많은 확장자를 지원하도록 코드를 지연시켜서 작성 할 수도 있고, 업그레이드를 할 수도 있고, 플러그인을 추가 할 수도 있고, 개발자의 코드를 안전하게 위임 할 수 있게 된다. 이로써 진짜 병렬적인 업무를 가능하게 해준다.
1
2
3
4
5
6
7
8
const loader = new Loader('hanwong', 'codespitz');

// 발생가능한 경우의 수를 값으로 기술
loader.add('jpg, png, gif', img, el('#a'));
loader.add('md', md, el('#b'));

loader.load('xx.png');
loader.load('xx.md');
  • 결론은 실행시점의 선택 위임을 통해서 발생가능한 경우의 수를 값으로 기술하게 하는 것이다.
  • 소유위임을 통해서 코드의 분산을 정적으로 정의하지 않고 경우의 수에 따라 동적으로 정의 할 수 있는 발판을 마련했다면, if문에 대한 분기는 문을 계속해서 값으로 바꿔주는 것으로써 대체한다.
  • 디자인패턴 중 if문을 제거하기 위한 패턴으로 템플릿메소드패턴, 전략패턴, 더 나아가서 상태패턴이라는 것이 있다.
    • for문을 제거하기 위한 패턴으로는 이터레이터, 컴포지트, 비지터 패턴이 있다.
    • for문을 돌다가 if문을 하고 싶다면? 데코레이터, 체인오브리스판서빌리티 패턴을 활용하여 문들을 제거하고 값으로 바꿀 수 있게 된다.
    • 절차적인 부분을 감싸서 값으로 만들고 싶다면? 커맨드패턴 또는 미디에이터
    • 여러 개의 객체들을 조립하는 과정들이 있다면? 빌더패턴, 파스드패턴

결국엔 문으로 구성하던 것들을 값으로 바꿔서 대체하고 싶은 것이다.


정리…

상태에 대한 분기는 사라지지 않는다. 그 분기가 필요해서 태어났기 때문에~

  • 정의 시점에 제거하는 방법은?
    1. 분기 수 만큼 객체를 만들고
    2. 실행시점에 경우의 수를 공급한다.

실행시점으로 분기를 옮길 때의 장단점

  • 장점

    1. 정의 시점에 모든 경우를 몰라도 됨
    2. 정의 시점에 그 경우를 처리하는 방법도 몰라도 됨
    • 일정한 통제 범위 내에서 확장 가능한 알고리즘 설계 가능
  • 단점

    1. 실행 시점에 모든 경우를 반드시 기술해야 함
      • 설정을 빼먹으면 런타임에 프로그램이 죽게된다.
      • 이것에 대해서는 외부에서 더블체크를 해야한다.
    2. 실행 시점마다 알고리즘의 안정성을 담보해야 함
    • 매 호스트코드마다 안정성을 따로 담보해야 함
  • 이러한 장단점을 기반으로 프로그램의 안정도와 확정성에 대한 밸런스를 잡아주는 것이 어렵다.

  • 런타임 안정성이 담보되지 않기 때문에 이 단점을 커버하는 다양한 추가 패턴을 사용하게 된다.
    • 런타임에 작성해야 할 코드들을 객체로 묶어서 관리하여 안정화 시킨다. 팩토리패턴, 빌더패턴
    • 패턴이 패턴을 부르게 된다. 패턴이 다른 패턴을 이용해서 자기 패턴의 약점을 보완하지 않으면 약점이 커지고 불안정해진다. 자신의 불안정성을 감추기 위해서 자신의 불안한 파트를 커버하는 커운터 패턴을 다시 사용하게 된다.
    • 이 의미를 이해하지 못하면 패턴을 그냥 번복만 할 뿐이지 안정성이 높아지지 않는다.

어떤 패턴을 쓸 때 어떤 패턴으로 카운터를 걸어서 안정성을 높여야 하는지 이해해야 한다.

Share Comments