개발 기록

> [GOF Design Pattern] 행동 패턴 : 전략 (Strategy) 패턴 - 개념과 구현 본문

디자인 패턴

> [GOF Design Pattern] 행동 패턴 : 전략 (Strategy) 패턴 - 개념과 구현

1z 2024. 2. 5. 22:19

 

* 행동 패턴 : 클래스와 객체들이 상호작용하는 방법과 역할을 분담하는 방법을 다루는 패턴

 

 

0. 개요 

 

초기에는 간단한 기능을 가진 애플리케이션에서 점진적으로 기능을 추가하고 확장하는 경우가 빈번히 일어난다. 그리고 이 과정에서 시스템의 복잡성이 증가하고 기술적인 문제가 발생한다.

 

아래의 그림을 예시를 들면

초기 떡볶이 전문으로 시작한 가게가 여러 메뉴를 추가하게 되는 경우를 생각해보자. 

1. 처음에는 떡볶이만을 위한 모든 제조 환경을 맞췄다.

2. 김밥 메뉴가 생기면서 김밥 제조 환경을 추가로 만들었다.

3. 근데 그 다음에는 튀김 메뉴도 생기면서 튀김기도 마련했다.

4. 여기까지는 감당할 수 있을까? 하지만 바로 그 다음에는 순대.. 돈까스 메뉴까지 생긴다면? 심지어 각 메뉴별로 맛도 다양하게 생긴다면???

 

결국에는  떡볶이 만드는 주방이 정체성을 잃고 아주 엉망이 될것이다. 제조 방법도 다르고, 떡볶이 전문 요리사가 다른 메뉴를 잘 만들 수 있을지 기술적인 문제와 떡볶이 국물이 옆에 있는 김밥에 튀는 등 아주 복잡한 문제가 생길것이다.

 

이럴때 활용할 수 있는게 전략 패턴이다. 

 

1. 전략 패턴 개념  

 

(1) 개념


알고리즘 집합을 정의하고 각각을 캡슐화하여 교환할 수 있도록 만드는 패턴으로 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있게 해준다.

ex. 전략: 알고리즘, 기능, 동작, 특정한 목표를 수행하기 위한 행동 계획 

 

 

 유연성과 확장성: 개방/폐쇄 원칙  

전략은 별도의 클래스에 캡슐화되므로 클라이언트 코드에 영향을 주지 않고 전략을 쉽게 추가, 제거 또는 수정할 수 있다. 

 코드 재사용성 향상

 쉬운 테스트: 각 전략이 별도의 클래스이기 때문에 테스트가 단순화된다. 

 런타임 동작

 단일 책임원칙 장려

 

 

 적절한 전략 선택의 어려움: 라이언트들은 런타임에 가장 적합한 전략을 선택할 수 있도록 모든 전략 클래스를 알고 있어야한다. 특히 여러 요인이 결정에 영향을 미치는 상황에서는 전략 선택 논리가 복잡해질 수 있다.

 클래스 수 증가:  각 전략에 대해 별도의 클래스를 생성해야 한다. * Flyweight 패턴을 사용하면 개체 수를 어느 정도 줄일 수는 있다.

 오버헤드 가능성: 클라이언트는 전략 개체를 인스턴스화하고 관리해야 하며, 다른 전략에 위임할 때 추가적인 함수 호출 오버헤드가 있을 수 있다.

 오용 가능성 : 전략 패턴이 오용될 경우 지나치게 복잡한 디자인이 될 수 있다.

 

소규모 프로젝트나 알고리즘이 몇 개밖에 되지 않고 거의 변하지 않는다면, 전략 패턴을 적용하는게 불필요한 복잡성을 발생시킬 수 있다.  익명 함수, 람다 함수, 함수형 프로그래밍으로 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 유연성을 유지할 수 있다. 

 

 

(2) 구조

 

 

 

