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

results matching ""

    No results matching ""