home
🎯

실무 사례로 알아보는 Strategy Pattern

Author
조하담 / Backend Engineer
Category
Service
Tags
Strategy Pattern
RAG
Passport.js
Published
2024/12/10
AI 요약
5 more properties
이번 아티클에서는 다음 4가지 항목을 다룹니다.
1.
전략 패턴의 기본 개념과 구조 설명
2.
Passport.js 라이브러리에서의 전략 패턴 활용 사례
3.
LLM 엔진 Failover 처리를 위한 TypeScript 예제를 통한 실무
4.
RAG 검색 단계 처리를 위한 Python 예제
Sionic AIMSA(Microservices Architecture) 구조로 이루어져 있어, 다양한 서비스가 독립적으로 운영되고 있습니다. 이러한 구조 속에서 효율적이고 확장 가능한 설계 패턴을 적용하고 있는데, 특히 전략(strategy) 패턴을 통해 서비스의 특정 요구 사항을 유연하게 구현하고 있습니다.
그중에서도 아래 두 서비스가 전략 패턴을 효과적으로 활용한 좋은 예라고 생각됩니다.
Open Gateway (TypeScript) : OpenAI와 Azure 등 다양한 LLM 엔진에 대한 Failover 처리를 구현하고 있습니다.
Pylon (Python) : RAG(Retrieval-Augmented Generation) 파이프라인의 검색 단계에서 여러 검색 전략을 조합하여 처리하고 있습니다.
이 글에서는 전략 패턴이 무엇인지 간단히 설명하고, 위 두 서비스에서 각각 어떤 문제를 해결하기 위해 사용되었는지 공유하려고 합니다. 전략 패턴이 실제 서비스에서 어떤 가치를 줄 수 있는지, 간단한 코드 예제와 함께 알아보겠습니다.

전략 패턴이란?

전략 패턴여러 알고리즘을 캡슐화하고, 실행 시점에 동적으로 교체할 수 있도록 설계된 객체지향 디자인 패턴입니다. 쉽게 말해, 특정 작업 방식을 여러 개 준비해 두고, 필요에 따라 선택적으로 적용할 수 있는 구조라고 볼 수 있습니다.
이는, 우리가 일상에서 선택지를 고민할 때와 비슷합니다.
모각코(모여서 각자 코딩)를 하기 위해 카페를 간다고 가정해 볼게요. 네이버 지도에서 길찾기 기능을 사용한다고 가정하면, 이동수단은 도보, 대중교통, 자전거 또는 승용차(택시)중 하나를 선택하게 됩니다. 선택 기준은 예산, 시간, 또는 날씨에 따라 달라질 수 있습니다.
즉, ‘목적지로 이동한다’라는 공통된 행동을 정의하고, 실행 시점에 상황에 따라 최적의 이동수단을 선택하는 방식입니다.
이렇게 구성하면 상황에 따라 유연하게 이동수단을 변경할 수 있습니다. 예를 들어, 비가 오면 택시를 타고, 날씨가 맑으면 도보를 선택할 수 있습니다.각 전략은 독립적으로 변경이 가능하며, 새로운 이동 수단이 추가되어도 기존 코드를 수정할 필요가 없습니다.

전략 패턴 구조

