개발/Medium

TIR ##5 [How To Study Design Patterns as a Web Developer]

JWOOKJ 2022. 3. 30. 06:40

학습을 목적으로 Medium에서 읽은 글을 번역한 글입니다.
번역이 완벽하지 않을 수 있고,
직역하기보다는 문맥에 맞추어 자연스럽게 정리하려고 했습니다.
오류나, 개선하면 좋을 부분들에 대한 피드백은 언제나 환영합니다.😁

 

 

How To Study Design Patterns as a Web Developer

몇 년 전 동료와 나는 프론트엔드 개발자 인터뷰 진행 시 개발자를 평가하는 회사의 기준을 수정하는 직무를 받게 되었다.

 

나는 우리 프로젝트를 위해 코드를 짜본 경험을 바탕으로 지원자와 이야기할 주제들을 목록으로 정리해 놓았다.

 

우리는(나와 동료) 지원자를 평가하는데에 있어서,

꼭 알아야 하는 중요한 개념은 무엇인지와, 어떠한 부분은 알고 있으면, (혹은 경험해 봤으면) 좋은 부분인지를 결정하는 데에 거의 모든 부분에서 의견이 일치했다.

하지만 특정한 부분에서는 서로의 의견이 맞지 않았다. '디자인 패턴 '

 

나느 이러한 부분에 대해서 다른 동료들과 이야기하던 중, 커뮤니티 내의 많은 시니어 개발자들이, 숙련된 프론트엔드 개발자의 작업에 디자인 패턴이 중요하다는 것을 믿지 않는다는 것을 알고 놀랐다.

 

이러한 디자인 패턴은 객체지향 패러다임에만 적용이 가능하고 자바스크립트에는 항상 적용할 수 없다는 생각이 이미 만연해 있었다.

 

이 글의 목표는 웹개발자로써, 당신이 디자인 패턴을 공부하고 연습하는 데에 자신의 시간을 투자해야 한다는  것을 확신시켜주는 것을 목표로 한다.

 

이것을 실천하기 위해서는 첫번째로 어떠한 패턴들이 존재하는지와 왜 사람들이 자바스크립트와 같은 언어와 디자인 패턴은 맞지 않다고 생각하는지를 분석해봐야 한다.

 

그 후 우리는 모든 패턴과 모든 프로그래밍 패러다임을 적용할 수 있는,  (예를 들어 chain of responsibility와 같은) 프레임 워크를 제안할 수 있다.

 

마지막으로, 실제로 이러한 패턴들이 적용된 코드를 통해 어떻게 그들이 당신이 매일 사용하는 서비스의 코드퀄리티를 상승시켰는지 알아볼 것이다.


📌 What a Design Pattern really is

디자인 패턴은 반복되는 소프트웨어 구조의 문제에 대한 간단하고, 유지보수가 용이하고, 높은 재사용성을 제공하는 해결책이다.

 

이러한 23가지의 패턴은 90년도 초기에 Gang of Four라는 이름의 베스트셀러 책으로 엮어져서 나왔고, 빠르게 널리 채택되어서 소프트웨어 개발 분야의 초석이 되었다.

 

우리가 알고 있어야 하는 점은, 책은 출판된 지 30년 정도 되었고, 이 당시는 객체지향 기반의 언어에 굉장히 많은 영향을 받고 있었고, 사람들은 객체지향 프로그래밍에 대한 아이디어에 빠져있었다는 것이다.

 

따라서, 몇몇 패턴들은 자바스크립트와 같은 현대의 스크립트 언어에서는 제대로 된 번역을 제공하지 못하는데, 객체지향 프로그래밍에서 사용되는 몇 가지 개념들이 빠져있기도 했었고, (심지어 지금도) 혹은 많은 언어들이 지금에는 자체적인 패턴 구조를 제공하기도 하기 때문이다.

 

이러한 점들을 보완하기 위해, 몇몇 패턴들에 대해, 많은 선의의 저자들이 객체지향 프로그래밍의 개념을 자바스크립트로 동일하게 변환하여 웹 개발자들에게 제공하려 한다.

 

이 글을 읽고 있는 많은 사람들이 class 개념을 매일 사용하는 것이 아니기 때문에 이러한 개념을 배우는 것이 시간낭비라고 생각할 수 있다.

 

저자와 독자 모두 간과할 수 있는 부분은, 클래스와 객체의 개념은 그저 도구일 뿐이라는 것이다.

반면, 디자인 패턴은 우리가 필수적으로 해야 하는 것에 대한 문제와 해결책을 말해준다.

 

엔지니어의 일은 도구를 사용하는 것이 아니라, 복잡한 문제에 대한 간단한 해결책을 제공하는 것이다.

도구는 그저 해결책을 표현하는 방식일 뿐이다.

 

