주요글: 도커 시작하기
반응형
Template Method 패턴을 JDBC 코드에 적용함으로써 중복 코드를 없애본다.

Template Method 패턴의 구현방식

Template Method 패턴은 거의 동일한 코드로 구성된 메소드가 여러 클래스에 존재할 때 사용된다. 예를 들어, DB로부터 사용자 정보를 읽어와 로그인을 수행하는 DBLogin 클래스와 중앙서버로부터 사용자 정보를 읽어와 로그인을 수행하는 SSOLogin 클래스가 있다고 가정해보자. 이 두 클래스는 사용자 정보를 읽어오는 부분의 코드만 다를 뿐 나머지 코드는 완전히 동일하다. 예를 들어, 두 클래스의 login 메소드는 다음과 같이 비슷할 것이다.

    public void login() {
        Dialog loginDialog = new LoginDialog();
        while(true) {
            ...
            if (loginDialog.isCancel()) {
                return;
            }
            String userID = loginDialog.getUserID();
            String password = loginDialog.getPassword();
            
            try {
                // 사용자 정보 로딩
                // 입력한 정보와 동일한지 검사
                // 동일한 경우 인증성공
                // 실패한 경우 예외 발생                
                
            } catch(Throwable e) {
                // 인증 처리 실패 메시지 출력!
            }
        }
    }

DBLogin 클래스와 SSOLogin 클래스는 인증을 처리하는 부분의 코드(위 코드에서 굵게 표시한 부분)만 다를 뿐 나머지 코드는 완전히 동일하다. 이렇게 거의 모든 코드가 동일하고 일부 코드만 다를 때 사용되는 패턴이 Template Method 패턴이다.

앞서 작성한 DBLogin 클래스와 SSOLogin 클래스에서 Template Method 패턴을 적용하려면 먼저 이 두 클래스가 공통으로 상속받을 클래스인 AbstractLogin 클래스를 작성해야 한다. AbstractLogin 클래스는 다음과 같은 코드를 갖는다.

    public abstract class AbstractLogin {
        public void login() {
            Dialog loginDialog = new LoginDialog();
            while(true) {
                ...
                if (loginDialog.isCancel()) {
                    return;
                }
                String userID = loginDialog.getUserID();
                String password = loginDialog.getPassword();
                
                try {
                    authenticate(userId, password);
                } catch(Throwable e) {
                    // 인증 처리 실패 메시지 출력!
                }
            }
        }
        
        public abstract void authenticate(String userId, String password)
        throws Throwable;
    }

AbstractLogin 클래스의 login() 메소드는 앞서 살펴봤던 login() 메소드와 거의 동일하다. 차이점이 있다면 인증 부분의 처리를 authenticate() 메소드를 통해서 수행한다는 점이다. 여기서, login() 메소드가 템플릿 메소드에 해당하며, authenticate() 메소드는 서로 다른 인증 코드를 수행하게 된다.

이제 남은 작업은 DBLogin 클래스와 SSOLogin 클래스가 AbstractLogin 클래스를 상속받도록 한 뒤, 알맞게 authenticate() 메소드를 구현하는 것이다. 예를 들어, DBLogin 클래스는 다음과 같이 구현할 수 있을 것이다.

    public class DBLogin extends AbstractLogin {
        
        public void authenticate(String userId, String password) throws Throwable {
            // DB로부터 사용자 정보를 읽어온다.
            // 암호화 아이디를 비교한다.
            ....
        }
    }

SSOLogin 클래스도 역시 authenticate() 메소드는 알맞게 구현해주면 된다.

Template Method 패턴을 알맞게 설계하면, 개발자는 하위 클래스에서 기본 구조를 그대로 사용하면 특정한 부분의 코드만 구현하면 된다. 즉, 어느 정도 로직 흐름의 보호를 받게 된다.

꼭 추상 클래스에 템플릿 메소드를 만들고 하위 클래스에서 구현하는 방식을 사용할 필요는 없다. Template Method 패턴의 핵심은 템플릿 메소드가 있고, 템플릿 메소드에서 다르게 실행되어야 할 부분을 쉽게 설정할 수 있다는 데이 있다. (상속을 사용하는 경우에는 특정 메소드를 호출함으로써 이를 구현한 것이다.) 뒤에서 JDBC 코드에 Template Method를 적용하는 방법을 살펴볼 것인데, 여기서는 추상 클래스를 상속받는 방식이 아닌 외부의 코드를 실행하는 방식을 사용할 것이다.

Template Method 패턴을 JDBC 코드에 적용하기

Template Method 패턴은 동일한 구조를 갖는 코드에 아주 유용하게 사용할 수 있다. 특히 try-catch 블럭이 전체 코드의 30~40%를 차지하는 JDBC 프로그래밍에서는 Template Method 패턴을 응용해서 적용함으로써 상당부분의 불필요한 코드를 줄일 수 있게 된다.

