오늘은 Spring의 설정과 관련된 이야기를 해볼 예정이다.

Spring 에서는 설정 정보들을 커스텀하고 좀 더 확장성 있게 변경할 수 있는 방법들을 제공한다. 그 중에 ImportAware, ImportSelector, ImportBeanDefinitionRegistrar 인터페이스가 있는데 ImportAware는 간단하고 저번에 포스팅한 부분이 있어서 제외 하고 오늘은 ImportSelector와 ImportBeanDefinitionRegistrar 대해서 알아보도록 하자.

ImportSelector

@Enable* 어노테이션으로 우리는 (Enable* 을 모른다면 다른 글들을 참고하거나 예전에 포스팅한 글이 있으니 참고하면 되겠다) 필요에 따라 미리 설정한 설정정보들을 확장하거나 변경할 수 있었다. @Enable* 어노테이션은 @Configuration 클래스의 재사용을 기반으로 한다. 하지만 모든 경우에 미리 설정한 정보들을 통째로 변경하기엔 쉽지 않은 일이다. 그래서 Spring이 좀 더 확장성 있게 설정정보들을 변경하라고 만든 인터페이스가 바로 ImportSelector 인터페이스이다. 이 인터페이스의 형태는 다음과 같다.

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata importingClassMetadata);
}

추상 메서드가 한 개 존재하는 인터페이스이다. 파라미터인 AnnotationMetadata 클래스는 클래스 이름 그대로 어노테이션의 메타정보가 담겨져있는 클래스이고 리턴타입 경우에는 String 배열로 리턴한다. 특정하게 사용하고 싶은 클래스 이름을 배열로 리턴하면 된다. 배열로 리턴하므로 여러개를 동시에 설정할 수 도 있다. 말로만 설명하면 감이 잡히지 않으니 예제를 만들어보면서 살펴 보도록 하자.
예를들어 다른 타 API 서버와 통신하기 위해 통신을 위한 설정을 등록한다고 가정하자. 어떤 프로젝트일 때는 동기식 통신만 사용하고, 또 다른 프로젝트에서는 비동기식 통신만 사용하고, 또 다른 프로젝트에선 모두 사용한다고 하자. 그럼 우리는 각각에 맞게 설정파일을 각자 만들어야 된다. 예를들면 다음과 같다.

@Bean
public RestTemplate restTemplate() {
  return new RestTemplate();
}

동기식 통신만 하는 프로젝트에 위와 같이 설정파일에 작성해야 된다. 그리고 비동기식 프로젝트에는 아래와 같이 설정해야 된다.

@Bean
public AsyncRestTemplate asyncRestTemplate() {
  return new AsyncRestTemplate();
}

만약 둘다 모두 사용한다면 둘다 모두 작성해야 된다. 각각의 프로젝트 별로 동일한 소스코드가 중복이 된다. 설정 파일들을 좀 더 재사용하고 싶은 마음이 든다.
이럴때 사용할 수 있는 Enable*ImportSelector로 설정들을 재사용할 수 있다. 일단 종류가 세가지(동기, 비동기, 모두) 이므로 enum 타입으로 NONE, ASYNC, ALL을 만들었다.

public enum Mode {
  NONE,
  ASYNC,
  ALL
}

필자의 경우 상수는 거의 대부분 enum타입으로 작성한다. 그래서 위와 같이 세개의 상수를 갖고 있는 enum 타입을 만들었다. 그리고 나서 작성할 코드는 Enable* 어노테이션이다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ImportTemplateSelector.class)
public @interface EnableTemplate {

  Mode mode() default Mode.NONE;
}

Enable* 어노테이션은 간단하다. Import할 클래스만 정해주면 된다. 그러면 Spring이 @Import 어노테이션을 찾아 ImportTemplateSelector라는 클래스를 찾아 호출해 준다. 다음으로 ImportTemplateSelector 클래스를 작성해보자.

public class ImportTemplateSelector implements ImportSelector {

  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    Map<String, Object> metaData = importingClassMetadata.getAnnotationAttributes(EnableTemplate.class.getName());
    AnnotationAttributes attributes = AnnotationAttributes.fromMap(metaData);
    Mode mode = attributes.getEnum("mode");
    if (mode == Mode.NONE) {
      return new String[]{RestTemplate.class.getName()};
    } else if (mode == Mode.ASYNC) {
      return new String[]{AsyncRestTemplate.class.getName()};
    } else if (mode == Mode.ALL){
      return new String[]{RestTemplate.class.getName(), AsyncRestTemplate.class.getName()};
    }
    return new String[0];
  }
}

아까 위에서 말했다 시피 ImportSelector 인터페이스만 구현해주면 된다. 어노테이션 속성중 mode라는 속성을 가지고 와서 그에 설정에 맞게 클래스풀네임을 리턴해주면 된다. NONE 일 경우에는 RestTemplate 클래스명을, ASYNC 일 경우에는 AsyncRestTemplate 클래스명을, ALL을 선택 했을 경우에는 둘의 클래스명을 리턴하면 된다.

한번 간단하게 테스트를 해보자.

@Configuration
@EnableTemplate(mode = Mode.ASYNC)
public class AppConfig {
}

위와 같이 ASYNC 모드로 테스트를 만들고 돌려보자.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppConfig.class)
public class RestTest {

  @Autowired(required = false)
  private AsyncRestTemplate asyncRestTemplate;

  @Test
  public void asyncRestTest() {
    assertThat(asyncRestTemplate).isNotNull();
  }
}

