Skip to main content

웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴

About 6 minNode.jsReact.jsArticle(s)blogyozm.wishket.comnodenodejsreactreactjsreact-js

웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴 관련

React.js > Article(s)

Article(s)

웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴 (1) | 요즘IT
웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴 (1)

[FEConf2023에서 발표한 웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴open in new window][1]를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 웹 기반 그래픽 편집기의 기초적인 아키텍처와 이 과정에 녹아있는 디자인 패턴에 대해 살펴봅니다. 2회에서는 실제 그래픽 편집기를 구현하고 문제점을 수정해 보면서 디자인 패턴에 대해 깊게 알아봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지open in new window에서 다운로드할 수 있습니다.

안녕하세요. 저는 네이버에서 플랫폼 개발에 참여하고 있는 프론트 엔지니어 심흥운입니다. 이번 글에서는 웹 기반 그래픽 편집기와 디자인 패턴에 대해 알아보겠습니다.

저는 이전 회사와 현재 회사에서 5번의 그래픽 편집기 개발 프로젝트에 참여했습니다. 제가 참여한 프로젝트는 UI 프로토 타이핑 도구, 그래픽 에디터 프레임워크, 비트맵 이미지 편집기 그리고 머신러닝 학습 데이터 생성을 위한 어노테이션 툴입니다. 제가 진행한 5가지의 프로젝트들은 한 뿌리에서 파생된 프로젝트들입니다. 바로 Eclipse(이하 이클립스)의 하위 프로젝트인 GEF입니다.

Eclipse GEF
Eclipse GEF

GEF는 2000년대 초반에 시작된 역사가 오래된 프로젝트입니다. 이 프로젝트는 이클립스에 통합되어 있고, 이클립스의 각종 모델을 GUI를 활용해 편집할 수 있도록 도와줍니다. 당시 저희 팀은 GEF 아키텍처를 외부로 옮겨와 프로젝트를 수행했는데, 그 과정에서 그래픽 편집기가 매우 다양한 디자인 패턴을 품고 있다는 것을 알게 됐습니다. 웹의 생태계가 변화하면서 이에 맞게 변화하고 개선하는 과정을 진행했지만, 여전히 그 안에 담겨있는 디자인 패턴은 유효했습니다.

실무 관점에서 디자인 패턴을 이해하는 데 도움이 될 것 같아 많은 분들에게 소개하고자 이 내용을 공유합니다. 이 내용이 웹 기반 그래픽 편집기 개발 프로젝트를 위한 작은 아이디어가 될 수 있기를 기대하고 디자인 패턴을 실무에 적용하고 싶은 분들한테 좋은 계기가 될 수 있으면 좋겠습니다.

이번 글에서 살펴볼 내용은 다음과 같습니다.

  1. 웹 기반 그래픽 편집기의 기초적인 아키텍처
  2. 이 과정에 녹아있는 디자인 패턴과 디자인 패턴이 해결해 주는 것

디자인 패턴 돌아보기

패턴이란 양식, 무늬, 견본 그리고 모범이라는 뜻을 가지고 있습니다. 프랑스어 ‘파통’에서 유래된 말로 되풀이되는 사건이나 물체의 형태를 의미합니다. 벌집, 건축 등에서의 기하학적인 반복이 쉬운 예가 될 수 있습니다.

건축과 패턴

엔지니어링 관점에서 패턴이란 개념은 건축가 크리스토퍼 알렉산더가 집필한 ‘패턴 랭귀지 - 도시, 건축, 시공'이라는 책에서 처음 소개되었습니다. 이 책은 우리가 살아가는 환경을 구성할 때 그 구성 요소들을 올바르게 구성하기 위한 253가지 패턴을 소개하고 있습니다. 아래 그림의 그리드, 토폴로지, 네트워크 등은 익숙한 개념이지 않나요?

도서 <패턴 랭귀지>
도서 <패턴 랭귀지>

우리가 코드에서 디자인 패턴을 발견할 때처럼 작가는 공간이나 건축물에서 반복되는 요소를 관찰하고 다른 요소들과의 관련성에 대해 고찰했습니다. 우리가 자주 사용하는 ‘포털'이라는 용어가 건축에서 유래된 말인 것을 아시나요? ‘건축을 뒤바꾼 아이디어 100’이라는 책을 보면 우리가 자주 사용하는 용어들이 많이 보입니다.

도서 <건축을 뒤바꾼 아이디어 100>
도서 <건축을 뒤바꾼 아이디어 100>

모듈, 레이어, 컴포지션, 스타일, 플랫폼, 컨텍스트 등 그 의미도 추상적인 맥락에서 우리가 사용하는 용어와 굉장히 유사합니다. 이처럼 건축과 프로그래밍은 비슷한 맥락을 많이 가지고 있습니다. 이런 의미에서 건축가가 물리적 공간을 설계하는 사람이라면 프로그래머, 특히 프론트엔드 개발자들은 정신적 공간을 설계하는 건축가가 아닐까라는 생각이 듭니다.

