IoC 컨테이너의 Environment
에 대해서 알아보겠습니다. Environment
는 ApplicationContext
가 가지고있는 기능중 하나로 Application에 등록되어있는 여러 환경들을 제어하고 이용할 수 있도록 도와주는 기능입니다. 아래에서 조금더 자세히 알아보도록 하겠습니다.
*ApplicationContext - IoC 컨테이너가 갖추어야 할 기능들을 정의하고 있는 인터페이스로 다른 추가적인 기능들을 상속받고 있습니다 그 중 하나가 바로 EnvironmentCapable
입니다.
Environment의 기능
1. Profile
Profile
이란 환경에 따라 필요한 Bean들이 달라질 수 있는데 이것을 쉽게 관리할 수 있도록 도와주는 기능입니다. 예를 들어 Test 환경에서 사용할 Bean들과 Develop중 사용할 Bean들 그리고 release에서 사용될 Bean들이 다른 경우 이것을 쉽게 관리할 수 있도록 돕는다는 것 입니다. 역시, 예제를 통해 알아보도록 하겠습니다.
우선 Environment
객체를 얻어오는 방법 부터 알아보겠습니다. 첫째로 ApplicationContext를 생성하도록 합니다. ApplicationContext에는 위에서 알아본것처럼 EnvironmentCapable
이라는 Interface를 상속받고 있습니다. 이 덕분에 ApplicationContext로 부터 쉽게 Environment 객체를 얻어올 수 있습니다.
xxxxxxxxxx
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(Arrays.toString(env.getActiveProfiles()));
}
}
== 결과 ==
[]
얻어온 environment 객체에는 getActiveProfiles()라는 메소드가 있습니다. 이름 그대로 현재 활성화된 Profile을 return해주는 메소드입니다. 결과에는 아무런 값이 나오질 않네요 그렇다면 Profile이 적용되어있지 않으니 아무런 Bean들도 IoC에 등록이 안되어 있는 걸까요? 아닙니다. 바로 Default Profile
이란 것이 활성화 되어있습니다.
xxxxxxxxxx
System.out.println(Arrays.toString(env.getDefaultProfiles()));
== 결과 ==
[default]
위의 코드에서 getActiveProfiles() 메소드만 getDefaultProfiles()
메소드로 변경했습니다. 결과로는 default가 출력되었습니다. 어떤 의미일까요? 이것은 어떤 Profile
이 활성화된 상태여도 기본적으로 추가되는 Profile이 있다는 의미일 것입니다. 우리는 아직 아무런 Profile도 적용하지 않은 상태이니까요. Default Profile에는 어떤 bean들이 들어가 있을까요?
public class BookService { ... }
바로 위의 Bean처럼 아무런 @Profile
에 대한 설정이 없는 Bean들이 모두 DefaultProfile
에 들어가게 됩니다.
1.1 Profile 지정 및 적용하기
우선 @Profile
어노테이션을 사용하는 방법을 알아본 뒤 그 Profile을 적용하는 방법을 알아 보겠습니다.
Profile 지정하기
"test") (
public class BookService { }
Profile을 지정하는 방법은 간단합니다. Bean으로 등록할 Class에 @Profile("Profile 이름")
어노테이션을 추가적으로 부여하면 됩니다. @Profile
어노테이션의 괄호안에 들어가는 문자가 Profile의 이름이 되게 됩니다.
Profile 적용하기
IntelliJ IDE 기준으로 Profile을 적용하기 위해서는 먼저 우측상단의 Application을 클릭하면 나타나는 Edit Configurations를 클릭합니다.
그 후 하단의 Active profile에 이전에 @Profile
어노테이션으로 지정했던 Profile의 이름을 적어주면 됩니다.
추가적으로 Intellij의 Ultimate 버전이 아닌 경우에는 상단쪽의 Environment를 확장시켜 위 그림과 같은 형태로 작성해주시면 됩니다.(-Dspring.profiles.active="Profile 이름"
)
Profile 사용
xxxxxxxxxx
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
BookService bookService;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(Arrays.toString(env.getActiveProfiles()));
System.out.println(bookService);
}
}
== 결과 ==
[test]
com.keesun.spring.BookService
ApplicationContext
로 부터 Environment
객체를 얻어와 우선 현재 활성화 된 Profile을 출력
하고 해당 Profile로 지정한 BookService Bean이 존재하는지 출력해보겠습니다. 결과를 보면 ActiveProfile이 test
이고 해당 Profile로 지정한 BookService Bean이 출력되는 것을 볼 수 있습니다.
"test") (
public class TestConfiguration {
public BookRepository bookRepository(){
return new TestBookRepository();
}
}
추가적으로 위와 같이 Bean Configuration
Class에도 Profile 지정이 가능합니다.
x
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
BookRepository bookRepository;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(Arrays.toString(env.getActiveProfiles()));
System.out.println(bookRepository);
}
}
== 결과 ==
[test]
com.keesun.spring.TestBookRepository
1.2 Profile을 적용한 경우 Default Profile의 Bean이 사용가능 한가요?
네 당연히 사용 가능합니다. 테스트를 해보도록 하겠습니다.
public class BookService { }
우선 BookService Class에 적용했던 @Profile
어노테이션을 삭제합니다.
test
Profile이 적용되어있음을 확인합니다.
xxxxxxxxxx
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
BookService bookService;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(Arrays.toString(env.getActiveProfiles()));
System.out.println(bookService);
}
}
== 결과 ==
[test]
com.keesun.spring.BookService
ActiveProfile
에 test
Profile이 들어있지만 Default Profile인 BookService가 사용가능한 것을 볼 수 있습니다.
1.3 Profile 연산(표현식)
- ! : not
- & : and
- | : or
Profile 연산? 이라고 하니 조금 이상하네요, 위와 같은 Profile 표현식을 이용해 Profile을 조합하여 사용할 수 있습니다. 간단한 하나의 예제를 보여드리겠습니다. 하나의 예제만 있다면 나머지는 충분히 알아서 하실 수 있다고 생각합니다.
"!test") (
public class BookService { }
우선 BookService에 @Profile()
어노테이션을 부여합니다. test
를 지정한 것 같지만 자세히 보면 앞쪽에 !(not)
이 있음을 볼 수 있습니다. 바로 실행을 해보도록 하겠습니다. 참고로 현재 적용된 Profile은 test
입니다.
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
BookService bookService;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(Arrays.toString(env.getActiveProfiles()));
System.out.println(bookService);
}
}
다음과 같이 BookService를 자동으로 주입받도록 했습니다. 결과는 어떨까요.
해당 타입의 Bean을 찾을 수 없다는 에러가 나타납니다.
2. Property (https://backback.tistory.com/160)
Property
란 Application의 외부에서 입력한 정보를 이용해 설정값을 변경하는 방법들을 제공하는 기능입니다. 외부에서 제공되는 정보들은 우선순위를 가지고 있고 다음과 같은 것들이 존재합니다.
우선순위
- ServletConfig 매개변수
- ServletContext 매개변수
- JNDI (java:comp/env/)
- JVM 시스템 프로퍼티 (-Dkey="value")
- JVM 시스템 환경 변수(운영체제 환경변수)
사용 예
Database 설정
예를 들면 Database에 대한 url, name, pw 등의 정보를 xml(application.properties) 파일에 기술하고 이것을 environment를 통해 가져와 설정하므로써 java 코드의 수정 없이 설정정보를 변경할 수 있습니다.
2.1 Property를 추가하는 방법 (Annotation 사용 전)
사용자가 Property를 직접 생성하여 Environment에 추가를 할 수도 있습니다. 우선 Environment에 외부 데이터를 객체화해 담는 과정을 살펴보겠습니다.
2.1.1 Environment에 Property가 추가되는 과정
1. 우선 Context
의 getEnvironment()
메소드를 통해 ConfigurableEnvironment를 가져옵니다. (이 때 꼭 ConfigurableEnvironment
를 가져와야 MutablePropertySources
를 가져올 수 있습니다. )
2. 그후 ConfigurableEnvironment
의 getPropertySources()
메소드를 이용해 MutablePropertySources
를 얻어 옵니다.
3. MutablePropertySources
에 존재하는 add*()
메소드들에 매개변수로 PropertySource
를 다시 PropertySource
의 매개변수로 외부파일
을 전달하여 Property들을 추가합니다. 이 때 각각의 값들은 PropertySource
Type으로 MutablePropertySources
안에 존재하는 propertySourceList
에 추가 됩니다.
자세히 살펴보기
ConfigurableEnvironment는 뭐고 MutablePropertySources는 무엇이며 ProperySource는 뭐지?... 각각에 대해 조금더 자세히 알아보도록 하겠습니다.
1. ConfigurableEnvironment
Environment에 데이터를 추가하기 위해서 왜 ConfigurableEnvironment
라는 Interface가 필요할까요? 우선 Environment 인터페이스를 살펴보도록 하겠습니다.
x
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
boolean acceptsProfiles(Profiles profiles);
}
먼저 앞서 알아보았던 첫기능인 Profile에 대한 메소드들이 보입니다.(getActiveProfiles(), getDefaultProfiles()) 음 Property를 추가하기위한 메소드들은 보이지 않네요. 상속받은 Interface인 PropertyResolver
를 살펴봅시다.
xxxxxxxxxx
public interface PropertyResolver {
boolean containsProperty(String key);
String getProperty(String key);
String getProperty(String key, String defaultValue);
<T> T getProperty(String key, Class<T> targetType);
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
String getRequiredProperty(String key) throws IllegalStateException;
<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
String resolvePlaceholders(String text);
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
getProperty()
등의 메소드들이 보입니다. 아, Environment에서 Property들을 가져올 수 있었던 이유가 이것이였나 봅니다. 음 그래도 Property를 추가하는 메소드들은 보이지 않는 것 같습니다. 무언가 값을 추가하기 위한 하위 Interface가 있지 않을까요?
Environment의 하위 Interface중 ConfigurableEnvironment
라는 Interface가 보입니다. 더 자세히 봅시다.
xxxxxxxxxx
public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
void setActiveProfiles(String... profiles);
void addActiveProfile(String profile);
void setDefaultProfiles(String... profiles);
MutablePropertySources getPropertySources();
Map<String, Object> getSystemProperties();
Map<String, Object> getSystemEnvironment();
void merge(ConfigurableEnvironment parent);
}
ConfigurableEnvironment Interface
입니다. MutablePropertySources
를 Return하는 getPropertySources()
메소드와 getSystem*()
메소드들이 추가된 것 같습니다. getSystem*()
메소들은 시스템 환경변수 관련한 설정들을 가져오는 메소드인 것 같군요, MutalblePropertySources
.. 변할 수 있는 PropertySource? 조금 수상합니다. 더 알아보아야 할 것 같습니다.
2. MutalblePropertySources
xxxxxxxxxx
public class MutablePropertySources implements PropertySources {
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
public PropertySource<?> get(String name) {
int index = this.propertySourceList.indexOf(PropertySource.named(name));
return (index != -1 ? this.propertySourceList.get(index) : null);
}
public void addFirst(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(0, propertySource);
}
public void addLast(PropertySource<?> propertySource) {
removeIfPresent(propertySource);
this.propertySourceList.add(propertySource);
}
public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
removeIfPresent(propertySource);
int index = assertPresentAndGetIndex(relativePropertySourceName);
addAtIndex(index, propertySource);
}
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {
assertLegalRelativeAddition(relativePropertySourceName, propertySource);
removeIfPresent(propertySource);
int index = assertPresentAndGetIndex(relativePropertySourceName);
addAtIndex(index + 1, propertySource);
}
}
아 PropertySources를 구현한 Class 이군요. add*()
관련 메소드들을 보면 PropertySource
를 인자로 받아 이것을 propertySourceList에 추가하는 것을 볼 수 있습니다.
2.1.2 Environment에 외부 파일을 이용해 Property 추가하기
조금 자세히 알아보았으니 이번에는 실습을 통해 어떤 식으로 추가가 되는지 직접 살펴보겠습니다.
우선 resources 하위에 app.properties라는 파일을 하나 생성한 뒤 =
을 기준으로 해서 key, value
쌍으로 데이터를 입력했습니다.
xxxxxxxxxx
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
public void run(ApplicationArguments args) throws Exception {
ConfigurableEnvironment env = (ConfigurableEnvironment)context.getEnvironment();
MutablePropertySources propertySources = env.getPropertySources();
propertySources.addLast(new ResourcePropertySource("classpath:app.properties"));
System.out.println(env.getProperty("app.name"));
System.out.println(env.getProperty("app.pw"));
}
}
== 결과 ==
"galid"
"1234"
우선 ApplicationContext를 생성한 뒤, ApplicationContext를 이용하여 ConfigurableEnvironment를 받아옵니다. ApplicationContext는 기본적으로 Environment를 return하므로 ConfigurableEnvironment 타입으로 캐스팅해주어야 합니다. 물론 ConfigurableApplicationContext
를 생성했다면 바로 ConfigurableEnvironment
를 return합니다.
그 후 앞서 살펴본 MutablePropertySources
Class의 add*()
메소드에 매개변수로 ResourcePropertySource
를 넘겨주어 Environment에 Property를 추가합니다. 이때 ResourcePropertySource
에 인자로 외부 File을 넘겨주어야 합니다.
2.2 Property를 추가하는 방법 (Annotation 사용)
@PropertyResource()
Annotation을 사용해 Property 추가하는 방법도 있습니다. 너무 너 ~ 무 간단합니다..
x
"classpath:app.properties") (
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(env.getProperty("app.name"));
System.out.println(env.getProperty("app.pw"));
}
}
== 결과 ==
"galid"
"1234"
@PropertySource
어노테이션의 괄호안에 외부파일을 입력해주면 바로 사용할 수 있습니다. 위의 결과로 Environment에 외부 파일안의 값들이 자동으로 Property 객체화되고 추가되는 과정까지 손쉽게 해결되었습니다. 그후 Environment
만을 얻어와 getProperty()
메소드를 이용해 필요한 값을 가져오면 됩니다.
2.3 @Value 어노테이션 사용하기
우선 application.properties에 값을 기입합니다.
그 후 값을 받아오기 위해 String 변수앞에 @Value("${key}") 어노테이션을 사용합니다. key 값은 위의 application.properties에서 기입한 값에서 `=`을 기준으로 앞에(app.name) 있는 값을 의미합니다.
쉽죠?
2.4 JVM 옵션을 이용한 방법
JVM의 옵션을 이용해 값을 Environment에 전달할 수도 있습니다. 바로 예제를 보겠습니다..
앞서 Profile을 Environment에 추가할 때도 사용했던 방식입니다. 간단히 -D이후 원하는 key,value 쌍으로 데이터를 입력해주시면 됩니다.
public class AppRunner implements ApplicationRunner {
ApplicationContext context;
public void run(ApplicationArguments args) throws Exception {
Environment env = context.getEnvironment();
System.out.println(env.getProperty("app.name"));
}
}
== 결과 ==
galid1234
마찬가지로 Environment를 얻어와 Configuration에 추가한 key를 이용해 데이터를 가져오면 됩니다.
'FrameWork > Spring' 카테고리의 다른 글
Spring - IoC 컨테이너의 기능 - 4 (ApplicationEventPublisher 란?) (4) | 2019.04.13 |
---|---|
Spring - IoC 컨테이너의 기능 - 3 (MessageSource 란?) (0) | 2019.04.13 |
Spring - IoC 컨테이너의 기능 - 1 (Bean의 Scope) (0) | 2019.04.11 |
Spring - @Autowired 분석! (2) | 2019.04.10 |
Spring - @ComponentScan 어노테이션이란? (2) | 2019.04.10 |