티스토리 뷰

SPRING

토비의 스프링 3. 템플릿

짜비 2022. 5. 27. 22:04

UserDao 리소스 반환 시 예외처리

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

            }
        }
    }
}
  • 리소스 반환 부분이 복잡하게 반복된다.

해결 방법1 : 메소드 추출

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    try {
        c = dataSource.getConnection();

        ps = makeStatement(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) {

            }
        }
    }
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
    return c.prepareStatement("delete from users");
}
  • 소득이 없다. 메소드 추출된 부분을 재활용할 수 있어야하는데, 추출된 부분은 계속해서 변하는 부분이기 때문에 재활용이 불가능하다.

해결방법2 : 템플릿 메소드 패턴

  • 상속을 통해 기능 확장
  • 변하지 않는 부분은 슈퍼클래스에, 변하는 부분은 추상 메소드로 정의 후 서브클래스에서 오버라이드하여 새롭게 정의해서 쓴다.

public abstract class UserDao {
    //...

    abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;

    //...
}

public class UserDaoDeleteAll extends UserDao {
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

  • 문제점
    • DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 함.

해결방법3: 전략 패턴

  • 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식
public class DeleteAllStatement implements StatementStrategy {
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}


public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;
    try {
        c = dataSource.getConnection();

        //구체 전략 클래스 --> 클라이언트에서 선택하는 것이 바람직.
        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePreparedStatement(c);

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
      }
    //...
}
  • 문제점
    • 구체 전략 클래스인 DeleteAllStatement 를 사용하도록 고정되어 있음
  • 해결방법
    • 구체 전략 클래스를 선택하는 클라이언트 메소드 생성
//클라이언트 역할을 하는 메소드
public void deleteAll() throws SQLException {
    StatementStrategy strategy = new DeleteAllStatement();
    jdbcContextWithStatementStrategy(strategy);
}

//컨텍스트 메소드
public 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) {

            }
        }
    }
}
  • 전략 패턴의 4 주체를 기억하자
    • 클라이언트
    • 오브젝트 팩토리(DI 컨테이너)
    • 의존 하는 오브젝트1
    • 의존 받는 오브젝트2

전략패턴 응용 : 새로운 전략 추가

  • add() 메소드 추가

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

추가 개선 사항

  • 메소드 추가시 클래스파일을 새로 만들어야 함
  • User와 같은 부가 정보 전달을 위해 인스턴스 변수와 생성자를 활용해야 함.

해결방법 : 로컬 클래스

//UserDao 내 add() 메소드
public void add(User user) throws SQLException {
    //Inner Class
    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;
        }
    }

    StatementStrategy st = new AddStatement(user);
    jdbcContextWithStatementStrategy(st);
}
  • 위 코드에서 AddStatement 클래스는 add(User user) 의 파라미터를 참조할 수 있으므로, 생성자와 인스턴스 변수를 생략해도 된다.
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);
}
  • 익명 내부 클래스를 활용하면 코드가 더욱 간결해진다.
public void add(final User user) throws SQLException {

    StatementStrategy st = 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;
        }
    };
    jdbcContextWithStatementStrategy(st);
}
  • 람다식 활용
public void add(final User user) throws SQLException {
    StatementStrategy st = c -> {
        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;
    };
    jdbcContextWithStatementStrategy(st);
}

컨텍스트 분리

  • jdbcContextWithStatementStrategy() 를 UserDao 밖으로 분리해서 모든 DAO가 컨텍스트를 사용할 수 있도록 개선
public class JdbcContext {
    private DataSource dataSource;

    public JdbcContext(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) {

                }
            }
        }
    }
}

//---------------------------------------

public class UserDao {

    private JdbcContext jdbcContext;

    public UserDao(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }
    //클라이언트
    public void add(final User user) throws SQLException {

        //콜백
        StatementStrategy st = c -> {
            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;
        };
        //템플릿
        this.jdbcContext.workWithStatementStrategy(st);
    }

    public void deleteAll() throws SQLException {
        StatementStrategy strategy = c -> {
            PreparedStatement ps = c.prepareStatement("delete from users");
            return ps;
        };
        this.jdbcContext.workWithStatementStrategy(strategy);
    }

}

