잊지 않겠습니다.

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


이 장의 제목은 Toby의 Spring Framework에서 붙인 이름을 그대로 표절해봤습니다. 매우 큰 문제를 가진 간단한 프로그램이 뛰어난 확장성과 처음의 너저분한 코드에서 점차 깔끔하게 구성 되어가는 코드로 점차 변경되어가는 것을 볼 수 있을겁니다.
먼저, 간단한 application입니다. bookStore라고 하나의 Project를 만들고, books 라는 table에 대한 CRUD와 count를 하는 application을 간단히 작성해보도록 하겠습니다.
매우 빠르게 코딩을 하면 다음과 같은 매우 문제가 심한 코드를 발견할 수 있을것입니다.
먼저, eclipse 에서 maven을 이용한 project를 생성합니다. simple application은 책방으로, 책을 관리하는 application을 작성하도록 하겠습니다.
작성된 project의 폴더 구조를 보면 pom.xml 파일이 존재합니다. pom.xml의 dependency 항목에 다음 항목을 추가합니다. 다음 항목이 추가 되면 우리 project는 mysql db connection Driver를 사용하게 되며, 버젼은 5.1.22라는 것을 명시하게 됩니다. 이와 같이 pom 파일은 작성된 project가 어떤 library들에 종속성을 갖게 되는지 확인할 수 있는 결과를 담고 있습니다.


    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>


또한, 사용할 junit의 version 정보를 4.11로 수정해줍니다.

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
추가후, pom.xml 파일을 저장하고 dependency가 어떻게 변화되었는지 확인해봅니다. mysql에 관련된 jar가 추가된 것을 알 수 있습니다. 이와 같이 maven을 이용하면, Project에 대한 종속성과 library들을 추가 설정 없이, pom.xml만으로 관리가 가능합니다.
그리고, 본격적인 코딩을 해봅시다.
책은 이름, 저자, 발행일, comment와 db에 저장되기 때문에 integer type의 id를 갖습니다. 이에 대한 java bean object는 다음과 같이 생성될 수 있습니다.


package com.xyzlast.mybook01.entity;
import java.util.Date;
public class Book {
     @Override
     public String toString() {
          return "Book [id=" + id + ", name=" + name + ", author=" + author
                    + ", publishDate=" + publishDate + ", comment=" + comment + "]";
     }
     private int id;
     private String name;
     private String author;
     private Date publishDate;
     private String comment;
    
     public int getId() {
          return id;
     }
     public void setId(int id) {
          this.id = id;
     }
     public String getName() {
          return name;
     }
     public void setName(String name) {
          this.name = name;
     }
     public String getAuthor() {
          return author;
     }
     public void setAuthor(String author) {
          this.author = author;
     }
     public Date getPublishDate() {
          return publishDate;
     }
     public void setPublishDate(Date publishDate) {
          this.publishDate = publishDate;
     }
     public String getComment() {
          return comment;
     }
     public void setComment(String comment) {
          this.comment = comment;
     }
}



그리고, 이 데이터를 저장하기 위한 db를 만들어줍니다. command 창에 들어가서 mysql monitor에 들어가 다음 query를 실행합니다.
create datbase bookstore;
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)
);

자, book에 대한 CRUD를 한다면, 다음 method들이 필요하게 될 것입니다.
void add(Book book);
Book get(int id);
void update(Book book);
void delete(Book book);

여기에 부가적인 코드를 추가하기 위해서 다음 method들을 추가하도록 합니다.
int countAll();
void deleteAll();
List<Book> getAll();
List<Book> search(String name);


이 부분에 대한 코딩을 빠르게 해주시길 바랍니다. package name은 자신의 package name에 domain을 붙여서 넣어주시면 됩니다.
public class BookApp {
     public void add(Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          String url = "jdbc:mysql://localhost/bookstore";
          Class.forName("com.mysql.jdbc.Driver").newInstance();
          Connection conn = DriverManager.getConnection (url, "root", "qwer12#$");
         
          PreparedStatement st = conn.prepareStatement("insert books(name, author, publishDate, comment) values(?, ?, ?, ?)");
          st.setString(1, book.getName());
          st.setString(2, book.getAuthor());
          java.sql.Date sqlDate = new java.sql.Date(book.getPublishDate().getTime());
          st.setDate(3, sqlDate);
          st.setString(4, book.getComment());
          st.execute();
         
          st.close();
          conn.close();
     }
    
