잊지 않겠습니다.

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


코드 정리 - 객체간의 중복 코드 정리

지금까지 우리는 books라는 한개의 table에 CRUD action을 진행해왔습니다. 그렇지만, 개발에서는 한개의 Table만에 CRUD를 하는 경우보다, 한 Action에 대하여 여러개의 Table을 CRUD 하게 되는 것이 일반적입니다. 

그래서, 이 두개의 개념을 나누게 되는데요.
하나의 Action이라는 것은 Business Logic이라고 할 수 있고, 한 Table에 대한 CRUD는 DB에 종속적인 작업이라고 할 수 있습니다.
전자를 일반적으로 Service라고 지칭하고, 후자를 DAO (Data Access Object)라고 지칭하는 것이 일반적입니다. 

이는 java에서 개발중에 package로 나누게 되는 것일 일반적입니다. dao package와 service package를 나누것과 같이 이런 식으로 나눠주는 것이 좋습니다.

1개의 Service는 여러개의 DAO를 가지고 있고, DAO를 이용한 BL을 서술하는 것이 일반적입니다. DAO는 최대한 단순하게 Table에 대한 Query들로 구성이 되고, 그 Query를 통해서 결과를 얻어내는 수단으로 주로 사용됩니다.
bookstore에 Business Logic(BL)을 추가하기 전에 book의 상태를 서술할수 있는 property와 현재 책을 빌려간 사용자의 정보를 저장할 수 있는 Column을 두개 추가하고, users table을 추가해서 Dao를 좀더 구성해보도록 하겠습니다. 

먼저 books table에 대한 작업부터 진행하도록 하겠습니다.
책의 상태를 나타낼 수 있는 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;


int type으로 status의 값을 0은 bookstore에 있는 대여 가능, 1은 대여중, 2는 분실 상태로 만들어줍니다. 그런데, 이런 식의 status code는 따로 code table을 만들어주거나 code 상의 enum 값을 이용해서 명확하게 구분하는 것이 훨씬 코드 관리에 용의합니다.  enum의 code는 다음과 같습니다.

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

불분명한 숫자값에 명확한 의미의 enum을 적용함으로서, 코드를 보다더 보기 편하게 만들어줬습니다. 이제 BookStatus를 Book에 추가하도록 하겠습니다.
또한, rentUserId column이 nullable인것에 주목해주세요. 이 부분을 반영하기 위한 Entity는 어떻게 작성해야지 될까요?

마지막으로 기존의 BookApp은 BookDao의 의미가 더 강하기 때문에, package의 이름을 dao로 변경하고, BookApp을 BookDao로 변경하도록 하겠습니다. 전체 코드에 대한 손을 좀 봐야지 됩니다. 한번 고쳐보시길 바랍니다.
코드의 수정이 모두 완료되었으면 테스트 코드를 통해서 코드가 정상적으로 수정이 된 것을 확인해야지 됩니다. 

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

이제 사용자를 추가해보도록 하겠습니다. users는 다음과 같습니다. 그리고, books와 users 간의 FK를 잡아주도록 합시다.

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;

간단히 이름과 비밀번호를 가진 너무나 단순한 키입니다. 역시 마찬가지로 User의 level에 대한 enum값을 만들고, User에 대한 entity를 정의하고, BookDao와 동일한 method들을 모두 만들어주겠습니다.
level에 대한 enum은 NORMAL, READER, MVP로 만들어주세요.

마지막으로 histories Table을 추가합니다. table query는 다음과 같고, users와 books에 FK를 갖습니다.
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);
 
users와 histories에 대한 모든 dao class를 만들어주세요.  그리고 Histoary의 ActionType은 RENT/RETURN을 각각 0, 1로 잡는 enum으로 구성해주면 전체 table의 구성이 모두 마쳐집니다. 
여기까지 구성을 해보는 것이 이번주 과제입니다. 조금 코드양이 많을것 같네요.  각각의 Dao class의 method 정의는 다음과 같습니다.  이 두개의 Dao 객체를 모두 작성하고, Test code를 작성해주세요.

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




Dao를 구현하면 또 다른 중복 코드가 발견되는 것을 알 수 있습니다. 지금까지 구현했던 Template-callback 코드가 계속해서 나타나게 됩니다. 이 부분에 대한 중복 코드를 제거하는 것이 필요합니다. SQL에 대한 직접적인 실행을 하는 객체로 SqlExecutor로 객체를 새로 생성하고, SqlExecutor를 외부에서 DI 할 수 있도록 코드를 수정하도록 합시다. 
SqlExecutor의 코드는 다음과 같습니다. 

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

지금까지 구현된 모든 Dao를 SqlExecutor를 사용하도록 코드를 작성하고, spring을 사용하도록 DI를 구성한 테스트 코드를 작성해서 검증하도록 합시다.

과제가 나타났습니다!!!! 


<?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>



코드 정리 - Exception의 처리

지금까지 보시면 SqlExecutor를 이용한 코드의 간결화 및 spring을 이용한 전체 코드의 최적화를 해온것을 알 수 있습니다. 

마지막으로, 지금 저희 코드에 중복이 되는 코드를 찾아보도록 합시다. 
딱히 문제가 특출나게 보이지는 않습니다. 지금까지 상당한 refactoring을 통해서 변경시킨 코드에 문제가 쉽게 보이면 그것 역시 문제가 될 수 있습니다.

지금 모든 코드에 나타나 있는 Exception이 선언되어 있습니다. DB access 코드에 일괄적으로 들어가 있는 Exception들은 다음과 같습니다.

# InstantiationException : Class.forName 에서 객체의 이름이 아닌, interface의 이름이 들어간 경우에 발생하는 에러.
# IllegalAccessException : Db Connection시, 권한이 없거나 id/password가 틀린 경우에 발생하는 에러
# ClassNotFoundException : Class.forName 을 이용, DB Connection 객체를 생성할 때 객체의 이름이 틀린 경우에 발생하는 에러
# SQLException : SQL query가 잘못된 Exception입니다. 

Java는 2개의 Exception type을 가지고 있습니다. checked exception과 Runtime Exception인데요. checked exception의 경우, 이 exception이 발생하는 경우에는 반드시 exception을 처리해줘야지 됩니다. 또는 상위 method로 throw를 해줘야지 됩니다.
Runtime exception은 상위에서 처리를 안해줘도 되고요. 대표적인 것은 NullPointerException, UnsupportedOperationException, IllegalArgumentException 등이 있습니다.

이 부분은 매우 중요한 개념입니다. java에서의 exception은 사용자가 처리해줘야지 될 것(체크 해야지 될 exception)과 Runtime 시(실행시에) 확인되어야지 될 것들로 나뉘게 됩니다. exception에 대하여 보다더 확실한 처리를 해주길 바란 java의 설계 원칙이지만, 근간에는 비판이 좀 많은 부분이기도 합니다. java의 exception에 대한 정리를 한번 해주시는 것이 필요합니다. 

그럼, 지금 저희가 사용한 코드중에서 getConnection() method를 확인해보겠습니다. 

    public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
        return conn;
    }


이 method는 이 4개의 exception을 반드시 처리하도록 되어 있습니다. 또는 이 4개의 exception을 사용한 method로 던져줘서 상위 method에서 처리하도록 되어 있는데요. DB 접속과 SQL query가 잘못된 경우에 대한 exception 처리는 과연 할 수 있을까요? 이 Exception은 처리할 수 없는 Exception을 넘겨줘서 되는 것이 아닌가. 라는 생각을 할 수 있습니다. 잘 보면 대부분의 DB 접속시에 나오는 대부분의 Exception은 처리가 불가능한 Exception이라고 할 수 있습니다. 에러의 내용은 도움이 될 수 있지만, 이 에러가 발생했을 때 어떠한 처리를 하지를 못하는 경우가 대다수라는거지요. 그래서 SQL Exception들을 다음과 같이 처리해줘도 괜찮습니다. 

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


모든 Exception들은 초기 값들. id, password, connectionString, driver name 등이 잘못된 것이기 때문에 input 값이 잘못되었다는 의미의 IllegalArgumentException으로 변경해서 던져줬습니다. IllegalArgumentException은 Runtime Exception 이기 때문에 처리를 해줄 필요가 없습니다. 이렇게 Dao 객체들의 method를 다시 처리해주면 좀더 코드가 깔끔해지는 것을 알 수 있습니다. 




Posted by Y2K
,

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


지난 장에서는 JDBC를 이용한 db의 접근. 그리고 간단한 table의 CRUD를 하는 방법에 대해서 조금 깊게 들어가봤습니다. 또한 저번 시간의 최대 포인트는 테스트입니다. 테스트를 어떻게 작성을 하는지에 대한 논의와 테스트 코드를 직접 사용해보는 시간을 가져봤습니다. 이번 시간에는 드디어 Spring을 이용한 코드에 대해서 논의해보도록 하겠습니다.기존 코드의 가장 큰 문제는 무엇인가요?

1.  DB connection이 변경되는 경우, compile을 다시 해줘야지 된다.
2.  DB connection의 open/close가 모든 method에서 반복된다.

크게 보면 이 두가지의 문제가 나오게 됩니다. 

Connection은 외부에서 설정할 수 있는 영역입니다. 특히 이런 부분은 config file로 관리가 되는 것이 일반적이고, Enterprise 구성환경에서는 JNDI를 이용한 DB Connection이 제공되는 경우도 많습니다. 따라서, 외부에서 설정이 가능한 것이라고 생각해도 좋습니다. 또한, BookApp은 books table에 CRUD를 하는 것을 목적으로 하는 객체입니다. 객체에 대한 가장 큰 원칙인 단일 책임의 원칙에 의해서, BookApp에서 Connection까지 관리가 되는 것은 영역을 넘어가게 됩니다. 또한, books 이외의 table이 존재할 때, Connection에 대한 코드는 중복될 수 밖에 없는 코드가 됩니다. 따라서 Connection을 제공하는 객체로 따로 분리를 해줍시다. 

Connection을 관리, 생성하는 객체이기 때문에 ConnectionFactory라고 명명하고, 객체를 구성합니다. 객체의 코드는 다음과 같습니다.

public class ConnectionFactory {
     private String connectionString;
     private String driverName;
     private String username;
     private String password;
    
     public Connection getConnection() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          Class.forName(this.driverName).newInstance();
          Connection conn = DriverManager.getConnection (this.connectionString, this.username, this.password);
          return conn;
     }
     public String getConnectionString() {
          return connectionString;
     }
     public void setConnectionString(String connectionString) {
          this.connectionString = connectionString;
     }
     public String getDriverName() {
          return driverName;
     }
     public void setDriverName(String driverName) {
          this.driverName = driverName;
     }
     public String getUsername() {
          return username;
     }
     public void setUsername(String username) {
          this.username = username;
     }
     public String getPassword() {
          return password;
     }
     public void setPassword(String password) {
          this.password = password;
     }
}

그리고, ConnectionFactory를 이용한 코드로 TestCode를 다시 구성해보도록 하겟습니다. 

public class BookAppTest {
     private BookApp bookApp = new BookApp();
     private ConnectionFactory connectionFactory = new ConnectionFactory("com.mysql.jdbc.Driver", "jdbc:mysql://localhost/bookstore", "root", "qwer12#$");
    
     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.setConnectionFactory(connectionFactory);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }
    
     @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));
     }
}

지금까지 구성된 BookApp을 지금 사용하는 객체는 test 객체만이 유일한 client입니다. 사용하는 객체(client)에서 자신이 사용할 객체의 구성을 결정하는 행위를 IoC(inverse of control)이라고 합니다. 

여기서, Spring의 가장 주요한 기능을 사용할 때가 왔습니다. 

Spring은 IoC를 지원하는 경량 container입니다. 

그런데, IoC를 (어떻게) 지원하는 지에 대한 설명을 한다면 어려운 말로 DI를 통해서 지원한다고 할 수 있습니다. DI란 Dependency Injection의 약자입니다. 사용할 객체의 Dependency를 inject 한다는 뜻입니다. 여기서 말이 좀더 어려워서 예시를 이용해서 바꿔주도록 하겠습니다. 

BookApp은 ConnectionFactory에 의존한다.
BookAppTest는 BookApp에 의존한다.
BookAppTest는 BookApp의 ConnectionFactory를 변경시켜서 사용한다.

