본문 바로가기
관리자

Programming-[Base]/Design, Architecture

Strategy 패턴 w/ Spring 다형성

728x90
반응형

 

 

 

https://integu.net/strategy-pattern/

 

1. Strategy 패턴 사용 배경 및 목적

 

Strategy 패턴은 코딩을 하다보면 자주 쓰이는 type별 if 분기구문을 정리하고 고도화하고 싶을 때 사용하기 좋다. 아래와 같은 코드를 예로 들 수 있다.

 

요청으로 들어오는 GrantType의 값에 따라 body 값이 형태가 조금씩 바뀌는 형태이다. 이런 경우 formatted 내부 메서드를 공통화하여 메서드로 만들 수 없으므로 if문으로 분기해놓은 것인데, 코드가 길고 유연성이 떨어진다.

String body;
String grantType = tokenPayload.getGrantType();
if(Objects.equals(grantType, GrantTypeEnum.AUTHORIZATION_CODE.getValue())) {
  body = """
  grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s
  """.formatted(
      tokenPayload.getGrantType(),
      tokenPayload.getClientId(),
      KeycloakClientInfo.getClientSecretBy(tokenPayload.getClientSecret()),
      tokenPayload.getCode(),
      tokenPayload.getRedirectUri());
} else if (Objects.equals(grantType, GrantTypeEnum.REFRESH_TOKEN.getValue())) {
  body = """
  grant_type=%s&client_id=%s&client_secret=%s&refresh_token=%s
  """.formatted(
      tokenPayload.getGrantType(),
      tokenPayload.getClientId(),
      KeycloakClientInfo.getClientSecretBy(tokenPayload.getClientId()),
      tokenPayload.getRefreshToken()
  );
} else {
  //...
}

 

Strategy 패턴(전략 패턴)을 사용하면 아래와 같이 두 줄로 줄일 수 있다. 그리고 인터페이스화하여 코드가 분리되고 유연성이 증가한다. 물론 클래스 파일을 많이 만들고 구조가 복잡해지는 단점은 있다.

GrantTypeStrategy strategy = grantTypeStrategyFactory.getStrategy(GrantTypeEnum.valueOf(tokenPayload.getGrantType()));
String body = strategy.createRequestBody(tokenPayload);

 

 

2. 구체적인 구현 방법

 

Strategy 인터페이스

위 예시에서 타입별로 다른 body가 만들어져야하는 필요성이 있었다. 이런 타입별로 body를 만드는 Strategy 인터페이스를 하나 만들어준다.

public interface GrantTypeStrategy {
  String createRequestBody(TokenPayload tokenPayload);
  }

 

Strategy 구현체

그리고 타입별로 body가 달라져야하기 때문에 이를 모두 클래스화하고, GrantTypeStrategy를 상속하도록 한다. 나중에 해당 타입으로 묶어서 빈으로 사용하기 위해 Spring의 @Component 어노테이션도 달아준다.

@Component
public class AuthorizationCodeGrantStrategy implements GrantTypeStrategy {
  @Override
  public String createRequestBody(TokenPayload tokenPayload) {
    return """
      grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s
      """.formatted(
        tokenPayload.getGrantType(),
        tokenPayload.getClientId(),
        KeycloakClientInfo.getClientSecretBy(tokenPayload.getClientSecret()),
        tokenPayload.getCode(),
        tokenPayload.getRedirectUri()
    );
  }

  @Override
  public GrantTypeEnum getGrantType() {
    return GrantTypeEnum.AUTHORIZATION_CODE;
  }

}

 

@Component
public class RefreshTokenGrantStrategy implements GrantTypeStrategy {
  @Override
  public String createRequestBody(TokenPayload tokenPayload) {
    return """
      grant_type=%s&client_id=%s&client_secret=%s&refresh_token=%s
      """.formatted(
        tokenPayload.getGrantType(),
        tokenPayload.getClientId(),
        KeycloakClientInfo.getClientSecretBy(tokenPayload.getClientId()),
        tokenPayload.getRefreshToken()
    );
  }

  @Override
  public GrantTypeEnum getGrantType() {
    return GrantTypeEnum.REFRESH_TOKEN;
  }
}

 

 

Factory

그리고 조건에 따라 이런 Strategy 구현체들을 생성할 수 있는 Factory 클래스를 만들어준다.

 

Spring 다형성 🌟

여기서 살펴볼 점은 List<GrantTypeStrategy> 형태로 해당 인터페이스에 속하는 모든 구현체를 다형성을 활용해 불러올 수 있다는 점이다. 평소에 빈들을 주입하듯이 @RequiredArgsConstructor를 선언해주면 인터페이스 타입의 List를 선언해주는 것만으로 모든 구현체를 불러와서 로직을 처리할 수 있어 편하다.

@Component
@RequiredArgsConstructor
public class GrantTypeStrategyFactory {
  private final List<GrantTypeStrategy> strategies;

  public GrantTypeStrategy getStrategy(GrantTypeEnum grantTypeEnum) {
    return strategies.stream()
        .filter(strategy -> strategy.checkType(grantTypeEnum))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Invalid grant type: " + grantTypeEnum.getValue()));
  }
}

 

Default 메서드 선언

부가적으로, strategy.checkType()이라는 메서드를 썼는데 각 Strategy 구현체가 아니라 interface에 default 메서드로 선언함으로써 각 구현체가 똑같은 메서드를 참조할 수 있도록 해주었다.

 

public interface GrantTypeStrategy {
  String createRequestBody(TokenPayload tokenPayload);

  GrantTypeEnum getGrantType();

  default Boolean checkType(GrantTypeEnum grantType) {
    return this.getGrantType().equals(grantType);
  }
}

 

이제 맨 처음에 본 것과 같이 Factory와 각 구현체의 메서드를 사용하여 처리하면 코드가 훨씬 간결해진다. 나중에 새로운 GrantType이 생성되더라도 GrantTypeStrategy로 묶어 응집력있게 관리할 수 있고 유연하게 대처할 수 있다.

GrantTypeStrategy strategy = grantTypeStrategyFactory.getStrategy(GrantTypeEnum.valueOf(tokenPayload.getGrantType()));
String body = strategy.createRequestBody(tokenPayload);

 

728x90
반응형