전략 (Strategy) : 모든 구상 전략에 공통이며, 콘텍스트가 전략을 실행하는 데 사용하는 메서드를 선언한다.

전략은 인터페이스(Interface)나 추상 클래스(Abstract Class)로 정의된다.

// 정렬 알고리즘을 위한 전략 인터페이스
public interface SortStrategy {
    void sort(int[] data);
}

 

구체적인 전략 (Concrete Strategy) : 전략 인터페이스를 구현한 클래스로 각각 독립적인 알고리즘을 구현한다.

// 버블 정렬 전략
public class BubbleSortStrategy implements SortStrategy {
    @Override
    public void sort(int[] data) {
        // 버블 정렬 알고리즘 구현
        // ...
    }
}

// 퀵 정렬 전략
public class QuickSortStrategy implements SortStrategy {
    @Override
    public void sort(int[] data) {
        // 퀵 정렬 알고리즘 구현
        // ...
    }
}

 

컨텍스트 (Context) :  전략 객체를 사용하는 클래스 또는 컴포넌트

1. 전략 객체에 대한 참조 필드 정의

2. 전략을 대체할 수 있도록 하는  setter 메서드 정의

3. 전략 객체에 실행을 위임하기 위한 메서드 정의.

 

 콘텍스트가 구상 전략들에 의존하지 않게 되므로 콘텍스트 또는 다른 전략들의 코드를 변경하지 않고도 새 알고리즘들을 추가하거나 기존 알고리즘들을 수정할 수 있다.

// 정렬을 수행하는 컨텍스트 클래스
public class SorterContext {
    //전략 객체에 대한 참조 필드
    private SortStrategy strategy;

    //생성자를 통해 전략을 주입
    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    //런타임에 전략이 전환될 setter 메서드 정의
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }

    // 연결된 전략 객체의 sort 메서드를 호출하는 메서드
    public void performSort(int[] data) {
        // 전략에 따라 정렬 수행
        strategy.sort(data);
    }
}

 

클라이언트(client) :  원하는 전략 객체를 만들어 콘텍스트에 전달한다.

public class Client {
    public static void main(String[] args) {
        // 컨텍스트 객체 생성 (기본적으로 버블 정렬 전략 사용)
        SorterContext sorter = new SorterContext(new BubbleSortStrategy());

        int[] data = {5, 3, 8, 1, 2};

        // 버블 정렬 수행
        sorter.performSort(data);
        System.out.println("Bubble Sort Result: " + Arrays.toString(data));

        // 퀵 정렬 전략으로 변경
        sorter.setStrategy(new QuickSortStrategy());

        // 퀵 정렬 수행
        sorter.performSort(data);
        System.out.println("Quick Sort Result: " + Arrays.toString(data));
    }
}

 

 

아! 근데 이런 생각이 들었다. 클라이언트에서 전략을 선택할 때 결국 if문 또는 switch문으로 분기처리가 들어가는데 이걸 효율적이라고 할 수 있을까ㅇ_ㅇ?? 그래서! 이 고민을 해결하기 위해 여러 패턴들과 함께 쓰는 방법을 아래 예제로 보여준다.  


 

2. 전략 디자인 패턴 + 팩토리 메서드 패턴 + 싱글톤 패턴으로 구현하는 유효성 검사 예제  

 

이 예시에서는 간단한 유효성 검사를 수행하는 클래스를 구현하고, 팩토리 메서드를 사용하여 해당 클래스의 인스턴스를 동적으로 생성하고,   싱글톤 패턴을 적용하여 유일한 인스턴스를 관리하는 방법을 보여준다.

 

(1) 전략 인터페이스 생성

ValidationStrategy: 유효성 검사 전략을 정의하는 인터페이스

ValidationStrategy 인터페이스는 제네릭 타입 T를 받는 메서드 isValid를 정의하여 다양한 유형의 데이터에 대한 유효성 검사를 수행한다. 

public interface ValidationStrategy<T> {
    boolean isValid(T input);
}

 