최종적으로, BookAppTest는 BookApp을 사용하지만, BookApp이 종속되어 있는 ConnectionFactory를 변경시켜서 사용하게 됩니다. 이때, 의존되어 있는 ConnectionFactory를 변경하는 행위를 DI(dependency Injection)이라고 합니다. 따라서, 위 코드를 좀 유식한 말로 풀어서 한다면, BookApp을 DI를 통해 사용하는 코드 라고 할 수 있습니다. 이를 그림으로 도식화 해보면 다음과 같습니다. 


지금 이야기드린 IoC, DI는 Spring에서 굉장히 중요한 개념입니다. Spring은 light DI container라고 할 정도로 Spring의 핵심 기능중 하나입니다. 



지금까지 작성된 코드를 보면 처음과는 다른 특징을 가지고 있습니다.
1. 테스트를 통해서 검증이 가능합니다.
2. connectionString 및 jdbc Driver를 사용하는 code내에서 찾아서 사용합니다.
3.중복 코드가 존재하지 않습니다.

위의 특징은 잘 짜진 코드라면 당연히 가져야지 되는 특징입니다.


Spring을 이용한 IoC/DI

spring을 이용하면 지금까지 구성되었던 코드를 보다 더 깔끔하게 만들어줄 수 있습니다. 먼저 spring에 대한 dependency를 추가시켜줍니다.

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.0.RELEASE</version>
      <scope>test</scope>
    </dependency>

spring-test는 scope를 이용해서, test code에서만 사용하도록 지정한것을 제외하고는 mysql dependency를 추가하는 방법과 동일하게 설정해줍니다.
applicationContext.xml 파일을 src/test/resource에 추가하고 다음과 같이 적어줍니다.
<?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.bookstore01.domain.ConnectionFactory">
        <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="bookApp" class="com.xyzlast.bookstore01.domain.BookApp">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean></beans>

solution의 directory 구조는 다음과 같습니다. 





테스트 코드를 다음과 같이 수정합니다.


    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        ApplicationContext context = new GenericXmlApplicationContext("/appContext.xml");
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }


테스트 코드가 모두 통과됨을 알 수 있습니다. 어떻게 해서 이렇게 된 것인지 서술해보도록 하겠습니다.
applicationContext.xml 파일은 사용할 객체에 대한 정의 파일입니다. spring에서는 ApplicationContext로 불리우는 객체에 대한 Hashtable 이라고 생각하시면 됩니다.
appContext에 정의된 ConnectionFactory 객체와 BookApp 이라는 객체에 대한 관계를 봐보시면 좀 더 생각이 쉬울 수 있습니다. 먼저 connectionFactory 라는 이름으로 ConnectionFactory를 정의합니다. 저희가 property로 뽑은 connectionString, driverName, username, password에 대한 값들을 모두 설정하는 것을 볼 수 있습니다. 그리고, bookApp이라는 이름으로 설정된 BookApp 객체를 보시면 좀더 재미잇는 코드를 볼 수 있습니다. ref 라는 키워드로 값을 설정하고 있는데. 이는 먼저 설정된 connectionFactory 객체를 bookApp에 주입(Inject) 시키고 있는 것을 알 수 있습니다.
그리고, 테스트 코드에 대해서 알아보도록 하겠습니다. 테스트 코드에서는 먼저 GenericXmlApplicationContext 객체를 이용해서 applicationContext를 전체 로드합니다. 로드된 xml을 기초로 객체들을 생성하고, 그 객체를 Hashtable 형태로 저장하는 일을 합니다.
Hashtable 형태로 저장된 객체들은 각각의 id를 통해서 로드가 가능하며, bookApp = (BookApp) context.getBean("bookApp"); 코드를 통해서 BookApp을 로드할 수 있습니다. 이렇게 spring을 통해서 객체를 사용하는 것이 어떤 장점을 갖게 될까요?

Spring을 사용한 객체의 사용의 장점은 크게 3가지로 볼 수 있습니다.

1. 정해진 규칙에 따른 객체의 선언
2. xml로 정의된 객체의 dependency 파악이 가능
3. 테스트의 편의성

먼저 정해진 규칙에 따른 객체의 선언은 팀단위의 개발자들에게 일정한 개발 패턴을 만들어줍니다. 정해진 개발 패턴은 정형화된 코드를 만들고, 서로간에 코드의 공유가 원활하게 할 수 있습니다. 그리고, xml을 통한 객체의 의존성 관리는 객체들이 어떠한 구조를 가지고 있는지를 파악하는데 도움을 줍니다. 마지막으로 테스트의 편의성을 들 수 있습니다. spring은 테스트 코드를 작성하는데, 지금까지 보던 코드보다 더욱 깔끔하고 쉬운 테스트 패턴을 제공하고 있습니다. Spring을 사용한 테스트 코드를 다시 한번 알아보도록 하겠습니다. 

지금까지 작성된 코드를 기반으로, Spring을 이용한 테스트 코드는 다음과 같이 구성될 수 있습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/applicationContext.xml")
public class BookAppTest {
    @Autowired
    private ApplicationContext context;
    private BookApp bookApp;

    @Before
    public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
        bookApp = (BookApp) context.getBean("bookApp");
        System.out.println(bookApp);
        bookApp.deleteAll();
        assertThat(bookApp.countAll(), is(0));
    }

Test class에 @RunWith와 @ContextConfiguration annotation이 붙어 있는 것을 알 수 있습니다. @RunWith는 Spring을 이용한 JUnit4 test class임을 명시하는 선언입니다. 중요한것은 @ContextConfiguration인데, 이는 방금 작성한 applicationContext.xml를 지정하는 영역입니다. @ContextConfiguration이 설정된 경우, Test class가 로드 되면서, xml 파일안에 위치한 객체를 생성해서 spring application context에 저장하고 있습니다. 4 line에 위치한 @Autowired는 spring application context 중에 type이 동일한 객체를 자동으로 할당하는 annotation입니다. spring test는 1개의 applicationContext를 로드하기 때문에, 그 객체를 context 변수에 자동으로 할당하게 됩니다. 자신이 구성한 applicationContext.xml은 반드시 이러한 과정을 거쳐서 테스트를 통과시켜야지 됩니다. 


Spring에서의 객체의 생명 주기


spring에서의 객체의 생명 주기는 기본적으로 한번 사용된 객체를 재사용합니다. 테스트 코드를 통해서 이를 확인해보도록 하겠습니다.
구성된 코드에 System.out.println 을 이용해서 bookApp 객체의 instance id를 확인해보도록 하겠습니다. 

     @Before
     public void setUp() throws InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
          //context = new GenericXmlApplicationContext("/appContext.xml"); @ContextConfiguration에 의하여 필요없는 코드가 되었습니다.
          bookApp = (BookApp) context.getBean("bookApp");
          System.out.println(bookApp);
          bookApp.deleteAll();
          assertThat(bookApp.countAll(), is(0));
     }

테스트 코드의 console 창의 결과는 다음과 같습니다.

INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@240615ef
2월 18, 2013 2:57:58 오후 org.springframework.context.support.AbstractApplicationContext doClose

보시면 BookApp의 모든 객체 Id가 동일함을 알 수 있습니다. ApplictionContext에 저장된 객체를 얻을 때, 새로 생성하는 것이 아닌 기존의 객체를 계속해서 사용하는 것을 알 수 있습니다.
왜 모든 객체들을 기본적으로 재사용하게 될까요? 기본적으로 spring은 enterprise development framework입니다. enterprise급의 대규모 시스템에서는 객체의 생성/삭제가 많은 부담을 주게 됩니다. 이에 대한 해결 방법으로 spring은 application context가 로드 될 때, 기본 객체들을 모두 생성, 로드 하는 것을 기본으로 하고 있습니다. 물론, 다른 방법역시 가능합니다.

Spring에서의 객체의 생명주기는 scope로 불리고, 다음 4가지로 관리가 가능합니다.

1.singleton
2.prototype
3.session
4.request

singleton은 default 값입니다. scope를 따로 설정하지 않으면 모두 singleton으로 동작합니다. 이는 객체를 static object와 동일하게 사용하게 됩니다.
prototype은 일반적으로 저희가 사용하던 객체의 생성방법과 동일합니다. new 를 통해서 객체를 생성하고, property 값을 모두 설정시킨 후, 그 객체를 넘겨주게 됩니다.
session과 request는 web programming에서 사용되는 scope입니다. 새로운 session이 생성될 때, 새로운 request가 생성이 될때 사용될 수 있는 scope 입니다.
크게는 singleton과 prototype을 이용하면 대부분의 객체 생명주기 관리는 가능하게 되며 그 차이를 한번 알아보도록 하겠습니다. scope를 prototype으로 선언해보도록 하겠습니다.

    <bean id="bookApp" class="com.xyzlast.bookstore01.domain.BookApp" scope="prototype">
        <property name="connectionFactory" ref="connectionFactory"/>
    </bean>

그리고, 테스트코드를 수행해보도록 하겠습니다.