JDBC 코드의 중복

JDBC 프로그래밍을 하면 다음과 같은 형태의 코드를 많이 사용하게 된다.

    Connection conn = null;
    Statement stmt = null;
    try {
        conn = DriverManager.getConnection(...);
        stmt = conn.createStatement();
        int count = stmt.executeUpdate(someQuery);        ...
    } catch(SQLException ex) {
        // 예외 처리
    } finally {
        if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    }

위 코드에서 로직과 관련된 코드는 전체 코드 중에서 파란색으로 표시한 부분이다. (위 코드에선 단지 한줄이다.) 나머지 코드는 JDBC 프로그래밍을 하기 위해 커넥션을 얻고, 처리가 완료된 뒤 자원을 반환하는 등의 로직과 관련없는 코드들이다. 상황에 따라 다르겠지만 일반적으로 JDBC 프로그래밍에서 많게는 코드의 70~80%, 적게는 30~40%의 코드가 로직과 관련없는 코드로 구성된다.

그런데, JDBC 프로그래밍의 대부분은 다음과 같이 중복된 코드를 갖는다.

    try {
        // 커넥션 생성
        
        [로직실행]        
    } catch(SQLException ex) {
        // 예외 처리
    } finally {
        // 자원반환
    }

이 시점에서 뭔가 떠오른 사람들도 있을 것이다. 바로 위의 코드는 JDBC 프로그래밍에서 늘 사용되는 템플릿 형태의 코드라는 점이다. 이는 곧 JDBC 프로그래밍을 할 때 Template Method 패턴을 응용함으로써 개발자는 try-catch-fianlly와 관련된 코딩을 상당량 없애고 로직코드를 작성하는 데에 집중할 수 있게 된다.

Template Method 패턴 적용1: Statement 단위 적용

먼저 살펴볼 내용은 Statement의 코드 중복을 없애는 방법이다. Statement.executeUpdate()를 실행하는 코드는 다음의 형태를 갖게 된다.

    Statement stmt = null;
    try {
        stmt = conn.createStatement();
        // 로직과 관련된 코드
        int count = stmt.executeUpdate(
                        "update member set email = 'a@a.com' where memberid = 'a'");
    } finally {
        if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
    }

여기서 바뀌는 부분은 Statemet.executeUpdate()에 전달되는 SQL 쿼리이다. 따라서, 템플릿을 제공하는 클래스를 다음과 같이 작성할 수 있을 것이다.

    public class QueryTemplate {
        public int executeUpdate(Connection conn, String query) throws SQLException {
            Statement stmt = null;
            try {
                stmt = conn.createStatement();
                return stmt.executeUpdate(query);
            } finally {
                if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
            }
        }
    }

위 QueryTemplate 클래스를 사용해서 다시 코드를 작성해보면 다음과 같이 간단하게 작성된다.

    QueryTemplate template = new QueryTemplate();
    int count = template.executeUpdate(conn,
                "update member set email = 'a@a.com' where memberid = 'a'");

PreparedStatement를 사용한 방식도 비슷하게 처리할 수 있다. 예를 들어, 다음과 같은 템플릿 메소드를 추가할 수 있을 것이다.

    public int executeUpdate(Connection conn, String query, Object[] parameters) 
    throws SQLException {
        PreparedStatement stmt = null;
        try {
            stmt = conn.prepareStatement(query);
            for (int i = 0 ; i < parameters.length ; i++) {
                if (parameters[i] instanceof String) {
                    stmt.setString(1, (String)parameters[i]);
                } else if (...) {
                    ...
                }
            }
            return stmt.executeUpdate();
        } finally {
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        }
    }

위의 추가된 템플릿 메소드는 다음과 같이 사용가능할 것이다.

    QueryTemplate template = new QueryTemplate();
    Object[] params = new Object[2];
    params[0] = "a@a.com";
    params[1] = "a";
    int count = template.executeUpdate(conn, "update member set email = ? " +
                       "where memberid = ?", params);

ResultSet과 관련된 템플릿 메소드는 다음의 형태를 띄게 될 것이다.

    public List executeQuery(Connection conn, String query) throws SQLException {
        Statement stmt = null;
        try {
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                List result = new java.util.ArrayList();
                do {
                    // ResultSet으로부터 값을 읽어와
                    // 알맞은 객체에 담은 뒤,
                    // 그 객체를 result에 추가
                } while(rs.next());
                return result;
            } else {
                return Collections.EMPTY_LIST;
            }
        } finally {
            if (rs != null) try { rs.close(); } catch(SQLException ex) {}
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        }
    }

ResultSet에 저장된 값을 어떤 형태로 가져올 것인가가 위 코드에서 이슈가 되는 부분이다. 몇가지 방식이 있을 수 있는데, 대표적인 방식으로 다음을 생각해 볼 수 있다.

  • ResultSet의 각 행의 값을 Object[]에 담기
  • ResultSet의 각 행의 값을 Map에 담기
  • ResultSet의 각 행의 값을 원하는 객체에 담기
