본문 바로가기

토비의 스프링 정리

토비의 스프링 - 7.6 스프링 3.1의 DI

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();
        }
    }
}