2월 18, 2013 2:45:30 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@61305d5b: defining beans [connectionFactory,bookApp,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
com.xyzlast.bookstore01.domain.BookApp@240615ef
com.xyzlast.bookstore01.domain.BookApp@3622102f
com.xyzlast.bookstore01.domain.BookApp@78f14616
com.xyzlast.bookstore01.domain.BookApp@c4e6fe5
2월 18, 2013 2:45:31 오후 org.springframework.context.support.AbstractApplicationContext doClose

각각 4개의 BookApp bean이 생성됨을 알 수 있습니다. spring 설정 만으로 다음과 같은 코드가 만들어지는 것과 동일한 효과를 가지고 오게 되는 것입니다.

BookApp bookApp = new BookApp();
bookApp.setConnectionFactory(new ConnectionFactory);

이 부분을 보면 Factory Pattern에서의 Factory와 동일한 기능을 가지게 되는 것을 알 수 있는데요. ApplicationContext는 bean의 Map과 Factory를 지원한다. 라고 할 수 있습니다.
spring을 사용하게 되면, 객체들을 new로 새롭게 할당하는 일들이 얼마 없습니다. 모든 객체 bean들은 spring을 통해서 관리가 되고, bean들간의 데이터 교환을 위한 POJO(Plain Old Java Object)들만 new로 생성되어서 데이터 교환이 되는 것이 일반적인 패턴입니다. POJO에 대해서는 좀더 나중에 알아보도록 하겠습니다.


Spring에서의 객체 주입과 이용에 대한 심화 학습

먼저 이야기한 내용에서 bean의 Map을 제공한다는 이야기를 했습니다. Spring은 ApplicationContext라는 bean Map / Factory를 가지고 있고, ApplicationContext를 통해서 객체를 얻어내거나 생성하게 됩니다. 이때, 객체의 초기화 방법은 다음 두가지로 나눌 수 있습니다. 
1. property를 이용한 주입
2. 생성자를 이용한 주입

property를 이용한 주입은 기존 connectionFactory의 xml 선언을 보면 쉽게 알 수 있습니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <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>


생성자를 이용한 주입은 constructor-arg를 이용해서 선언 가능합니다.

<bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.ConnectionFactory">
    <constructor-arg index="0" value="jdbc:mysql://localhost/bookstore"/>
    <constructor-arg index="1" value="com.mysql.jdbc.Driver"/>
    <constructor-arg index="2" value="root"/>
    <constructor-arg index="3" value="qwer12#$"/>
</bean>   


어떤 방법이 좋은지에 대해서는 찬/반이 나뉘고 있습니다. 장점과 단점은 다음과 같습니다.
방법장점단점
Property를 이용한 방법1. 설정값에 대한 설명을 Property를 통해 명확히 알 수 있다.
2. 객체가 변경되었을 때, 확장이 용의하다.
1. 사용되는 Bean의 내용을 정확히 알지 못하면, Property 값을 설정하는 것을 빼먹을 수 있다.
생성자를 이용한 방법1. 필요한 값을 모두 설정하는 것이 가능하다.2. 값을 index와 같이, 순서를 이용하기 때문에 어떤 값을 설정하는지 파악하기 힘들다.

둘다 좋은 방법이지만, spring을 이용하는 대부분의 library 들은 property를 이용한 주입 을 주로 하고 있습니다. 이건 개발을 하는 팀에서 한가지로 정해서 가는 것이 좋습니다.

객체를 사용하다보면, 객체를 초기화 시켜야지 되는 경우가 자주 생깁니다. 지금 코드에서는 ConnectionFactory의 Class.forName().newInstace()의 경우에는 한번만 실행되어도 코드의 동작에는 아무런 문제가 없기 때문에, 생성 후, 한번만 실행이 되어도 아무런 문제가 없습니다. 이러한 초기화 method를 실행시키는 xml선언은 다음과 같습니다. 

    <bean id="connectionFactory" class="com.xyzlast.bookstore01.domain.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>


init-method가 포함된 객체의 생성 process는 다음과 같습니다. (singleton scope의 경우)

1. 객체의 생성
2. property 값들의 설정
3. init-method 의 실행

init-method가 property값들이 모두 설정 된 후에 실행됨을 확인해주셔야지 됩니다. 


Summary

이번 장에서는 Spring을 이용한 객체의 관리를 위주로 Simple Application을 작성해봤습니다. 다음 개념들은 반드시 다시 정리해보시는 것이 좋습니다.

1. ApplicationContext : Spring에서 제공하는 bean의 Map/ObjectFactory
2. property, constructor, init-method를 이용한 객체 초기화 방법
3. IoC, DI


Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 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
,

5. Spring 소개

Java 2013. 9. 6. 14:15

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



이 장에서는 Spring Framework에 대한 소개와 왜 Spring을 써야지 되는지에 대한 당위성을 간단한 application을 작성하면서 알아보도록 하겠습니다.
application을 작성하면서 놀랍게 줄어드는 코드 양과 Spring의 강력함을 느끼실 수 있으실겁니다.

Spring Framework 란 엔터프라이즈급 자바 어플리케이션 개발에서 필요로 하는 경량형 어플리케이션 프레임워크입니다. 
스프링 프레임워크는 J2EE[Java 2 Enterprise Edition] 에서 제공하는 대부분의 기능을 지원하기 때문에, J2EE를 대체하는 프레임워크로 자리잡고 있습니다. Spring이라는 이름의 기원은 기존 EJB로 대표되는 Enterprise Framework의 시대를 겨울(winter)로 정의하고, 이젠 봄(Spring)이 왔다 라는 의미로 지어졌습니다. 시작은 한권의 책의 예제에서부터 시작이 되었습니다. 

Spring Framework는 다음과 같은 특징을 가지고 있습니다.

1) 경량 컨테이너입니다. (light container) 스프링은 객체를 담고 있는 컨테이너로써 자바 객체의 생성과 소멸과 같은 라이프사이클을 관리하고, 언제든 필요한 객체를 가져다 사용할 수 있도록 도와주는 기능을 가지고 있습니다.
2) DI[Dependency Injection] 패턴 지원을 지원합니다. (DI : 의존성 주입)
= 별도의 설정 파일을 통해 객체들간의 의존 관계등을 설정할 수 있습니다.  그로인해 객체들간의 느슨한 결합을 유지하고 직접 의존하고 있는 객체를 굳이 생성하거나 검색할 필요성이 없이 구성이 가능합니다. 이는 IoC(Inversion of Controller)로 이야기되기도 합니다. 정확히는 DI로 인한 IoC를 가능하게 하는 Framework라고 할 수 있습니다.
3) AOP[Aspect Oriented Programming] 지원 (AOP : 측면 지향 프로그래밍 )
= AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법이다. 이는 문제를 해결하기 위한 핵심 관심 사항과 전체에 적용되는 공통관심 사항을 기준으로 프로그래밍 함으로써 공통 모듈을 여러 코드에 쉽게 적용할 수 있도록 한다.
스프링은 자체적으로 프록시 기반의 AOP를 지원하므로 트랜잭션이나 로깅, 보안등과 같이 여러 모듈에서 공통적으로 필요하지만 실제모듈핵심은 아닌 기능들을 분리하여 각 모듈에 적용할 수 있도록 한다.

Spring Framework는 위의 3가지의 특징을 가진 Framework입니다. 또한, 부가적인 기능으로서 ruby on rails에서 표방한 non shared status web 개발을 지원하는 @MVC 역시 지원하고 있습니다.


Spring Framework의 기본구조입니다. 위에서 말한 3가지의 특징은 Spring Core와 Spring AOP, Spring Context에 의하여 구성이 되어 있습니다. 나머지 ORM, WEB, DAO, WEB MVC의 경우에는 부가적 기능이라고도 볼 수 있습니다.

이런식으로만 적어두면, Spring이 과연 무엇을 하는 녀석인지를 알 수가 없습니다. 그래서 간단한 예제를 통해서 Spring을 통해서 점점 진화가 되어가는 코드의 변화를 보면서 Spring을 익혀보도록 하겠습니다. 


Posted by Y2K
,

4. 개발환경의 구성

Java 2013. 9. 6. 13:52

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



이번 장에서는 개발 환경을 구성해보도록 하겠습니다.

Edit

java sdk 설치

java sdk를 다운받아 설치합니다. windows에서는 oracle home page 및 google 검색을 통해 손쉽게 설치가 가능합니다. 하지만, linux에서는 조금 다른 문제가 있는데요. linux는 기본적으로 open jdk가 설치되어 있습니다. oracle에서 제공한 것이 아닌, jdk 표준 규약에 따른 open source로 구성된 jdk가 이미 설치가 되어 있습니다. 그런데 이 open jdk가 성능이 oracle 것에 비하여 떨어집니다.. 다음과 같은 절차를 통해서 open jdk를 삭제하고 oracle java를 설치하는 것이 좋습니다.

sudo apt-get purge openjdk-\* icedtea-\* icedtea6-\*
deb http://ppa.launchpad.net/webupd8team/java
apt-get install oracle-jdk7-installer
apt-get install oracle-java7-installer 
sudo ln -s /usr/lib/jni/libswt-* -t ~/.swt/lib/linux/x86/
sudo ln -s /usr/lib/jni/libswt-* -t ~/.swt/lib/linux/x86_64/

그리고, JAVA_HOME을 등록합니다. linux의 경우에는

sudo vi /etc/profile 

을 실행해서, JAVA_HOME을 처리해야지 됩니다.
 JAVA_HOME="/usr/lib/jvm/java-7-sun" 

을 설정해주는 것으로 java 설치가 모두 마쳐집니다. 
개발의 편의성을 위해서 path를 설정해주는것이 좋습니다. 
command 창에서 java -version을 실행시켜 java version을 확인합니다.


maven의 설치

먼저 maven에 대한 간단한 소개부터 하도록 하겠습니다.

Maven이란 Apache Software Foundation에서 개발되고 있는 소프트웨어 프로젝트 관리툴입니다.
Maven은 Project Object Model (POM) 이라는 것에 기초를 두어 프로젝트의 빌드, 테스트, 도큐멘테이션, 성과물의 배치등, 프로젝트의 라이프사이클 전체를 관리합니다. 프로젝트의 빌드툴인 Ant와 달리, Maven은 프로젝트 관리툴로서 프로젝트에 관련한 여러가지 정보를 POM에 집약해, POM의 정보에 기초를 두어 프로젝트 전체를 관리합니다.

Maven의 주 기능은 다음과 같습니다.

  1. 프로젝트 라이브러리 관리
  2. 프로젝트 정보의 전달 (사이트의 작성, 배치나 유니트테스트의 레포트등)
  3. 프로젝트 작성부터 컴파일, 테스트, 패키지, 배치등의 프로젝트 프로젝트 라이프사이클에 포함되는 각 태스크의 지원이 가능합니다..

또, Maven은 소프트웨어 프로젝트의 관리 툴임과 동시에 이해툴로서 있습니다.
Maven의 제1목적은 개발자가 단기간에 프로젝트를 이해할 수 있도록 도움을 주는 것입니다. 그를 위해 Maven 에서는

  1. 빌드프로세스를 간단히 한다
  2. 통일한 빌드시스템을 제공한다.
  3. 양질의 프로젝트 정보를 제공한다.
  4. 개발의 가이드 라인을 지원한다.
  5. 신기술에 대해서는 투과적인 이행을 고려하여 넣는다.

과 같은 일을 하고 있습니다. 이런것들에 의해 Maven을 이용한 프로젝트는 어떠한 것이라도 빌드방법이 동일하다거나 디렉토리 구성이 거의 같아서 전체를 파악하기 쉽다등의 장점이 있습니다.
Maven의 구성은 작은 코어와 대량의 플러그인으로 되어 있습니다. 그리고 플러그인이나 라이브러는 필요에 따라서 자동적으로 다운로드가 행해집니다. 이 구조에 의해서 Maven이나 플러그인이 버전업할 때 신기술에 대해 투과적 이행이 가능합니다.
Maven은 당초 Jakarta Turbine의 프로젝트를 위해 작성되었습니다. 복수의 서브프로젝트로 구성된 Jakarta Turbine의 빌드 프로세스를 단순화 하기위해 작성되어졌던 것이 개별의 프로젝트로 독립한 것입니다.


그럼 maven의 설치를 해보도록 하겠습니다. maven은 http://maven.apache.com 에서 배포가 되고 있으며, 최신 버젼인 3.0.4를 다운받아 원하는 위치에 압축을 풀면 기본 설치는 완료됩니다. 사용을 편하게 하기 위해서, MAVEN_HOME을 다음과 같이 등록을 합니다.





또한 maven을 자주 사용하기 때문에 MAVEN_HOME\bin 을 path에 등록하면 maven 설치는 완료됩니다. 
명령어 창에 다음 명령어를 실행시켜, maven의 설치가 완료됨을 확인하도록 합니다.





maven은 추가 library를 관리하기 위해서 local repository를 이용하게 되는데, local repository는 기본적으로 사용자 directory에 .m 폴더를 만들어서 사용하게 됩니다. 기본 설정이 windows에서는 사용자 폴더 안에 위치하게 되어 관리가 힘들게 됩니다. windows 환경에서는 변경이 필요합니다. 
MAVEN_HOME\conf\settings.xml 파일을 에서 repository 항목을 자신이 편한 위치로 변경하면 됩니다.

이제 기본적인 maven 설정은 모두 완료되었습니다.


eclipse & eclipse plug in의 설치

eclipse 의 설치는 매우 간단하게 처리가 가능합니다. 그냥 다운 받아서 workspace를 지정후, 실행하면 됩니다. 
기본적으로 모든 project는 spring과 maven을 이용해서 구성이 되기 때문에, maven과 spring plug in은 설치하는 것이 도움이 됩니다. 
maven과 spring plug in을 설치하도록 합니다. 
help > Eclipse marketplace 에 들어가서 spring으로 검색을 합니다.






STS를 찾아 설치를 완료합니다.

maven 역시 같이 검색을 해서 다음 plug-in을 설치합니다.



maven plugin은 기본적으로 maven 3.0.4를 포함하고 있습니다. 먼저 설치한 maven의 설정을 따라가기 위해서 maven의 설정을 수정해줄 필요가 있습니다. 
windows > preferences 에 들어가서 maven 항목을 확인합니다. installation 에서 설치된 maven 위치로 설정하면 eclipse의 설정은 모두 마쳐집니다.





mysql 설치

mysql은 open source database로, 정부 표준 프레임워크에서 인정된 DB입니다. 개발용 DB로 사용할 수 있을정도로 가볍고, 빠른 속도를 자랑합니다.
그리고, 이번 강의는 모두 mysql 기준으로 행해지기 때문에, mysql을 설치해주시길 바랍니다.



유용한 eclipse plug in

개인적으로 유용하다고 생각되는 eclipse plug in들입니다. 입맛대로 골라서 사용해주시면 됩니다.

  1. Easy Shell : 필수 입니다. 이게 없으면 maven을 사용하기가 매우 힘듭니다.
  2. Color and theme : 색상 변경을 쉽게 해주는 plug in입니다.
  3. dbBear : db client ui입니다.
  4. amaterasUML
  5. moreUnit

Hello World의 실행

먼저, eclipse 에서 새로운 프로젝트를 하나 생성합니다. 
기존의 eclipse에서 새로운 프로젝트를 만드는 법을 사용하지 않고, maven을 이용한 application으로 작성합니다.





maven-archtype-quickstart를 선택하고, project를 작성합니다. 작성된 project의 파일구조는 다음과 같습니다. (resource folder는 존재하지 않을수 있습니다.)





각 Folder가 의미하는 것은 다음과 같습니다. 이와 같은 Folder 구조에 매우 익숙해질 필요가 있습니다. 이는 maven을 이용한 Project의 기본 Folder 구조입니다.

