잊지 않겠습니다.

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



지금까지 우리는 Template-callback 구조의 SqlExecutor와 Connection을 처리하는 ConnectionFactory를 이용한 Dao 객체들을 구성하였습니다. 지금까지 만든 Dao 객체들을 Spring에서 제공하는 Jdbc객체들을 이용해서 변환시키는 과정을 한번 알아보도록 하겠습니다. 

Spring JDBC를 이용한 Dao 의 개발

Spring JDBC는 지금까지 이야기한 모든 기능들이 다 포함되어있습니다.
# Template, callback 구조
# DataSource를 이용한 ConnectionFactory 구현
# Checked Exception을 Runtime Exception으로 변경

먼저, maven을 이용해서 spring jdbc를 추가하도록 합니다. 

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>3.2.0.RELEASE</version>
</dependency>

먼저, DataSource를 구성합니다. DataSource는 기존 ConnectionFactory를 대신합니다. 이는 이미 Spring JDBC에서 제공하는 객체이기 때문에 따로 구현할 필요가 없습니다. ApplicationContext에 다음 항목을 추가해주면 됩니다. 
여기서 보시면 아시겠지만, 지금 여기서 사용하는 DataSource는 DriverManagerDataSource객체이고, DataSource는 하나의 interface입니다. Spring에서 제공되는 모든 DB connection은 DataSource interface를 구현하고 있습니다. 이번에 사용한 DriverManagerDataSource는 가장 단순한 DataSource라고 할 수 있습니다.

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>

지금까지 개발한 BookDao를 변경합니다. BookDaoImplWithSqlExecutor로 이름을 변경시킵니다. JdbcTemplate을 이용해서 같은 기능의 코드를 작성할 예정이기 때문에 지금까지 구현된 method들을 모든 method를 interface로 따로 뽑습니다. 
구성된 interface BookDao는 다음과 같습니다. 이러한 Interface를 위주로하는 설계는 프로그램의 확장성을 높입니다. 지금 저희는 기존에 만들어진 것과 기능적으로는 완벽하게 동일한 객체를 만들어서 테스트코드를 통과시킬 예정입니다. 기존 테스트 코드가 객체 자체를 가지고 왔다면, 이제는 Interface를 가지고 오는 형식으로 변경을 시킬 예정입니다. 

public interface BookDao {
    int countAll();
    void add(Book book);
    void update(Book book);
    void delete(Book book);
    void deleteAll();
    Book get(int id);
    List<Book> getAll();
    List<Book> search(String name);
}

그리고, 변경시킨 BookDaoImplWithSqlExecutor에서 BookDao interface를 implements 해주도록 합니다. 
마지막으로 class를 추가합니다. BookDaoImplWithJdbcTemplate를 추가하고, JdbcTemplate을 사용하도록 코드를 작성합니다. 지금까지 만들었던 SqlExecutor와 설정이 너무나도 유사합니다. Spring을 통해 다음과 같이 선언하도록 합니다. 

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>


다음은 JdbcTemplate를 이용한 BookDao 코드입니다. 기존 코드를 비교해보도록 합니다.