프로그래밍과 패턴

아래의 책은 여러분들이 잘 아는 ‘디자인 패턴'이라는 책입니다. 이클립스와 비쥬얼스튜디오 코드의 설계자이기도 한 에릭 감마, 존 블라시디스, 랄프 존슨, 리처드 헬름에 의해 1994년 발간되었습니다. 이 책에 의해 프로그래밍 설계에서의 패턴에 대한 개념이 널리 알려졌습니다.

도서 <디자인 패턴>
도서 <디자인 패턴>

이 책은 소프트웨어의 구성 요소를 올바르게 설계하기 위한 23가지 패턴을 소개하고 있고, 이 책을 통해 패턴 접근법이 인기를 끌며 이후에 다양한 패턴들이 발견되고 고안되었습니다.

디자인 패턴

그렇다면 디자인 패턴은 정확하게 무엇일까요? 위키피디아에 나온 정의에 따르면 ‘소프트웨어 개발 과정에서 발견된 설계 노하우를 축적하고 이름을 붙여서 재사용하기 좋은 형태로 정리한 것'이라고 합니다. 더 간결하게 표현하면 특정 상황에서 반복적으로 일어나는 문제에 대한 해결책입니다.

디자인 패턴은 발명되기보다는 경험에 의해 발견됩니다. DRY (Do not repeat yourself)가 코드의 재사용을 의미하는 반면, DP (Design pattern)은 경험의 재사용을 의미합니다.

소프트웨어 디자인 패턴은 크게 다음과 같이 분류할 수 있습니다.

  1. 아키텍처 패턴 : MVC / PUB-SUB / DAO / DTO / Broker
  2. GoF (Gang of four) 패턴 : Creational / Structural / Behavioral
  3. Concurrency 패턴 : Event-based / Scheduler / Reactor

이외에도 많은 디자인 패턴이 있습니다. 오늘 글에서는 아래에 표시한 7가지 패턴에 대해 소개합니다.

이 글에서 소개할 디자인 패턴
이 글에서 소개할 디자인 패턴

디자인 패턴의 장점

1. 좋은 설계는 구현을 쉽게 한다

설계 없이 구현하면 처음에는 빠른 것처럼 보이지만 시간이 갈수록 비효율과 복잡도가 빠르게 증가합니다.

2. 효율적인 의사소통 가능

시스템을 설명할 때 패턴을 활용해 설명하면 간단하고 정확하게 전달할 수 있습니다. 오해를 줄이고 효율적으로 작업할 수 있습니다.

3. 객체 지향 원칙을 잘 지킬 수 있다

소프트웨어 품질의 기반이 되는 SOLID 원칙의 목적은 재사용성, 확장성 그리고 관리의 용이성을 높이는 것인데, 디자인 패턴은 기본적으로 SOLID 원칙을 기반으로 하고 있기 때문에 디자인 패턴을 잘 익혀두면 유연하고 확장 가능한 코드를 구현할 수 있습니다.

4. 설계 우선주의

디자인 패턴은 여러분의 머릿속으로 먼저 들어갑니다. 문제를 가진 대규모 코드를 리팩토링할 때 디자인 패턴을 알고 있으면 바람직한 패턴이나 구조로 전체 흐름을 재설계하는 데 도움이 됩니다.

5. 하이레벨 뷰

하이 레벨 뷰를 통해 큰 그림을 볼 수 있습니다. 디자인 패턴은 라이브러리나 프레임워크보다 더 높은 관점에 바라보는 개념입니다. 따라서 디자인 패턴에 대한 개념을 바탕으로 프레임워크나 코드 등을 더 쉽게 이해할 수 있습니다. 그리고 그 안에 담겨 있는 의도를 더 명확하게 이해할 수 있습니다.

디자인 패턴이 문제를 해결하는 방법

디자인 패턴이 문제를 해결하는 방법은 3가지로 생각해 볼 수 있습니다.

1. 쉽게 변경하는 방법 제공

디자인 패턴의 원칙은 소프트웨어의 변경과 관련이 깊습니다. 그래서 시스템 일부를 쉽게 변경하는 방법을 제공하는 것이 핵심입니다.

2. 추상화와 위임

쉽게 변경하기 위해서는 변경되는 부분의 처리를 전문가에게 위임하는 방법을 씁니다. 시스템에서 변경되는 부분을 분리해서 추상화하고 인터페이스를 통해서 전문성을 가진 모듈에게 작업을 위임합니다.

3. 상속 보다 구성

