본문 바로가기

[Project] 프로젝트 삽질기49 (feat 전략 패턴 활용)

어가며

NestJS와 TypeORM을 활용하여 프로덕트를 만들고 있습니다. 비즈니스 로직을 구성하는데, if else if 문이 계속해서 추가되다 보니 코드의 가독성이 좋지 못한 문제가 발생했습니다. if - else 문을 개선하기 위해 노력하면서 전략 패턴에 대해 알 수 있었습니다. 이번 글은 전략 패턴을 NestJS에 적용하기 위해 노력하면서 작성된 글입니다. 

 

 

 

 


 

 

 

요구 사항 분석

유저를 멘션 할 수 있는 기능을 개발해야 했습니다. 멘션 기능을 구성하기 위해선, 먼저 유저 닉네임 검색 시스템을 개발해야 했습니다.

 

 

 

 

 

 

예를 들어 "@검색어"를 입력했을 때, 검색어와 관련된 유저의 닉네임의 목록들이 노출되어야 했습니다. 만약 검색어를 포함하지 않고, "@"만 입력했을 경우엔, 한 번이라도 검색했던 유저들의 닉네임 목록들이 노출되어야 했습니다. 이 기능을 개발하기 위해선 아래와 같은 if문이 필요했습니다. 

 

 

if(검색어가 있다면) {
	return 검색어 조회 결과
} else {
	return 검색어 조회 결과 
}

 

 

지금은 댓글 기능에서 유저의 닉네임을 검색하고 있지만, 만약 게시글에서도 유저를 멘션 하는 기능이 생긴다면 어떻게 될까요? 아마 아래와 같을 것입니다. 

 

 

if(댓글 기능 && 검색어가 있다면) {
	return 검색어 조회 결과
} else if(댓글 기능 && 검색어가 없다면) {
	return 검색어 조회 결과 
} else if(게시글 기능 && 검색어가 있다면) {
	return 검색어 조회 결과 
} else if(게시글 기능 && 검색어가 없다면) {
	return 검색어 조회 결과 
}

 

 

기획이 고도화되면 될수록, if else if 문이 추가되는 문제가 발생할 수 있습니다. 이 문제를 해결하기 위해 전략 패턴을 활용했습니다.

 

 

 

 

 

 

 

 

 


전략 패턴 활용기

전략 패턴을 활용하기 위해 먼저 비즈니스 로직에서 어떤 전략을 활용할 것인지 확인하는 클래스를 구성했습니다.

 

 

// NicknameSearchStrategyFactory.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class NicknameSearchStrategyFactory {
  constructor(private nicknameSearchStrategies: INicknameSearchStrategy[]) {}

  public findStrategy(
    readUserNicknameSearchApiQueryDto: ReadUserNicknameSearchApiQuery,
  ): INicknameSearchStrategy {
    for (const strategy of this.nicknameSearchStrategies) {
      if (strategy.canFind(readUserNicknameSearchApiQueryDto)) {
        return strategy;
      }
    }
  }
}
// INicknameSearchStrategy.ts

export interface INicknameSearchStrategy {
  canFind: (dto: ReadUserNicknameSearchApiQuery) => boolean;
  findAllUserNickname: (
    dto: ReadUserNicknameSearchApiQuery,
    userDto: JwtPayload,
    queryRunner: QueryRunner,
    userBlockList: number[],
  ) => Promise<ReadUserNicknameSearchApiRes>;
}

 

 

 

NicknameSearchStrategyFactory 클래스에서 INicknameSearchStrategy 인터페이스 타입의 배열을 주입받습니다. 그 후 findStrategy 메서드를 활용하여 주입받은 nicknameSearchStrategies 중, strategy의 canFind 메서드를 활용하여 사용할 전략을 찾습니다.

 

 

// UserApiModule.ts

import { Module, Provider } from '@nestjs/common';