public class BookDaoImplWithJdbcTemplate implements BookDao {
    private JdbcTemplate jdbcTemplate;

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }
    
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    private Book convertToBook(ResultSet rs) throws SQLException {
        Book book = new Book();
        book.setId(rs.getInt("id"));
        book.setName(rs.getString("name"));
        book.setAuthor(rs.getString("author"));
        java.util.Date date = new java.util.Date(rs.getTimestamp("publishDate").getTime());
        book.setPublishDate(date);
        book.setComment(rs.getString("comment"));
        book.setStatus(BookStatus.get(rs.getInt("status")));
        int rentUserId = rs.getInt("rentUserId");
        if(rentUserId == 0) {
            book.setRentUserId(null);
        }
        else {
            book.setRentUserId(rentUserId);
        }
        return book;
    }
    
    private Book convertToBook(Map<String, Object> rs) {
        Book book = new Book();
        book.setId((int) rs.get("id"));
        book.setName((String) rs.get("name"));
        book.setAuthor((String) rs.get("author"));
        book.setPublishDate((Timestamp) rs.get("publishDate"));
        book.setComment((String) rs.get("comment"));
        book.setStatus(BookStatus.get((int) rs.get("status")));
        return book;
    }
    @Override
    public void add(final Book book) {      
        this.jdbcTemplate.update("insert books(id, name, author, publishDate, comment, status, rentUserId) values(?, ?, ?, ?, ?, ?, ?)", 
                book.getId(), book.getName(), book.getAuthor(), book.getPublishDate(), book.getComment(), book.getStatus().intValue(), book.getRentUserId());
    }
    @Override
    public Book get(final int id) {
        return this.jdbcTemplate.queryForObject("select id, name, author, publishDate, comment, status, rentUserId from books where id=?", new Object[] { id}, 
                new RowMapper<Book>() {
            @Override
            public Book mapRow(ResultSet rs, int rowNum)
                    throws SQLException {
                return convertToBook(rs);
            }
        });
    }
    @Override
    public List<Book> search(final String name) {
        List<Book> books = new ArrayList<>();
        String query = "select id, name, author, publishDate, comment, status, rentUserId from books where name like '%" + name +"%'";
        List<Map<String, Object>> rows = getJdbcTemplate().queryForList(query);
        for(Map<String, Object> row : rows) {
            books.add(convertToBook(row));
        }
        return books;
    }
    @Override
    public int countAll() {
        return this.jdbcTemplate.queryForInt("select count(*) from books");
    }
    @Override
    public void update(final Book book) {
        this.jdbcTemplate.update("update books set name=?, author=?, publishDate=?, comment=?, status=?, rentUserId=? where id=?",
                book.getName(), book.getAuthor(), book.getPublishDate(), book.getComment(), book.getStatus().intValue(), book.getRentUserId(), book.getId());
    }
    @Override
    public List<Book> getAll() {
        List<Book> books = new ArrayList<>();
        List<Map<String, Object>> rows = getJdbcTemplate().queryForList("select id, name, author, publishDate, comment, status, rentUserId from books");
        for(Map<String, Object> row : rows) {
            books.add(convertToBook(row));
        }
        return books;
    }
    @Override
    public void deleteAll() {
        this.jdbcTemplate.update("delete from books");
    }

    @Override
    public void delete(Book book) {
        this.jdbcTemplate.update("delete from books where id = ?", book.getId());
    }
}


매우 비슷한 코드가 나오게 됨을 알 수 있습니다. 이와 같이 Spring을 사용해서 코드를 만드는 과정 자체는 좋은 코드를 만드는 과정으로 이끌어가게 됩니다. 결론만 나오는 것 같아도, 기본적으로 이러한 과정을 무조건 거치게 만드니까요. 

Spring Jdbc는 다음 method를 주로 사용하게 됩니다. 

# queryForObject : 객체 한개를 return 하는 method. ResultMap<T>를 재정의 해서 사용
# update : insert, update, delete query에서 사용. return값이 존재하지 않고 query를 바로 반영할 때 사용
# queryForList : select에 의해서 List return이 발생할 때 사용. List<Map<String, Object>>가 return 되어, 객체로 변경하거나 map을 읽어서 사용 가능
# queryForInt, queryForLong, queryForDate : query 결과에 따른 값을 return 받을 때 사용.

위 method를 이용해서 UserDao, HistoryDao에 대한 JdbcTemplate 구현 코드를 모두 작성해주세요. 

모든 객체를 구현한다면 다음과 같은 코드 구조를 가지게 될 것입니다. 