//---------------------------------

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(jdbcContext());
        return userDao;
    }


    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        return dataSource;
    }

    @Bean
    public JdbcContext jdbcContext() {
        return new JdbcContext(dataSource());
    }
}
  • JdbcContext : 인터페이스를 구현하지 않고 구체 클래스로 바로 활용.
    • UserDao - JdbcContext 사이 강결합 발생.
      • JdbcContext는 변경될 일 없고, UserDao 는 항상 JdbcContext를 사용해야하기 때문에 문제될 일이 없음.
  • 장점 : 오브젝트 간 의존관계가 설정 파일에 드러남
  • 단점 : 구체적인 클래스와의 의존관계가 있어서 DI의 근본적인 원칙 위배

변경되지 않는 JdbcContext를 내부로 감추기


public class UserDao {

    private JdbcContext jdbcContext;

    //DI를 위한 부가적인 코드
    public UserDao(DataSource dataSource) {
        jdbcContext = new JdbcContext(dataSource);
    }

    //...
}

@Configuration
public class DaoFactory {
    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(dataSource());
        return userDao;
    }

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
        return dataSource;
    }
}
  • 장점 : JdbcContext가 UserDao 내부에서 만들어지고 사용되면서 둘 사이 관계가 외부에 드러나지 않음.
  • 단점 : JdbcContext 를 싱글톤으로 만들 수 없고, DI 를 위한 부가적인 코드 필요.

템플릿/콜백 패턴

  • 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

  • 전략 패턴의 컨텍스트 == 템플릿

    • 고정된 작업 흐름을 가진 코드
  • 익명 내부 클래스로 만들어지는 오브젝트 == 콜백

    • 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트
  • 전략 패턴과의 차이점

    • 매번 메소드 단위로 사용할 오브젝트를 새롭게 전달 받는다.
    • 콜백 오브젝트가 내부 클래스로서, 자신을 생성한 클라이언트 메소드 내 정보를 직접 참조
    • 클라이언트와 콜백이 강하게 결합

템플릿/콜백 응용 예시

public class Calculator {
    public Integer calcSum(String filePath) throws IOException {

        BufferedReaderCallback sumCallback =
                new BufferedReaderCallback() {
                    @Override
                    public Integer doSomethingWithReader(BufferedReader br) throws IOException {
                        Integer sum = 0;
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            sum += Integer.valueOf(line);
                        }
                        return sum;
                    }
                };
        return fileReadTemplate(filePath, sumCallback);
    }

    public Integer calcMultiply(String filePath) throws IOException {
        BufferedReaderCallback multiplyCallback =
                new BufferedReaderCallback() {
                    @Override
                    public Integer doSomethingWithReader(BufferedReader br) throws IOException {
                        Integer mul = 1;
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            mul *= Integer.valueOf(line);
                        }
                        return mul;
                    }
                };
        return fileReadTemplate(filePath, multiplyCallback);
    }

    public Integer fileReadTemplate(String filePath, BufferedReaderCallback callback) throws IOException{
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            int ret = callback.doSomethingWithReader(br);
            return ret;

        } catch (IOException e) {
            System.out.println(e.getMessage());
            throw e;
        }
        finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    System.out.println("e.getMessage() = " + e.getMessage());
                }
            }
        }
    }
}
public class CalcSumTest {

    Calculator calculator;
    String numFilepath;

    @BeforeEach
    void setUp() {
        this.calculator = new Calculator();
        this.numFilepath = getClass().getResource("/static/numbers.txt").getPath();
    }

    @Test
    void sumOfNumbers() throws IOException {
        Assertions.assertThat(calculator.calcSum(this.numFilepath)).isEqualTo(10);
    }

    @Test
    void multiplyOfNumbers() throws IOException {
        Assertions.assertThat(calculator.calcMultiply(this.numFilepath)).isEqualTo(24);
    }
}
  • 초기 코드에서 변화가 자주 일어나는 부분 : sum 을 계산
    • callback으로 분리
  • 변하지 않는 부분(공통된 부분) : filePath 를 이용해서 BufferedReader를 만들고, 사칙연산 결과를 반환
    • template 으로 만들기

추가 개선

  • multiplyCallback, sumCallback 에서 중복되는 코드가 존재한다.
    • 결과 저장을 위한 변수 초기화
    • bufferedReader를 이용해 마지막 라인까지 순차적으로 읽는다.
    • 라인 별 숫자와 결과 변수에 저장된 값을 이용해 계산한다.
  • 위와 같이 중복된 로직을 템플릿에 녹여보자.
