Spring Boot: 전략 디자인 패턴 - 편의성과 한계

11007 단어 javaspringboot
사용하기 매우 편리한 Spring Boot와 관련하여 이미 strategy pattern을 사용했을 수 있습니다.

예를 들어 인터페이스를 정의하기만 하면 됩니다(명확성을 위해 이 예제에서만 접두사I를 사용함).

public interface IOneStrategy {
  void executeTheThing();
}

다음과 같이 일부 구현을 정의합니다.

@Service("FIRST")
public class OneStrategyFirst implements IOneStrategy {

  @Override
  public void executeTheThing() {
    System.out.println("OneStrategyFirst.executeTheThing");
  }
}

이제 다음과 같이 보이는 주어진 이름을 기반으로 적절한 전략을 실행하는 서비스를 간단히 구현할 수 있습니다.

@Service
public class ExecuteStrategyOne {
  private Map<String, IOneStrategy> strategies;

  public ExecuteStrategyOne(Map<String, IOneStrategy> strategies) {
    this.strategies = strategies;
  }

  public void executeStrategyOne(String name) {
    if (!strategies.containsKey(name)) {
      throw new IllegalArgumentException("The strategy " + name + " does not exist.");
    }
    strategies.get(name).executeTheThing();
  }

}

실제 세계에서는 OneStrategyFirst , OneStrategySecondOneStrategyThird 와 같은 전략 인터페이스의 여러 구현을 만듭니다. 때때로 사용법은 다른 구현이 필요한 REST API 또는 기타 도메인 특정 코드에서 제공하는 executeStrategyOne 매개변수를 사용하는 것입니다.

여기서 편리함은 Spring Boot(더 정확하게는 Spring Framework)가 생성자를 통해 strategies 내의 ExecuteStrategyOne 맵에 다른 구현을 주입하는 것을 처리한다는 것입니다. 결과적으로 키가 @Service("FIRST")에 의해 제공되는 값이고 맵의 값이 찾을 수 있는 인터페이스IOneStrategy의 모든 구현에 대한 인스턴스화 클래스를 포함하는 맵이 생성됩니다.

정말 편리합니다.

실생활에서 예제의 FIRST , SECONDTHIRD 와 동일한 키를 사용하는 다른 전략이 필요합니까? 다음을 정의합시다.

@Service("FIRST")
public class TwoStrategyFirst implements ITwoStrategy {

  @Override
  public void executeTheThing() {
    System.out.println("TwoStrategyFirst.executeTheThing");
  }
}

해당 Spring Boot 애플리케이션을 시작하려고 하면 다음과 같은 예외가 표시됩니다.

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: 
Annotation-specified bean name 'FIRST' for bean class [com.soebes.examples.strategies.functions.two.TwoStrategyFirst] 
conflicts with existing, non-compatible bean definition of same name and class 
[com.soebes.examples.strategies.functions.one.OneStrategyFirst]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:349) ~[spring-context-5.2.9.RELEASE.jar:5.2.9.RELEASE]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:287) ~[spring-context-5.2.9.RELEASE.jar:5.2.9.RELEASE]

그렇다면 여기서 Spring Boot가 우리에게 제공하는 많은 편의를 잃지 않고 문제를 해결하기 위해 우리는 무엇을 할 수 있습니까?

먼저 각 전략 구현 클래스에서 다음과 같은 주석을 정의해야 합니다.

@Service
@Qualifier("FIRST")
public class TwoStrategyFirst implements ITwoStrategy {

  @Override
  public void executeTheThing() {
    System.out.println("TwoStrategyFirst.executeTheThing");
  }
}

다른 주석의 키를 사용하여 모순된 빈 이름의 중복을 방지하여 대신 사용합니다@Service("FIRST"). @Qualifier("FIRST")의 사용법은 우리에게 다른 것을 처리하는 기준을 제공합니다.

이제 다음과 같이 ExecuteStrategyOne 클래스를 변경해야 합니다.

@Service
public class ExecuteStrategyOne {

  private Map<String, IOneStrategy> strategies;

  public ExecuteStrategyOne(List<IOneStrategy> strategies) {
    this.strategies = strategies.stream()
        .collect(
            toMap(k -> k.getClass().getDeclaredAnnotation(Qualifier.class).value(), 
                  Function.identity()));
  }
  ...
}

이전에 사용된List<IOneStrategy> strategies 대신 생성자 매개변수Map<String, IOneStrategy> strategies의 사용을 강조하고 싶습니다. 이는 주어진 인터페이스에서 Spring Boot에 의해 해당 목록으로 모든 구현 목록을 가져오는 데 편리합니다. 이제 @Qualifier 주석을 사용하여 정의한 키로 맵으로 변환해야 합니다. 다음과 같은 스트림으로 모든 것을 해결할 수 있습니다.

this.strategies = strategies
  .stream()
  .collect(
    Collectors.toMap(k -> k.getClass().getDeclaredAnnotation(Qualifier.class).value(), 
                     Function.identity()));

구현을 살펴보고 주석@Qualifier을 추출하고 갖고 싶은 키value()를 읽습니다. Collectors.toMap를 사용하여 결과를 Map에 수집하고 그 결과를 인스턴스 변수private Map<String, IOneStrategy> strategies;에 할당합니다.
필요에 따라 인스턴스 변수를 final로 정의하는 것이 물론 가능하며 필요한 경우 Collectors.toUnmodifiableMap 대신 적절한 toMap(..)를 사용하여 수정할 수 없는 맵을 만들 수 있습니다.

따라서 코드를 약간 변경하면 코드에서 동일한 키를 사용하는 다른 전략을 사용하는 문제를 쉽게 해결할 수 있습니다.

주어진 코드는 full working example on GitHub 으로 사용할 수 있습니다.

좋은 웹페이지 즐겨찾기