Spring

스프링 IoC 컨테이너와 Bean ( 3 )

들어가기에 앞서 본 포스팅은 백기선님의 인프런 강의를 기반으로 작성된 포스팅임을 알려드립니다.


처음 스프링을 배우는 입장이라 정확하지 않은 정보가 있을 수 있습니다.

댓글을 통해 알려주신다면 최대한 빨리 피드백하도록 하겠습니다!

 

 

ApplicationContext

 

AppicationContext는 컨테이너에서 Bean을 꺼내오는 기능말고도 다양한기능을 가지고 있습니다.

 

Bean들의 그룹을 의미하는 Profile과 Property를 다루는 Environment

 

 

 

 

 

 

 

 

 

 

Environment 

 

Spring Framework Docs

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

 

 

 

 

 

 

ApplicationContext가 상속하고있는 EnvironmentCapable을 통하여 Environment 객체를 얻을 수 있습니다.

이 Environment를 통하여 Profile과 Property에 대한 작업을 수행할 수 있습니다.

 

 

 

 

 

Profile

 

프로파일은 환경에 따라 사용될 Bean들을 분류하고 싶을때 사용됩니다.

 

예를들어 " 테스트 환경에서는 A라는 Bean을 사용하고 배포 환경에서는 B라는 Bean을 사용하겠다. " 라는 상황이 있을 수 있습니다.

 

 

 

프로파일의 정의방법은 클래스에 정의하는 방법과 메소드에 정의하는 방법이 있습니다.

 

 

먼저 자바 Config파일에 프로파일을 지정해서 해당 Config내의 빈들이 어떤 프로파일에서 사용되는지를 결정할 수 있습니다.

 

이 외에도 @Component가 붙은 클래스에도 동일한 방법으로 프로파일을 설정할 수 있습니다.

 

 

 

 

 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Environment environment = context.getEnvironment();
        String[] activeProfiles = environment.getActiveProfiles();

        Arrays.stream(activeProfiles).forEach(System.out::println);
    }
}

 

이런 프로파일의 설정은 IntelliJ의 Edit Configurations에서 -Dspring.profiles.active="프로파일 명" 옵션을 줌으로써 할 수 있고, 활성화된 Profile은 Environment 객체를 이용하여 확인할 수 있습니다.

 

 

 

 

 

 

이렇게 활성화를 시키게되면 해당 Config내의 Bean들이 컨테이너에 들어있는 것을 확인할 수 있습니다.

 

 

 

 

 

Test 프로파일만 활성화되어 realService가 등록되지 않음.

 

이 외에도 메소드에 프로파일을 주는것도 가능하며 Config가 아닌 @Component가 붙은 클래스에서도 설정할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

테스트에서 프로파일을 적용하기 위해서는 @ActiveProfiles 애노테이션을 사용하면 됩니다.

 

 

 

 

 

 

 

Property

 

Environment객체를 이용하여 프로퍼티 값을 가져올 수 있습니다. 

 

프로퍼티란 애플리케이션에 등록되는 다양한 종류의 key : value 쌍을 의미합니다. 

 

제공되는 프로퍼티들에 대하여 우선순위에 따라 계층형으로 접근합니다.

 

 

 

 

 

 

 

 

예를들어 이렇게 넘겨준 프로퍼티는 app.name : spring의 key : value 쌍이 되며 Environment를 통하여 접근할 수 있습니다.

 

더보기
@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("app.name");

        System.out.println(property);
    }
}

 

 

 

 

 

 

 

프로퍼티를 처리하는 클래스는 Environment가 상속받고있는 PropertyResolver입니다.

 

PropertyResolver 내에는 getProperty, containsProperty와 같은 property 관련 메소드들이 존재합니다.

 

 

 

 

 

스프링 공식문서에 의하면 @PropertySource를 통해 파일로 관리되는 property들을 사용할 수 있다고 합니다.

 

 

