[토비의 스프링] Week13(7.6.3~7.7)

8 분 소요

토비의 스프링 3.1 Chapter 7.6.3 ~ 7.7

컨텍스트 분리와 @import

지금까지는 테스트용 testUserService 빈과 userService빈을 한 곳의 XML로 담아 놓았다. 성격이 다른 DI 정보는 따로 관리되어야 하기 때문에, DI 설정 클래스를 추가하고 관련된 빈 설정과 애너테이션, 필드, 메서드를 옮긴다.

기존의 하나였던 설정을

  • 테스트에만 쓰이는 TestAppContext(DummyMailSender()등을 포함한)
  • 실제 앱의 동작에 쓰이는 AppContext

로 분리하였으면, 테스트에는 두 개 모두 Import를, 실제 로직에는 AppContext만 Import 하도록 한다.

@ContextConfiguration(classes={TestAppContext.class, AppContext.class})
public class UserDaoTest {
		//...
}

지금까지 만들어왔던 SqlService는 다른 데에서도 충분히 쓰일 수 있고, AppContext 내의 빈들과 구분되는 특징이 있으므로 SqlServiceContext 클래스로 분리하여 모듈처럼 관리하고, 그래도 여전히 실 서비스와 깊은 연관이 있으므로 AppContext와 @Import를 통해 연관을 지어준다.

@Configuration
@EnableTransactionManagement

@ComponentScan(basePackages="springbook.user")

@Import(SqlServiceContext.class) 
// AppContext에 접근할 수 있다면 여전히 SqlServiceContext의 빈을 사용할 수 있다.
public class AppContext {
		// ...
}

프로파일

테스트 환경과 운영환경에서 각각 다른 빈 정의가 필요한 경우가 있다. 예를 들면 테스트와 운영환경에서 양쪽 모두에 필요한 Bean이지만 내용이 달라져야 하는 경우다. 파일을 분리하는 방법으로는 복잡한 개발환경에서 문제가 있을 수 있다.

그래서 @Profile과 @ActiveProfiles를 사용할 수 있다.

환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의하고, 실행 시점에 어떤 프로파일의 빈을 사용할 지 지정한다.

@Configuration
@Profile("test")
public class TestAppContext {
		//테스트 환경에 사용될 빈
}

@Configuration
@Profile("production")
public class ProductionAppContext {
		//운영환경에 사용될 빈
}

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class}) 
// AppContext에 접근할 수 있다면 여전히 SqlServiceContext의 빈을 사용할 수 있다.
public class AppContext {
		// ...
}

@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test") // 프로파일 사용
@ContextConfiguration(classes=AppContext.class)
public class UserServiceTest {
		// test 프로파일을 가지고 있는 TestAppContext에 정의된 빈이 사용된다.
		// ProductionAppContext의 빈들은 적용되지 않는다.
}

실제 ProductionAppContext에 정의된 Bean이 무시되고 있는지 알고싶다면, 아래와 같은 코드를 작성할 수 있다.

@Autowired 
DefaultListableBeanFactory bf;
// 스프링 컨테이너는 BeanFactory 인터페이스를 구현한다.
// 그 중 DefaultListableBeanFactory 구현 클래스는 대부분의 스프링 컨테이너에서 사용된다.
// getBeanDefinitionNames()는 컨테이너에 모든 빈 이름을 가져올 수 있다.

@Test
public void beans(){
		for(String str : bf.getBeanDefinitionNames()){
				System.out.println(str + "\t" + bf.getBean(str).getClass().getName());
		}
}

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

거대한 하나의 빈 설정을 @Import를 이용해 나누고, 프로파일을 적용하여 상황에 따른 빈 설정이 가능하게 했다. 근데 파일이 많아지면 이 모든 것 들을 한 눈에 보기 어려워 분리했던 설정정보를 중첩 클래스를 이용해 하나로 모아보자.

기껏 열심히 분리해놨더니 모으면 허무하지 않을까 싶지만 프로파일 설정과, 목적이 다른 빈을 다른 클래스로 분리해 놓은것은 여전히 유효하도록 하면서, 가독성을 높이는 방법이다.


@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@Import({SqlServiceContext.class, 
					/*AppContext.TestAppContext.class, // 이젠 AppContext의 스태틱 클래스가 되어, 이 부분이 바뀐다. 
					AppContext.ProductionAppContext.class*/ // Import 애너테이션으로 지정해주지 않아도 작동이 잘 된다.
				}) 
public class AppContext {
		// ...
		
		@Configuration
		@Profile("test")
		public static class TestAppContext {
				//테스트 환경에 사용될 빈
		}
		
		@Configuration
		@Profile("production")
		public static class ProductionAppContext {
				//운영환경에 사용될 빈
		}
}

프로퍼티 소스

