오늘은 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의
ImportSelector
와
ImportBeanDefinitionRegistrar
대해서 살펴봤다.