그리고 상속보다는 구성(컴포지션)을 사용해서 문제를 해결합니다. 그 이유는 상속은 컴파일 타임에 결정되지만 구성(컴포지션)은 런타임에 결정되기 때문에 더욱 유연합니다. 이런 이유로 아래 그림처럼 GoF의 대부분의 패턴이 구성(컴포지션) 기반으로 이루어져 있습니다.

디자인 패턴이 문제를 해결하는 방법
디자인 패턴이 문제를 해결하는 방법

언제 디자인 패턴을 사용하면 좋을까?

특정 부분이 계속 변경될 것이라고 예상되는 경우에 사용하면 좋습니다. 그래서 프레임워크에서 디자인 패턴이 자주 사용됩니다. 프레임워크는 기반이 있고 변경되는 부분을 교체하면서 개발합니다. 그리고 코드에 if 문이 계속 수정되어야 하는 경우가 있다면 우선 고려해볼 수 있습니다.

어떻게 디자인 패턴을 사용하면 좋을까?

간단하게 KISS 원칙을 따르면 됩니다. 이 원칙은 미 해군에서 개발된 원칙입니다. Keep it small and simple. 즉, 디자인 패턴을 통해서 문제가 간단해질 때 사용하면 좋습니다. 그러나 정답은 없습니다. 디자인 패턴은 ‘바람직한 경험’이기 때문에 상황에 맞게 유연하게 적용하면 됩니다.


그림 그리기의 디자인 패턴

이번에는 그림 그리기에 담긴 디자인 패턴을 알아보겠습니다. 우선 도화지에 그림을 그리는 편집 작업에 대해 생각해보겠습니다. 도화지에 그림을 그리는 작업은 아래와 같은 순서를 따라 진행됩니다.

  1. 연필을 움직인다.
  2. 흑연이 도화지에 전달된다.
  3. 도화지의 상태가 변한다.

그렇다면 프로그램이 그림을 그리는 과정은 어떨까요?

  1. 입력 장치를 움직인다.
  2. 이벤트가 전달된다.
  3. 화면의 상태가 변한다.

즉, 편집 작업은 이벤트를 상태 변화로 전환하는 과정입니다. 여기서 사용자가 이벤트를 전달하면 도화지나 모니터의 화면이 바뀔 것이라고 기대합니다. 이것이 바로 사용자의 멘탈 모델입니다.

멘탈 모델
멘탈 모델

따라서 이 과정을 다시 정리하면, 편집 작업은 이벤트를 통해 사용자의 의도인 멘탈 모델을 컴퓨터 모델로 변환하는 과정이라고 할 수 있습니다.

편집 작업의 속성
편집 작업의 속성

MVC 패턴

앞서 설명한 과정을 위해 고안된 구조가 있습니다. 바로 1976년 제록스 팔로알토 연구소에서 노르웨이 컴퓨터 공학자 트리베 린스카우그가 창안한 MVC 패턴입니다. 이 모델에서 멘탈 모델은 뷰가 담당하고 편집 장치는 컨트롤러가 담당하고 컴퓨터 모델은 모델이 담당합니다.

MVC와 편집 작업
MVC와 편집 작업

팔로알토 연구소는 현재 우리가 누리고 있는 수많은 IT 장치들의 원형을 개발한 곳입니다. 당시 그는 아이패드의 먼 조상이라고 할 수 있는 다이나북을 개발하는 팀에서 일하고 있었습니다. 이 팀은 객체 지향 프로그래밍과 GUI 분야의 선구자이며 스몰토크(객체 지향 언어의 시초) 개발을 주도했던 앨런 케이가 이끄는 팀이었습니다.

이 팀은 사용자들이 GUI를 쉽게 사용하게 하기 위해 개선하는 데 집중했습니다. 이 과정에서 트리베 린스카우그는 한 가지를 깨닫게 됩니다. 바로 사용자의 멘탈 모델과 컴퓨터의 정보 처리 방식이 다르다는 것이었습니다.

그래서 트리베 린스카우그는 멘탈 모델을 컴퓨터 모델로 변환하는 과정을 위한 구조로 MVC를 고안합니다. 아래 그림은 린스카우그가 MVC에 대해 작성한 논문의 일부입니다. 이 논문에는 이런 구절이 있습니다. “이 이야기는 인간과 기계의 간극을 이어주는 것에 포커스하고 있다.”

인간과 기계의 간극 - MVC 논문
인간과 기계의 간극 - MVC 논문

MVC의 본질과 MVC가 해결하는 것

그는 “MVC의 본질적인 목적은 사용자의 정신 모델과 컴퓨터에 존재하는 디지털 모델 사이의 격차를 해소하는 것”, “이상적인 MVC 솔루션은 도메인 정보를 직접 보고 조작하는 사용자의 환상을 지원한다.”, “이 구조는 사용자가 다른 관점에서 동일한 모델을 동시에 보아야 하는 경우에도 유용하다.”라고 말합니다.

