Chapter3. 고급 와이어링
3.1 환경과 프로파일
개발환경별로 다른 데이터베이스 구성
//Develop @Bean(destroyMethod = "shutdown") public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); }
//Production @Bean public DataSource dataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); }
//QA @Bean(destroyMethod = "shutdown") public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl("jdbc:h2:tcp://dbserver/~/test"); dataSource.setDriverClassName("org.h2.Driver"); dataSource.setUsername("sa"); dataSource.setPassword("password"); dataSource.setInitialSize(20); dataSource.setMaxActive(30); return dataSource; }
각 환경에 맞게 빈 설정파일(클래스 또는 XML)을 달리해 빌드 타임때 해결할 순 있지만, 각 환경별로 설정파일을 만들어야 하므로 어려운 점이 있다.
3.1.1 빈 프로파일 설정하기
- 스프링은 빌드 시 빈 구성을 결정하지 않고 런타임때까지 기다린다
- @Profile 어노테이션으로 사용하고, 해당 프로파일이 활성화되어 있는 경우에만 해당 빈을 사용할 수 있다.
프로파일이 지정되지 않은 모든 빈은 항상 활성화된다.
@Configuration public class DataSourceConfig { @Bean(destroyMethod = "shutdown") @Profile("dev") public DataSource embeddedDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript("classpath:schema.sql") .addScript("classpath:test-data.sql") .build(); } @Bean @Profile("prod") public DataSource jndiDataSource() { JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean(); jndiObjectFactoryBean.setJndiName("jdbc/myDS"); jndiObjectFactoryBean.setResourceRef(true); jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class); return (DataSource) jndiObjectFactoryBean.getObject(); } }
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <beans profile="dev"> <jdbc:embedded-database id="dataSource" type="H2"> <jdbc:script location="classpath:schema.sql" /> <jdbc:script location="classpath:test-data.sql" /> </jdbc:embedded-database> </beans> <beans profile="prod"> <jee:jndi-lookup id="dataSource" lazy-init="true" jndi-name="jdbc/myDatabase" resource-ref="true" proxy-interface="javax.sql.DataSource" /> </beans> </beans>
3.1.2 프로파일 활성화하기
프로파일 활성화 상태를 결정하는 두 가지 프로퍼티
- spring.profiles.active
- spring.profiles.default
- active 프로파일 설정이 우선함
- 동시에 여러 프로파일을 쉽표로 구분하여 사용가능
활성화 프로퍼티를 설정하기 위한 여러가지 방법
- DispatcherServlet에 초기화된 파라미터
- 웹 어플리케이션의 컨텍스트 파라미터
- JNDI 엔트리
- 환경 변수
- JVM 시스템 프로퍼티
- 통합 테스트 클래스에서 @ActiveProfiles 사용
@ActiveProfiles 어노테이션을 통합테스트코드 예제
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes=DataSourceConfig.class) @ActiveProfiles("dev") public static class DevDataSourceTest { @Autowired private DataSource dataSource; @Test public void shouldBeEmbeddedDatasource() { assertNotNull(dataSource); JdbcTemplate jdbc = new JdbcTemplate(dataSource); List<String> results = jdbc.query("select id, name from Things", new RowMapper<String>() { @Override public String mapRow(ResultSet rs, int rowNum) throws SQLException { return rs.getLong("id") + ":" + rs.getString("name"); } }); assertEquals(1, results.size()); assertEquals("1:A", results.get(0)); } } }
3.2 조건부 빈
스프링 4.0에서는 @Bean을 적용할 수 있는 새로운 @Conditional 어노테이션이 소개되었다.
@Bean @Conditional(MagicExistsCondition.class) public MagicBean magicBean() { return new MagicBean(); }
public class MagicExistsCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment env = context.getEnvironment(); return env.containsProperty("magic"); } }
- ConditionalContext 수행절차
- getRegistry()반환을 통한 BeanDefinitionRegistry로 빈 정의 확인
- getBeanFactory()에서 반환되는 ConfigurableListableBeanFactory를 통해 빈 프로퍼티 발굴
- getEnvironment()로부터 얻은 Environment를 통해 환경 변수 값 확인
- getResourceLoader()에서 반환된 ResourceLoader를 통해 로드된 자원 내용을 읽고 검사
- getClassLoadeer()에서 반환된 ClassLoader()를 통해 클래스의 존재를 로드하고 확인
- AnnotatedTypeMetadata는 @Bean 메서드의 어노테이션을 검사할 수 있는 기회를 제공한다.
@Profile 어노테이션은 @Conditional 및 Condition 인터페이스에 기초하여 리팩토링된다.
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); }
class ProfileRegexCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); if (environment != null) { String[] profiles = environment.getActiveProfiles(); MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ProfileRegex.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { for (String profile : profiles) { boolean matches = Pattern.matches((String) value, profile); if (matches) { return true; } } } return false; } } return true; } }
3.3 오토와이어링의 모호성
- 오토와이어링은 응용 프로그램 구성 요소를 조립하는 데 필요한 명시적 설정의 양을 감소시킨다.
- 정확히 하나의 빈이 원하는 결과와 일치할 때 오토와이어링은 동작한다.
@Autowire
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
@Component
public class Cake implements Dessert {...}
@Component
public class Cookies implements Dessert {...}
@Component
public class IceCream implements Dessert {...}
- 스프링은 단일 후로보 선택을 좁히기 위해 한정자를 사용한다.
3.3.1 기본 빈 지정
- 기본 빈 지정 : @Primary 어노테이션
@Component
@Primary
public class IceCream implements Dessert {...}
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}
3.3.2 오토와이어링의 자격
- @Primary 한계점 : 하나의 명백한 옵션 선택을 하지 못한다
- 스프링의 수식은 결국 소정의 자격을 충족하는 모든 후보 빈에 적용되고, 단일 빈 대상으로 협소화 작업을 적용한다.
@Autowire
@Qualfier("iceCream") // "iceCream"은 주입할 빈의 ID
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
- 클래스 이름을 변경하면 수식자는 잘못된 것으로 랜더링될 수 있다.
- 빈에 자신의 수식자를 지정할 수 있다.
@Component
@Qualfier("cold")
public class IceCream implements Dessert {...}
@Autowire
@Qualfier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
@Bean
@Qualfier("cold")
public Dessert iceCream() {
- 하지만 여전히 한정자가 "cold"인 디저트가 있다면 오토와이어링의 모호함에 직면한다
- 해결책은 양쪽의 주입지점과 빈 정의에 다른 @Qualifier를 고정시키는 방법을 사용하는 것이다.
@Component
@Qualfier("cold")
@Qualfier("creamy")
public class IceCream implements Dessert {...}
@Component
@Qualfier("cold")
@Qualfier("fruity")
public class Popsicle implements Dessert {...}
@Autowire
@Qualfier("cold")
@Qualfier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
- 하지만, 자바는 동일한 유형의 여러 어노테이션이 같은 항목에 반복 될 수 없다.
- 그래서 사용자 지정 수식자 어노테이션을 사용한다.
- 맞춤형 어노테이션을 사용하는 것은 @Qualifier 어노테이션을 사용하는 것과 문자열로 수식을 지정하는 것보다 더 type-safe하다
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD
, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qaulifier
public @interface Cold {}
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD
, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qaulifier
public @interface Creamy {}
@Component
@Cold
@Creamy
public class IceCream implements Dessert {...}
@Component
@Cold
@Fruity
public class Popsicle implements Dessert {...}
@Autowire
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
3.4 빈 범위
기본적으로 스프링의 생성되는 빈은 싱글톤이다.
- 싱글톤(Singleton) : 전체 애플리케이션을 위해 생성되는 빈의 인스턴스 하나
- 프로토타입(Prototype) : 빈이 주입될 때마다 생성되거나 스프링 애플리케이션 컨텍스트에서 얻는 빈의 인스턴스 하나
- 세션(Session) : 웹 애플리케이션에서 각 세션용으로 생성되는 빈의 인스턴스 하나
- 요청(Requet) : 웹 애플리케이션에서 각 요청요으로 생성되는 빈의 인스턴스 하나
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}
또는 @Scope("prototype")
을 사용할 수 있지만 TypeSafe 하지 않으므로 권장하지 않는다.
자바빈 설정은 다음처럼
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
}
xml 은 다음처럼
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
3.4.1 요청과 세션 범위 작업하기
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION, proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart() {
}
세션 내 에서 싱글튼임 범위 프록시는 요청과세선 범위 빈 주입을 연기한다.
3.4.2 xml 로 범위 프록시 선언하기
<bean id="cart" class="com.mysqpp.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>
기본적으로 타킷 클래스 프록시를 생성하기 위해 cglib 을 사용한다. 아래와 같이 하면 인터페이스 기반의 프락시를 사용한다.
<bean id="cart" class="com.mysqpp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false" />
</bean>
3.4.3 범위빈을 런타임시에 생성하기
스프링에 어노테이션은 범위빈 사용을 빈주입시에만 가능한다. 만약 런타임시마다 빈을 받고 싶다면 JavaEE6 의 @Inject 와 javax.inject.Provider
가 필요하다.
@Autowired
private Client client; // 빈 인젝션에서만 사용됨
@Inject
private Provider<Client> clientProvider; // 팩토리를 주입
public void useClient() {
clientProivder.get().doSomething();
}
3.5 런타임 값 주입
- 프로퍼티 플레이스 홀더(Property placeholders)
- 스프링 표현 언어(SpEL, Spring Expression Language)
3.5.1 외부 값 주입
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BlankDisk disk() {
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));
}
}
app.properties
disc.title=Sgt. Pepers..
disc.arties=The Beatles
getProperty 오버로딩
String getProperty(String key)
String getProperty(String key, String defaultValue)
T getProperty(String key, Class<T> type)
T getProperty(String key, class<T> type, T defaultValue)
int connectionCount = env.getProperty("db.connection.count", Integer.clsss, 30);
프로퍼티 조회시에 값이 없을 경우 null 을 리턴한다. 강제하고 싶다면 getReuiqredProeprty()
를 사용한다.
프로퍼티가 정의되지 않으면 IllegalStateException 이 발생한다. 프로퍼티 존재를 확인해야할 경우 containsProperty() 를 호출한다.
Class<CompactDisc> cdClass = env.getPropertyasClass("dis.class", CompactDisc.class);
Environment 는 프로파일이 활성화되어있는지를 확인하는 몇가지 메소드를 제공한다.
- String[] getAcitveProfiles() : 활성화된 프로파일 명 배열을 리턴
- String[] getDefaultProfiles() : 기본 프로파일 명 배열을 반환
- boolean acceptProfiles(String... profiles) : 환경에 주어진 프로파일을 지원하면 true 리턴
프로퍼티 플레이스홀더 처리하기
프로퍼티 와이어링(?)
<bean id="sgtPapers" class="soundsystem.BalnkDisc" c:_title="${disk.title}" c:_artist="${disc.artist}" />
public BalnkDisc(@Value("${disk.title}") String title, @Value("${disc.artist}")String artist) {
}
설정하기
PropertyPlaceholderConfiguraer
빈 또는 PropertySourcePlaceholderConfigurer
빈을 설정한다.
@Bean
public static PropertysourcesPlaceholderconfigurer placeholderConfigurer() {
return new PropertySourcesPlaceHolderConfigurer();
}
<context:property-placeholder />
3.5.2 스프링 표현식 와이어링
스프링3부터 SpEL 을 이용해 런타임시 주입하기 위한 값을 계산할 수 있다.
- ID 로 빈을 참조하는 기능
- 메소드 호출과 객체의 프로퍼티 액세스
- 값에서의 수학적인 동작, 관계와 관련된 동작, 논리연산 동작
- 정규 표현식 매칭
- 컬렉션 처리
SpEL 은 종속객체 주입보다는 다은 용도로 사용된아. 예를 들면 스프링 시큐리티는 SpEL 표현식을 포함하는 보안을 정의하는데 도움된다. 스프링 MVC 애플리케이션의 뷰처럼 Tnymeleaf 템플리승ㄹ 사용한다면 이 템플릿은 모델 데이터를 참조하기 위해 SpEL 표현식을 사용한다.
SpEL 의 표현식 : #{ ... }
형식, 프로퍼티 플레이스 홀더 : ${ ... }
#{1}
: 상수 1#{T(System).currentTimeMillis()}
: T 연산자는 타입을 평가하고, 메서드를 수행{sgtPeppers.artist}
: 빈 참조{systemProperties['disc.title']}
: systemProperties 객체를 통해 시스템 프로퍼티 참조
빈 와이어링
public BalnkDisc(
@Value("#{systemProperties['disk.title]}")String title,
@Value("#{systemProperties['disk.artist]}")String artist
}
<bean id="sgtPapers" class="soundsystem.BalnkDisc" c:_title="#{systemProperties['disk.title']}" c:_artist="#{systemProperties['disc.artist']}" />
#{3.14159}
#{9.87E4}
#{'Hello'}
#{false}
#{artistSelector.selectArtist()}
#{artistSelector.selectArtist().toUpperCase()}
#{artistSelector.selectArtist()?.toUpperCase()}
: NullPointerException 방지#{2 * T(java.lnag.Math).PI * circle.radius}
#{disc.title + ' by ' + dist.artist}
#{counter.total == 100}
#{scoreBoard.score > 1000 ? "winnder!" : "Looser"}
#{disk.title ?: 'i am null'}
#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'}
#{jukebox.songs[4].title}
#{'this is a test'[3]}
: 문자열에서 한글자 추출#{jukebox.songs.?[artist eq 'Aerosmith']}
: 컬렉션에서 특정 프로퍼티가 일치하는 요소 찾기#{jukebox.songs.^[artist eq 'Aerosmith']}
#{jukebox.songs.$[artist eq 'Aerosmith']}
#{jukebox.songs.![title]}
: 요소의 프로퍼티로 새로운 컬렉션 생성#{jukebox.songs.?[artist eq 'Aerosmith'].![title]}
Operator
연산 유형 | 연산자 | |
---|---|---|
산술 | +, -, *, /, %, ^ | |
비교 | <, lt, >, gt, ==, eq, <=, le, >=, ge | |
논리 | and, or, not, | |
조건 | ?: (ternary), ?:(elvis) | |
정규표현식 | matches |