타르의 구덩이에서 탈출하기 2

클린코드가 가르쳐주지 않는 것

Live Demo

https://bootcamp-maze.netlify.app/

Binary Tree

- 무작위로 오른쪽, 아래쪽을 선택

- 대각선 편향

Sidewinder

- 무작위로 가로 방향 n회 확장 & 아래 행의 무작의 셀과 연결

- 수직 편향

Aldous-Broder

- 모든 셀을 방문할 때 까지 무작위 보행 (브라운 운동?)

- 편향 없음 / 점점 느려짐

Recursive Backtracker

- DFS, 각 셀을 2번씩 방문

- 길다 / 굽이치는 미로 / 막다른 길이 없음

Hunt-and-Kill

- visited에서 시작, 막다를 때 까지 무작위 보행

- 길다 / 굽이치는 / 적은 메모리

Wilson's

- loop-erased 무작위 보행

- 편향 없음 / 점점 빨라짐

Kruskal

- 신장 나무 알고리즘, but 무작위로 간선 선택

- 비편향 / 균질함

- 정방형이 아닌 미로에 사용

Prim

- 신장 나무 알고리즘, but 무작위로 간선 선택

- 깨진 유리같은(radial) / 많은 막다른 길

Recursive Division

- 네모의 느낌 / 길쭉한 통로(bottleneck)

- 다양한 변형이 가능

🌯🌯🌯

같이 생각해봅시다!

앞서 보여드린 미로를

짝 프로그래밍으로 만들어봅시다. 

1. OOP 설계 원칙을 따르면서

2. 클린코드 원칙을 생각하면서

Q. 2차원 미로를 생성하고 그리세요.

class Generator {
  private maze;
  
  void generate() {
    // update this.maze
  }
  
  void draw() {
    // canvas.lineTo(...)
  }
}

function onLoad() {
  let m = new Generator();
  m.generate();
  m.draw();
}

Q. 여러 알고리즘을 선택할 수 있도록 하세요.

interface IGenerator {
  void generate();
  void draw();
}

class BinaryTree implements IGenerator {/*...*/}
class Sidewinder implements IGenerator {/*...*/}

function onLoad() {
  let m = new BinaryTree(); // or Sidewinder
  m.generate();
  m.draw();
}

Polymorphism!

interface IGenerator {
  void generate();
  void draw();
}

enum MazeType {
  BinaryTree,
  Sidewinder,
  /*...*/
}

class MazeFactory {
  IGenerator create(MazeType type) {/*...*/}
}

function onLoad() {
  let m = MazeFactory.create(BINARY_TREE);
  m.generate();
  m.draw();
}

클래스가 많아지면...

Q. 렌더링 과정을 애니메이션으로 표현하세요.

interface IGenerator {
  void step();
  bool isCompleted();
  void draw();
}

async function run() {
  let m = new BinaryTree();

  while (!m.isCompleted()) {
    m.step();
    m.draw();
    await sleep(50);
  }
}

Q. 렌더링 속도를 조절할 수 있게 해보세요.

interface IGenerator {
  void step();
  bool isCompleted();
  void draw();
}

let g_delay = 50;

async function run() {
  let m = new BinaryTree();

  while (!m.isCompleted()) {
    m.step();
    m.draw();
    await sleep(g_delay);
  }
}

전역변수가 찝찝하지만 딱 한개니까...

Q. 애니메이션 도중 재시작을 할 수 있게 해보세요.

let g_delay = 50;
let g_shouldRestart = false;

await function run() {
  let m = new BinaryTree();

  while (!m.isCompleted() && !g_shouldRestart) {
    m.step();
    m.draw();
    await sleep(g_delay);
  }
  
  if (g_shouldRestart) {
    g_shouldRestart = true;
    run();
  }
}

1. 전역변수가 2개가 되었다.

2. 이번에는 읽는 것 뿐만이 아니라 값을 변경하기까지 한다.