srcsource code가 위치합니다.
src/main개발 code가 위치합니다.
src/test테스트 code가 위치합니다.
src/main/javajava code가 위치합니다. ClassLoader가 접근하는 root위치입니다.
src/main/resourcejava code이외의 파일들을 관리할 때 사용됩니다.
src/test/javaUnit Test code가 위치합니다.
src/test/resourceUnit Test code에서 사용되는 파일들을 관리할 때 사용됩니다.
targetcompile된 파일들이 위치하고, test의 결과가 문서화되는 폴더입니다.
target/classessrc/main에 있는 파일들이 compile된 class 파일들이 위치합니다. 그리고, resource안에 있는 파일들이 copy됩니다.
target/test-classessrc/test에 있는 파일들이 compile된 class파일들이 위치합니다.
target/generated-sourcesAspectJ에 의해서 새롭게 생성된 main code들이 위치합니다.
target/generated-test-sourcesAspectJ에 의해서 새롭게 생성된 test code들이 위치합니다.
target/surefiretest에 대한 xml문서 등을 만드는데 사용되는 surefire jar가 임시로 위치합니다.
target/surefire-reportstest에 대한 결과를 txt파일과 xml파일로 제공합니다.

이 폴더 구조는 maven의 명령어를 실행시킬 때마다 동적으로 생성 또는 삭제가 되지만 위의 구조는 기본구조이기 때문에 변하지 않습니다. 위 구조를 반드시 숙지하시길 바랍니다.

Easy Shell을 이용해서, Console 창을 하나 실행합니다.

mvn compile 

을 실행 시키면, hello world project가 compile이 되는 것을 볼 수 있습니다.





pom.xml 파일을 한번 열어보도록 하겠습니다. maven은 POM에 기초하여 프로젝트의 빌드, 테스트, 도큐멘테이션, 성과물의 배치등, 프로젝트의 라이프사이클 전체를 관리를 한다고 했습니다. 그럼 maven에서 과연 어떤 정보들을 관리하고 있는지 한번 알아보도록 하겠습니다.

mvn site

를 실행시켜보시길 바랍니다.

무언가 maven에서 열심히 작업을 하고 있습니다. 그리고 Easy Shell을 이용해서 target/site 안의 index.html 파일을 브라우져로 열어보시길 바랍니다. 
index.html의 좌측을 보시면 pom.xml에 담길수 있는 모든 정보들이 보이는 것을 알 수 있습니다.

pom.xml에 담길수 있는 정보들은 다음과 같습니다.

DocumentDescription
About이 Project에 대한 간략한 서술을 적습니다.
Project Team팀 맴버에 대한 정보를 담습니다. email과 같은 연락 수단역시 이곳에 담겨집니다.
Dependency Information만약에 이 project를 다른 maven project에서 연결해서 사용할 때, pom.xml에 설정할 정볼르 담습니다.
Project Plugins이 Project를 compile 할때, 사용된 maven plug in에 대한 정보를 담습니다.
Continuous Integration이 Project가 관리되는 CI의 URL을 적습니다.
Issue Tracking이 Project가 관리되는 Issue Tracker의 주소를 적습니다.
Source Repository이 Project가 관리되는 svn의 정보를 적습니다.
Project License이 Project의 license를 기록합니다.
Plugin Management이 project가 Build될 때, 사용된 maven plug in에 대한 버젼 정보등 상세 정보를 기록 합니다.
Distribution Management이 Project의 배포 서버를 적습니다.
Project SummaryProject의 버젼 정보 및 GroupId, ArticleId 등을 기록합니다.
Mailing ListsProject에 대한 Mailing list를 적습니다.
DependenciesProject가 참조하는 library들에 대한 정보가 기록됩니다.

엄청나게 많은 정보가 pom.xml에 기록됨을 알 수 있습니다. 어찌보면 project의 상세 명세서와 동일하다고 할 수 있습니다. 여기서 가장 유용하게 쓰이는 것은 Dependencies에 대한 정보입니다.

http://mvnrepository.com/ 에 방문해보시길 바랍니다. 이 사이트는 maven을 통해서 배포가 되는 라이브러리들이 위치한 사이트입니다. 우리가 maven에 특정 library를 등록하고 저장을 하면, maven은 자동으로 파일을 다운받아서 우리가 만든 프로젝트에 include 시킵니다.

지금 프로젝트에서는 junit 3.8.1이 사용되고 있지만, 이를 junit 4.11로 변경해보도록 하겠습니다. pom.xml에서 junit 3.8.1 부분을 찾아, version을 4.11로 바꿔주시고, pom.xml 파일을 저장해주세요. 저장을 하기 전에는 junit 3.8.1이 maven dependency에 들어있었지만, 저장 후, 바로 junit 4.11로 바뀌는 것을 알 수 있습니다.



maven을 이용하기 전에 어떤 jar를 다운받아서 project에 포함시키는 여러 작업들이 이제는 아무런 필요가 없습니다. 이건 모두 maven이 대신 해주게 되니까요. 그리고, maven에서 repository path를 설정해줬던 path에 한번 가서 파일을 봐보시길 바랍니다. 그곳에 파일들이 모두 위치하고 있는 것을 알 수 있습니다. 이것이 maven을 이용한 dependency 관리입니다.






자, 이젠 maven을 이용한 project 관리를 한번 봐보시도록 하겠습니다. 지금 보면 project의 java version이 1.5로 되어 있는 것을 알 수 있습니다. 이것을 pom.xml 파일을 변경하는 것으로 1.7로 변경하도록 합시다. pom.xml 파일에 다음 내용을 기록합니다.

그리고 project의 우클릭후, maven > update project를 선택해줍시다. 그럼, project가 JDK 1.5에서 1.7로 변경되어 있는 것을 알 수 있습니다.

간단한 코드를 짜보도록 하겠습니다. 
실제 개발 코드는 모두 src/main에 위치해야지 됩니다. 안에 HelloWorld 객체를 만들고, 두개의 method를 추가해줍니다.

 1public class HelloWorld {
 2    public String sayHello(String name) {
 3        return "Hello " + name;
 4    }
 5    public String sayHi(String name) {
 7        return "Hi " + name;
 8    }
 9}

그리고 테스트 코드를 작성합니다. 테스트 코드에 대해서는 다음 시간에 깊게 들어갈 예정이니 지금은 그냥 만들어주시기만 하면 됩니다. 테스트 코드는 모두 src/test에 위치해야지 됩니다. 그리고, 반드시 객체의 이름은 Test로 끝나야지 됩니다. 규칙을 만들어주기 편하게 하기 위해서 more unit의 기능을 이용하도록 합시다.

1public class HelloWorldTest {
2    @Test3    public void test() {
4        System.out.println("test running");
5    }
6}

그리고 console창에서 다음 명령어를 실행해보세요.

mvn test

maven이 test를 행하는 것을 알 수 있습니다. maven은 기본적으로 compile, test, package, install 이라는 phase를 갖습니다. 이 phase는 서로간에 종속적입니다.
compile > test > package > install 이라는 과정을 반드시 거치게 되지요. package를 할 때는 반드시 compile과 test를 거친 이후에 package phase가 실행이 되는 구조입니다. 따라서, test code를 잘 다듬지 못하면 배포 자체를 못하게 될 때도 있습니다.



Summary

maven을 이용한 test project를 만들어봤습니다. maven의 folder구조는 잘 숙지를 하고 계셔야지 됩니다. maven의 많은 plug in들은 이 folder구조가 유지되는 것을 전제로 움직입니다.


Posted by Y2K
,

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



이번 장에서는 전자정부 표준 프레임워크에 대해서 알아보도록 하겠습니다.

전자 정부 표준프레임워크는 국가 정보화 투자 효율성 제고, 중소 SI 경쟁력 확보, 선진 국가정보화 추진 기반환경의 제공이라는 목적하에 2007년 12월부터 시작하여 2010년 11월까지 총 3단계에 걸쳐 사업이 진행된 결과입니다. 
최종적으로는 효율적 운영에 필수적인 "운영환경" 및 47종의 공통 컴포넌트를 선정하고 신규 사업 및 고도화 사업을 대상으로 점진적 확대 적용을 하고 있는 추세입니다. 당장 정부쪽 SI 사업은 모두 이 전자정부 표준 프레임워크를 통해서 구축, 운영되고 있습니다. 국가대표포털, 기업 경쟁력 지원 등 총 69개의 공공,민간 정보화 사업(89개 System)에 적응되었습니다. (2011년 2월 기준) 그리고 정부 소프트웨어 기술성 평가 기준에 표준 프레임워크 도입여부를 반영함으로서 표준프레임워크 도입을 하지 않으면 정부측 SI를 할 수 없게 막아버린 효과를 가져왔습니다.


해야지 됩니다. 지금 이 바닥에서 먹고 살려면요. ㅋ



표준 프레임워크의 개발 - 개념

전자 정부 프레임워크의 구축 이유 : 정보 시스템 개발을 위해 필요한 기능 및 아키텍쳐를 미리 만들어 제공함으로써 효율적인 애플리케이션 구축을 지원하고 있습니다.
개발프레임워크는 일련의 문제 해결을 위한 추상화된 디자인을 구현한 클래스들의 집합으로서 클래스 보다는 큰 규모의 재사용을 지원하는 것을 목적으로 가지고 있습니다. 라이브러리와 개발 프레임워크의 차이는 어플리케이션의 틀과 구조를 프레임워크에서 제어하며, 프레임워크 위에서 개발자의 코드가 동작하는 차이가 있습니다. 반면에 라이브러리는 개발자의 코드 안에서 재 사용되는 것을 총칭합니다.






정부가 기대하는 전자 정부 프레임워크의 효과는 다음과 같습니다.

  1. 전자 정부 서비스의 품질 향상
  2. 정보화 투자 효율성의 향상
  3. 대/중/소 기업이 동일한 개발기반 위에서 공정경쟁을 한다.

라는 효과를 표방하고 있습니다. 개인적으로는 3번째인 대/중/소 기업이 동일한 개발기반 위에서 공정 경쟁을 한다는 것이 인상적인데요. 공정 경쟁이라는 것이 어떤 것인가.. 라는 생각은 조금 하게됩니다. 다행인 것이 예전의 my platform과 같은 기업 독자적인 framework를 따로 공부를 해야지 되는 상황은 조금 피하게 된 것 같습니다.



표준 프레임워크의 개발

실행, 개발, 운영 등 4개의 환경과 13개의 서비스 그룹, 그리고 54개의 서비스로 구성. 대기업/중소기업 대상 온라인 설문과 인터뷰 및 자료조사를 통해 구축되었습니다. 구축 과정을 보시면 아시겠지만, 설문조사를 기반으로 개발되었습니다. 현장에서 주로 사용되고 있는 open source framework가 무엇인지에 대한 설문 조사 결과가 표준프레임워크이다. 라고 할 수 있습니다. 다음은 각 open source의 선정 기준입니다.






표준 프레임워크의 개발 범위

표준 프레임워크의 개발 범위는 다음과 같습니다.





Open source framework & library

그리고, 선정된 Open source 및 Solution은 다음과 같습니다. (2010년 8월 기준)
레이어서비스명open sourcedescriptionurl
화면처리ajax supportAjax Tags 1.5jquery로 인하여 거의 사용되지 않음http://sourceforge.net/projects/ajaxtags/?source=navbar
InternationalizationSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
MVCSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
SecurityApache Commons Validator 1.3
UI Adaptor선정하지 않음
업무처리Process ControlSpring Web Flow 3.0http://springsource.org
Exception HandlingSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
데이터 처리Data AccessiBatis SQL Maps 2.3
Data SourceSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
ORMHibernatehttp://www.hibernate.org
TransactionSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
연계 통합Naming Service SupportSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
Integration Service선정하지 않음
Web Service InterfaceApache CXF 2.2
공통 기반AOPSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
CacheEhcache
Compress/DecompressApache Commons Compress 1.0
Encryption/Decryptionjava simplified encrypion (jasypt) 1.5
ExcelApache POI 3.0Excel File Handling
File HandlingJakarta Commons VFS 1.0
File Upload/DownloadApache Commons FileUpload 1.2
FTPApache Commons NET 2.0
ID Generator선정하지 않음UUID 생성 기능 개발
IoC ContainserSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
LoggingLog4j 1.3
MailApache Commons Email 1.1
Marsharling/unmarsharlingCastor 1.2, Apache XML Beans 2.4
PropertySpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
ResourceSpring 2.52012년 8월 Spring 3.1로 upgradehttp://www.springsource.org
SchedulingQuartz 1.6
Server SecuritySpring Security 2.0http://www.springsource.org
String UtilsJakarta Regexp 1.5
XML ManipulationApache Xerces2 2.9, JDOM 1.0
MobileMobile WebJquery Mobile 1.22012년 8월 선정