     public Book get(int id) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          String url = "jdbc:mysql://localhost/bookstore";
          Class.forName("com.mysql.jdbc.Driver").newInstance();
          Connection conn = DriverManager.getConnection (url, "root", "qwer12#$");
         
          PreparedStatement st = conn.prepareStatement("select id, name, author, publishDate, comment from books where id=?");
          st.setInt(1, id);
          ResultSet rs = st.executeQuery();
          rs.next();
         
          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.getDate("publishDate").getTime());
          book.setPublishDate(date);
          book.setComment(rs.getString("comment"));
         
          rs.close();
          st.close();
          conn.close();
         
          return book;
     }
    
     public List<Book> search(String name) {
          return null;
     }
    
     public int countAll() {
          return 0;
     }
    
     public Book update(Book book) {
          return null;
     }
    
     public List<Book> getAll() {
          return null;
     }
    
     public void deleteAll() {
         
     }
}

코딩을 빨리 해주시고 나면, 이 코드들을 보고 다음 질문에 답해주세요.

1. 정상적인 코드인지 확인해볼 수 있나요?
2. 중복된 코드가 존재하나요?
3. connection, statement, ResultSet 에 대한 반환은 이루어지고 있나요?

이중 하나라도 걸리게 된다면, 이 코드는 좋은 코드라고는 할 수 없습니다. 좋은 코드보다도 일단 정상적인 코드가 아닙니다. 
먼저, 이 코드가 정상적으로 돌아가는지를 확인하기 위해서 main method를 추가해서 동작을 확인해보도록 합니다.


Test code의 기초 - 코드의 동작을 확인하는 방법

코드의 동작을 확인하기 위해서 main 함수를 추가하도록 합니다. main 함수는 user를 생성해서 add method를 통해서 insert후, getAll() method를 통해서 입력된 모든 정보들이 정상적으로 입력이 되었는지를 확인하돌고 합니다. main 함수의 코드는 다음과 같습니다.


     public static void main(String[] args)
               throws InstantiationException,
                      IllegalAccessException,
                      ClassNotFoundException,
                      SQLException {
          System.out.println("start main app");
         
          Book book = new Book();
          book.setName("Spring 3.1");
          book.setAuthor("작가");
          book.setPublishDate(new java.util.Date());
          book.setComment("좋은 책입니다.");
         
          BookApp app = new BookApp();
          app.add(book);
         
          List<Book> books = app.getAll();
          for(Book b : books) {
               System.out.println(b);
          }
         
          return;
     }

위 코드를 실행시키면 다음과 같은 결과를 Console 창에서 확인 가능합니다.


start main app
Book [id=1, name=bookName01, author=author1, publishDate=Sat Jan 26 00:00:00 KST 2013, comment=null]
Book [id=4, name=Spring 3.1, author=작가, publishDate=Sat Jan 26 00:00:00 KST 2013, comment=좋은 책입니다.]

 이 방법이 좋은 것일까요? 이런식으로 개발하는 사람이 꽤나 많습니다. 이 방법의 문제가 어떤 것이 있을까요?

올바른 테스트란, 사람이 개입되어서는 안됩니다. 사람이 눈으로 테스트를 확인하는 경우는, 그것을 보지 않는다면 또는 테스트가 너무나 많아져서 테스트의 정보를 확인할 수 없다면 완전히 무용지물이 되어버리고 맙니다. 그리고, 개발을 해보시면 좀더 느끼시지만 하나를 만드니 다른쪽에서 에러가 발생할 수도 있습니다. 마지막으로 CI 환경에서 자동화된 build를 지원할 수가 없습니다.

