본문 바로가기
Framework & Library/Spring & Egov

7.6.4 프로파일/ 7.6.5 프로퍼티 소스/ 7.6.6 빈 설정의 재사용과 @Enable

by 뒹굴거리는프로도 2018. 7. 20.
반응형

토비의 스프링 3.1

 

 

 

7.6 스프링 3.1의 DI 

 

 

7.6.4 프로파일

 

: 운영환경에서는 반드시 필요하지만 테스트 실행 중에는 배제되어야 하는 빈 설정을 별도의 설정 클래스를 만들어 따로 관리할 필요가 있다. MailSender 타입의 빈이 그렇다.

 운영환경에서 TestAppContext 없이 AppContext만 DI 설정정보로 지정하면, AppContext에는 메일서비스를 제공하는 빈이 존재하지 않아, 이 빈에 의존하는 userSerivce 빈에서 에러가 발생할 것이다.  그렇다고 AppContext 에 MailSender 타입 빈을 넣으면, 충돌로 인하여 테스트에 문제가 생긴다.  

1
@ContextConfiguration(classes={TestAppContext.class, AppContext.class})
cs

이 순서로 설정 클래스를 배열했기 때문에, AppContext에 추가한 운영용 메일 서비스 빈이 테스트에 사용되어 문제가 발생한다. 해결하기 위해 운영환경에서는 반드시 필요하지만 테스트 실행 중에는 배제돼야 하는 빈 설정을 별도의 설정 클래스를 만들어 따로 관리해야 한다. AppContext에 추가했던 mailSender 빈 설정을 ProductionAppContext라는 이름의 새로운 클래스로 옮겨보자.

 

운영환경에서만 필요한 빈을 담은 빈 설정 클래스

1
2
3
4
5
6
7
8
9
@Configuration
public class ProductionAppContext{
    @Bean 
    public MailSender mailSender(){
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost("localhost");
        return mailSender;
    }
}
cs

AppContext에 @Import로 가져오기 어렵다.  AppContet가 테스트에도 쓰이기 때문이다. 테스트 환경에서는 @ContextConfiguration에 AppContext와 TestAppContext를 지정하여 두 개 클래스의 빈 설정이 조합되어 동작하게 만든다. 운영환경에서는 AppContext와 ProductionContext가 DI 정보로 사용되게 설정하면 된다.

 

 

- @Profile과 @ActiveProfiles

 

: 스프링 3.1은 빈 설정정보를 위해 파일을 여러개로 나누거나 조합하는 방법보다 쉬운 방법을 제공한다. 실행 환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행 시점에 어떤 프로파일의 빈 설정을 사용할지 지정하는 것이다. 프로파일은 간단한 이름과 빈 설정으로 구성된다. 이것을적용하면 하나의 설정 클래스만 가지고 환경에 따라 다른 빈 설정 조합을 만들어낼 수 있다.

 프로파일은 설정 클래스 단위로 지정한다. 스프링은 프로파일이 지정되어 있지 않은 빈 설정은 디폴트 빈 설정정보로 취급되어 항상 적용된다.

 

1
2
3
@Configuration
@Profile("test")
public class TestAppContext{
cs

이제 TestAppContext는 test 프로파일의 빈 설정정보를 담은 클래스가 되었다. 

 

@Profile이 붙은 클래스는 @Import로 가져오든 @ContextConfiguration에 직접 명시하는것과 관계 없이, 현재 컨테이너의 활성(activce) 프로파일 목록에 자신의 프로파일 이름이 들어있지 않으면 무시된다. 활성 프로파일을 지정하려면 @ActiveProfiles 애노테이션을 사용한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@Import에 모든 설정 클래스 추가
@Configuration 
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class})
public class AppContext{
    ...
}
 
 
//활성 프로파일을 지정한 UserServiceTest
@RunWith(SpringUnit4ClassRunner.class)
@ActiveProfiles("test")
@ContextConfiguration(classes=AppContext.class)
//AppContext가 모든 설정 클래스를 @import 하고 있으니, @ContextConfiguration 수정
public class UserServiceTest{
    ...
}
 
...
 
 
cs

애플리케이션이 정식으로 동작하는 환경이라면 설정 클래스를 AppContext로 하고, 활성 프로파일을 production으로 지정해주면 된다.

 

 

 

- 컨테이너의 빈 등록 정보 확인

