본문 바로가기
project

전략 패턴, 팩토리 패턴으로 데이터 처리 로직 리팩토링

by qjatjs123123 2025. 4. 30.

 

 

안녕하세요!

 

프론트엔드 개발이라면, 한 번쯤 백엔드에서 받아온 데이터를 그룹화하거나 정렬하는 등의 데이터 처리를 해본 경험이 있을 것입니다.

 

최근 디자인 패턴을 공부하고 직접 적용하면서, 데이터 처리 관련하여 리팩토링 한 경험을 공유하고자 합니다.

 

 


 

 

 

디자인 패턴

전략 패턴

전략 패턴은 실행 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 입니다.

 

즉, 어떤 일을 수행하는 알고리즘이 여러가지 일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴입니다.

 

 

→ 여기서, 저는 다양한 그룹핑 및 정렬 알고리즘에 전략 패턴을 적용해 리팩토링을 진행했습니다.

 

 

 

팩토리 패턴

객체 생성을 공장(Factory) 클래스로 캡슐화 처리하여 대신 생성하게 하는 생성 디자인 패턴 입니다.

 

즉, 클라이언트에서 직접 new 연산자를 통해 제품 객체를 생성하는 것이 아닌, 제품 객체들을 도맡아 생성하는 공장 클래스를 만들고, 이를 상속하는 서브 공장 클래스의 메서드에서 여러가지 제품 객체 생성을 각각 책임 지는 것이다.

 

 

→ 클라이언트 코드에서 직접 객체를 생성하는 대신, 객체 생성을 팩토리 클래스에 위임함으로써 책임을 분리했습니다. 

 

 

 


 

 

 

코드

전략 패턴

 

전략 패턴은 다음과 같이 다이어그램으로 나타낼 수 있습니다.

 

 

다음과 같은 다이어그램으로 코드를 구현해 봤습니다.

 

interface Group {
  groupBy(datas: ConsumptionDataProps): any;
}

class Date implements Group {
  groupBy(datas: ConsumptionDataProps): any {
    const result: FilterByDate = {};

    datas.consumptions.forEach((data: ConsumptionData) => {
      const curDate = data.transactionTime.split(' ')[0];

      if (curDate in result) {
        result[curDate].push(data);
      } else {
        result[curDate] = [data];
      }
    });

    return Object.entries(result).map(([date, items]) => ({
      date,
      dayText: `${Number(date.split('-')[2])}일 ${getDayByDate(date)}`,
      items,
    }));
  }
}

class Category implements Group {
  groupBy(datas: ConsumptionDataProps): any {
    const result: Record<TypeKey, any[]> = {
      ACCOUNT: [],
      CARD: [],
      ETC: [],
    };

    const koreanCategory: Record<TypeKey, string> = {
      ACCOUNT: '계좌',
      CARD: '카드',
      ETC: '기타',
    };

    datas.consumptions.forEach(data => {
      result[data.category].push(data);
    });

    return (Object.entries(result) as Array<[TypeKey, any[]]>).map(([dayText, items]) => ({
      dayText: koreanCategory[dayText],
      items,
    }));
  }
}

export class GroupingStrategy {
  private strategy: Category | Date;

  constructor(strategy: Category | Date) {
    this.strategy = strategy;
  }

  groupBy(datas: ConsumptionDataProps): any {
    return this.strategy.groupBy(datas);
  }
}

 

  • Group이라는 인터페이스를 정의하고, groupBy 메서드를 추상화 했습니다.
  • Date(일자)를 기준으로 그룹핑하는 알고리즘 구현을 구체화 했습니다.
  • Category(카테고리)를 기준으로 그룹핑하는 알고리즘 구현을 구체화 했습니다.
  • GroupingStrategy는 전략을 등록하고 실행하는 컨텍스트를 만들었습니다.

 

Date, Category 외의 그룹핑 방식이 필요하다면, Group 인터페이스를 구현한 새로운 전략 클래스를 추가하기만 하면 됩니다.

 

OCP 원칙 - 확장에는 열려있고, 변경에 닫혀 있는 원칙을 지키게 됩니다. 

 

 


 

 

팩토리 패턴

export class GroupFactory {
  createGroup(type: string) {
    switch (type) {
      case '날짜':
        return new Date();
      case '카테고리':
        return new Category();
      default:
        throw new Error('');
    }
  }
}

 

 

클라이언트에서 직접 객체를 생성하는 대신, 팩토리에 객체 생성 책임을 위임하도록 구성했습니다.

 

그 결과, 클라이언트 코드에서 변경할 필요 없이, 팩토리 코드만 변경하면 됩니다.

 

더 나아가, switch 분기문 대신, Map 방식으로 리팩토링 가능합니다.

 

 

 


 

 

클라이언트 코드

//... 코드생략

const filterData = useCallback(
    (data: any): any => {
      const groupingStrategy = groupFactory.createGroup(groupState);
      const groupingAlgorithm = new GroupingStrategy(groupingStrategy);

      return sorted(groupingAlgorithm.groupBy(data));
    },
    [groupFactory, groupState, sorted]
  );

  return useSuspenseQuery<any, Error>({
    queryKey: [`ConsumptionByMonthKey_${month}`],
    queryFn: async () => {
      const response = await http.get<any>(`/api/consumptions?month=${month}`);
      return response;
    },
    select: data => filterData(data),
    staleTime: 10000,
    retry: 3,
  });
}

 

  • GroupFactory에서 원하는 전략을 가져옵니다.
  • GroupStrategy 컨텍스트에 해당 전략을 주입하여 사용합니다.
  • 해당 전략을 groupBy 실행합니다.

 

 

 

백엔드에서 데이터를 다양한 알고리즘으로 처리할 때, 전략 패턴, 팩토리 패턴을 사용하면 리팩토링 가능합니다.

 

감사합니다.