전략 패턴은 특히 행동 패턴(Behavioral Pattern) 범주에 속합니다. 행동 패턴은 객체들 간의 상호작용 방식책임 분배를 정의하여, 시스템 내의 객체들이 서로 협력하는 방식을 유연하게 만듭니다.
전략 패턴은 크게 세 가지 주요 구성 요소로 이루어져 있습니다.
1.
컨텍스트(Context) : 전략을 실행하고 관리하는 역할을 맡은 객체. 클라이언트는 컨텍스트를 통해 원하는 전략을 설정하고 실행합니다. 이를 위해,클라이언트에게 런타임에 컨텍스트와 관련된 전략을 대체할 수 있도록 하는 setter​를 선언합니다.
2.
전략 인터페이스 (IStrategy) : 모든 전략 구현체에 대한 공통 인터페이스이며, 컨텍스트가 전략을 실행하는 데 사용하는 메서드를 선언합니다.
3.
전략 구현체 (StrategyA, StrategyB): 알고리즘의 다양한 변형들을 구현합니다.
프로그래밍에서 컨텍스트콘텐츠를 담는 그릇과 같습니다. 특정 객체(콘텐츠)를 효율적으로 관리하거나 조작하기 위한 접근 수단을 제공합니다. 물컵(컨텍스트) 안에 물(콘텐츠)을 담았다고 상상해보세요. 상황에 따라 물컵에 담긴 물을 차, 커피, 주스 등으로 바꿀 수 있습니다.
전략 패턴에서의 컨텍스트도 마찬가지로, 특정 전략(알고리즘)을 다루기 위한 그릇으로 작동합니다. 물컵(컨텍스트)은 그대로 유지되지만, 안에 담긴 물(전략)은 필요에 따라 교체됩니다. 이처럼 컨텍스트는 전략을 담고 실행하며, 유연한 교체를 가능하게 해줍니다.
위에서 보여드린 구조도를 코드로 살펴보겠습니다.
interface IStrategy { doSomething(): void; } class StrategyA implements IStrategy { doSomething(): void { // Implementation for Strategy A } } class StrategyB implements IStrategy { doSomething(): void { // Implementation for Strategy B } } class Context { private strategy!: IStrategy; // 전략 교체 메소드 setStrategy(strategy: IStrategy): void { this.strategy = strategy; } // 전략 실행 메소드 doSomething(): void { this.strategy.doSomething(); } } // 1. 컨텍스트 생성 const c = new Context(); // 2. A 전략 설정 c.setStrategy(new StrategyA()); // 3. A 전략 실행 c.doSomething(); // 4. B 전략 설정 c.setStrategy(new StrategyB()); // 5. B 전략 시행 c.doSomething();
TypeScript
복사

전략 패턴을 사용하는 라이브러리: passport.js (JavaScript)

passport.org 에 등록되어있는 500+가지 전략들
passport.js는 네이버, 카카오, 페이스북 로그인을 포함한 다양한 OAuth 로그인을 구현할 때 널리 사용되는 유명한 자바스크립트 라이브러리입니다. 여러 인증 방식을 하나의 공통 인터페이스로 통합한 전략 패턴을 기반으로 설계되었습니다.
const passport = require('passport'); const KakaoStrategy = require('passport-kakao').Strategy; const NaverStrategy = require('passport-naver-v2').Strategy; const GoogleStrategy = require('passport-google-oauth20').Strategy; // 카카오 로그인 전략 등록 passport.use(new KakaoStrategy({ clientID, callbackURL }, async (accessToken, refreshToken, profile, done) => { // 인증 로직 }); // 네이버 로그인 전략 등록 passport.use(new NaverStrategy({ clientID, clientSecret, callbackURL }, async (accessToken, refreshToken, profile, done) => { // 인증 로직 }); // 구글 로그인 전략 등록 passport.use(new GoogleStrategy({ clientID, clientSecret, callbackURL}, async (accessToken, refreshToken, profile, done) => { // 인증 로직 }));
TypeScript
복사
클라이언트: passport.use()로 전략을 컨텍스트에 등록합니다. 클라이언트 요청 시 컨텍스트가 해당 전략을 실행하여 인증을 처리합니다.
컨텍스트(Context): passport가 컨텍스트로 작동하며, 클라이언트 요청에 따라 적절한 인증 전략을 실행합니다.
전략 인터페이스: 모든 인증 전략은 passport.Strategy 인터페이스를 구현하여 공통된 구조를 따릅니다.
전략 구현체: KakaoStrategy, NaverStrategy, GoogleStrategy는 각각의 로그인 로직을 독립적으로 구현한 전략입니다.
참고로, 실제 passport.js에서 전략은 class 대신 함수형 구현을 사용하고 있습니다. 이는 JavaScript에서 class 키워드가 도입되기 이전(ES6 이전)의 객체지향 스타일을 기반으로 하고 있음을 감안하고 보시면 좋을것 같습니다.
passport.Strategy는 인터페이스처럼 동작하지만, 실제로는 함수로 구현되었습니다.
이를 바탕으로 OpenIdPassport.StrategyOAuthPassport.Strategy, 그리고 이후의 FacebookPassport.Strategy, GooglePassport.Strategy와 같은 구체적인 전략이 상속 구조를 통해 구현됩니다.
이러한 구조 덕분에 새로운 인증 전략이 추가되더라도 기존 코드의 변경 없이 확장할 수 있으며, 각 전략은 인증 로직을 독립적으로 관리해 유지보수성과 확장성을 크게 향상시킵니다. 더 구체적으로 이해하려면 아래의 github 리포지토리에서 코드를 순차적으로 참고해보시는것을 추천드립니다.
passport-oauth2: passport-strategy를 상속받아 OAuth 2.0 인증 프로세스를 구현
strategy.js#L2: passport-oauth2를 상속받아 Facebook의 OAuth 2.0 엔드포인트와 통신하며, Facebook 사용자 프로필 데이터 처리 로직 추가

실무 적용 사례 1: LLM 엔진 Failover 처리 (TypeScript)

LLM(대규모 언어 모델)은 자연어 질문에 대해 답변을 생성하거나, 텍스트를 요약하고 번역 등을 수행하는 인공지능 모델입니다. 이러한 모델은 OpenAI, Azure 등 여러 엔진을 통해 제공되며, 질의에 대한 답변을 생성합니다.
이 코드는 LLM 엔진 호출 과정에서 Failover를 처리합니다. 여러 API 호출 전략을 순차적으로 실행하며, 실패 시 다음 전략으로 넘어갑니다.
OpenAiStrategy: OpenAi 엔진을 호출해서 답변 생성
AzureStrategy: Azure 엔진을 호출해서 답변 생성
interface ChatStrategy { execute(prompt: string): string; } class OpenAiStrategy implements ChatStrategy { execute(prompt: string): string { return `Processing prompt with OpenAI: ${prompt}`; } } class AzureStrategy implements ChatStrategy { execute(prompt: string): string { return `Processing prompt with Azure: ${prompt}`; } } class ChatService { private strategies: ChatStrategy[] = []; // 전략 setter setStrategies(strategies: ChatStrategy[]) { if (strategies.length === 0) throw new Error('At least one strategy must be provided'); this.strategies = strategies; } executeChat(prompt: string): string { if (this.strategies.length === 0) throw new Error('No strategies set'); for (const strategy of this.strategies) { try { return strategy.execute(prompt); // 첫 번째 성공 전략의 결과 반환 } catch (error) { console.warn(`Strategy failed. Trying next...`); } } throw new Error('All strategies failed'); // 모든 전략 실패 시 예외 처리 } } const chatService = new ChatService(); // OpenAI가 실패하면 Azure로 대체 실행 chatService.setStrategies([new OpenAiStrategy(), new AzureStrategy()]); chatService.executeChat('Hello, world!');
TypeScript
복사
참고로, 위 코드는 실제 서비스의 Failover 처리 로직을 간소화한 버전입니다.
실무에서는 OpenAI와 Azure 외에도 여러 엔진을 포함합니다.
전략 로직과 다양한 예외를 구체화하지 않고 간단히 처리했습니다.
단순한 쿼리 대신, 컨텍스트와 추가 정보를 결합한 복잡한 프롬프트를 생성해 엔진에 전달합니다.

실무 적용 사례 2: RAG 검색 단계 처리 (Python)

RAG는 질의 응답 또는 텍스트 생성 작업에서 외부 데이터를 활용하기 위해, 검색과 생성 단계를 결합한 기법입니다. 검색 단계에서 유저가 에이전트에 등록한 문서들 중에서 적합한 문서를 찾아 모델 입력으로 전달하고, 생성 단계에서 이를 바탕으로 답변을 생성합니다.
아래 코드는 RAG의 검색 단계를 처리하기 위한 파이프라인을 구성합니다. 검색 전략을 체인 형태로 연결하여, 여러 검색 방법을 순차적으로 실행하고 그 결과를 결합합니다.
FeedbackRetrievalStrategy: 사용자 피드백 데이터(Q&A set)를 검색
DocumentRetrievalStrategy: 사용자가 등록한 문서를 검색
class SearchChain: def __init__(self, strategy): self.strategy = strategy # 현재 체인의 검색 전략 self.next_chain = None # 다음 체인 연결 def set_next(self, chain): self.next_chain = chain # 다음 체인 설정 return chain def execute(self, query): results = self.strategy.search(query) # 현재 체인의 전략 실행 if self.next_chain: # 다음 체인이 있다면 실행 results += self.next_chain.execute(query) return results # 결과 반환 class FeedbackRetrievalStrategy: def search(self, query): return [f"Feedback results for '{query}'"] class DocumentRetrievalStrategy: def search(self, query): return [f"Document results for '{query}'"] # 체인 구성 feedback_chain = SearchChain(FeedbackRetrievalStrategy()) document_chain = SearchChain(DocumentRetrievalStrategy()) feedback_chain.set_next(document_chain) # 체인 연결 # 체인 실행 results = feedback_chain.execute("example query") print("Final results:", results)
Python
복사
참고로, 위 코드는 실무에서 사용된 검색 파이프라인을 최대한 단순화하여 핵심 개념만 전달할 수 있도록 변형했습니다.
실제 코드는 검색 전략의 종류와 실행 조건이 더 복잡하며, 전략별 초기화와 에러 처리 로직이 포함됩니다.
블로그에서는 이해를 돕기 위해 복잡한 요소를 제거하고, 두 가지 간단한 검색 전략만 사용했습니다.

마치며

이번 글에서는 전략 패턴이 라이브러리와 실무 프로젝트에서 어떻게 유연성과 확장성을 높이는지 코드 예제를 통해 살펴보았습니다.
passport.js 라이브러리는 다양한 OAuth 인증 방식을 통합적으로 처리할 수 있도록 설계된 라이브러리로, 전략 패턴을 활용하여 각기 다른 인증 로직을 공통된 인터페이스로 관리합니다.
TypeScript 예제는 단일 LLM 엔진에 대한 Failover 처리를 구현하여, 엔진 호출 실패 시 유연하게 대체 엔진으로 전환할 수 있도록 설계되었습니다.
Python 예제는 여러 검색 전략을 순차적으로 실행하여, 복합적인 검색 파이프라인을 구축하는 데 중점을 두었습니다.
전략 패턴은 동작이 자주 바뀌거나 조건에 따라 달라져야 하는 복잡한 로직을 처리하는 데 특히 유용합니다. 프로젝트에서 새로운 기능을 확장해야 할 때, 조건문을 늘리는 대신 전략 패턴을 적용해 더 깔끔하고 유지보수하기 쉬운 코드를 작성해 보시길 바랍니다.

참고 자료