const domain: Provider[] = [
  ...
  NicknameUnnamedSearchStrategy,
  NicknameNamedSearchStrategy,
  {
    provide: NicknameSearchStrategyFactory,
    useFactory: (
      strategy1: INicknameSearchStrategy,
      strategy2: INicknameSearchStrategy,
    ) => {
      return new NicknameSearchStrategyFactory([strategy1, strategy2]);
    },
    inject: [NicknameUnnamedSearchStrategy, NicknameNamedSearchStrategy],
  },
];

@Module({
  imports: [
    ...
  ],
  controllers: [...controller],
  providers: [...service, ...infrastructure, ...domain],
})

export class UserApiModule {}

 

 

 

INicknameSearchStrategy 인터페이스를 상속받은 NicknameUnnamedSearchStrategy, NicknameNamedSearchStategy 클래스를 NicknameSearchStrategyFactory 클래스에 주입해 주기 위해 위와 같이 코드를 작성합니다. 

 

 

 

// NicknameNamedSearchStrategy.ts

import { Injectable } from '@nestjs/common';
import { QueryRunner } from 'typeorm';

@Injectable()
export class NicknameNamedSearchStrategy implements INicknameSearchStrategy {
  public canFind(dto: ReadUserNicknameSearchApiQuery): boolean {
    if (dto.isNicknameNamedSearchStrategy()) {
      return true;
    }
    return false;
  }
 }

 

 

strategy의 canFind 메서드를 활용하여 특정 조건이 맞다면 true를, 맞지 않다면 false를 전달합니다.

 

 

 

// ReadUserNicknameSearchApiService.ts

import {
  Injectable,
} from '@nestjs/common';

@Injectable()
export class ReadUserNicknameSearchApiService {
  constructor(
    private nicknameSearchStrategyFactory?: NicknameSearchStrategyFactory,
  ) {}

  async searchUserNicknameList(
    queryDto?: ReadUserNicknameSearchApiQuery,
  ): Promise<ReadUserNicknameSearchApiRes> {
    const strategy = this.nicknameSearchStrategyFactory.findStrategy(queryDto);
   		...
}

 

 

 

INicknameSearchStrategy.ts를 활용하여 Strategy를 구성했고, NicknameNamedSearchStrategy.ts를 활용하여 ConcreteStrategy를 구성했다면, 이제 Strategy 패턴을 이용하는 Context를 위와 같이 구성했습니다. 

 

service 계층에서 전략 패턴을 활용했습니다. NicknameSearchStrategyFactory 클래스의 findStrategy 메서드를 활용하여 특정 조건이 true라면, true인 조건에 해당하는 ConcreteStrategy를 활용할 것입니다. 

 

 

만약 기획이 추가적으로 변경되어서, 댓글뿐 아니라 게시글에서도 멘션 기능이 필요하다고 하더라도, Service 계층의 코드는 더 이상 변경되는 것이 없어집니다. 게시글 멘션 기능에 필요한 Strategy 클래스만 주입해서 사용하면 기존에 있던 코드는 변경하지 않아도 됩니다. 

 

 

만약 전략 패턴을 활용하지 않았다면 else if 문을 추가적으로 만들어줘야 하는 문제가 있었을 텐데, 전략 패턴을 활용하여 확장에는 열려있고, 변경에는 닫혀있는 코드를 구성할 수 있었습니다. 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

마치며

공부를 하며 그동안 아주 기초적인 것들을 제대로 공부하지 않았다는 것을 깨닫습니다. 좋은 기술을 배우는 것도 좋지만, 기술의 기반이 되는 기초적인 지식을 먼저 쌓는 것이 중요하다는 것을 다시금 깨달았습니다. 기초가 튼튼한 개발자가 되고 싶습니다.

 

 

 

 

 

 

 


 

 

 

출처

 

[Project] 프로젝트 삽질기48 (feat 전략 패턴)

들어가며 NestJS와 TypeORM을 활용하여 프로덕트를 만들고 있습니다. 비즈니스 로직을 구성하는데, if else if 문이 계속해서 추가되다 보니 코드의 가독성이 좋지 못한 문제가 발생했습니다. if - else

overcome-the-limits.tistory.com