개발 Tool

EditorEclipse
AmaterasUML
AmaterasERD
DebugEcliopse
Unit TestJUnit
Easy Mock
Dbunit
emma
ant
maven
Buildmaven
hudson
jenkins
Configuration managementSVN
Change ManagementjTrac

보시면 아시겠지만, 35개의 open source component (개발 환경 제외)중에서 10개가 Spring입니다. 그리고 나머지 기능들에서 Spring 기능을 사용한 Web Flow와 Security를 합치면 1/3이 Spring이라고 할 수 있습니다. 거기에다가 Excel, Encrypt/decrypt와 같은 일반 기능성 component를 제외시키면 그냥 정부 표준 프레임워크는 spring framework를 사용해서 만드는 것이다. 라고 할 수도 있습니다. 이제 java를 알아도 개발을 하지 못하고, spring을 알아야지만 개발을 할 수 있는 상황이 되어버렸습니다.


정부 표준 프레임워크의 특징

  • 5개 서비스 그룹, 34개 서비스로 구성
    • 화면처리, 업무처리, 데이터처리, 연계통합, 공통기반의 5개 서비스 그룹으로 구성
    • MVC, IoC 컨테이너, AOP, Data Access, Integration 등의 핵심 서비스를 제공
  • 전자정부 프로젝트에 최적화된 오픈 소스 소프트웨어 선정
    • 34개 서비스 별 최적의 오픈 소스 소프트웨어 선정 과정을 통해 32개 오픈 소스 소프트웨어를 선정
  • 경량화된 개발프레임워크로서 사실상 업계 표준에 가까운 Spring 프레임워크를 적용
    • J2EE 표준을 준수하는 Spring 프레임워크를 채택
    • 특정 업체의 WAS나 DBMS에 독립적인 환경을 제공
  • DI 방식의 의존 관계 처리
    • Dependency Injection을 통해 개체나 컴포넌트 간의 의존성을 정의함으로써 변경용이성과 재사용성을 향상
  • AOP 지원
    • 트랜잭션, 예외처리와 같은 공통 관심 대상을 분리하여 정의함
  • MVC Model2 아키텍처 구조 제공 및 다양한 UI 클라이언트 연계 지원
    • Spring MVC를 기반으로 하며, 다양한 UI 클라이언트 연계를 위한 인터페이스를 정의함
  • 전자정부 개발프레임워크 표준 연계 인터페이스 정의
    • 표준 연계 인터페이스를 정의하여 연계 솔루션에 대한 의존성을 배제하고 독립적인 어플리케이션 개발이 가능함

이와 같이 정부 표준 프레임워크가 어떻게 구성이 되어있는지에 대해서 간략하게 알아봤습니다. 그럼 다음 장에서는 정부 표준 프레임워크를 이용한 개발 환경의 구성을 해보도록 하겠습니다.


Posted by Y2K
,

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


개발을 할때는 환경이 매우 중요합니다. 개발 환경을 팀의 관점으로 볼때는 단순히 업무 흐름만을 보는것은 너무 옛날 이야기가 되었습니다. 
이제 팀 단위의 개발환경에서 거의 필수가 된 항목들은 다음과 같습니다.

  1. 코드 관리 시스템
  2. 이슈 관리 시스템
  3. 자동 빌드 시스템
  4. Code Review 시스템
  5. 자동 배포 시스템
  6. 로컬 개발 서버(DB 포함) - 개발자 PC
  7. 테스트 서버 (DB 포함)
  8. 배포 서버 (DB 포함)

각 시스템이 유동적으로 연결되어 하나의 프로젝트가 완성이 되는 것이 일반적입니다.
먼저, 간단한 예를 들어보기로 하겠습니다. 여기 신입 개발자 A군과 고참 개발자 B양이 있습니다.



아침

  • B양은 이슈 관리 시스템을 통해서 자신의 팀에 주어진 일들을 확인합니다. 그리고 간단한 팀 미팅을 통해 서로간에 지금 어제 어떤 일을 했고, 오늘 어떤 일을 할 것이며, 오늘 이슈 사항이 무엇이 있는지를 파악합니다. 파악한 후, 추가로 들어온 일이 있는 경우에 그 사항을 이슈 관리시스템이 등록 또는 수정을 하고 담당자를 A군으로 지정합니다.
  • A군은 이슈 관리 시스템을 통해서 자신에게 주어진 일을 확인합니다. 그리고, 이미 분석된 업무이고, 바로 개발을 들어가게 됩니다. 먼저 코드 관리 시스템에서 소스를 최신의 것으로 Update 받습니다. 받은 Source의 History를 파악하고, DB schema의 변경사항이 있는 경우, 자신의 개발 환경에 반영을 하고 코딩을 시작합니다.

점심

  • A군은 코딩을 열심히 해서 일을 마쳤습니다. 그리고, 소스를 코드관리 시스템에 commit을 진행합니다. 소스관리 시스템은 commit된 코드가 있음을 관리자에게 알립니다.
  • B양은 A군의 코딩 내용을 보고 Code Review를 진행합니다. 맘에 안들면 불러다가, 또는 email로 깨고, reject 시킵니다. 그렇게 되면 이 코드는 반영되지 않게 됩니다.
  • A군은 다시 코딩을 합니다. 지적 받은 사항에 대해서 다시 고치고, 다시 commit을 합니다.
  • B양은 A군의 코드를 review하면서 흐뭇해하면서 코드를 accept합니다. accept된 코드는 이제 자동 빌드 시스템으로 넘어갑니다.
  • A군은 자신이 한 일에 대해서 이슈 관리 시스템에 일의 진행정도를 알리고, 완료를 시킵니다. 물론 약간의 문서 작업도 진행합니다.
  • 자동빌드 시스템은 열심히 코드를 build하고, 자동화된 test code를 모두 실행하고 그 결과를 팀원 전체에게 email로 알립니다.
  • test code가 모두 완료되면 build한 결과를 테스트 서버에 배포하게 됩니다. 이제 테스트 서버를 통해서 QC 팀들이 개발팀의 결과를 검증하게 됩니다.
  • QC 팀이 검증을 모두 마쳤습니다. 자동 배포 시스템을 통해서 실제 동작하는 서비스로 배포하게 됩니다. (모두들 수고하셨습니다.)

이러한 과정을 거치게 되는 것이 일반적인 개발회사에서의 흐름입니다. 그리고 계속되는 통합 과정을 통해, 코드의 품질을 계속해서 높이는 과정을 하게 됩니다. 그러나... 불행히도 저희 회사는 저기 위에 있는 시스템중 반도 존재하지 않습니다.

실질적인 예가 아닌, 이제 좀더 본격적인 설명에 들어가보도록 하겠습니다.


코드 관리 시스템

주로 사용되는 것은 svn과 요즘 인기를 끌고 있는 git가 있습니다. 기능으로는 code의 중앙 저장소 및 source 변경에 대한 history 관리 기능과 sync, merge 등이 있습니다. 
저희가 자주 사용하고 있지만, 조금은 다르게 사용하고 있는것 같긴 합니다. 주 기능은 code의 저장입니다. 그렇지만, 그 이상으로 중요하다고 생각되는 기능은 code의 history입니다. 어느날 갑자기 code를 받아보니 변경사항이 있었고, 그 내용이 왜, 그리고 무엇이 변경된것인지 모른다면 예전에 code를 usb같은것으로 카피해서 주는것과 차이가 없습니다. 코드를 변경했으면, 그 code를 왜 변경했는지에 대한 log를 적어주는 것이 이 시스템을 보다 더 잘 쓰는 방법입니다.

이슈 관리 시스템

많은 이슈 관리 시스템이 존재합니다. 대표적인것으로 무료로 배포되는 것들은 trac, jtrac, redmine, bugzillar 등이 있으며 상용은 jira가 있습니다. 상용으로 판매되는 jira는 정말 좋습니다. 개인적으론 구매를 해서 사용해보고 싶은 욕심이 무척 많이 듭니다. ㅠㅠ
코드 관리시스템이 코드에 대한 중앙 집중형 관리라면, 이슈 관리 시스템은 왜 그 코드를 작성하게 되었는지. 우리가 프로젝트를 완료하기 위해서는 앞으로 어떤 일들을 해야지 되는지. 그리고 지금까지 우리가 어떤 일을 해왔는지에 대한 "일, 작업, Issue, Task"에 대한 기록을 남기는 장소입니다. 이슈 관리 시스템은 코드 관리 시스템과 연동되어 코드의 변경사항이 어떤 이슈와 관련이 있는지까지 같이 보여줄 수 있습니다. 또한, 대부분의 코드 관리 시스템은 wiki라는 문서 작업 공간을 같이 제공합니다. wiki를 통해 서로간에 공유할 수 있는 내용을 기록하는 것이 가능합니다.

++ 자동 빌드 시스템, 자동 배포 시스템
이 두개의 시스템은 거의 하나로 만들어져서 사용됩니다. 이를 CI 시스템이라고 하며, hudson, jenkins와 jetBrains 사의 TeamCity가 유명합니다. CI가 CI 가 하는 일은 소스 관리 시스템과 연결되어 commit된 소스가 있는 경우, 자신이 다운받아 build 후, test code를 실행하고 그 결과를 report로 남기는 일을 반복합니다.또한 build 결과 또는 수동으로 실서버 또는 개발서버에 배포를 할 수 있기때문에 신속하고 중지 되지 않는 통합을 가능하게 하고 있습니다.





CI에 대하여 조금 더 알아보도록 하겠습니다.

다음은 CI의 기본 동작 Process를 나타내고 있습니다.

  1. Commit source code : 개발자가 자신의 code 를 commit 한다
  2. Unit Test : CI가 코드에 포함된 Unit Test code를 실행한다.
  3. CPM ( Continiuous Performance Management) : Unit Test 결과를 기록하고, Unit test의 실행 시간등을 기록한다.
  4. License check : 만약 코드 안에 상용 library등이 포함되어 있다면, license에 대한 check를 진행한다.
  5. Build : Build 및 추가 파일 복사 작업 진행
  6. Deploy : Test 환경으로의 배포 ( > QI 영역)









Code Review System

약간은 옵션적인 시스템이지만, 많은 기업들이 채용하고 있는중인 Code Review System입니다. Code Review란 간단히 사용자의 자신의 Code를 코드 관리 시스템에 Commit을 할때, 중간 단계를 하나 거치게 하는 것입니다. 그 중간단계에서 상임 개발자들이 그 코드를 평가하고, 원활히 변경된 경우에 그 코드를 코드 관리 시스템에 반영하는 절차를 추가한 시스템입니다. CodeCollaboration 이 대표적 시스템입니다.

++ 개발자 개발 환경의 구축

개발자들은 자신의 로컬 개발 환경을 반드시 갖춰야지 됩니다. 자신의 PC 자체가 원 운영 시스템과 최대한 동일할 필요가 있습니다. 이와 같은 시스템을 구축하는데 가장 큰 문제가 될 수 있는 것은 다음 방법들이 있습니다.

  1. DB
  2. OS
  3. Servlet Container (Web Server)

먼저, DB는 Project에 따라 다른 DB를 사용하게 되는 경우가 많습니다. 그럼 그 DB를 모두다 설치를 해야지 되는가. 에 대한 의문이 나오게 됩니다. 일단, 자신의 PC 상황이 최대한 좋다면.. 모든 DB가 다 설치가 되어있는 것이 좋겠지요. 그렇지 않다면 mysql이나 hsqldb가 해답이 될 수 있습니다. 그럼 db에 따라 다른 항목들은 어떻게 처리하느냐라는 문제가 남게 됩니다. 이 문제는 다음과 같이 처리합니다.

DB에 종속적인 개발을 하지 않는다. 가 우선 해결책이 될 수 있습니다.

