중첩클래스와 익명 클래스로 클래스 파일 간소화

2022. 1. 17. 00:19북리뷰/토비의 봄

728x90

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

public class UserDao {
    private DataSource dataSource;

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

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

    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 {
        StatementStrategy st = new DeleteAllStatement();
        jdbcContextWithStatementStrategy(st);
    }

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

    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){}}
        }
    }
}
public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
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 class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

add()와 deleteAll()메서드를 중심적으로 위 코드들을 살펴보면 두 가지 아쉬운 점이 있다. 먼저 DAO 메서드 마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점이다. 이렇게 되면 기존 UserDao때보다 클래스 파일의 개수가 많아진다. 또 다른 아쉬운 점은 DAO 메서드에서 StatementStrategy에 전달한 User와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 점이다. 이 오브젝트가 사용되는 시점은 컨텍스트가 전략 오브젝트를 호출할 때이므로 잠시라도 어딘가 다시 저장해둘 수밖에 없다. 이 두 문제를 어떻게 해결할 수 있을까?

중첩 클래스

클래스 파일이 많아지는 문제는 간단한 해결 방법이 있다. StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao클래스 안에 내부 클래스로 정의하는 것이다. 어차피 DeleteAllStatement나 AddStatement는 UserDao 말고는 사용하지 않으니 말이다. 다음은 add()내부에 중첩 클래스를 만든 예시이다.

public void add(final User user) throws SQLException {

    class AddStatement implements StatementStrategy {
        @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;
        }
    }

    StatementStrategy st = new AddStatement();
    jdbcContextWithStatementStrategy(st);
}

이렇게 하면 add 메서드 내 AddStatement 클래스를 정의하면 번거롭게 생성자를 통해 User오브젝트를 전해줄 필요가 없다. 자신이 정의된 메서드에 로컬 변수에 직접 접근할 수 있다. 다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해주어야 한다. 위 user 파라미터는 매서드 내부에서 변경될 일이 없으므로 final로 선언해도 무방하다.

익명 클래스

AddStatement는 add() 메서드에서만 사용할 용도로 만들어졌다. 그렇다면 좀 더 간결하게 클래스 이름도 제거할 수 있다.

public void add(final User user) throws SQLException {

    new StatementStrategy() {
        @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;
        }
    }

    StatementStrategy st = new AddStatement();
    jdbcContextWithStatementStrategy(st);
}

만들어진 오브젝트는 딱 한 번만 사용할테니 굳이 변수에 담지 말고 바로 jdbcContextWithStatementStrategy에 선언해도 된다.

public void add(final User user) throws SQLException {
    jdbcContextWithStatementStrategy(new StatementStrategy() {
        @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;
        }
    });
}

deleteAll() 메서드도 동일한 방식으로 만들 수 있다.

public void deleteAll() throws SQLException {
    jdbcContextWithStatementStrategy(new StatementStrategy() {
        @Override
        public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
            PreparedStatement ps = c.prepareStatement("delete from users");
            return ps;
        }
    });
}

jdbcContextWithStatementStrategy 독립

jdbcContextWithStatementStrategy은 JDBC의 일반적인 작업 흐름을 담고 있어 다른 DAO에서도 충분히 사용 가능하다. 그러니 UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해보자.
분리해서 만들 클래스 이름은 JdbcContext이다.

public class JdbcContext {
    private DataSource dataSource;

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

    public void workWithStatementStrategy(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){}}
        }
    }
}

UserDao에서 새로 생성한 JdbcContext를 사용할 수 있도록 수정해보자

public class UserDao {
    private DataSource dataSource;
    private JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }

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

    public void add(User user) throws SQLException {
        this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
            @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 deleteAll() throws SQLException {
        this.jdbcContext.workWithStatementStrategy(new StatementStrategy() {
            @Override
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                PreparedStatement ps = c.prepareStatement("delete from users");
                return ps;
            }
        });
    }

   ...
}

현재는 모든 코드를 수정하지 않아 아직도 UserDao에서 dataSource를 받아와야 하지만, 나중에 get과 getCount코드를 수정하여 jdbcContext를 사용하여 실행하면 UserDao에서 dataSource를 받아올 필요가 없어진다.

UserDao에 새로운 빈을 주입하도록 Config 파일을 수정하자

@Configuration  
public class Config {  

    @Bean
    public UserDao userDao() {  
        UserDao userDao = new UserDao();  
        userDao.setDataSource(dataSource());  
        userDao.setJdbcContext(jdbcContext());  
        return userDao;  
    }

    @Bean
    public JdbcContext jdbcContext() {
        JdbcContext jdbcContext = new JdbcContext();
        jdbcContext.setDataSource(dataSource());
        return jdbcContext;
    }
}
728x90