각 Dao Impl 들의 테스트 코드 역시 2개씩 존재하게 됩니다. 그리고, 그에 따른 applicationContext.xml 파일은 다음과 같이 구성됩니다. 

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
  <bean id="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor">
    <property name="connectionFactory" ref="connectionFactory" />
  </bean>
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="connectionFactory" class="com.xyzlast.bookstore02.dao.ConnectionFactory"
    init-method="init">
    <property name="connectionString" value="jdbc:mysql://localhost/bookstore" />
    <property name="driverName" value="com.mysql.jdbc.Driver" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost/bookstore" />
    <property name="username" value="root" />
    <property name="password" value="qwer12#$" />
  </bean>
  
  <bean id="bookDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.BookDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="bookDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.BookDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>
  <bean id="userDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.UserDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="userDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.UserDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>  
  <bean id="historyDaoImplWithJdbcTemplate" class="com.xyzlast.bookstore02.dao.HistoryDaoImplWithJdbcTemplate">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
  </bean>
  <bean id="historyDaoImplWithSqlExecutor" class="com.xyzlast.bookstore02.dao.HistoryDaoImplWithSqlExecutor">
    <property name="sqlExecutor" ref="sqlExecutor"/>
  </bean>
  
  <bean id="userService" class="com.xyzlast.bookstore02.services.UserServiceImpl">
    <property name="bookDao" ref="bookDaoImplWithJdbcTemplate"/>
    <property name="userDao" ref="userDaoImplWithJdbcTemplate"/>
    <property name="historyDao" ref="historyDaoImplWithJdbcTemplate"/>
  </bean>
  <bean id="bookService" class="com.xyzlast.bookstore02.services.BookServiceImpl">
    <property name="bookDao" ref="bookDaoImplWithJdbcTemplate"/>
  </bean>
</beans>


applicationContext.xml의 내용이 점점 복잡해지기 시작합니다. 이렇게 복잡해져가는 applicationContext.xml 의 정리 방법을 한번 알아보도록 하겠습니다. 

properties file을 이용한 중복 데이터의 설정파일화

구성된 applicationContext.xml에서 중복된 데이터가 지금 존재합니다. ConnectionFactory와 DataSource가 바로 그것인데요. 중복되는 String일 뿐 아니라, 환경의 구성에 따라 달리 되는 환경상의 설정이기 때문에 applicationContext.xml과는 달리 관리가 되는 것이 좋을 것 같습니다. 따로 spring.properties 파일을 작성하도록 합니다. 파일 내용은 다음과 같습니다. 

connect.driver=com.mysql.jdbc.Driver
connect.url=jdbc:mysql://localhost/bookstore
connect.username=root
connect.password=qwer12#$

그리고, applicationContext 파일에서 namespace 항목에서 context를 추가합니다. 
context 항목을 추가후, applicationContext에 다음 항목을 추가하고 변경하도록 합니다. 



<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
  <context:property-placeholder location="spring.property"/>
  <bean id="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor">
    <property name="connectionFactory" ref="connectionFactory" />
  </bean>
  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="connectionFactory" class="com.xyzlast.bookstore02.dao.ConnectionFactory"
    init-method="init">
    <property name="connectionString" value="${connect.url}" />
    <property name="driverName" value="${connect.driver}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${connect.driver}" />
    <property name="url" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>

${PropertyName} 이라는 표현을 통해서, property 파일내에 있는 속성 값을 applicationContext에 등록하게 되는 것을 알 수 있습니다. 이와 같이 property file은 중복되는 applicationContext의 설정을 통합 관리하거나, 환경이나 build path에 종속적이고 개발자 PC에 종속적인 항목들을 관리할 때 주로 사용됩니다. 그리고 ${PropertyName} 표현은 spring, jsp 등에서 객체에 직접 접근할 때 사용되는 표현법으로 자주 나오게 됩니다. 꼭 익혀두도록 합시다. 


annotation을 이용한 bean의 자동 등록