public interface LineCallback {
    Integer doSomethingWithLine(String line, Integer value);
}
public class Calculator {
    public Integer calcSum(String filePath) throws IOException {
        LineCallback sumCallback =
                new LineCallback() {
                    @Override
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value + Integer.valueOf(line);
                    }
                };
        return lineReadTemplate(filePath, sumCallback, 0);
    }

    public Integer calcMultiply(String filePath) throws IOException {
        LineCallback multiplyCallback =
                new LineCallback() {
                    @Override
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value * Integer.valueOf(line);
                    }
                };
        return lineReadTemplate(filePath, multiplyCallback, 1);
    }

    public Integer lineReadTemplate(String filePath, LineCallback callback, int initVal) throws IOException {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(filePath));
            Integer res = initVal;
            String line = null;
            while ((line = br.readLine()) != null) {
                res = callback.doSomethingWithLine(line, res);
            }
            return res;

        } catch (IOException e) {
            System.out.println(e.getMessage());
            throw e;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    System.out.println("e.getMessage() = " + e.getMessage());
                }
            }
        }
    }
}

스프링 속 템플릿/콜백 예시 : JdbcTemplate

public class UserDao {

    private JdbcTemplate jdbcTemplate;

    private RowMapper<User> userMapper =
            new RowMapper<User>() {
                @Override
                public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                    User user = new User();
                    user.setId(rs.getString("id"));
                    user.setName(rs.getString("name"));
                    user.setPassword(rs.getString("password"));
                    return user;
                }
            };

    public UserDao(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void add(final User user) throws SQLException {
        this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)",
                user.getId(), user.getName(), user.getPassword());
    }

    public void deleteAll() throws SQLException {
        //jdbcContext.executeSql("delete from users");
        this.jdbcTemplate.update(
                new PreparedStatementCreator() {
                    @Override
                    public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                        return con.prepareStatement("delete from users");
                    }
                }
        );
    }

    public int getCount() {
        // 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();
        // ps.close();
        // c.close();
        //
        // return count;
        return this.jdbcTemplate.query(new PreparedStatementCreator() {
                                           @Override
                                           public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
                                               return con.prepareStatement("select count(*) from users");
                                           }
                                       },
                new ResultSetExtractor<Integer>() {
                    @Override
                    public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
                        rs.next();
                        return rs.getInt(1);
                    }
                });
    }

    public User get(String id) {
        // Connection c = this.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"));
        // }
        //
        // rs.close();
        // ps.close();
        // c.close();
        //
        // if (user == null) throw new EmptyResultDataAccessException(1);
        //
        // return user;

        return this.jdbcTemplate.queryForObject("select * from users where id = ?",
                new Object[] {id}, userMapper);
    }

    public List<User> getAll() {
        return this.jdbcTemplate.query("select * from users order by id", userMapper);
    }
}

  • 기존 jdbcContext -> 스프링에서 제공하는 jdbcTemplate변경
  • 기존 StatementStrategy 인터페이스 -> PrepardStatementCreator 인터페이스 변경
  • query()는 callback을 두 개 이상 받을 수 있음.
    • e.g. getCount() 에서 query() 는 PreparedStatementCreator, ResultSetExtractor 두 개의 callback을 사용.
  • 중복되는 RowMapper 인터페이스를 인스턴스 변수로 분리

개선된 userDao 특징

  • UserDao 에는 User 정보를 DB에 넣거나 가져오거나 조작하는 방법에 대한 핵심적인 로직만 담겨 있음
  • JDBC API 사용하는 방식, 예외처리, 리소스 반납, DB 연결 가져오기 등의 책임과 관심은 JdbcTemplate 에 담겨 있음.
    • 위 사항에 대해 변경이 일어나더라도 UserDao 코드에는 영향을 주지 않음. (낮은 결합도)
  • 스프링에는 JdbcTemplate 외에도 템플릿/콜백 패턴을 적용한 API가 다수 존재. 클래스 이름이 ’Template’ 으로 끝나거나 인터페이스 이름이 ‘Callback’ 으로 끝날 경우 템플릿/콜백이 적용된 것.
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함