Spring Framework/Spring IoC

Environment Abstraction

헬로우월드 2024. 11. 14. 20:27

Environment 인터페이스는 애플리케이션 환경의 두 가지 주요 측면인 profile과 properties을 모델링하는 컨테이너에 통합된 추상화입니다.

profile은 지정된 profile이 활성 상태일 때만 컨테이너에 등록할 논리적 빈 정의 그룹입니다. 빈은 XML이나 애노테이션으로 정의되든 profile에 할당될 수 있습니다. Environment 객체의 profile과 관련된 역할은 현재 활성화된 profile이 무엇인지, 디폴트로 활성화되어야 할 profile이 무엇인지를 결정하는 것입니다.

properties은 거의 모든 애플리케이션에서 중요한 역할을 하며, properties 파일, JVM 시스템 속성, 시스템 환경 변수, JNDI, 서블릿 컨텍스트 파라미터, 임시 Properties 객체, Map 객체 등 다양한 출처에서 올 수 있습니다. properties과 관련된 Environment 객체의 역할은 properties 소스를 구성하고, 해당 소스에서 properties을 해결하기 위한 편리한 서비스 인터페이스를 사용자에게 제공하는 것입니다.

 

Bean Definition Profiles

빈 정의 프로필은 코어 컨테이너에서 서로 다른 환경에서 다른 빈을 등록할 수 있는 메커니즘을 제공합니다. "Environment"이라는 단어는 사용자마다 다른 의미로 해석될 수 있으며, 이 기능은 다음과 같은 다양한 사용 사례에 도움이 될 수 있습니다:

  • 개발 환경에서는 인메모리 데이터 소스를 사용하고, QA나 프로덕션 환경에서는 동일한 데이터 소스를 JNDI에서 조회하는 방식.
  • 애플리케이션을 Performance 환경에 배포할 때만 모니터링 인프라를 등록하는 것.
  • 고객 A 배포를 위한 빈의 맞춤 구현과 고객 B 배포를 위한 빈의 맞춤 구현을 등록하는 것.

DataSource를 필요로 하는 실제 애플리케이션에서 첫 번째 사용 사례를 고려해 보십시오. 테스트 환경에서의 설정은 다음과 같을 수 있습니다.

@Bean
public DataSource dataSource() {
	return new EmbeddedDatabaseBuilder()
		.setType(EmbeddedDatabaseType.HSQL)
		.addScript("my-schema.sql")
		.addScript("my-test-data.sql")
		.build();
}

 

이제 애플리케이션의 데이터 소스가 프로덕션 애플리케이션 서버의 JNDI 디렉터리에 등록되어 있다고 가정하고, 이 애플리케이션을 QA 또는 프로덕션 환경에 배포하는 방법을 고려해 보십시오. 우리의 dataSource 빈은 이제 다음과 같이 보입니다.

@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
	Context ctx = new InitialContext();
	return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

 

문제는 현재 환경에 따라 이 두 가지 변형을 전환하는 방법입니다. 그동안 Spring 사용자들은 시스템 환경 변수와 XML <import/> 문을 조합하고 ${placeholder} 토큰을 사용하여 환경 변수 값에 따라 올바른 설정 파일 경로로 해석되도록 하는 여러 방법을 고안해 왔습니다. 빈 정의 프로필은 이 문제에 대한 해결책을 제공하는 핵심 컨테이너 기능입니다.

앞서 예제에서 환경별 빈 정의의 사용 사례를 일반화하면, 특정 컨텍스트에서는 특정 빈 정의를 등록해야 하지만 다른 컨텍스트에서는 등록하지 않아야 할 필요성이 생깁니다. 상황 A에서는 특정 프로필의 빈 정의를 등록하고, 상황 B에서는 다른 프로필을 등록하고자 할 수 있습니다. 이러한 필요를 반영하기 위해 구성을 업데이트하는 것부터 시작합니다.

 

Using @Profile

@Profile 애노테이션을 사용하면 하나 이상의 지정된 프로파일이 활성화될 때 해당 컴포넌트가 등록 대상임을 나타낼 수 있습니다. 앞서 사용한 예제를 사용하여 dataSource 구성을 다음과 같이 다시 작성할 수 있습니다.

@Configuration
@Profile("development")
public class StandaloneDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}
}

 

@Configuration
@Profile("production")
public class JndiDataConfig {