MVC가 해결하는 것
MVC가 해결하는 것

MVC 패턴의 관심 분리

이 패턴의 핵심은 역할에 따라 모듈을 분리하는 것입니다. 즉, 관심을 분리하는 것입니다. 이렇게 해서 MVC 패턴은 멘탈모델을 컴퓨터 모델로 잘 변환할 수 있고, MVC와 그 파생 패턴들이 40년이 지간 지금도 다양한 아키텍처의 바탕으로 활용되고 있습니다.

MVC의 관심 분리
MVC의 관심 분리

현재의 MVC는 주어진 상황에 맞게 약간씩 변형된 형태로 구성되지만 표준적인 구성은 아래 그림과 같습니다. 컨트롤러가 뷰를 통해서 사용자 이벤트를 전달받으면 모델을 수정하기 위한 명령을 생성하고 전달합니다. 모델이 수정되면 변경을 토대로 뷰가 갱신됩니다. 이 MVC 패턴은 사실 여러 패턴이 결합된 복합 패턴입니다.

현대적 MVC 패턴의 구조
현대적 MVC 패턴의 구조

모델은 전형적인 옵저버 패턴의 서브젝트의 예입니다. 뷰나 컨트롤러는 모델의 변경 상태에 귀를 기울이고 있지만 모델은 이 둘의 존재를 몰라야 합니다. 또한 런타임의 뷰나 컨트롤러가 라이프 사이클에 의해서 사라질 때 구독이 해지될 수 있어야 합니다.

모델의 옵저버 패턴
모델의 옵저버 패턴

뷰를 이루고 있는 GUI 구성 요소는 흔히 중첩된 구조의 윈도우, 패널, 레이블, 버튼 등으로 구성됩니다. 컴포지트 패턴은 이러한 중첩 구조를 단일 객체처럼 다루는 패턴으로 중첩을 추상화합니다.

뷰의 컴포지트 패턴
뷰의 컴포지트 패턴

전략 패턴은 컨트롤러의 전형적인 패턴입니다. 뷰는 컨트롤러에게 이벤트를 전달할 뿐 모델의 변경에는 관여하지 않습니다. 컨트롤러의 로직이 어떻게 구성되어 있는가에 따라서 모델이 변경되는 방식이 결정됩니다. 그리고 뷰와 연결된 이러한 컨트롤러는 런타임에 변경될 수 있습니다.

컨트롤러의 전략 패턴
컨트롤러의 전략 패턴

위 패턴들에 대해서는 뒷장에서 더 자세히 살펴보겠습니다.


그래픽 편집기 아키텍처

이번에는 그래픽 편집기의 기본 구조에 대해 알아보겠습니다. 아래 그림은 그래픽 편집기의 외부 구조입니다. 바깥을 감싸고 있는 워크벤치가 있고, 메뉴와 툴바 그리고 왼쪽에 툴이 있습니다. 중앙에는 캔버스가 있고 오른쪽 위에서 아래로 보면 프로퍼티, 팔레트, 히스토리, 레이어가 있습니다. 그리고 제일 하단의 스테이터스까지 확인할 수 있습니다.

그래픽 편집기의 외부 구조
그래픽 편집기의 외부 구조

이번 장에서는 위 그림에 주황색으로 표시된 툴, 캔버스 그리고 히스토리 영역을 구현하면서 디자인 패턴에 대해 알아보겠습니다.

내부 장치

내부 구조는 애플리케이션마다 달라서 일반화하기 어렵지만 다른 애플리케이션들도 유사한 컴포넌트를 가졌을 것이라 생각됩니다. 그래픽 에디터는 내부적으로 아래와 같이 구성되어 있습니다.

그래픽 편집기의 내부 장치
그래픽 편집기의 내부 장치
  • 모델: 편집기가 편집할 수 있는 고유한 도메인 모델
  • 그래픽 뷰어: 그래픽 이벤트를 기반으로 편집이 일어나는 공간
  • 이벤트 디스패처: 웹브라우저의 저 수준 이벤트를 이벤트 에디터 수준의 고수준 이벤트로 전환
  • 커맨드 스택: 히스토리의 undo, redo를 지원
  • 루트 파트: 편집을 위한 최소 단위는 파트로 구성되어 있고, 파트가 편집 행위를 수행
내부 장치의 RootPart
내부 장치의 RootPart
  • : 외부 구조의 툴을 구현
  • 액션 레지스트리: 단축키 처리
  • 리퀘스트: 이벤트를 추상화한 것
  • EditPolicy: 실제 편집을 집행하는 마이크로 컨트롤러