: 컨테이너가 생성한 빈에 어떤 것들인지 확인해 볼 수 있다. 스프링 컨테이너는 모두 BeanFactory라는 인터페이스를 구현하고 있다. 이것의 구현 클래스 중에 DefaultListableBeanFactory가 있는데, 대부분 스프링 컨테이너는 이 클래스를 이용해 빈을 등록, 관리한다. @Autowired로 이 오브젝트를 주입받아서 이용할 수 있으며, getBeanDefinitionNames() 메소드로 컨테이너에 등록된 모든 빈 이름과, 실제 빈, 빈 클래스 정보도 조회해 볼 수 있다. 

1
2
3
4
5
6
7
8
@Autowired DefaultListableBeanFactory bf;
 
@Test
public void beans(){
    for(String n : bf.getBeanDefinitionNames()){
        System.out.println(n + "\t" + bf.getBean(n).getClass().getName());
    }
}
cs

 

 

-중첩 클래스를 이용한 프로파일 적용

: 여기서는 전체 구성을 한 눈에 볼 수 있도록 프로파일에 따라 분리했던 설정정보를 하나의 파일로 모아본다. 프로파일이 지정된 독립된 설정 클래스의 구조는 그대로 유지한 채, 단지 static 중첩 클래스를 이용하여 소스코드의 위치를 통합한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class,
            AppContext.TestAppContext.class, AppContext.ProductionAppContext.class})
 
public class AppContext{
    ...
//이렇게 중첩 멤버 클래스로 프로파일 설정 클래스를 포함시키면, @Import에서 생략가능
    @Configuration
    @Profile("production")
    public static class ProductionAppContext{
        ...
    }
 
    @Configuration
    @Profile("test")
    public static class TestAppContext{
        ...
    }
}
cs

 두 개의 클래스에 static만 붙여서, AppContext 내부로 가져온 뒤, @Import에 지정했던 클래스 이름을 바꿔주면 된다. 이렇게 한 후 @Import에 지정했던 두 개의 프로파일 설정 클래스를 아예 제거해도 된다.

1
@Import(SqlServiceContext.class)
cs

 

 

 

 

 

7.6.5 프로퍼티 소스

: AppContext의 dataSource, DB 연결 정보는 테스트 환경에 종속된다. 스프링 3.1의 새로운 기능을 사용하여 환경에 따라 DB 연결 정보를 다르게 설정해보자. 

 

 

- @PropertySource

: 자바의 프로퍼티 파일 포맷을 사용하여, 텍스트로 된 이름과 값의 쌍으로 DB 연결정보를 구성한다. 스프링 3.1은 빈 설정 작업에 필요한 프로퍼티 정보를 컨테이너가 관리하고 제공해준다. 스프링 컨테이너가 지정된 정보 소스(프로퍼티 소스)로부터 프로퍼티 값을 빈 설정 작업에 사용할 수 있게 해준다. DB 연결정보는 database.properties을 프로퍼티 소스로 등록해준다. 등록에는 @PropertySource 애노테이션을 이용한다.