따라서, 테스트는 확인 가능하나 완전 자동적인 코드로서 구성이 되어야지 됩니다. 테스트는 크게 2가지로 나눌 수 있습니다. 전에 CI에 대해서 간략하게 설명을 할 때, QA 부서에서 담당하기 전 단계라고 할 수 있는 단위 테스트(Unit TEST)와 QA 부서에서 진행하는 통합 테스트가 존재합니다. 잘 만들어진 개발 조직에서, 개발자는 자신의 단위 테스트를 유지하고 관리할 의무를 가지고 있습니다. 그리고 자신의 코드가 단위적으로는 에러가 발생하지 않는다는 결과를 보여줄 수 있는 방법이 있어야지 됩니다. 이를 위해 java에서는 junit이라는 테스트 도구를 배포하고 있고, 이 테스트 코드를 사용하면 자동화 되고, 확인이 가능한 코드로서 사용할 수가 있습니다.

junit은 version 3.x 와 4.x간의 큰 차이를 가지고 있습니다. 기존 3.x대에서는 TestCase class를 상속받아서 처리를 했으나, 지금은 @Test annotation을 이용하는 것만으로 쉽게 테스트 코드를 작성할 수 있습니다.

junit을 사용할 때 기억할 annotation 목록입니다.

1. @Test : Test method를 지정할 때 사용합니다. Test method는 반드시 public에 return type은 void, input 값은 하나도 없는 형태여야지 됩니다.
2. @Before : Test method를 시작하기 전에 반드시 실행될 method입니다.
3. @After : Test method를 수행 후, 실행될 method입니다.
4. @BeforeClass : 전체 테스트 코드 method가 수행되기 전에 실행됩니다. 반드시 public, static type의 method여야지 됩니다.
5. @AfterClass : 전체 테스트 코드 method가 수행된 후에 실행됩니다. 반드시 public, static type의 method여야지 됩니다.

테스트 코드의 annotation을 모두 사용한 테스트 코드입니다. 한번 내용을 확인해보도록 하겠습니다.

public class AppTest {
     @BeforeClass
     public static void beforeClass() {
          System.out.println("#1. BeforeClass");
     }
    
     @AfterClass
     public static void afterClass() {
          System.out.println("#6. AfterClass");
     }
    
     @Before
     public void before() {
          System.out.println("#2. Before");
     }
    
     @After
     public void after() {
          System.out.println("#3. After");
     }
    
     @Test
     public void test01() {
          System.out.println("#4. Test01");
     }
    
     @Test
     public void test02() {
          System.out.println("#5. Test02");
     }
    
     @Test
     public void test03() {
          System.out.println("#6. Test03");
     }
}

output :

#1. BeforeClass
#2. Before
#4. Test01
#3. After
#2. Before
#5. Test02
#3. After
#2. Before
#6. Test03
#3. After
#6. AfterClass

자. 지금까지 구성된 application 의 전체 테스트 코드를 작성해보도록 합니다. 모든 method가 다 테스트가 되어야지 되며, 테스트에 대한 내용은 자동으로 확인이 될 수 있도록, 예측되는 값들을 assert 문으로 확인이 가능해야지 됩니다.
테스트 코드를 작성해보고 그 결과를 한번 확인해보도록 하겠습니다.

public class BookAppTest {
    private BookApp bookApp;

    private List<Book> getBooks() {
        Book book1 = new Book();
        book1.setId(1);
        book1.setName("book name01");
        book1.setAuthor("autor name 01");
        book1.setComment("comment01");
        book1.setPublishDate(new Date());

        Book book2 = new Book();
        book2.setId(2);
        book2.setName("book name02");
        book2.setAuthor("autor name 02");
        book2.setComment("comment02");
        book2.setPublishDate(new Date());

        Book book3 = new Book();
        book3.setId(3);
        book3.setName("book name03");
        book3.setAuthor("autor name 03");
        book3.setComment("comment03");
        book3.setPublishDate(new Date());

        List<Book> books = Arrays.asList(book1, book2, book3);
        return books;
    }

