Spring Framework/토비의 스프링

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

계란💕 2023. 8. 2. 00:45

Environment 추상화와 프로퍼티

 

  • 지금까지 자동구성에 대한 과정을 살펴봤다. 
  • 자동 구성에 의해 만들어지는 빈 오브젝트에는 보통 기본값이 들어가있다.  ex) port: 8080
  • cf) Environment 인터페이스 
    • 스프링 애플리케이션의 환경 설정과 관련된 정보를 제공한다. 
    • 프로퍼티 값을 가져오거나 환경 변수 값을 조회하는 메서드를 제공한다. getProperty()

 

 

 

 

  • 스프링은 단일화된 방식으로 접근할 수 있도록 설계되어있다. 

 

  • 1) standardEnviroment
    • System propertyies
    • System Enviroment Variables 
  • 2) standardServletEnvironment: 웹 환경일 때 사용 가능
    • ServletConfig Parameters
    • servletcontext Parameters; 
    • JNDI
  • @PropertySource: 커스텀해서 새로운 property를 추가할 수 있다. 
  • 3) SpringBoot 
    • application.properties, xml, yml

 

  • Environment.getProperty("property.name")
    • 스프링 부트에서 위와 같이 사용하면 아래 네 가지의 변형들을 모두 확인한다. 
    • property.name
    • property_name
    • PROPERTY.NAME
    • PROPERTY_NAME

 


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

  • ApplicationRunner 인터페이스를 구현한 빈을 등록하면?
    • 스프링 부트의 모든 초기화 작업 이후,  run() 메서드를 통해 해당 빈을 실행한다. 

 

  • 기존에 만들어둔 톰캣 웹 서버를 삭제하고 시작한다. 
    • 다음과 같이 bean을 추가한다. 
    • Environment: 환경 정보를 추상화한 오브젝트

 

<hide/>
@MySpringBootApplication
public class HellobootApplication {
    @Bean
    ApplicationRunner applicationRunner(Environment env) {
        return args -> {
            String name = env.getProperty("my.name");
            System.out.println("my.name: " + name);
    };
    }
    public static void main(String[] args) {
        SpringApplication.run(HellobootApplication.class, args);
    }
}

 

  Note) 실행 결과

  • 설정 정보가 없기 때문에 null 출력된다. 

 

 

  • 설정 정보를 만들고 나면 다음 값이 출력된다. 
    • properties 파일에 다음과 같이 저장한다. 
my.name=egg

 

 

  • application.properties 보다 우선하는 것은 환경 변수이다. 
    • 환경 변수명은 일반적으로 대문자로 시작하는 게 관례이다. 
    • 실행 디버그 구성에 다음과 같이 설정해주면 아래의 설정이 우선시된다. 

 

 

  Note) 실행 결과

 

 

  • 시스템 변수는 방금 살펴본 환경 변수보다도 우선 순위에 있다. 
    • 옵션 수정의 vm 옵션 추가 버튼을 누르고다음과 같이 시스템 변수를 설정해준다. 
    • "-D" + properties 파일에 넣었던 내용을 넣는다.  (자바 표준 방식)

 

  Note) 실행 결과

 

 

  • 다음과같이 properties 를 설정한다. 
contextPath=/app

 

 

  • test 코드도 다음과 같이 바꿔줘야한다. 
<hide/>    @Test
void helloApi() {
    TestRestTemplate rest = new TestRestTemplate();
    ResponseEntity<String> response = rest.getForEntity(
        "http://localhost:8080/app/hello?name={name}", String.class, "Spring");
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)).startsWith(
        MediaType.TEXT_PLAIN_VALUE);
    assertThat(response.getBody()).isEqualTo("*Hello! Spring*");
}

@Test
void failHelloApi() {
    TestRestTemplate rest = new TestRestTemplate();
    ResponseEntity<String> response = rest.getForEntity(
        "http://localhost:8080/app/hello?name=", String.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
}

 

 


@Value와 PropertySourcesPlaceholderConfigurer

  • @Value를 이용해서 설정 값을 컴포넌트 내에서 이용하려고 한다. 
  • 다음과 같이 가져올 수 있다. 
<hide/>
@Value("${contextPath}")
String contextPath;

@Bean("TomcatWebServerFactory")
@ConditionalOnMissingBean
public ServletWebServerFactory servletWebServerFactory(Environment env) {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.setContextPath(this.contextPath);
    return factory;
}

           

 

  Note) 실행 결과

  • 위와 같이 실행하면  contextPath 에 값이 주입되지 않는다. PropertyPlaceholderConfig 를 설정하려고 한다.  

 

  • PropertySourcesPlaceholderConfigurer
    • 빈 정보를 우선적으로 모은 다음에 후처리기(PostProcessor)를 돌린다.
    •  다음 설정 정보를 resources. 안의 META-INF.spring.tobyspring.config.MyAutoConfig 파일의 안에 넣어준다. 
    • (오류) 로 인해 selectImport()의 반환 값에 추가해줬다. 