다음 글에서는 위 내부 장치들에 대한 자세한 소개와 함께 가상의 그래픽 편집기를 구현해 보면서 디자인 패턴을 어떻게 적용할 수 있는지 알아보겠습니다.


웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴 (2) | 요즘IT

웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴 (2)

그래픽 편집기 구현과 디자인 패턴 적용

이번 장에서는 가상의 그래픽 편집기를 구현해 보면서 5가지 디자인 패턴을 어떻게 적용할 수 있는지 살펴보겠습니다. 자세한 구현보다는 전체적인 흐름과 개념에 집중해서 따라오시면 좋을 것 같습니다.

1. 저장된 모델로부터 컨트롤러 (Part) 생성하기

우리가 만들 가상의 그래픽 편집기는 선택 툴, 사각형 툴, 펜 툴을 지원한다고 가정하겠습니다. 그러면 이 그래픽 편집기의 도메인 모델은 사각형, 원, 그리고 경로로 구성된 아래와 같은 JSON으로 표현할 수 있습니다.

그래픽 편집기의 도메인 모델
그래픽 편집기의 도메인 모델

이제 우리의 첫 번째 목표인 “모델로부터 컨트롤러 생성하기"에 대해 알아보겠습니다. 앞서 내부 구조가 아래 그림과 같이 구현되어 있다고 말씀드렸습니다. 아래 그림에서 강조한 부분이 생성에 관여하는 구조물입니다. 그래픽 에디터는 모델을 읽어서 그래픽 뷰어에 나타냅니다. 그래서 모델이 그래픽 에디터에 들어가면 그래픽 뷰어에 나타나게 됩니다.

모델로 부터 컨트롤러(파트) 생성하기
모델로 부터 컨트롤러(파트) 생성하기

이 과정을 수도 코드로 표현하면 다음과 같습니다. 왼쪽 코드를 보면 그래픽 에디터가 그래픽 뷰어에게 모델을 파라미터로 던지면서 파트를 만들라고 합니다. 그러면 그래프 뷰어는 createParts를 호출하면서 모델을 파라미터로 받고 배열인 모델을 순회하면서 배열의 아이템들의 타입에 따라 각각의 파트를 만듭니다. 파트를 만들고 나서 addPart를 통해 렌더링을 하고 화면에 나타냅니다.

모델로 부터 컨트롤러를 생성하는 수도 코드
모델로 부터 컨트롤러를 생성하는 수도 코드

그런데 파트를 생성하는 부분이 어딘가 이상하지 않나요? 아래 표시한 부분의 코드는 도형이 추가될 때마다 수정되어야 합니다. 앞서 우리는 디자인 패턴을 이용해 변화하는 부분을 분리할 수 있다고 했습니다. 여기서는 심플 팩토리를 이용해서 변화하는 부분을 분리해 보겠습니다.

파트를 생성하는 비효율적인 코드
파트를 생성하는 비효율적인 코드

팩토리 삼형제

해결 방법을 알아보기 전에 팩토리에 대해 알아보겠습니다. 팩토리에는 세 가지 종류가 있습니다.

  1. 심플 팩토리: 아래 패턴들을 만들기 위한 기본 클래스
  2. 팩토리 메서드 패턴: 추상 클래스를 상속받고 메서드를 구현하는 패턴
  3. 추상 팩토리 패턴: 인터페이스를 구현하고 구성(컴포지션)으로 제품군을 구현하는 패턴

이번 예제는 심플 팩토리만으로 해결하는 데 충분하기 때문에 심플 팩토리를 이용하겠습니다.

해결 방법 - 변화하는 부분 분리하기

앞서 표시한 코드에서 if-else 조건문을 분리해서 PartFactory로 옮깁니다. 그리고 PartFactory는 분리되었기 때문에 PartFactory를 호출하는 GraphicViewer에서는 더 이상 타입을 알 필요가 없어집니다. 따라서 createParts 함수는 모델을 순회하면서 PartFactory에게 파트만 만들어 달라고 호출하면 됩니다. 즉, 타입이 늘어나거나 변경되어도 GraphViewer는 변경될 필요 없고, 오로지 PartFactory만 수정하면 됩니다.

변화하는 부분 분리하기
변화하는 부분 분리하기

위 방법을 통해 얻을 수 있는 장점은 2가지가 있습니다.

  1. 개방-폐쇄 원칙 (OCP, Open-Closed Principle)
  2. 의존성 역전 원칙 (DIP, Dependency Inversion Principle)

심플 팩토리를 이용해서 확장에는 열려 있고, 수정에는 닫혀 있는 OCP 원칙을 지키게 됩니다. 또한, 의존성을 심플 팩토리로 보내고 파트라는 추상에 의존하게 되는 DIP 원칙도 지킬 수 있습니다.