    private void compareBook(Book book) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Book dbBook = bookApp.get(book.getId());
        assertThat(dbBook.getName(), is(book.getName()));
        assertThat(dbBook.getAuthor(), is(book.getAuthor()));
        assertThat(dbBook.getComment(), is(book.getComment()));
        assertThat(dbBook.getPublishDate().toString(), is(book.getPublishDate().toString()));
    }

    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        bookApp = new BookApp();
    }

    @Test
    public void addAndCount() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        List<Book> books = getBooks();
        int count = 0;
        for(Book book : books) {
            bookApp.add(book);
            count++;
            assertThat(bookApp.countAll(), is(count));
        }
    }

    @Test
    public void update() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        List<Book> books = getBooks();
        int count = 0;
        for(Book book : books) {
            bookApp.add(book);
            count++;
            assertThat(bookApp.countAll(), is(count));

            book.setName("changed name");
            book.setPublishDate(new Date());
            book.setAuthor("changed author");
            bookApp.update(book);

            compareBook(book);
        }
    }

    @Test
    public void getAll() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        List<Book> books = getBooks();
        int count = 0;
        for(Book book : books) {
            bookApp.add(book);
            count++;
            assertThat(bookApp.countAll(), is(count));
        }

        List<Book> books2 = bookApp.getAll();
        assertThat(books2.size(), is(books.size()));
    }

    @Test
    public void search() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        List<Book> books = getBooks();
        int count = 0;
        for(Book book : books) {
            bookApp.add(book);
            count++;
            assertThat(bookApp.countAll(), is(count));
        }

        List<Book> searchedBooks = bookApp.search("01");
        assertThat(searchedBooks.size(), is(1));

        searchedBooks = bookApp.search("02");
        assertThat(searchedBooks.size(), is(1));

        searchedBooks = bookApp.search("03");
        assertThat(searchedBooks.size(), is(1));

        searchedBooks = bookApp.search("name");
        assertThat(searchedBooks.size(), is(3));
    }
}


자신이 만든 테스트 코드에 대한 확인을 하는 과정 역시 중요합니다. 테스트에 대한 확인 방법은 테스트가 정상적으로 마쳐지는지를 확인하고, code coverage를 통해서, test를 통해 자신의 코드가 얼마나 잘 테스트가 이루어졌는지를 확인하는 것이 필요합니다.  code coverage는 eclipse의 EclEmma를 통해서 확인 가능합니다. 


중복된 코드의 제거

다음은 중복된 코드의 제거입니다. 중복된 코드를 제거하는 것은 굉장히 중요한 일입니다. 만약에 프로그램 상에 오류가 발생하거나, 로직이 바뀌게 된다면 그 부분에 대하여 전 코드를 다 바꿔주야지 되는 상황이 발생합니다. 그렇지만, 중복된 코드를 하나의 코드로 작성해준다면, 한 method 또는 code block만 변경을 시키면 그 변경사항을 다른 코드에서도 사용할 수 있기때문에 최소한의 변경으로 원하는 결과를 얻어낼 수 있습니다.
중복되는 코드를 하나만 생각을 해서는 안됩니다. 이 코드가 같은 일을 하는 객체가 또 있다면 어떻게 되는걸까. 라는 질문을 자신에게 해봐야지 됩니다. 지금 있는 객체중에서 가장 문제가 될 수 있는 중복 영역에 대해서 알아보도록 하겠습니다.
만들어진 코드의 중복된 코드는 명확합니다. connection을 얻어내는 곳과 ResultSet을 이용한 Book bean과의 convert 영역이지요.

connection을 얻는 코드와 Book으로 변환하는 코드를 다음과 같이 변경해서 공통 method로 뽑아냅니다. 

     private Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          String url = "jdbc:mysql://localhost/bookstore";
          Class.forName("com.mysql.jdbc.Driver").newInstance();
          Connection conn = DriverManager.getConnection (url, "root", "qwer12#$");
         
          return conn;
     }
    
     private Book convertToBook(ResultSet rs) throws SQLException {
          Book book = new Book();
          book.setId(rs.getInt(1));
          book.setName(rs.getString(2));
          book.setAuthor(rs.getString(3));
          java.util.Date date = new java.util.Date(rs.getTimestamp(4).getTime());
          book.setPublishDate(date);
          book.setComment(rs.getString(5));
         
          return book;
     }