(2) 전략 구현 클래스 생성과 싱글톤 패턴 적용

 EmailValidationStrategy: 이메일과 유효성 검사 전략을 구현하는 클래스

  PasswordValidationStrategy: 비밀번호 유효성 검사 전략을 구현하는 클래스

☞  싱글톤 패턴: 각 클래스는 싱글톤 패턴을 적용하여 유일한 인스턴스를 가지고 있다.

- instance 변수는 클래스 내부에서 유일한 인스턴스를 보관하고 있다.

- getInstance() 메서드를 통해 이 유일한 인스턴스에 접근할 수 있다.

// 이메일 유효성 검사 전략
public class EmailValidationStrategy implements ValidationStrategy<String> {
    // 클래스 내부에서만 인스턴스를 생성한다.
    private static final EmailValidationStrategy instance = new EmailValidationStrategy();

    // 싱글톤 패턴
    // 생성자를 'private'로 선언하여 외부에서 해당 클래스의 인스턴스를 생성하지 못하도록 한다.
    private EmailValidationStrategy() {
    }

    public static EmailValidationStrategy getInstance() {
        return instance;
    }

    @Override
    public boolean isValid(String input) {
        // 이메일 유효성 검사 로직
        return input != null && input.matches("[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
    }
}

// 비밀번호 유효성 검사 전략
public class PasswordValidationStrategy implements ValidationStrategy<String> {
    // 클래스 내부에서 인스턴스 보관
    private static final PasswordValidationStrategy instance = new PasswordValidationStrategy();

    private PasswordValidationStrategy() {
        // private 생성자로 외부에서 인스턴스 생성을 막음
    }

    // 이 메서드를 통해 해당 클래스의 인스턴스에 접근한다.
    public static PasswordValidationStrategy getInstance() {
        return instance;
    }

    @Override
    public boolean isValid(String input) {
        // 비밀번호 유효성 검사 로직
        return input != null && input.length() >= 8;
    }
}

 

(3) 팩토리 메서드를 통한 유효성 검사 전략 생성

  ValidtionStrategyFactory: 토리 메서드 패턴을 사용하여 유효성 검사 전략을 생성하는 클래스.

: 사용자가 요청한 유효성 검사 타입에 따라 적절한 전략 인스턴스를 반환한다.

public class ValidationStrategyFactory {
	// 유효성 검사 전략 인스턴스를 생성하고 반환한다.
    public static <T> ValidationStrategy<T> getValidationStrategy(String type) {
        if ("email".equalsIgnoreCase(type)) {
            return (ValidationStrategy<T>) EmailValidationStrategy.getInstance();
        } else if ("password".equalsIgnoreCase(type)) {
            return (ValidationStrategy<T>) PasswordValidationStrategy.getInstance();
        }
        throw new IllegalArgumentException("Unsupported validation type: " + type);
    }
}

 

(4) Context class 생성 

  Validtor: 유효성 검사를 수행하는 클래스.

: 요청값의 유효성을 검사하는 데 사용되며, 생성자에서 필요한 유효성 검사 전략을 선택한다. 유효성 검사 타입에 따라 적절한 전략을 선택하여 요청값을 검사하고, 필요에 따라 검사 전략을 변경할 수 있다.

// 제네릭 타입 T를 받아 해당 유형의 데이터에 대한 유효성 검사를 수행
public class Validator<T> {
    private ValidationStrategy<T> validationStrategy;

    // 생성자에서 유효성 검사 타입을 받아 해당 타입의 전략 인스턴스를 생성합니다
    public Validator(String type) {
        this.validationStrategy = ValidationStrategyFactory.getValidationStrategy(type);
    }