2. 컨트롤러로부터 뷰(Figure) 생성하기

이번에는 뷰 생성하기를 살펴보겠습니다. 앞서 살펴본 모델은 1차원 모델이었습니다. 그러나 우리는 1차원 모델만 편집하지 않고 중첩 구조나 레이어, 그룹 등도 사용합니다. 이러한 다차원 구조를 표현하려면 어떻게 해야 할까요? 중첩된 모델을 생각해 보면 아래 그림과 같습니다.

중첩 모델
중첩 모델

중첩된 모델을 쉽게 다룰 수 있는 패턴이 있습니다. 바로 컴포지트 패턴입니다. 컴포지트 패턴은 클라이언트가 개별 객체와 복합 객체를 같은 방법으로 다룰 수 있게 합니다.

컴포지트 패턴
컴포지트 패턴

이 컴포지트 패턴이라는 개념은 부분-전체 계층 구조라는 개념에서 출발합니다. 하나의 트리 노드를 단일 객체인 것처럼 다룹니다. 즉, 아래 그림의 회색으로 칠해진 그룹 영역이 하늘색 노드 객체와 동일한 것처럼 다루는 구조입니다.

부분-전체 계층 구조
부분-전체 계층 구조

그래서 이 패턴은 그래픽 유저 인터페이스를 구현할 때 많이 사용되고, 리액트의 렌더링 과정과도 굉장히 유사하다는 것을 알 수 있습니다.

이 패턴을 사용하면 개별 뷰나 중첩된 뷰를 같은 타입으로 취급해서 렌더링 할 수 있습니다. 아래 왼쪽 코드를 보면 addChild 함수는 파트를 파라미터로 받는데, 이 파트는 단일 객체일 수도 있고 복합 객체일 수도 있습니다. 그러나 그것과 상관없이 자식으로 추가할 수 있습니다. 다음으로 render() 코드를 보면 모델을 처음 읽어드렸거나 변경되었을 때 파트가 렌더링을 호출합니다. 즉, 자신을 렌더링하고 자식을 순회하면서 렌더링 합니다.

그러나 이 부분에 문제가 하나 있습니다. 텍스트 파트같이 자식을 갖지 못하는 구성 요소인 경우에는 오른쪽 코드와 같이 addChild가 호출되었을 때 에러를 던지는 코드가 필요합니다.

컴포지트 패턴
컴포지트 패턴

정리하면, 중첩된 뷰의 렌더링과 같은 작업을 간단한 코드를 통해 전체 구조를 대상으로 반복해서 적용할 수 있습니다.

컴포지트 패턴의 특징

앞선 예제는 SRP(Single Responsibility Principle: 단일 책임 원칙)를 위배했습니다. 그래서 컴포지트 패턴은 SRP를 버리고 투명성을 취급하는 패턴으로 알려져 있습니다. 여기서 투명성이란 개별 객체와 복합 객체를 같은 타입으로 바라보는 것을 의미합니다. 이 때문에 타입 안정성이 떨어집니다. 이전 예제의 텍스트 파트처럼 논리에 맞지 않는 동작이 생길 수도 있고, 이경우 이를 막아야 합니다. 따라서 이 패턴을 설계할 때 투명성과 안정성의 균형이 필요한 패턴이라고 말합니다.

컴포지트 패턴의 특징
컴포지트 패턴의 특징

3. 툴을 이용해 모델 수정하기

다음으로 툴을 이용해 모델을 수정하는 것에 대해 알아보겠습니다. 편집 과정과 관련된 구조는 아래 그림처럼 이벤트 디스패처, 커맨드 스택, 루트 리포트, 툴, 리퀘스트, EditPolicy가 있습니다.

편집과 관련된 구조
편집과 관련된 구조

앞에서 편집 작업은 “이벤트를 상태변화로 전환하는 과정"이라고 했습니다. 이 과정에는 특별한 장치가 하나 있습니다. 바로 “툴”입니다. 그리고 이 과정을 앞서 소개 드린 구조를 곁들여 자세히 살펴보면 아래 그림과 같습니다.

이벤트 흐름 내부의 구조
이벤트 흐름 내부의 구조

이벤트 디스패처는 브라우저의 저수준 이벤트를 에디터의 고수준 이벤트로 변환하는 역할을 합니다. 이벤트 디스패처가 마스킹 레이어를 통해 이벤트를 수신하고, 이벤트를 가공해서 캔버스로 전달하는 과정입니다.

툴의 특징

앞서 설명한 툴은 “같은 이벤트라도 선택에 따라서 다른 결과가 나와야 한다”는 특징이 있습니다. 예를 들어 사각형 툴을 선택한 상태라면 드래그했을 때 사각형이 그려져야 하고, 펜 툴을 선택한 상태라면 펜이 그려져 합니다. 즉, 그래픽 에디터는 한 번에 한 개의 상태를 가집니다.