Config파일에 @PropertySource(" 프로퍼티 파일 경로 " ) 를 입력하게되면 해당 파일에 접근하여 프로퍼티들을 얻어옵니다.

 

 

 

 

 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        Environment environment = context.getEnvironment();
        boolean value = environment.containsProperty("value");
        boolean myvalue = environment.containsProperty("my.value");
        
        System.out.println("value : " + value);
        System.out.println("my.value : " + myvalue);
    }
}

Environment를 이용하여 해당 property의 key값이 존재하는지를 확인해볼 수 있습니다.

 

 

 

 

 

 

 

 

key값에 대한 value값을 입력하지 않으면 어떻게 동작할지 궁금해서 실험해봤더니 빈 문자열이 value값으로 들어가는 것을 확인할 수 있었습니다.

 

 

 

 

앞서 프로퍼티들을 우선순위에 따라 계층형으로 접근한다고 말을 했는데, 공식문서에 따르면 다음과 같은 우선순위를 가집니다.

 

 

기본적으로 시스템 프로퍼티가 환경변수보다 높은 우선순위를 가진다. 

즉,  시스템 속성과 환경변수에 동일한 key값의 프로퍼티가 있다면 시스템 속성쪽의 프로퍼티가 사용된다.

 

 

StandardServletEnvironment의 우선순위

  1. ServletConfig 파라미터
  2. ServletContext 파라미터
  3. JNDI 환경 변수 ( java:comp/env/ )
  4. JVM 시스템 속성 ( -D 옵션을 통하여 입력된 프로퍼티 ) 
  5. JVM 시스템 환경 ( OS 환경변수 ) 

 

 

@PropertySource를 통한 프로퍼티는 5번에 해당하고 -D옵션을 준 경우는 4번에 해당하므로 동일한 key값의 프로퍼티를 정의하면 4번에 해당하는 것이 나올 것으로 예상됩니다.

 

 

 

 

 

예상대로 4번에 해당되는 프로퍼티가 적용되어 출력되는 모습이네요.

 

 

 

 

 

 

주의사항으로 MutablePropertySources 라는 클래스를 이용하여 위에서 설명한 우선순위를 커스텀하게 변경할 수 있다는 사실이 있습니다.

 

직접 확인해봅시다!

 

 

 

 

 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ConfigurableApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ConfigurableEnvironment environment = context.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();

        propertySources.stream().forEach(propertySource -> {
            System.out.println(propertySource.toString() + " : " + propertySources.precedenceOf(propertySource));
        });
    }
}

현재 프로퍼티들의 우선순위를 확인해보면 이러한 출력결과를 보여줍니다.

여기서 제가 등록했던 application.properties 파일은 4와 5의 우선순위를 가지고 있습니다.

 

 

 

 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ConfigurableApplicationContext context;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ConfigurableEnvironment environment = context.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        
        PropertySource<?> MypropertySource = propertySources.get("class path resource [application.properties]");

        propertySources.addFirst(MypropertySource);

        propertySources.stream().forEach(propertySource -> {
            System.out.println(propertySource.toString() + " : " + propertySources.precedenceOf(propertySource));
            System.out.println(propertySource.getName());
        });
    }
}

위 코드처럼 해당 properties 파일에 대한 PropertySource를 가져와서 우선순위를 최상위로 만들어주는것이 가능합니다.

 

 

 

 

 

 

 

 

 

 

MessageSource

 

ApplicationContext는 MessageSource라는 클래스를 상속하고 있으며 이는 국제화 ( i18n ) 기능을 제공합니다.

여기서 국제화란 다양한 언어들을 지원하는 기능을 의미합니다.

 

즉, Message에 대하여 다양한 언어들을 지원합니다.

 

 

 

스프링 부트 기준으로 Message들은 messages.properties 라는 파일을 이용하여 관리할 수 있습니다.

messages.properties는 기본 Locale을 의미하며 messages와 properties사이의 값을 통하여 지역설정을 할 수 있습니다. 

 

 

 

 

 

 

 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    MessageSource messageSource;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        String koKRMessage = messageSource.getMessage("greeting", new String[]{"ddings"}, Locale.KOREA);
        String enUSMessage = messageSource.getMessage("greeting", new String[]{"ddings"}, Locale.US);

        System.out.println("ko_KR properties => " + koKRMessage);
        System.out.println("en_US properties => " + enUSMessage);
    }
}

 

 

 