만약에 DB에 종속적인 query를 만들어내고, SP를 실행해야지 되는 경우가 오게되면, local 개발 환경에 그 db를 반드시 설치해야지 됩니다. 개발자가 개발 환경 구축에 시간을 사용하고, 최대한 개발환경과 동일하게 구축하는 것은 당연한 요구사항입니다. 그렇지만, 단순히 crud만을 하는데, 내부에서 trim, substring 등의 함수를 이용해서 날짜 및 숫자를 뽑아내서 db에서 계산을 해서 넘어와야지 된다면, 처음 db 설계가 잘못된 것이 아닌가 생각을 해보는 것이 좋습니다.

다음은 OS입니다. 개발환경이 대체적으로 windows로 구성이 되는 반면에 서버측은 linux환경이 되는 경우가 대다수입니다. 여기서 가장 문제가 되는 것은 파일 구조입니다. 그 이유는 큰 이유는 로그 파일과 각 설정 파일의 절대적 위치때문입니다. 이 부분은 maven이나 ant를 이용한 build system으로 해결하는 것이 좋습니다. local 환경, 테스트 환경, 운영 환경에서 각각 다른 설정파일을 사용하도록 각각의 profile을 이용해서 설정하는 방법으로 해결할 수 있습니다.

다음으로 Servlet Container입니다. java는 상용 WAS들은 모두 tomcat의 servlet interface를 따르고 있습니다. 개발환경에서는 큰 부하를 견딜 필요가 없기 때문에 tomcat 또는 glass fish를 설치하는 것이 좋습니다. eclipse에 기본으로 추가 되어있는 tomcat이 가장 좋은 선택입니다. 
.net의 경우에는 윈도우즈를 사용하는 경우에는 IIS가 모두 기본으로 들어가 있기 때문에 별다른 문제가 없습니다.

일단 최대한 개인 PC와 개발 환경은 일치시키는 것이 중요합니다. 그렇지만, 현실적인 다른 문제가 발생하기도 하는데. 다음과 같습니다.


Step 01. 시작입니다. 간편합니다.




Step 02. 뭐. 이정도 즈음이야..

Step 03. 아직까지는 견딜 수 있어.

Step 04. 이제는 버틸수가 없어... 난장판이군요. ㅠㅠ

가상화 서버 또는 개발 서버를 통해서 해결을 하지만, 그 정도까지 큰 프로젝트는 아직까지는 아닙니다. 대부분의 프로젝트는 Step 03 정도면 개발이 가능합니다.


Posted by Y2K
,

Java를 1년정도 공부해보면서 이젠 슬슬 내용을 blog에 공개를 해도 될 것 같아서 공개를 해봅니다. 부끄러운 내용이지만, 도움이 될 수 있으면 좋겠습니다.


웹기술은 standalone에서 부터 시작해서, 2 tier, 3 tier, n-tier로 발전하게 되었고, 그에 따른 기술 역시 계속해서 발전해나간것을 알 수 있습니다.



standalone 시기

HTML이 처음 나오던 시기입니다. HTML에 대한 기술 습득과 http를 서비스할 수 있는 web server에 대한 기술이 발전했습니다. 지금은 누구나 하는 html 수정이 굉장한 기술이 되던 시기였습니다. 오히려 기술적으로는 web server를 직접 개발해서 서비스를 하는 문제가 더욱더 큰 이슈를 가지고 왔습니다. 그리고, 이때 개발의 방향을 바꾼 vm 기술이 나오기 시작했습니다. 전에 이야기한 .net과 java 기술이였지요. 이 두 기술 역시 처음부터 web에 집중한것은 아닙니다. 이때는 주로 사용되는 web server들이 Apache httpd 가 주로 사용이 되었고, static resource를 이용하는 형태가 주가 되었기 때문에 HTML coder 또는 web server 기술, 두 기술만이 발전하고 있었습니다. 
이때 기술의 승자는 apache 재단에서 시작한 httpd와 IIS 입니다. Http daemon 의 약자인 httpd는 아직도 Apache 서버라는 이름으로 불리우고 있습니다. (정식이름은 Apache Httpd 서버입니다.) 또한 MS의 IIS 역시 시장에서 많이 사용되는 Platform으로 사랑받기 시작했지요.


2 tier 시기

Web 기술이 폭발적으로 발전하던 시기입니다. RDBMS의 발전으로 인하여 DB의 내용에 따른 dynamic web이 발전하게 됩니다. 이때, dynamic web기술의 어려움에 의하여 여러 변형 기술들이 계속해서 나오게 됩니다. 큰 기술 흐름만을 보면 다음과 같습니다.


asp의 폭발적인 발전

기존 개발자들의 70%정도를 차지하고 있던 visual basic 개발자들을 모두 web 개발자로 만들어주는 asp와 iis가 처음 선을 보이게 됩니다. 기존 visual basic 개발자들은 모두 db와 db를 표현하는 방식에 대해서 경험이 풍부한 사람들이였습니다. 그 개발자들을 모두 web 개발자로 만드는 엄청난 일을 MS가 성공하게 됩니다. 지금보면 참 문제가 많은 개발방법으로 개발하게 되지만, 그 당시에는 최고의 기술이였으며 누구나 쉽게 웹 프로그래밍에 접근할 수 있다는 점이 가장 큰 장점이였습니다.


php의 발전 및 apache httpd의 확장

기존 standalone 서버에서 주로 사용되던 apache httpd를 이용한 dynamic web programming이 대두되게 됩니다. php는 asp의 장점을 흡수하고, 보다 쉬운 표현 방법을 제시합니다. 그리고 LAMP 라는 환경을 제시하기 때문에, 기업은 새로운 x86 windows server를 구매하는 것이 아닌 공짜로 web을 서비스할 수 있다는 점에서 기존 asp 시장을 잠식하기 시작합니다. 또한 linux 서버환경의 폭발적인 증가로 인하여 서버 환경이 기존 UNIX에서 linux 계열로 발전하게 된 계기가 됩니다.


servlet과 asp .net의 발전

asp와 php로 인하여 dynamic web application이 엄청난 발전을 하게 됩니다. 하지만 asp의 경우 언어의 특성상 큰 약점을 가지고 있습니다. page단위로 동작하게 되기 때문에, 현대적 프로그래밍 기법인 OOP를 사용할 수 없습니다. (방법은 있으나, 매우 괴악한 방법입니다.;) 점점 application이 거대화되어가면서 코드의 재 사용성 및 유지보수의 약점을 극복하기 위해서 asp와 php에 가려서 잠시 존재감을 잊어가고 있던 java와 .net이 다시 등장하게 됩니다.
두 언어는 RDBMS를 표현하는데 있어서 최선(?)의 방식을 가지고 있었으며, vm 기술의 발전으로 memory 및 performance에 강점을 가지고 있었기 때문에, 기존의 web 개발 시장을 빠르게 잠식해나가기 시작합니다.


fat client의 발전

servlet과 asp .net의 발전은 RDBMS와 dynamic web application간의 여러 문제를 모두 해결해준것같지만 사용자들에게 다양한 경험을 보여주기 위한 View를 제공하는데에 있어서, 기존의 HTML만은 한계를 가지고 있었다. 동영상과 같은 멀티미디어를 포함하기에는 기존의 HTML이 지원하지 않는 부분이 너무나 많은 영역들이 있었기 때문입니다. 멀티미디어에 대한 지원을 강화하기 위해서 나온 첫 기술이 micromedia의 Flash입니다. Flash는 멀티미디어의 지원뿐 아니라, 웹을 보다 더 아름답게 만드는데 큰 공헌을 하게 됩니다. Flash의 대성공으로 MS는 asp로 마련했던 서버 시장에 심각한 타격을 입게 됩니다. 그래서 자사의 IIS + .NET Framework 기술로 동작하는 silverlight를 출시하기에 이릅니다. 이러한 멀티 미디어의 지원과 더불어, RDBMS에 특화된 fat client들이 나오게 되는데, PowerBuilder가 바로 그것입니다. 
fat client의 발전은 기존 servlet과 asp .net 기술과 충돌을 일으키게 됩니다. 단순히 servlet과 asp .net을 fat client의 container로 이용하게 되고 모든 로직을 fat client에서 처리하게 되는 개발 방법이 한 때 유행하게 됩니다.


3 tier, n tier 시기

2 tier 가 발전한 형태인 3 tier에서는 다양한 요구사항이 생기게 됩니다. 기존 2 tier system의 확장을 통한 여러 시스템간의 결합으로 최종적으로는 enterprise system으로의 진화, 발전이 바로 그것입니다. 기존 2 tier system에서는 해결할 수 없는 기술적 이슈들이 발생하게 됩니다. 다양한 시스템들이 나오게 되고 그 시스템들간의 상호 통신에 의하여 기존 2 tier 에서 n tier로 계속되는 발전이 이루어지게 됩니다. 이 때는 다음과 같은 기술들이 발전하기 시작합니다.

script language의 발전

기존 .net과 java와는 다른 언어들이 발전하게 됩니다. 기존의 asp와 php가 OOP적 성격을 갖지 못한 단점을 해결하고 OOP적 장점과 개발의 편의성을 극대화한 script language가 발전하게 됩니다. python, ruby가 대두되기 시작하지요. 이 언어들은 기존의 객체 지향적인 특징과 asp, php와 같은 script 적 성격을 모두 갖게 됩니다. 개발의 속도, 변화 가능성에 대한 열린 대응을 토대로 이와 같은 script language가 계속된 발전을 하게 됩니다. 해외의 많은 시스템들이 하부 tier의 경우에는 ruby, python으로 개발된 것을 지금도 자주 볼 수 있습니다.

web service의 개발

n tier system의 발전은 web service가 없었다면 불가능하다고 할 정도로 web service는 n tier system에 깊이 관여되어 있습니다. web service는 기존의 tier를 종적으로나 횡적으로 모두 확장을 시키는 가장 결정적인 역활을 하게 되는 계기가 되었습니다. "Service로서의 Web"에 대한 개념은 수많은 파생적 개념을 만들어 내고, 기술적 발전을 가지고 왔습니다. 기존에는 SOAP을 기반으로 한 web service가 주로 사용되었으나, 지금은 client(javascript) 등에서의 호출 문제로 인하여 xmlrpc의 경량화된 버젼인 REST를 주로 사용하고 있습니다. 이 REST에 대해서는 다시 한번 설명할 기회를 갖도록 하겠습니다.

MVC의 발전

기존의 웹의 개발은 단순히 DB의 결과값을 웹에 표현하는 방식이였습니다. 대부분의 Business Logic은 DB에서 가지고 있고, 그 Business Logic을 web에 표현하는 방식이 대부분이였지요. 이렇게 된 가장 큰 이유중 하나는 웹으로 개발을 한 내용은 웹에 너무나 밀착되어있는 프로그램이라서, Business Logic만의 테스트가 거의 불가능하다는 것에서 시작되었습니다. 그렇지만, n tier system으로 발전해나가면서 tier만의 중복된 Business Logic을 정리 및 관리하는데 있어서 기존 프로그래밍 언어로 하고자 하는 욕구가 계속해서 발전되어 갑니다. SP에 대한 Handling과 계속되는 개발은 필연적인 중복코드와 로직의 누수가 발생하기 마련이니까요.

이때, ruby를 기반으로 한 ruby on rails가 발표됩니다. rails framework라고도 불리우는 이 framework는 기존의 web 개발 방법을 완전히 바꿔놓게 됩니다. 사용자에게 보여지는 영역인 View, Http response/request를 처리하는 Controller 마지막으로 Domain의 Business Logic을 처리하는 Model로 완벽한 영역을 분리할 수 있음을 ruby on rails는 보여주게 됩니다. 이러한 장점은 각 영역을 개발자들이 테스트를 해볼 수 있고, 영역에 대한 전문화를 분리시킬 수 있기 때문에 단숨에 웹 개발의 주도적 방법으로 대두되었습니다. java에서는 struct2가, .net에서는 asp .net mvc가 기존 ruby on rails의 사상을 반영한 MVC web framework입니다.

개발자는 지금 MVC를 중요하게 봐야지 됩니다. MVC 개발 방법은 웹뿐 아니라, 모든 application에 적용되어 있는 상황이고 각 영역에 대한 테스트를 통해서 자신의 코드의 완벽성을 스스로 검증할 수 있는 기회를 가지게 되었습니다. 에러에 대한 명확한 정의가 가능하게 되었으며, 에러가 발생했을때의 장애 처리와 같은 새로운 프로세스 정립 역시 MVC의 확립으로 가능하게 되었습니다. 이 부분은 지금까지는 개발자가 아닌, 기획이나 의사결정자들의 손에 있던 부분이였지만, 지금은 개발자들이 제시하는 방법을 선택하는 방향으로 전환이 된 상태입니다.