프론트엔드 개발자로서 우리는 다른 소프트웨어 엔지니어보다 클래스나 객체의 개념을 많이 사용하지 않을 수도 있다. 하지만 우리가 직면하는 문제는 결국 프로그래밍적 문제이다.

 

다행히, 디자인 패턴은 클래스나 객체,  혹은 상속 vs 결합에 관한 것이 아니다. 

디자인 패턴은 복잡한 문제와 문제에 대한 간단하고, 확장성있고, 높은 재사용성을 가지는 해결책들의 요약본이며, 우리는 이러한 본질에 집중해야한다.


📌 how to think about patters

패턴을 공부하는 가장 좋은 방법은 먼저, 문제에 대해 바로 단순하게 생각해 낼 수 있는 해결책을 생각한 후에 패턴에서 제안하는 해결책과 비교하는 것이다.

현실적인 예시에 두 가지 해결책을 모두 적용해 보는 것도 이러한 개념을 습득하는 데에 많은 도움을 준다.

이러한 과정을 거친 후에야 특정한 패러다임의 관점에서 패턴에 대해 이야기할 수 있다.

 

예시를 위해 chain of responsibility 패턴을 살펴보자

 

What problem does it solve?

여러 개의 독립된 작업들은 하나의 액션에 대한 응답으로 실행되어야 한다. 

각각의 작업들은 서로 다른 그룹, 다른 순서, 문맥에 따라서 실행되고, 하나의 작업에 대한 결과로 인해 전체 코드의 실행을 중지해야 할 수도 있다.

 

What is the naïve solution?

모든 대기열(큐)의 작업들을 하나의 개체(함수 혹은 클래스 메서드)로 정의하고 액션이 실행될 때마다 작업을 실행시킨다.

새로운 문맥(작업)은 동일한 타입의 추가 개체로 정의된다.

 

What is the solution proposed by the pattern?

각각의 응답을 각자의 개체로 이동시킨다. (즉 핸들러)

각 컨텍스트에 대한 핸들러를 서로 다른 대기열과 순서로 배치한다.

각 핸들러에게 작업을 처리하거나, 다음 핸들러로 넘기거나, 중지할 수 있는 기능을 부여한다.

 

현실적인 예

간단한 배틀 시스템을 가진 브라우저 기반 RPG 게임을 만들고 있다고 가정하자.

MVP(최소 기능 제품)의 일부로, 먼저 아이템을 이용해 공격 혹은 방어가 가능한 두 캐릭터를 생성할 수 있다.(Ninja , Monster)

직관적으로 생각했을 때의 공격 시스템은 이런식으로 구현될 있다.

 

출처: https://medium.com/arctouch/how-to-study-design-patterns-as-a-web-developer-e54284958e48

MVP승인 이후 프로덕트 팀은 공격을 수정하거나 무효화 할 있는 다른 아이템과 능력을 가진 Wizard Ghost, 2개의 새로운 캐릭터를 추가하기로 결정했다.

 

출처: https://medium.com/arctouch/how-to-study-design-patterns-as-a-web-developer-e54284958e48

 

아마 컨트롤 하기에는 너무 복잡해졌다고 느낄 것이다.

새로운 캐릭터마다 공격 기능에 대한 함수를 여러 번 작성해 줘야 할 뿐 아니라,

장착할 수 있는 아이템이 완전히 동일하지 않기 때문에

(예를 들어, wizard는 ninja처럼 sword를 사용할 수 있지만, ghost는 monster처럼 shield를 들 수 없다.) 

아이템에 대한 코드를 각 캐릭터의 공격 기능 함수에 따로 반복해서 적어주어야 한다.

 

셀 수 없는 단위 테스트와 수동 테스트를 거쳐 이제 출시해도 되겠다고 느낄 만큼의 자신감이 생긴다.

몇 주 후에 당신의 최악의 악몽은 현실이 된다.

=> 당신의 MVP의 두 번째 사이클이 굉장히 성공적이어서 프로덕트 팀은 최소 10개의 새로운 캐릭터와 각 캐릭터마다 공격을 특정한 방법으로 수정할 수 있는 20개의 아이템을 추가하기로 결정했다.

naïve 한 해결책을 통한 유지보수와 확장은 불가능한 작업임이 증명되었다.

 

Applying the pattern

첫 번째로알 있는 점은, 공격은 액션이고 아이템은 액션에 대응하는 응답이라는 것이다. 따라서, 아이템을 액션 핸들러에 따라 쉽게 분류할 있고 게임 상태에 따라 대기열내에 모을 있다.

 

출처: https://medium.com/arctouch/how-to-study-design-patterns-as-a-web-developer-e54284958e48


📌 Implementing a pattern

패턴을 추상적이고 패러다임 독립 개념의 관점에서 설명한 후에야 패러다임 종속 구조를 사용하여 실제로 구현해 볼 수 있다.

