7.6.1 애노테이션의 메타정보 활용
- 초기 리플렉션 API는 자바 코드나 컴포넌트를 작성하는데 사용되는 툴을 개발할 때 이용하도록 만들어졌음
- 이후, 자바 코드의 메타정보를 데이터를 활용하는 스타일의 프로그래밍 방식에서 리플렉션 API를 활용하도록 변화됨
- 해당 프로그래밍 방식의 절정이 애노테이션임
- 리프렉션 API를 이용해 애노테이션의 메타정보를 조회하고 가져오는 방법이 전부임
- 애노테이션 자체가 클래스의 타입에 영향을 주지 못하고, 코드에서 활용될수 없어 OOP 스타일의 코드나 패턴을 적용할 수 없음
- 하지만, 애노테이션은 애플리케이션의 핵심 로직과 이를 지원하는 Ioc 프레임워크와 잘 어울림
- 애노테이션은 Ioc 프레임워크가 참조하는 메타정보로 사용되기 때문에 애노테이션의 활용도가 증가하고 있음
- 프레임워크에서 XML을 DI용 메타정보로 활용하기 때문에, 간결한 코딩이 가능했음
- 하지만, XML은 표현하려는 정보를 모두 명시적으로 나타내야 함
<x:special target="type" class="com.mycompany.myproject.MyClass" />
- 이와 대조적으로 애노테이션을 추가하는 것 만으로 리플렉션 API를 활용해 패키지, 클래스 이름, 접근 제한자, 구현 인터페이스 등의 여러 정보를 얻을 수 있음
@Special
public class MyClass{...}
- 물론, 애노테이션에도 단점이 존재함
- XML 변경 시 빌드 과정을 생략해도 되지만, 애노테이션은 자바 코드에 존재하므로 빌드를 새로 해 줘야 함
- 자바 개발 흐름은 점차 XML 같은 텍스트 형태의 메타정보를 활용을 자바 코드에 내장된 애노테이션을 대체하는 쪽으로 가고 있음[Spring-Boot]
- 스프링 2.5 버전에서 부터 스프링 일부에 애노테이션을 적용하기 시작했음
- 3.0까지 XML을 완전히 배제하는 것이 불가능 했지만, 3.1 부터 거의 모든 영역에 XML 대신 애노테이션으로 대체할 수 있게 변경됨
7.6.2 정책과 관례를 이용한 프로그래밍
- 애노테이션 같은 메타정보를 활용하는 프로그래밍 방식은 명시적으로 동작 코드를 기술하는 대신, 코드 없이도 미리 약속된 규칙 또는 관례를 따라 프로그램이 동작하도록 함
- 미리 약속된 규칙과 관례에 따라 프레임워크가 작업을 수행하기 때문에, 많은 코드의 내용을 생략할 수 있음
- 하지만, 미리 정의된 규칙과 관례를 기억 해야하는 부담이 있음
- 스프링은 루비 언어를 기반으로한 RoR 프레임워크에 영향을 받았음
- 스프링은 XML 또는 애노테이션의 메타정보를 활용한 프레임워크이기 때문에, 코드가 매우 간략할 수 있음
- 스프링은 애노테이션으로 메타정보를 활용하는 방식을 적극 도입하고 있음
7.6.3 지금 까지 만든 XML
<?xml version="1.0" encoding="UTF-8"?>
<beans >
<tx:annotation-driven/>
<context:annotation-config/>
<jdbc:embedded-database id="embeddedDatabase" type="HSQL">
<jdbc:script location="schema.sql"/>
</jdbc:embedded-database>
<bean id="testUserService"
class="com.ksb.spring.UserServiceImpl$TestUserService"
parent="userService">
</bean>
<bean id="userService" class="com.ksb.spring.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<bean id="mailSender"
class="com.ksb.spring.DummyMailSender">
<property name="host" value="mail.server.com"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="userDao" class="com.ksb.spring.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
<property name="sqlService" ref="sqlService"/>
</bean>
<bean id="sqlService" class="com.ksb.spring.OxmSqlService">
<property name="unmarshaller" ref="unmarshaller"/>
<!--디폴트-->
<!--<property name="sqlmap" value="classpath:/sqlmap.xml"/>-->
<property name="sqlRegistry" ref="sqlRegistry"/>
</bean>
<bean id="sqlRegistry" class="com.ksb.spring.EmbeddedDbSqlRegistry">
<property name="dataSource" ref="embeddedDatabase"/>
</bean>
<bean id="unmarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPath" value="com.ksb.spring.jaxb"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
</beans>
7.6.4 테스트 컨텍스트 변경
- 애노테이션과 자바코드로 기존의 XML로 만든 설정정보를 대체할 것임
- XML을 더 이상 사용하지 않게 하는 것이 최종 목적임
- 스프링 3.1은 자바 코드 설정 정보에서 XML과 자바 코드로 만들어진 DI을 동시에 사용할 방법을 제공하고 있음
- @ImportResource가 바로 그것임
/*애노테이션을 활용한 자바 코드 설정 정보를 만듦.
아직 아무런 설정 정보가 없기 때문에, XML의 설정 정보를 가져옴
*/
@Configuration
@ImportResource("/applicationContext.xml")
public class TestApplicationContext {
}
- 정상적으로 동작하는 것을 확인하기 위해 테스트 코드를 돌려봐야 함
- 기존 XML을 사용하는 테스트인 UserDaoTest와 UserServiceTest에서 설정 정보를 가져오는 부분을 수정해야 함
//@ContextConfiguration에서 XML아닌 자바 코드 설정정보를 가져오도록 수정
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestApplicationContext.class)
public class UserDaoTest {
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestApplicationContext.class)
public class UserServiceTest {
}
7.6.5 <context:annotation-config /> 제거
- <context:annotation-config />는 @PostConstruct를 붙인 메소드가 빈이 초기화된 이후에 자동으로 실행되도록 함
- 즉, @PostConstruct와 같은 표준 애노테이션을 인식해 자동으로 메소도를 실행시킴
- 예를 들어, OxmSqlService의 loadsql() 메소드는 OxmSqlService가 올바르게 동작하기 위해 미리 실행돼야 하므로 @PostConstruct를 부여했음
- XML에 담긴 DI 정보를 이용하는 스프링 컨테이너를 사용하는 경우에 @PostConstruct와 같은 애노테이션의 기능이 필요하면 반드시 <context:annotation-config />를 포함해 필요한 빈 후처리기가 등록되게 만들어야 함
- 반면에, XML이 아닌 @Configaration이 붙은 설정 클래스를 사용하는 경우 <context:annotation-config />가 필요 없음
- 이유는, 컨테이너가 직접 @PostConstruct 애노테이션을 처리하는 빈 후처리기를 등록하기 때문임
<beans>
<!--제거-->
<context:annotation-config/>
</beans>
7.6.6 dataSource 빈의 전환
- <bean>은 @Bean과 거의 1:1로 매핑됨
- dataSource 빈에서 id 값은 메소드 이름이고, 리턴 값은 class 값임
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
- 단, 리턴 값은 DI의 원리에 따라 빈의 구현 클래스는 자유롭게 변경하기 때문에 실제 구현 클래스가 아닌, 구현한 인터페이스로 반환하는 것이 좋음
@Bean
public DataSource dataSource(){...}
- 하지만, XML과 같이 프로퍼티 값 주입을 위해 실제 반환되는 변수 타입은 인터페이스를 구현한 클래스(SimpleDriverDataSource)로 선언해야 함
- Datasource에는 getConnection()만 있기 때문에, 프로퍼티 값 주입을 위해 구현 클래스가 필요함
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
- 자바 코드에서 프로퍼티 driverClass의 스트링 값을 자동으로 변환하지 않기 때문에, 적절한 타입으로 변환해 주어야 함
<!--삭제-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
...
</bean>
public class TestApplicationContext {
@Bean
public DataSource dataSource();
public class TestApplicationContext {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName("com.mysql.cj.jdbc.Driver"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl("jdbc:mysql://localhost/toby?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
}
7.6.7 transactionManager 빈의 전환
- transactionManager는 프로퍼티로 dataSource 빈을 의존하고 있음
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
- 자바 코드를 통한 빈의 의존 관계는 빈 메소드를 직접 호출해 리턴 값을 수정자 메소드에 넣으면 됨
<!--제거-->
<bean id="transactionManager">
public class TestApplicationContext {
...
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
}
7.6.8 나머지 빈의 전환 - 1
- userDao, userService, testUserService, mailSender 빈 전환
- testUserService는 userService의 프로퍼티 정의 부분을 그대로 상속하게 만들었음
<bean id="testUserService"
class="com.ksb.spring.UserServiceImpl$TestUserService"
parent="userService">
</bean>
- 이전에 만든 TestUserService 클래스는 테스트 용도로 만들었고, 내부적으로 리플렉션 API를 사용했기 때문에 private 접근 제한자를 사용해도 문제가 없었음
- 하지만, 자바 코드의 설정 파일은 public만 접근할 수 있기 때문에 public으로 전환 해 줘야 함
public static class TestUserService extends UserServiceImpl {...}
<!--전부 제거-->
<bean id="userDao" >
<bean id="sqlService" >
<bean id="testUserService" >
<bean id="mailSender" >
public class TestApplicationContext {
...
@Autowired
SqlService sqlService;
@Bean
public UserDao userDao() {
UserDaoJdbc dao = new UserDaoJdbc();
dao.setDataSource(dataSource());
dao.setSqlService(this.sqlService);
return dao;
}
@Bean
public UserService userService() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(userDao());
service.setMailSender(mailSender());
return service;
}
@Bean
public UserService testUserService(){
UserServiceImpl.TestUserService testService =
new UserServiceImpl.TestUserService();
testService.setUserDao(userDao());
testService.setMailSender(mailSender());
return testService;
}
@Bean
public MailSender mailSender(){
return new DummyMailSender();
}
}
7.6.9 나머지 빈의 전환 - 2
- sqlService, sqlRegistry, unmarshaller 빈 전환
- embeddedDatabase를 주입 받을 때 @Resource 애노테이션 사용
- @Resource는 @Autowired와 유사하지만, @Resource는 필드 이름을 기준으로 빈을 찾음
- 💡 @Autowired는 타입을 기준으로 빈을 찾음
- 세 개의 빈 전환
<!--전부 제거-->
<bean id="sqlService" >
<bean id="sqlRegistry" >
<bean id="unmarshaller" >
public class TestApplicationContext {
@Resource
DataSource embeddedDatabase;
...
@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(this.embeddedDatabase);
return sqlRegistry;
}
@Bean
public Unmarshaller unmarshaller(){
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.ksb.spring.jaxb");
return marshaller;
}
}
- embeddedDatabase 빈의 경우 전용 태그로 만들었음
<!--제거-->
<jdbc:embedded-database id="embeddedDatabase" type="HSQL">
public class TestApplicationContext {
@Resource
DataSource embeddedDatabase;
...
@Bean
public SqlRegistry sqlRegistry(){
EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry();
sqlRegistry.setDataSource(embeddedDatabase());
return sqlRegistry;
}
@Bean
public DataSource embeddedDatabase(){
return new EmbeddedDatabaseBuilder()
.setName("embeddedDatabase")
.setType(HSQL)
.addScript("schema.sql")
.build();
}
}
- 마지막 태그인 <tx:annotation-driven/> 대체
- <tx:annotation-driven/>은 @Transaction이 붙은 곳에 어드바이스와 포인트컷을 적용하는 기능을 함
- 이 역시 전용 태그로 기본적으로 네 가지 클래스를 빈으로 등록함
- 해당 네 가지 빈을 등록하기 번거롭기도 하고, 기억하기 힘듦
- 스프링 3.1 부터 전용 태그에 대응되는 애노테이션을 제공함
- 해당 애노테이션들은 @Enable로 시작함
- <tx:annotation-driven/는> @EnableTransactionManagement로 대체할 수 있음
<!--제거-->
<tx:annotation-driven/>
//모든 XML 설정정보를 대체했기 때문에, ImportResource 삭제 가능
@ImportResource("/applicationContext.xml")
@EnableTransactionManagement
public class TestApplicationContext {...}
- 완성된 애노테이션 기반 설정정보
@Configuration
@EnableTransactionManagement
public class TestApplicationContext {
@Autowired
SqlService sqlService;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName("com.mysql.cj.jdbc.Driver"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl("jdbc:mysql://localhost/toby?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
@Bean
public UserDao userDao() {
UserDaoJdbc dao = new UserDaoJdbc();
dao.setDataSource(dataSource());
dao.setSqlService(this.sqlService);
return dao;
}
@Bean
public UserService userService() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(userDao());
service.setMailSender(mailSender());
return service;
}
@Bean
public UserService testUserService(){
UserServiceImpl.TestUserService testService =
new UserServiceImpl.TestUserService();
testService.setUserDao(userDao());
testService.setMailSender(mailSender());
return testService;
}
@Bean
public MailSender mailSender(){
return new DummyMailSender();
}
@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 marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.ksb.spring.jaxb");
return marshaller;
}
@Bean
public DataSource embeddedDatabase(){
return new EmbeddedDatabaseBuilder()
.setName("embeddedDatabase")
.setType(HSQL)
.addScript("schema.sql")
.build();
}
}
7.6.10 @Autowired를 이용한 자동와이어링
- @Autowired는 파라미터 타입을 통해 주입 가능한 빈을 찾아 주입함
- 💡 같은 타입의 빈이 여러개면 필드 이름과 일치하는 빈을 찾아 주입함
- 수정자 메소드나 필드에 자동으로 조건에 맞는 빈을 주입함
- userDaoJdbc는 dataSource와 sqlService 두 개에 의존함
@Bean
public UserDao userDao() {
UserDaoJdbc dao = new UserDaoJdbc();
dao.setDataSource(dataSource());
dao.setSqlService(this.sqlService);
return dao;
}
- dataSource 수정자 메소드에 @Autowired 적용
// 삭제
dao.setDataSource(dataSource());
public class UserDaoJdbc implements UserDao {
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
}
- 💡 dataSource를 주입 받아 jdbcTemplate를 만들기 때문에 필드로 주입받으면 안됨
7.6.11 @Component를 이용한 자동 빈 등록 - userDao
- @Component가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록 됨
- userDao 빈은 간단한 오브젝트 생성 코드만 남았음
@Bean
public UserDao userDao() {
return new UserDaoJdbc();
}
- 일단 userDao 빈을 제거하고, @Autowired로 주입 받음
// 삭제
@Bean
public UserDao userDao() {...}
public class TestApplicationContext {
@Autowired
UserDao userDao;
@Bean
public UserService userService() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(this.userDao);
service.setMailSender(mailSender());
return service;
}
@Bean
public UserService testUserService(){
UserServiceImpl.TestUserService testService =
new UserServiceImpl.TestUserService();
testService.setUserDao(this.userDao);
testService.setMailSender(mailSender());
return testService;
}
- UserDaoJdbc에 @Component 적용하면, 해당 클래스를 빈으로 등록하겠다는 설정정보임
@Component
public class UserDaoJdbc implements UserDao {...}
- @Component가 붙은 클래스를 찾아 빈으로 등록하려면 빈 스캐너인 @ComponentScan이 필요함
- 스프링이 빈 자동등록을 디폴트로 제공하지 않는 이유는, @Component가 붙은 정보를 일일이 찾기 부담되기 때문임
- 해당 부담을 줄이기 위해 @ComponentScan에 특정 패키지만 검색하도록 지정해야 함
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
public class TestApplicationContext {...}
- @Component로 등록한 빈은 userDaoJdbc 빈이지만, UserDaoJdbc가 UserDao를 구현하기 때문에 @Autowired에 의해 의존성 주입이 가능 한 것임
- 하지만, @Resource와 같이 타입이 아닌 이름을 통해 주입하는 경우 문제가 발생할 수 있음
- 이러한 이유로 @Component에 빈 아이디 설정 가능
@Component("userDao")
public class UserDaoJdbc implements UserDao {...}
- @Component 애노테이션 정의는 다음과 같음
public @interface Component{...}
- 빈 자동등록에 @Component 애노테이션만 사용할 수 있는 것은 아님
- @Component을 메타 애노테이션으로 갖는 애노테이션에서도 사용할 수 있음
@Component // 메타 애노테이션
public @interface SnsConnector{...} //애노테이션 정의
@SnsConnector
public class FacebookConnector{...}
- 애노테이션은 클래스와 달리 같은 인터페이스 구현이나 상속을 통해 여러 개를 그룹화 할 수 없음
- 때문에, 애노테이션에 공통적인 속성을 부여하여 그룹화하기 위해 메타 애노테이션을 사용함
- 스프링에서 DAO 빈을 자동등록 대상으로 만들 때, @Component 대신 @Repository를 사용하도록 권장하고 있음
@Repository
public class UserDaoJdbc implements UserDao {...}
7.6.12 @Component를 이용한 자동 빈 등록 - userService
- UserServiceImpl 내부 클래스로 TestUserService가 있고, 둘 다 userDao와 mailSender를 의존하고 있음
public class UserServiceImpl implements UserService {
...
public static class TestUserService extends UserServiceImpl {...}
}
public class TestApplicationContext {
@Bean
public UserService userService() {
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(this.userDao);
service.setMailSender(mailSender());
return service;
}
@Bean
public UserService testUserService(){
UserServiceImpl.TestUserService testService =
new UserServiceImpl.TestUserService();
testService.setUserDao(this.userDao);
testService.setMailSender(mailSender());
return testService;
}
}
- 의존을 @Autowired로 주입받음
// 삭제
@Bean
public UserService userService() {...}
@Component
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private MailSender mailSender;
...
}
- 여기서 문제가 있음
- UserServiceImpl 와 TestUserService는 둘 다 UserService의 타입임
- @Autowired는 하나의 빈을 찾아 주입하지만, 타입이 같은 두 개의 빈이 있고 이름이 주입하려는 필드와 일치하지 않을 경우 문제가 발생함
- 💡 userService의 이름을 가진 빈이 필요하지만, 현재 userServiceImpl와 testUserService만 존재 하고 있는 경우임
- 빈의 아이디를 지정하면서, 서비스 계층 빈은 @Component보단 @Service를 사용
@Service("userService")
public class UserServiceImpl implements UserService {
//testUserService 빈은 바로 밑인 테스트 컨텍스트에서 빈 추가할 것임
public static class TestUserService extends UserServiceImpl {...}
...
}
7.6.13 테스트용 컨텍스트 분리
- 자동 빈을 등록한 userDao와 userService 빈 및 트랜잭션 관리, SQL 서비스는 항상 필요함
- 기존의 TestApplicationContext를 AppContext로 이름 변경
- 하지만, testUserService나 mailSender의 경우 테스트 용이기 때문에 운영에서는 필요 없음
- 테스트용 빈 두 개를 따로 분리함
- 테스트용 빈 두개가 저장된 설정정보 자바 클래스 이름은 TestAppContext라 함
public class AppContext {
// 필드 두 개 삭제
@Bean
public TestUserService testUserService() {...}
@Bean
public MailSender mailSender(){...}
}
@Configuration
public class TestAppContext {
@Bean
public UserService testUserService() {
return new UserServiceImpl.TestUserService();
}
@Bean
public MailSender mailSender(){
return new DummyMailSender();
}
}
- 설정용 DI 클래스가 두 가지 됐으므로, 두 설정파일 모두 적용
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppContext.class, TestAppContext.class})
public class UserDaoTest {...}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppContext.class, TestAppContext.class})
public class UserServiceTest {...}
7.6.14 SqlServiceContext분리와 @Import
- 운영와 테스트의 컨텍스트 분리는 했음
- 하지만, SQL의 서비스는 그 자체로 독립적으로 모듈로 취급되야 하기 때문에 SQL의 서비스 부분도 분리하는 것이 좋음
- 💡 SqlService의 구현 클래스와 이를 지원하는 클래스는 다른 애플리케이션 구성들과 달리 독립적으로 개발되거나 변경되기 때문
- SQL의 서비스 컨텍스트의 이름을 SqlServiceContext로 설정
@Configuration
public class AppContext {
...
// 필드 네 개 삭제
@Bean
public SqlService sqlService() {...}
@Bean
public SqlRegistry sqlRegistry() {...}
@Bean
public Unmarshaller unmarshaller() {...}
@Bean
public DataSource embeddedDatabase() {...}
}
@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 marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.ksb.spring.jaxb");
return marshaller;
}
@Bean
public DataSource embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.setName("embeddedDatabase")
.setType(HSQL)
.addScript("schema.sql")
.build();
}
}
- 새로운 설정 정보가 생겼으니 해당 설정정보를 추가해야 함
- 기존의 설정정보 추가인 @ContextConfiguration에서 추가하는 것 보다 더 좋은 방법이 있음
@ContextConfiguration(classes = {AppContext.class, TestAppContext.class})
public class UserServiceTest {...}
- AppContext에서 SqlServiceContext가 필요하기 때문에 긴밀히 연결하는 것이 좋음
- @Import를 사용하면 한 설정정보에서 특정 설정정보를 가져올 수 있음
- 가져오는 특정 설정정보를 보조 설정정보로 사용
- AppContext가 메인 설정정보가 되고 SqlServiceContext는 보조 설정정보로 사용하는 방법임
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@Import(SqlServiceContext.class)
public class AppContext {...}
7.6.15 프로파일
- mailSender의 경우 실제 메일이 전송되지 않도록 dummyMailSender을 만들어 테스트용으로만 사용했음
@Bean
public MailSender mailSender(){
return new DummyMailSender();
}
- 운영환경에서는 실제 메일이 전송되야 함
- 운영환경에서만 필요한 빈을 담은 빈 설정 클래스인 ProductionAppContext를 만듦
@Configuration
public class ProductionAppContext {
@Bean
public MailSender mailSender(){
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("localhost");
return mailSender;
}
}
- 해당 빈을 테스트에서 운영으로 변결될 때 마다, 매번 운영환경에서 DI가 변경되도록 바꿔야 한다는 단점이 있음
- 프로파일을 적용하면 해당 문제를 해결할 수 있음
- 프로파일로 활성 및 비활성 설정정보를 지정할 수 있음
- 프로파일은 하나의 설정 클래스만 가지고 환경에 따라 빈 설정 조합을 만들 수 있음
- 프로파일은 @Profile을 사용하면 됨
@Configuration
@Profile("production")
public class ProductionAppContext {...}
@Configuration
@Profile("test")
public class TestAppContext {...}
- 프로파일을 지정하지 않으면 디폴트 빈 설정정보로 취급되어 항상 적용 됨
- 프로파일을 적용하면 모든 설정 클래스를 부담 없이 메인 설정 클래스에 @Import할 수 있다는 장점이 있음
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@Import({SqlServiceContext.class,
TestAppContext.class, ProductionAppContext.class})
public class AppContext {...}
- 또한, AppContext가 모든 설정 파일을 가지고 있기 때문에 @ContextConfiguration에 하나의 설정정보만 남길 수 있음.
- 활성 프로파일은 @ActiveProfiles에 지정하면 됨
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = AppContext.class)
public class UserServiceTest {...}
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = AppContext.class)
public class UserDaoTest {...}
7.6.16 컨테이너의 빈 등록 정보 확인
- 활성 프로파일이 제대로 적용되어 프로파일의 빈 설정만 적용돼어, 해당 빈이 만들어지는지 의문이 있음
- 스프링 컨테이너에 등록된 빈 정보를 조회할 수 있음
- 스프링 컨테이너는 모두 BeanFactory를 구현함
- BeanFactory를 구현 클래스 중 DefaultListableBeanFactory를 통해 대부분의 스프링 컨테이너는 빈을 등록 및 관리 함
- 해당 클래스의 getBeanDefinitionNames()로 스프링 컨테이너의 모든 빈을 가져올 수 있음
public class UserServiceTest {
@Autowired
DefaultListableBeanFactory bf;
...
@Test
public void beans() {
for (String n : bf.getBeanDefinitionNames())
System.out.println("컨테이너 내부 빈 : " + n +
"\t" + bf.getBean(n).getClass().getName());
}
}
7.6.17 중첩 클래스를 이용한 프로파일 적용
- 여러 설정정보를 @Import로 모아 메인 설정 클래스만으로 설정이 가능했음
- 또한, 프로파일을 적용해 환경에 맞는 빈만 적용할 수 있었음
- 하지만, 프로파일을 각 클래스 파일로 분리하니 파일의 개수가 많아졌음
- 중첩 파일로 프로파일에 따라 나눈 클래스를 하나의 클래스 파일로 모으는 것이 좋음
- 💡 내부 설정 정보 클래스는 static임
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@Import({SqlServiceContext.class,
AppContext.TestAppContext.class, AppContext.ProductionAppContext.class})
public class AppContext {
...
@Configuration
@Profile("production")
public static class ProductionAppContext {...}
@Configuration
@Profile("test")
public static class TestAppContext {...}
}
- 중첩 내부 클래스는 메인 설정정보에 있기 때문에 생략 가능
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@Import(SqlServiceContext.class)
public class AppContext {...}
7.6.18 프로퍼티 소스
- 프로파일을 통해 테스트와 운영 환경에서 각각 다른 빈 설정이 적용되게 만들었음
- 하지만, 아직 AppContext에 테스트 환경에 종속적인 DB 연결 정보가 남아 있음
public class AppContext {
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName("com.mysql.cj.jdbc.Driver"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl("jdbc:mysql://localhost/toby?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
...
}
- DB 연결 정보는 손쉽게 편집하기 위해 빌드가 필요하지 않는 외부 텍스트 파일에서 하는것이 옳음
- 자바의 프로퍼티 파일 포맷을 이용해, 이름과 값의 쌍으로 구성되게 만듦
db.driverClass=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost/toby?serverTimezone=UTC
db.username=root
db.password=1234
- 파일의 이름을 database.properties이라 정함
- @PropertySource를 사용해 프로퍼티 파일의 정보를 불러올 수 있음
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@Import(SqlServiceContext.class)
@PropertySource("/database.properties")
public class AppContext {...}
- 프로퍼티의 값은 Environment 타입의 환경 오브젝트에 저장 됨
@PropertySource("classpath:/database.properties")
public class AppContext {
@Autowired Environment env;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName(env.getProperty("db.driverClass")));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl(env.getProperty("db.url"));
dataSource.setUsername(env.getProperty("db.username"));
dataSource.setPasswordenv.getProperty("db.password"));
return dataSource;
}
}
- 테스트 환경에서는 값을 불러올 수 없어 실행되지 않고 있음.. 해결 방법좀;;
- 프로퍼티 값을 직접 DI 받아 Environment을 사용하지 않는 방법이 있음
- 특별한 빈이 필요함
- 해당 빈이 치환자에 값을 주입해 줌
@Bean
public static PropertySourcePlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcePlaceholderConfigurer();
}
- 치환자(Placeholder)인 @Value를 사용하면 됨
@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;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try {
dataSource.setDriverClass(this.driverCalss);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl(this.url);
dataSource.setUsername(this.username);
dataSource.setPasswordenv.getProperty(this.password);
return dataSource;
}
...
}
7.6.19 빈 설정자
- SQL관련 파일들은 독립적으로 개발되어 사용돼야 함
- SQL 서비스를 재사용 가능한 독립적인 모듈로 만들려면 OxmlReader의 XML 의존성을 제거해야 함
public class OxmSqlService implements SqlService{
...
public void setSqlmap(Resource sqlmap){
oxmSqlReader.setSqlmap(sqlmap);
}
private class OxmSqlReader implements SqlReader{
...
private Resource sqlmap = new ClassPathResource("/sqlmap.xml",
UserDao.class);
}
...
}
- XML 의존성을 설정정보인 sqlContext에서 설정할 수 있음
@Configuration
public class 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;
}
...
}
- 하지만, 설정정보에서 애플리케이션 의존 정보가 남게 됨
- 일반적인 DI 방식으로 의존성 문제를 해결 할 수 있음
public interface SqlMapConfig {
Resource getSqlMapResource();
}
/*
@ComponentScan 필수!!
만약 빼면 빈 스캔 위치를 못찾아 sqlMapConfig을 주입할 수 없음
책에 @ComponentScan가 없어서 개고생함;;
SqlServiceContext은 독립적으로 개발 해야하기 때문에
@ComponentScan에 패키지를 지정하지 않았음
*/
@ComponentScan
@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;
}
}
public class UserSqlMapConfig implements SqlMapConfig{
@Override
public Resource getSqlMapResource() {
return new ClassPathResource("/sqlmap.xml", UserDao.class);
}
}
public class AppContext {
...
@Bean
public SqlMapConfig sqlMapConfig(){
return new UserSqlMapConfig();
}
}
- 일반적 DI 방식은 전략패턴을 쓰기 때문에 파일의 개수가 많아진다는 단점이 있음
- AppContext가 직접 SqlMapConfig를 구현하면 문제를 해결할 수 있음
- AppContext에 붙어 있는 @Configration은 @Component를 메타 애노테이션으로 갖고있는 자동 빈 등록용 애노테이션이기도 함
@Component
public @interface Configration{...}
- 즉, AppContext 역시 빈으로 등록되므로, SqlMapConfig를 직접 구현하면 SqlMapConfig 타입을 가지는 sqlMapConfig 빈으로 등록 될 것임
- SqlServiceContext는 sqlMapConfig 빈만 주입받으면 되므로 문제 없음
public class SqlServiceContext {
@Autowired
SqlMapConfig sqlMapConfig;
...
}
- 코드
public class AppContext implements SqlMapConfig{
// 이것만 삭제
@Bean
public SqlMapConfig sqlMapConfig(){...}
@Override
public Resource getSqlMapResource() {
return new ClassPathResource("/sqlmap.xml", UserDao.class);
}
}
7.6.20 @Enable* 애노테이션
- SqlServiceContext는 라이브러리 모듈에 포함되서 재사용될 수 있음
- 스프링 3.1은 SqlServiceContext 처럼 모듈화된 빈을 가져올 때 사용하는 @Import 대신, 다른 애노테이션으로 대체할 수 있는 방법을 제공하고 있음
- @Component와 동일한 기능을 하지만, @Repository나 @Service 처럼 좀 더 의미있는 애노테이션으로 대체해서 사용하는것과 동일하게 @Import 대신 다른 애노테이션 사용
- 즉, 새로운 애노테이션을 정의해서 사용하는 것임
@Import(value = SqlServiceContext.class)
public @interface EnableSqlService {
}
- EnableSqlService 라는 새로운 애노테이션으로 좀 더 의미있는 애노테이션으로 바꿀 수 있음위에서 <tx:annotation-driven/>을 대체한 @EnableTransactionManagement 역시 @Import를 메타 애노테이션으로 갖고 있음
@Import(TransactionManagementConfigrationSelector.class)
public @interface EnableTransactionManagement{...}
- 최종 AppContext 코드
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.ksb.spring")
@EnableSqlService
@PropertySource("classpath:/database.properties")
public class AppContext implements SqlMapConfig{
@Autowired
SqlService sqlService;
@Autowired
UserDao userDao;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
//Enviroment의 값 null 문제라 값을 주입할 수 없음
try {
dataSource.setDriverClass((Class<? extends Driver>) Class.forName("com.mysql.cj.jdbc.Driver"));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
dataSource.setUrl("jdbc:mysql://localhost/toby?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource());
return tm;
}
@Override
public Resource getSqlMapResource() {
return new ClassPathResource("/sqlmap.xml", UserDao.class);
}
@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 UserServiceImpl.TestUserService();
}
@Bean
public MailSender mailSender(){
return new DummyMailSender();
}
}
}
'토비의 스프링 정리' 카테고리의 다른 글
토비의 스프링 - 8.1 스프링의 정의 (0) | 2022.10.26 |
---|---|
토비의 스프링 - 7.7 7장 정리 (0) | 2022.10.26 |
토비의 스프링 - 7.5 DI를 이용해 다양한 구현 방법 적용하기 (0) | 2022.10.25 |
프로그래머스 - 7.4 인터페이스 상속을 통한 안전한 기능확장 (0) | 2022.10.25 |
토비의 스프링 - 7.3 서비스 추상화 적용 (0) | 2022.10.25 |