툴 구현하기

왼쪽 코드는 이벤트 디스패처의 코드입니다. 이벤트를 리스닝 하는 부분을 수도코드로 표현했습니다. 예를 들어 mousemove 이벤트를 받아서 transmitMouseMove를 호출합니다. transmitMouseMove에서는 뷰어에게 이벤트를 전달합니다. 오른쪽의 뷰어 코드에서 이벤트를 받아 툴의 상태에 따라 사각형, 원, 펜 등의 각각에 맞는 요소를 화면에 그립니다.

툴 구현
툴 구현

위 코드에서도 문제점이 있습니다. 툴이 추가될 때마다 오른쪽에 표시된 if-else 문이 계속 늘어나야 합니다. 즉, GraphicViewer 코드가 닫혀 있지 않은 상태가 됩니다. 또한, 유사한 코드가 반복되고 있습니다.

해결 방법 - 상태 패턴

위 문제점을 해결하는 방법이 바로 상태 패턴입니다. 오른쪽의 코드를 보면 if-else 문을 제거하고 receiveEvent 부분에서 툴에게 바로 이벤트를 전달했습니다. 이 툴은 추상 클래스가 되는데 특정 상태에 대한 처리를 특정 클래스에게 모두 위임합니다. 즉, 현재 상태를 하나의 클래스로 나타내는 것이 바로 상태 패턴입니다. 이렇게 하면 특정 상태에서의 행동을 고립시킬 수 있어 코드 관리가 쉬워지고, 상속을 통해 반복되는 코드 문제를 해결할 수 있습니다.

상태 패턴을 통해 해결한 코드에서 GraphicViewer는 확장에 열려있게 되고, 툴은 변경에 닫혀 있게 되어 OCP를 구현할 수 있게 됐습니다.

상태 패턴
상태 패턴

정리하면, 앞선 예제의 사각형, 원, 펜 툴은 셀렉션 툴이라는 클래스에게 특정 상태에 대한 처리를 모두 위임합니다. 이렇게 하면 실제 기능을 위해 셀렉션 툴을 상속받아 사용하기만 하면 됩니다.

4. MVC 구조를 완성하자

이번에는 지금까지의 이벤트 흐름을 다시 살펴보며 MVC 구조를 완성해 보겠습니다. 방금 툴에 대해 알아봤는데, 그렇다면 조금 전 예제와 같이 툴이 직접 모델을 수정하는 것이 바람직할까요? MVC 구조에서 모델 수정 방법은 누가 알고 있어야 할까요?

예를 들어 비트맵을 지우는 지우개 도구가 있다고 가정하겠습니다. 이 비트맵 지우개는 비트맵만 지우는 도구이기 때문에 왼쪽의 비트맵 그림은 지울 수 있어야 하고, 오른쪽의 벡터 그림은 지울 수 없어야 합니다.

이런 상태에서 지우개 도구가 직접 모델을 수정하려면 지울 수 있는 파트와 지울 수 없는 파트를 모두 알고 있어야 합니다. 그러면 앞으로 파트가 늘어날 때마다 지울 수 있는 요소인지 아닌지 다 알고 있어야 합니다. 즉, 요소들이 늘어날 때마다 코드를 계속 수정해야 합니다. 따라서 지우개 도구의 개방 폐쇄 원칙을 어기게 됩니다.

이를 해결하기 위해서는 파트 스스로 이벤트에 대한 정보를 바탕으로 이벤트를 어떻게 해석해야 하는지 알고 있어야 합니다. 지우개가 요소를 지우라고 요청하면 파트가 이 이벤트를 받아 처리하는 것이죠.

지우개 도구의 이벤트 처리
지우개 도구의 이벤트 처리

더 근본적으로 생각해 보겠습니다. MVC를 사용한다면 모델을 누가 수정할까요? 바로 컨트롤러입니다. 우리의 예제에서는 파트가 컨트롤러라고 할 수 있습니다. 따라서 파트는 이벤트를 어떻게 해석하는지 알고 있어야 하고 모델을 수정할 수 있어야 합니다. 그리고 툴은 파트의 이벤트를 바로 전달하는 것이 아니라 리퀘스트라는 형태로 가공해서 모델 수정을 요청합니다. 이렇게 하면 다양한 편집 요청을 리퀘스트에 담을 수 있습니다. 여기서 리퀘스트는 하이레벨 이벤트라고 할 수 있습니다.

그럼 파트 내부에 모델 수정 로직을 직접 넣으면 될까요? 이것도 문제가 됩니다. 왜냐하면 유사한 편집 로직이라면 파트 안에서도 중복 로직이 발생할 수도 있습니다. 이렇게 되면 파트 안에서 또다시 개방 폐쇄 원칙이 무너집니다.

