@lerrybe
- 2024.07
- 기능을 추가할 때 비효율성
- 새로운 기능을 추가할 때 여러 클래스를 동시에 수정해야하는 문제 (높은 결합도)
- 새로운 유형의 도구나 기능을 추가하기 위해 기존 코드를 수정해야함 (확장성 부족)
- 공동 작업자가 들어왔을 때의 비효율성
- 코드 구조가 복잡하고 책임 분리가 명확하지 않아, 코드 온보딩에 시간 소요
- 의존성 파악의 문제, 클래스 간의 강결합으로 인해 한 부분의 변경이 발생시키는 나비효과 예측이 어려움
- 추후 테스트에 문제가 발생할 것이라 예측 (이 모듈에서는 무엇을 테스트해야하는가?의 어려움)
- 상태 관리 라이브러리의 이원화
- 옵저빙을 하기 위해 에디터 쪽에서만 mobX를 사용하고 있어, 통일성이 떨어짐 → 옵저빙 하는 것은 직접 구현하자
⇒ 결국 개발 효율성을 위한 투자다!
-
기존 코드 작업자 @lerrybe
-
React + MobX(상태관리)를 사용
-
클래스 기반의 객체 관리, Tool, Shape 등의 요소들이 밀접하게 연관되어 있음
-
MVC (Model-View-Controller) 패턴에 기반하려고 하지만, 한 클래스가 여러 책임을 지고 있어 상태 추적 및 유지보수가 어려움
Model:
-
Stage
,ActionExecuter
,StageStore
등의 클래스가 데이터와 비즈니스 로직을 담당 -
이 클래스들이 데이터 관리뿐만 아니라 UI 관련 로직도 포함하고 있어, 단일 책임 원칙(S)을 위반
-
예시
-
Stage 클래스
// Stage.ts export class Stage extends Konva.Stage { // ... (생략) onMouseDown = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { // 이벤트 처리 로직 }; // ... (다른 이벤트 핸들러들) }
이 클래스는 데이터 모델, 비즈니스 로직, 이벤트 핸들링을 모두 담당하고 있어 너무 많은 책임을 가지고 있음
-
StageStore 클래스
class StageStore { stage: Stage | null = null; colors = { bg: "#e9e9e9" }; // ... (생략) }
이 클래스는 Model의 역할을 하지만, UI 관련 데이터(colors)도 포함하고 있어 관심사 분리가 불명확
-
DrawTool 클래스
export class DrawTool extends BaseTool { // ... (생략) onMouseDown = (e: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => { // 이벤트 처리 및 그리기 로직 }; // ... (생략) }
-
-
Controller:
Stage
클래스의 이벤트 핸들러 메서드들 (onMouseDown
등)이 Controller 역할을 담당- Controller 로직이 Model(Stage 클래스)에 직접 구현되어 있어, 관심사 분리가 제대로 이루어지지 않음
-
-
SOLID 관련 기타 위반사항들 예시
-
리스코프 치환 원칙(LSP) 위반:
-
BaseTool
을 상속받는DrawTool
과PointerTool
클래스들이 있는데, 이 클래스들이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 적용
- 컴포넌트 단에서의 외부 라이브러리에 대한 직접 참조 제거