	@Bean(destroyMethod = "")   (1)
	public DataSource dataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

 

(1) @Bean(destroyMethod = "")는 디폴트 destroy 메서드 추론을 비활성화합니다.

앞서 언급한 바와 같이, @Bean 메서드를 사용할 때는 일반적으로 Spring의 JndiTemplate/JndiLocatorDelegate 헬퍼나 이전에 보여준 JNDI InitialContext 사용법을 통해 프로그래밍 방식의 JNDI 조회를 선택하지만, JndiObjectFactoryBean 변형은 사용하지 않습니다. 이는 리턴 타입을 FactoryBean 타입으로 선언하도록 강제하기 때문입니다.

 

profile 스트링은 단순한 프로파일 이름(예: production)이나 프로파일 표현식을 포함할 수 있습니다. 프로파일 표현식은 더 복잡한 프로파일 로직을 표현할 수 있게 해줍니다(예: production & us-east). 프로파일 표현식에서는 다음과 같은 연산자가 지원됩니다.

  • ! : logical NOT of the profiles
  • & : logical AND of the profiles
  • | : logical OR of the profiles 
괄호를 사용하지 않고는 &와 | 연산자를 혼합할 수 없습니다. 예를 들어, production & us-east | eu-central은 유효한 표현이 아닙니다. 이는 production & (us-east | eu-central)로 표현해야 합니다.

 

@Profile을 메타 애노테이션으로 사용하여 사용자 정의 조합 애노테이션을 만들 수 있습니다. 다음 예제는 @Profile("production")을 대신하여 사용할 수 있는 사용자 정의 @Production 애노테이션을 정의합니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

 

@Configuration 클래스에 @Profile이 지정된 경우, 하나 이상의 지정된 프로파일이 활성화되지 않으면 해당 클래스와 관련된 모든 @Bean 메서드와 @Import 애노테이션이 무시됩니다. @Component 또는 @Configuration 클래스가 @Profile({"p1", "p2"})로 표시된 경우, 프로파일 p1 또는 p2가 활성화되지 않으면 해당 클래스는 등록되거나 처리되지 않습니다. 지정된 프로파일이 NOT 연산자(!)로 접두사가 붙으면, 해당 프로파일이 활성화되지 않았을 때만 애노테이션이 적용된 요소가 등록됩니다. 예를 들어, @Profile({"p1", "!p2"})가 주어지면 프로파일 p1이 활성화되었거나 프로파일 p2가 활성화되지 않은 경우에만 등록이 이루어집니다.
spring:
  profiles:
    active: p1  # 'p1' 프로파일 활성화

 

@Profile은 특정 빈의 대체 버전을 위해 구성 클래스의 특정 빈만 포함하도록 메서드 레벨에서도 선언할 수 있습니다. 다음 예제가 이를 보여줍니다.

@Configuration
public class AppConfig {

	@Bean("dataSource")
	@Profile("development")  (1)
	public DataSource standaloneDataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.addScript("classpath:com/bank/config/sql/test-data.sql")
			.build();
	}

	@Bean("dataSource")
	@Profile("production")  (2)
	public DataSource jndiDataSource() throws Exception {
		Context ctx = new InitialContext();
		return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
	}
}

(1) standaloneDataSource 메서드는 development 프로파일에서만 이용 가능합니다.

(2) jndiDataSource 메서드는 production 프로파일에서만 이용 가능합니다.

 

@Profile을 @Bean 메서드에 사용할 때는 특별한 시나리오가 적용될 수 있습니다. 동일한 Java 메서드 이름을 가진 오버로딩된 @Bean 메서드(생성자 오버로딩과 유사)의 경우, 모든 오버로딩된 메서드에 대해 일관되게 @Profile 조건을 선언해야 합니다. 조건이 일관되지 않으면, 오버로딩된 메서드 중 첫 번째 선언에 있는 조건만 유효합니다. 따라서, @Profile을 사용하여 특정 아규먼트 시그니처를 가진 오버로딩 메서드를 선택할 수 없습니다. 동일한 빈에 대한 모든 팩토리 메서드는 생성 시 Spring의 생성자 해석 알고리즘을 따릅니다.
다른 프로파일 조건을 가진 대체 빈을 정의하려면, 이전 예제에서처럼 @Bean의 name 속성을 사용하여 동일한 빈 이름을 가리키는 별도의 Java 메서드 이름을 사용하십시오. 아규먼트 시그니처가 모두 동일한 경우(예: 모든 버전이 아규먼트가 없는 팩토리 메서드를 가질 때), 이는 유효한 Java 클래스에서 이러한 구성을 나타내는 유일한 방법입니다(특정 이름과 아규먼트 시그니처를 가진 메서드는 하나만 존재할 수 있기 때문입니다).

 

