WEB/Spring Boot

외부 설정을 이용한 자동 구성

Tony Lim 2023. 2. 13. 13:57
728x90

Environment 추상화와 프로퍼티

imports 를 selector 가 로딩을하고 class level @Conditional 을 확인하고 method level(@Bean을 생성하는 팩토리메소드) 의 @Conditional을 확인하게 된다.

custom bean구성정보는 개발자가 추가한 (custom tomcat) 팩토리 빈 메소드를 의미한다.

기본 tomcat port를 바꾸고 싶다든지 다양한 property를 변경을 가능하게 한다. 읽어와서 사용을 할 수 있다.

 

getProperty에 넣는 인자는 다음 사진처럼 4가지로 spring boot가 알아서 converting 해서 인식을 하게 된다. 


자동 구성에 Environment 프로퍼티 적용

@MySpringBootApplication
public class HellobootApplication {

    ApplicationRunner test(Environment environment) {
        return args -> {
            System.out.println(environment.getProperty("my.name"));
        };
    }
    public static void main(String[] args) {
        SpringApplication.run(HellobootApplication.class, args);
    }
}

스프링에서 제공해주는 Environment는 jvm option(System property) > Environment property(환경변수) > application.properties 순으로 우선순위를 가지게 된다.
key를 동일하게 my.name으로 해주는 경우를 말한다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
    @Bean("tomcatWebServerFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(Environment env) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.setContextPath(env.getProperty("contextPath"));
        return factory;
    }
}

contextpath는 해당 servlet container의 시작지점을 의미한다. 
localhost:8080/hello  에서 localhost:8080/app/hello 처럼 작성해줘야 제대로 요청을 처리하게 된다.


@Value 와 PropertySourcesPlaceholderConfigurer

@Value("${contextPath}")
String contextPath;

application.properties에 있는 값을 $ 로 읽어오는 기술을 spring에서 바로제공해주는것은아니다.

@MyAutoConfiguration
public class PropertyPlaceholderConfig {
    @Bean PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

BeanPostProcess에서 확장된 것이다. 해당 클래스를 빈으로 등록하면 위에 @Value값을 property 로부터
제대로 읽어올 수 있다.
Springboot에서는 자동 구성에 포함된 녀석이다.

지금은 Springboot의 autoconfiguration을 사용하지 않으니
imports에 적어서 부팅시 해당 클래스를 자동으로 빈으로 등록할 수 있게 한다.


프로퍼티 클래스의 분리

@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {
    @Bean("tomcatWebServerFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(ServerProperties properties) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

        factory.setContextPath(properties.getContextPath());
        factory.setPort(properties.getPort());

        return factory;
    }
}
@MyAutoConfiguration
public class ServerPropertiesConfig {
    @Bean
    public ServerProperties serverProperties(Environment environment) {
        return Binder.get(environment).bind("", ServerProperties.class).get();
    }
}

properties 에서 가져올 설정들이 많으면 별도의 클래스를 지정해서 getter 로 가져오는것이 용이하다.

Binder를 통해 ServerProperties 클래스안에 있는 필드들의 getter,setter를 통해서 자동으로 알아서 binding을해준다.


프로퍼티 빈의 후처리기 도입

@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
@EnableMyConfigurationProperties(ServerProperties.class)
public class TomcatWebServerConfig {
    @Bean("tomcatWebServerFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory(ServerProperties properties) {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

        factory.setContextPath(properties.getContextPath());
        factory.setPort(properties.getPort());

        return factory;
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyConfigurationPropertiesImportSelector.class)
public @interface EnableMyConfigurationProperties {
    Class<?> value();
}

주로 Enable~ Annotation을 사용하는 이유는 meta annotation으로 @Import를 사용하여 가져오는것을 한번 숨기는 역할을 하게 된다.

public class MyConfigurationPropertiesImportSelector implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        MultiValueMap<String, Object> attr = importingClassMetadata.getAllAnnotationAttributes(EnableMyConfigurationProperties.class.getName());
        Class propertyClass = (Class) attr.getFirst("value");
        return new String[] { propertyClass.getName() };
    }
}

인자로 들어온 ServerProperties 클래스를 여기서 읽어들어와 bean으로 등록하게 된다.

단순히 SeverProperties를 빈으로 등록하는 것만으로는 ServerProperties에 우리가 원하는 property 값이 채워져 있지 않다. 
밑에나오는 BeanPostProcessor 를 통해서 채워 넣는 과정이 필요하다.

@MyConfigurationProperties(prefix = "server")
public class ServerProperties {
    private String contextPath;

    private int port;

    public String getContextPath() {
        return contextPath;
    }

    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }
}

prefix를 통해 server.port 이런식으로 어떤 맥락에서 사용되는지 알 수 있게 된다.

@MyAutoConfiguration
public class PropertyPostProcessorConfig {
    @Bean BeanPostProcessor propertyPostProcessor(Environment env) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                MyConfigurationProperties annotation = findAnnotation(bean.getClass(), MyConfigurationProperties.class);
                if (annotation == null) return bean;

                Map<String, Object> attrs = getAnnotationAttributes(annotation);
                String prefix = (String) attrs.get("prefix");

                return Binder.get(env).bindOrCreate(prefix, bean.getClass());
            }
        };
    }
}

Environment 를 통해서 property들을 읽어와서 ServerProperties에 맵핑해준다.

@MyConfigurationProperties이 달려있는 (marker annotation)  bean에만 후처리기가 동작하게 된다.

Binder를 통해서 맵핑이 진행된다. prefix 와 bean 클래스정보를 넘겨주면 알아서 prefix를 앞에 붙인상태로 class 의 field property와 일치하는것을 찾는다.

prefix = server이면 server.port , server.ip 가 application.yml 에 적혀있는지 찾게 된다.

728x90