tobyspring.config.autoconfig.PropertyPlaceholderConfig

 

  Note) 실행 결과

  • 그러면 스프링 부트가 잘 돌아간다. 

 


프로퍼티 클래스의 분리

  • 지난 시간에는 프로퍼티를 클래스 내에 필드로 선언했는데 이는 번거롭다는 단점이있다. 
  • 따라서, 프로퍼티를 따로 추출해서 재사용성을 높이려고 한다. 
  • 필드로 설정하는 경우에 만약 설정 정보가 없다면? 
    • 이런 경우를 위해 default 값을 정할 수 있다. 

 

  • 필드에 직접 넣는 방법
    • port 뒤에 8080을 넣어주면 8080을 default 값으로 설정 가능
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatWebServerConfig {

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

    @Value("${port:8080}")
    int port;

    @Bean("TomcatWebServerFactory")
    @ConditionalOnMissingBean
    public ServletWebServerFactory servletWebServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.setContextPath(this.contextPath);
        factory.setPort(port);
        return factory;                  
    }
}

 

 

  • properties
contextPath=/app
port=9090

 

 

  • 기존 테스트 코드의 port도 9090으로 바꾼다. 

 

  • 아래와 같이 설정하면
    • 프로퍼티 소스의 프로퍼티명과 클래스의 프로퍼티명이 일치하는 값을 찾아서 자동으로 바인딩한다.
    • bind(name, 어떤 클래스로 바인딩할 것인지)
    •  새로운 프러퍼티가 추가될 때, 서버 프로퍼티즈 라는 클래스에 필드를 추가해주고 그 프라퍼티를 사용한다. 
<hide/>
@MyAutoConfiguration
public class ServerPropertiesConfig {
    
    @Bean
    public  ServerProperties serverProperties(Environment environment){
        return Binder.get(environment).bind("", ServerProperties.class).get();
    }
    
}

 

 


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

  • 기존에 만든 ServerPropertiesConfig 를 우선 제거한다. 

 

  • ServerProperties 클래스에 컴포넌트를 적용한다. 

 

  • 톰캣 클래스에 @Import 애너테이션을 붙인다. 
    • 빈으로 등록한다. 
<hide/>
@MyAutoConfiguration
@ConditionalMyOnClass("org.apache.catalina.startup.Tomcat")
@Import(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;
    }
}

 

 

  • MyConfigurationProperties 에너테이션을 생성한다. 
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyConfigurationProperties {
}

 

 

  • ServerProperties 인스턴스가 만들어지면 필드를 바인딩하려고한다. 
    • bindOrCreate() 바인딩을 시도했다가 없으면 새로 만든다. 
<hide/>
@MyAutoConfiguration
public class PropertyPostProcessorConfig {
    @Bean
    BeanPostProcessor propertyPostProcessor(Environment env) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class);
                if (annotation == null) {
                    return null;
                }
                return Binder.get(env).bindOrCreate("", bean.getClass());
            }
        };
    }
}

 

 

  • MyConfigurationProperties
    • prefix 적용
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
public @interface MyConfigurationProperties {
    String prefix();
}

 

 

  • properties
<hide/>
server.contextPath=/app
server.port=9090

 

 

  • ServerProperties
    • prefix를 적용한다. 
<hide/>
@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;
    }
}

 

 

  • PropertyPostProcessConfig
    • 아래와 같이 만들면 프로퍼티를 바인딩할 때, prefix를 붙여서 뒤에 나오는 이름과 prefix의 이름과 일치하는지를 체크한다. 
<hide/>

@MyAutoConfiguration
public class PropertyPostProcessorConfig {

    @Bean
    BeanPostProcessor propertyPostProcessor(Environment env) {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                MyConfigurationProperties annotation = AnnotationUtils.findAnnotation(bean.getClass(), MyConfigurationProperties.class);
                if (annotation == null) {
                    return null;
                }
                Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(annotation);
                String prefix = (String) attrs.get("prefix");
                return Binder.get(env).bindOrCreate(prefix, bean.getClass());
            }
        };
    }
}

 

  Note)실행 결과 - 서버가 잘 뜬다. 

 

 

  • 톰캣에 붙인  @Import를 @EnableMyConfigurationProeprties로 바꾸려고한다. 
    • 애너테이션 앞에 "Enable"이 붙는 경우는 메타 애너테이션 
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyConfigurationPropertiesImportSelector.class)
public @interface EnableMyConfigurationProperties {
}

 

 

  • MyConfigurationPropertiesImportSelector 클래스 생성

 

 


title

  • content
  • content

 

 

 


title

  • content
  • content

 


title

  • content
  • content

 

출처 - 인프런 토비의 스프링

https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard