Strategy 패턴을 사용해보기
들어가며
- 최근 개발하는 프로젝트에서 같은 데이터에 대해서 조건에 따라서 다른 로직을 실행하는 요구사항이 있었습니다.
- 해당 데이터는 로그성 데이터로 아래와 같습니다.
{ // 로그를 발생시킨 유저 userId: string; // 로그가 찍힌 날짜 date: Date; // 로그의 종류 logType: string; // 로그의 행위 (입장, 수강, 퇴장) action: string; // 고유 사업별 종류 businessType: string; } - 이에 대해서 디자인 패턴을 도입해보기로 하였고, Strategy 패턴을 고려하게 되었습니다.
Strategy pattern

Strategy Design Pattern - GeeksforGeeks
Your All-in-One Learning Portal: GeeksforGeeks is a comprehensive educational platform that empowers learners across domains-spanning computer science and programming, school education, upskilling, commerce, software tools, competitive exams, and more.
- Strategy Pattern은 행동 또는 알고리즘을 캡슐화하여 런타임 시 교체하거나 선택할 수 있게 만드는 행동 패턴 - Behavioral Pattern입니다.
- 클라이언트가 사용하는 알고리즘을 인터페이스로 정의하고, 이를 구현한 여러 전략 - Strategy을 제공합니다.
- 클라이언트는 특정 알고리즘의 구현체를 알 필요 없이 동적으로 행동을 변경할 수 있습니다.
설계
- 데이터 처리에 대한 설계는 아래와 같이 진행하였습니다.
graph TD
A[ElasticSearch watcher] --> |로그성 데이터 전송| B
B{로그의 종류가 무엇인가?} --> |A타입 - setStrategyA| C
B{로그의 종류가 무엇인가?} --> |B타입 - setStrategyB| D
B{로그의 종류가 무엇인가?} --> |C타입 - setStrategyC| E
C{로그의 행위가 무엇인가?} --> |입장| F[입장 데이터 처리 로직 실행]
C{로그의 행위가 무엇인가?} --> |퇴장| G[퇴장 데이터 처리 로직 실행]
D{로그의 행위가 무엇인가?} --> |입장| H[입장 데이터 처리 로직 실행]
D{로그의 행위가 무엇인가?} --> |퇴장| I[퇴장 데이터 처리 로직 실행]
E{로그의 행위가 무엇인가?} --> |입장| J[입장 데이터 처리 로직 실행]
E{로그의 행위가 무엇인가?} --> |퇴장| K[퇴장 데이터 처리 로직 실행]
- ElasticSearch watcher를 통해서 ES에 적재된 새로운 데이터를 1분마다 가져옵니다.
- 새로운 데이터들을 순회하며 각각의 데이터를 확인합니다.
- 로그 데이터의 종류에 따라서 다른 strategy를 적용합니다. (setStrategy 메서드를 통해서 strategy를 세팅합니다)
- 로그 데이터의 행위에 따라서 각각 적용된 strategy의 메서드들을 호출합니다.
- 행위가 입장이라면, 입장 데이터 처리 로직(refineInLog)을 실행합니다.
- 행위가 퇴장이라면, 입장 데이터 처리 로직(refineOutLog)을 실행합니다.
구현
processor
@Injectable()
export class TrainLogRefineProcessor {
private trainLogRefineStrategy: RefineStrategy | null = null;
setStrategy(trainLogRefineStrategy: RefineStrategy) {
this.trainLogRefineStrategy = trainLogRefineStrategy;
}
refineEnterLog<T extends TrainLogDocument>(trainLogDocument: T) {
if (!this.trainLogRefineStrategy) {
throw new Error('정제 전략이 설정되지 않았습니다.');
}
this.trainLogRefineStrategy.refineInLog(trainLogDocument);
}
refineLeaveLog<T extends TrainLogDocument>(trainLogDocument: T) {
if (!this.trainLogRefineStrategy) {
throw new Error('정제 전략이 설정되지 않았습니다.');
}
this.trainLogRefineStrategy.refineOutLog(trainLogDocument);
}
}
- 공통으로 사용될 processor입니다.
- 필드로 Strategy를 가지고 있고, setStrategy 메서드를 통해서 startegy가 주입됩니다.
- refineEnterLog /refineLeaveLog 메서드는 모든 strategy에 공통으로 사용되는 메서드인 refineInLog / refineOutLog를 각각 호출하는 메서드입니다.
Strategy interface
export interface RefineStrategy {
refineInLog(trainLogDocument: TrainLogDocument): void;
refineOutLog(trainLogDocument: TrainLogDocument): void;
}
- 앞서 언급했듯이 Strategy마다 공통으로 사용되는 메서드인 refineInLog, refineOutLog가 있습니다. 이에 따라서 인터페이스를 작성하였습니다.
Strategy
@Injectable()
export class ARefineStrategy implements RefineStrategy {
constructor(
// 각 stategy에 필요한 의존성
) {}
async refineInLog(
alwaysAttendanceTrainLogDocument: AlwaysAttendanceTrainLogDocument,
) {
// 각 stategy에 맞는 상세 구현
}
async refineOutLog(
alwaysAttendanceTrainLogDocument: AlwaysAttendanceTrainLogDocument,
) {
// 각 stategy에 맞는 상세 구현
}
}
- Strategy 구현체입니다. 위에 작성한 RefineStrategy 인터페이스를 상속합니다.
- 각 strategy마다 가져가야할 비즈니스 로직이 다른데요, 그중에 한가지 예시로는 처리 후 저장하는 데이터베이스 table이 다르다는 점 등이 있습니다.
- 해당 Strategy 메서드에 각각 고유한 비즈니스 로직을 작성합니다.
service
@Injectable()
export class RefineService {
constructor(
private readonly trainLogRefineProcessor: TrainLogRefineProcessor,
private readonly aRefineStrategy: ARefineStrategy,
private readonly bRefineStrategy: BRefineStrategy,
private readonly cRefineStrategy: CRefineStrategy,
) {}
refineTrainLog<T extends TrainLogDocument>(
searchHit: SearchHit<T>[],
trainLogType: TrainLogType,
) {
// TODO: needs Queue or Kafka
searchHit.forEach((hit) => {
const source = hit._source;
if (!source) return;
const strategy = this.getStrategyByLogType(trainLogType);
if (!strategy) {
throw new Error('존재하지 않는 정제 전략입니다.');
}
this.trainLogRefineProcessor.setStrategy(strategy);
this.refineByAction(source);
});
}
private getStrategyByLogType(trainLogType: TrainLogType) {
switch (trainLogType) {
case TrainLogType.A:
return this.aRefineStrategy;
case TrainLogType.B:
return this.bRefineStrategy;
case TrainLogType.C:
return this.cRefineStrategy;
default:
return null;
}
}
private refineByAction<T extends TrainLogDocument>(source: T) {
const { action } = source;
switch (action) {
case TrainLogAction.IN:
this.trainLogRefineProcessor.refineEnterLog(source);
break;
case TrainLogAction.STAY:
case TrainLogAction.OUT:
this.trainLogRefineProcessor.refineLeaveLog(source);
break;
default:
return;
}
}
}
- A,B,C refineStrategy는 예시로 각각 다른 strategy를 표현하고자 합니다.
- es watcher를 통해 새로운 데이터를 1분마다 수신합니다.
- 수신된 데이터는 refineTrainLog에 전달됩니다. 해당 로직에서 데이터의 종류에 따라서 분기처리합니다.
- getStrategyByLogType을 통해 알맞은 strategy를 가져오는 로직을 실행합니다.
- 가져온 strategy를 processor에 setStrategy를 통해서 동적으로 주입힙니다.
- 마지막으로 데이터의 행위에 따라서 strategy의 메서드를 호출합니다.
- 입장일 경우 refineEnterLog 메서드를 호출합니다.
- 퇴장일 경우 refineEnterLog 메서드를 호출합니다.
마치며
- 프로젝트에서 동적으로 런타임에 다른 로직을 유동적으로 실행하기 위해서 Strategy 패턴을 적용해보았습니다.
- 우선 여러 디자인 패턴에 대해서 리서치해보고, 상황에 맞는 디자인 패턴을 찾고 공부하고 도입해보아서 좋은 경험이였던 것 같습니다.
- 추후에 더 많은 strategy가 필요하게되는 상황에서, 간단하게 strategy를 추가할 수 있다는 점이 장점이라고 생각합니다.
- 실제로 운영해보아야 해당 디자인 패턴이 올바른 패턴이였는지는 더 알 수 있다고 생각합니다. 모든 디자인 패턴은 특정 상황에 따른 최적의 가이드라인이기 때문에 문제가 복잡해지면 모든 요구사항을 충족시키기란 어렵기 때문입니다. 따라서 요구사항에 맞춰서 해당 디자인 패턴이 유효한지 확인하며 계속 수정해나가고, 코드를 유연하고 유지보수하기 좋게 구상하는 것이 목표입니다.
