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

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

Live Demo

https://mazing.netlify.com

Binary Tree

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

- 대각선 편향

Sidewinder

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

- 수직 편향

Aldous-Broder

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

- 편향 없음 / 점점 느려짐

Wilson's

- loop-erased 무작위 보행

- 편향 없음 / 점점 빨라짐

Recursive Backtracker

- DFS, 각 셀을 2번씩 방문

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

Hunt-and-Kill

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

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

Kruskal

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

- 비편향 / 균질함

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

Simplified Prim

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

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

True Prim

- 셀에 가중치를 할당

- Kruskal과 비슷한 모양새 / 매우 많은 막다른 길

Recursive Division

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

- 다양한 변형이 가능

Eller's

- Sidewinder와 비슷 / 1982년 생

- 마지막 행의 편향

Clean Code 관점에서

🌯🌯🌯

Food for thought

앞서 보여드린 미로를

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

OO approach

(가상의 시나리오)

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. Tail position이 아니면 스택 오버플로우가 발생할 수 있다.

4.

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

sleep 때문에 비동기 처리로 인한 복잡도가 크게 증가했다.

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();
  }
}

⛔️

PLEASE STOP

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 역할을 하는 클래스를 분리해 보자!

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

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

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

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

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

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는 테스트를 도와줄 뿐 구조를 단순하게 만들지 않음.

본질적인 복잡도는 그대로

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

죄질이 나쁘다

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

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

본질적인 복잡도는 그대로

유기적 기능추가는 본질적 복잡도라

어쩔 수 없다는데...

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

어떻게 푸냐면 이렇게.

F(R)P approach

Functional Relational Programming

State + Program

state

state'

state''

f

f

step

step

step

g_shouldRestart → reset seq

g_fastForward → seq.last()

State Sequence의

생성과 관찰을 분리

Maze as a (mutable) object

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

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

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

 

 

Maze as an (immutable) sequence

미로는 처음부터 이산적 변화를 간직하며 "존재"할 뿐

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

🌮🌮🌮

Free Lunch

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. 온고지신

시퀀스: 1996년 부터 강조되어온 개념

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

By Hyunwoo Nam

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

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

  • 333