Activating a Profile

이제 구성을 업데이트했으므로, Spring에 어떤 프로파일이 활성화되었는지 지시해야 합니다. 

프로파일을 활성화하는 방법은 여러 가지가 있지만, 가장 간단한 방법은 ApplicationContext를 통해 사용할 수 있는 Environment API를 통해 프로그래밍 방식으로 활성화하는 것입니다. 다음 예제는 그 방법을 보여줍니다.

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

 

추가적으로, spring.profiles.active 속성을 통해 선언적으로 프로파일을 활성화할 수 있습니다. 이 속성은 시스템 환경 변수, JVM 시스템 속성, web.xml의 서블릿 컨텍스트 파라미터, 또는 JNDI 항목으로 지정할 수 있습니다(자세한 내용은 PropertySource Abstraction 참조). 통합 테스트에서는 spring-test 모듈의 @ActiveProfiles 애노테이션을 사용하여 활성화된 프로파일을 선언할 수 있습니다( context configuration with environment profiles 참조).

프로파일은 "둘 중 하나"로만 선택하는 방식이 아니라는 점에 유의하세요. 여러 프로파일을 동시에 활성화할 수 있습니다. 프로그래밍 방식으로는 setActiveProfiles() 메서드에 여러 프로파일 이름을 전달하여 활성화할 수 있으며, 이 메서드는 String... 가변 아규먼트를 받습니다. 다음 예제는 여러 프로파일을 활성화하는 방법을 보여줍니다.

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

 

선언할 때 spring.profiles.active에 쉼표로 구분된 프로파일 이름 리스트를 전달할 수 있습니다. 다음 예제는 그 방법을 보여줍니다.

-Dspring.profiles.active="profile1,profile2"

 

Default Profile

디폴트 프로파일은 활성화된 프로파일이 없을 때 활성화되는 프로파일을 나타냅니다. 다음 예제를 참고하세요.

@Configuration
@Profile("default")
public class DefaultDataConfig {

	@Bean
	public DataSource dataSource() {
		return new EmbeddedDatabaseBuilder()
			.setType(EmbeddedDatabaseType.HSQL)
			.addScript("classpath:com/bank/config/sql/schema.sql")
			.build();
	}
}

 

활성화된 프로파일이 없으면 dataSource가 생성됩니다. 이를 통해 하나 이상의 빈에 대한 디폴트 정의를 제공하는 방법으로 볼 수 있습니다. 어떤 프로파일이든 활성화되면 디폴트 프로파일은 적용되지 않습니다.

디폴트 프로파일의 이름은 default입니다. 디폴트 프로파일의 이름은 Environment에서 setDefaultProfiles() 메서드를 사용하거나 선언적으로 spring.profiles.default 속성을 사용하여 변경할 수 있습니다.

 

PropertySource Abstraction

Spring의 Environment 추상화는 구성 가능한 property source 계층에 대한 검색 작업을 제공합니다. 다음 리스트를 참고하세요.

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

 

