이번장도 역시 토비의 스프링을 보고 요약한 포스팅이다. 이번 포스팅에 관련해서는 디자인패턴이랑 항목으로 이전에 정리한 내용이 많기 때문에 함께 참고해서 읽으면 좋을것으로 보인다.
문제가 있을 시 lshn1007@hanyang.ac.kr 로 메일주시면 삭제하겠습니다.
OCP : 확장에는 열려있고, 변경에는 닫혀있게 설계하는 방식을 말한다.
/**
* 슈퍼 클래스
*/
@RequiredArgsConstructor
public abstract class UserDao {
private final DataSource dataSource;
private final Logger log = LoggerFactory.getLogger(UserDao.class);
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = makeStatement(c); // 추상 메소드의 구현으로 변하는 부분의 기능을 수행
ps.executeUpdate();
} catch (SQLException e) {
...
} finally {
...
}
}
// 변하는 부분을 추상 메소드로 선언
protected abstract PreparedStatement makeStatement(Connection c) throws SQLException;
}
/**
* 서브 클래스
*/
public class UserDaoDeleteAll extends UserDao {
public UserDaoDeleteAll(DataSource dataSource) {
super(dataSource);
}
// 슈퍼 클래스의 추상 메소드를 구현해 변하는 부분을 정의
@Override
protected PreparedStatement makeStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
// 변경될 수 있는 부분을 하나의 인터페이스로 분리
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
// 클라이언트에서 필요에 따라 바꿔서 사용할 수 있는 클래스(전략)
public class DeleteAllStatement implements StatementStrategy {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
@AllArgsConstructor
public class AddStatement implements StatementStrategy {
private 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 UserDao {
...
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 구체적인 전략 오브젝트 생성
jdbcContextWithStatementStrategy(st); // context method(변경되지 않는 고정된 코드) 호출. 생성한 전략 오브젝트 전달
}
public void add(User user) throws SQLException {
StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}
public void jdbcContextWithStatementStrategy(StatementStrategy st) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = st.makePreparedStatement(c); // 전달 받은 전략(오브젝트 구현체)에 따라 기능이 다르게 수행된다.
ps.executeUpdate();
} catch (SQLException e) {
...
} finally {
...
}
}
}
public class UserDao {
private final DataSource dataSource;
private final Logger log = LoggerFactory.getLogger(UserDao.class);
// 템플릿 메소드 호출 시 콜백을 전달. 별도 클래스로 만들어둘 필요가 없어 코드가 간결해짐
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
});
}
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;
}
});
}
public void jdbcContextWithStatementStrategy(StatementStrategy st) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = st.makePreparedStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
...
} finally {
...
}
}
}
템플릿
- 고정된 작업 흐름을 가진 코드로 재사용을 위해서 분리한 코드를 말한다.
콜백
- 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말함
- 템플릿에 파라미터로 절달되지만 값을 참조하기 위한 것이 아니라, 특정 로직을 담은 메소드를 실행시키기 위함
- 자바에서 람다식을 제외하고는 메소드 자체를 파라미터로 전달할 수는 없기 때문에 익명클래스를 활용하는 방식
static nested class : 독립적으로 객체 생성이 가능하도록 static으로 선언된 중첩 클래스
Member inner class : 멤버필드처럼 오브젝트 레벨에 정의되는 중첩 클래스(instance value처럼)
Local inner class : 메소드 레벨에서 정의되는 클래스(마치 로컬변수와 같이 선언)
Anonymous inner class : 인터페이스를 활용해서 정의되는 클래스
각 중첩 클래스의 접근 범위와, 스코프의 경우에는 자세한 포스팅을 구글링해보도록 하자.. (추후 추가하려고는 계획중)
/**
* [콜백을 직접 만들어 전달한 경우]
*/
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(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;
}
});
/**
* [JdbcTemplate의 내장 콜백을 사용한 경우]
* 바인딩할 파라미터는 순서대로 넣어주면 된다.
*/
jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
user.getId(), user.getName(), user.getPassword());
/**
* [콜백을 직접 만들어 전달한 경우]
*/
return jdbcTemplate.query(
// 첫번째 콜백 - PreparedStatement 생성
new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("select count(*) from users");
}
},
// 두번째 콜백 - PreparedStatement 실행 결과로 받은 ResultSet에서 데이터 알맞게 가공
new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
rs.next();
return rs.getInt(1);
}
}
);
/**
* [JdbcTemplate의 내장 콜백을 사용한 경우]
* jdbcTemplate.queryForInt()는 @Deprecated 되어 다른 방법을 사용
*/
return jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
/**
* ResultSetExtractor는 ResultSet을 한 번만 전달해주기 때문에 수동으로 반복문을 돌려서 원하는 타입에 맞게 매핑해줘야 되지만,
* RowMapper는 ResultSet의 각 행을 반복적으로 리턴해주기 때문에 반복문 없이 각 행을 원하는 타입에 맞게 매핑할 수 있음
*/
jdbcTemplate.queryForObject("select * from users where id = ?",
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int i) throws SQLException {
return User.builder()
.id(rs.getString("id"))
.name(rs.getString("name"))
.password(rs.getString("password"))
.build();
}
});
jdbcTemplate.query("select * from users order by id",
new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int i) throws SQLException {
return User.builder()
.id(rs.getString("id"))
.name(rs.getString("name"))
.password(rs.getString("password"))
.build();
}
});