파트가 수정 로직을 알고 있을때의 문제점
파트가 수정 로직을 알고 있을때의 문제점

해결 방법 - EditPolicy 패턴

위 문제를 스마트하게 해결하는 구조가 바로 EditPolicy입니다. EditPolicy란 파트가 알고 있어야 하는 모델 수정 로직을 파트가 재활용할 수 있게 완전히 분리해서 마이크로 컨트롤러로 만든 것입니다.

EditPolicy
EditPolicy

아래 코드는 툴의 드래그 동작을 나타내는 코드입니다. 마우스 다운 이벤트가 일어나면 드래그가 시작되고 마우스 업 이벤트가 일어나면 드래그 엔드가 호출됩니다. 드래그 앤드에서는 모델을 바꾸라고 changeModel 함수를 호출합니다. 그리고 ResizeToolchangeModel이 아래와 같이 특정 작업을 진행합니다.

EditPolicy 예제 - 리사이즈 도구
EditPolicy 예제 - 리사이즈 도구

이제 ResizeTool에서 Policy를 분리해 보겠습니다. 모델 수정 코드를 ResizeTool에서 ResizePolicy로 이동시킵니다. 이렇게 하면 ResizeToolchnageModel은 추상화될 수 있습니다. 그리고 파트에 installPolicy 함수를 추가해서 policy를 파라미터로 받을 수 있게 하여 다중 정책을 처리할 수 있게 합니다. 즉 policy가 배열이 되고 여러 가지 컨트롤러 로직을 받을 수 있습니다.

EditPolicy 추출
EditPolicy 추출

즉, ResizeTool은 요청만 보내면 되고 ResizeTool에서 하던 changeModel 부분이 없어지고, 상위 클래스인 툴에서 처리하게 됩니다.

상위 클래스에 위임
상위 클래스에 위임

전략 패턴

방금 살펴본 EditPolicy는 전략 패턴입니다. 변하는 부분을 찾아내서 툴과 파트와 같이 변하지 않는 부분을 분리하고 모델 수정 작업을 Policy에 위임합니다. 파트는 Policy를 선택해서 런타임을 포함해 다양한 상황에서 작동 방식을자유롭게 바꿀 수 있습니다. 또한 알고리즘을 재사용하기 쉽습니다.

전략 패턴의 특징
전략 패턴의 특징

5. 작업 히스토리 관리하기

이 부분은 히스토리를 구현하는 undo, redo와 관련된 부분입니다. 지금까지 살펴본 이벤트 흐름은 아래 그림과 같이 이벤트 디스패처에서 그래프 뷰어, 그리고 툴과 파트를 통해 EditPolicy가 모델을 수정합니다.

EditPolicy의 커맨드 생성
EditPolicy의 커맨드 생성

앞에서 봤던 EditPolicy 과정을 한 번 더 개선하면, EditPolicy가 모델을 직접 수정하는 것이 아니라 EditPolicy는 커맨드라는 객체를 생성합니다. 이 커맨드는 커맨드 스택 안에 담겨 전달되고 커맨드 스택이 모델을 변경합니다. 커맨드 스택 안에는 undo, redo와 같은 모델을 수정하기 위한 명령들이 캡슐화돼서 하나씩 쌓여있습니다.

커맨드 스택
커맨드 스택

지금까지의 흐름을 최종적으로 정리하면 아래 그림과 같습니다. 툴에서 이벤트를 받고 파트의 EditPolicy에서 커맨드를 만들라고 요청하고 이 커맨드로 모델을 수정합니다. 모델이 수정되면 다시 파트에서 리프레시를 요청하고 뷰에서 갱신됩니다.

최종적인 MVC의 흐름
최종적인 MVC의 흐름

맺음말

지금까지 ​​웹 기반 그래픽 편집기의 구조를 통해 7가지 디자인 패턴에 대해 알아봤습니다. 또, 실제 구현 코드를 직접 개선하며 디자인 패턴에 대해 생각해 봤습니다.

오늘 소개한 내용은 디자인 패턴에 대한 한 가지 경험일 뿐입니다. 제가 공유드린 경험이 그래픽 편집기를 만들 때 의미 있는 내용이 되기를 바라고, 실제 디자인 패턴을 적용하는 데 도움이 되기를 바랍니다. 다양한 상황에서 경험을 통해 더 나은 패턴을 찾고 이를 다른 사람들에게 공유하고 함께 적용해 보시길 바랍니다. 감사합니다.


이찬희 (MarkiiimarK)
Never Stop Learning.

  1. FEConf2023에서 발표된 ‘웹 기반 그래픽 편집기의 구조와 7가지 디자인 패턴’/심흥운 네이버 프론트엔드 엔지니어 ↩︎