DDD 도입 시도기

Taesol Kwon·2024년 2월 22일
0
post-custom-banner

제가 프로젝트에 합류했을 당시 Presentational & Container 패턴과 hooks 패턴이 결합된 형태로 만들어져있었고 폴더 뎁스가 7-8중까지 중첩되면서 props drilling 문제가 심각했었습니다.
그러다보니 컴포넌트간의 종속성이 강하게 결합되어 있었고 가독성이나 유지보수 측면에서도 유리하지 못하다고 판단을 했습니다.

이 프로젝트의 현재 문제점인 폴더구조와 props drilling을 해결할 필요가 있었습니다. 하지만 저희에겐 리팩토링 공수 기간이 3일밖에 없었습니다. 해당 기간 안에 끝내려면 너무 복잡하거나 러닝커브가 높은 리팩토링은 오히려 개발비용이 더 들기때문에 쉽사리 이를 실행하기 어려웠습니다.
해당 문제점들을 개선하기 위해서는 우선적으로 단순하면서도 명확하게 비즈니스의 흐름대로 구조를 변경할 필요가 있다고 생각했습니다. 현재 BE팀은 DDD(Domain Driven Design)가 이미 적용된 상태였고 그렇다면 FE팀에서도 DDD로 설계를 해서 전사적으로 시스템을 통일시키는게 나중에 MSA를 도입할 때(추후 예정된 계획)를 위해서 유리하겠지만 이를 제대로 적용하기엔 3일이란 시간은 충분하지 않았습니다.
그래서 저희는 추후에 점진적 도입을 하고 지금 당장은 느슨하게 적용을 시켜보기로 하였습니다. 사실 해당 프로젝트의 문제점을 해결하기 위해서 DDD는 필요한 해결수단은 아니지만 이를 설계하는 과정, 아키텍처를 설계하고, 도메인과 비즈니스 로직들에 대해 고민하는 시간들이 해당 문제점을 개선하는데 어느정도의 도움을 줄꺼라고 생각했습니다.

먼저, 그 당시 문제점을 가지고 있던 프로젝트 구조를 똑같진 않지만 간단한 예시로 가져와봤습니다.

/src
├── /components
├── /pages
    ├── /PageA
        ├── /Component1
	        ├── /Component1_A
		        ├── /Component1_A_a
			        ├── /Component1_A_a_a
        ├── /Component2
        ├── index.js
        └── PageA.module.css
    └── /PageB

위와 같이 계층적 폴더 구조를 사용함으로써, 뎁스가 너무 깊은 계층 구조로 인해 유지보수가 어렵다는 단점이 있습니다. 뎁스가 깊어지면 깊어질수록 props를 전달하는데 의미없이 전달만 하게되는 컴포넌트들이 너무 많아 지게 됩니다. 이에 따라, props drilling이라는 문제를 야기하게 되고, 이는 재사용성과 유지보수를 어렵게 하며 불필요한 리렌더링을 발생시키게 됩니다.

DDD(Domain Driven Desgin) 채택 이유

현재 프로젝트에서 도메인별로 관리하게 된다면, 도메인 별로 느슨한 의존도와 도메인 내에서는 높은 응집도를 가짐으로써, 유지보수나 확장성 측면에서 용이한 장점을 가지게 됩니다.

설계 방법 예시

  1. 도메인을 feature 기반으로 유저, 챗봇, 북마크, 인텐트, 히스토리 등으로 나눴습니다.
  2. 전체적인 구조를 변경하기 위해서 [[헥사고날 아키텍쳐]]를 기반으로 폴더를 계층화하는 작업 진행
    • UI 계층(React), Adapter계층(zustand의 store), Application 계층(비즈니스 로직), Domain 계층(도메인 Dto 정의), Infrastructure 계층(api)
    • 계층화 작업과 SOLID의 상관관계
      - SRP: 각 계층이 단일 책임을 가지게 됩니다.
      - OCP: 변경사항이 있거나 새로운 기능이 추가될 때, 기존 코드를 수정하지 않고 새로은 모듈이나 클래스를 추가할 수 있습니다.
      - LSP: 각 계층이 추상화를 기반으로 구성되어 있으므로, 하위 구현체를 상위 타입으로 대체가 가능합니다. 예를 들어, Adaptor에 zustand를 사용하고 있는데, 이를 다른 라이브러리로 대체가 가능합니다.
      - ISP: 각 계층은 필요한 인터페이스만 노출하게 됩니다. UI계층은 비즈니스 로직이나 API로직에 대한 세부사항을 알 필요가 없으며, Application 계층의 인터페이스만을 사용하게 됩니다.
      - DIP: 고수준 모듈(Application, Domain 계층)이 저수준 모듈(UI, Infra 계층)의 구현에 의존하지 않습니다. 이를 통해 각 계층은 독립성을 유지하게 됩니다.

