try/catch/finally로 예외 처리 및 전략 패턴으로 중복 컨텍스트 추출

2022. 1. 16. 23:07북리뷰/토비의 봄

728x90

이전 포스팅에 작성한 코드를 다시 살펴보자

public class UserDao {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws SQLException {
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values (?,?,?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.execute();

        ps.close();
        c.close();
    }
    public User get(String id) throws SQLException {
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("select * from users where id  = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();

        User user = null;
        if(rs.next()) {
            user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
        }

        if(user == null) throw new EmptyResultDataAccessException(1);

        rs.close();
        ps.close();
        c.close();

        return user;
    }

    public void deleteAll() throws SQLException {
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("delete from users");
        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public int getCount() throws SQLException {
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("select  count(*) from users");

        ResultSet rs = ps.executeQuery();
        rs.next();
        int count = rs.getInt(1);

        rs.close();
        c.close();
        return count;
    }
}

위 코드에는 심각한 문제점이 있다. 바로 예외상황에 대한 처리 이다
DB 커넥션이라는 제한적인 리소스를 공유해 사용하는 서버에서 동작하는 JDBC 서버에는 예외처리를 반드시 시켜야 한다. 중간에 어떤 이유로든 예외가 발생했을 경우라도 사용한 리소스는 반드시 반환하도록 만들어야 한다.
위 메서드 중에 deleteAll()을 살펴보자

public void deleteAll() throws SQLException {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("delete from users");
    ps.executeUpdate();

    ps.close();
    c.close();
}

이 메서드에는 Connection과 PreparedStatement라는 두 개의 공유 리소스를 가져와서 사용한다. 물론 정상적으로 처리되면 메서드를 마치기 전에 각각 close()를 호출해 리소스를 반환한다. 허나, 만약 PreparedStatement를 처리하는 중에 예외가 발생하면 어떻게 될까? 이때는 메서드 실행을 끝내지 못하고 바로 메서드를 빠져나가게 된다. 이때 문제는 Connection과 PreparedStatement의 close()의 메서드가 실행되지 않아서 제대로 리소스를 반환되지 않을 수 있다는 점이다.
그래서 이런 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있다. 예외 상황에서도 리소스를 제대로 반환할 수 있도록 try/catch/finally를 적용해보자

public class UserDao {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void add(User user) throws SQLException {

        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("insert into users(id, name, password) values (?,?,?)");

            ps.setString(1, user.getId());
            ps.setString(2, user.getName());
            ps.setString(3, user.getPassword());

            ps.execute();
        } catch (SQLException e) {
            throw e;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {

                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }
    }

    public User get(String id) throws SQLException {

        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("select * from users where id  = ?");
            ps.setString(1, id);

            rs = ps.executeQuery();

            User user = null;
            if (rs.next()) {
                user = new User();
                user.setId(rs.getString("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
            }
            if (user == null) throw new EmptyResultDataAccessException(1);

            return user;

        }catch (SQLException e) {
            throw e;
        }finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e){

                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {

                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }

    }

    public void deleteAll() throws SQLException {

        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("delete from users");
            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {

                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }
    }

    public int getCount() throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            c = dataSource.getConnection();
            ps = c.prepareStatement("select  count(*) from users");
            rs = ps.executeQuery();
            rs.next();
            return rs.getInt(1);

        }catch (SQLException e) {
            throw e;
        }finally {
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e){

                }
            }
            if (ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {

                }
            }
            if (c != null) {
                try {
                    c.close();
                } catch (SQLException e) {
                }
            }
        }
    }
}

이제 예외상황에서도 안전한 코드가 됐다. finally는 try 블록을 수행항 후에 예외가 발생하든 정상적으로 처리되든 상관없이 반드시 실행되는 코드를 넣을 때 사용한다.
테스트까지 수행해보고 이상이 없으나, 위 코드에 뭔가 아쉬운 점이 있다.

try/catch/finally 코드의 문제점

위 코드의 문제가 뭘까? 일단 보기만 해도 복잡하다. try catch문이 이중으로 중첩되어있는데다가 모든 메서드마다 반복된다.
물론, 예외처리 부분은 변경될 일이 많지 않다. 그래서 새로 메서드를 생성할 때 마다 예외처리 부분을 복붙해서 생성하면 된다. 허나, 실수로 한 줄을 뺴먹고 복사하거나 삭제했다면 어떻게 될까? 당장은 컴파일 에러가 나지 않겠지만, 커넥션이 반환되지 않고 쌓여나가다가 서버가 터지는 대참사가 발생할 수 있다.
이런 코드를 효과적으로 다룰 수 있는 방법은 없을까?