기존의 bean들의 등록은 applicationContext.xml을 통해서 명시적으로 해주고 있습니다. 그리고, applicationContext를 통해서 객체를 직접 가지고 오는 코드를 구성해서 테스트를 하고 있습니다. 전에 소개되었던 @Autowired를 이용하면 applicationContext.xml을 간소화 시킬 수 있습니다.
먼저, Spring에서 bean들의 종류를 나누는 기준에 대해서 알아보겠습니다. Spring에서 보는 bean의 기준에 따라 적용되는 @Autowired annotation이 달라집니다. 

종류설명
@Component일반적인 Util 객체에 사용됩니다. 지금 구성되는 bookstore에서는 ConnectionFactory가 이에 해당됩니다.
@RepositoryDB에 접근되는 객체에 사용됩니다. 일반적으로 dao 객체에 적용이 됩니다.
@ServiceBusiness Logic이 구성되는 Service 객체에 사용됩니다. Business Logic은 여러개의 @Repository의 구성으로 만들어지게 됩니다. Service에 대해서는 다음에 좀더 깊숙히 들어가보도록 하겠습니다.
@ControllerWeb에서 사용되는 객체입니다. Url Request가 연결되는 객체를 지정할 때 사용됩니다. Spring Controller 때 깊게 들어가보도록 하겠습니다.
@Value위 annotation과는 성격이 조금 다릅니다. 다른 annotation은 객체에 할당이 되는 형태이지만, Value annotation은 property의 값을 지정할 때 사용됩니다. property file의 값을 대응시킬때 사용됩니다.


위의 기준으로 지금까지 만들어진 객체들을 나누면 다음과 같습니다.

종류annotation
ConnectionFactory@Component
SqlExecutor@Component
BookDaoImplWithJdbcTemplate@Repository
BookDaoImplWithSqlExecutor@Repository
UserDaoImplWithJdbcTemplate@Repository
UserDaoImplWithSqlExecutor@Repository
HistoryDaoImplWithJdbcTemplate@Repository
HistoryDaoImplWithSqlExecutor@Repository

객체들의 class 선언부에 각 annotation을 적용하고, 자동 등록될 ConnectionFactory와 SqlExecutor에 모두 @Autowired를 달아주도록 합시다. BookDaoImplWithSqlExecutor의 코드는 다음과 같이 변경됩니다.

@Repository
public class BookDaoImplWithSqlExecutor implements BookDao {
    @Autowired
    private SqlExecutor sqlExecutor;
    
    public SqlExecutor getSqlExecutor() {
        return sqlExecutor;
    }

    public void setSqlExecutor(SqlExecutor sqlExecutor) {
        this.sqlExecutor = sqlExecutor;
    }

    private Book convertToBook(ResultSet rs) throws SQLException {
        Book book = new Book();
        book.setId(rs.getInt("id"));
        book.setName(rs.getString("name"));
        book.setAuthor(rs.getString("author"));
        java.util.Date date = new java.util.Date(rs.getTimestamp("publishDate").getTime());
        book.setPublishDate(date);
        book.setComment(rs.getString("comment"));
        book.setStatus(com.xyzlast.bookstore02.entities.BookStatus.get(rs.getInt("status")));
        int rentUserId = rs.getInt("rentUserId");
        if(rentUserId == 0) {
            book.setRentUserId(null);
        }
        else {
            book.setRentUserId(rentUserId);
        }
        return book;
    }
    