    // validate 메서드를 통해 입력값의 유효성을 검사합니다.
    public boolean validate(T input) {
        return validationStrategy.isValid(input);
    }
}

 

(4) Client class

public class PatternApplication {
    public static void main(String[] args) {
        // 이메일 유효성 검사
        Validator<String> emailValidator = new Validator<>("email");
        System.out.println("Email validation result: " + emailValidator.validate("test@example.com"));

        // 비밀번호 유효성 검사
        Validator<String> passwordValidator = new Validator<>("password");
        System.out.println("Password validation result: " + passwordValidator.validate("12345678"));
    }
}

 

 

이렇게 구현된 예시에서는

 전략 디자인 패턴을 통해 유효성 검사 전략을 쉽게 교체할 수 있다.

 제네릭을 활용하여 다양한 유형의 데이터에 대한 검사를 처리할 수 있다.

 팩토리 메서드 패턴과 싱글톤 패턴을 함께 사용하여 전략 인스턴스의 생성과 관리를 효율적으로 처리할 수 있다.

 


3. 전략 디자인 패턴 사용 사례 

 

(1) JAVA 정렬(Comparison)

Java의 Comparator 인터페이스와 Comparable 인터페이스는 전략 패턴의 좋은 예시이다. Comparator는 객체를 정렬할 때 사용되는 비교 로직을 캡슐화하며, 여러 다른 비교 전략을 제공할 수 있다. 예를 들어, 문자열을 길이에 따라 정렬하거나 숫자를 오름차순이나 내림차순으로 정렬할 수 있다.

// Comparator를 사용한 정렬 예시
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.sort(Comparator.comparing(String::length)); // 길이에 따라 정렬
names.sort(Comparator.comparing(String::length).reversed()); // 역순 정렬

 

(2) Spring Framework 에서 사용하는 전략 디자인 패턴

  Spring Framework 의 IOC 컨테이너

Spring IoC 컨테이너는 의존성 주입을 통해 전략을 교체하는 기능을 제공한다. 주로 생성자 주입, 필드 주입, 메서드 주입 등의 의존성 주입 전략을 사용한다.

@Service
public class FileUploadService {

    private final FileUploader fileUploader;  // 전략 인터페이스

    // FileUploader 인터페이스를 주입 
    // @Autowired나 @Qualifier 등의 주입 방식을 통해 원하는 전략을 선택하여 주입한다.
    @Autowired
    public FileUploadService(@Qualifier("s3FileUploader") FileUploader fileUploader) {
        this.fileUploader = fileUploader;
    }

    public void processFileUpload(String filePath) {
        fileUploader.uploadFile(filePath);
    }
}

 

  Spring security  보안 기능 구현

Spring Security에서 전략 디자인 패턴이 사용된 주요 부분은 AuthenticationProvider, UserDetailsService,  AccessDecisionManager 등 있다.

그 중 AuthenticationProvider를 봐보자. 

 

1. Strategy: AuthenticationProvider는 사용자의 인증을 처리하는 인터페이스로, 커스텀 인증 로직을 구현할 때 사용한다.

// 전략 인터페이스: 사용자 인증을 처리한다.
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

 

2. Concrete Strategy: 각각의 전략들은  AuthenticationProvider 인터페이스를 구현하여 사용자 인증 로직을 구현한다.

public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

...

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
...
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

3. Context: AuthenticationManager는 여러 AuthenticationProvider를 사용하여 인증 전략을 선택한다.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //AuthenticationManagerBuilder를 사용하여 AuthenticationProvider(전략)을 등록하고 교체한다.
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 		..
        //다른 인증 전략 등록
        auth.authenticationProvider(customAuthenticationProvider);
    }
}

 

 

(3) 전략 디자인 패턴을 사용할 수 있는 예 

 정렬 알고리즘: 배열을 정렬할 때 버블 정렬, 퀵 정,렬, 병합 정렬 등의 다양한 정렬 알고리즘을 전략 패턴으로 구현
 결제 처리: 각 결제 수단(신용카드, 페이팔, 은행 송금 등)에 대한 처리 전략을 구현
 게임 AI: 캐릭터의 행동에 전략(이동, 공격, 회피, 추적) 울 구현.
 캐시 전략: 캐시 시스템에서는 캐시에 데이터를 저장하고 검색하는 전략을 구현