connection, statement, ResultSet에 대한 반환

지금까지 작성한 코드는 실은 엄청난 버그를 하나 가지고 있습니다. RDBMS에 connection을 맺고, sql query를 실행하고, 그 결과를 얻어내는 것은 프로그램 영역에서는 IO 접근과 동일합니다. 이건 파일을 접근하는 것과 동일한 상황이지요. 따라서, 모든 Programming Language에서는 IO에 대한 자원을 매우 소중히 여기고 있습니다. 이 IO에 대한 자원 해재는 추후에 엄청난 사태를 불러올 수 있기 때문에 명확히 해줘야지 되는 문제입니다. 지금 개발 된 코드에서 get(int id) method를 기준으로 자원 해재를 시켜주면 다음과 같습니다.


    public Book get(int id) throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Connection conn = getConnection();
          PreparedStatement st = null;
          ResultSet rs = null;
         
          try {
               st = conn.prepareStatement("select id, name, author, publishDate, comment from books where id=?");
               st.setInt(1, id);
               rs = st.executeQuery();
               rs.next();
               return convertToBook(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) {}
               }
          }
     }


여기까지 작성된 application을 보면 또 다른 코드의 중복이 나타나게 됩니다. 역시 자원을 얻고, 해재하는 과정 자체가 코드의 중복으로 나타나게 되는데요. 이를 해결하는 방법이 무엇일까에 대해서 고민을 해봐야지 됩니다. 여기에서 중복되는 코드는 connection을 얻고, 얻은 connection에 대하여 해재를 하는 상단과 하단은 완전히 동일한 코드가 나타나게 됩니다. 그렇지만, 중간의 preparedStatement와 ResultSet을 이용하는 부분은 각각 코드들마다 다른 모습을 보이게 되는데요. 이 부분을 어떻게 하면 해결해줄지를 한번 고민해보도록 합시다.

만약에 code block을 넣어주면 해결될수 있지 않을까? 라는 고민을 하셨으면 정답이라고 할 수 있습니다. 

이런 code block을 callback이라는 용어로 사용하고, callback을 호출하는 구문을 template이라고 합니다. 이유는 template에서 call이 되고, template의 code로 back 하는 구조를 가진 code pattern을 template-callback pattern이라고 합니다.
callback을 만들때는 interface로 만드는 것이 일반적이고, 대부분 inner interface로 구성되는 경우가 많습니다. 

지금 만든 BookApp의 Template 적 요소는 2가지로 볼 수 있습니다. return값을 갖는 경우와 return값을 갖지 않는 경우로 나눌수 있습니다. 그리고, 이 두가지 경우는 input값이 ResultSet을 갖는 method와 ResultSet을 갖지 않는 method로 구분할 수 있습니다. 이 두가지 경우를 각각 나눈 Template code와 Template code의 인자가 되는 callback은 다음과 같습니다. 

    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) {}
            }
        }
    }

callback interface를 2개를 만들고, template을 역시 2개 작성하였습니다. 


위 3가지 요소를 모두 반영한 BookApp의 전체 코드는 다음과 같습니다. 자신이 직접 코드를 만들어서 과정을 따라가보시길 바랍니다. 

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;

import com.xyzlast.bookstore01.entities.Book;

public class BookApp {
    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"));

        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) 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());
                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=? where id=?");
                st.setInt(5, 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());
                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;
    }
}

지금까지 간단한 RDBMS에 대한 CRUD 코드에 대해서 조금 깊게 들어가봤습니다. 
다음은 Spring을 통해서 이러한 잘못된 코드를 어떻게 해서 좋은 코드로 수정을 하게 되는지에 대한 과정을 알아보도록 하겠습니다. 


Posted by Y2K
,