1
2
3
4
5
6
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import(SqlServiceContext.class)
@PropertySource("/database.properties"//프로퍼티 
public class AppContext{
cs

 등록한 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장된다. 이것은 @Autowired 를 통해 필드로 주입받을 수 있다. 주입 받은 후 Environment 오브젝트의 getProperty() 메소드를 이용하여 프로퍼티 값을 가져올 수 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Autowired Environment env;
 
@Bean 
pubilc DataSource dataSource(){
    SimpleDriverDataSource ds = new SimpleDriverDataSource();
 
    try{
        ds.setDriverClass((class<?
            extends java.sql.Driver>)Class.forName(env.getProperty("db.driverClass")));
    //db 연결 드라이버의 클래스인 Class 타입의 오브젝트를 넘겨야 한다.
    //따라서 Class.forName() 메소드의 도움으로 클래스 이름을 Class 타입으로 변환한 뒤 사용한다.
    }
    catch(ClassNotFoundException e){
        throw new RuntimeException(e);
    }
    ds.setUrl(env.getProperty("db.url"));
    ds.setUsername(env.getProperty("db.username"));
    ds.setPassword(env.getProperty("db.password"));
 
    return ds;
}
cs

 

 

- PropertySourcePlaceholderConfigurer

: Environment 오브젝트 대신 프로퍼티 값을 직접 DI 받는 방법도 가능하다. DataSource 빈의 프로퍼티는 빈 오브젝트가 아니므로 @Autowired를 사용할 수 없다. 하지만 @Value 애노테이션을 이용하면 된다. 이름 그대로 값을 주입받을 때 사용하는 것이다. 여기서는 프로퍼티 소스로부터 값을 주입받을 수 있게 치환자(placeholder)를 이용한다. @Value에는 프로퍼티 이름을 ${}안에 넣은 문자열을 디폴트 엘리먼트 값으로 지정해준다.

1
2
3
4
5
6
7
@PropertySource("/database.properties")
public class AppContext{
    @Value("${db.driverClass}") Class<extends Driver> driverClass;
    @Value("${db.url}"String url;
    @Value("${db.username}"String username;
    @Value("${db.password}"String password;
}
cs

 @Value의 디폴트 값으로 넣은 ${db.driverClass}는 XML에서 <property>의 value에 사용하는 값 치환 방식과 유사하다.

1
<property name="driverClass" value="${db.driverClass}" />
cs

 

@Value와 치환자( ${} )를 이용해 프로퍼티 값을 필드에 주입하려면, 프로퍼티 소스로부터 가져온 값을 @Value 필드에 주입하는 기능을 제공해주는 PropoertySourcePlaceholderConfigurer를 빈으로 정의해줘야 한다. 

1
2
3
4
@Bean 
public static PropertySourcePlaceholderConfigurer placeholderConfigurer(){
    return new PropertySourcePlaceholderConfigurer();
}
cs

빈 팩토리 후처리기로 사용되는 빈을 정의해주는 것인데 이 빈 설정 메소드는 반드시 static 메소드로 선언해야 한다.

 @Value로 선언한 네 개의 필드에는 @PropertySource로 지정한 파일에서 가져온 프로퍼티 값이 자동으로 주입될 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
@Bean 
pubilc DataSource dataSource(){
    SimpleDriverDataSource ds = new SimpleDriverDataSource();
 
    ds.setDriverClass(this.driverClass);
    ds.setUrl(this.url));
    ds.setUsername(this.username);
    ds.setPassword(this.password);
 
    return ds;
}
 
cs

장점 :

1) driverClass 처럼 문자열을 그대로 사용하지 않고, 타입 변환이 필요한 프로퍼티를 스프링이 알아서 해준다.

2) 리플렉션 API나 try/catch가 없어서 깔끔하다.

단점 :

1) dataSource 빈에서만 사용되는 프로퍼티인데, 값을 주입받도록 클래스에 필드를 선언하는 것이 부담스러워 보인다.

 

 

 

7.6.6 빈 설정의 재사용과 @Enable

: SQL 서비스 빈은 서비스 인터페이스, 즉 API인 SqlService만 DAO에 노출하면 되고, 나머지 구현 기술이나 방법은 내부로 감추고 필요에 따라 자유롭게 변경하도록 한다. 

 

- 빈 설정자

: SQL 서비스를 재사용 가능한 독립적인 모듈로 만들기 위해 해결할 문제가 한 가지 있다. 

1
2
3
4
5
private class OxmSqlReader implements SqlReader{
    private Unmarshaller unmarshaller;
    private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
    //특정 sqlmap 파일 위치 종속적인 OxmSqlReader 
}
cs

 SQL 서비스를 사용하는 각 애플리케이션은, SQL 매핑파일의 위치를 직접 지정할 수 있어야 하는데 지금은 UserDao.class 위치로 고정되어있다.

SQL 매핑 리소스 위치인 sqlmap 프로퍼티의 디폴트 값을 Userdao 같은 사용자 예제에 종속되지 않게 바꿔보자.

1
2
private Resource sqlmap = new ClassPathResource("/sqlmap.xml");
//설정을 생략하여, 디폴트 값을 UserDao와 같은 사용자 예제에 종속되지 않게 하기.
cs

 

 sql 매핑 리소스는 빈 클래스 외부에서 설정할 수 있어야 한다. 

1
2
3
4
5
6
7
8
9
@Bean 
public SqlService sqlService(){
    OxmSqlService sqlService = new OxmSqlService();
    sqlService.setUnmarshaller(unmarshaller());
    sqlService.setSqlRegistry(sqlRegistry());
    sqlService.setSqlmap(new ClassPathResource("sqlmap.xml", UserDao.class));
   //UserDao 클래스패스에 있는 sqlmap.xml 파일을 이용하게 함.
    return sqlService;
}
cs

 

sql 서비스 구현 클래스 내부의 의존성은 제거했지만, 아직 설정 클래스에는 UserDao.class라는 특정 애플리케이션에 종속된 정보가 남아있다. SQL 매핑파일의 위치를 지정하는 작업을 분리해보자. 파일의 위치와 리소스 종류가 달라지더라도 SqlServiceContext는 수정할 필요가 없어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1
import org.springframework.core.io.Resource;
 
public interface SqlMapConfig{
    Resource getSqlMapResource();
    //SQL 매핑파일의 리소스를 돌려주는 메소드 추가
}
 
// 2
//SqlMapConfig 인터페이스의 구현 클래스 
public class UserSqlMapConfig implements SqlMapConfig{
    
    @Override
    public Resource getSqlMapResource(){
        return new ClassPathResource("sqlmap.xml", UserDao.class);
    }
}
 
// 3
// SqlServiceContext가 변하지 않는 SqlMapConfig 인터페이스에 의존하게 만들기
@Configuration
public class SqlServiceContext{
    @Autowired SqlMapConfig sqlMapConfig;
 
    @Bean
    public SqlService sqlService(){
        OxmSqlService sqlService = new OxmSqlService();
        sqlService.setUnmarshaller(unmarshaller());        
        sqlService.setSqlRegistry(sqlRegistry());
        sqlService.setSqlmap(this.sqlMapConfig.getSqlMapResource());
        return sqlService;
    }
}
 
cs

 

 마지막으로 SqlMapConfig를 구현한 UserSqlMapConfig 클래스를 빈으로 등록하자

1
2
3
4
5
6
7
public class AppContext{
    ...
    @Bean 
    public SqlMapConfig sqlMapConfig(){
        return new UserSqlMapConfig();
    }
}
cs

sqlMapConfig 빈은 SqlConfigService 빈에 @Autowired를 통해 주입되어 사용될 것이다.

 

 지금까지 별도의 모듈로 분리할 SqlServiceContext를 제외하고 나머지 빈 설정은 AppContext로 통합했다. 하지만 파일을 줄이고 좀 더 간결하게 만드는 방법이 있다.  AppContext는 설정용 클래스이면서 스스로 빈으로 사용된다. AppContext가 SqlMapConfig를 직접 구현하게 하면 어떨까? 그러면 UserSqlMapConfig 클래스를 추가할 필요가 없게된다.

1
2
3
4
5
6
7
8
public class AppContext implements SqlMapConfig{
    ...
    @Override
    public Resource getSqlMapResource(){
        return new ClassPathResource("sqlamp.xml", UserDao.class);
    }
}
 
cs

 이제 UserSqlMapConfig 클래스와 AppContext에 넣었던 @Bean sqlMapConfig() 메소드를 제거한다.

 

 

 

 

 

- @Enable* 애노테이션

: 스프링 3.1은 SqlServiceContext처럼 모듈화된 빈 설정을 가져올 때 사용하는 @Import를, 다른 애노테이션으로 대체할 수 있는 방법을 제공한다.

 @Component는 빈 자동등록 대상을 지정할 때 사용되는 애노테이션인데, 직접 사용하기 보다는 @Repository나 @Service처럼 좀 더 의미 있는 이름의 애노테이션을 만들어 사용한다. 비슷한 방식으로 @Import도 다른 이름으로 대체 가능하다. 

 

1
2
3
@Import(value="SqlServiceContext.class")
public @interface EnableSqlService{
}
cs

 새로 정의한 애노테이션의 이름은 @EnableSqlService이며, SqlService를 사용하겠다는 의미로 보면 된다. 

 

@Enable로 시작하는 애노테이션의 종류에는 @EnableTransactionManagement가 있다. @Import를 메타 애노테이션으로 가지고 있다.

1
2
3
4
5
<tx:annotation-driven />
//기능 
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement{
 
cs

 

@EnableSqlService 적용

1
2
3
4
5
6
7
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
//@Import(SqlServiceContext.class) 
@EnableSqlService
@PropertySource("/database.properties"//프로퍼티 
public class AppContext implements SqlMapConfig{
cs

 

 

 

반응형