앞의 코드 조각에서, 현재 환경에 대해 my-property 속성이 정의되어 있는지 Spring에 질문하는 높은 레벨의 방법을 볼 수 있습니다. 이 질문에 답하기 위해 Environment 객체는 여러 PropertySource 객체들에 대한 검색을 수행합니다. PropertySource는 key-value pair의 소스를 추상화한 간단한 구조이며, Spring의 StandardEnvironment`는 두 개의 PropertySource 객체로 구성됩니다. 하나는 JVM 시스템 프로퍼티 집합(System.getProperties())을 나타내고, 다른 하나는 시스템 환경 변수 집합(System.getenv())을 나타냅니다.

이러한 디폴트 프로퍼티 소스들은 독립형 애플리케이션에서 사용하기 위해 StandardEnvironment에 존재합니다. StandardServletEnvironment는 서블릿 구성, 서블릿 컨텍스트 파라미터, 그리고 JNDI가 사용 가능한 경우 JndiPropertySource를 포함하여 추가적인 기본 프로퍼티 소스들로 채워집니다.

 

구체적으로, StandardEnvironment를 사용할 때 env.containsProperty("my-property") 호출은 실행 시 my-property 시스템 프로퍼티 또는 my-property 환경 변수가 존재하면 true를 반환합니다.

이러한 검색은 계층적으로 수행됩니다. 디폴트로 시스템 프로퍼티가 환경 변수보다 우선합니다. 따라서 env.getProperty("my-property") 호출 시 my-property 프로퍼티가 두 위치에 모두 설정된 경우, 시스템 프로퍼티 값이 "우선"하여 리턴됩니다. 프로퍼티 값은 병합되지 않으며, 이전 항목에 의해 완전히 덮어씌워진다는 점에 유의하세요.
일반적인 StandardServletEnvironment에서 전체 계층 구조는 다음과 같으며, 가장 높은 우선순위 항목이 상단에 위치합니다.
1. ServletConfig parameters (if applicable — for example, in case of a DispatcherServlet context)
2. ServletContext parameters (web.xml context-param entries)
3. JNDI environment variables (java:comp/env/ entries)
4. JVM system properties (-D command-line arguments)
5. JVM system environment (operating system environment variables)

 

가장 중요한 점은 전체 메커니즘이 구성 가능하다는 것입니다. 커스텀 프로퍼티 소스를 이 검색에 통합하고 싶은 경우, 직접 PropertySource를 구현하고 인스턴스화하여 현재 Environment의 PropertySources 집합에 추가하면 됩니다. 다음 예제는 그 방법을 보여줍니다.

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

 

앞의 코드에서 MyPropertySource는 검색에서 가장 높은 우선순위로 추가되었습니다. 만약 MyPropertySource에 my-property 프로퍼티가 포함되어 있으면, 다른 PropertySource에 있는 my-property보다 우선하여 감지되고 반환됩니다. MutablePropertySources API는 프로퍼티 소스 세트를 정밀하게 조작할 수 있는 여러 메서드를 제공합니다.

 

Using @PropertySource

@PropertySource 애노테이션은 Spring의 Environment에 PropertySource를 추가하는 편리하고 선언적인 메커니즘을 제공합니다.

app.properties 파일에 testbean.name=myTestBean이라는 키-값 쌍이 포함되어 있다고 가정할 때, 다음 @Configuration 클래스는 @PropertySource를 사용하여 testBean.getName() 호출 시 myTestBean을 반환하도록 설정합니다.

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

 @Autowired
 Environment env;

 @Bean
 public TestBean testBean() {
  TestBean testBean = new TestBean();
  testBean.setName(env.getProperty("testbean.name"));
  return testBean;
 }
}

 

@PropertySource 리소스 위치에 있는 ${…​} 플레이스홀더들은 이미 환경에 등록된 프로퍼티 소스 세트들을 기준으로 해석됩니다. 다음 예제가 이를 보여줍니다.

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

 @Autowired
 Environment env;

 @Bean
 public TestBean testBean() {
  TestBean testBean = new TestBean();
  testBean.setName(env.getProperty("testbean.name"));
  return testBean;
 }
}

 

my.placeholder가 이미 등록된 프로퍼티 소스(예: 시스템 프로퍼티 또는 환경 변수) 중 하나에 존재한다고 가정하면, 해당 플레이스홀더는 해당 값으로 해석됩니다. 그렇지 않으면 default/path가 기본값으로 사용됩니다. 디폴트 값이 지정되지 않았고 프로퍼티를 해석할 수 없는 경우 IllegalArgumentException이 발생합니다.

@PropertySource는 반복 가능한 애노테이션으로 사용할 수 있습니다.
@PropertySource는 메타 애노테이션으로도 사용되어 속성 재정의를 통해 사용자 정의 조합 애노테이션을 만들 수 있습니다.

 

Placeholder Resolution in Statements

과거에는 엘리먼트들의 플레이스홀더 값이 JVM 시스템 프로퍼티나 환경 변수에 대해서만 해석될 수 있었습니다. 이제는 그렇지 않습니다. Environment 추상화가 컨테이너 전체에 통합되어 있기 때문에, 플레이스홀더 해석을 쉽게 Environment를 통해 라우팅할 수 있습니다. 즉, 해석 프로세스를 원하는 방식으로 구성할 수 있습니다. 시스템 프로퍼티와 환경 변수를 통한 검색 우선순위를 변경하거나 이를 완전히 제거할 수 있으며, 적절하게 사용자 정의 프로퍼티 소스를 추가할 수도 있습니다.

구체적으로, 다음은 customer 프로퍼티가 Environment에만 존재한다면 어디에 정의되어 있든지 작동합니다.

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.env.Environment;

@Configuration
public class AppConfig {

    @Value("${customer}")
    private String customer;

    @Bean
    public ConfigClass config(Environment environment) {
        String resourcePath = "com/bank/service/" + customer + "-config.xml";
        return new ConfigClass(resourcePath);
    }
}