AppContext에는 여전히 테스트 환경에 종속되는 정보가 남아있다. dataSource의 DB 연결 정보다. 드라이버 클래스, URL, 계정 정보는 환경에 따라 달라진다.

그래서 이런 정보들은 자바 코드에 직접 하드코딩 하는 대신, XML이나 프로퍼티 파일같은 텍스트 파일에 저장해 두는 것이 좋다.

# 빈 설정에 필요한 프로퍼티를 외부 정보로 부터 가져올 수 있다.
# 이렇게 프로퍼티 값을 가져오는 대상을 프로퍼티 소스라고 부른다.
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://....
db.username=spring
db.password=book
@PropertySource("/database.properties") // 해당 프로퍼티 소스에서 프로퍼티를 읽어온다.
public class AppContext {
}
@Autowired Environment env;
@Bean
public DataSource dataSource {
    ...
    try {
        ds.setDriverClass(Class<? extends java.sql.Driver>) // Class 타입 캐스팅이 필요
								Class.forName(env.getProperty("db.driverClass"));
    } catch (ClassNotFoundException e) {
        ...
    }
    ds.setUrl(env.getProperty("db.url"));
		ds.setUsername(env.getProperty("db.username"));
		ds.setPassword(env.getProperty("db.password"));

		return ds;
}

코드가 좀 지저분하다. Environment 오브젝트를 주입받아 귀찮게 코드를 작성하는 대신 PropertySourcesPlaceholderConfigurer에서 @Value 애너테이션을 통해 치환자로 프로퍼티를 소스로부터 직접 주입받을 수 있다.

//...
public class AppContext{
		@Value("${db.driverClass}") Class<? extends Driver> driverClass;
		@Value("${db.url}") String url;

		//....
}

@Value와 치환자로 프로퍼티 값을 필드에 주입하려면 PropertySourcesPlaceholderConfigurer 빈을 정의해주어야 한다.

@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
}

빈 설정의 재사용과 @Enable*

SqlServiceContext는 조금 특별한 특징이 있어 AppContext와 분리하도록 했다. 그래서 여러 프로젝트에서 재사용이 쉽고, 빈 설정을 깔끔하게 유지할 수 있었다.

그러나 여전히 SQL 서비스는 특정 위치의 특정 파일에 의존적인 문제가 있다.

private class OxmSqlReader implements SqlReader {
    private Unmarshaller unmarshaller;
    private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
		//UserDao의 클래스 패스의 sqlmap.xml로 고정되어있다.
		//..
}
// SQL 매핑파일의 리소스를 돌려주는 메서드를 구현한다.
public class UserSqlMapConfig implements SqlMapConfig{
    @Override
    public Resource getSqlMapResource() {
        return new ClassPathResource("/sqlmap.xml", UserDao.class);
    }
}
		@Bean
    public SqlService sqlService() throws IOException {
				@Autowired
				SqlMapConfig sqlMapConfig
				//....
				sqlService.setSqlRegistry(sqlRegistry());       
        sqlService.setSqlmap(this.sqlMapConfig.getSqlMapResource());
				// 외부에서 주입받은 sqlMapConfig에서 xml파일을 주입받는다.
				// 이제 이 서비스를 사용하고자 하는 개발자는 UserSqlMapConfig의 메서드를 구현하여
				// 원하는 위치의 xml 파일을 읽을 수 있도록 하였다.
        return sqlService;
    }

SQL 매핑파일 리소스 위치도 빈 설정에 관련된 정보인데, 새로운 클래스를 추가한게 영 아쉽다.조금 더 간결하게 만들어 보고 싶다.

AppContext도 빈이라서 @Autowired를 통해 DI받을 수 있다. AppContext가 SqlMapConfig 인터페이스를 직접 구현하도록 한다.

빈을 DI받아서 사용하는 쪽에서는 특정 인터페이스를 구현하는지에만 관심이 있기 때문에, SqlMapConfig 인터페이스를 직접 구현하고, 그 안에 메서드를 Override하면 굳이 클래스 하나를 더 만들 필요가 없어진다.

public class AppContext implements SqlMapConfig{
		//...

		@Override
		public Resource getSqlMapResource(){
				return //...
		}
}

@Enable* 애너테이션

SqlServiceContext는 모듈화가 되어 빈 설정에 재사용될 수 있다. @Import를 이용해야 하지만, 애너테이션을 보다 가독성을 높이는 방향으로 쓰기 위해 @Enable* 애너테이션을 구현하자.

@Import(value = SqlServiceContext.class) // 메타 애너테이션을 @Enable*로 감쌌다.
public @interface EnableSqlService {
}
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages="springbook.user")
@EnableSqlService // 보다 더 가독성이 좋아졌다.
@PropertySource("/database.properties")
public class AppContext implements SqlMapConfig{
		//...

}

댓글남기기