에러 없이 잘 실행된다. 그럼 여기서 이 상태에서 RestTemplate으로 바꾸어 보자.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppConfig1.class)
public class RestTest {

  @Autowired(required = false)
  private RestTemplate restTemplate;

  @Test
  public void restTest() {
    assertThat(restTemplate).isNotNull();
  }
}

위와 같이 했다면 테스트를 통과하지 못했을 것이다. 이유는 아시다피 ASYNC 모드로 설정했기 때문이다. 위 코드 모두 성공하려면 MODE를 ALL로 하면 된다.

@Configuration
@EnableTemplate(mode = Mode.ALL)
public class AppConfig {
}

이렇게 하면 RestTemplate과 AsyncRestTemplate 모두 빈으로 등록되니 모두 사용하고 싶다면 위와 같이 설정하면 된다. 하나의 설정파일로 각각의 프로젝트 별로 원하는 설정을 마음대로 변경 할 수 있는 장점이 있다. 하지만 여기에서 조금 문제가 있다. 물론 위와 같이 간단하게 설정할 수 있는 설정이라면 ImportSelector 인터페이스로만 사용하면 되겠지만 설정하는 대상의 클래스에 속성값들을 변경 하고 싶다면 위와 같은 방법으로 해결할 수 없다.

ImportBeanDefinitionRegistrar

예를들어 RestTemplate에 생성자 중에 ClientHttpRequestFactory 인터페이스를 받는 생성자가 존재한다. ClientHttpRequestFactory 인터페이스는 어떠한 Http Client를 사용할 것인지 결정하는 것이다. 예를들어 Netty 혹은 Okhttp, apache, httpclient 등으로 사용하고 싶은 HttpClient를 설정할 수 있다. 만약 필요에 따라 Netty, 혹은 Okhttp를 설정한다고 하면 우리는 ImportBeanDefinitionRegistrar 인터페이스를 활용해 좀 더 구체적으로 설정할 수 있다. 이 인터페이스의 형태는 다음과 같다.

public interface ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}

리턴타입을 void이며 파라미터로는 아까와 동일하게 AnnotationMetadata 클래스와 빈을 직접 등록 할 수 있는 BeanDefinitionRegistry 인터페이스가 존재한다. 한번 어떻게 만드는지 예제로 살펴보자. 일단 동일하게 Enable* 어노테이션을 만들자.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ImportRestTemplateRegistrar.class)
public @interface EnableRestTemplate {

  Class<? extends ClientHttpRequestFactory> value() default SimpleClientHttpRequestFactory.class;
}

기본적으로는 SimpleClientHttpRequestFactory 구현체를 사용하고 동일하게 @Import 사용해서 ImportRestTemplateRegistrar클래스로 설정하고 클래스를 만들자.

public class ImportRestTemplateRegistrar implements ImportBeanDefinitionRegistrar {

  private final static String BEAN_NAME = "restTemplate";

  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    Map<String, Object> metaData = importingClassMetadata.getAnnotationAttributes(EnableRestTemplate.class.getName());

    Class<? extends ClientHttpRequestFactory> value = (Class<? extends ClientHttpRequestFactory>) metaData.get("value");
    BeanDefinitionBuilder bdb = BeanDefinitionBuilder.rootBeanDefinition(RestTemplate.class);
    bdb.addConstructorArgValue(BeanUtils.instantiate(value));
    registry.registerBeanDefinition(BEAN_NAME, bdb.getBeanDefinition());

  }
}

설정한 Class 정보를 가져온 다음에 그 클래스를 인스턴스화 시켜서 빈으로 등록하면 된다. 물론 이 경우뿐만 아니라 여러 형태의 다양한 옵션들을 이때 넣어 줘도 된다.
다음으로 설정파일을 만들고 설정이 제대로 동작하는지 테스트를 해보자.

@Configuration
@EnableRestTemplate
public class AppConfig {
}

일단 기본적으로 아무 설정하지 않았을 경우를 살펴보자. 이 경우에는 SimpleClientHttpRequestFactory가 사용된다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppConfig.class)
public class HttpFactoryTests {

  @Autowired
  private RestTemplate restTemplate;

  @Test
  public void restTest() {
    System.out.println(restTemplate.getRequestFactory());
  }
}

출력되는 requestFactory는 설정과 동일하게 SimpleClientHttpRequestFactory이 출력 된다. 만약 다른 okhttp나 기타 다른 설정을 하고 싶으면 어떻게 하면 될까? 일단 okhttp만 테스트를 해보자.

<dependency>
    <groupId>com.squareup.okhttp</groupId>
    <artifactId>okhttp</artifactId>
    <version>2.7.5</version>
</dependency>

위와 같이 okhttp를 디펜더시 받고 @EnableRestTemplate 속성 값을 변경해 보자.

@Configuration
@EnableRestTemplate(OkHttpClientHttpRequestFactory.class)
public class AppConfig {
}

이렇게 해서 다시 테스트를 해보면 우리가 설정했던 것과 동일하게 OkHttpClientHttpRequestFactory 가 출력 된다.

이렇게 Spring Bean 설정들을 확장성있고 재사용성이 강한 빈 설정을 할 수 있다. 만약 회사에서 항상 사용하고 공통적인 빈 설정들이 있다면 위와 같이 설정파일들을 모아둔 프로젝트를 생성해 관리해도 나쁘지 않을 것 같다. 물론 관리를 잘 해야 겠지만..

이렇게 오늘 Spring의 ImportSelectorImportBeanDefinitionRegistrar 대해서 살펴봤다.