이러한 순서로 작업을 진행했을 때, 우리는 클래스 다이어그램을 이용했다면 찾지 못했을 여러 구현 방법을 알게 된다.

 

Chain of Responsibility(CoR)를 구현해 보기 위해 고전적인 oop방식으로 구현을 해보려고 한다.

하지만 여기서 구현하고 멈추면 안된다. 클래스와 객체가 사용되지 않는 함수의 합성과 함수의 배열만으로도 충분히 패턴을 구현할 있다.


📌 OOP implementation 

전통적인 CoR의 구현은 작업 큐에 모아놓기 위해 객체 구성을 사용한다. 각 작업은 클래스로 구현되며, 작업 큐의 다음 작업을 가리키는 next라는 레퍼런스를 포함하고 있다.

일반적으로, 레퍼런스는 모든 핸들러에 의해 확장될 있는 추상 클래스 내에서 캡슐화되어 있다.

 

타입 스크립트로구현한 간단한 예제

interface Item {
  new (next: Item): Item;
  next: Item;
  process: (attack: Attack) => Attack
};
abstract class BaseItem implements Item {
  constructor(public next: Item) {}
  abstract process(attack: Attack): Attack;
}
class Sword extends BaseItem {
  process(attack: Attack) {
    const copy = { ...attack, attack.strength *= 3 };
    return this.next ? this.next.process(copy) : copy;
  }
}
class GhostlyShape extends BaseItem {
  process(attack: Attack) {
    if (attack.isPhysical()) return null;
    else next(attack);
  }
}

이것은 GoF책에서도 볼 수 있고, 블로그 글에서 가장 많이 찾아볼 수 있는 구현 예시이다. 

Refactorin Guru에서는 이것은 어떻데 프로젝트에서 활용해야 하는지에 대한 좋은 예시 보여준다.


📌 Closure-based implementation

CoR을 함수 구성으로 사용하기 위해서는 클로저의 개념을 이해해야 한다.

클로저는 함수 밖에서 함수 스코프를 가지는 변수에 접근할 수 없게 된 후에도, 생성 당시 스코프를 유지하고 있는 변수를 계속 참조하고 있는 (기술? 현상?)이다. (본문에서는 생성 당시 스코프를 유지하고 있는 변수를 계속 참조하고 있는 함수라고 설명)

 

클로저 기반의 CoR은 작업 큐 내 다음 핸들러를 가리키는 래퍼런스(next)를 유지하기 위해서 핸들러의 초기 스코프를 이용한다.

 

간단한 타입스크립트 구현 예시

type Modifier = (attack: Attack) => Attack;
type Item = (next?: Modifier) => Modifier;
const sword: Item = next => attack => {
  const copy = { ...attack, strength: attack.strength * 3 };
  return next ? next(copy) : copy;
}
const ghostlyShape: Item = next => attack => {
  if (isPhysical(attack) return null;
  else next(attack);
}

핸들러의 조합을 간단한 compose를 사용하여 처리할 수도 있다.

 

compose 배열의 다음 항목으로 항목을 호출하며, 이는 다음 변수의 클로저에 의해 유지된다. 항목은 next 호출하여 실행을 다음 핸들러로 전달하거나 중지할 있다.


📌 CoR as an array of functions

이것은 CoR을 구현하는 가장 간단한 방법이다. 그래서 오히려 사람들이 패턴으로 생각하지 않는다.

작업 큐는 순서대로 호출되는 간단한 함수의 배열로 구현될 수 있다.

function sword (attack) {
  return { ...attack, strength: attack.strength * 3 };
};
function getAttack() {
  const { attacker, foe } = game.state;
  const modifiers = [
    ...attacker.attackItems,
    ...foe.defenseItems
  ];
  return modifiers.reduce((acc, currentItem) => {
    return currentItem(acc);
  }, attacker.getAttack());
}

위 접근방법의 장점은 간단하다는 것이다.

단점은 복잡하게 추가하지 않으면 실행을 중단하기 어렵다는 것이다.


📌 Real-world applications

실제로 현업에서 일을 할 때, 혹은 프로젝트를 진행할 때의 예시를 포함하지 않는다면, 프런트엔드 개발자가 디자인 패턴을 공부해야 한다는 주장은 그저 moot point에 불과하게 된다.

moot point : 현재 상황과 관련이 없기 때문에 중요하지 않은 사실

 

CoR패턴은 프로젝트를 사용자의 의지에 따라 확장성 있게 만들어준다는 점에서 이미 강력한 도구임을 증명했으며, 몇몇의 성공적인 JS라이브러리들은 이러한 점을 위해 CoR패턴을 사용한다.

 

리덕스와 웹팩이 각각 CoR 함수 composition, 함수배열을 사용하여 어떻게 개발자들이 기능을 확장하여 사용할 있도록 만들었는지 살펴보자


📌 Redux Middleware

리덕스 문서는 dispatch 함수의 응답 내에서 실행되는, 개발자가 정의한 동작을 설치할 수 있도록 applyMiddleware API에 대해 설명되어 있다.

각 middleware는 리덕스 API에 접근 가능하고, 위에서 보았던 클로저 기반 CoR구현과 정확히 같은 방법을 사용하는 핸들러를 반환하는 factory function에 의해 생성된다.

function actionLogger({ getState }) {
  return next => action => {
    console.group(`Action of type ${action.type}`);
    console.log('action:', JSON.stringify(action, null, 1));
    console.log('prev state:', getState());
    console.groupEnd();
    return next(action);
  }
}

내부적으로 리덕스는 접근 가능한 API를 통해 factory function을 호출한 후에 compose를 사용하여 반환된 핸들러들을 모아놓는다. (source code here.)

 

이러한 접근은 action-based fetching solutions부터 function action types의 범위까지 리덕스가 무수히 많은 커스텀 된 동작들로 확장될 수 있도록 해주었다.

 

 Redux Ecosystem page에서 리덕스에서 제공하는 CoR 적용된 커스텀 기능들에 대한 리스트를 있다. 


📌 Webpack loader

웹팩의 핵심기능은 규칙(test와 loader들을 사용하여 어떻게 소스파일을 다룰 것인지를 명시한 객체)으로 구현된다. 

매번 파일은 의존성 그래프에 추가되고 런타임은 정의된 테스트를 사용하여 하나의 규칙과 일치시킨다.

그 후, 일치 규칙에 의해 명시된 loader chain이 해당 소스에 적용되어 webpack이 임의의 언어를 변환하여 JS로 출력할 수 있게 된다.

 

웹팩 설정 예시 

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: { modules: true }
          },
          { loader: 'sass-loader' }
        ]
      }
    ]
  }
};