    @Override
    public int countAll() {
        return (int) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select count(*) from books");
                    rs = st.executeQuery();
                    rs.next();
                    return rs.getInt(1);
                }
                catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void add(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("insert books(id, name, author, publishDate, status, comment, rentUserId) values(?, ?, ?, ?, ?, ?, ?)");
                    st.setInt(1, book.getId());
                    st.setString(2, book.getName());
                    st.setString(3, book.getAuthor());
                    st.setTimestamp(4, new Timestamp(book.getPublishDate().getTime()));
                    st.setInt(5, book.getStatus().intValue());
                    st.setString(6, book.getComment());
                    if(book.getRentUserId() == null) {
                        st.setNull(7, Types.INTEGER);
                    }
                    else {
                        st.setInt(7, book.getRentUserId());
                    }
                    st.executeUpdate();
                }
                catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void update(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("update books set name=?, author=?, publishDate=?, status=?, comment=?, rentUserId=? where id=?");
                    st.setString(1, book.getName());
                    st.setString(2, book.getAuthor());
                    st.setTimestamp(3, new Timestamp(book.getPublishDate().getTime()));
                    st.setInt(4, book.getStatus().intValue());
                    st.setString(5, book.getComment());
                    if(book.getRentUserId() == null) {
                        st.setNull(6, Types.INTEGER);
                    }
                    else {
                        st.setInt(6, book.getRentUserId());
                    }
                    st.setInt(7, book.getId());
                    st.executeUpdate();
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void delete(final Book book) {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("delete from books where id = ?");
                    st.setInt(1, book.getId());
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public void deleteAll() {
        sqlExecutor.execute(new ExecuteUpdateQuery() {
            @Override
            public void execute(Connection conn, PreparedStatement st) {
                try {
                    st = conn.prepareStatement("delete from books");
                    st.executeUpdate();
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @Override
    public Book get(final int id) {
        return (Book) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books where id = ?");
                    st.setInt(1, id);
                    rs = st.executeQuery();
                    rs.next();
                    return convertToBook(rs);
                } catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> getAll() {
        return (List<Book>) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books");
                    rs = st.executeQuery();
                    List<Book> books = new ArrayList<>();
                    while(rs.next()) {
                        books.add(convertToBook(rs));
                    }
                    return books;
                }catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) sqlExecutor.execute(new ExecuteSelectQuery() {
            @Override
            public Object execute(Connection conn, PreparedStatement st, ResultSet rs) {
                try {
                    st = conn.prepareStatement("select id, name, author, publishDate, comment, status, rentUserId from books where name like '%" + name + "%'");
                    rs = st.executeQuery();
                    List<Book> books = new ArrayList<>();
                    while(rs.next()) {
                        books.add(convertToBook(rs));
                    }
                    return books;
                }catch(SQLException ex) {
                    throw new IllegalArgumentException(ex);
                }
            }
        });
    }
}

다른 코드들 역시 같이 변경해보도록 합니다. JdbcTemplate을 사용하는 객체 역시 Property의 jdbcTemplate에 @Autowired를 달아주도록 합니다. 

그리고, applicationContext.xml을 다음과 같이 변경하도록 합니다. 

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">

  <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
  </bean>
  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${connect.driver}" />
    <property name="url" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
  </bean>
  <context:property-placeholder location="classpath:spring.property"/>
  <context:component-scan base-package="com.xyzlast.bookstore02.dao"/>
</beans>

지금까지 만들어진 코드가 엄청나게 많이 바뀌게 됩니다. 이렇게 된 후에 다음 테스트 코드를 작성해서 ApplicationContext.xml을 통해 객체가 어떻게 구성이 되었는지 알아보도록 하겠습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContextWithAutowired.xml")
public class ApplicationContextTest {
    @Autowired
    ApplicationContext applicationContext;

    @Test
    public void getBeansInApplicationContext() {
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for(String beanName : beanNames) {
            System.out.println(beanName + " : " + applicationContext.getBean(beanName));
        }
    }
}


jdbcTemplate : org.springframework.jdbc.core.JdbcTemplate@5d63838
dataSource : org.springframework.jdbc.datasource.DriverManagerDataSource@3304e786
org.springframework.context.support.PropertySourcesPlaceholderConfigurer#0 : org.springframework.context.support.PropertySourcesPlaceholderConfigurer@6fc2895
bookDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.BookDaoImplWithJdbcTemplate@14cc51c8
bookDaoImplWithSqlExecutor : com.xyzlast.bookstore02.dao.BookDaoImplWithSqlExecutor@720d2c22
connectionFactory : com.xyzlast.bookstore02.dao.ConnectionFactory@3ecca6ad
historyDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.HistoryDaoImplWithJdbcTemplate@6dd2c810
sqlExecutor : com.xyzlast.bookstore02.dao.SqlExecutor@294ccac4
userDaoImplWithJdbcTemplate : com.xyzlast.bookstore02.dao.UserDaoImplWithJdbcTemplate@70941f0a
userDaoImplWithSqlExecutor : com.xyzlast.bookstore02.dao.UserDaoImplWithSqlExecutor@c820344
org.springframework.context.annotation.internalConfigurationAnnotationProcessor : org.springframework.context.annotation.ConfigurationClassPostProcessor@2ba46bc6
org.springframework.context.annotation.internalAutowiredAnnotationProcessor : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@379faa8c
org.springframework.context.annotation.internalRequiredAnnotationProcessor : org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor@5375e9db
org.springframework.context.annotation.internalCommonAnnotationProcessor : org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@624c53ab
org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor : org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor@2af9150

보시면, context:component-scan을 통해서 package안에 annotation이 된 모든 객체들이 모두 ApplicationContext에 등록된 것을 알 수 있습니다. 

@Autowired는 어떻게 처리가 되는 것일까요? @Autowired는 먼저 @Autowired가 필요 없는 bean들이 먼저 등록이 된 후에 @Autowired로 property를 설정할 수 있는 객체들을 차례로 등록하게  됩니다. 지금 코드에서는 jdbcTemplate와 dataSource는 @Autowired되는 속성이 하나도 없기 때문에 먼저 ApplicationContext에 등록이 되고 나머지 객체들이 등록되고 있는 것을 알 수 있습니다. 그럼 spring은 어떻게 Autowired를 이용해서 bean들을 등록할 수 있을까요? Autowired 동작은 다음 조건 중 선행되는 조건을 확인하고 우선적으로 wired되게 됩니다. 

1. 객체 type이 동일하고, property의 이름이 bean 이름과 동일할 때
2. 객체 type이 동일 할때

여기서 문제가 될 수 있는 항목이 2번째입니다. 객체 type이 동일하지만 bean의 이름이 property랑 맞지 않는 객체가 여러개가 존재를 할 수 있습니다. 이때, Spring은 Not Unique Beans in application context 에러를 발생시키며 applicationContext.xml 로드를 실패하게 됩니다. 여러 객체가 한개의 interface를 구현해서 사용하게 되었을 때, 객체를 Autowired해서 사용하기 위해서는 사용할 객체의 이름을 Spring 규칙에 맞추어 변경한 Property 이름으로 정해줘야지 됩니다. Spring은 기본적으로 객체 이름의 첫자를 소문자로 만들어 bean 이름으로 등록하게 됩니다. 아니면 다른 방법이 있습니다. @Repository, @Component와 같은 Autowird annotation은 생성자로 객체의 자동 등록 이름을 정해줄 수 있습니다. 자동 등록 이름을 이용해서 객체를 등록하고 그 이름에 맞는 Property 이름으로 사용하는 경우 동일한 결과를 가지고 올 수 있습니다. Spring에서 이름을 만드는 규칙과 같이 이를 이해하는 것이 중요합니다. 


Summary

JdbcTemplate을 통한 Dao의 구성방법에 대해서 알아봤습니다. 그리고 applicationContext의 autowired 방법에 대해서 알아봤습니다. applicationContext의 autowired의 사용법은 잘 알아두시길 바랍니다. 객체의 type위주로 autowired되는 것을 명시하고, 그 사용법을 잘 익혀두지 않으면 실제 프로젝트에서 어떻게 사용되는지 알기가 힘들어집니다. 지금까지 구성된 BookDao, UserDao, HistoryDao를 모두 JdbcTemplate으로 구성해보시고, Autowired를 이용해서 applicationContext에 모두 등록시켜주세요. 



Posted by Y2K
,