이에 대한 자세한 내용은 여기서는 다루지 않겠다. Jakarta Commons DBUtil과 Jakarta Commons BeanUtils 같은 것들을 사용하면 좀더 쉽게 위의 기능을 구현할 수 있으니 구현에 관심이 있다면 참고해보기 바란다.

Template Method 패턴 적용2: Connection 단위 적용

앞에서 살펴본 코드는 Statement 단위의 코드였다. 따라서, Connection과 관련된 코드는 여전히 중복된 형태의 코드가 산재하게 된다. 예를 들어, 트랜잭션 처리를 하는 코드는 다음과 같아질 것이다.

    Connection conn = null;
    try {
        conn = .... // 커넥션을 생성
        conn.setAutoCommit(false);
        
        QueryTemplate template = new QueryTemplate();
        int count = template.executeUpdate(conn, "update member set ... ");
        int count1 = template.executeUpdate(conn, "update member_property set ... ");
        
        conn.commit();
    } catch(SQLException ex) {
        conn.rollback();
    } finally {
        if (conn != null) {
            conn.setAutoCommit(true);
            conn.close();
        }
    }

Connection과 관련된 코드도 중복을 없애고 싶은 충동이 일어날 것이다. 앞서 Statement나 PreparedStatement의 템플릿 코드는 실행할 쿼리와 할당할 파라미터 목록만 필요했기 때문에 코드가 단순했는데, Connection의 경우는 경우가 다르다. Connection의 트랜잭션을 시작(conn.setAutoCommit(true))하고 커밋(conn.commit())하는 사이에 코드가 정해지 있지 않기 대문이다.

이를 해결하려면 다음과 같이 트랜잭션 안에서 실행될 코드를 담은 객체를 전달받는 방법을 생각해볼 수 있다.

    public Object executeInTransaction(Worker worker) throws SQLException {
        Connection conn = null;
        try {
            conn = ... // 커넥션 구함
            conn.setAutoCommit(false);
            Object returnValue = worker.work(conn);
            conn.commit();
            
            return returnValue;
        } catch(SQLException ex) {
            conn.rollback();
        } finally {
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    }

여기서 worker 파라미터는 트랜잭션 안에서 실행될 코드를 담고 있는 객체로서, Worker는 다음과 같은 인터페이스가 될 것이다.

    public interface Worker {
        public Object work(Connection conn) throws SQLException;    }

이제 다음과 같이 Worker 인터페이스를 활용하여 트랜잭션 관련 템플릿 메소드를 사용할 수 있게 된다.

    QueryTemplate template = new QueryTemplate();
    List resultList = (List)template.executeInTransaction(new Worker() {
        public Object work(Connection conn) throws SQLException {
            QueryTemplate t = new QueryTemplate();
            int count = t.executeUpdate(conn, "query");
            List result = t.executeQuery(conn, somequery);
            ....
            return result;
        }
    });

Template Method 패턴을 응용함으로써 Connection의 10여줄이나 되던 트랜잭션 처리 관련 코드를 단 두줄로 줄일 수 있게 되었다. (한줄은 QueryTemplate을 생성하는 코드, 또 다른 한줄은 QueryTemplate.executeInTransaction() 메소드를 호출하는 코드이다.)

결론

본 글에서는 Template Method 패턴과 JDBC 프로그래밍에 Template Method 패턴을 적용함으로써 로직과 관련되지 않은커넥션 생성, Statement 생성, 자원 반환 등의 코드를 대폭 줄이고 로직관련 코드에 집중할 수 있다는 것을 살펴보았다.

Template Method 패턴은 JDBC 프로그래밍에서 뿐만 아니라 try-catch-finally의 형태를 지닌 대부분의 코드에 적용할 수 있다. 거의 대부분의 try-catch-finally 블럭은 내부의 로직 부분을 제외하면 중복된 코드로 구성되어 있는 경우가 많기 때문에 코드를 상당부분 줄일 수 있게 된다. 뿐만 아니라 로직 코드 위주로 코드가 작성되기 때문에 코드의 가독성도 높아지게 된다.

아마 이 글을 읽는 개발자중에는 본 글에서 보여준 Template Method 패턴의 응용에는 추상 클래스도 나오지 않고, 추상 클래스를 상속한 클래스도 나오지 않으므로 이는 Template Method 패턴이 아니라고 할지도 모르겠다. 하지만, 패턴에서 중요한 건 얼마나 그 패턴에서 소개한 클래스를 그대로 쓰느냐고 아니라, 패턴을 만들게 된 이유를 정확하게 알고 그 이유에 맞게 패턴을 적용하는 것이라고 생각한다. 그런 이유로, 본 글에서는 Template Method 패턴을 먼저 간단하게 소개하고, 그 응용 방법을 설명해보았다.

관련링크:


+ Recent posts