헥사고날 아키텍처이란?

헥사고날 아키텍처 또는 '어댑터와 포트 아키텍처'는 소프트웨어 설계 패턴 중 하나로, 애플리케이션의 핵심 로직을 그 주변의 서비스나 외부 요소로부터 분리하는 방법을 제공합니다. 이 구조는 중앙에 위치한 애플리케이션의 핵심(비즈니스 로직)과 외부 요소(예: 데이터베이스, 웹 서비스, 사용자 인터페이스) 사이에 명확한 경계를 설정합니다. 한마디로, 외부와의 통신을 인터페이스로 추상화하고 비즈니스로직안에 외부의 코드나 로직의 주입을 막는것이 헥사고날의 핵심입니다. 바꿔 말하자면, 컴퓨터 본체가 제공하는 운영체제, 어플리케이션은 바뀌지 않지만 이를 출력하거나 입력을 하는 모니터, 키보드, 마우스와 같은 디자인들은 포트에 맞춰 변경 할 수 있습니다. 어플리케이션 중심으로 어플리케이션 외부의 모듈은 어댑터와 포트만 맞춘다면 변경 가능하도록 하는 것입니다.

UI계층

// UserComponent.js (UI 계층)
import React, { useState, useEffect } from 'react';
import { userStore } from '../stores/UserStore';

function UserComponent() {
    const [users, setUsers] = useState([]);

    useEffect(() => {
        userStore.fetchUsers().then(setUsers);
    }, []);

    return (
        <div>
            {users.map(user => <div key={user.id}>{user.name}</div>)}
        </div>
    );
}

Adaptor 계층

// UserStore.js (Adapter 계층)
import create from 'zustand';
import { userService } from '../services/UserService';

export const userStore = create(set => ({
    fetchUsers: async () => {
        const users = await userService.getUsers();
        set({ users });
    }
}));

Application 계층

// UserService.js (Application 계층)
import { userRepository } from '../repositories/UserRepository';

export class UserService {
    async getUsers() {
        return await userRepository.getUsers();
    }
}

Domain 계층

// UserDTO.js (Domain 계층)
export class UserDTO {
    constructor(id, name, email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

Infrastructure 계층

// UserRepository.js (Infrastructure 계층)
export const userRepository = {
    getUsers: async () => {
        // API 호출을 통해 사용자 데이터를 가져옴
        const response = await fetch('/api/users');
        const data = await response.json();
        return data.map(user => new UserDTO(user.id, user.name, user.email));
    }
}

이런 설계 방식은 사실 DDD를 온전히 잘 반영했다고 보기 어렵습니다. 오히려 헥사고날 아키텍처를 잘 반영했다고 볼 수 있습니다. 하지만 이전에 언급했듯이 느슨한 DDD를 적용하고 점진적으로 개선해나갈 예정이었습니다.처음부터 완벽한 설계는 할 수 없다고 생각합니다.

이렇게 리팩토링하면서 어떤 결과를 얻었나요?

전역상태 관리 라이브러리인 zustand를 도입함으로써, props drilling 문제를 완화 할 수 있었습니다.
계층화 작업으로 인해 명확한 책임 분리가 되어 서로 간의 결합도를 낮추고, 재사용성과 확장성을 향상 시켰습니다.

하지만 짧은 리팩토링 기간으로 인해 팀웜과 설계 방식의 혼란이 오는 부분들이 있어 개발 시 어려움을 겪었습니다. 확실히 러닝 커브가 낮은 작업은 아니었다고 볼 수 있습니다.

아쉬운점으로는 도메인을 나누는 과정에서 저희는 큰 feature를 기준으로 도메인을 나눴는데, 이는 실제로 좋은 도메인 모델링이 아니었다고 봅니다. 프로젝트의 비스니스 요구사항과 문제 영역을 정확히 이해하는 것이 중요한데, 이를 위해선 사실 팀 전체가 DDD 모델링에 같이 관여하고, 바운디드 컨텍스트를 설정하고, 팀이 공유하는 '유비쿼터스 랭귀지'를 개발하는 등 더 많은 작업과 노력을 했어야 했습니다.

profile
사진촬영을 좋아하는 프론트엔드 개발자입니다.
post-custom-banner

0개의 댓글