분리와 재사용을 위한 디자인 패턴 적용

일단, 비슷한 기능의 메서드에서 동일하게 나타날 수 있는 변하지 않고 고정되는 부분과, 각 메서드마다 로직에 따라 변하는 부분을 구분해야한다. deleteAll() 메서드를 예시로 들면

ps = c.prepareStatement("delete from users");

이 부분만 로직에 따라서 변동되는 부분이고, 나머지는 변하지 않는 부분이다. 변하지 않는 부분을 따로 빼서 재사용 할 수 있게 하면 좋을 것 같다. 그렇다면 이 두 부분을 어떻게 분리할 수 있을까?

전략 패턴 적용

전락패턴은 개방 폐쇄 원칙(OCP)을 잘 지키는 구조이면서도 유연하고 확장성이 뛰어난 패턴이다. 전략 패턴을 통해 오브젝트를 둘로 분리하여 그 사이에 인터페이스를 통애서만 의존하게 만드는 패턴이다.

deleteAll() 메서드에서 변하지 않는 부분이라고 명시한 것이 바로 contextMethod()가 된다. deleteAll()의 컨텍스트를 정리해보면 다음과 같다.

  1. DB 커넥션 가져오기
  2. PrepatedStatement를 만들어줄 외부 기능 호출하기
  3. 전달받은 PrepatedStatement 실행하기
    4, 예외가 발생하면 이를 다시 메서드 밖으로 던지기
  4. 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기
    두 번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 전략 패턴의 구조를 따라 이 기능을 인터페이스로 만들어두고 인터페이스의 메서드를 통해 PreparedStatement 생성 전략을 호출해주면 된다.

PreparedStatement를 만드는 전략의 인터페이스는 컨텍스트가 만들어준 Connection을 전달받아서, PreparedStatement를 만들고 만들어진 PreparedStatement 오브젝트를 돌려준다.

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

이 인터페이스를 상송해서 실제 전략, 즉 바뀌는 부분인 PreparedStatement를 생성하는 클래스를 만들어보자.

public class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

이제 확장된 PreparedStrategy 전략인 DeleteAllStatement가 만들어졌다 이제 UserDao의 deleteAll() 메서드에서 사용해보자

 public void deleteAll() throws SQLException {
     ...
    try {
         c = dataSource.getConnection();

         StatementStrategy st = new DeleteAllStatement();
        ps = st.makePreparedStatement(c);
          ps.excuteUpdate();
    } catch (SQLException e) {
        ...
    }
    ...
 }

하지만 위 코드도 문제가 있다. 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스(DeleteAllStatement)를 사용하도록 고정되어있다면 결국 코드 내 제어가 수동적이지 못하여 유지보수에 문제가 있을 것이다. StatementStrategy를 메서드 파라미터로 지정하여 컨텍스트에 해당하는 부분을 별도의 메서드로 독립시키자.

private void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();

            ps = stmt.makePreparedStatement(c);

            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if(ps!= null) {try {ps.close();}catch (SQLException e){}}
            if(c!= null) {try {c.close();}catch (SQLException e){}}
        }
}

이 메서드는 컨텍스트의 핵심 내용을 잘 담고 있다. 클라이언트로부터 StatementStrategy를 제공받아 try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행하다, PreparedStatement 생성이 필요한 시점에서 호출되어 사용한다. 이 메서드를 사용하게끔 deletaAll() 메서드를 수정하자

public void deleteAll() throws SQLException {
        StatementStrategy st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
}

add() 메서드도 동일하게 적용할 수 있다. AddStatement를 생성한 후에 add() 메서드에서 jdbcContextWithStatementStrategy를 사용하게끔 수정하자

public class AddStatement implements StatementStrategy {
    User user;

    public AddStatement(User user) {
        this.user = user;
    }
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values (?,?,?)");

        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        return ps;
    }
}
public void add(User user) throws SQLException {
        StatementStrategy st = new AddStatement(user);
        jdbcContextWithStatementStrategy(st);
}
728x90