3. 비동기 처리 때문에 코드가 어려워졌다.

4.

일단 여기까지의 생각나는 문제점?

동시성 문제가 생길 것 같다.

Q. 애니메이션 Fast Forward를 넣어주세요.

let g_delay = 50;
let g_shouldRestart = false;
let g_fastForward = false;

function run() {
  let m = new BinaryTree();

  while (!m.isCompleted() && !g_shouldRestart) {
    if (g_fastForward) {
      m.generate();
      g_fastForward = false;
    } else {
      m.step();  
    }
    m.draw();
    sleep(g_delay);
  }
  
  if (g_shouldRestart) {
    g_shouldRestart = true;
    run();
  }
}

Q. 애니메이션 될 때 소리가 나게 해보세요.

let g_delay = 50;
let g_shouldRestart = false;
let g_fastForward = false;
let g_soundEnabled = true;

function run() {
  let m = new BinaryTree();

  while (!m.isCompleted() && !g_shouldRestart) {
    if (g_fastForward) {
      m.generate();
      g_fastForward = false;
    } else {
      m.step();  
    }
    m.draw();
    if (g_soundEnabled) {
      m.playSound();
    }
    sleep(g_delay);
  }
  
  if (g_shouldRestart) {
    g_shouldRestart = true;
    run();
  }
}

⛔️

이대로 계속 해도 괜찮을까?

1. 전역변수가 어느새 4개

2. 값을 양쪽에서 조작하는 변수가 2개가 되었다.

3. 함수가 너무 많은 일을 하는 것 같다.

4. 확장성이 떨어지고 구현 난이도가 증가한다.

 

(대부분의 리팩토링 관련된 책에서 지적하는 부분)

무엇이 문제인가?

연출만 변경할 뿐인데, 로직 구현부가 영향을 받는다.

특정 step이 오래 걸릴 경우 렌더링 성능 저하가 생긴다.

 

뷰(View)에 해당하는 부분: draw, playSound, sleep

첫째. MVC가 분리되지 않았다.

class Renderer {
  let _delay = 50;
  let _soundEnabled = true;

  void render(IGenerator g) {/*...*/}
}

let g_shouldRestart = false;
let g_fastForward = false;
let g_renderer = new Renderer();

function run() {
  let m = new BinaryTree();

  while (!m.isCompleted() && !g_shouldRestart) {
    if (g_fastForward) {
      m.generate();
      g_fastForward = false;
    } else {
      m.step();  
    }
    g_renderer.render(m);
  }
  
  if (g_shouldRestart) {
    g_shouldRestart = true;
    run();
  }
}

🛠 Renderer 역할을 하는 클래스를 분리해 보자!

테스트 하기 어렵다고 배웠다.

의존성 주입(DI)을 통해 제어를 역전(IoC)하라고 배웠다.

객체의 생성을 외부에 위임(deligation)하라고 배웠다.

그러면 결합(decouple)이 줄어든다고 배웠다.

둘째. 전역 변수는 나쁘다고 배웠다.

class ControlConfig {
  let shouldRestart;
  let fastForward;
}

class BaseMaze implements IGenerator {
  constructor(ControlConfig config) {
    this.config = config;
  }
  
  /* ... */
}

class BinaryTree extends BaseMaze {/*...*/}

function run() {
  let m = new BinaryTree();

  while (!m.isCompleted()) {
    m.generate();
    g_renderer.render(m);
  }
}

🛠 DI... IoC...

정말 문제가 해결되었는가?

🤔

전역 변수가, 다른 클래스들의 지역 변수로 흩어졌다.

Mock Object는 테스트를 도와줄 뿐 구조를 단순하게 만들지 않음.

본질적인 복잡도는 그대로

* 본질적인 복잡도 = 요구사항의 복잡도 + 구현의 복잡도

복잡도의 본질을 숨긴다는 점에서