이러한 메시지들은 ReloadableResourceBundleMessageSource 클래스를 통하여 프로젝트가 실행되고 있는 와중에 메시지를 변경하는 것도 가능합니다.

 

 

 

@SpringBootApplication
public class SpringhwApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringhwApplication.class);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.run(args);
    }

    @Bean
    public MessageSource messageSource(){
        var messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(3);
        return messageSource;
    }

}

위 코드와 같이 사용자가 임의로 ReloadableResourceBundleMessageSource 클래스를 반환도록 Bean을 만든 다음 프로그램을 실행한 뒤, 빌드를 통하여 출력되는 메시지를 변경할 수 있습니다.

 

 

 

 

 

 

 

 

 

 

ApplicationEventPublisher

 

 

ApplicationEventPublisher는 옵저버 패턴을 구현하고있는 클래스로, Event기반의 프로그래밍 시에 유용한 인터페이스입니다.

 

Event기반의 프로그래밍이란, 외부의 어떤 이벤트 ( ex : 마우스 클릭 ) 가 발생하면 그에 대응하는 방식으로 프로그래밍하는 것을 의미합니다.

 

 

Spring이 제공하는 표준 이벤트
Event 설명
ContextRefreshedEvent ApplicationContext가 초기화되거나, refreshed 되었을 때.

초기화 되었다는 것은 모든 Bean들이 로드된걸 의미한다.
ContextStartedEvent ApplicationContext가 실행되었을 때. ( ConfigurableApplicationContext의 start()  ) 

모든 lifecycle Bean들이 시작신호를 받은 시점
ContextStoppedEvent ApplicationContext가 정지했을 때. ( ConfigurableApplicationContext의 stop()  )

모든 lifecycle Bean들이 정지신호를 받은 시점
ContextClosedEvent ApplicationContext가 close되었을 때. ( ConfigurableApplicationContext의 close() 또는 JVM이 종료) 

모든 싱글톤 Bean들이 소멸된 시점
RequestHandledEvent web쪽의 이벤트. 모든 Bean들이 HTTP request 가 제공되었음을 의미.

request요청이 끝난 시점.

Spring의 DispatcherServlet가 사용된 web 애플리케이션에 적용된다.
ServletRequestHandledEvent RequestHandledEvent의 서브클래스로 서블릿쪽 정보가 추가되었다.

 

 

 

Spring에선 이런 Event들을 ApplicationEvent를 상속함으로써 만들 수 있습니다.

 

 

 

 

public class MyEvent extends ApplicationEvent {

    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public MyEvent(Object source) {
        super(source);
    }

}

 

이렇게 만들어진 이벤트들을 발생시키는 기능을 ApplicationEventPublisher가 가지고 있습니다. 

 

이때 이벤트를 받아서 처리하는 기능을 EventHandler 라고 부릅니다

 

 

 

즉, 만들어진 이벤트를 Publisher가 발생시키면 Handler가 받아서 처리하는 식으로 동작합니다.

여기서 EventHandler는 Bean으로 등록되어있어야 하며, Spring Framework 4.2 이후로는 @EventListener 를 사용하여 만들 수 있습니다.

 

 

 

 

 

Spring Framework 4.2 전의 EventHandler
더보기
@Component
public class MyEventHandler implements ApplicationListener<MyEvent> {

    @Override
    public void onApplicationEvent(MyEvent event) {
        System.out.println("이벤트를 받았습니다. " + event);
    }
}

 

 

 

@Component
public class MyEventHandler {

    @EventListener({MyEvent.class, SecondEvent.class})
    public void handle(Object event){
        System.out.println(event.getClass());
    }

}

애노테이션 기반의 EventHandler는 다른 클래스를 상속받을 필요가 없으며, 하나의 메소드에서 여러개의 이벤트를 처리할 수 있습니다.

 

또한 @Order를 통하여 이벤트의 처리순서를 지정할 수 있으며, @EnableAsync와 @Async를 통하여 병렬적으로 이벤트를 처리할 수도 있습니다.