✅ 테스트용 컨텍스트 분리
TestApplicationContext를 실제 서비스할 DI 설정 정보라 생각하고 AppContext로 이름을 변경합니다. 그리고 Test용으로 DI 설정을 새롭게 구성하기 위해 TestAppContext 클래스를 생성합니다.
- TestAppContext
package vol1.jhcode.ch7; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mail.MailSender; import vol1.jhcode.ch7.user.service.DummyMailSender; import vol1.jhcode.ch7.user.service.UserService; import vol1.jhcode.ch7.user.service.UserServiceTest.TestUserService; @Configuration public class TestAppContext { @Bean public UserService testUserSerivce() { TestUserService testService = new TestUserService(); return testService; } @Bean public MailSender mailSender() { return new DummyMailSender(); } }
TestUserService는 UserServiceTest 안에 있는 내부 static 클래스로 UserServiceImpl을 상속 받았습니다. UserServiceImpl이 가지고 있는 필드는 @Autowired로 주입 받고 있어 위와 같이 간단하게 빈 구성을 할 수 있습니다.
- UserDaoTest
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestAppContext.class, AppContext.class}) public class UserDaoTest {
위와 같이 하나 이상의 설정 클래스가 스프링 테스트에 사용되게 할 수 있습니다.
🔹@Import
SQL 서비스는 그 자체로 독립적인 모듈처럼 userDao 빈과 같은 DAO에서 SqlService 타입의 빈을 DI 받을 수 있기만 하면 되지 구체적인 구현 방법을 알 필요가 없습니다. 그래서 SQL 서비스와 관련된 빈들을 분리해보겠습니다.
- SqlServiceContext
package vol1.jhcode.ch7; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import vol1.jhcode.ch7.user.sqlservice.OxmSqlService; import vol1.jhcode.ch7.user.sqlservice.SqlRegistry; import vol1.jhcode.ch7.user.sqlservice.SqlService; import vol1.jhcode.ch7.user.sqlservice.updatable.EmbeddedDbSqlRegistry; @Configuration public class SqlServiceContext { @Bean public SqlService sqlService() { OxmSqlService sqlService = new OxmSqlService(); sqlService.setUnmarshaller(unmarshaller()); sqlService.setSqlRegistry(sqlRegistry()); return sqlService; } @Bean public SqlRegistry sqlRegistry() { EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry(); sqlRegistry.setDataSource(embeddedDatabase()); return sqlRegistry; } @Bean public Unmarshaller unmarshaller() { Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller(); jaxb2Marshaller.setContextPath("vol1.jhcode.ch7.user.sqlservice.jaxb"); return jaxb2Marshaller; } @Bean public DataSource embeddedDatabase() { return new EmbeddedDatabaseBuilder() .setName("embeddedDatabase") .setType(EmbeddedDatabaseType.H2) .addScript("classpath:vol1/jhcode/ch7/user/sqlservice/updatable/sqlRegistrySchema.sql") .build(); } }
- AppContext
@Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @Import(SqlServiceContext.class) public class AppContext {
기존의 SqlService와 관련된 빈을 지우고, 테스트가 정상적으로 작동되는지 확인합니다.
✅ 프로파일
실제 서비스가 될 때 메일 발송 기능을 사용하기 위한 빈을 다시 정의해보겠습니다.
- AppContext
@Bean public MailSender mailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost("localhost"); return mailSender; }
여기서 문제는 Test를 할 때, TestAppContext와 AppContext 설정을 동시에 사용하고 있기 때문에 두 개의 mailSender 빈이 생성되는 것입니다. 스프링이 빈 정보를 읽는 순서에 따라 뒤의 빈 설정이 앞에서 발견된 빈 설정보다 우선적으로 적용되어 AppContext에 등록된 빈이 적용되게 됩니다.
🔹@Profile과 @ActiveProfiles
환경에 따라서 빈 설정 정보가 달라져야 하는 경우에 파일을 여러 개로 쪼개고 조합하는 등의 번거로운 방법 대신 간단한 설정 정보를 구성할 수 있는 방법을 제공합니다.
- @Profile : 클래스나 메서드에 부여하여 해당 빈이 어떤 프로파일에서 활성화되어야 하는지 지정합니다.
- @ActiveProfiles : 테스트 클래스에서 사용하여 특정 프로파일을 활성화하는 데 사용됩니다. 테스트 시에 특정 프로파일을 활성화하여 테스트에 필요한 빈들만 사용하거나 특정 프로파일에 맞게 테스트를 수행할 수 있습니다.
- 중첩 클래스로 모은 AppContext
package vol1.jhcode.ch7; import javax.sql.DataSource; import org.mariadb.jdbc.Driver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Profile; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.mail.MailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import vol1.jhcode.ch7.user.service.DummyMailSender; import vol1.jhcode.ch7.user.service.UserService; import vol1.jhcode.ch7.user.service.UserServiceImpl; import vol1.jhcode.ch7.user.service.UserServiceTest.TestUserService; @Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @Import(SqlServiceContext.class) public class AppContext { @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(Driver.class); ds.setUrl("jdbc:mariadb://localhost:3306/testdb?characterEncoding=UTF-8"); ds.setUsername("root"); ds.setPassword("1234"); return ds; } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; } @Configuration @Profile("production") public static class ProductionAppContext { @Bean public MailSender mailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost("localhost"); return mailSender; } } @Configuration @Profile("test") public static class TestAppContext { @Bean public UserService testUserService() { return new TestUserService(); } @Bean public MailSender mailSender() { return new DummyMailSender(); } } }
코드의 양이 많지 않기 때문에 중첩 클래스로 하여 각각의 프로파일을 설정했습니다. 따라서 UserDaoTest에서는 AppContext만을 설정 정보로 하여, 필요한 테스트에 맞게 테스트할 수 있습니다.
- UserDaoTest
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {AppContext.class}) @ActiveProfiles("test") public class UserDaoTest {
하나의 설정 파일만 가지고 여러 환경에 대해 Profile을 지정하여 테스트할 수 있게 되었습니다.
- UserDaoTest
✅ 프로퍼티 소스
DataSource의 DB 연결 정보는 DB 드라이버 클래스, 접속 URL, 로그인 계정 정보 등 개발환경이나 테스트 환경, 운영 환경에 따라 달라집니다. 이런 외부 서비스 연결에 필요한 정보는 자바 클래스에서 제거하고 손 쉽게 편집할 수 있도록 빌드 작업이 따로 필요 없는 XML이나 프로퍼티 파일 같은 텍스트 파일에 저장해두는 편이 좋습니다.
🔹@PropertySource
@PropertySource 어노테이션은 스프링 프레임워크에서 외부 프로퍼티 파일을 로드하여 애플리케이션 설정에 사용하는 데 사용되는 어노테이션입니다. 이 어노테이션을 사용하면 애플리케이션 설정을 XML이나 Java Config에서 외부 프로퍼티 파일에 매핑하여 관리할 수 있습니다.
- database.properties
db.driverClass=org.mariadb.jdbc.Driver db.url=jdbc:mariadb://localhost:3306/testdb?characterEncoding=UTF-8 db.username=root db.password=1234
- AppContext
@Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @Import(SqlServiceContext.class) @PropertySource("classpath:/vol1/jhcode/ch7/database.properties") public class AppContext { @Autowired Environment env; @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); try { ds.setDriverClass((Class<? extends java.sql.Driver>) Class.forName(env.getProperty("db.driverClass"))); } 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; }
@PropertySource로 등록한 리소스로부터 가져오는 프러퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장됩니다. 환경 오브젝트는 빈처럼 주입 받을 수 있기 때문에 주입 받은 빈을 통해서 프로퍼티의 값을 가져와 DataSource를 설정했습니다.
🔹@Value, PropertySourcesPlaceholderConfigurer
프로퍼티 값을 직접 DI 받는 방법도 가능합니다. @Value 어노테이션을 사용하여 $ { } 안에 넣은 문자열을 디폴트 엘리먼트 값으로 지정해주면 됩니다.
- AppContext
@Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @Import(SqlServiceContext.class) @PropertySource("classpath:/vol1/jhcode/ch7/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; @Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(this.driverClass); ds.setUrl(this.url); ds.setUsername(this.username); ds.setPassword(this.password); return ds; }
✅ 빈 설정의 재사용과 @Enable
OxmSqlService의 내부 클래스인 OxmSqlRegistry는 sqlmap.xml에 대한 위치 정보를 특정 클래스에 종속적인 형태로 남아있습니다. 이를 외부에서도 설정할 수 있도록 변경해보겠습니다.
- SqlServiceContext
@Bean public SqlService sqlService() { OxmSqlService sqlService = new OxmSqlService(); sqlService.setUnmarshaller(unmarshaller()); sqlService.setSqlRegistry(sqlRegistry()); sqlService.setSqlmap(new ClassPathResource("sqlmap.xml", UserDao.class)); return sqlService; }
외부에서도 이렇게 setSqlmap() 메소드를 통하여 변경할 수 있지만 여전히 UserDao.class라는 특정 애플리케이션에 종속된 정보가 남아있ㅅ브니다. 이대로 두면 다른 애플리케이션에서 SqlServiceContext를 수정 없이 @Import로 가져다가 사용할 수 없습니다. 이를 기본적인 DI를 사용하여 다시 수정해보겠습니다.
- SqlMapConfig
package vol1.jhcode.ch7; import org.springframework.core.io.Resource; public interface SqlMapConfig { Resource getSqlMapResource(); }
- UserSqlMapConfig
package vol1.jhcode.ch7; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import vol1.jhcode.ch1.dao.UserDao; public class UserSqlMapConfig implements SqlMapConfig { @Override public Resource getSqlMapResource() { return new ClassPathResource("sqlmap.xml", UserDao.class); } }
- SqlServiceContext
@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; }
- AppContext
@Bean public SqlMapConfig sqlMapConfig() { return new UserSqlMapConfig(); }
SqlServiceContext는 변하지 않는 SqlMapConfig 인터페이스에만 의존하게 만들고, SqlMapConfig 구현 클래스는 빈으로 정의해 런타임 시에 주입되도록 만들었습니다.
그렇지만 조금 더 간단하게 할 수는 없을까요? AppContext가 SqlmapConfig 인터페이스를 구현하게 만들면 어떻게 될까요? AppContext가 빈이긴 하지만 어노테이션과 메소드를 통해 컨테이너의 기본 빈 설정 정보로 사용되는 것이 주 목적이라 별다른 인터페이스를 구현하지 않았지만 얼마든지 인터페이스를 구현하게 만들 수도 있습니다.
- SqlMapConfig를 구현한 AppContext
@Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @Import(SqlServiceContext.class) @PropertySource("classpath:/vol1/jhcode/ch7/database.properties") public class AppContext implements SqlMapConfig { @Value("${db.driverClass}") Class<? extends Driver> driverClass; @Value("${db.url}") String url; @Value("${db.username}") String username; @Value("${db.password}") String password; @Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } @Override public Resource getSqlMapResource() { return new ClassPathResource("sqlmap.xml", UserDao.class); }
@Configuration 어노테이션도 일종의 @Component 어노테이션으로 설정 정보도 하나의 빈으로 등록되기 때문에, SqlMapConfig를 구현한 AppContext는 SqlServiceContext의 필드인 sqlMapConfig에 필드에 자동 주입이 될 수 있습니다.
🔹Enable* 어노테이션
개발자가 직접 어노테이션을 정의하여 사용할 수도 있습니다.
- EnableSqlService
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(value=SqlServiceContext.class) public @interface EnableSqlService { }
@Retention(RetentionPolicy.RUNTIME)
: 해당 어노테이션을 실행 시점에도 유지(retain)하도록 지정합니다. 즉, 런타임에도 어노테이션 정보를 유지하게 됩니다.
@Target(ElementType.TYPE)
: 해당 어노테이션을 클래스 레벨에서만 사용할 수 있도록 제한합니다. 즉, 클래스에만 @EnableSqlService 어노테이션을 적용할 수 있습니다.
@Import(value=SqlServiceContext.class)
: SqlServiceContext라는 설정 클래스를 임포트하도록 지정합니다. SqlServiceContext는 스프링의 @Configuration 어노테이션이 부여된 클래스로서, 스프링 빈들을 설정하고 관리하는 역할을 합니다.
- AppContext 전체 코
package vol1.jhcode.ch7; import javax.sql.DataSource; import org.mariadb.jdbc.Driver; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.mail.MailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import vol1.jhcode.ch7.user.dao.UserDao; import vol1.jhcode.ch7.user.service.DummyMailSender; import vol1.jhcode.ch7.user.service.UserService; import vol1.jhcode.ch7.user.service.UserServiceTest.TestUserService; @Configuration @EnableTransactionManagement @ComponentScan(basePackages = "vol1.jhcode.ch7.user") @EnableSqlService @PropertySource("classpath:/vol1/jhcode/ch7/database.properties") public class AppContext implements SqlMapConfig { @Value("${db.driverClass}") Class<? extends Driver> driverClass; @Value("${db.url}") String url; @Value("${db.username}") String username; @Value("${db.password}") String password; @Bean public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } @Override public Resource getSqlMapResource() { return new ClassPathResource("sqlmap.xml", UserDao.class); } @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(this.driverClass); ds.setUrl(this.url); ds.setUsername(this.username); ds.setPassword(this.password); return ds; } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; } @Configuration @Profile("production") public static class ProductionAppContext { @Bean public MailSender mailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost("localhost"); return mailSender; } } @Configuration @Profile("test") public static class TestAppContext { @Bean public UserService testUserService() { return new TestUserService(); } @Bean public MailSender mailSender() { return new DummyMailSender(); } } }
📖 토비 스프링 3.1 -p682~711
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T