* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.
하나의 Action이라는 것은 Business Logic이라고 할 수 있고, 한 Table에 대한 CRUD는 DB에 종속적인 작업이라고 할 수 있습니다.
전자를 일반적으로 Service라고 지칭하고, 후자를 DAO (Data Access Object)라고 지칭하는 것이 일반적입니다.
bookstore에 Business Logic(BL)을 추가하기 전에 book의 상태를 서술할수 있는 property와 현재 책을 빌려간 사용자의 정보를 저장할 수 있는 Column을 두개 추가하고, users table을 추가해서 Dao를 좀더 구성해보도록 하겠습니다.
책의 상태를 나타낼 수 있는 state property를 추가합니다.
create table books ( id Integer AUTO_INCREMENT PRIMARY KEY, name varchar(255) NOT NULL, author varchar(50) NOT NULL, publishDate timestamp NOT NULL, comment varchar(255), status Integer NOT NULL, rentUserId Integer ); ALTER TABLE bookstore.books ADD status Integer NOT NULL; ALTER TABLE bookstore.books ADD rentUserId Integer;
public enum BookStatus { CanRent(0), RentNow(1), Missing(2); private int value; private BookStatus(int value) { this.value = value; } public int intValue() { return this.value; } public static BookStatus valueOf(int value) { switch(value) { case 0 : return CanRent; case 1 : return RentNow; case 2 : return Missing; default: throw new IllegalArgumentException(); } } }
public class BookDao { private ConnectionFactory connectionFactory; interface ExecuteUpdateQuery { PreparedStatement getPreparedStatement(Connection conn) throws SQLException; } interface ExecuteSelectQuery { PreparedStatement getPreparedStatement(Connection conn) throws SQLException; Object parsetResultSet(ResultSet rs) throws SQLException; } private void execute(ExecuteUpdateQuery query) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { Connection conn = this.connectionFactory.getConnection(); PreparedStatement st = null; try { st = query.getPreparedStatement(conn); st.executeUpdate(); } finally { if(st != null) { try { st.close(); }catch(Exception ex) {} } if(conn != null) { try { conn.close(); }catch(Exception ex) {} } } } private Object execute(ExecuteSelectQuery query) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { Connection conn = this.connectionFactory.getConnection(); PreparedStatement st = null; ResultSet rs = null; try { st = query.getPreparedStatement(conn); rs = st.executeQuery(); return query.parsetResultSet(rs); } finally { if(rs != null) { try { rs.close(); } catch(Exception ex) {} } if(st != null) { try { st.close(); }catch(Exception ex) {} } if(conn != null) { try { conn.close(); }catch(Exception ex) {} } } } 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.valueOf(rs.getInt("status"))); return book; } public void add(final Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { execute(new ExecuteUpdateQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { PreparedStatement st = conn.prepareStatement("insert books(id, name, author, publishDate, comment, status) values(?, ?, ?, ?, ?, ?)"); st.setInt(1, book.getId()); st.setString(2, book.getName()); st.setString(3, book.getAuthor()); java.sql.Date sqlDate = new java.sql.Date(book.getPublishDate().getTime()); st.setDate(4, sqlDate); st.setString(5, book.getComment()); st.setInt(6, book.getStatus().intValue()); return st; } }); } public Book get(final int id) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { return (Book) execute(new ExecuteSelectQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { PreparedStatement st = conn.prepareStatement("select id, name, author, publishDate, comment from books where id=?"); st.setInt(1, id); return st; } @Override public Object parsetResultSet(ResultSet rs) throws SQLException { rs.next(); Book book = convertToBook(rs); return book; } }); } @SuppressWarnings("unchecked") public List<Book> search(final String name) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { return (List<Book>) execute(new ExecuteSelectQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { String query = "select id, name, author, publishDate, comment from books where name like '%" + name +"%'"; return conn.prepareStatement(query); } @Override public Object parsetResultSet(ResultSet rs) throws SQLException { List<Book> books = new ArrayList<>(); while(rs.next()) { books.add(convertToBook(rs)); } return books; } }); } public int countAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { return (Integer) execute(new ExecuteSelectQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { return conn.prepareStatement("select count(*) from books"); } @Override public Object parsetResultSet(ResultSet rs) throws SQLException { rs.next(); return rs.getInt(1); } }); } public void update(final Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { execute(new ExecuteUpdateQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { PreparedStatement st = conn.prepareStatement("update books set name=?, author=?, publishDate=?, comment=?, status=? where id=?"); st.setInt(6, book.getId()); st.setString(1, book.getName()); st.setString(2, book.getAuthor()); st.setTimestamp(3, new Timestamp(book.getPublishDate().getTime())); st.setString(4, book.getComment()); st.setInt(5, book.getStatus().intValue()); return st; } }); } @SuppressWarnings("unchecked") public List<Book> getAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { return (List<Book>) execute(new ExecuteSelectQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { return conn.prepareStatement("select id, name, author, publishDate, comment from books"); } @Override public Object parsetResultSet(ResultSet rs) throws SQLException { List<Book> books = new ArrayList<>(); while(rs.next()) { books.add(convertToBook(rs)); } return books; } }); } public void deleteAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { execute(new ExecuteUpdateQuery() { @Override public PreparedStatement getPreparedStatement(Connection conn) throws SQLException { return conn.prepareStatement("delete from books"); } }); } public ConnectionFactory getConnectionFactory() { return connectionFactory; } public void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } }
create table users ( id Integer AUTO_INCREMENT PRIMARY KEY, name varchar(50) NOT NULL, password varchar(12) NOT NULL, point Integer NOT NULL, level Integer NOT NULL ); ALTER TABLE bookstore.books ADD CONSTRAINT books_users_FK FOREIGN KEY (rentUserId) REFERENCES bookstore.users(id) ON DELETE SET NULL;
level에 대한 enum은 NORMAL, READER, MVP로 만들어주세요.
create table histories ( id Integer AUTO_INCREMENT PRIMARY KEY, userId Integer NOT NULL, bookId Integer NOT NULL, actionType Integer NOT NULL, insertDate timestamp NOT NULL ); ALTER TABLE bookstore.histories ADD CONSTRAINT history_userFK FOREIGN KEY (userId) REFERENCES bookstore.users(id); ALTER TABLE bookstore.histories ADD CONSTRAINT history_bookFK FOREIGN KEY (bookId) REFERENCES bookstore.books(id);
public class UserDao { public User get(int userId) { } public void deleteAll() { } public int countAll() { } public void add(User user) { } public void update(User user) { } public List<User> getAll() { } }
public class HistoryDao { public void deleteAll() { } public void add(History history) { } public int countAll() { } public List<History> getAll() { } public List<History> getByUser(int userId) { } public List<History> getByBook(int bookId) { } }
public class SqlExecutor { private ConnectionFactory connectionFactory; public ConnectionFactory getConnectionFactory() { return this.connectionFactory; } public void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public void execute(ExecuteUpdateQuery query) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { Connection conn = this.connectionFactory.getConnection(); PreparedStatement st = null; try { st = query.getPreparedStatement(conn); st.executeUpdate(); } finally { if(st != null) { try { st.close(); }catch(Exception ex) {} } if(conn != null) { try { conn.close(); }catch(Exception ex) {} } } } public Object execute(ExecuteSelectQuery query) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { Connection conn = this.connectionFactory.getConnection(); PreparedStatement st = null; ResultSet rs = null; try { st = query.getPreparedStatement(conn); rs = st.executeQuery(); return query.parsetResultSet(rs); } finally { if(rs != null) { try { rs.close(); } catch(Exception ex) {} } if(st != null) { try { st.close(); }catch(Exception ex) {} } if(conn != null) { try { conn.close(); }catch(Exception ex) {} } } } }
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <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="sqlExecutor" class="com.xyzlast.bookstore02.dao.SqlExecutor"> <property name="connectionFactory" ref="connectionFactory"/> </bean> <bean id="bookDao" class="com.xyzlast.bookstore02.dao.BookDao"> <property name="sqlExecutor" ref="sqlExecutor"/> </bean> <bean id="userDao" class="com.xyzlast.bookstore02.dao.UserDao"> <property name="sqlExecutor" ref="sqlExecutor"/> </bean> <bean id="historyDao" class="com.xyzlast.bookstore02.dao.HistoryDao"> <property name="sqlExecutor" ref="sqlExecutor"/> </bean></beans>
딱히 문제가 특출나게 보이지는 않습니다. 지금까지 상당한 refactoring을 통해서 변경시킨 코드에 문제가 쉽게 보이면 그것 역시 문제가 될 수 있습니다.
지금 모든 코드에 나타나 있는 Exception이 선언되어 있습니다. DB access 코드에 일괄적으로 들어가 있는 Exception들은 다음과 같습니다.
# InstantiationException : Class.forName 에서 객체의 이름이 아닌, interface의 이름이 들어간 경우에 발생하는 에러.
# IllegalAccessException : Db Connection시, 권한이 없거나 id/password가 틀린 경우에 발생하는 에러
# ClassNotFoundException : Class.forName 을 이용, DB Connection 객체를 생성할 때 객체의 이름이 틀린 경우에 발생하는 에러
# SQLException : SQL query가 잘못된 Exception입니다.
Runtime exception은 상위에서 처리를 안해줘도 되고요. 대표적인 것은 NullPointerException, UnsupportedOperationException, IllegalArgumentException 등이 있습니다.
이 부분은 매우 중요한 개념입니다. java에서의 exception은 사용자가 처리해줘야지 될 것(체크 해야지 될 exception)과 Runtime 시(실행시에) 확인되어야지 될 것들로 나뉘게 됩니다. exception에 대하여 보다더 확실한 처리를 해주길 바란 java의 설계 원칙이지만, 근간에는 비판이 좀 많은 부분이기도 합니다. java의 exception에 대한 정리를 한번 해주시는 것이 필요합니다.
public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException { Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password); return conn; }
public Connection getConnection() { Connection conn = null; try { conn = DriverManager.getConnection (this.connectionString, this.username, this.password); } catch (SQLException e) { throw new IllegalArgumentException(e); } return conn; } public void init() { try { Class.forName(this.driverName).newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { throw new IllegalArgumentException(e); } }