Skip to content

Latest commit

 

History

History
186 lines (141 loc) · 7.02 KB

File metadata and controls

186 lines (141 loc) · 7.02 KB

Graphic-editor 아키텍처 개선 작업

@lerrybe

  • 2024.07

문제점 및 리팩토링의 목적

  1. 기능을 추가할 때 비효율성
  • 새로운 기능을 추가할 때 여러 클래스를 동시에 수정해야하는 문제 (높은 결합도)
  • 새로운 유형의 도구나 기능을 추가하기 위해 기존 코드를 수정해야함 (확장성 부족)
  1. 공동 작업자가 들어왔을 때의 비효율성
  • 코드 구조가 복잡하고 책임 분리가 명확하지 않아, 코드 온보딩에 시간 소요
  • 의존성 파악의 문제, 클래스 간의 강결합으로 인해 한 부분의 변경이 발생시키는 나비효과 예측이 어려움
  • 추후 테스트에 문제가 발생할 것이라 예측 (이 모듈에서는 무엇을 테스트해야하는가?의 어려움)
  1. 상태 관리 라이브러리의 이원화
  • 옵저빙을 하기 위해 에디터 쪽에서만 mobX를 사용하고 있어, 통일성이 떨어짐 → 옵저빙 하는 것은 직접 구현하자

⇒ 결국 개발 효율성을 위한 투자다!

현재 코드 분석

  • 기존 코드 작업자 @lerrybe

  • React + MobX(상태관리)를 사용

  • 클래스 기반의 객체 관리, Tool, Shape 등의 요소들이 밀접하게 연관되어 있음

  • MVC (Model-View-Controller) 패턴에 기반하려고 하지만, 한 클래스가 여러 책임을 지고 있어 상태 추적 및 유지보수가 어려움

    Model:

    • Stage, ActionExecuter, StageStore 등의 클래스가 데이터와 비즈니스 로직을 담당

    • 이 클래스들이 데이터 관리뿐만 아니라 UI 관련 로직도 포함하고 있어, 단일 책임 원칙(S)을 위반

    • 예시

      1. Stage 클래스

        // Stage.ts
        
        export class Stage extends Konva.Stage {
          // ... (생략)
          onMouseDown = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
            // 이벤트 처리 로직
          };
          // ... (다른 이벤트 핸들러들)
        }

        이 클래스는 데이터 모델, 비즈니스 로직, 이벤트 핸들링을 모두 담당하고 있어 너무 많은 책임을 가지고 있음

      2. StageStore 클래스

        class StageStore {
          stage: Stage | null = null;
          colors = { bg: "#e9e9e9" };
          // ... (생략)
        }

        이 클래스는 Model의 역할을 하지만, UI 관련 데이터(colors)도 포함하고 있어 관심사 분리가 불명확

      3. DrawTool 클래스

        export class DrawTool extends BaseTool {
          // ... (생략)
          onMouseDown = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
            // 이벤트 처리 및 그리기 로직
          };
          // ... (생략)
        }
    • Controller:

      • Stage 클래스의 이벤트 핸들러 메서드들 (onMouseDown 등)이 Controller 역할을 담당
      • Controller 로직이 Model(Stage 클래스)에 직접 구현되어 있어, 관심사 분리가 제대로 이루어지지 않음
  • SOLID 관련 기타 위반사항들 예시

    • 리스코프 치환 원칙(LSP) 위반:

      • BaseTool을 상속받는 DrawToolPointerTool 클래스들이 있는데, 이 클래스들이 BaseTool을 완전히 대체할 수 있는지 확실하지 않음. 예를 들어,

        export class DrawTool extends BaseTool {
          // BaseTool의 메서드를 오버라이드하지만,// 예상치 못한 동작을 할 가능성이 있음
          onMouseDown = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
            // DrawTool에 특화된 로직
          };
        }
    • 인터페이스 분리 원칙(ISP) 위반:

      • Stage 클래스가 너무 많은 메서드와 속성을 가지고 있어, 클라이언트(Stage를 사용하는 코드)가 실제로 사용하지 않는 메서드에도 의존하게 됨. (예를 들어, 클라이언트 A는 Stage의 메서드 중 3개만 사용하지만, 20개의 메서드가 있는 전체 Stage 클래스에 의존하게 되는 것)

        export class Stage extends Konva.Stage {
          // 많은 메서드들...
          copySelectedNodes = () => {
            /* ... */
          };
          pasteNodes = (x?: number, y?: number) => {
            /* ... */
          };
          deleteSelectedNodes = () => {
            /* ... */
          };
          // 등등...
        }
    • 의존관계 역전 원칙(DIP) 위반:

      • 고수준 모듈이 저수준 모듈에 직접 의존하고 있음. 예를 들어, Stage 클래스가 Konva.Stage를 직접 상속받고 있음. 이는 Konva.Stage의 변경이 Stage 클래스에 직접적인 영향을 미친다는 유연성 부족의 문제로 이어질 수 있음. 고수준의 애플리케이션 로직이 구체적인 구현 (Konva.Stage)에 직접 의존하는 것이 바람직하지 않음, 추상화와 의존성 주입을 통해 해결해야 함

        export class Stage extends Konva.Stage {
          // Konva.Stage에 직접 의존
        }
    • 개방-폐쇄 원칙(OCP) 위반: 새로운 기능을 추가하려면 기존 클래스를 수정해야 할 가능성이 높음. 예를 들어, 새로운 도구를 추가하려면 Stage 클래스를 수정해야 함

      ```tsx
      export class Stage extends Konva.Stage {
        setTool = (toolType: ToolType) => {
          switch (toolType) {
            case ToolType.DRAW:
              this.tool = new DrawTool({ stage: this })
              break
            case ToolType.POINTER:
              this.tool = new PointerTool({ stage: this })
              break
            // 새로운 도구를 추가하려면 여기를 수정해야 함
          }
        }
      }
      ```
      
    • 단일 책임 원칙(SRP) 추가 위반: _ 더 자세히 보면 ActionExecuter 클래스도 여러 책임을 가지고 있음. 실행, 취소, 다시 실행 등의 책임이 한 클래스에 모여 있음

      export class ActionExecuter {
        commitHistory = (action: Action) => {
          // ...
        };
        undo = () => {
          // ...
        };
        redo = () => {
          // ...
        };
        execute = (action: Action) => {
          // ...
        };
      }
  • 타 라이브러리와의 강결합 및 미들웨어 부재

    • 현재는 많은 컴포넌트 / 모듈들이 Konva.js (현재 사용하고 있는 캔버스 라이브러리)의 존재를 알고 있음
    • 내부 코드에서는 Konva에 대한 의존성이 없어야, (추상화 계층을 강화해야) 후에 Konva를 떼어내고 순수 Canvas로 구현하기에 용이할 것이라 생각
    • 변화에 유연하기 위해 추상화, 캡슐화가 필요해보임.

왜 이렇게 짜여졌는가?

  • 프로토타입 특성상 빠른 개발 속도
  • 기술적 부채의 수용: “일단 동작하게 만들자”
  • 초기 요구 사항의 단순성

개선 작업

요약

  • M(V)C 적용
  • 컴포넌트 단에서의 외부 라이브러리에 대한 직접 참조 제거

구체적인 내용