죄질이 더 나쁠지도

구현 난이도가 올라갔던 이유는 구현 자체의 문제가 아님.

새로운 상태의 추가가 기존 코드얽힘을 만들기 때문.

본질적인 복잡도는 그대로

유기적 기능추가는 본질적 복잡도라
어쩔 수 없다는데요?

맨먼스 미신 - 은탄환은 없다 中

꼭 그렇지는 않다고 하는 주장도 있다.

(오늘 발표의 제목)

어떻게 푸냐면 이렇게.

너도 모르게 복잡해지는거야!

복잡도 = 로직의 복잡도 + 상태의 복잡도
 

로직의 복잡도 = 필수 로직의 복잡도 + 우발적 로직의 복잡도

상태의 복잡도 = 필수 상태의 복잡도 + 우발적 상태의 복잡도

우발적 로직은 논리형 프로그래밍으로

우발적 상태은 함수형 프로그래밍으로

필수 로직은 informal한 표현을 formal한 표현으로

필수 상태는 관계형 모델링으로

TL;DR:

State + Program

state

state'

state''

f

f

step

step

step

g_shouldRestart → reset seq   

g_fastForward → seq.last()

State Sequence의

생성과 관찰을 분리

Clojure의 관점이죠!

미로를, 가변적인 객체로 바라본다면...

관찰자(렌더러)의 필요에 따라 미로의 상태가 변함

변경과 관찰이 분리되면 메모리 경합 발생

시간 제어를 위한 기법이 필요해짐

 

 

미로를, 불변 시퀀스로 바라본다면...

미로는 이산적 변화로 구성된 lazy-seq

외부에서는 특정 시간의 스냅샷을 "관찰"할 뿐

동시성 제어가 불필요

🌮🌮🌮

공짜 점심

Hot Reloading

 

- Live Demo의 비밀

- Essential State는 항상 유지되고 있었기 때문

- React/Redux 철학과 비슷

Redux

store, action → store'

store', action → store''

Maze

maze, step → maze'

maze', step → maze''

 

Reducer! (reducing function)

Time Travel

 

- undo/redo를 위한 커맨드 패턴 (X)

- 작성한 코드: 약 10줄 (깨닫는데 10년)

(defn do-step!
  "시퀀스의 head를 그리고 기록한 뒤 한 칸 전진시킴"
  []
  (when-let [m (first (:mseq @*state))]
    (swap! *state assoc :output m)
    (swap! *state update :mseq rest)
    (swap! *history conj m)
    (redraw)))

(defn time-travel!
  "특정 idx의 상태로 돌아가서 렌더링"
  [idx]
  (let [m (get @*history idx)]
    (swap! *state assoc :output m)
    (redraw)))

이게 다 불변 자료구조 덕분

Purely Functional Data structures - Chris Okasaki, 1996

Hash Array Mapped Trie - Phill Begwell, 2001

Hickey Tree* - Rich Hickey, 2007

RRB-Trees - Phil Begwell, 2012

CHAMP - Steinforder & Vinju, 2015

불변 자료구조의 길고 짧은 역사

*Hickey Tree라는 이름은 임의로 부여한 것입니다.

다양한 분야에서 영향력을 발휘 중

Takeaways

1. 코드 가독성보다 중요한 것은 복잡도를 낮추는 것

2. 복잡도는 상태 때문이다.

3. 상태 관리를 위한 다양한 접근법에 관심을 갖자

4. 가변성을 멀리하고 불변적을 가까이하자

?!

⁉️

Recommended Reading/Watching

1. Out of the Tar Pit

2. SICP / SDF

3. Meetings With Remarkable Trees

4. Live React: Hot Loading with Time Travel

5. Immutability changes everything

Copy of 타르의 구덩이에서 탈출하기

By Hyunwoo Nam

Copy of 타르의 구덩이에서 탈출하기

클린코드가 가르쳐주지 않는 것

  • 202