BackEnd/JDBC

[JDBC] DAO 리팩토링 2 - 하나의 Template 으로 이동

샤아이인 2022. 3. 13.

다음과 같이 하나의 Template 안에 기존의 코드들을 전부 모아두었다.

기존 코드는 직전 글에서 설명했었다.

 

[JDBC] DAO 리팩토링 1 - 개별 Template 만들기

이전 글의 코드를 리팩토링 한 과정입니다. 변경 전의 코드는 다음과 같습니다. [JDBC] 순수 JDBC CRUD 코드 리팩토링 하기전의 중복 부분이 많은 코드. 이후의 글에서 점차 리팩토링 해가는 모습을

blogshine.tistory.com

 

1. 하나의 Template 안에 모인 코드들

- DbTemplate

public abstract class DbTemplate {
    private final DataSource dataSource;

    public DbTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }

    public Long executeUpdate(String sql) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            setParameters(pstmt);
            pstmt.executeUpdate();

            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                return rs.getLong(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);;
        }
        return -1L;
    }

    public Object executeQuery(String sql) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql);
            setParameters(pstmt);
            rs = pstmt.executeQuery();

            Object value = rowMapper(rs);
            if (value != null) return value;
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);
        }
        return null;
    }

    public List<Object> findAllQuery(String sql) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        List<Object> list = null;

        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql);
            rs = pstmt.executeQuery();

            list = rowsMapper(rs);
            return list;
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);
        }
        return list;
    }

    public abstract void setParameters(PreparedStatement pstmt) throws SQLException;
    public abstract Object rowMapper(ResultSet rs) throws SQLException;
    public abstract List<Object> rowsMapper(ResultSet rs) throws SQLException;
}

하지만 이렇게 되면 모든 경우에 3가지 abstract 메서드를 항상 구현해주어야 한다는 단점이 발생한다.

 

다음 코드를 살펴보면 abstract 메서드 들을 전부 구현하고 있는것를 볼 수 있다.

심지어 필요도 없는데 말이다! 따라서 Repository 코드는 다음과 같아진다.

 

- DbUserRepository

@Primary
@Repository
public class DbUserRepository implements UserRepository {
    private final DataSource dataSource;

    public DbUserRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Long save(User user) {
        DbTemplate template = new DbTemplate(dataSource) {
            @Override
            public void setParameters(PreparedStatement pstmt) throws SQLException {
                pstmt.setString(1, user.getUserId());
                pstmt.setString(2, user.getPassword());
                pstmt.setString(3, user.getName());
                pstmt.setString(4, user.getEmail());
            }

            @Override
            public Object rowMapper(ResultSet rs) throws SQLException {
                return null;
            }

            @Override
            public List<Object> rowsMapper(ResultSet rs) throws SQLException {
                return null;
            }
        };
        String SQL = "INSERT INTO user_info (user_id, password, name, email) VALUES (?, ?, ?, ?)";
        Long saveId = template.executeUpdate(SQL);
        user.setId(saveId);
        return saveId;
    }

    @Override
    public Optional<User> findByUserId(String userId) {
        DbTemplate template = new DbTemplate(dataSource) {
            @Override
            public void setParameters(PreparedStatement pstmt) throws SQLException {
                pstmt.setString(1, userId);
            }

            @Override
            public Object rowMapper(ResultSet rs) throws SQLException {
                while(rs.next()){
                    User user = new User(rs.getString("user_id"), rs.getString("password"), rs.getString("name"), rs.getString("email"));
                    user.setId(rs.getLong("id"));
                    return user;
                }
                return null;
            }

            @Override
            public List<Object> rowsMapper(ResultSet rs) throws SQLException {
                return null;
            }
        };
        String SQL = "SELECT id, user_id, password, name, email FROM user_info WHERE user_id = (?)";
        return Optional.ofNullable((User)template.executeQuery(SQL));
    }

    @Override
    public List<User> findAll() {
        DbSelectAllTemplate template = new DbSelectAllTemplate(dataSource) {
            @Override
            public List<Object> rowMapper(ResultSet rs) throws SQLException {
                List<Object> list = new ArrayList<>();
                while(rs.next()){
                    User user = new User(rs.getString("user_id"), rs.getString("password"), rs.getString("name"), rs.getString("email"));
                    user.setId(rs.getLong("id"));
                    list.add(user);
                }
                return list;
            }
        };
        String SQL = "SELECT id, user_id, password, name, email FROM user_info";
        return template.executeQuery(SQL).stream()
                .map(o -> (User)o)
                .collect(Collectors.toList());
    }

    @Override
    public boolean delete(String userId) {
        DbTemplate template = new DbTemplate(dataSource) {
            @Override
            public void setParameters(PreparedStatement pstmt) throws SQLException {
                pstmt.setString(1, userId);
            }

            @Override
            public Object rowMapper(ResultSet rs) throws SQLException {
                return null;
            }

            @Override
            public List<Object> rowsMapper(ResultSet rs) throws SQLException {
                return null;
            }
        };

        String SQL = "DELETE FROM user_info WHERE user_id = (?)";
        Long resultId = template.executeUpdate(SQL);

        if(resultId != -1){
            return true;
        }
        return false;
    }