loader chain들은 reverse order of declaration이라고 하는 CoR의 함수의 배열을 사용하여 구현되었다.

각 loader는 체인에 존재하는 다음 로더의 출력으로 인해 실행되고 마지막 순서의 로더는 유효한 자바스크립트 코드를 제공할 것으로 예상한다.

 

 

체인 내에서 실행을 중지하거나 다음 노드로 넘어가기 위해 각 로더들은 this.emitError, this.callback과 같이 this객체에 바인딩되어있는 함수를 사용할 수 있다.

값을 반환하는 방식으로도 동일한 작업을 실행할 있다.


📌 Other patterns

다른 많은 라이브러리들은 디자인의 단순성을 유지하며 확장성을 최대로 가져가기 위해 패턴들을 광범위하게 사용한다.

 

예시:

  • React는 Composite(합성) 패턴을 활용하여 렌더링 할 수 있는 작은 ui 단위들을 이용해, 복잡한 ui를 구현한다. 
  • React-Redux는 React의 컴포넌트들을 Redux store의 데이터와 연결하기 위해 dercorator패턴을 사용한다.
  • Vue는 상태 관리에 따른 ui의 rerender를 위해 Observer패턴을 광범위하게 사용한다.
  • Babel은  plugin들이 소스코드를 검사하고 수정할 수 있도록 Visitor패턴을 사용한다.

디자인 패턴의 사용 예는 비단, 라이브러리에만 국한되지 않으며 당신의 애플리케이션에서도 이 글에서 논의된 디자인 패턴들의 개념을 사용하여 동일한 결과를 얻을 있다.


결론

디자인 패턴에 대한 깊은 이해는 소프트웨어 개발자가 가질 수 있는 가장 강력한 도구 중 하나이다.

이 귀중한 개념들이 오랫동안 객체지향적 설계를 설명하는 관점에서만 사용되었기 때문에 

충분히 자신의 코드와 프로젝트의 질을 높일 수 있는 프론트엔드 개발자들에게서 이러한 개념들을 멀어지게 만들었다.

 

다행히, 패턴들은 일반적인 상황에서 마주치는 문제의 해결책에 대해 설명해주기 때문에 

틀림없이 프론트엔드 개발자들은 그들의 커리어 중에 몇몇 패턴들을 자연스럽게 다시 찾아보게 될 것이다.

 

해결책을 모두가 아는 명칭을 사용한다면 팀과 소통하기 훨씬 수월해질 것이다.

 

그러니 클래스 다이어그램을 통해 가르치는 패턴을 발견한다면 포기하지 말자.

글에서 설명된 과정을 적용한다면 당신은 어떤 패턴이라도 마스터 하고 당신의 무기고에서 강력한 무기를 만들 있다.


원문

https://medium.com/arctouch/how-to-study-design-patterns-as-a-web-developer-e54284958e48

 

How To Study Design Patterns as a Web Developer

A framework for describing Design Patterns in a paradigm-independent fashion

medium.com