mobile 기기의 발전 및 확산

기존 n tier system의 발전은 획적 확장에 해당된다면 mobile 기기의 발전 및 확산은 종적 확장에 해당됩니다.기존까지 있던 web application의 대상은 모두 PC의 browser를 대상으로 하고 있었습니다. PC를 기반으로 하고 있기 때문에 PC의 특정 browser만을 target으로 하고 개발이 가능해졌지요. 그렇지만, iPhone을 시작으로 한 mobile 기기의 발전은 이러한 생각을 모두 바꾸어놓게됩니다. 누구나 가지고 다니는 mobile 기기는 언제 어디에서나 접근이 가능한 특징을 가지고 있습니다. 이는 기존보다 많은 접속을 만들어 내고, 모든 device에서 동일하게 보여야지 된다는 문제를 가지고 오게 됩니다. 그리고, mobile device의 빈약한 시스템 자원과 기존 windows system과 다른 OS 환경으로 인하여 HTML의 표준화에 대한 요구 사항이 높아지게 됩니다.

fat client의 쇠퇴와 HTML5의 대두

기존 flash와 silverlight와 같은 fat client가 mobile device에서 정상적으로 동작하지 않는 문제가 발생하게 됩니다. 이는 기존 PC에서도 windows-IE 환경 이외에서도 계속 지적되던 문제였지만, 본격적인 문제로서 국내에서는 대두되기 시작한 것은 바로 mobile 기기의 확산때문입니다. 기존 HTML에서 지원되지 않던 multimedia에 대한 지원을 비롯하여 animation과 websocket등의 표준화로 인하여 기존 flash와 silverlight가 설 자리가 없어지기 시작합니다. 지금 fat client는 flash만을 제외하고 거의 사장되어가는 분위기입니다. Flash의 경우에도 HTML5의 확산 전까지 잠시의 대체제로서의 의미 이외에는 퇴색해가는 것이 현실입니다. 기존 Flex 개발자들이 설 자리가 많이 없어지고 있지요.

Big data와 Cloud 의 대두

끊임없이 이야기가 나오고 있는 Big data와 Cloud는 mobile 기기의 발전 및 확산과 기존 web system의 오랜 발전으로 인하여 나온 기술이라고 할 수 있습니다. 기존의 RDBMS에서는 처리를 할 수 없을 정도의 데이터가 이제는 수집이 된 상태이고, 이 데이터들을 어떻게 분석을 해야지 될지. 이 데이터들을 어떻게 활용을 해야지 될지. 그리고 많은 mobile 기기에서 동시 다발적으로 들어오는 데이터를 어떻게 해야지 될지에 대한 물음에서 Big Data와 Cloud를 활용하는 방법을 찾아보는것이 방법일것 같습니다. 이러한 Big Data를 저장하기 위한 방법으로 cassandra, mongoDB, 등이 있으며 Big Data를 처리하기 위한 방법으로 Hadoop이 대두되게 됩니다. 또한 Cloud 시장은 아직 춘추전국의 시대와 같이 복잡한 상황이며, 시장 1위인 amazon의 EC2에 MS의 Azure, Google의 Google Cloud Platform등이 경쟁을 하고 있습니다.

Open source의 대두

기존에는 특정 회사의 특정 Solution 위주의 시스템에서부터 Open Source를 조합한 Open Platform이 대두됩니다. stacktrace, github 등의 사이트에서 여러 개발자들이 참여한 open source들은 어머어마한 양과 회사들이 만든 Solution보다 더 뛰어난 성능을 자랑하며 모든 시장을 휩쓸고 있습니다.

이러한 기술적 흐름속에 개발자가 익혀야지 될 기술은 어마어마하게 많은 것이 사실입니다. 그렇지만, 지금(2013)년을 기준으로 우리가 해야지 되는 기술들은 조금 목표가 정해질 수 있습니다. 어찌보면 지금까지 서론을 이야기해왔다면, 이제는 어떤 기술을 익혀야지 되고, 어떤 기술을 중심으로 다른 기술들을 곁다리로 붙여서 발전해나가야지 되는지에 대한 이정표가 될 수 있겠지요.


웹 개발자가 익혀야지 될 기술들 - 2013년 기준

.net보다는 java

대한민국에서 개발자로 먹고 살기 위해서는 .net은 이미 밀렸습니다. 행안부 기준의 정부표준 프레임워크가 java로 발표가 되고, 정부 표준 프레임워크로 모든 SI 사업이 진행되어가고 있는 현 상황에서 더이상 .NET을 한다는 것은 이제는 개발자로서의 자신을 스스로 학대하는 일 그 이상, 그 이하도 안되게 되었습니다. 일단 java에 집중하는 것이 맞습니다. 그리고 세계적으로도 java의 놀라운 발전은 이제 JVM의 성능이 Ch2.의 성능까지 따라왔다는 이야기가 나올정도로 최적화 및 향상이 되었습니다. 더이상 느리다는 이야기가 나오지를 않는 추세지요.

spring

정부표준프레임워크의 핵심입니다. 정부표준프레임워크는 spring으로 구성이 되어 있고, spring으로 돌아갑니다. 일단 spring을 잘 할 줄 알면, 정부 표준 프레임워크의 반이상은 해결하고 들어간다고 보면 됩니다. spring에 대한 소개에서 다시 이야기를 하겠지만, spring 자체가 java의 표준화에 영향을 주고 있고, java의 표준 자체가 spring이 되어가고 있는 현실입니다. 이 추세로 간다면 spring은 하나의 open source framework가 아닌 java의 일부분이 될 수 있을 것 같습니다.

DB 기술

dao(data access object) 기술은 오래되었지만, 그 기술을 표현하는 방식은 계속해서 바뀌어왔습니다. DB의 특정 데이터들을 보이기 위한 VO로 접근하는 방식과 DB역시 객체화 하여 객체적으로 접근을 하는 ORM 방식으로 크게 나뉘고 있습니다. 현장에서는 국내는 VO 방식을 더 많이 사용하고 있으나, 세계적으로는 ORM 방식이 압도적으로 많이 사용하고 있습니다. 두가지 모두 알아야지 됩니다.

Controller 기술

spring의 세부 기술중 하나입니다. spring에 대해서 잘 안다면...에 포함되는 기술 영역이긴 하지만, HTTP가 어떻게 활용되어가는지에 대한 이해가 필요합니다. 이는 Servlet Container(ex:tomcat)에 대한 이해와도 같이 연결이 됩니다. web이 돌아가는 시스템에 대한 이해라고 할 수 있습니다.

Modeling 기술

Modeling은 소위 말하는 '업무분석' 과정입니다. 업무를 분석하고 분석한 업무를 programming language로 구현 가능한 형태로 추상화를 시키는 과정을 의미합니다. 개발자의 생각의 방향은 학교에서 자주 들은 Divide & conquer 입니다. 작게 나누고, 하나하나 해결해나가고. 그 해결한 조그마한 것들을 다시 붙이는 작업들이 필요합니다.

개발 방법론

소프트웨어 공학은 매우 변화가 심한 학문입니다. 건축학에서 많은 개념들을 차용해왔으나, 지금은 건축학과는 많이 다르다는 것을 인지하고 접근하고 있습니다. 요즘 S/W 개발 방법론의 추세는 agile 개발 방법론입니다. agile에 대해서는 차후에 보다 더 심도있게 짚어보도록 하겠습니다.

패턴 (Pattern)

개발 방법론과 매우 유사한 분야입니다. 개발을 할때, 코드를 최적화 하는 패턴들이 존재합니다. 패턴을 익히는 것보다는 패턴을 이용해서 개발자들끼리의 의사소통이 가능해져야지 됩니다. 개발자들끼리 사용하는 약어가 되는 경우가 꽤나 많습니다. 그리고 책을 통해서 학습을 할때도 유용합니다.

단위 테스트 (Unit Test)

개발자는 자신이 만든 모든 s/w를 테스트할 수 있어야지 되고, 그 테스트한 결과로서 자신의 s/w의 품질을 증명할 수 있어야지 됩니다. "내가 하면 되었는데.", "어제는 되었는데." 식의 이야기는 곤란합니다. in/out이 결정나면 그 in/out에 대한 명확한 테스트를 하고 그 테스트 결과를 보여줄 수 있어야지 됩니다. 또한 s/w의 규모가 크면 클수록 그 코드를 검증할 수 있어야지 됩니다. 이제는 테스트를 돌리기 위해서 개발을 한다. 라는 말이 나올 정도로 테스트는 일반화된 기술입니다. 자동화된 테스트를 구성하는 능력은 반드시 필요합니다.



Posted by Y2K
,

CheckStyle Rule 정의 목록

Java 2013. 5. 13. 14:50

사내의 코드 품질 평가를 위해서 static analysis 방법을 조사하면서 항목에 대해서 list up을 할 필요성이 발견되어 조사한 내용들을 공유합니다. 


항목

원인

회피방법

사용여부

비고

JavadocPackage

모든 method, class에는 help 존재해야지 된다.

시간상 힘들고, 관리되지 않는 주석은 더욱 혼란을 가지고 온다. method 이름 규칙으로 대신하기로 한다.

X

NewlineAtEndOfFile

java code 가장 마지막 줄은 빈공백열로 마쳐져야지 된다.

마지막 line에는 항시 빈공백을 넣는다.

O

Translation

Properties file 이용한 경우, 국가별 번역이 모두 존재해야지 된다.

국가별 번역 파일을 따로 만들거나 default 문자열만을 이용한다.

O

FileLength

java file length 2000 line 넘지 않도록 작성한다.

2000 line 넘어가는 경우, 설계상의 문제가 있기 때문에 class 재정의한다.

O

FileTabCharacter

java file 내부에 tab 문자열이 있으면 안된다.

tab 모두 space 치환해서 사용하도록 한다.

O

RegexpSingleline

1 line에는 한개의 method만이 존재해야지 된다.

1 line 대한 설정을 명확하게 해서 사용하도록 한다.

O

ConstantName, LocalFinalVariableName, LocalVariableName, MemberName, MethodName, PackageName

상수, class, method, parameter, package 대한 naming 규칙이 틀릴 경우 발생한다.

naming 규칙에 맞는 명명법을 사용한다.

O

AvoidStarImport

package안에 있는 모든 객체들을 import할때 발생한다.

package안에서 사용되는 객체만을 import 한다.

O

테스트 코드 작성시에는 예외로 한다.

UnusedImports

package안에 사용하지 않는 객체를 import하면 발생한다.

package안에 사용되지 않는 객체들은 import 하지 않는다.

O

테스트 코드 작성시에는 예외로 한다.

LineLength

Line 길이가 80자가 넘는 경우 발생한다.

Line 길이를 120자로 수정해서 사용한다.

O

80 -> 120

MethodLength

Method 길이는 150자가 넘는 경우 발생한다.

Method 길이를 150 이내로 사용한다.

O

ParameterNumber

Method parameter 7개가 넘지 않도록 한다.

Method안의 input parameter 갯수를 제한한다.

O

ModifierOrder

Method 앞에 붙는 order 다음과 같은 순서를 갖는다. (public, abstract, static, final, transient, volatile, synchronized, native, strictfp)

순서를 따르도록 한다.

O

AvoidNestedBlocks

내부 {} 사용하지 않는다. - switch 구문 제외

내부 {} 사용하지 않는다.

O

EmptyBlock

{}안에 아무런 구문이 없는 경우에 발생한다.

{}안에 구문이 없는 경우, 제거한다.

O

LeftCurly

'{' interface method 구현문 끝에 넣어준다.

이집트 표기법을 사용하도록 한다.

O

NeedBraces

code안의 {} 반드시 짝이 맞아야지 된다.

Compile error 발생하지 않도록 만들어준다.

O

RightCurly

'}' 뒤에는 반드시 CRLF만이 존재해야지 된다.

이집트 표기법을 사용하도록 한다.

O

AvoidInlineConditionals

1 line에서 if 문을 이용해서 처리하지 않는다.

condition 제거하도록 한다.

X

1 line에서 가독성이 높은 경우가 존재한다.

EmptyStatement