    @Override
    public boolean update(String userId, User updateParam) {
        DbTemplate template = new DbTemplate(dataSource) {
            @Override
            public void setParameters(PreparedStatement pstmt) throws SQLException {
                pstmt.setString(1, updateParam.getUserId());
                pstmt.setString(2, updateParam.getPassword());
                pstmt.setString(3, updateParam.getName());
                pstmt.setString(4, updateParam.getEmail());
                pstmt.setString(5, updateParam.getUserId());
            }

            @Override
            public Object rowMapper(ResultSet rs) throws SQLException {
                return null;
            }

            @Override
            public List<Object> rowsMapper(ResultSet rs) throws SQLException {
                return null;
            }
        };

        String SQL = "UPDATE user_info SET user_id = (?), password = (?), name = (?), email = (?) WHERE user_id = (?)";
        Long resultId = template.executeUpdate(SQL);

        if(resultId != -1){
            return true;
        }
        return false;
    }
}

위 코드는 매우 좋지 못하다.

필요한 경우에 필요한 메서드만 오버라이딩 하여 사용하고 싶다면 어떻게 해야할까??

=> 바로 메서드를 인터페이스로 분리하면 된다!

 

각각을 독립적으로 분리하는 것 이다. 각각의 인터페이스를 필요로 하는 시점에 인터페이스를 구현하여 전달하면 된다.

메서드를 인자로 전달하는 것 이다!

 

2. 메서드를 인터페이스로 분리하기

우선 추출할 메서드의 인터페이스를 다음과 같이 만들자.

나같은 경우 select, update, findAll 기능에 해당되는 인터페이스를 만들어 주었다.

 

- PreparedStatementSetter

public interface PreparedStatementSetter {
    void setParameters(PreparedStatement pstmt) throws SQLException;
}

 

- RowMapper

public interface RowMapper<T> {
    T rowMapper(ResultSet rs) throws SQLException;
}

 

- RowsMapper

public interface RowsMapper<T> {
    List<T> rowsMapper(ResultSet rs) throws SQLException;
}

 

이제 위 3개의 인터페이스를 사용하는 DbTemplate 의 변경된 코드는 다음과 같다.

- 변경된 DbTemplate

public class DbTemplate {
    private final DataSource dataSource;

    public DbTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }

    public Long executeUpdate(String sql, PreparedStatementSetter pss) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            pss.setParameters(pstmt);
            pstmt.executeUpdate();

            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                return rs.getLong(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);
            ;
        }
        return -1L;
    }

    public <T> T executeQuery(String sql, RowMapper<T> mapper, PreparedStatementSetter pss) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql);
            pss.setParameters(pstmt);
            rs = pstmt.executeQuery();

            T value = mapper.rowMapper(rs);
            if (value != null) return value;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);
        }
        return null;
    }

    public <T> List<T> executeQuery(String sql, RowsMapper<T> mapper) {
        Connection connection = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        List<T> list = null;

        try {
            connection = getConnection();
            pstmt = connection.prepareStatement(sql);
            rs = pstmt.executeQuery();

            list = mapper.rowsMapper(rs);
            return list;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DbCleaner.close(connection, pstmt, rs, dataSource);
        }
        return list;
    }
}

기존의 abtract 메서드가 전부 사라졌다. 따라서 Class 에서도 abstract 를 제거해 주었다.

위 template는 더이상 자식 class에서 메서드를 구현하여 사용하지 않는다.

 

변경 된 후의 코드는 인터페이스를 통해 외부로 부터 함수 자체를 전달받아 사용한다.

 

인터페이스 분리 전의 코드는 필요없는 abstract 메서드 전부를 구현해야 했지만, 변경된 template는 필요한 메서드만 함수의 인자로 전달받아서 사용하면 된다.

 

- 변경된 DbUserRepository

대표적으로 findByUserId 메서드를 살펴보자.

메서드 내부에서 Interface를 익명 class로 바로 구현하여 이를 template.executeQuery()의 인자로 전달하고 있다.

다음 코드를 살펴보자.

public Optional<User> findByUserId(String userId) {
    PreparedStatementSetter pss = new PreparedStatementSetter() {
        @Override
        public void setParameters(PreparedStatement pstmt) throws SQLException {
            pstmt.setString(1, userId);
        }
    };

    RowMapper<User> mapper = new RowMapper<User>() {
        @Override
        public User rowMapper(ResultSet rs) throws SQLException {
            while (rs.next()) {
                User user = new User(rs.getString("user_id"), rs.getString("password"), rs.getString("name"), rs.getString("email"));
                user.setId(rs.getLong("id"));
                return user;
            }
            return null;
        }
    };

    DbTemplate template = new DbTemplate(dataSource);

    String SQL = "SELECT id, user_id, password, name, email FROM user_info WHERE user_id = (?)";
    return Optional.ofNullable(template.executeQuery(SQL, mapper, pss)); // 함수를 전달
}

나머지 코드들도 위와 거의 유사하다!!

댓글