⑤ 통신 프로토콜: 전송하는 데이터의 형식이나 프로토콜(HTTP, HTTPS, FTP )에 따라 통신 전략을 다르게 적용
⑥ 파일 시스템 접근: 로컬 파일, 네트워크 파일, 암호화된 파일 등에 대한 접근 방법을 전략 패턴으로 구현

 

 


 

4. 다른 패턴과의 관계 

 

(1) 함께 쓰는 패턴

 전략 패턴 + 팩토리 메서드 패턴+ 싱글톤 패턴 :  객체의 행위와 생성을 추상화하여 유연성과 확장성을 제공하고, 객체 인스턴스의 공유와 라이프사이클 관리를 효율적으로 처리할 수 있다.

 

(2) 패턴간의 차이점

ⓛ 브리지, 상태, 전략, 어댑터 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 한다는 점에서 유사하다.  하지만 적용 목적이 다른다. 

ex. 적용 예시

- 브리지 패턴: 자동차와 엔진의 관계, 플랫폼 별 유저 인터페이스 구현 등 

- 상태 패턴: 자판기의 동작 상태, 주문 처리 상태 등

- 전략 패턴: 정렬 알고리즘 선택, 결제 처리 방식 선택 등

-  어댑터 패턴: 새로운 인터페이스와 기존 코드 간의 호환성 유지, 외부 라이브러리와의 통합 등

 

 템플릿 메서드 패턴 vs 전략 패턴  

템플릿 메서드 패턴 전략 패턴
상속 기반 합성 기반
정적 (클래스 수준에서 작동) 동적 (객체 수준에서 작동)
알고리즘의 일부분을 변경하고 확장할 때 사용 동일한 작업을 수행하는 다양한 알고리즘을 캡슐화하고 교체할 때 사용
생명주기 메서드를 오버라이드하여 특정 동작을
추가 또는 변경하는 경우
정렬 알고리즘을 선택하여 데이터를 정렬하는 경우

 

 

③ 커맨드 패턴 vs 전략 패턴

커맨드 전략 패턴은 어떤 작업으로 객체를 매개변수화하는 데 사용할 수 있기 때문에 비슷해 보일 수 있지만, 이 둘의 의도는 매우 다르다.

  커맨드 패턴 전략 패턴
의도 작업을 객체로 캡슐화하여 매개변수화하고 실행연기,대기열에 작업 추가, 원격 전송 등의 기능 제공 다양한 알고리즘을 캡슐화하여 동적으로 교체하고 객체의 동작을 유연하게 변경할 수 있도록 함
객체 변환 모든 작업을 객체로 변환하여 작업의 매개변수들을 해당 객체의 필드로 사용 함 같은 작업을 수행하는 다양한 알고리즘을 객체로 캡슐화하여 교체할 수 있도록 함
사용 예시 리모컨의 버튼(커맨드)을 누르면 각 버튼에 매핑된 동작(예: TV 켜기, 라디오 끄기) 실행 다양한 결제 전략

 

 

④ 데코레이터 패턴 vs 전략 패턴

  데코레이터 패턴 전략 패턴
  동적 동적
확장  객체의 기능를 변경하고 확장 객체의 핵심 기능(알고리즘)을 교환 또는 확장
비유 객체의 외관를 바꾸는 것으로 비유 객체의 내부(핵심 로직)를 교환하는 것으로 비유
예시 입출력 스트림에 데이터 압축, 암호화 등을 추가하는 경우 정렬 알고리즘을 선택하여 데이터를 정렬하는 경우

 

 

 

 

 

 

참고

https://www.geeksforgeeks.org/strategy-method-design-pattern-in-java/?ref=lbp

https://refactoring.guru/ko/design-patterns/strategy