for loop문에서 무한 loop 발생시킬 있는 empty statement 존재한다.

반드시 for loop 경우에는 statement 존재한다. loop 안에서 조건이 걸리는 경우, while문을 사용하도록 한다.

O

EqualsHashCode

equals(), hashCode() 어느하나 override 경우, 둘다 재정의 되어야지 된다.

반드시 method 쌍으로 재정의 하도록 한다.

O

IllegalInstantiation

boolean, String 같이 java 기본 type 재정의하는 경우 발생

java 기본 type 그대로 사용하도록 한다.

O

InnerAssignment

if문이나 toString() 같은 내부에서 변수에 값을 할당한다.

값의 할당은 따로 line 잡아서 사용하도록 한다.

O

MagicNumber

상수값을 사용하는 경우 발생한다.

상수값을 static final 이름을 지정해서 사용한다.

O

MissingSwitchDefault

switch 문에 default case 없는 경우에 발생한다.

switch문은 반드시 default case 넣어서 작성한다.

O

RedundantThrows

try-catch 시에 throws 순서로 인하여 실행될 없는 catch문이 존재한다.

catch 만들때, exception 상속 상태를 확인하고 구성하도록 한다.

O

SimplifyBooleanExpression

if문내에서 1 line으로 return한다.

if 문안에서 return 하지 않고, return값에 대한 명명을 정확히 한다.

O

SimplifyBooleanReturn

if문의 결과를 그대로 return 한다.

if문의 로직 자체를 return 값으로 변경한다.

O

DesignForExtension

객체는 확장 가능하도록 되어야지 되고, public문은 반드시 final 재정의 되는 것을 막아줘야지 된다.

spring 사용하는 경우 Proxy aspectJ 의해서 재정의 되는 method 다음 규칙에서 에러를 발생시킬 있기 때문에 사용하지 않는다.

X

FinalClass

final class 생성자가 private 되어있는 경우 발생한다.

final class 경우에는 특별한 경우를 제외하고 사용하지 않는다.

O

spring 이용하는 경우에는 특히 사용할 필요가 없는 구성이다.

HideUtilityClassConstructor

public static method만이 존재하는 class 생성자가 protected, private 되어 있다.

UtilityClass 경우에는 모두 public modifier 이용한다.

O

InterfaceIsType

interface type만이 존재하고, method 존재하지 않는다.

type descript interface 사용하지 않는다.

O

VisibilityModifier

getter/setter 사용하지 않고, 내부 변수에 접근 가능하다.

getter/setter 이용해서 property 처리를 하도록 한다.

O

ArrayTypeStyle

java style input array parameter 이용한다. (java style : main(String[] args), C style : main(String args[])

java style 사용하도록 한다.

O

FinalParameters

input parameter 내부에서 참조만 하는 경우, final 선언한다.

모든 input parameter final 사용하는 것을 기본 원칙으로 갖는다.

O

coding style 밀접한 연관이 있다.

TodoComment

"TODO: " 정확히 사용하지 않는 경우에 발생한다. (대소문자, 공백위치)

TODO 정확히 사용한다.

O

UpperEll

'L', '1', 'I', 'i' 명확히 구분할 있도록 method 이름과 객체이름을 짓는다.

명명규칙에 따라 이름을 작성하도록 한다.

O

Posted by Y2K
,

Spring 3.1에서 강화된 @Configuration을 사용한 설정에서 재미있는 @EnableXX annotation을 이용한 @EnableOrm을 만들어보고자한다. 

먼저, 요구사항

1. 기본적으로 BoneCP를 이용
2. packagesToScan 을 통해서 entity가 위치한 package를 지정해줄 수 있어야지 된다.
3. Hibernate와 Hibernate를 이용한 JPA를 모두 지원하는 형태로 구성한다.
4. Ehcache를 사용할 수 있어야지 된다. 

기본적으로 구성되는 pom.xml의 구성은 다음과 같습니다. 

        <dependency>
            <groupId>com.jolbox</groupId>
            <artifactId>bonecp</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-sql</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>provided</scope>
        </dependency>

먼저, @EnableOrm interface는 다음과 같이 정의 될 수 있습니다. 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(OrmConfigurationSelector.class)
public @interface EnableOrm {
    String[] packagesToScan() default {};

    boolean enableCache() default false;

    boolean showSql() default false;

    OrmType ormType() default OrmType.Hibernate;
}

public enum OrmType {
    Hibernate, Jpa;
}

packageToScan과 enableCache, 그리고 console 창에 sql query문을 출력해야지 되는 경우를 기본적으로 고려해줄 수 있습니다.  그리고 OrmType을 통해서 기본 Hibernate를 이용한 Orm과 Jpa를 이용하는 두가지를 모두 사용할 수 있도록 합니다.  OrmType에 따라 각각 Load되는 Configuration이 바뀌어야지 되기 때문에 Import class는 ImportSelector를 구현한 객체여야지 됩니니다. 

ImportSelector를 구현한 객체는 다음과 같이 구성됩니다. 

public class OrmConfigurationSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> metadata = importingClassMetadata.getAnnotationAttributes(EnableOrm.class.getName());
        OrmType ormType = (OrmType) metadata.get("ormType");
        if (ormType == OrmType.Hibernate) {
            return new String[] { HibernateConfiguration.class.getName() };
        } else {
            return new String[] { JpaConfiguration.class.getName() };
        }
    }
}

그리고, Hibernate를 이용할 때와 JPA를 이용할 때의 공통 코드가 존재하는 abstract 객체를 하나 만들어주는 것이 좋을 것 같습니다. 기본적으로 Hibernate JPA를 사용할 예정이기 때문에 공통적으로 DataSource와 Hibernate Property는 완벽하게 중복되는 코드가 만들어질테니까요. 
공통 Configuration객체인 OrmConfiguration객체의 기능은 다음과 같습니다.

1. BoneCP datasource 제공
2. Hibernate Property의 제공
3. enableCache, packateToScan, showSql 등 protected 변수의 제공

OrmConfiguration 객체는 다음과 같이 구성될 수 있습니다. 

public abstract class OrmConfiguration implements ImportAware {
    public static final String HIBERNATE_DIALECT = "hibernate.dialect";
    public static final String CONNECT_USERNAME = "connect.username";
    public static final String CONNECT_PASSWORD = "connect.password";
    public static final String CONNECT_DRIVER = "connect.driver";
    public static final String CONNECT_URL = "connect.url";

    public static final String HIBERNATE_SHOW_SQL = "hibernate.show_sql";
    public static final String ORG_HIBERNATE_CACHE_EHCACHE_EH_CACHE_REGION_FACTORY = "org.hibernate.cache.ehcache.EhCacheRegionFactory";
    public static final String HIBERNATE_CACHE_USE_QUERY_CACHE = "hibernate.cache.use_query_cache";
    public static final String HIBERNATE_CACHE_USE_SECOND_LEVEL_CACHE = "hibernate.cache.use_second_level_cache";
    public static final String HIBERNATE_CACHE_REGION_FACTORY_CLASS = "hibernate.cache.region.factory_class";

    @Autowired
    protected Environment env;

    protected boolean showSql;
    protected boolean enableCache;
    protected String[] packagesToScan;

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));
        dataSource.setDriverClass(env.getProperty(CONNECT_DRIVER));
        dataSource.setJdbcUrl(env.getProperty(CONNECT_URL));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    public abstract PlatformTransactionManager transactionManager();

    protected Properties getHibernateProperties() {
        Properties properties = new Properties();
        properties.put(HIBERNATE_DIALECT, env.getProperty(HIBERNATE_DIALECT));
        if (enableCache) {
            properties.put(HIBERNATE_CACHE_REGION_FACTORY_CLASS, ORG_HIBERNATE_CACHE_EHCACHE_EH_CACHE_REGION_FACTORY);
            properties.put(HIBERNATE_CACHE_USE_SECOND_LEVEL_CACHE, true);
            properties.put(HIBERNATE_CACHE_USE_QUERY_CACHE, true);
        }
        return properties;
    }

    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        if (env.getProperty(HIBERNATE_DIALECT) == null || env.getProperty(CONNECT_USERNAME) == null
                || env.getProperty(CONNECT_PASSWORD) == null || env.getProperty(CONNECT_DRIVER) == null
                || env.getProperty(CONNECT_URL) == null) {
            throw new IllegalArgumentException("properties is not completed! check properties (hibernate.dialect, "
                    + "connec.username, connect.password, connect.driver, connect.url)");
        }
        Map<String, Object> metaData = importMetadata.getAnnotationAttributes(EnableOrm.class.getName());
        enableCache = (boolean) metaData.get("enableCache");
        packagesToScan = (String[]) metaData.get("packagesToScan");
        showSql = (boolean) metaData.get("showSql");
    }
}

기본적인 Property들은 모두 Properties 파일에 정의되지 않으면 에러가 발생하도록 객체들을 구성하였습니다. 이제 HibernateConfiguration을 한번 구성해보도록 하겠습니다. 

@Configuration
@EnableTransactionManagement
public class HibernateConfiguration extends OrmConfiguration implements HibernateConfigurer {
    private static final String HIBERNATE_SHOW_SQL = "hibernate.show_sql";

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        setLocalSessionFactoryBean(sessionFactory);
        return sessionFactory;
    }

    @Override
    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory().getObject());
        return transactionManager;
    }

    @Override
    public void setLocalSessionFactoryBean(LocalSessionFactoryBean localSessionFactoryBean) {
        Properties properties = getHibernateProperties();
        if (showSql) {
            properties.put(HIBERNATE_SHOW_SQL, "true");
        }
        localSessionFactoryBean.setHibernateProperties(properties);
        localSessionFactoryBean.setPackagesToScan(packagesToScan);
    }
}


기본적으로 항시 사용되는 SessionFactory와 그에 대한 설정부분을 Load 시켜주고, PlatformTransactionManager를 return 시켜주는 매우 단순한 @Configuration class입니다. 

이제 JpaConfiguration입니다. 
@Configuration
@EnableTransactionManagementpublic class JpaConfiguration extends OrmConfiguration implements JpaConfigurer {
    @Override
    public void setEntityManagerFactoryBeanProperties(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
        entityManagerFactoryBean.setPackagesToScan(packagesToScan);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        setEntityManagerFactoryBeanProperties(entityManagerFactory);
        entityManagerFactory.setDataSource(dataSource());
        entityManagerFactory.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactory.setJpaProperties(getHibernateProperties());
        return entityManagerFactory;
    }

    @Bean
    public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(showSql);
        return hibernateJpaVendorAdapter;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslation() {
        return new PersistenceExceptionTranslationPostProcessor();
    }

    @Override
    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }
}


둘의 코드는 거의 완전히 동일합니다. Hibernate를 이용할 것인가, 아니면 Jpa를 이용할 것인가에 대한 기본적인 차이만이 존재합니다. 
테스트 코드 구성은 다음과 같습니다. 

@Configuration
@EnableOrm(ormType = OrmType.Hibernate, enableCache = true, packagesToScan = "com.xyzlast.domain.configs", showSql = true)
@PropertySource(value = "classpath:spring.properties")
@ComponentScan("com.xyzlast.domain.repositories")
public class TestHibernateConfiguration {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        Properties properties = new Properties();
        properties.setProperty("org.jboss.logging.provier", "slf4j");
        configHolder.setProperties(properties);
        return configHolder;
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestHibernateConfiguration.class)
public class HibernateConfigurationTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Before
    public void setUp() {
        assertThat(applicationContext, is(not(nullValue())));
    }

    @Test
    public void dataSource() {
        DataSource dataSource = applicationContext.getBean(DataSource.class);
        assertThat(dataSource, is(not(nullValue())));
    }

    @Test
    public void transactionManager() {
        PlatformTransactionManager transactionManager = applicationContext.getBean(PlatformTransactionManager.class);
        assertThat(transactionManager, is(not(nullValue())));
    }

    @Test
    public void sessionFactory() {
        SessionFactory sessionFactory = applicationContext.getBean(SessionFactory.class);
        assertThat(sessionFactory, is(not(nullValue())));
    }
}

이제 Hibernate와 JPA에 따른 각각의 configuration을 따로 구성하지 않아도 되는 멋진 코드가 만들어졌습니다.
회사에서 다른 팀원들이 사용할 수 있도록 jar를 만들어서 사내 nexus 서버에 올려야지 되겠습니다. ㅋㅋ




Posted by Y2K
,